@osolmaz/pi-workflows 0.15.2 → 0.16.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 +6 -6
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +101 -0
- package/dist/client/client.js +733 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +269 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +118 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +64 -0
- package/dist/controllers/sqlite.js +219 -3
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +384 -156
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +21 -0
- package/dist/extension/session-view.js +127 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +15 -7
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +12 -0
- package/dist/host/runner.js +565 -43
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +8 -3
- package/dist/host/state.js +59 -27
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +73 -0
- package/dist/host/view.js +871 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +6 -8
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +12 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +188 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/store.d.ts +62 -1
- package/dist/workflows/store.js +350 -44
- package/dist/workflows/store.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
- package/docs/SQLITE_STATE.md +13 -11
- package/docs/WORKFLOW_HOST.md +81 -64
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +70 -132
- package/docs/tui-viewer.md +10 -14
- package/docs/workflows.md +56 -3
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +935 -0
- package/src/client/index.ts +24 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +327 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +138 -0
- package/src/controllers/sqlite.ts +342 -3
- package/src/extension/index.ts +482 -171
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +154 -0
- package/src/extension/widget.ts +18 -9
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +749 -75
- package/src/host/state.ts +82 -44
- package/src/host/view.ts +1084 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +11 -11
- package/src/state/schema.ts +12 -1
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/tui.ts +196 -124
- package/src/workflows/store.ts +500 -45
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { MAX_PROTOCOL_MESSAGE_BYTES } from "../client/protocol.js";
|
|
1
2
|
import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
|
|
2
|
-
import { MAX_PROTOCOL_MESSAGE_BYTES } from "./protocol.js";
|
|
3
3
|
|
|
4
4
|
export const WORKER_MESSAGE_SCHEMA = "pi-workflows.worker-message.v1" as const;
|
|
5
5
|
export const WORKER_RESPONSE_SCHEMA = "pi-workflows.worker-response.v1" as const;
|
package/src/state/database.ts
CHANGED
|
@@ -110,6 +110,19 @@ export class StateDatabase {
|
|
|
110
110
|
this.connection.close();
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
readTransaction<T>(operation: () => T): T {
|
|
114
|
+
if (this.connection.inTransaction) return operation();
|
|
115
|
+
this.connection.exec("BEGIN");
|
|
116
|
+
try {
|
|
117
|
+
const result = operation();
|
|
118
|
+
this.connection.exec("COMMIT");
|
|
119
|
+
return result;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (this.connection.inTransaction) this.connection.exec("ROLLBACK");
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
transaction<T>(operation: () => T): T {
|
|
114
127
|
if (this.mode === "read-only") {
|
|
115
128
|
throw new Error("Cannot mutate a read-only Pi Workflows database");
|
package/src/state/prune.ts
CHANGED
|
@@ -34,25 +34,26 @@ export type StatePruneReport = {
|
|
|
34
34
|
applied: boolean;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
options: StatePruneOptions,
|
|
40
|
-
): Promise<StatePruneReport> {
|
|
41
|
-
const cutoff = parseCutoff(options.before);
|
|
37
|
+
export function validateStatePruneOptions(options: StatePruneOptions): void {
|
|
38
|
+
parseCutoff(options.before);
|
|
42
39
|
if (options.apply && options.backupPath === undefined) {
|
|
43
40
|
throw new Error("state prune --apply requires --backup <absolute-path>");
|
|
44
41
|
}
|
|
45
42
|
if (options.backupPath !== undefined && !path.isAbsolute(options.backupPath)) {
|
|
46
43
|
throw new Error("state prune backup path must be absolute");
|
|
47
44
|
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function pruneState(
|
|
48
|
+
state: StateDatabase,
|
|
49
|
+
databasePath: string,
|
|
50
|
+
options: StatePruneOptions,
|
|
51
|
+
): Promise<StatePruneReport> {
|
|
52
|
+
validateStatePruneOptions(options);
|
|
53
|
+
const cutoff = parseCutoff(options.before);
|
|
48
54
|
const lockPath = `${databasePath}.maintenance.lock`;
|
|
49
55
|
const lock = options.apply ? acquireMaintenanceLock(lockPath) : undefined;
|
|
50
|
-
let state: StateDatabase | undefined;
|
|
51
56
|
try {
|
|
52
|
-
state = new StateDatabase({
|
|
53
|
-
filePath: databasePath,
|
|
54
|
-
mode: options.apply ? "read-write" : "read-only",
|
|
55
|
-
});
|
|
56
57
|
const selection = selectRunTrees(state, cutoff);
|
|
57
58
|
const sizeBefore = databaseBytes(databasePath);
|
|
58
59
|
const base = {
|
|
@@ -136,7 +137,6 @@ export async function pruneState(
|
|
|
136
137
|
applied: true,
|
|
137
138
|
};
|
|
138
139
|
} finally {
|
|
139
|
-
state?.close();
|
|
140
140
|
if (lock !== undefined) releaseMaintenanceLock(lockPath, lock);
|
|
141
141
|
}
|
|
142
142
|
}
|
package/src/state/schema.ts
CHANGED
|
@@ -137,6 +137,17 @@ CREATE INDEX runs_project_idx ON runs(project_id, created_at DESC);
|
|
|
137
137
|
CREATE INDEX runs_status_idx ON runs(status, updated_at DESC);
|
|
138
138
|
CREATE INDEX runs_parent_idx ON runs(parent_run_id);
|
|
139
139
|
|
|
140
|
+
CREATE TABLE run_view_content (
|
|
141
|
+
run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
142
|
+
content_hash BLOB NOT NULL,
|
|
143
|
+
media_type TEXT NOT NULL CHECK (media_type IN ('application/json', 'text/plain')),
|
|
144
|
+
byte_length INTEGER NOT NULL CHECK (byte_length >= 0),
|
|
145
|
+
content BLOB NOT NULL,
|
|
146
|
+
created_at INTEGER NOT NULL,
|
|
147
|
+
PRIMARY KEY (run_id, content_hash, media_type),
|
|
148
|
+
CHECK (byte_length = length(content))
|
|
149
|
+
) STRICT;
|
|
150
|
+
|
|
140
151
|
CREATE TABLE viewer_runs (
|
|
141
152
|
run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
|
|
142
153
|
presentation_revision INTEGER NOT NULL CHECK (presentation_revision >= 1),
|
|
@@ -303,7 +314,7 @@ CREATE TABLE host_commands (
|
|
|
303
314
|
'checkpoint.answer', 'decision.answer', 'interaction.submit', 'interaction.update',
|
|
304
315
|
'notification.claim', 'notification.deliver', 'turn.claim', 'turn.resolve',
|
|
305
316
|
'controller.list', 'controller.get', 'controller.apply', 'controller.reconcile', 'controller.delete',
|
|
306
|
-
'host.status', 'host.stop'
|
|
317
|
+
'host.status', 'host.stop', 'state.backup', 'state.prune'
|
|
307
318
|
)),
|
|
308
319
|
idempotency_key TEXT NOT NULL,
|
|
309
320
|
request_fingerprint BLOB NOT NULL CHECK (length(request_fingerprint) = 32),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
4
|
+
|
|
5
|
+
/** Verify only an explicit inactive backup. Active state is verified through WorkflowClient. */
|
|
6
|
+
export function verifyInactiveBackup(
|
|
7
|
+
databasePath: string,
|
|
8
|
+
activeDatabasePath: string = workflowStatePath(),
|
|
9
|
+
): void {
|
|
10
|
+
const resolved = path.resolve(databasePath);
|
|
11
|
+
const active = path.resolve(activeDatabasePath);
|
|
12
|
+
if (sameFile(resolved, active)) {
|
|
13
|
+
throw new Error("Active workflow state must be verified through the workflow host");
|
|
14
|
+
}
|
|
15
|
+
const state = new StateDatabase({ filePath: resolved, mode: "read-only" });
|
|
16
|
+
try {
|
|
17
|
+
state.integrityCheck();
|
|
18
|
+
} finally {
|
|
19
|
+
state.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sameFile(left: string, right: string): boolean {
|
|
24
|
+
if (left === right) return true;
|
|
25
|
+
if (!fs.existsSync(left) || !fs.existsSync(right)) return false;
|
|
26
|
+
const leftStat = fs.statSync(left);
|
|
27
|
+
const rightStat = fs.statSync(right);
|
|
28
|
+
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
|
29
|
+
}
|
package/src/viewer/cli.ts
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
import {
|
|
6
|
+
import { WorkflowClient } from "../client/client.js";
|
|
7
|
+
import { materializeRunView } from "../client/materialize.js";
|
|
6
8
|
import { syncHerdrPlugin } from "../herdr/setup.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
formatDuration,
|
|
14
|
-
renderRunDetailLines,
|
|
15
|
-
renderRunListLines,
|
|
16
|
-
runElapsedMs,
|
|
17
|
-
statusLabel,
|
|
18
|
-
} from "./render.js";
|
|
19
|
-
import { runViewer } from "./tui.js";
|
|
9
|
+
import type { JsonValue } from "../state/json.js";
|
|
10
|
+
import { verifyInactiveBackup } from "./backup.js";
|
|
11
|
+
import { renderClientView, runViewer } from "./tui.js";
|
|
12
|
+
|
|
13
|
+
const CLI_CLIENT_ID = "pi-workflows-cli";
|
|
20
14
|
|
|
21
15
|
const USAGE = `pi-workflows — workflow runs and controller resources
|
|
22
16
|
|
|
@@ -25,7 +19,8 @@ Usage:
|
|
|
25
19
|
pi-workflows runs
|
|
26
20
|
pi-workflows controllers
|
|
27
21
|
pi-workflows controller <controller> <key>
|
|
28
|
-
pi-workflows state status
|
|
22
|
+
pi-workflows state status
|
|
23
|
+
pi-workflows state verify [inactive-backup]
|
|
29
24
|
pi-workflows state backup <destination>
|
|
30
25
|
pi-workflows state prune --before <timestamp> --dry-run
|
|
31
26
|
pi-workflows state prune --before <timestamp> --backup <absolute-path> --apply
|
|
@@ -36,7 +31,7 @@ Usage:
|
|
|
36
31
|
pi-workflows herdr sync [--json]
|
|
37
32
|
pi-workflows herdr setup [--json]
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
Active state is available only through the package-owned workflow host.
|
|
40
35
|
`;
|
|
41
36
|
|
|
42
37
|
export type CliArgs = {
|
|
@@ -48,6 +43,7 @@ export type CliArgs = {
|
|
|
48
43
|
hostAction?: "start" | "status" | "stop" | "run";
|
|
49
44
|
stateAction?: "status" | "verify" | "backup" | "prune";
|
|
50
45
|
backupDestination?: string;
|
|
46
|
+
verifyBackup?: string;
|
|
51
47
|
pruneBefore?: string;
|
|
52
48
|
pruneApply?: boolean;
|
|
53
49
|
once: boolean;
|
|
@@ -58,6 +54,13 @@ export type CliArgs = {
|
|
|
58
54
|
export function parseCliArgs(argv: string[]): CliArgs {
|
|
59
55
|
const args = [...argv];
|
|
60
56
|
const command = args[0] && !args[0].startsWith("-") ? (args.shift() as string) : "view";
|
|
57
|
+
if (
|
|
58
|
+
!["view", "runs", "controllers", "controller", "state", "host", "herdr", "help"].includes(
|
|
59
|
+
command,
|
|
60
|
+
)
|
|
61
|
+
) {
|
|
62
|
+
throw new Error(`Unknown command: ${command}`);
|
|
63
|
+
}
|
|
61
64
|
let once = false;
|
|
62
65
|
let json = false;
|
|
63
66
|
let project: string | undefined;
|
|
@@ -143,12 +146,22 @@ export function parseCliArgs(argv: string[]): CliArgs {
|
|
|
143
146
|
json,
|
|
144
147
|
};
|
|
145
148
|
}
|
|
149
|
+
if (action === "verify") {
|
|
150
|
+
if (positionals.length > 2) throw new Error("state verify accepts one inactive backup path");
|
|
151
|
+
return {
|
|
152
|
+
command,
|
|
153
|
+
stateAction: action,
|
|
154
|
+
...(positionals[1] === undefined ? {} : { verifyBackup: positionals[1] }),
|
|
155
|
+
once,
|
|
156
|
+
json,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
146
159
|
if (positionals.length !== 1) throw new Error(`state ${action} accepts no other arguments`);
|
|
147
160
|
return { command, stateAction: action, once, json };
|
|
148
161
|
}
|
|
149
162
|
if (command === "herdr") {
|
|
150
163
|
if (positionals.length !== 1 || !["sync", "setup"].includes(positionals[0] as string)) {
|
|
151
|
-
throw new Error("herdr requires the sync action");
|
|
164
|
+
throw new Error("herdr requires the sync or setup action");
|
|
152
165
|
}
|
|
153
166
|
return { command, herdrAction: positionals[0] as string, once, json };
|
|
154
167
|
}
|
|
@@ -161,101 +174,12 @@ export function parseCliArgs(argv: string[]): CliArgs {
|
|
|
161
174
|
};
|
|
162
175
|
}
|
|
163
176
|
|
|
164
|
-
function printRuns(databasePath: string): void {
|
|
165
|
-
const store = new WorkflowRunStore(databasePath, { readOnly: true });
|
|
166
|
-
try {
|
|
167
|
-
const runs = store.listRuns();
|
|
168
|
-
if (runs.length === 0) {
|
|
169
|
-
process.stdout.write("No workflow runs found.\n");
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
for (const run of runs) {
|
|
173
|
-
const state = run.state;
|
|
174
|
-
const title = state.runTitle ? ` — ${sanitizeText(state.runTitle)}` : "";
|
|
175
|
-
process.stdout.write(
|
|
176
|
-
`${statusLabel(state.status)} ${sanitizeText(state.workflowName)}${title} ${state.runId} ${formatDuration(runElapsedMs(state))}\n`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
} finally {
|
|
180
|
-
store.close();
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function printOnce(databasePath: string, runId: string | undefined): void {
|
|
185
|
-
const store = new WorkflowRunStore(databasePath, { readOnly: true });
|
|
186
|
-
try {
|
|
187
|
-
const size = { width: process.stdout.columns ?? 100, height: 1_000 };
|
|
188
|
-
if (runId === undefined) {
|
|
189
|
-
process.stdout.write(`${renderRunListLines(store.listRuns(), 0, size).join("\n")}\n`);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const run = store.readRun(runId, { includeTrace: true });
|
|
193
|
-
if (run === null) throw new Error(`Run not found: ${runId}`);
|
|
194
|
-
process.stdout.write(`${renderRunDetailLines(run, size).join("\n")}\n`);
|
|
195
|
-
} finally {
|
|
196
|
-
store.close();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function printControllers(databasePath: string): void {
|
|
201
|
-
const store = openControllerStore(databasePath);
|
|
202
|
-
if (store === undefined) {
|
|
203
|
-
process.stdout.write("No controller resources found.\n");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
const resources = store.listResources();
|
|
208
|
-
if (resources.length === 0) {
|
|
209
|
-
process.stdout.write("No controller resources found.\n");
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
for (const resource of resources) {
|
|
213
|
-
const condition =
|
|
214
|
-
resource.status.conditions.find((item) => item.type === "Ready") ??
|
|
215
|
-
resource.status.conditions[0];
|
|
216
|
-
const conditionText =
|
|
217
|
-
condition === undefined
|
|
218
|
-
? "unknown"
|
|
219
|
-
: `${String(condition.status)}:${sanitizeText(condition.reason)}`;
|
|
220
|
-
process.stdout.write(
|
|
221
|
-
`${sanitizeText(resource.metadata.controller)} ${sanitizeText(resource.metadata.key)} generation=${resource.metadata.generation} ready=${conditionText}\n`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
} finally {
|
|
225
|
-
store.close();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function printController(databasePath: string, controller: string, key: string): void {
|
|
230
|
-
const store = openControllerStore(databasePath);
|
|
231
|
-
if (store === undefined) throw new Error("Controller state database not found");
|
|
232
|
-
try {
|
|
233
|
-
const resource = store.getResource({ controller, key });
|
|
234
|
-
if (resource === undefined)
|
|
235
|
-
throw new Error(`Controller resource not found: ${controller}/${key}`);
|
|
236
|
-
process.stdout.write(
|
|
237
|
-
`${JSON.stringify(
|
|
238
|
-
{
|
|
239
|
-
resource,
|
|
240
|
-
effects: store.listEffects(resource.metadata.uid),
|
|
241
|
-
workflows: store.listWorkflows(resource.metadata.uid),
|
|
242
|
-
events: store.listEvents({ controller, key, limit: 50 }),
|
|
243
|
-
},
|
|
244
|
-
null,
|
|
245
|
-
2,
|
|
246
|
-
)}\n`,
|
|
247
|
-
);
|
|
248
|
-
} finally {
|
|
249
|
-
store.close();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
177
|
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
254
178
|
let args: CliArgs;
|
|
255
179
|
try {
|
|
256
180
|
args = parseCliArgs(argv);
|
|
257
181
|
} catch (error) {
|
|
258
|
-
process.stderr.write(`${
|
|
182
|
+
process.stderr.write(`${errorMessage(error)}\n\n${USAGE}`);
|
|
259
183
|
return 2;
|
|
260
184
|
}
|
|
261
185
|
if (args.command === "help") {
|
|
@@ -263,24 +187,7 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
263
187
|
return 0;
|
|
264
188
|
}
|
|
265
189
|
|
|
266
|
-
const databasePath = workflowStateDatabasePath();
|
|
267
190
|
try {
|
|
268
|
-
if (args.command === "runs") {
|
|
269
|
-
printRuns(databasePath);
|
|
270
|
-
return 0;
|
|
271
|
-
}
|
|
272
|
-
if (args.command === "controllers") {
|
|
273
|
-
printControllers(databasePath);
|
|
274
|
-
return 0;
|
|
275
|
-
}
|
|
276
|
-
if (args.command === "controller") {
|
|
277
|
-
printController(databasePath, args.controllerName as string, args.resourceKey as string);
|
|
278
|
-
return 0;
|
|
279
|
-
}
|
|
280
|
-
if (args.command === "state") {
|
|
281
|
-
await runStateCommand(databasePath, args);
|
|
282
|
-
return 0;
|
|
283
|
-
}
|
|
284
191
|
if (args.command === "host") {
|
|
285
192
|
return await runHost(args.hostAction as "start" | "status" | "stop" | "run", args.piArgs);
|
|
286
193
|
}
|
|
@@ -289,65 +196,106 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
289
196
|
process.stdout.write(args.json ? `${JSON.stringify(result)}\n` : `${result.message}\n`);
|
|
290
197
|
return 0;
|
|
291
198
|
}
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
199
|
+
if (
|
|
200
|
+
args.command === "state" &&
|
|
201
|
+
args.stateAction === "verify" &&
|
|
202
|
+
args.verifyBackup !== undefined
|
|
203
|
+
) {
|
|
204
|
+
verifyInactiveBackup(args.verifyBackup);
|
|
205
|
+
process.stdout.write(`Inactive SQLite backup is valid: ${path.resolve(args.verifyBackup)}\n`);
|
|
295
206
|
return 0;
|
|
296
207
|
}
|
|
208
|
+
|
|
209
|
+
if (
|
|
210
|
+
args.command === "state" &&
|
|
211
|
+
args.stateAction === "prune" &&
|
|
212
|
+
args.backupDestination !== undefined &&
|
|
213
|
+
!path.isAbsolute(args.backupDestination)
|
|
214
|
+
) {
|
|
215
|
+
throw new Error("state prune backup path must be absolute");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const client = new WorkflowClient({ clientId: CLI_CLIENT_ID });
|
|
219
|
+
try {
|
|
220
|
+
await client.ensureRunning();
|
|
221
|
+
if (args.command === "runs") {
|
|
222
|
+
const runs = await firstRunsView(client);
|
|
223
|
+
printJsonLines(runs);
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
if (args.command === "controllers" || args.command === "controller") {
|
|
227
|
+
const response = await client.request({
|
|
228
|
+
operation: args.command === "controllers" ? "controller.list" : "controller.get",
|
|
229
|
+
payload: {
|
|
230
|
+
projectPath: process.cwd(),
|
|
231
|
+
...(args.command === "controller"
|
|
232
|
+
? { controller: args.controllerName as string, key: args.resourceKey as string }
|
|
233
|
+
: {}),
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
requireAccepted(response);
|
|
237
|
+
process.stdout.write(`${JSON.stringify(response.receipt ?? null, null, 2)}\n`);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
if (args.command === "state") {
|
|
241
|
+
await runStateCommand(client, args);
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
if (args.command === "view") {
|
|
245
|
+
if (args.once || !process.stdout.isTTY) {
|
|
246
|
+
const view =
|
|
247
|
+
args.runId === undefined
|
|
248
|
+
? await firstRunsView(client)
|
|
249
|
+
: await firstRunView(client, args.runId);
|
|
250
|
+
process.stdout.write(
|
|
251
|
+
`${renderClientView(view, process.stdout.columns ?? 100, Number.MAX_SAFE_INTEGER, 0, 0).join("\n")}\n`,
|
|
252
|
+
);
|
|
253
|
+
} else {
|
|
254
|
+
await runViewer({ client, runId: args.runId });
|
|
255
|
+
}
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
await client.close();
|
|
260
|
+
}
|
|
297
261
|
process.stderr.write(`Unknown command: ${args.command}\n\n${USAGE}`);
|
|
298
262
|
return 2;
|
|
299
263
|
} catch (error) {
|
|
300
|
-
process.stderr.write(`${
|
|
264
|
+
process.stderr.write(`${errorMessage(error)}\n`);
|
|
301
265
|
return 1;
|
|
302
266
|
}
|
|
303
267
|
}
|
|
304
268
|
|
|
305
|
-
async function runStateCommand(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
(SELECT count(*) FROM runs) AS runs,
|
|
336
|
-
(SELECT count(*) FROM controller_resources) AS controllers,
|
|
337
|
-
(SELECT count(*) FROM human_decisions) AS decisions,
|
|
338
|
-
(SELECT count(*) FROM workflow_settings) AS settingsScopes,
|
|
339
|
-
(SELECT count(*) FROM workflow_follow_ups WHERE status IN ('queued', 'pending_presentation', 'ready')) AS pendingFollowUps,
|
|
340
|
-
(SELECT count(*) FROM effects WHERE status IN ('pending', 'applying', 'ambiguous')) AS unsettledEffects,
|
|
341
|
-
(SELECT count(*) FROM leases
|
|
342
|
-
WHERE owner_id IS NOT NULL AND expires_at > ?) AS activeLeases`,
|
|
343
|
-
)
|
|
344
|
-
.get(Date.now());
|
|
345
|
-
process.stdout.write(
|
|
346
|
-
`Database: ${databasePath}\nSize: ${fs.statSync(databasePath).size} bytes\n${JSON.stringify(counts)}\n`,
|
|
347
|
-
);
|
|
348
|
-
} finally {
|
|
349
|
-
state.close();
|
|
350
|
-
}
|
|
269
|
+
async function runStateCommand(client: WorkflowClient, args: CliArgs): Promise<void> {
|
|
270
|
+
const operation =
|
|
271
|
+
args.stateAction === "status"
|
|
272
|
+
? "state.status"
|
|
273
|
+
: args.stateAction === "verify"
|
|
274
|
+
? "state.verify"
|
|
275
|
+
: args.stateAction === "backup"
|
|
276
|
+
? "state.backup"
|
|
277
|
+
: "state.prune";
|
|
278
|
+
const payload: JsonValue =
|
|
279
|
+
args.stateAction === "backup"
|
|
280
|
+
? { destination: path.resolve(args.backupDestination as string) }
|
|
281
|
+
: args.stateAction === "prune"
|
|
282
|
+
? {
|
|
283
|
+
before: args.pruneBefore as string,
|
|
284
|
+
apply: args.pruneApply === true,
|
|
285
|
+
...(args.backupDestination === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: { backupPath: path.resolve(args.backupDestination) }),
|
|
288
|
+
}
|
|
289
|
+
: {};
|
|
290
|
+
const durable =
|
|
291
|
+
args.stateAction === "backup" || (args.stateAction === "prune" && args.pruneApply === true);
|
|
292
|
+
const idempotencyKey = durable ? `state-${randomUUID()}` : undefined;
|
|
293
|
+
const response =
|
|
294
|
+
idempotencyKey === undefined
|
|
295
|
+
? await client.request({ operation, payload })
|
|
296
|
+
: await client.requestDurable({ operation, payload, idempotencyKey });
|
|
297
|
+
requireAccepted(response);
|
|
298
|
+
process.stdout.write(`${JSON.stringify(response.receipt ?? {})}\n`);
|
|
351
299
|
}
|
|
352
300
|
|
|
353
301
|
async function runHost(
|
|
@@ -368,22 +316,56 @@ async function runHost(
|
|
|
368
316
|
});
|
|
369
317
|
return 0;
|
|
370
318
|
}
|
|
371
|
-
const client = new
|
|
372
|
-
|
|
373
|
-
const response =
|
|
319
|
+
const client = new WorkflowClient({ clientId: CLI_CLIENT_ID });
|
|
320
|
+
try {
|
|
321
|
+
const response =
|
|
322
|
+
action === "start"
|
|
323
|
+
? await client.ensureRunning()
|
|
324
|
+
: await client.request({ operation: action === "status" ? "host.status" : "host.stop" });
|
|
374
325
|
process.stdout.write(`${JSON.stringify(response.receipt ?? {})}\n`);
|
|
375
326
|
return response.outcome === "accepted" || response.outcome === "adopted" ? 0 : 1;
|
|
327
|
+
} finally {
|
|
328
|
+
await client.close();
|
|
376
329
|
}
|
|
377
|
-
|
|
378
|
-
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function firstRunsView(client: WorkflowClient): Promise<JsonValue> {
|
|
333
|
+
return await firstSubscriptionEvent((listener) => client.watchRuns(listener));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function firstRunView(client: WorkflowClient, runId: string): Promise<JsonValue> {
|
|
337
|
+
const view = await client.getRun(runId);
|
|
338
|
+
if (view === null) throw new Error(`Workflow run not found: ${runId}`);
|
|
339
|
+
return await materializeRunView(client, view);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function firstSubscriptionEvent(
|
|
343
|
+
subscribe: (listener: (event: { payload: JsonValue }) => void) => Promise<() => Promise<void>>,
|
|
344
|
+
): Promise<JsonValue> {
|
|
345
|
+
let resolveEvent!: (value: JsonValue) => void;
|
|
346
|
+
const event = new Promise<JsonValue>((resolve) => {
|
|
347
|
+
resolveEvent = resolve;
|
|
379
348
|
});
|
|
380
|
-
|
|
381
|
-
|
|
349
|
+
const unsubscribe = await subscribe((received) => resolveEvent(received.payload));
|
|
350
|
+
try {
|
|
351
|
+
return await event;
|
|
352
|
+
} finally {
|
|
353
|
+
await unsubscribe();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function printJsonLines(value: JsonValue): void {
|
|
358
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
359
|
+
process.stdout.write("No workflow runs found.\n");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
for (const item of value) process.stdout.write(`${JSON.stringify(item)}\n`);
|
|
382
363
|
}
|
|
383
364
|
|
|
384
|
-
function
|
|
385
|
-
if (
|
|
386
|
-
|
|
365
|
+
function requireAccepted(response: { outcome: string; error?: string }): void {
|
|
366
|
+
if (response.outcome !== "accepted" && response.outcome !== "adopted") {
|
|
367
|
+
throw new Error(response.error ?? `Workflow host returned ${response.outcome}`);
|
|
368
|
+
}
|
|
387
369
|
}
|
|
388
370
|
|
|
389
371
|
function packageRoot(): string {
|
|
@@ -396,6 +378,10 @@ function requiredValue(args: string[], option: string): string {
|
|
|
396
378
|
return value;
|
|
397
379
|
}
|
|
398
380
|
|
|
381
|
+
function errorMessage(error: unknown): string {
|
|
382
|
+
return error instanceof Error ? error.message : String(error);
|
|
383
|
+
}
|
|
384
|
+
|
|
399
385
|
const entryPath = process.argv[1];
|
|
400
386
|
const resolvedEntry = entryPath === undefined ? undefined : realpathSyncSafe(entryPath);
|
|
401
387
|
if (resolvedEntry !== undefined && import.meta.url === pathToFileURL(resolvedEntry).href) {
|