claude-code-session-manager 0.76.0 → 0.77.0
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/assets/{AgentLibrary-CBx9l4zN.js → AgentLibrary-B2ie8bbw.js} +2 -2
- package/dist/assets/{DataModel-Bf0EIE_t.js → DataModel-BIJPYw32.js} +1 -1
- package/dist/assets/{History-CpdtWhC8.js → History-CeY6dk9S.js} +2 -2
- package/dist/assets/{Hooks-DyUbMDmg.js → Hooks-BFH2ocKg.js} +2 -2
- package/dist/assets/{HostBilko-By-wIpry.js → HostBilko-36gj9wLz.js} +1 -1
- package/dist/assets/{Library-CQmo4QVC.js → Library-C-hBct39.js} +1 -1
- package/dist/assets/{ListDetail-BQMd6NOm.js → ListDetail-CNq64VWV.js} +1 -1
- package/dist/assets/{MarkdownEditor-DEp43FXX.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
- package/dist/assets/{McpServers-CLarzwqA.js → McpServers-DpGN0oyz.js} +1 -1
- package/dist/assets/{Memory-B0sCdIy1.js → Memory-D59hUjC4.js} +6 -6
- package/dist/assets/{Panel-BhWPVOCD.js → Panel-DCgbaoci.js} +1 -1
- package/dist/assets/{Permissions-Ddlq8T_O.js → Permissions-DAmQ0DYV.js} +2 -2
- package/dist/assets/{Plugins-D2oA_2Jl.js → Plugins-Dyfgn6Is.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DgAgavUM.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
- package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
- package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
- package/dist/assets/{ScopeSwitcher-C_zWEtIl.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
- package/dist/assets/{Settings-2Vx3X5SI.js → Settings-BL4vf5aX.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-BDEUjlTQ.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
- package/dist/assets/{Skills-Cmrz_LeN.js → Skills-BV08gDUH.js} +2 -2
- package/dist/assets/{SystemPrompt-DVA1eYDP.js → SystemPrompt-CLftSsDw.js} +1 -1
- package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
- package/dist/assets/{TiptapBody-DmPc3amD.js → TiptapBody-jCpuB6E5.js} +1 -1
- package/dist/assets/{Toggle-zfd5LJkK.js → Toggle-D2paA1xf.js} +1 -1
- package/dist/assets/{index-B_4PNh9T.js → index-BDRSqBl3.js} +175 -175
- package/dist/assets/{index-DIjnPkRN.css → index-CYhdtisq.css} +1 -1
- package/dist/assets/{settingsSchema-B9es6fdA.js → settingsSchema-6IOLjZZN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -2
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
- package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
- package/scripts/render-project-pages.cjs +70 -0
- package/scripts/scheduler-mcp-server.cjs +115 -1
- package/scripts/validate-project-pages-summary.cjs +62 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
- package/src/main/__tests__/prdAgentType.test.cjs +103 -0
- package/src/main/__tests__/prdCreate.test.cjs +138 -0
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
- package/src/main/__tests__/projectPages.test.cjs +73 -1
- package/src/main/__tests__/rcaReport.test.cjs +54 -0
- package/src/main/__tests__/runVerify.test.cjs +94 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +43 -0
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +147 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
- package/src/main/config.cjs +4 -1
- package/src/main/index.cjs +8 -1
- package/src/main/ipcSchemas.cjs +51 -0
- package/src/main/lib/__tests__/childWithLog.test.cjs +78 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +152 -2
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +4 -2
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +277 -4
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
- package/src/main/lib/__tests__/jobWorktree.test.cjs +5 -3
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
- package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +1 -0
- package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
- package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +22 -1
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
- package/src/main/lib/agentModelResolve.cjs +58 -0
- package/src/main/lib/childWithLog.cjs +40 -5
- package/src/main/lib/claudeBin.cjs +54 -1
- package/src/main/lib/definitionOfDone.cjs +3 -2
- package/src/main/lib/delegationReadiness.cjs +115 -9
- package/src/main/lib/epicWorktreeMint.cjs +5 -2
- package/src/main/lib/fixChainDepth.cjs +45 -0
- package/src/main/lib/gitWorktree.cjs +464 -19
- package/src/main/lib/jobWorktree.cjs +1 -0
- package/src/main/lib/landedSinceRun.cjs +55 -0
- package/src/main/lib/launchFailure.cjs +357 -0
- package/src/main/lib/mcpToolCatalog.cjs +87 -2
- package/src/main/lib/opsOwnership.cjs +12 -0
- package/src/main/lib/prdAgentType.cjs +84 -0
- package/src/main/lib/prdCreate.cjs +57 -1
- package/src/main/lib/prdDeclaredPaths.cjs +70 -0
- package/src/main/lib/prdFrontmatter.cjs +17 -3
- package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
- package/src/main/lib/projectPageSummarySchema.cjs +181 -0
- package/src/main/lib/queueHealth.cjs +38 -0
- package/src/main/lib/queueStore.cjs +9 -2
- package/src/main/lib/quietMachineLease.cjs +48 -0
- package/src/main/lib/rcaReport.cjs +53 -3
- package/src/main/lib/reaperHelpers.cjs +18 -1
- package/src/main/lib/scheduleJobSchema.cjs +31 -0
- package/src/main/lib/scheduleJobTransitions.cjs +6 -2
- package/src/main/lib/schedulerBatch.cjs +133 -29
- package/src/main/lib/schedulerConfig.cjs +19 -0
- package/src/main/projectPages.cjs +160 -2
- package/src/main/runVerify.cjs +50 -9
- package/src/main/scheduler/prdParser.cjs +18 -1
- package/src/main/scheduler.cjs +1371 -97
- package/src/main/seedAgentPersonas.cjs +62 -21
- package/src/main/templates/project-pages-catalog.json +741 -0
- package/src/main/templates/project-pages-pipeline.md +417 -0
- package/src/preload/api.d.ts +118 -2
- package/src/preload/index.cjs +7 -0
- package/src/seed/agents/project-home-builder.md +59 -0
- package/dist/assets/SaveBar-Qvc4Ek-H.js +0 -1
- package/dist/assets/Scheduler-BmYJvNzK.js +0 -14
- package/dist/assets/TagLibrary-DYJGAKZu.js +0 -1
|
@@ -26,6 +26,18 @@ const { readBody, sendJson } = require('./localAdminHttp.cjs');
|
|
|
26
26
|
const { readActiveIndex } = require('./epicMint.cjs');
|
|
27
27
|
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
28
28
|
const { resolveProjectContext } = require('./projectRootResolve.cjs');
|
|
29
|
+
const { fixChainDepthOf, baseSlugOf } = require('./fixChainDepth.cjs');
|
|
30
|
+
const { DEFAULT_PRD_AGENT_TYPE, assertAgentTypeWritable } = require('./prdAgentType.cjs');
|
|
31
|
+
|
|
32
|
+
// Fix-chain depth cap (PRD 1113): a fix-of-a-fix (depth >= 2) is refused at
|
|
33
|
+
// this shared write path rather than caught later — scheduler.cjs's
|
|
34
|
+
// MAX_INVESTIGATION_DEPTH=1 caps the auto-fix loop's OWN recursion the same
|
|
35
|
+
// way, so the two limits must stay numerically consistent (depth 1 allowed,
|
|
36
|
+
// depth 2 refused). This guard exists because that auto-fix cap only ever
|
|
37
|
+
// sees PRDs `spawnInvestigation` itself authors; a human or planner session
|
|
38
|
+
// authoring a `*-fix-fix-*` slug through this API bypasses it entirely —
|
|
39
|
+
// which is exactly what happened on starry-night-ships (see PRD 1113's Goal).
|
|
40
|
+
const FIX_CHAIN_DEPTH_CAP = 1;
|
|
29
41
|
|
|
30
42
|
const STANDARDS_PATH = path.join(
|
|
31
43
|
__dirname, '..', '..', '..',
|
|
@@ -51,7 +63,7 @@ function deriveSlugFromTitle(title) {
|
|
|
51
63
|
function buildPrdBody(input) {
|
|
52
64
|
const {
|
|
53
65
|
title, cwd, estimateMinutes, goal, acceptanceCriteria,
|
|
54
|
-
implementationNotes, outOfScope, sourcePromptId, sourceTabId, tag, dependsOn,
|
|
66
|
+
implementationNotes, outOfScope, sourcePromptId, sourceTabId, tag, agentType, dependsOn, quietMachine,
|
|
55
67
|
} = input;
|
|
56
68
|
|
|
57
69
|
// No `parallelGroup` frontmatter key by convention (SKILL.md) — the NN-
|
|
@@ -76,8 +88,21 @@ function buildPrdBody(input) {
|
|
|
76
88
|
// Optional, additive: the user-selected Feature/Bug tag (PRD 774) carried
|
|
77
89
|
// through from the originating PromptTicket — deterministic, never LLM-classified.
|
|
78
90
|
if (tag) fmLines.push(`tag: ${tag}`);
|
|
91
|
+
// WHO executes this PRD — distinct from `tag` (the WORK TYPE, above).
|
|
92
|
+
// Always emitted (unlike tag): a PRD with no agentType is ambiguous about
|
|
93
|
+
// who runs it, so createPrd() fills in DEFAULT_PRD_AGENT_TYPE ('dev-lead')
|
|
94
|
+
// before calling this function when the caller omitted one. Validated at
|
|
95
|
+
// write time by createPrd() via prdAgentType.cjs's assertAgentTypeWritable
|
|
96
|
+
// — this function only serializes, it never validates.
|
|
97
|
+
fmLines.push(`agentType: ${agentType || DEFAULT_PRD_AGENT_TYPE}`);
|
|
79
98
|
// Explicit ordering (PRD 832): replaces the retired shared-NN convention.
|
|
80
99
|
if (dependsOn && dependsOn.length) fmLines.push(`dependsOn: [${dependsOn.join(', ')}]`);
|
|
100
|
+
// Opt-in exclusive-lease flag (PRD 1107): serializes this job against
|
|
101
|
+
// every other job machine-wide for its run, for a PRD whose acceptance
|
|
102
|
+
// criteria are wall-clock/timing measurements that CPU contention from
|
|
103
|
+
// sibling jobs would otherwise invalidate. Only emitted when true — see
|
|
104
|
+
// prdFrontmatter.cjs's applyKey for the matching "only true opts in" parse.
|
|
105
|
+
if (quietMachine === true) fmLines.push('quietMachine: true');
|
|
81
106
|
fmLines.push('---', '');
|
|
82
107
|
|
|
83
108
|
const acLines = acceptanceCriteria.map((line) => `- [ ] ${line}`).join('\n');
|
|
@@ -217,6 +242,23 @@ async function createPrd(input, remote) {
|
|
|
217
242
|
const nn = await remote.allocateParallelGroup(input.cwd);
|
|
218
243
|
const filenameSlug = `${nn}-${slug}`;
|
|
219
244
|
|
|
245
|
+
// Fix-chain depth guard (PRD 1113): refuse a fix-of-a-fix before it's ever
|
|
246
|
+
// written. depth 0/1 pass through unchanged (an ordinary PRD, or a first
|
|
247
|
+
// fix-plan, is never blocked); depth >= 2 means the base job has already
|
|
248
|
+
// failed to close via at least one prior fix attempt for reasons a repeat
|
|
249
|
+
// attempt won't resolve.
|
|
250
|
+
const chainDepth = fixChainDepthOf(filenameSlug);
|
|
251
|
+
if (chainDepth > FIX_CHAIN_DEPTH_CAP) {
|
|
252
|
+
const base = baseSlugOf(filenameSlug);
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
status: 409,
|
|
256
|
+
error: `Fix-chain depth cap exceeded: "${base}" has already failed to close ${chainDepth} time(s) for ` +
|
|
257
|
+
`infrastructure reasons (refusing to write "${filenameSlug}.md" at depth ${chainDepth}). Authoring a ` +
|
|
258
|
+
'deeper fix PRD is not the correct response — stop and surface this to a human instead of retrying again.',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
220
262
|
// An explicit `parallelGroup` bypasses allocateParallelGroup()'s
|
|
221
263
|
// collision-proof reservation, so re-check for an existing file at
|
|
222
264
|
// this exact destination before writing — remote.writePrd itself has
|
|
@@ -228,6 +270,20 @@ async function createPrd(input, remote) {
|
|
|
228
270
|
return { ok: false, status: 409, error: `PRD already exists: ${filenameSlug}.md` };
|
|
229
271
|
}
|
|
230
272
|
|
|
273
|
+
// Write-time FK check (throw on write, report on read — see
|
|
274
|
+
// prdAgentType.cjs's header): a caller-supplied agentType that doesn't
|
|
275
|
+
// resolve to a readable persona file is rejected before anything is
|
|
276
|
+
// written, naming the available personas. A no-op when agentType is
|
|
277
|
+
// omitted — buildPrdBody fills in DEFAULT_PRD_AGENT_TYPE for that case,
|
|
278
|
+
// and the default persona is trusted without a lookup.
|
|
279
|
+
if (input.agentType) {
|
|
280
|
+
try {
|
|
281
|
+
await assertAgentTypeWritable(input.cwd, input.agentType);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return { ok: false, status: 400, error: e?.message ?? 'invalid agentType' };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
231
287
|
const body = buildPrdBody(input);
|
|
232
288
|
const writeResult = await remote.writePrd(filenameSlug, body, input.cwd);
|
|
233
289
|
if (!writeResult?.ok) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* prdDeclaredPaths.cjs — derive the file paths a PRD declares it touches, for
|
|
5
|
+
* the reverify self-heal pass's widened evidence window (PRD 1102).
|
|
6
|
+
*
|
|
7
|
+
* Reuses definitionOfDone.cjs's existing `extractSection` (already the PRD
|
|
8
|
+
* markdown section parser used by flagRiskySurfaces) rather than
|
|
9
|
+
* re-implementing one — see standards.md's API-reuse rule.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
14
|
+
const { extractSection } = require('./definitionOfDone.cjs');
|
|
15
|
+
|
|
16
|
+
// Backtick-quoted, extension-bearing relative paths, e.g. `src/main/scheduler.cjs`
|
|
17
|
+
// or `src/main/scheduler.cjs:490` (trailing :line dropped). Requires at least
|
|
18
|
+
// one `/` so bare identifiers like `npm` or `RESCANNABLE_VERDICTS` never match.
|
|
19
|
+
const PATH_RE = /`([a-zA-Z0-9_][\w.-]*(?:\/[\w.-]+)+\.[a-zA-Z0-9]+)(?::\d+)?`/g;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract declared file paths from a PRD body (frontmatter already
|
|
23
|
+
* stripped). Scans the `# Implementation notes` and `# Acceptance criteria`
|
|
24
|
+
* sections only — the same sections flagRiskySurfaces and the PRD-authoring
|
|
25
|
+
* convention already treat as where files-touched are named. Returns `[]`
|
|
26
|
+
* (never fabricates a path) when neither section names one.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} prdBody
|
|
29
|
+
* @returns {string[]} deduped, in first-seen order
|
|
30
|
+
*/
|
|
31
|
+
function extractDeclaredPaths(prdBody) {
|
|
32
|
+
if (!prdBody || typeof prdBody !== 'string') return [];
|
|
33
|
+
const text = [
|
|
34
|
+
extractSection(prdBody, 'Implementation notes'),
|
|
35
|
+
extractSection(prdBody, 'Acceptance criteria'),
|
|
36
|
+
].join('\n');
|
|
37
|
+
|
|
38
|
+
const paths = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
let m;
|
|
41
|
+
PATH_RE.lastIndex = 0;
|
|
42
|
+
while ((m = PATH_RE.exec(text))) {
|
|
43
|
+
if (!seen.has(m[1])) {
|
|
44
|
+
seen.add(m[1]);
|
|
45
|
+
paths.push(m[1]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return paths;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read a PRD file and extract its declared paths. Never throws — a missing
|
|
53
|
+
* or unreadable PRD (e.g. archived and swept) resolves to `[]`.
|
|
54
|
+
*
|
|
55
|
+
* @param {string|null} prdPath
|
|
56
|
+
* @returns {string[]}
|
|
57
|
+
*/
|
|
58
|
+
function declaredPathsForPrd(prdPath) {
|
|
59
|
+
if (!prdPath) return [];
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = fs.readFileSync(prdPath, 'utf8');
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const { body } = splitFrontmatter(raw);
|
|
67
|
+
return extractDeclaredPaths(body);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { extractDeclaredPaths, declaredPathsForPrd };
|
|
@@ -62,7 +62,7 @@ function splitFrontmatter(raw) {
|
|
|
62
62
|
* either surface never reorders or drops a key it didn't touch. Recognized
|
|
63
63
|
* key set intentionally mirrors the TS module exactly (title, cwd,
|
|
64
64
|
* estimateMinutes, parallelGroup, sourcePromptId, sourceTabId, tag,
|
|
65
|
-
* createdVia, issuedAt); every other frontmatter key (e.g. dependsOn)
|
|
65
|
+
* agentType, createdVia, issuedAt); every other frontmatter key (e.g. dependsOn)
|
|
66
66
|
* round-trips via `extras`, same as the renderer's editor today.
|
|
67
67
|
*
|
|
68
68
|
* `createdVia`/`issuedAt` (PRD provenance-lockdown) are recognized here so
|
|
@@ -72,8 +72,8 @@ function splitFrontmatter(raw) {
|
|
|
72
72
|
* into `extras`.
|
|
73
73
|
*/
|
|
74
74
|
|
|
75
|
-
const RECOGNIZED_KEYS = new Set(['title', 'cwd', 'estimateMinutes', 'parallelGroup', 'sourcePromptId', 'sourceTabId', 'tag', 'createdVia', 'issuedAt']);
|
|
76
|
-
const EMIT_ORDER = ['title', 'cwd', 'estimateMinutes', 'parallelGroup', 'sourcePromptId', 'sourceTabId', 'tag', 'createdVia', 'issuedAt'];
|
|
75
|
+
const RECOGNIZED_KEYS = new Set(['title', 'cwd', 'estimateMinutes', 'parallelGroup', 'sourcePromptId', 'sourceTabId', 'tag', 'agentType', 'createdVia', 'issuedAt', 'quietMachine']);
|
|
76
|
+
const EMIT_ORDER = ['title', 'cwd', 'estimateMinutes', 'parallelGroup', 'sourcePromptId', 'sourceTabId', 'tag', 'agentType', 'createdVia', 'issuedAt', 'quietMachine'];
|
|
77
77
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
78
78
|
|
|
79
79
|
function indentOf(line) {
|
|
@@ -125,12 +125,26 @@ function applyKey(fm, key, after) {
|
|
|
125
125
|
// Matches the TS source's applyKey exactly — see PRD_TAG_VALUES above.
|
|
126
126
|
if (PRD_TAG_VALUES.has(v)) fm.tag = v;
|
|
127
127
|
return;
|
|
128
|
+
case 'agentType':
|
|
129
|
+
// WHO executes this PRD — a persona name, not a closed enum (the
|
|
130
|
+
// Agent Library is user-editable). FK resolution against the Agent
|
|
131
|
+
// Library happens in prdAgentType.cjs, not here — this parser only
|
|
132
|
+
// round-trips the string.
|
|
133
|
+
fm.agentType = String(v);
|
|
134
|
+
return;
|
|
128
135
|
case 'createdVia':
|
|
129
136
|
fm.createdVia = String(v);
|
|
130
137
|
return;
|
|
131
138
|
case 'issuedAt':
|
|
132
139
|
fm.issuedAt = String(v);
|
|
133
140
|
return;
|
|
141
|
+
case 'quietMachine':
|
|
142
|
+
// Opt-in exclusive-lease flag (PRD 1107) — only a literal `true`
|
|
143
|
+
// frontmatter value opts in; anything else (including an explicit
|
|
144
|
+
// `false`) is treated as "not set" so the field never round-trips as
|
|
145
|
+
// a no-op line for the overwhelming majority of PRDs that omit it.
|
|
146
|
+
if (v === true) fm.quietMachine = true;
|
|
147
|
+
return;
|
|
134
148
|
}
|
|
135
149
|
}
|
|
136
150
|
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* projectHomeAdminRoutes.cjs — the app-side half of "the generating session
|
|
3
|
+
* needs zero repo knowledge to build a Project Home page" (PRD:
|
|
4
|
+
* project-home-admin-routes). Before this file, the project-home-builder
|
|
5
|
+
* Epic had to know session-manager's own repo layout (where the catalog
|
|
6
|
+
* lives, how to invoke the validator/renderer CLIs, which build step to run
|
|
7
|
+
* first) — which is exactly why generation silently hung on a foreign
|
|
8
|
+
* machine with no repo checked out. These four routes make the app itself
|
|
9
|
+
* the authority on paths/schema/catalog/rendering, reusing the shipped
|
|
10
|
+
* assets PRD 1088 put in the npm tarball (scripts/render-project-pages/dist/
|
|
11
|
+
* renderer.cjs, scripts/project-pages-logic/dist/logic.cjs,
|
|
12
|
+
* src/main/templates/project-pages-catalog.json,
|
|
13
|
+
* src/main/templates/project-pages-pipeline.md) rather than reimplementing
|
|
14
|
+
* any of them.
|
|
15
|
+
*
|
|
16
|
+
* Registered against the same injected localAdminHttp.cjs transport as
|
|
17
|
+
* prdAdminRoutes.cjs — see that file's header for the transport contract
|
|
18
|
+
* (`registerRoute(method, url, handler)`, handlers get
|
|
19
|
+
* `(req, res, query: URLSearchParams)`).
|
|
20
|
+
*
|
|
21
|
+
* The MCP tool surface that calls these routes is the NEXT PRD in this
|
|
22
|
+
* chain — this file only exposes the HTTP contract.
|
|
23
|
+
*/
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const { readBody, sendJson } = require('./localAdminHttp.cjs');
|
|
29
|
+
const { resolveProjectContext } = require('./projectRootResolve.cjs');
|
|
30
|
+
const { opsPath, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
31
|
+
const { PROJECT_PAGE_SUMMARY_JSON_SCHEMA, PROJECT_PAGE_PICKS_JSON_SCHEMA } = require('./projectPageSummarySchema.cjs');
|
|
32
|
+
const config = require('../config.cjs');
|
|
33
|
+
const { schemas } = require('../ipcSchemas.cjs');
|
|
34
|
+
|
|
35
|
+
const LENSES = ['home', 'marketing', 'feature', 'architecture', 'brief'];
|
|
36
|
+
|
|
37
|
+
// Shipped, build-time assets — fixed app-relative paths, read once and
|
|
38
|
+
// cached in-process, same pattern as projectPages.cjs's
|
|
39
|
+
// cachedDefaultHomeHtml (this file never changes at runtime; a repo edit
|
|
40
|
+
// requires an app restart to pick up during dev, same as that module).
|
|
41
|
+
const CATALOG_PATH = path.join(__dirname, '..', 'templates', 'project-pages-catalog.json');
|
|
42
|
+
const SPEC_PATH = path.join(__dirname, '..', 'templates', 'project-pages-pipeline.md');
|
|
43
|
+
const RENDERER_BUNDLE_PATH = path.join(__dirname, '..', '..', '..', 'scripts', 'render-project-pages', 'dist', 'renderer.cjs');
|
|
44
|
+
const LOGIC_BUNDLE_PATH = path.join(__dirname, '..', '..', '..', 'scripts', 'project-pages-logic', 'dist', 'logic.cjs');
|
|
45
|
+
|
|
46
|
+
let cachedCatalog = null;
|
|
47
|
+
function loadCatalog() {
|
|
48
|
+
if (!cachedCatalog) {
|
|
49
|
+
const raw = fs.readFileSync(CATALOG_PATH, 'utf8');
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
// Drop $comment — it names the repo-relative source files the catalog
|
|
52
|
+
// was generated from (build-tooling provenance), which would violate
|
|
53
|
+
// this route's "never point the caller at a repo-relative path"
|
|
54
|
+
// contract if echoed back verbatim.
|
|
55
|
+
cachedCatalog = { lenses: parsed.lenses };
|
|
56
|
+
}
|
|
57
|
+
return cachedCatalog;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let cachedSpecText = null;
|
|
61
|
+
function loadSpecText() {
|
|
62
|
+
if (cachedSpecText === null) {
|
|
63
|
+
cachedSpecText = fs.readFileSync(SPEC_PATH, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
return cachedSpecText;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let cachedRenderer = null;
|
|
69
|
+
function loadRenderer() {
|
|
70
|
+
if (!cachedRenderer) {
|
|
71
|
+
({ renderProjectPages: cachedRenderer } = require(RENDERER_BUNDLE_PATH));
|
|
72
|
+
}
|
|
73
|
+
return cachedRenderer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let cachedValidator = null;
|
|
77
|
+
function loadValidator() {
|
|
78
|
+
if (!cachedValidator) {
|
|
79
|
+
({ validateProjectPageSummary: cachedValidator } = require(LOGIC_BUNDLE_PATH));
|
|
80
|
+
}
|
|
81
|
+
return cachedValidator;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Same definition crossProjectFeedback.cjs uses for "is this a Session
|
|
85
|
+
* Manager project": it already has an operations root. Deliberately does
|
|
86
|
+
* NOT require project-pages/ specifically to exist — a project that has
|
|
87
|
+
* never generated a Project Home page yet is still a Session Manager
|
|
88
|
+
* project (see the "no output dir yet" edge case on /status and /render). */
|
|
89
|
+
function isSessionManagerProject(cwd) {
|
|
90
|
+
try {
|
|
91
|
+
return fs.statSync(path.join(cwd, OPS_ROOT_DIR)).isDirectory();
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function projectPagesPaths(cwd) {
|
|
98
|
+
const dir = opsPath(cwd, 'project-pages');
|
|
99
|
+
return {
|
|
100
|
+
summaryPath: path.join(dir, 'summary.json'),
|
|
101
|
+
picksPath: path.join(dir, 'picks.json'),
|
|
102
|
+
outputDir: path.join(dir, 'output'),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve+validate a caller-supplied cwd into a real Session Manager
|
|
108
|
+
* project root. Never throws — returns a structured verdict so every route
|
|
109
|
+
* can turn a bad cwd into a JSON error instead of an unhandled throw (this
|
|
110
|
+
* PRD's edge-case requirement).
|
|
111
|
+
*
|
|
112
|
+
* Deliberately does NOT call config.addAllowedRoot here — that widens
|
|
113
|
+
* config.cjs's process-lifetime write boundary and belongs only on an
|
|
114
|
+
* actual write path (the render route calls it itself, right before
|
|
115
|
+
* writing), not on every GET this function also gates.
|
|
116
|
+
*/
|
|
117
|
+
function resolveCwd(rawCwd) {
|
|
118
|
+
if (!rawCwd || typeof rawCwd !== 'string') {
|
|
119
|
+
return { ok: false, status: 400, error: 'cwd is required' };
|
|
120
|
+
}
|
|
121
|
+
if (!path.isAbsolute(rawCwd)) {
|
|
122
|
+
return { ok: false, status: 400, error: 'cwd must be an absolute path' };
|
|
123
|
+
}
|
|
124
|
+
// Normalizes a worktree/ops-internal cwd to its real project root (see
|
|
125
|
+
// projectRootResolve.cjs's header) so a headless job running inside an
|
|
126
|
+
// Epic's worktree still targets the right project.
|
|
127
|
+
const resolved = resolveProjectContext({ cwd: rawCwd });
|
|
128
|
+
const candidate = resolved.cwd || rawCwd;
|
|
129
|
+
let realCwd;
|
|
130
|
+
try {
|
|
131
|
+
realCwd = config.validatePath(candidate);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return { ok: false, status: 400, error: `cwd rejected: ${e?.message ?? 'outside allowed roots'}` };
|
|
134
|
+
}
|
|
135
|
+
if (!isSessionManagerProject(realCwd)) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
status: 400,
|
|
139
|
+
error: `${realCwd} is not a Session Manager project — it has no ${OPS_ROOT_DIR}/ directory`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { ok: true, cwd: realCwd };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Each validateProjectPageSummary error string is "<field.path> must ...".
|
|
146
|
+
* Split it into { field, message } for the route's documented response
|
|
147
|
+
* shape — the validator itself only returns flat strings. */
|
|
148
|
+
function toFieldErrors(errors) {
|
|
149
|
+
return errors.map((message) => {
|
|
150
|
+
const m = /^([\w.]+)\s/.exec(message);
|
|
151
|
+
return { field: m ? m[1] : 'summary', message };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Structural check for ProjectPagePicks (lensId -> slotId -> variantId) —
|
|
157
|
+
* the shipped validator only covers `summary`, so this is the one guard
|
|
158
|
+
* against handing the renderer bundle a shape it doesn't expect. Returns
|
|
159
|
+
* "<field> must ..." strings in the same format toFieldErrors expects, so
|
|
160
|
+
* both routes can share one error-shape.
|
|
161
|
+
*/
|
|
162
|
+
function validatePicksShape(picks) {
|
|
163
|
+
if (picks === null || typeof picks !== 'object' || Array.isArray(picks)) {
|
|
164
|
+
return ['picks must be an object'];
|
|
165
|
+
}
|
|
166
|
+
const errors = [];
|
|
167
|
+
for (const [lensId, slotMap] of Object.entries(picks)) {
|
|
168
|
+
if (slotMap === null || typeof slotMap !== 'object' || Array.isArray(slotMap)) {
|
|
169
|
+
errors.push(`picks.${lensId} must be an object`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
for (const [slotId, variantId] of Object.entries(slotMap)) {
|
|
173
|
+
if (typeof variantId !== 'string' || variantId.length === 0) {
|
|
174
|
+
errors.push(`picks.${lensId}.${slotId} must be a non-empty string`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return errors;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Shared readBody -> JSON.parse -> zod-schema.parse pipeline for the two
|
|
182
|
+
* POST routes — collapses the 3-step try/catch chain that was previously
|
|
183
|
+
* duplicated in both handlers into one call. Returns either
|
|
184
|
+
* { ok: true, input } or { ok: false, status, error, details }. */
|
|
185
|
+
async function parseJsonBody(req, schema) {
|
|
186
|
+
const raw = await readBody(req);
|
|
187
|
+
let body;
|
|
188
|
+
try {
|
|
189
|
+
body = raw ? JSON.parse(raw) : {};
|
|
190
|
+
} catch {
|
|
191
|
+
return { ok: false, status: 400, error: 'invalid JSON body' };
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return { ok: true, input: schema.parse(body) };
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return { ok: false, status: 400, error: 'invalid payload', details: e?.issues ?? e?.message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildProtocol() {
|
|
201
|
+
return [
|
|
202
|
+
'Call GET /admin/project-home/status?cwd=<abs> to see what already exists for this project (summary.json, picks.json, and any prior output).',
|
|
203
|
+
'Compose a ProjectPageSummary object matching the schema in this response\'s "summarySchema" field. Never fabricate content — every field must trace to something concrete about this project (an Epic goal, a source file/dir, a convention, a git log entry). Omit "brief" entirely if this project has no generated brief.json yet; omit "quotes" entries rather than inventing a testimonial.',
|
|
204
|
+
'For each lens in this response\'s "catalog.lenses", and for each of that lens\'s slots, pick exactly one variant id by judging the candidate variants\' "note" text against the summary content you composed. Assemble the picks into a ProjectPagePicks object (lensId -> slotId -> variantId) matching "picksSchema".',
|
|
205
|
+
'POST /admin/project-home/validate-summary with {cwd, summary}. If "valid" is false, fix every listed {field, message} error and re-validate before continuing.',
|
|
206
|
+
'POST /admin/project-home/render with {cwd, summary, picks}. This re-validates the summary server-side (rejecting with no writes on failure), renders all 5 lenses, and writes summary.json, picks.json, and output/{home,marketing,feature,architecture,brief}.html + output/manifest.json at the absolute paths in this response\'s "paths" field.',
|
|
207
|
+
'Optionally call GET /admin/project-home/status again to confirm the new files landed and to read manifest.json\'s generatedAt.',
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function registerAdminRoute(adminHttp) {
|
|
212
|
+
// GET /admin/project-home/contract?cwd=<abs> — the self-sufficient
|
|
213
|
+
// contract: protocol, schema, catalog, absolute paths, spec. Nothing in
|
|
214
|
+
// this response should require the caller to read a repo-relative path.
|
|
215
|
+
adminHttp.registerRoute('GET', '/admin/project-home/contract', async (req, res, query) => {
|
|
216
|
+
let input;
|
|
217
|
+
try {
|
|
218
|
+
input = schemas.projectHomeAdminCwdQuery.parse(Object.fromEntries(query ?? []));
|
|
219
|
+
} catch (e) {
|
|
220
|
+
sendJson(res, 400, { ok: false, error: 'invalid query', details: e?.issues ?? e?.message });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const resolved = resolveCwd(input.cwd);
|
|
224
|
+
if (!resolved.ok) {
|
|
225
|
+
sendJson(res, resolved.status, { ok: false, error: resolved.error });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// projectPagesPaths()->opsPath() can throw for a cwd resolveCwd accepted
|
|
229
|
+
// but opsPath's own (separate) ephemeral-root check refuses — keep this
|
|
230
|
+
// route's "never an unhandled throw" contract even on that edge.
|
|
231
|
+
try {
|
|
232
|
+
const { summaryPath, picksPath, outputDir } = projectPagesPaths(resolved.cwd);
|
|
233
|
+
sendJson(res, 200, {
|
|
234
|
+
ok: true,
|
|
235
|
+
protocol: buildProtocol(),
|
|
236
|
+
summarySchema: PROJECT_PAGE_SUMMARY_JSON_SCHEMA,
|
|
237
|
+
picksSchema: PROJECT_PAGE_PICKS_JSON_SCHEMA,
|
|
238
|
+
catalog: loadCatalog(),
|
|
239
|
+
paths: { summaryPath, picksPath, outputDir },
|
|
240
|
+
spec: { text: loadSpecText(), path: SPEC_PATH },
|
|
241
|
+
});
|
|
242
|
+
} catch (e) {
|
|
243
|
+
sendJson(res, 400, { ok: false, error: e?.message ?? 'failed to resolve project-pages paths' });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// POST /admin/project-home/validate-summary {cwd, summary}
|
|
248
|
+
adminHttp.registerRoute('POST', '/admin/project-home/validate-summary', async (req, res) => {
|
|
249
|
+
const parsed = await parseJsonBody(req, schemas.projectHomeAdminValidateSummaryBody);
|
|
250
|
+
if (!parsed.ok) {
|
|
251
|
+
sendJson(res, parsed.status, { ok: false, error: parsed.error, details: parsed.details });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const { input } = parsed;
|
|
255
|
+
const resolved = resolveCwd(input.cwd);
|
|
256
|
+
if (!resolved.ok) {
|
|
257
|
+
sendJson(res, resolved.status, { ok: false, error: resolved.error });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const validateProjectPageSummary = loadValidator();
|
|
261
|
+
const result = validateProjectPageSummary(input.summary);
|
|
262
|
+
if (!result.ok) {
|
|
263
|
+
sendJson(res, 200, { ok: true, valid: false, errors: toFieldErrors(result.errors) });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
sendJson(res, 200, { ok: true, valid: true, errors: [] });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// POST /admin/project-home/render {cwd, summary, picks}
|
|
270
|
+
adminHttp.registerRoute('POST', '/admin/project-home/render', async (req, res) => {
|
|
271
|
+
const parsed = await parseJsonBody(req, schemas.projectHomeAdminRenderBody);
|
|
272
|
+
if (!parsed.ok) {
|
|
273
|
+
sendJson(res, parsed.status, { ok: false, error: parsed.error, details: parsed.details });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const { input } = parsed;
|
|
277
|
+
const resolved = resolveCwd(input.cwd);
|
|
278
|
+
if (!resolved.ok) {
|
|
279
|
+
sendJson(res, resolved.status, { ok: false, error: resolved.error });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const validateProjectPageSummary = loadValidator();
|
|
283
|
+
const validation = validateProjectPageSummary(input.summary);
|
|
284
|
+
const picksErrors = validatePicksShape(input.picks);
|
|
285
|
+
if (!validation.ok || picksErrors.length > 0) {
|
|
286
|
+
sendJson(res, 400, {
|
|
287
|
+
ok: false,
|
|
288
|
+
valid: false,
|
|
289
|
+
errors: [...toFieldErrors(validation.ok ? [] : validation.errors), ...toFieldErrors(picksErrors)],
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Only a route that is actually about to write registers the cwd as a
|
|
295
|
+
// write-allowed root — see resolveCwd's header for why this must not
|
|
296
|
+
// happen on the GET routes above.
|
|
297
|
+
config.addAllowedRoot(resolved.cwd);
|
|
298
|
+
try {
|
|
299
|
+
const result = await serializedPerCwd(resolved.cwd, () => doRender(resolved.cwd, input.summary, input.picks));
|
|
300
|
+
sendJson(res, 200, { ok: true, ...result });
|
|
301
|
+
} catch (e) {
|
|
302
|
+
sendJson(res, 500, { ok: false, error: e?.message ?? 'render failed' });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// GET /admin/project-home/status?cwd=<abs>
|
|
307
|
+
adminHttp.registerRoute('GET', '/admin/project-home/status', async (req, res, query) => {
|
|
308
|
+
let input;
|
|
309
|
+
try {
|
|
310
|
+
input = schemas.projectHomeAdminCwdQuery.parse(Object.fromEntries(query ?? []));
|
|
311
|
+
} catch (e) {
|
|
312
|
+
sendJson(res, 400, { ok: false, error: 'invalid query', details: e?.issues ?? e?.message });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const resolved = resolveCwd(input.cwd);
|
|
316
|
+
if (!resolved.ok) {
|
|
317
|
+
sendJson(res, resolved.status, { ok: false, error: resolved.error });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const status = await getStatus(resolved.cwd);
|
|
322
|
+
sendJson(res, 200, { ok: true, ...status });
|
|
323
|
+
} catch (e) {
|
|
324
|
+
sendJson(res, 400, { ok: false, error: e?.message ?? 'failed to read project-pages status' });
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Per-cwd in-process promise chain — the render route's only concurrency
|
|
330
|
+
// guard. Sufficient because this admin HTTP server is a single Node
|
|
331
|
+
// process (Electron main) with no worker pool for these routes; a second
|
|
332
|
+
// concurrent render for the SAME cwd simply waits for the first to finish
|
|
333
|
+
// writing before it starts, so output/ is never observed half-written by
|
|
334
|
+
// two overlapping /render calls (it does NOT defend against a
|
|
335
|
+
// project-home-builder Epic's own separate Write-tool calls racing this
|
|
336
|
+
// route from a different OS process — see project-pages/README.md's
|
|
337
|
+
// concurrency section). Entries are evicted once their chain settles so
|
|
338
|
+
// this Map doesn't grow for the life of the process across many cwds.
|
|
339
|
+
const renderChains = new Map();
|
|
340
|
+
function serializedPerCwd(cwd, fn) {
|
|
341
|
+
const prev = renderChains.get(cwd) || Promise.resolve();
|
|
342
|
+
const next = prev.catch(() => {}).then(fn);
|
|
343
|
+
const settled = next.catch(() => {});
|
|
344
|
+
renderChains.set(cwd, settled);
|
|
345
|
+
settled.finally(() => {
|
|
346
|
+
if (renderChains.get(cwd) === settled) renderChains.delete(cwd);
|
|
347
|
+
});
|
|
348
|
+
return next;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function doRender(cwd, summary, picks) {
|
|
352
|
+
const renderProjectPages = loadRenderer();
|
|
353
|
+
const pages = renderProjectPages(summary, picks);
|
|
354
|
+
const generatedAt = new Date().toISOString();
|
|
355
|
+
const { summaryPath, picksPath, outputDir } = projectPagesPaths(cwd);
|
|
356
|
+
|
|
357
|
+
// The 5 lens files plus summary.json/picks.json have no ordering
|
|
358
|
+
// dependency on each other — only manifest.json's presence is the
|
|
359
|
+
// "generation complete" signal getStatus() relies on, so it's written
|
|
360
|
+
// last, after everything else has landed.
|
|
361
|
+
const lensWrites = LENSES.map((lens) => {
|
|
362
|
+
const lensPath = path.join(outputDir, `${lens}.html`);
|
|
363
|
+
return config.writeTextAtomic(lensPath, pages[lens], { writer: 'project-home' }).then(() => lensPath);
|
|
364
|
+
});
|
|
365
|
+
const [, , ...lensPaths] = await Promise.all([
|
|
366
|
+
config.writeJson(summaryPath, summary, { writer: 'project-home' }).then(() => summaryPath),
|
|
367
|
+
config.writeJson(picksPath, picks, { writer: 'project-home' }).then(() => picksPath),
|
|
368
|
+
...lensWrites,
|
|
369
|
+
]);
|
|
370
|
+
const manifestPath = path.join(outputDir, 'manifest.json');
|
|
371
|
+
await config.writeJson(manifestPath, { generatedAt }, { writer: 'project-home' });
|
|
372
|
+
|
|
373
|
+
return { filesWritten: [summaryPath, picksPath, ...lensPaths, manifestPath], generatedAt };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function getStatus(cwd) {
|
|
377
|
+
const { summaryPath, picksPath, outputDir } = projectPagesPaths(cwd);
|
|
378
|
+
const [summaryRes, picksRes, manifestRes, ...lensRes] = await Promise.all([
|
|
379
|
+
config.readJson(summaryPath),
|
|
380
|
+
config.readJson(picksPath),
|
|
381
|
+
config.readJson(path.join(outputDir, 'manifest.json')),
|
|
382
|
+
...LENSES.map((lens) => config.readText(path.join(outputDir, `${lens}.html`))),
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
const output = {};
|
|
386
|
+
LENSES.forEach((lens, i) => {
|
|
387
|
+
const r = lensRes[i];
|
|
388
|
+
output[lens] = { exists: r.exists, mtimeMs: r.exists ? r.mtimeMs : null };
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
summary: { exists: summaryRes.exists, mtimeMs: summaryRes.exists ? summaryRes.mtimeMs : null },
|
|
393
|
+
picks: { exists: picksRes.exists, mtimeMs: picksRes.exists ? picksRes.mtimeMs : null },
|
|
394
|
+
output,
|
|
395
|
+
manifest: {
|
|
396
|
+
exists: manifestRes.exists,
|
|
397
|
+
generatedAt: manifestRes.exists && manifestRes.data ? (manifestRes.data.generatedAt ?? null) : null,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = { registerAdminRoute };
|