@smartmemory/compose 0.2.37-beta → 0.2.39-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/lib/deps.js +14 -2
- package/lib/discover-workspaces.js +28 -5
- package/lib/resolve-workspace.js +50 -3
- package/package.json +1 -1
package/lib/deps.js
CHANGED
|
@@ -73,7 +73,14 @@ export function checkExternalSkills(deps, home = homedir()) {
|
|
|
73
73
|
|
|
74
74
|
const bareInstalled = new Set()
|
|
75
75
|
const namespacedInstalled = new Set()
|
|
76
|
-
|
|
76
|
+
// Leaf names of namespaced (plugin-provided) skills. Claude Code surfaces many
|
|
77
|
+
// plugin skills under their bare leaf name (e.g. coder-config's `refactor`),
|
|
78
|
+
// so a bare manifest dep is satisfied by a same-leaf plugin skill.
|
|
79
|
+
const namespacedLeaves = new Set()
|
|
80
|
+
const addNs = (ns, leaf) => {
|
|
81
|
+
namespacedInstalled.add(`${ns}:${leaf}`)
|
|
82
|
+
namespacedLeaves.add(leaf)
|
|
83
|
+
}
|
|
77
84
|
|
|
78
85
|
// Helpers: list only entries of a given dirent kind, swallow scandir errors.
|
|
79
86
|
const listDirs = (path) => {
|
|
@@ -139,7 +146,12 @@ export function checkExternalSkills(deps, home = homedir()) {
|
|
|
139
146
|
const missing = []
|
|
140
147
|
for (const dep of deps.external_skills) {
|
|
141
148
|
const isNamespaced = dep.id.includes(':')
|
|
142
|
-
|
|
149
|
+
// Namespaced dep: require the exact <plugin>:<skill> match.
|
|
150
|
+
// Bare dep: a user skill at ~/.claude/skills/<id>/ OR any plugin skill whose
|
|
151
|
+
// leaf name is <id> (Claude Code exposes those under the bare name).
|
|
152
|
+
const found = isNamespaced
|
|
153
|
+
? namespacedInstalled.has(dep.id)
|
|
154
|
+
: (bareInstalled.has(dep.id) || namespacedLeaves.has(dep.id))
|
|
143
155
|
if (found) present.push(dep); else missing.push(dep)
|
|
144
156
|
}
|
|
145
157
|
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
* discover-workspaces.js — bounded bidirectional discovery of compose workspaces.
|
|
3
3
|
*
|
|
4
4
|
* Walks upward to find an "anchor" (any of ANCHOR_MARKERS), then scans the anchor
|
|
5
|
-
* subtree to MAX_DEPTH for `.compose/` markers.
|
|
6
|
-
*
|
|
5
|
+
* subtree to MAX_DEPTH for `.compose/` markers. The scan prunes aggressively —
|
|
6
|
+
* a workspace root is never a dot-directory (`.foo`) or a vendor/build/cache dir,
|
|
7
|
+
* so those whole subtrees are skipped. This keeps the visit count proportional to
|
|
8
|
+
* real source directories (a large monorepo scans in tens of ms), not to incidental
|
|
9
|
+
* tooling cruft. A generous MAX_VISITED cap remains as a backstop against
|
|
10
|
+
* pathological roots (e.g. $HOME); over-cap throws code='WorkspaceDiscoveryTooBroad',
|
|
11
|
+
* which resolveWorkspace degrades to the nearest enclosing workspace. Permission
|
|
7
12
|
* errors during readdir are skipped silently — discovery is best-effort, not
|
|
8
13
|
* authoritative for individual subtrees.
|
|
9
14
|
*
|
|
@@ -17,9 +22,27 @@ import fs from 'node:fs';
|
|
|
17
22
|
|
|
18
23
|
export const ANCHOR_MARKERS = ['.compose', '.stratum.yaml', '.git'];
|
|
19
24
|
export const WORKSPACE_MARKER = '.compose';
|
|
20
|
-
|
|
25
|
+
// Non-dot directories that never contain a workspace root but are often huge.
|
|
26
|
+
// (Dot-directories are pruned separately — see shouldSkipDir.)
|
|
27
|
+
export const SKIP_DIRS = new Set([
|
|
28
|
+
'node_modules', 'dist', 'build', 'out', 'coverage',
|
|
29
|
+
'vendor', 'target', '__pycache__', 'venv',
|
|
30
|
+
]);
|
|
21
31
|
export const MAX_DEPTH = 3;
|
|
22
|
-
|
|
32
|
+
// Generous backstop, not a tuning knob. With dot/vendor pruning the visit count
|
|
33
|
+
// tracks real source dirs (~1.5k for a large monorepo, scanned in tens of ms),
|
|
34
|
+
// so this only trips on pathological roots like $HOME.
|
|
35
|
+
export const MAX_VISITED = 10000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A workspace root is always a normal project directory — never a dot-directory
|
|
39
|
+
* (`.git`, `.venv`, `.playwright-mcp`, …) and never a vendor/build/cache dir.
|
|
40
|
+
* Pruning these whole subtrees is both correct (no marker can live there) and
|
|
41
|
+
* the single biggest cost lever for discovery on real repos.
|
|
42
|
+
*/
|
|
43
|
+
export function shouldSkipDir(name) {
|
|
44
|
+
return name.startsWith('.') || SKIP_DIRS.has(name);
|
|
45
|
+
}
|
|
23
46
|
|
|
24
47
|
/**
|
|
25
48
|
* Walk upward from startDir; return the first directory containing any
|
|
@@ -75,7 +98,7 @@ function walkDescendants(dir, depth, out, visited) {
|
|
|
75
98
|
return;
|
|
76
99
|
}
|
|
77
100
|
for (const entry of entries) {
|
|
78
|
-
if (!entry.isDirectory() ||
|
|
101
|
+
if (!entry.isDirectory() || shouldSkipDir(entry.name)) continue;
|
|
79
102
|
const child = path.join(dir, entry.name);
|
|
80
103
|
if (fs.existsSync(path.join(child, WORKSPACE_MARKER))) {
|
|
81
104
|
out.push({ root: child });
|
package/lib/resolve-workspace.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* 1. explicit hint.workspaceId (cheap upward walk first; falls back to discovery)
|
|
6
6
|
* 2. COMPOSE_TARGET env (absolute path bypasses discovery; id routes through it)
|
|
7
7
|
* 3. hint.getBinding() (MCP binding)
|
|
8
|
-
* 4. discovery (auto-pick when exactly one candidate; throws otherwise)
|
|
8
|
+
* 4. discovery (auto-pick when exactly one candidate; throws otherwise) —
|
|
9
|
+
* degrades to the nearest-enclosing workspace if the subtree is too broad
|
|
10
|
+
* to enumerate within the visit bound.
|
|
9
11
|
*
|
|
10
12
|
* Throws structured errors with `.code`: WorkspaceUnknown, WorkspaceAmbiguous,
|
|
11
13
|
* WorkspaceIdCollision, WorkspaceUnset. The CLI's dieOnWorkspaceError consumes them.
|
|
@@ -13,6 +15,14 @@
|
|
|
13
15
|
* Design intent: explicit-flag path uses findWorkspaceById (cheap upward walk)
|
|
14
16
|
* BEFORE invoking discoverWorkspaces — this lets users escape WorkspaceDiscoveryTooBroad
|
|
15
17
|
* by passing --workspace=<ancestor-id>. A descendant id still routes through discovery.
|
|
18
|
+
*
|
|
19
|
+
* The no-hint path mirrors that instinct only when the descendant scan can't
|
|
20
|
+
* enumerate the subtree (WorkspaceDiscoveryTooBroad): rather than failing, it
|
|
21
|
+
* falls back to the nearest enclosing `.compose` ancestor (upward walk), so
|
|
22
|
+
* running from a large workspace root resolves instead of dying. A tractable
|
|
23
|
+
* tree is still enumerated in full — genuine nested-workspace ambiguity (a
|
|
24
|
+
* parent workspace with a child below it) deliberately surfaces and forces an
|
|
25
|
+
* explicit --workspace.
|
|
16
26
|
*/
|
|
17
27
|
import path from 'node:path';
|
|
18
28
|
import fs from 'node:fs';
|
|
@@ -96,8 +106,26 @@ export function resolveWorkspace(hint = {}) {
|
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
// 4. Discovery —
|
|
100
|
-
|
|
109
|
+
// 4. Discovery — fan out under the anchor; collisions/ambiguity matter
|
|
110
|
+
// because we're auto-picking. A normal tree enumerates fine, so genuine
|
|
111
|
+
// nested-workspace ambiguity (e.g. a parent workspace with a child below)
|
|
112
|
+
// still surfaces as WorkspaceAmbiguous and forces an explicit --workspace.
|
|
113
|
+
let candidates;
|
|
114
|
+
try {
|
|
115
|
+
({ candidates } = discoverWorkspaces(cwd));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// The subtree was too large to enumerate within the visit bound. That cap
|
|
118
|
+
// is a cost guard, not a verdict — if cwd is itself inside a workspace, the
|
|
119
|
+
// nearest enclosing `.compose` ancestor is the unambiguous answer, so we
|
|
120
|
+
// degrade to it instead of failing (best-effort, same philosophy as the
|
|
121
|
+
// scan silently skipping EACCES subtrees). Only when cwd is inside NO
|
|
122
|
+
// workspace must the caller disambiguate explicitly — then we rethrow.
|
|
123
|
+
if (err.code === 'WorkspaceDiscoveryTooBroad') {
|
|
124
|
+
const enclosing = findEnclosingWorkspace(cwd);
|
|
125
|
+
if (enclosing) return { ...deriveId({ root: enclosing }), source: 'discovery' };
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
101
129
|
detectCollisions(candidates);
|
|
102
130
|
if (candidates.length === 0) throw new WorkspaceUnset();
|
|
103
131
|
if (candidates.length === 1) return { ...candidates[0], source: 'discovery' };
|
|
@@ -124,6 +152,25 @@ function findWorkspaceById(startDir, targetId) {
|
|
|
124
152
|
}
|
|
125
153
|
}
|
|
126
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Cheap upward-only lookup: walk ancestors from startDir and return the first
|
|
157
|
+
* directory containing a `.compose/` marker, or null if none exists before the
|
|
158
|
+
* filesystem root. This is the no-hint "nearest enclosing workspace" resolver —
|
|
159
|
+
* identical walk to findWorkspaceById but id-agnostic (returns the closest
|
|
160
|
+
* workspace regardless of its derived id).
|
|
161
|
+
*/
|
|
162
|
+
function findEnclosingWorkspace(startDir) {
|
|
163
|
+
let dir = path.resolve(startDir);
|
|
164
|
+
const { root } = path.parse(dir);
|
|
165
|
+
while (true) {
|
|
166
|
+
if (fs.existsSync(path.join(dir, '.compose'))) return dir;
|
|
167
|
+
if (dir === root) return null;
|
|
168
|
+
const parent = path.dirname(dir);
|
|
169
|
+
if (parent === dir) return null;
|
|
170
|
+
dir = parent;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
127
174
|
function resolveByIdScopedCollisionCheck(id, candidates, source) {
|
|
128
175
|
const matching = candidates.filter((c) => c.id === id);
|
|
129
176
|
if (matching.length === 0) throw new WorkspaceUnknown(id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.39-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|