@intentius/chant 0.0.5 → 0.0.9

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 (91) hide show
  1. package/bin/chant +20 -0
  2. package/package.json +18 -17
  3. package/src/bench.test.ts +1 -1
  4. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
  5. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
  6. package/src/cli/commands/build.ts +1 -2
  7. package/src/cli/commands/doctor.ts +8 -3
  8. package/src/cli/commands/import.ts +2 -2
  9. package/src/cli/commands/init-lexicon.test.ts +0 -3
  10. package/src/cli/commands/init-lexicon.ts +1 -79
  11. package/src/cli/commands/init.test.ts +44 -4
  12. package/src/cli/commands/init.ts +69 -26
  13. package/src/cli/commands/lint.ts +27 -13
  14. package/src/cli/commands/list.ts +2 -2
  15. package/src/cli/commands/update.ts +5 -3
  16. package/src/cli/conflict-check.test.ts +0 -1
  17. package/src/cli/handlers/dev.ts +1 -9
  18. package/src/cli/handlers/init.ts +1 -0
  19. package/src/cli/lsp/server.ts +1 -1
  20. package/src/cli/main.ts +17 -3
  21. package/src/cli/mcp/server.test.ts +233 -4
  22. package/src/cli/mcp/server.ts +6 -0
  23. package/src/cli/mcp/tools/explain.ts +134 -0
  24. package/src/cli/mcp/tools/scaffold.ts +125 -0
  25. package/src/cli/mcp/tools/search.ts +98 -0
  26. package/src/cli/registry.ts +1 -0
  27. package/src/cli/reporters/stylish.test.ts +212 -1
  28. package/src/cli/reporters/stylish.ts +133 -36
  29. package/src/codegen/docs-rules.test.ts +112 -0
  30. package/src/codegen/docs-rules.ts +129 -0
  31. package/src/codegen/docs.ts +3 -1
  32. package/src/codegen/generate-registry.test.ts +1 -1
  33. package/src/codegen/generate-registry.ts +2 -3
  34. package/src/codegen/generate-typescript.test.ts +70 -6
  35. package/src/codegen/generate-typescript.ts +15 -9
  36. package/src/codegen/generate.ts +1 -12
  37. package/src/codegen/package.ts +1 -1
  38. package/src/codegen/typecheck.ts +6 -11
  39. package/src/composite.test.ts +83 -16
  40. package/src/composite.ts +7 -5
  41. package/src/config.ts +4 -0
  42. package/src/detectLexicon.test.ts +2 -2
  43. package/src/discovery/collect.test.ts +2 -2
  44. package/src/discovery/collect.ts +1 -1
  45. package/src/index.ts +2 -1
  46. package/src/lexicon-integrity.ts +5 -4
  47. package/src/lexicon-schema.ts +8 -0
  48. package/src/lexicon.ts +15 -7
  49. package/src/lint/config.ts +8 -6
  50. package/src/lint/declarative.ts +6 -0
  51. package/src/lint/engine.test.ts +287 -11
  52. package/src/lint/engine.ts +101 -23
  53. package/src/lint/rule-registry.test.ts +112 -0
  54. package/src/lint/rule-registry.ts +118 -0
  55. package/src/lint/rule.ts +8 -0
  56. package/src/lint/rules/cor017-composite-name-match.ts +2 -1
  57. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
  58. package/src/lint/rules/declarable-naming-convention.ts +1 -0
  59. package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
  60. package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
  61. package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
  62. package/src/lint/rules/evl004-spread-non-const.ts +1 -0
  63. package/src/lint/rules/evl005-resource-block-body.ts +1 -0
  64. package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
  65. package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
  66. package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
  67. package/src/lint/rules/export-required.ts +1 -0
  68. package/src/lint/rules/file-declarable-limit.ts +1 -0
  69. package/src/lint/rules/flat-declarations.test.ts +8 -7
  70. package/src/lint/rules/flat-declarations.ts +2 -3
  71. package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
  72. package/src/lint/rules/no-redundant-type-import.ts +1 -0
  73. package/src/lint/rules/no-redundant-value-cast.ts +1 -0
  74. package/src/lint/rules/no-string-ref.ts +1 -0
  75. package/src/lint/rules/no-unused-declarable-import.ts +1 -0
  76. package/src/lint/rules/no-unused-declarable.test.ts +8 -0
  77. package/src/lint/rules/no-unused-declarable.ts +4 -0
  78. package/src/lint/rules/single-concern-file.ts +1 -0
  79. package/src/lsp/lexicon-providers.ts +7 -0
  80. package/src/lsp/types.ts +1 -0
  81. package/src/resource-attributes.test.ts +79 -0
  82. package/src/resource-attributes.ts +42 -0
  83. package/src/runtime-adapter.ts +158 -0
  84. package/src/runtime.ts +4 -3
  85. package/src/serializer-walker.test.ts +0 -9
  86. package/src/serializer-walker.ts +1 -3
  87. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  88. package/src/codegen/case.test.ts +0 -30
  89. package/src/codegen/case.ts +0 -11
  90. package/src/codegen/rollback.test.ts +0 -92
  91. package/src/codegen/rollback.ts +0 -115
