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.
Files changed (65) hide show
  1. package/dist/pi-gsd-hooks.js +1532 -0
  2. package/package.json +3 -5
  3. package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
  4. package/src/cli.ts +0 -644
  5. package/src/commands/base.ts +0 -67
  6. package/src/commands/commit.ts +0 -22
  7. package/src/commands/config.ts +0 -71
  8. package/src/commands/frontmatter.ts +0 -51
  9. package/src/commands/index.ts +0 -76
  10. package/src/commands/init.ts +0 -43
  11. package/src/commands/milestone.ts +0 -37
  12. package/src/commands/phase.ts +0 -92
  13. package/src/commands/progress.ts +0 -71
  14. package/src/commands/roadmap.ts +0 -40
  15. package/src/commands/scaffold.ts +0 -19
  16. package/src/commands/state.ts +0 -102
  17. package/src/commands/template.ts +0 -52
  18. package/src/commands/verify.ts +0 -70
  19. package/src/commands/workstream.ts +0 -98
  20. package/src/commands/wxp.ts +0 -65
  21. package/src/lib/commands.ts +0 -1040
  22. package/src/lib/config.ts +0 -385
  23. package/src/lib/core.ts +0 -1167
  24. package/src/lib/frontmatter.ts +0 -462
  25. package/src/lib/init.ts +0 -517
  26. package/src/lib/milestone.ts +0 -290
  27. package/src/lib/model-profiles.ts +0 -272
  28. package/src/lib/phase.ts +0 -1012
  29. package/src/lib/profile-output.ts +0 -237
  30. package/src/lib/profile-pipeline.ts +0 -556
  31. package/src/lib/roadmap.ts +0 -378
  32. package/src/lib/schemas.ts +0 -290
  33. package/src/lib/security.ts +0 -176
  34. package/src/lib/state.ts +0 -1175
  35. package/src/lib/template.ts +0 -246
  36. package/src/lib/uat.ts +0 -289
  37. package/src/lib/verify.ts +0 -879
  38. package/src/lib/workstream.ts +0 -524
  39. package/src/output.ts +0 -45
  40. package/src/schemas/pi-gsd-settings.schema.json +0 -80
  41. package/src/schemas/wxp.xsd +0 -619
  42. package/src/schemas/wxp.zod.ts +0 -318
  43. package/src/wxp/__tests__/arguments.test.ts +0 -86
  44. package/src/wxp/__tests__/conditions.test.ts +0 -106
  45. package/src/wxp/__tests__/executor.test.ts +0 -95
  46. package/src/wxp/__tests__/helpers.ts +0 -26
  47. package/src/wxp/__tests__/integration.test.ts +0 -166
  48. package/src/wxp/__tests__/new-features.test.ts +0 -222
  49. package/src/wxp/__tests__/parser.test.ts +0 -159
  50. package/src/wxp/__tests__/paste.test.ts +0 -66
  51. package/src/wxp/__tests__/schema.test.ts +0 -120
  52. package/src/wxp/__tests__/security.test.ts +0 -87
  53. package/src/wxp/__tests__/shell.test.ts +0 -85
  54. package/src/wxp/__tests__/string-ops.test.ts +0 -25
  55. package/src/wxp/__tests__/variables.test.ts +0 -65
  56. package/src/wxp/arguments.ts +0 -89
  57. package/src/wxp/conditions.ts +0 -78
  58. package/src/wxp/executor.ts +0 -191
  59. package/src/wxp/index.ts +0 -191
  60. package/src/wxp/parser.ts +0 -198
  61. package/src/wxp/paste.ts +0 -51
  62. package/src/wxp/security.ts +0 -102
  63. package/src/wxp/shell.ts +0 -81
  64. package/src/wxp/string-ops.ts +0 -44
  65. package/src/wxp/variables.ts +0 -109
@@ -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
- }