@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.
- package/bin/chant +20 -0
- package/package.json +18 -17
- package/src/bench.test.ts +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
- package/src/cli/commands/build.ts +1 -2
- package/src/cli/commands/doctor.ts +8 -3
- package/src/cli/commands/import.ts +2 -2
- package/src/cli/commands/init-lexicon.test.ts +0 -3
- package/src/cli/commands/init-lexicon.ts +1 -79
- package/src/cli/commands/init.test.ts +44 -4
- package/src/cli/commands/init.ts +69 -26
- package/src/cli/commands/lint.ts +27 -13
- package/src/cli/commands/list.ts +2 -2
- package/src/cli/commands/update.ts +5 -3
- package/src/cli/conflict-check.test.ts +0 -1
- package/src/cli/handlers/dev.ts +1 -9
- package/src/cli/handlers/init.ts +1 -0
- package/src/cli/lsp/server.ts +1 -1
- package/src/cli/main.ts +17 -3
- package/src/cli/mcp/server.test.ts +233 -4
- package/src/cli/mcp/server.ts +6 -0
- package/src/cli/mcp/tools/explain.ts +134 -0
- package/src/cli/mcp/tools/scaffold.ts +125 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- package/src/cli/registry.ts +1 -0
- package/src/cli/reporters/stylish.test.ts +212 -1
- package/src/cli/reporters/stylish.ts +133 -36
- package/src/codegen/docs-rules.test.ts +112 -0
- package/src/codegen/docs-rules.ts +129 -0
- package/src/codegen/docs.ts +3 -1
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +2 -3
- package/src/codegen/generate-typescript.test.ts +70 -6
- package/src/codegen/generate-typescript.ts +15 -9
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/package.ts +1 -1
- package/src/codegen/typecheck.ts +6 -11
- package/src/composite.test.ts +83 -16
- package/src/composite.ts +7 -5
- package/src/config.ts +4 -0
- package/src/detectLexicon.test.ts +2 -2
- package/src/discovery/collect.test.ts +2 -2
- package/src/discovery/collect.ts +1 -1
- package/src/index.ts +2 -1
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon-schema.ts +8 -0
- package/src/lexicon.ts +15 -7
- package/src/lint/config.ts +8 -6
- package/src/lint/declarative.ts +6 -0
- package/src/lint/engine.test.ts +287 -11
- package/src/lint/engine.ts +101 -23
- package/src/lint/rule-registry.test.ts +112 -0
- package/src/lint/rule-registry.ts +118 -0
- package/src/lint/rule.ts +8 -0
- package/src/lint/rules/cor017-composite-name-match.ts +2 -1
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
- package/src/lint/rules/declarable-naming-convention.ts +1 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
- package/src/lint/rules/evl004-spread-non-const.ts +1 -0
- package/src/lint/rules/evl005-resource-block-body.ts +1 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
- package/src/lint/rules/export-required.ts +1 -0
- package/src/lint/rules/file-declarable-limit.ts +1 -0
- package/src/lint/rules/flat-declarations.test.ts +8 -7
- package/src/lint/rules/flat-declarations.ts +2 -3
- package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
- package/src/lint/rules/no-redundant-type-import.ts +1 -0
- package/src/lint/rules/no-redundant-value-cast.ts +1 -0
- package/src/lint/rules/no-string-ref.ts +1 -0
- package/src/lint/rules/no-unused-declarable-import.ts +1 -0
- package/src/lint/rules/no-unused-declarable.test.ts +8 -0
- package/src/lint/rules/no-unused-declarable.ts +4 -0
- package/src/lint/rules/single-concern-file.ts +1 -0
- package/src/lsp/lexicon-providers.ts +7 -0
- package/src/lsp/types.ts +1 -0
- package/src/resource-attributes.test.ts +79 -0
- package/src/resource-attributes.ts +42 -0
- package/src/runtime-adapter.ts +158 -0
- package/src/runtime.ts +4 -3
- package/src/serializer-walker.test.ts +0 -9
- package/src/serializer-walker.ts +1 -3
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
- package/src/codegen/case.test.ts +0 -30
- package/src/codegen/case.ts +0 -11
- package/src/codegen/rollback.test.ts +0 -92
- 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.
|
|
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
|
-
|
|
9
|
-
},
|
|
10
|
-
"bin": {
|
|
11
|
-
|
|
12
|
-
},
|
|
13
|
-
"exports": {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
},
|
|
19
|
-
"dependencies": {
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
191
|
-
const
|
|
192
|
-
expect(existsSync(
|
|
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
|
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -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":
|
|
87
|
-
[`@intentius/chant-lexicon-${lexicon}`]:
|
|
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:
|
|
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
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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(
|
|
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 {
|
package/src/cli/commands/lint.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
254
|
-
diagnostics.push(...
|
|
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
|
-
|
|
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
|
-
|
|
287
|
+
const postResult = await runLint(files, options.rules, undefined);
|
|
288
|
+
diagnostics = postResult.diagnostics;
|
|
289
|
+
suppressed = postResult.suppressed;
|
|
283
290
|
} else if (hasOverrides) {
|
|
284
|
-
|
|
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
|
|
289
|
-
|
|
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
|
-
|
|
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:
|
package/src/cli/commands/list.ts
CHANGED
|
@@ -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
|
}
|