@intentius/chant 0.0.5 → 0.0.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.
Files changed (38) hide show
  1. package/bin/chant +20 -0
  2. package/package.json +18 -17
  3. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
  4. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
  5. package/src/cli/commands/build.ts +1 -2
  6. package/src/cli/commands/import.ts +2 -2
  7. package/src/cli/commands/init-lexicon.test.ts +0 -3
  8. package/src/cli/commands/init-lexicon.ts +1 -79
  9. package/src/cli/commands/init.ts +14 -3
  10. package/src/cli/commands/list.ts +2 -2
  11. package/src/cli/commands/update.ts +5 -3
  12. package/src/cli/conflict-check.test.ts +0 -1
  13. package/src/cli/handlers/dev.ts +1 -9
  14. package/src/cli/main.ts +13 -3
  15. package/src/cli/mcp/server.test.ts +207 -4
  16. package/src/cli/mcp/server.ts +6 -0
  17. package/src/cli/mcp/tools/explain.ts +134 -0
  18. package/src/cli/mcp/tools/scaffold.ts +107 -0
  19. package/src/cli/mcp/tools/search.ts +98 -0
  20. package/src/codegen/generate-registry.test.ts +1 -1
  21. package/src/codegen/generate-registry.ts +2 -3
  22. package/src/codegen/generate-typescript.test.ts +6 -6
  23. package/src/codegen/generate-typescript.ts +2 -6
  24. package/src/codegen/generate.ts +1 -12
  25. package/src/codegen/typecheck.ts +6 -11
  26. package/src/config.ts +4 -0
  27. package/src/index.ts +1 -1
  28. package/src/lexicon-integrity.ts +5 -4
  29. package/src/lexicon.ts +2 -6
  30. package/src/lint/config.ts +8 -6
  31. package/src/runtime-adapter.ts +158 -0
  32. package/src/serializer-walker.test.ts +0 -9
  33. package/src/serializer-walker.ts +1 -3
  34. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  35. package/src/codegen/case.test.ts +0 -30
  36. package/src/codegen/case.ts +0 -11
  37. package/src/codegen/rollback.test.ts +0 -92
  38. package/src/codegen/rollback.ts +0 -115
@@ -51,7 +51,7 @@ export function writeResourceClass(
51
51
  const attrs = [...attributes].sort((a, b) => a.name.localeCompare(b.name));
52
52
  for (const a of attrs) {
53
53
  const attrType = resolveConstructorType(a.type, remap);
54
- lines.push(` readonly ${toCamelCase(a.name)}: ${attrType};`);
54
+ lines.push(` readonly ${a.name}: ${attrType};`);
55
55
  }
56
56
 
57
57
  lines.push("}");
@@ -98,7 +98,7 @@ export function writeConstructor(
98
98
  if (p.description) {
99
99
  lines.push(` /** ${p.description} */`);
100
100
  }
101
- lines.push(` ${toCamelCase(p.name)}${optional}: ${tsType};`);
101
+ lines.push(` ${p.name}${optional}: ${tsType};`);
102
102
  }
103
103
  lines.push(" });");
104
104
  }
@@ -155,7 +155,3 @@ export function resolveConstructorType(tsType: string, remap: Map<string, string
155
155
 
156
156
  return tsType;
157
157
  }
158
-
159
- function toCamelCase(name: string): string {
160
- return name.charAt(0).toLowerCase() + name.slice(1);
161
- }
@@ -180,26 +180,15 @@ export interface WriteConfig {
180
180
  generatedSubdir?: string;
181
181
  /** Map of filename → content to write. */
182
182
  files: Record<string, string>;
183
- /** Optional snapshot function called before overwriting. */
184
- snapshot?: (generatedDir: string) => void;
185
183
  }
186
184
 
187
185
  /**
188
- * Write generated artifacts to disk with optional auto-snapshot.
186
+ * Write generated artifacts to disk.
189
187
  */
