@intentius/chant 0.1.6 → 0.1.8
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 +1 -1
- package/src/cli/commands/build.test.ts +58 -5
- package/src/cli/commands/build.ts +24 -3
- package/src/cli/handlers/graph.test.ts +91 -0
- package/src/cli/handlers/graph.ts +23 -0
- package/src/cli/handlers/run-client.ts +134 -0
- package/src/cli/handlers/run-report.ts +160 -0
- package/src/cli/handlers/run.test.ts +448 -0
- package/src/cli/handlers/run.ts +453 -0
- package/src/cli/handlers/state.test.ts +409 -0
- package/src/cli/handlers/state.ts +232 -10
- package/src/cli/main.test.ts +65 -0
- package/src/cli/main.ts +32 -18
- package/src/cli/mcp/op-tools.ts +204 -0
- package/src/cli/mcp/resource-handlers.ts +69 -50
- package/src/cli/mcp/resources/context.ts +27 -0
- package/src/cli/mcp/server.test.ts +176 -3
- package/src/cli/mcp/server.ts +7 -3
- package/src/cli/mcp/state-tools.ts +0 -51
- package/src/cli/mcp/tools/search.ts +6 -1
- package/src/cli/registry.ts +3 -0
- package/src/composite.ts +10 -5
- package/src/index.ts +1 -2
- package/src/lexicon-plugin-helpers.ts +13 -5
- package/src/lexicon.ts +57 -1
- package/src/lint/config.test.ts +21 -0
- package/src/lint/config.ts +19 -3
- package/src/op/discover.test.ts +43 -0
- package/src/op/discover.ts +89 -0
- package/src/op/index.ts +3 -1
- package/src/op/types.ts +13 -6
- package/src/state/digest.test.ts +117 -0
- package/src/state/git.test.ts +191 -0
- package/src/state/git.ts +63 -11
- package/src/state/live-diff.test.ts +184 -0
- package/src/state/live-diff.ts +215 -0
- package/src/state/snapshot.test.ts +171 -0
- package/src/state/snapshot.ts +39 -19
- package/src/state/types.ts +4 -2
- package/src/cli/handlers/spell.ts +0 -396
- package/src/spell/discovery.ts +0 -183
- package/src/spell/index.ts +0 -3
- package/src/spell/prompt.ts +0 -133
- package/src/spell/types.ts +0 -89
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import { spawn as spawnChild, type ChildProcess } from "node:child_process";
|
|
5
|
+
import { loadChantConfig } from "../../config";
|
|
6
|
+
import { discoverOps } from "../../op/discover";
|
|
7
|
+
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
8
|
+
import type { CommandContext } from "../registry";
|
|
9
|
+
import {
|
|
10
|
+
loadTemporalClient,
|
|
11
|
+
connectionOptions,
|
|
12
|
+
resolveWorkflowId,
|
|
13
|
+
resolveProfile,
|
|
14
|
+
type WorkflowHandleRaw,
|
|
15
|
+
type WorkflowExecutionDescription,
|
|
16
|
+
type WorkflowHistoryRaw,
|
|
17
|
+
} from "./run-client";
|
|
18
|
+
import { generateReport, writeReport } from "./run-report";
|
|
19
|
+
|
|
20
|
+
function kebabToCamel(s: string): string {
|
|
21
|
+
return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function workflowFnName(opName: string): string {
|
|
25
|
+
return kebabToCamel(opName) + "Workflow";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function makeTemporalClient(profileName: string | undefined, projectPath: string) {
|
|
29
|
+
const { config } = await loadChantConfig(projectPath);
|
|
30
|
+
const profile = resolveProfile(config as Record<string, unknown>, profileName);
|
|
31
|
+
const { Connection, Client } = await loadTemporalClient();
|
|
32
|
+
const connection = await Connection.connect(connectionOptions(profile));
|
|
33
|
+
const client = new Client({ connection, namespace: profile.namespace });
|
|
34
|
+
return { client, profile, config };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── chant run list ──────────────────────────────────���─────────────────────────
|
|
38
|
+
|
|
39
|
+
export async function runOpList(ctx: CommandContext): Promise<number> {
|
|
40
|
+
const { ops, errors } = await discoverOps();
|
|
41
|
+
|
|
42
|
+
for (const err of errors) {
|
|
43
|
+
console.error(formatError({ message: err }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ops.size === 0) {
|
|
47
|
+
console.error(formatWarning({ message: "No Op definitions found (*.op.ts)" }));
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(
|
|
52
|
+
"NAME".padEnd(22) +
|
|
53
|
+
"PHASES".padEnd(8) +
|
|
54
|
+
"TASK-QUEUE".padEnd(20) +
|
|
55
|
+
"DEPENDS".padEnd(20) +
|
|
56
|
+
"OVERVIEW",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
let runStatus: Map<string, string> | undefined;
|
|
60
|
+
try {
|
|
61
|
+
const projectPath = resolve(".");
|
|
62
|
+
const { config } = await loadChantConfig(projectPath);
|
|
63
|
+
const profile = resolveProfile(config as Record<string, unknown>, ctx.args.profile);
|
|
64
|
+
const { Connection, Client } = await loadTemporalClient();
|
|
65
|
+
const connection = await Connection.connect(connectionOptions(profile));
|
|
66
|
+
const client = new Client({ connection, namespace: profile.namespace });
|
|
67
|
+
runStatus = new Map();
|
|
68
|
+
for (const [name] of ops) {
|
|
69
|
+
try {
|
|
70
|
+
const desc = await client.workflow.getHandle(resolveWorkflowId(name)).describe();
|
|
71
|
+
runStatus.set(name, desc.status.name);
|
|
72
|
+
} catch {
|
|
73
|
+
runStatus.set(name, "—");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Temporal not available — degrade gracefully
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const [name, { config }] of ops) {
|
|
81
|
+
const phases = String(config.phases.length);
|
|
82
|
+
const tq = config.taskQueue ?? config.name;
|
|
83
|
+
const deps = config.depends?.join(",") ?? "—";
|
|
84
|
+
const overview = config.overview.length > 36
|
|
85
|
+
? config.overview.slice(0, 33) + "..."
|
|
86
|
+
: config.overview;
|
|
87
|
+
const status = runStatus?.get(name);
|
|
88
|
+
const statusStr = status ? ` [${status}]` : "";
|
|
89
|
+
|
|
90
|
+
console.log(
|
|
91
|
+
(name + statusStr).padEnd(22) +
|
|
92
|
+
phases.padEnd(8) +
|
|
93
|
+
tq.padEnd(20) +
|
|
94
|
+
deps.padEnd(20) +
|
|
95
|
+
overview,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── chant run status <name> ───────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export async function runOpStatus(ctx: CommandContext): Promise<number> {
|
|
105
|
+
const name = ctx.args.extraPositional;
|
|
106
|
+
if (!name) {
|
|
107
|
+
console.error(formatError({ message: "Op name is required: chant run status <name>" }));
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const projectPath = resolve(".");
|
|
112
|
+
let client, desc: WorkflowExecutionDescription, history: WorkflowHistoryRaw;
|
|
113
|
+
try {
|
|
114
|
+
({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
|
|
115
|
+
const handle = client.workflow.getHandle(resolveWorkflowId(name));
|
|
116
|
+
desc = await handle.describe();
|
|
117
|
+
history = await handle.fetchHistory();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(formatBold(`Op: ${name}`));
|
|
124
|
+
console.log(` Workflow ID : ${desc.workflowId}`);
|
|
125
|
+
console.log(` Run ID : ${desc.runId}`);
|
|
126
|
+
console.log(` Status : ${desc.status.name}`);
|
|
127
|
+
console.log(` Task Queue : ${desc.taskQueue}`);
|
|
128
|
+
console.log(` Started : ${desc.startTime.toISOString()}`);
|
|
129
|
+
if (desc.closeTime) console.log(` Closed : ${desc.closeTime.toISOString()}`);
|
|
130
|
+
|
|
131
|
+
const events = history.events ?? [];
|
|
132
|
+
const completed = events.filter((e) => e.eventType === "ActivityTaskCompleted").length;
|
|
133
|
+
const scheduled = events.filter((e) => e.eventType === "ActivityTaskScheduled").length;
|
|
134
|
+
if (scheduled > 0) {
|
|
135
|
+
console.log(` Activities : ${completed}/${scheduled} completed`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── chant run log <name> ──────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export async function runOpLog(ctx: CommandContext): Promise<number> {
|
|
144
|
+
const name = ctx.args.extraPositional;
|
|
145
|
+
if (!name) {
|
|
146
|
+
console.error(formatError({ message: "Op name is required: chant run log <name>" }));
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const projectPath = resolve(".");
|
|
151
|
+
let client;
|
|
152
|
+
try {
|
|
153
|
+
({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(
|
|
160
|
+
"RUN-ID".padEnd(36) +
|
|
161
|
+
"STATUS".padEnd(16) +
|
|
162
|
+
"STARTED".padEnd(26) +
|
|
163
|
+
"CLOSED",
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const fnName = workflowFnName(name);
|
|
168
|
+
for await (const run of client.workflow.list({ query: `WorkflowType = "${fnName}"` })) {
|
|
169
|
+
const start = run.startTime.toISOString().slice(0, 19).replace("T", " ");
|
|
170
|
+
const close = run.closeTime ? run.closeTime.toISOString().slice(0, 19).replace("T", " ") : "—";
|
|
171
|
+
console.log(
|
|
172
|
+
run.runId.padEnd(36) +
|
|
173
|
+
run.status.name.padEnd(16) +
|
|
174
|
+
start.padEnd(26) +
|
|
175
|
+
close,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── chant run signal <name> <signal> ─────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export async function runOpSignal(ctx: CommandContext): Promise<number> {
|
|
189
|
+
const name = ctx.args.extraPositional;
|
|
190
|
+
const signalName = ctx.args.extraPositional2;
|
|
191
|
+
|
|
192
|
+
if (!name || !signalName) {
|
|
193
|
+
console.error(formatError({ message: "Usage: chant run signal <op-name> <signal-name>" }));
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const projectPath = resolve(".");
|
|
198
|
+
let handle: WorkflowHandleRaw;
|
|
199
|
+
try {
|
|
200
|
+
const { client } = await makeTemporalClient(ctx.args.profile, projectPath);
|
|
201
|
+
handle = client.workflow.getHandle(resolveWorkflowId(name));
|
|
202
|
+
await handle.signal(signalName);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.error(formatSuccess(`Signal "${signalName}" sent to Op "${name}"`));
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── chant run cancel <name> ───────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
export async function runOpCancel(ctx: CommandContext): Promise<number> {
|
|
215
|
+
const name = ctx.args.extraPositional;
|
|
216
|
+
if (!name) {
|
|
217
|
+
console.error(formatError({ message: "Op name is required: chant run cancel <name>" }));
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!ctx.args.force) {
|
|
222
|
+
console.error(formatWarning({
|
|
223
|
+
message: `Cancelling "${name}" will stop the active workflow run`,
|
|
224
|
+
hint: "Use --force to confirm cancellation",
|
|
225
|
+
}));
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const projectPath = resolve(".");
|
|
230
|
+
let handle: WorkflowHandleRaw;
|
|
231
|
+
try {
|
|
232
|
+
const { client } = await makeTemporalClient(ctx.args.profile, projectPath);
|
|
233
|
+
handle = client.workflow.getHandle(resolveWorkflowId(name));
|
|
234
|
+
await handle.cancel();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.error(formatSuccess(`Cancellation requested for Op "${name}"`));
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── chant run <name> — main command ───────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
const TERMINAL_STATUSES = new Set(["COMPLETED", "FAILED", "CANCELLED", "TERMINATED", "TIMED_OUT"]);
|
|
247
|
+
const POLL_INTERVAL_MS = 3000;
|
|
248
|
+
|
|
249
|
+
async function waitForTemporalServer(address: string, maxWaitMs = 30_000): Promise<void> {
|
|
250
|
+
const [host, portStr] = address.split(":");
|
|
251
|
+
const port = parseInt(portStr ?? "7233", 10);
|
|
252
|
+
const deadline = Date.now() + maxWaitMs;
|
|
253
|
+
while (Date.now() < deadline) {
|
|
254
|
+
try {
|
|
255
|
+
await new Promise<void>((res, rej) => {
|
|
256
|
+
const socket = createConnection({ host, port }, () => { socket.destroy(); res(); });
|
|
257
|
+
socket.on("error", rej);
|
|
258
|
+
socket.setTimeout(1000, () => { socket.destroy(); rej(new Error("timeout")); });
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
} catch {
|
|
262
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`Temporal server at ${address} did not become ready within ${maxWaitMs / 1000}s`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
|
|
269
|
+
const events = history.events ?? [];
|
|
270
|
+
const completed = events.filter((e) => e.eventType === "ActivityTaskCompleted").length;
|
|
271
|
+
const scheduled = events.filter((e) => e.eventType === "ActivityTaskScheduled").length;
|
|
272
|
+
process.stderr.write(
|
|
273
|
+
`\r${formatInfo(`[${opName}]`)} ${completed}/${scheduled} activities completed`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function runOp(ctx: CommandContext): Promise<number> {
|
|
278
|
+
const opName = ctx.args.path;
|
|
279
|
+
|
|
280
|
+
if (!opName || opName === ".") {
|
|
281
|
+
console.error(formatError({
|
|
282
|
+
message: "Op name is required: chant run <name>",
|
|
283
|
+
hint: "Run `chant run list` to see available Ops",
|
|
284
|
+
}));
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Discover Ops
|
|
289
|
+
const { ops, errors } = await discoverOps();
|
|
290
|
+
for (const err of errors) console.error(formatWarning({ message: err }));
|
|
291
|
+
|
|
292
|
+
const discovered = ops.get(opName);
|
|
293
|
+
if (!discovered) {
|
|
294
|
+
const names = [...ops.keys()];
|
|
295
|
+
console.error(formatError({
|
|
296
|
+
message: `Op "${opName}" not found`,
|
|
297
|
+
hint: names.length > 0
|
|
298
|
+
? `Available: ${names.join(", ")}`
|
|
299
|
+
: "No *.op.ts files found — create one or run `chant run list`",
|
|
300
|
+
}));
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { config } = discovered;
|
|
305
|
+
const projectPath = resolve(".");
|
|
306
|
+
|
|
307
|
+
// Load config + profile
|
|
308
|
+
const { config: chantConfig } = await loadChantConfig(projectPath);
|
|
309
|
+
let profile;
|
|
310
|
+
try {
|
|
311
|
+
profile = resolveProfile(chantConfig as Record<string, unknown>, ctx.args.profile);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle --report flag: just print the last run report
|
|
318
|
+
if (ctx.args.report) {
|
|
319
|
+
let client, desc: WorkflowExecutionDescription, history: WorkflowHistoryRaw;
|
|
320
|
+
try {
|
|
321
|
+
({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
|
|
322
|
+
const handle = client.workflow.getHandle(resolveWorkflowId(opName));
|
|
323
|
+
desc = await handle.describe();
|
|
324
|
+
history = await handle.fetchHistory();
|
|
325
|
+
} catch (err) {
|
|
326
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
const md = generateReport(opName, config, desc, history);
|
|
330
|
+
process.stdout.write(md);
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check built worker exists
|
|
335
|
+
const workerPath = join(projectPath, "dist", "ops", opName, "worker.ts");
|
|
336
|
+
if (!existsSync(workerPath)) {
|
|
337
|
+
console.error(formatError({
|
|
338
|
+
message: `dist/ops/${opName}/worker.ts not found`,
|
|
339
|
+
hint: "Run `chant build` first to generate the worker",
|
|
340
|
+
}));
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// autoStart: spin up temporal server if needed
|
|
345
|
+
if (profile.autoStart) {
|
|
346
|
+
console.error(formatInfo("autoStart: checking Temporal server..."));
|
|
347
|
+
try {
|
|
348
|
+
await waitForTemporalServer(profile.address, 2000);
|
|
349
|
+
console.error(formatInfo("Temporal server already running."));
|
|
350
|
+
} catch {
|
|
351
|
+
console.error(formatInfo("Starting temporal server start-dev..."));
|
|
352
|
+
spawnChild("temporal", ["server", "start-dev"], {
|
|
353
|
+
cwd: projectPath,
|
|
354
|
+
stdio: "ignore",
|
|
355
|
+
detached: true,
|
|
356
|
+
}).unref();
|
|
357
|
+
await waitForTemporalServer(profile.address, 30_000);
|
|
358
|
+
console.error(formatSuccess("Temporal server ready."));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Load Temporal client
|
|
363
|
+
let client;
|
|
364
|
+
try {
|
|
365
|
+
({ client } = await makeTemporalClient(ctx.args.profile, projectPath));
|
|
366
|
+
} catch (err) {
|
|
367
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Spawn worker process
|
|
372
|
+
const profileName = ctx.args.profile ??
|
|
373
|
+
(((chantConfig as Record<string, unknown>).temporal as Record<string, unknown> | undefined)?.defaultProfile as string | undefined) ??
|
|
374
|
+
"local";
|
|
375
|
+
|
|
376
|
+
console.error(formatInfo(`Spawning worker for Op "${opName}" (profile: ${profileName})...`));
|
|
377
|
+
const workerProcess: ChildProcess = spawnChild("npx", ["tsx", workerPath], {
|
|
378
|
+
cwd: projectPath,
|
|
379
|
+
env: { ...process.env, TEMPORAL_PROFILE: profileName },
|
|
380
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Submit workflow
|
|
384
|
+
const workflowId = resolveWorkflowId(opName);
|
|
385
|
+
const fnName = workflowFnName(opName);
|
|
386
|
+
const taskQueue = profile.taskQueue ?? opName;
|
|
387
|
+
|
|
388
|
+
let handle: WorkflowHandleRaw;
|
|
389
|
+
try {
|
|
390
|
+
handle = await client.workflow.start(fnName, {
|
|
391
|
+
taskQueue,
|
|
392
|
+
workflowId,
|
|
393
|
+
workflowIdConflictPolicy: "FAIL",
|
|
394
|
+
});
|
|
395
|
+
console.error(formatSuccess(`Workflow started: ${workflowId}`));
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
398
|
+
return 1;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Poll for progress until terminal state
|
|
402
|
+
let finalDesc: WorkflowExecutionDescription | undefined;
|
|
403
|
+
let finalHistory: WorkflowHistoryRaw | undefined;
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
while (true) {
|
|
407
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
408
|
+
const desc = await handle.describe();
|
|
409
|
+
const history = await handle.fetchHistory();
|
|
410
|
+
|
|
411
|
+
renderProgress(opName, history);
|
|
412
|
+
|
|
413
|
+
if (TERMINAL_STATUSES.has(desc.status.name)) {
|
|
414
|
+
process.stderr.write("\n");
|
|
415
|
+
finalDesc = desc;
|
|
416
|
+
finalHistory = history;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch (err) {
|
|
421
|
+
process.stderr.write("\n");
|
|
422
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
423
|
+
return 1;
|
|
424
|
+
} finally {
|
|
425
|
+
// Kill worker process (best-effort)
|
|
426
|
+
try { workerProcess.kill(); } catch { /* ignore */ }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!finalDesc || !finalHistory) return 1;
|
|
430
|
+
|
|
431
|
+
const status = finalDesc.status.name;
|
|
432
|
+
console.error(status === "COMPLETED"
|
|
433
|
+
? formatSuccess(`Op "${opName}" completed successfully.`)
|
|
434
|
+
: formatError({ message: `Op "${opName}" ended with status: ${status}` }),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Write deployment report
|
|
438
|
+
const md = generateReport(opName, config, finalDesc, finalHistory);
|
|
439
|
+
const reportPath = writeReport(opName, md);
|
|
440
|
+
console.error(formatInfo(`Report written to ${reportPath}`));
|
|
441
|
+
|
|
442
|
+
return status === "COMPLETED" ? 0 : 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── fallback ────────────────────────────────────────────────────────────────���─
|
|
446
|
+
|
|
447
|
+
export function runOpUnknown(ctx: CommandContext): Promise<number> {
|
|
448
|
+
console.error(formatError({
|
|
449
|
+
message: `Unknown run subcommand: ${ctx.args.extraPositional ?? ctx.args.path}`,
|
|
450
|
+
hint: "Available: chant run <name>, run list, run status, run signal, run cancel, run log",
|
|
451
|
+
}));
|
|
452
|
+
return Promise.resolve(1);
|
|
453
|
+
}
|