package/bin/chant ADDED
@@ -0,0 +1,20 @@
1
+ #!/bin/sh
2
+ # Cross-runtime CLI wrapper. Tries bun first, falls back to npx tsx.
3
+ set -e
4
+
5
+ # Resolve symlinks to find the real script location
6
+ SELF="$0"
7
+ while [ -L "$SELF" ]; do
8
+ DIR="$(cd "$(dirname "$SELF")" && pwd)"
9
+ SELF="$(readlink "$SELF")"
10
+ # Handle relative symlink targets
11
+ case "$SELF" in /*) ;; *) SELF="$DIR/$SELF" ;; esac
12
+ done
13
+ SCRIPT_DIR="$(cd "$(dirname "$SELF")" && pwd)"
14
+ MAIN_TS="$SCRIPT_DIR/../src/cli/main.ts"
15
+
16
+ if command -v bun >/dev/null 2>&1; then
17
+ exec bun "$MAIN_TS" "$@"
18
+ else
19
+ exec npx tsx "$MAIN_TS" "$@"
20
+ fi
package/package.json CHANGED
@@ -1,23 +1,24 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.0.5",
3
+ "version": "0.0.9",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
- "files": ["src/"],
6
+ "files": ["src/", "bin/"],
7
7
  "publishConfig": {
8
- "access": "public"
9
- },
10
- "bin": {
11
- "chant": "./src/cli/main.ts"
12
- },
13
- "exports": {
14
- ".": "./src/index.ts",
15
- "./cli": "./src/cli/index.ts",
16
- "./cli/*": "./src/cli/*",
17
- "./*": "./src/*"
18
- },
19
- "dependencies": {
20
- "fflate": "^0.8.2",
21
- "zod": "^4.3.6"
22
- }
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "chant": "./bin/chant"
12
+ },
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./cli": "./src/cli/index.ts",
16
+ "./cli/*": "./src/cli/*",
17
+ "./*": "./src/*"
18
+ },
19
+ "dependencies": {
20
+ "fflate": "^0.8.2",
21
+ "picomatch": "^4.0.3",
22
+ "zod": "^4.3.6"
23
+ }
23
24
  }
package/src/bench.test.ts CHANGED
@@ -131,7 +131,7 @@ describe("performance benchmarks", () => {
131
131
  for (const size of sizes) {
132
132
  await withTestDir(async (dir) => {
133
133
  const files = await generateFixture(dir, size);
134
- const { avg } = await benchmark(() => runLint(files, coreRules), 3);
134
+ const { avg } = await benchmark(() => runLint(files, coreRules).then(r => r.diagnostics), 3);
135
135
  output.push(` ${pad(size.name, 10)} ${fmt(avg)} (avg of 3 runs)`);
136
136
  });
137
137
  }
@@ -42,31 +42,6 @@ export const fixturePlugin: LexiconPlugin = {
42
42
  console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
43
43
  },
44
44
 
45
- async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
46
- const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
47
- const { join, dirname } = await import("path");
48
- const { fileURLToPath } = await import("url");
49
-
50
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
51
- const snapshotsDir = join(pkgDir, ".snapshots");
52
-
53
- if (options?.restore) {
54
- const generatedDir = join(pkgDir, "src", "generated");
55
- restoreSnapshot(String(options.restore), generatedDir);
56
- console.error(`Restored snapshot: ${options.restore}`);
57
- } else {
58
- const snapshots = listSnapshots(snapshotsDir);
59
- if (snapshots.length === 0) {
60
- console.error("No snapshots available.");
61
- } else {
62
- console.error(`Available snapshots (${snapshots.length}):`);
63
- for (const s of snapshots) {
64
- console.error(` ${s.timestamp} ${s.resources} resources ${s.path}`);
65
- }
66
- }
67
- }
68
- },
69
-
70
45
  // ── Optional extensions (uncomment and implement as needed) ───
71
46
 
72
47
  // lintRules(): LintRule[] {
@@ -152,31 +152,6 @@ export const fixturePlugin: LexiconPlugin = {
152
152
  console.error(\`Packaged \${stats.resources} resources, \${stats.ruleCount} rules, \${stats.skillCount} skills\`);
153
153
  },
154
154
 
155
- async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
156
- const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
157
- const { join, dirname } = await import("path");
158
- const { fileURLToPath } = await import("url");
159
-
160
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
161
- const snapshotsDir = join(pkgDir, ".snapshots");
162
-
163
- if (options?.restore) {
164
- const generatedDir = join(pkgDir, "src", "generated");
165
- restoreSnapshot(String(options.restore), generatedDir);
166
- console.error(\`Restored snapshot: \${options.restore}\`);
167
- } else {
168
- const snapshots = listSnapshots(snapshotsDir);
169
- if (snapshots.length === 0) {
170
- console.error("No snapshots available.");
171
- } else {
172
- console.error(\`Available snapshots (\${snapshots.length}):\`);
173
- for (const s of snapshots) {
174
- console.error(\` \${s.timestamp} \${s.resources} resources \${s.path}\`);
175
- }
176
- }
177
- }
178
- },
179
-
180
155
  // ── Optional extensions (uncomment and implement as needed) ───
181
156
 
182
157
  // lintRules(): LintRule[] {
@@ -5,7 +5,7 @@ import { runPostSynthChecks } from "../../lint/post-synth";
5
5
  import type { PostSynthCheck } from "../../lint/post-synth";
6
6
  import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
7
7
  import { writeFileSync } from "fs";
8
- import { resolve } from "path";
8
+ import { resolve, dirname, join } from "path";
9
9
  import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
10
10
 
11
11
  /**
@@ -172,7 +172,6 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
172
172
 
173
173
  // Write additional files (e.g. nested stack templates) alongside the primary output
174
174
  if (additionalFiles.size > 0) {
175
- const { dirname, join } = require("path");
176
175
  const outputDir = dirname(outputPath);
177
176
  for (const [filename, content] of additionalFiles) {
178
177
  let fileContent = content;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "fs";
2
2
  import { execSync } from "child_process";
3
- import { join, resolve } from "path";
3
+ import { join, resolve, dirname } from "path";
4
+ import { fileURLToPath } from "url";
4
5
  import { checkVersionCompatibility } from "../../lexicon-manifest";
5
6
  import { debug } from "../debug";
6
7
  import { loadPlugins, resolveProjectLexicons } from "../plugins";
@@ -109,8 +110,12 @@ export async function doctorCommand(path: string): Promise<DoctorReport> {
109
110
  try {
110
111
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
111
112
  if (manifest.chantVersion) {
112
- // Use a placeholder current version for now
113
- const currentVersion = "0.1.0";
113
+ let currentVersion = "0.0.8";
114
+ try {
115
+ const pkgDir = dirname(dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))));
116
+ const corePkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf-8"));
117
+ currentVersion = corePkg.version ?? currentVersion;
118
+ } catch { /* fallback */ }
114
119
  if (!checkVersionCompatibility(manifest.chantVersion, currentVersion)) {
115
120
  checks.push({ name: `lexicon-${lex}-compat`, status: "warn", message: `Lexicon ${lex} requires chant ${manifest.chantVersion}` });
116
121
  } else {
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
2
2
  import { join, resolve, basename } from "path";
3
3
  import { formatSuccess, formatWarning, formatError } from "../format";
4
4
  import type { TemplateIR, ResourceIR, TemplateParser } from "../../import/parser";
@@ -266,7 +266,7 @@ export async function importCommand(options: ImportOptions): Promise<ImportResul
266
266
 
267
267
  // Check output directory
268
268
  if (existsSync(outputDir) && !options.force) {
269
- const files = require("fs").readdirSync(outputDir);
269
+ const files = readdirSync(outputDir);
270
270
  if (files.length > 0) {
271
271
  warnings.push(`Output directory ${outputDir} is not empty. Use --force to overwrite.`);
272
272
  }
@@ -40,7 +40,6 @@ describe("initLexiconCommand", () => {
40
40
  "src/codegen/generate-cli.ts",
41
41
  "src/codegen/naming.ts",
42
42
  "src/codegen/package.ts",
43
- "src/codegen/rollback.ts",
44
43
  "src/codegen/docs.ts",
45
44
  "src/spec/fetch.ts",
46
45
  "src/spec/parse.ts",
@@ -65,7 +64,6 @@ describe("initLexiconCommand", () => {
65
64
  "docs/src/content/docs/index.mdx",
66
65
  "src/generated/.gitkeep",
67
66
  "examples/getting-started/.gitkeep",
68
- ".snapshots/.gitkeep",
69
67
  ];
70
68
 
71
69
  for (const file of expectedFiles) {
@@ -83,7 +81,6 @@ describe("initLexiconCommand", () => {
83
81
  expect(pluginContent).toContain("async validate(");
84
82
  expect(pluginContent).toContain("async coverage(");
85
83
  expect(pluginContent).toContain("async package(");
86
- expect(pluginContent).toContain("async rollback(");
87
84
  });
88
85
 
89
86
  test("package name uses the provided lexicon name", async () => {
@@ -106,31 +106,6 @@ export const ${names.pluginVarName}: LexiconPlugin = {
106
106
  console.error(\`Packaged \${stats.resources} resources, \${stats.ruleCount} rules, \${stats.skillCount} skills\`);
107
107
  },
108
108
 
109
- async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
110
- const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
111
- const { join, dirname } = await import("path");
112
- const { fileURLToPath } = await import("url");
113
-
114
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
115
- const snapshotsDir = join(pkgDir, ".snapshots");
116
-
117
- if (options?.restore) {
118
- const generatedDir = join(pkgDir, "src", "generated");
119
- restoreSnapshot(String(options.restore), generatedDir);
120
- console.error(\`Restored snapshot: \${options.restore}\`);
121
- } else {
122
- const snapshots = listSnapshots(snapshotsDir);
123
- if (snapshots.length === 0) {
124
- console.error("No snapshots available.");
125
- } else {
126
- console.error(\`Available snapshots (\${snapshots.length}):\`);
127
- for (const s of snapshots) {
128
- console.error(\` \${s.timestamp} \${s.resources} resources \${s.path}\`);
129
- }
130
- }
131
- }
132
- },
133
-
134
109
  // ── Optional extensions (uncomment and implement as needed) ───
135
110
 
136
111
  // lintRules(): LintRule[] {
@@ -382,55 +357,6 @@ export async function packageLexicon(options?: { verbose?: boolean; force?: bool
382
357
  `;
383
358
  }
384
359
 
385
- function generateCodegenRollbackTs(): string {
386
- return `import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, cpSync } from "fs";
387
- import { join, basename } from "path";
388
-
389
- export interface Snapshot {
390
- timestamp: string;
391
- resources: number;
392
- path: string;
393
- }
394
-
395
- /**
396
- * List available generation snapshots.
397
- */
398
- export function listSnapshots(snapshotsDir: string): Snapshot[] {
399
- if (!existsSync(snapshotsDir)) return [];
400
-
401
- return readdirSync(snapshotsDir)
402
- .filter((d) => !d.startsWith("."))
403
- .sort()
404
- .reverse()
405
- .map((dir) => {
406
- const fullPath = join(snapshotsDir, dir);
407
- const metaPath = join(fullPath, "meta.json");
408
- let resources = 0;
409
- if (existsSync(metaPath)) {
410
- try {
411
- const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
412
- resources = meta.resources ?? 0;
413
- } catch {}
414
- }
415
- return { timestamp: dir, resources, path: fullPath };
416
- });
417
- }
418
-
419
- /**
420
- * Restore a snapshot to the generated directory.
421
- */
422
- export function restoreSnapshot(timestamp: string, generatedDir: string): void {
423
- const snapshotsDir = join(generatedDir, "..", "..", ".snapshots");
424
- const snapshotDir = join(snapshotsDir, timestamp);
425
- if (!existsSync(snapshotDir)) {
426
- throw new Error(\`Snapshot not found: \${timestamp}\`);
427
- }
428
- mkdirSync(generatedDir, { recursive: true });
429
- cpSync(snapshotDir, generatedDir, { recursive: true });
430
- }
431
- `;
432
- }
433
-
434
360
  function generateCodegenDocsTs(name: string): string {
435
361
  return `import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
436
362
 
@@ -735,8 +661,7 @@ package: generate validate
735
661
  }
736
662
 
737
663
  function generateGitignore(): string {
738
- return `.snapshots/
739
- dist/
664
+ return `dist/
740
665
  node_modules/
741
666
  .cache/
742
667
  `;
@@ -899,7 +824,6 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
899
824
  "docs/src/content",
900
825
  "docs/src/content/docs",
901
826
  "examples/getting-started",
902
- ".snapshots",
903
827
  ];
904
828
 
905
829
  for (const dir of dirs) {
@@ -918,7 +842,6 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
918
842
  "src/codegen/generate-cli.ts": generateCodegenGenerateCliTs(),
919
843
  "src/codegen/naming.ts": generateCodegenNamingTs(),
920
844
  "src/codegen/package.ts": generateCodegenPackageTs(name),
921
- "src/codegen/rollback.ts": generateCodegenRollbackTs(),
922
845
  "src/codegen/docs.ts": generateCodegenDocsTs(name),
923
846
  "src/spec/fetch.ts": generateSpecFetchTs(),
924
847
  "src/spec/parse.ts": generateSpecParseTs(),
@@ -947,7 +870,6 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
947
870
  const gitkeeps = [
948
871
  "src/generated/.gitkeep",
949
872
  "examples/getting-started/.gitkeep",
950
- ".snapshots/.gitkeep",
951
873
  ];
952
874
 
953
875
  for (const gk of gitkeeps) {
@@ -176,7 +176,7 @@ describe("initCommand", () => {
176
176
  });
177
177
  });
178
178
 
179
- test("does not generate barrel file", async () => {
179
+ test("does not generate re-export file", async () => {
180
180
  await withTestDir(async (testDir) => {
181
181
  const options: InitOptions = {
182
182
  path: testDir,
@@ -187,9 +187,9 @@ describe("initCommand", () => {
187
187
 
188
188
  await initCommand(options);
189
189
 
190
- // No _.ts barrel — direct imports are used instead
191
- const barrelPath = join(testDir, "src", "_.ts");
192
- expect(existsSync(barrelPath)).toBe(false);
190
+ // No _.ts re-export — direct imports are used instead
191
+ const reexportPath = join(testDir, "src", "_.ts");
192
+ expect(existsSync(reexportPath)).toBe(false);
193
193
 
194
194
  // No index.ts either
195
195
  const indexPath = join(testDir, "src", "index.ts");
@@ -309,5 +309,45 @@ describe("initCommand", () => {
309
309
  expect(existsSync(newDir)).toBe(true);
310
310
  });
311
311
  });
312
+
313
+ test("gitlab init creates src files", async () => {
314
+ await withTestDir(async (testDir) => {
315
+ const options: InitOptions = {
316
+ path: testDir,
317
+ lexicon: "gitlab",
318
+ skipMcp: true,
319
+ skipInstall: true,
320
+ };
321
+
322
+ const result = await initCommand(options);
323
+
324
+ expect(result.success).toBe(true);
325
+ expect(result.createdFiles).toContain("src/config.ts");
326
+ expect(result.createdFiles).toContain("src/pipeline.ts");
327
+
328
+ const pkg = JSON.parse(readFileSync(join(testDir, "package.json"), "utf-8"));
329
+ expect(pkg.scripts.build).toContain("gitlab");
330
+ });
331
+ });
332
+
333
+ test("gitlab init with --template passes template to plugin", async () => {
334
+ await withTestDir(async (testDir) => {
335
+ const options: InitOptions = {
336
+ path: testDir,
337
+ lexicon: "gitlab",
338
+ template: "node-pipeline",
339
+ skipMcp: true,
340
+ skipInstall: true,
341
+ };
342
+
343
+ const result = await initCommand(options);
344
+
345
+ expect(result.success).toBe(true);
346
+ expect(result.createdFiles).toContain("src/pipeline.ts");
347
+
348
+ const pipeline = readFileSync(join(testDir, "src", "pipeline.ts"), "utf-8");
349
+ expect(pipeline).toContain("NodePipeline");
350
+ });
351
+ });
312
352
  });
313
353
 
@@ -1,11 +1,23 @@
1
1
  import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from "fs";
2
- import { join, resolve } from "path";
2
+ import { join, resolve, dirname } from "path";
3
+ import { fileURLToPath } from "url";
3
4
  import { homedir } from "os";
4
5
  import { createInterface } from "readline";
5
6
  import { z } from "zod";
6
7
  import { formatSuccess, formatWarning } from "../format";
7
8
  import { loadPlugin } from "../plugins";
8
9
 
10
+ /** Read the current chant package version from our own package.json. */
11
+ function getChantVersion(): string {
12
+ try {
13
+ const pkgDir = dirname(dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))));
14
+ const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf-8"));
15
+ return pkg.version ?? "0.0.8";
16
+ } catch {
17
+ return "0.0.8";
18
+ }
19
+ }
20
+
9
21
  /**
10
22
  * Schema for validating generated package.json — catches template bugs early.
11
23
  */
@@ -25,6 +37,8 @@ export interface InitOptions {
25
37
  path?: string;
26
38
  /** Lexicon to use */
27
39
  lexicon: string;
40
+ /** Template name (e.g. "node-pipeline", "docker-build") */
41
+ template?: string;
28
42
  /** Force init even in non-empty directory */
29
43
  force?: boolean;
30
44
  /** Skip MCP config generation */
@@ -81,20 +95,22 @@ function getMcpConfigDir(ide: "claude-code" | "cursor" | "generic"): string {
81
95
  /**
82
96
  * Generate package.json content
83
97
  */
84
- function generatePackageJson(lexicon: string): string {
98
+ function generatePackageJson(lexicon: string, extraScripts?: Record<string, string>): string {
99
+ const ver = getChantVersion();
85
100
  const dependencies: Record<string, string> = {
86
- "@intentius/chant": "^0.1.0",
87
- [`@intentius/chant-lexicon-${lexicon}`]: "^0.1.0",
101
+ "@intentius/chant": `^${ver}`,
102
+ [`@intentius/chant-lexicon-${lexicon}`]: `^${ver}`,
88
103
  };
89
104
 
90
105
  const pkg = {
91
106
  name: "chant-project",
92
- version: "0.1.0",
107
+ version: ver,
93
108
  type: "module" as const,
94
109
  scripts: {
95
110
  build: `chant build src --lexicon ${lexicon}`,
96
111
  lint: "chant lint src",
97
112
  dev: `chant build src --lexicon ${lexicon} --watch`,
113
+ ...extraScripts,
98
114
  },
99
115
  dependencies,
100
116
  devDependencies: {
@@ -301,6 +317,17 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
301
317
  mkdirSync(targetDir, { recursive: true });
302
318
  }
303
319
 
320
+ // Load plugin early to get template set (used for scripts + source files)
321
+ let templateSet: import("../../lexicon").InitTemplateSet | undefined;
322
+ try {
323
+ const plugin = await loadPlugin(options.lexicon);
324
+ if (plugin.initTemplates) {
325
+ templateSet = plugin.initTemplates(options.template);
326
+ }
327
+ } catch {
328
+ // Plugin not yet installed — no source files to scaffold
329
+ }
330
+
304
331
  // Create src directory
305
332
  const srcDir = join(targetDir, "src");
306
333
  if (!existsSync(srcDir)) {
@@ -310,7 +337,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
310
337
  // Generate package.json
311
338
  writeIfNotExists(
312
339
  join(targetDir, "package.json"),
313
- generatePackageJson(options.lexicon),
340
+ generatePackageJson(options.lexicon, templateSet?.scripts),
314
341
  "package.json",
315
342
  createdFiles,
316
343
  warnings,
@@ -343,24 +370,29 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
343
370
  warnings,
344
371
  );
345
372
 
346
- // Generate source files from plugin (if available)
347
- let sourceFiles: Record<string, string> = {};
348
- try {
349
- const plugin = await loadPlugin(options.lexicon);
350
- if (plugin.initTemplates) {
351
- sourceFiles = plugin.initTemplates();
373
+ // Write source files from plugin template set
374
+ if (templateSet) {
375
+ for (const [filename, content] of Object.entries(templateSet.src)) {
376
+ writeIfNotExists(
377
+ join(srcDir, filename),
378
+ content,
379
+ `src/${filename}`,
380
+ createdFiles,
381
+ warnings,
382
+ );
383
+ }
384
+ // Write root scaffold files (e.g. index.js, test.js, Dockerfile)
385
+ if (templateSet.root) {
386
+ for (const [filename, content] of Object.entries(templateSet.root)) {
387
+ writeIfNotExists(
388
+ join(targetDir, filename),
389
+ content,
390
+ filename,
391
+ createdFiles,
392
+ warnings,
393
+ );
394
+ }
352
395
  }
353
- } catch {
354
- // Plugin not yet installed — no source files to scaffold
355
- }
356
- for (const [filename, content] of Object.entries(sourceFiles)) {
357
- writeIfNotExists(
358
- join(srcDir, filename),
359
- content,
360
- `src/${filename}`,
361
- createdFiles,
362
- warnings,
363
- );
364
396
  }
365
397
 
366
398
  // Scaffold .chant/types/core/ with embedded type definitions
@@ -426,18 +458,29 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
426
458
  }
427
459
 
428
460
  // Install skills from the lexicon's plugin
461
+ // Write to both .chant/skills/ (chant's own location) and .claude/skills/ (Claude Code discovery)
429
462
  try {
430
463
  const plugin = await loadPlugin(options.lexicon);
431
464
  if (plugin.skills) {
432
465
  const skills = plugin.skills();
433
466
  if (skills.length > 0) {
434
- const skillsDir = join(targetDir, ".chant", "skills", options.lexicon);
435
- mkdirSync(skillsDir, { recursive: true });
467
+ // .chant/skills/ chant's own skill storage
468
+ const chantSkillsDir = join(targetDir, ".chant", "skills", options.lexicon);
469
+ mkdirSync(chantSkillsDir, { recursive: true });
436
470
  for (const skill of skills) {
437
- const skillPath = join(skillsDir, `${skill.name}.md`);
471
+ const skillPath = join(chantSkillsDir, `${skill.name}.md`);
438
472
  writeFileSync(skillPath, skill.content);
439
473
  createdFiles.push(`.chant/skills/${options.lexicon}/${skill.name}.md`);
440
474
  }
475
+
476
+ // .claude/skills/ — Claude Code skill discovery format
477
+ for (const skill of skills) {
478
+ const claudeSkillDir = join(targetDir, ".claude", "skills", skill.name);
479
+ mkdirSync(claudeSkillDir, { recursive: true });
480
+ const claudeSkillPath = join(claudeSkillDir, "SKILL.md");
481
+ writeFileSync(claudeSkillPath, skill.content);
482
+ createdFiles.push(`.claude/skills/${skill.name}/SKILL.md`);
483
+ }
441
484
  }
442
485
  }
443
486
  } catch {
@@ -243,19 +243,25 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
243
243
 
244
244
  // Run lint — use per-file rules when overrides are present
245
245
  let diagnostics: LintDiagnostic[];
246
+ let suppressed: Array<LintDiagnostic & { reason?: string }> = [];
246
247
  if (options.rules) {
247
- diagnostics = await runLint(files, options.rules, undefined);
248
+ const result = await runLint(files, options.rules, undefined);
249
+ diagnostics = result.diagnostics;
250
+ suppressed = result.suppressed;
248
251
  } else if (hasOverrides) {
249
252
  diagnostics = [];
250
253
  for (const file of files) {
251
254
  const relativePath = file.slice(infraPath.length + 1);
252
255
  const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
253
- const fileDiagnostics = await runLint([file], fileRules, ruleOptions);
254
- diagnostics.push(...fileDiagnostics);
256
+ const result = await runLint([file], fileRules, ruleOptions);
257
+ diagnostics.push(...result.diagnostics);
258
+ suppressed.push(...result.suppressed);
255
259
  }
256
260
  } else {
257
261
  const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
258
- diagnostics = await runLint(files, rules, ruleOptions);
262
+ const result = await runLint(files, rules, ruleOptions);
263
+ diagnostics = result.diagnostics;
264
+ suppressed = result.suppressed;
259
265
  }
260
266
 
261
267
  // Apply fixes if requested
@@ -277,23 +283,26 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
277
283
  }
278
284
 
279
285
  // Re-lint after fixes to get updated diagnostics
280
- let postFixDiagnostics: LintDiagnostic[];
281
286
  if (options.rules) {
282
- postFixDiagnostics = await runLint(files, options.rules, undefined);
287
+ const postResult = await runLint(files, options.rules, undefined);
288
+ diagnostics = postResult.diagnostics;
289
+ suppressed = postResult.suppressed;
283
290
  } else if (hasOverrides) {
284
- postFixDiagnostics = [];
291
+ diagnostics = [];
292
+ suppressed = [];
285
293
  for (const file of files) {
286
294
  const relativePath = file.slice(infraPath.length + 1);
287
295
  const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
288
- const fileDiagnostics = await runLint([file], fileRules, ruleOptions);
289
- postFixDiagnostics.push(...fileDiagnostics);
296
+ const postResult = await runLint([file], fileRules, ruleOptions);
297
+ diagnostics.push(...postResult.diagnostics);
298
+ suppressed.push(...postResult.suppressed);
290
299
  }
291
300
  } else {
292
301
  const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
293
- postFixDiagnostics = await runLint(files, rules, ruleOptions);
302
+ const postResult = await runLint(files, rules, ruleOptions);
303
+ diagnostics = postResult.diagnostics;
304
+ suppressed = postResult.suppressed;
294
305
  }
295
- diagnostics.length = 0;
296
- diagnostics.push(...postFixDiagnostics);
297
306
  }
298
307
 
299
308
  // Count errors and warnings
@@ -308,6 +317,11 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
308
317
  }
309
318
  }
310
319
 
320
+ // Collect all loaded rules for SARIF enrichment
321
+ const allLoadedRules = options.rules
322
+ ? options.rules
323
+ : [...allRules.values()];
324
+
311
325
  // Format output
312
326
  let output: string;
313
327
  switch (options.format) {
@@ -315,7 +329,7 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
315
329
  output = formatJson(diagnostics);
316
330
  break;
317
331
  case "sarif":
318
- output = formatSarif(diagnostics);
332
+ output = formatSarif(diagnostics, allLoadedRules, suppressed);
319
333
  break;
320
334
  case "stylish":
321
335
  default:
@@ -51,8 +51,8 @@ export async function listCommand(options: ListOptions): Promise<ListResult> {
51
51
  for (const [name, decl] of result.entities) {
52
52
  entities.push({
53
53
  name,
54
- lexicon: decl.lexicon,
55
- entityType: decl.entityType,
54
+ lexicon: decl.lexicon ?? "",
55
+ entityType: decl.entityType ?? "",
56
56
  kind: decl.kind ?? "resource",
57
57
  });
58
58
  }