190
188
  export function writeGeneratedArtifacts(config: WriteConfig): void {
191
189
  const generatedDir = join(config.baseDir, config.generatedSubdir ?? "src/generated");
192
190
  mkdirSync(generatedDir, { recursive: true });
193
191
 
194
- // Auto-snapshot before overwriting
195
- if (config.snapshot) {
196
- try {
197
- config.snapshot(generatedDir);
198
- } catch {
199
- // Best effort — don't fail generation if snapshot fails
200
- }
201
- }
202
-
203
192
  for (const [filename, content] of Object.entries(config.files)) {
204
193
  writeFileSync(join(generatedDir, filename), content);
205
194
  }
@@ -4,6 +4,7 @@
4
4
  import { writeFileSync, mkdirSync, rmSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { tmpdir } from "os";
7
+ import { getRuntime } from "../runtime-adapter";
7
8
 
8
9
  export interface TypeCheckResult {
9
10
  ok: boolean;
@@ -39,17 +40,11 @@ export async function typecheckDTS(content: string): Promise<TypeCheckResult> {
39
40
  writeFileSync(join(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
40
41
 
41
42
  // Run tsc
42
- const proc = Bun.spawn(["bunx", "tsc", "--noEmit", "--project", "tsconfig.json"], {
43
- cwd: dir,
44
- stdout: "pipe",
45
- stderr: "pipe",
46
- });
47
-
48
- const [stdout, stderr] = await Promise.all([
49
- new Response(proc.stdout).text(),
50
- new Response(proc.stderr).text(),
51
- ]);
52
- const exitCode = await proc.exited;
43
+ const rt = getRuntime();
44
+ const { stdout, stderr, exitCode } = await rt.spawn(
45
+ [rt.commands.exec, "tsc", "--noEmit", "--project", "tsconfig.json"],
46
+ { cwd: dir },
47
+ );
53
48
 
54
49
  // Parse diagnostics from stdout (tsc writes errors to stdout)
55
50
  const output = stdout + stderr;
package/src/config.ts CHANGED
@@ -7,6 +7,7 @@ import type { LintConfig } from "./lint/config";
7
7
  * Zod schema for ChantConfig validation.
8
8
  */
9
9
  export const ChantConfigSchema = z.object({
10
+ runtime: z.enum(["bun", "node"]).optional(),
10
11
  lexicons: z.array(z.string().min(1)).optional(),
11
12
  lint: z.record(z.string(), z.unknown()).optional(),
12
13
  }).passthrough();
@@ -17,6 +18,9 @@ export const ChantConfigSchema = z.object({
17
18
  * Loaded from `chant.config.ts` (preferred) or `chant.config.json`.
18
19
  */
19
20
  export interface ChantConfig {
21
+ /** JS runtime to use for spawned commands: "bun" (default) or "node" */
22
+ runtime?: "bun" | "node";
23
+
20
24
  /** Lexicon package names to load (e.g. ["aws"]) */
21
25
  lexicons?: string[];
22
26
 
package/src/index.ts CHANGED
@@ -47,11 +47,11 @@ export * from "./codegen/fetch";
47
47
  export * from "./codegen/generate";
48
48
  export * from "./codegen/package";
49
49
  export * from "./codegen/typecheck";
50
- export * from "./codegen/rollback";
51
50
  export * from "./codegen/coverage";
52
51
  export * from "./codegen/validate";
53
52
  export * from "./codegen/docs";
54
53
  export * from "./runtime";
54
+ export * from "./runtime-adapter";
55
55
  export * from "./stack-output";
56
56
  export * from "./child-project";
57
57
  export * from "./lsp/types";
@@ -2,18 +2,19 @@
2
2
  * Content hashing and integrity verification for lexicon artifacts.
3
3
  */
4
4
  import type { BundleSpec } from "./lexicon";
5
+ import { getRuntime } from "./runtime-adapter";
5
6
 
6
7
  export interface ArtifactIntegrity {
7
- algorithm: "xxhash64";
8
+ algorithm: string;
8
9
  artifacts: Record<string, string>;
9
10
  composite: string;
10
11
  }
11
12
 
12
13
  /**
13
- * Hash a single artifact's content using xxhash64.
14
+ * Hash a single artifact's content using the runtime's hash algorithm.
14
15
  */
15
16
  export function hashArtifact(content: string): string {
16
- return Bun.hash(content).toString(16);
17
+ return getRuntime().hash(content);
17
18
  }
18
19
 
19
20
  /**
@@ -39,7 +40,7 @@ export function computeIntegrity(spec: BundleSpec): ArtifactIntegrity {
39
40
  const compositeInput = sorted.map(([k, v]) => `${k}:${v}`).join("\n");
40
41
  const composite = hashArtifact(compositeInput);
41
42
 
42
- return { algorithm: "xxhash64", artifacts, composite };
43
+ return { algorithm: getRuntime().hashAlgorithm, artifacts, composite };
43
44
  }
44
45
 
45
46
  /**
package/src/lexicon.ts CHANGED
@@ -101,7 +101,7 @@ export interface IntrinsicDef {
101
101
  * Plugin interface for lexicon packages.
102
102
  *
103
103
  * Required lifecycle methods enforce consistency: every lexicon must support
104
- * generate, validate, coverage, package, and rollback operations.
104
+ * generate, validate, coverage, and package operations.
105
105
  */
106
106
  export interface LexiconPlugin {
107
107
  // ── Required ──────────────────────────────────────────────
@@ -123,9 +123,6 @@ export interface LexiconPlugin {
123
123
  /** Package lexicon into distributable tarball */
124
124
  package(options?: { verbose?: boolean; force?: boolean }): Promise<void>;
125
125
 
126
- /** List or restore generation snapshots */
127
- rollback(options?: { restore?: string; verbose?: boolean }): Promise<void>;
128
-
129
126
  // ── Optional extensions ───────────────────────────────────
130
127
  /** Return lint rules provided by this lexicon */
131
128
  lintRules?(): LintRule[];
@@ -206,7 +203,6 @@ export function isLexiconPlugin(value: unknown): value is LexiconPlugin {
206
203
  typeof obj.generate === "function" &&
207
204
  typeof obj.validate === "function" &&
208
205
  typeof obj.coverage === "function" &&
209
- typeof obj.package === "function" &&
210
- typeof obj.rollback === "function"
206
+ typeof obj.package === "function"
211
207
  );
212
208
  }
@@ -1,13 +1,15 @@
1
1
  import { readFileSync, existsSync } from "fs";
2
2
  import { join, dirname, resolve } from "path";
3
+ import { createRequire } from "module";
3
4
  import { z } from "zod";
4
5
  import type { Severity, RuleConfig } from "./rule";
6
+ import { moduleDir, getRuntime } from "../runtime-adapter";
5
7
  import strictPreset from "./presets/strict.json";
6
8
 
7
9
  /** Mapping of built-in preset names to their file paths */
8
10
  const BUILTIN_PRESETS: Record<string, string> = {
9
- "@intentius/chant/lint/presets/strict": resolve(import.meta.dir, "presets/strict.json"),
10
- "@intentius/chant/lint/presets/relaxed": resolve(import.meta.dir, "presets/relaxed.json"),
11
+ "@intentius/chant/lint/presets/strict": resolve(moduleDir(import.meta.url), "presets/strict.json"),
12
+ "@intentius/chant/lint/presets/relaxed": resolve(moduleDir(import.meta.url), "presets/relaxed.json"),
11
13
  };
12
14
 
13
15
  // ── Zod schemas for lint config validation ─────────────────────────
@@ -307,11 +309,12 @@ function loadConfigFile(configPath: string, visited: Set<string> = new Set()): L
307
309
  * @returns Loaded and merged configuration, or default config if not found
308
310
  */
309
311
  export function loadConfig(dir: string): LintConfig {
310
- // Try chant.config.ts first — Bun supports synchronous require() for .ts
312
+ // Try chant.config.ts first — Bun has native require() for .ts, Node uses tsx's loader
311
313
  const tsConfigPath = join(dir, "chant.config.ts");
312
314
  if (existsSync(tsConfigPath)) {
313
315
  try {
314
- const mod = require(tsConfigPath);
316
+ const _require = createRequire(join(dir, "package.json"));
317
+ const mod = _require(tsConfigPath);
315
318
  const config = mod.default ?? mod.config ?? mod;
316
319
  if (typeof config === "object" && config !== null) {
317
320
  // ChantConfig format: extract lint property
@@ -362,8 +365,7 @@ export function resolveRulesForFile(config: LintConfig, filePath: string): Recor
362
365
 
363
366
  for (const override of config.overrides) {
364
367
  const matches = override.files.some((pattern) => {
365
- const glob = new Bun.Glob(pattern);
366
- return glob.match(filePath);
368
+ return getRuntime().globMatch(pattern, filePath);
367
369
  });
368
370
 
369
371
  if (matches) {
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Runtime adapter — abstracts Bun-specific APIs so chant can run on Bun or Node.js.
3
+ *
4
+ * Auto-detects the host runtime and delegates to the appropriate implementation.
5
+ * The `target` parameter (from config) controls what gets spawned (bun vs node/npx/npm),
6
+ * not which adapter class is used.
7
+ */
8
+ import { dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { createHash } from "crypto";
11
+ import { execFile } from "child_process";
12
+ // @ts-ignore — picomatch has no types declaration
13
+ import picomatch from "picomatch";
14
+
15
+ export interface SpawnResult {
16
+ stdout: string;
17
+ stderr: string;
18
+ exitCode: number;
19
+ }
20
+
21
+ export interface RuntimeCommands {
22
+ /** Runtime binary: "bun" | "node" */
23
+ runner: string;
24
+ /** Package executor: "bunx" | "npx" */
25
+ exec: string;
26
+ /** Pack command: ["bun", "pm", "pack"] | ["npm", "pack"] */
27
+ packCmd: string[];
28
+ }
29
+
30
+ export interface RuntimeAdapter {
31
+ readonly name: "bun" | "node";
32
+ /** Hash content and return hex string */
33
+ hash(content: string): string;
34
+ /** Algorithm name recorded in integrity.json */
35
+ readonly hashAlgorithm: string;
36
+ /** Test whether filePath matches a glob pattern */
37
+ globMatch(pattern: string, filePath: string): boolean;
38
+ /** Spawn a child process and collect output */
39
+ spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult>;
40
+ /** Commands to use when spawning package manager / executor */
41
+ readonly commands: RuntimeCommands;
42
+ }
43
+
44
+ // ── Bun adapter ──────────────────────────────────────────────────
45
+
46
+ class BunRuntimeAdapter implements RuntimeAdapter {
47
+ readonly name = "bun" as const;
48
+ readonly hashAlgorithm = "xxhash64";
49
+ readonly commands: RuntimeCommands;
50
+
51
+ constructor(target: "bun" | "node") {
52
+ this.commands =
53
+ target === "node"
54
+ ? { runner: "node", exec: "npx", packCmd: ["npm", "pack"] }
55
+ : { runner: "bun", exec: "bunx", packCmd: ["bun", "pm", "pack"] };
56
+ }
57
+
58
+ hash(content: string): string {
59
+ return Bun.hash(content).toString(16);
60
+ }
61
+
62
+ globMatch(pattern: string, filePath: string): boolean {
63
+ const glob = new Bun.Glob(pattern);
64
+ return glob.match(filePath);
65
+ }
66
+
67
+ async spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult> {
68
+ const proc = Bun.spawn(cmd, {
69
+ cwd: opts?.cwd,
70
+ stdout: "pipe",
71
+ stderr: "pipe",
72
+ });
73
+ const [stdout, stderr] = await Promise.all([
74
+ new Response(proc.stdout).text(),
75
+ new Response(proc.stderr).text(),
76
+ ]);
77
+ const exitCode = await proc.exited;
78
+ return { stdout, stderr, exitCode };
79
+ }
80
+ }
81
+
82
+ // ── Node adapter ─────────────────────────────────────────────────
83
+
84
+ class NodeRuntimeAdapter implements RuntimeAdapter {
85
+ readonly name = "node" as const;
86
+ readonly hashAlgorithm = "sha256";
87
+ readonly commands: RuntimeCommands;
88
+
89
+ constructor(target: "bun" | "node") {
90
+ this.commands =
91
+ target === "bun"
92
+ ? { runner: "bun", exec: "bunx", packCmd: ["bun", "pm", "pack"] }
93
+ : { runner: "node", exec: "npx", packCmd: ["npm", "pack"] };
94
+ }
95
+
96
+ hash(content: string): string {
97
+ return createHash("sha256").update(content).digest("hex");
98
+ }
99
+
100
+ globMatch(pattern: string, filePath: string): boolean {
101
+ return picomatch(pattern)(filePath);
102
+ }
103
+
104
+ async spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult> {
105
+ return new Promise((resolve) => {
106
+ execFile(cmd[0], cmd.slice(1), { cwd: opts?.cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
107
+ resolve({
108
+ stdout: stdout ?? "",
109
+ stderr: stderr ?? "",
110
+ exitCode: err ? (err as any).code ?? 1 : 0,
111
+ });
112
+ });
113
+ });
114
+ }
115
+ }
116
+
117
+ // ── Singleton ────────────────────────────────────────────────────
118
+
119
+ let _runtime: RuntimeAdapter | undefined;
120
+
121
+ /**
122
+ * Detect whether we're running under Bun.
123
+ */
124
+ function isBun(): boolean {
125
+ return typeof globalThis.Bun !== "undefined";
126
+ }
127
+
128
+ /**
129
+ * Initialize the runtime adapter singleton.
130
+ *
131
+ * @param target - Which commands to spawn ("bun" or "node"). Defaults to auto-detect.
132
+ * Controls the `commands` property, not which adapter class is used.
133
+ */
134
+ export function initRuntime(target?: "bun" | "node"): RuntimeAdapter {
135
+ const resolvedTarget = target ?? (isBun() ? "bun" : "node");
136
+ _runtime = isBun()
137
+ ? new BunRuntimeAdapter(resolvedTarget)
138
+ : new NodeRuntimeAdapter(resolvedTarget);
139
+ return _runtime;
140
+ }
141
+
142
+ /**
143
+ * Get the runtime adapter. Lazily initializes with auto-detection if not yet set.
144
+ */
145
+ export function getRuntime(): RuntimeAdapter {
146
+ if (!_runtime) {
147
+ return initRuntime();
148
+ }
149
+ return _runtime;
150
+ }
151
+
152
+ /**
153
+ * Convert `import.meta.url` to a directory path.
154
+ * Works on both Bun and Node (replaces Bun-only `import.meta.dir`).
155
+ */
156
+ export function moduleDir(importMetaUrl: string): string {
157
+ return dirname(fileURLToPath(importMetaUrl));
158
+ }
@@ -93,15 +93,6 @@ describe("walkValue", () => {
93
93
  expect(walkValue({ a: 1, b: { c: 2 } }, names, mockVisitor)).toEqual({ a: 1, b: { c: 2 } });
94
94
  });
95
95
 
96
- test("applies transformKey when provided", () => {
97
- const visitor: SerializerVisitor = {
98
- ...mockVisitor,
99
- transformKey: (k) => k.toUpperCase(),
100
- };
101
- const names = new Map<Declarable, string>();
102
- expect(walkValue({ foo: 1, bar: 2 }, names, visitor)).toEqual({ FOO: 1, BAR: 2 });
103
- });
104
-
105
96
  test("complex nested structure", () => {
106
97
  const resource = makeDeclarable("Test::Role");
107
98
  const ref = new AttrRef(resource, "arn");
@@ -17,8 +17,6 @@ export interface SerializerVisitor {
17
17
  resourceRef(logicalName: string): unknown;
18
18
  /** Format a property-level Declarable by walking its props. */
19
19
  propertyDeclarable(entity: Declarable, walk: (v: unknown) => unknown): unknown;
20
- /** Optional key transformation (e.g. camelCase → PascalCase). */
21
- transformKey?(key: string): string;
22
20
  }
23
21
 
24
22
  /**
@@ -73,7 +71,7 @@ export function walkValue(
73
71
  if (typeof value === "object") {
74
72
  const result: Record<string, unknown> = {};
75
73
  for (const [key, val] of Object.entries(value)) {
76
- const outKey = visitor.transformKey ? visitor.transformKey(key) : key;
74
+ const outKey = key;
77
75
  result[outKey] = walkValue(val, entityNames, visitor);
78
76
  }
79
77
  return result;
@@ -1,45 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, cpSync } from "fs";
2
- import { join, basename } from "path";
3
-
4
- export interface Snapshot {
5
- timestamp: string;
6
- resources: number;
7
- path: string;
8
- }
9
-
10
- /**
11
- * List available generation snapshots.
12
- */
13
- export function listSnapshots(snapshotsDir: string): Snapshot[] {
14
- if (!existsSync(snapshotsDir)) return [];
15
-
16
- return readdirSync(snapshotsDir)
17
- .filter((d) => !d.startsWith("."))
18
- .sort()
19
- .reverse()
20
- .map((dir) => {
21
- const fullPath = join(snapshotsDir, dir);
22
- const metaPath = join(fullPath, "meta.json");
23
- let resources = 0;
24
- if (existsSync(metaPath)) {
25
- try {
26
- const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
27
- resources = meta.resources ?? 0;
28
- } catch {}
29
- }
30
- return { timestamp: dir, resources, path: fullPath };
31
- });
32
- }
33
-
34
- /**
35
- * Restore a snapshot to the generated directory.
36
- */
37
- export function restoreSnapshot(timestamp: string, generatedDir: string): void {
38
- const snapshotsDir = join(generatedDir, "..", "..", ".snapshots");
39
- const snapshotDir = join(snapshotsDir, timestamp);
40
- if (!existsSync(snapshotDir)) {
41
- throw new Error(`Snapshot not found: ${timestamp}`);
42
- }
43
- mkdirSync(generatedDir, { recursive: true });
44
- cpSync(snapshotDir, generatedDir, { recursive: true });
45
- }
@@ -1,30 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { toCamelCase, toPascalCase } from "./case";
3
-
4
- describe("toCamelCase", () => {
5
- test("lowercases first character", () => {
6
- expect(toCamelCase("BucketName")).toBe("bucketName");
7
- });
8
-
9
- test("preserves already-camelCase", () => {
10
- expect(toCamelCase("already")).toBe("already");
11
- });
12
-
13
- test("handles single character", () => {
14
- expect(toCamelCase("A")).toBe("a");
15
- });
16
- });
17
-
18
- describe("toPascalCase", () => {
19
- test("uppercases first character", () => {
20
- expect(toPascalCase("bucketName")).toBe("BucketName");
21
- });
22
-
23
- test("preserves already-PascalCase", () => {
24
- expect(toPascalCase("Already")).toBe("Already");
25
- });
26
-
27
- test("handles single character", () => {
28
- expect(toPascalCase("a")).toBe("A");
29
- });
30
- });
@@ -1,11 +0,0 @@
1
- /**
2
- * Case conversion utilities for codegen.
3
- */
4
-
5
- export function toCamelCase(name: string): string {
6
- return name.charAt(0).toLowerCase() + name.slice(1);
7
- }
8
-
9
- export function toPascalCase(name: string): string {
10
- return name.charAt(0).toUpperCase() + name.slice(1);
11
- }
@@ -1,92 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { mkdirSync, writeFileSync, rmSync, readFileSync } from "fs";
3
- import { join } from "path";
4
- import { tmpdir } from "os";
5
- import { snapshotArtifacts, saveSnapshot, restoreSnapshot, listSnapshots } from "./rollback";
6
-
7
- function makeTempDir(): string {
8
- const dir = join(tmpdir(), `chant-rollback-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
9
- mkdirSync(dir, { recursive: true });
10
- return dir;
11
- }
12
-
13
- describe("rollback", () => {
14
- test("snapshot captures generated files with default artifact names", () => {
15
- const dir = makeTempDir();
16
- const genDir = join(dir, "generated");
17
- mkdirSync(genDir, { recursive: true });
18
-
19
- writeFileSync(join(genDir, "lexicon.json"), '{"Bucket":{"kind":"resource"}}');
20
- writeFileSync(join(genDir, "index.d.ts"), "declare class Bucket {}");
21
- writeFileSync(join(genDir, "index.ts"), "export const Bucket = {};");
22
-
23
- const snapshot = snapshotArtifacts(genDir);
24
- expect(snapshot.files["lexicon.json"]).toBeDefined();
25
- expect(snapshot.files["index.d.ts"]).toBeDefined();
26
- expect(snapshot.files["index.ts"]).toBeDefined();
27
- expect(snapshot.hashes["lexicon.json"]).toBeDefined();
28
- expect(snapshot.resourceCount).toBe(1);
29
-
30
- rmSync(dir, { recursive: true, force: true });
31
- });
32
-
33
- test("snapshot uses custom artifact names", () => {
34
- const dir = makeTempDir();
35
- const genDir = join(dir, "generated");
36
- mkdirSync(genDir, { recursive: true });
37
-
38
- writeFileSync(join(genDir, "my-lexicon.json"), '{"Resource":{"kind":"resource"}}');
39
- writeFileSync(join(genDir, "types.d.ts"), "declare class Resource {}");
40
-
41
- const snapshot = snapshotArtifacts(genDir, ["my-lexicon.json", "types.d.ts"]);
42
- expect(snapshot.files["my-lexicon.json"]).toBeDefined();
43
- expect(snapshot.files["types.d.ts"]).toBeDefined();
44
- expect(snapshot.resourceCount).toBe(1);
45
-
46
- rmSync(dir, { recursive: true, force: true });
47
- });
48
-
49
- test("save and list snapshots", () => {
50
- const dir = makeTempDir();
51
- const snapshotsDir = join(dir, ".snapshots");
52
-
53
- const snapshot = {
54
- timestamp: "2025-01-01T00:00:00.000Z",
55
- files: { "test.json": "{}" },
56
- hashes: { "test.json": "abc123" },
57
- resourceCount: 0,
58
- };
59
-
60
- saveSnapshot(snapshot, snapshotsDir);
61
- const list = listSnapshots(snapshotsDir);
62
- expect(list.length).toBe(1);
63
- expect(list[0].timestamp).toBe("2025-01-01T00:00:00.000Z");
64
-
65
- rmSync(dir, { recursive: true, force: true });
66
- });
67
-
68
- test("restore snapshot overwrites generated files", () => {
69
- const dir = makeTempDir();
70
- const genDir = join(dir, "generated");
71
- const snapshotsDir = join(dir, ".snapshots");
72
- mkdirSync(genDir, { recursive: true });
73
-
74
- writeFileSync(join(genDir, "lexicon.json"), '{"original":true}');
75
-
76
- const snapshot = snapshotArtifacts(genDir);
77
- const snapshotPath = saveSnapshot(snapshot, snapshotsDir);
78
-
79
- writeFileSync(join(genDir, "lexicon.json"), '{"modified":true}');
80
- expect(readFileSync(join(genDir, "lexicon.json"), "utf-8")).toBe('{"modified":true}');
81
-
82
- restoreSnapshot(snapshotPath, genDir);
83
- expect(readFileSync(join(genDir, "lexicon.json"), "utf-8")).toBe('{"original":true}');
84
-
85
- rmSync(dir, { recursive: true, force: true });
86
- });
87
-
88
- test("listSnapshots returns empty for nonexistent dir", () => {
89
- const list = listSnapshots("/nonexistent/path");
90
- expect(list).toHaveLength(0);
91
- });
92
- });