@intentius/chant 0.0.1

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 (271) hide show
  1. package/README.md +365 -0
  2. package/package.json +22 -0
  3. package/src/attrref.test.ts +148 -0
  4. package/src/attrref.ts +50 -0
  5. package/src/barrel.test.ts +157 -0
  6. package/src/barrel.ts +101 -0
  7. package/src/bench.test.ts +227 -0
  8. package/src/build.test.ts +437 -0
  9. package/src/build.ts +425 -0
  10. package/src/builder.test.ts +312 -0
  11. package/src/builder.ts +56 -0
  12. package/src/child-project.ts +44 -0
  13. package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
  14. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
  15. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
  16. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
  17. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
  18. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
  19. package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
  20. package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
  21. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
  22. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
  23. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
  24. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
  25. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
  26. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
  27. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
  28. package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
  29. package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
  30. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
  31. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
  32. package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
  33. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
  34. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
  35. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
  36. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
  37. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
  38. package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
  39. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
  40. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
  41. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
  42. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
  43. package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
  44. package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
  45. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
  46. package/src/cli/commands/build.test.ts +149 -0
  47. package/src/cli/commands/build.ts +344 -0
  48. package/src/cli/commands/diff.test.ts +148 -0
  49. package/src/cli/commands/diff.ts +221 -0
  50. package/src/cli/commands/doctor.test.ts +239 -0
  51. package/src/cli/commands/doctor.ts +224 -0
  52. package/src/cli/commands/import.test.ts +379 -0
  53. package/src/cli/commands/import.ts +335 -0
  54. package/src/cli/commands/init-lexicon.test.ts +297 -0
  55. package/src/cli/commands/init-lexicon.ts +993 -0
  56. package/src/cli/commands/init.test.ts +317 -0
  57. package/src/cli/commands/init.ts +505 -0
  58. package/src/cli/commands/licenses.ts +165 -0
  59. package/src/cli/commands/lint.test.ts +332 -0
  60. package/src/cli/commands/lint.ts +408 -0
  61. package/src/cli/commands/list.test.ts +100 -0
  62. package/src/cli/commands/list.ts +108 -0
  63. package/src/cli/commands/update.test.ts +38 -0
  64. package/src/cli/commands/update.ts +207 -0
  65. package/src/cli/conflict-check.test.ts +255 -0
  66. package/src/cli/conflict-check.ts +89 -0
  67. package/src/cli/debug.ts +8 -0
  68. package/src/cli/format.test.ts +140 -0
  69. package/src/cli/format.ts +133 -0
  70. package/src/cli/handlers/build.ts +58 -0
  71. package/src/cli/handlers/dev.ts +38 -0
  72. package/src/cli/handlers/init.ts +46 -0
  73. package/src/cli/handlers/lint.ts +36 -0
  74. package/src/cli/handlers/misc.ts +57 -0
  75. package/src/cli/handlers/serve.ts +26 -0
  76. package/src/cli/index.ts +3 -0
  77. package/src/cli/lsp/capabilities.ts +46 -0
  78. package/src/cli/lsp/diagnostics.ts +52 -0
  79. package/src/cli/lsp/server.test.ts +618 -0
  80. package/src/cli/lsp/server.ts +393 -0
  81. package/src/cli/main.test.ts +257 -0
  82. package/src/cli/main.ts +224 -0
  83. package/src/cli/mcp/resources/context.ts +59 -0
  84. package/src/cli/mcp/server.test.ts +747 -0
  85. package/src/cli/mcp/server.ts +402 -0
  86. package/src/cli/mcp/tools/build.ts +117 -0
  87. package/src/cli/mcp/tools/import.ts +48 -0
  88. package/src/cli/mcp/tools/lint.ts +45 -0
  89. package/src/cli/plugins.test.ts +31 -0
  90. package/src/cli/plugins.ts +94 -0
  91. package/src/cli/registry.ts +73 -0
  92. package/src/cli/reporters/stylish.test.ts +282 -0
  93. package/src/cli/reporters/stylish.ts +186 -0
  94. package/src/cli/watch.test.ts +81 -0
  95. package/src/cli/watch.ts +101 -0
  96. package/src/codegen/case.test.ts +30 -0
  97. package/src/codegen/case.ts +11 -0
  98. package/src/codegen/coverage.ts +167 -0
  99. package/src/codegen/docs.ts +634 -0
  100. package/src/codegen/fetch.test.ts +119 -0
  101. package/src/codegen/fetch.ts +261 -0
  102. package/src/codegen/generate-registry.test.ts +118 -0
  103. package/src/codegen/generate-registry.ts +107 -0
  104. package/src/codegen/generate-runtime-index.test.ts +81 -0
  105. package/src/codegen/generate-runtime-index.ts +99 -0
  106. package/src/codegen/generate-typescript.test.ts +146 -0
  107. package/src/codegen/generate-typescript.ts +161 -0
  108. package/src/codegen/generate.ts +206 -0
  109. package/src/codegen/json-patch.test.ts +113 -0
  110. package/src/codegen/json-patch.ts +151 -0
  111. package/src/codegen/json-schema.test.ts +196 -0
  112. package/src/codegen/json-schema.ts +209 -0
  113. package/src/codegen/naming.ts +201 -0
  114. package/src/codegen/package.ts +161 -0
  115. package/src/codegen/rollback.test.ts +92 -0
  116. package/src/codegen/rollback.ts +115 -0
  117. package/src/codegen/topo-sort.test.ts +69 -0
  118. package/src/codegen/topo-sort.ts +46 -0
  119. package/src/codegen/typecheck.test.ts +37 -0
  120. package/src/codegen/typecheck.ts +74 -0
  121. package/src/codegen/validate.test.ts +86 -0
  122. package/src/codegen/validate.ts +143 -0
  123. package/src/composite.test.ts +426 -0
  124. package/src/composite.ts +243 -0
  125. package/src/config.test.ts +91 -0
  126. package/src/config.ts +87 -0
  127. package/src/declarable.test.ts +160 -0
  128. package/src/declarable.ts +47 -0
  129. package/src/detectLexicon.test.ts +236 -0
  130. package/src/detectLexicon.ts +37 -0
  131. package/src/discovery/cache.test.ts +78 -0
  132. package/src/discovery/cache.ts +86 -0
  133. package/src/discovery/collect.test.ts +269 -0
  134. package/src/discovery/collect.ts +51 -0
  135. package/src/discovery/cycles.test.ts +238 -0
  136. package/src/discovery/cycles.ts +107 -0
  137. package/src/discovery/files.test.ts +154 -0
  138. package/src/discovery/files.ts +61 -0
  139. package/src/discovery/graph.test.ts +476 -0
  140. package/src/discovery/graph.ts +150 -0
  141. package/src/discovery/import.test.ts +199 -0
  142. package/src/discovery/import.ts +20 -0
  143. package/src/discovery/index.test.ts +272 -0
  144. package/src/discovery/index.ts +132 -0
  145. package/src/discovery/resolve.test.ts +267 -0
  146. package/src/discovery/resolve.ts +54 -0
  147. package/src/errors.test.ts +138 -0
  148. package/src/errors.ts +86 -0
  149. package/src/import/base-parser.test.ts +67 -0
  150. package/src/import/base-parser.ts +48 -0
  151. package/src/import/generator.ts +21 -0
  152. package/src/import/ir-utils.test.ts +103 -0
  153. package/src/import/ir-utils.ts +87 -0
  154. package/src/import/parser.ts +41 -0
  155. package/src/index.ts +60 -0
  156. package/src/intrinsic-interpolation.test.ts +91 -0
  157. package/src/intrinsic-interpolation.ts +89 -0
  158. package/src/intrinsic.test.ts +69 -0
  159. package/src/intrinsic.ts +43 -0
  160. package/src/lexicon-integrity.test.ts +94 -0
  161. package/src/lexicon-integrity.ts +69 -0
  162. package/src/lexicon-manifest.test.ts +101 -0
  163. package/src/lexicon-manifest.ts +71 -0
  164. package/src/lexicon-output.test.ts +182 -0
  165. package/src/lexicon-output.ts +82 -0
  166. package/src/lexicon-schema.test.ts +239 -0
  167. package/src/lexicon-schema.ts +144 -0
  168. package/src/lexicon.ts +212 -0
  169. package/src/lint/config-overrides.test.ts +254 -0
  170. package/src/lint/config.test.ts +644 -0
  171. package/src/lint/config.ts +375 -0
  172. package/src/lint/declarative.test.ts +256 -0
  173. package/src/lint/declarative.ts +187 -0
  174. package/src/lint/engine.test.ts +465 -0
  175. package/src/lint/engine.ts +172 -0
  176. package/src/lint/named-checks.test.ts +37 -0
  177. package/src/lint/named-checks.ts +33 -0
  178. package/src/lint/parser.test.ts +129 -0
  179. package/src/lint/parser.ts +42 -0
  180. package/src/lint/post-synth.test.ts +113 -0
  181. package/src/lint/post-synth.ts +76 -0
  182. package/src/lint/presets/relaxed.json +19 -0
  183. package/src/lint/presets/strict.json +19 -0
  184. package/src/lint/rule-loader.test.ts +67 -0
  185. package/src/lint/rule-loader.ts +67 -0
  186. package/src/lint/rule-options.test.ts +141 -0
  187. package/src/lint/rule.test.ts +196 -0
  188. package/src/lint/rule.ts +98 -0
  189. package/src/lint/rules/barrel-import-style.test.ts +80 -0
  190. package/src/lint/rules/barrel-import-style.ts +59 -0
  191. package/src/lint/rules/composite-scope.ts +55 -0
  192. package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
  193. package/src/lint/rules/cor017-composite-name-match.ts +108 -0
  194. package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
  195. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
  196. package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
  197. package/src/lint/rules/declarable-naming-convention.ts +70 -0
  198. package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
  199. package/src/lint/rules/enforce-barrel-import.ts +81 -0
  200. package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
  201. package/src/lint/rules/enforce-barrel-ref.ts +75 -0
  202. package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
  203. package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
  204. package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
  205. package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
  206. package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
  207. package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
  208. package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
  209. package/src/lint/rules/evl004-spread-non-const.ts +111 -0
  210. package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
  211. package/src/lint/rules/evl005-resource-block-body.ts +49 -0
  212. package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
  213. package/src/lint/rules/evl006-barrel-usage.ts +95 -0
  214. package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
  215. package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
  216. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
  217. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
  218. package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
  219. package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
  220. package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
  221. package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
  222. package/src/lint/rules/export-required.test.ts +213 -0
  223. package/src/lint/rules/export-required.ts +158 -0
  224. package/src/lint/rules/file-declarable-limit.test.ts +148 -0
  225. package/src/lint/rules/file-declarable-limit.ts +96 -0
  226. package/src/lint/rules/flat-declarations.test.ts +210 -0
  227. package/src/lint/rules/flat-declarations.ts +70 -0
  228. package/src/lint/rules/index.ts +99 -0
  229. package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
  230. package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
  231. package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
  232. package/src/lint/rules/no-redundant-type-import.ts +85 -0
  233. package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
  234. package/src/lint/rules/no-redundant-value-cast.ts +46 -0
  235. package/src/lint/rules/no-string-ref.test.ts +100 -0
  236. package/src/lint/rules/no-string-ref.ts +66 -0
  237. package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
  238. package/src/lint/rules/no-unused-declarable-import.ts +103 -0
  239. package/src/lint/rules/no-unused-declarable.test.ts +134 -0
  240. package/src/lint/rules/no-unused-declarable.ts +118 -0
  241. package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
  242. package/src/lint/rules/prefer-namespace-import.ts +63 -0
  243. package/src/lint/rules/single-concern-file.test.ts +156 -0
  244. package/src/lint/rules/single-concern-file.ts +98 -0
  245. package/src/lint/rules/stale-barrel-types.ts +60 -0
  246. package/src/lint/selectors.test.ts +113 -0
  247. package/src/lint/selectors.ts +188 -0
  248. package/src/lsp/lexicon-providers.ts +191 -0
  249. package/src/lsp/types.ts +79 -0
  250. package/src/mcp/types.ts +22 -0
  251. package/src/project/scan.test.ts +178 -0
  252. package/src/project/scan.ts +182 -0
  253. package/src/project/sync.test.ts +87 -0
  254. package/src/project/sync.ts +46 -0
  255. package/src/project-validation.test.ts +64 -0
  256. package/src/project-validation.ts +79 -0
  257. package/src/pseudo-parameter.test.ts +39 -0
  258. package/src/pseudo-parameter.ts +47 -0
  259. package/src/runtime.ts +68 -0
  260. package/src/serializer-walker.test.ts +124 -0
  261. package/src/serializer-walker.ts +83 -0
  262. package/src/serializer.ts +42 -0
  263. package/src/sort.test.ts +290 -0
  264. package/src/sort.ts +58 -0
  265. package/src/stack-output.ts +82 -0
  266. package/src/types.test.ts +307 -0
  267. package/src/types.ts +46 -0
  268. package/src/utils.test.ts +195 -0
  269. package/src/utils.ts +46 -0
  270. package/src/validation.test.ts +308 -0
  271. package/src/validation.ts +50 -0
