@intentius/chant 0.0.14 → 0.0.15
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 +10 -4
- package/src/cli/commands/check-lexicon.ts +47 -0
- package/src/cli/commands/doctor.ts +4 -5
- package/src/cli/commands/init.test.ts +2 -3
- package/src/cli/commands/init.ts +23 -29
- package/src/cli/commands/update.ts +3 -3
- package/src/codegen/generate-typescript.ts +5 -2
- package/src/serializer-walker.test.ts +23 -0
- package/src/serializer-walker.ts +8 -2
- package/src/yaml.test.ts +21 -0
- package/src/yaml.ts +72 -5
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"files": [
|
|
6
|
+
"files": [
|
|
7
|
+
"src/",
|
|
8
|
+
"bin/"
|
|
9
|
+
],
|
|
7
10
|
"publishConfig": {
|
|
8
11
|
"access": "public"
|
|
9
12
|
},
|
|
@@ -13,13 +16,16 @@
|
|
|
13
16
|
"exports": {
|
|
14
17
|
".": "./src/index.ts",
|
|
15
18
|
"./cli": "./src/cli/index.ts",
|
|
16
|
-
"./cli/*": "./src/cli
|
|
17
|
-
"./*": "./src
|
|
19
|
+
"./cli/*": "./src/cli/*.ts",
|
|
20
|
+
"./*": "./src/*.ts"
|
|
18
21
|
},
|
|
19
22
|
"dependencies": {
|
|
20
23
|
"fflate": "^0.8.2",
|
|
21
24
|
"picomatch": "^4.0.3",
|
|
22
25
|
"typescript": "^5.5.0",
|
|
23
26
|
"zod": "^4.3.6"
|
|
27
|
+
},
|
|
28
|
+
"optionalDependencies": {
|
|
29
|
+
"tsx": "^4.0.0"
|
|
24
30
|
}
|
|
25
31
|
}
|
|
@@ -208,6 +208,35 @@ export function checkLexicon(dir: string): CheckResult {
|
|
|
208
208
|
detail: `${mdxFiles.length} page(s)`,
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
// Post-synth check count (excluding helpers, tests, and support files)
|
|
212
|
+
const postSynthCheckFiles = listTsFiles(join(dir, "src/lint/post-synth"), ["index.ts"])
|
|
213
|
+
.filter((f) => !f.endsWith(".test.ts") && !f.endsWith("-helpers.ts") && f !== "helpers.ts" && !f.startsWith("arm-") && !f.startsWith("k8s-"));
|
|
214
|
+
items.push({
|
|
215
|
+
name: "At least 15 post-synth checks",
|
|
216
|
+
tier: 2,
|
|
217
|
+
pass: postSynthCheckFiles.length >= 15,
|
|
218
|
+
detail: `${postSynthCheckFiles.length} check(s)`,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Skills count
|
|
222
|
+
const skillsDir = join(dir, "src/skills");
|
|
223
|
+
const skillFiles = existsSync(skillsDir) ? readdirSync(skillsDir).filter((f) => f.endsWith(".md")) : [];
|
|
224
|
+
items.push({
|
|
225
|
+
name: "At least 3 skills",
|
|
226
|
+
tier: 2,
|
|
227
|
+
pass: skillFiles.length >= 3,
|
|
228
|
+
detail: `${skillFiles.length} skill(s)`,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// initTemplates count — check for template branches in plugin.ts
|
|
232
|
+
const initTemplateBranches = (pluginContent.match(/template\s*===\s*["']/g) || []).length + 1; // +1 for default
|
|
233
|
+
items.push({
|
|
234
|
+
name: "At least 3 initTemplates",
|
|
235
|
+
tier: 2,
|
|
236
|
+
pass: initTemplateBranches >= 3,
|
|
237
|
+
detail: `${initTemplateBranches} template(s)`,
|
|
238
|
+
});
|
|
239
|
+
|
|
211
240
|
// ── Tier 3: Thoroughness ───────────────────────────────────────
|
|
212
241
|
|
|
213
242
|
// Each lint rule has a test (per-file or consolidated)
|
|
@@ -276,6 +305,24 @@ export function checkLexicon(dir: string): CheckResult {
|
|
|
276
305
|
pass: hasActions,
|
|
277
306
|
});
|
|
278
307
|
|
|
308
|
+
// Validate required names count
|
|
309
|
+
const validateContent = readOr(join(dir, "src/validate.ts"));
|
|
310
|
+
const requiredNamesMatches = validateContent.match(/["'][A-Z][a-zA-Z]+["']/g) || [];
|
|
311
|
+
items.push({
|
|
312
|
+
name: "validate.ts checks at least 30 required names",
|
|
313
|
+
tier: 3,
|
|
314
|
+
pass: requiredNamesMatches.length >= 30,
|
|
315
|
+
detail: `${requiredNamesMatches.length} required name(s)`,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Composite test file exists
|
|
319
|
+
const hasCompositeTest = existsSync(join(dir, "src/composites/composites.test.ts"));
|
|
320
|
+
items.push({
|
|
321
|
+
name: "Composite test file exists",
|
|
322
|
+
tier: 3,
|
|
323
|
+
pass: hasCompositeTest,
|
|
324
|
+
});
|
|
325
|
+
|
|
279
326
|
// Examples with tests (per-example or consolidated root test file)
|
|
280
327
|
const examplesDir = join(dir, "examples");
|
|
281
328
|
let examplesWithTests = 0;
|
|
@@ -147,7 +147,7 @@ export async function doctorCommand(path: string): Promise<DoctorReport> {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
// Check 8: tsconfig.json
|
|
150
|
+
// Check 8: tsconfig.json does NOT have paths (they break runtime resolution)
|
|
151
151
|
const tsconfigPath = join(projectPath, "tsconfig.json");
|
|
152
152
|
if (existsSync(tsconfigPath)) {
|
|
153
153
|
try {
|
|
@@ -156,8 +156,8 @@ export async function doctorCommand(path: string): Promise<DoctorReport> {
|
|
|
156
156
|
// Strip single-line comments for basic parsing
|
|
157
157
|
const cleaned = raw.replace(/\/\/.*$/gm, "");
|
|
158
158
|
const tsconfig = JSON.parse(cleaned);
|
|
159
|
-
if (
|
|
160
|
-
checks.push({ name: "tsconfig-paths", status: "warn", message: "tsconfig.json
|
|
159
|
+
if (tsconfig.compilerOptions?.paths) {
|
|
160
|
+
checks.push({ name: "tsconfig-paths", status: "warn", message: "tsconfig.json has compilerOptions.paths — these break runtime module resolution (bun and tsx follow them). Remove the paths block." });
|
|
161
161
|
} else {
|
|
162
162
|
checks.push({ name: "tsconfig-paths", status: "pass" });
|
|
163
163
|
}
|
|
@@ -203,10 +203,9 @@ export async function doctorCommand(path: string): Promise<DoctorReport> {
|
|
|
203
203
|
if (!plugin.skills) continue;
|
|
204
204
|
const skills = plugin.skills();
|
|
205
205
|
if (skills.length === 0) continue;
|
|
206
|
-
const skillsDir = join(projectPath, ".chant", "skills", plugin.name);
|
|
207
206
|
let missing = 0;
|
|
208
207
|
for (const skill of skills) {
|
|
209
|
-
if (!existsSync(join(
|
|
208
|
+
if (!existsSync(join(projectPath, "skills", skill.name, "SKILL.md"))) {
|
|
210
209
|
missing++;
|
|
211
210
|
}
|
|
212
211
|
}
|
|
@@ -94,7 +94,7 @@ describe("initCommand", () => {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
test("generates valid tsconfig.json
|
|
97
|
+
test("generates valid tsconfig.json without path mappings", async () => {
|
|
98
98
|
await withTestDir(async (testDir) => {
|
|
99
99
|
const options: InitOptions = {
|
|
100
100
|
path: testDir,
|
|
@@ -114,8 +114,7 @@ describe("initCommand", () => {
|
|
|
114
114
|
expect(tsconfig.compilerOptions.strict).toBe(true);
|
|
115
115
|
expect(tsconfig.compilerOptions.rootDir).toBe("./src");
|
|
116
116
|
expect(tsconfig.include).toContain("src");
|
|
117
|
-
expect(tsconfig.compilerOptions.paths
|
|
118
|
-
expect(tsconfig.compilerOptions.paths["@intentius/chant-lexicon-aws"]).toEqual(["./.chant/types/lexicon-aws"]);
|
|
117
|
+
expect(tsconfig.compilerOptions.paths).toBeUndefined();
|
|
119
118
|
});
|
|
120
119
|
});
|
|
121
120
|
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -61,6 +61,16 @@ export interface InitResult {
|
|
|
61
61
|
error?: string;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Detect whether the user's project uses bun or npm.
|
|
66
|
+
* Checks for lock files first, then falls back to runtime detection.
|
|
67
|
+
*/
|
|
68
|
+
function detectPackageManager(dir?: string): "bun" | "npm" {
|
|
69
|
+
if (dir && (existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock")))) return "bun";
|
|
70
|
+
if (typeof globalThis.Bun !== "undefined") return "bun";
|
|
71
|
+
return "npm";
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
/**
|
|
65
75
|
* Detect the IDE environment for MCP config
|
|
66
76
|
*/
|
|
@@ -142,12 +152,6 @@ function generateTsConfig(lexicon: string): string {
|
|
|
142
152
|
declaration: true,
|
|
143
153
|
outDir: "./dist",
|
|
144
154
|
rootDir: "./src",
|
|
145
|
-
paths: {
|
|
146
|
-
"@intentius/chant": ["./.chant/types/core"],
|
|
147
|
-
"@intentius/chant/*": ["./.chant/types/core/*"],
|
|
148
|
-
[`@intentius/chant-lexicon-${lexicon}`]: [`./.chant/types/lexicon-${lexicon}`],
|
|
149
|
-
[`@intentius/chant-lexicon-${lexicon}/*`]: [`./.chant/types/lexicon-${lexicon}/*`],
|
|
150
|
-
},
|
|
151
155
|
},
|
|
152
156
|
include: ["src"],
|
|
153
157
|
exclude: ["node_modules", "dist"],
|
|
@@ -177,7 +181,7 @@ node_modules/
|
|
|
177
181
|
.chant/types/
|
|
178
182
|
.chant/meta/
|
|
179
183
|
.chant/rules/
|
|
180
|
-
|
|
184
|
+
skills/
|
|
181
185
|
`;
|
|
182
186
|
}
|
|
183
187
|
|
|
@@ -236,11 +240,11 @@ export interface ChantConfig {
|
|
|
236
240
|
/**
|
|
237
241
|
* Generate MCP config
|
|
238
242
|
*/
|
|
239
|
-
function generateMcpConfig(_ide: "claude-code" | "cursor" | "generic"): string {
|
|
243
|
+
function generateMcpConfig(_ide: "claude-code" | "cursor" | "generic", pm: "bun" | "npm"): string {
|
|
240
244
|
const config = {
|
|
241
245
|
mcpServers: {
|
|
242
246
|
chant: {
|
|
243
|
-
command: "npx",
|
|
247
|
+
command: pm === "bun" ? "bunx" : "npx",
|
|
244
248
|
args: ["chant", "serve", "mcp"],
|
|
245
249
|
},
|
|
246
250
|
},
|
|
@@ -450,7 +454,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
|
|
|
450
454
|
|
|
451
455
|
writeIfNotExists(
|
|
452
456
|
join(mcpDir, "mcp.json"),
|
|
453
|
-
generateMcpConfig(ide),
|
|
457
|
+
generateMcpConfig(ide, detectPackageManager(targetDir)),
|
|
454
458
|
`~/.${ide === "generic" ? "config/mcp" : ide}/mcp.json`,
|
|
455
459
|
createdFiles,
|
|
456
460
|
warnings,
|
|
@@ -458,28 +462,16 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
|
|
|
458
462
|
}
|
|
459
463
|
|
|
460
464
|
// Install skills from the lexicon's plugin
|
|
461
|
-
// Write to both .chant/skills/ (chant's own location) and .claude/skills/ (Claude Code discovery)
|
|
462
465
|
try {
|
|
463
466
|
const plugin = await loadPlugin(options.lexicon);
|
|
464
467
|
if (plugin.skills) {
|
|
465
468
|
const skills = plugin.skills();
|
|
466
469
|
if (skills.length > 0) {
|
|
467
|
-
// .chant/skills/ — chant's own skill storage
|
|
468
|
-
const chantSkillsDir = join(targetDir, ".chant", "skills", options.lexicon);
|
|
469
|
-
mkdirSync(chantSkillsDir, { recursive: true });
|
|
470
470
|
for (const skill of skills) {
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
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`);
|
|
471
|
+
const skillDir = join(targetDir, "skills", skill.name);
|
|
472
|
+
mkdirSync(skillDir, { recursive: true });
|
|
473
|
+
writeFileSync(join(skillDir, "SKILL.md"), skill.content);
|
|
474
|
+
createdFiles.push(`skills/${skill.name}/SKILL.md`);
|
|
483
475
|
}
|
|
484
476
|
}
|
|
485
477
|
}
|
|
@@ -519,6 +511,8 @@ export async function printInitResult(
|
|
|
519
511
|
|
|
520
512
|
console.log("");
|
|
521
513
|
|
|
514
|
+
const pm = detectPackageManager(options?.cwd);
|
|
515
|
+
|
|
522
516
|
// Interactive install prompt
|
|
523
517
|
if (!options?.skipInstall) {
|
|
524
518
|
const shouldInstall = await promptInstall();
|
|
@@ -527,9 +521,9 @@ export async function printInitResult(
|
|
|
527
521
|
const cwd = options?.cwd ?? ".";
|
|
528
522
|
console.log("Installing dependencies...");
|
|
529
523
|
try {
|
|
530
|
-
execSync(
|
|
524
|
+
execSync(`${pm} install`, { cwd, stdio: "inherit" });
|
|
531
525
|
} catch {
|
|
532
|
-
console.error(formatWarning({ message:
|
|
526
|
+
console.error(formatWarning({ message: `Install failed. Run '${pm} install' manually.` }));
|
|
533
527
|
}
|
|
534
528
|
}
|
|
535
529
|
}
|
|
@@ -538,6 +532,6 @@ export async function printInitResult(
|
|
|
538
532
|
console.log("Next steps:");
|
|
539
533
|
console.log(" 1. Edit src/config.ts");
|
|
540
534
|
console.log(" 2. Add resources in src/");
|
|
541
|
-
console.log(
|
|
535
|
+
console.log(` 3. ${pm} run build`);
|
|
542
536
|
}
|
|
543
537
|
|
|
@@ -164,10 +164,10 @@ export async function updateCommand(options: UpdateOptions): Promise<UpdateResul
|
|
|
164
164
|
if (plugin.skills) {
|
|
165
165
|
const skills = plugin.skills();
|
|
166
166
|
if (skills.length > 0) {
|
|
167
|
-
const skillsDir = join(projectDir, ".chant", "skills", plugin.name);
|
|
168
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
169
167
|
for (const skill of skills) {
|
|
170
|
-
|
|
168
|
+
const skillDir = join(projectDir, "skills", skill.name);
|
|
169
|
+
mkdirSync(skillDir, { recursive: true });
|
|
170
|
+
writeFileSync(join(skillDir, "SKILL.md"), skill.content);
|
|
171
171
|
}
|
|
172
172
|
synced.push(`${plugin.name} skills (${skills.length})`);
|
|
173
173
|
}
|
|
@@ -102,9 +102,12 @@ export function writeConstructor(
|
|
|
102
102
|
const optional = p.required ? "" : "?";
|
|
103
103
|
const tsType = resolveConstructorType(p.type, remap);
|
|
104
104
|
if (p.description) {
|
|
105
|
-
|
|
105
|
+
// Sanitize: escape */ to prevent breaking JSDoc comments
|
|
106
|
+
const safeDesc = p.description.replace(/\*\//g, "*\\/");
|
|
107
|
+
lines.push(` /** ${safeDesc} */`);
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(p.name) ? p.name : JSON.stringify(p.name);
|
|
110
|
+
lines.push(` ${safeName}${optional}: ${tsType};`);
|
|
108
111
|
}
|
|
109
112
|
if (resourceAttributesType) {
|
|
110
113
|
lines.push(` }, attributes?: ${resourceAttributesType});`);
|
|
@@ -101,6 +101,29 @@ describe("walkValue", () => {
|
|
|
101
101
|
expect(walkValue((resource as any).Ref, names, mockVisitor)).toEqual({ __ref: "MyTable" });
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
test("resolves __attrRef envelope inside intrinsic toJSON output", () => {
|
|
105
|
+
// Simulate an intrinsic whose toJSON() produces an __attrRef envelope
|
|
106
|
+
// (e.g., AttrRef inside a Sub template literal)
|
|
107
|
+
const intrinsic = {
|
|
108
|
+
[INTRINSIC_MARKER]: true as const,
|
|
109
|
+
toJSON: () => ({
|
|
110
|
+
MyIntrinsic: { __attrRef: { entity: "MyResource", attribute: "Arn" } },
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
const names = new Map<Declarable, string>();
|
|
114
|
+
expect(walkValue(intrinsic, names, mockVisitor)).toEqual({
|
|
115
|
+
MyIntrinsic: { __getAtt: ["MyResource", "Arn"] },
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("resolves standalone __attrRef envelope in plain object", () => {
|
|
120
|
+
const names = new Map<Declarable, string>();
|
|
121
|
+
const value = { __attrRef: { entity: "MyBucket", attribute: "DomainName" } };
|
|
122
|
+
expect(walkValue(value, names, mockVisitor)).toEqual({
|
|
123
|
+
__getAtt: ["MyBucket", "DomainName"],
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
104
127
|
test("complex nested structure", () => {
|
|
105
128
|
const resource = makeDeclarable("Test::Role");
|
|
106
129
|
const ref = new AttrRef(resource, "arn");
|
package/src/serializer-walker.ts
CHANGED
|
@@ -43,10 +43,10 @@ export function walkValue(
|
|
|
43
43
|
return visitor.attrRef(name, value.attribute);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Handle Intrinsics
|
|
46
|
+
// Handle Intrinsics — walk the toJSON() result to resolve any embedded AttrRef markers
|
|
47
47
|
if (typeof value === "object" && value !== null && INTRINSIC_MARKER in value) {
|
|
48
48
|
if ("toJSON" in value && typeof value.toJSON === "function") {
|
|
49
|
-
return value.toJSON();
|
|
49
|
+
return walkValue(value.toJSON(), entityNames, visitor);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -67,6 +67,12 @@ export function walkValue(
|
|
|
67
67
|
return value.map((item) => walkValue(item, entityNames, visitor));
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Handle serialized AttrRef envelopes (produced by AttrRef.toJSON() inside intrinsics)
|
|
71
|
+
if (typeof value === "object" && "__attrRef" in value) {
|
|
72
|
+
const ref = (value as { __attrRef: { entity: string; attribute: string } }).__attrRef;
|
|
73
|
+
return visitor.attrRef(ref.entity, ref.attribute);
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
// Handle objects
|
|
71
77
|
if (typeof value === "object") {
|
|
72
78
|
const result: Record<string, unknown> = {};
|
package/src/yaml.test.ts
CHANGED
|
@@ -88,6 +88,27 @@ describe("emitYAML", () => {
|
|
|
88
88
|
const result = emitYAML({ key: "val" }, 1);
|
|
89
89
|
expect(result).toBe("\n key: val");
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
test("multiline string emits as | block scalar", () => {
|
|
93
|
+
const result = emitYAML("line1\nline2\nline3", 0);
|
|
94
|
+
expect(result).toBe("|\nline1\nline2\nline3");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("multiline string trims trailing empty line", () => {
|
|
98
|
+
// Template literals often end with \n producing a trailing empty line
|
|
99
|
+
const result = emitYAML("line1\nline2\n", 0);
|
|
100
|
+
expect(result).toBe("|\nline1\nline2");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("multiline string inside object value is properly indented", () => {
|
|
104
|
+
const result = emitYAML({ config: "line1\nline2\nline3" }, 0);
|
|
105
|
+
expect(result).toContain("config: |\n line1\n line2\n line3");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("multiline string inside array item is properly indented", () => {
|
|
109
|
+
const result = emitYAML([{ data: "a\nb" }], 0);
|
|
110
|
+
expect(result).toContain("data: |\n a\n b");
|
|
111
|
+
});
|
|
91
112
|
});
|
|
92
113
|
|
|
93
114
|
// ---------------------------------------------------------------------------
|
package/src/yaml.ts
CHANGED
|
@@ -35,6 +35,13 @@ export function emitYAML(value: unknown, indent: number): string {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (typeof value === "string") {
|
|
38
|
+
// Multiline strings use YAML literal block scalar (|)
|
|
39
|
+
if (value.includes("\n")) {
|
|
40
|
+
const lines = value.split("\n");
|
|
41
|
+
// Trim trailing empty line if present (common for template strings)
|
|
42
|
+
const trimmed = lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
|
|
43
|
+
return "|\n" + trimmed.map((l) => `${prefix}${l}`).join("\n");
|
|
44
|
+
}
|
|
38
45
|
// Quote strings that could be misinterpreted
|
|
39
46
|
if (
|
|
40
47
|
value === "" ||
|
|
@@ -225,6 +232,58 @@ export function parseYAMLLines(
|
|
|
225
232
|
return { value: result, endIndex: i };
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Parse the value of a key inside an array item.
|
|
237
|
+
* If the inline value is empty, look ahead for a nested object or array.
|
|
238
|
+
*/
|
|
239
|
+
function parseArrayItemValue(
|
|
240
|
+
inlineValue: string,
|
|
241
|
+
lines: string[],
|
|
242
|
+
currentIndex: number,
|
|
243
|
+
childIndent: number,
|
|
244
|
+
): unknown {
|
|
245
|
+
if (inlineValue !== "" && !inlineValue.startsWith("#")) {
|
|
246
|
+
if (inlineValue.startsWith("[")) {
|
|
247
|
+
try { return JSON.parse(inlineValue); } catch { return inlineValue; }
|
|
248
|
+
}
|
|
249
|
+
if (inlineValue.startsWith("{")) {
|
|
250
|
+
try { return JSON.parse(inlineValue); } catch { return inlineValue; }
|
|
251
|
+
}
|
|
252
|
+
return parseScalar(inlineValue);
|
|
253
|
+
}
|
|
254
|
+
// Empty inline value — check for nested block
|
|
255
|
+
const nextIdx = currentIndex + 1;
|
|
256
|
+
if (nextIdx < lines.length) {
|
|
257
|
+
const nextLine = lines[nextIdx];
|
|
258
|
+
if (nextLine.trim() !== "" && !nextLine.trim().startsWith("#")) {
|
|
259
|
+
const ni = nextLine.search(/\S/);
|
|
260
|
+
if (ni >= childIndent) {
|
|
261
|
+
if (nextLine.trimStart().startsWith("- ")) {
|
|
262
|
+
return parseYAMLArray(lines, nextIdx, ni).value;
|
|
263
|
+
}
|
|
264
|
+
return parseYAMLLines(lines, nextIdx, ni).value;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Skip past a nested block (object or array) starting at startIndex with the given indent.
|
|
273
|
+
* Returns the index of the first line that is NOT part of the nested block.
|
|
274
|
+
*/
|
|
275
|
+
function skipNestedBlock(lines: string[], startIndex: number, childIndent: number): number {
|
|
276
|
+
let j = startIndex;
|
|
277
|
+
while (j < lines.length) {
|
|
278
|
+
const l = lines[j];
|
|
279
|
+
if (l.trim() === "" || l.trim().startsWith("#")) { j++; continue; }
|
|
280
|
+
const ni = l.search(/\S/);
|
|
281
|
+
if (ni < childIndent) break;
|
|
282
|
+
j++;
|
|
283
|
+
}
|
|
284
|
+
return j;
|
|
285
|
+
}
|
|
286
|
+
|
|
228
287
|
/**
|
|
229
288
|
* Parse a block array (lines starting with `- `).
|
|
230
289
|
*/
|
|
@@ -253,10 +312,12 @@ export function parseYAMLArray(
|
|
|
253
312
|
const kvMatch = itemValue.match(/^([^\s:][^:]*?):\s*(.*)$/);
|
|
254
313
|
if (kvMatch) {
|
|
255
314
|
const obj: Record<string, unknown> = {};
|
|
256
|
-
obj[kvMatch[1].trim()] =
|
|
315
|
+
obj[kvMatch[1].trim()] = parseArrayItemValue(kvMatch[2].trim(), lines, i, indent + 2);
|
|
257
316
|
// Check for more keys at indent+2
|
|
258
317
|
const nextIndent = indent + 2;
|
|
259
|
-
let j =
|
|
318
|
+
let j = kvMatch[2].trim() === "" || kvMatch[2].trim().startsWith("#")
|
|
319
|
+
? skipNestedBlock(lines, i + 1, nextIndent)
|
|
320
|
+
: i + 1;
|
|
260
321
|
while (j < lines.length) {
|
|
261
322
|
const nextLine = lines[j];
|
|
262
323
|
if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
|
|
@@ -264,11 +325,17 @@ export function parseYAMLArray(
|
|
|
264
325
|
continue;
|
|
265
326
|
}
|
|
266
327
|
const ni = nextLine.search(/\S/);
|
|
267
|
-
if (ni
|
|
328
|
+
if (ni < nextIndent) break;
|
|
329
|
+
if (ni > nextIndent) break; // belongs to a nested block already consumed
|
|
268
330
|
const nextKV = nextLine.match(/^(\s*)([^\s:][^:]*?):\s*(.*)$/);
|
|
269
331
|
if (nextKV) {
|
|
270
|
-
|
|
271
|
-
j
|
|
332
|
+
const nextVal = nextKV[3].trim();
|
|
333
|
+
obj[nextKV[2].trim()] = parseArrayItemValue(nextVal, lines, j, ni + 2);
|
|
334
|
+
if (nextVal === "" || nextVal.startsWith("#")) {
|
|
335
|
+
j = skipNestedBlock(lines, j + 1, ni + 2);
|
|
336
|
+
} else {
|
|
337
|
+
j++;
|
|
338
|
+
}
|
|
272
339
|
} else {
|
|
273
340
|
break;
|
|
274
341
|
}
|