smithers-orchestrator 0.25.0 → 0.25.1
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/package.json +23 -23
- package/src/bin/smithers-delegation.js +98 -0
- package/src/bin/smithers.js +7 -98
- package/src/migrateSmithersStore.js +86 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smithers-orchestrator",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Public Smithers facade for durable coding-agent workflows and Gateway operation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://smithers.sh",
|
|
@@ -92,28 +92,28 @@
|
|
|
92
92
|
"incur": "^0.4.1",
|
|
93
93
|
"react": "^19.2.5",
|
|
94
94
|
"zod": "^4.3.6",
|
|
95
|
-
"@smithers-orchestrator/agents": "0.25.
|
|
96
|
-
"@smithers-orchestrator/
|
|
97
|
-
"@smithers-orchestrator/
|
|
98
|
-
"@smithers-orchestrator/
|
|
99
|
-
"@smithers-orchestrator/
|
|
100
|
-
"@smithers-orchestrator/
|
|
101
|
-
"@smithers-orchestrator/
|
|
102
|
-
"@smithers-orchestrator/
|
|
103
|
-
"@smithers-orchestrator/gateway-
|
|
104
|
-
"@smithers-orchestrator/
|
|
105
|
-
"@smithers-orchestrator/
|
|
106
|
-
"@smithers-orchestrator/
|
|
107
|
-
"@smithers-orchestrator/
|
|
108
|
-
"@smithers-orchestrator/react
|
|
109
|
-
"@smithers-orchestrator/sandbox": "0.25.
|
|
110
|
-
"@smithers-orchestrator/
|
|
111
|
-
"@smithers-orchestrator/
|
|
112
|
-
"@smithers-orchestrator/server": "0.25.
|
|
113
|
-
"@smithers-orchestrator/time-travel": "0.25.
|
|
114
|
-
"@smithers-orchestrator/
|
|
115
|
-
"@smithers-orchestrator/
|
|
116
|
-
"@smithers-orchestrator/
|
|
95
|
+
"@smithers-orchestrator/agents": "0.25.1",
|
|
96
|
+
"@smithers-orchestrator/db": "0.25.1",
|
|
97
|
+
"@smithers-orchestrator/engine": "0.25.1",
|
|
98
|
+
"@smithers-orchestrator/errors": "0.25.1",
|
|
99
|
+
"@smithers-orchestrator/components": "0.25.1",
|
|
100
|
+
"@smithers-orchestrator/cli": "0.25.1",
|
|
101
|
+
"@smithers-orchestrator/control-plane": "0.25.1",
|
|
102
|
+
"@smithers-orchestrator/memory": "0.25.1",
|
|
103
|
+
"@smithers-orchestrator/gateway-client": "0.25.1",
|
|
104
|
+
"@smithers-orchestrator/observability": "0.25.1",
|
|
105
|
+
"@smithers-orchestrator/driver": "0.25.1",
|
|
106
|
+
"@smithers-orchestrator/openapi": "0.25.1",
|
|
107
|
+
"@smithers-orchestrator/react-reconciler": "0.25.1",
|
|
108
|
+
"@smithers-orchestrator/gateway-react": "0.25.1",
|
|
109
|
+
"@smithers-orchestrator/sandbox": "0.25.1",
|
|
110
|
+
"@smithers-orchestrator/scheduler": "0.25.1",
|
|
111
|
+
"@smithers-orchestrator/scorers": "0.25.1",
|
|
112
|
+
"@smithers-orchestrator/server": "0.25.1",
|
|
113
|
+
"@smithers-orchestrator/time-travel": "0.25.1",
|
|
114
|
+
"@smithers-orchestrator/tool-context": "0.25.1",
|
|
115
|
+
"@smithers-orchestrator/graph": "0.25.1",
|
|
116
|
+
"@smithers-orchestrator/vcs": "0.25.1"
|
|
117
117
|
},
|
|
118
118
|
"optionalDependencies": {
|
|
119
119
|
"@electric-sql/pglite": "0.5.1",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, parse, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const WORKFLOW_PATH_COMMANDS = new Set([
|
|
5
|
+
"up",
|
|
6
|
+
"graph",
|
|
7
|
+
"fork",
|
|
8
|
+
"replay",
|
|
9
|
+
"revert",
|
|
10
|
+
"timetravel",
|
|
11
|
+
]);
|
|
12
|
+
const WORKFLOW_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".mts"]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} value
|
|
16
|
+
*/
|
|
17
|
+
export function isOptionLike(value) {
|
|
18
|
+
return value.startsWith("-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} value
|
|
23
|
+
*/
|
|
24
|
+
export function looksLikeWorkflowPath(value) {
|
|
25
|
+
if (isOptionLike(value))
|
|
26
|
+
return false;
|
|
27
|
+
return WORKFLOW_EXTENSIONS.has(parse(value).ext);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string[]} args
|
|
32
|
+
*/
|
|
33
|
+
export function getExplicitWorkflowPath(args) {
|
|
34
|
+
if (args.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
if (looksLikeWorkflowPath(args[0]))
|
|
37
|
+
return args[0];
|
|
38
|
+
for (let index = 0; index < args.length; index++) {
|
|
39
|
+
const arg = args[index];
|
|
40
|
+
if (!WORKFLOW_PATH_COMMANDS.has(arg))
|
|
41
|
+
continue;
|
|
42
|
+
for (let nextIndex = index + 1; nextIndex < args.length; nextIndex++) {
|
|
43
|
+
const candidate = args[nextIndex];
|
|
44
|
+
if (looksLikeWorkflowPath(candidate))
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
for (const arg of args) {
|
|
50
|
+
if (looksLikeWorkflowPath(arg))
|
|
51
|
+
return arg;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the local `smithers-orchestrator` package's bin JS file under
|
|
58
|
+
* `<directory>/node_modules/`. Going through `package.json` (instead of the
|
|
59
|
+
* `.bin/smithers` shell shim npm/pnpm generate) is the whole point: the shim
|
|
60
|
+
* is `#!/bin/sh` and re-execing it with `process.execPath` (bun) makes bun
|
|
61
|
+
* parse shell as JavaScript, which crashes with `Expected ")" but found
|
|
62
|
+
* "$(echo "`.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} directory
|
|
65
|
+
*/
|
|
66
|
+
export function resolveLocalSmithersBinJs(directory) {
|
|
67
|
+
const pkgJsonPath = resolve(directory, "node_modules/smithers-orchestrator/package.json");
|
|
68
|
+
if (!existsSync(pkgJsonPath))
|
|
69
|
+
return null;
|
|
70
|
+
let pkg;
|
|
71
|
+
try {
|
|
72
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const binEntry = typeof pkg?.bin === "string" ? pkg.bin : pkg?.bin?.smithers;
|
|
77
|
+
if (typeof binEntry !== "string" || binEntry.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
const binPath = resolve(dirname(pkgJsonPath), binEntry);
|
|
80
|
+
return existsSync(binPath) ? binPath : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} cwd
|
|
85
|
+
* @param {string} workflowPath
|
|
86
|
+
*/
|
|
87
|
+
export function findNearestWorkflowLocalCli(cwd, workflowPath) {
|
|
88
|
+
let current = dirname(resolve(cwd, workflowPath));
|
|
89
|
+
while (true) {
|
|
90
|
+
const localBin = resolveLocalSmithersBinJs(current);
|
|
91
|
+
if (localBin)
|
|
92
|
+
return localBin;
|
|
93
|
+
const parent = dirname(current);
|
|
94
|
+
if (parent === current)
|
|
95
|
+
return null;
|
|
96
|
+
current = parent;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/bin/smithers.js
CHANGED
|
@@ -1,104 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"replay",
|
|
12
|
-
"revert",
|
|
13
|
-
"timetravel",
|
|
14
|
-
]);
|
|
15
|
-
const WORKFLOW_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".mts"]);
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @param {string} value
|
|
19
|
-
*/
|
|
20
|
-
function isOptionLike(value) {
|
|
21
|
-
return value.startsWith("-");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {string} value
|
|
26
|
-
*/
|
|
27
|
-
function looksLikeWorkflowPath(value) {
|
|
28
|
-
if (isOptionLike(value))
|
|
29
|
-
return false;
|
|
30
|
-
return WORKFLOW_EXTENSIONS.has(parse(value).ext);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {string[]} args
|
|
35
|
-
*/
|
|
36
|
-
function getExplicitWorkflowPath(args) {
|
|
37
|
-
if (args.length === 0)
|
|
38
|
-
return null;
|
|
39
|
-
if (looksLikeWorkflowPath(args[0]))
|
|
40
|
-
return args[0];
|
|
41
|
-
for (let index = 0; index < args.length; index++) {
|
|
42
|
-
const arg = args[index];
|
|
43
|
-
if (!WORKFLOW_PATH_COMMANDS.has(arg))
|
|
44
|
-
continue;
|
|
45
|
-
for (let nextIndex = index + 1; nextIndex < args.length; nextIndex++) {
|
|
46
|
-
const candidate = args[nextIndex];
|
|
47
|
-
if (looksLikeWorkflowPath(candidate))
|
|
48
|
-
return candidate;
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
for (const arg of args) {
|
|
53
|
-
if (looksLikeWorkflowPath(arg))
|
|
54
|
-
return arg;
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Resolve the local `smithers-orchestrator` package's bin JS file under
|
|
61
|
-
* `<directory>/node_modules/`. Going through `package.json` (instead of the
|
|
62
|
-
* `.bin/smithers` shell shim npm/pnpm generate) is the whole point: the shim
|
|
63
|
-
* is `#!/bin/sh` and re-execing it with `process.execPath` (bun) makes bun
|
|
64
|
-
* parse shell as JavaScript, which crashes with `Expected ")" but found
|
|
65
|
-
* "$(echo "`.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} directory
|
|
68
|
-
*/
|
|
69
|
-
function resolveLocalSmithersBinJs(directory) {
|
|
70
|
-
const pkgJsonPath = resolve(directory, "node_modules/smithers-orchestrator/package.json");
|
|
71
|
-
if (!existsSync(pkgJsonPath))
|
|
72
|
-
return null;
|
|
73
|
-
let pkg;
|
|
74
|
-
try {
|
|
75
|
-
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
const binEntry = typeof pkg?.bin === "string" ? pkg.bin : pkg?.bin?.smithers;
|
|
80
|
-
if (typeof binEntry !== "string" || binEntry.length === 0)
|
|
81
|
-
return null;
|
|
82
|
-
const binPath = resolve(dirname(pkgJsonPath), binEntry);
|
|
83
|
-
return existsSync(binPath) ? binPath : null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {string} cwd
|
|
88
|
-
* @param {string} workflowPath
|
|
89
|
-
*/
|
|
90
|
-
function findNearestWorkflowLocalCli(cwd, workflowPath) {
|
|
91
|
-
let current = dirname(resolve(cwd, workflowPath));
|
|
92
|
-
while (true) {
|
|
93
|
-
const localBin = resolveLocalSmithersBinJs(current);
|
|
94
|
-
if (localBin)
|
|
95
|
-
return localBin;
|
|
96
|
-
const parent = dirname(current);
|
|
97
|
-
if (parent === current)
|
|
98
|
-
return null;
|
|
99
|
-
current = parent;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
6
|
+
import {
|
|
7
|
+
getExplicitWorkflowPath,
|
|
8
|
+
findNearestWorkflowLocalCli,
|
|
9
|
+
resolveLocalSmithersBinJs,
|
|
10
|
+
} from "./smithers-delegation.js";
|
|
102
11
|
|
|
103
12
|
/**
|
|
104
13
|
* When a workflow directory (`.smithers/`) exists in the user's cwd and it's
|
|
@@ -471,6 +471,78 @@ function writeMigrationMarker(result, sourceStats) {
|
|
|
471
471
|
writeFileSync(result.markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
// bun:sqlite surfaces a corrupt, encrypted, or non-SQLite source file with one
|
|
475
|
+
// of these substrings (case-insensitive). Detecting them lets us replace the
|
|
476
|
+
// raw, unactionable engine text with guidance the operator can follow.
|
|
477
|
+
const CORRUPT_SQLITE_MARKERS = ["malformed", "not a database", "file is encrypted", "disk image is malformed"];
|
|
478
|
+
const UNOPENABLE_SQLITE_MARKERS = ["unable to open database file"];
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* @param {unknown} error
|
|
482
|
+
* @returns {boolean}
|
|
483
|
+
*/
|
|
484
|
+
function isCorruptSqliteError(error) {
|
|
485
|
+
const message = (error instanceof Error ? error.message : String(error ?? "")).toLowerCase();
|
|
486
|
+
return CORRUPT_SQLITE_MARKERS.some((marker) => message.includes(marker));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @param {unknown} error
|
|
491
|
+
* @returns {boolean}
|
|
492
|
+
*/
|
|
493
|
+
function isUnopenableSqliteError(error) {
|
|
494
|
+
const message = (error instanceof Error ? error.message : String(error ?? "")).toLowerCase();
|
|
495
|
+
return UNOPENABLE_SQLITE_MARKERS.some((marker) => message.includes(marker));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Open the legacy SQLite source and read its schema-level metadata. A corrupt,
|
|
500
|
+
* encrypted, or non-SQLite file fails here (open, BEGIN, or the first query);
|
|
501
|
+
* we wrap that into an actionable SmithersError so the CLI maps it to a clean
|
|
502
|
+
* exit code instead of leaking the raw bun:sqlite text. The source is opened
|
|
503
|
+
* read-only and nothing has been written to the target yet, so no partial
|
|
504
|
+
* output can exist when this throws.
|
|
505
|
+
*
|
|
506
|
+
* @param {string} dbPath
|
|
507
|
+
* @returns {{ sqlite: Database; runCount: number; schemaVersion: string; tables: Array<{ name: string; sql: string }>; indexes: Array<{ name: string; sql: string }> }}
|
|
508
|
+
*/
|
|
509
|
+
function openSourceStore(dbPath) {
|
|
510
|
+
/** @type {Database | undefined} */
|
|
511
|
+
let sqlite;
|
|
512
|
+
try {
|
|
513
|
+
sqlite = new Database(dbPath, { readonly: true });
|
|
514
|
+
sqlite.exec("BEGIN");
|
|
515
|
+
const runCount = sqliteTableExists(sqlite, "_smithers_runs") ? countSqliteRows(sqlite, "_smithers_runs") : 0;
|
|
516
|
+
const schemaVersion = sqliteSchemaVersion(sqlite);
|
|
517
|
+
const tables = orderedTables(sourceTables(sqlite));
|
|
518
|
+
const indexes = sourceIndexes(sqlite);
|
|
519
|
+
return { sqlite, runCount, schemaVersion, tables, indexes };
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
try {
|
|
523
|
+
sqlite?.exec("ROLLBACK");
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// No open transaction to roll back; closing the handle is enough.
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
sqlite?.close();
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// Best-effort read-only source cleanup.
|
|
533
|
+
}
|
|
534
|
+
if (isCorruptSqliteError(error)) {
|
|
535
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
536
|
+
throw new SmithersError("DB_QUERY_FAILED", `The legacy SQLite store at ${dbPath} appears to be corrupted (${original}). Smithers cannot migrate a corrupt store. Verify with: sqlite3 ${dbPath} 'PRAGMA integrity_check'. If it reports corruption, restore from a backup or start fresh; the original file was left untouched.`, { dbPath }, { cause: error });
|
|
537
|
+
}
|
|
538
|
+
if (isUnopenableSqliteError(error)) {
|
|
539
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
540
|
+
throw new SmithersError("DB_QUERY_FAILED", `Could not open the legacy SQLite store at ${dbPath} (${original}). It may be locked by another process, have unreadable permissions, or be missing its -wal/-shm sidecar files (copy smithers.db together with smithers.db-wal and smithers.db-shm). The original file was left untouched.`, { dbPath }, { cause: error });
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
474
546
|
/**
|
|
475
547
|
* @param {string} dbPath
|
|
476
548
|
*/
|
|
@@ -498,6 +570,16 @@ export async function migrateSmithersStore(opts = {}) {
|
|
|
498
570
|
});
|
|
499
571
|
}
|
|
500
572
|
const target = normalizeTarget(opts.to);
|
|
573
|
+
// Validate the Postgres connection target BEFORE opening the source store, so
|
|
574
|
+
// `migrate --to postgres` with no url fails fast with a clear INVALID_INPUT
|
|
575
|
+
// instead of being masked by an unrelated source-open error (e.g. an
|
|
576
|
+
// unreadable or sidecar-less smithers.db).
|
|
577
|
+
const postgresUrl = target === "postgres" ? (opts.url ?? env.SMITHERS_POSTGRES_URL ?? env.DATABASE_URL) : undefined;
|
|
578
|
+
if (target === "postgres" && !postgresUrl) {
|
|
579
|
+
throw new SmithersError("INVALID_INPUT", "smithers migrate --to postgres requires --url, SMITHERS_POSTGRES_URL, or DATABASE_URL.", {
|
|
580
|
+
target,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
501
583
|
const markerPath = markerPathFor(dbPath, workspaceRoot);
|
|
502
584
|
const batchSize = Math.max(1, Math.floor(opts.batchSize ?? DEFAULT_BATCH_SIZE));
|
|
503
585
|
const keepSqlite = opts.keepSqlite ?? true;
|
|
@@ -507,12 +589,9 @@ export async function migrateSmithersStore(opts = {}) {
|
|
|
507
589
|
/** @type {(import("./CreateSmithersApi.ts").CreateSmithersApi<Record<string, import("zod").ZodObject<any>>> & { close?: () => Promise<void> }) | undefined} */
|
|
508
590
|
let targetApi;
|
|
509
591
|
try {
|
|
510
|
-
|
|
511
|
-
sqlite.
|
|
512
|
-
const runCount
|
|
513
|
-
const schemaVersion = sqliteSchemaVersion(sqlite);
|
|
514
|
-
const tables = orderedTables(sourceTables(sqlite));
|
|
515
|
-
const indexes = sourceIndexes(sqlite);
|
|
592
|
+
const source = openSourceStore(dbPath);
|
|
593
|
+
sqlite = source.sqlite;
|
|
594
|
+
const { runCount, schemaVersion, tables, indexes } = source;
|
|
516
595
|
await emitMigrationLog("info", "smithers.migration.started", {
|
|
517
596
|
dbPath,
|
|
518
597
|
target,
|
|
@@ -521,15 +600,9 @@ export async function migrateSmithersStore(opts = {}) {
|
|
|
521
600
|
schemaVersion,
|
|
522
601
|
}, "smithers:migrate");
|
|
523
602
|
if (target === "postgres") {
|
|
524
|
-
const connectionString = opts.url ?? env.SMITHERS_POSTGRES_URL ?? env.DATABASE_URL;
|
|
525
|
-
if (!connectionString) {
|
|
526
|
-
throw new SmithersError("INVALID_INPUT", "smithers migrate --to postgres requires --url, SMITHERS_POSTGRES_URL, or DATABASE_URL.", {
|
|
527
|
-
target,
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
603
|
targetApi = await createSmithersPostgres({}, {
|
|
531
604
|
provider: "postgres",
|
|
532
|
-
connectionString,
|
|
605
|
+
connectionString: postgresUrl,
|
|
533
606
|
});
|
|
534
607
|
}
|
|
535
608
|
else {
|