@@ -0,0 +1,379 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { importCommand, type ImportOptions } from "./import";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ describe("importCommand", () => {
9
+ let testDir: string;
10
+ let templateDir: string;
11
+ let outputDir: string;
12
+
13
+ beforeEach(async () => {
14
+ testDir = join(tmpdir(), `chant-import-test-${Date.now()}-${Math.random()}`);
15
+ templateDir = join(testDir, "templates");
16
+ outputDir = join(testDir, "output");
17
+ await mkdir(templateDir, { recursive: true });
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await rm(testDir, { recursive: true, force: true });
22
+ });
23
+
24
+ test("imports CloudFormation template", async () => {
25
+ const template = {
26
+ AWSTemplateFormatVersion: "2010-09-09",
27
+ Description: "Test CloudFormation template",
28
+ Resources: {
29
+ MyBucket: {
30
+ Type: "AWS::S3::Bucket",
31
+ Properties: {
32
+ BucketName: "my-bucket",
33
+ VersioningConfiguration: {
34
+ Status: "Enabled",
35
+ },
36
+ },
37
+ },
38
+ },
39
+ };
40
+
41
+ const templatePath = join(templateDir, "template.json");
42
+ await writeFile(templatePath, JSON.stringify(template));
43
+
44
+ const options: ImportOptions = {
45
+ templatePath,
46
+ output: outputDir,
47
+ };
48
+
49
+ const result = await importCommand(options);
50
+
51
+ expect(result.success).toBe(true);
52
+ expect(result.lexicon).toBe("aws");
53
+ expect(result.generatedFiles.length).toBeGreaterThan(0);
54
+ expect(existsSync(outputDir)).toBe(true);
55
+ });
56
+
57
+ test("imports CloudFormation template with multiple resources", async () => {
58
+ const template = {
59
+ AWSTemplateFormatVersion: "2010-09-09",
60
+ Resources: {
61
+ MyQueue: {
62
+ Type: "AWS::SQS::Queue",
63
+ Properties: {
64
+ QueueName: "my-queue",
65
+ },
66
+ },
67
+ },
68
+ };
69
+
70
+ const templatePath = join(templateDir, "template.json");
71
+ await writeFile(templatePath, JSON.stringify(template));
72
+
73
+ const options: ImportOptions = {
74
+ templatePath,
75
+ output: outputDir,
76
+ };
77
+
78
+ const result = await importCommand(options);
79
+
80
+ expect(result.success).toBe(true);
81
+ expect(result.lexicon).toBe("aws");
82
+ expect(result.generatedFiles.length).toBeGreaterThan(0);
83
+ });
84
+
85
+ test("auto-detects AWS lexicon with AWSTemplateFormatVersion", async () => {
86
+ const template = {
87
+ AWSTemplateFormatVersion: "2010-09-09",
88
+ Parameters: { Env: { Type: "String" } },
89
+ Resources: {},
90
+ };
91
+
92
+ const templatePath = join(templateDir, "template.json");
93
+ await writeFile(templatePath, JSON.stringify(template));
94
+
95
+ const result = await importCommand({
96
+ templatePath,
97
+ output: outputDir,
98
+ });
99
+
100
+ expect(result.lexicon).toBe("aws");
101
+ });
102
+
103
+ test("auto-detects AWS lexicon with AWS:: resource types", async () => {
104
+ const template = {
105
+ Resources: {
106
+ Bucket: {
107
+ Type: "AWS::S3::Bucket",
108
+ },
109
+ },
110
+ };
111
+
112
+ const templatePath = join(templateDir, "template.json");
113
+ await writeFile(templatePath, JSON.stringify(template));
114
+
115
+ const result = await importCommand({
116
+ templatePath,
117
+ output: outputDir,
118
+ });
119
+
120
+ expect(result.lexicon).toBe("aws");
121
+ });
122
+
123
+ test("fails for unknown lexicon", async () => {
124
+ const template = {
125
+ version: "1.0",
126
+ unknownField: {},
127
+ };
128
+
129
+ const templatePath = join(templateDir, "template.json");
130
+ await writeFile(templatePath, JSON.stringify(template));
131
+
132
+ const result = await importCommand({
133
+ templatePath,
134
+ output: outputDir,
135
+ });
136
+
137
+ expect(result.success).toBe(false);
138
+ expect(result.error).toContain("Could not detect");
139
+ });
140
+
141
+ test("fails for non-existent template", async () => {
142
+ const result = await importCommand({
143
+ templatePath: "/nonexistent/template.json",
144
+ output: outputDir,
145
+ });
146
+
147
+ expect(result.success).toBe(false);
148
+ expect(result.error).toContain("not found");
149
+ });
150
+
151
+ test("uses default output directory", async () => {
152
+ const template = {
153
+ AWSTemplateFormatVersion: "2010-09-09",
154
+ Resources: {
155
+ Bucket: { Type: "AWS::S3::Bucket", Properties: {} },
156
+ },
157
+ };
158
+
159
+ const templatePath = join(templateDir, "template.json");
160
+ await writeFile(templatePath, JSON.stringify(template));
161
+
162
+ // Change to test dir so default ./infra/ is relative to it
163
+ const originalCwd = process.cwd();
164
+ process.chdir(testDir);
165
+
166
+ try {
167
+ const result = await importCommand({ templatePath });
168
+
169
+ expect(result.success).toBe(true);
170
+ expect(existsSync(join(testDir, "infra"))).toBe(true);
171
+ } finally {
172
+ process.chdir(originalCwd);
173
+ }
174
+ });
175
+
176
+ test("generates TypeScript with correct syntax", async () => {
177
+ const template = {
178
+ AWSTemplateFormatVersion: "2010-09-09",
179
+ Resources: {
180
+ DataBucket: {
181
+ Type: "AWS::S3::Bucket",
182
+ Properties: {
183
+ BucketName: "data-bucket",
184
+ VersioningConfiguration: {
185
+ Status: "Enabled",
186
+ },
187
+ },
188
+ },
189
+ },
190
+ };
191
+
192
+ const templatePath = join(templateDir, "template.json");
193
+ await writeFile(templatePath, JSON.stringify(template));
194
+
195
+ const result = await importCommand({
196
+ templatePath,
197
+ output: outputDir,
198
+ });
199
+
200
+ expect(result.success).toBe(true);
201
+
202
+ // Find a generated file and check content
203
+ const mainFile = result.generatedFiles.find((f) => f.endsWith(".ts"));
204
+ expect(mainFile).toBeDefined();
205
+
206
+ const content = readFileSync(join(outputDir, mainFile!), "utf-8");
207
+ expect(content).toContain("import {");
208
+ expect(content).toContain("export const");
209
+ expect(content).toContain("Bucket");
210
+ });
211
+
212
+ test("warns about non-empty output directory", async () => {
213
+ const template = {
214
+ AWSTemplateFormatVersion: "2010-09-09",
215
+ Resources: {
216
+ Bucket: { Type: "AWS::S3::Bucket", Properties: {} },
217
+ },
218
+ };
219
+
220
+ const templatePath = join(templateDir, "template.json");
221
+ await writeFile(templatePath, JSON.stringify(template));
222
+
223
+ // Create output dir with existing file
224
+ await mkdir(outputDir, { recursive: true });
225
+ await writeFile(join(outputDir, "existing.ts"), "// existing");
226
+
227
+ const result = await importCommand({
228
+ templatePath,
229
+ output: outputDir,
230
+ });
231
+
232
+ expect(result.success).toBe(true);
233
+ expect(result.warnings.some((w) => w.includes("not empty"))).toBe(true);
234
+ });
235
+
236
+ test("force overwrites existing files", async () => {
237
+ const template = {
238
+ AWSTemplateFormatVersion: "2010-09-09",
239
+ Resources: {
240
+ Bucket: { Type: "AWS::S3::Bucket", Properties: {} },
241
+ },
242
+ };
243
+
244
+ const templatePath = join(templateDir, "template.json");
245
+ await writeFile(templatePath, JSON.stringify(template));
246
+
247
+ // Create output dir with existing file
248
+ await mkdir(outputDir, { recursive: true });
249
+ await writeFile(join(outputDir, "main.ts"), "// old content");
250
+
251
+ const result = await importCommand({
252
+ templatePath,
253
+ output: outputDir,
254
+ force: true,
255
+ });
256
+
257
+ expect(result.success).toBe(true);
258
+
259
+ // Check that main.ts was overwritten
260
+ const content = readFileSync(join(outputDir, "main.ts"), "utf-8");
261
+ expect(content).not.toContain("old content");
262
+ expect(content).toContain("Bucket");
263
+ });
264
+
265
+ test("organizes resources by category for large templates", async () => {
266
+ const template = {
267
+ AWSTemplateFormatVersion: "2010-09-09",
268
+ Resources: {
269
+ Bucket1: { Type: "AWS::S3::Bucket", Properties: {} },
270
+ Bucket2: { Type: "AWS::S3::Bucket", Properties: {} },
271
+ Queue1: { Type: "AWS::SQS::Queue", Properties: {} },
272
+ LB1: { Type: "AWS::ElasticLoadBalancingV2::LoadBalancer", Properties: {} },
273
+ },
274
+ };
275
+
276
+ const templatePath = join(templateDir, "template.json");
277
+ await writeFile(templatePath, JSON.stringify(template));
278
+
279
+ const result = await importCommand({
280
+ templatePath,
281
+ output: outputDir,
282
+ });
283
+
284
+ expect(result.success).toBe(true);
285
+ // With 4 resources, should create separate files
286
+ expect(result.generatedFiles.length).toBeGreaterThan(1);
287
+ });
288
+
289
+ test("creates index.ts for organized imports", async () => {
290
+ const template = {
291
+ AWSTemplateFormatVersion: "2010-09-09",
292
+ Resources: {
293
+ Bucket1: { Type: "AWS::S3::Bucket", Properties: {} },
294
+ Bucket2: { Type: "AWS::S3::Bucket", Properties: {} },
295
+ Lambda1: { Type: "AWS::Lambda::Function", Properties: {} },
296
+ LB1: { Type: "AWS::ElasticLoadBalancingV2::LoadBalancer", Properties: {} },
297
+ },
298
+ };
299
+
300
+ const templatePath = join(templateDir, "template.json");
301
+ await writeFile(templatePath, JSON.stringify(template));
302
+
303
+ const result = await importCommand({
304
+ templatePath,
305
+ output: outputDir,
306
+ });
307
+
308
+ expect(result.success).toBe(true);
309
+
310
+ if (result.generatedFiles.includes("index.ts")) {
311
+ const indexContent = readFileSync(join(outputDir, "index.ts"), "utf-8");
312
+ expect(indexContent).toContain("export {");
313
+ }
314
+ });
315
+
316
+ test("handles template with parameters", async () => {
317
+ const template = {
318
+ AWSTemplateFormatVersion: "2010-09-09",
319
+ Parameters: {
320
+ Environment: { Type: "String" },
321
+ BucketName: { Type: "String" },
322
+ },
323
+ Resources: {
324
+ MyBucket: {
325
+ Type: "AWS::S3::Bucket",
326
+ Properties: {
327
+ BucketName: { Ref: "BucketName" },
328
+ },
329
+ },
330
+ },
331
+ };
332
+
333
+ const templatePath = join(templateDir, "template.json");
334
+ await writeFile(templatePath, JSON.stringify(template));
335
+
336
+ const result = await importCommand({
337
+ templatePath,
338
+ output: outputDir,
339
+ });
340
+
341
+ expect(result.success).toBe(true);
342
+
343
+ // Find main file and check for Parameter imports
344
+ const files = result.generatedFiles.filter((f) => f.endsWith(".ts"));
345
+ let hasParameter = false;
346
+ for (const file of files) {
347
+ const content = readFileSync(join(outputDir, file), "utf-8");
348
+ if (content.includes("Parameter")) {
349
+ hasParameter = true;
350
+ break;
351
+ }
352
+ }
353
+ expect(hasParameter).toBe(true);
354
+ });
355
+
356
+ test("prints generated files on success", async () => {
357
+ const template = {
358
+ AWSTemplateFormatVersion: "2010-09-09",
359
+ Resources: {
360
+ Bucket: { Type: "AWS::S3::Bucket", Properties: {} },
361
+ },
362
+ };
363
+
364
+ const templatePath = join(templateDir, "template.json");
365
+ await writeFile(templatePath, JSON.stringify(template));
366
+
367
+ const result = await importCommand({
368
+ templatePath,
369
+ output: outputDir,
370
+ });
371
+
372
+ expect(result.success).toBe(true);
373
+ expect(result.generatedFiles.length).toBeGreaterThan(0);
374
+ // Each file should be a .ts file
375
+ for (const file of result.generatedFiles) {
376
+ expect(file.endsWith(".ts")).toBe(true);
377
+ }
378
+ });
379
+ });
@@ -0,0 +1,335 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
2
+ import { join, resolve, basename } from "path";
3
+ import { formatSuccess, formatWarning, formatError } from "../format";
4
+ import type { TemplateIR, ResourceIR, TemplateParser } from "../../import/parser";
5
+ import type { GeneratedFile, TypeScriptGenerator } from "../../import/generator";
6
+ import { loadPlugins, resolveProjectLexicons } from "../plugins";
7
+ import type { LexiconPlugin } from "../../lexicon";
8
+
9
+ /**
10
+ * Import command options
11
+ */
12
+ export interface ImportOptions {
13
+ /** Path to template file */
14
+ templatePath: string;
15
+ /** Output directory (defaults to ./infra/) */
16
+ output?: string;
17
+ /** Force overwrite existing files */
18
+ force?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Import command result
23
+ */
24
+ export interface ImportResult {
25
+ /** Whether import succeeded */
26
+ success: boolean;
27
+ /** Generated files */
28
+ generatedFiles: string[];
29
+ /** Warning messages */
30
+ warnings: string[];
31
+ /** Error message if failed */
32
+ error?: string;
33
+ /** Detected lexicon */
34
+ lexicon?: string;
35
+ }
36
+
37
+ /**
38
+ * Resource category for organizing files
39
+ */
40
+ type ResourceCategory = "storage" | "compute" | "network" | "other";
41
+
42
+ /**
43
+ * Detect which plugin handles a template by asking each plugin.
44
+ * @param data - Parsed JSON object
45
+ * @param plugins - Loaded lexicon plugins
46
+ * @returns The matching plugin, or undefined if none match
47
+ */
48
+ function detectPlugin(data: unknown, plugins: LexiconPlugin[]): LexiconPlugin | undefined {
49
+ for (const plugin of plugins) {
50
+ if (plugin.detectTemplate?.(data)) {
51
+ return plugin;
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ /**
58
+ * Get the category for a resource type
59
+ */
60
+ function getResourceCategory(type: string): ResourceCategory {
61
+ const typeLower = type.toLowerCase();
62
+
63
+ // Storage resources
64
+ if (typeLower.includes("bucket") || typeLower.includes("storage") || typeLower.includes("queue")) {
65
+ return "storage";
66
+ }
67
+
68
+ // Compute resources
69
+ if (typeLower.includes("container") || typeLower.includes("service") || typeLower.includes("function")) {
70
+ return "compute";
71
+ }
72
+
73
+ // Network resources
74
+ if (typeLower.includes("loadbalancer") || typeLower.includes("lb") || typeLower.includes("network")) {
75
+ return "network";
76
+ }
77
+
78
+ return "other";
79
+ }
80
+
81
+ /**
82
+ * Organize resources into categories
83
+ */
84
+ function organizeByCategory(ir: TemplateIR): Map<ResourceCategory, ResourceIR[]> {
85
+ const categories = new Map<ResourceCategory, ResourceIR[]>();
86
+
87
+ for (const resource of ir.resources) {
88
+ const category = getResourceCategory(resource.type);
89
+ const existing = categories.get(category) ?? [];
90
+ existing.push(resource);
91
+ categories.set(category, existing);
92
+ }
93
+
94
+ return categories;
95
+ }
96
+
97
+ /**
98
+ * Generate organized files with separate modules
99
+ */
100
+ function generateOrganizedFiles(
101
+ ir: TemplateIR,
102
+ generator: TypeScriptGenerator,
103
+ ): GeneratedFile[] {
104
+ const files: GeneratedFile[] = [];
105
+ const categories = organizeByCategory(ir);
106
+ const exports: string[] = [];
107
+
108
+ // If all resources fit in one file, just generate main.ts
109
+ if (ir.resources.length <= 3) {
110
+ return generator.generate(ir);
111
+ }
112
+
113
+ // Generate files for each category
114
+ for (const [category, resources] of categories) {
115
+ if (resources.length === 0) continue;
116
+
117
+ const categoryIr: TemplateIR = {
118
+ parameters: category === "other" ? ir.parameters : [],
119
+ resources,
120
+ };
121
+
122
+ const generated = generator.generate(categoryIr);
123
+ const fileName = `${category}.ts`;
124
+
125
+ files.push({
126
+ path: fileName,
127
+ content: generated[0].content,
128
+ });
129
+
130
+ // Track exports
131
+ for (const resource of resources) {
132
+ const varName = resource.logicalId.charAt(0).toLowerCase() + resource.logicalId.slice(1);
133
+ exports.push(`export { ${varName} } from "./${category}";`);
134
+ }
135
+ }
136
+
137
+ // Handle parameters separately if not included in other category
138
+ if (ir.parameters.length > 0 && !categories.has("other")) {
139
+ const paramsIr: TemplateIR = {
140
+ parameters: ir.parameters,
141
+ resources: [],
142
+ };
143
+ const generated = generator.generate(paramsIr);
144
+ files.push({
145
+ path: "parameters.ts",
146
+ content: generated[0].content,
147
+ });
148
+
149
+ for (const param of ir.parameters) {
150
+ const varName = param.name.charAt(0).toLowerCase() + param.name.slice(1);
151
+ exports.push(`export { ${varName} } from "./parameters";`);
152
+ }
153
+ }
154
+
155
+ // Generate index.ts
156
+ if (exports.length > 0) {
157
+ files.push({
158
+ path: "index.ts",
159
+ content: exports.join("\n") + "\n",
160
+ });
161
+ }
162
+
163
+ return files;
164
+ }
165
+
166
+ /**
167
+ * Execute the import command
168
+ */
169
+ export async function importCommand(options: ImportOptions): Promise<ImportResult> {
170
+ const templatePath = resolve(options.templatePath);
171
+ const outputDir = resolve(options.output ?? "./infra/");
172
+ const generatedFiles: string[] = [];
173
+ const warnings: string[] = [];
174
+
175
+ // Check if template exists
176
+ if (!existsSync(templatePath)) {
177
+ return {
178
+ success: false,
179
+ generatedFiles: [],
180
+ warnings: [],
181
+ error: `Template file not found: ${templatePath}`,
182
+ };
183
+ }
184
+
185
+ // Read template content
186
+ let content: string;
187
+ try {
188
+ content = readFileSync(templatePath, "utf-8");
189
+ } catch (err) {
190
+ return {
191
+ success: false,
192
+ generatedFiles: [],
193
+ warnings: [],
194
+ error: `Failed to read template: ${err}`,
195
+ };
196
+ }
197
+
198
+ // Load plugins and detect lexicon
199
+ let data: unknown;
200
+ try {
201
+ data = JSON.parse(content);
202
+ } catch {
203
+ return {
204
+ success: false,
205
+ generatedFiles: [],
206
+ warnings: [],
207
+ error: "Template is not valid JSON.",
208
+ };
209
+ }
210
+
211
+ // Load plugins from project config, falling back to all installed lexicons
212
+ let plugins: LexiconPlugin[];
213
+ try {
214
+ const lexiconNames = await resolveProjectLexicons(resolve("."));
215
+ plugins = await loadPlugins(lexiconNames);
216
+ } catch {
217
+ plugins = [];
218
+ }
219
+
220
+ // If no plugins resolved (no config, no source files), try common lexicons
221
+ if (plugins.length === 0) {
222
+ try {
223
+ plugins = await loadPlugins(["aws"]);
224
+ } catch {
225
+ // No lexicons available at all
226
+ }
227
+ }
228
+
229
+ const plugin = detectPlugin(data, plugins);
230
+ if (!plugin) {
231
+ return {
232
+ success: false,
233
+ generatedFiles: [],
234
+ warnings: [],
235
+ error: "Could not detect template lexicon. No installed lexicon recognizes this template.",
236
+ };
237
+ }
238
+
239
+ const lexicon = plugin.name;
240
+
241
+ if (!plugin.templateParser || !plugin.templateGenerator) {
242
+ return {
243
+ success: false,
244
+ generatedFiles: [],
245
+ warnings: [],
246
+ error: `Lexicon "${plugin.name}" does not support template import.`,
247
+ lexicon,
248
+ };
249
+ }
250
+
251
+ // Parse template
252
+ let ir: TemplateIR;
253
+ try {
254
+ const parser = plugin.templateParser();
255
+ ir = parser.parse(content);
256
+ } catch (err) {
257
+ return {
258
+ success: false,
259
+ generatedFiles: [],
260
+ warnings: [],
261
+ error: `Failed to parse template: ${err}`,
262
+ };
263
+ }
264
+
265
+ const generator = plugin.templateGenerator();
266
+
267
+ // Check output directory
268
+ if (existsSync(outputDir) && !options.force) {
269
+ const files = require("fs").readdirSync(outputDir);
270
+ if (files.length > 0) {
271
+ warnings.push(`Output directory ${outputDir} is not empty. Use --force to overwrite.`);
272
+ }
273
+ }
274
+
275
+ // Create output directory
276
+ if (!existsSync(outputDir)) {
277
+ mkdirSync(outputDir, { recursive: true });
278
+ }
279
+
280
+ // Generate files
281
+ const files = generateOrganizedFiles(ir, generator);
282
+
283
+ // Write files
284
+ for (const file of files) {
285
+ const filePath = join(outputDir, file.path);
286
+ const dirPath = join(outputDir, file.path.split("/").slice(0, -1).join("/"));
287
+
288
+ if (dirPath && !existsSync(dirPath)) {
289
+ mkdirSync(dirPath, { recursive: true });
290
+ }
291
+
292
+ // Check for existing file
293
+ if (existsSync(filePath) && !options.force) {
294
+ warnings.push(`File ${file.path} already exists, skipping`);
295
+ continue;
296
+ }
297
+
298
+ writeFileSync(filePath, file.content);
299
+ generatedFiles.push(file.path);
300
+ }
301
+
302
+ return {
303
+ success: true,
304
+ generatedFiles,
305
+ warnings,
306
+ lexicon,
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Print import result
312
+ */
313
+ export function printImportResult(result: ImportResult): void {
314
+ if (!result.success) {
315
+ console.error(formatError({ message: result.error ?? "Import failed" }));
316
+ return;
317
+ }
318
+
319
+ for (const warning of result.warnings) {
320
+ console.error(formatWarning({ message: warning }));
321
+ }
322
+
323
+ if (result.lexicon) {
324
+ console.log(`Detected lexicon: ${result.lexicon}`);
325
+ }
326
+
327
+ if (result.generatedFiles.length > 0) {
328
+ console.log(formatSuccess("Generated files:"));
329
+ for (const file of result.generatedFiles) {
330
+ console.log(` ${file}`);
331
+ }
332
+ } else {
333
+ console.log("No files generated.");
334
+ }
335
+ }