@smartmemory/compose 0.3.8 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/compose/SKILL.md +12 -3
- package/.compose-deps.json +50 -12
- package/README.md +7 -2
- package/bin/compose.js +25 -9
- package/lib/deps.js +164 -7
- package/package.json +1 -1
|
@@ -537,7 +537,7 @@ When `/compose` is invoked, always scan first:
|
|
|
537
537
|
| `refactor` | Phase 7 — when review finds large files |
|
|
538
538
|
| `update-docs` | Phase 9 |
|
|
539
539
|
|
|
540
|
-
**Note on dependencies:** these skills are referenced by name and must be installed on the user's machine. See `## Dependencies` below for the install contract — `compose setup`
|
|
540
|
+
**Note on dependencies:** these skills are referenced by name and must be installed on the user's machine. See `## Dependencies` below for the install contract — `compose setup` ships compose-owned skills and auto-installs the plugins behind missing REQUIRED external deps (COMP-DEPS-AUTOINSTALL); optional deps remain documented hints.
|
|
541
541
|
|
|
542
542
|
## Memory
|
|
543
543
|
|
|
@@ -559,11 +559,20 @@ After completing a feature OR bug-fix lifecycle, update project memory:
|
|
|
559
559
|
| `stratum` | `compose/skills/stratum/` (if present) | Execution substrate |
|
|
560
560
|
| `compose-explorer`, `compose-architect` | agents under `compose/.claude/agents/` | Required by Phases 1, 3, 4 |
|
|
561
561
|
|
|
562
|
-
### External dependencies
|
|
562
|
+
### External dependencies
|
|
563
563
|
|
|
564
564
|
Authoritative list lives in `compose/.compose-deps.json`. Run `compose doctor` to see what's installed locally and `compose doctor --json` for machine-readable output. The manifest is the single source of truth for external dep IDs and per-dep `fallback` behavior — this SKILL.md never duplicates per-dep fallback strings.
|
|
565
565
|
|
|
566
|
-
The manifest declares 12 external skills/commands across `superpowers:*`, `interface-design:*`, `codex:review`, `refactor`, and `update-docs`. Each entry carries `id`, `required_for`, `install`, `fallback` (or null), and `optional
|
|
566
|
+
The manifest declares 12 external skills/commands across `superpowers:*`, `interface-design:*`, `codex:review`, `refactor`, and `update-docs`. Each entry carries `id`, `required_for`, `install`, `fallback` (or null), and `optional`, plus two optional fields used by auto-install: `plugin` (the `<plugin>@<marketplace>` spec passed to `claude plugin install`) and `marketplace_source` (the `owner/repo` registered first when that marketplace is not configured yet).
|
|
567
|
+
|
|
568
|
+
**Auto-install (COMP-DEPS-AUTOINSTALL).** `compose setup` / `init` / `update` install the plugins behind missing **required** deps before printing the dep report, so the report describes the state you end up in rather than the state you started from. Rules:
|
|
569
|
+
|
|
570
|
+
- Only deps with a `plugin` spec are installable. Deps whose `install` is prose (`refactor`, `update-docs`) stay hints.
|
|
571
|
+
- **Optional deps are never auto-installed** — the ones we ship live in third-party marketplaces that each need a separate `marketplace add`, a larger consent step than pulling from a marketplace the user already has.
|
|
572
|
+
- Specs are deduped: six `superpowers:*` deps produce one install.
|
|
573
|
+
- On a machine with no marketplaces registered the first install fails; compose registers the declared `marketplace_source` and retries once.
|
|
574
|
+
- Opt out with `--no-install-deps` or `COMPOSE_NO_PLUGIN_INSTALL=1`. Failures print the real stderr and never change the exit code.
|
|
575
|
+
- `compose doctor` stays read-only; it reports, it never installs.
|
|
567
576
|
|
|
568
577
|
The manifest also has an optional `external_binaries` array for CLI tools (not skills). Today it declares `rtk` ([Rust Token Killer](https://github.com/rtk-ai/rtk), COMP-RTK-INTEROP) — a lossy output compressor compose routes its one LLM-bound `git diff` (the Codex review diff) through when present, degrading byte-identically when absent. `compose doctor` reports binaries alongside skills and surfaces each binary's `recommend` (for rtk: `rtk init -g`, which installs RTK's Claude Code hook so the agent's *own* Bash output is compressed too — the larger token win). Binary entries carry `id`, `detect`, `install`, `recommend` (or null), and `optional`; all are optional and never block the lifecycle.
|
|
569
578
|
|
package/.compose-deps.json
CHANGED
|
@@ -3,84 +3,122 @@
|
|
|
3
3
|
"external_skills": [
|
|
4
4
|
{
|
|
5
5
|
"id": "superpowers:systematic-debugging",
|
|
6
|
-
"required_for": [
|
|
6
|
+
"required_for": [
|
|
7
|
+
"bug-fix Phase F3",
|
|
8
|
+
"any unexpected failure"
|
|
9
|
+
],
|
|
7
10
|
"install": "claude plugin install superpowers",
|
|
11
|
+
"plugin": "superpowers@claude-plugins-official",
|
|
12
|
+
"marketplace_source": "anthropics/claude-plugins-official",
|
|
8
13
|
"fallback": "general-purpose Agent with debugging prompt",
|
|
9
14
|
"optional": false
|
|
10
15
|
},
|
|
11
16
|
{
|
|
12
17
|
"id": "superpowers:test-driven-development",
|
|
13
|
-
"required_for": [
|
|
18
|
+
"required_for": [
|
|
19
|
+
"bug-fix Phase F2",
|
|
20
|
+
"build Phase 7 step 1"
|
|
21
|
+
],
|
|
14
22
|
"install": "claude plugin install superpowers",
|
|
23
|
+
"plugin": "superpowers@claude-plugins-official",
|
|
24
|
+
"marketplace_source": "anthropics/claude-plugins-official",
|
|
15
25
|
"fallback": "inline TDD instructions in SKILL.md",
|
|
16
26
|
"optional": false
|
|
17
27
|
},
|
|
18
28
|
{
|
|
19
29
|
"id": "superpowers:verification-before-completion",
|
|
20
|
-
"required_for": [
|
|
30
|
+
"required_for": [
|
|
31
|
+
"all phase exits"
|
|
32
|
+
],
|
|
21
33
|
"install": "claude plugin install superpowers",
|
|
34
|
+
"plugin": "superpowers@claude-plugins-official",
|
|
35
|
+
"marketplace_source": "anthropics/claude-plugins-official",
|
|
22
36
|
"fallback": "inline verification checklist in SKILL.md",
|
|
23
37
|
"optional": false
|
|
24
38
|
},
|
|
25
39
|
{
|
|
26
40
|
"id": "superpowers:requesting-code-review",
|
|
27
|
-
"required_for": [
|
|
41
|
+
"required_for": [
|
|
42
|
+
"Phase 7 review fallback"
|
|
43
|
+
],
|
|
28
44
|
"install": "claude plugin install superpowers",
|
|
45
|
+
"plugin": "superpowers@claude-plugins-official",
|
|
46
|
+
"marketplace_source": "anthropics/claude-plugins-official",
|
|
29
47
|
"fallback": "general-purpose Agent reviewer with canonical ReviewResult prompt",
|
|
30
48
|
"optional": true
|
|
31
49
|
},
|
|
32
50
|
{
|
|
33
51
|
"id": "superpowers:executing-plans",
|
|
34
|
-
"required_for": [
|
|
52
|
+
"required_for": [
|
|
53
|
+
"Phase 7 sequential execution"
|
|
54
|
+
],
|
|
35
55
|
"install": "claude plugin install superpowers",
|
|
56
|
+
"plugin": "superpowers@claude-plugins-official",
|
|
57
|
+
"marketplace_source": "anthropics/claude-plugins-official",
|
|
36
58
|
"fallback": "inline plan-execution instructions",
|
|
37
59
|
"optional": false
|
|
38
60
|
},
|
|
39
61
|
{
|
|
40
62
|
"id": "superpowers:dispatching-parallel-agents",
|
|
41
|
-
"required_for": [
|
|
63
|
+
"required_for": [
|
|
64
|
+
"Phase 7 parallel execution"
|
|
65
|
+
],
|
|
42
66
|
"install": "claude plugin install superpowers",
|
|
67
|
+
"plugin": "superpowers@claude-plugins-official",
|
|
68
|
+
"marketplace_source": "anthropics/claude-plugins-official",
|
|
43
69
|
"fallback": "sequential execution",
|
|
44
70
|
"optional": true
|
|
45
71
|
},
|
|
46
72
|
{
|
|
47
73
|
"id": "interface-design:init",
|
|
48
|
-
"required_for": [
|
|
74
|
+
"required_for": [
|
|
75
|
+
"Phase 7 \u2014 new UI components"
|
|
76
|
+
],
|
|
49
77
|
"install": "claude plugin install interface-design",
|
|
50
78
|
"fallback": null,
|
|
51
79
|
"optional": true
|
|
52
80
|
},
|
|
53
81
|
{
|
|
54
82
|
"id": "interface-design:critique",
|
|
55
|
-
"required_for": [
|
|
83
|
+
"required_for": [
|
|
84
|
+
"Phase 7 \u2014 UI critique pass"
|
|
85
|
+
],
|
|
56
86
|
"install": "claude plugin install interface-design",
|
|
57
87
|
"fallback": null,
|
|
58
88
|
"optional": true
|
|
59
89
|
},
|
|
60
90
|
{
|
|
61
91
|
"id": "interface-design:audit",
|
|
62
|
-
"required_for": [
|
|
92
|
+
"required_for": [
|
|
93
|
+
"Phase 7 \u2014 UI design-system audit"
|
|
94
|
+
],
|
|
63
95
|
"install": "claude plugin install interface-design",
|
|
64
96
|
"fallback": null,
|
|
65
97
|
"optional": true
|
|
66
98
|
},
|
|
67
99
|
{
|
|
68
100
|
"id": "codex:review",
|
|
69
|
-
"required_for": [
|
|
101
|
+
"required_for": [
|
|
102
|
+
"Phase 7 review gate (human-driven)"
|
|
103
|
+
],
|
|
70
104
|
"install": "claude plugin install openai-codex",
|
|
71
105
|
"fallback": "mcp__stratum__stratum_agent_run type=codex",
|
|
72
106
|
"optional": true
|
|
73
107
|
},
|
|
74
108
|
{
|
|
75
109
|
"id": "refactor",
|
|
76
|
-
"required_for": [
|
|
110
|
+
"required_for": [
|
|
111
|
+
"Phase 7 large-file split"
|
|
112
|
+
],
|
|
77
113
|
"install": "user-installed skill at ~/.claude/skills/refactor/",
|
|
78
114
|
"fallback": null,
|
|
79
115
|
"optional": true
|
|
80
116
|
},
|
|
81
117
|
{
|
|
82
118
|
"id": "update-docs",
|
|
83
|
-
"required_for": [
|
|
119
|
+
"required_for": [
|
|
120
|
+
"Phase 9 docs sync"
|
|
121
|
+
],
|
|
84
122
|
"install": "user-installed skill at ~/.claude/skills/update-docs/",
|
|
85
123
|
"fallback": "manual TODO surfaced in implementation report",
|
|
86
124
|
"optional": true
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ compose plan "a tool that summarizes my team's standups"
|
|
|
76
76
|
|
|
77
77
|
## Quick install
|
|
78
78
|
|
|
79
|
-
Prerequisites: Node.js 18
|
|
79
|
+
Prerequisites: Node.js 18+. [Stratum](https://github.com/smartmemory/stratum) needs no separate install — `@smartmemory/stratum` is a dependency, and `compose init` registers the installed copy's MCP entrypoint automatically (a sibling `stratum/` checkout is a development convenience, not a requirement; the python `stratum-mcp` PyPI package is retired). Codex steps additionally need the OpenAI `codex` CLI. Full prereqs in [docs/install.md](docs/install.md).
|
|
80
80
|
|
|
81
81
|
The package is published to npm as `@smartmemory/compose`. Pick one install style:
|
|
82
82
|
|
|
@@ -84,7 +84,7 @@ The package is published to npm as `@smartmemory/compose`. Pick one install styl
|
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
npm install -g @smartmemory/compose
|
|
87
|
-
compose setup # install bundled skills
|
|
87
|
+
compose setup # install bundled skills, auto-install missing required plugins, register the Stratum MCP server (alias: compose sync)
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
**Option B — git clone (for development):**
|
|
@@ -118,6 +118,11 @@ One command — auto-detects whether compose was installed via npm or git clone:
|
|
|
118
118
|
compose update
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
Versions across the three published packages move together: `@smartmemory/compose-mcp` carries
|
|
122
|
+
compose's exact version, and `@smartmemory/compose` shares a minor with `@smartmemory/stratum`
|
|
123
|
+
(patches move independently). So compose 0.4.x pairs with stratum 0.4.x. See
|
|
124
|
+
[.claude/rules/versioning.md](.claude/rules/versioning.md).
|
|
125
|
+
|
|
121
126
|
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.
|
|
122
127
|
|
|
123
128
|
Check what you're running:
|
package/bin/compose.js
CHANGED
|
@@ -90,7 +90,7 @@ function resolveCwdWithWorkspace(args) {
|
|
|
90
90
|
// --team flag (COMP-TEAMS)
|
|
91
91
|
// ---------------------------------------------------------------------------
|
|
92
92
|
import { parseTeamFlag } from '../lib/team-flag.js';
|
|
93
|
-
import { loadDeps, checkExternalSkills, printDepReport, buildDepReport, checkExternalBinaries, printBinaryReport, buildBinaryReport } from '../lib/deps.js';
|
|
93
|
+
import { loadDeps, checkExternalSkills, printDepReport, buildDepReport, checkExternalBinaries, printBinaryReport, buildBinaryReport, installMissingPlugins, printInstallReport } from '../lib/deps.js';
|
|
94
94
|
import {
|
|
95
95
|
checkLatestVersion,
|
|
96
96
|
checkPackageVersion,
|
|
@@ -172,7 +172,7 @@ function detectAgents() {
|
|
|
172
172
|
* - PACKAGE_ROOT/.claude/skills/* (compose skill)
|
|
173
173
|
* - PACKAGE_ROOT/skills/* (stratum base skill)
|
|
174
174
|
*/
|
|
175
|
-
function syncSkills(agents) {
|
|
175
|
+
function syncSkills(agents, setupFlags = []) {
|
|
176
176
|
// Collect source skills: { name -> sourcePath }
|
|
177
177
|
const sourceSkills = new Map()
|
|
178
178
|
const skillSourceDirs = [
|
|
@@ -271,12 +271,28 @@ function syncSkills(agents) {
|
|
|
271
271
|
// actionable install hints. Soft check: warnings only, exit code unaffected.
|
|
272
272
|
const deps = loadDeps(PACKAGE_ROOT)
|
|
273
273
|
if (deps) {
|
|
274
|
-
|
|
274
|
+
let result = checkExternalSkills(deps)
|
|
275
|
+
// COMP-DEPS-AUTOINSTALL: close the loop rather than printing a hint and
|
|
276
|
+
// running degraded. Only required deps with a `plugin` spec are installed.
|
|
277
|
+
// The report printed below is the POST-install scan, so what the user reads
|
|
278
|
+
// is the state they actually end up in, never the state we started from.
|
|
279
|
+
const installReport = installMissingPlugins(result, { enabled: pluginInstallEnabled(setupFlags) })
|
|
280
|
+
printInstallReport(installReport)
|
|
281
|
+
if (installReport.installed.length > 0) result = checkExternalSkills(deps)
|
|
275
282
|
printDepReport(result)
|
|
276
283
|
printBinaryReport(checkExternalBinaries(deps))
|
|
277
284
|
}
|
|
278
285
|
}
|
|
279
286
|
|
|
287
|
+
/**
|
|
288
|
+
* COMP-DEPS-AUTOINSTALL opt-out. Off for `--no-install-deps` and for
|
|
289
|
+
* COMPOSE_NO_PLUGIN_INSTALL=1 (CI, sandboxes, anyone who manages plugins by hand).
|
|
290
|
+
*/
|
|
291
|
+
function pluginInstallEnabled(flags = []) {
|
|
292
|
+
if (process.env.COMPOSE_NO_PLUGIN_INSTALL === '1') return false
|
|
293
|
+
return !flags.includes('--no-install-deps')
|
|
294
|
+
}
|
|
295
|
+
|
|
280
296
|
// ---------------------------------------------------------------------------
|
|
281
297
|
// compose doctor — re-run the external dep check
|
|
282
298
|
// ---------------------------------------------------------------------------
|
|
@@ -578,7 +594,7 @@ async function runInit(flags, cwdOverride) {
|
|
|
578
594
|
}
|
|
579
595
|
|
|
580
596
|
// 9. Sync all compose-owned skills to detected agents
|
|
581
|
-
syncSkills(agents)
|
|
597
|
+
syncSkills(agents, flags)
|
|
582
598
|
|
|
583
599
|
// 10. Summary
|
|
584
600
|
console.log('')
|
|
@@ -592,7 +608,7 @@ async function runInit(flags, cwdOverride) {
|
|
|
592
608
|
// compose setup — user-global skill setup
|
|
593
609
|
// ---------------------------------------------------------------------------
|
|
594
610
|
|
|
595
|
-
function runSetup() {
|
|
611
|
+
function runSetup(setupFlags = []) {
|
|
596
612
|
// 1. Sync all compose-owned skills to detected agents.
|
|
597
613
|
// If none detected, fall back to Claude — setup is unconditional
|
|
598
614
|
// "install global skill" per its help text.
|
|
@@ -600,7 +616,7 @@ function runSetup() {
|
|
|
600
616
|
if (agents.length === 0) {
|
|
601
617
|
agents = [{ name: 'claude', skillDir: join(homedir(), '.claude', 'skills', 'stratum') }]
|
|
602
618
|
}
|
|
603
|
-
syncSkills(agents)
|
|
619
|
+
syncSkills(agents, setupFlags)
|
|
604
620
|
|
|
605
621
|
}
|
|
606
622
|
|
|
@@ -713,7 +729,7 @@ async function runUpdate(flags) {
|
|
|
713
729
|
// Refresh global skills.
|
|
714
730
|
console.log('')
|
|
715
731
|
console.log('Refreshing global skill installation...')
|
|
716
|
-
runSetup()
|
|
732
|
+
runSetup(args)
|
|
717
733
|
|
|
718
734
|
// If invoked from inside a Compose project, refresh project artifacts too
|
|
719
735
|
if (existsSync(join(cwd, '.compose', 'compose.json'))) {
|
|
@@ -828,7 +844,7 @@ if (cmd === 'setup' || cmd === 'sync') {
|
|
|
828
844
|
// agent skill dirs. The name `sync` better signals
|
|
829
845
|
// the idempotent "reconcile local skills with this install" job (run it after
|
|
830
846
|
// editing skills locally, when there's no new version to `update` to).
|
|
831
|
-
runSetup()
|
|
847
|
+
runSetup(args)
|
|
832
848
|
process.exit(0)
|
|
833
849
|
}
|
|
834
850
|
|
|
@@ -882,7 +898,7 @@ if (cmd === 'migrate-anon') {
|
|
|
882
898
|
if (cmd === 'install') {
|
|
883
899
|
// Backwards-compat: run both init + setup
|
|
884
900
|
await runInit(args)
|
|
885
|
-
runSetup()
|
|
901
|
+
runSetup(args)
|
|
886
902
|
process.exit(0)
|
|
887
903
|
}
|
|
888
904
|
|
package/lib/deps.js
CHANGED
|
@@ -45,7 +45,16 @@ export function loadDeps(packageRoot) {
|
|
|
45
45
|
const installOk = typeof dep?.install === 'string'
|
|
46
46
|
const fallbackOk = dep?.fallback === null || typeof dep?.fallback === 'string'
|
|
47
47
|
const optOk = typeof dep?.optional === 'boolean'
|
|
48
|
-
|
|
48
|
+
// COMP-DEPS-AUTOINSTALL: `plugin` is optional and additive. When present it
|
|
49
|
+
// is the `<plugin>[@marketplace]` spec passed to `claude plugin install`.
|
|
50
|
+
// Its absence means "no automated install path" (several deps are prose,
|
|
51
|
+
// e.g. "user-installed skill at ~/.claude/skills/refactor/"), never an error.
|
|
52
|
+
const pluginOk = dep?.plugin === undefined || typeof dep?.plugin === 'string'
|
|
53
|
+
// `marketplace_source` is the `owner/repo` (or URL) used to register the
|
|
54
|
+
// marketplace when the plugin's marketplace is not configured yet — the
|
|
55
|
+
// normal state on a brand-new machine.
|
|
56
|
+
const srcOk = dep?.marketplace_source === undefined || typeof dep?.marketplace_source === 'string'
|
|
57
|
+
if (!(idOk && reqOk && installOk && fallbackOk && optOk && pluginOk && srcOk)) {
|
|
49
58
|
console.warn(`Warning: skipping invalid dep entry in .compose-deps.json: ${JSON.stringify(dep)}`)
|
|
50
59
|
continue
|
|
51
60
|
}
|
|
@@ -93,6 +102,36 @@ export function loadDeps(packageRoot) {
|
|
|
93
102
|
* - <home>/.claude/plugins/marketplaces/<m>/.claude/commands/<n>.md (pattern B')
|
|
94
103
|
* - <home>/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/<s>/SKILL.md (pattern C)
|
|
95
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* Read `~/.claude/plugins/installed_plugins.json` — Claude Code's authoritative
|
|
107
|
+
* record of which plugins are installed and where their tree lives.
|
|
108
|
+
*
|
|
109
|
+
* Returns `[{ plugin, installPath }]`, or `null` when the file is absent or
|
|
110
|
+
* unreadable, which the caller treats as "fall back to walking the cache".
|
|
111
|
+
* `null` and `[]` mean different things: `[]` is "nothing is installed".
|
|
112
|
+
*/
|
|
113
|
+
function readInstalledPluginPaths(home) {
|
|
114
|
+
const path = join(home, '.claude', 'plugins', 'installed_plugins.json')
|
|
115
|
+
if (!existsSync(path)) return null
|
|
116
|
+
let parsed
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(readFileSync(path, 'utf-8'))
|
|
119
|
+
} catch {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
const plugins = parsed?.plugins
|
|
123
|
+
if (!plugins || typeof plugins !== 'object') return null
|
|
124
|
+
const out = []
|
|
125
|
+
for (const [key, entries] of Object.entries(plugins)) {
|
|
126
|
+
// Key is "<plugin>@<marketplace>"; the namespace agents use is the plugin half.
|
|
127
|
+
const plugin = key.split('@')[0]
|
|
128
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
129
|
+
if (typeof entry?.installPath === 'string') out.push({ plugin, installPath: entry.installPath })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
export function checkExternalSkills(deps, home = homedir()) {
|
|
97
136
|
const userSkillsRoot = join(home, '.claude', 'skills')
|
|
98
137
|
const marketplacesRoot = join(home, '.claude', 'plugins', 'marketplaces')
|
|
@@ -159,15 +198,34 @@ export function checkExternalSkills(deps, home = homedir()) {
|
|
|
159
198
|
}
|
|
160
199
|
|
|
161
200
|
// 3. Cache — pattern C: cache/<marketplace>/<plugin>/<version>/skills/<s>/SKILL.md
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
201
|
+
//
|
|
202
|
+
// COMP-DEPS-AUTOINSTALL: the cache OUTLIVES an uninstall — `claude plugin
|
|
203
|
+
// uninstall` drops the plugin from installed_plugins.json but leaves its
|
|
204
|
+
// cached tree on disk, so walking the cache alone reports an uninstalled
|
|
205
|
+
// plugin as present. Claude Code will not load it, and auto-install would
|
|
206
|
+
// never re-install it (nothing looks missing). When installed_plugins.json is
|
|
207
|
+
// readable it is the authority: only its recorded installPaths count. Older
|
|
208
|
+
// Claude Code versions have no such file — there, fall back to the cache walk
|
|
209
|
+
// rather than reporting every plugin missing.
|
|
210
|
+
const installedPaths = readInstalledPluginPaths(home)
|
|
211
|
+
if (installedPaths === null) {
|
|
212
|
+
for (const marketplace of listDirs(cacheRoot)) {
|
|
213
|
+
for (const plugin of listDirs(join(cacheRoot, marketplace))) {
|
|
214
|
+
for (const version of listDirs(join(cacheRoot, marketplace, plugin))) {
|
|
215
|
+
const skillsDir = join(cacheRoot, marketplace, plugin, version, 'skills')
|
|
216
|
+
for (const s of listDirs(skillsDir)) {
|
|
217
|
+
if (existsSync(join(skillsDir, s, 'SKILL.md'))) addNs(plugin, s)
|
|
218
|
+
}
|
|
168
219
|
}
|
|
169
220
|
}
|
|
170
221
|
}
|
|
222
|
+
} else {
|
|
223
|
+
for (const { plugin, installPath } of installedPaths) {
|
|
224
|
+
const skillsDir = join(installPath, 'skills')
|
|
225
|
+
for (const s of listDirs(skillsDir)) {
|
|
226
|
+
if (existsSync(join(skillsDir, s, 'SKILL.md'))) addNs(plugin, s)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
171
229
|
}
|
|
172
230
|
|
|
173
231
|
const present = []
|
|
@@ -206,6 +264,8 @@ export function buildDepReport(result) {
|
|
|
206
264
|
id: d.id,
|
|
207
265
|
required_for: d.required_for,
|
|
208
266
|
install: d.install,
|
|
267
|
+
plugin: d.plugin ?? null,
|
|
268
|
+
marketplace_source: d.marketplace_source ?? null,
|
|
209
269
|
fallback: d.fallback ?? null,
|
|
210
270
|
optional: d.optional,
|
|
211
271
|
})
|
|
@@ -263,6 +323,103 @@ export function buildBinaryReport(result) {
|
|
|
263
323
|
}
|
|
264
324
|
}
|
|
265
325
|
|
|
326
|
+
/**
|
|
327
|
+
* COMP-DEPS-AUTOINSTALL — install the plugins that provide missing REQUIRED deps.
|
|
328
|
+
*
|
|
329
|
+
* Only deps carrying a `plugin` spec are installable; everything else stays a
|
|
330
|
+
* printed hint. Optional deps are never auto-installed: the ones we ship
|
|
331
|
+
* (interface-design, openai-codex) live in third-party marketplaces that would
|
|
332
|
+
* each need a `claude plugin marketplace add` first, which is a consent step of
|
|
333
|
+
* a different size than pulling a plugin from a marketplace the user already has.
|
|
334
|
+
*
|
|
335
|
+
* Several deps resolve to the SAME plugin (six superpowers skills, one plugin),
|
|
336
|
+
* so specs are deduped — six missing deps produce one install.
|
|
337
|
+
*
|
|
338
|
+
* Never throws and never blocks: a missing `claude` binary, a non-zero exit or a
|
|
339
|
+
* timeout all come back as data for the caller to print. `-y` is required
|
|
340
|
+
* whenever stdin/stdout is not a TTY, so it is always passed.
|
|
341
|
+
*
|
|
342
|
+
* @param {object} result - from checkExternalSkills
|
|
343
|
+
* @param {object} [opts]
|
|
344
|
+
* @param {(cmd:string,args:string[])=>{status:number|null,stderr?:string,error?:Error}} [opts.spawn]
|
|
345
|
+
* @param {boolean} [opts.enabled] - false short-circuits to a skip (opt-out flag / env)
|
|
346
|
+
* @returns {{installed:string[], failed:{spec:string,reason:string}[], skipped:string|null}}
|
|
347
|
+
*/
|
|
348
|
+
export function installMissingPlugins(result, opts = {}) {
|
|
349
|
+
const none = { installed: [], failed: [], skipped: null }
|
|
350
|
+
if (opts.enabled === false) return { ...none, skipped: 'disabled' }
|
|
351
|
+
|
|
352
|
+
const specs = [...new Set(
|
|
353
|
+
result.missing.filter(d => !d.optional && typeof d.plugin === 'string' && d.plugin).map(d => d.plugin),
|
|
354
|
+
)]
|
|
355
|
+
if (specs.length === 0) return none
|
|
356
|
+
|
|
357
|
+
const spawn = opts.spawn ?? ((cmd, args) => {
|
|
358
|
+
// 120s: installing clones a git repo, which is slow on a cold cache.
|
|
359
|
+
try {
|
|
360
|
+
return spawnSync(cmd, args, { encoding: 'utf-8', timeout: 120000, stdio: 'pipe' })
|
|
361
|
+
} catch (error) {
|
|
362
|
+
return { status: null, error }
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// spec -> marketplace source, for the add-then-retry path below.
|
|
367
|
+
const sourceFor = new Map()
|
|
368
|
+
for (const d of result.missing) {
|
|
369
|
+
if (d.plugin && d.marketplace_source && !sourceFor.has(d.plugin)) {
|
|
370
|
+
sourceFor.set(d.plugin, d.marketplace_source)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const installed = []
|
|
375
|
+
const failed = []
|
|
376
|
+
const install = (spec) => spawn('claude', ['plugin', 'install', spec, '-y', '--scope', 'user'])
|
|
377
|
+
|
|
378
|
+
for (const spec of specs) {
|
|
379
|
+
let res = install(spec)
|
|
380
|
+
|
|
381
|
+
// A brand-new machine has no marketplaces registered, so the first install
|
|
382
|
+
// fails with "not found in marketplace". Register the declared source and
|
|
383
|
+
// retry exactly once — measured against a clean HOME, where the un-retried
|
|
384
|
+
// path leaves every required dep missing. Only ever adds the marketplace the
|
|
385
|
+
// manifest names, never one derived from the failure text.
|
|
386
|
+
const notFound = /not found in marketplace|marketplace .* not found|unknown marketplace/i
|
|
387
|
+
.test(`${res?.stderr ?? ''}${res?.stdout ?? ''}`)
|
|
388
|
+
if (res?.status !== 0 && notFound && sourceFor.has(spec)) {
|
|
389
|
+
const add = spawn('claude', ['plugin', 'marketplace', 'add', sourceFor.get(spec)])
|
|
390
|
+
if (add?.status === 0) res = install(spec)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (res?.status === 0) { installed.push(spec); continue }
|
|
394
|
+
// ENOENT on the `claude` binary is a skip for the whole batch, not a failure
|
|
395
|
+
// of this spec: there is no install path at all on this machine.
|
|
396
|
+
if (res?.error?.code === 'ENOENT') {
|
|
397
|
+
return { installed, failed, skipped: 'the `claude` CLI is not on PATH' }
|
|
398
|
+
}
|
|
399
|
+
const reason = (res?.stderr || '').trim()
|
|
400
|
+
|| res?.error?.message
|
|
401
|
+
|| `exit ${res?.status ?? 'unknown'}`
|
|
402
|
+
failed.push({ spec, reason })
|
|
403
|
+
}
|
|
404
|
+
return { installed, failed, skipped: null }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Print what installMissingPlugins did. Returns nothing; the caller re-scans and
|
|
409
|
+
* prints the authoritative dep report afterwards.
|
|
410
|
+
*/
|
|
411
|
+
export function printInstallReport(report) {
|
|
412
|
+
if (report.skipped === 'disabled') return
|
|
413
|
+
if (!report.installed.length && !report.failed.length && !report.skipped) return
|
|
414
|
+
|
|
415
|
+
console.log('\nInstalling missing required plugins:')
|
|
416
|
+
for (const spec of report.installed) console.log(` + ${spec}`)
|
|
417
|
+
for (const { spec, reason } of report.failed) {
|
|
418
|
+
console.log(` ! ${spec} — install failed: ${reason}`)
|
|
419
|
+
}
|
|
420
|
+
if (report.skipped) console.log(` - skipped: ${report.skipped}`)
|
|
421
|
+
}
|
|
422
|
+
|
|
266
423
|
/**
|
|
267
424
|
* Print a human-readable external-binary report. Returns true if all required
|
|
268
425
|
* (non-optional) binaries are present. Prints nothing when no binaries are declared.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Structured AI dev pipeline: your agent writes the code, Compose makes it prove it. Gated design decisions, enforced postconditions, and independent review from goal to shipped code.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|