@intentius/chant 0.44.14 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/audit/discover.d.ts +26 -2
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-model.d.ts +10 -0
  4. package/dist/audit/report-model.d.ts.map +1 -1
  5. package/dist/audit/rules-doc.d.ts.map +1 -1
  6. package/dist/cli/build-params-cli.d.ts +16 -7
  7. package/dist/cli/build-params-cli.d.ts.map +1 -1
  8. package/dist/cli/commands/audit.d.ts +20 -0
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/build.d.ts +9 -0
  11. package/dist/cli/commands/build.d.ts.map +1 -1
  12. package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
  13. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
  15. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  16. package/dist/cli/commands/check-lexicon.d.ts +14 -0
  17. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  18. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  19. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
  20. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  21. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  22. package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
  23. package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
  24. package/dist/cli/commands/lint.d.ts.map +1 -1
  25. package/dist/cli/commands/onboard.d.ts +28 -0
  26. package/dist/cli/commands/onboard.d.ts.map +1 -1
  27. package/dist/cli/handlers/build.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.d.ts.map +1 -1
  29. package/dist/cli/handlers/lifecycle.d.ts +13 -0
  30. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts.map +1 -1
  32. package/dist/cli/handlers/search.d.ts +27 -0
  33. package/dist/cli/handlers/search.d.ts.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/registry.d.ts +7 -0
  36. package/dist/cli/registry.d.ts.map +1 -1
  37. package/dist/codegen/docs-pages.d.ts +39 -0
  38. package/dist/codegen/docs-pages.d.ts.map +1 -0
  39. package/dist/codegen/docs-sections.d.ts.map +1 -1
  40. package/dist/codegen/docs-sidebar.d.ts +13 -2
  41. package/dist/codegen/docs-sidebar.d.ts.map +1 -1
  42. package/dist/codegen/docs-types.d.ts +30 -10
  43. package/dist/codegen/docs-types.d.ts.map +1 -1
  44. package/dist/codegen/docs.d.ts +14 -2
  45. package/dist/codegen/docs.d.ts.map +1 -1
  46. package/dist/codegen/lexicon-regen.d.ts +11 -0
  47. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  48. package/dist/codegen/surface-snapshot.d.ts +27 -3
  49. package/dist/codegen/surface-snapshot.d.ts.map +1 -1
  50. package/dist/codegen/validate.d.ts +21 -3
  51. package/dist/codegen/validate.d.ts.map +1 -1
  52. package/dist/components/capability-plugin.d.ts +13 -2
  53. package/dist/components/capability-plugin.d.ts.map +1 -1
  54. package/dist/components/driver.d.ts +38 -0
  55. package/dist/components/driver.d.ts.map +1 -1
  56. package/dist/components/index.d.ts +1 -1
  57. package/dist/components/index.d.ts.map +1 -1
  58. package/dist/components/starter-plugin.d.ts.map +1 -1
  59. package/dist/config.d.ts +85 -5
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/discovery/fold-import.d.ts.map +1 -1
  62. package/dist/env.d.ts +12 -1
  63. package/dist/env.d.ts.map +1 -1
  64. package/dist/fold/fold.d.ts +100 -1
  65. package/dist/fold/fold.d.ts.map +1 -1
  66. package/dist/graph-ir.d.ts +12 -0
  67. package/dist/graph-ir.d.ts.map +1 -1
  68. package/dist/graph-ops.d.ts +17 -0
  69. package/dist/graph-ops.d.ts.map +1 -0
  70. package/dist/lexicon.d.ts +182 -2
  71. package/dist/lexicon.d.ts.map +1 -1
  72. package/dist/lifecycle/change-set.d.ts +23 -2
  73. package/dist/lifecycle/change-set.d.ts.map +1 -1
  74. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  75. package/dist/lifecycle/index.d.ts +1 -0
  76. package/dist/lifecycle/index.d.ts.map +1 -1
  77. package/dist/lifecycle/observe.d.ts +8 -0
  78. package/dist/lifecycle/observe.d.ts.map +1 -1
  79. package/dist/lifecycle/replay.d.ts.map +1 -1
  80. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  81. package/dist/lifecycle/teardown.d.ts +130 -0
  82. package/dist/lifecycle/teardown.d.ts.map +1 -0
  83. package/dist/lifecycle/types.d.ts +7 -0
  84. package/dist/lifecycle/types.d.ts.map +1 -1
  85. package/dist/lint/engine.d.ts +6 -2
  86. package/dist/lint/engine.d.ts.map +1 -1
  87. package/dist/lint/policy.d.ts.map +1 -1
  88. package/dist/lint/rule.d.ts +31 -0
  89. package/dist/lint/rule.d.ts.map +1 -1
  90. package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
  91. package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
  92. package/dist/lint/rules/index.d.ts +2 -1
  93. package/dist/lint/rules/index.d.ts.map +1 -1
  94. package/dist/managed-fields.d.ts +33 -21
  95. package/dist/managed-fields.d.ts.map +1 -1
  96. package/dist/observation.d.ts +23 -1
  97. package/dist/observation.d.ts.map +1 -1
  98. package/dist/op/builders.d.ts +36 -7
  99. package/dist/op/builders.d.ts.map +1 -1
  100. package/dist/op/index.d.ts +1 -1
  101. package/dist/op/index.d.ts.map +1 -1
  102. package/dist/testing.d.ts +136 -0
  103. package/dist/testing.d.ts.map +1 -0
  104. package/dist/yaml.d.ts +0 -8
  105. package/dist/yaml.d.ts.map +1 -1
  106. package/package.json +6 -1
  107. package/src/__snapshots__/okf.test.ts.snap +20 -0
  108. package/src/audit/discover.test.ts +55 -1
  109. package/src/audit/discover.ts +70 -2
  110. package/src/audit/edge-init-safety.test.ts +149 -18
  111. package/src/audit/report-model.ts +12 -1
  112. package/src/audit/rules-doc.ts +1 -0
  113. package/src/build-params.test.ts +52 -0
  114. package/src/cli/build-params-cli.test.ts +25 -8
  115. package/src/cli/build-params-cli.ts +37 -23
  116. package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
  117. package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
  118. package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
  119. package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
  120. package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
  121. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
  122. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
  123. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
  124. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
  125. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
  126. package/src/cli/commands/audit.test.ts +83 -1
  127. package/src/cli/commands/audit.ts +121 -28
  128. package/src/cli/commands/build.test.ts +275 -5
  129. package/src/cli/commands/build.ts +86 -14
  130. package/src/cli/commands/check-lexicon-docs.ts +30 -0
  131. package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
  132. package/src/cli/commands/check-lexicon-examples.ts +66 -17
  133. package/src/cli/commands/check-lexicon.test.ts +46 -2
  134. package/src/cli/commands/check-lexicon.ts +64 -4
  135. package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
  136. package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
  137. package/src/cli/commands/init-lexicon.test.ts +42 -1
  138. package/src/cli/commands/init-lexicon.ts +3 -1
  139. package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
  140. package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
  141. package/src/cli/commands/lexicon-surface-diff.ts +10 -1
  142. package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
  143. package/src/cli/commands/lint.ts +19 -6
  144. package/src/cli/commands/onboard.test.ts +319 -225
  145. package/src/cli/commands/onboard.ts +144 -58
  146. package/src/cli/handlers/build.test.ts +3 -3
  147. package/src/cli/handlers/build.ts +2 -0
  148. package/src/cli/handlers/components.ts +2 -2
  149. package/src/cli/handlers/graph.test.ts +44 -0
  150. package/src/cli/handlers/graph.ts +24 -4
  151. package/src/cli/handlers/lifecycle.test.ts +348 -1
  152. package/src/cli/handlers/lifecycle.ts +235 -3
  153. package/src/cli/handlers/run.test.ts +5 -5
  154. package/src/cli/handlers/run.ts +1 -0
  155. package/src/cli/handlers/search-live.test.ts +217 -0
  156. package/src/cli/handlers/search.test.ts +41 -1
  157. package/src/cli/handlers/search.ts +60 -8
  158. package/src/cli/main.ts +17 -4
  159. package/src/cli/registry.ts +7 -0
  160. package/src/cli/security-boundary.test.ts +2 -1
  161. package/src/codegen/docs-pages.test.ts +194 -0
  162. package/src/codegen/docs-pages.ts +138 -0
  163. package/src/codegen/docs-sections.ts +0 -8
  164. package/src/codegen/docs-sidebar.ts +50 -37
  165. package/src/codegen/docs-types.ts +32 -4
  166. package/src/codegen/docs.ts +78 -40
  167. package/src/codegen/lexicon-regen.ts +22 -2
  168. package/src/codegen/rolling-upgrade.test.ts +1 -1
  169. package/src/codegen/rolling-upgrade.ts +1 -1
  170. package/src/codegen/surface-snapshot.test.ts +139 -0
  171. package/src/codegen/surface-snapshot.ts +297 -122
  172. package/src/codegen/validate.test.ts +49 -3
  173. package/src/codegen/validate.ts +36 -7
  174. package/src/components/auto-release.ts +1 -1
  175. package/src/components/capability-plugin.ts +23 -2
  176. package/src/components/driver.test.ts +45 -0
  177. package/src/components/driver.ts +64 -27
  178. package/src/components/index.ts +2 -0
  179. package/src/components/starter-plugin.ts +5 -2
  180. package/src/config.test.ts +40 -0
  181. package/src/config.ts +159 -8
  182. package/src/discovery/fold-import.test.ts +427 -2
  183. package/src/discovery/fold-import.ts +163 -7
  184. package/src/discovery/sandbox/driver.test.ts +11 -0
  185. package/src/env.test.ts +35 -1
  186. package/src/env.ts +17 -3
  187. package/src/fold/fold.test.ts +41 -0
  188. package/src/fold/fold.ts +372 -2
  189. package/src/graph-ir-live.test.ts +42 -0
  190. package/src/graph-ir.ts +42 -2
  191. package/src/graph-ops.test.ts +69 -0
  192. package/src/graph-ops.ts +42 -0
  193. package/src/lexicon.ts +182 -2
  194. package/src/lifecycle/change-set.test.ts +41 -0
  195. package/src/lifecycle/change-set.ts +30 -2
  196. package/src/lifecycle/deep-observe.ts +4 -0
  197. package/src/lifecycle/index.ts +1 -0
  198. package/src/lifecycle/observe.ts +29 -2
  199. package/src/lifecycle/replay.ts +3 -0
  200. package/src/lifecycle/snapshot.ts +3 -0
  201. package/src/lifecycle/teardown.test.ts +537 -0
  202. package/src/lifecycle/teardown.ts +357 -0
  203. package/src/lifecycle/types.ts +7 -0
  204. package/src/lint/engine.ts +7 -1
  205. package/src/lint/policy.ts +14 -3
  206. package/src/lint/rule.ts +23 -0
  207. package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
  208. package/src/lint/rules/cor021-env-literal-name.ts +114 -0
  209. package/src/lint/rules/index.ts +4 -1
  210. package/src/managed-fields.test.ts +15 -42
  211. package/src/managed-fields.ts +43 -26
  212. package/src/observation.test.ts +24 -8
  213. package/src/observation.ts +48 -4
  214. package/src/op/builders.ts +40 -7
  215. package/src/op/index.ts +1 -1
  216. package/src/ownership.test.ts +63 -1
  217. package/src/testing.test.ts +261 -0
  218. package/src/testing.ts +338 -0
  219. package/src/yaml.test.ts +15 -0
  220. package/src/yaml.ts +19 -2
