thachvd-kit 1.0.37 → 1.0.38
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 +241 -11
- package/bin/cli.js +1791 -1785
- package/bin/config.js +164 -0
- package/bin/entry.js +11 -1
- package/bin/native-skills.js +183 -149
- package/bin/spec-doctor.js +251 -0
- package/bin/spec-link.js +97 -0
- package/bin/spec-recipe.js +74 -0
- package/bin/spec-state.js +415 -0
- package/bin/spec.js +859 -0
- package/package.json +3 -3
- package/skills/system-discovery/SKILL.md +140 -0
- package/skills/system-reverse-engineer/SKILL.md +208 -0
- package/skills/system-spec-review/SKILL.md +177 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const state = require('./spec-state');
|
|
5
|
+
const links = require('./spec-link');
|
|
6
|
+
|
|
7
|
+
const SYSTEM_SKILLS = ['system-discovery', 'system-reverse-engineer', 'system-spec-review'];
|
|
8
|
+
|
|
9
|
+
function commandExists(command) {
|
|
10
|
+
const checker = process.platform === 'win32' ? 'where' : 'command';
|
|
11
|
+
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
12
|
+
const result = spawnSync(checker, args, {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
shell: process.platform !== 'win32'
|
|
15
|
+
});
|
|
16
|
+
return result.status === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function add(checks, level, name, message) {
|
|
20
|
+
checks.push({ level, name, message });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runGit(cwd, args) {
|
|
24
|
+
const result = spawnSync('git', args, {
|
|
25
|
+
cwd,
|
|
26
|
+
encoding: 'utf8'
|
|
27
|
+
});
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: (result.stderr || '').trim() || `git ${args.join(' ')} failed`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, stdout: (result.stdout || '').trim() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function canonicalPath(value) {
|
|
38
|
+
const resolved = path.resolve(value);
|
|
39
|
+
try {
|
|
40
|
+
return fs.realpathSync.native ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved);
|
|
41
|
+
} catch {
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function inspectSpecStorage(workspaceRoot, manifest) {
|
|
47
|
+
const specRoot = canonicalPath(path.resolve(workspaceRoot, manifest.spec_root));
|
|
48
|
+
if (!fs.existsSync(specRoot)) {
|
|
49
|
+
return { ok: false, message: `spec root is missing: ${manifest.spec_root}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const top = runGit(specRoot, ['rev-parse', '--show-toplevel']);
|
|
53
|
+
if (!top.ok) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
message: 'spec root is not inside a Git worktree; generated specs/provenance are not version-controlled'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const gitRoot = canonicalPath(top.stdout);
|
|
61
|
+
const required = [
|
|
62
|
+
path.join(specRoot, 'README.md'),
|
|
63
|
+
path.join(specRoot, '_meta', 'workspace.json'),
|
|
64
|
+
path.join(specRoot, 'architecture', 'capability-map.md'),
|
|
65
|
+
state.verificationPath(workspaceRoot, manifest),
|
|
66
|
+
state.reviewPath(workspaceRoot, manifest)
|
|
67
|
+
].filter(filePath => fs.existsSync(filePath));
|
|
68
|
+
|
|
69
|
+
const untracked = [];
|
|
70
|
+
for (const filePath of required) {
|
|
71
|
+
const relative = path.relative(gitRoot, canonicalPath(filePath)).split(path.sep).join('/');
|
|
72
|
+
const tracked = runGit(gitRoot, ['ls-files', '--error-unmatch', '--', relative]);
|
|
73
|
+
if (!tracked.ok) untracked.push(relative);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const specRelative = path.relative(gitRoot, specRoot).split(path.sep).join('/') || '.';
|
|
77
|
+
const status = runGit(gitRoot, ['status', '--porcelain', '--', specRelative]);
|
|
78
|
+
if (!status.ok) return { ok: false, message: status.error };
|
|
79
|
+
|
|
80
|
+
if (untracked.length > 0 || status.stdout) {
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (untracked.length > 0) parts.push(`${untracked.length} key spec file(s) are not tracked`);
|
|
83
|
+
if (status.stdout) parts.push('spec root has uncommitted changes');
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
message: `${parts.join('; ')}. Commit reviewed specs/provenance before treating them as durable.`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { ok: true, message: `spec root is clean and version-controlled in ${gitRoot}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function runDoctor(workspaceRoot, manifest, options = {}) {
|
|
94
|
+
const checks = [];
|
|
95
|
+
const binaryExists = options.commandExists || commandExists;
|
|
96
|
+
|
|
97
|
+
add(checks, 'ok', 'manifest', `.thachvd/system.json valid for ${manifest.name}; profile=${manifest.profile || 'auto'}; language=${manifest.language || 'en'}`);
|
|
98
|
+
|
|
99
|
+
if (binaryExists('codebase-memory-mcp')) {
|
|
100
|
+
add(checks, 'ok', 'codebase-memory', 'codebase-memory-mcp is installed');
|
|
101
|
+
} else {
|
|
102
|
+
add(checks, 'warn', 'codebase-memory', 'codebase-memory-mcp is not available on PATH');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const heads = state.captureRepoHeads(workspaceRoot, manifest, options);
|
|
106
|
+
for (const repo of manifest.repos) {
|
|
107
|
+
const repoRoot = path.resolve(workspaceRoot, repo.path);
|
|
108
|
+
if (!fs.existsSync(repoRoot)) {
|
|
109
|
+
add(checks, 'error', `repo:${repo.name}`, `repository path is missing: ${repo.path}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const head = heads[repo.name];
|
|
113
|
+
if (!head || !head.ok) {
|
|
114
|
+
add(checks, 'error', `repo:${repo.name}`, head ? head.error : 'Git HEAD unavailable');
|
|
115
|
+
} else {
|
|
116
|
+
add(checks, 'ok', `repo:${repo.name}`, `${repo.path} @ ${head.commit.slice(0, 12)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const skill of SYSTEM_SKILLS) {
|
|
121
|
+
const missing = [];
|
|
122
|
+
for (const surface of ['.agents', '.claude']) {
|
|
123
|
+
const filePath = path.join(workspaceRoot, surface, 'skills', skill, 'SKILL.md');
|
|
124
|
+
if (!fs.existsSync(filePath)) missing.push(surface);
|
|
125
|
+
}
|
|
126
|
+
if (missing.length === 0) add(checks, 'ok', `skill:${skill}`, 'installed on agent + Claude surfaces');
|
|
127
|
+
else add(checks, 'warn', `skill:${skill}`, `missing from: ${missing.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const storage = inspectSpecStorage(workspaceRoot, manifest);
|
|
131
|
+
add(checks, storage.ok ? 'ok' : 'warn', 'spec-storage', storage.message);
|
|
132
|
+
|
|
133
|
+
const workspaceSnapshot = path.join(workspaceRoot, manifest.spec_root, '_meta', 'workspace.json');
|
|
134
|
+
if (!fs.existsSync(workspaceSnapshot)) {
|
|
135
|
+
add(checks, 'warn', 'workspace-snapshot', 'missing; rerun spec init --yes to create a durable topology snapshot');
|
|
136
|
+
} else {
|
|
137
|
+
try {
|
|
138
|
+
const snapshot = JSON.parse(fs.readFileSync(workspaceSnapshot, 'utf8'));
|
|
139
|
+
const comparable = value => JSON.stringify({
|
|
140
|
+
schema_version: value.schema_version,
|
|
141
|
+
name: value.name,
|
|
142
|
+
spec_root: value.spec_root,
|
|
143
|
+
language: value.language,
|
|
144
|
+
profile: value.profile || 'auto',
|
|
145
|
+
repos: value.repos
|
|
146
|
+
});
|
|
147
|
+
if (comparable(snapshot) === comparable(manifest)) {
|
|
148
|
+
add(checks, 'ok', 'workspace-snapshot', 'durable topology snapshot matches the active manifest');
|
|
149
|
+
} else {
|
|
150
|
+
add(checks, 'warn', 'workspace-snapshot', 'snapshot differs from .thachvd/system.json; refresh with spec init --yes');
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
add(checks, 'error', 'workspace-snapshot', `cannot parse workspace snapshot: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const capabilityMap = path.join(workspaceRoot, manifest.spec_root, 'architecture', 'capability-map.md');
|
|
158
|
+
if (fs.existsSync(capabilityMap)) add(checks, 'ok', 'capability-map', 'capability map exists');
|
|
159
|
+
else add(checks, 'warn', 'capability-map', 'missing; run spec discover + /system-discovery');
|
|
160
|
+
|
|
161
|
+
const progress = state.readProgress(workspaceRoot, manifest);
|
|
162
|
+
if (!progress) {
|
|
163
|
+
add(checks, 'warn', 'reverse-progress', 'not started');
|
|
164
|
+
} else if (progress.status === 'completed') {
|
|
165
|
+
const capabilityStates = Object.values(progress.capabilities || {});
|
|
166
|
+
const blocked = capabilityStates.filter(item => item.status === 'blocked').length;
|
|
167
|
+
add(checks, blocked > 0 ? 'warn' : 'ok', 'reverse-progress',
|
|
168
|
+
`completed; ${capabilityStates.length} capabilities, ${blocked} blocked`);
|
|
169
|
+
} else {
|
|
170
|
+
const capabilityStates = Object.values(progress.capabilities || {});
|
|
171
|
+
const completed = capabilityStates.filter(item => item.status === 'completed').length;
|
|
172
|
+
add(checks, 'warn', 'reverse-progress',
|
|
173
|
+
`${progress.status}; ${completed}/${capabilityStates.length || '?'} capabilities completed`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let verification = null;
|
|
177
|
+
let verificationFailed = false;
|
|
178
|
+
try {
|
|
179
|
+
verification = state.readVerification(workspaceRoot, manifest);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
verificationFailed = true;
|
|
182
|
+
add(checks, 'error', 'verification', error.message);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!verification && !verificationFailed) {
|
|
186
|
+
add(checks, 'warn', 'verification', 'not independently verified');
|
|
187
|
+
} else if (verification) {
|
|
188
|
+
try {
|
|
189
|
+
state.validateVerification(manifest, verification);
|
|
190
|
+
const statuses = Object.values(verification.capabilities || {});
|
|
191
|
+
const verified = statuses.filter(item => item.status === 'verified').length;
|
|
192
|
+
const unresolved = statuses.length - verified;
|
|
193
|
+
add(checks, unresolved === 0 ? 'ok' : 'warn', 'verification',
|
|
194
|
+
`${verified}/${statuses.length} capabilities verified; ${unresolved} unresolved`);
|
|
195
|
+
|
|
196
|
+
const reviewFile = state.reviewPath(workspaceRoot, manifest);
|
|
197
|
+
if (fs.existsSync(reviewFile)) add(checks, 'ok', 'review-report', 'review.md exists');
|
|
198
|
+
else add(checks, 'warn', 'review-report', 'verification exists but review.md is missing');
|
|
199
|
+
|
|
200
|
+
const drift = state.checkSpecDrift(workspaceRoot, manifest, verification, options);
|
|
201
|
+
add(checks, drift.stale_count === 0 ? 'ok' : 'warn', 'drift',
|
|
202
|
+
drift.stale_count === 0
|
|
203
|
+
? 'no stale/unknown verified capabilities detected'
|
|
204
|
+
: `${drift.stale_count} stale/unknown capabilities detected; run spec check`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
add(checks, 'error', 'verification', error.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const repo of manifest.repos) {
|
|
211
|
+
const agentsPath = path.join(workspaceRoot, repo.path, 'AGENTS.md');
|
|
212
|
+
if (!fs.existsSync(agentsPath)) {
|
|
213
|
+
add(checks, 'warn', `agent-link:${repo.name}`, 'AGENTS.md missing or not linked');
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const content = fs.readFileSync(agentsPath, 'utf8');
|
|
217
|
+
if (content.includes(links.START) && content.includes(links.END)) {
|
|
218
|
+
add(checks, 'ok', `agent-link:${repo.name}`, 'system spec managed block present');
|
|
219
|
+
} else {
|
|
220
|
+
add(checks, 'warn', `agent-link:${repo.name}`, 'system spec managed block missing; run spec link after verification');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const counts = {
|
|
225
|
+
ok: checks.filter(item => item.level === 'ok').length,
|
|
226
|
+
warn: checks.filter(item => item.level === 'warn').length,
|
|
227
|
+
error: checks.filter(item => item.level === 'error').length
|
|
228
|
+
};
|
|
229
|
+
return { checks, counts };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatDoctorReport(report) {
|
|
233
|
+
const label = { ok: 'OK', warn: 'WARN', error: 'ERROR' };
|
|
234
|
+
const lines = ['System spec doctor', ''];
|
|
235
|
+
for (const check of report.checks) {
|
|
236
|
+
lines.push(`${label[check.level]} ${check.name}: ${check.message}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push(`Summary: ${report.counts.ok} OK, ${report.counts.warn} WARN, ${report.counts.error} ERROR`);
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
SYSTEM_SKILLS,
|
|
245
|
+
canonicalPath,
|
|
246
|
+
commandExists,
|
|
247
|
+
runGit,
|
|
248
|
+
inspectSpecStorage,
|
|
249
|
+
runDoctor,
|
|
250
|
+
formatDoctorReport
|
|
251
|
+
};
|
package/bin/spec-link.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const START = '<!-- thachvd-kit:system-specs:start -->';
|
|
5
|
+
const END = '<!-- thachvd-kit:system-specs:end -->';
|
|
6
|
+
|
|
7
|
+
function portable(value) {
|
|
8
|
+
return value.split(path.sep).join('/');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function relativeReference(fromDir, targetPath) {
|
|
12
|
+
const relative = portable(path.relative(fromDir, targetPath) || '.');
|
|
13
|
+
return relative.startsWith('.') ? relative : `./${relative}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function renderBlock(workspaceRoot, repoRoot, manifest) {
|
|
17
|
+
const specRoot = path.resolve(workspaceRoot, manifest.spec_root);
|
|
18
|
+
const manifestPath = path.join(workspaceRoot, '.thachvd', 'system.json');
|
|
19
|
+
const specRef = relativeReference(repoRoot, specRoot);
|
|
20
|
+
const manifestRef = relativeReference(repoRoot, manifestPath);
|
|
21
|
+
return `${START}
|
|
22
|
+
## System Specifications
|
|
23
|
+
|
|
24
|
+
This repository is part of the **${manifest.name}** documented system.
|
|
25
|
+
|
|
26
|
+
- System manifest: \`${manifestRef}\`
|
|
27
|
+
- System specs: \`${specRef}\`
|
|
28
|
+
|
|
29
|
+
Before changing observable behavior:
|
|
30
|
+
|
|
31
|
+
1. Read \`${specRef}/architecture/system-overview.md\`.
|
|
32
|
+
2. Read \`${specRef}/architecture/capability-map.md\` and identify the affected capability.
|
|
33
|
+
3. Read the relevant capability PRD/design and any cross-repository flow or contract it touches.
|
|
34
|
+
4. If \`${specRef}/_meta/verification.json\` exists, run \`thachvd-kit spec check\` before relying on the spec; investigate STALE/UNKNOWN capabilities against code.
|
|
35
|
+
5. Treat implementation and tests as the current source of truth. If code and reviewed spec disagree, report the drift; do not silently choose one.
|
|
36
|
+
6. Update/re-verify reviewed specs when an approved behavior or contract change makes them stale.
|
|
37
|
+
|
|
38
|
+
Do not infer business rationale from code when the rationale is undocumented.
|
|
39
|
+
${END}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function upsertManagedBlock(content, block) {
|
|
43
|
+
const start = content.indexOf(START);
|
|
44
|
+
const end = content.indexOf(END);
|
|
45
|
+
if ((start >= 0) !== (end >= 0) || (start >= 0 && end < start)) {
|
|
46
|
+
throw new Error('Malformed thachvd-kit system-spec markers in AGENTS.md');
|
|
47
|
+
}
|
|
48
|
+
if (start >= 0 && end >= start) {
|
|
49
|
+
const after = end + END.length;
|
|
50
|
+
return content.slice(0, start) + block + content.slice(after);
|
|
51
|
+
}
|
|
52
|
+
const trimmed = content.replace(/\s+$/, '');
|
|
53
|
+
return `${trimmed}${trimmed ? '\n\n' : ''}${block}\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function linkRepositories(workspaceRoot, manifest, options = {}) {
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const repo of manifest.repos) {
|
|
59
|
+
const repoRoot = path.resolve(workspaceRoot, repo.path);
|
|
60
|
+
if (!fs.existsSync(repoRoot) || !fs.statSync(repoRoot).isDirectory()) {
|
|
61
|
+
results.push({ name: repo.name, ok: false, error: 'repository path is missing' });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
65
|
+
const existing = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf8') : '';
|
|
66
|
+
try {
|
|
67
|
+
const block = renderBlock(workspaceRoot, repoRoot, manifest);
|
|
68
|
+
const next = upsertManagedBlock(existing, block);
|
|
69
|
+
const changed = next !== existing;
|
|
70
|
+
if (changed && !options.dryRun) fs.writeFileSync(agentsPath, next, 'utf8');
|
|
71
|
+
results.push({
|
|
72
|
+
name: repo.name,
|
|
73
|
+
ok: true,
|
|
74
|
+
changed,
|
|
75
|
+
dryRun: options.dryRun === true,
|
|
76
|
+
path: portable(path.relative(workspaceRoot, agentsPath))
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
results.push({
|
|
80
|
+
name: repo.name,
|
|
81
|
+
ok: false,
|
|
82
|
+
path: portable(path.relative(workspaceRoot, agentsPath)),
|
|
83
|
+
error: error.message
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
START,
|
|
92
|
+
END,
|
|
93
|
+
relativeReference,
|
|
94
|
+
renderBlock,
|
|
95
|
+
upsertManagedBlock,
|
|
96
|
+
linkRepositories
|
|
97
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const MARKETPLACE = 'shinpr/claude-code-workflows';
|
|
6
|
+
const MARKETPLACE_NAME = 'claude-code-workflows';
|
|
7
|
+
|
|
8
|
+
function commandExists(command) {
|
|
9
|
+
const checker = process.platform === 'win32' ? 'where' : 'command';
|
|
10
|
+
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
11
|
+
return spawnSync(checker, args, { encoding: 'utf8', shell: process.platform !== 'win32' }).status === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function setupRecipePlugin(workspaceRoot, manifest, options = {}) {
|
|
15
|
+
const plugin = options.fullstack ? 'dev-workflows-fullstack' : 'dev-workflows';
|
|
16
|
+
const commands = [
|
|
17
|
+
['plugin', 'marketplace', 'add', MARKETPLACE, '--scope', 'project'],
|
|
18
|
+
['plugin', 'install', `${plugin}@${MARKETPLACE_NAME}`, '--scope', 'project']
|
|
19
|
+
];
|
|
20
|
+
if (options.skipBinaryCheck !== true && !options.dryRun && !commandExists('claude')) {
|
|
21
|
+
return { ok: false, plugin, commands, error: 'Claude Code CLI is not installed; install the plugin manually or use another client.', results: [] };
|
|
22
|
+
}
|
|
23
|
+
const spawn = options.spawn || spawnSync;
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const repo of manifest.repos) {
|
|
26
|
+
const cwd = path.resolve(workspaceRoot, repo.path);
|
|
27
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
28
|
+
results.push({ name: repo.name, ok: false, error: 'repository path is missing' });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (options.dryRun) {
|
|
32
|
+
results.push({ name: repo.name, ok: true, dryRun: true, cwd, commands });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let repoOk = true;
|
|
36
|
+
for (const args of commands) {
|
|
37
|
+
const result = spawn('claude', args, {
|
|
38
|
+
cwd,
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
shell: process.platform === 'win32'
|
|
42
|
+
});
|
|
43
|
+
if (result.status !== 0) {
|
|
44
|
+
results.push({ name: repo.name, ok: false, cwd, failedCommand: ['claude', ...args], status: result.status });
|
|
45
|
+
repoOk = false;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (repoOk) results.push({ name: repo.name, ok: true, cwd });
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: results.length === manifest.repos.length && results.every(result => result.ok),
|
|
53
|
+
plugin,
|
|
54
|
+
commands,
|
|
55
|
+
dryRun: options.dryRun === true,
|
|
56
|
+
results
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeCapability(value) {
|
|
61
|
+
if (!value || typeof value !== 'string') throw new Error('Capability name is required');
|
|
62
|
+
const slug = value.trim().toLowerCase()
|
|
63
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
64
|
+
.replace(/^-+|-+$/g, '');
|
|
65
|
+
if (!slug) throw new Error('Capability name must contain letters or numbers');
|
|
66
|
+
return slug;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
MARKETPLACE,
|
|
71
|
+
MARKETPLACE_NAME,
|
|
72
|
+
setupRecipePlugin,
|
|
73
|
+
normalizeCapability
|
|
74
|
+
};
|