@smartmemory/compose 0.2.50-beta → 0.2.51
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/.claude/skills/compose/SKILL.md +24 -1
- package/.claude/skills/compose/references/hermes-tools.md +80 -0
- package/README.md +1 -1
- package/bin/compose.js +310 -52
- package/dist/assets/App-CT0vXPgd.js +724 -0
- package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
- package/dist/assets/{arc-DeHak63Z.js → arc-BXht3LyE.js} +1 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +132 -0
- package/dist/assets/{browser-DW2JlJCC.js → browser-CnKiSnlr.js} +6 -6
- package/dist/assets/{c4Diagram-AAUBKEIU-BsFJmPuS.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
- package/dist/assets/channel-Dd4XaiYv.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-XO4S2_89.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
- package/dist/assets/{chunk-55IACEB6-CtFSLInj.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
- package/dist/assets/{chunk-2J33WTMH-AEu6HIoY.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BMGd_Y9a.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
- package/dist/assets/{chunk-QZHKN3VN-DHhUJ8RK.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-BlrOECEr.js → chunk-YZCP3GAM-DE6_5aqh.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +1 -0
- package/dist/assets/clone-CBsbNGAa.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-C3M0mZqk.js → cose-bilkent-S5V4N54A-DzzRyJtB.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +43 -0
- package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +24 -0
- package/dist/assets/{erDiagram-TEJ5UH35-AMDitV0b.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-C0t2ZGQv.js → flowDiagram-DWJPFMVM-BzzO4JYU.js} +1 -1
- package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +106 -0
- package/dist/assets/graph-DZe55uk8.js +331 -0
- package/dist/assets/graph-Tq_bs_r0.js +1 -0
- package/dist/assets/index-8UhRLbGq.js +123 -0
- package/dist/assets/index-CRqB9els.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +2 -0
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CCBwgjFs.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
- package/dist/assets/{journeyDiagram-JHISSGLW-zUJN38EU.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-BNxWi-8J.js → kanban-definition-6JOO6SKY-CEq930ew.js} +1 -1
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-qtUgN9BC.js +1 -0
- package/dist/assets/{linear-CRH9b4g7.js → linear-D158F7OT.js} +1 -1
- package/dist/assets/min-hESxN-c0.js +1 -0
- package/dist/assets/{mindmap-definition-RKZ34NQL-DolYwq7Y.js → mindmap-definition-QFDTVHPH-d9SSh2nG.js} +7 -7
- package/dist/assets/mobile-1gVCT0OK.css +1 -0
- package/dist/assets/mobile-Chw8RWyH.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +7 -0
- package/dist/assets/{requirementDiagram-4Y6WPE33-DsXZ05jo.js → requirementDiagram-MS252O5E-B1ZFIotK.js} +1 -1
- package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-Bvnhw58e.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-5WnyjZqf.js → vennDiagram-DHZGUBPP-TZAW60b0.js} +4 -4
- package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +20 -0
- package/dist/assets/{xychartDiagram-2RQKCTM6-BCShes6B.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
- package/dist/index.html +4 -4
- package/lib/build.js +272 -33
- package/lib/codex-preflight.js +151 -0
- package/lib/completion-writer.js +21 -1
- package/lib/feature-validator.js +5 -1
- package/lib/hooks-status.js +135 -0
- package/lib/install-agent-defs.js +22 -0
- package/lib/migrate-anon.js +277 -0
- package/lib/roadmap-parser.js +1 -1
- package/lib/test-bootstrap.js +165 -2
- package/lib/vocabulary-inject.js +103 -0
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +433 -0
- package/pipelines/build.stratum.yaml +60 -3
- package/scripts/watch-server.sh +32 -0
- package/server/build-routes.js +89 -9
- package/server/build-stream-bridge.js +12 -0
- package/server/compose-mcp-tools.js +73 -1
- package/server/compose-mcp.js +27 -0
- package/server/feature-scaffold-routes.js +82 -0
- package/server/health-routes.js +261 -0
- package/server/qa-scope-routes.js +96 -0
- package/server/validate-routes.js +70 -0
- package/server/vision-routes.js +29 -0
- package/server/vision-server.js +21 -0
- package/dist/assets/App-Bxyif1yv.js +0 -706
- package/dist/assets/architectureDiagram-3BPJPVTR-C5Nkyl9y.js +0 -36
- package/dist/assets/blockDiagram-GPEHLZMM-BilnUBG2.js +0 -132
- package/dist/assets/channel-ZgbQ1k0u.js +0 -1
- package/dist/assets/chunk-727SXJPM-CZeBRpM9.js +0 -206
- package/dist/assets/chunk-AQP2D5EJ-_vmxcBc4.js +0 -231
- package/dist/assets/classDiagram-4FO5ZUOK-KXxOorUx.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-KXxOorUx.js +0 -1
- package/dist/assets/dagre-BM42HDAG-DJlm4fEk.js +0 -4
- package/dist/assets/diagram-2AECGRRQ-BmcONQG5.js +0 -43
- package/dist/assets/diagram-5GNKFQAL-5JbWGIwd.js +0 -10
- package/dist/assets/diagram-KO2AKTUF-DAnIvyI0.js +0 -3
- package/dist/assets/diagram-LMA3HP47-Bmh7tvmI.js +0 -24
- package/dist/assets/diagram-OG6HWLK6-hnHVMNnk.js +0 -24
- package/dist/assets/ganttDiagram-6RSMTGT7-BPlM-BEN.js +0 -292
- package/dist/assets/gitGraphDiagram-PVQCEYII-Lv_YTxnb.js +0 -106
- package/dist/assets/graph-BBXaecIU.js +0 -331
- package/dist/assets/graph-CAnANduQ.js +0 -1
- package/dist/assets/index-BwLfbbOu.css +0 -1
- package/dist/assets/index-COq21Zym.js +0 -119
- package/dist/assets/infoDiagram-5YYISTIA-Dul1vdUm.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DGIYPm2g.js +0 -1
- package/dist/assets/mobile-Cag5dHlF.css +0 -1
- package/dist/assets/mobile-DwmxS_O4.js +0 -17
- package/dist/assets/pieDiagram-4H26LBE5-oxNfr2TX.js +0 -30
- package/dist/assets/quadrantDiagram-W4KKPZXB-BmZCvD-z.js +0 -7
- package/dist/assets/sankeyDiagram-5OEKKPKP-DsnYaawP.js +0 -40
- package/dist/assets/sequenceDiagram-3UESZ5HK-Dt5mu3g8.js +0 -162
- package/dist/assets/stateDiagram-AJRCARHV-BFR5ZINQ.js +0 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-BDrQD8fR.js +0 -1
- package/dist/assets/wardley-L42UT6IY-CTxW4xow.js +0 -173
- package/dist/assets/wardleyDiagram-YWT4CUSO-K8Y1EOvb.js +0 -78
package/server/build-routes.js
CHANGED
|
@@ -13,15 +13,36 @@
|
|
|
13
13
|
* tests; it lets a caller inject mock `runBuild` / `abortBuild` / `getDataDir`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
16
18
|
import { runBuild as defaultRunBuild, abortBuild as defaultAbortBuild } from '../lib/build.js';
|
|
19
|
+
import { runNew as defaultRunNew } from '../lib/new.js';
|
|
20
|
+
import { runBuildAll as defaultRunBuildAll } from '../lib/build-all.js';
|
|
21
|
+
import { runGsd as defaultRunGsd } from '../lib/gsd.js';
|
|
17
22
|
import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
|
|
18
|
-
import { getDataDir as defaultGetDataDir } from './project-root.js';
|
|
23
|
+
import { getDataDir as defaultGetDataDir, getTargetRoot as defaultGetTargetRoot } from './project-root.js';
|
|
19
24
|
import { readBuildHistory } from '../lib/build-history.js';
|
|
20
25
|
|
|
26
|
+
// A launcher conflict (another build/GSD run already owns the feature) is a
|
|
27
|
+
// user-facing 409, not a 500. runBuild surfaces "already active"; runGsd
|
|
28
|
+
// refuses a concurrent run ("Refusing to start a concurrent run").
|
|
29
|
+
function launcherErrorStatus(err) {
|
|
30
|
+
const msg = err && err.message ? err.message : '';
|
|
31
|
+
return /already active|already running|concurrent run|refusing to start/i.test(msg) ? 409 : 500;
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export function attachBuildRoutes(app, deps = {}) {
|
|
22
35
|
const runBuild = deps.runBuild || defaultRunBuild;
|
|
23
36
|
const abortBuild = deps.abortBuild || defaultAbortBuild;
|
|
37
|
+
const runNew = deps.runNew || defaultRunNew;
|
|
38
|
+
const runBuildAll = deps.runBuildAll || defaultRunBuildAll;
|
|
39
|
+
const runGsd = deps.runGsd || defaultRunGsd;
|
|
24
40
|
const getDataDir = deps.getDataDir || defaultGetDataDir;
|
|
41
|
+
// Bind build dispatch to the active project root (the same global target the
|
|
42
|
+
// vision store + getDataDir() use). Without this the runners default to
|
|
43
|
+
// process.cwd(), which switchProject() does not chdir — so after a project
|
|
44
|
+
// switch the cockpit and a launched build could target different workspaces.
|
|
45
|
+
const getTargetRoot = deps.getTargetRoot || defaultGetTargetRoot;
|
|
25
46
|
|
|
26
47
|
// COMP-COCKPIT-3: read-only past-builds history. No sensitive token —
|
|
27
48
|
// mirrors GET /api/build/state (read-only, no secrets in the records).
|
|
@@ -38,22 +59,81 @@ export function attachBuildRoutes(app, deps = {}) {
|
|
|
38
59
|
|
|
39
60
|
app.post('/api/build/start', requireSensitiveToken, async (req, res) => {
|
|
40
61
|
const body = req.body || {};
|
|
41
|
-
const { featureCode, mode = 'feature', description = '' } = body;
|
|
42
|
-
|
|
62
|
+
const { featureCode, mode = 'feature', description = '', resume = false } = body;
|
|
63
|
+
|
|
64
|
+
// featureCode is required for feature/bug/gsd; 'new' takes a free-text
|
|
65
|
+
// intent (no code yet) and 'all' sweeps every PLANNED feature.
|
|
66
|
+
if (!featureCode && mode !== 'all' && mode !== 'new') {
|
|
43
67
|
return res.status(400).json({ error: 'featureCode required' });
|
|
44
68
|
}
|
|
45
|
-
if (
|
|
46
|
-
return res.status(400).json({ error: "mode must be 'feature' or '
|
|
69
|
+
if (!['feature', 'bug', 'new', 'all', 'gsd'].includes(mode)) {
|
|
70
|
+
return res.status(400).json({ error: "mode must be 'feature', 'bug', 'new', 'all', or 'gsd'" });
|
|
47
71
|
}
|
|
72
|
+
|
|
73
|
+
// PARITY-2: launch the `new` lifecycle from a description (intent).
|
|
74
|
+
if (mode === 'new') {
|
|
75
|
+
const intent = (description || '').trim();
|
|
76
|
+
if (!intent) {
|
|
77
|
+
return res.status(400).json({ error: 'intent (description) required for mode=new' });
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const result = await runNew(intent, { cwd: getTargetRoot() });
|
|
81
|
+
return res.json(result ?? { ok: true });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return res.status(500).json({ error: err.message || String(err) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// PARITY-8: build-all sweep.
|
|
88
|
+
if (mode === 'all') {
|
|
89
|
+
try {
|
|
90
|
+
const result = await runBuildAll({ cwd: getTargetRoot() });
|
|
91
|
+
return res.json(result ?? { ok: true });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return res.status(launcherErrorStatus(err)).json({ error: err.message || String(err) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// PARITY-8: GSD autonomous run for a single feature.
|
|
98
|
+
if (mode === 'gsd') {
|
|
99
|
+
try {
|
|
100
|
+
const result = await runGsd(featureCode, { cwd: getTargetRoot() });
|
|
101
|
+
return res.json(result ?? { ok: true });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return res.status(launcherErrorStatus(err)).json({ error: err.message || String(err) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// PARITY-2: resume an interrupted bug-fix flow from active-build.json.
|
|
108
|
+
let resumeFlowId = null;
|
|
109
|
+
if (resume === true) {
|
|
110
|
+
if (mode !== 'bug') {
|
|
111
|
+
return res.status(400).json({ error: 'resume is only supported for mode=bug' });
|
|
112
|
+
}
|
|
113
|
+
let active = null;
|
|
114
|
+
try {
|
|
115
|
+
const activePath = path.join(getDataDir(), 'active-build.json');
|
|
116
|
+
active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
|
|
117
|
+
} catch {
|
|
118
|
+
active = null;
|
|
119
|
+
}
|
|
120
|
+
if (!active || active.featureCode !== featureCode || !active.flowId) {
|
|
121
|
+
return res.status(409).json({ error: 'no resumable build for featureCode' });
|
|
122
|
+
}
|
|
123
|
+
if (active.mode && active.mode !== 'bug') {
|
|
124
|
+
return res.status(409).json({ error: `active build is mode=${active.mode}, not resumable as bug` });
|
|
125
|
+
}
|
|
126
|
+
resumeFlowId = active.flowId;
|
|
127
|
+
}
|
|
128
|
+
|
|
48
129
|
try {
|
|
49
130
|
const opts = mode === 'bug'
|
|
50
|
-
? { mode, template: 'bug-fix', description }
|
|
51
|
-
: { mode, description };
|
|
131
|
+
? { mode, template: 'bug-fix', description, cwd: getTargetRoot(), ...(resumeFlowId ? { resumeFlowId } : {}) }
|
|
132
|
+
: { mode, description, cwd: getTargetRoot() };
|
|
52
133
|
const result = await runBuild(featureCode, opts);
|
|
53
134
|
res.json(result ?? { ok: true });
|
|
54
135
|
} catch (err) {
|
|
55
|
-
|
|
56
|
-
res.status(code).json({ error: err.message || String(err) });
|
|
136
|
+
res.status(launcherErrorStatus(err)).json({ error: err.message || String(err) });
|
|
57
137
|
}
|
|
58
138
|
});
|
|
59
139
|
|
|
@@ -471,6 +471,18 @@ export class BuildStreamBridge {
|
|
|
471
471
|
};
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
// COMP-TEST-BOOTSTRAP-4-1: advisory review of the tests the coverage step
|
|
475
|
+
// generated this build. Forwarded so the cockpit can surface the findings
|
|
476
|
+
// for human verification (the step never blocks ship).
|
|
477
|
+
case 'test_review':
|
|
478
|
+
return {
|
|
479
|
+
type: 'system', subtype: 'test_review',
|
|
480
|
+
clean: event.clean ?? true,
|
|
481
|
+
summary: event.summary ?? '',
|
|
482
|
+
findings: event.findings ?? [],
|
|
483
|
+
_source: 'build',
|
|
484
|
+
};
|
|
485
|
+
|
|
474
486
|
// COMP-HEALTH item 118: health score after build completion
|
|
475
487
|
case 'health_score':
|
|
476
488
|
return {
|
|
@@ -359,10 +359,82 @@ export async function toolProposeFollowup(args) {
|
|
|
359
359
|
// propose_followup also accepts a caller-supplied `status` and routes to
|
|
360
360
|
// addRoadmapEntry — gate lifecycle-owned terminal statuses the same way.
|
|
361
361
|
assertTerminalStatusAuthorized(args, 'propose_followup');
|
|
362
|
-
|
|
362
|
+
let proposeFollowup;
|
|
363
|
+
try {
|
|
364
|
+
({ proposeFollowup } = await import('../lib/followup-writer.js'));
|
|
365
|
+
} catch (err) {
|
|
366
|
+
// COMP-MCP-FOLLOWUP-1: a long-running stdio MCP server caches ESM module
|
|
367
|
+
// records. If lib/ changed under it (e.g. a later commit added an export
|
|
368
|
+
// this lazily-imported module now depends on), linking fails with a cryptic
|
|
369
|
+
// "does not provide an export named ..." even though the on-disk code is
|
|
370
|
+
// correct — the running server just needs to reload. Rethrow with an
|
|
371
|
+
// actionable hint instead of the raw ESM error.
|
|
372
|
+
if (/does not provide an export named|Cannot find module/i.test(err?.message || '')) {
|
|
373
|
+
const hint = new Error(
|
|
374
|
+
`propose_followup could not load lib/followup-writer.js: ${err.message}. ` +
|
|
375
|
+
`This usually means the compose MCP server is running stale code (module-cache skew after a ` +
|
|
376
|
+
`code change) — reconnect the MCP server (/mcp) to reload it. If it persists after reconnecting, ` +
|
|
377
|
+
`an export is genuinely missing on disk.`,
|
|
378
|
+
);
|
|
379
|
+
hint.cause = err;
|
|
380
|
+
throw hint;
|
|
381
|
+
}
|
|
382
|
+
throw err;
|
|
383
|
+
}
|
|
363
384
|
return proposeFollowup(getTargetRoot(), args);
|
|
364
385
|
}
|
|
365
386
|
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// Eager boot-time module preload — COMP-MCP-FOLLOWUP-1-1
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Modules reachable only via a lazy `import()` on a hot tool path, which
|
|
393
|
+
* therefore escape boot-time ESM linking. Eager-importing them at server start
|
|
394
|
+
* makes a genuine on-disk break (missing module OR missing export) fail fast at
|
|
395
|
+
* boot instead of surfacing on first tool invocation — complementing the
|
|
396
|
+
* COMP-MCP-FOLLOWUP-1 CI import-graph test.
|
|
397
|
+
*
|
|
398
|
+
* The default set is exactly one module: `lib/followup-writer.js`, lazily
|
|
399
|
+
* imported by `toolProposeFollowup`. Its own dynamic import
|
|
400
|
+
* (`server/artifact-manager.js`) is already statically imported at the top of
|
|
401
|
+
* this file, so it links at boot regardless — no need to list it here.
|
|
402
|
+
*/
|
|
403
|
+
export const EAGER_PRELOAD_MODULES = [
|
|
404
|
+
{ specifier: '../lib/followup-writer.js', expect: 'proposeFollowup' },
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Eagerly import each spec and assert its expected export is a function.
|
|
409
|
+
* Throws (naming the offending module) on a missing module or missing export.
|
|
410
|
+
*
|
|
411
|
+
* `await import()` does NOT throw when a module loads but no longer provides the
|
|
412
|
+
* expected named export — it resolves with that binding `undefined`. So the
|
|
413
|
+
* binding check, not the import alone, is what turns a missing export into a
|
|
414
|
+
* boot failure (the exact COMP-MCP-FOLLOWUP-1 failure class).
|
|
415
|
+
*
|
|
416
|
+
* `specs` is injectable for testing and for the `COMPOSE_PRELOAD_PROBE` boot
|
|
417
|
+
* seam; entries without an `expect` are import-only checks.
|
|
418
|
+
*/
|
|
419
|
+
export async function preloadEagerModules(specs = EAGER_PRELOAD_MODULES) {
|
|
420
|
+
const loaded = {};
|
|
421
|
+
for (const { specifier, expect } of specs) {
|
|
422
|
+
let mod;
|
|
423
|
+
try {
|
|
424
|
+
mod = await import(specifier);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
throw new Error(`eager preload failed for ${specifier}: ${err.message}`, { cause: err });
|
|
427
|
+
}
|
|
428
|
+
if (expect && typeof mod[expect] !== 'function') {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`eager preload failed for ${specifier}: expected export '${expect}' to be a function, got ${typeof mod[expect]}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
loaded[specifier] = mod;
|
|
434
|
+
}
|
|
435
|
+
return loaded;
|
|
436
|
+
}
|
|
437
|
+
|
|
366
438
|
// ---------------------------------------------------------------------------
|
|
367
439
|
// Checkpoints / resume — COMP-RESUME
|
|
368
440
|
// ---------------------------------------------------------------------------
|
package/server/compose-mcp.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
27
27
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
28
28
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
29
|
+
import { writeSync } from 'node:fs';
|
|
29
30
|
import {
|
|
30
31
|
toolGetVisionItems,
|
|
31
32
|
toolGetRoadmap,
|
|
@@ -71,6 +72,8 @@ import {
|
|
|
71
72
|
assertToolPhaseAllowed,
|
|
72
73
|
_getSessionProfile,
|
|
73
74
|
resolveBoundPhase,
|
|
75
|
+
preloadEagerModules,
|
|
76
|
+
EAGER_PRELOAD_MODULES,
|
|
74
77
|
} from './compose-mcp-tools.js';
|
|
75
78
|
import { isToolAllowed } from './mcp-tool-policy.js';
|
|
76
79
|
import { switchProject, getTargetRoot, loadProjectConfig } from './project-root.js';
|
|
@@ -817,6 +820,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
817
820
|
// Start
|
|
818
821
|
// ---------------------------------------------------------------------------
|
|
819
822
|
|
|
823
|
+
// COMP-MCP-FOLLOWUP-1-1: eager-preload lazily-imported hot-path modules so a
|
|
824
|
+
// genuine on-disk break (missing module/export) fails fast at boot instead of
|
|
825
|
+
// on first tool call. COMPOSE_PRELOAD_PROBE is an ops/test seam: comma-separated
|
|
826
|
+
// entries appended to the default set, each `specifier` or `specifier#export`
|
|
827
|
+
// (the `#export` form also exercises the export assertion). Absent → default.
|
|
828
|
+
const probeExtra = (process.env.COMPOSE_PRELOAD_PROBE || '')
|
|
829
|
+
.split(',')
|
|
830
|
+
.map((s) => s.trim())
|
|
831
|
+
.filter(Boolean)
|
|
832
|
+
.map((entry) => {
|
|
833
|
+
const hash = entry.indexOf('#');
|
|
834
|
+
return hash === -1
|
|
835
|
+
? { specifier: entry }
|
|
836
|
+
: { specifier: entry.slice(0, hash), expect: entry.slice(hash + 1) };
|
|
837
|
+
});
|
|
838
|
+
try {
|
|
839
|
+
await preloadEagerModules([...EAGER_PRELOAD_MODULES, ...probeExtra]);
|
|
840
|
+
} catch (err) {
|
|
841
|
+
// writeSync (not stderr.write) so the line is guaranteed flushed before
|
|
842
|
+
// process.exit — an async pipe write can otherwise be truncated/dropped.
|
|
843
|
+
writeSync(2, `[compose-mcp] boot aborted: ${err.message}\n`);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
|
|
820
847
|
const transport = new StdioServerTransport();
|
|
821
848
|
await server.connect(transport);
|
|
822
849
|
// Server runs until stdin closes — no explicit exit needed
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feature-scaffold-routes.js — POST /api/features/scaffold.
|
|
3
|
+
*
|
|
4
|
+
* Cockpit equivalent of `compose feature <CODE>`: scaffolds docs/features/<CODE>/
|
|
5
|
+
* (feature.json + seed design.md) and the ROADMAP.md row. Reuses the typed writer
|
|
6
|
+
* addRoadmapEntry (lib/feature-writer.js) — never reimplements scaffolding and
|
|
7
|
+
* never echoes the regenerated roadmap back (the lib return is already compact).
|
|
8
|
+
*
|
|
9
|
+
* Sensitive (mutating). attachFeatureScaffoldRoutes(app, deps?) — deps is for tests.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
|
|
14
|
+
import { addRoadmapEntry as defaultAddRoadmapEntry } from '../lib/feature-writer.js';
|
|
15
|
+
import { isFeatureCode } from '../lib/feature-code.js';
|
|
16
|
+
import { getTargetRoot as defaultGetProjectRoot } from './project-root.js';
|
|
17
|
+
import { resolveFeaturesPathFromConfig, relForDisplay } from '../lib/project-paths.js';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PHASE = 'Backlog';
|
|
20
|
+
|
|
21
|
+
export function attachFeatureScaffoldRoutes(app, deps = {}) {
|
|
22
|
+
const addRoadmapEntry = deps.addRoadmapEntry || defaultAddRoadmapEntry;
|
|
23
|
+
const getProjectRoot = deps.getProjectRoot || defaultGetProjectRoot;
|
|
24
|
+
|
|
25
|
+
app.post('/api/features/scaffold', requireSensitiveToken, async (req, res) => {
|
|
26
|
+
const body = req.body || {};
|
|
27
|
+
const code = typeof body.code === 'string' ? body.code.trim().toUpperCase() : '';
|
|
28
|
+
const description = typeof body.description === 'string' ? body.description.trim() : '';
|
|
29
|
+
const phase = (typeof body.phase === 'string' && body.phase.trim())
|
|
30
|
+
|| (typeof body.group === 'string' && body.group.trim())
|
|
31
|
+
|| DEFAULT_PHASE;
|
|
32
|
+
|
|
33
|
+
if (!isFeatureCode(code)) {
|
|
34
|
+
return res.status(400).json({ error: 'Invalid feature code (e.g. COMP-FOO-1)' });
|
|
35
|
+
}
|
|
36
|
+
if (!description) {
|
|
37
|
+
return res.status(400).json({ error: 'description is required' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const projectRoot = getProjectRoot();
|
|
41
|
+
try {
|
|
42
|
+
// 1. feature.json + ROADMAP.md row (compact return — no roadmap echo).
|
|
43
|
+
const result = await addRoadmapEntry(projectRoot, { code, description, phase });
|
|
44
|
+
|
|
45
|
+
// 2. Seed design.md stub (addRoadmapEntry does not write it). Idempotent.
|
|
46
|
+
// Resolve the ABSOLUTE features dir (may be relocated) — mirror ideabox.
|
|
47
|
+
let featurePath = null;
|
|
48
|
+
try {
|
|
49
|
+
const composeJsonPath = path.join(projectRoot, '.compose', 'compose.json');
|
|
50
|
+
let cfg = {};
|
|
51
|
+
if (fs.existsSync(composeJsonPath)) {
|
|
52
|
+
try { cfg = JSON.parse(fs.readFileSync(composeJsonPath, 'utf-8')); } catch { /* default cfg */ }
|
|
53
|
+
}
|
|
54
|
+
const featuresBase = resolveFeaturesPathFromConfig(projectRoot, cfg);
|
|
55
|
+
const featureDir = path.join(featuresBase, code);
|
|
56
|
+
const designPath = path.join(featureDir, 'design.md');
|
|
57
|
+
if (!fs.existsSync(designPath)) {
|
|
58
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
59
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
60
|
+
fs.writeFileSync(designPath,
|
|
61
|
+
`# ${code}: ${description}\n\n**Status:** PLANNED\n**Created:** ${today}\n\n---\n\n## Intent\n\n${description}\n\n---\n\n## Notes\n\n_Seed design doc created by the New Feature dialog. \`compose build\` will expand it._\n`);
|
|
62
|
+
}
|
|
63
|
+
featurePath = relForDisplay(projectRoot, featureDir);
|
|
64
|
+
} catch { /* design seed is best-effort; feature.json + roadmap already written */ }
|
|
65
|
+
|
|
66
|
+
// COMPACT result — never the full roadmap (memory: MCP add_roadmap_entry echo
|
|
67
|
+
// blows the token cap; the lib return is small, keep it small here too).
|
|
68
|
+
return res.json({
|
|
69
|
+
ok: true,
|
|
70
|
+
code: result.code,
|
|
71
|
+
phase: result.phase,
|
|
72
|
+
position: result.position,
|
|
73
|
+
roadmap_path: result.roadmap_path,
|
|
74
|
+
featurePath,
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const msg = err && err.message ? err.message : String(err);
|
|
78
|
+
const code409 = /already exists/i.test(msg);
|
|
79
|
+
return res.status(code409 ? 409 : 400).json({ error: msg });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-routes.js — Read-only environment-health REST API (COMP-PARITY-3).
|
|
3
|
+
*
|
|
4
|
+
* Routes:
|
|
5
|
+
* GET /api/environment-health — surfaces `compose doctor` (external-dep
|
|
6
|
+
* presence + version drift) and `compose hooks status` (git-hook drift) as
|
|
7
|
+
* structured JSON for the cockpit's header health-dot + popover.
|
|
8
|
+
* POST /api/environment-health/repair-hooks (COMP-PARITY-3-1) — guarded,
|
|
9
|
+
* local, idempotent remediation: runs `compose hooks install --post-commit
|
|
10
|
+
* --pre-push --workspace=<id>` (+ `--force` when body.force) in the request
|
|
11
|
+
* workspace, then returns the recomputed hook states. Mutating → gated by
|
|
12
|
+
* requireSensitiveOrPaired. Dependency installs and `compose update` are
|
|
13
|
+
* deliberately NOT executed server-side (the panel surfaces command text).
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT under `/api/health/*`: that prefix is in the remote auth
|
|
16
|
+
* allowlist (server/index.js + auth-middleware.js prefix-match), so nesting
|
|
17
|
+
* here would make local dependency inventory and `?refresh` registry fetches
|
|
18
|
+
* publicly reachable in remote mode. This path is non-allowlisted → default-deny
|
|
19
|
+
* in remote mode (sensitive token / paired JWT), open on localhost.
|
|
20
|
+
*
|
|
21
|
+
* Degrade-never-fail: each section is computed defensively; a failing sub-check
|
|
22
|
+
* becomes `{ unavailable: true }` (deps/binaries/hooks) or `null` (version).
|
|
23
|
+
* The endpoint never 500s on a sub-check.
|
|
24
|
+
*
|
|
25
|
+
* Reuses (no logic fork):
|
|
26
|
+
* - lib/deps.js — dep/binary presence (off the compose PACKAGE_ROOT)
|
|
27
|
+
* - lib/version-check.js — version drift (off PACKAGE_ROOT + homedir cache)
|
|
28
|
+
* - lib/hooks-status.js — git-hook drift (off the request's workspace root)
|
|
29
|
+
*/
|
|
30
|
+
import { join, dirname, resolve } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { readFileSync } from 'node:fs';
|
|
33
|
+
import { spawnSync } from 'node:child_process';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
loadDeps,
|
|
37
|
+
checkExternalSkills,
|
|
38
|
+
buildDepReport,
|
|
39
|
+
checkExternalBinaries,
|
|
40
|
+
buildBinaryReport,
|
|
41
|
+
} from '../lib/deps.js';
|
|
42
|
+
import { checkLatestVersion } from '../lib/version-check.js';
|
|
43
|
+
import { computeHooksStatus } from '../lib/hooks-status.js';
|
|
44
|
+
import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
|
|
45
|
+
|
|
46
|
+
/** The compose package root (where .compose-deps.json + package.json live). */
|
|
47
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
48
|
+
|
|
49
|
+
function safe(fn, fallback) {
|
|
50
|
+
try {
|
|
51
|
+
return fn();
|
|
52
|
+
} catch {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Map a raw hook status (from computeHooksStatus) to the API-facing shape.
|
|
59
|
+
* The key honesty rule: an installed-current hook whose workspace id could not
|
|
60
|
+
* be verified (expectedWsId was null) is reported as `workspace-unverified`,
|
|
61
|
+
* never a false `installed-current`.
|
|
62
|
+
*/
|
|
63
|
+
export function hookRawToApi(raw) {
|
|
64
|
+
if (!raw || typeof raw !== 'object') return { state: 'unavailable' };
|
|
65
|
+
if (raw.state === 'installed-current') {
|
|
66
|
+
return raw.wsVerified
|
|
67
|
+
? { state: 'installed-current', workspace: raw.bakedWorkspace ?? null }
|
|
68
|
+
: { state: 'workspace-unverified' };
|
|
69
|
+
}
|
|
70
|
+
if (raw.state === 'installed-stale') {
|
|
71
|
+
const out = { state: 'installed-stale', reason: raw.reason ?? null };
|
|
72
|
+
if (raw.reason === 'STALE_WORKSPACE_ID') out.expected = { workspaceId: raw.expectedWorkspace ?? null };
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
return { state: raw.state }; // 'absent' | 'foreign'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Worst-wins summary rollup driving the header dot color.
|
|
80
|
+
* error: any required dep/binary missing OR any hook `foreign`
|
|
81
|
+
* warn: version behind OR hook installed-stale/workspace-unverified OR
|
|
82
|
+
* optional dep/binary missing OR a section is unavailable
|
|
83
|
+
* ok: everything present/current/up-to-date (version null & hook absent are neutral)
|
|
84
|
+
*/
|
|
85
|
+
export function rollupSummary({ dependencies, binaries, version, hooks }) {
|
|
86
|
+
let error = false;
|
|
87
|
+
let warn = false;
|
|
88
|
+
|
|
89
|
+
for (const section of [dependencies, binaries]) {
|
|
90
|
+
if (!section) continue;
|
|
91
|
+
if (section.unavailable) {
|
|
92
|
+
warn = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const d of section.missing || []) {
|
|
96
|
+
if (d.optional) warn = true;
|
|
97
|
+
else error = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (version && version.behind) warn = true;
|
|
102
|
+
|
|
103
|
+
if (hooks && hooks.unavailable) {
|
|
104
|
+
warn = true;
|
|
105
|
+
} else {
|
|
106
|
+
for (const h of Object.values(hooks || {})) {
|
|
107
|
+
if (h?.state === 'foreign') error = true;
|
|
108
|
+
else if (h?.state === 'installed-stale' || h?.state === 'workspace-unverified') warn = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return error ? 'error' : warn ? 'warn' : 'ok';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Default command runner: spawn `node <composeBin> <args...>` synchronously in
|
|
117
|
+
* the given cwd. Injectable via attachHealthRoutes so tests can stub it without
|
|
118
|
+
* shelling out. Returns a normalized `{ status, stdout, stderr, error }` so the
|
|
119
|
+
* route never has to know about spawnSync's shape and never throws on failure.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} opts
|
|
122
|
+
* @param {string} opts.composeNode — node binary to run compose with
|
|
123
|
+
* @param {string} opts.composeBin — path to bin/compose.js
|
|
124
|
+
* @param {string[]} opts.args — compose CLI args (e.g. ['hooks','install',…])
|
|
125
|
+
* @param {string} opts.cwd — working directory (the workspace root)
|
|
126
|
+
* @returns {{status:number|null, stdout:string, stderr:string, error:string|null}}
|
|
127
|
+
*/
|
|
128
|
+
function defaultRunCommand({ composeNode, composeBin, args, cwd }) {
|
|
129
|
+
const r = spawnSync(composeNode, [composeBin, ...args], {
|
|
130
|
+
cwd,
|
|
131
|
+
encoding: 'utf-8',
|
|
132
|
+
timeout: 30000,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
status: r.status,
|
|
136
|
+
stdout: r.stdout || '',
|
|
137
|
+
stderr: r.stderr || '',
|
|
138
|
+
error: r.error ? r.error.message : null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {import('express').Express} app
|
|
144
|
+
* @param {object} [deps]
|
|
145
|
+
* @param {string} [deps.packageRoot] — compose package root (default: resolved from this module)
|
|
146
|
+
* @param {string} [deps.composeBin] — expected COMPOSE_BIN baked in hooks
|
|
147
|
+
* @param {string} [deps.composeNode] — expected COMPOSE_NODE baked in hooks
|
|
148
|
+
* @param {Function} [deps.runCommand] — injectable command runner (default: spawnSync-based)
|
|
149
|
+
*/
|
|
150
|
+
export function attachHealthRoutes(app, {
|
|
151
|
+
packageRoot = PACKAGE_ROOT,
|
|
152
|
+
composeBin = join(PACKAGE_ROOT, 'bin', 'compose.js'),
|
|
153
|
+
composeNode = process.execPath,
|
|
154
|
+
runCommand = defaultRunCommand,
|
|
155
|
+
} = {}) {
|
|
156
|
+
app.get('/api/environment-health', async (req, res) => {
|
|
157
|
+
// --- dependencies + binaries (compose install) ---------------------------
|
|
158
|
+
let dependencies;
|
|
159
|
+
let binaries;
|
|
160
|
+
const deps = safe(() => loadDeps(packageRoot), null);
|
|
161
|
+
if (!deps) {
|
|
162
|
+
// loadDeps returns null (not throws) on a missing/invalid manifest — must
|
|
163
|
+
// branch explicitly; the checkers would throw or mislead on a null deps.
|
|
164
|
+
dependencies = { unavailable: true };
|
|
165
|
+
binaries = { unavailable: true };
|
|
166
|
+
} else {
|
|
167
|
+
dependencies = safe(() => {
|
|
168
|
+
// Drop scannedPaths — it leaks absolute host filesystem paths
|
|
169
|
+
// (e.g. ~/.claude/...) the UI doesn't need; this route is remotely
|
|
170
|
+
// reachable when paired. The contract is {present, missing}.
|
|
171
|
+
const { scannedPaths, ...rest } = buildDepReport(checkExternalSkills(deps));
|
|
172
|
+
return rest;
|
|
173
|
+
}, { unavailable: true });
|
|
174
|
+
binaries = safe(() => buildBinaryReport(checkExternalBinaries(deps)), { unavailable: true });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- version drift (compose install + homedir cache) ---------------------
|
|
178
|
+
let version = null;
|
|
179
|
+
try {
|
|
180
|
+
const current = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8')).version;
|
|
181
|
+
version = await checkLatestVersion(current, { force: req.query.refresh === '1' });
|
|
182
|
+
} catch {
|
|
183
|
+
version = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- git hooks (this request's workspace) --------------------------------
|
|
187
|
+
let hooks;
|
|
188
|
+
try {
|
|
189
|
+
const root = req.workspace?.root;
|
|
190
|
+
if (!root) throw new Error('no workspace root');
|
|
191
|
+
const raw = computeHooksStatus({
|
|
192
|
+
projectRoot: root,
|
|
193
|
+
expectedWsId: req.workspace?.id ?? null,
|
|
194
|
+
composeNode,
|
|
195
|
+
composeBin,
|
|
196
|
+
});
|
|
197
|
+
hooks = {};
|
|
198
|
+
for (const [type, r] of Object.entries(raw)) hooks[type] = hookRawToApi(r);
|
|
199
|
+
} catch {
|
|
200
|
+
hooks = { unavailable: true };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const summary = rollupSummary({ dependencies, binaries, version, hooks });
|
|
204
|
+
res.json({ summary, dependencies, binaries, version, hooks });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// --- POST /api/environment-health/repair-hooks (COMP-PARITY-3-1) ----------
|
|
208
|
+
// Guarded, local, idempotent remediation. Only the hook repair is executed
|
|
209
|
+
// server-side; dependency installs and `compose update` are intentionally NOT
|
|
210
|
+
// run here (the panel surfaces their command text instead).
|
|
211
|
+
app.post('/api/environment-health/repair-hooks', requireSensitiveToken, (req, res) => {
|
|
212
|
+
const root = req.workspace?.root;
|
|
213
|
+
const wsId = req.workspace?.id ?? null;
|
|
214
|
+
if (!root) {
|
|
215
|
+
// No workspace to operate in — a guarded failure, not a 500.
|
|
216
|
+
return res.json({ ok: false, error: 'no workspace root', stderr: '' });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const force = req.body && req.body.force === true;
|
|
220
|
+
const args = ['hooks', 'install', '--post-commit', '--pre-push'];
|
|
221
|
+
if (wsId != null) args.push(`--workspace=${wsId}`);
|
|
222
|
+
if (force) args.push('--force');
|
|
223
|
+
const ranCommand = `compose ${args.join(' ')}`;
|
|
224
|
+
|
|
225
|
+
// Run the repair. The runner normalizes spawnSync's shape and never throws;
|
|
226
|
+
// a non-zero exit / spawn error becomes { ok:false }, never a 500.
|
|
227
|
+
let result;
|
|
228
|
+
try {
|
|
229
|
+
result = runCommand({ composeNode, composeBin, args, cwd: root });
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return res.json({ ok: false, error: e?.message || 'command failed', stderr: '' });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const failed = result.error != null || (result.status != null && result.status !== 0);
|
|
235
|
+
if (failed) {
|
|
236
|
+
return res.json({
|
|
237
|
+
ok: false,
|
|
238
|
+
error: result.error || `compose hooks install exited with status ${result.status}`,
|
|
239
|
+
stderr: result.stderr || '',
|
|
240
|
+
ranCommand,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Recompute hook states so the panel can refresh from the response (or refetch).
|
|
245
|
+
let hooks;
|
|
246
|
+
try {
|
|
247
|
+
const raw = computeHooksStatus({
|
|
248
|
+
projectRoot: root,
|
|
249
|
+
expectedWsId: wsId,
|
|
250
|
+
composeNode,
|
|
251
|
+
composeBin,
|
|
252
|
+
});
|
|
253
|
+
hooks = {};
|
|
254
|
+
for (const [type, r] of Object.entries(raw)) hooks[type] = hookRawToApi(r);
|
|
255
|
+
} catch {
|
|
256
|
+
hooks = { unavailable: true };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
res.json({ ok: true, ranCommand, hooks });
|
|
260
|
+
});
|
|
261
|
+
}
|