pi-gsd 2.0.1 → 2.0.2
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/dist/pi-gsd-hooks.js +1532 -0
- package/package.json +3 -5
- package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
- package/src/cli.ts +0 -644
- package/src/commands/base.ts +0 -67
- package/src/commands/commit.ts +0 -22
- package/src/commands/config.ts +0 -71
- package/src/commands/frontmatter.ts +0 -51
- package/src/commands/index.ts +0 -76
- package/src/commands/init.ts +0 -43
- package/src/commands/milestone.ts +0 -37
- package/src/commands/phase.ts +0 -92
- package/src/commands/progress.ts +0 -71
- package/src/commands/roadmap.ts +0 -40
- package/src/commands/scaffold.ts +0 -19
- package/src/commands/state.ts +0 -102
- package/src/commands/template.ts +0 -52
- package/src/commands/verify.ts +0 -70
- package/src/commands/workstream.ts +0 -98
- package/src/commands/wxp.ts +0 -65
- package/src/lib/commands.ts +0 -1040
- package/src/lib/config.ts +0 -385
- package/src/lib/core.ts +0 -1167
- package/src/lib/frontmatter.ts +0 -462
- package/src/lib/init.ts +0 -517
- package/src/lib/milestone.ts +0 -290
- package/src/lib/model-profiles.ts +0 -272
- package/src/lib/phase.ts +0 -1012
- package/src/lib/profile-output.ts +0 -237
- package/src/lib/profile-pipeline.ts +0 -556
- package/src/lib/roadmap.ts +0 -378
- package/src/lib/schemas.ts +0 -290
- package/src/lib/security.ts +0 -176
- package/src/lib/state.ts +0 -1175
- package/src/lib/template.ts +0 -246
- package/src/lib/uat.ts +0 -289
- package/src/lib/verify.ts +0 -879
- package/src/lib/workstream.ts +0 -524
- package/src/output.ts +0 -45
- package/src/schemas/pi-gsd-settings.schema.json +0 -80
- package/src/schemas/wxp.xsd +0 -619
- package/src/schemas/wxp.zod.ts +0 -318
- package/src/wxp/__tests__/arguments.test.ts +0 -86
- package/src/wxp/__tests__/conditions.test.ts +0 -106
- package/src/wxp/__tests__/executor.test.ts +0 -95
- package/src/wxp/__tests__/helpers.ts +0 -26
- package/src/wxp/__tests__/integration.test.ts +0 -166
- package/src/wxp/__tests__/new-features.test.ts +0 -222
- package/src/wxp/__tests__/parser.test.ts +0 -159
- package/src/wxp/__tests__/paste.test.ts +0 -66
- package/src/wxp/__tests__/schema.test.ts +0 -120
- package/src/wxp/__tests__/security.test.ts +0 -87
- package/src/wxp/__tests__/shell.test.ts +0 -85
- package/src/wxp/__tests__/string-ops.test.ts +0 -25
- package/src/wxp/__tests__/variables.test.ts +0 -65
- package/src/wxp/arguments.ts +0 -89
- package/src/wxp/conditions.ts +0 -78
- package/src/wxp/executor.ts +0 -191
- package/src/wxp/index.ts +0 -191
- package/src/wxp/parser.ts +0 -198
- package/src/wxp/paste.ts +0 -51
- package/src/wxp/security.ts +0 -102
- package/src/wxp/shell.ts +0 -81
- package/src/wxp/string-ops.ts +0 -44
- package/src/wxp/variables.ts +0 -109
package/src/lib/frontmatter.ts
DELETED
|
@@ -1,462 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* frontmatter.ts - YAML frontmatter parsing, serialization, and CRUD commands.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import path from "path";
|
|
7
|
-
import { gsdError, normalizeMd, output, safeReadFile } from "./core.js";
|
|
8
|
-
|
|
9
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
// Recursive YAML value type — covers all YAML primitives, arrays, and nested objects (TYP-01)
|
|
12
|
-
export type YamlValue =
|
|
13
|
-
| string
|
|
14
|
-
| number
|
|
15
|
-
| boolean
|
|
16
|
-
| null
|
|
17
|
-
| YamlValue[]
|
|
18
|
-
| { [key: string]: YamlValue };
|
|
19
|
-
|
|
20
|
-
export type FrontmatterObject = Record<string, YamlValue>;
|
|
21
|
-
|
|
22
|
-
// ─── YamlValue type guards (TYP-01) ───────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
/** Narrow a YamlValue to string | undefined */
|
|
25
|
-
export function asStr(v: YamlValue | undefined): string | undefined {
|
|
26
|
-
return typeof v === "string" ? v : undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Narrow a YamlValue to YamlValue[] | undefined */
|
|
30
|
-
export function asArr(v: YamlValue | undefined): YamlValue[] | undefined {
|
|
31
|
-
return Array.isArray(v) ? v : undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Narrow a YamlValue to Record<string, YamlValue> | undefined */
|
|
35
|
-
export function asObj(v: YamlValue | undefined): Record<string, YamlValue> | undefined {
|
|
36
|
-
return v !== null && typeof v === "object" && !Array.isArray(v)
|
|
37
|
-
? (v as Record<string, YamlValue>)
|
|
38
|
-
: undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type FrontmatterSchema = "plan" | "summary" | "verification";
|
|
42
|
-
|
|
43
|
-
export const FRONTMATTER_SCHEMAS: Record<
|
|
44
|
-
FrontmatterSchema,
|
|
45
|
-
{ required: string[] }
|
|
46
|
-
> = {
|
|
47
|
-
plan: {
|
|
48
|
-
required: [
|
|
49
|
-
"phase",
|
|
50
|
-
"plan",
|
|
51
|
-
"type",
|
|
52
|
-
"wave",
|
|
53
|
-
"depends_on",
|
|
54
|
-
"files_modified",
|
|
55
|
-
"autonomous",
|
|
56
|
-
"must_haves",
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
summary: {
|
|
60
|
-
required: ["phase", "plan", "subsystem", "tags", "duration", "completed"],
|
|
61
|
-
},
|
|
62
|
-
verification: { required: ["phase", "verified", "status", "score"] },
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// ─── Parsing engine ───────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
export function extractFrontmatter(content: string): FrontmatterObject {
|
|
68
|
-
const frontmatter: FrontmatterObject = {};
|
|
69
|
-
// Find ALL frontmatter blocks at the start of the file.
|
|
70
|
-
// If multiple blocks exist (corruption from CRLF mismatch), use the LAST one.
|
|
71
|
-
const allBlocks = [
|
|
72
|
-
...content.matchAll(/(?:^|\n)\s*---\r?\n([\s\S]+?)\r?\n---/g),
|
|
73
|
-
];
|
|
74
|
-
const match = allBlocks.length > 0 ? allBlocks[allBlocks.length - 1] : null;
|
|
75
|
-
if (!match) return frontmatter;
|
|
76
|
-
|
|
77
|
-
const yaml = match[1];
|
|
78
|
-
const lines = yaml.split(/\r?\n/);
|
|
79
|
-
|
|
80
|
-
// Stack to track nested objects: [{obj, key, indent}]
|
|
81
|
-
const stack: Array<{ obj: Record<string, YamlValue>; key: string | null; indent: number }> = [
|
|
82
|
-
{ obj: frontmatter, key: null, indent: -1 },
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
for (const line of lines) {
|
|
86
|
-
if (line.trim() === "") continue;
|
|
87
|
-
|
|
88
|
-
const indentMatch = line.match(/^(\s*)/);
|
|
89
|
-
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
90
|
-
|
|
91
|
-
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
92
|
-
stack.pop();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const current = stack[stack.length - 1];
|
|
96
|
-
|
|
97
|
-
const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
|
|
98
|
-
if (keyMatch) {
|
|
99
|
-
const key = keyMatch[2];
|
|
100
|
-
const value = keyMatch[3].trim();
|
|
101
|
-
|
|
102
|
-
if (value === "" || value === "[") {
|
|
103
|
-
current.obj[key] = value === "[" ? [] : {};
|
|
104
|
-
current.key = null;
|
|
105
|
-
const nested = current.obj[key];
|
|
106
|
-
// nested is either [] or {} — safe to push as obj entry
|
|
107
|
-
if (nested !== null && typeof nested === "object") {
|
|
108
|
-
stack.push({ obj: nested as Record<string, YamlValue>, key: null, indent });
|
|
109
|
-
}
|
|
110
|
-
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
111
|
-
current.obj[key] = value
|
|
112
|
-
.slice(1, -1)
|
|
113
|
-
.split(",")
|
|
114
|
-
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
115
|
-
.filter(Boolean);
|
|
116
|
-
current.key = null;
|
|
117
|
-
} else {
|
|
118
|
-
current.obj[key] = value.replace(/^["']|["']$/g, "");
|
|
119
|
-
current.key = null;
|
|
120
|
-
}
|
|
121
|
-
} else if (line.trim().startsWith("- ")) {
|
|
122
|
-
const itemValue = line
|
|
123
|
-
.trim()
|
|
124
|
-
.slice(2)
|
|
125
|
-
.replace(/^["']|["']$/g, "");
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
typeof current.obj === "object" &&
|
|
129
|
-
!Array.isArray(current.obj) &&
|
|
130
|
-
Object.keys(current.obj).length === 0
|
|
131
|
-
) {
|
|
132
|
-
const parent = stack.length > 1 ? stack[stack.length - 2] : null;
|
|
133
|
-
if (parent) {
|
|
134
|
-
for (const k of Object.keys(parent.obj)) {
|
|
135
|
-
if (parent.obj[k] === current.obj) {
|
|
136
|
-
parent.obj[k] = [itemValue];
|
|
137
|
-
current.obj = parent.obj[k] as unknown as Record<string, YamlValue>;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} else if (Array.isArray(current.obj)) {
|
|
143
|
-
current.obj.push(itemValue);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return frontmatter;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function reconstructFrontmatter(obj: FrontmatterObject): string {
|
|
152
|
-
const lines: string[] = [];
|
|
153
|
-
|
|
154
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
155
|
-
if (value === null || value === undefined) continue;
|
|
156
|
-
|
|
157
|
-
if (Array.isArray(value)) {
|
|
158
|
-
if (value.length === 0) {
|
|
159
|
-
lines.push(`${key}: []`);
|
|
160
|
-
} else if (
|
|
161
|
-
value.every((v) => typeof v === "string") &&
|
|
162
|
-
value.length <= 3 &&
|
|
163
|
-
value.join(", ").length < 60
|
|
164
|
-
) {
|
|
165
|
-
lines.push(`${key}: [${value.join(", ")}]`);
|
|
166
|
-
} else {
|
|
167
|
-
lines.push(`${key}:`);
|
|
168
|
-
for (const item of value) {
|
|
169
|
-
const s = String(item);
|
|
170
|
-
lines.push(
|
|
171
|
-
` - ${typeof item === "string" && (s.includes(":") || s.includes("#")) ? `"${s}"` : s}`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
} else if (typeof value === "object") {
|
|
176
|
-
lines.push(`${key}:`);
|
|
177
|
-
for (const [subkey, subval] of Object.entries(value)) {
|
|
178
|
-
if (subval === null || subval === undefined) continue;
|
|
179
|
-
if (Array.isArray(subval)) {
|
|
180
|
-
if (subval.length === 0) {
|
|
181
|
-
lines.push(` ${subkey}: []`);
|
|
182
|
-
} else if (
|
|
183
|
-
subval.every((v: unknown) => typeof v === "string") &&
|
|
184
|
-
subval.length <= 3 &&
|
|
185
|
-
(subval as string[]).join(", ").length < 60
|
|
186
|
-
) {
|
|
187
|
-
lines.push(` ${subkey}: [${(subval as string[]).join(", ")}]`);
|
|
188
|
-
} else {
|
|
189
|
-
lines.push(` ${subkey}:`);
|
|
190
|
-
for (const item of subval) {
|
|
191
|
-
lines.push(` - ${item}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
} else if (typeof subval === "object") {
|
|
195
|
-
lines.push(` ${subkey}:`);
|
|
196
|
-
for (const [subsubkey, subsubval] of Object.entries(
|
|
197
|
-
subval as Record<string, unknown>,
|
|
198
|
-
)) {
|
|
199
|
-
if (subsubval === null || subsubval === undefined) continue;
|
|
200
|
-
if (Array.isArray(subsubval)) {
|
|
201
|
-
if (subsubval.length === 0) {
|
|
202
|
-
lines.push(` ${subsubkey}: []`);
|
|
203
|
-
} else {
|
|
204
|
-
lines.push(` ${subsubkey}:`);
|
|
205
|
-
for (const item of subsubval) lines.push(` - ${item}`);
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
lines.push(` ${subsubkey}: ${subsubval}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} else {
|
|
212
|
-
const sv = String(subval);
|
|
213
|
-
lines.push(
|
|
214
|
-
` ${subkey}: ${sv.includes(":") || sv.includes("#") ? `"${sv}"` : sv}`,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
const sv = String(value);
|
|
220
|
-
if (
|
|
221
|
-
sv.includes(":") ||
|
|
222
|
-
sv.includes("#") ||
|
|
223
|
-
sv.startsWith("[") ||
|
|
224
|
-
sv.startsWith("{")
|
|
225
|
-
) {
|
|
226
|
-
lines.push(`${key}: "${sv}"`);
|
|
227
|
-
} else {
|
|
228
|
-
lines.push(`${key}: ${sv}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return lines.join("\n");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function spliceFrontmatter(
|
|
237
|
-
content: string,
|
|
238
|
-
newObj: FrontmatterObject,
|
|
239
|
-
): string {
|
|
240
|
-
const yamlStr = reconstructFrontmatter(newObj);
|
|
241
|
-
const match = content.match(/^---\r?\n[\s\S]+?\r?\n---/);
|
|
242
|
-
if (match) {
|
|
243
|
-
return `---\n${yamlStr}\n---` + content.slice(match[0].length);
|
|
244
|
-
}
|
|
245
|
-
return `---\n${yamlStr}\n---\n\n` + content;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function parseMustHavesBlock(
|
|
249
|
-
content: string,
|
|
250
|
-
blockName: string,
|
|
251
|
-
): unknown[] {
|
|
252
|
-
const fmMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
253
|
-
if (!fmMatch) return [];
|
|
254
|
-
|
|
255
|
-
const yaml = fmMatch[1];
|
|
256
|
-
const mustHavesMatch = yaml.match(/^(\s*)must_haves:\s*$/m);
|
|
257
|
-
if (!mustHavesMatch) return [];
|
|
258
|
-
const mustHavesIndent = mustHavesMatch[1].length;
|
|
259
|
-
|
|
260
|
-
const blockPattern = new RegExp(`^(\\s+)${blockName}:\\s*$`, "m");
|
|
261
|
-
const blockMatch = yaml.match(blockPattern);
|
|
262
|
-
if (!blockMatch) return [];
|
|
263
|
-
|
|
264
|
-
const blockIndent = blockMatch[1].length;
|
|
265
|
-
if (blockIndent <= mustHavesIndent) return [];
|
|
266
|
-
|
|
267
|
-
const blockStart = yaml.indexOf(blockMatch[0]);
|
|
268
|
-
if (blockStart === -1) return [];
|
|
269
|
-
|
|
270
|
-
const afterBlock = yaml.slice(blockStart);
|
|
271
|
-
const blockLines = afterBlock.split(/\r?\n/).slice(1);
|
|
272
|
-
|
|
273
|
-
const items: unknown[] = [];
|
|
274
|
-
let current: FrontmatterObject | string | null = null;
|
|
275
|
-
let listItemIndent = -1;
|
|
276
|
-
|
|
277
|
-
for (const line of blockLines) {
|
|
278
|
-
if (line.trim() === "") continue;
|
|
279
|
-
const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
280
|
-
if (indent <= blockIndent && line.trim() !== "") break;
|
|
281
|
-
|
|
282
|
-
const trimmed = line.trim();
|
|
283
|
-
|
|
284
|
-
if (trimmed.startsWith("- ")) {
|
|
285
|
-
if (listItemIndent === -1) listItemIndent = indent;
|
|
286
|
-
|
|
287
|
-
if (indent === listItemIndent) {
|
|
288
|
-
if (current) items.push(current);
|
|
289
|
-
current = {};
|
|
290
|
-
const afterDash = trimmed.slice(2);
|
|
291
|
-
if (!afterDash.includes(":")) {
|
|
292
|
-
current = afterDash.replace(/^["']|["']$/g, "");
|
|
293
|
-
} else {
|
|
294
|
-
const kvMatch = afterDash.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
|
|
295
|
-
if (kvMatch) {
|
|
296
|
-
current = {};
|
|
297
|
-
current[kvMatch[1]] = kvMatch[2];
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (current && typeof current === "object" && indent > listItemIndent) {
|
|
305
|
-
if (trimmed.startsWith("- ")) {
|
|
306
|
-
const arrVal = trimmed.slice(2).replace(/^["']|["']$/g, "");
|
|
307
|
-
const keys = Object.keys(current);
|
|
308
|
-
const lastKey = keys[keys.length - 1];
|
|
309
|
-
if (lastKey && !Array.isArray(current[lastKey])) {
|
|
310
|
-
current[lastKey] = current[lastKey] ? [current[lastKey]] : [];
|
|
311
|
-
}
|
|
312
|
-
const arr = current[lastKey];
|
|
313
|
-
if (lastKey && Array.isArray(arr)) arr.push(arrVal);
|
|
314
|
-
} else {
|
|
315
|
-
const kvMatch = trimmed.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
|
|
316
|
-
if (kvMatch) {
|
|
317
|
-
const val = kvMatch[2];
|
|
318
|
-
current[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (current) items.push(current);
|
|
324
|
-
|
|
325
|
-
return items;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// ─── Frontmatter CRUD commands ────────────────────────────────────────────────
|
|
329
|
-
|
|
330
|
-
export function cmdFrontmatterGet(
|
|
331
|
-
cwd: string,
|
|
332
|
-
filePath: string | undefined,
|
|
333
|
-
field: string | null,
|
|
334
|
-
raw: boolean,
|
|
335
|
-
): void {
|
|
336
|
-
if (!filePath) {
|
|
337
|
-
gsdError("file path required");
|
|
338
|
-
}
|
|
339
|
-
if (filePath.includes("\0")) {
|
|
340
|
-
gsdError("file path contains null bytes");
|
|
341
|
-
}
|
|
342
|
-
const fullPath = path.isAbsolute(filePath)
|
|
343
|
-
? filePath
|
|
344
|
-
: path.join(cwd, filePath);
|
|
345
|
-
const content = safeReadFile(fullPath);
|
|
346
|
-
if (!content) {
|
|
347
|
-
output({ error: "File not found", path: filePath }, raw);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const fm = extractFrontmatter(content);
|
|
351
|
-
if (field) {
|
|
352
|
-
const value = fm[field];
|
|
353
|
-
if (value === undefined) {
|
|
354
|
-
output({ error: "Field not found", field }, raw);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
output({ [field]: value }, raw, JSON.stringify(value));
|
|
358
|
-
} else {
|
|
359
|
-
output(fm, raw);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export function cmdFrontmatterSet(
|
|
364
|
-
cwd: string,
|
|
365
|
-
filePath: string | undefined,
|
|
366
|
-
field: string | undefined,
|
|
367
|
-
value: string | undefined,
|
|
368
|
-
raw: boolean,
|
|
369
|
-
): void {
|
|
370
|
-
if (!filePath || !field || value === undefined) {
|
|
371
|
-
gsdError("file, field, and value required");
|
|
372
|
-
}
|
|
373
|
-
if (filePath.includes("\0")) {
|
|
374
|
-
gsdError("file path contains null bytes");
|
|
375
|
-
}
|
|
376
|
-
const fullPath = path.isAbsolute(filePath)
|
|
377
|
-
? filePath
|
|
378
|
-
: path.join(cwd, filePath);
|
|
379
|
-
if (!fs.existsSync(fullPath)) {
|
|
380
|
-
output({ error: "File not found", path: filePath }, raw);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
384
|
-
const fm = extractFrontmatter(content);
|
|
385
|
-
let parsedValue: YamlValue;
|
|
386
|
-
try {
|
|
387
|
-
const parsed: unknown = JSON.parse(value);
|
|
388
|
-
// Only accept YAML-compatible parsed values
|
|
389
|
-
parsedValue = parsed as YamlValue;
|
|
390
|
-
} catch {
|
|
391
|
-
parsedValue = value;
|
|
392
|
-
}
|
|
393
|
-
fm[field] = parsedValue;
|
|
394
|
-
const newContent = spliceFrontmatter(content, fm);
|
|
395
|
-
fs.writeFileSync(fullPath, normalizeMd(newContent), "utf-8");
|
|
396
|
-
output({ updated: true, field, value: parsedValue }, raw, "true");
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
export function cmdFrontmatterMerge(
|
|
400
|
-
cwd: string,
|
|
401
|
-
filePath: string | undefined,
|
|
402
|
-
data: string | undefined,
|
|
403
|
-
raw: boolean,
|
|
404
|
-
): void {
|
|
405
|
-
if (!filePath || !data) {
|
|
406
|
-
gsdError("file and data required");
|
|
407
|
-
}
|
|
408
|
-
const fullPath = path.isAbsolute(filePath)
|
|
409
|
-
? filePath
|
|
410
|
-
: path.join(cwd, filePath);
|
|
411
|
-
if (!fs.existsSync(fullPath)) {
|
|
412
|
-
output({ error: "File not found", path: filePath }, raw);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
416
|
-
const fm = extractFrontmatter(content);
|
|
417
|
-
let mergeData: FrontmatterObject;
|
|
418
|
-
try {
|
|
419
|
-
mergeData = JSON.parse(data);
|
|
420
|
-
} catch {
|
|
421
|
-
gsdError("Invalid JSON for --data");
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
Object.assign(fm, mergeData);
|
|
425
|
-
const newContent = spliceFrontmatter(content, fm);
|
|
426
|
-
fs.writeFileSync(fullPath, normalizeMd(newContent), "utf-8");
|
|
427
|
-
output({ merged: true, fields: Object.keys(mergeData) }, raw, "true");
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
export function cmdFrontmatterValidate(
|
|
431
|
-
cwd: string,
|
|
432
|
-
filePath: string | undefined,
|
|
433
|
-
schemaName: string | undefined,
|
|
434
|
-
raw: boolean,
|
|
435
|
-
): void {
|
|
436
|
-
if (!filePath || !schemaName) {
|
|
437
|
-
gsdError("file and schema required");
|
|
438
|
-
}
|
|
439
|
-
const schema = FRONTMATTER_SCHEMAS[schemaName as FrontmatterSchema];
|
|
440
|
-
if (!schema) {
|
|
441
|
-
gsdError(
|
|
442
|
-
`Unknown schema: ${schemaName}. Available: ${Object.keys(FRONTMATTER_SCHEMAS).join(", ")}`,
|
|
443
|
-
);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
const fullPath = path.isAbsolute(filePath)
|
|
447
|
-
? filePath
|
|
448
|
-
: path.join(cwd, filePath);
|
|
449
|
-
const content = safeReadFile(fullPath);
|
|
450
|
-
if (!content) {
|
|
451
|
-
output({ error: "File not found", path: filePath }, raw);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const fm = extractFrontmatter(content);
|
|
455
|
-
const missing = schema.required.filter((f) => fm[f] === undefined);
|
|
456
|
-
const present = schema.required.filter((f) => fm[f] !== undefined);
|
|
457
|
-
output(
|
|
458
|
-
{ valid: missing.length === 0, missing, present, schema: schemaName },
|
|
459
|
-
raw,
|
|
460
|
-
missing.length === 0 ? "valid" : "invalid",
|
|
461
|
-
);
|
|
462
|
-
}
|