@intentius/chant 0.1.13 → 0.1.15
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/build.ts +18 -2
- package/src/cli/commands/build.ts +9 -1
- package/src/cli/commands/import-live.test.ts +126 -0
- package/src/cli/commands/import.ts +152 -2
- package/src/cli/commands/migrate.ts +2 -2
- package/src/cli/handlers/{state.test.ts → lifecycle.test.ts} +37 -37
- package/src/cli/handlers/{state.ts → lifecycle.ts} +148 -36
- package/src/cli/handlers/misc.ts +31 -2
- package/src/cli/handlers/run.test.ts +98 -0
- package/src/cli/handlers/run.ts +123 -0
- package/src/cli/main.test.ts +14 -0
- package/src/cli/main.ts +38 -12
- package/src/cli/mcp/{state-tools.ts → lifecycle-tools.ts} +9 -9
- package/src/cli/mcp/op-tools.ts +2 -2
- package/src/cli/mcp/resource-handlers.ts +1 -1
- package/src/cli/mcp/server.test.ts +2 -2
- package/src/cli/mcp/server.ts +1 -1
- package/src/cli/registry.ts +21 -2
- package/src/codegen/fetch.test.ts +103 -2
- package/src/codegen/fetch.ts +62 -10
- package/src/config.ts +31 -0
- package/src/detectLexicon.test.ts +2 -2
- package/src/index.ts +2 -2
- package/src/lexicon-export.test.ts +92 -0
- package/src/lexicon.ts +88 -9
- package/src/lifecycle/change-set.test.ts +151 -0
- package/src/lifecycle/change-set.ts +172 -0
- package/src/{state → lifecycle}/git.test.ts +15 -15
- package/src/{state → lifecycle}/git.ts +14 -14
- package/src/{state → lifecycle}/index.ts +2 -0
- package/src/{state → lifecycle}/snapshot.test.ts +5 -5
- package/src/{state → lifecycle}/snapshot.ts +9 -9
- package/src/{state → lifecycle}/types.ts +1 -1
- package/src/op/activity-registry.test.ts +59 -0
- package/src/op/activity-registry.ts +91 -0
- package/src/op/builders.ts +3 -3
- package/src/op/index.ts +6 -1
- package/src/op/local-executor.test.ts +247 -0
- package/src/op/local-executor.ts +300 -0
- package/src/op/local-output.test.ts +54 -0
- package/src/op/local-output.ts +63 -0
- package/src/op/op.test.ts +4 -4
- package/src/op/types.ts +1 -1
- package/src/ownership.test.ts +109 -0
- package/src/ownership.ts +142 -0
- package/src/serializer.ts +19 -1
- package/src/toml-parse.ts +3 -3
- /package/src/{state → lifecycle}/digest.test.ts +0 -0
- /package/src/{state → lifecycle}/digest.ts +0 -0
- /package/src/{state → lifecycle}/live-diff.test.ts +0 -0
- /package/src/{state → lifecycle}/live-diff.ts +0 -0
|
@@ -43,6 +43,9 @@ function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
|
43
43
|
return {
|
|
44
44
|
command: "run", path: ".",
|
|
45
45
|
format: "", fix: false, watch: false, verbose: false, help: false, live: false,
|
|
46
|
+
// These suites exercise the Temporal path; local mode is the CLI default,
|
|
47
|
+
// so opt in explicitly here. Local-mode behavior is covered separately below.
|
|
48
|
+
temporal: true,
|
|
46
49
|
...overrides,
|
|
47
50
|
};
|
|
48
51
|
}
|
|
@@ -446,3 +449,98 @@ describe("runOp", () => {
|
|
|
446
449
|
}
|
|
447
450
|
});
|
|
448
451
|
});
|
|
452
|
+
|
|
453
|
+
// ── local mode dispatcher + guards ──────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
function localOp(name: string, steps: unknown[]) {
|
|
456
|
+
return [name, { config: { name, overview: `${name} overview`, phases: [{ name: "Phase", steps }] } }] as const;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
describe("runOp dispatcher", () => {
|
|
460
|
+
beforeEach(() => {
|
|
461
|
+
discoverOpsMock.mockReset();
|
|
462
|
+
loadTemporalClientMock.mockReset();
|
|
463
|
+
loadChantConfigMock.mockReset();
|
|
464
|
+
resolveProfileMock.mockReset();
|
|
465
|
+
existsSyncMock.mockReset();
|
|
466
|
+
spawnChildMock.mockReset();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("no flag → local executor (no Temporal client or worker spawned)", async () => {
|
|
470
|
+
discoverOpsMock.mockResolvedValue({
|
|
471
|
+
ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
|
|
472
|
+
errors: [],
|
|
473
|
+
});
|
|
474
|
+
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
475
|
+
const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false }), plugins: [], serializers: [] });
|
|
476
|
+
expect(exit).toBe(0);
|
|
477
|
+
expect(loadTemporalClientMock).not.toHaveBeenCalled();
|
|
478
|
+
expect(spawnChildMock).not.toHaveBeenCalled();
|
|
479
|
+
stderrWrite.mockRestore();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("--temporal → Temporal path (missing worker.ts → exit 1)", async () => {
|
|
483
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
|
|
484
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
485
|
+
resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
|
|
486
|
+
existsSyncMock.mockReturnValue(false);
|
|
487
|
+
const stderr = makeStderrSpy();
|
|
488
|
+
const exit = await runOp({ args: makeArgs({ path: "hello", temporal: true }), plugins: [], serializers: [] });
|
|
489
|
+
expect(exit).toBe(1);
|
|
490
|
+
expect(stderr.join("\n")).toContain("worker.ts not found");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("gate in local mode → fast-fail before execution, suggests --temporal", async () => {
|
|
494
|
+
discoverOpsMock.mockResolvedValue({
|
|
495
|
+
ops: new Map([localOp("gated", [{ kind: "gate", signalName: "approve-prod" }])]),
|
|
496
|
+
errors: [],
|
|
497
|
+
});
|
|
498
|
+
const stderr = makeStderrSpy();
|
|
499
|
+
const exit = await runOp({ args: makeArgs({ path: "gated", temporal: false }), plugins: [], serializers: [] });
|
|
500
|
+
expect(exit).toBe(1);
|
|
501
|
+
expect(stderr.join("\n")).toContain("--temporal");
|
|
502
|
+
expect(loadTemporalClientMock).not.toHaveBeenCalled();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("--local and --temporal together → exit 1 before any work", async () => {
|
|
506
|
+
const stderr = makeStderrSpy();
|
|
507
|
+
const exit = await runOp({ args: makeArgs({ path: "hello", local: true, temporal: true }), plugins: [], serializers: [] });
|
|
508
|
+
expect(exit).toBe(1);
|
|
509
|
+
expect(stderr.join("\n")).toContain("mutually exclusive");
|
|
510
|
+
expect(discoverOpsMock).not.toHaveBeenCalled();
|
|
511
|
+
expect(loadTemporalClientMock).not.toHaveBeenCalled();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("--json → structured result on stdout", async () => {
|
|
515
|
+
discoverOpsMock.mockResolvedValue({
|
|
516
|
+
ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
|
|
517
|
+
errors: [],
|
|
518
|
+
});
|
|
519
|
+
const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
520
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
521
|
+
const exit = await runOp({ args: makeArgs({ path: "hello", temporal: false, json: true }), plugins: [], serializers: [] });
|
|
522
|
+
expect(exit).toBe(0);
|
|
523
|
+
const printed = stdoutWrite.mock.calls.map((c) => String(c[0])).join("");
|
|
524
|
+
const parsed = JSON.parse(printed.trim());
|
|
525
|
+
expect(parsed.op).toBe("hello");
|
|
526
|
+
expect(parsed.ok).toBe(true);
|
|
527
|
+
vi.restoreAllMocks();
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe("Temporal-only subcommand guards", () => {
|
|
532
|
+
const cases: Array<[string, (ctx: { args: ParsedArgs; plugins: never[]; serializers: never[] }) => Promise<number>]> = [
|
|
533
|
+
["list", runOpList],
|
|
534
|
+
["status", runOpStatus],
|
|
535
|
+
["log", runOpLog],
|
|
536
|
+
["signal", runOpSignal],
|
|
537
|
+
["cancel", runOpCancel],
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
test.each(cases)("run %s without --temporal → exit 1 + actionable message", async (_name, handler) => {
|
|
541
|
+
const stderr = makeStderrSpy();
|
|
542
|
+
const exit = await handler({ args: makeArgs({ temporal: false, extraPositional: "x", extraPositional2: "y" }), plugins: [], serializers: [] });
|
|
543
|
+
expect(exit).toBe(1);
|
|
544
|
+
expect(stderr.join("\n")).toContain("not available in local mode");
|
|
545
|
+
});
|
|
546
|
+
});
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { createConnection } from "node:net";
|
|
|
4
4
|
import { spawn as spawnChild, type ChildProcess } from "node:child_process";
|
|
5
5
|
import { loadChantConfig } from "../../config";
|
|
6
6
|
import { discoverOps } from "../../op/discover";
|
|
7
|
+
import { loadActivities, loadProfiles } from "../../op/activity-registry";
|
|
8
|
+
import { runOpLocally, findGate, LocalGateUnsupportedError, OpRunFailure } from "../../op/local-executor";
|
|
9
|
+
import { renderHuman, renderJson } from "../../op/local-output";
|
|
7
10
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
8
11
|
import type { CommandContext } from "../registry";
|
|
9
12
|
import {
|
|
@@ -25,6 +28,21 @@ function workflowFnName(opName: string): string {
|
|
|
25
28
|
return kebabToCamel(opName) + "Workflow";
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Guard for Temporal-only commands (run list/status/log/signal/cancel, --report).
|
|
33
|
+
* These query durable run state that only exists under Temporal. Returns true
|
|
34
|
+
* when `--temporal` was passed; otherwise prints an actionable message and the
|
|
35
|
+
* caller should return non-zero.
|
|
36
|
+
*/
|
|
37
|
+
function requireTemporalMode(ctx: CommandContext, what: string): boolean {
|
|
38
|
+
if (ctx.args.temporal) return true;
|
|
39
|
+
console.error(formatError({
|
|
40
|
+
message: `\`${what}\` is not available in local mode`,
|
|
41
|
+
hint: "pass --temporal or configure a profile",
|
|
42
|
+
}));
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
export async function makeTemporalClient(profileName: string | undefined, projectPath: string) {
|
|
29
47
|
const { config } = await loadChantConfig(projectPath);
|
|
30
48
|
const profile = resolveProfile(config as Record<string, unknown>, profileName);
|
|
@@ -37,6 +55,7 @@ export async function makeTemporalClient(profileName: string | undefined, projec
|
|
|
37
55
|
// ── chant run list ──────────────────────────────────���─────────────────────────
|
|
38
56
|
|
|
39
57
|
export async function runOpList(ctx: CommandContext): Promise<number> {
|
|
58
|
+
if (!requireTemporalMode(ctx, "chant run list")) return 1;
|
|
40
59
|
const { ops, errors } = await discoverOps();
|
|
41
60
|
|
|
42
61
|
for (const err of errors) {
|
|
@@ -102,6 +121,7 @@ export async function runOpList(ctx: CommandContext): Promise<number> {
|
|
|
102
121
|
// ── chant run status <name> ───────────────────────────────────────────────────
|
|
103
122
|
|
|
104
123
|
export async function runOpStatus(ctx: CommandContext): Promise<number> {
|
|
124
|
+
if (!requireTemporalMode(ctx, "chant run status")) return 1;
|
|
105
125
|
const name = ctx.args.extraPositional;
|
|
106
126
|
if (!name) {
|
|
107
127
|
console.error(formatError({ message: "Op name is required: chant run status <name>" }));
|
|
@@ -141,6 +161,7 @@ export async function runOpStatus(ctx: CommandContext): Promise<number> {
|
|
|
141
161
|
// ── chant run log <name> ──────────────────────────────────────────────────────
|
|
142
162
|
|
|
143
163
|
export async function runOpLog(ctx: CommandContext): Promise<number> {
|
|
164
|
+
if (!requireTemporalMode(ctx, "chant run log")) return 1;
|
|
144
165
|
const name = ctx.args.extraPositional;
|
|
145
166
|
if (!name) {
|
|
146
167
|
console.error(formatError({ message: "Op name is required: chant run log <name>" }));
|
|
@@ -186,6 +207,7 @@ export async function runOpLog(ctx: CommandContext): Promise<number> {
|
|
|
186
207
|
// ── chant run signal <name> <signal> ─────────────────────────────────────────
|
|
187
208
|
|
|
188
209
|
export async function runOpSignal(ctx: CommandContext): Promise<number> {
|
|
210
|
+
if (!requireTemporalMode(ctx, "chant run signal")) return 1;
|
|
189
211
|
const name = ctx.args.extraPositional;
|
|
190
212
|
const signalName = ctx.args.extraPositional2;
|
|
191
213
|
|
|
@@ -212,6 +234,7 @@ export async function runOpSignal(ctx: CommandContext): Promise<number> {
|
|
|
212
234
|
// ── chant run cancel <name> ───────────────────────────────────────────────────
|
|
213
235
|
|
|
214
236
|
export async function runOpCancel(ctx: CommandContext): Promise<number> {
|
|
237
|
+
if (!requireTemporalMode(ctx, "chant run cancel")) return 1;
|
|
215
238
|
const name = ctx.args.extraPositional;
|
|
216
239
|
if (!name) {
|
|
217
240
|
console.error(formatError({ message: "Op name is required: chant run cancel <name>" }));
|
|
@@ -274,7 +297,107 @@ function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
|
|
|
274
297
|
);
|
|
275
298
|
}
|
|
276
299
|
|
|
300
|
+
/**
|
|
301
|
+
* `chant run <name>` dispatcher.
|
|
302
|
+
*
|
|
303
|
+
* Local mode is the default — it runs the Op in-process with no Temporal
|
|
304
|
+
* server. `--temporal` opts into a cluster (gates, schedules, durable resume).
|
|
305
|
+
* `--report` reads a past durable run and is therefore Temporal-only.
|
|
306
|
+
*/
|
|
277
307
|
export async function runOp(ctx: CommandContext): Promise<number> {
|
|
308
|
+
if (ctx.args.local && ctx.args.temporal) {
|
|
309
|
+
console.error(formatError({
|
|
310
|
+
message: "--local and --temporal are mutually exclusive",
|
|
311
|
+
hint: "omit both for local mode (the default), or pass exactly one",
|
|
312
|
+
}));
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
if (ctx.args.report) {
|
|
316
|
+
if (!requireTemporalMode(ctx, "chant run --report")) return 1;
|
|
317
|
+
return runOpTemporal(ctx);
|
|
318
|
+
}
|
|
319
|
+
return ctx.args.temporal ? runOpTemporal(ctx) : runOpLocal(ctx);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Run an Op in-process via the local executor — no Temporal worker, server, or
|
|
324
|
+
* built `worker.ts`. Reads the Op config straight from discovery and resolves
|
|
325
|
+
* activities from the Temporal lexicon package.
|
|
326
|
+
*/
|
|
327
|
+
export async function runOpLocal(ctx: CommandContext): Promise<number> {
|
|
328
|
+
const opName = ctx.args.path;
|
|
329
|
+
if (!opName || opName === ".") {
|
|
330
|
+
console.error(formatError({
|
|
331
|
+
message: "Op name is required: chant run <name>",
|
|
332
|
+
hint: "Run `chant run list --temporal` to see available Ops",
|
|
333
|
+
}));
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const { ops, errors } = await discoverOps();
|
|
338
|
+
for (const err of errors) console.error(formatWarning({ message: err }));
|
|
339
|
+
|
|
340
|
+
const discovered = ops.get(opName);
|
|
341
|
+
if (!discovered) {
|
|
342
|
+
const names = [...ops.keys()];
|
|
343
|
+
console.error(formatError({
|
|
344
|
+
message: `Op "${opName}" not found`,
|
|
345
|
+
hint: names.length > 0
|
|
346
|
+
? `Available: ${names.join(", ")}`
|
|
347
|
+
: "No *.op.ts files found — create one",
|
|
348
|
+
}));
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const { config } = discovered;
|
|
353
|
+
|
|
354
|
+
// Pre-flight: gates/schedules need a durable runtime — fail before any step.
|
|
355
|
+
const gate = findGate(config);
|
|
356
|
+
if (gate) {
|
|
357
|
+
console.error(formatError({
|
|
358
|
+
message: new LocalGateUnsupportedError(gate.signalName).message,
|
|
359
|
+
}));
|
|
360
|
+
return 1;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let activities, profiles;
|
|
364
|
+
try {
|
|
365
|
+
[activities, profiles] = await Promise.all([loadActivities(), loadProfiles()]);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Ctrl-C aborts in-flight activities (kills their child processes) instead of
|
|
372
|
+
// orphaning them. The handler is removed in `finally` so it never leaks.
|
|
373
|
+
const controller = new AbortController();
|
|
374
|
+
const onSigint = () => {
|
|
375
|
+
console.error(formatWarning({ message: "interrupted — stopping Op" }));
|
|
376
|
+
controller.abort();
|
|
377
|
+
};
|
|
378
|
+
process.once("SIGINT", onSigint);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const result = await runOpLocally(config, activities, profiles, controller.signal);
|
|
382
|
+
if (ctx.args.json) renderJson(result); else renderHuman(result);
|
|
383
|
+
return 0;
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (err instanceof OpRunFailure) {
|
|
386
|
+
if (ctx.args.json) renderJson(err.result); else renderHuman(err.result);
|
|
387
|
+
return 1;
|
|
388
|
+
}
|
|
389
|
+
if (err instanceof LocalGateUnsupportedError) {
|
|
390
|
+
console.error(formatError({ message: err.message }));
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
394
|
+
return 1;
|
|
395
|
+
} finally {
|
|
396
|
+
process.removeListener("SIGINT", onSigint);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function runOpTemporal(ctx: CommandContext): Promise<number> {
|
|
278
401
|
const opName = ctx.args.path;
|
|
279
402
|
|
|
280
403
|
if (!opName || opName === ".") {
|
package/src/cli/main.test.ts
CHANGED
|
@@ -187,6 +187,8 @@ describe("resolveCommand", () => {
|
|
|
187
187
|
{ name: "init", handler: noop },
|
|
188
188
|
{ name: "init lexicon", handler: noop },
|
|
189
189
|
{ name: "dev", handler: noop },
|
|
190
|
+
{ name: "lifecycle plan", handler: noop },
|
|
191
|
+
{ name: "lifecycle", handler: noop },
|
|
190
192
|
];
|
|
191
193
|
|
|
192
194
|
function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
@@ -256,6 +258,18 @@ describe("resolveCommand", () => {
|
|
|
256
258
|
expect(result!.compound).toBe(true);
|
|
257
259
|
});
|
|
258
260
|
|
|
261
|
+
test("`lc` is an alias for `lifecycle` (compound)", () => {
|
|
262
|
+
const result = resolveCommand(makeArgs({ command: "lc", path: "plan" }), testRegistry);
|
|
263
|
+
expect(result!.def.name).toBe("lifecycle plan");
|
|
264
|
+
expect(result!.compound).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("`lc` is an alias for `lifecycle` (simple)", () => {
|
|
268
|
+
const result = resolveCommand(makeArgs({ command: "lc" }), testRegistry);
|
|
269
|
+
expect(result!.def.name).toBe("lifecycle");
|
|
270
|
+
expect(result!.compound).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
|
|
259
273
|
test("resolves run status as compound command", () => {
|
|
260
274
|
const registry: CommandDef[] = [
|
|
261
275
|
{ name: "run list", handler: noop },
|
package/src/cli/main.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { runServeLsp, runServeMcp, runServeUnknown } from "./handlers/serve";
|
|
|
13
13
|
import { runInit, runInitLexicon } from "./handlers/init";
|
|
14
14
|
import { runList, runImport, runUpdate, runDoctor } from "./handlers/misc";
|
|
15
15
|
import { runMigrate } from "./handlers/migrate";
|
|
16
|
-
import {
|
|
16
|
+
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecyclePlan, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
17
17
|
import { runGraph } from "./handlers/graph";
|
|
18
18
|
import { runOp, runOpList, runOpStatus, runOpSignal, runOpCancel, runOpLog } from "./handlers/run";
|
|
19
19
|
|
|
@@ -37,6 +37,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
37
37
|
help: false,
|
|
38
38
|
profile: undefined,
|
|
39
39
|
report: undefined,
|
|
40
|
+
local: undefined,
|
|
41
|
+
temporal: undefined,
|
|
42
|
+
json: undefined,
|
|
40
43
|
live: false,
|
|
41
44
|
migrateFrom: undefined,
|
|
42
45
|
migrateTo: undefined,
|
|
@@ -85,7 +88,17 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
85
88
|
} else if (arg === "--live") {
|
|
86
89
|
result.live = true;
|
|
87
90
|
} else if (arg === "--from") {
|
|
91
|
+
// Shared by `migrate --from <lexicon>` and `import --from <env>`; the
|
|
92
|
+
// two commands never run together, so one field carries both.
|
|
88
93
|
result.migrateFrom = args[++i];
|
|
94
|
+
} else if (arg === "--type") {
|
|
95
|
+
result.selectType = args[++i];
|
|
96
|
+
} else if (arg === "--name") {
|
|
97
|
+
result.selectName = args[++i];
|
|
98
|
+
} else if (arg === "--owned") {
|
|
99
|
+
result.owned = true;
|
|
100
|
+
} else if (arg === "--verbatim") {
|
|
101
|
+
result.verbatim = true;
|
|
89
102
|
} else if (arg === "--to") {
|
|
90
103
|
result.migrateTo = args[++i];
|
|
91
104
|
} else if (arg === "--emit") {
|
|
@@ -98,6 +111,12 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
98
111
|
result.useComposites = true;
|
|
99
112
|
} else if (arg === "--skill") {
|
|
100
113
|
result.skill = args[++i];
|
|
114
|
+
} else if (arg === "--local") {
|
|
115
|
+
result.local = true;
|
|
116
|
+
} else if (arg === "--temporal") {
|
|
117
|
+
result.temporal = true;
|
|
118
|
+
} else if (arg === "--json") {
|
|
119
|
+
result.json = true;
|
|
101
120
|
} else if (!arg.startsWith("-")) {
|
|
102
121
|
if (!result.command) {
|
|
103
122
|
result.command = arg;
|
|
@@ -146,12 +165,14 @@ Ops:
|
|
|
146
165
|
|
|
147
166
|
graph Show Op dependency graph
|
|
148
167
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
Lifecycle (alias: lc):
|
|
169
|
+
lifecycle snapshot <env> Query API, save metadata to orphan branch
|
|
170
|
+
lifecycle show <env> Show latest lifecycle snapshot
|
|
171
|
+
lifecycle diff <env> Compare current build against last snapshot
|
|
172
|
+
--live: query cloud now and detect drift
|
|
173
|
+
lifecycle plan <env> Typed change set (create/update/delete/adopt) vs live
|
|
174
|
+
--json: emit the ChangeSet as JSON
|
|
175
|
+
lifecycle log [env] History of lifecycle snapshots
|
|
155
176
|
|
|
156
177
|
Lexicon development:
|
|
157
178
|
dev generate Generate lexicon artifacts (+ validate + coverage)
|
|
@@ -182,6 +203,9 @@ Options:
|
|
|
182
203
|
-v, --verbose Show stack traces on errors
|
|
183
204
|
-h, --help Show this help message
|
|
184
205
|
-p, --profile <name> Temporal worker profile to use (run command)
|
|
206
|
+
--local Run an Op with the local in-process executor (default)
|
|
207
|
+
--temporal Run an Op via a Temporal cluster (gates, schedules, durable resume)
|
|
208
|
+
--json Emit the structured run result as JSON (run command)
|
|
185
209
|
--report Print deployment report instead of running (run command)
|
|
186
210
|
OR with a path arg: SARIF report destination (migrate)
|
|
187
211
|
--from <name> Source lexicon for migrate (default: github)
|
|
@@ -197,6 +221,7 @@ Examples:
|
|
|
197
221
|
chant build ./infra/ --format yaml
|
|
198
222
|
chant build ./infra/ --watch
|
|
199
223
|
chant import template.json --output ./infra/
|
|
224
|
+
chant import --from prod --name my-bucket --output src/
|
|
200
225
|
chant lint ./infra/
|
|
201
226
|
chant lint ./infra/ --format sarif
|
|
202
227
|
chant lint ./infra/ --watch
|
|
@@ -261,17 +286,18 @@ const registry: CommandDef[] = [
|
|
|
261
286
|
{ name: "graph", handler: runGraph },
|
|
262
287
|
|
|
263
288
|
// State subcommands
|
|
264
|
-
{ name: "
|
|
265
|
-
{ name: "
|
|
266
|
-
{ name: "
|
|
267
|
-
{ name: "
|
|
289
|
+
{ name: "lifecycle snapshot", requiresPlugins: true, handler: runLifecycleSnapshot },
|
|
290
|
+
{ name: "lifecycle show", handler: runLifecycleShow },
|
|
291
|
+
{ name: "lifecycle diff", requiresPlugins: true, handler: runLifecycleDiff },
|
|
292
|
+
{ name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
|
|
293
|
+
{ name: "lifecycle log", handler: runLifecycleLog },
|
|
268
294
|
|
|
269
295
|
// Serve subcommands
|
|
270
296
|
{ name: "serve lsp", requiresPlugins: true, handler: runServeLsp },
|
|
271
297
|
{ name: "serve mcp", requiresPlugins: true, handler: runServeMcp },
|
|
272
298
|
|
|
273
299
|
// Fallback for unknown subcommands (must come after compound entries)
|
|
274
|
-
{ name: "
|
|
300
|
+
{ name: "lifecycle", handler: runLifecycleUnknown },
|
|
275
301
|
{ name: "dev", handler: runDevUnknown },
|
|
276
302
|
{ name: "serve", handler: runServeUnknown },
|
|
277
303
|
];
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import type { LexiconPlugin } from "../../lexicon";
|
|
3
3
|
import type { ToolDefinition, ToolHandler } from "./types";
|
|
4
|
-
import { readSnapshot } from "../../
|
|
4
|
+
import { readSnapshot } from "../../lifecycle/git";
|
|
5
5
|
import { build } from "../../build";
|
|
6
|
-
import { computeBuildDigest, diffDigests } from "../../
|
|
7
|
-
import { takeSnapshot } from "../../
|
|
8
|
-
import type {
|
|
6
|
+
import { computeBuildDigest, diffDigests } from "../../lifecycle/digest";
|
|
7
|
+
import { takeSnapshot } from "../../lifecycle/snapshot";
|
|
8
|
+
import type { LifecycleSnapshot } from "../../lifecycle/types";
|
|
9
9
|
export interface ToolRegistration {
|
|
10
10
|
definition: ToolDefinition;
|
|
11
11
|
handler: ToolHandler;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Create
|
|
15
|
+
* Create lifecycle-snapshot tool definition and handler
|
|
16
16
|
*/
|
|
17
17
|
export function createSnapshotTool(plugins: LexiconPlugin[]): ToolRegistration {
|
|
18
18
|
return {
|
|
19
19
|
definition: {
|
|
20
|
-
name: "
|
|
20
|
+
name: "lifecycle-snapshot",
|
|
21
21
|
description: "Capture deployed state for an environment",
|
|
22
22
|
inputSchema: {
|
|
23
23
|
type: "object",
|
|
@@ -46,12 +46,12 @@ export function createSnapshotTool(plugins: LexiconPlugin[]): ToolRegistration {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Create
|
|
49
|
+
* Create lifecycle-diff tool definition and handler
|
|
50
50
|
*/
|
|
51
51
|
export function createDiffTool(plugins: LexiconPlugin[]): ToolRegistration {
|
|
52
52
|
return {
|
|
53
53
|
definition: {
|
|
54
|
-
name: "
|
|
54
|
+
name: "lifecycle-diff",
|
|
55
55
|
description: "Compare current build declarations against last snapshot's digest",
|
|
56
56
|
inputSchema: {
|
|
57
57
|
type: "object",
|
|
@@ -75,7 +75,7 @@ export function createDiffTool(plugins: LexiconPlugin[]): ToolRegistration {
|
|
|
75
75
|
const content = await readSnapshot(env, lex);
|
|
76
76
|
let previousDigest = undefined;
|
|
77
77
|
if (content) {
|
|
78
|
-
const snapshot:
|
|
78
|
+
const snapshot: LifecycleSnapshot = JSON.parse(content);
|
|
79
79
|
previousDigest = snapshot.digest;
|
|
80
80
|
}
|
|
81
81
|
results[lex] = diffDigests(currentDigest, previousDigest);
|
package/src/cli/mcp/op-tools.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { discoverOps } from "../../op/discover";
|
|
|
3
3
|
import { makeTemporalClient } from "../handlers/run";
|
|
4
4
|
import { resolveWorkflowId } from "../handlers/run-client";
|
|
5
5
|
import { generateReport } from "../handlers/run-report";
|
|
6
|
-
import type { ToolRegistration } from "./
|
|
6
|
+
import type { ToolRegistration } from "./lifecycle-tools";
|
|
7
7
|
|
|
8
8
|
function workflowFnName(opName: string): string {
|
|
9
9
|
return opName.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + "Workflow";
|
|
@@ -63,7 +63,7 @@ export function createOpRunTool(): ToolRegistration {
|
|
|
63
63
|
definition: {
|
|
64
64
|
name: "op-run",
|
|
65
65
|
description:
|
|
66
|
-
"Submit an Op workflow to Temporal.
|
|
66
|
+
"Submit an Op workflow to Temporal (Temporal mode). A Temporal worker must already be running — start it with `chant run <name> --temporal`.",
|
|
67
67
|
inputSchema: {
|
|
68
68
|
type: "object",
|
|
69
69
|
properties: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import type { ResourceDefinition } from "./types";
|
|
3
3
|
import { getContext } from "./resources/context";
|
|
4
|
-
import { readSnapshot, readEnvironmentSnapshots } from "../../
|
|
4
|
+
import { readSnapshot, readEnvironmentSnapshots } from "../../lifecycle/git";
|
|
5
5
|
import { discoverOps } from "../../op/discover";
|
|
6
6
|
import { makeTemporalClient } from "../handlers/run";
|
|
7
7
|
import { resolveWorkflowId } from "../handlers/run-client";
|
|
@@ -1133,9 +1133,9 @@ describe("McpServer", () => {
|
|
|
1133
1133
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
1134
1134
|
expect(tools).toHaveLength(13);
|
|
1135
1135
|
expect(tools.map((t) => t.name).sort()).toEqual([
|
|
1136
|
-
"build", "explain", "import", "lint",
|
|
1136
|
+
"build", "explain", "import", "lifecycle-diff", "lifecycle-snapshot", "lint",
|
|
1137
1137
|
"op-list", "op-report", "op-run", "op-signal", "op-status",
|
|
1138
|
-
"scaffold", "search",
|
|
1138
|
+
"scaffold", "search",
|
|
1139
1139
|
]);
|
|
1140
1140
|
|
|
1141
1141
|
const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
|
package/src/cli/mcp/server.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { scaffoldTool, createScaffoldHandler } from "./tools/scaffold";
|
|
|
8
8
|
import { searchTool, createSearchHandler } from "./tools/search";
|
|
9
9
|
import type { LexiconPlugin } from "../../lexicon";
|
|
10
10
|
import type { McpRequest, McpResponse, ToolDefinition, ToolHandler, ResourceDefinition } from "./types";
|
|
11
|
-
import { createSnapshotTool, createDiffTool } from "./
|
|
11
|
+
import { createSnapshotTool, createDiffTool } from "./lifecycle-tools";
|
|
12
12
|
import { createOpListTool, createOpRunTool, createOpStatusTool, createOpSignalTool, createOpReportTool } from "./op-tools";
|
|
13
13
|
import { buildResourcesList, handleResourcesRead } from "./resource-handlers";
|
|
14
14
|
|
package/src/cli/registry.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface ParsedArgs {
|
|
|
20
20
|
help: boolean;
|
|
21
21
|
profile?: string;
|
|
22
22
|
report?: boolean;
|
|
23
|
+
/** `chant run` — force the local in-process executor (the default). */
|
|
24
|
+
local?: boolean;
|
|
25
|
+
/** `chant run` — run via a Temporal cluster instead of the local executor. */
|
|
26
|
+
temporal?: boolean;
|
|
27
|
+
/** `chant run` — emit the structured OpRunResult as JSON on stdout. */
|
|
28
|
+
json?: boolean;
|
|
23
29
|
live: boolean;
|
|
24
30
|
/** `chant migrate --from <name>` (default "github") */
|
|
25
31
|
migrateFrom?: string;
|
|
@@ -37,6 +43,14 @@ export interface ParsedArgs {
|
|
|
37
43
|
reportFile?: string;
|
|
38
44
|
/** `chant init --skill <name>` filter (added in #95 commit) */
|
|
39
45
|
skill?: string;
|
|
46
|
+
/** `chant import --type <ResourceType>` selector */
|
|
47
|
+
selectType?: string;
|
|
48
|
+
/** `chant import --name <name>` selector */
|
|
49
|
+
selectName?: string;
|
|
50
|
+
/** `chant import --owned` — restrict live import to chant-owned resources */
|
|
51
|
+
owned?: boolean;
|
|
52
|
+
/** `chant import --verbatim` — keep server-defaulted fields in live import */
|
|
53
|
+
verbatim?: boolean;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
/**
|
|
@@ -75,16 +89,21 @@ export interface ResolvedCommand {
|
|
|
75
89
|
* Supports compound commands like "dev generate" where args.command="dev"
|
|
76
90
|
* and args.path="generate". Falls back to simple command matching.
|
|
77
91
|
*/
|
|
92
|
+
/** Short command aliases. `chant lc …` is sugar for `chant lifecycle …`. */
|
|
93
|
+
const COMMAND_ALIASES: Record<string, string> = { lc: "lifecycle" };
|
|
94
|
+
|
|
78
95
|
export function resolveCommand(args: ParsedArgs, registry: CommandDef[]): ResolvedCommand | null {
|
|
96
|
+
const command = COMMAND_ALIASES[args.command] ?? args.command;
|
|
97
|
+
|
|
79
98
|
// Try compound command first: "dev generate", "serve lsp", "init lexicon"
|
|
80
|
-
const compound = `${
|
|
99
|
+
const compound = `${command} ${args.path}`;
|
|
81
100
|
const compoundMatch = registry.find((c) => c.name === compound);
|
|
82
101
|
if (compoundMatch) {
|
|
83
102
|
return { def: compoundMatch, compound: true };
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
// Try simple command
|
|
87
|
-
const simpleMatch = registry.find((c) => c.name ===
|
|
106
|
+
const simpleMatch = registry.find((c) => c.name === command);
|
|
88
107
|
if (simpleMatch) {
|
|
89
108
|
return { def: simpleMatch, compound: false };
|
|
90
109
|
}
|