@intentius/chant 0.0.8 → 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/package.json +1 -1
- package/src/bench.test.ts +1 -1
- package/src/cli/commands/doctor.ts +8 -3
- package/src/cli/commands/init.test.ts +44 -4
- package/src/cli/commands/init.ts +55 -23
- package/src/cli/commands/lint.ts +27 -13
- package/src/cli/handlers/init.ts +1 -0
- package/src/cli/lsp/server.ts +1 -1
- package/src/cli/main.ts +4 -0
- package/src/cli/mcp/server.test.ts +28 -2
- package/src/cli/mcp/tools/scaffold.ts +21 -3
- 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-typescript.test.ts +64 -0
- package/src/codegen/generate-typescript.ts +13 -3
- package/src/codegen/package.ts +1 -1
- package/src/composite.test.ts +83 -16
- package/src/composite.ts +7 -5
- 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 +1 -0
- package/src/lexicon-schema.ts +8 -0
- package/src/lexicon.ts +13 -1
- 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.ts +4 -3
package/package.json
CHANGED
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
|
}
|
|
@@ -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 {
|
|
@@ -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
|
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/handlers/init.ts
CHANGED
|
@@ -16,6 +16,7 @@ export async function runInit(ctx: CommandContext): Promise<number> {
|
|
|
16
16
|
const result = await initCommand({
|
|
17
17
|
path: args.path === "." ? undefined : args.path,
|
|
18
18
|
lexicon: args.lexicon,
|
|
19
|
+
template: args.template,
|
|
19
20
|
force: args.force,
|
|
20
21
|
skipInstall: true,
|
|
21
22
|
});
|
package/src/cli/lsp/server.ts
CHANGED
|
@@ -362,7 +362,7 @@ export class LspServer {
|
|
|
362
362
|
|
|
363
363
|
if (rules.length === 0) return [];
|
|
364
364
|
|
|
365
|
-
const diagnostics = await runLint([filePath], rules);
|
|
365
|
+
const { diagnostics } = await runLint([filePath], rules);
|
|
366
366
|
return toLspDiagnostics(diagnostics);
|
|
367
367
|
} catch {
|
|
368
368
|
return [];
|
package/src/cli/main.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
27
27
|
force: undefined,
|
|
28
28
|
fix: false,
|
|
29
29
|
lexicon: undefined,
|
|
30
|
+
template: undefined,
|
|
30
31
|
watch: false,
|
|
31
32
|
verbose: false,
|
|
32
33
|
help: false,
|
|
@@ -44,6 +45,8 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
44
45
|
result.format = args[++i];
|
|
45
46
|
} else if (arg === "--lexicon" || arg === "-d") {
|
|
46
47
|
result.lexicon = args[++i];
|
|
48
|
+
} else if (arg === "--template" || arg === "-t") {
|
|
49
|
+
result.template = args[++i];
|
|
47
50
|
} else if (arg === "--force") {
|
|
48
51
|
result.force = true;
|
|
49
52
|
} else if (arg === "--fix") {
|
|
@@ -107,6 +110,7 @@ Options:
|
|
|
107
110
|
- list: text (default) or json
|
|
108
111
|
- lint: stylish (default), json, or sarif
|
|
109
112
|
-d, --lexicon <name> Build only the specified lexicon (e.g. aws, gitlab)
|
|
113
|
+
-t, --template <name> Init template (e.g. node-pipeline, docker-build)
|
|
110
114
|
--fix Auto-fix fixable issues (lint command)
|
|
111
115
|
--force Force overwrite existing files (import command)
|
|
112
116
|
-w, --watch Watch for changes and rebuild/re-lint (build, lint)
|
|
@@ -378,10 +378,10 @@ describe("McpServer", () => {
|
|
|
378
378
|
test("matches plugin init templates", async () => {
|
|
379
379
|
const plugin = createMockPlugin({
|
|
380
380
|
name: "test-lex",
|
|
381
|
-
initTemplates: () => ({
|
|
381
|
+
initTemplates: () => ({ src: {
|
|
382
382
|
"config.ts": "export const config = {};",
|
|
383
383
|
"data-bucket.ts": "export const dataBucket = {};",
|
|
384
|
-
}),
|
|
384
|
+
} }),
|
|
385
385
|
});
|
|
386
386
|
|
|
387
387
|
const s = new McpServer([plugin]);
|
|
@@ -397,6 +397,32 @@ describe("McpServer", () => {
|
|
|
397
397
|
expect(parsed.files.length).toBe(1);
|
|
398
398
|
expect(parsed.files[0].filename).toBe("data-bucket.ts");
|
|
399
399
|
});
|
|
400
|
+
|
|
401
|
+
test("passes template name to initTemplates", async () => {
|
|
402
|
+
const plugin = createMockPlugin({
|
|
403
|
+
name: "test-lex",
|
|
404
|
+
initTemplates: (template?: string) => {
|
|
405
|
+
if (template === "special") {
|
|
406
|
+
return { src: { "special.ts": "export const special = {};" } };
|
|
407
|
+
}
|
|
408
|
+
return { src: { "default.ts": "export const def = {};" } };
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const s = new McpServer([plugin]);
|
|
413
|
+
const response = await s.handleRequest({
|
|
414
|
+
jsonrpc: "2.0",
|
|
415
|
+
id: 1,
|
|
416
|
+
method: "tools/call",
|
|
417
|
+
params: { name: "scaffold", arguments: { pattern: "special", lexicon: "test-lex", template: "special" } },
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const parsed = JSON.parse((response.result as { content: Array<{ text: string }> }).content[0].text);
|
|
421
|
+
expect(parsed.lexicon).toBe("test-lex");
|
|
422
|
+
expect(parsed.template).toBe("special");
|
|
423
|
+
expect(parsed.files.length).toBe(1);
|
|
424
|
+
expect(parsed.files[0].filename).toBe("special.ts");
|
|
425
|
+
});
|
|
400
426
|
});
|
|
401
427
|
|
|
402
428
|
// -----------------------------------------------------------------------
|
|
@@ -17,6 +17,10 @@ export const scaffoldTool = {
|
|
|
17
17
|
type: "string",
|
|
18
18
|
description: "Lexicon to use for scaffolding (e.g. 'aws', 'gitlab'). Auto-detected if omitted.",
|
|
19
19
|
},
|
|
20
|
+
template: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Named init template (e.g. 'node-pipeline', 'docker-build'). When provided, returns the template's source files instead of pattern-matching.",
|
|
23
|
+
},
|
|
20
24
|
},
|
|
21
25
|
required: ["pattern"],
|
|
22
26
|
},
|
|
@@ -31,6 +35,7 @@ export function createScaffoldHandler(
|
|
|
31
35
|
return async (params) => {
|
|
32
36
|
const pattern = params.pattern as string;
|
|
33
37
|
const lexiconName = params.lexicon as string | undefined;
|
|
38
|
+
const templateName = params.template as string | undefined;
|
|
34
39
|
|
|
35
40
|
// Try to find a matching plugin
|
|
36
41
|
const candidates = lexiconName
|
|
@@ -39,14 +44,27 @@ export function createScaffoldHandler(
|
|
|
39
44
|
|
|
40
45
|
// Search plugin init templates for a pattern match
|
|
41
46
|
for (const plugin of candidates) {
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
47
|
+
const templateSet = plugin.initTemplates?.(templateName);
|
|
48
|
+
if (!templateSet) continue;
|
|
49
|
+
|
|
50
|
+
// If a named template was requested, return all its source files
|
|
51
|
+
if (templateName) {
|
|
52
|
+
const files = Object.entries(templateSet.src).map(([filename, content]) => ({ filename, content }));
|
|
53
|
+
if (files.length > 0) {
|
|
54
|
+
return {
|
|
55
|
+
lexicon: plugin.name,
|
|
56
|
+
pattern,
|
|
57
|
+
template: templateName,
|
|
58
|
+
files,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
44
62
|
|
|
45
63
|
// Match template filenames against the pattern (case-insensitive substring)
|
|
46
64
|
const lowerPattern = pattern.toLowerCase();
|
|
47
65
|
const matched: Array<{ filename: string; content: string }> = [];
|
|
48
66
|
|
|
49
|
-
for (const [filename, content] of Object.entries(
|
|
67
|
+
for (const [filename, content] of Object.entries(templateSet.src)) {
|
|
50
68
|
if (filename.toLowerCase().includes(lowerPattern)) {
|
|
51
69
|
matched.push({ filename, content });
|
|
52
70
|
}
|