Compare commits
1 Commits
cb5d30646a
...
docs/gatew
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37545de79c |
193
docs/plans/gateway-token-recovery.md
Normal file
193
docs/plans/gateway-token-recovery.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Gateway Admin Token Recovery — Implementation Plan
|
||||
|
||||
**Mission:** `cli-unification-20260404`
|
||||
**Task:** `CU-03-01` (planning only — no runtime code changes)
|
||||
**Status:** Design locked (Session 1) — BetterAuth cookie-based recovery
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The gateway installer strands operators when the admin user exists but the admin
|
||||
API token is missing. Concrete trigger:
|
||||
|
||||
- `~/.config/mosaic/gateway/meta.json` was deleted / regenerated.
|
||||
- The installer was re-run after a previous successful bootstrap.
|
||||
|
||||
Flow today (`packages/mosaic/src/commands/gateway/install.ts:375-400`):
|
||||
|
||||
1. `bootstrapFirstUser` hits `GET /api/bootstrap/status`.
|
||||
2. Server returns `needsSetup: false` because `users` count > 0.
|
||||
3. Installer logs `Admin user already exists — skipping setup. (No admin token on file — sign in via the web UI to manage tokens.)` and returns.
|
||||
4. The operator now has:
|
||||
- No token in `meta.json`.
|
||||
- No CLI path to mint a new one (`mosaic gateway <anything>` that needs the token fails).
|
||||
- `POST /api/bootstrap/setup` locked out — it only runs when `users` count is zero (`apps/gateway/src/admin/bootstrap.controller.ts:34-37`).
|
||||
- `POST /api/admin/tokens` gated by `AdminGuard` — requires either a bearer token (which they don't have) or a BetterAuth session (which they don't have in the CLI).
|
||||
|
||||
Dead end. The web UI is the only escape hatch today, and for headless installs even that may be inaccessible.
|
||||
|
||||
## 2. Design Summary
|
||||
|
||||
The BetterAuth session cookie is the authority. The operator runs
|
||||
`mosaic gateway login` to sign in with email/password, which persists a session
|
||||
cookie via `saveSession` (reusing `packages/mosaic/src/auth.ts`). With a valid
|
||||
session, `mosaic gateway config recover-token` (stranded-operator entry point)
|
||||
and `mosaic gateway config rotate-token` call the existing authenticated admin
|
||||
endpoint `POST /api/admin/tokens` using the cookie, then persist the returned
|
||||
plaintext to `meta.json` via `writeMeta`. **No new server endpoints are
|
||||
required** — `AdminGuard` already accepts BetterAuth session cookies via its
|
||||
`validateSession` path (`apps/gateway/src/admin/admin.guard.ts:90-120`).
|
||||
|
||||
## 3. Surface Contract
|
||||
|
||||
### 3.1 Server — no changes required
|
||||
|
||||
| Endpoint | Status | Notes |
|
||||
| ------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `POST /api/admin/tokens` | **Reuse as-is** | `admin-tokens.controller.ts:46-72`. Returns `{ id, label, scope, expiresAt, lastUsedAt, createdAt, plaintext }`. |
|
||||
| `GET /api/admin/tokens` | **Reuse** | Useful for `mosaic gateway config tokens list` follow-on (out of scope for CU-03-01, but trivial once auth path exists). |
|
||||
| `DELETE /api/admin/tokens/:id` | **Reuse** | Used by rotate flow for optional old-token revocation. |
|
||||
| `POST /api/bootstrap/setup` | **Unchanged** | Remains first-user-only; not part of recovery. |
|
||||
|
||||
`AdminGuard.validateSession` takes BetterAuth cookies from `request.raw.headers`
|
||||
via `fromNodeHeaders` and calls `auth.api.getSession({ headers })`. It also
|
||||
enforces `role === 'admin'`. This is exactly the path the CLI will hit with
|
||||
`Cookie: better-auth.session_token=...`.
|
||||
|
||||
**Confirmed feasible** during CU-03-01 investigation.
|
||||
|
||||
### 3.2 `mosaic gateway login`
|
||||
|
||||
Thin wrapper over the existing top-level `mosaic login`
|
||||
(`packages/mosaic/src/cli.ts:42-76`) with gateway-specific defaults pulled from
|
||||
`readMeta()`.
|
||||
|
||||
| Aspect | Behavior |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Default gateway URL | `http://${meta.host}:${meta.port}` from `readMeta()`, fallback `http://localhost:14242`. |
|
||||
| Flow | Prompt email + password -> `signIn()` -> `saveSession()`. |
|
||||
| Persistence | `~/.mosaic/session.json` via existing `saveSession` (7-day expiry). |
|
||||
| Decision | **Thin wrapper**, not alias. Rationale: defaults differ (reads `meta.json`), and discoverability under `mosaic gateway --help`. |
|
||||
| Implementation | Share the sign-in logic by extracting a small `runLogin(gatewayUrl, email?, password?)` helper; both commands call it. |
|
||||
|
||||
### 3.3 `mosaic gateway config rotate-token`
|
||||
|
||||
| Aspect | Behavior |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Precondition | Valid session (via `loadSession` + `validateSession`). On failure, print: "Not signed in — run `mosaic gateway login`" and exit non-zero. |
|
||||
| Request | `POST ${gatewayUrl}/api/admin/tokens` with header `Cookie: <session>`, body `{ label: "CLI token (rotated YYYY-MM-DD)" }`. |
|
||||
| On success | Read meta via `readMeta()`, set `meta.adminToken = plaintext`, `writeMeta(meta)`. Print the token banner (reuse `printAdminTokenBanner` shape). |
|
||||
| Old token | **Optional `--revoke-old`** flag. When set and a previous `meta.adminToken` existed, call `DELETE /api/admin/tokens/:id` after rotation. Requires listing first to find the id; punt to CU-03-02 decision. Document as nice-to-have. |
|
||||
| Exit codes | `0` success; `1` network error; `2` auth error; `3` server rejection. |
|
||||
|
||||
### 3.4 `mosaic gateway config recover-token`
|
||||
|
||||
Superset of `rotate-token` with an inline login nudge — the "stranded operator"
|
||||
entry point.
|
||||
|
||||
| Step | Action |
|
||||
| ---- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `readMeta()` — derive gateway URL. If meta is missing entirely, fall back to `--gateway` flag or default. |
|
||||
| 2 | `loadSession(gatewayUrl)` then `validateSession`. If either fails, prompt inline: email + password -> `signIn` -> `saveSession`. |
|
||||
| 3 | `POST /api/admin/tokens` with cookie, label `"Recovered via CLI YYYY-MM-DDTHH:mm"`. |
|
||||
| 4 | Persist plaintext to `meta.json` via `writeMeta`. |
|
||||
| 5 | Print the token banner and next-steps hints (e.g. `mosaic gateway status`). |
|
||||
| 6 | Exit `0`. |
|
||||
|
||||
Key property: this command is **runnable with nothing but email+password in hand**.
|
||||
It assumes the gateway is up but assumes no prior CLI session state.
|
||||
|
||||
### 3.5 File touch list (for CU-03-02..05 execution)
|
||||
|
||||
| File | Change |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `packages/mosaic/src/commands/gateway.ts` | Register `login`, `config recover-token`, `config rotate-token` subcommands under `gw`. |
|
||||
| `packages/mosaic/src/commands/gateway/config.ts` | Add `runRecoverToken`, `runRotateToken` handlers; export from module. |
|
||||
| `packages/mosaic/src/commands/gateway/login.ts` (new) | Thin wrapper calling shared `runLogin` helper with meta-derived default URL. |
|
||||
| `packages/mosaic/src/auth.ts` | No change expected. Possibly export a `requireSession(gatewayUrl)` helper (reuse pattern). |
|
||||
| `packages/mosaic/src/commands/gateway/install.ts` | `bootstrapFirstUser` branch: "user exists, no token" -> offer recovery (see Section 4). |
|
||||
|
||||
## 4. Installer Fix (CU-03-06 preview)
|
||||
|
||||
Current stranding point is `install.ts:388-395`. The fix:
|
||||
|
||||
```
|
||||
if (!status.needsSetup) {
|
||||
if (meta.adminToken) {
|
||||
// unchanged — happy path
|
||||
} else {
|
||||
// NEW: prompt "Admin exists but no token on file. Recover now? [Y/n]"
|
||||
// If yes -> call runRecoverToken(gatewayUrl) inline (interactive):
|
||||
// - prompt email + password
|
||||
// - signIn -> saveSession
|
||||
// - POST /api/admin/tokens
|
||||
// - writeMeta(meta) with returned plaintext
|
||||
// - print banner
|
||||
// If no -> print the current stranded message but include:
|
||||
// "Run `mosaic gateway config recover-token` when ready."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Shape notes (actual code lands in CU-03-06):
|
||||
|
||||
- Extract the recovery body so it can be called **both** from the standalone
|
||||
command and from `bootstrapFirstUser` without duplicating prompts.
|
||||
- Reuse the same `rl` readline interface already open in `bootstrapFirstUser`
|
||||
for the inline prompts.
|
||||
- Preserve non-interactive behavior: if `process.stdin.isTTY` is false, skip the
|
||||
prompt and emit the "run recover-token" hint only.
|
||||
|
||||
## 5. Test Strategy (CU-03-07 scope)
|
||||
|
||||
### 5.1 Happy paths
|
||||
|
||||
| Command | Scenario | Expected |
|
||||
| ------------------------------------- | ------------------------------------------------ | -------------------------------------------------------- |
|
||||
| `mosaic gateway login` | Valid creds | `session.json` written, 7-day expiry, exit 0 |
|
||||
| `mosaic gateway config rotate-token` | Valid session, server reachable | `meta.json` updated, banner printed, new token usable |
|
||||
| `mosaic gateway config recover-token` | No session, valid creds, server reachable | Prompts for creds, writes session + meta, exit 0 |
|
||||
| Installer inline recovery | Re-run after `meta.json` wipe, operator says yes | Meta restored, banner printed, no manual CLI step needed |
|
||||
|
||||
### 5.2 Error paths (must all produce actionable messages and non-zero exit)
|
||||
|
||||
| Failure | Expected handling |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| Invalid email/password | BetterAuth 401 surfaced as "Sign-in failed: <server message>", exit 2 |
|
||||
| Expired stored session | Recover command silently re-prompts; rotate command exits 2 with "run login" hint |
|
||||
| Gateway down / connection refused | "Could not reach gateway at <url>" exit 1 |
|
||||
| Server rejects token creation | Print status + body excerpt, exit 3 |
|
||||
| Meta file missing (recover) | Fall back to `--gateway` flag or default; warn that meta will be created |
|
||||
| Non-admin user | `AdminGuard` 403 surfaced as "User is not an admin", exit 2 |
|
||||
|
||||
### 5.3 Integration test (recommended)
|
||||
|
||||
Spin up gateway in test harness, create admin user via `/api/bootstrap/setup`,
|
||||
wipe `meta.json`, invoke `mosaic gateway config recover-token` programmatically,
|
||||
assert new `meta.adminToken` works against `GET /api/admin/tokens`.
|
||||
|
||||
## 6. Risks & Open Questions
|
||||
|
||||
| # | Item | Severity | Mitigation |
|
||||
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `AdminGuard.validateSession` calls `getSession` with `fromNodeHeaders(request.raw.headers)`. CLI sends `Cookie:` header only. Confirm BetterAuth reads from `Cookie`, not `Set-Cookie`. | Low | Confirmed — `mosaic login` + `mosaic tui` already use this flow successfully (`cli.ts:137-181`). |
|
||||
| 2 | Session cookie local expiry (7d) vs BetterAuth server-side expiry may drift. | Low | `validateSession` hits `get-session`; handle 401 by re-prompting. |
|
||||
| 3 | Label collision / unbounded token growth if operators run `recover-token` repeatedly. | Low | Include ISO timestamp in label. Optional `--revoke-old` in CU-03-02. Add `tokens list/prune` later. |
|
||||
| 4 | `mosaic login` exists at top level and `mosaic gateway login` is a wrapper — risk of confusion. | Low | Document that `gateway login` is the preferred entry for gateway operators; top-level stays for compatibility. |
|
||||
| 5 | `meta.json` write is not atomic. Crash between token creation and `writeMeta` leaves an orphan token server-side with no plaintext on disk. | Medium | Accept for now — re-running `recover-token` mints a fresh token. Document as known limitation. |
|
||||
| 6 | Non-TTY installer runs (CI, headless provisioners) cannot prompt for creds interactively. | Medium | Installer inline recovery must skip prompt when `!process.stdin.isTTY`; emit the recover-token hint. |
|
||||
| 7 | If `BETTER_AUTH_SECRET` rotates between login and recover, the session cookie is invalid — user must re-login. Acceptable but surface a clear error. | Low | Error handler maps 401 on recover -> "Session invalid; re-run `mosaic gateway login`". |
|
||||
| 8 | No MFA today. When MFA lands, BetterAuth sign-in will return a challenge, not a cookie — recovery UX will need a second prompt step. | Future | Out of scope for this mission. Flag for future CLI work. |
|
||||
|
||||
## 7. Downstream Task Hooks
|
||||
|
||||
| Task | Scope |
|
||||
| -------- | -------------------------------------------------------------------------- |
|
||||
| CU-03-02 | Implement `mosaic gateway login` wrapper + shared `runLogin` extraction. |
|
||||
| CU-03-03 | Implement `mosaic gateway config rotate-token`. |
|
||||
| CU-03-04 | Implement `mosaic gateway config recover-token`. |
|
||||
| CU-03-05 | Wire commands into `gateway.ts` registration, update `--help` copy. |
|
||||
| CU-03-06 | Installer inline recovery hook in `bootstrapFirstUser`. |
|
||||
| CU-03-07 | Tests per Section 5. |
|
||||
| CU-03-08 | Docs: update gateway install README + operator runbook with recovery flow. |
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaicstack/db": "workspace:*",
|
||||
"commander": "^13.0.0",
|
||||
"drizzle-orm": "^0.45.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Command } from 'commander';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { registerLogCommand } from './cli.js';
|
||||
|
||||
function buildTestProgram(): Command {
|
||||
const program = new Command('mosaic');
|
||||
program.exitOverride(); // prevent process.exit in tests
|
||||
registerLogCommand(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
describe('registerLogCommand', () => {
|
||||
it('registers a "log" subcommand on the parent', () => {
|
||||
const program = buildTestProgram();
|
||||
const names = program.commands.map((c) => c.name());
|
||||
expect(names).toContain('log');
|
||||
});
|
||||
|
||||
it('log command has tail, search, export, and level subcommands', () => {
|
||||
const program = buildTestProgram();
|
||||
const logCmd = program.commands.find((c) => c.name() === 'log');
|
||||
expect(logCmd).toBeDefined();
|
||||
const subNames = logCmd!.commands.map((c) => c.name());
|
||||
expect(subNames).toContain('tail');
|
||||
expect(subNames).toContain('search');
|
||||
expect(subNames).toContain('export');
|
||||
expect(subNames).toContain('level');
|
||||
});
|
||||
|
||||
it('tail subcommand has expected options', () => {
|
||||
const program = buildTestProgram();
|
||||
const logCmd = program.commands.find((c) => c.name() === 'log')!;
|
||||
const tailCmd = logCmd.commands.find((c) => c.name() === 'tail')!;
|
||||
const optionNames = tailCmd.options.map((o) => o.long);
|
||||
expect(optionNames).toContain('--agent');
|
||||
expect(optionNames).toContain('--level');
|
||||
expect(optionNames).toContain('--category');
|
||||
expect(optionNames).toContain('--tier');
|
||||
expect(optionNames).toContain('--limit');
|
||||
expect(optionNames).toContain('--db');
|
||||
});
|
||||
|
||||
it('search subcommand accepts a positional query argument', () => {
|
||||
const program = buildTestProgram();
|
||||
const logCmd = program.commands.find((c) => c.name() === 'log')!;
|
||||
const searchCmd = logCmd.commands.find((c) => c.name() === 'search')!;
|
||||
// Commander stores positional args in _args
|
||||
const argNames = searchCmd.registeredArguments.map((a) => a.name());
|
||||
expect(argNames).toContain('query');
|
||||
});
|
||||
|
||||
it('export subcommand accepts a positional path argument', () => {
|
||||
const program = buildTestProgram();
|
||||
const logCmd = program.commands.find((c) => c.name() === 'log')!;
|
||||
const exportCmd = logCmd.commands.find((c) => c.name() === 'export')!;
|
||||
const argNames = exportCmd.registeredArguments.map((a) => a.name());
|
||||
expect(argNames).toContain('path');
|
||||
});
|
||||
|
||||
it('level subcommand accepts a positional level argument', () => {
|
||||
const program = buildTestProgram();
|
||||
const logCmd = program.commands.find((c) => c.name() === 'log')!;
|
||||
const levelCmd = logCmd.commands.find((c) => c.name() === 'level')!;
|
||||
const argNames = levelCmd.registeredArguments.map((a) => a.name());
|
||||
expect(argNames).toContain('level');
|
||||
});
|
||||
});
|
||||
@@ -1,177 +0,0 @@
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
import type { Command } from 'commander';
|
||||
|
||||
import type { LogCategory, LogLevel, LogTier } from './agent-logs.js';
|
||||
|
||||
interface FilterOptions {
|
||||
agent?: string;
|
||||
level?: string;
|
||||
category?: string;
|
||||
tier?: string;
|
||||
limit?: string;
|
||||
db?: string;
|
||||
}
|
||||
|
||||
function parseLimit(raw: string | undefined, defaultVal = 50): number {
|
||||
if (!raw) return defaultVal;
|
||||
const n = parseInt(raw, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : defaultVal;
|
||||
}
|
||||
|
||||
function buildQuery(opts: FilterOptions) {
|
||||
return {
|
||||
...(opts.agent ? { sessionId: opts.agent } : {}),
|
||||
...(opts.level ? { level: opts.level as LogLevel } : {}),
|
||||
...(opts.category ? { category: opts.category as LogCategory } : {}),
|
||||
...(opts.tier ? { tier: opts.tier as LogTier } : {}),
|
||||
limit: parseLimit(opts.limit),
|
||||
};
|
||||
}
|
||||
|
||||
async function openDb(connectionString: string) {
|
||||
const { createDb } = await import('@mosaicstack/db');
|
||||
return createDb(connectionString);
|
||||
}
|
||||
|
||||
function resolveConnectionString(opts: FilterOptions): string | undefined {
|
||||
return opts.db ?? process.env['DATABASE_URL'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register log subcommands on an existing Commander program.
|
||||
* This avoids cross-package Commander version mismatches by using the
|
||||
* caller's Command instance directly.
|
||||
*/
|
||||
export function registerLogCommand(parent: Command): void {
|
||||
const log = parent.command('log').description('Query and manage agent logs');
|
||||
|
||||
// ─── tail ───────────────────────────────────────────────────────────────
|
||||
|
||||
log
|
||||
.command('tail')
|
||||
.description('Tail recent agent logs')
|
||||
.option('--agent <id>', 'Filter by agent/session ID')
|
||||
.option('--level <level>', 'Filter by log level (debug|info|warn|error)')
|
||||
.option('--category <cat>', 'Filter by category (decision|tool_use|learning|error|general)')
|
||||
.option('--tier <tier>', 'Filter by tier (hot|warm|cold)')
|
||||
.option('--limit <n>', 'Number of logs to return (default 50)', '50')
|
||||
.option('--db <connection-string>', 'Database connection string (or set DATABASE_URL)')
|
||||
.action(async (opts: FilterOptions) => {
|
||||
const connStr = resolveConnectionString(opts);
|
||||
if (!connStr) {
|
||||
console.error('Database connection required: use --db or set DATABASE_URL');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const handle = await openDb(connStr);
|
||||
try {
|
||||
const { createLogService } = await import('./log-service.js');
|
||||
const svc = createLogService(handle.db);
|
||||
const query = buildQuery(opts);
|
||||
|
||||
const logs = await svc.logs.query(query);
|
||||
if (logs.length === 0) {
|
||||
console.log('No logs found.');
|
||||
return;
|
||||
}
|
||||
for (const entry of logs) {
|
||||
const ts = new Date(entry.createdAt).toISOString();
|
||||
console.log(`[${ts}] [${entry.level}] [${entry.category}] ${entry.content}`);
|
||||
}
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── search ─────────────────────────────────────────────────────────────
|
||||
|
||||
log
|
||||
.command('search <query>')
|
||||
.description('Full-text search over agent logs')
|
||||
.option('--agent <id>', 'Filter by agent/session ID')
|
||||
.option('--level <level>', 'Filter by log level (debug|info|warn|error)')
|
||||
.option('--category <cat>', 'Filter by category (decision|tool_use|learning|error|general)')
|
||||
.option('--tier <tier>', 'Filter by tier (hot|warm|cold)')
|
||||
.option('--limit <n>', 'Number of logs to return (default 50)', '50')
|
||||
.option('--db <connection-string>', 'Database connection string (or set DATABASE_URL)')
|
||||
.action(async (query: string, opts: FilterOptions) => {
|
||||
const connStr = resolveConnectionString(opts);
|
||||
if (!connStr) {
|
||||
console.error('Database connection required: use --db or set DATABASE_URL');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const handle = await openDb(connStr);
|
||||
try {
|
||||
const { createLogService } = await import('./log-service.js');
|
||||
const svc = createLogService(handle.db);
|
||||
const baseQuery = buildQuery(opts);
|
||||
|
||||
const logs = await svc.logs.query(baseQuery);
|
||||
const lowerQ = query.toLowerCase();
|
||||
const matched = logs.filter(
|
||||
(e) =>
|
||||
e.content.toLowerCase().includes(lowerQ) ||
|
||||
(e.metadata != null && JSON.stringify(e.metadata).toLowerCase().includes(lowerQ)),
|
||||
);
|
||||
|
||||
if (matched.length === 0) {
|
||||
console.log('No matching logs found.');
|
||||
return;
|
||||
}
|
||||
for (const entry of matched) {
|
||||
const ts = new Date(entry.createdAt).toISOString();
|
||||
console.log(`[${ts}] [${entry.level}] [${entry.category}] ${entry.content}`);
|
||||
}
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── export ─────────────────────────────────────────────────────────────
|
||||
|
||||
log
|
||||
.command('export <path>')
|
||||
.description('Export matching logs to an NDJSON file')
|
||||
.option('--agent <id>', 'Filter by agent/session ID')
|
||||
.option('--level <level>', 'Filter by log level (debug|info|warn|error)')
|
||||
.option('--category <cat>', 'Filter by category (decision|tool_use|learning|error|general)')
|
||||
.option('--tier <tier>', 'Filter by tier (hot|warm|cold)')
|
||||
.option('--limit <n>', 'Number of logs to export (default 50)', '50')
|
||||
.option('--db <connection-string>', 'Database connection string (or set DATABASE_URL)')
|
||||
.action(async (outputPath: string, opts: FilterOptions) => {
|
||||
const connStr = resolveConnectionString(opts);
|
||||
if (!connStr) {
|
||||
console.error('Database connection required: use --db or set DATABASE_URL');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const handle = await openDb(connStr);
|
||||
try {
|
||||
const { createLogService } = await import('./log-service.js');
|
||||
const svc = createLogService(handle.db);
|
||||
const query = buildQuery(opts);
|
||||
|
||||
const logs = await svc.logs.query(query);
|
||||
const ndjson = logs.map((e) => JSON.stringify(e)).join('\n');
|
||||
writeFileSync(outputPath, ndjson, 'utf8');
|
||||
console.log(`Exported ${logs.length} log(s) to ${outputPath}`);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── level ──────────────────────────────────────────────────────────────
|
||||
|
||||
log
|
||||
.command('level <level>')
|
||||
.description('Set runtime log level for the connected log service')
|
||||
.action((level: string) => {
|
||||
void level;
|
||||
console.log(
|
||||
'Runtime log level adjustment is not supported in current mode (DB-backed log service).',
|
||||
);
|
||||
process.exitCode = 0;
|
||||
});
|
||||
}
|
||||
@@ -9,4 +9,3 @@ export {
|
||||
type LogTier,
|
||||
type LogQuery,
|
||||
} from './agent-logs.js';
|
||||
export { registerLogCommand } from './cli.js';
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"dependencies": {
|
||||
"@mosaicstack/config": "workspace:*",
|
||||
"@mosaicstack/forge": "workspace:*",
|
||||
"@mosaicstack/log": "workspace:*",
|
||||
"@mosaicstack/macp": "workspace:*",
|
||||
"@mosaicstack/prdy": "workspace:*",
|
||||
"@mosaicstack/quality-rails": "workspace:*",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import { Command } from 'commander';
|
||||
import { registerLogCommand } from '@mosaicstack/log';
|
||||
import { registerQualityRails } from '@mosaicstack/quality-rails';
|
||||
import { registerAgentCommand } from './commands/agent.js';
|
||||
import { registerMissionCommand } from './commands/mission.js';
|
||||
@@ -319,10 +318,6 @@ registerMissionCommand(program);
|
||||
|
||||
registerQualityRails(program);
|
||||
|
||||
// ─── log ─────────────────────────────────────────────────────────────────
|
||||
|
||||
registerLogCommand(program);
|
||||
|
||||
// ─── update ─────────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -401,9 +401,6 @@ importers:
|
||||
'@mosaicstack/db':
|
||||
specifier: workspace:*
|
||||
version: link:../db
|
||||
commander:
|
||||
specifier: ^13.0.0
|
||||
version: 13.1.0
|
||||
drizzle-orm:
|
||||
specifier: ^0.45.1
|
||||
version: 0.45.1(@electric-sql/pglite@0.2.17)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(better-sqlite3@12.8.0)(kysely@0.28.11)(postgres@3.4.8)
|
||||
@@ -463,9 +460,6 @@ importers:
|
||||
'@mosaicstack/forge':
|
||||
specifier: workspace:*
|
||||
version: link:../forge
|
||||
'@mosaicstack/log':
|
||||
specifier: workspace:*
|
||||
version: link:../log
|
||||
'@mosaicstack/macp':
|
||||
specifier: workspace:*
|
||||
version: link:../macp
|
||||
|
||||
Reference in New Issue
Block a user