@intentius/chant 0.1.14 → 0.1.16

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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/src/build.ts +18 -2
  3. package/src/cli/commands/build.ts +9 -1
  4. package/src/cli/commands/import-live.test.ts +126 -0
  5. package/src/cli/commands/import.ts +152 -2
  6. package/src/cli/commands/migrate.ts +2 -2
  7. package/src/cli/handlers/{state.test.ts → lifecycle.test.ts} +80 -37
  8. package/src/cli/handlers/{state.ts → lifecycle.ts} +166 -40
  9. package/src/cli/handlers/misc.ts +31 -2
  10. package/src/cli/handlers/run.test.ts +98 -0
  11. package/src/cli/handlers/run.ts +123 -0
  12. package/src/cli/main.test.ts +14 -0
  13. package/src/cli/main.ts +49 -15
  14. package/src/cli/mcp/{state-tools.ts → lifecycle-tools.ts} +9 -9
  15. package/src/cli/mcp/op-tools.ts +2 -2
  16. package/src/cli/mcp/resource-handlers.ts +1 -1
  17. package/src/cli/mcp/server.test.ts +2 -2
  18. package/src/cli/mcp/server.ts +1 -1
  19. package/src/cli/registry.ts +23 -2
  20. package/src/codegen/fetch.test.ts +103 -2
  21. package/src/codegen/fetch.ts +62 -10
  22. package/src/config.ts +41 -0
  23. package/src/detectLexicon.test.ts +2 -2
  24. package/src/index.ts +2 -2
  25. package/src/lexicon-export.test.ts +92 -0
  26. package/src/lexicon.ts +88 -9
  27. package/src/lifecycle/change-set.test.ts +151 -0
  28. package/src/lifecycle/change-set.ts +172 -0
  29. package/src/{state → lifecycle}/git.test.ts +15 -15
  30. package/src/{state → lifecycle}/git.ts +14 -14
  31. package/src/{state → lifecycle}/index.ts +2 -0
  32. package/src/{state → lifecycle}/snapshot.test.ts +5 -5
  33. package/src/{state → lifecycle}/snapshot.ts +9 -9
  34. package/src/{state → lifecycle}/types.ts +1 -1
  35. package/src/op/activity-registry.test.ts +59 -0
  36. package/src/op/activity-registry.ts +98 -0
  37. package/src/op/builders.ts +56 -20
  38. package/src/op/index.ts +6 -1
  39. package/src/op/local-executor.test.ts +263 -0
  40. package/src/op/local-executor.ts +300 -0
  41. package/src/op/local-output.test.ts +54 -0
  42. package/src/op/local-output.ts +63 -0
  43. package/src/op/op.test.ts +41 -4
  44. package/src/op/types.ts +2 -2
  45. package/src/ownership.test.ts +109 -0
  46. package/src/ownership.ts +142 -0
  47. package/src/serializer.ts +19 -1
  48. package/src/toml-parse.ts +3 -3
  49. /package/src/{state → lifecycle}/digest.test.ts +0 -0
  50. /package/src/{state → lifecycle}/digest.ts +0 -0
  51. /package/src/{state → lifecycle}/live-diff.test.ts +0 -0
  52. /package/src/{state → lifecycle}/live-diff.ts +0 -0
@@ -1,11 +1,12 @@
1
1
  import { describe, test, expect, vi, beforeEach } from "vitest";
2
+ import { sep } from "node:path";
2
3
  import { createMockPlugin, staticDescribeResources, staticListArtifacts } from "@intentius/chant-test-utils";
3
4
  import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
4
5
  import type { BuildResult } from "../../build";
5
6
  import type { ParsedArgs } from "../registry";
6
7
 
7
8
  const buildMock = vi.fn();
8
- const fetchStateMock = vi.fn();
9
+ const fetchLifecycleMock = vi.fn();
9
10
  const readSnapshotMock = vi.fn();
10
11
  const readEnvironmentSnapshotsMock = vi.fn();
11
12
  const listSnapshotsMock = vi.fn();
@@ -13,20 +14,20 @@ const takeSnapshotMock = vi.fn();
13
14
  const loadChantConfigMock = vi.fn();
14
15
 
15
16
  vi.mock("../../build", () => ({ build: (...args: unknown[]) => buildMock(...args) }));
