@crouton-kit/crouter 0.1.1 → 0.1.3
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/bin/crouter +2 -0
- package/bin/crtr +2 -0
- package/dist/cli.js +36 -4
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +134 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +216 -0
- package/dist/commands/marketplace.d.ts +2 -0
- package/dist/commands/marketplace.js +365 -0
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.js +9 -0
- package/dist/commands/plugin.d.ts +2 -0
- package/dist/commands/plugin.js +364 -0
- package/dist/commands/skill.d.ts +2 -0
- package/dist/commands/skill.js +405 -0
- package/dist/commands/spec.d.ts +2 -0
- package/dist/commands/spec.js +9 -0
- package/dist/commands/update.d.ts +4 -0
- package/dist/commands/update.js +140 -0
- package/dist/core/artifact.d.ts +14 -0
- package/dist/core/artifact.js +187 -0
- package/dist/core/auto-update.d.ts +1 -0
- package/dist/core/auto-update.js +86 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.js +96 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +28 -0
- package/dist/core/frontmatter.d.ts +8 -0
- package/dist/core/frontmatter.js +156 -0
- package/dist/core/fs-utils.d.ts +18 -0
- package/dist/core/fs-utils.js +115 -0
- package/dist/core/git.d.ts +18 -0
- package/dist/core/git.js +71 -0
- package/dist/core/manifest.d.ts +5 -0
- package/dist/core/manifest.js +15 -0
- package/dist/core/output.d.ts +35 -0
- package/dist/core/output.js +99 -0
- package/dist/core/resolver.d.ts +28 -0
- package/dist/core/resolver.js +228 -0
- package/dist/core/scope.d.ts +12 -0
- package/dist/core/scope.js +87 -0
- package/dist/prompts/plan.d.ts +1 -0
- package/dist/prompts/plan.js +106 -0
- package/dist/prompts/skill.d.ts +1 -0
- package/dist/prompts/skill.js +49 -0
- package/dist/prompts/spec.d.ts +1 -0
- package/dist/prompts/spec.js +113 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.js +33 -0
- package/package.json +8 -5
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { SKILL_ENTRY_FILE, SKILLS_DIR, } from '../types.js';
|
|
4
|
+
import { skillConfigKey } from '../types.js';
|
|
5
|
+
import { notFound, usage, general } from '../core/errors.js';
|
|
6
|
+
import { out, hint, info, jsonOut, handleError, } from '../core/output.js';
|
|
7
|
+
import { listScopes, requireScopeRoot, resolveScopeArg, projectScopeRoot, } from '../core/scope.js';
|
|
8
|
+
import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, } from '../core/resolver.js';
|
|
9
|
+
import { updateConfig, ensureScopeInitialized } from '../core/config.js';
|
|
10
|
+
import { parseFrontmatter, serializeFrontmatter } from '../core/frontmatter.js';
|
|
11
|
+
import { ensureDir, pathExists, readText, walkFiles } from '../core/fs-utils.js';
|
|
12
|
+
import { skillPrompt } from '../prompts/skill.js';
|
|
13
|
+
const KNOWN_VERBS = new Set([
|
|
14
|
+
'list',
|
|
15
|
+
'show',
|
|
16
|
+
'path',
|
|
17
|
+
'grep',
|
|
18
|
+
'new',
|
|
19
|
+
'where',
|
|
20
|
+
'enable',
|
|
21
|
+
'disable',
|
|
22
|
+
'search',
|
|
23
|
+
]);
|
|
24
|
+
const AUTHORING_GUIDE_SKILL = 'authoring-skills';
|
|
25
|
+
function buildShowFooter(skillPath) {
|
|
26
|
+
return (`crtr: edit this skill directly at ${skillPath} — ` +
|
|
27
|
+
`for SKILL.md authoring guidance run \`crtr skill ${AUTHORING_GUIDE_SKILL}\``);
|
|
28
|
+
}
|
|
29
|
+
function wrapSkill(name, path, content) {
|
|
30
|
+
return `<skill name="${name}" path="${path}">\n${content.endsWith('\n') ? content : content + '\n'}</skill>`;
|
|
31
|
+
}
|
|
32
|
+
export function registerSkillCommands(program) {
|
|
33
|
+
const skill = program
|
|
34
|
+
.command('skill [nameOrVerb] [rest...]')
|
|
35
|
+
.description('manage and inspect skills')
|
|
36
|
+
.option('--frontmatter', 'include YAML frontmatter in the printed body')
|
|
37
|
+
.action(async (nameOrVerb, _rest, opts) => {
|
|
38
|
+
if (nameOrVerb === undefined) {
|
|
39
|
+
out(skillPrompt());
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!KNOWN_VERBS.has(nameOrVerb)) {
|
|
43
|
+
try {
|
|
44
|
+
const skillObj = resolveSkill(nameOrVerb);
|
|
45
|
+
const content = readText(skillObj.path);
|
|
46
|
+
const body = opts.frontmatter ? content : parseFrontmatter(content).body;
|
|
47
|
+
out(wrapSkill(skillObj.name, skillObj.path, body));
|
|
48
|
+
hint(buildShowFooter(skillObj.path));
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
handleError(e);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// list
|
|
56
|
+
skill
|
|
57
|
+
.command('list')
|
|
58
|
+
.description('list installed skills (disabled hidden unless -a)')
|
|
59
|
+
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
60
|
+
.option('--plugin <name>', 'filter by plugin name')
|
|
61
|
+
.option('-a, --all', 'include disabled skills')
|
|
62
|
+
.option('--json', 'emit JSON')
|
|
63
|
+
.action(async (opts) => {
|
|
64
|
+
try {
|
|
65
|
+
const scopes = listScopes(opts.scope);
|
|
66
|
+
const skills = scopes
|
|
67
|
+
.flatMap((s) => listAllSkills(s))
|
|
68
|
+
.filter((sk) => {
|
|
69
|
+
if (opts.plugin !== undefined && sk.plugin !== opts.plugin)
|
|
70
|
+
return false;
|
|
71
|
+
if (!opts.all && !sk.enabled)
|
|
72
|
+
return false;
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
if (opts.json) {
|
|
76
|
+
jsonOut({
|
|
77
|
+
skills: skills.map((sk) => ({
|
|
78
|
+
name: sk.name,
|
|
79
|
+
plugin: sk.plugin,
|
|
80
|
+
scope: sk.scope,
|
|
81
|
+
path: sk.path,
|
|
82
|
+
description: sk.frontmatter.description,
|
|
83
|
+
enabled: sk.enabled,
|
|
84
|
+
disabled_in: sk.disabledIn,
|
|
85
|
+
})),
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const sk of skills) {
|
|
90
|
+
const desc = sk.frontmatter.description !== undefined ? sk.frontmatter.description : '';
|
|
91
|
+
const marker = sk.enabled ? '' : ` [disabled${sk.disabledIn ? `@${sk.disabledIn}` : ''}]`;
|
|
92
|
+
const line = `${sk.scope}:${sk.plugin}/${sk.name}${marker}${desc ? ` — ${desc}` : ''}`;
|
|
93
|
+
out(line);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
handleError(e, { json: opts.json });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// show
|
|
101
|
+
skill
|
|
102
|
+
.command('show <name>')
|
|
103
|
+
.description('print SKILL.md body to stdout (default verb)')
|
|
104
|
+
.option('--scope <scope>', 'user|project')
|
|
105
|
+
.option('--plugin <name>', 'filter by plugin name')
|
|
106
|
+
.option('--frontmatter', 'include YAML frontmatter in the printed body')
|
|
107
|
+
.option('--json', 'emit JSON')
|
|
108
|
+
.action(async (name, opts) => {
|
|
109
|
+
try {
|
|
110
|
+
const scopeArg = resolveScopeArg(opts.scope);
|
|
111
|
+
const resolveOpts = scopeArg !== 'all' ? { scope: scopeArg } : {};
|
|
112
|
+
if (opts.plugin !== undefined) {
|
|
113
|
+
Object.assign(resolveOpts, { pluginFilter: opts.plugin });
|
|
114
|
+
}
|
|
115
|
+
const skillObj = resolveSkill(name, resolveOpts);
|
|
116
|
+
const content = readText(skillObj.path);
|
|
117
|
+
const body = opts.frontmatter ? content : parseFrontmatter(content).body;
|
|
118
|
+
if (opts.json) {
|
|
119
|
+
jsonOut({
|
|
120
|
+
name: skillObj.name,
|
|
121
|
+
plugin: skillObj.plugin,
|
|
122
|
+
scope: skillObj.scope,
|
|
123
|
+
path: skillObj.path,
|
|
124
|
+
content,
|
|
125
|
+
authoring_guide_command: `crtr skill ${AUTHORING_GUIDE_SKILL}`,
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
out(wrapSkill(skillObj.name, skillObj.path, body));
|
|
130
|
+
hint(buildShowFooter(skillObj.path));
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
handleError(e, { json: opts.json });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// path
|
|
137
|
+
skill
|
|
138
|
+
.command('path <name>')
|
|
139
|
+
.description('print absolute path to SKILL.md')
|
|
140
|
+
.option('--scope <scope>', 'user|project')
|
|
141
|
+
.option('--plugin <name>', 'filter by plugin name')
|
|
142
|
+
.action(async (name, opts) => {
|
|
143
|
+
try {
|
|
144
|
+
const scopeArg = resolveScopeArg(opts.scope);
|
|
145
|
+
const resolveOpts = scopeArg !== 'all' ? { scope: scopeArg } : {};
|
|
146
|
+
if (opts.plugin !== undefined) {
|
|
147
|
+
Object.assign(resolveOpts, { pluginFilter: opts.plugin });
|
|
148
|
+
}
|
|
149
|
+
const skillObj = resolveSkill(name, resolveOpts);
|
|
150
|
+
out(skillObj.path);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
handleError(e);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// grep
|
|
157
|
+
skill
|
|
158
|
+
.command('grep <pattern>')
|
|
159
|
+
.description('search skill file contents for a regex pattern')
|
|
160
|
+
.option('--scope <scope>', 'user|project|all')
|
|
161
|
+
.option('--plugin <name>', 'filter by plugin name')
|
|
162
|
+
.option('--json', 'emit JSON')
|
|
163
|
+
.action(async (pattern, opts) => {
|
|
164
|
+
try {
|
|
165
|
+
let regex;
|
|
166
|
+
try {
|
|
167
|
+
regex = new RegExp(pattern);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw usage(`invalid regex pattern: ${pattern}`);
|
|
171
|
+
}
|
|
172
|
+
const scopes = listScopes(opts.scope);
|
|
173
|
+
const pluginSkillsDirs = [];
|
|
174
|
+
for (const s of scopes) {
|
|
175
|
+
for (const plugin of listInstalledPlugins(s)) {
|
|
176
|
+
if (!plugin.enabled)
|
|
177
|
+
continue;
|
|
178
|
+
if (opts.plugin !== undefined && plugin.name !== opts.plugin)
|
|
179
|
+
continue;
|
|
180
|
+
pluginSkillsDirs.push(join(plugin.root, SKILLS_DIR));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const matchLines = [];
|
|
184
|
+
for (const skillsDir of pluginSkillsDirs) {
|
|
185
|
+
const files = walkFiles(skillsDir);
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
const content = readText(file);
|
|
188
|
+
const lines = content.split('\n');
|
|
189
|
+
lines.forEach((lineText, idx) => {
|
|
190
|
+
if (regex.test(lineText)) {
|
|
191
|
+
matchLines.push({ path: file, line: idx + 1, text: lineText });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (opts.json) {
|
|
197
|
+
jsonOut({ matches: matchLines });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
for (const m of matchLines) {
|
|
201
|
+
out(`${m.path}:${m.line}: ${m.text}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
handleError(e, { json: opts.json });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// new
|
|
209
|
+
skill
|
|
210
|
+
.command('new <qualifier>')
|
|
211
|
+
.description('scaffold a new skill as <plugin>:<name>')
|
|
212
|
+
.option('--scope <scope>', 'user|project (default: project then user)')
|
|
213
|
+
.option('--description <text>', 'skill description for frontmatter')
|
|
214
|
+
.action(async (qualifier, opts) => {
|
|
215
|
+
try {
|
|
216
|
+
if (!qualifier.includes(':')) {
|
|
217
|
+
throw usage('qualifier must be in the form <plugin>:<name> (e.g. authoring:my-skill)');
|
|
218
|
+
}
|
|
219
|
+
const { plugin: pluginName, name: skillName } = parseSkillQualifier(qualifier);
|
|
220
|
+
if (!pluginName) {
|
|
221
|
+
throw usage('qualifier must be in the form <plugin>:<name>');
|
|
222
|
+
}
|
|
223
|
+
const scopeArg = opts.scope !== undefined ? resolveScopeArg(opts.scope) : undefined;
|
|
224
|
+
let plugin;
|
|
225
|
+
if (scopeArg !== undefined && scopeArg !== 'all') {
|
|
226
|
+
plugin = findPluginByName(pluginName, scopeArg);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
plugin = findPluginByName(pluginName);
|
|
230
|
+
}
|
|
231
|
+
if (!plugin) {
|
|
232
|
+
throw notFound(`plugin not found: ${pluginName}`);
|
|
233
|
+
}
|
|
234
|
+
const skillDir = join(plugin.root, SKILLS_DIR, ...skillName.split('/'));
|
|
235
|
+
const skillFile = join(skillDir, SKILL_ENTRY_FILE);
|
|
236
|
+
if (pathExists(skillFile)) {
|
|
237
|
+
throw general(`skill already exists: ${skillFile}`);
|
|
238
|
+
}
|
|
239
|
+
ensureDir(skillDir);
|
|
240
|
+
const fm = serializeFrontmatter({
|
|
241
|
+
name: skillName,
|
|
242
|
+
description: opts.description,
|
|
243
|
+
});
|
|
244
|
+
writeFileSync(skillFile, fm, 'utf8');
|
|
245
|
+
out(skillFile);
|
|
246
|
+
hint(`crtr: scaffolded ${skillFile} — edit directly, then ` +
|
|
247
|
+
`\`crtr skill ${AUTHORING_GUIDE_SKILL}\` for SKILL.md authoring guidance`);
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
handleError(e);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
// where
|
|
254
|
+
skill
|
|
255
|
+
.command('where <name>')
|
|
256
|
+
.description('show resolution info as JSON')
|
|
257
|
+
.option('--scope <scope>', 'user|project')
|
|
258
|
+
.option('--plugin <name>', 'filter by plugin name')
|
|
259
|
+
.action(async (name, opts) => {
|
|
260
|
+
try {
|
|
261
|
+
const scopeArg = resolveScopeArg(opts.scope);
|
|
262
|
+
const resolveOpts = scopeArg !== 'all' ? { scope: scopeArg } : {};
|
|
263
|
+
if (opts.plugin !== undefined) {
|
|
264
|
+
Object.assign(resolveOpts, { pluginFilter: opts.plugin });
|
|
265
|
+
}
|
|
266
|
+
const skillObj = resolveSkill(name, resolveOpts);
|
|
267
|
+
jsonOut({
|
|
268
|
+
name: skillObj.name,
|
|
269
|
+
plugin: skillObj.plugin,
|
|
270
|
+
scope: skillObj.scope,
|
|
271
|
+
path: skillObj.path,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
handleError(e);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
// enable
|
|
279
|
+
skill
|
|
280
|
+
.command('enable <name>')
|
|
281
|
+
.description('enable a skill (clears any disable in the chosen scope)')
|
|
282
|
+
.option('--scope <scope>', 'user|project (default: project if available, else user)')
|
|
283
|
+
.action(async (name, opts) => {
|
|
284
|
+
try {
|
|
285
|
+
await toggleSkill(name, true, opts.scope);
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
handleError(e);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
// disable
|
|
292
|
+
skill
|
|
293
|
+
.command('disable <name>')
|
|
294
|
+
.description('disable a skill (hides from list and agent discovery)')
|
|
295
|
+
.option('--scope <scope>', 'user|project (default: project if available, else user)')
|
|
296
|
+
.action(async (name, opts) => {
|
|
297
|
+
try {
|
|
298
|
+
await toggleSkill(name, false, opts.scope);
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
handleError(e);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
// search
|
|
305
|
+
skill
|
|
306
|
+
.command('search <query>')
|
|
307
|
+
.description('search skills by name, description, and keywords')
|
|
308
|
+
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
309
|
+
.option('--plugin <name>', 'filter by plugin name')
|
|
310
|
+
.option('-a, --all', 'include disabled skills')
|
|
311
|
+
.option('--body', 'also search SKILL.md body')
|
|
312
|
+
.option('--json', 'emit JSON')
|
|
313
|
+
.action(async (query, opts) => {
|
|
314
|
+
try {
|
|
315
|
+
const needle = query.toLowerCase();
|
|
316
|
+
const scopes = listScopes(opts.scope);
|
|
317
|
+
const candidates = scopes
|
|
318
|
+
.flatMap((s) => listAllSkills(s))
|
|
319
|
+
.filter((sk) => {
|
|
320
|
+
if (opts.plugin !== undefined && sk.plugin !== opts.plugin)
|
|
321
|
+
return false;
|
|
322
|
+
if (!opts.all && !sk.enabled)
|
|
323
|
+
return false;
|
|
324
|
+
return true;
|
|
325
|
+
});
|
|
326
|
+
const hits = [];
|
|
327
|
+
for (const sk of candidates) {
|
|
328
|
+
const matched = [];
|
|
329
|
+
let score = 0;
|
|
330
|
+
if (sk.name.toLowerCase().includes(needle)) {
|
|
331
|
+
score += 10;
|
|
332
|
+
matched.push('name');
|
|
333
|
+
}
|
|
334
|
+
const desc = sk.frontmatter.description;
|
|
335
|
+
if (desc !== undefined && desc.toLowerCase().includes(needle)) {
|
|
336
|
+
score += 4;
|
|
337
|
+
matched.push('description');
|
|
338
|
+
}
|
|
339
|
+
const kws = sk.frontmatter.keywords;
|
|
340
|
+
if (kws && kws.some((k) => k.toLowerCase().includes(needle))) {
|
|
341
|
+
score += 6;
|
|
342
|
+
matched.push('keywords');
|
|
343
|
+
}
|
|
344
|
+
if (opts.body) {
|
|
345
|
+
const text = readText(sk.path).toLowerCase();
|
|
346
|
+
if (text.includes(needle)) {
|
|
347
|
+
score += 1;
|
|
348
|
+
matched.push('body');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (score > 0)
|
|
352
|
+
hits.push({ skill: sk, score, matched });
|
|
353
|
+
}
|
|
354
|
+
hits.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
355
|
+
if (opts.json) {
|
|
356
|
+
jsonOut({
|
|
357
|
+
query,
|
|
358
|
+
hits: hits.map((h) => ({
|
|
359
|
+
name: h.skill.name,
|
|
360
|
+
plugin: h.skill.plugin,
|
|
361
|
+
scope: h.skill.scope,
|
|
362
|
+
path: h.skill.path,
|
|
363
|
+
description: h.skill.frontmatter.description,
|
|
364
|
+
keywords: h.skill.frontmatter.keywords,
|
|
365
|
+
enabled: h.skill.enabled,
|
|
366
|
+
score: h.score,
|
|
367
|
+
matched: h.matched,
|
|
368
|
+
})),
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
for (const h of hits) {
|
|
373
|
+
const desc = h.skill.frontmatter.description !== undefined
|
|
374
|
+
? h.skill.frontmatter.description
|
|
375
|
+
: '';
|
|
376
|
+
const marker = h.skill.enabled ? '' : ' [disabled]';
|
|
377
|
+
const line = `${h.skill.scope}:${h.skill.plugin}/${h.skill.name}${marker}\t${h.matched.join(',')}\t${desc}`;
|
|
378
|
+
out(line);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
handleError(e, { json: opts.json });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async function toggleSkill(name, enabled, scopeArgRaw) {
|
|
387
|
+
let scope;
|
|
388
|
+
if (scopeArgRaw !== undefined) {
|
|
389
|
+
const resolved = resolveScopeArg(scopeArgRaw);
|
|
390
|
+
if (resolved === 'all')
|
|
391
|
+
throw usage('--scope must be user or project for enable/disable');
|
|
392
|
+
scope = resolved;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
scope = projectScopeRoot() !== null ? 'project' : 'user';
|
|
396
|
+
}
|
|
397
|
+
const skillObj = resolveSkill(name);
|
|
398
|
+
const key = skillConfigKey(skillObj.plugin, skillObj.name);
|
|
399
|
+
const scopeRootPath = requireScopeRoot(scope);
|
|
400
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
401
|
+
updateConfig(scope, (cfg) => {
|
|
402
|
+
cfg.skills[key] = { enabled };
|
|
403
|
+
});
|
|
404
|
+
info(`${enabled ? 'enabled' : 'disabled'} ${skillObj.plugin}:${skillObj.name} in ${scope} scope`);
|
|
405
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { registerArtifactCommand } from '../core/artifact.js';
|
|
2
|
+
import { specPrompt } from '../prompts/spec.js';
|
|
3
|
+
export function registerSpecCommand(program) {
|
|
4
|
+
registerArtifactCommand(program, {
|
|
5
|
+
command: 'spec',
|
|
6
|
+
kind: 'specs',
|
|
7
|
+
promptFn: specPrompt,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { err, warn, handleError } from '../core/output.js';
|
|
6
|
+
import { projectScopeRoot } from '../core/scope.js';
|
|
7
|
+
import { updateState } from '../core/config.js';
|
|
8
|
+
import { listAllPlugins, listAllMarketplaces } from '../core/resolver.js';
|
|
9
|
+
import { pull, fetch, currentSha, remoteSha } from '../core/git.js';
|
|
10
|
+
import { nowIso } from '../core/fs-utils.js';
|
|
11
|
+
import { network, general } from '../core/errors.js';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
const PKG_ROOT = join(__dirname, '..', '..', '..');
|
|
15
|
+
const PACKAGE_JSON_PATH = join(PKG_ROOT, 'package.json');
|
|
16
|
+
function currentVersion() {
|
|
17
|
+
const raw = readFileSync(PACKAGE_JSON_PATH, 'utf8');
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
return parsed.version;
|
|
20
|
+
}
|
|
21
|
+
function selfUpdate() {
|
|
22
|
+
const res = spawnSync('npm', ['i', '-g', '@crouton-kit/crtr@latest'], { stdio: 'inherit' });
|
|
23
|
+
if (res.status !== 0) {
|
|
24
|
+
throw general('npm install failed');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function selfCheck() {
|
|
28
|
+
const res = spawnSync('npm', ['view', '@crouton-kit/crtr', 'version'], { encoding: 'utf8' });
|
|
29
|
+
if (res.status !== 0) {
|
|
30
|
+
warn('could not check for crtr updates (network unavailable)');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const latest = res.stdout.trim();
|
|
34
|
+
const current = currentVersion();
|
|
35
|
+
if (latest !== current) {
|
|
36
|
+
err(`crtr: v${latest} available (current ${current}) — run \`crtr update --self\``);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function contentUpdate() {
|
|
40
|
+
const marketplaces = listAllMarketplaces();
|
|
41
|
+
for (const mkt of marketplaces) {
|
|
42
|
+
const res = pull(mkt.root);
|
|
43
|
+
if (res.status !== 0) {
|
|
44
|
+
throw network(`git pull failed for marketplace ${mkt.name}: ${res.stderr.trim()}`);
|
|
45
|
+
}
|
|
46
|
+
updateState(mkt.scope, (s) => {
|
|
47
|
+
if (!s.marketplaces[mkt.name])
|
|
48
|
+
s.marketplaces[mkt.name] = {};
|
|
49
|
+
s.marketplaces[mkt.name].last_updated = nowIso();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const plugins = listAllPlugins();
|
|
53
|
+
for (const plugin of plugins) {
|
|
54
|
+
if (!plugin.enabled)
|
|
55
|
+
continue;
|
|
56
|
+
if (plugin.sourceMarketplace)
|
|
57
|
+
continue;
|
|
58
|
+
const res = pull(plugin.root);
|
|
59
|
+
if (res.status !== 0) {
|
|
60
|
+
throw network(`git pull failed for plugin ${plugin.name}: ${res.stderr.trim()}`);
|
|
61
|
+
}
|
|
62
|
+
updateState(plugin.scope, (s) => {
|
|
63
|
+
if (!s.plugins[plugin.name])
|
|
64
|
+
s.plugins[plugin.name] = {};
|
|
65
|
+
s.plugins[plugin.name].last_updated = nowIso();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function contentCheck() {
|
|
70
|
+
const marketplaces = listAllMarketplaces();
|
|
71
|
+
for (const mkt of marketplaces) {
|
|
72
|
+
const fetchRes = fetch(mkt.root, mkt.ref);
|
|
73
|
+
if (fetchRes.status !== 0) {
|
|
74
|
+
warn(`could not fetch ${mkt.name} (network unavailable)`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const head = currentSha(mkt.root);
|
|
78
|
+
const remote = remoteSha(mkt.root, mkt.ref);
|
|
79
|
+
if (head !== null && remote !== null && head !== remote) {
|
|
80
|
+
err(`crtr: marketplace ${mkt.name} has updates available — run \`crtr update --content\``);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const plugins = listAllPlugins();
|
|
84
|
+
for (const plugin of plugins) {
|
|
85
|
+
if (!plugin.enabled)
|
|
86
|
+
continue;
|
|
87
|
+
if (plugin.sourceMarketplace)
|
|
88
|
+
continue;
|
|
89
|
+
const ref = 'main';
|
|
90
|
+
const fetchRes = fetch(plugin.root, ref);
|
|
91
|
+
if (fetchRes.status !== 0) {
|
|
92
|
+
warn(`could not fetch ${plugin.name} (network unavailable)`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const head = currentSha(plugin.root);
|
|
96
|
+
const remote = remoteSha(plugin.root, ref);
|
|
97
|
+
if (head !== null && remote !== null && head !== remote) {
|
|
98
|
+
err(`crtr: plugin ${plugin.name} has updates available — run \`crtr plugin update ${plugin.name}\``);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function registerUpdateCommand(program) {
|
|
103
|
+
program
|
|
104
|
+
.command('update')
|
|
105
|
+
.description('update crtr itself and/or installed plugins and marketplaces')
|
|
106
|
+
.option('--self', 'update crtr binary via npm')
|
|
107
|
+
.option('--content', 'pull updates for all installed plugins and marketplaces')
|
|
108
|
+
.option('--check', 'check for updates without applying them')
|
|
109
|
+
.action(async (opts) => {
|
|
110
|
+
try {
|
|
111
|
+
const runSelf = opts.self === true;
|
|
112
|
+
const runContent = opts.content === true;
|
|
113
|
+
const runBoth = !runSelf && !runContent;
|
|
114
|
+
if (opts.check) {
|
|
115
|
+
if (runSelf || runBoth)
|
|
116
|
+
selfCheck();
|
|
117
|
+
if (runContent || runBoth)
|
|
118
|
+
contentCheck();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (runSelf || runBoth) {
|
|
122
|
+
selfUpdate();
|
|
123
|
+
const scopes = ['user'];
|
|
124
|
+
if (projectScopeRoot())
|
|
125
|
+
scopes.unshift('project');
|
|
126
|
+
for (const scope of scopes) {
|
|
127
|
+
updateState(scope, (s) => {
|
|
128
|
+
s.last_self_check = nowIso();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (runContent || runBoth) {
|
|
133
|
+
contentUpdate();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
handleError(e);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export type ArtifactKind = 'plans' | 'specs';
|
|
3
|
+
export declare function mangleCwd(cwd?: string): string;
|
|
4
|
+
export declare function artifactsRoot(kind: ArtifactKind, cwd?: string): string;
|
|
5
|
+
export declare function sanitizeName(raw: string): string;
|
|
6
|
+
export declare function artifactPath(kind: ArtifactKind, name: string, cwd?: string): string;
|
|
7
|
+
export declare function inTmux(): boolean;
|
|
8
|
+
export declare function openInTmuxPane(path: string): void;
|
|
9
|
+
export interface RegisterArtifactOptions {
|
|
10
|
+
command: 'plan' | 'spec';
|
|
11
|
+
kind: ArtifactKind;
|
|
12
|
+
promptFn: (artifactsDir: string) => string;
|
|
13
|
+
}
|
|
14
|
+
export declare function registerArtifactCommand(program: Command, opts: RegisterArtifactOptions): void;
|