@smartmemory/compose 0.1.6-beta → 0.1.8-beta
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/README.md +32 -5
- package/bin/compose.js +353 -60
- package/bin/git-hooks/pre-push.template +26 -0
- package/contracts/feature-json.schema.json +115 -0
- package/contracts/roadmap-row.schema.json +23 -0
- package/contracts/vision-state.schema.json +64 -0
- package/dist/assets/{_baseUniq-D-avYfn5.js → _baseUniq-3jW4HAOf.js} +1 -1
- package/dist/assets/{arc-BC4dfQ-X.js → arc-DzzDimyd.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BZmFXnGI.js → architectureDiagram-Q4EWVU46-CtAgwORz.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DlfWSuux.js → blockDiagram-DXYQGD6D-Bryby0c_.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-Y__uJrRx.js → c4Diagram-AHTNJAMY-C7N9RTJ8.js} +1 -1
- package/dist/assets/channel-DDkv7DUd.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BfMePfTp.js → chunk-4BX2VUAB-wijkFgZY.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-BdlMSdEA.js → chunk-4TB4RGXK-zdSZGRS2.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-vrQHZTdv.js → chunk-55IACEB6-6zqzTZQQ.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-B8wioVlW.js → chunk-EDXVE4YY-frd1Vwf-.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Cd6Hrux2.js → chunk-FMBD7UC4-CdkRK5Hx.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-CfrhdQXY.js → chunk-OYMX7WX6-C6bMB0cf.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B9JQerOU.js → chunk-QZHKN3VN-4vsxN3jq.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-DFN9X99H.js → chunk-YZCP3GAM-DbNARKip.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-J6ZTeCbW.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-J6ZTeCbW.js +1 -0
- package/dist/assets/clone-5MVZ89iV.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BAn0ap_E.js → cose-bilkent-S5V4N54A-BpXeV7Vj.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DyxnVq1g.js → dagre-KV5264BT-DQLu_W8r.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-XCrzqski.js → diagram-5BDNPKRD-skaOoe5A.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-MBCAXft_.js → diagram-G4DWMVQ6-DezlfFH4.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-DbtB2yS6.js → diagram-MMDJMWI5-BUu-v-wT.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Bb5NzX61.js → diagram-TYMM5635-CziQ6LPs.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CpIeCOh2.js → erDiagram-SMLLAGMA-BsAyOVTI.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CHyoKnhW.js → flowDiagram-DWJPFMVM-CbYWJOLq.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-DErKteO_.js → ganttDiagram-T4ZO3ILL-CAwgDkLl.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-KFVAtj2F.js → gitGraphDiagram-UUTBAWPF-DK4RlkjO.js} +1 -1
- package/dist/assets/{graph-CRnO_ifT.js → graph-orv1XHGx.js} +1 -1
- package/dist/assets/{index-DkRKLuNr.js → index-Ceywghsu.js} +143 -143
- package/dist/assets/{infoDiagram-42DDH7IO-BZFnuSp5.js → infoDiagram-42DDH7IO-DQyA75sK.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-4Xe2Szde.js → ishikawaDiagram-UXIWVN3A-C-F_5q4k.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CZRByfS-.js → journeyDiagram-VCZTEJTY-Bj8UIvK-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-B95sk6Fk.js → kanban-definition-6JOO6SKY-DZYr8Dp1.js} +1 -1
- package/dist/assets/{layout-BqNQzxWT.js → layout-CBaTKjpX.js} +1 -1
- package/dist/assets/{linear-CUh7qb64.js → linear-j1sI_SiN.js} +1 -1
- package/dist/assets/{min-wXgOS3ig.js → min-DtJISjld.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-DB6iaAbO.js → mindmap-definition-QFDTVHPH-Bulb64RS.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CHkZHrTW.js → pieDiagram-DEJITSTG-D11keQxr.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DoTEO8e3.js → quadrantDiagram-34T5L4WZ-BEcWQiEG.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-Dn8peXYp.js → requirementDiagram-MS252O5E-Cbp23uDf.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DRXs6Ipb.js → sankeyDiagram-XADWPNL6-Dae1hMc5.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-wBBYZ0aq.js → sequenceDiagram-FGHM5R23-C16abORi.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DPlBNGmf.js → stateDiagram-FHFEXIEX-CbEtfhbx.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-CyY84hEA.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-CbbyTlHk.js → timeline-definition-GMOUNBTQ-BV7JTNMI.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-Bj4GaFfj.js → vennDiagram-DHZGUBPP-DBZiT48j.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-RtNzq8KU.js → wardley-RL74JXVD-Cc8uoiL3.js} +37 -37
- package/dist/assets/{wardleyDiagram-NUSXRM2D-CDfE3zSj.js → wardleyDiagram-NUSXRM2D-DEYcWGo5.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CZXHHYD5.js → xychartDiagram-5P7HB3ND-bFhLXv2b.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build.js +193 -19
- package/lib/completion-writer.js +8 -6
- package/lib/deps.js +17 -6
- package/lib/feature-code.js +29 -0
- package/lib/feature-events.js +3 -0
- package/lib/feature-validator.js +629 -0
- package/lib/feature-writer.js +35 -23
- package/lib/followup-writer.js +556 -0
- package/lib/journal-writer.js +1 -1
- package/lib/mcp-enforcement.js +173 -0
- package/lib/migrate-roadmap.js +4 -1
- package/lib/project-paths.js +36 -0
- package/lib/review-lenses.js +23 -8
- package/lib/review-normalize.js +42 -3
- package/lib/roadmap-drift.js +54 -0
- package/lib/roadmap-gen.js +297 -27
- package/lib/roadmap-preservers.js +353 -0
- package/lib/step-prompt.js +15 -0
- package/lib/triage.js +2 -1
- package/lib/version-check.js +110 -0
- package/package.json +1 -1
- package/server/compose-mcp-tools.js +34 -2
- package/server/compose-mcp.js +52 -1
- package/server/schema-validator.js +50 -9
- package/server/vision-routes.js +51 -2
- package/templates/ROADMAP.md +6 -0
- package/dist/assets/channel-LRG9kHqJ.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +0 -1
- package/dist/assets/clone-dRxgFrBv.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +0 -1
package/README.md
CHANGED
|
@@ -34,9 +34,20 @@ compose build TODO-1
|
|
|
34
34
|
|
|
35
35
|
Prerequisites: Node.js 18+ and `stratum-mcp` on PATH (`pip install stratum`). Codex steps additionally need the OpenAI `codex` CLI. Full prereqs in [docs/install.md](docs/install.md).
|
|
36
36
|
|
|
37
|
+
The package is published to npm as `@smartmemory/compose`. Pick one install style:
|
|
38
|
+
|
|
39
|
+
**Option A — npm (recommended for users):**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g @smartmemory/compose
|
|
43
|
+
compose setup # global skill + stratum-mcp registration
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Option B — git clone (for development):**
|
|
47
|
+
|
|
37
48
|
```bash
|
|
38
49
|
git clone https://github.com/smartmemory/compose.git && cd compose && npm install
|
|
39
|
-
npx compose setup
|
|
50
|
+
npx @smartmemory/compose setup # or: node bin/compose.js setup
|
|
40
51
|
ln -s "$(pwd)/bin/compose.js" ~/bin/compose && chmod +x ~/bin/compose # optional: bare `compose` command
|
|
41
52
|
```
|
|
42
53
|
|
|
@@ -44,15 +55,31 @@ Then in your project:
|
|
|
44
55
|
|
|
45
56
|
```bash
|
|
46
57
|
cd /path/to/your/project
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
compose init # writes .compose/, registers MCP, scaffolds ROADMAP and pipeline specs
|
|
59
|
+
compose new "what you want to build"
|
|
49
60
|
```
|
|
50
61
|
|
|
51
62
|
Add an isolated feature to an existing project:
|
|
52
63
|
|
|
53
64
|
```bash
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
compose feature AUTH-1 "JWT middleware with refresh tokens"
|
|
66
|
+
compose build AUTH-1
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Upgrading
|
|
70
|
+
|
|
71
|
+
One command — auto-detects whether compose was installed via npm or git clone:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
compose update
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For npm installs, this runs `npm install -g @smartmemory/compose@latest`. For git clones, it runs `git pull --ff-only && npm install`. Either way it then refreshes the global skill and (if invoked from inside a Compose project) re-runs `compose init` to refresh `.mcp.json` and pipeline templates. Use `compose update --force` to bypass the dirty-tree check on git clones.
|
|
78
|
+
|
|
79
|
+
Check what you're running:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
compose --version
|
|
56
83
|
```
|
|
57
84
|
|
|
58
85
|
## Documentation
|
package/bin/compose.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* compose fix — headless bug-fix lifecycle runner (pipelines/bug-fix.stratum.yaml)
|
|
11
11
|
*/
|
|
12
12
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'fs'
|
|
13
|
-
import { resolve, join, basename, dirname } from 'path'
|
|
13
|
+
import { resolve, join, basename, dirname, sep } from 'path'
|
|
14
14
|
import { homedir } from 'os'
|
|
15
15
|
import { spawn, spawnSync } from 'child_process'
|
|
16
16
|
import { fileURLToPath } from 'url'
|
|
@@ -23,7 +23,8 @@ const PACKAGE_ROOT = resolve(__dirname, '..')
|
|
|
23
23
|
// --team flag (COMP-TEAMS)
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
import { parseTeamFlag } from '../lib/team-flag.js';
|
|
26
|
-
import { loadDeps, checkExternalSkills, printDepReport } from '../lib/deps.js';
|
|
26
|
+
import { loadDeps, checkExternalSkills, printDepReport, buildDepReport } from '../lib/deps.js';
|
|
27
|
+
import { checkLatestVersion } from '../lib/version-check.js';
|
|
27
28
|
|
|
28
29
|
const [,, cmd, ...args] = process.argv
|
|
29
30
|
|
|
@@ -31,6 +32,20 @@ const [,, cmd, ...args] = process.argv
|
|
|
31
32
|
// Help
|
|
32
33
|
// ---------------------------------------------------------------------------
|
|
33
34
|
|
|
35
|
+
if (cmd === '--version' || cmd === '-V' || cmd === 'version') {
|
|
36
|
+
const pkgPath = join(PACKAGE_ROOT, 'package.json')
|
|
37
|
+
let version = 'unknown'
|
|
38
|
+
try { version = JSON.parse(readFileSync(pkgPath, 'utf-8')).version } catch {}
|
|
39
|
+
console.log(`compose ${version}`)
|
|
40
|
+
const gitDir = join(PACKAGE_ROOT, '.git')
|
|
41
|
+
if (existsSync(gitDir)) {
|
|
42
|
+
const sha = spawnSync('git', ['-C', PACKAGE_ROOT, 'rev-parse', '--short', 'HEAD'], { encoding: 'utf-8' })
|
|
43
|
+
if (sha.status === 0) console.log(` git: ${sha.stdout.trim()}`)
|
|
44
|
+
}
|
|
45
|
+
console.log(` root: ${PACKAGE_ROOT}`)
|
|
46
|
+
process.exit(0)
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
35
50
|
console.log('Usage: compose <command>')
|
|
36
51
|
console.log('')
|
|
@@ -49,7 +64,9 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
|
49
64
|
console.log(' qa-scope Show affected routes from a feature\'s changed files')
|
|
50
65
|
console.log(' init Initialize Compose in the current project')
|
|
51
66
|
console.log(' setup Install global skill + register stratum-mcp')
|
|
67
|
+
console.log(' update Pull latest compose, reinstall deps, refresh global skill')
|
|
52
68
|
console.log(' doctor Check external skill dependencies')
|
|
69
|
+
console.log(' --version Print compose version, git SHA, and install root')
|
|
53
70
|
process.exit(0)
|
|
54
71
|
}
|
|
55
72
|
|
|
@@ -195,10 +212,11 @@ function syncSkills(agents) {
|
|
|
195
212
|
// compose doctor — re-run the external dep check
|
|
196
213
|
// ---------------------------------------------------------------------------
|
|
197
214
|
|
|
198
|
-
function runDoctor(flags = []) {
|
|
215
|
+
async function runDoctor(flags = []) {
|
|
199
216
|
const json = flags.includes('--json')
|
|
200
217
|
const strict = flags.includes('--strict')
|
|
201
218
|
const verbose = flags.includes('--verbose') || flags.includes('-v')
|
|
219
|
+
const refresh = flags.includes('--refresh-versions')
|
|
202
220
|
|
|
203
221
|
const deps = loadDeps(PACKAGE_ROOT)
|
|
204
222
|
if (!deps) {
|
|
@@ -206,8 +224,39 @@ function runDoctor(flags = []) {
|
|
|
206
224
|
process.exit(1)
|
|
207
225
|
}
|
|
208
226
|
const result = checkExternalSkills(deps)
|
|
209
|
-
const allRequiredPresent = printDepReport(result, { json, verbose })
|
|
210
227
|
|
|
228
|
+
// Version drift check — never fails the doctor run.
|
|
229
|
+
const currentVersion = (() => {
|
|
230
|
+
try { return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8')).version }
|
|
231
|
+
catch { return null }
|
|
232
|
+
})()
|
|
233
|
+
const versionInfo = await checkLatestVersion(currentVersion, { force: refresh })
|
|
234
|
+
|
|
235
|
+
if (json) {
|
|
236
|
+
// Single top-level JSON document — the deps report and the version block share one root
|
|
237
|
+
// so consumers like `JSON.parse(stdout)` work. (Previously two concatenated objects.)
|
|
238
|
+
const report = buildDepReport(result)
|
|
239
|
+
console.log(JSON.stringify({ ...report, version: versionInfo }, null, 2))
|
|
240
|
+
const allRequiredPresent = result.missing.every(d => d.optional)
|
|
241
|
+
if (strict && !allRequiredPresent) process.exit(1)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Version section — printed first so it's visible above long dep lists.
|
|
246
|
+
console.log('Version:')
|
|
247
|
+
console.log(` installed: ${currentVersion ?? 'unknown'}`)
|
|
248
|
+
if (versionInfo) {
|
|
249
|
+
console.log(` latest: ${versionInfo.latest} (${versionInfo.source})`)
|
|
250
|
+
if (versionInfo.behind) {
|
|
251
|
+
console.log(` ⚠ behind — run: compose update`)
|
|
252
|
+
} else {
|
|
253
|
+
console.log(` ✓ up to date`)
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
console.log(` latest: unavailable (registry unreachable or cache missing)`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const allRequiredPresent = printDepReport(result, { json: false, verbose })
|
|
211
260
|
if (strict && !allRequiredPresent) process.exit(1)
|
|
212
261
|
}
|
|
213
262
|
|
|
@@ -434,6 +483,114 @@ function runSetup() {
|
|
|
434
483
|
}
|
|
435
484
|
}
|
|
436
485
|
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// compose update — pull latest, reinstall deps, refresh global skill
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
function detectInstallStyle() {
|
|
491
|
+
// npm install resolves bin to either:
|
|
492
|
+
// - global: /<prefix>/lib/node_modules/@smartmemory/compose/bin/compose.js
|
|
493
|
+
// - local: <project>/node_modules/@smartmemory/compose/bin/compose.js
|
|
494
|
+
// - npx cache: ~/.npm/_npx/<hash>/node_modules/@smartmemory/compose/bin/compose.js
|
|
495
|
+
// git clone places it under any path WITHOUT a node_modules ancestor that
|
|
496
|
+
// contains the package itself, but WITH a .git directory at PACKAGE_ROOT.
|
|
497
|
+
if (PACKAGE_ROOT.includes(`${sep}node_modules${sep}`)) {
|
|
498
|
+
return { style: 'npm', root: PACKAGE_ROOT }
|
|
499
|
+
}
|
|
500
|
+
if (existsSync(join(PACKAGE_ROOT, '.git'))) {
|
|
501
|
+
return { style: 'git', root: PACKAGE_ROOT }
|
|
502
|
+
}
|
|
503
|
+
return { style: 'unknown', root: PACKAGE_ROOT }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function getPkgVersion() {
|
|
507
|
+
try {
|
|
508
|
+
return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8')).version
|
|
509
|
+
} catch { return 'unknown' }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function getGitSha(repoPath) {
|
|
513
|
+
const r = spawnSync('git', ['-C', repoPath, 'rev-parse', '--short', 'HEAD'], { encoding: 'utf-8' })
|
|
514
|
+
return r.status === 0 ? r.stdout.trim() : null
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function runUpdate(flags) {
|
|
518
|
+
const force = flags.includes('--force')
|
|
519
|
+
const cwd = process.cwd()
|
|
520
|
+
const { style, root } = detectInstallStyle()
|
|
521
|
+
|
|
522
|
+
console.log(`compose update — install style: ${style}`)
|
|
523
|
+
console.log(` root: ${root}`)
|
|
524
|
+
console.log(` current: v${getPkgVersion()}${style === 'git' ? ` @ ${getGitSha(root) || '?'}` : ''}`)
|
|
525
|
+
console.log('')
|
|
526
|
+
|
|
527
|
+
if (style === 'unknown') {
|
|
528
|
+
console.error('Cannot determine install style. Expected either:')
|
|
529
|
+
console.error(` - npm install: PACKAGE_ROOT inside node_modules`)
|
|
530
|
+
console.error(` - git clone: .git directory at ${root}`)
|
|
531
|
+
console.error('Reinstall with: npm install -g @smartmemory/compose')
|
|
532
|
+
process.exit(1)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (style === 'npm') {
|
|
536
|
+
// Decide global vs local: global if root is under a global prefix.
|
|
537
|
+
const npmPrefix = spawnSync('npm', ['prefix', '-g'], { encoding: 'utf-8' }).stdout.trim()
|
|
538
|
+
const isGlobal = npmPrefix && root.startsWith(npmPrefix)
|
|
539
|
+
const installCmd = isGlobal
|
|
540
|
+
? ['install', '-g', '@smartmemory/compose@latest']
|
|
541
|
+
: ['install', '@smartmemory/compose@latest']
|
|
542
|
+
console.log(`Running: npm ${installCmd.join(' ')}`)
|
|
543
|
+
const r = spawnSync('npm', installCmd, { stdio: 'inherit' })
|
|
544
|
+
if (r.status !== 0) {
|
|
545
|
+
console.error('npm install failed')
|
|
546
|
+
process.exit(r.status || 1)
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
// git clone — check clean, fast-forward pull, npm install
|
|
550
|
+
const status = spawnSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf-8' })
|
|
551
|
+
if (status.stdout.trim() && !force) {
|
|
552
|
+
console.error(`Working tree at ${root} has uncommitted changes.`)
|
|
553
|
+
console.error('Commit/stash them, or re-run with --force to skip the check.')
|
|
554
|
+
process.exit(1)
|
|
555
|
+
}
|
|
556
|
+
const beforeSha = getGitSha(root)
|
|
557
|
+
console.log(`Running: git fetch && git pull --ff-only`)
|
|
558
|
+
const fetch = spawnSync('git', ['-C', root, 'fetch'], { stdio: 'inherit' })
|
|
559
|
+
if (fetch.status !== 0) { process.exit(fetch.status || 1) }
|
|
560
|
+
const pull = spawnSync('git', ['-C', root, 'pull', '--ff-only'], { stdio: 'inherit' })
|
|
561
|
+
if (pull.status !== 0) {
|
|
562
|
+
console.error('git pull --ff-only failed (likely diverged from remote).')
|
|
563
|
+
console.error(`Reconcile manually in ${root}, then re-run compose update.`)
|
|
564
|
+
process.exit(pull.status || 1)
|
|
565
|
+
}
|
|
566
|
+
const afterSha = getGitSha(root)
|
|
567
|
+
if (beforeSha === afterSha) {
|
|
568
|
+
console.log(`Already up to date at ${afterSha}.`)
|
|
569
|
+
} else {
|
|
570
|
+
console.log(`Updated ${beforeSha} → ${afterSha}`)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
console.log('Running: npm install')
|
|
574
|
+
const ni = spawnSync('npm', ['install'], { cwd: root, stdio: 'inherit' })
|
|
575
|
+
if (ni.status !== 0) { process.exit(ni.status || 1) }
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Refresh global skill + stratum-mcp registration
|
|
579
|
+
console.log('')
|
|
580
|
+
console.log('Refreshing global skill installation...')
|
|
581
|
+
runSetup()
|
|
582
|
+
|
|
583
|
+
// If invoked from inside a Compose project, refresh project artifacts too
|
|
584
|
+
if (existsSync(join(cwd, '.compose', 'compose.json'))) {
|
|
585
|
+
console.log('')
|
|
586
|
+
console.log(`Refreshing project at ${cwd}...`)
|
|
587
|
+
await runInit([])
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
console.log('')
|
|
591
|
+
console.log(`compose updated to v${getPkgVersion()}${style === 'git' ? ` @ ${getGitSha(root) || '?'}` : ''}`)
|
|
592
|
+
}
|
|
593
|
+
|
|
437
594
|
// ---------------------------------------------------------------------------
|
|
438
595
|
// Command dispatch
|
|
439
596
|
// ---------------------------------------------------------------------------
|
|
@@ -449,7 +606,12 @@ if (cmd === 'setup') {
|
|
|
449
606
|
}
|
|
450
607
|
|
|
451
608
|
if (cmd === 'doctor') {
|
|
452
|
-
runDoctor(args)
|
|
609
|
+
await runDoctor(args)
|
|
610
|
+
process.exit(0)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (cmd === 'update' || cmd === 'upgrade') {
|
|
614
|
+
await runUpdate(args)
|
|
453
615
|
process.exit(0)
|
|
454
616
|
}
|
|
455
617
|
|
|
@@ -1180,92 +1342,129 @@ if (cmd === 'hooks') {
|
|
|
1180
1342
|
const hooksDir = pjoin(gitDir, 'hooks')
|
|
1181
1343
|
const { mkdirSync: mSync } = await import('fs')
|
|
1182
1344
|
mSync(hooksDir, { recursive: true })
|
|
1183
|
-
const hookPath = pjoin(hooksDir, 'post-commit')
|
|
1184
|
-
|
|
1185
|
-
// Marker line used to identify our hooks
|
|
1186
|
-
const HOOK_MARKER = '# Compose post-commit hook —'
|
|
1187
1345
|
|
|
1188
1346
|
// Resolve absolute paths for substitution
|
|
1189
1347
|
const composeBin = presolve(presolve(futp(import.meta.url), '..'), 'compose.js')
|
|
1190
1348
|
const composeNode = process.execPath
|
|
1191
1349
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1350
|
+
// Hook-type table. Each entry knows its template, marker, and destination.
|
|
1351
|
+
const HOOK_TYPES = {
|
|
1352
|
+
'post-commit': {
|
|
1353
|
+
template: pjoin(presolve(futp(import.meta.url), '..'), 'git-hooks', 'post-commit.template'),
|
|
1354
|
+
marker: '# Compose post-commit hook —',
|
|
1355
|
+
dest: pjoin(hooksDir, 'post-commit'),
|
|
1356
|
+
},
|
|
1357
|
+
'pre-push': {
|
|
1358
|
+
template: pjoin(presolve(futp(import.meta.url), '..'), 'git-hooks', 'pre-push.template'),
|
|
1359
|
+
marker: '# Compose pre-push hook —',
|
|
1360
|
+
dest: pjoin(hooksDir, 'pre-push'),
|
|
1361
|
+
},
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Determine which hook types this invocation operates on.
|
|
1365
|
+
// Flags: --pre-push, --post-commit, or none (default = post-commit, back-compat).
|
|
1366
|
+
const selectedTypes = []
|
|
1367
|
+
if (hookFlags['pre-push']) selectedTypes.push('pre-push')
|
|
1368
|
+
if (hookFlags['post-commit']) selectedTypes.push('post-commit')
|
|
1369
|
+
if (selectedTypes.length === 0) selectedTypes.push('post-commit') // default = back-compat
|
|
1370
|
+
|
|
1371
|
+
function installOne(type) {
|
|
1372
|
+
const { template: tplPath, marker, dest } = HOOK_TYPES[type]
|
|
1195
1373
|
let template
|
|
1196
1374
|
try {
|
|
1197
|
-
template = rfSync(
|
|
1375
|
+
template = rfSync(tplPath, 'utf-8')
|
|
1198
1376
|
} catch (err) {
|
|
1199
|
-
console.error(`Error: could not read
|
|
1200
|
-
|
|
1377
|
+
console.error(`Error: could not read ${type} template: ${err.message}`)
|
|
1378
|
+
return 1
|
|
1201
1379
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const existing = rfSync(hookPath, 'utf-8')
|
|
1206
|
-
const isOurs = existing.includes(HOOK_MARKER)
|
|
1380
|
+
if (exSync(dest)) {
|
|
1381
|
+
const existing = rfSync(dest, 'utf-8')
|
|
1382
|
+
const isOurs = existing.includes(marker)
|
|
1207
1383
|
if (!isOurs && !hookFlags.force) {
|
|
1208
|
-
console.error(
|
|
1384
|
+
console.error(`Error: a foreign ${type} hook already exists at ${dest}`)
|
|
1209
1385
|
console.error('')
|
|
1210
|
-
console.error(
|
|
1211
|
-
|
|
1212
|
-
console.error(` "${composeNode}" "${composeBin}" record-completion ...`)
|
|
1213
|
-
console.error('')
|
|
1214
|
-
console.error('Or run `compose hooks install --force` to overwrite.')
|
|
1215
|
-
process.exit(1)
|
|
1386
|
+
console.error(`Run \`compose hooks install --${type} --force\` to overwrite.`)
|
|
1387
|
+
return 1
|
|
1216
1388
|
}
|
|
1217
1389
|
}
|
|
1218
|
-
|
|
1219
|
-
// Substitute placeholders
|
|
1220
1390
|
const substituted = template
|
|
1221
1391
|
.replace(/__COMPOSE_NODE__/g, composeNode)
|
|
1222
1392
|
.replace(/__COMPOSE_BIN__/g, composeBin)
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
console.log(`Installed post-commit hook at ${hookPath}`)
|
|
1393
|
+
wfSync(dest, substituted)
|
|
1394
|
+
chmodSync(dest, 0o755)
|
|
1395
|
+
console.log(`Installed ${type} hook at ${dest}`)
|
|
1227
1396
|
console.log(` COMPOSE_NODE=${composeNode}`)
|
|
1228
1397
|
console.log(` COMPOSE_BIN=${composeBin}`)
|
|
1229
|
-
|
|
1398
|
+
return 0
|
|
1230
1399
|
}
|
|
1231
1400
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1401
|
+
function uninstallOne(type) {
|
|
1402
|
+
const { marker, dest } = HOOK_TYPES[type]
|
|
1403
|
+
if (!exSync(dest)) {
|
|
1404
|
+
console.log(`No ${type} hook installed.`)
|
|
1405
|
+
return 0
|
|
1236
1406
|
}
|
|
1237
|
-
const content = rfSync(
|
|
1238
|
-
if (!content.includes(
|
|
1239
|
-
console.warn(
|
|
1240
|
-
|
|
1407
|
+
const content = rfSync(dest, 'utf-8')
|
|
1408
|
+
if (!content.includes(marker)) {
|
|
1409
|
+
console.warn(`Warning: ${type} hook exists but does not appear to be a Compose hook (marker not found). Leaving alone.`)
|
|
1410
|
+
return 0
|
|
1241
1411
|
}
|
|
1242
|
-
const { rmSync: rmS } =
|
|
1243
|
-
rmS(
|
|
1244
|
-
console.log(`Removed
|
|
1245
|
-
|
|
1412
|
+
const { rmSync: rmS } = require('fs')
|
|
1413
|
+
rmS(dest)
|
|
1414
|
+
console.log(`Removed ${type} hook at ${dest}`)
|
|
1415
|
+
return 0
|
|
1246
1416
|
}
|
|
1247
1417
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
if (!exSync(
|
|
1251
|
-
console.log(
|
|
1252
|
-
|
|
1418
|
+
function statusOne(type) {
|
|
1419
|
+
const { marker, dest } = HOOK_TYPES[type]
|
|
1420
|
+
if (!exSync(dest)) {
|
|
1421
|
+
console.log(`${type}: absent — no hook installed`)
|
|
1422
|
+
return
|
|
1253
1423
|
}
|
|
1254
|
-
const content = rfSync(
|
|
1255
|
-
if (!content.includes(
|
|
1256
|
-
console.log(
|
|
1257
|
-
|
|
1424
|
+
const content = rfSync(dest, 'utf-8')
|
|
1425
|
+
if (!content.includes(marker)) {
|
|
1426
|
+
console.log(`${type}: foreign — hook exists but is not a Compose hook`)
|
|
1427
|
+
return
|
|
1258
1428
|
}
|
|
1259
|
-
|
|
1260
|
-
const
|
|
1261
|
-
const binMatch = content.includes(`COMPOSE_BIN="${composeBin}"`)
|
|
1429
|
+
const nodeMatch = content.includes(`COMPOSE_NODE="${composeNode}"`)
|
|
1430
|
+
const binMatch = content.includes(`COMPOSE_BIN="${composeBin}"`)
|
|
1262
1431
|
if (nodeMatch && binMatch) {
|
|
1263
|
-
console.log(
|
|
1432
|
+
console.log(`${type}: installed (current)`)
|
|
1264
1433
|
} else {
|
|
1265
|
-
console.log(
|
|
1434
|
+
console.log(`${type}: installed (stale paths — re-run install)`)
|
|
1266
1435
|
if (!nodeMatch) console.log(` expected COMPOSE_NODE="${composeNode}"`)
|
|
1267
1436
|
if (!binMatch) console.log(` expected COMPOSE_BIN="${composeBin}"`)
|
|
1268
1437
|
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (sub === 'install') {
|
|
1441
|
+
let exitCode = 0
|
|
1442
|
+
for (const t of selectedTypes) exitCode = installOne(t) || exitCode
|
|
1443
|
+
process.exit(exitCode)
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (sub === 'uninstall') {
|
|
1447
|
+
const { rmSync: _rmS } = await import('fs') // ensure fs.rmSync is available
|
|
1448
|
+
// uninstallOne calls require('fs') but we're ESM — replace with import-based deletion
|
|
1449
|
+
for (const t of selectedTypes) {
|
|
1450
|
+
const { marker, dest } = HOOK_TYPES[t]
|
|
1451
|
+
if (!exSync(dest)) { console.log(`No ${t} hook installed.`); continue }
|
|
1452
|
+
const content = rfSync(dest, 'utf-8')
|
|
1453
|
+
if (!content.includes(marker)) {
|
|
1454
|
+
console.warn(`Warning: ${t} hook exists but does not appear to be a Compose hook (marker not found). Leaving alone.`)
|
|
1455
|
+
continue
|
|
1456
|
+
}
|
|
1457
|
+
_rmS(dest)
|
|
1458
|
+
console.log(`Removed ${t} hook at ${dest}`)
|
|
1459
|
+
}
|
|
1460
|
+
process.exit(0)
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// status (default)
|
|
1464
|
+
if (!sub || sub === 'status') {
|
|
1465
|
+
// Status reports on ALL known hook types (selection flags ignored), so users
|
|
1466
|
+
// see the full picture. Selection only affects install/uninstall.
|
|
1467
|
+
for (const t of Object.keys(HOOK_TYPES)) statusOne(t)
|
|
1269
1468
|
process.exit(0)
|
|
1270
1469
|
}
|
|
1271
1470
|
|
|
@@ -1273,6 +1472,100 @@ if (cmd === 'hooks') {
|
|
|
1273
1472
|
process.exit(1)
|
|
1274
1473
|
}
|
|
1275
1474
|
|
|
1475
|
+
if (cmd === 'validate') {
|
|
1476
|
+
// compose validate [--scope=feature|project] [--code=CODE] [--block-on=error|warning|info] [--json]
|
|
1477
|
+
let scope = 'project'
|
|
1478
|
+
let code = null
|
|
1479
|
+
let blockOn = 'error'
|
|
1480
|
+
let asJson = false
|
|
1481
|
+
for (let i = 0; i < args.length; i++) {
|
|
1482
|
+
const a = args[i]
|
|
1483
|
+
if (a === '--help' || a === '-h') {
|
|
1484
|
+
console.log(`Usage: compose validate [options]
|
|
1485
|
+
|
|
1486
|
+
Options:
|
|
1487
|
+
--scope=feature|project Scope (default: project)
|
|
1488
|
+
--code=CODE Feature code (required when scope=feature)
|
|
1489
|
+
--block-on=LEVEL Exit non-zero if any finding >= LEVEL (default: error)
|
|
1490
|
+
LEVEL: error | warning | info
|
|
1491
|
+
--json Emit findings as JSON (default: human-readable)
|
|
1492
|
+
|
|
1493
|
+
Exit codes:
|
|
1494
|
+
0 no findings >= block-on threshold
|
|
1495
|
+
1 findings >= block-on threshold present
|
|
1496
|
+
2 usage error`)
|
|
1497
|
+
process.exit(0)
|
|
1498
|
+
}
|
|
1499
|
+
if (a === '--json') { asJson = true; continue }
|
|
1500
|
+
if (a.startsWith('--scope=')) scope = a.slice('--scope='.length)
|
|
1501
|
+
else if (a === '--scope') scope = args[++i]
|
|
1502
|
+
else if (a.startsWith('--code=')) code = a.slice('--code='.length)
|
|
1503
|
+
else if (a === '--code') code = args[++i]
|
|
1504
|
+
else if (a.startsWith('--block-on=')) blockOn = a.slice('--block-on='.length)
|
|
1505
|
+
else if (a === '--block-on') blockOn = args[++i]
|
|
1506
|
+
else if (a.startsWith('--')) {
|
|
1507
|
+
console.error(`Unknown flag: ${a}`)
|
|
1508
|
+
process.exit(2)
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (!['feature', 'project'].includes(scope)) {
|
|
1512
|
+
console.error(`Invalid --scope=${scope}; expected feature or project`)
|
|
1513
|
+
process.exit(2)
|
|
1514
|
+
}
|
|
1515
|
+
if (scope === 'feature' && !code) {
|
|
1516
|
+
console.error(`--scope=feature requires --code=<CODE>`)
|
|
1517
|
+
process.exit(2)
|
|
1518
|
+
}
|
|
1519
|
+
if (!['error', 'warning', 'info'].includes(blockOn)) {
|
|
1520
|
+
console.error(`Invalid --block-on=${blockOn}; expected error, warning, or info`)
|
|
1521
|
+
process.exit(2)
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
const { validateFeature, validateProject } = await import('../lib/feature-validator.js')
|
|
1525
|
+
let result
|
|
1526
|
+
try {
|
|
1527
|
+
result = scope === 'feature'
|
|
1528
|
+
? await validateFeature(process.cwd(), code)
|
|
1529
|
+
: await validateProject(process.cwd())
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
if (err.code === 'INVALID_INPUT') {
|
|
1532
|
+
console.error(`Error [INVALID_INPUT]: ${err.message}`)
|
|
1533
|
+
process.exit(2)
|
|
1534
|
+
}
|
|
1535
|
+
console.error(`Error: ${err.message}`)
|
|
1536
|
+
process.exit(2)
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Threshold: findings at or above this severity block the exit code
|
|
1540
|
+
const SEV_RANK = { error: 3, warning: 2, info: 1 }
|
|
1541
|
+
const threshold = SEV_RANK[blockOn]
|
|
1542
|
+
const blocking = result.findings.filter((f) => SEV_RANK[f.severity] >= threshold)
|
|
1543
|
+
|
|
1544
|
+
if (asJson) {
|
|
1545
|
+
console.log(JSON.stringify(result, null, 2))
|
|
1546
|
+
} else {
|
|
1547
|
+
const byKind = {}
|
|
1548
|
+
for (const f of result.findings) {
|
|
1549
|
+
const sev = f.severity.toUpperCase()
|
|
1550
|
+
const tag = `[${sev}] ${f.kind}${f.feature_code ? ' ' + f.feature_code : ''}`
|
|
1551
|
+
if (!byKind[tag]) byKind[tag] = []
|
|
1552
|
+
byKind[tag].push(f.detail)
|
|
1553
|
+
}
|
|
1554
|
+
if (result.findings.length === 0) {
|
|
1555
|
+
console.log(`compose validate: no findings (scope=${scope}${code ? ' code=' + code : ''})`)
|
|
1556
|
+
} else {
|
|
1557
|
+
console.log(`compose validate findings (scope=${scope}${code ? ' code=' + code : ''}):`)
|
|
1558
|
+
for (const tag of Object.keys(byKind).sort()) {
|
|
1559
|
+
console.log(` ${tag}`)
|
|
1560
|
+
for (const detail of byKind[tag]) console.log(` - ${detail}`)
|
|
1561
|
+
}
|
|
1562
|
+
console.log(`\n${result.findings.length} finding(s); ${blocking.length} at or above --block-on=${blockOn}`)
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
process.exit(blocking.length > 0 ? 1 : 0)
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1276
1569
|
if (cmd === 'pipeline') {
|
|
1277
1570
|
const { runPipelineCli } = await import('../lib/pipeline-cli.js')
|
|
1278
1571
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Compose pre-push hook — runs `compose validate` and blocks the push on
|
|
3
|
+
# any error-severity drift finding. Installed by `compose hooks install --pre-push`;
|
|
4
|
+
# placeholders below are substituted at install time.
|
|
5
|
+
|
|
6
|
+
set -u
|
|
7
|
+
COMPOSE_NODE="__COMPOSE_NODE__"
|
|
8
|
+
COMPOSE_BIN="__COMPOSE_BIN__"
|
|
9
|
+
LOG="${COMPOSE_HOOK_LOG:-.compose/data/pre-push.log}"
|
|
10
|
+
mkdir -p "$(dirname "$LOG")" 2>/dev/null || true
|
|
11
|
+
|
|
12
|
+
OUTPUT=$("$COMPOSE_NODE" "$COMPOSE_BIN" validate --scope=project --block-on=error 2>&1)
|
|
13
|
+
EXIT_CODE=$?
|
|
14
|
+
|
|
15
|
+
if [ "$EXIT_CODE" -ne 0 ]; then
|
|
16
|
+
echo "$OUTPUT" | tee -a "$LOG" >&2
|
|
17
|
+
echo "" >&2
|
|
18
|
+
echo "compose validate found error-severity drift. Push aborted." >&2
|
|
19
|
+
echo "Run \`compose validate\` to see findings, then fix and retry." >&2
|
|
20
|
+
echo "Bypass at your own risk: git push --no-verify" >&2
|
|
21
|
+
exit "$EXIT_CODE"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Below-threshold findings (warnings/info) are still printed for visibility.
|
|
25
|
+
echo "$OUTPUT"
|
|
26
|
+
exit 0
|