@sienklogic/plan-build-run 2.0.0 → 2.0.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/CHANGELOG.md +56 -56
- package/CLAUDE.md +149 -149
- package/LICENSE +21 -21
- package/README.md +247 -247
- package/dashboard/bin/cli.js +25 -25
- package/dashboard/package.json +34 -34
- package/dashboard/public/css/layout.css +406 -406
- package/dashboard/public/css/status-colors.css +98 -98
- package/dashboard/public/js/htmx-title.js +5 -5
- package/dashboard/public/js/sidebar-toggle.js +20 -20
- package/dashboard/src/app.js +78 -78
- package/dashboard/src/middleware/errorHandler.js +52 -52
- package/dashboard/src/middleware/notFoundHandler.js +9 -9
- package/dashboard/src/repositories/planning.repository.js +128 -128
- package/dashboard/src/routes/events.routes.js +40 -40
- package/dashboard/src/routes/index.routes.js +31 -31
- package/dashboard/src/routes/pages.routes.js +245 -195
- package/dashboard/src/server.js +42 -42
- package/dashboard/src/services/dashboard.service.js +222 -222
- package/dashboard/src/services/phase.service.js +220 -167
- package/dashboard/src/services/project.service.js +57 -57
- package/dashboard/src/services/roadmap.service.js +171 -171
- package/dashboard/src/services/sse.service.js +58 -58
- package/dashboard/src/services/todo.service.js +254 -254
- package/dashboard/src/services/watcher.service.js +48 -48
- package/dashboard/src/views/coming-soon.ejs +11 -11
- package/dashboard/src/views/error.ejs +13 -13
- package/dashboard/src/views/index.ejs +5 -5
- package/dashboard/src/views/layout.ejs +1 -1
- package/dashboard/src/views/partials/dashboard-content.ejs +77 -77
- package/dashboard/src/views/partials/footer.ejs +3 -3
- package/dashboard/src/views/partials/head.ejs +21 -21
- package/dashboard/src/views/partials/header.ejs +12 -12
- package/dashboard/src/views/partials/layout-bottom.ejs +15 -15
- package/dashboard/src/views/partials/layout-top.ejs +8 -8
- package/dashboard/src/views/partials/phase-content.ejs +188 -181
- package/dashboard/src/views/partials/phase-doc-content.ejs +38 -0
- package/dashboard/src/views/partials/phases-content.ejs +117 -117
- package/dashboard/src/views/partials/roadmap-content.ejs +142 -142
- package/dashboard/src/views/partials/sidebar.ejs +38 -38
- package/dashboard/src/views/partials/todo-create-content.ejs +53 -53
- package/dashboard/src/views/partials/todo-detail-content.ejs +38 -38
- package/dashboard/src/views/partials/todos-content.ejs +53 -53
- package/dashboard/src/views/phase-detail.ejs +5 -5
- package/dashboard/src/views/phase-doc.ejs +5 -0
- package/dashboard/src/views/phases.ejs +5 -5
- package/dashboard/src/views/roadmap.ejs +5 -5
- package/dashboard/src/views/todo-create.ejs +5 -5
- package/dashboard/src/views/todo-detail.ejs +5 -5
- package/dashboard/src/views/todos.ejs +5 -5
- package/package.json +57 -57
- package/plugins/pbr/.claude-plugin/plugin.json +13 -13
- package/plugins/pbr/UI-CONSISTENCY-GAPS.md +61 -61
- package/plugins/pbr/agents/codebase-mapper.md +279 -271
- package/plugins/pbr/agents/debugger.md +281 -281
- package/plugins/pbr/agents/executor.md +428 -407
- package/plugins/pbr/agents/general.md +164 -164
- package/plugins/pbr/agents/integration-checker.md +169 -141
- package/plugins/pbr/agents/plan-checker.md +296 -280
- package/plugins/pbr/agents/planner.md +358 -358
- package/plugins/pbr/agents/researcher.md +363 -363
- package/plugins/pbr/agents/synthesizer.md +230 -230
- package/plugins/pbr/agents/verifier.md +489 -454
- package/plugins/pbr/commands/begin.md +5 -5
- package/plugins/pbr/commands/build.md +5 -5
- package/plugins/pbr/commands/config.md +5 -5
- package/plugins/pbr/commands/continue.md +5 -5
- package/plugins/pbr/commands/debug.md +5 -5
- package/plugins/pbr/commands/discuss.md +5 -5
- package/plugins/pbr/commands/explore.md +5 -5
- package/plugins/pbr/commands/health.md +5 -5
- package/plugins/pbr/commands/help.md +5 -5
- package/plugins/pbr/commands/import.md +5 -5
- package/plugins/pbr/commands/milestone.md +5 -5
- package/plugins/pbr/commands/note.md +5 -5
- package/plugins/pbr/commands/pause.md +5 -5
- package/plugins/pbr/commands/plan.md +5 -5
- package/plugins/pbr/commands/quick.md +5 -5
- package/plugins/pbr/commands/resume.md +5 -5
- package/plugins/pbr/commands/review.md +5 -5
- package/plugins/pbr/commands/scan.md +5 -5
- package/plugins/pbr/commands/setup.md +5 -5
- package/plugins/pbr/commands/status.md +5 -5
- package/plugins/pbr/commands/todo.md +5 -5
- package/plugins/pbr/contexts/dev.md +27 -27
- package/plugins/pbr/contexts/research.md +28 -28
- package/plugins/pbr/contexts/review.md +36 -36
- package/plugins/pbr/hooks/hooks.json +183 -183
- package/plugins/pbr/references/agent-anti-patterns.md +24 -24
- package/plugins/pbr/references/agent-interactions.md +134 -134
- package/plugins/pbr/references/agent-teams.md +54 -54
- package/plugins/pbr/references/checkpoints.md +157 -157
- package/plugins/pbr/references/common-bug-patterns.md +13 -13
- package/plugins/pbr/references/config-reference.md +441 -0
- package/plugins/pbr/references/continuation-format.md +212 -212
- package/plugins/pbr/references/deviation-rules.md +112 -112
- package/plugins/pbr/references/git-integration.md +226 -226
- package/plugins/pbr/references/integration-patterns.md +117 -117
- package/plugins/pbr/references/model-profiles.md +99 -99
- package/plugins/pbr/references/model-selection.md +31 -31
- package/plugins/pbr/references/pbr-rules.md +193 -193
- package/plugins/pbr/references/plan-authoring.md +181 -181
- package/plugins/pbr/references/plan-format.md +287 -283
- package/plugins/pbr/references/planning-config.md +213 -213
- package/plugins/pbr/references/questioning.md +214 -214
- package/plugins/pbr/references/reading-verification.md +127 -127
- package/plugins/pbr/references/stub-patterns.md +160 -160
- package/plugins/pbr/references/subagent-coordination.md +119 -119
- package/plugins/pbr/references/ui-formatting.md +461 -399
- package/plugins/pbr/references/verification-patterns.md +198 -198
- package/plugins/pbr/references/wave-execution.md +95 -95
- package/plugins/pbr/scripts/auto-continue.js +80 -80
- package/plugins/pbr/scripts/check-dangerous-commands.js +136 -136
- package/plugins/pbr/scripts/check-doc-sprawl.js +102 -102
- package/plugins/pbr/scripts/check-phase-boundary.js +196 -196
- package/plugins/pbr/scripts/check-plan-format.js +270 -270
- package/plugins/pbr/scripts/check-roadmap-sync.js +322 -252
- package/plugins/pbr/scripts/check-skill-workflow.js +262 -262
- package/plugins/pbr/scripts/check-state-sync.js +476 -476
- package/plugins/pbr/scripts/check-subagent-output.js +144 -144
- package/plugins/pbr/scripts/config-schema.json +251 -251
- package/plugins/pbr/scripts/context-budget-check.js +287 -287
- package/plugins/pbr/scripts/event-handler.js +151 -151
- package/plugins/pbr/scripts/event-logger.js +92 -92
- package/plugins/pbr/scripts/hook-logger.js +80 -76
- package/plugins/pbr/scripts/hooks-schema.json +79 -79
- package/plugins/pbr/scripts/log-subagent.js +164 -152
- package/plugins/pbr/scripts/log-tool-failure.js +88 -88
- package/plugins/pbr/scripts/pbr-tools.js +1378 -1301
- package/plugins/pbr/scripts/post-write-dispatch.js +66 -66
- package/plugins/pbr/scripts/post-write-quality.js +207 -207
- package/plugins/pbr/scripts/pre-bash-dispatch.js +86 -56
- package/plugins/pbr/scripts/pre-write-dispatch.js +97 -62
- package/plugins/pbr/scripts/progress-tracker.js +281 -228
- package/plugins/pbr/scripts/run-hook.js +92 -0
- package/plugins/pbr/scripts/session-cleanup.js +254 -254
- package/plugins/pbr/scripts/status-line.js +288 -285
- package/plugins/pbr/scripts/suggest-compact.js +119 -119
- package/plugins/pbr/scripts/task-completed.js +45 -45
- package/plugins/pbr/scripts/track-context-budget.js +149 -119
- package/plugins/pbr/scripts/validate-commit.js +200 -200
- package/plugins/pbr/scripts/validate-plugin-structure.js +183 -172
- package/plugins/pbr/scripts/validate-task.js +106 -0
- package/plugins/pbr/skills/begin/SKILL.md +594 -545
- package/plugins/pbr/skills/begin/templates/PROJECT.md.tmpl +33 -33
- package/plugins/pbr/skills/begin/templates/REQUIREMENTS.md.tmpl +18 -18
- package/plugins/pbr/skills/begin/templates/STATE.md.tmpl +49 -49
- package/plugins/pbr/skills/begin/templates/config.json.tmpl +64 -63
- package/plugins/pbr/skills/begin/templates/researcher-prompt.md.tmpl +19 -19
- package/plugins/pbr/skills/begin/templates/roadmap-prompt.md.tmpl +30 -30
- package/plugins/pbr/skills/begin/templates/synthesis-prompt.md.tmpl +16 -16
- package/plugins/pbr/skills/build/SKILL.md +943 -962
- package/plugins/pbr/skills/config/SKILL.md +256 -241
- package/plugins/pbr/skills/continue/SKILL.md +164 -127
- package/plugins/pbr/skills/debug/SKILL.md +515 -489
- package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +16 -16
- package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +27 -27
- package/plugins/pbr/skills/discuss/SKILL.md +347 -338
- package/plugins/pbr/skills/discuss/templates/CONTEXT.md.tmpl +61 -61
- package/plugins/pbr/skills/discuss/templates/decision-categories.md +9 -9
- package/plugins/pbr/skills/explore/SKILL.md +378 -362
- package/plugins/pbr/skills/health/SKILL.md +221 -186
- package/plugins/pbr/skills/health/templates/check-pattern.md.tmpl +30 -30
- package/plugins/pbr/skills/health/templates/output-format.md.tmpl +63 -63
- package/plugins/pbr/skills/help/SKILL.md +155 -140
- package/plugins/pbr/skills/import/SKILL.md +504 -490
- package/plugins/pbr/skills/milestone/SKILL.md +704 -673
- package/plugins/pbr/skills/milestone/templates/audit-report.md.tmpl +48 -48
- package/plugins/pbr/skills/milestone/templates/stats-file.md.tmpl +30 -30
- package/plugins/pbr/skills/note/SKILL.md +231 -212
- package/plugins/pbr/skills/pause/SKILL.md +249 -235
- package/plugins/pbr/skills/pause/templates/continue-here.md.tmpl +71 -71
- package/plugins/pbr/skills/plan/SKILL.md +685 -628
- package/plugins/pbr/skills/plan/decimal-phase-calc.md +98 -98
- package/plugins/pbr/skills/plan/templates/checker-prompt.md.tmpl +21 -21
- package/plugins/pbr/skills/plan/templates/gap-closure-prompt.md.tmpl +32 -32
- package/plugins/pbr/skills/plan/templates/planner-prompt.md.tmpl +38 -38
- package/plugins/pbr/skills/plan/templates/researcher-prompt.md.tmpl +19 -19
- package/plugins/pbr/skills/plan/templates/revision-prompt.md.tmpl +23 -23
- package/plugins/pbr/skills/quick/SKILL.md +354 -335
- package/plugins/pbr/skills/resume/SKILL.md +402 -388
- package/plugins/pbr/skills/review/SKILL.md +686 -652
- package/plugins/pbr/skills/review/templates/debugger-prompt.md.tmpl +60 -60
- package/plugins/pbr/skills/review/templates/gap-planner-prompt.md.tmpl +40 -40
- package/plugins/pbr/skills/review/templates/verifier-prompt.md.tmpl +115 -115
- package/plugins/pbr/skills/scan/SKILL.md +304 -269
- package/plugins/pbr/skills/scan/templates/mapper-prompt.md.tmpl +201 -201
- package/plugins/pbr/skills/setup/SKILL.md +253 -227
- package/plugins/pbr/skills/shared/commit-planning-docs.md +35 -35
- package/plugins/pbr/skills/shared/config-loading.md +102 -102
- package/plugins/pbr/skills/shared/context-budget.md +40 -40
- package/plugins/pbr/skills/shared/context-loader-task.md +86 -86
- package/plugins/pbr/skills/shared/digest-select.md +79 -79
- package/plugins/pbr/skills/shared/domain-probes.md +125 -125
- package/plugins/pbr/skills/shared/error-reporting.md +79 -79
- package/plugins/pbr/skills/shared/gate-prompts.md +388 -388
- package/plugins/pbr/skills/shared/phase-argument-parsing.md +45 -45
- package/plugins/pbr/skills/shared/progress-display.md +53 -53
- package/plugins/pbr/skills/shared/revision-loop.md +81 -81
- package/plugins/pbr/skills/shared/state-loading.md +62 -62
- package/plugins/pbr/skills/shared/state-update.md +161 -161
- package/plugins/pbr/skills/shared/universal-anti-patterns.md +33 -33
- package/plugins/pbr/skills/status/SKILL.md +367 -353
- package/plugins/pbr/skills/todo/SKILL.md +198 -181
- package/plugins/pbr/templates/CONTEXT.md.tmpl +52 -52
- package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +151 -151
- package/plugins/pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -97
- package/plugins/pbr/templates/ROADMAP.md.tmpl +40 -40
- package/plugins/pbr/templates/SUMMARY.md.tmpl +81 -81
- package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +116 -116
- package/plugins/pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -98
- package/plugins/pbr/templates/codebase/CONCERNS.md.tmpl +93 -93
- package/plugins/pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -104
- package/plugins/pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -78
- package/plugins/pbr/templates/codebase/STACK.md.tmpl +78 -78
- package/plugins/pbr/templates/codebase/STRUCTURE.md.tmpl +80 -80
- package/plugins/pbr/templates/codebase/TESTING.md.tmpl +107 -107
- package/plugins/pbr/templates/continue-here.md.tmpl +73 -73
- package/plugins/pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -37
- package/plugins/pbr/templates/research/ARCHITECTURE.md.tmpl +124 -124
- package/plugins/pbr/templates/research/STACK.md.tmpl +71 -71
- package/plugins/pbr/templates/research/SUMMARY.md.tmpl +112 -112
- package/plugins/pbr/templates/research-outputs/phase-research.md.tmpl +81 -81
- package/plugins/pbr/templates/research-outputs/project-research.md.tmpl +99 -99
- package/plugins/pbr/templates/research-outputs/synthesis.md.tmpl +36 -36
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve, relative, normalize } from 'node:path';
|
|
3
|
-
import matter from 'gray-matter';
|
|
4
|
-
import { marked } from 'marked';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Strip UTF-8 BOM (Byte Order Mark) if present.
|
|
8
|
-
* Windows editors (Notepad, older VS Code) may prepend BOM to UTF-8 files.
|
|
9
|
-
* gray-matter will fail to detect frontmatter delimiters if BOM is present.
|
|
10
|
-
* @param {string} content - Raw file content
|
|
11
|
-
* @returns {string} Content without BOM
|
|
12
|
-
*/
|
|
13
|
-
function stripBOM(content) {
|
|
14
|
-
return content.replace(/^\uFEFF/, '');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Validate that a resolved path stays within the base directory.
|
|
19
|
-
* Prevents path traversal attacks (e.g., ../../etc/passwd).
|
|
20
|
-
*
|
|
21
|
-
* @param {string} basePath - Absolute base directory path
|
|
22
|
-
* @param {string} userPath - User-provided path (may be relative)
|
|
23
|
-
* @returns {string} Validated absolute path
|
|
24
|
-
* @throws {Error} With status 403 if path escapes base directory
|
|
25
|
-
*/
|
|
26
|
-
export function validatePath(basePath, userPath) {
|
|
27
|
-
const resolvedBase = normalize(resolve(basePath));
|
|
28
|
-
const resolvedUser = normalize(resolve(basePath, userPath));
|
|
29
|
-
|
|
30
|
-
// Compute the relative path from base to target
|
|
31
|
-
const rel = relative(resolvedBase, resolvedUser);
|
|
32
|
-
|
|
33
|
-
// If relative path starts with '..' it escaped the base directory.
|
|
34
|
-
// If rel resolves to an absolute path, it was an absolute path injection.
|
|
35
|
-
if (rel.startsWith('..') || resolve(rel) === rel) {
|
|
36
|
-
const err = new Error('Path traversal attempt detected');
|
|
37
|
-
err.status = 403;
|
|
38
|
-
err.code = 'PATH_TRAVERSAL';
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return resolvedUser;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Read and parse a single markdown file with YAML frontmatter.
|
|
47
|
-
*
|
|
48
|
-
* @param {string} filePath - Absolute path to the markdown file
|
|
49
|
-
* @returns {Promise<{frontmatter: object, html: string, rawContent: string}>}
|
|
50
|
-
* @throws {Error} ENOENT if file does not exist, parse error if YAML is malformed
|
|
51
|
-
*/
|
|
52
|
-
export async function readMarkdownFile(filePath) {
|
|
53
|
-
const fileContent = await readFile(filePath, 'utf-8');
|
|
54
|
-
const cleanContent = stripBOM(fileContent);
|
|
55
|
-
|
|
56
|
-
let data, content;
|
|
57
|
-
try {
|
|
58
|
-
({ data, content } = matter(cleanContent, {
|
|
59
|
-
engines: {
|
|
60
|
-
javascript: false
|
|
61
|
-
}
|
|
62
|
-
}));
|
|
63
|
-
} catch (error) {
|
|
64
|
-
// gray-matter throws YAMLException for malformed frontmatter.
|
|
65
|
-
// Wrap it in a user-friendly error with status 400.
|
|
66
|
-
if (error.name === 'YAMLException' || (error.constructor && error.constructor.name === 'YAMLException')) {
|
|
67
|
-
const wrapped = new Error(`Invalid YAML frontmatter in ${filePath}: ${error.message}`);
|
|
68
|
-
wrapped.status = 400;
|
|
69
|
-
wrapped.cause = error;
|
|
70
|
-
throw wrapped;
|
|
71
|
-
}
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const html = marked.parse(content);
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
frontmatter: data,
|
|
79
|
-
html,
|
|
80
|
-
rawContent: content
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Read and parse multiple markdown files in parallel (fail-fast).
|
|
86
|
-
* If any file fails, the entire operation rejects immediately.
|
|
87
|
-
*
|
|
88
|
-
* @param {string[]} filePaths - Array of absolute file paths
|
|
89
|
-
* @returns {Promise<Array<{frontmatter: object, html: string, rawContent: string}>>}
|
|
90
|
-
*/
|
|
91
|
-
export async function readMarkdownFiles(filePaths) {
|
|
92
|
-
return Promise.all(
|
|
93
|
-
filePaths.map(filePath => readMarkdownFile(filePath))
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Read and parse multiple markdown files with partial failure tolerance.
|
|
99
|
-
* All reads complete even if some fail. Returns settled results.
|
|
100
|
-
*
|
|
101
|
-
* @param {string[]} filePaths - Array of absolute file paths
|
|
102
|
-
* @returns {Promise<Array<{status: 'fulfilled', value: object} | {status: 'rejected', reason: Error}>>}
|
|
103
|
-
*/
|
|
104
|
-
export async function readMarkdownFilesSettled(filePaths) {
|
|
105
|
-
return Promise.allSettled(
|
|
106
|
-
filePaths.map(filePath => readMarkdownFile(filePath))
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* List all markdown files under the .planning/ directory recursively.
|
|
112
|
-
* Uses native fs.readdir with recursive option (Node.js 18.17+).
|
|
113
|
-
*
|
|
114
|
-
* @param {string} projectDir - Absolute path to the project root
|
|
115
|
-
* @returns {Promise<string[]>} Array of absolute file paths to .md files
|
|
116
|
-
* @throws {Error} ENOENT if .planning/ directory does not exist
|
|
117
|
-
*/
|
|
118
|
-
export async function listPlanningFiles(projectDir) {
|
|
119
|
-
const planningDir = join(projectDir, '.planning');
|
|
120
|
-
const entries = await readdir(planningDir, {
|
|
121
|
-
recursive: true,
|
|
122
|
-
withFileTypes: true
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return entries
|
|
126
|
-
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
|
|
127
|
-
.map(entry => join(entry.parentPath || entry.path, entry.name));
|
|
128
|
-
}
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve, relative, normalize } from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { marked } from 'marked';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Strip UTF-8 BOM (Byte Order Mark) if present.
|
|
8
|
+
* Windows editors (Notepad, older VS Code) may prepend BOM to UTF-8 files.
|
|
9
|
+
* gray-matter will fail to detect frontmatter delimiters if BOM is present.
|
|
10
|
+
* @param {string} content - Raw file content
|
|
11
|
+
* @returns {string} Content without BOM
|
|
12
|
+
*/
|
|
13
|
+
function stripBOM(content) {
|
|
14
|
+
return content.replace(/^\uFEFF/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a resolved path stays within the base directory.
|
|
19
|
+
* Prevents path traversal attacks (e.g., ../../etc/passwd).
|
|
20
|
+
*
|
|
21
|
+
* @param {string} basePath - Absolute base directory path
|
|
22
|
+
* @param {string} userPath - User-provided path (may be relative)
|
|
23
|
+
* @returns {string} Validated absolute path
|
|
24
|
+
* @throws {Error} With status 403 if path escapes base directory
|
|
25
|
+
*/
|
|
26
|
+
export function validatePath(basePath, userPath) {
|
|
27
|
+
const resolvedBase = normalize(resolve(basePath));
|
|
28
|
+
const resolvedUser = normalize(resolve(basePath, userPath));
|
|
29
|
+
|
|
30
|
+
// Compute the relative path from base to target
|
|
31
|
+
const rel = relative(resolvedBase, resolvedUser);
|
|
32
|
+
|
|
33
|
+
// If relative path starts with '..' it escaped the base directory.
|
|
34
|
+
// If rel resolves to an absolute path, it was an absolute path injection.
|
|
35
|
+
if (rel.startsWith('..') || resolve(rel) === rel) {
|
|
36
|
+
const err = new Error('Path traversal attempt detected');
|
|
37
|
+
err.status = 403;
|
|
38
|
+
err.code = 'PATH_TRAVERSAL';
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return resolvedUser;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read and parse a single markdown file with YAML frontmatter.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} filePath - Absolute path to the markdown file
|
|
49
|
+
* @returns {Promise<{frontmatter: object, html: string, rawContent: string}>}
|
|
50
|
+
* @throws {Error} ENOENT if file does not exist, parse error if YAML is malformed
|
|
51
|
+
*/
|
|
52
|
+
export async function readMarkdownFile(filePath) {
|
|
53
|
+
const fileContent = await readFile(filePath, 'utf-8');
|
|
54
|
+
const cleanContent = stripBOM(fileContent);
|
|
55
|
+
|
|
56
|
+
let data, content;
|
|
57
|
+
try {
|
|
58
|
+
({ data, content } = matter(cleanContent, {
|
|
59
|
+
engines: {
|
|
60
|
+
javascript: false
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// gray-matter throws YAMLException for malformed frontmatter.
|
|
65
|
+
// Wrap it in a user-friendly error with status 400.
|
|
66
|
+
if (error.name === 'YAMLException' || (error.constructor && error.constructor.name === 'YAMLException')) {
|
|
67
|
+
const wrapped = new Error(`Invalid YAML frontmatter in ${filePath}: ${error.message}`);
|
|
68
|
+
wrapped.status = 400;
|
|
69
|
+
wrapped.cause = error;
|
|
70
|
+
throw wrapped;
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const html = marked.parse(content);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
frontmatter: data,
|
|
79
|
+
html,
|
|
80
|
+
rawContent: content
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read and parse multiple markdown files in parallel (fail-fast).
|
|
86
|
+
* If any file fails, the entire operation rejects immediately.
|
|
87
|
+
*
|
|
88
|
+
* @param {string[]} filePaths - Array of absolute file paths
|
|
89
|
+
* @returns {Promise<Array<{frontmatter: object, html: string, rawContent: string}>>}
|
|
90
|
+
*/
|
|
91
|
+
export async function readMarkdownFiles(filePaths) {
|
|
92
|
+
return Promise.all(
|
|
93
|
+
filePaths.map(filePath => readMarkdownFile(filePath))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read and parse multiple markdown files with partial failure tolerance.
|
|
99
|
+
* All reads complete even if some fail. Returns settled results.
|
|
100
|
+
*
|
|
101
|
+
* @param {string[]} filePaths - Array of absolute file paths
|
|
102
|
+
* @returns {Promise<Array<{status: 'fulfilled', value: object} | {status: 'rejected', reason: Error}>>}
|
|
103
|
+
*/
|
|
104
|
+
export async function readMarkdownFilesSettled(filePaths) {
|
|
105
|
+
return Promise.allSettled(
|
|
106
|
+
filePaths.map(filePath => readMarkdownFile(filePath))
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* List all markdown files under the .planning/ directory recursively.
|
|
112
|
+
* Uses native fs.readdir with recursive option (Node.js 18.17+).
|
|
113
|
+
*
|
|
114
|
+
* @param {string} projectDir - Absolute path to the project root
|
|
115
|
+
* @returns {Promise<string[]>} Array of absolute file paths to .md files
|
|
116
|
+
* @throws {Error} ENOENT if .planning/ directory does not exist
|
|
117
|
+
*/
|
|
118
|
+
export async function listPlanningFiles(projectDir) {
|
|
119
|
+
const planningDir = join(projectDir, '.planning');
|
|
120
|
+
const entries = await readdir(planningDir, {
|
|
121
|
+
recursive: true,
|
|
122
|
+
withFileTypes: true
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return entries
|
|
126
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
|
|
127
|
+
.map(entry => join(entry.parentPath || entry.path, entry.name));
|
|
128
|
+
}
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import { Router } from 'express';
|
|
2
|
-
import { addClient, removeClient } from '../services/sse.service.js';
|
|
3
|
-
|
|
4
|
-
const router = Router();
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* GET /stream - Server-Sent Events endpoint.
|
|
8
|
-
* Establishes a long-lived SSE connection. Events are pushed by the SSE service
|
|
9
|
-
* when the file watcher detects changes. Heartbeat comments every 30s keep the
|
|
10
|
-
* connection alive.
|
|
11
|
-
*/
|
|
12
|
-
router.get('/stream', (req, res) => {
|
|
13
|
-
// Set SSE headers
|
|
14
|
-
res.writeHead(200, {
|
|
15
|
-
'Content-Type': 'text/event-stream',
|
|
16
|
-
'Cache-Control': 'no-cache',
|
|
17
|
-
'Connection': 'keep-alive',
|
|
18
|
-
'X-Accel-Buffering': 'no'
|
|
19
|
-
});
|
|
20
|
-
res.flushHeaders();
|
|
21
|
-
|
|
22
|
-
// Send initial connection confirmation
|
|
23
|
-
res.write(': connected\n\n');
|
|
24
|
-
|
|
25
|
-
// Register this client for broadcasts
|
|
26
|
-
addClient(res);
|
|
27
|
-
|
|
28
|
-
// Heartbeat every 30 seconds to keep connection alive
|
|
29
|
-
const heartbeat = setInterval(() => {
|
|
30
|
-
res.write(': heartbeat\n\n');
|
|
31
|
-
}, 30000);
|
|
32
|
-
|
|
33
|
-
// Clean up on client disconnect
|
|
34
|
-
req.on('close', () => {
|
|
35
|
-
clearInterval(heartbeat);
|
|
36
|
-
removeClient(res);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
export default router;
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { addClient, removeClient } from '../services/sse.service.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /stream - Server-Sent Events endpoint.
|
|
8
|
+
* Establishes a long-lived SSE connection. Events are pushed by the SSE service
|
|
9
|
+
* when the file watcher detects changes. Heartbeat comments every 30s keep the
|
|
10
|
+
* connection alive.
|
|
11
|
+
*/
|
|
12
|
+
router.get('/stream', (req, res) => {
|
|
13
|
+
// Set SSE headers
|
|
14
|
+
res.writeHead(200, {
|
|
15
|
+
'Content-Type': 'text/event-stream',
|
|
16
|
+
'Cache-Control': 'no-cache',
|
|
17
|
+
'Connection': 'keep-alive',
|
|
18
|
+
'X-Accel-Buffering': 'no'
|
|
19
|
+
});
|
|
20
|
+
res.flushHeaders();
|
|
21
|
+
|
|
22
|
+
// Send initial connection confirmation
|
|
23
|
+
res.write(': connected\n\n');
|
|
24
|
+
|
|
25
|
+
// Register this client for broadcasts
|
|
26
|
+
addClient(res);
|
|
27
|
+
|
|
28
|
+
// Heartbeat every 30 seconds to keep connection alive
|
|
29
|
+
const heartbeat = setInterval(() => {
|
|
30
|
+
res.write(': heartbeat\n\n');
|
|
31
|
+
}, 30000);
|
|
32
|
+
|
|
33
|
+
// Clean up on client disconnect
|
|
34
|
+
req.on('close', () => {
|
|
35
|
+
clearInterval(heartbeat);
|
|
36
|
+
removeClient(res);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export default router;
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { Router } from 'express';
|
|
2
|
-
import { getHomepage } from '../services/project.service.js';
|
|
3
|
-
import { getDashboardData } from '../services/dashboard.service.js';
|
|
4
|
-
|
|
5
|
-
const router = Router();
|
|
6
|
-
|
|
7
|
-
router.get('/', async (req, res) => {
|
|
8
|
-
const projectDir = req.app.locals.projectDir;
|
|
9
|
-
|
|
10
|
-
const [homepageData, dashboardData] = await Promise.all([
|
|
11
|
-
getHomepage(projectDir),
|
|
12
|
-
getDashboardData(projectDir)
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
const templateData = {
|
|
16
|
-
...homepageData,
|
|
17
|
-
...dashboardData,
|
|
18
|
-
activePage: 'dashboard',
|
|
19
|
-
currentPath: '/'
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
res.setHeader('Vary', 'HX-Request');
|
|
23
|
-
|
|
24
|
-
if (req.get('HX-Request') === 'true') {
|
|
25
|
-
res.render('partials/dashboard-content', templateData);
|
|
26
|
-
} else {
|
|
27
|
-
res.render('index', templateData);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
export default router;
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { getHomepage } from '../services/project.service.js';
|
|
3
|
+
import { getDashboardData } from '../services/dashboard.service.js';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
router.get('/', async (req, res) => {
|
|
8
|
+
const projectDir = req.app.locals.projectDir;
|
|
9
|
+
|
|
10
|
+
const [homepageData, dashboardData] = await Promise.all([
|
|
11
|
+
getHomepage(projectDir),
|
|
12
|
+
getDashboardData(projectDir)
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const templateData = {
|
|
16
|
+
...homepageData,
|
|
17
|
+
...dashboardData,
|
|
18
|
+
activePage: 'dashboard',
|
|
19
|
+
currentPath: '/'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
res.setHeader('Vary', 'HX-Request');
|
|
23
|
+
|
|
24
|
+
if (req.get('HX-Request') === 'true') {
|
|
25
|
+
res.render('partials/dashboard-content', templateData);
|
|
26
|
+
} else {
|
|
27
|
+
res.render('index', templateData);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export default router;
|