@wanbicoi/next-steps 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +203 -2
- package/dist/cli.js +1440 -0
- package/package.json +59 -4
- package/viz/template.html +707 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1440 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { join as join4 } from "path";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
7
|
+
import { parseArgs } from "util";
|
|
8
|
+
|
|
9
|
+
// src/types.ts
|
|
10
|
+
var END = "END";
|
|
11
|
+
|
|
12
|
+
class AppError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
exitCode;
|
|
15
|
+
details;
|
|
16
|
+
constructor(code, message, exitCode = 1, details) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.exitCode = exitCode;
|
|
20
|
+
this.details = details;
|
|
21
|
+
this.name = "AppError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/format.ts
|
|
26
|
+
function table(headers, rows) {
|
|
27
|
+
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
|
|
28
|
+
const render = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
|
|
29
|
+
return [render(headers), ...rows.map(render)].join(`
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
function formatScope(scope) {
|
|
33
|
+
return scope.length > 0 ? scope.join(" > ") : "-";
|
|
34
|
+
}
|
|
35
|
+
function formatTime(iso) {
|
|
36
|
+
return iso.slice(0, 19).replace("T", " ");
|
|
37
|
+
}
|
|
38
|
+
function optionLine(option, index) {
|
|
39
|
+
const label = option.edge ? `${option.edge} -> ${option.node}` : option.node;
|
|
40
|
+
const title = option.node === END ? "" : option.title;
|
|
41
|
+
const head = ` ${index + 1}. ${label}${title ? ` ${title}` : ""}`.trimEnd();
|
|
42
|
+
const lines = [head, ` ${option.description}`];
|
|
43
|
+
if (option.condition)
|
|
44
|
+
lines.push(` when: ${option.condition}`);
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
function formatNext(next) {
|
|
48
|
+
if (next.done && next.next.length === 0)
|
|
49
|
+
return "done: true";
|
|
50
|
+
if (next.next.length === 0)
|
|
51
|
+
return "next: (none)";
|
|
52
|
+
const lines = ["next:"];
|
|
53
|
+
for (const [index, option] of next.next.entries()) {
|
|
54
|
+
lines.push(...optionLine(option, index));
|
|
55
|
+
}
|
|
56
|
+
return lines.join(`
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
function formatCurrent(next) {
|
|
60
|
+
if (next.done) {
|
|
61
|
+
return `current: ${next.current ?? END} (finished) scope: ${formatScope(next.scope)}`;
|
|
62
|
+
}
|
|
63
|
+
if (next.current === null) {
|
|
64
|
+
return `current: (not started) scope: ${formatScope(next.scope)}`;
|
|
65
|
+
}
|
|
66
|
+
const title = next.currentTitle ? ` (${next.currentTitle})` : "";
|
|
67
|
+
return `current: ${next.current}${title} scope: ${formatScope(next.scope)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/commands/shared.ts
|
|
71
|
+
function usageError(message) {
|
|
72
|
+
return new AppError("usage", message, 2);
|
|
73
|
+
}
|
|
74
|
+
function requireString(value, flag) {
|
|
75
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
76
|
+
throw usageError(`--${flag} is required`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function toJson(value) {
|
|
81
|
+
return JSON.stringify(value, null, 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/commands/session.ts
|
|
85
|
+
function cmdCreate(service, input) {
|
|
86
|
+
const workflowId = requireString(input.workflowId, "workflow");
|
|
87
|
+
const { session, workflowTitle, next } = service.create({ workflowId, key: input.key });
|
|
88
|
+
if (input.json) {
|
|
89
|
+
return toJson({
|
|
90
|
+
key: session.key,
|
|
91
|
+
workflowId: session.workflowId,
|
|
92
|
+
workflowTitle,
|
|
93
|
+
status: session.status,
|
|
94
|
+
current: null,
|
|
95
|
+
scope: next.scope,
|
|
96
|
+
done: next.done,
|
|
97
|
+
next: next.next,
|
|
98
|
+
statePath: service.statePath
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return [
|
|
102
|
+
`session: ${session.key}`,
|
|
103
|
+
`workflow: ${session.workflowId} ${workflowTitle}`,
|
|
104
|
+
`status: ${session.status}`,
|
|
105
|
+
"",
|
|
106
|
+
formatNext(next)
|
|
107
|
+
].join(`
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
110
|
+
function cmdLog(service, input) {
|
|
111
|
+
const key = requireString(input.key, "key");
|
|
112
|
+
const node = requireString(input.node, "node");
|
|
113
|
+
const { session, step, next } = service.log(key, node);
|
|
114
|
+
if (input.json) {
|
|
115
|
+
return toJson({
|
|
116
|
+
step,
|
|
117
|
+
state: session,
|
|
118
|
+
done: next.done,
|
|
119
|
+
next: next.next,
|
|
120
|
+
statePath: service.statePath
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const origin = step.from ? ` (from ${step.from}${step.edge ? ` via ${step.edge}` : ""})` : " (entry)";
|
|
124
|
+
return [
|
|
125
|
+
`logged: #${step.seq} ${step.node}${origin}`,
|
|
126
|
+
`scope: ${formatScope(step.scope)} status: ${session.status}`,
|
|
127
|
+
"",
|
|
128
|
+
formatNext(next)
|
|
129
|
+
].join(`
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
function cmdNext(service, input) {
|
|
133
|
+
const key = requireString(input.key, "key");
|
|
134
|
+
const { session, next } = service.next(key);
|
|
135
|
+
if (input.json) {
|
|
136
|
+
return toJson({
|
|
137
|
+
key,
|
|
138
|
+
workflowId: session.workflowId,
|
|
139
|
+
status: session.status,
|
|
140
|
+
current: next.current === null ? null : { node: next.current, title: next.currentTitle, at: session.updatedAt },
|
|
141
|
+
scope: next.scope,
|
|
142
|
+
done: next.done,
|
|
143
|
+
next: next.next,
|
|
144
|
+
statePath: service.statePath
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return [formatCurrent(next), "", formatNext(next)].join(`
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
function cmdSteps(service, input) {
|
|
151
|
+
const key = requireString(input.key, "key");
|
|
152
|
+
const { session, steps } = service.steps(key, { limit: input.limit, after: input.after });
|
|
153
|
+
if (input.json) {
|
|
154
|
+
return toJson({
|
|
155
|
+
key,
|
|
156
|
+
workflowId: session.workflowId,
|
|
157
|
+
status: session.status,
|
|
158
|
+
current: session.current,
|
|
159
|
+
scope: session.stack.map((frame) => frame.workflowId),
|
|
160
|
+
steps,
|
|
161
|
+
statePath: service.statePath
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const head = [
|
|
165
|
+
`session: ${key} status: ${session.status} current: ${session.current ?? "(not started)"}`,
|
|
166
|
+
`scope: ${formatScope(session.stack.map((frame) => frame.workflowId))}`,
|
|
167
|
+
""
|
|
168
|
+
].join(`
|
|
169
|
+
`);
|
|
170
|
+
if (steps.length === 0)
|
|
171
|
+
return `${head}no steps yet`;
|
|
172
|
+
const rows = steps.map((step) => [
|
|
173
|
+
String(step.seq),
|
|
174
|
+
step.node,
|
|
175
|
+
step.edge ?? "-",
|
|
176
|
+
step.from ?? "-",
|
|
177
|
+
formatScope(step.scope),
|
|
178
|
+
formatTime(step.at)
|
|
179
|
+
]);
|
|
180
|
+
return head + table(["SEQ", "NODE", "EDGE", "FROM", "SCOPE", "AT"], rows);
|
|
181
|
+
}
|
|
182
|
+
function cmdKeys(service, input) {
|
|
183
|
+
const sessions = service.keys(input.workflowId);
|
|
184
|
+
if (input.json) {
|
|
185
|
+
return toJson({ statePath: service.statePath, sessions });
|
|
186
|
+
}
|
|
187
|
+
const head = `state: ${service.statePath}
|
|
188
|
+
`;
|
|
189
|
+
if (sessions.length === 0)
|
|
190
|
+
return `${head}no sessions`;
|
|
191
|
+
const rows = sessions.map((session) => [
|
|
192
|
+
session.key,
|
|
193
|
+
session.workflowId,
|
|
194
|
+
session.status,
|
|
195
|
+
session.current ?? "-",
|
|
196
|
+
formatTime(session.updatedAt)
|
|
197
|
+
]);
|
|
198
|
+
return head + table(["KEY", "WORKFLOW", "STATUS", "CURRENT", "UPDATED"], rows);
|
|
199
|
+
}
|
|
200
|
+
function cmdReset(service, input) {
|
|
201
|
+
const key = requireString(input.key, "key");
|
|
202
|
+
const result = service.reset(key);
|
|
203
|
+
if (input.json) {
|
|
204
|
+
return toJson({ ...result, statePath: service.statePath });
|
|
205
|
+
}
|
|
206
|
+
return `reset: ${result.key} (${result.deletedSteps} steps deleted)`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/commands/shared.ts
|
|
210
|
+
function usageError2(message) {
|
|
211
|
+
return new AppError("usage", message, 2);
|
|
212
|
+
}
|
|
213
|
+
function optionalInt(value, flag) {
|
|
214
|
+
if (value === undefined)
|
|
215
|
+
return;
|
|
216
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) {
|
|
217
|
+
throw usageError2(`--${flag} must be a non-negative integer`);
|
|
218
|
+
}
|
|
219
|
+
return Number(value);
|
|
220
|
+
}
|
|
221
|
+
function toJson2(value) {
|
|
222
|
+
return JSON.stringify(value, null, 2);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/commands/viz.ts
|
|
226
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
227
|
+
import { createServer } from "node:http";
|
|
228
|
+
import { createRequire } from "node:module";
|
|
229
|
+
import { dirname, join } from "node:path";
|
|
230
|
+
import { fileURLToPath } from "node:url";
|
|
231
|
+
var require2 = createRequire(import.meta.url);
|
|
232
|
+
function findTemplate() {
|
|
233
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
234
|
+
for (let depth = 0;depth < 6; depth++) {
|
|
235
|
+
const candidate = join(dir, "viz", "template.html");
|
|
236
|
+
if (existsSync(candidate))
|
|
237
|
+
return candidate;
|
|
238
|
+
const parent = dirname(dir);
|
|
239
|
+
if (parent === dir)
|
|
240
|
+
break;
|
|
241
|
+
dir = parent;
|
|
242
|
+
}
|
|
243
|
+
throw new AppError("viz_template_missing", "could not find viz/template.html next to the package", 3);
|
|
244
|
+
}
|
|
245
|
+
function renderVizHtml(template, workflows) {
|
|
246
|
+
const order = Object.keys(workflows).sort();
|
|
247
|
+
const data = JSON.stringify({ workflows, order }).replaceAll("<", "\\u003c");
|
|
248
|
+
return template.replace("__WORKFLOW_DATA__", data);
|
|
249
|
+
}
|
|
250
|
+
function startVizServer(registry, port) {
|
|
251
|
+
const template = readFileSync(findTemplate(), "utf8");
|
|
252
|
+
const cytoscapeJs = readFileSync(require2.resolve("cytoscape/dist/cytoscape.min.js"), "utf8");
|
|
253
|
+
const dagreJs = readFileSync(require2.resolve("cytoscape-dagre"), "utf8");
|
|
254
|
+
const workflows = {};
|
|
255
|
+
for (const workflow of registry.values())
|
|
256
|
+
workflows[workflow.id] = workflow;
|
|
257
|
+
const html = renderVizHtml(template, workflows);
|
|
258
|
+
const server = createServer((request, response) => {
|
|
259
|
+
const path = (request.url ?? "/").split("?")[0];
|
|
260
|
+
if (path === "/" || path === "/index.html") {
|
|
261
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
262
|
+
response.end(html);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (path === "/vendor/cytoscape.min.js") {
|
|
266
|
+
response.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8" });
|
|
267
|
+
response.end(cytoscapeJs);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (path === "/vendor/cytoscape-dagre.js") {
|
|
271
|
+
response.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8" });
|
|
272
|
+
response.end(dagreJs);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
276
|
+
response.end("not found");
|
|
277
|
+
});
|
|
278
|
+
server.on("error", (error) => {
|
|
279
|
+
console.error(`viz: ${error.message}`);
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
});
|
|
282
|
+
server.listen(port, "127.0.0.1");
|
|
283
|
+
return { url: `http://127.0.0.1:${port}`, workflows: Object.keys(workflows).sort() };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/commands/workflow.ts
|
|
287
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
|
|
288
|
+
import { join as join2 } from "node:path";
|
|
289
|
+
|
|
290
|
+
// src/workflow/validate.ts
|
|
291
|
+
import { z } from "zod";
|
|
292
|
+
var nodeSchema = z.object({
|
|
293
|
+
id: z.string().min(1),
|
|
294
|
+
type: z.enum(["node", "subgraph"]),
|
|
295
|
+
title: z.string().min(1),
|
|
296
|
+
description: z.string(),
|
|
297
|
+
workflow: z.string().min(1).optional()
|
|
298
|
+
}).strict();
|
|
299
|
+
var edgeSchema = z.object({
|
|
300
|
+
id: z.string().min(1),
|
|
301
|
+
from: z.string().min(1),
|
|
302
|
+
to: z.string().min(1),
|
|
303
|
+
description: z.string(),
|
|
304
|
+
condition: z.string().min(1).optional()
|
|
305
|
+
}).strict();
|
|
306
|
+
var workflowSchema = z.object({
|
|
307
|
+
id: z.string().min(1),
|
|
308
|
+
title: z.string().min(1),
|
|
309
|
+
start: z.string().min(1),
|
|
310
|
+
nodes: z.array(nodeSchema).min(1),
|
|
311
|
+
edges: z.array(edgeSchema)
|
|
312
|
+
}).strict();
|
|
313
|
+
function validateWorkflow(raw, source) {
|
|
314
|
+
const errors = [];
|
|
315
|
+
const warnings = [];
|
|
316
|
+
const report = (message) => {
|
|
317
|
+
errors.push(`${source}: ${message}`);
|
|
318
|
+
};
|
|
319
|
+
const parsed = workflowSchema.safeParse(raw);
|
|
320
|
+
if (!parsed.success) {
|
|
321
|
+
for (const issue of parsed.error.issues) {
|
|
322
|
+
const path = issue.path.length > 0 ? `.${issue.path.join(".")}` : "";
|
|
323
|
+
report(`${path} ${issue.message}`.trim());
|
|
324
|
+
}
|
|
325
|
+
return { errors, warnings };
|
|
326
|
+
}
|
|
327
|
+
const wf = parsed.data;
|
|
328
|
+
const nodes = new Map;
|
|
329
|
+
for (const node of wf.nodes) {
|
|
330
|
+
if (node.id === END) {
|
|
331
|
+
report(`node id "${END}" is reserved; point an edge at END instead`);
|
|
332
|
+
}
|
|
333
|
+
if (nodes.has(node.id)) {
|
|
334
|
+
report(`duplicate node id "${node.id}"`);
|
|
335
|
+
}
|
|
336
|
+
nodes.set(node.id, node);
|
|
337
|
+
if (node.type === "subgraph") {
|
|
338
|
+
if (!node.workflow) {
|
|
339
|
+
report(`subgraph node "${node.id}" needs a "workflow" field`);
|
|
340
|
+
} else if (node.workflow === wf.id) {
|
|
341
|
+
report(`subgraph node "${node.id}" references its own workflow "${wf.id}"`);
|
|
342
|
+
}
|
|
343
|
+
} else if (node.workflow) {
|
|
344
|
+
report(`node "${node.id}" is not a subgraph but has a "workflow" field`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (wf.start === END) {
|
|
348
|
+
report(`start must be a node, not END`);
|
|
349
|
+
} else if (!nodes.has(wf.start)) {
|
|
350
|
+
report(`start "${wf.start}" is not a node`);
|
|
351
|
+
}
|
|
352
|
+
const edgeIds = new Set;
|
|
353
|
+
const pairs = new Set;
|
|
354
|
+
for (const edge of wf.edges) {
|
|
355
|
+
if (edgeIds.has(edge.id)) {
|
|
356
|
+
report(`duplicate edge id "${edge.id}"`);
|
|
357
|
+
}
|
|
358
|
+
edgeIds.add(edge.id);
|
|
359
|
+
const pair = `${edge.from} -> ${edge.to}`;
|
|
360
|
+
if (pairs.has(pair)) {
|
|
361
|
+
report(`duplicate edge ${pair}; transitions must be unambiguous`);
|
|
362
|
+
}
|
|
363
|
+
pairs.add(pair);
|
|
364
|
+
if (!nodes.has(edge.from)) {
|
|
365
|
+
report(`edge "${edge.id}" starts at unknown node "${edge.from}"`);
|
|
366
|
+
}
|
|
367
|
+
if (edge.to !== END && !nodes.has(edge.to)) {
|
|
368
|
+
report(`edge "${edge.id}" targets unknown node "${edge.to}" (use END to finish)`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const node of wf.nodes) {
|
|
372
|
+
if (!wf.edges.some((edge) => edge.from === node.id)) {
|
|
373
|
+
report(`node "${node.id}" has no outgoing edge (add one to END if it is the last step)`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!wf.edges.some((edge) => edge.to === END)) {
|
|
377
|
+
report(`workflow "${wf.id}" has no edge to END`);
|
|
378
|
+
}
|
|
379
|
+
if (errors.length === 0) {
|
|
380
|
+
const reachable = new Set([wf.start]);
|
|
381
|
+
const queue = [wf.start];
|
|
382
|
+
while (queue.length > 0) {
|
|
383
|
+
const id = queue.shift();
|
|
384
|
+
for (const edge of wf.edges) {
|
|
385
|
+
if (edge.from !== id)
|
|
386
|
+
continue;
|
|
387
|
+
if (edge.to === END || reachable.has(edge.to))
|
|
388
|
+
continue;
|
|
389
|
+
reachable.add(edge.to);
|
|
390
|
+
queue.push(edge.to);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const node of wf.nodes) {
|
|
394
|
+
if (!reachable.has(node.id)) {
|
|
395
|
+
warnings.push(`${source}: node "${node.id}" is unreachable from start "${wf.start}"`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const canFinish = new Set;
|
|
399
|
+
for (const edge of wf.edges) {
|
|
400
|
+
if (edge.to === END)
|
|
401
|
+
canFinish.add(edge.from);
|
|
402
|
+
}
|
|
403
|
+
let grew = true;
|
|
404
|
+
while (grew) {
|
|
405
|
+
grew = false;
|
|
406
|
+
for (const edge of wf.edges) {
|
|
407
|
+
if (edge.to === END)
|
|
408
|
+
continue;
|
|
409
|
+
if (!canFinish.has(edge.to) || canFinish.has(edge.from))
|
|
410
|
+
continue;
|
|
411
|
+
canFinish.add(edge.from);
|
|
412
|
+
grew = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const node of wf.nodes) {
|
|
416
|
+
if (!canFinish.has(node.id)) {
|
|
417
|
+
warnings.push(`${source}: node "${node.id}" cannot reach END`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { workflow: wf, errors, warnings };
|
|
422
|
+
}
|
|
423
|
+
function outgoingEdges(wf, nodeId) {
|
|
424
|
+
return wf.edges.filter((edge) => edge.from === nodeId);
|
|
425
|
+
}
|
|
426
|
+
function onlyEnd(wf, nodeId) {
|
|
427
|
+
const out = outgoingEdges(wf, nodeId);
|
|
428
|
+
return out.length === 1 && out[0].to === END;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/workflow/load.ts
|
|
432
|
+
function requireWorkflow(workflows, id) {
|
|
433
|
+
const workflow = workflows.get(id);
|
|
434
|
+
if (!workflow) {
|
|
435
|
+
throw new AppError("workflow_not_found", `workflow "${id}" not found in ./workflows`, 3, {
|
|
436
|
+
available: [...workflows.keys()].sort()
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return workflow;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/commands/workflow.ts
|
|
443
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
444
|
+
function skeleton(id) {
|
|
445
|
+
return {
|
|
446
|
+
id,
|
|
447
|
+
title: id,
|
|
448
|
+
start: "start",
|
|
449
|
+
nodes: [
|
|
450
|
+
{
|
|
451
|
+
id: "start",
|
|
452
|
+
type: "node",
|
|
453
|
+
title: "Start",
|
|
454
|
+
description: "Describe the first step."
|
|
455
|
+
}
|
|
456
|
+
],
|
|
457
|
+
edges: [
|
|
458
|
+
{
|
|
459
|
+
id: "e1",
|
|
460
|
+
from: "start",
|
|
461
|
+
to: END,
|
|
462
|
+
description: "Done."
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function cmdWorkflowNew(workflowsDir, id, json) {
|
|
468
|
+
if (!id)
|
|
469
|
+
throw usageError("workflow id is required: next-steps workflow new <id>");
|
|
470
|
+
if (!ID_PATTERN.test(id)) {
|
|
471
|
+
throw usageError(`invalid workflow id "${id}"; use A-Z a-z 0-9 . _ - (max 64)`);
|
|
472
|
+
}
|
|
473
|
+
const file = join2(workflowsDir, `${id}.json`);
|
|
474
|
+
if (existsSync2(file)) {
|
|
475
|
+
throw usageError(`workflow file already exists: ${file}`);
|
|
476
|
+
}
|
|
477
|
+
mkdirSync(workflowsDir, { recursive: true });
|
|
478
|
+
writeFileSync(file, `${JSON.stringify(skeleton(id), null, 2)}
|
|
479
|
+
`, "utf8");
|
|
480
|
+
if (json)
|
|
481
|
+
return toJson({ created: file, workflowId: id });
|
|
482
|
+
return `created: ${file}`;
|
|
483
|
+
}
|
|
484
|
+
function cmdWorkflowList(load, json) {
|
|
485
|
+
if (json) {
|
|
486
|
+
return toJson({
|
|
487
|
+
dir: load.dir,
|
|
488
|
+
workflows: load.reports.map((report) => ({
|
|
489
|
+
file: report.file,
|
|
490
|
+
id: report.id,
|
|
491
|
+
title: report.title,
|
|
492
|
+
nodes: report.nodes,
|
|
493
|
+
edges: report.edges,
|
|
494
|
+
valid: report.valid,
|
|
495
|
+
errors: report.errors
|
|
496
|
+
})),
|
|
497
|
+
errors: load.errors,
|
|
498
|
+
warnings: load.warnings
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (load.reports.length === 0) {
|
|
502
|
+
return `no workflows in ${load.dir} (run "next-steps workflow new <id>")`;
|
|
503
|
+
}
|
|
504
|
+
const rows = load.reports.map((report) => [
|
|
505
|
+
report.id,
|
|
506
|
+
report.title,
|
|
507
|
+
String(report.nodes),
|
|
508
|
+
String(report.edges),
|
|
509
|
+
report.valid ? "ok" : "invalid"
|
|
510
|
+
]);
|
|
511
|
+
const body = table(["ID", "TITLE", "NODES", "EDGES", "STATUS"], rows);
|
|
512
|
+
const failures = load.reports.filter((report) => !report.valid);
|
|
513
|
+
if (failures.length === 0)
|
|
514
|
+
return body;
|
|
515
|
+
const details = failures.flatMap((report) => report.errors.map((error) => ` ${error}`));
|
|
516
|
+
return `${body}
|
|
517
|
+
|
|
518
|
+
invalid:
|
|
519
|
+
${details.join(`
|
|
520
|
+
`)}`;
|
|
521
|
+
}
|
|
522
|
+
function findReport(load, id) {
|
|
523
|
+
if (!id)
|
|
524
|
+
throw usageError("workflow id is required");
|
|
525
|
+
const report = load.reports.find((candidate) => candidate.id === id);
|
|
526
|
+
if (!report) {
|
|
527
|
+
throw new AppError("workflow_not_found", `workflow "${id}" not found in ${load.dir}`, 3, {
|
|
528
|
+
available: load.reports.map((candidate) => candidate.id).sort()
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
return report;
|
|
532
|
+
}
|
|
533
|
+
function cmdWorkflowShow(load, id, json) {
|
|
534
|
+
const report = findReport(load, id);
|
|
535
|
+
if (!report.valid) {
|
|
536
|
+
throw new AppError("invalid_workflow", `workflow "${report.id}" is invalid`, 5, {
|
|
537
|
+
errors: report.errors
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const wf = requireWorkflow(load.registry, report.id);
|
|
541
|
+
if (json)
|
|
542
|
+
return toJson({ workflow: wf, warnings: report.warnings });
|
|
543
|
+
const nodeRows = wf.nodes.map((node) => [
|
|
544
|
+
node.id,
|
|
545
|
+
node.type,
|
|
546
|
+
node.type === "subgraph" ? `${node.title} -> ${node.workflow ?? "?"}` : node.title
|
|
547
|
+
]);
|
|
548
|
+
const edgeRows = wf.edges.map((edge) => [
|
|
549
|
+
edge.id,
|
|
550
|
+
`${edge.from} -> ${edge.to}`,
|
|
551
|
+
edge.condition ? `${edge.description} [when: ${edge.condition}]` : edge.description
|
|
552
|
+
]);
|
|
553
|
+
const indent = (block) => block.split(`
|
|
554
|
+
`).map((line) => ` ${line}`).join(`
|
|
555
|
+
`);
|
|
556
|
+
return [
|
|
557
|
+
`${wf.id} ${wf.title}`,
|
|
558
|
+
`start: ${wf.start}`,
|
|
559
|
+
"",
|
|
560
|
+
"nodes:",
|
|
561
|
+
indent(table(["ID", "TYPE", "TITLE"], nodeRows)),
|
|
562
|
+
"",
|
|
563
|
+
"edges:",
|
|
564
|
+
indent(table(["ID", "TRANSITION", "DESCRIPTION"], edgeRows))
|
|
565
|
+
].join(`
|
|
566
|
+
`);
|
|
567
|
+
}
|
|
568
|
+
function cmdWorkflowCheck(load, id, json) {
|
|
569
|
+
const reports = id ? [findReport(load, id)] : load.reports;
|
|
570
|
+
const errors = reports.flatMap((report) => report.errors);
|
|
571
|
+
const warnings = reports.flatMap((report) => report.warnings);
|
|
572
|
+
if (errors.length > 0) {
|
|
573
|
+
throw new AppError("invalid_workflow", "workflow validation failed", 5, { errors });
|
|
574
|
+
}
|
|
575
|
+
if (json) {
|
|
576
|
+
return toJson({
|
|
577
|
+
ok: true,
|
|
578
|
+
workflows: reports.map((report) => ({
|
|
579
|
+
id: report.id,
|
|
580
|
+
title: report.title,
|
|
581
|
+
nodes: report.nodes,
|
|
582
|
+
edges: report.edges
|
|
583
|
+
})),
|
|
584
|
+
warnings
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (reports.length === 0) {
|
|
588
|
+
return `no workflows in ${load.dir}`;
|
|
589
|
+
}
|
|
590
|
+
const lines = reports.map((report) => `ok: ${report.id} (${report.nodes} nodes, ${report.edges} edges)`);
|
|
591
|
+
for (const warning of warnings)
|
|
592
|
+
lines.push(`warning: ${warning}`);
|
|
593
|
+
return lines.join(`
|
|
594
|
+
`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/workflow/graph.ts
|
|
598
|
+
function getNode(wf, id) {
|
|
599
|
+
return wf.nodes.find((node) => node.id === id);
|
|
600
|
+
}
|
|
601
|
+
function effectiveStack(lookup, stack) {
|
|
602
|
+
const frames = stack.map((frame) => ({ ...frame }));
|
|
603
|
+
while (frames.length > 1) {
|
|
604
|
+
const top = frames[frames.length - 1];
|
|
605
|
+
if (!top || top.nodeId === null)
|
|
606
|
+
break;
|
|
607
|
+
const wf = lookup.get(top.workflowId);
|
|
608
|
+
if (!wf || !onlyEnd(wf, top.nodeId))
|
|
609
|
+
break;
|
|
610
|
+
frames.pop();
|
|
611
|
+
}
|
|
612
|
+
return frames;
|
|
613
|
+
}
|
|
614
|
+
function computeNext(lookup, session) {
|
|
615
|
+
if (session.status === "done") {
|
|
616
|
+
return {
|
|
617
|
+
done: true,
|
|
618
|
+
current: session.current,
|
|
619
|
+
currentTitle: null,
|
|
620
|
+
scope: session.stack.map((frame) => frame.workflowId),
|
|
621
|
+
next: []
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const frames = effectiveStack(lookup, session.stack);
|
|
625
|
+
const scope = frames.map((frame) => frame.workflowId);
|
|
626
|
+
const top = frames[frames.length - 1];
|
|
627
|
+
if (!top) {
|
|
628
|
+
return { done: true, current: null, currentTitle: null, scope, next: [] };
|
|
629
|
+
}
|
|
630
|
+
const wf = lookup.get(top.workflowId);
|
|
631
|
+
if (!wf) {
|
|
632
|
+
return { done: true, current: null, currentTitle: null, scope, next: [] };
|
|
633
|
+
}
|
|
634
|
+
if (top.nodeId === null) {
|
|
635
|
+
const entry = getNode(wf, wf.start);
|
|
636
|
+
return {
|
|
637
|
+
done: false,
|
|
638
|
+
current: null,
|
|
639
|
+
currentTitle: null,
|
|
640
|
+
scope,
|
|
641
|
+
next: [
|
|
642
|
+
{
|
|
643
|
+
edge: null,
|
|
644
|
+
node: wf.start,
|
|
645
|
+
title: entry?.title ?? wf.start,
|
|
646
|
+
description: entry?.description ?? ""
|
|
647
|
+
}
|
|
648
|
+
]
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const node = getNode(wf, top.nodeId);
|
|
652
|
+
if (onlyEnd(wf, top.nodeId)) {
|
|
653
|
+
return {
|
|
654
|
+
done: true,
|
|
655
|
+
current: top.nodeId,
|
|
656
|
+
currentTitle: node?.title ?? null,
|
|
657
|
+
scope,
|
|
658
|
+
next: []
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const next = outgoingEdges(wf, top.nodeId).map((edge) => {
|
|
662
|
+
if (edge.to === END) {
|
|
663
|
+
return {
|
|
664
|
+
edge: edge.id,
|
|
665
|
+
node: END,
|
|
666
|
+
title: "finish",
|
|
667
|
+
description: edge.description,
|
|
668
|
+
...edge.condition ? { condition: edge.condition } : {}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const target = getNode(wf, edge.to);
|
|
672
|
+
return {
|
|
673
|
+
edge: edge.id,
|
|
674
|
+
node: edge.to,
|
|
675
|
+
title: target?.title ?? edge.to,
|
|
676
|
+
description: edge.description,
|
|
677
|
+
...edge.condition ? { condition: edge.condition } : {}
|
|
678
|
+
};
|
|
679
|
+
});
|
|
680
|
+
return {
|
|
681
|
+
done: false,
|
|
682
|
+
current: top.nodeId,
|
|
683
|
+
currentTitle: node?.title ?? null,
|
|
684
|
+
scope,
|
|
685
|
+
next
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/runs/service.ts
|
|
690
|
+
var KEY_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
|
|
691
|
+
var MAX_DEPTH = 16;
|
|
692
|
+
function legalOptions(wf, current) {
|
|
693
|
+
if (current === null) {
|
|
694
|
+
const entry = getNode(wf, wf.start);
|
|
695
|
+
return [{ edge: null, node: wf.start, description: entry?.description ?? "" }];
|
|
696
|
+
}
|
|
697
|
+
return outgoingEdges(wf, current).map((edge) => ({
|
|
698
|
+
edge: edge.id,
|
|
699
|
+
node: edge.to,
|
|
700
|
+
description: edge.description
|
|
701
|
+
}));
|
|
702
|
+
}
|
|
703
|
+
function invalidTransition(wf, current, node, scope) {
|
|
704
|
+
return new AppError("invalid_transition", `cannot move to "${node}" from "${current ?? "(start)"}" in workflow "${wf.id}"`, 4, { current, node, scope, legal: legalOptions(wf, current) });
|
|
705
|
+
}
|
|
706
|
+
function keyNotFound(key) {
|
|
707
|
+
return new AppError("key_not_found", `session "${key}" does not exist`, 3);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
class SessionService {
|
|
711
|
+
registry;
|
|
712
|
+
store;
|
|
713
|
+
constructor(registry, store) {
|
|
714
|
+
this.registry = registry;
|
|
715
|
+
this.store = store;
|
|
716
|
+
}
|
|
717
|
+
get statePath() {
|
|
718
|
+
return this.store.path;
|
|
719
|
+
}
|
|
720
|
+
create(input) {
|
|
721
|
+
const workflow = requireWorkflow(this.registry, input.workflowId);
|
|
722
|
+
const key = (input.key ?? "").trim() || crypto.randomUUID();
|
|
723
|
+
if (!KEY_PATTERN.test(key)) {
|
|
724
|
+
throw new AppError("key_invalid", `key "${key}" is invalid; use 1-128 characters from A-Z a-z 0-9 . _ -`, 2);
|
|
725
|
+
}
|
|
726
|
+
const now = new Date().toISOString();
|
|
727
|
+
const session = {
|
|
728
|
+
key,
|
|
729
|
+
workflowId: workflow.id,
|
|
730
|
+
status: "running",
|
|
731
|
+
current: null,
|
|
732
|
+
stack: [{ workflowId: workflow.id, nodeId: null }],
|
|
733
|
+
version: 0,
|
|
734
|
+
createdAt: now,
|
|
735
|
+
updatedAt: now
|
|
736
|
+
};
|
|
737
|
+
if (!this.store.createSession(session)) {
|
|
738
|
+
throw new AppError("key_exists", `session "${key}" already exists`, 2, { key });
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
session,
|
|
742
|
+
workflowTitle: workflow.title,
|
|
743
|
+
next: computeNext(this.registry, session)
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
log(key, node) {
|
|
747
|
+
return this.store.transaction(() => {
|
|
748
|
+
const session = this.requireSession(key);
|
|
749
|
+
if (session.status === "done") {
|
|
750
|
+
throw new AppError("invalid_transition", `session "${key}" is already done`, 4, {
|
|
751
|
+
current: END,
|
|
752
|
+
node,
|
|
753
|
+
legal: []
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const frames = effectiveStack(this.registry, session.stack);
|
|
757
|
+
const top = frames[frames.length - 1];
|
|
758
|
+
if (!top) {
|
|
759
|
+
throw new AppError("invalid_transition", `session "${key}" has no active scope`, 4);
|
|
760
|
+
}
|
|
761
|
+
const wf = requireWorkflow(this.registry, top.workflowId);
|
|
762
|
+
const scope = frames.map((frame) => frame.workflowId);
|
|
763
|
+
const now = new Date().toISOString();
|
|
764
|
+
const stack = frames.map((frame) => ({ ...frame }));
|
|
765
|
+
let stepInput;
|
|
766
|
+
let current;
|
|
767
|
+
let endsRun = false;
|
|
768
|
+
if (node === END) {
|
|
769
|
+
if (top.nodeId === null)
|
|
770
|
+
throw invalidTransition(wf, top.nodeId, node, scope);
|
|
771
|
+
const edge = outgoingEdges(wf, top.nodeId).find((candidate) => candidate.to === END);
|
|
772
|
+
if (!edge)
|
|
773
|
+
throw invalidTransition(wf, top.nodeId, node, scope);
|
|
774
|
+
stepInput = {
|
|
775
|
+
key,
|
|
776
|
+
node: END,
|
|
777
|
+
from: top.nodeId,
|
|
778
|
+
edge: edge.id,
|
|
779
|
+
workflowId: top.workflowId,
|
|
780
|
+
scope,
|
|
781
|
+
at: now
|
|
782
|
+
};
|
|
783
|
+
if (stack.length === 1) {
|
|
784
|
+
current = END;
|
|
785
|
+
endsRun = true;
|
|
786
|
+
} else {
|
|
787
|
+
stack.pop();
|
|
788
|
+
current = stack[stack.length - 1]?.nodeId ?? null;
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
const target = getNode(wf, node);
|
|
792
|
+
if (!target) {
|
|
793
|
+
throw new AppError("unknown_node", `node "${node}" does not exist in workflow "${wf.id}"`, 4, { legal: legalOptions(wf, top.nodeId) });
|
|
794
|
+
}
|
|
795
|
+
let edge = null;
|
|
796
|
+
if (top.nodeId === null) {
|
|
797
|
+
if (node !== wf.start)
|
|
798
|
+
throw invalidTransition(wf, top.nodeId, node, scope);
|
|
799
|
+
} else {
|
|
800
|
+
const found = outgoingEdges(wf, top.nodeId).find((candidate) => candidate.to === node);
|
|
801
|
+
if (!found)
|
|
802
|
+
throw invalidTransition(wf, top.nodeId, node, scope);
|
|
803
|
+
edge = found;
|
|
804
|
+
}
|
|
805
|
+
const last = stack[stack.length - 1];
|
|
806
|
+
if (last)
|
|
807
|
+
last.nodeId = node;
|
|
808
|
+
if (target.type === "subgraph") {
|
|
809
|
+
if (stack.length >= MAX_DEPTH) {
|
|
810
|
+
throw new AppError("max_depth_exceeded", `subgraph nesting exceeded ${MAX_DEPTH} levels`, 4, { scope });
|
|
811
|
+
}
|
|
812
|
+
const child = requireWorkflow(this.registry, target.workflow);
|
|
813
|
+
stack.push({ workflowId: child.id, nodeId: null });
|
|
814
|
+
}
|
|
815
|
+
current = node;
|
|
816
|
+
stepInput = {
|
|
817
|
+
key,
|
|
818
|
+
node,
|
|
819
|
+
from: top.nodeId,
|
|
820
|
+
edge: edge?.id ?? null,
|
|
821
|
+
workflowId: top.workflowId,
|
|
822
|
+
scope,
|
|
823
|
+
at: now
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
session.stack = stack;
|
|
827
|
+
session.current = current;
|
|
828
|
+
session.status = endsRun ? "done" : "running";
|
|
829
|
+
session.version += 1;
|
|
830
|
+
session.updatedAt = now;
|
|
831
|
+
const next = this.settle(session);
|
|
832
|
+
const step = this.store.appendStep(stepInput);
|
|
833
|
+
this.store.updateSession(session);
|
|
834
|
+
return { session, step, next };
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
next(key) {
|
|
838
|
+
const session = this.requireSession(key);
|
|
839
|
+
return { session, next: computeNext(this.registry, session) };
|
|
840
|
+
}
|
|
841
|
+
steps(key, query = {}) {
|
|
842
|
+
const session = this.requireSession(key);
|
|
843
|
+
return { session, steps: this.store.listSteps(key, query) };
|
|
844
|
+
}
|
|
845
|
+
keys(workflowId) {
|
|
846
|
+
if (workflowId)
|
|
847
|
+
requireWorkflow(this.registry, workflowId);
|
|
848
|
+
return this.store.listSessions(workflowId);
|
|
849
|
+
}
|
|
850
|
+
reset(key) {
|
|
851
|
+
this.requireSession(key);
|
|
852
|
+
return { key, deletedSteps: this.store.reset(key) };
|
|
853
|
+
}
|
|
854
|
+
settle(session) {
|
|
855
|
+
const next = computeNext(this.registry, session);
|
|
856
|
+
if (!next.done)
|
|
857
|
+
return next;
|
|
858
|
+
session.status = "done";
|
|
859
|
+
session.current = END;
|
|
860
|
+
return computeNext(this.registry, session);
|
|
861
|
+
}
|
|
862
|
+
requireSession(key) {
|
|
863
|
+
const session = this.store.getSession(key);
|
|
864
|
+
if (!session)
|
|
865
|
+
throw keyNotFound(key);
|
|
866
|
+
return session;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/runs/store.ts
|
|
871
|
+
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
872
|
+
import { dirname as dirname2 } from "node:path";
|
|
873
|
+
|
|
874
|
+
// src/runs/driver.ts
|
|
875
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
876
|
+
|
|
877
|
+
class BunDriver {
|
|
878
|
+
db;
|
|
879
|
+
constructor(db) {
|
|
880
|
+
this.db = db;
|
|
881
|
+
}
|
|
882
|
+
exec(sql) {
|
|
883
|
+
this.db.exec(sql);
|
|
884
|
+
}
|
|
885
|
+
run(sql, params) {
|
|
886
|
+
return Number(this.db.run(sql, params).changes);
|
|
887
|
+
}
|
|
888
|
+
get(sql, params) {
|
|
889
|
+
return this.db.query(sql).get(...params) ?? null;
|
|
890
|
+
}
|
|
891
|
+
all(sql, params) {
|
|
892
|
+
return this.db.query(sql).all(...params);
|
|
893
|
+
}
|
|
894
|
+
transaction(fn) {
|
|
895
|
+
return this.db.transaction(fn)();
|
|
896
|
+
}
|
|
897
|
+
close() {
|
|
898
|
+
this.db.close();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
class NodeDriver {
|
|
903
|
+
db;
|
|
904
|
+
constructor(db) {
|
|
905
|
+
this.db = db;
|
|
906
|
+
}
|
|
907
|
+
exec(sql) {
|
|
908
|
+
this.db.exec(sql);
|
|
909
|
+
}
|
|
910
|
+
run(sql, params) {
|
|
911
|
+
return Number(this.db.prepare(sql).run(...params).changes);
|
|
912
|
+
}
|
|
913
|
+
get(sql, params) {
|
|
914
|
+
return this.db.prepare(sql).get(...params) ?? null;
|
|
915
|
+
}
|
|
916
|
+
all(sql, params) {
|
|
917
|
+
return this.db.prepare(sql).all(...params);
|
|
918
|
+
}
|
|
919
|
+
transaction(fn) {
|
|
920
|
+
this.db.exec("BEGIN");
|
|
921
|
+
try {
|
|
922
|
+
const result = fn();
|
|
923
|
+
this.db.exec("COMMIT");
|
|
924
|
+
return result;
|
|
925
|
+
} catch (error) {
|
|
926
|
+
this.db.exec("ROLLBACK");
|
|
927
|
+
throw error;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
close() {
|
|
931
|
+
this.db.close();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function openDriver(path) {
|
|
935
|
+
const load = createRequire2(import.meta.url);
|
|
936
|
+
if ("bun" in process.versions) {
|
|
937
|
+
const { Database } = load("bun:sqlite");
|
|
938
|
+
return new BunDriver(new Database(path, { create: true }));
|
|
939
|
+
}
|
|
940
|
+
const { DatabaseSync } = load("node:sqlite");
|
|
941
|
+
return new NodeDriver(new DatabaseSync(path));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/runs/store.ts
|
|
945
|
+
var SCHEMA = `
|
|
946
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
947
|
+
key TEXT PRIMARY KEY,
|
|
948
|
+
workflow_id TEXT NOT NULL,
|
|
949
|
+
status TEXT NOT NULL,
|
|
950
|
+
current TEXT,
|
|
951
|
+
stack TEXT NOT NULL,
|
|
952
|
+
version INTEGER NOT NULL DEFAULT 0,
|
|
953
|
+
created_at TEXT NOT NULL,
|
|
954
|
+
updated_at TEXT NOT NULL
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
CREATE TABLE IF NOT EXISTS steps (
|
|
958
|
+
key TEXT NOT NULL REFERENCES runs(key) ON DELETE CASCADE,
|
|
959
|
+
seq INTEGER NOT NULL,
|
|
960
|
+
node TEXT NOT NULL,
|
|
961
|
+
from_node TEXT,
|
|
962
|
+
edge TEXT,
|
|
963
|
+
workflow_id TEXT NOT NULL,
|
|
964
|
+
scope TEXT NOT NULL,
|
|
965
|
+
at TEXT NOT NULL,
|
|
966
|
+
PRIMARY KEY (key, seq)
|
|
967
|
+
);
|
|
968
|
+
`;
|
|
969
|
+
function toSession(row) {
|
|
970
|
+
return {
|
|
971
|
+
key: row.key,
|
|
972
|
+
workflowId: row.workflow_id,
|
|
973
|
+
status: row.status,
|
|
974
|
+
current: row.current,
|
|
975
|
+
stack: JSON.parse(row.stack),
|
|
976
|
+
version: row.version,
|
|
977
|
+
createdAt: row.created_at,
|
|
978
|
+
updatedAt: row.updated_at
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function toStep(row) {
|
|
982
|
+
return {
|
|
983
|
+
key: row.key,
|
|
984
|
+
seq: row.seq,
|
|
985
|
+
node: row.node,
|
|
986
|
+
from: row.from_node,
|
|
987
|
+
edge: row.edge,
|
|
988
|
+
workflowId: row.workflow_id,
|
|
989
|
+
scope: JSON.parse(row.scope),
|
|
990
|
+
at: row.at
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
class SqliteRunStore {
|
|
995
|
+
path;
|
|
996
|
+
db;
|
|
997
|
+
constructor(path) {
|
|
998
|
+
this.path = path;
|
|
999
|
+
if (path !== ":memory:") {
|
|
1000
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1001
|
+
}
|
|
1002
|
+
this.db = openDriver(path);
|
|
1003
|
+
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
1004
|
+
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
1005
|
+
if (path !== ":memory:") {
|
|
1006
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
1007
|
+
}
|
|
1008
|
+
this.db.exec(SCHEMA);
|
|
1009
|
+
}
|
|
1010
|
+
createSession(session) {
|
|
1011
|
+
return this.db.run(`INSERT OR IGNORE INTO runs
|
|
1012
|
+
(key, workflow_id, status, current, stack, version, created_at, updated_at)
|
|
1013
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1014
|
+
session.key,
|
|
1015
|
+
session.workflowId,
|
|
1016
|
+
session.status,
|
|
1017
|
+
session.current,
|
|
1018
|
+
JSON.stringify(session.stack),
|
|
1019
|
+
session.version,
|
|
1020
|
+
session.createdAt,
|
|
1021
|
+
session.updatedAt
|
|
1022
|
+
]) > 0;
|
|
1023
|
+
}
|
|
1024
|
+
getSession(key) {
|
|
1025
|
+
const row = this.db.get("SELECT * FROM runs WHERE key = ?", [key]);
|
|
1026
|
+
return row ? toSession(row) : null;
|
|
1027
|
+
}
|
|
1028
|
+
updateSession(session) {
|
|
1029
|
+
this.db.run(`UPDATE runs
|
|
1030
|
+
SET status = ?, current = ?, stack = ?, version = ?, updated_at = ?
|
|
1031
|
+
WHERE key = ?`, [
|
|
1032
|
+
session.status,
|
|
1033
|
+
session.current,
|
|
1034
|
+
JSON.stringify(session.stack),
|
|
1035
|
+
session.version,
|
|
1036
|
+
session.updatedAt,
|
|
1037
|
+
session.key
|
|
1038
|
+
]);
|
|
1039
|
+
}
|
|
1040
|
+
appendStep(step) {
|
|
1041
|
+
const row = this.db.get("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM steps WHERE key = ?", [
|
|
1042
|
+
step.key
|
|
1043
|
+
]);
|
|
1044
|
+
const seq = row?.next ?? 1;
|
|
1045
|
+
this.db.run(`INSERT INTO steps (key, seq, node, from_node, edge, workflow_id, scope, at)
|
|
1046
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1047
|
+
step.key,
|
|
1048
|
+
seq,
|
|
1049
|
+
step.node,
|
|
1050
|
+
step.from,
|
|
1051
|
+
step.edge,
|
|
1052
|
+
step.workflowId,
|
|
1053
|
+
JSON.stringify(step.scope),
|
|
1054
|
+
step.at
|
|
1055
|
+
]);
|
|
1056
|
+
return { ...step, seq };
|
|
1057
|
+
}
|
|
1058
|
+
listSteps(key, query = {}) {
|
|
1059
|
+
const limit = Math.min(Math.max(query.limit ?? 50, 1), 500);
|
|
1060
|
+
const after = Math.max(query.after ?? 0, 0);
|
|
1061
|
+
const rows = this.db.all("SELECT * FROM steps WHERE key = ? AND seq > ? ORDER BY seq ASC LIMIT ?", [key, after, limit]);
|
|
1062
|
+
return rows.map(toStep);
|
|
1063
|
+
}
|
|
1064
|
+
listSessions(workflowId) {
|
|
1065
|
+
const rows = workflowId ? this.db.all("SELECT * FROM runs WHERE workflow_id = ? ORDER BY updated_at DESC", [
|
|
1066
|
+
workflowId
|
|
1067
|
+
]) : this.db.all("SELECT * FROM runs ORDER BY updated_at DESC", []);
|
|
1068
|
+
return rows.map(toSession);
|
|
1069
|
+
}
|
|
1070
|
+
reset(key) {
|
|
1071
|
+
const count = this.db.get("SELECT COUNT(*) AS n FROM steps WHERE key = ?", [key]);
|
|
1072
|
+
this.db.run("DELETE FROM runs WHERE key = ?", [key]);
|
|
1073
|
+
return count?.n ?? 0;
|
|
1074
|
+
}
|
|
1075
|
+
transaction(fn) {
|
|
1076
|
+
return this.db.transaction(fn);
|
|
1077
|
+
}
|
|
1078
|
+
close() {
|
|
1079
|
+
this.db.close();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/types.ts
|
|
1084
|
+
class AppError2 extends Error {
|
|
1085
|
+
code;
|
|
1086
|
+
exitCode;
|
|
1087
|
+
details;
|
|
1088
|
+
constructor(code, message, exitCode = 1, details) {
|
|
1089
|
+
super(message);
|
|
1090
|
+
this.code = code;
|
|
1091
|
+
this.exitCode = exitCode;
|
|
1092
|
+
this.details = details;
|
|
1093
|
+
this.name = "AppError";
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/workflow/load.ts
|
|
1098
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1099
|
+
import { join as join3 } from "node:path";
|
|
1100
|
+
function rawId(raw) {
|
|
1101
|
+
return typeof raw === "object" && raw !== null && typeof raw.id === "string" ? raw.id : "(invalid)";
|
|
1102
|
+
}
|
|
1103
|
+
function loadWorkflows(dir) {
|
|
1104
|
+
const registry = new Map;
|
|
1105
|
+
const reports = [];
|
|
1106
|
+
if (!existsSync3(dir)) {
|
|
1107
|
+
const message = `workflow directory not found: ${dir} (run "next-steps workflow new <id>")`;
|
|
1108
|
+
return { dir, registry, reports, errors: [message], warnings: [] };
|
|
1109
|
+
}
|
|
1110
|
+
const parsed = [];
|
|
1111
|
+
for (const file of readdirSync(dir).filter((name) => name.endsWith(".json")).sort()) {
|
|
1112
|
+
const path = join3(dir, file);
|
|
1113
|
+
let raw;
|
|
1114
|
+
try {
|
|
1115
|
+
raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
const message = `${file}: invalid JSON (${error instanceof Error ? error.message : String(error)})`;
|
|
1118
|
+
reports.push({
|
|
1119
|
+
file,
|
|
1120
|
+
id: "(unreadable)",
|
|
1121
|
+
title: "",
|
|
1122
|
+
nodes: 0,
|
|
1123
|
+
edges: 0,
|
|
1124
|
+
valid: false,
|
|
1125
|
+
errors: [message],
|
|
1126
|
+
warnings: []
|
|
1127
|
+
});
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const check = validateWorkflow(raw, file);
|
|
1131
|
+
if (check.workflow) {
|
|
1132
|
+
parsed.push({
|
|
1133
|
+
file,
|
|
1134
|
+
workflow: check.workflow,
|
|
1135
|
+
errors: check.errors,
|
|
1136
|
+
warnings: check.warnings
|
|
1137
|
+
});
|
|
1138
|
+
} else {
|
|
1139
|
+
reports.push({
|
|
1140
|
+
file,
|
|
1141
|
+
id: rawId(raw),
|
|
1142
|
+
title: "",
|
|
1143
|
+
nodes: 0,
|
|
1144
|
+
edges: 0,
|
|
1145
|
+
valid: false,
|
|
1146
|
+
errors: check.errors,
|
|
1147
|
+
warnings: check.warnings
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const byId = new Map;
|
|
1152
|
+
for (const entry of parsed) {
|
|
1153
|
+
const group = byId.get(entry.workflow.id) ?? [];
|
|
1154
|
+
group.push(entry);
|
|
1155
|
+
byId.set(entry.workflow.id, group);
|
|
1156
|
+
}
|
|
1157
|
+
for (const [id, group] of byId) {
|
|
1158
|
+
if (group.length < 2)
|
|
1159
|
+
continue;
|
|
1160
|
+
const message = `workflow id "${id}" is defined in more than one file: ${group.map((entry) => entry.file).join(", ")}`;
|
|
1161
|
+
for (const entry of group)
|
|
1162
|
+
entry.errors.push(message);
|
|
1163
|
+
}
|
|
1164
|
+
const knownIds = new Set(parsed.map((entry) => entry.workflow.id));
|
|
1165
|
+
for (const entry of parsed) {
|
|
1166
|
+
for (const node of entry.workflow.nodes) {
|
|
1167
|
+
if (node.type !== "subgraph" || !node.workflow)
|
|
1168
|
+
continue;
|
|
1169
|
+
if (!knownIds.has(node.workflow)) {
|
|
1170
|
+
entry.errors.push(`${entry.file}: subgraph node "${node.id}" references unknown workflow "${node.workflow}"`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
for (const entry of parsed) {
|
|
1175
|
+
const valid = entry.errors.length === 0;
|
|
1176
|
+
reports.push({
|
|
1177
|
+
file: entry.file,
|
|
1178
|
+
id: entry.workflow.id,
|
|
1179
|
+
title: entry.workflow.title,
|
|
1180
|
+
nodes: entry.workflow.nodes.length,
|
|
1181
|
+
edges: entry.workflow.edges.length,
|
|
1182
|
+
valid,
|
|
1183
|
+
errors: entry.errors,
|
|
1184
|
+
warnings: entry.warnings
|
|
1185
|
+
});
|
|
1186
|
+
if (valid)
|
|
1187
|
+
registry.set(entry.workflow.id, entry.workflow);
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
dir,
|
|
1191
|
+
registry,
|
|
1192
|
+
reports,
|
|
1193
|
+
errors: reports.flatMap((report) => report.errors),
|
|
1194
|
+
warnings: reports.flatMap((report) => report.warnings)
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/cli.ts
|
|
1199
|
+
var VERSION = "0.1.0";
|
|
1200
|
+
var HELP = `next-steps \u2014 record the current step, ask what comes next
|
|
1201
|
+
|
|
1202
|
+
usage:
|
|
1203
|
+
next-steps create --workflow <id> [--key <key>] [--json]
|
|
1204
|
+
next-steps log --key <key> --node <node> [--json]
|
|
1205
|
+
next-steps next --key <key> [--json]
|
|
1206
|
+
next-steps steps --key <key> [--limit <n>] [--after <seq>] [--json]
|
|
1207
|
+
next-steps keys [--workflow <id>] [--json]
|
|
1208
|
+
next-steps reset --key <key> [--json]
|
|
1209
|
+
|
|
1210
|
+
next-steps workflow new <id> [--json]
|
|
1211
|
+
next-steps workflow list [--json]
|
|
1212
|
+
next-steps workflow show <id> [--json]
|
|
1213
|
+
next-steps workflow check [id] [--json]
|
|
1214
|
+
|
|
1215
|
+
next-steps viz [--port <n>] [--json] (serve a graph viewer on a local port)
|
|
1216
|
+
|
|
1217
|
+
next-steps help
|
|
1218
|
+
next-steps version
|
|
1219
|
+
|
|
1220
|
+
state: ./.next-steps/state.db (created on first use, gitignored)
|
|
1221
|
+
graphs: ./workflows/*.json
|
|
1222
|
+
|
|
1223
|
+
exit codes:
|
|
1224
|
+
0 ok 2 usage 3 not found 4 invalid transition 5 invalid workflow`;
|
|
1225
|
+
function parseOptions(args, options, allowPositionals = false) {
|
|
1226
|
+
try {
|
|
1227
|
+
return parseArgs({ args, options, allowPositionals, strict: true });
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
throw usageError2(error instanceof Error ? error.message : String(error));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function asString(value) {
|
|
1233
|
+
return typeof value === "string" ? value : undefined;
|
|
1234
|
+
}
|
|
1235
|
+
function requireRegistry(workflowsDir) {
|
|
1236
|
+
const load = loadWorkflows(workflowsDir);
|
|
1237
|
+
if (load.errors.length > 0) {
|
|
1238
|
+
throw new AppError2("invalid_workflow", "workflow files are invalid", 5, {
|
|
1239
|
+
errors: load.errors
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
return load.registry;
|
|
1243
|
+
}
|
|
1244
|
+
function execute(argv, options) {
|
|
1245
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1246
|
+
const workflowsDir = options.workflowsDir ?? join4(cwd, "workflows");
|
|
1247
|
+
const dbPath = options.dbPath ?? join4(cwd, ".next-steps", "state.db");
|
|
1248
|
+
const [command, ...rest] = argv;
|
|
1249
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
1250
|
+
return HELP;
|
|
1251
|
+
}
|
|
1252
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
1253
|
+
return `next-steps ${VERSION}`;
|
|
1254
|
+
}
|
|
1255
|
+
if (command === "workflow") {
|
|
1256
|
+
const { positionals, values } = parseOptions(rest, { json: { type: "boolean" } }, true);
|
|
1257
|
+
const [subcommand, id] = positionals;
|
|
1258
|
+
const json = Boolean(values.json);
|
|
1259
|
+
const load = loadWorkflows(workflowsDir);
|
|
1260
|
+
switch (subcommand) {
|
|
1261
|
+
case "new":
|
|
1262
|
+
return cmdWorkflowNew(workflowsDir, id, json);
|
|
1263
|
+
case "list":
|
|
1264
|
+
return cmdWorkflowList(load, json);
|
|
1265
|
+
case "show":
|
|
1266
|
+
return cmdWorkflowShow(load, id, json);
|
|
1267
|
+
case "check":
|
|
1268
|
+
return cmdWorkflowCheck(load, id, json);
|
|
1269
|
+
default:
|
|
1270
|
+
throw usageError2("usage: next-steps workflow <new|list|show|check> ...");
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (command === "viz") {
|
|
1274
|
+
const { values } = parseOptions(rest, { port: { type: "string" }, json: { type: "boolean" } });
|
|
1275
|
+
const port = optionalInt(values.port, "port") ?? 3000;
|
|
1276
|
+
if (port < 1 || port > 65535) {
|
|
1277
|
+
throw usageError2("--port must be between 1 and 65535");
|
|
1278
|
+
}
|
|
1279
|
+
const registry2 = requireRegistry(workflowsDir);
|
|
1280
|
+
if (registry2.size === 0) {
|
|
1281
|
+
throw new AppError2("no_workflows", `no workflows in ${workflowsDir}`, 3);
|
|
1282
|
+
}
|
|
1283
|
+
const result = startVizServer(registry2, port);
|
|
1284
|
+
return values.json ? toJson2({ url: result.url, workflows: result.workflows }) : `viewer: ${result.url}
|
|
1285
|
+
press Ctrl+C to stop`;
|
|
1286
|
+
}
|
|
1287
|
+
const registry = requireRegistry(workflowsDir);
|
|
1288
|
+
const store = new SqliteRunStore(dbPath);
|
|
1289
|
+
try {
|
|
1290
|
+
const service = new SessionService(registry, store);
|
|
1291
|
+
switch (command) {
|
|
1292
|
+
case "create": {
|
|
1293
|
+
const { values } = parseOptions(rest, {
|
|
1294
|
+
workflow: { type: "string" },
|
|
1295
|
+
key: { type: "string" },
|
|
1296
|
+
json: { type: "boolean" }
|
|
1297
|
+
});
|
|
1298
|
+
return cmdCreate(service, {
|
|
1299
|
+
workflowId: asString(values.workflow),
|
|
1300
|
+
key: asString(values.key),
|
|
1301
|
+
json: Boolean(values.json)
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
case "log": {
|
|
1305
|
+
const { values } = parseOptions(rest, {
|
|
1306
|
+
key: { type: "string" },
|
|
1307
|
+
node: { type: "string" },
|
|
1308
|
+
json: { type: "boolean" }
|
|
1309
|
+
});
|
|
1310
|
+
return cmdLog(service, {
|
|
1311
|
+
key: asString(values.key),
|
|
1312
|
+
node: asString(values.node),
|
|
1313
|
+
json: Boolean(values.json)
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
case "next": {
|
|
1317
|
+
const { values } = parseOptions(rest, {
|
|
1318
|
+
key: { type: "string" },
|
|
1319
|
+
json: { type: "boolean" }
|
|
1320
|
+
});
|
|
1321
|
+
return cmdNext(service, {
|
|
1322
|
+
key: asString(values.key),
|
|
1323
|
+
json: Boolean(values.json)
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
case "steps": {
|
|
1327
|
+
const { values } = parseOptions(rest, {
|
|
1328
|
+
key: { type: "string" },
|
|
1329
|
+
limit: { type: "string" },
|
|
1330
|
+
after: { type: "string" },
|
|
1331
|
+
json: { type: "boolean" }
|
|
1332
|
+
});
|
|
1333
|
+
return cmdSteps(service, {
|
|
1334
|
+
key: asString(values.key),
|
|
1335
|
+
limit: optionalInt(values.limit, "limit"),
|
|
1336
|
+
after: optionalInt(values.after, "after"),
|
|
1337
|
+
json: Boolean(values.json)
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
case "keys": {
|
|
1341
|
+
const { values } = parseOptions(rest, {
|
|
1342
|
+
workflow: { type: "string" },
|
|
1343
|
+
json: { type: "boolean" }
|
|
1344
|
+
});
|
|
1345
|
+
return cmdKeys(service, {
|
|
1346
|
+
workflowId: asString(values.workflow),
|
|
1347
|
+
json: Boolean(values.json)
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
case "reset": {
|
|
1351
|
+
const { values } = parseOptions(rest, {
|
|
1352
|
+
key: { type: "string" },
|
|
1353
|
+
json: { type: "boolean" }
|
|
1354
|
+
});
|
|
1355
|
+
return cmdReset(service, {
|
|
1356
|
+
key: asString(values.key),
|
|
1357
|
+
json: Boolean(values.json)
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
default:
|
|
1361
|
+
throw usageError2(`unknown command "${command}" (try: next-steps help)`);
|
|
1362
|
+
}
|
|
1363
|
+
} finally {
|
|
1364
|
+
store.close();
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function prettyError(error) {
|
|
1368
|
+
const lines = [`error: ${error.message}`];
|
|
1369
|
+
const details = error.details;
|
|
1370
|
+
if (details) {
|
|
1371
|
+
if (Array.isArray(details.errors)) {
|
|
1372
|
+
for (const message of details.errors)
|
|
1373
|
+
lines.push(` - ${String(message)}`);
|
|
1374
|
+
}
|
|
1375
|
+
if (Array.isArray(details.legal)) {
|
|
1376
|
+
if (details.legal.length === 0) {
|
|
1377
|
+
lines.push(" no legal next steps");
|
|
1378
|
+
} else {
|
|
1379
|
+
lines.push(" legal next steps:");
|
|
1380
|
+
for (const option of details.legal) {
|
|
1381
|
+
const label = option.edge ? `${option.edge} -> ${option.node}` : option.node;
|
|
1382
|
+
lines.push(` - ${label} ${option.description}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (Array.isArray(details.available)) {
|
|
1387
|
+
lines.push(` available: ${details.available.join(", ")}`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return lines.join(`
|
|
1391
|
+
`);
|
|
1392
|
+
}
|
|
1393
|
+
function formatError(error, json) {
|
|
1394
|
+
if (error instanceof AppError2) {
|
|
1395
|
+
if (json) {
|
|
1396
|
+
return JSON.stringify({
|
|
1397
|
+
error: {
|
|
1398
|
+
code: error.code,
|
|
1399
|
+
message: error.message,
|
|
1400
|
+
...error.details === undefined ? {} : { details: error.details }
|
|
1401
|
+
}
|
|
1402
|
+
}, null, 2);
|
|
1403
|
+
}
|
|
1404
|
+
return prettyError(error);
|
|
1405
|
+
}
|
|
1406
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1407
|
+
if (json) {
|
|
1408
|
+
return JSON.stringify({ error: { code: "internal", message } }, null, 2);
|
|
1409
|
+
}
|
|
1410
|
+
return `error: ${message}`;
|
|
1411
|
+
}
|
|
1412
|
+
async function run(argv, options = {}) {
|
|
1413
|
+
const wantsJson = argv.includes("--json");
|
|
1414
|
+
try {
|
|
1415
|
+
return { exitCode: 0, stdout: execute(argv, options), stderr: "" };
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
const exitCode = error instanceof AppError2 ? error.exitCode : 1;
|
|
1418
|
+
return { exitCode, stdout: "", stderr: formatError(error, wantsJson) };
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function isEntrypoint() {
|
|
1422
|
+
const meta = import.meta;
|
|
1423
|
+
if (typeof meta.main === "boolean")
|
|
1424
|
+
return meta.main;
|
|
1425
|
+
const entry = process.argv[1];
|
|
1426
|
+
return entry !== undefined && import.meta.url === pathToFileURL(entry).href;
|
|
1427
|
+
}
|
|
1428
|
+
if (isEntrypoint()) {
|
|
1429
|
+
const result = await run(process.argv.slice(2));
|
|
1430
|
+
if (result.stdout)
|
|
1431
|
+
process.stdout.write(`${result.stdout}
|
|
1432
|
+
`);
|
|
1433
|
+
if (result.stderr)
|
|
1434
|
+
process.stderr.write(`${result.stderr}
|
|
1435
|
+
`);
|
|
1436
|
+
process.exitCode = result.exitCode;
|
|
1437
|
+
}
|
|
1438
|
+
export {
|
|
1439
|
+
run
|
|
1440
|
+
};
|