@crouton-kit/crouter 0.2.6 → 0.3.2
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/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +9 -9
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
- package/dist/cli.js +42 -37
- package/dist/commands/__tests__/human.test.d.ts +1 -0
- package/dist/commands/__tests__/human.test.js +214 -0
- package/dist/commands/__tests__/skill.test.d.ts +1 -0
- package/dist/commands/__tests__/skill.test.js +294 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +179 -0
- package/dist/commands/flow.d.ts +2 -0
- package/dist/commands/flow.js +24 -0
- package/dist/commands/human.d.ts +2 -0
- package/dist/commands/human.js +480 -0
- package/dist/commands/job.d.ts +2 -0
- package/dist/commands/job.js +669 -0
- package/dist/commands/pkg.d.ts +2 -0
- package/dist/commands/pkg.js +1021 -0
- package/dist/commands/plan.d.ts +4 -2
- package/dist/commands/plan.js +306 -22
- package/dist/commands/skill.d.ts +2 -2
- package/dist/commands/skill.js +613 -456
- package/dist/commands/spec.d.ts +3 -2
- package/dist/commands/spec.js +283 -10
- package/dist/commands/sys.d.ts +2 -0
- package/dist/commands/sys.js +712 -0
- package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
- package/dist/core/__tests__/argv-parser.test.js +199 -0
- package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
- package/dist/core/__tests__/flow-leaves.test.js +248 -0
- package/dist/core/__tests__/job.test.d.ts +1 -0
- package/dist/core/__tests__/job.test.js +346 -0
- package/dist/core/__tests__/pkg.test.d.ts +1 -0
- package/dist/core/__tests__/pkg.test.js +218 -0
- package/dist/core/__tests__/sys.test.d.ts +1 -0
- package/dist/core/__tests__/sys.test.js +208 -0
- package/dist/core/artifact.d.ts +29 -18
- package/dist/core/artifact.js +78 -221
- package/dist/core/auto-update.js +11 -4
- package/dist/core/command.d.ts +36 -0
- package/dist/core/command.js +287 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +5 -0
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +4 -0
- package/dist/core/help.d.ts +98 -0
- package/dist/core/help.js +163 -0
- package/dist/core/io.d.ts +29 -0
- package/dist/core/io.js +83 -0
- package/dist/core/jobs.d.ts +87 -0
- package/dist/core/jobs.js +353 -0
- package/dist/core/pagination.d.ts +33 -0
- package/dist/core/pagination.js +89 -0
- package/dist/core/self-update.d.ts +21 -0
- package/dist/core/self-update.js +105 -0
- package/dist/core/spawn.d.ts +47 -65
- package/dist/core/spawn.js +78 -228
- package/dist/prompts/agent.d.ts +10 -5
- package/dist/prompts/agent.js +51 -74
- package/dist/prompts/debug.d.ts +8 -0
- package/dist/prompts/debug.js +37 -0
- package/dist/prompts/review.js +4 -11
- package/dist/prompts/skill.d.ts +0 -1
- package/dist/prompts/skill.js +95 -149
- package/package.json +4 -2
- package/dist/commands/agent.d.ts +0 -2
- package/dist/commands/agent.js +0 -265
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -146
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -268
- package/dist/commands/marketplace.d.ts +0 -2
- package/dist/commands/marketplace.js +0 -365
- package/dist/commands/plugin.d.ts +0 -2
- package/dist/commands/plugin.js +0 -367
- package/dist/commands/update.d.ts +0 -4
- package/dist/commands/update.js +0 -140
- package/dist/prompts/plan.d.ts +0 -1
- package/dist/prompts/plan.js +0 -175
- package/dist/prompts/spec.d.ts +0 -1
- package/dist/prompts/spec.js +0 -153
package/dist/commands/agent.js
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { artifactPath, artifactsRoot } from '../core/artifact.js';
|
|
3
|
-
import { readConfig } from '../core/config.js';
|
|
4
|
-
import { spawnAgent, spawnAndDetach, awaitSession, submitToSession, DEFAULT_PANE_OPTS, } from '../core/spawn.js';
|
|
5
|
-
import { pathExists } from '../core/fs-utils.js';
|
|
6
|
-
import { notFound, usage } from '../core/errors.js';
|
|
7
|
-
import { handleError, hint, out, info } from '../core/output.js';
|
|
8
|
-
import { implementHandoffPrompt, planHandoffPrompt, reviewHandoffPrompt, } from '../prompts/agent.js';
|
|
9
|
-
// Seconds before the originating pane is closed in fire-and-forget workflow
|
|
10
|
-
// handoffs (plan/implement/review). Override with --kill-after.
|
|
11
|
-
const DEFAULT_KILL_SECS = 2;
|
|
12
|
-
async function readStdin() {
|
|
13
|
-
const chunks = [];
|
|
14
|
-
for await (const chunk of process.stdin) {
|
|
15
|
-
chunks.push(chunk);
|
|
16
|
-
}
|
|
17
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
18
|
-
}
|
|
19
|
-
async function resolvePrompt(positional) {
|
|
20
|
-
if (positional !== undefined && positional !== '')
|
|
21
|
-
return positional;
|
|
22
|
-
if (!process.stdin.isTTY) {
|
|
23
|
-
const piped = await readStdin();
|
|
24
|
-
if (piped.trim() !== '')
|
|
25
|
-
return piped;
|
|
26
|
-
}
|
|
27
|
-
throw usage('no prompt provided. Pass a positional arg or pipe via stdin.');
|
|
28
|
-
}
|
|
29
|
-
function resolveMaxPanes(override) {
|
|
30
|
-
if (override !== undefined) {
|
|
31
|
-
const n = Number(override);
|
|
32
|
-
if (!Number.isFinite(n) || n < 1) {
|
|
33
|
-
throw usage(`--max-panes must be an integer >= 1 (got: ${override})`);
|
|
34
|
-
}
|
|
35
|
-
return Math.floor(n);
|
|
36
|
-
}
|
|
37
|
-
const cfg = readConfig('user');
|
|
38
|
-
return cfg.max_panes_per_window;
|
|
39
|
-
}
|
|
40
|
-
function emitDetach(result, label) {
|
|
41
|
-
if (result.status === 'spawned') {
|
|
42
|
-
const paneLabel = result.paneId === undefined ? '(unknown)' : result.paneId;
|
|
43
|
-
out(`handoff: ${label} launched in pane ${paneLabel}`);
|
|
44
|
-
hint(result.message);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (result.status === 'not-in-tmux') {
|
|
48
|
-
throw usage(result.message);
|
|
49
|
-
}
|
|
50
|
-
throw new Error(result.message);
|
|
51
|
-
}
|
|
52
|
-
function parseKillAfter(raw) {
|
|
53
|
-
const n = Number(raw);
|
|
54
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
55
|
-
throw usage(`--kill-after must be a non-negative number (got: ${raw})`);
|
|
56
|
-
}
|
|
57
|
-
return n;
|
|
58
|
-
}
|
|
59
|
-
function baseDetachOpts() {
|
|
60
|
-
return {
|
|
61
|
-
cwd: process.cwd(),
|
|
62
|
-
placement: 'split-h',
|
|
63
|
-
killAfterSeconds: DEFAULT_KILL_SECS,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
export function registerAgentCommand(program) {
|
|
67
|
-
const agent = program
|
|
68
|
-
.command('agent')
|
|
69
|
-
.description('spawn, fork, await, and submit between sibling claude sessions');
|
|
70
|
-
agent
|
|
71
|
-
.command('new [prompt]')
|
|
72
|
-
.description('spawn a fresh claude in a sibling pane; prints session id, returns async')
|
|
73
|
-
.option('--max-panes <n>', 'max panes per window before overflowing to a new window')
|
|
74
|
-
.action(async (prompt, options) => {
|
|
75
|
-
try {
|
|
76
|
-
const body = await resolvePrompt(prompt);
|
|
77
|
-
const maxPanes = resolveMaxPanes(options.maxPanes);
|
|
78
|
-
const result = spawnAgent({
|
|
79
|
-
prompt: body,
|
|
80
|
-
cwd: process.cwd(),
|
|
81
|
-
maxPanesPerWindow: maxPanes,
|
|
82
|
-
});
|
|
83
|
-
if (result.status === 'not-in-tmux')
|
|
84
|
-
throw usage(result.message);
|
|
85
|
-
if (result.status === 'spawn-failed')
|
|
86
|
-
throw new Error(result.message);
|
|
87
|
-
const sessionId = result.sessionId;
|
|
88
|
-
if (sessionId === undefined) {
|
|
89
|
-
throw new Error('spawn succeeded but no session id returned');
|
|
90
|
-
}
|
|
91
|
-
out(sessionId);
|
|
92
|
-
info(result.message);
|
|
93
|
-
hint(`await with: crtr agent await ${sessionId}`);
|
|
94
|
-
}
|
|
95
|
-
catch (e) {
|
|
96
|
-
handleError(e);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
agent
|
|
100
|
-
.command('fork [prompt]')
|
|
101
|
-
.description('fork the current Claude Code session into a sibling pane with a new prompt; prints session id')
|
|
102
|
-
.option('--max-panes <n>', 'max panes per window before overflowing to a new window')
|
|
103
|
-
.action(async (prompt, options) => {
|
|
104
|
-
try {
|
|
105
|
-
const parentSessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
106
|
-
if (parentSessionId === undefined || parentSessionId === '') {
|
|
107
|
-
throw usage('crtr agent fork requires $CLAUDE_CODE_SESSION_ID — must run inside Claude Code');
|
|
108
|
-
}
|
|
109
|
-
const body = await resolvePrompt(prompt);
|
|
110
|
-
const maxPanes = resolveMaxPanes(options.maxPanes);
|
|
111
|
-
const result = spawnAgent({
|
|
112
|
-
prompt: body,
|
|
113
|
-
cwd: process.cwd(),
|
|
114
|
-
fork: { sessionId: parentSessionId },
|
|
115
|
-
maxPanesPerWindow: maxPanes,
|
|
116
|
-
});
|
|
117
|
-
if (result.status === 'not-in-tmux')
|
|
118
|
-
throw usage(result.message);
|
|
119
|
-
if (result.status === 'spawn-failed')
|
|
120
|
-
throw new Error(result.message);
|
|
121
|
-
const sessionId = result.sessionId;
|
|
122
|
-
if (sessionId === undefined) {
|
|
123
|
-
throw new Error('spawn succeeded but no session id returned');
|
|
124
|
-
}
|
|
125
|
-
out(sessionId);
|
|
126
|
-
info(result.message);
|
|
127
|
-
hint(`await with: crtr agent await ${sessionId}`);
|
|
128
|
-
}
|
|
129
|
-
catch (e) {
|
|
130
|
-
handleError(e);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
agent
|
|
134
|
-
.command('await <id>')
|
|
135
|
-
.description('block until agent <id> calls `crtr agent submit`; prints submitted content')
|
|
136
|
-
.option('--timeout <seconds>', `seconds before giving up (default ${DEFAULT_PANE_OPTS.timeoutMs / 1000})`)
|
|
137
|
-
.option('--keep-pane', 'do not kill the child pane after submission')
|
|
138
|
-
.action(async (id, options) => {
|
|
139
|
-
try {
|
|
140
|
-
let timeoutMs = DEFAULT_PANE_OPTS.timeoutMs;
|
|
141
|
-
if (options.timeout !== undefined) {
|
|
142
|
-
const n = Number(options.timeout);
|
|
143
|
-
if (!Number.isFinite(n) || n <= 0) {
|
|
144
|
-
throw usage(`--timeout must be a positive number (got: ${options.timeout})`);
|
|
145
|
-
}
|
|
146
|
-
timeoutMs = Math.floor(n * 1000);
|
|
147
|
-
}
|
|
148
|
-
const killPane = options.keepPane !== true;
|
|
149
|
-
const result = await awaitSession(id, { timeoutMs, killPane });
|
|
150
|
-
if (result.status === 'submitted') {
|
|
151
|
-
process.stdout.write(result.content);
|
|
152
|
-
if (!result.content.endsWith('\n'))
|
|
153
|
-
process.stdout.write('\n');
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (result.status === 'timeout') {
|
|
157
|
-
throw new Error(`agent ${id} did not submit before timeout`);
|
|
158
|
-
}
|
|
159
|
-
if (result.status === 'pane-closed') {
|
|
160
|
-
throw new Error(`agent ${id} pane closed before submission`);
|
|
161
|
-
}
|
|
162
|
-
throw new Error(`agent ${id} await failed: ${result.status}`);
|
|
163
|
-
}
|
|
164
|
-
catch (e) {
|
|
165
|
-
handleError(e);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
agent
|
|
169
|
-
.command('submit [content]')
|
|
170
|
-
.description('inside a crtr-spawned session, deliver content back to the parent (uses $CRTR_PIPE)')
|
|
171
|
-
.action(async (content) => {
|
|
172
|
-
try {
|
|
173
|
-
const sessionDir = process.env.CRTR_PIPE;
|
|
174
|
-
if (sessionDir === undefined || sessionDir === '') {
|
|
175
|
-
throw usage('not in a crtr session — $CRTR_PIPE is not set. ' +
|
|
176
|
-
'`crtr agent submit` is only valid inside a crtr-spawned pane.');
|
|
177
|
-
}
|
|
178
|
-
if (!existsSync(sessionDir)) {
|
|
179
|
-
throw notFound(`session directory not found: ${sessionDir} (the parent may have timed out)`);
|
|
180
|
-
}
|
|
181
|
-
let body;
|
|
182
|
-
if (content !== undefined) {
|
|
183
|
-
body = content;
|
|
184
|
-
}
|
|
185
|
-
else if (!process.stdin.isTTY) {
|
|
186
|
-
body = await readStdin();
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
throw usage('no content provided. Pass content as a positional arg or pipe via stdin.');
|
|
190
|
-
}
|
|
191
|
-
if (body.trim() === '') {
|
|
192
|
-
throw usage('content is empty');
|
|
193
|
-
}
|
|
194
|
-
submitToSession(sessionDir, body);
|
|
195
|
-
out('submitted');
|
|
196
|
-
}
|
|
197
|
-
catch (e) {
|
|
198
|
-
handleError(e);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
agent
|
|
202
|
-
.command('plan')
|
|
203
|
-
.description('launch a planner for an approved spec in a new pane; close current pane')
|
|
204
|
-
.requiredOption('--spec <name>', 'name of the spec to plan')
|
|
205
|
-
.option('--kill-after <seconds>', `seconds before closing the originating pane (default ${DEFAULT_KILL_SECS})`, String(DEFAULT_KILL_SECS))
|
|
206
|
-
.action((options) => {
|
|
207
|
-
try {
|
|
208
|
-
const specPath = artifactPath('specs', options.spec);
|
|
209
|
-
if (!pathExists(specPath)) {
|
|
210
|
-
throw notFound(`spec not found: ${options.spec} (looked at ${specPath})`);
|
|
211
|
-
}
|
|
212
|
-
const killAfter = parseKillAfter(options.killAfter);
|
|
213
|
-
const result = spawnAndDetach({
|
|
214
|
-
...baseDetachOpts(),
|
|
215
|
-
prompt: planHandoffPrompt(specPath, artifactsRoot('plans')),
|
|
216
|
-
killAfterSeconds: killAfter,
|
|
217
|
-
});
|
|
218
|
-
emitDetach(result, `planner for spec ${options.spec}`);
|
|
219
|
-
}
|
|
220
|
-
catch (e) {
|
|
221
|
-
handleError(e);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
agent
|
|
225
|
-
.command('implement')
|
|
226
|
-
.description('launch an implementer for an approved plan in a new pane; close current pane')
|
|
227
|
-
.requiredOption('--plan <name>', 'name of the plan to implement')
|
|
228
|
-
.option('--kill-after <seconds>', `seconds before closing the originating pane (default ${DEFAULT_KILL_SECS})`, String(DEFAULT_KILL_SECS))
|
|
229
|
-
.action((options) => {
|
|
230
|
-
try {
|
|
231
|
-
const planPath = artifactPath('plans', options.plan);
|
|
232
|
-
if (!pathExists(planPath)) {
|
|
233
|
-
throw notFound(`plan not found: ${options.plan} (looked at ${planPath})`);
|
|
234
|
-
}
|
|
235
|
-
const killAfter = parseKillAfter(options.killAfter);
|
|
236
|
-
const result = spawnAndDetach({
|
|
237
|
-
...baseDetachOpts(),
|
|
238
|
-
prompt: implementHandoffPrompt(planPath),
|
|
239
|
-
killAfterSeconds: killAfter,
|
|
240
|
-
});
|
|
241
|
-
emitDetach(result, `implementer for plan ${options.plan}`);
|
|
242
|
-
}
|
|
243
|
-
catch (e) {
|
|
244
|
-
handleError(e);
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
agent
|
|
248
|
-
.command('review')
|
|
249
|
-
.description('launch a code reviewer of the working tree in a new pane; close current pane')
|
|
250
|
-
.option('--kill-after <seconds>', `seconds before closing the originating pane (default ${DEFAULT_KILL_SECS})`, String(DEFAULT_KILL_SECS))
|
|
251
|
-
.action((options) => {
|
|
252
|
-
try {
|
|
253
|
-
const killAfter = parseKillAfter(options.killAfter);
|
|
254
|
-
const result = spawnAndDetach({
|
|
255
|
-
...baseDetachOpts(),
|
|
256
|
-
prompt: reviewHandoffPrompt(),
|
|
257
|
-
killAfterSeconds: killAfter,
|
|
258
|
-
});
|
|
259
|
-
emitDetach(result, 'code reviewer');
|
|
260
|
-
}
|
|
261
|
-
catch (e) {
|
|
262
|
-
handleError(e);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
package/dist/commands/config.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { readConfig, writeConfig, configPath } from '../core/config.js';
|
|
2
|
-
import { usage, notFound } from '../core/errors.js';
|
|
3
|
-
import { out, jsonOut, handleError } from '../core/output.js';
|
|
4
|
-
import { scopeRoot, listScopes } from '../core/scope.js';
|
|
5
|
-
const TOP_LEVEL_KEYS = new Set([
|
|
6
|
-
'auto_update',
|
|
7
|
-
'marketplaces',
|
|
8
|
-
'plugins',
|
|
9
|
-
'max_panes_per_window',
|
|
10
|
-
]);
|
|
11
|
-
function getNestedValue(obj, key) {
|
|
12
|
-
const parts = key.split('.');
|
|
13
|
-
let current = obj;
|
|
14
|
-
for (const part of parts) {
|
|
15
|
-
if (current === null || typeof current !== 'object')
|
|
16
|
-
return undefined;
|
|
17
|
-
current = current[part];
|
|
18
|
-
}
|
|
19
|
-
return current;
|
|
20
|
-
}
|
|
21
|
-
function parseValue(raw) {
|
|
22
|
-
if (raw === 'true')
|
|
23
|
-
return true;
|
|
24
|
-
if (raw === 'false')
|
|
25
|
-
return false;
|
|
26
|
-
if (/^-?\d+$/.test(raw))
|
|
27
|
-
return parseInt(raw, 10);
|
|
28
|
-
return raw;
|
|
29
|
-
}
|
|
30
|
-
function setNestedValue(cfg, key, value) {
|
|
31
|
-
const parts = key.split('.');
|
|
32
|
-
const topKey = parts[0];
|
|
33
|
-
if (!TOP_LEVEL_KEYS.has(topKey)) {
|
|
34
|
-
throw usage(`unknown config key: ${topKey} (expected: ${[...TOP_LEVEL_KEYS].join('|')})`);
|
|
35
|
-
}
|
|
36
|
-
if (key === 'auto_update.content') {
|
|
37
|
-
if (value !== 'notify' && value !== 'apply' && value !== false) {
|
|
38
|
-
throw usage(`auto_update.content must be 'notify', 'apply', or false`);
|
|
39
|
-
}
|
|
40
|
-
cfg.auto_update.content = value;
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
if (key === 'auto_update.crtr') {
|
|
44
|
-
const coerced = value === true ? 'notify' : value;
|
|
45
|
-
if (coerced !== 'notify' && coerced !== 'apply' && coerced !== false) {
|
|
46
|
-
throw usage(`auto_update.crtr must be 'notify', 'apply', or false`);
|
|
47
|
-
}
|
|
48
|
-
cfg.auto_update.crtr = coerced;
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (key === 'max_panes_per_window') {
|
|
52
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {
|
|
53
|
-
throw usage(`max_panes_per_window must be an integer >= 1`);
|
|
54
|
-
}
|
|
55
|
-
cfg.max_panes_per_window = Math.floor(value);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (parts.length === 1) {
|
|
59
|
-
cfg[topKey] = value;
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (parts.length === 2 && topKey === 'auto_update') {
|
|
63
|
-
const subKey = parts[1];
|
|
64
|
-
cfg.auto_update[subKey] = value;
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
throw usage(`unsupported key path for set: ${key}`);
|
|
68
|
-
}
|
|
69
|
-
export function registerConfigCommands(program) {
|
|
70
|
-
const config = program
|
|
71
|
-
.command('config')
|
|
72
|
-
.description('read and write crtr configuration');
|
|
73
|
-
config
|
|
74
|
-
.command('get <key>')
|
|
75
|
-
.description('print a config value by dotted key (default scope: user)')
|
|
76
|
-
.option('--scope <scope>', 'user|project (default: user)')
|
|
77
|
-
.action(async (key, opts) => {
|
|
78
|
-
try {
|
|
79
|
-
const scope = opts.scope === 'project' ? 'project' : 'user';
|
|
80
|
-
const cfg = readConfig(scope);
|
|
81
|
-
const value = getNestedValue(cfg, key);
|
|
82
|
-
if (value === undefined) {
|
|
83
|
-
throw notFound(`config key not found: ${key}`);
|
|
84
|
-
}
|
|
85
|
-
if (typeof value === 'object') {
|
|
86
|
-
out(JSON.stringify(value));
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
out(String(value));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
catch (e) {
|
|
93
|
-
handleError(e);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
config
|
|
97
|
-
.command('set <key> <value>')
|
|
98
|
-
.description('set a config value by dotted key (default scope: user)')
|
|
99
|
-
.option('--scope <scope>', 'user|project (default: user)')
|
|
100
|
-
.action(async (key, rawValue, opts) => {
|
|
101
|
-
try {
|
|
102
|
-
const scope = opts.scope === 'project' ? 'project' : 'user';
|
|
103
|
-
const cfg = readConfig(scope);
|
|
104
|
-
const parsed = parseValue(rawValue);
|
|
105
|
-
setNestedValue(cfg, key, parsed);
|
|
106
|
-
writeConfig(scope, cfg);
|
|
107
|
-
}
|
|
108
|
-
catch (e) {
|
|
109
|
-
handleError(e);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
config
|
|
113
|
-
.command('path')
|
|
114
|
-
.description('print the absolute path(s) to config.json')
|
|
115
|
-
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
116
|
-
.option('--json', 'emit JSON')
|
|
117
|
-
.action(async (opts) => {
|
|
118
|
-
try {
|
|
119
|
-
const scopes = listScopes(opts.scope);
|
|
120
|
-
if (opts.json) {
|
|
121
|
-
const paths = scopes
|
|
122
|
-
.map((s) => {
|
|
123
|
-
const root = scopeRoot(s);
|
|
124
|
-
if (!root)
|
|
125
|
-
return null;
|
|
126
|
-
const p = configPath(s);
|
|
127
|
-
if (!p)
|
|
128
|
-
return null;
|
|
129
|
-
return { scope: s, path: p };
|
|
130
|
-
})
|
|
131
|
-
.filter((x) => x !== null);
|
|
132
|
-
jsonOut({ paths });
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
for (const s of scopes) {
|
|
136
|
-
const p = configPath(s);
|
|
137
|
-
if (p) {
|
|
138
|
-
out(p);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch (e) {
|
|
143
|
-
handleError(e, { json: opts.json });
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
}
|
package/dist/commands/doctor.js
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { SKILL_TYPES, isSkillType } from '../types.js';
|
|
3
|
-
import { out, jsonOut, handleError, stdoutColor } from '../core/output.js';
|
|
4
|
-
import { scopeRoot, pluginsDir, marketplacesDir, listScopes, builtinSkillsRoot } from '../core/scope.js';
|
|
5
|
-
import { readConfig, updateConfig } from '../core/config.js';
|
|
6
|
-
import { listInstalledPlugins, listSkillsInPlugin } from '../core/resolver.js';
|
|
7
|
-
import { pathExists, listDirs, removePath, readText } from '../core/fs-utils.js';
|
|
8
|
-
import { readPluginManifest, readMarketplaceManifest } from '../core/manifest.js';
|
|
9
|
-
import { parseFrontmatter } from '../core/frontmatter.js';
|
|
10
|
-
import { lsRemote } from '../core/git.js';
|
|
11
|
-
import { ExitCode } from '../types.js';
|
|
12
|
-
function pass(scope, name, message) {
|
|
13
|
-
return { scope, name, status: 'pass', message };
|
|
14
|
-
}
|
|
15
|
-
function fail(scope, name, message) {
|
|
16
|
-
return { scope, name, status: 'fail', message };
|
|
17
|
-
}
|
|
18
|
-
function warn(scope, name, message) {
|
|
19
|
-
return { scope, name, status: 'warn', message };
|
|
20
|
-
}
|
|
21
|
-
function readRawTypeField(skillPath) {
|
|
22
|
-
const content = readText(skillPath);
|
|
23
|
-
const { raw } = parseFrontmatter(content);
|
|
24
|
-
if (!raw)
|
|
25
|
-
return undefined;
|
|
26
|
-
const m = raw.match(/^type:\s*(.+?)\s*$/m);
|
|
27
|
-
if (!m)
|
|
28
|
-
return undefined;
|
|
29
|
-
let v = m[1].trim();
|
|
30
|
-
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
31
|
-
v = v.slice(1, -1);
|
|
32
|
-
}
|
|
33
|
-
return v;
|
|
34
|
-
}
|
|
35
|
-
function runChecksForBuiltin() {
|
|
36
|
-
const root = builtinSkillsRoot();
|
|
37
|
-
const plugins = listInstalledPlugins('builtin');
|
|
38
|
-
if (plugins.length === 0) {
|
|
39
|
-
return [fail('builtin', 'builtin:crtr:root', `builtin-skills root missing or has no valid plugin.json: ${root}`)];
|
|
40
|
-
}
|
|
41
|
-
const results = [
|
|
42
|
-
pass('builtin', 'builtin:crtr:root', `builtin-skills root present: ${root}`),
|
|
43
|
-
];
|
|
44
|
-
for (const plugin of plugins) {
|
|
45
|
-
results.push(pass('builtin', `builtin:${plugin.name}:manifest`, `manifest valid`));
|
|
46
|
-
const skills = listSkillsInPlugin(plugin);
|
|
47
|
-
for (const skill of skills) {
|
|
48
|
-
if (!skill.frontmatter.name) {
|
|
49
|
-
results.push(fail('builtin', `builtin:${plugin.name}:skill:${skill.name}:frontmatter`, `frontmatter missing or name field empty`));
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
results.push(pass('builtin', `builtin:${plugin.name}:skill:${skill.name}:frontmatter`, `frontmatter valid`));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return results;
|
|
57
|
-
}
|
|
58
|
-
function runChecksForScope(scope, opts) {
|
|
59
|
-
if (scope === 'builtin')
|
|
60
|
-
return runChecksForBuiltin();
|
|
61
|
-
const results = [];
|
|
62
|
-
const root = scopeRoot(scope);
|
|
63
|
-
if (!root)
|
|
64
|
-
return results;
|
|
65
|
-
const cfg = readConfig(scope);
|
|
66
|
-
// Check: every config marketplace entry has a corresponding directory
|
|
67
|
-
const mktDir = marketplacesDir(scope);
|
|
68
|
-
for (const name of Object.keys(cfg.marketplaces)) {
|
|
69
|
-
if (!mktDir) {
|
|
70
|
-
results.push(fail(scope, `marketplace:${name}:dir`, `marketplaces directory unavailable`));
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const dir = join(mktDir, name);
|
|
74
|
-
if (!pathExists(dir)) {
|
|
75
|
-
if (opts.fix) {
|
|
76
|
-
updateConfig(scope, (c) => {
|
|
77
|
-
delete c.marketplaces[name];
|
|
78
|
-
});
|
|
79
|
-
results.push({ scope, name: `marketplace:${name}:dir`, status: 'fail', message: `directory missing — removed stale config entry`, fixed: true });
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
results.push(fail(scope, `marketplace:${name}:dir`, `directory missing: ${dir}`));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
results.push(pass(scope, `marketplace:${name}:dir`, `directory exists`));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// Check: every config plugin entry has a corresponding directory
|
|
90
|
-
const plugDir = pluginsDir(scope);
|
|
91
|
-
for (const name of Object.keys(cfg.plugins)) {
|
|
92
|
-
if (!plugDir) {
|
|
93
|
-
results.push(fail(scope, `plugin:${name}:dir`, `plugins directory unavailable`));
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const dir = join(plugDir, name);
|
|
97
|
-
if (!pathExists(dir)) {
|
|
98
|
-
if (opts.fix) {
|
|
99
|
-
updateConfig(scope, (c) => {
|
|
100
|
-
delete c.plugins[name];
|
|
101
|
-
});
|
|
102
|
-
results.push({ scope, name: `plugin:${name}:dir`, status: 'fail', message: `directory missing — removed stale config entry`, fixed: true });
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
results.push(fail(scope, `plugin:${name}:dir`, `directory missing: ${dir}`));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
results.push(pass(scope, `plugin:${name}:dir`, `directory exists`));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Check: every marketplace directory has a valid manifest
|
|
113
|
-
if (mktDir && pathExists(mktDir)) {
|
|
114
|
-
for (const name of listDirs(mktDir)) {
|
|
115
|
-
const dir = join(mktDir, name);
|
|
116
|
-
const manifest = readMarketplaceManifest(dir);
|
|
117
|
-
if (!manifest) {
|
|
118
|
-
if (opts.fix) {
|
|
119
|
-
removePath(dir);
|
|
120
|
-
results.push({ scope, name: `marketplace:${name}:manifest`, status: 'fail', message: `no valid marketplace.json — removed dangling directory`, fixed: true });
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
results.push(fail(scope, `marketplace:${name}:manifest`, `no valid marketplace.json in ${dir}`));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
results.push(pass(scope, `marketplace:${name}:manifest`, `manifest valid`));
|
|
128
|
-
// Check: marketplace plugins[].source paths resolve (relative paths only)
|
|
129
|
-
for (const entry of manifest.plugins) {
|
|
130
|
-
if (entry.source.startsWith('http://') || entry.source.startsWith('https://') || entry.source.startsWith('git@')) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const resolved = join(dir, entry.source);
|
|
134
|
-
if (!pathExists(resolved)) {
|
|
135
|
-
results.push(fail(scope, `marketplace:${name}:plugin-source:${entry.name}`, `source path does not resolve: ${resolved}`));
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
results.push(pass(scope, `marketplace:${name}:plugin-source:${entry.name}`, `source resolves`));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// Check: every plugin directory has a valid manifest + no duplicate names
|
|
145
|
-
const seenPluginNames = new Map();
|
|
146
|
-
if (plugDir && pathExists(plugDir)) {
|
|
147
|
-
for (const name of listDirs(plugDir)) {
|
|
148
|
-
const dir = join(plugDir, name);
|
|
149
|
-
const manifest = readPluginManifest(dir);
|
|
150
|
-
if (!manifest) {
|
|
151
|
-
if (opts.fix) {
|
|
152
|
-
removePath(dir);
|
|
153
|
-
results.push({ scope, name: `plugin:${name}:manifest`, status: 'fail', message: `no valid plugin.json — removed dangling directory`, fixed: true });
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
results.push(fail(scope, `plugin:${name}:manifest`, `no valid plugin.json in ${dir}`));
|
|
157
|
-
}
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
results.push(pass(scope, `plugin:${name}:manifest`, `manifest valid`));
|
|
161
|
-
// Duplicate names
|
|
162
|
-
if (seenPluginNames.has(name)) {
|
|
163
|
-
results.push(fail(scope, `plugin:${name}:duplicate`, `duplicate plugin name within scope (also at ${seenPluginNames.get(name)})`));
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
seenPluginNames.set(name, dir);
|
|
167
|
-
}
|
|
168
|
-
// Check: skills frontmatter parses and name matches directory
|
|
169
|
-
const plugin = listInstalledPlugins(scope).find((p) => p.name === name);
|
|
170
|
-
if (plugin) {
|
|
171
|
-
const skills = listSkillsInPlugin(plugin);
|
|
172
|
-
for (const skill of skills) {
|
|
173
|
-
if (!skill.frontmatter.name) {
|
|
174
|
-
results.push(fail(scope, `plugin:${name}:skill:${skill.name}:frontmatter`, `frontmatter missing or name field empty`));
|
|
175
|
-
}
|
|
176
|
-
else if (skill.frontmatter.name !== skill.name) {
|
|
177
|
-
results.push(warn(scope, `plugin:${name}:skill:${skill.name}:frontmatter`, `name mismatch: frontmatter says "${skill.frontmatter.name}", directory is "${skill.name}"`));
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
results.push(pass(scope, `plugin:${name}:skill:${skill.name}:frontmatter`, `frontmatter valid`));
|
|
181
|
-
}
|
|
182
|
-
const typeCheckName = `plugin:${name}:skill:${skill.name}:type`;
|
|
183
|
-
const rawType = readRawTypeField(skill.path);
|
|
184
|
-
if (rawType === undefined) {
|
|
185
|
-
results.push(warn(scope, typeCheckName, `missing type field — add one of: ${SKILL_TYPES.join(' | ')}`));
|
|
186
|
-
}
|
|
187
|
-
else if (!isSkillType(rawType)) {
|
|
188
|
-
results.push(fail(scope, typeCheckName, `invalid type "${rawType}" — valid: ${SKILL_TYPES.join(' | ')}`));
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
results.push(pass(scope, typeCheckName, `type: ${rawType}`));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
// Git remote check (slow, opt-in)
|
|
196
|
-
if (opts.remote && manifest.source) {
|
|
197
|
-
const res = lsRemote(manifest.source);
|
|
198
|
-
if (res.status !== 0) {
|
|
199
|
-
results.push(fail(scope, `plugin:${name}:remote`, `git remote unreachable: ${manifest.source}`));
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
results.push(pass(scope, `plugin:${name}:remote`, `git remote reachable`));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return results;
|
|
208
|
-
}
|
|
209
|
-
function printResults(results) {
|
|
210
|
-
const byScopeMap = new Map();
|
|
211
|
-
for (const r of results) {
|
|
212
|
-
const existing = byScopeMap.get(r.scope);
|
|
213
|
-
if (existing) {
|
|
214
|
-
existing.push(r);
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
byScopeMap.set(r.scope, [r]);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
for (const [scope, checks] of byScopeMap) {
|
|
221
|
-
out(stdoutColor.bold(`[${scope}]`));
|
|
222
|
-
for (const c of checks) {
|
|
223
|
-
if (c.status === 'pass') {
|
|
224
|
-
out(stdoutColor.green(` PASS ${c.name}: ${c.message}`));
|
|
225
|
-
}
|
|
226
|
-
else if (c.status === 'warn') {
|
|
227
|
-
out(stdoutColor.yellow(` WARN ${c.name}: ${c.message}`));
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
const fixSuffix = c.fixed ? ' (fixed)' : '';
|
|
231
|
-
out(stdoutColor.red(` FAIL ${c.name}: ${c.message}${fixSuffix}`));
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
export function registerDoctorCommand(program) {
|
|
237
|
-
program
|
|
238
|
-
.command('doctor')
|
|
239
|
-
.description('diagnose missing manifests, broken config entries, and skill frontmatter drift')
|
|
240
|
-
.option('--fix', 'drop stale config entries and prune directories without manifests')
|
|
241
|
-
.option('--remote', 'check git remotes with ls-remote (slow)')
|
|
242
|
-
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
243
|
-
.option('--json', 'emit JSON')
|
|
244
|
-
.action(async (opts) => {
|
|
245
|
-
try {
|
|
246
|
-
const scopes = listScopes(opts.scope);
|
|
247
|
-
const fix = opts.fix === true;
|
|
248
|
-
const remote = opts.remote === true;
|
|
249
|
-
const allResults = [];
|
|
250
|
-
for (const scope of scopes) {
|
|
251
|
-
const results = runChecksForScope(scope, { fix, remote });
|
|
252
|
-
allResults.push(...results);
|
|
253
|
-
}
|
|
254
|
-
if (opts.json) {
|
|
255
|
-
jsonOut({ checks: allResults });
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
printResults(allResults);
|
|
259
|
-
const anyUnresolvedFail = allResults.some((r) => r.status === 'fail' && r.fixed !== true);
|
|
260
|
-
if (anyUnresolvedFail) {
|
|
261
|
-
process.exit(ExitCode.GENERAL);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
catch (e) {
|
|
265
|
-
handleError(e, { json: opts.json });
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
}
|