@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,207 @@
1
+ import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, statSync } from "fs";
2
+ import { join, resolve } from "path";
3
+ import { formatSuccess, formatWarning, formatError } from "../format";
4
+ import { loadChantConfig } from "../../config";
5
+ import { loadPlugins } from "../plugins";
6
+
7
+ /**
8
+ * Update command options
9
+ */
10
+ export interface UpdateOptions {
11
+ /** Project directory (defaults to cwd) */
12
+ path: string;
13
+ }
14
+
15
+ /**
16
+ * Update command result
17
+ */
18
+ export interface UpdateResult {
19
+ /** Whether update succeeded */
20
+ success: boolean;
21
+ /** Synced items */
22
+ synced: string[];
23
+ /** Warning messages */
24
+ warnings: string[];
25
+ /** Error message if failed */
26
+ error?: string;
27
+ }
28
+
29
+ /**
30
+ * Copy directory contents recursively, filtering to .ts and .d.ts files
31
+ */
32
+ function copyTypeFiles(src: string, dest: string): number {
33
+ if (!existsSync(src)) return 0;
34
+ mkdirSync(dest, { recursive: true });
35
+
36
+ let count = 0;
37
+ const entries = readdirSync(src);
38
+
39
+ for (const entry of entries) {
40
+ const srcPath = join(src, entry);
41
+ const destPath = join(dest, entry);
42
+ const stat = statSync(srcPath);
43
+
44
+ if (stat.isDirectory()) {
45
+ // Skip node_modules, tests, examples
46
+ if (entry === "node_modules" || entry === "__test__") continue;
47
+ count += copyTypeFiles(srcPath, destPath);
48
+ } else if (
49
+ entry.endsWith(".d.ts") ||
50
+ entry.endsWith(".json")
51
+ ) {
52
+ cpSync(srcPath, destPath);
53
+ count++;
54
+ }
55
+ }
56
+
57
+ return count;
58
+ }
59
+
60
+ /**
61
+ * Resolve the path to a package in node_modules
62
+ */
63
+ function resolvePackagePath(packageName: string, projectDir: string): string | undefined {
64
+ // Try resolve from project dir
65
+ try {
66
+ const entryPoint = require.resolve(packageName, { paths: [projectDir] });
67
+ // Walk up from entry point to find package root
68
+ let dir = entryPoint;
69
+ while (dir !== "/") {
70
+ dir = join(dir, "..");
71
+ if (existsSync(join(dir, "package.json"))) {
72
+ const pkg = JSON.parse(require("fs").readFileSync(join(dir, "package.json"), "utf-8"));
73
+ if (pkg.name === packageName) return dir;
74
+ }
75
+ }
76
+ } catch {
77
+ // Package not installed
78
+ }
79
+
80
+ // Fallback: check common node_modules locations
81
+ const candidates = [
82
+ join(projectDir, "node_modules", ...packageName.split("/")),
83
+ join(projectDir, "..", "node_modules", ...packageName.split("/")),
84
+ ];
85
+
86
+ for (const candidate of candidates) {
87
+ if (existsSync(join(candidate, "package.json"))) {
88
+ return candidate;
89
+ }
90
+ }
91
+
92
+ return undefined;
93
+ }
94
+
95
+ /**
96
+ * Execute the update command — sync lexicon types into .chant/
97
+ */
98
+ export async function updateCommand(options: UpdateOptions): Promise<UpdateResult> {
99
+ const projectDir = resolve(options.path);
100
+ const synced: string[] = [];
101
+ const warnings: string[] = [];
102
+
103
+ // Load config to get lexicons
104
+ const { config } = await loadChantConfig(projectDir);
105
+ const lexicons = config.lexicons ?? [];
106
+
107
+ if (lexicons.length === 0) {
108
+ return {
109
+ success: false,
110
+ synced: [],
111
+ warnings: [],
112
+ error: "No lexicons configured. Add lexicons to chant.config.ts.",
113
+ };
114
+ }
115
+
116
+ const typesDir = join(projectDir, ".chant", "types");
117
+
118
+ // Sync core types
119
+ const corePkgPath = resolvePackagePath("@intentius/chant", projectDir);
120
+ if (corePkgPath) {
121
+ const coreDestDir = join(typesDir, "core");
122
+ const srcDir = join(corePkgPath, "src");
123
+ if (existsSync(srcDir)) {
124
+ const count = copyTypeFiles(srcDir, coreDestDir);
125
+ // Also copy package.json
126
+ if (existsSync(join(corePkgPath, "package.json"))) {
127
+ cpSync(join(corePkgPath, "package.json"), join(coreDestDir, "package.json"));
128
+ }
129
+ synced.push(`@intentius/chant (${count} files)`);
130
+ }
131
+ } else {
132
+ warnings.push("@intentius/chant not found in node_modules");
133
+ }
134
+
135
+ // Sync each lexicon
136
+ for (const lexicon of lexicons) {
137
+ const pkgName = `@intentius/chant-lexicon-${lexicon}`;
138
+ const pkgPath = resolvePackagePath(pkgName, projectDir);
139
+
140
+ if (!pkgPath) {
141
+ warnings.push(`${pkgName} not found in node_modules. Run "npm install" first.`);
142
+ continue;
143
+ }
144
+
145
+ const lexiconDestDir = join(typesDir, `lexicon-${lexicon}`);
146
+ const srcDir = join(pkgPath, "src");
147
+ if (existsSync(srcDir)) {
148
+ const count = copyTypeFiles(srcDir, lexiconDestDir);
149
+ if (existsSync(join(pkgPath, "package.json"))) {
150
+ cpSync(join(pkgPath, "package.json"), join(lexiconDestDir, "package.json"));
151
+ }
152
+ synced.push(`${pkgName} (${count} files)`);
153
+ } else {
154
+ warnings.push(`${pkgName} has no src/ directory`);
155
+ }
156
+ }
157
+
158
+ // Install skills from plugins
159
+ try {
160
+ const plugins = await loadPlugins(lexicons);
161
+ for (const plugin of plugins) {
162
+ if (plugin.skills) {
163
+ const skills = plugin.skills();
164
+ if (skills.length > 0) {
165
+ const skillsDir = join(projectDir, ".chant", "skills", plugin.name);
166
+ mkdirSync(skillsDir, { recursive: true });
167
+ for (const skill of skills) {
168
+ writeFileSync(join(skillsDir, `${skill.name}.md`), skill.content);
169
+ }
170
+ synced.push(`${plugin.name} skills (${skills.length})`);
171
+ }
172
+ }
173
+ }
174
+ } catch {
175
+ // Skills are optional — don't fail the update if plugin loading fails
176
+ warnings.push("Could not load plugins for skill installation");
177
+ }
178
+
179
+ return {
180
+ success: true,
181
+ synced,
182
+ warnings,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Print update result
188
+ */
189
+ export function printUpdateResult(result: UpdateResult): void {
190
+ if (!result.success) {
191
+ console.error(formatError({ message: result.error ?? "Update failed" }));
192
+ return;
193
+ }
194
+
195
+ for (const warning of result.warnings) {
196
+ console.error(formatWarning({ message: warning }));
197
+ }
198
+
199
+ if (result.synced.length > 0) {
200
+ console.log(formatSuccess("Synced types:"));
201
+ for (const item of result.synced) {
202
+ console.log(` ${item}`);
203
+ }
204
+ } else {
205
+ console.log("No types synced.");
206
+ }
207
+ }
@@ -0,0 +1,255 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { checkConflicts } from "./conflict-check";
3
+ import type { LexiconPlugin } from "../lexicon";
4
+
5
+ const mockSerializer = { name: "test", serialize: () => ({}) } as any;
6
+
7
+ const noopAsync = async () => {};
8
+
9
+ function makePlugin(
10
+ name: string,
11
+ opts: {
12
+ rules?: { id: string }[];
13
+ skills?: { name: string }[];
14
+ mcpTools?: { name: string }[];
15
+ mcpResources?: { uri: string }[];
16
+ } = {},
17
+ ): LexiconPlugin {
18
+ const plugin: LexiconPlugin = {
19
+ name,
20
+ serializer: { ...mockSerializer, name },
21
+ generate: noopAsync,
22
+ validate: noopAsync,
23
+ coverage: noopAsync,
24
+ package: noopAsync,
25
+ rollback: noopAsync,
26
+ };
27
+
28
+ if (opts.rules) {
29
+ const rules = opts.rules.map((r) => ({
30
+ id: r.id,
31
+ severity: "error" as const,
32
+ category: "correctness" as const,
33
+ check: () => [],
34
+ }));
35
+ (plugin as any).lintRules = () => rules;
36
+ }
37
+
38
+ if (opts.skills) {
39
+ const skills = opts.skills.map((s) => ({
40
+ name: s.name,
41
+ description: "",
42
+ content: "",
43
+ }));
44
+ (plugin as any).skills = () => skills;
45
+ }
46
+
47
+ if (opts.mcpTools) {
48
+ const tools = opts.mcpTools.map((t) => ({
49
+ name: t.name,
50
+ description: "",
51
+ inputSchema: { type: "object" as const, properties: {} },
52
+ handler: async () => "",
53
+ }));
54
+ (plugin as any).mcpTools = () => tools;
55
+ }
56
+
57
+ if (opts.mcpResources) {
58
+ const resources = opts.mcpResources.map((r) => ({
59
+ uri: r.uri,
60
+ name: "",
61
+ description: "",
62
+ handler: async () => "",
63
+ }));
64
+ (plugin as any).mcpResources = () => resources;
65
+ }
66
+
67
+ return plugin;
68
+ }
69
+
70
+ describe("checkConflicts", () => {
71
+ // -----------------------------------------------------------------------
72
+ // No conflicts
73
+ // -----------------------------------------------------------------------
74
+
75
+ test("returns clean report when no conflicts exist", () => {
76
+ const plugins = [
77
+ makePlugin("aws", { rules: [{ id: "AWS001" }] }),
78
+ makePlugin("gcp", { rules: [{ id: "GCP001" }] }),
79
+ ];
80
+ const report = checkConflicts(plugins);
81
+ expect(report.conflicts).toHaveLength(0);
82
+ expect(report.warnings).toHaveLength(0);
83
+ });
84
+
85
+ test("handles plugins with no optional methods", () => {
86
+ const plugins = [
87
+ makePlugin("aws"),
88
+ makePlugin("gcp"),
89
+ ];
90
+ const report = checkConflicts(plugins);
91
+ expect(report.conflicts).toHaveLength(0);
92
+ expect(report.warnings).toHaveLength(0);
93
+ });
94
+
95
+ // -----------------------------------------------------------------------
96
+ // Rule ID conflicts (hard)
97
+ // -----------------------------------------------------------------------
98
+
99
+ test("detects duplicate rule IDs as hard conflicts", () => {
100
+ const plugins = [
101
+ makePlugin("aws", { rules: [{ id: "SHARED001" }, { id: "AWS001" }] }),
102
+ makePlugin("gcp", { rules: [{ id: "SHARED001" }, { id: "GCP001" }] }),
103
+ ];
104
+ const report = checkConflicts(plugins);
105
+ expect(report.conflicts).toHaveLength(1);
106
+ expect(report.conflicts[0]).toEqual({
107
+ type: "rule-id",
108
+ key: "SHARED001",
109
+ plugins: ["aws", "gcp"],
110
+ });
111
+ expect(report.warnings).toHaveLength(0);
112
+ });
113
+
114
+ // -----------------------------------------------------------------------
115
+ // Skill conflicts (warnings)
116
+ // -----------------------------------------------------------------------
117
+
118
+ test("detects duplicate skill names as warnings", () => {
119
+ const plugins = [
120
+ makePlugin("aws", { skills: [{ name: "scaffold" }] }),
121
+ makePlugin("gcp", { skills: [{ name: "scaffold" }] }),
122
+ ];
123
+ const report = checkConflicts(plugins);
124
+ expect(report.conflicts).toHaveLength(0);
125
+ expect(report.warnings).toHaveLength(1);
126
+ expect(report.warnings[0]).toEqual({
127
+ type: "skill-name",
128
+ key: "scaffold",
129
+ plugins: ["aws", "gcp"],
130
+ });
131
+ });
132
+
133
+ // -----------------------------------------------------------------------
134
+ // MCP tool conflicts (warnings)
135
+ // -----------------------------------------------------------------------
136
+
137
+ test("detects duplicate MCP tool names as warnings", () => {
138
+ const plugins = [
139
+ makePlugin("aws", { mcpTools: [{ name: "diff" }] }),
140
+ makePlugin("gcp", { mcpTools: [{ name: "diff" }] }),
141
+ ];
142
+ const report = checkConflicts(plugins);
143
+ expect(report.conflicts).toHaveLength(0);
144
+ expect(report.warnings).toHaveLength(1);
145
+ expect(report.warnings[0]).toEqual({
146
+ type: "mcp-tool",
147
+ key: "diff",
148
+ plugins: ["aws", "gcp"],
149
+ });
150
+ });
151
+
152
+ test("no MCP tool conflict when names differ", () => {
153
+ const plugins = [
154
+ makePlugin("aws", { mcpTools: [{ name: "diff" }] }),
155
+ makePlugin("gcp", { mcpTools: [{ name: "deploy" }] }),
156
+ ];
157
+ const report = checkConflicts(plugins);
158
+ expect(report.warnings.filter((w) => w.type === "mcp-tool")).toHaveLength(0);
159
+ });
160
+
161
+ test("MCP tool conflict across three plugins", () => {
162
+ const plugins = [
163
+ makePlugin("aws", { mcpTools: [{ name: "scan" }] }),
164
+ makePlugin("gcp", { mcpTools: [{ name: "scan" }] }),
165
+ makePlugin("azure", { mcpTools: [{ name: "scan" }] }),
166
+ ];
167
+ const report = checkConflicts(plugins);
168
+ const toolWarnings = report.warnings.filter((w) => w.type === "mcp-tool");
169
+ expect(toolWarnings).toHaveLength(1);
170
+ expect(toolWarnings[0].plugins).toEqual(["aws", "gcp", "azure"]);
171
+ });
172
+
173
+ // -----------------------------------------------------------------------
174
+ // MCP resource conflicts (warnings)
175
+ // -----------------------------------------------------------------------
176
+
177
+ test("detects duplicate MCP resource URIs as warnings", () => {
178
+ const plugins = [
179
+ makePlugin("aws", { mcpResources: [{ uri: "catalog" }] }),
180
+ makePlugin("gcp", { mcpResources: [{ uri: "catalog" }] }),
181
+ ];
182
+ const report = checkConflicts(plugins);
183
+ expect(report.conflicts).toHaveLength(0);
184
+ expect(report.warnings).toHaveLength(1);
185
+ expect(report.warnings[0]).toEqual({
186
+ type: "mcp-resource",
187
+ key: "catalog",
188
+ plugins: ["aws", "gcp"],
189
+ });
190
+ });
191
+
192
+ test("no MCP resource conflict when URIs differ", () => {
193
+ const plugins = [
194
+ makePlugin("aws", { mcpResources: [{ uri: "aws-catalog" }] }),
195
+ makePlugin("gcp", { mcpResources: [{ uri: "gcp-catalog" }] }),
196
+ ];
197
+ const report = checkConflicts(plugins);
198
+ expect(report.warnings.filter((w) => w.type === "mcp-resource")).toHaveLength(0);
199
+ });
200
+
201
+ // -----------------------------------------------------------------------
202
+ // Combined
203
+ // -----------------------------------------------------------------------
204
+
205
+ test("detects multiple conflict types simultaneously", () => {
206
+ const plugins = [
207
+ makePlugin("aws", {
208
+ rules: [{ id: "DUPE_RULE" }],
209
+ skills: [{ name: "scaffold" }],
210
+ }),
211
+ makePlugin("gcp", {
212
+ rules: [{ id: "DUPE_RULE" }],
213
+ skills: [{ name: "scaffold" }],
214
+ }),
215
+ ];
216
+ const report = checkConflicts(plugins);
217
+ expect(report.conflicts).toHaveLength(1);
218
+ expect(report.conflicts[0].type).toBe("rule-id");
219
+ expect(report.warnings).toHaveLength(1);
220
+ expect(report.warnings.map((w) => w.type)).toContain("skill-name");
221
+ });
222
+
223
+ test("detects all four conflict types at once", () => {
224
+ const plugins = [
225
+ makePlugin("aws", {
226
+ rules: [{ id: "SHARED" }],
227
+ skills: [{ name: "scaffold" }],
228
+ mcpTools: [{ name: "scan" }],
229
+ mcpResources: [{ uri: "catalog" }],
230
+ }),
231
+ makePlugin("gcp", {
232
+ rules: [{ id: "SHARED" }],
233
+ skills: [{ name: "scaffold" }],
234
+ mcpTools: [{ name: "scan" }],
235
+ mcpResources: [{ uri: "catalog" }],
236
+ }),
237
+ ];
238
+ const report = checkConflicts(plugins);
239
+ expect(report.conflicts).toHaveLength(1);
240
+ expect(report.warnings).toHaveLength(3);
241
+ const warningTypes = report.warnings.map((w) => w.type).sort();
242
+ expect(warningTypes).toEqual(["mcp-resource", "mcp-tool", "skill-name"]);
243
+ });
244
+
245
+ test("detects conflict across three plugins", () => {
246
+ const plugins = [
247
+ makePlugin("aws", { rules: [{ id: "SHARED" }] }),
248
+ makePlugin("gcp", { rules: [{ id: "SHARED" }] }),
249
+ makePlugin("azure", { rules: [{ id: "SHARED" }] }),
250
+ ];
251
+ const report = checkConflicts(plugins);
252
+ expect(report.conflicts).toHaveLength(1);
253
+ expect(report.conflicts[0].plugins).toEqual(["aws", "gcp", "azure"]);
254
+ });
255
+ });
@@ -0,0 +1,89 @@
1
+ import type { LexiconPlugin } from "../lexicon";
2
+
3
+ export interface ConflictEntry {
4
+ type: "rule-id" | "skill-name" | "mcp-tool" | "mcp-resource";
5
+ key: string;
6
+ plugins: string[];
7
+ }
8
+
9
+ export interface ConflictReport {
10
+ conflicts: ConflictEntry[];
11
+ warnings: ConflictEntry[];
12
+ }
13
+
14
+ /**
15
+ * Check loaded lexicon plugins for cross-lexicon conflicts.
16
+ *
17
+ * - Duplicate rule IDs are hard conflicts (should cause a throw at load time).
18
+ * - Duplicate skill, MCP tool, or MCP resource names are soft conflicts (warnings).
19
+ */
20
+ export function checkConflicts(plugins: LexiconPlugin[]): ConflictReport {
21
+ const conflicts: ConflictEntry[] = [];
22
+ const warnings: ConflictEntry[] = [];
23
+
24
+ // Check rule ID conflicts (hard conflict)
25
+ const ruleIds = new Map<string, string[]>();
26
+ for (const plugin of plugins) {
27
+ const rules = plugin.lintRules?.() ?? [];
28
+ for (const rule of rules) {
29
+ const existing = ruleIds.get(rule.id) ?? [];
30
+ existing.push(plugin.name);
31
+ ruleIds.set(rule.id, existing);
32
+ }
33
+ }
34
+ for (const [id, owners] of ruleIds) {
35
+ if (owners.length > 1) {
36
+ conflicts.push({ type: "rule-id", key: id, plugins: owners });
37
+ }
38
+ }
39
+
40
+ // Check skill name conflicts (warning)
41
+ const skillNames = new Map<string, string[]>();
42
+ for (const plugin of plugins) {
43
+ const skills = plugin.skills?.() ?? [];
44
+ for (const skill of skills) {
45
+ const existing = skillNames.get(skill.name) ?? [];
46
+ existing.push(plugin.name);
47
+ skillNames.set(skill.name, existing);
48
+ }
49
+ }
50
+ for (const [name, owners] of skillNames) {
51
+ if (owners.length > 1) {
52
+ warnings.push({ type: "skill-name", key: name, plugins: owners });
53
+ }
54
+ }
55
+
56
+ // Check MCP tool name conflicts (warning)
57
+ const mcpToolNames = new Map<string, string[]>();
58
+ for (const plugin of plugins) {
59
+ const tools = plugin.mcpTools?.() ?? [];
60
+ for (const tool of tools) {
61
+ const existing = mcpToolNames.get(tool.name) ?? [];
62
+ existing.push(plugin.name);
63
+ mcpToolNames.set(tool.name, existing);
64
+ }
65
+ }
66
+ for (const [name, owners] of mcpToolNames) {
67
+ if (owners.length > 1) {
68
+ warnings.push({ type: "mcp-tool", key: name, plugins: owners });
69
+ }
70
+ }
71
+
72
+ // Check MCP resource URI conflicts (warning)
73
+ const mcpResourceUris = new Map<string, string[]>();
74
+ for (const plugin of plugins) {
75
+ const resources = plugin.mcpResources?.() ?? [];
76
+ for (const resource of resources) {
77
+ const existing = mcpResourceUris.get(resource.uri) ?? [];
78
+ existing.push(plugin.name);
79
+ mcpResourceUris.set(resource.uri, existing);
80
+ }
81
+ }
82
+ for (const [uri, owners] of mcpResourceUris) {
83
+ if (owners.length > 1) {
84
+ warnings.push({ type: "mcp-resource", key: uri, plugins: owners });
85
+ }
86
+ }
87
+
88
+ return { conflicts, warnings };
89
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Debug logger — outputs to stderr when CHANT_DEBUG env var or --verbose/-v flag is set.
3
+ */
4
+ export function debug(...args: unknown[]): void {
5
+ if (process.env.CHANT_DEBUG || process.argv.includes("--verbose") || process.argv.includes("-v")) {
6
+ console.error("[chant:debug]", ...args);
7
+ }
8
+ }
@@ -0,0 +1,140 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import {
3
+ formatError,
4
+ formatWarning,
5
+ formatSuccess,
6
+ formatInfo,
7
+ formatBold,
8
+ } from "./format";
9
+
10
+ describe("formatError", () => {
11
+ // Save original env
12
+ const originalNoColor = process.env.NO_COLOR;
13
+
14
+ beforeEach(() => {
15
+ // Enable colors for testing
16
+ delete process.env.NO_COLOR;
17
+ });
18
+
19
+ afterEach(() => {
20
+ // Restore original env
21
+ if (originalNoColor !== undefined) {
22
+ process.env.NO_COLOR = originalNoColor;
23
+ } else {
24
+ delete process.env.NO_COLOR;
25
+ }
26
+ });
27
+
28
+ test("formats error with file, line, and column", () => {
29
+ const result = formatError({
30
+ file: "test.ts",
31
+ line: 10,
32
+ column: 5,
33
+ message: "Something went wrong",
34
+ });
35
+
36
+ expect(result).toContain("test.ts:10:5");
37
+ expect(result).toContain("error");
38
+ expect(result).toContain("Something went wrong");
39
+ });
40
+
41
+ test("formats error with file and line only", () => {
42
+ const result = formatError({
43
+ file: "test.ts",
44
+ line: 10,
45
+ message: "Something went wrong",
46
+ });
47
+
48
+ expect(result).toContain("test.ts:10");
49
+ expect(result).not.toContain("test.ts:10:");
50
+ expect(result).toContain("error");
51
+ });
52
+
53
+ test("formats error with file only", () => {
54
+ const result = formatError({
55
+ file: "test.ts",
56
+ message: "Something went wrong",
57
+ });
58
+
59
+ expect(result).toContain("test.ts");
60
+ expect(result).toContain("error");
61
+ });
62
+
63
+ test("formats error with message only", () => {
64
+ const result = formatError({
65
+ message: "Something went wrong",
66
+ });
67
+
68
+ expect(result).toContain("error");
69
+ expect(result).toContain("Something went wrong");
70
+ // Should not have file location prefix (no " - " before error)
71
+ expect(result).not.toContain(" - ");
72
+ });
73
+
74
+ test("respects NO_COLOR environment variable", () => {
75
+ process.env.NO_COLOR = "1";
76
+
77
+ const result = formatError({
78
+ file: "test.ts",
79
+ line: 10,
80
+ message: "Something went wrong",
81
+ });
82
+
83
+ // Should not contain ANSI escape codes
84
+ expect(result).not.toContain("\x1b[");
85
+ expect(result).toContain("test.ts:10");
86
+ expect(result).toContain("error");
87
+ });
88
+ });
89
+
90
+ describe("formatWarning", () => {
91
+ beforeEach(() => {
92
+ delete process.env.NO_COLOR;
93
+ });
94
+
95
+ test("formats warning with location", () => {
96
+ const result = formatWarning({
97
+ file: "test.ts",
98
+ line: 10,
99
+ column: 5,
100
+ message: "Potential issue",
101
+ });
102
+
103
+ expect(result).toContain("test.ts:10:5");
104
+ expect(result).toContain("warning");
105
+ expect(result).toContain("Potential issue");
106
+ });
107
+
108
+ test("formats warning with message only", () => {
109
+ const result = formatWarning({
110
+ message: "Potential issue",
111
+ });
112
+
113
+ expect(result).toContain("warning");
114
+ expect(result).toContain("Potential issue");
115
+ });
116
+ });
117
+
118
+ describe("formatSuccess", () => {
119
+ test("formats success message", () => {
120
+ const result = formatSuccess("Build complete");
121
+
122
+ expect(result).toContain("Build complete");
123
+ });
124
+ });
125
+
126
+ describe("formatInfo", () => {
127
+ test("formats info message", () => {
128
+ const result = formatInfo("Processing files...");
129
+
130
+ expect(result).toContain("Processing files...");
131
+ });
132
+ });
133
+
134
+ describe("formatBold", () => {
135
+ test("formats bold text", () => {
136
+ const result = formatBold("Important");
137
+
138
+ expect(result).toContain("Important");
139
+ });
140
+ });