@@ -2,15 +2,12 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
2
  import { mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { tmpdir } from "os";
5
-
6
- // We test the patching logic by creating minimal fixture files in a temp dir
7
- // and calling the internal functions. Since the functions use findRepoRoot()
8
- // (which resolves from import.meta.url), we test by directly importing the
9
- // module and exercising the exported onboardCommand after setting up a
10
- // fake repo structure.
11
-
12
- // Rather than mocking findRepoRoot, we test the observable behaviour via
13
- // the CLI entry point. The unit tests below validate file-patching logic.
5
+ import {
6
+ onboardCommand,
7
+ patchDockerfile,
8
+ patchRootTsconfigPaths,
9
+ insertPrepackAfterEach,
10
+ } from "./onboard";
14
11
 
15
12
  function makeTempDir(): string {
16
13
  const dir = join(tmpdir(), `chant-onboard-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
@@ -18,46 +15,29 @@ function makeTempDir(): string {
18
15
  return dir;
19
16
  }
20
17
 
21
- describe("onboard patching logic", () => {
22
- let root: string;
23
-
24
- beforeEach(() => {
25
- root = makeTempDir();
26
- // Create a minimal repo structure
27
- mkdirSync(join(root, ".github/workflows"), { recursive: true });
28
- mkdirSync(join(root, "test"), { recursive: true });
29
- });
30
-
31
- afterEach(() => {
32
- rmSync(root, { recursive: true, force: true });
33
- });
34
-
35
- // ── package.json ──────────────────────────────────────
36
-
37
- describe("root package.json", () => {
38
- test("adds workspace dependency", () => {
39
- const pkg = {
40
- workspaces: ["packages/*"],
41
- dependencies: {
42
- "@intentius/chant-lexicon-aws": "workspace:*",
43
- },
44
- };
45
- writeFileSync(join(root, "package.json"), JSON.stringify(pkg, null, 2));
46
-
47
- // Simulate the patch by reading+writing
48
- const content = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
49
- content.dependencies["@intentius/chant-lexicon-terraform"] = "workspace:*";
50
- writeFileSync(join(root, "package.json"), JSON.stringify(content, null, 2));
51
-
52
- const result = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
53
- expect(result.dependencies["@intentius/chant-lexicon-terraform"]).toBe("workspace:*");
54
- });
55
- });
56
-
57
- // ── chant.yml ──────────────────────────────────────────
58
-
59
- describe("chant.yml patching", () => {
60
- const ciContent = `name: chant
18
+ const rootPackageJson = {
19
+ workspaces: ["packages/*", "lexicons/*"],
20
+ dependencies: {
21
+ "@intentius/chant-lexicon-aws": "workspace:*",
22
+ "@intentius/chant-lexicon-k8s": "workspace:*",
23
+ },
24
+ };
25
+
26
+ const rootTsconfig = {
27
+ compilerOptions: {
28
+ moduleResolution: "node",
29
+ baseUrl: ".",
30
+ paths: {
31
+ "@intentius/chant": ["packages/core/src/index.ts"],
32
+ "@intentius/chant/*": ["packages/core/src/*"],
33
+ "@intentius/chant-lexicon-aws": ["lexicons/aws/src/index.ts"],
34
+ "@intentius/chant-lexicon-aws/*": ["lexicons/aws/src/*"],
35
+ },
36
+ },
37
+ include: ["packages/core/src/**/*.ts", "lexicons/*/src/**/*.ts", "lexicons/*/examples/*/src/**/*.ts"],
38
+ };
39
+
40
+ const ciContent = `name: chant
61
41
  on: [push]
62
42
  jobs:
63
43
  check:
@@ -92,163 +72,53 @@ jobs:
92
72
  run: npm run --prefix lexicons/k8s prepack
93
73
  `;
94
74
 
95
- test("inserts prepack line in multi-line blocks", () => {
96
- writeFileSync(join(root, ".github/workflows/chant.yml"), ciContent);
97
-
98
- // Use the same logic as the command
99
- const content = readFileSync(join(root, ".github/workflows/chant.yml"), "utf-8");
100
- const lines = content.split("\n");
101
-
102
- // Find contiguous groups and insert
103
- const groups: { start: number; end: number }[] = [];
104
- let groupStart = -1;
105
- for (let i = 0; i <= lines.length; i++) {
106
- const isPrepack =
107
- i < lines.length &&
108
- lines[i].includes("npm run --prefix lexicons/") &&
109
- lines[i].includes("prepack");
110
- if (isPrepack && groupStart === -1) {
111
- groupStart = i;
112
- } else if (!isPrepack && groupStart !== -1) {
113
- groups.push({ start: groupStart, end: i - 1 });
114
- groupStart = -1;
115
- }
116
- }
117
-
118
- // Should find 2 contiguous groups (check + test) and 3 standalone (validate)
119
- const multiLineGroups = groups.filter((g) => g.end > g.start);
120
- expect(multiLineGroups.length).toBe(2);
121
-
122
- // Insert into multi-line groups only
123
- const insertAfter = multiLineGroups.map((g) => g.end);
124
- for (const idx of insertAfter.reverse()) {
125
- const newLine = lines[idx].replace(/lexicons\/[a-z0-9-]+/i, "lexicons/terraform");
126
- lines.splice(idx + 1, 0, newLine);
127
- }
128
-
129
- const result = lines.join("\n");
130
- // Should have 2 new terraform prepack lines (in the run: | blocks)
131
- const matches = result.match(/lexicons\/terraform prepack/g);
132
- expect(matches?.length).toBe(2);
133
-
134
- // Should NOT have altered the validate standalone steps
135
- const validateSection = result.split("validate:")[1];
136
- expect(validateSection).not.toContain("lexicons/terraform");
137
- });
138
-
139
- test("adds validate step after last existing validate step", () => {
140
- writeFileSync(join(root, ".github/workflows/chant.yml"), ciContent);
141
- const content = readFileSync(join(root, ".github/workflows/chant.yml"), "utf-8");
142
- const lines = content.split("\n");
143
-
144
- let lastValidateRunIdx = -1;
145
- for (let i = 0; i < lines.length; i++) {
146
- if (lines[i].includes("Generate and validate")) {
147
- if (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("run:")) {
148
- lastValidateRunIdx = i + 1;
149
- }
150
- }
151
- }
152
-
153
- expect(lastValidateRunIdx).toBeGreaterThan(0);
154
-
155
- const block = [
156
- "",
157
- " - name: Generate and validate Terraform lexicon",
158
- " run: npm run --prefix lexicons/terraform prepack",
159
- ];
160
- lines.splice(lastValidateRunIdx + 1, 0, ...block);
161
- const result = lines.join("\n");
162
-
163
- expect(result).toContain("Generate and validate Terraform lexicon");
164
- expect(result).toContain("npm run --prefix lexicons/terraform prepack");
165
- });
166
- });
167
-
168
- // ── publish.yml ──────────────────────────────────────────
169
-
170
- describe("publish.yml patching", () => {
171
- const publishContent = `name: publish
75
+ const publishContent = `name: publish
172
76
  on:
173
77
  push:
174
78
  tags: ['v*']
175
79
  jobs:
176
80
  test:
81
+ # 12 lexicon prepacks (build + validate) plus the full vitest suite
177
82
  steps:
178
83
  - run: npm run --prefix lexicons/aws prepack
179
84
  - run: npm run --prefix lexicons/gitlab prepack
180
85
  - run: npm run --prefix lexicons/k8s prepack
181
86
  - run: npx vitest run
182
-
183
- publish:
184
- steps:
185
- - name: Publish @intentius/chant-lexicon-aws
186
- working-directory: lexicons/aws
187
- run: npm publish --access public
188
-
189
- - name: Publish @intentius/chant-lexicon-k8s
190
- working-directory: lexicons/k8s
191
- run: npm publish --access public
192
87
  `;
193
88
 
194
- test("adds prepack line in test job", () => {
195
- writeFileSync(join(root, ".github/workflows/publish.yml"), publishContent);
196
- const content = readFileSync(join(root, ".github/workflows/publish.yml"), "utf-8");
197
- const lines = content.split("\n");
198
-
199
- // Find contiguous prepack group in test job
200
- const insertAfter: number[] = [];
201
- for (let i = 0; i < lines.length; i++) {
202
- if (!lines[i].includes("npm run --prefix lexicons/") || !lines[i].includes("prepack")) continue;
203
- const nextIsAlsoPrepack =
204
- i + 1 < lines.length &&
205
- lines[i + 1].includes("npm run --prefix lexicons/") &&
206
- lines[i + 1].includes("prepack");
207
- if (!nextIsAlsoPrepack) insertAfter.push(i);
208
- }
209
-
210
- expect(insertAfter.length).toBe(1);
211
-
212
- const idx = insertAfter[0];
213
- const newLine = lines[idx].replace(/lexicons\/[a-z0-9-]+/i, "lexicons/terraform");
214
- lines.splice(idx + 1, 0, newLine);
215
-
216
- const result = lines.join("\n");
217
- expect(result).toContain("lexicons/terraform prepack");
218
- });
219
-
220
- test("adds publish step after last publish step", () => {
221
- writeFileSync(join(root, ".github/workflows/publish.yml"), publishContent);
222
- const content = readFileSync(join(root, ".github/workflows/publish.yml"), "utf-8");
223
- const lines = content.split("\n");
224
-
225
- let lastPublishRunIdx = -1;
226
- for (let i = 0; i < lines.length; i++) {
227
- if (lines[i].includes("npm publish --access public")) {
228
- lastPublishRunIdx = i;
229
- }
230
- }
231
-
232
- expect(lastPublishRunIdx).toBeGreaterThan(0);
233
-
234
- const block = [
235
- "",
236
- " - name: Publish @intentius/chant-lexicon-terraform",
237
- " working-directory: lexicons/terraform",
238
- " run: npm publish --access public",
239
- ];
240
- lines.splice(lastPublishRunIdx + 1, 0, ...block);
241
- const result = lines.join("\n");
242
-
243
- expect(result).toContain("Publish @intentius/chant-lexicon-terraform");
244
- expect(result).toContain("working-directory: lexicons/terraform");
245
- });
246
- });
89
+ // Mirrors test/Dockerfile.smoke on main: a loop, a comment and an echo that
90
+ // all contain the placeholder substring the old matcher anchored on (#1678).
91
+ const dockerLoopContent = `FROM node:22-slim
92
+ WORKDIR /app
93
+ COPY . .
94
+ RUN npm install
95
+ # Run: just prepack (or npm run --prefix lexicons/<lex> prepack) before building this Dockerfile.
96
+ RUN for lex in aws azure gcp gitlab k8s docker cedar; do \\
97
+ if [ ! -d "lexicons/$lex/dist" ]; then \\
98
+ echo "ERROR: lexicons/$lex/dist/ not found. Run codegen first: npm run --prefix lexicons/$lex prepack" >&2; \\
99
+ exit 1; \\
100
+ fi; \\
101
+ done
102
+ COPY test/integration.sh /app/test/integration.sh
103
+ `;
247
104
 
248
- // ── Dockerfile ──────────────────────────────────────────
105
+ // Mirrors test/Dockerfile.smoke-npm: two loops, no prepack invocation at all.
106
+ const dockerNpmContent = `FROM node:22-slim AS build
107
+ WORKDIR /app
108
+ COPY . .
109
+ RUN CORE_VERSION=$(jq -r '.version' packages/core/package.json) && \\
110
+ for lex in aws azure gcp gitlab k8s docker fly fountain; do \\
111
+ cd /app/lexicons/$lex && jq 'del(.scripts)' package.json > tmp.json && mv tmp.json package.json; \\
112
+ done
113
+
114
+ # smoke.sh ran prepack locally; COPY . . already brought dist/ into this stage.
115
+ RUN mkdir -p /tarballs && \\
116
+ for lex in aws azure gcp gitlab k8s docker fly fountain; do \\
117
+ tar czf /tarballs/lexicon-\${lex}.tgz -C /app/lexicons/$lex ./package.json ./src ./dist || exit 1; \\
118
+ done
119
+ `;
249
120
 
250
- describe("Dockerfile patching", () => {
251
- const dockerContent = `FROM node:22-slim
121
+ const dockerPerLineContent = `FROM node:22-slim
252
122
  WORKDIR /app
253
123
  COPY . .
254
124
  RUN npm install
@@ -258,38 +128,262 @@ RUN npm run --prefix lexicons/k8s prepack
258
128
  COPY test/integration.sh /app/test/integration.sh
259
129
  `;
260
130
 
261
- test("adds prepack RUN line after last existing one", () => {
262
- writeFileSync(join(root, "test/Dockerfile.smoke"), dockerContent);
263
- const content = readFileSync(join(root, "test/Dockerfile.smoke"), "utf-8");
264
- const lines = content.split("\n");
265
-
266
- let lastIdx = -1;
267
- for (let i = 0; i < lines.length; i++) {
268
- if (lines[i].includes("npm run --prefix lexicons/") && lines[i].includes("prepack")) {
269
- lastIdx = i;
270
- }
271
- }
272
-
273
- expect(lastIdx).toBeGreaterThan(0);
274
-
275
- const newLine = lines[lastIdx].replace(/lexicons\/[a-z0-9-]+/i, "lexicons/terraform");
276
- lines.splice(lastIdx + 1, 0, newLine);
277
- const result = lines.join("\n");
278
-
279
- expect(result).toContain("RUN npm run --prefix lexicons/terraform prepack");
280
- // Should come after k8s line
281
- const k8sIdx = result.indexOf("lexicons/k8s prepack");
282
- const tfIdx = result.indexOf("lexicons/terraform prepack");
283
- expect(tfIdx).toBeGreaterThan(k8sIdx);
284
- });
285
-
286
- test("does not add duplicate", () => {
287
- const withTerraform = dockerContent + "RUN npm run --prefix lexicons/terraform prepack\n";
288
- writeFileSync(join(root, "test/Dockerfile.smoke"), withTerraform);
289
- const content = readFileSync(join(root, "test/Dockerfile.smoke"), "utf-8");
290
-
291
- const matches = content.match(/lexicons\/terraform prepack/g);
292
- expect(matches?.length).toBe(1);
293
- });
131
+ function writeFixtureRepo(root: string): void {
132
+ mkdirSync(join(root, ".github/workflows"), { recursive: true });
133
+ mkdirSync(join(root, "test"), { recursive: true });
134
+ writeFileSync(join(root, "package.json"), JSON.stringify(rootPackageJson, null, 2) + "\n");
135
+ writeFileSync(join(root, "tsconfig.json"), JSON.stringify(rootTsconfig, null, 2) + "\n");
136
+ writeFileSync(join(root, ".github/workflows/chant.yml"), ciContent);
137
+ writeFileSync(join(root, ".github/workflows/publish.yml"), publishContent);
138
+ writeFileSync(join(root, "test/Dockerfile.smoke"), dockerLoopContent);
139
+ writeFileSync(join(root, "test/Dockerfile.smoke-npm"), dockerNpmContent);
140
+ }
141
+
142
+ const TRACKED = [
143
+ "package.json",
144
+ "tsconfig.json",
145
+ ".github/workflows/chant.yml",
146
+ ".github/workflows/publish.yml",
147
+ "test/Dockerfile.smoke",
148
+ "test/Dockerfile.smoke-npm",
149
+ ];
150
+
151
+ function snapshot(root: string): Record<string, string> {
152
+ const out: Record<string, string> = {};
153
+ for (const f of TRACKED) out[f] = readFileSync(join(root, f), "utf-8");
154
+ return out;
155
+ }
156
+
157
+ describe("onboardCommand", () => {
158
+ let root: string;
159
+
160
+ beforeEach(() => {
161
+ root = makeTempDir();
162
+ writeFixtureRepo(root);
163
+ });
164
+
165
+ afterEach(() => {
166
+ rmSync(root, { recursive: true, force: true });
167
+ });
168
+
169
+ test("first run patches every file once", () => {
170
+ const result = onboardCommand({ name: "terraform", root });
171
+ expect(result.success).toBe(true);
172
+ expect(result.patched).toEqual([
173
+ "package.json (root dependency)",
174
+ "tsconfig.json (paths mapping)",
175
+ "chant.yml (prepack + validate)",
176
+ "publish.yml (prepack)",
177
+ "Dockerfile.smoke (lexicon list)",
178
+ "Dockerfile.smoke-npm (lexicon list)",
179
+ ]);
180
+ expect(result.skipped).toEqual([]);
181
+
182
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
183
+ expect(pkg.dependencies["@intentius/chant-lexicon-terraform"]).toBe("workspace:*");
184
+
185
+ const ci = readFileSync(join(root, ".github/workflows/chant.yml"), "utf-8");
186
+ expect(ci.match(/lexicons\/terraform prepack/g)?.length).toBe(3); // check + test + validate
187
+ expect(ci).toContain("Generate and validate Terraform lexicon");
188
+
189
+ const pub = readFileSync(join(root, ".github/workflows/publish.yml"), "utf-8");
190
+ expect(pub.match(/lexicons\/terraform prepack/g)?.length).toBe(1);
191
+ expect(pub).toContain(" - run: npm run --prefix lexicons/terraform prepack\n - run: npx vitest run");
192
+ });
193
+
194
+ test("second run is a byte-identical no-op and reports already covered (#1678)", () => {
195
+ onboardCommand({ name: "terraform", root });
196
+ const after1 = snapshot(root);
197
+
198
+ const result = onboardCommand({ name: "terraform", root });
199
+ const after2 = snapshot(root);
200
+
201
+ expect(after2).toEqual(after1);
202
+ expect(result.patched).toEqual([]);
203
+ expect(result.skipped).toEqual([
204
+ "package.json: @intentius/chant-lexicon-terraform already in dependencies",
205
+ "tsconfig.json: @intentius/chant-lexicon-terraform already in paths",
206
+ "chant.yml: terraform already in chant.yml",
207
+ "publish.yml: prepack for terraform already present",
208
+ "Dockerfile.smoke: already covers terraform",
209
+ "Dockerfile.smoke-npm: already covers terraform",
210
+ ]);
211
+ });
212
+
213
+ test("running onboard three times leaves exactly one copy of everything", () => {
214
+ for (let i = 0; i < 3; i++) onboardCommand({ name: "terraform", root });
215
+ const ci = readFileSync(join(root, ".github/workflows/chant.yml"), "utf-8");
216
+ expect(ci.match(/lexicons\/terraform prepack/g)?.length).toBe(3);
217
+ const docker = readFileSync(join(root, "test/Dockerfile.smoke"), "utf-8");
218
+ expect(docker.match(/terraform/g)?.length).toBe(1);
219
+ });
220
+ });
221
+
222
+ describe("root tsconfig.json paths (#1614)", () => {
223
+ let root: string;
224
+
225
+ beforeEach(() => {
226
+ root = makeTempDir();
227
+ writeFixtureRepo(root);
228
+ });
229
+
230
+ afterEach(() => {
231
+ rmSync(root, { recursive: true, force: true });
232
+ });
233
+
234
+ test("adds bare and subpath mappings", () => {
235
+ const result = patchRootTsconfigPaths(root, "terraform");
236
+ expect(result.patched).toBe(true);
237
+
238
+ const cfg = JSON.parse(readFileSync(join(root, "tsconfig.json"), "utf-8"));
239
+ expect(cfg.compilerOptions.paths["@intentius/chant-lexicon-terraform"]).toEqual([
240
+ "lexicons/terraform/src/index.ts",
241
+ ]);
242
+ expect(cfg.compilerOptions.paths["@intentius/chant-lexicon-terraform/*"]).toEqual(["lexicons/terraform/src/*"]);
243
+ // Existing entries untouched
244
+ expect(cfg.compilerOptions.paths["@intentius/chant-lexicon-aws"]).toEqual(["lexicons/aws/src/index.ts"]);
245
+ expect(cfg.compilerOptions.moduleResolution).toBe("node");
246
+ });
247
+
248
+ test("is idempotent", () => {
249
+ patchRootTsconfigPaths(root, "terraform");
250
+ const before = readFileSync(join(root, "tsconfig.json"), "utf-8");
251
+ const result = patchRootTsconfigPaths(root, "terraform");
252
+ expect(result.patched).toBe(false);
253
+ expect(result.reason).toContain("already in paths");
254
+ expect(readFileSync(join(root, "tsconfig.json"), "utf-8")).toBe(before);
255
+ });
256
+
257
+ test("skips an already-present lexicon", () => {
258
+ const result = patchRootTsconfigPaths(root, "aws");
259
+ expect(result.patched).toBe(false);
260
+ });
261
+
262
+ test("creates the paths map when missing", () => {
263
+ writeFileSync(join(root, "tsconfig.json"), JSON.stringify({ compilerOptions: {} }) + "\n");
264
+ const result = patchRootTsconfigPaths(root, "terraform");
265
+ expect(result.patched).toBe(true);
266
+ const cfg = JSON.parse(readFileSync(join(root, "tsconfig.json"), "utf-8"));
267
+ expect(Object.keys(cfg.compilerOptions.paths)).toEqual([
268
+ "@intentius/chant-lexicon-terraform",
269
+ "@intentius/chant-lexicon-terraform/*",
270
+ ]);
271
+ });
272
+ });
273
+
274
+ describe("Dockerfile patching (#1678)", () => {
275
+ let root: string;
276
+
277
+ beforeEach(() => {
278
+ root = makeTempDir();
279
+ mkdirSync(join(root, "test"), { recursive: true });
280
+ });
281
+
282
+ afterEach(() => {
283
+ rmSync(root, { recursive: true, force: true });
284
+ });
285
+
286
+ test("loop-based Dockerfile.smoke gains exactly one list entry", () => {
287
+ const file = join(root, "test/Dockerfile.smoke");
288
+ writeFileSync(file, dockerLoopContent);
289
+
290
+ const result = patchDockerfile(file, "terraform");
291
+ expect(result.patched).toBe(true);
292
+
293
+ const out = readFileSync(file, "utf-8");
294
+ expect(out).toContain("RUN for lex in aws azure gcp gitlab k8s docker cedar terraform; do \\");
295
+ // No RUN line was appended after the comment or the echo
296
+ expect(out).not.toContain("RUN npm run --prefix lexicons/terraform prepack");
297
+ expect(out.match(/terraform/g)?.length).toBe(1);
298
+ // Everything else is untouched
299
+ expect(out.split("\n").length).toBe(dockerLoopContent.split("\n").length);
300
+ });
301
+
302
+ test("loop-based file already covering the lexicon is a no-op", () => {
303
+ const file = join(root, "test/Dockerfile.smoke");
304
+ writeFileSync(file, dockerLoopContent);
305
+
306
+ const result = patchDockerfile(file, "cedar");
307
+ expect(result.patched).toBe(false);
308
+ expect(result.reason).toBe("already covers cedar");
309
+ expect(readFileSync(file, "utf-8")).toBe(dockerLoopContent);
310
+ });
311
+
312
+ test("second patch of a loop-based file is byte-identical", () => {
313
+ const file = join(root, "test/Dockerfile.smoke");
314
+ writeFileSync(file, dockerLoopContent);
315
+
316
+ patchDockerfile(file, "terraform");
317
+ const once = readFileSync(file, "utf-8");
318
+ const result = patchDockerfile(file, "terraform");
319
+ expect(result.patched).toBe(false);
320
+ expect(readFileSync(file, "utf-8")).toBe(once);
321
+ });
322
+
323
+ test("Dockerfile.smoke-npm adds the name to both loops and nothing else", () => {
324
+ const file = join(root, "test/Dockerfile.smoke-npm");
325
+ writeFileSync(file, dockerNpmContent);
326
+
327
+ const result = patchDockerfile(file, "terraform");
328
+ expect(result.patched).toBe(true);
329
+
330
+ const out = readFileSync(file, "utf-8");
331
+ expect(out.match(/for lex in aws azure gcp gitlab k8s docker fly fountain terraform; do/g)?.length).toBe(2);
332
+ expect(out.match(/terraform/g)?.length).toBe(2);
333
+ expect(out.split("\n").length).toBe(dockerNpmContent.split("\n").length);
334
+
335
+ // Reported as a no-op the second time, not as patched
336
+ expect(patchDockerfile(file, "terraform")).toEqual({ patched: false, reason: "already covers terraform" });
337
+ expect(readFileSync(file, "utf-8")).toBe(out);
338
+ });
339
+
340
+ test("per-lexicon RUN layout still gets a new RUN line after the last one", () => {
341
+ const file = join(root, "test/Dockerfile.smoke");
342
+ writeFileSync(file, dockerPerLineContent);
343
+
344
+ const result = patchDockerfile(file, "terraform");
345
+ expect(result.patched).toBe(true);
346
+
347
+ const out = readFileSync(file, "utf-8");
348
+ expect(out).toContain("RUN npm run --prefix lexicons/k8s prepack\nRUN npm run --prefix lexicons/terraform prepack\n");
349
+ expect(out.match(/lexicons\/terraform prepack/g)?.length).toBe(1);
350
+
351
+ expect(patchDockerfile(file, "terraform").patched).toBe(false);
352
+ expect(readFileSync(file, "utf-8")).toBe(out);
353
+ });
354
+
355
+ test("a file with nothing to anchor on is reported, not silently 'patched'", () => {
356
+ const file = join(root, "test/Dockerfile.smoke-npm");
357
+ const content = "FROM node:22-slim\nCOPY . .\n";
358
+ writeFileSync(file, content);
359
+
360
+ const result = patchDockerfile(file, "terraform");
361
+ expect(result.patched).toBe(false);
362
+ expect(result.reason).toContain("anchor");
363
+ expect(readFileSync(file, "utf-8")).toBe(content);
364
+ });
365
+ });
366
+
367
+ describe("insertPrepackAfterEach", () => {
368
+ test("ignores comments and echo lines that mention prepack", () => {
369
+ const lines = dockerLoopContent.split("\n");
370
+ const before = [...lines];
371
+ expect(insertPrepackAfterEach(lines, "terraform")).toBe(false);
372
+ expect(lines).toEqual(before);
373
+ });
374
+
375
+ test("anchors on real RUN lines only", () => {
376
+ const lines = [
377
+ "# npm run --prefix lexicons/<lex> prepack",
378
+ "RUN npm run --prefix lexicons/aws prepack",
379
+ 'RUN echo "npm run --prefix lexicons/$lex prepack"',
380
+ ];
381
+ expect(insertPrepackAfterEach(lines, "terraform")).toBe(true);
382
+ expect(lines).toEqual([
383
+ "# npm run --prefix lexicons/<lex> prepack",
384
+ "RUN npm run --prefix lexicons/aws prepack",
385
+ "RUN npm run --prefix lexicons/terraform prepack",
386
+ 'RUN echo "npm run --prefix lexicons/$lex prepack"',
387
+ ]);
294
388
  });
295
389
  });