16
- vi.mock("../../state/git", () => ({
17
- fetchState: () => fetchStateMock(),
17
+ vi.mock("../../lifecycle/git", () => ({
18
+ fetchLifecycle: () => fetchLifecycleMock(),
18
19
  readSnapshot: (...args: unknown[]) => readSnapshotMock(...args),
19
20
  readEnvironmentSnapshots: (...args: unknown[]) => readEnvironmentSnapshotsMock(...args),
20
21
  listSnapshots: (...args: unknown[]) => listSnapshotsMock(...args),
21
22
  }));
22
- vi.mock("../../state/snapshot", () => ({
23
+ vi.mock("../../lifecycle/snapshot", () => ({
23
24
  takeSnapshot: (...args: unknown[]) => takeSnapshotMock(...args),
24
25
  }));
25
26
  vi.mock("../../config", () => ({
26
27
  loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
27
28
  }));
28
29
 
29
- const { runStateDiff, runStateSnapshot, runStateShow, runStateLog, runStateUnknown } = await import("./state");
30
+ const { runLifecycleDiff, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleUnknown } = await import("./lifecycle");
30
31
 
31
32
  function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
32
33
  return {
@@ -71,7 +72,7 @@ const meta = (overrides: Partial<ResourceMetadata> = {}): ResourceMetadata => ({
71
72
  ...overrides,
72
73
  });
73
74
 
74
- describe("runStateDiff --live", () => {
75
+ describe("runLifecycleDiff --live", () => {
75
76
  let stdoutSpy: ReturnType<typeof vi.spyOn>;
76
77
  let stderrSpy: ReturnType<typeof vi.spyOn>;
77
78
  let stdoutBuf: string[];
@@ -83,13 +84,15 @@ describe("runStateDiff --live", () => {
83
84
  stdoutSpy = vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
84
85
  stderrSpy = vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
85
86
  buildMock.mockReset();
86
- fetchStateMock.mockReset();
87
+ fetchLifecycleMock.mockReset();
87
88
  readSnapshotMock.mockReset();
89
+ loadChantConfigMock.mockReset();
90
+ loadChantConfigMock.mockResolvedValue({ config: {} });
88
91
  });
89
92
 
90
93
  test("surfaces drift between previous snapshot and live state", async () => {
91
94
  buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
92
- fetchStateMock.mockResolvedValue(undefined);
95
+ fetchLifecycleMock.mockResolvedValue(undefined);
93
96
  readSnapshotMock.mockResolvedValue(JSON.stringify({
94
97
  lexicon: "aws",
95
98
  environment: "prod",
@@ -113,7 +116,7 @@ describe("runStateDiff --live", () => {
113
116
  serializers: plugins.map((p) => p.serializer),
114
117
  };
115
118
 
116
- const exit = await runStateDiff(ctx);
119
+ const exit = await runLifecycleDiff(ctx);
117
120
 
118
121
  expect(exit).toBe(0);
119
122
  const output = stdoutBuf.join("\n");
@@ -124,9 +127,49 @@ describe("runStateDiff --live", () => {
124
127
  expect(output).toContain("UPDATE_COMPLETE");
125
128
  });
126
129
 
130
+ test("builds from config.sourceDir on a mixed-layout project", async () => {
131
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
132
+ fetchLifecycleMock.mockResolvedValue(undefined);
133
+ readSnapshotMock.mockResolvedValue(null);
134
+ loadChantConfigMock.mockResolvedValue({ config: { sourceDir: "src" } });
135
+
136
+ const plugins: LexiconPlugin[] = [
137
+ createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
138
+ ];
139
+ const exit = await runLifecycleDiff({
140
+ args: makeArgs({ path: "diff", extraPositional: "prod", extraPositional2: "aws", live: true }),
141
+ plugins,
142
+ serializers: plugins.map((p) => p.serializer),
143
+ });
144
+
145
+ expect(exit).toBe(0);
146
+ const builtPath = buildMock.mock.calls[0][0] as string;
147
+ expect(builtPath.endsWith(`${sep}src`)).toBe(true);
148
+ });
149
+
150
+ test("--src overrides config.sourceDir for the build root", async () => {
151
+ buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
152
+ fetchLifecycleMock.mockResolvedValue(undefined);
153
+ readSnapshotMock.mockResolvedValue(null);
154
+ loadChantConfigMock.mockResolvedValue({ config: { sourceDir: "src" } });
155
+
156
+ const plugins: LexiconPlugin[] = [
157
+ createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
158
+ ];
159
+ const exit = await runLifecycleDiff({
160
+ args: makeArgs({ path: "diff", extraPositional: "prod", extraPositional2: "aws", live: true, src: "infra" }),
161
+ plugins,
162
+ serializers: plugins.map((p) => p.serializer),
163
+ });
164
+
165
+ expect(exit).toBe(0);
166
+ const builtPath = buildMock.mock.calls[0][0] as string;
167
+ expect(builtPath.endsWith(`${sep}infra`)).toBe(true);
168
+ });
169
+
127
170
  test("warns and skips lexicons without describeResources", async () => {
128
171
  buildMock.mockResolvedValue(makeBuildResult({ k8s: ["pod"] }));
129
- fetchStateMock.mockResolvedValue(undefined);
172
+ fetchLifecycleMock.mockResolvedValue(undefined);
130
173
  readSnapshotMock.mockResolvedValue(null);
131
174
 
132
175
  const plugins: LexiconPlugin[] = [
@@ -139,7 +182,7 @@ describe("runStateDiff --live", () => {
139
182
  serializers: plugins.map((p) => p.serializer),
140
183
  };
141
184
 
142
- const exit = await runStateDiff(ctx);
185
+ const exit = await runLifecycleDiff(ctx);
143
186
 
144
187
  expect(exit).toBe(1);
145
188
  const stderr = stderrBuf.join("\n");
@@ -149,7 +192,7 @@ describe("runStateDiff --live", () => {
149
192
 
150
193
  test("--live with a listArtifacts-only plugin diffs artifacts (no resources path)", async () => {
151
194
  buildMock.mockResolvedValue(makeBuildResult({ helm: [] }));
152
- fetchStateMock.mockResolvedValue(undefined);
195
+ fetchLifecycleMock.mockResolvedValue(undefined);
153
196
  // Previous snapshot has no artifact entry for the new release → expect ARTIFACTS ADDED
154
197
  readSnapshotMock.mockResolvedValue(JSON.stringify({
155
198
  lexicon: "helm",
@@ -175,7 +218,7 @@ describe("runStateDiff --live", () => {
175
218
  serializers: plugins.map((p) => p.serializer),
176
219
  };
177
220
 
178
- const exit = await runStateDiff(ctx);
221
+ const exit = await runLifecycleDiff(ctx);
179
222
 
180
223
  expect(exit).toBe(0);
181
224
  const output = stdoutBuf.join("\n");
@@ -185,7 +228,7 @@ describe("runStateDiff --live", () => {
185
228
 
186
229
  test("legacy digest mode still works without --live", async () => {
187
230
  buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
188
- fetchStateMock.mockResolvedValue(undefined);
231
+ fetchLifecycleMock.mockResolvedValue(undefined);
189
232
  readSnapshotMock.mockResolvedValue(null);
190
233
 
191
234
  const plugins: LexiconPlugin[] = [createMockPlugin({ name: "aws" })];
@@ -196,7 +239,7 @@ describe("runStateDiff --live", () => {
196
239
  serializers: plugins.map((p) => p.serializer),
197
240
  };
198
241
 
199
- const exit = await runStateDiff(ctx);
242
+ const exit = await runLifecycleDiff(ctx);
200
243
 
201
244
  expect(exit).toBe(0);
202
245
  const output = stdoutBuf.join("\n");
@@ -206,7 +249,7 @@ describe("runStateDiff --live", () => {
206
249
  });
207
250
  });
208
251
 
209
- describe("runStateSnapshot", () => {
252
+ describe("runLifecycleSnapshot", () => {
210
253
  let stdoutBuf: string[];
211
254
  let stderrBuf: string[];
212
255
 
@@ -227,7 +270,7 @@ describe("runStateSnapshot", () => {
227
270
  plugins: [],
228
271
  serializers: [],
229
272
  };
230
- const exit = await runStateSnapshot(ctx);
273
+ const exit = await runLifecycleSnapshot(ctx);
231
274
  expect(exit).toBe(1);
232
275
  expect(stderrBuf.join("\n")).toContain("Environment is required");
233
276
  });
@@ -238,7 +281,7 @@ describe("runStateSnapshot", () => {
238
281
  plugins: [],
239
282
  serializers: [],
240
283
  };
241
- const exit = await runStateSnapshot(ctx);
284
+ const exit = await runLifecycleSnapshot(ctx);
242
285
  expect(exit).toBe(1);
243
286
  expect(stderrBuf.join("\n")).toContain('Unknown environment "unknown"');
244
287
  });
@@ -251,7 +294,7 @@ describe("runStateSnapshot", () => {
251
294
  plugins,
252
295
  serializers: plugins.map((p) => p.serializer),
253
296
  };
254
- const exit = await runStateSnapshot(ctx);
297
+ const exit = await runLifecycleSnapshot(ctx);
255
298
  expect(exit).toBe(1);
256
299
  expect(stderrBuf.join("\n")).toContain("No plugins implement describeResources");
257
300
  });
@@ -275,14 +318,14 @@ describe("runStateSnapshot", () => {
275
318
  plugins,
276
319
  serializers: plugins.map((p) => p.serializer),
277
320
  };
278
- const exit = await runStateSnapshot(ctx);
321
+ const exit = await runLifecycleSnapshot(ctx);
279
322
  expect(exit).toBe(0);
280
323
  expect(stderrBuf.join("\n")).toContain("Snapshot saved");
281
324
  expect(takeSnapshotMock).toHaveBeenCalledTimes(1);
282
325
  });
283
326
  });
284
327
 
285
- describe("runStateShow", () => {
328
+ describe("runLifecycleShow", () => {
286
329
  let stdoutBuf: string[];
287
330
  let stderrBuf: string[];
288
331
 
@@ -291,7 +334,7 @@ describe("runStateShow", () => {
291
334
  stderrBuf = [];
292
335
  vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
293
336
  vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
294
- fetchStateMock.mockReset();
337
+ fetchLifecycleMock.mockReset();
295
338
  readSnapshotMock.mockReset();
296
339
  readEnvironmentSnapshotsMock.mockReset();
297
340
  });
@@ -301,12 +344,12 @@ describe("runStateShow", () => {
301
344
  args: makeArgs({ command: "state", path: "show" }),
302
345
  plugins: [], serializers: [],
303
346
  };
304
- expect(await runStateShow(ctx)).toBe(1);
347
+ expect(await runLifecycleShow(ctx)).toBe(1);
305
348
  expect(stderrBuf.join("\n")).toContain("Environment is required");
306
349
  });
307
350
 
308
351
  test("specific lexicon: prints snapshot table when found", async () => {
309
- fetchStateMock.mockResolvedValue(undefined);
352
+ fetchLifecycleMock.mockResolvedValue(undefined);
310
353
  readSnapshotMock.mockResolvedValue(JSON.stringify({
311
354
  lexicon: "aws", environment: "prod", commit: "x", timestamp: "t",
312
355
  resources: { bucket: { type: "AWS::S3::Bucket", physicalId: "b-1", status: "OK" } },
@@ -315,25 +358,25 @@ describe("runStateShow", () => {
315
358
  args: makeArgs({ command: "state", path: "show", extraPositional: "prod", extraPositional2: "aws" }),
316
359
  plugins: [], serializers: [],
317
360
  };
318
- expect(await runStateShow(ctx)).toBe(0);
361
+ expect(await runLifecycleShow(ctx)).toBe(0);
319
362
  const out = stdoutBuf.join("\n");
320
363
  expect(out).toContain("bucket");
321
364
  expect(out).toContain("AWS::S3::Bucket");
322
365
  });
323
366
 
324
367
  test("specific lexicon: returns 1 when no snapshot found", async () => {
325
- fetchStateMock.mockResolvedValue(undefined);
368
+ fetchLifecycleMock.mockResolvedValue(undefined);
326
369
  readSnapshotMock.mockResolvedValue(null);
327
370
  const ctx = {
328
371
  args: makeArgs({ command: "state", path: "show", extraPositional: "prod", extraPositional2: "aws" }),
329
372
  plugins: [], serializers: [],
330
373
  };
331
- expect(await runStateShow(ctx)).toBe(1);
374
+ expect(await runLifecycleShow(ctx)).toBe(1);
332
375
  expect(stderrBuf.join("\n")).toContain("No snapshot found");
333
376
  });
334
377
 
335
378
  test("no lexicon: lists all lexicons in env", async () => {
336
- fetchStateMock.mockResolvedValue(undefined);
379
+ fetchLifecycleMock.mockResolvedValue(undefined);
337
380
  readEnvironmentSnapshotsMock.mockResolvedValue(new Map([
338
381
  ["aws", JSON.stringify({ lexicon: "aws", environment: "prod", commit: "x", timestamp: "t", resources: {} })],
339
382
  ["gcp", JSON.stringify({ lexicon: "gcp", environment: "prod", commit: "x", timestamp: "t", resources: {} })],
@@ -342,14 +385,14 @@ describe("runStateShow", () => {
342
385
  args: makeArgs({ command: "state", path: "show", extraPositional: "prod" }),
343
386
  plugins: [], serializers: [],
344
387
  };
345
- expect(await runStateShow(ctx)).toBe(0);
388
+ expect(await runLifecycleShow(ctx)).toBe(0);
346
389
  const out = stdoutBuf.join("\n");
347
390
  expect(out).toContain("prod/aws");
348
391
  expect(out).toContain("prod/gcp");
349
392
  });
350
393
  });
351
394
 
352
- describe("runStateLog", () => {
395
+ describe("runLifecycleLog", () => {
353
396
  let stdoutBuf: string[];
354
397
  let stderrBuf: string[];
355
398
 
@@ -358,23 +401,23 @@ describe("runStateLog", () => {
358
401
  stderrBuf = [];
359
402
  vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
360
403
  vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
361
- fetchStateMock.mockReset();
404
+ fetchLifecycleMock.mockReset();
362
405
  listSnapshotsMock.mockReset();
363
406
  });
364
407
 
365
408
  test("returns 1 with message when no entries exist", async () => {
366
- fetchStateMock.mockResolvedValue(undefined);
409
+ fetchLifecycleMock.mockResolvedValue(undefined);
367
410
  listSnapshotsMock.mockResolvedValue([]);
368
411
  const ctx = {
369
412
  args: makeArgs({ command: "state", path: "log" }),
370
413
  plugins: [], serializers: [],
371
414
  };
372
- expect(await runStateLog(ctx)).toBe(1);
415
+ expect(await runLifecycleLog(ctx)).toBe(1);
373
416
  expect(stderrBuf.join("\n")).toContain("No state snapshots");
374
417
  });
375
418
 
376
419
  test("prints commit / date / message rows for each entry", async () => {
377
- fetchStateMock.mockResolvedValue(undefined);
420
+ fetchLifecycleMock.mockResolvedValue(undefined);
378
421
  listSnapshotsMock.mockResolvedValue([
379
422
  { commit: "abcdef1234567890", date: "2026-05-01T00:00:00Z", message: "Snapshot prod" },
380
423
  { commit: "fedcba9876543210", date: "2026-05-02T00:00:00Z", message: "Snapshot staging" },
@@ -383,7 +426,7 @@ describe("runStateLog", () => {
383
426
  args: makeArgs({ command: "state", path: "log" }),
384
427
  plugins: [], serializers: [],
385
428
  };
386
- expect(await runStateLog(ctx)).toBe(0);
429
+ expect(await runLifecycleLog(ctx)).toBe(0);
387
430
  const out = stdoutBuf.join("\n");
388
431
  expect(out).toContain("abcdef1");
389
432
  expect(out).toContain("Snapshot prod");
@@ -391,7 +434,7 @@ describe("runStateLog", () => {
391
434
  });
392
435
  });
393
436
 
394
- describe("runStateUnknown", () => {
437
+ describe("runLifecycleUnknown", () => {
395
438
  test("returns 1 with subcommand list", async () => {
396
439
  const stderrBuf: string[] = [];
397
440
  vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
@@ -399,7 +442,7 @@ describe("runStateUnknown", () => {
399
442
  args: makeArgs({ command: "state", path: "garbage" }),
400
443
  plugins: [], serializers: [],
401
444
  };
402
- expect(await runStateUnknown(ctx)).toBe(1);
445
+ expect(await runLifecycleUnknown(ctx)).toBe(1);
403
446
  const stderr = stderrBuf.join("\n");
404
447
  expect(stderr).toContain("snapshot");
405
448
  expect(stderr).toContain("show");