pan-wizard 2.8.1
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/LICENSE +21 -0
- package/README.md +772 -0
- package/agents/pan-debugger.md +1246 -0
- package/agents/pan-document_code.md +965 -0
- package/agents/pan-executor.md +469 -0
- package/agents/pan-integration-checker.md +443 -0
- package/agents/pan-phase-researcher.md +572 -0
- package/agents/pan-plan-checker.md +763 -0
- package/agents/pan-planner.md +1297 -0
- package/agents/pan-project-researcher.md +647 -0
- package/agents/pan-research-synthesizer.md +239 -0
- package/agents/pan-reviewer.md +112 -0
- package/agents/pan-roadmapper.md +642 -0
- package/agents/pan-verifier.md +672 -0
- package/assets/pan-logo-2000-transparent.svg +30 -0
- package/assets/pan-logo-2000.svg +43 -0
- package/assets/terminal.svg +119 -0
- package/bin/install-lib.cjs +616 -0
- package/bin/install.js +1936 -0
- package/commands/pan/add-phase.md +44 -0
- package/commands/pan/assumptions.md +47 -0
- package/commands/pan/audit-deployment.md +378 -0
- package/commands/pan/debug.md +168 -0
- package/commands/pan/discord.md +19 -0
- package/commands/pan/discuss-phase.md +84 -0
- package/commands/pan/exec-phase.md +45 -0
- package/commands/pan/focus-auto.md +323 -0
- package/commands/pan/focus-design.md +816 -0
- package/commands/pan/focus-exec.md +316 -0
- package/commands/pan/focus-plan.md +101 -0
- package/commands/pan/focus-scan.md +272 -0
- package/commands/pan/focus-sync.md +104 -0
- package/commands/pan/health.md +23 -0
- package/commands/pan/help.md +23 -0
- package/commands/pan/insert-phase.md +33 -0
- package/commands/pan/map-codebase.md +72 -0
- package/commands/pan/milestone-audit.md +37 -0
- package/commands/pan/milestone-cleanup.md +19 -0
- package/commands/pan/milestone-done.md +137 -0
- package/commands/pan/milestone-gaps.md +35 -0
- package/commands/pan/milestone-new.md +45 -0
- package/commands/pan/new-project.md +43 -0
- package/commands/pan/patches.md +110 -0
- package/commands/pan/pause.md +39 -0
- package/commands/pan/phase-budget.md +23 -0
- package/commands/pan/phase-tests.md +42 -0
- package/commands/pan/plan-phase.md +46 -0
- package/commands/pan/profile.md +36 -0
- package/commands/pan/progress.md +25 -0
- package/commands/pan/quick.md +42 -0
- package/commands/pan/remove-phase.md +32 -0
- package/commands/pan/research-phase.md +190 -0
- package/commands/pan/resume.md +41 -0
- package/commands/pan/retro.md +33 -0
- package/commands/pan/settings.md +37 -0
- package/commands/pan/todo-add.md +48 -0
- package/commands/pan/todo-check.md +46 -0
- package/commands/pan/update.md +38 -0
- package/commands/pan/verify-phase.md +39 -0
- package/hooks/dist/pan-check-update.js +62 -0
- package/hooks/dist/pan-context-monitor.js +122 -0
- package/hooks/dist/pan-statusline.js +108 -0
- package/package.json +66 -0
- package/pan-wizard-core/bin/lib/codebase.cjs +746 -0
- package/pan-wizard-core/bin/lib/commands.cjs +1435 -0
- package/pan-wizard-core/bin/lib/config.cjs +611 -0
- package/pan-wizard-core/bin/lib/constants.cjs +696 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +150 -0
- package/pan-wizard-core/bin/lib/core.cjs +650 -0
- package/pan-wizard-core/bin/lib/focus.cjs +900 -0
- package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -0
- package/pan-wizard-core/bin/lib/init.cjs +881 -0
- package/pan-wizard-core/bin/lib/milestone.cjs +276 -0
- package/pan-wizard-core/bin/lib/phase.cjs +1212 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +470 -0
- package/pan-wizard-core/bin/lib/state.cjs +1029 -0
- package/pan-wizard-core/bin/lib/template.cjs +314 -0
- package/pan-wizard-core/bin/lib/utils.cjs +171 -0
- package/pan-wizard-core/bin/lib/verify.cjs +1808 -0
- package/pan-wizard-core/bin/pan-tools.cjs +773 -0
- package/pan-wizard-core/references/checkpoints.md +776 -0
- package/pan-wizard-core/references/continuation-format.md +249 -0
- package/pan-wizard-core/references/decimal-phase-calculation.md +65 -0
- package/pan-wizard-core/references/git-integration.md +248 -0
- package/pan-wizard-core/references/git-planning-commit.md +38 -0
- package/pan-wizard-core/references/model-profile-resolution.md +34 -0
- package/pan-wizard-core/references/model-profiles.md +111 -0
- package/pan-wizard-core/references/phase-argument-parsing.md +61 -0
- package/pan-wizard-core/references/planning-config.md +196 -0
- package/pan-wizard-core/references/questioning.md +145 -0
- package/pan-wizard-core/references/tdd.md +263 -0
- package/pan-wizard-core/references/ui-brand.md +160 -0
- package/pan-wizard-core/references/verification-patterns.md +612 -0
- package/pan-wizard-core/templates/codebase/architecture.md +283 -0
- package/pan-wizard-core/templates/codebase/best-practices.md +133 -0
- package/pan-wizard-core/templates/codebase/concerns.md +325 -0
- package/pan-wizard-core/templates/codebase/conventions.md +307 -0
- package/pan-wizard-core/templates/codebase/integrations.md +305 -0
- package/pan-wizard-core/templates/codebase/relationships.md +124 -0
- package/pan-wizard-core/templates/codebase/stack.md +199 -0
- package/pan-wizard-core/templates/codebase/structure.md +298 -0
- package/pan-wizard-core/templates/codebase/testing.md +480 -0
- package/pan-wizard-core/templates/config.json +37 -0
- package/pan-wizard-core/templates/context.md +283 -0
- package/pan-wizard-core/templates/continue-here.md +78 -0
- package/pan-wizard-core/templates/debug-subagent-prompt.md +91 -0
- package/pan-wizard-core/templates/debug.md +164 -0
- package/pan-wizard-core/templates/discovery.md +146 -0
- package/pan-wizard-core/templates/milestone-archive.md +123 -0
- package/pan-wizard-core/templates/milestone.md +115 -0
- package/pan-wizard-core/templates/phase-prompt.md +593 -0
- package/pan-wizard-core/templates/planner-subagent-prompt.md +117 -0
- package/pan-wizard-core/templates/project.md +184 -0
- package/pan-wizard-core/templates/requirements.md +231 -0
- package/pan-wizard-core/templates/research-project/architecture.md +204 -0
- package/pan-wizard-core/templates/research-project/features.md +147 -0
- package/pan-wizard-core/templates/research-project/pitfalls.md +200 -0
- package/pan-wizard-core/templates/research-project/stack.md +120 -0
- package/pan-wizard-core/templates/research-project/summary.md +170 -0
- package/pan-wizard-core/templates/research.md +552 -0
- package/pan-wizard-core/templates/retrospective.md +54 -0
- package/pan-wizard-core/templates/roadmap.md +202 -0
- package/pan-wizard-core/templates/standards.md +24 -0
- package/pan-wizard-core/templates/state.md +176 -0
- package/pan-wizard-core/templates/summary-complex.md +59 -0
- package/pan-wizard-core/templates/summary-minimal.md +41 -0
- package/pan-wizard-core/templates/summary-standard.md +49 -0
- package/pan-wizard-core/templates/summary.md +249 -0
- package/pan-wizard-core/templates/uat.md +247 -0
- package/pan-wizard-core/templates/user-setup.md +311 -0
- package/pan-wizard-core/templates/validation.md +76 -0
- package/pan-wizard-core/templates/verification-report.md +322 -0
- package/pan-wizard-core/workflows/add-phase.md +111 -0
- package/pan-wizard-core/workflows/assumptions.md +178 -0
- package/pan-wizard-core/workflows/diagnose-issues.md +219 -0
- package/pan-wizard-core/workflows/discuss-phase.md +542 -0
- package/pan-wizard-core/workflows/exec-phase.md +572 -0
- package/pan-wizard-core/workflows/execute-plan.md +448 -0
- package/pan-wizard-core/workflows/health.md +156 -0
- package/pan-wizard-core/workflows/help.md +431 -0
- package/pan-wizard-core/workflows/insert-phase.md +129 -0
- package/pan-wizard-core/workflows/map-codebase.md +401 -0
- package/pan-wizard-core/workflows/milestone-audit.md +297 -0
- package/pan-wizard-core/workflows/milestone-cleanup.md +152 -0
- package/pan-wizard-core/workflows/milestone-gaps.md +274 -0
- package/pan-wizard-core/workflows/milestone-new.md +382 -0
- package/pan-wizard-core/workflows/new-project.md +1178 -0
- package/pan-wizard-core/workflows/pause.md +122 -0
- package/pan-wizard-core/workflows/phase-tests.md +388 -0
- package/pan-wizard-core/workflows/plan-phase.md +569 -0
- package/pan-wizard-core/workflows/profile.md +115 -0
- package/pan-wizard-core/workflows/progress.md +381 -0
- package/pan-wizard-core/workflows/quick.md +453 -0
- package/pan-wizard-core/workflows/remove-phase.md +154 -0
- package/pan-wizard-core/workflows/research-phase.md +73 -0
- package/pan-wizard-core/workflows/resume-project.md +306 -0
- package/pan-wizard-core/workflows/retro.md +121 -0
- package/pan-wizard-core/workflows/settings.md +213 -0
- package/pan-wizard-core/workflows/todo-add.md +157 -0
- package/pan-wizard-core/workflows/todo-check.md +176 -0
- package/pan-wizard-core/workflows/transition.md +544 -0
- package/pan-wizard-core/workflows/update.md +219 -0
- package/pan-wizard-core/workflows/verify-phase.md +301 -0
- package/scripts/build-hooks.js +43 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter -- YAML frontmatter parsing, serialization, and CRUD commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { safeReadFile, output, error } = require('./core.cjs');
|
|
8
|
+
const { FIELD_VALUE_RE, PRIORITY_LEVELS, EFFORT_SIZES } = require('./constants.cjs');
|
|
9
|
+
|
|
10
|
+
// --- Inline array rendering thresholds -------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** Maximum number of items before switching from inline [a, b] to multi-line array */
|
|
13
|
+
const MAX_INLINE_ARRAY_ITEMS = 3;
|
|
14
|
+
|
|
15
|
+
/** Maximum character width of joined items before switching to multi-line array */
|
|
16
|
+
const MAX_INLINE_ARRAY_WIDTH = 60;
|
|
17
|
+
|
|
18
|
+
// --- Parsing engine --------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Process a single YAML line: detect key-value pairs, inline arrays, nested objects, or list items.
|
|
22
|
+
* @param {string} line - The raw YAML line
|
|
23
|
+
* @param {number} indent - Indentation level of this line
|
|
24
|
+
* @param {Object} current - Current stack frame { obj, key, indent }
|
|
25
|
+
* @param {Object[]} stack - Full parser stack
|
|
26
|
+
*/
|
|
27
|
+
function processYamlLine(line, indent, current, stack) {
|
|
28
|
+
const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
|
|
29
|
+
if (keyMatch) {
|
|
30
|
+
const key = keyMatch[2];
|
|
31
|
+
const value = keyMatch[3].trim();
|
|
32
|
+
|
|
33
|
+
if (value === '' || value === '[') {
|
|
34
|
+
current.obj[key] = value === '[' ? [] : {};
|
|
35
|
+
current.key = null;
|
|
36
|
+
stack.push({ obj: current.obj[key], key: null, indent });
|
|
37
|
+
} else if (value.startsWith('[') && value.endsWith(']')) {
|
|
38
|
+
current.obj[key] = value.slice(1, -1).split(',').map(item => item.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
39
|
+
current.key = null;
|
|
40
|
+
} else {
|
|
41
|
+
current.obj[key] = value.replace(/^["']|["']$/g, '');
|
|
42
|
+
current.key = null;
|
|
43
|
+
}
|
|
44
|
+
} else if (line.trim().startsWith('- ')) {
|
|
45
|
+
const itemValue = line.trim().slice(2).replace(/^["']|["']$/g, '');
|
|
46
|
+
|
|
47
|
+
if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
|
|
48
|
+
const parent = stack.length > 1 ? stack[stack.length - 2] : null;
|
|
49
|
+
if (parent) {
|
|
50
|
+
for (const parentKey of Object.keys(parent.obj)) {
|
|
51
|
+
if (parent.obj[parentKey] === current.obj) {
|
|
52
|
+
parent.obj[parentKey] = [itemValue];
|
|
53
|
+
current.obj = parent.obj[parentKey];
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else if (Array.isArray(current.obj)) {
|
|
59
|
+
current.obj.push(itemValue);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse YAML frontmatter from markdown content into a plain object.
|
|
66
|
+
* @param {string} content - Markdown content with optional ---delimited frontmatter
|
|
67
|
+
* @returns {Object} Parsed frontmatter key-value pairs (empty object if none found)
|
|
68
|
+
*/
|
|
69
|
+
function extractFrontmatter(content) {
|
|
70
|
+
/*
|
|
71
|
+
* Stack-based parser that tracks nesting depth via indentation.
|
|
72
|
+
* Each stack entry represents a nested object or array context.
|
|
73
|
+
*
|
|
74
|
+
* Algorithm overview:
|
|
75
|
+
* 1. Extract the raw YAML between the opening and closing --- delimiters.
|
|
76
|
+
* 2. Split into lines and iterate, tracking indentation level per line.
|
|
77
|
+
* 3. Maintain a stack where each frame holds:
|
|
78
|
+
* - obj: the current object or array being populated
|
|
79
|
+
* - key: (unused reservation for future array grouping)
|
|
80
|
+
* - indent: the indentation level at which this frame was pushed
|
|
81
|
+
* 4. On each line, pop stack frames whose indent is >= current indent,
|
|
82
|
+
* effectively "closing" nested contexts when de-indented.
|
|
83
|
+
* 5. Detect key-value pairs, inline arrays, nested objects, and list items
|
|
84
|
+
* and insert them into the current stack frame's object.
|
|
85
|
+
* 6. When a list item (- value) is found inside what was initialized as an
|
|
86
|
+
* empty object {}, convert that object to an array in the parent frame.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
const frontmatter = {};
|
|
90
|
+
|
|
91
|
+
// --- Frontmatter delimiter detection ---
|
|
92
|
+
// Match the first YAML block bounded by opening "---\n" and closing "\n---"
|
|
93
|
+
const match = content.match(/^---\n([\s\S]+?)\n---/);
|
|
94
|
+
if (!match) return frontmatter;
|
|
95
|
+
|
|
96
|
+
const yaml = match[1];
|
|
97
|
+
const lines = yaml.split('\n');
|
|
98
|
+
|
|
99
|
+
// Stack to track nested objects: [{obj, key, indent}]
|
|
100
|
+
// obj = object to write to, key = current key collecting array items, indent = indentation level
|
|
101
|
+
let stack = [{ obj: frontmatter, key: null, indent: -1 }];
|
|
102
|
+
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
// Skip empty lines -- they carry no YAML data
|
|
105
|
+
if (line.trim() === '') continue;
|
|
106
|
+
|
|
107
|
+
// --- Indentation-based stack pop logic ---
|
|
108
|
+
// Calculate indentation as the count of leading whitespace characters.
|
|
109
|
+
// This determines which nesting level the current line belongs to.
|
|
110
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
111
|
+
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
112
|
+
|
|
113
|
+
// Pop stack frames until we find one whose indent is strictly less than
|
|
114
|
+
// the current line's indent. This "closes" any nested contexts that have
|
|
115
|
+
// ended due to de-indentation (e.g., returning from a nested object back
|
|
116
|
+
// to the parent level).
|
|
117
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
118
|
+
stack.pop();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const current = stack[stack.length - 1];
|
|
122
|
+
processYamlLine(line, indent, current, stack);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return frontmatter;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Serialize a plain object back into YAML frontmatter string (without --- delimiters).
|
|
130
|
+
*
|
|
131
|
+
* Serialization strategy:
|
|
132
|
+
* - Short string arrays (at most MAX_INLINE_ARRAY_ITEMS items whose joined width
|
|
133
|
+
* is under MAX_INLINE_ARRAY_WIDTH characters) render inline: key: [a, b, c]
|
|
134
|
+
* - Longer or non-string arrays render multi-line with "- item" syntax
|
|
135
|
+
* - Nested objects recurse up to 3 levels deep (top > sub > subsub)
|
|
136
|
+
* - Scalar values containing colons, hashes, or leading brackets are quoted
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} obj - Key-value pairs to serialize as YAML
|
|
139
|
+
* @returns {string} YAML-formatted string
|
|
140
|
+
*/
|
|
141
|
+
function reconstructFrontmatter(obj) {
|
|
142
|
+
const lines = [];
|
|
143
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
144
|
+
renderYamlEntry(lines, key, value, 0);
|
|
145
|
+
}
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render a single key-value pair as YAML lines at the given indent depth.
|
|
151
|
+
* Handles scalars, arrays (inline or multi-line), and nested objects recursively.
|
|
152
|
+
*/
|
|
153
|
+
function renderYamlEntry(lines, key, value, depth) {
|
|
154
|
+
if (value === null || value === undefined) return;
|
|
155
|
+
const indent = ' '.repeat(depth);
|
|
156
|
+
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
if (value.length === 0) {
|
|
159
|
+
lines.push(`${indent}${key}: []`);
|
|
160
|
+
} else if (value.every(item => typeof item === 'string') && value.length <= MAX_INLINE_ARRAY_ITEMS && value.join(', ').length < MAX_INLINE_ARRAY_WIDTH) {
|
|
161
|
+
lines.push(`${indent}${key}: [${value.join(', ')}]`);
|
|
162
|
+
} else {
|
|
163
|
+
lines.push(`${indent}${key}:`);
|
|
164
|
+
const itemIndent = ' '.repeat(depth + 1);
|
|
165
|
+
for (const item of value) {
|
|
166
|
+
lines.push(`${itemIndent}- ${quoteIfNeeded(item)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else if (typeof value === 'object') {
|
|
170
|
+
lines.push(`${indent}${key}:`);
|
|
171
|
+
for (const [childKey, childVal] of Object.entries(value)) {
|
|
172
|
+
renderYamlEntry(lines, childKey, childVal, depth + 1);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
lines.push(`${indent}${key}: ${quoteIfNeeded(value)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Quote a value if it contains YAML-special characters.
|
|
181
|
+
*/
|
|
182
|
+
function quoteIfNeeded(value) {
|
|
183
|
+
const str = String(value);
|
|
184
|
+
if (str.includes(':') || str.includes('#') || str.startsWith('[') || str.startsWith('{')) {
|
|
185
|
+
return `"${str}"`;
|
|
186
|
+
}
|
|
187
|
+
return str;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Replace or insert frontmatter in markdown content with a new object.
|
|
192
|
+
* @param {string} content - Original markdown content
|
|
193
|
+
* @param {Object} newObj - New frontmatter key-value pairs
|
|
194
|
+
* @returns {string} Content with updated frontmatter block
|
|
195
|
+
*/
|
|
196
|
+
function spliceFrontmatter(content, newObj) {
|
|
197
|
+
const yamlStr = reconstructFrontmatter(newObj);
|
|
198
|
+
const match = content.match(/^---\n[\s\S]+?\n---/);
|
|
199
|
+
if (match) {
|
|
200
|
+
return `---\n${yamlStr}\n---` + content.slice(match[0].length);
|
|
201
|
+
}
|
|
202
|
+
return `---\n${yamlStr}\n---\n\n` + content;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Parse a specific block (artifacts, key_links, truths) from must_haves in raw YAML frontmatter.
|
|
207
|
+
*
|
|
208
|
+
* Block structure being parsed:
|
|
209
|
+
* The must_haves section is a 3-level nested YAML structure:
|
|
210
|
+
* must_haves: (level 1 -- 2-space indent)
|
|
211
|
+
* artifacts: (level 2 -- 4-space indent, the blockName)
|
|
212
|
+
* - path: foo (level 3 -- 6-space indent, list items)
|
|
213
|
+
* provides: bar (level 3+ -- 8-space indent, continuation k-v pairs)
|
|
214
|
+
*
|
|
215
|
+
* This function finds the named block at the 4-space level, then parses
|
|
216
|
+
* its list items (at 6-space indent) and their nested key-value pairs
|
|
217
|
+
* (at 8+ space indent) into an array of objects or strings.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} content - Full markdown content with frontmatter
|
|
220
|
+
* @param {string} blockName - Block name to extract (e.g., "artifacts", "key_links")
|
|
221
|
+
* @returns {Array} Parsed array of block items (objects or strings)
|
|
222
|
+
*/
|
|
223
|
+
function parseMustHavesBlock(content, blockName) {
|
|
224
|
+
// Extract raw YAML between --- delimiters
|
|
225
|
+
const fmMatch = content.match(/^---\n([\s\S]+?)\n---/);
|
|
226
|
+
if (!fmMatch) return [];
|
|
227
|
+
|
|
228
|
+
const yaml = fmMatch[1];
|
|
229
|
+
|
|
230
|
+
// Locate the block header at exactly 4-space indent (must_haves child level)
|
|
231
|
+
const blockPattern = new RegExp(`^\\s{4}${blockName}:\\s*$`, 'm');
|
|
232
|
+
const blockStart = yaml.search(blockPattern);
|
|
233
|
+
if (blockStart === -1) return [];
|
|
234
|
+
|
|
235
|
+
const afterBlock = yaml.slice(blockStart);
|
|
236
|
+
// Skip the header line itself, then process remaining lines
|
|
237
|
+
const blockLines = afterBlock.split('\n').slice(1);
|
|
238
|
+
|
|
239
|
+
const items = [];
|
|
240
|
+
let currentItem = null;
|
|
241
|
+
|
|
242
|
+
for (const line of blockLines) {
|
|
243
|
+
// Skip blank lines within the block
|
|
244
|
+
if (line.trim() === '') continue;
|
|
245
|
+
|
|
246
|
+
// --- Indentation-based grouping ---
|
|
247
|
+
// Measure indent to detect when we've left the block (indent <= 4
|
|
248
|
+
// means we've returned to must_haves level or a sibling block)
|
|
249
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
250
|
+
if (indent <= 4 && line.trim() !== '') break;
|
|
251
|
+
|
|
252
|
+
if (line.match(/^\s{6}-\s+/)) {
|
|
253
|
+
// New list item at 6-space indent (direct child of the block)
|
|
254
|
+
if (currentItem) items.push(currentItem);
|
|
255
|
+
currentItem = {};
|
|
256
|
+
|
|
257
|
+
// Check if it is a simple string item (no colon means not key-value)
|
|
258
|
+
const simpleMatch = line.match(/^\s{6}-\s+"?([^"]+)"?\s*$/);
|
|
259
|
+
if (simpleMatch && !line.includes(':')) {
|
|
260
|
+
currentItem = simpleMatch[1];
|
|
261
|
+
} else {
|
|
262
|
+
// Key-value on same line as dash: "- path: value"
|
|
263
|
+
const kvMatch = line.match(/^\s{6}-\s+(\w+):\s*"?([^"]*)"?\s*$/);
|
|
264
|
+
if (kvMatch) {
|
|
265
|
+
currentItem = {};
|
|
266
|
+
currentItem[kvMatch[1]] = kvMatch[2];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} else if (currentItem && typeof currentItem === 'object') {
|
|
270
|
+
// Continuation key-value at 8+ space indent (properties of current list item)
|
|
271
|
+
const kvMatch = line.match(/^\s{8,}(\w+):\s*"?([^"]*)"?\s*$/);
|
|
272
|
+
if (kvMatch) {
|
|
273
|
+
const val = kvMatch[2];
|
|
274
|
+
// Coerce pure-integer strings to numbers for convenience
|
|
275
|
+
currentItem[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
276
|
+
}
|
|
277
|
+
// Array items nested under a property at 10+ space indent
|
|
278
|
+
const arrMatch = line.match(/^\s{10,}-\s+"?([^"]+)"?\s*$/);
|
|
279
|
+
if (arrMatch) {
|
|
280
|
+
// Convert the most recently added key's scalar value into an array,
|
|
281
|
+
// then append this item to that array
|
|
282
|
+
const keys = Object.keys(currentItem);
|
|
283
|
+
const lastKey = keys[keys.length - 1];
|
|
284
|
+
if (lastKey && !Array.isArray(currentItem[lastKey])) {
|
|
285
|
+
currentItem[lastKey] = currentItem[lastKey] ? [currentItem[lastKey]] : [];
|
|
286
|
+
}
|
|
287
|
+
if (lastKey) currentItem[lastKey].push(arrMatch[1]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Push the final item if one was being accumulated
|
|
292
|
+
if (currentItem) items.push(currentItem);
|
|
293
|
+
|
|
294
|
+
return items;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- Focus field helpers ---------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Extract priority and effort from frontmatter with validation and defaults.
|
|
301
|
+
* @param {Object} fm - Parsed frontmatter object from extractFrontmatter()
|
|
302
|
+
* @returns {{ priority: string, effort: string, priorityValid: boolean, effortValid: boolean }}
|
|
303
|
+
*/
|
|
304
|
+
function extractPriorityEffort(fm) {
|
|
305
|
+
const rawPriority = fm.priority ? String(fm.priority).toUpperCase() : null;
|
|
306
|
+
const rawEffort = fm.effort ? String(fm.effort).toUpperCase() : null;
|
|
307
|
+
return {
|
|
308
|
+
priority: PRIORITY_LEVELS.includes(rawPriority) ? rawPriority : 'P3',
|
|
309
|
+
effort: EFFORT_SIZES.includes(rawEffort) ? rawEffort : 'M',
|
|
310
|
+
priorityValid: rawPriority === null || PRIORITY_LEVELS.includes(rawPriority),
|
|
311
|
+
effortValid: rawEffort === null || EFFORT_SIZES.includes(rawEffort),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- Frontmatter CRUD commands ---------------------------------------------------
|
|
316
|
+
|
|
317
|
+
const FRONTMATTER_SCHEMAS = {
|
|
318
|
+
plan: { required: ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'] },
|
|
319
|
+
summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
|
|
320
|
+
verification: { required: ['phase', 'verified', 'status', 'score'] },
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get frontmatter from a file, optionally filtered to a single field.
|
|
325
|
+
* @param {string} cwd - Working directory path
|
|
326
|
+
* @param {string} filePath - Path to the markdown file
|
|
327
|
+
* @param {string} field - Optional specific field to extract
|
|
328
|
+
* @param {boolean} raw - If true, output raw value instead of JSON
|
|
329
|
+
* @returns {void}
|
|
330
|
+
*/
|
|
331
|
+
function cmdFrontmatterGet(cwd, filePath, field, raw) {
|
|
332
|
+
if (!filePath) { error('file path required'); }
|
|
333
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
334
|
+
const content = safeReadFile(fullPath);
|
|
335
|
+
if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
336
|
+
const fm = extractFrontmatter(content);
|
|
337
|
+
if (field) {
|
|
338
|
+
const value = fm[field];
|
|
339
|
+
if (value === undefined) { output({ error: 'Field not found', field }, raw); return; }
|
|
340
|
+
output({ [field]: value }, raw, JSON.stringify(value));
|
|
341
|
+
} else {
|
|
342
|
+
output(fm, raw);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Set a single frontmatter field value in a markdown file.
|
|
348
|
+
* @param {string} cwd - Working directory path
|
|
349
|
+
* @param {string} filePath - Path to the markdown file
|
|
350
|
+
* @param {string} field - Field name to set
|
|
351
|
+
* @param {string} value - Value to set (JSON-parsed if valid)
|
|
352
|
+
* @param {boolean} raw - If true, output raw value instead of JSON
|
|
353
|
+
* @returns {void}
|
|
354
|
+
*/
|
|
355
|
+
function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
|
|
356
|
+
if (!filePath || !field || value === undefined) { error('file, field, and value required'); }
|
|
357
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
358
|
+
let content;
|
|
359
|
+
try {
|
|
360
|
+
content = fs.readFileSync(fullPath, 'utf-8');
|
|
361
|
+
} catch {
|
|
362
|
+
output({ error: 'File not found', path: filePath }, raw); return;
|
|
363
|
+
}
|
|
364
|
+
const fm = extractFrontmatter(content);
|
|
365
|
+
let parsedValue;
|
|
366
|
+
// Attempt JSON parse so callers can pass structured values (arrays, objects);
|
|
367
|
+
// fall back to raw string if the value is not valid JSON
|
|
368
|
+
try { parsedValue = JSON.parse(value); } catch { parsedValue = value; }
|
|
369
|
+
fm[field] = parsedValue;
|
|
370
|
+
const newContent = spliceFrontmatter(content, fm);
|
|
371
|
+
try {
|
|
372
|
+
fs.writeFileSync(fullPath, newContent, 'utf-8');
|
|
373
|
+
} catch (err) {
|
|
374
|
+
output({ error: 'Failed to write file: ' + err.message, path: filePath }, raw); return;
|
|
375
|
+
}
|
|
376
|
+
output({ updated: true, field, value: parsedValue }, raw, 'true');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Merge multiple fields into existing frontmatter from a JSON data string.
|
|
381
|
+
* @param {string} cwd - Working directory path
|
|
382
|
+
* @param {string} filePath - Path to the markdown file
|
|
383
|
+
* @param {string} data - JSON string of key-value pairs to merge
|
|
384
|
+
* @param {boolean} raw - If true, output raw value instead of JSON
|
|
385
|
+
* @returns {void}
|
|
386
|
+
*/
|
|
387
|
+
function cmdFrontmatterMerge(cwd, filePath, data, raw) {
|
|
388
|
+
if (!filePath || !data) { error('file and data required'); }
|
|
389
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
390
|
+
let content;
|
|
391
|
+
try {
|
|
392
|
+
content = fs.readFileSync(fullPath, 'utf-8');
|
|
393
|
+
} catch {
|
|
394
|
+
output({ error: 'File not found', path: filePath }, raw); return;
|
|
395
|
+
}
|
|
396
|
+
const fm = extractFrontmatter(content);
|
|
397
|
+
let mergeData;
|
|
398
|
+
// Parse the JSON data string; abort with error if the caller passed invalid JSON
|
|
399
|
+
try { mergeData = JSON.parse(data); } catch { error('Invalid JSON for --data'); return; }
|
|
400
|
+
Object.assign(fm, mergeData);
|
|
401
|
+
const newContent = spliceFrontmatter(content, fm);
|
|
402
|
+
try {
|
|
403
|
+
fs.writeFileSync(fullPath, newContent, 'utf-8');
|
|
404
|
+
} catch (err) {
|
|
405
|
+
output({ error: 'Failed to write file: ' + err.message, path: filePath }, raw); return;
|
|
406
|
+
}
|
|
407
|
+
output({ merged: true, fields: Object.keys(mergeData) }, raw, 'true');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Validate frontmatter against a named schema (plan, summary, or verification).
|
|
412
|
+
* @param {string} cwd - Working directory path
|
|
413
|
+
* @param {string} filePath - Path to the markdown file
|
|
414
|
+
* @param {string} schemaName - Schema name: "plan", "summary", or "verification"
|
|
415
|
+
* @param {boolean} raw - If true, output raw value instead of JSON
|
|
416
|
+
* @returns {void}
|
|
417
|
+
*/
|
|
418
|
+
function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
|
|
419
|
+
if (!filePath || !schemaName) { error('file and schema required'); }
|
|
420
|
+
const schema = FRONTMATTER_SCHEMAS[schemaName];
|
|
421
|
+
if (!schema) { error(`Unknown schema: ${schemaName}. Available: ${Object.keys(FRONTMATTER_SCHEMAS).join(', ')}`); }
|
|
422
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
423
|
+
const content = safeReadFile(fullPath);
|
|
424
|
+
if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
425
|
+
const fm = extractFrontmatter(content);
|
|
426
|
+
const missing = schema.required.filter(field => fm[field] === undefined);
|
|
427
|
+
const present = schema.required.filter(field => fm[field] !== undefined);
|
|
428
|
+
output({ valid: missing.length === 0, missing, present, schema: schemaName }, raw, missing.length === 0 ? 'valid' : 'invalid');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = {
|
|
432
|
+
extractFrontmatter,
|
|
433
|
+
reconstructFrontmatter,
|
|
434
|
+
spliceFrontmatter,
|
|
435
|
+
parseMustHavesBlock,
|
|
436
|
+
extractPriorityEffort,
|
|
437
|
+
FRONTMATTER_SCHEMAS,
|
|
438
|
+
cmdFrontmatterGet,
|
|
439
|
+
cmdFrontmatterSet,
|
|
440
|
+
cmdFrontmatterMerge,
|
|
441
|
+
cmdFrontmatterValidate,
|
|
442
|
+
};
|