@routecraft/cli 0.5.0-canary.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/chunk-3CAI2NPQ.js +2 -0
- package/dist/chunk-3CAI2NPQ.js.map +1 -0
- package/dist/{chunk-DPJWCR2V.js → chunk-5UHEZNN5.js} +14 -14
- package/dist/chunk-5UHEZNN5.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/tui/app.js +3 -3
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/db.d.ts +3 -3
- package/dist/tui/db.js +1 -1
- package/package.json +9 -12
- package/dist/chunk-DPJWCR2V.js.map +0 -1
package/dist/tui/db.d.ts
CHANGED
|
@@ -39,9 +39,9 @@ declare class TelemetryDb {
|
|
|
39
39
|
/**
|
|
40
40
|
* Open a telemetry database in read-only mode.
|
|
41
41
|
*
|
|
42
|
-
* Uses dynamic `import()` so the module resolves
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Uses dynamic `import("bun:sqlite")` so the module resolves at runtime
|
|
43
|
+
* (the CLI is Bun-only; `bun:sqlite` is a Bun built-in and is not a
|
|
44
|
+
* resolvable spec under Node).
|
|
45
45
|
*/
|
|
46
46
|
static open(dbPath: string): Promise<TelemetryDb>;
|
|
47
47
|
/**
|
package/dist/tui/db.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export{a as TelemetryDb}from'../chunk-
|
|
1
|
+
export{a as TelemetryDb}from'../chunk-5UHEZNN5.js';//# sourceMappingURL=db.js.map
|
|
2
2
|
//# sourceMappingURL=db.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@routecraft/cli",
|
|
3
|
-
"version": "0.5.0
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI for running Routecraft routes",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -16,32 +16,29 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "tsup src/index.ts src/tui/app.tsx src/tui/db.ts --format esm --dts --external react --external ink --external
|
|
19
|
+
"build": "tsup src/index.ts src/tui/app.tsx src/tui/db.ts --format esm --dts --external react --external ink --external bun:sqlite",
|
|
20
20
|
"test": "vitest",
|
|
21
21
|
"test:coverage": "vitest --coverage",
|
|
22
|
-
"prepublishOnly": "
|
|
22
|
+
"prepublishOnly": "bun run build"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@opentelemetry/sdk-trace-base": "^2.7.
|
|
26
|
-
"@routecraft/routecraft": "^0.5.0
|
|
25
|
+
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
26
|
+
"@routecraft/routecraft": "^0.5.0",
|
|
27
27
|
"agent-browser": "^0.17.1",
|
|
28
|
-
"better-sqlite3": "^11.10.0",
|
|
29
28
|
"cheerio": "^1.2.0",
|
|
30
29
|
"commander": "^14.0.3",
|
|
31
30
|
"croner": "^10.0.1",
|
|
32
31
|
"dotenv": "^17.4.2",
|
|
33
|
-
"imapflow": "^1.3.
|
|
32
|
+
"imapflow": "^1.3.3",
|
|
34
33
|
"ink": "^5.2.1",
|
|
35
|
-
"jose": "^6.2.
|
|
34
|
+
"jose": "^6.2.3",
|
|
36
35
|
"mailparser": "^3.9.8",
|
|
37
36
|
"nodemailer": "^6.10.1",
|
|
38
37
|
"papaparse": "^5.5.3",
|
|
39
38
|
"pino-pretty": "^13.1.3",
|
|
40
|
-
"react": "^18.3.1"
|
|
41
|
-
"tsx": "^4.21.0"
|
|
39
|
+
"react": "^18.3.1"
|
|
42
40
|
},
|
|
43
41
|
"devDependencies": {
|
|
44
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
45
42
|
"@types/mailparser": "^3.4.6",
|
|
46
43
|
"@types/nodemailer": "^6.4.23",
|
|
47
44
|
"@types/react": "^18.3.28",
|
|
@@ -55,7 +52,7 @@
|
|
|
55
52
|
"craft": "./dist/index.js"
|
|
56
53
|
},
|
|
57
54
|
"engines": {
|
|
58
|
-
"
|
|
55
|
+
"bun": ">=1.1.0"
|
|
59
56
|
},
|
|
60
57
|
"keywords": [
|
|
61
58
|
"routecraft",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tui/db.ts"],"names":["escapeLike","TelemetryDb","_TelemetryDb","db","key","sql","s","dbPath","Database","mod","wdb","c","routeId","limit","exchangeId","correlationId","searchId","info","col","options","sinceId","params","escaped","row","durations","r","percentile","bucketCount","bucketWidthSec","nowSec","snapped","windowStart","rows","result","tRows","eRow","throughput","sorted","p","idx","lo","hi"],"mappings":"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","file":"chunk-DPJWCR2V.js","sourcesContent":["import type { TelemetryEvent } from \"@routecraft/routecraft\";\n\n/** Exchange row shape from the telemetry SQLite database. */\ninterface TelemetryExchange {\n id: string;\n routeId: string;\n contextId: string;\n correlationId: string;\n status: string;\n startedAt: string;\n completedAt: string | null;\n durationMs: number | null;\n error: string | null;\n}\n\n/**\n * Minimal type for the better-sqlite3 database to avoid hard dependency.\n */\ninterface Database {\n prepare(sql: string): Statement;\n close(): void;\n pragma(pragma: string): unknown;\n}\n\ninterface Statement {\n all(...params: unknown[]): unknown[];\n get(...params: unknown[]): unknown;\n run(...params: unknown[]): unknown;\n}\n\ntype DatabaseConstructor = new (\n filename: string,\n options?: { readonly?: boolean },\n) => Database;\n\n/**\n * Read-only accessor for the telemetry SQLite database.\n * Used by the TUI to query historical data without affecting the running engine.\n *\n * Use the static `open()` factory instead of the constructor directly.\n */\n/** Escape LIKE metacharacters so they match literally. */\nfunction escapeLike(s: string): string {\n return s.replace(/[%_\\\\]/g, \"\\\\$&\");\n}\n\nexport class TelemetryDb {\n private db: Database;\n private stmtCache = new Map<string, Statement>();\n\n private constructor(db: Database) {\n this.db = db;\n }\n\n /** Prepare and cache a statement by key. Avoids re-parsing on every call. */\n private stmt(key: string, sql: string): Statement {\n let s = this.stmtCache.get(key);\n if (!s) {\n s = this.db.prepare(sql);\n this.stmtCache.set(key, s);\n }\n return s;\n }\n\n /**\n * Open a telemetry database in read-only mode.\n *\n * Uses dynamic `import()` so the module resolves correctly under pnpm's\n * strict hoisting (CJS `require()` from an ESM bundle does not follow\n * the package's own dependency graph).\n */\n static async open(dbPath: string): Promise<TelemetryDb> {\n let Database: DatabaseConstructor;\n try {\n const mod = await import(\"better-sqlite3\");\n Database = (\n \"default\" in mod && typeof mod.default === \"function\"\n ? mod.default\n : mod\n ) as DatabaseConstructor;\n } catch {\n throw new Error(\n \"better-sqlite3 is not installed. Install it with: pnpm add better-sqlite3\",\n );\n }\n\n const db = new Database(dbPath, { readonly: true });\n try {\n db.pragma(\"journal_mode = WAL\");\n } catch {\n // Read-only connection cannot change journal mode; safe to ignore\n }\n\n // Ensure indexes exist for TUI query performance.\n // If the DB was created before indexes were added, create them now\n // using a separate writable connection.\n {\n const wdb = new Database(dbPath);\n try {\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_exchanges_started_at ON exchanges(started_at)\",\n )\n .run();\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_exchanges_route_started ON exchanges(route_id, started_at)\",\n )\n .run();\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_exchanges_correlation_id ON exchanges(correlation_id)\",\n )\n .run();\n // Add exchange_id/correlation_id columns to events table if missing\n const cols = wdb.prepare(\"PRAGMA table_info(events)\").all() as Array<{\n name: string;\n }>;\n if (!cols.some((c) => c.name === \"exchange_id\")) {\n wdb.prepare(\"ALTER TABLE events ADD COLUMN exchange_id TEXT\").run();\n wdb\n .prepare(\"ALTER TABLE events ADD COLUMN correlation_id TEXT\")\n .run();\n }\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_events_exchange_id ON events(exchange_id)\",\n )\n .run();\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_events_correlation_id ON events(correlation_id)\",\n )\n .run();\n // Ensure exchange_snapshots table exists for older databases\n wdb\n .prepare(\n `CREATE TABLE IF NOT EXISTS exchange_snapshots (\n exchange_id TEXT NOT NULL,\n context_id TEXT NOT NULL,\n headers TEXT NOT NULL,\n body TEXT,\n truncated INTEGER NOT NULL DEFAULT 0,\n captured_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY (exchange_id, context_id)\n )`,\n )\n .run();\n } catch {\n // Best-effort; the writer will create them on next restart\n } finally {\n wdb.close();\n }\n }\n\n return new TelemetryDb(db);\n }\n\n /**\n * Get a summary of all routes with aggregated metrics.\n * Routes are grouped by ID across all context runs so that restarting\n * the same application does not create duplicate rows.\n */\n getRouteSummary(): Array<{\n id: string;\n status: string;\n totalExchanges: number;\n completedExchanges: number;\n failedExchanges: number;\n droppedExchanges: number;\n avgDurationMs: number | null;\n }> {\n const s = this.stmt(\n \"routeSummary\",\n `WITH unique_routes AS (\n SELECT\n id,\n MAX(registered_at) AS registered_at,\n (SELECT r2.status FROM routes r2\n WHERE r2.id = r.id\n ORDER BY r2.registered_at DESC LIMIT 1) AS status\n FROM routes r\n GROUP BY id\n ),\n exchange_counts AS (\n SELECT\n route_id,\n COUNT(*) AS total,\n SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,\n SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,\n SUM(CASE WHEN status = 'dropped' THEN 1 ELSE 0 END) AS dropped\n FROM exchanges\n GROUP BY route_id\n ),\n recent_avg AS (\n SELECT route_id, AVG(duration_ms) AS avgDur\n FROM exchanges\n WHERE started_at >= datetime('now', '-5 minutes')\n GROUP BY route_id\n )\n SELECT\n ur.id,\n ur.status,\n COALESCE(ec.total, 0) AS totalExchanges,\n COALESCE(ec.completed, 0) AS completedExchanges,\n COALESCE(ec.failed, 0) AS failedExchanges,\n COALESCE(ec.dropped, 0) AS droppedExchanges,\n ra.avgDur AS avgDurationMs\n FROM unique_routes ur\n LEFT JOIN exchange_counts ec ON ur.id = ec.route_id\n LEFT JOIN recent_avg ra ON ur.id = ra.route_id\n ORDER BY ur.registered_at DESC`,\n );\n return s.all() as Array<{\n id: string;\n status: string;\n totalExchanges: number;\n completedExchanges: number;\n failedExchanges: number;\n droppedExchanges: number;\n avgDurationMs: number | null;\n }>;\n }\n\n /**\n * Get exchanges for a specific route, ordered by most recent first.\n */\n getExchangesByRoute(routeId: string, limit = -1): TelemetryExchange[] {\n const s = this.stmt(\n \"exchangesByRoute\",\n `SELECT\n id,\n route_id AS routeId,\n context_id AS contextId,\n correlation_id AS correlationId,\n status,\n started_at AS startedAt,\n completed_at AS completedAt,\n duration_ms AS durationMs,\n error\n FROM exchanges\n WHERE route_id = ?\n ORDER BY started_at DESC\n LIMIT ?`,\n );\n return s.all(routeId, limit) as TelemetryExchange[];\n }\n\n /**\n * Get all exchanges across all routes, ordered by most recent first.\n */\n getAllExchanges(limit = -1): TelemetryExchange[] {\n const s = this.stmt(\n \"allExchanges\",\n `SELECT\n id,\n route_id AS routeId,\n context_id AS contextId,\n correlation_id AS correlationId,\n status,\n started_at AS startedAt,\n completed_at AS completedAt,\n duration_ms AS durationMs,\n error\n FROM exchanges\n ORDER BY started_at DESC\n LIMIT ?`,\n );\n return s.all(limit) as TelemetryExchange[];\n }\n\n /**\n * Get all failed exchanges across all routes, ordered by most recent first.\n * Shows every failed exchange including child exchanges from split/multicast,\n * without deduplication by correlation chain.\n */\n getFailedExchanges(limit = -1): TelemetryExchange[] {\n const s = this.stmt(\n \"failedExchanges\",\n `SELECT\n id,\n route_id AS routeId,\n context_id AS contextId,\n correlation_id AS correlationId,\n status,\n started_at AS startedAt,\n completed_at AS completedAt,\n duration_ms AS durationMs,\n error\n FROM exchanges\n WHERE status = 'failed'\n ORDER BY started_at DESC\n LIMIT ?`,\n );\n return s.all(limit) as TelemetryExchange[];\n }\n\n /**\n * Get events for an exchange and all related exchanges sharing the\n * same correlation ID. Uses indexed exchange_id/correlation_id columns\n * with a fallback to details LIKE for databases created before the\n * column migration.\n */\n getEventsByExchange(\n exchangeId: string,\n correlationId: string,\n ): TelemetryEvent[] {\n const searchId = correlationId || exchangeId;\n\n // Try indexed column lookup first (fast path)\n const hasColumns = this.hasEventIdColumns();\n if (hasColumns) {\n const s = this.stmt(\n \"eventsByExchangeIndexed\",\n `SELECT\n id,\n timestamp,\n context_id AS contextId,\n event_name AS eventName,\n details\n FROM events\n WHERE exchange_id = ? OR correlation_id = ?\n ORDER BY id ASC`,\n );\n return s.all(searchId, searchId) as TelemetryEvent[];\n }\n\n // Fallback: full-text LIKE scan for older databases\n const s = this.stmt(\n \"eventsByExchangeFallback\",\n `SELECT\n id,\n timestamp,\n context_id AS contextId,\n event_name AS eventName,\n details\n FROM events\n WHERE details LIKE '%' || ? || '%' ESCAPE '\\\\'\n ORDER BY id ASC`,\n );\n return s.all(escapeLike(searchId)) as TelemetryEvent[];\n }\n\n /** Check whether the events table has the exchange_id column. */\n private hasEventIdColumns(): boolean {\n if (this._hasEventIdColumns !== undefined) return this._hasEventIdColumns;\n try {\n const info = this.db.prepare(\"PRAGMA table_info(events)\").all() as Array<{\n name: string;\n }>;\n this._hasEventIdColumns = info.some((col) => col.name === \"exchange_id\");\n } catch {\n this._hasEventIdColumns = false;\n }\n return this._hasEventIdColumns;\n }\n private _hasEventIdColumns: boolean | undefined;\n\n /**\n * Get recent events, optionally filtered by event name pattern or route ID.\n */\n getRecentEvents(options?: {\n limit?: number;\n eventNameFilter?: string;\n routeIdFilter?: string;\n sinceId?: number;\n }): TelemetryEvent[] {\n const limit = options?.limit ?? 100;\n const sinceId = options?.sinceId ?? 0;\n\n let sql = `\n SELECT\n id,\n timestamp,\n context_id AS contextId,\n event_name AS eventName,\n details\n FROM events\n WHERE id > ?\n `;\n const params: unknown[] = [sinceId];\n\n if (options?.eventNameFilter) {\n sql += \" AND event_name LIKE ? ESCAPE '\\\\'\";\n params.push(`%${escapeLike(options.eventNameFilter)}%`);\n }\n\n if (options?.routeIdFilter) {\n sql +=\n \" AND (event_name LIKE ? ESCAPE '\\\\' OR details LIKE ? ESCAPE '\\\\')\";\n const escaped = escapeLike(options.routeIdFilter);\n params.push(`%${escaped}%`, `%${escaped}%`);\n }\n\n sql += \" ORDER BY id DESC LIMIT ?\";\n params.push(limit);\n\n const stmt = this.db.prepare(sql);\n return stmt.all(...params) as TelemetryEvent[];\n }\n\n /**\n * Get aggregated metrics for the dashboard, including duration percentiles.\n * Percentiles are computed over exchanges from the last 5 minutes.\n */\n getMetrics(): import(\"./types.js\").Metrics {\n const s = this.stmt(\n \"metrics\",\n `SELECT\n (SELECT COUNT(DISTINCT id) FROM routes) AS totalRoutes,\n (SELECT COUNT(*) FROM exchanges) AS totalExchanges,\n (SELECT COUNT(*) FROM exchanges WHERE status = 'completed') AS completedExchanges,\n (SELECT COUNT(*) FROM exchanges WHERE status = 'failed') AS failedExchanges,\n (SELECT COUNT(*) FROM exchanges WHERE status = 'dropped') AS droppedExchanges,\n (SELECT AVG(duration_ms) FROM exchanges\n WHERE started_at >= datetime('now', '-5 minutes')) AS avgDurationMs`,\n );\n const row = s.get() as {\n totalRoutes: number;\n totalExchanges: number;\n completedExchanges: number;\n failedExchanges: number;\n droppedExchanges: number;\n avgDurationMs: number | null;\n };\n\n // Compute percentiles from recent exchanges with non-null durations\n const pctStmt = this.stmt(\n \"percentiles\",\n `SELECT duration_ms\n FROM exchanges\n WHERE duration_ms IS NOT NULL\n AND started_at >= datetime('now', '-5 minutes')\n ORDER BY duration_ms ASC`,\n );\n const durations = (pctStmt.all() as Array<{ duration_ms: number }>).map(\n (r) => r.duration_ms,\n );\n\n return {\n ...row,\n errorRate:\n row.totalExchanges > 0 ? row.failedExchanges / row.totalExchanges : 0,\n p90DurationMs: percentile(durations, 0.9),\n p95DurationMs: percentile(durations, 0.95),\n p99DurationMs: percentile(durations, 0.99),\n };\n }\n\n /**\n * Get exchange counts for a rolling window using fine-grained buckets.\n * Default: 60 buckets of 5 seconds = 5-minute rolling window.\n * Returns values oldest-first for sparkline rendering.\n */\n getLiveTrafficBuckets(bucketCount = 60, bucketWidthSec = 5): number[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const snapped = Math.floor(nowSec / bucketWidthSec) * bucketWidthSec;\n const windowStart = snapped - (bucketCount - 1) * bucketWidthSec;\n\n const s = this.stmt(\n \"liveTraffic\",\n `SELECT\n CAST((CAST(strftime('%s', started_at) AS INTEGER) - ?) / ? AS INTEGER) AS bucket,\n COUNT(*) AS cnt\n FROM exchanges\n WHERE CAST(strftime('%s', started_at) AS INTEGER) >= ?\n AND CAST(strftime('%s', started_at) AS INTEGER) <= ?\n GROUP BY bucket`,\n );\n const rows = s.all(\n windowStart,\n bucketWidthSec,\n windowStart,\n snapped + bucketWidthSec,\n ) as Array<{ bucket: number; cnt: number }>;\n\n const result = new Array(bucketCount).fill(0) as number[];\n for (const row of rows) {\n if (row.bucket >= 0 && row.bucket < bucketCount) {\n result[row.bucket] += row.cnt;\n }\n }\n return result;\n }\n\n /**\n * Get activity data for a single route: throughput sparkline + recent error count.\n */\n getSingleRouteActivity(\n routeId: string,\n bucketCount = 12,\n bucketWidthSec = 5,\n ): { throughput: number[]; recentErrors: number } {\n const nowSec = Math.floor(Date.now() / 1000);\n const snapped = Math.floor(nowSec / bucketWidthSec) * bucketWidthSec;\n const windowStart = snapped - (bucketCount - 1) * bucketWidthSec;\n\n const tStmt = this.stmt(\n \"singleRouteActivity\",\n `SELECT\n CAST((CAST(strftime('%s', started_at) AS INTEGER) - ?) / ? AS INTEGER) AS bucket,\n COUNT(*) AS cnt\n FROM exchanges\n WHERE route_id = ?\n AND CAST(strftime('%s', started_at) AS INTEGER) >= ?\n GROUP BY bucket`,\n );\n const tRows = tStmt.all(\n windowStart,\n bucketWidthSec,\n routeId,\n windowStart,\n ) as Array<{ bucket: number; cnt: number }>;\n\n const eStmt = this.stmt(\n \"singleRouteErrors\",\n `SELECT COUNT(*) AS cnt\n FROM exchanges\n WHERE route_id = ?\n AND status = 'failed'\n AND CAST(strftime('%s', started_at) AS INTEGER) >= ?`,\n );\n const eRow = eStmt.get(routeId, windowStart) as { cnt: number } | undefined;\n\n const throughput = new Array(bucketCount).fill(0) as number[];\n for (const row of tRows) {\n if (row.bucket >= 0 && row.bucket < bucketCount) {\n throughput[row.bucket] = row.cnt;\n }\n }\n\n return { throughput, recentErrors: eRow?.cnt ?? 0 };\n }\n\n /**\n * Get the maximum event ID (for tailing/polling).\n */\n getMaxEventId(): number {\n const stmt = this.db.prepare(\n \"SELECT COALESCE(MAX(id), 0) AS maxId FROM events\",\n );\n const row = stmt.get() as { maxId: number };\n return row.maxId;\n }\n\n /**\n * Get the exchange snapshot (headers and body) for a given exchange.\n * Returns null if no snapshot was captured.\n */\n getExchangeSnapshot(\n exchangeId: string,\n ): import(\"./types.js\").ExchangeSnapshot | null {\n try {\n const s = this.stmt(\n \"exchangeSnapshot\",\n `SELECT headers, body, truncated\n FROM exchange_snapshots\n WHERE exchange_id = ?\n LIMIT 1`,\n );\n const row = s.get(exchangeId) as\n | { headers: string; body: string | null; truncated: number }\n | undefined;\n if (!row) return null;\n return {\n headers: row.headers,\n body: row.body,\n truncated: row.truncated !== 0,\n };\n } catch {\n // Table may not exist in very old databases\n return null;\n }\n }\n\n /**\n * Close the database connection.\n */\n close(): void {\n this.db.close();\n }\n}\n\n/**\n * Compute a percentile from a sorted array of numbers using linear interpolation.\n * Returns null if the array is empty.\n */\nfunction percentile(sorted: number[], p: number): number | null {\n if (sorted.length === 0) return null;\n const idx = p * (sorted.length - 1);\n const lo = Math.floor(idx);\n const hi = Math.ceil(idx);\n if (lo === hi) return sorted[lo]!;\n return sorted[lo]! + (sorted[hi]! - sorted[lo]!) * (idx - lo);\n}\n"]}
|