@crouton-kit/crouter 0.1.9 → 0.2.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/.crouter-plugin/plugin.json +5 -0
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +157 -0
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +156 -0
- package/dist/builtin-skills/skills/crouter-development/skills/SKILL.md +166 -0
- package/dist/commands/doctor.js +54 -2
- package/dist/commands/plugin.js +4 -1
- package/dist/commands/skill.js +61 -7
- package/dist/core/frontmatter.d.ts +1 -1
- package/dist/core/frontmatter.js +5 -0
- package/dist/core/resolver.d.ts +2 -0
- package/dist/core/resolver.js +63 -2
- package/dist/core/scope.d.ts +1 -0
- package/dist/core/scope.js +13 -2
- package/dist/prompts/skill.js +275 -27
- package/dist/types.d.ts +6 -1
- package/dist/types.js +4 -0
- package/package.json +2 -2
package/dist/commands/skill.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { writeFileSync } from 'node:fs';
|
|
3
|
-
import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, } from '../types.js';
|
|
3
|
+
import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, SKILL_TYPES, isSkillType, } from '../types.js';
|
|
4
4
|
import { skillConfigKey } from '../types.js';
|
|
5
5
|
import { notFound, usage, general } from '../core/errors.js';
|
|
6
6
|
import { out, hint, info, jsonOut, handleError, } from '../core/output.js';
|
|
7
7
|
import { listScopes, requireScopeRoot, resolveScopeArg, projectScopeRoot, scopeSkillsDir, } from '../core/scope.js';
|
|
8
|
-
import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, } from '../core/resolver.js';
|
|
8
|
+
import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, listSkillSiblings, listSkillChildren, } from '../core/resolver.js';
|
|
9
9
|
import { updateConfig, ensureScopeInitialized } from '../core/config.js';
|
|
10
10
|
import { parseFrontmatter, serializeFrontmatter } from '../core/frontmatter.js';
|
|
11
11
|
import { ensureDir, pathExists, readText, walkFiles } from '../core/fs-utils.js';
|
|
@@ -23,7 +23,7 @@ const KNOWN_VERBS = new Set([
|
|
|
23
23
|
'disable',
|
|
24
24
|
'search',
|
|
25
25
|
]);
|
|
26
|
-
const AUTHORING_GUIDE_SKILL = '
|
|
26
|
+
const AUTHORING_GUIDE_SKILL = 'crouter-development/skills';
|
|
27
27
|
function buildShowFooter(skillPath) {
|
|
28
28
|
return (`crtr: edit this skill directly at ${skillPath} — ` +
|
|
29
29
|
`for SKILL.md authoring guidance run \`crtr skill ${AUTHORING_GUIDE_SKILL}\``);
|
|
@@ -31,6 +31,46 @@ function buildShowFooter(skillPath) {
|
|
|
31
31
|
function wrapSkill(name, path, content) {
|
|
32
32
|
return `<skill name="${name}" path="${path}">\n${content.endsWith('\n') ? content : content + '\n'}</skill>`;
|
|
33
33
|
}
|
|
34
|
+
function formatNeighborQualifier(s) {
|
|
35
|
+
return s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}:${s.name}` : `${s.plugin}/${s.name}`;
|
|
36
|
+
}
|
|
37
|
+
function buildNeighborsSection(skill) {
|
|
38
|
+
const siblings = listSkillSiblings(skill);
|
|
39
|
+
const children = listSkillChildren(skill);
|
|
40
|
+
if (siblings.length === 0 && children.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
const lines = [
|
|
43
|
+
'## Neighbors',
|
|
44
|
+
'*Auto-discovered from filesystem. Use `--no-neighbors` to suppress.*',
|
|
45
|
+
'',
|
|
46
|
+
];
|
|
47
|
+
if (siblings.length > 0) {
|
|
48
|
+
lines.push('**Siblings:**');
|
|
49
|
+
for (const s of siblings) {
|
|
50
|
+
const desc = s.frontmatter.description !== undefined ? s.frontmatter.description : '';
|
|
51
|
+
lines.push(`- \`${formatNeighborQualifier(s)}\`${desc ? ` — ${desc}` : ''}`);
|
|
52
|
+
}
|
|
53
|
+
if (children.length > 0)
|
|
54
|
+
lines.push('');
|
|
55
|
+
}
|
|
56
|
+
if (children.length > 0) {
|
|
57
|
+
lines.push('**Nested:**');
|
|
58
|
+
for (const s of children) {
|
|
59
|
+
const desc = s.frontmatter.description !== undefined ? s.frontmatter.description : '';
|
|
60
|
+
lines.push(`- \`${formatNeighborQualifier(s)}\`${desc ? ` — ${desc}` : ''}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
function appendNeighbors(skill, body, suppress) {
|
|
66
|
+
if (suppress)
|
|
67
|
+
return body;
|
|
68
|
+
const section = buildNeighborsSection(skill);
|
|
69
|
+
if (section === null)
|
|
70
|
+
return body;
|
|
71
|
+
const sep = body.endsWith('\n') ? '\n' : '\n\n';
|
|
72
|
+
return body + sep + `<neighbors>\n${section}\n</neighbors>\n`;
|
|
73
|
+
}
|
|
34
74
|
const SKILL_IDENTIFIER_HELP = 'Skill identifier forms (accepted by show, path, where, enable, disable):\n' +
|
|
35
75
|
' <name> bare name — resolves scope-root first, then plugins\n' +
|
|
36
76
|
' <plugin>:<name> explicit plugin (canonical)\n' +
|
|
@@ -52,7 +92,8 @@ export function registerSkillCommands(program) {
|
|
|
52
92
|
try {
|
|
53
93
|
const skillObj = resolveSkill(nameOrVerb);
|
|
54
94
|
const content = readText(skillObj.path);
|
|
55
|
-
const
|
|
95
|
+
const rawBody = opts.frontmatter ? content : parseFrontmatter(content).body;
|
|
96
|
+
const body = appendNeighbors(skillObj, rawBody, false);
|
|
56
97
|
out(wrapSkill(skillObj.name, skillObj.path, body));
|
|
57
98
|
hint(buildShowFooter(skillObj.path));
|
|
58
99
|
}
|
|
@@ -117,6 +158,7 @@ export function registerSkillCommands(program) {
|
|
|
117
158
|
.option('--scope <scope>', 'user|project')
|
|
118
159
|
.option('--plugin <name>', 'filter by plugin name')
|
|
119
160
|
.option('--frontmatter', 'include YAML frontmatter in the printed body')
|
|
161
|
+
.option('--no-neighbors', 'suppress the auto-appended ## Neighbors section')
|
|
120
162
|
.option('--json', 'emit JSON')
|
|
121
163
|
.addHelpText('after', '\nExamples:\n' +
|
|
122
164
|
' crtr skill show rules # bare name\n' +
|
|
@@ -133,7 +175,9 @@ export function registerSkillCommands(program) {
|
|
|
133
175
|
}
|
|
134
176
|
const skillObj = resolveSkill(name, resolveOpts);
|
|
135
177
|
const content = readText(skillObj.path);
|
|
136
|
-
const
|
|
178
|
+
const rawBody = opts.frontmatter ? content : parseFrontmatter(content).body;
|
|
179
|
+
const suppressNeighbors = opts.neighbors === false;
|
|
180
|
+
const body = appendNeighbors(skillObj, rawBody, suppressNeighbors);
|
|
137
181
|
if (opts.json) {
|
|
138
182
|
jsonOut({
|
|
139
183
|
name: skillObj.name,
|
|
@@ -235,12 +279,20 @@ export function registerSkillCommands(program) {
|
|
|
235
279
|
.description('scaffold a new skill — <name> (scope-direct) or <plugin>:<name>')
|
|
236
280
|
.option('--scope <scope>', 'user|project (default: project then user)')
|
|
237
281
|
.option('--description <text>', 'skill description for frontmatter')
|
|
282
|
+
.option('--type <type>', `skill type for frontmatter — one of: ${SKILL_TYPES.join(' | ')}`)
|
|
238
283
|
.action(async (qualifier, opts) => {
|
|
239
284
|
try {
|
|
240
285
|
const { plugin: pluginName, name: skillName } = parseSkillQualifier(qualifier);
|
|
241
286
|
if (!skillName) {
|
|
242
287
|
throw usage('skill name required');
|
|
243
288
|
}
|
|
289
|
+
let skillType;
|
|
290
|
+
if (opts.type !== undefined) {
|
|
291
|
+
if (!isSkillType(opts.type)) {
|
|
292
|
+
throw usage(`unknown skill type: ${opts.type} / valid: ${SKILL_TYPES.join(' | ')}`);
|
|
293
|
+
}
|
|
294
|
+
skillType = opts.type;
|
|
295
|
+
}
|
|
244
296
|
const scopeArg = opts.scope !== undefined ? resolveScopeArg(opts.scope) : undefined;
|
|
245
297
|
// Scope-direct: no plugin qualifier, or explicit `_:` sentinel
|
|
246
298
|
if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
|
|
@@ -266,6 +318,7 @@ export function registerSkillCommands(program) {
|
|
|
266
318
|
const fm = serializeFrontmatter({
|
|
267
319
|
name: skillName,
|
|
268
320
|
description: opts.description,
|
|
321
|
+
type: skillType,
|
|
269
322
|
});
|
|
270
323
|
writeFileSync(skillFile, fm, 'utf8');
|
|
271
324
|
out(skillFile);
|
|
@@ -292,6 +345,7 @@ export function registerSkillCommands(program) {
|
|
|
292
345
|
const fm = serializeFrontmatter({
|
|
293
346
|
name: skillName,
|
|
294
347
|
description: opts.description,
|
|
348
|
+
type: skillType,
|
|
295
349
|
});
|
|
296
350
|
writeFileSync(skillFile, fm, 'utf8');
|
|
297
351
|
out(skillFile);
|
|
@@ -305,7 +359,7 @@ export function registerSkillCommands(program) {
|
|
|
305
359
|
// create — pick a template type
|
|
306
360
|
skill
|
|
307
361
|
.command('create [topic...]')
|
|
308
|
-
.description(
|
|
362
|
+
.description(`pick a template type for a new skill (${SKILL_TYPES.join(' | ')})`)
|
|
309
363
|
.action(async (topic) => {
|
|
310
364
|
const arg = topic && topic.length > 0 ? topic.join(' ') : '';
|
|
311
365
|
out(skillCreatePrompt(arg));
|
|
@@ -313,7 +367,7 @@ export function registerSkillCommands(program) {
|
|
|
313
367
|
// template — full workflow + skeleton for one template type
|
|
314
368
|
skill
|
|
315
369
|
.command('template <type> [topic...]')
|
|
316
|
-
.description(
|
|
370
|
+
.description(`full workflow + skeleton for a template type (${SKILL_TYPES.join(' | ')})`)
|
|
317
371
|
.action(async (type, topic) => {
|
|
318
372
|
const arg = topic && topic.length > 0 ? topic.join(' ') : '';
|
|
319
373
|
out(skillTemplatePrompt(type, arg));
|
package/dist/core/frontmatter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSkillType } from '../types.js';
|
|
1
2
|
const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
|
|
2
3
|
export function parseFrontmatter(source) {
|
|
3
4
|
const match = source.match(FRONTMATTER_RE);
|
|
@@ -126,6 +127,7 @@ function parseSimpleYaml(yaml) {
|
|
|
126
127
|
name: typeof out.name === 'string' ? out.name : '',
|
|
127
128
|
description: typeof out.description === 'string' ? out.description : undefined,
|
|
128
129
|
keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
|
|
130
|
+
type: isSkillType(out.type) ? out.type : undefined,
|
|
129
131
|
};
|
|
130
132
|
return fm;
|
|
131
133
|
}
|
|
@@ -141,6 +143,9 @@ export function serializeFrontmatter(data) {
|
|
|
141
143
|
if (data.description !== undefined) {
|
|
142
144
|
lines.push(`description: ${quoteIfNeeded(data.description)}`);
|
|
143
145
|
}
|
|
146
|
+
if (data.type !== undefined) {
|
|
147
|
+
lines.push(`type: ${data.type}`);
|
|
148
|
+
}
|
|
144
149
|
if (data.keywords && data.keywords.length) {
|
|
145
150
|
const inline = `[${data.keywords.map(quoteIfNeeded).join(', ')}]`;
|
|
146
151
|
lines.push(`keywords: ${inline}`);
|
package/dist/core/resolver.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare function effectiveSkillEnabled(pluginName: string, skillName: str
|
|
|
13
13
|
export declare function listSkillsInPlugin(plugin: InstalledPlugin, cfgs?: ScopeConfigs): Skill[];
|
|
14
14
|
export declare function listScopeRootSkills(scope: Scope, cfgs?: ScopeConfigs): Skill[];
|
|
15
15
|
export declare function listAllSkills(scopeFilter?: Scope): Skill[];
|
|
16
|
+
export declare function listSkillSiblings(skill: Skill): Skill[];
|
|
17
|
+
export declare function listSkillChildren(skill: Skill): Skill[];
|
|
16
18
|
export interface SkillResolutionOpts {
|
|
17
19
|
scope?: Scope;
|
|
18
20
|
pluginFilter?: string;
|
package/dist/core/resolver.js
CHANGED
|
@@ -5,8 +5,29 @@ import { listDirs, pathExists, readText, walkFiles, } from './fs-utils.js';
|
|
|
5
5
|
import { readMarketplaceManifest, readPluginManifest } from './manifest.js';
|
|
6
6
|
import { parseFrontmatter } from './frontmatter.js';
|
|
7
7
|
import { ambiguous, notFound, usage } from './errors.js';
|
|
8
|
-
import { marketplacesDir, pluginsDir, projectScopeRoot, scopeSkillsDir, userScopeRoot, } from './scope.js';
|
|
8
|
+
import { builtinSkillsRoot, marketplacesDir, pluginsDir, projectScopeRoot, scopeSkillsDir, userScopeRoot, } from './scope.js';
|
|
9
|
+
function getBuiltinPlugin() {
|
|
10
|
+
const root = builtinSkillsRoot();
|
|
11
|
+
if (!pathExists(root))
|
|
12
|
+
return null;
|
|
13
|
+
const manifest = readPluginManifest(root);
|
|
14
|
+
if (!manifest)
|
|
15
|
+
return null;
|
|
16
|
+
return {
|
|
17
|
+
name: manifest.name,
|
|
18
|
+
scope: 'builtin',
|
|
19
|
+
root,
|
|
20
|
+
manifest,
|
|
21
|
+
enabled: true,
|
|
22
|
+
builtin: true,
|
|
23
|
+
version: manifest.version,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
9
26
|
export function listInstalledPlugins(scope) {
|
|
27
|
+
if (scope === 'builtin') {
|
|
28
|
+
const builtin = getBuiltinPlugin();
|
|
29
|
+
return builtin ? [builtin] : [];
|
|
30
|
+
}
|
|
10
31
|
const dir = pluginsDir(scope);
|
|
11
32
|
if (!dir || !pathExists(dir))
|
|
12
33
|
return [];
|
|
@@ -40,13 +61,14 @@ export function listAllPlugins() {
|
|
|
40
61
|
if (projectScopeRoot())
|
|
41
62
|
scopes.push('project');
|
|
42
63
|
scopes.push('user');
|
|
64
|
+
scopes.push('builtin');
|
|
43
65
|
return scopes.flatMap(listInstalledPlugins);
|
|
44
66
|
}
|
|
45
67
|
export function findPluginByName(name, scope) {
|
|
46
68
|
if (scope) {
|
|
47
69
|
return listInstalledPlugins(scope).find((p) => p.name === name) ?? null;
|
|
48
70
|
}
|
|
49
|
-
for (const s of ['project', 'user'].filter((sc) => sc === 'project' ? projectScopeRoot() !== null : true)) {
|
|
71
|
+
for (const s of ['project', 'user', 'builtin'].filter((sc) => sc === 'project' ? projectScopeRoot() !== null : true)) {
|
|
50
72
|
const match = listInstalledPlugins(s).find((p) => p.name === name);
|
|
51
73
|
if (match)
|
|
52
74
|
return match;
|
|
@@ -100,6 +122,8 @@ export function listSkillsInPlugin(plugin, cfgs) {
|
|
|
100
122
|
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
123
|
}
|
|
102
124
|
export function listScopeRootSkills(scope, cfgs) {
|
|
125
|
+
if (scope === 'builtin')
|
|
126
|
+
return [];
|
|
103
127
|
const skillsRoot = scopeSkillsDir(scope);
|
|
104
128
|
if (!skillsRoot || !pathExists(skillsRoot))
|
|
105
129
|
return [];
|
|
@@ -138,6 +162,41 @@ export function listAllSkills(scopeFilter) {
|
|
|
138
162
|
...plugins.filter((p) => p.enabled).flatMap((p) => listSkillsInPlugin(p, cfgs)),
|
|
139
163
|
];
|
|
140
164
|
}
|
|
165
|
+
function enumerateNeighborPool(skill) {
|
|
166
|
+
if (skill.plugin === SCOPE_SKILL_PLUGIN) {
|
|
167
|
+
return listScopeRootSkills(skill.scope);
|
|
168
|
+
}
|
|
169
|
+
const plugin = listInstalledPlugins(skill.scope).find((p) => p.name === skill.plugin);
|
|
170
|
+
if (!plugin)
|
|
171
|
+
return [];
|
|
172
|
+
return listSkillsInPlugin(plugin);
|
|
173
|
+
}
|
|
174
|
+
export function listSkillSiblings(skill) {
|
|
175
|
+
const pool = enumerateNeighborPool(skill);
|
|
176
|
+
const segs = skill.name.split('/');
|
|
177
|
+
const depth = segs.length;
|
|
178
|
+
const parentPrefix = segs.slice(0, -1).join('/');
|
|
179
|
+
return pool.filter((s) => {
|
|
180
|
+
if (s.name === skill.name)
|
|
181
|
+
return false;
|
|
182
|
+
const sSegs = s.name.split('/');
|
|
183
|
+
if (sSegs.length !== depth)
|
|
184
|
+
return false;
|
|
185
|
+
if (parentPrefix === '')
|
|
186
|
+
return !s.name.includes('/');
|
|
187
|
+
return s.name.startsWith(parentPrefix + '/');
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
export function listSkillChildren(skill) {
|
|
191
|
+
const pool = enumerateNeighborPool(skill);
|
|
192
|
+
const prefix = skill.name + '/';
|
|
193
|
+
return pool.filter((s) => {
|
|
194
|
+
if (!s.name.startsWith(prefix))
|
|
195
|
+
return false;
|
|
196
|
+
const rest = s.name.slice(prefix.length);
|
|
197
|
+
return rest.length > 0 && !rest.includes('/');
|
|
198
|
+
});
|
|
199
|
+
}
|
|
141
200
|
export function resolveSkill(rawName, opts = {}) {
|
|
142
201
|
const parsed = parseSkillQualifier(rawName);
|
|
143
202
|
if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
|
|
@@ -353,6 +412,8 @@ export function parseSkillQualifier(raw) {
|
|
|
353
412
|
}
|
|
354
413
|
function orderPluginsByResolution(plugins) {
|
|
355
414
|
const score = (p) => {
|
|
415
|
+
if (p.scope === 'builtin')
|
|
416
|
+
return 4;
|
|
356
417
|
const fromMarketplace = Boolean(p.sourceMarketplace);
|
|
357
418
|
if (p.scope === 'project' && !fromMarketplace)
|
|
358
419
|
return 0;
|
package/dist/core/scope.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Scope } from '../types.js';
|
|
2
|
+
export declare function builtinSkillsRoot(): string;
|
|
2
3
|
export declare function userScopeRoot(): string;
|
|
3
4
|
export declare function findProjectScopeRoot(startDir?: string): string | null;
|
|
4
5
|
export declare function projectScopeRoot(startDir?: string): string | null;
|
package/dist/core/scope.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { join, resolve, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
5
6
|
import { usage } from './errors.js';
|
|
6
7
|
let cachedProjectRoot;
|
|
8
|
+
export function builtinSkillsRoot() {
|
|
9
|
+
// Resolve relative to this file: src/core/scope.ts → src/builtin-skills/ OR dist/core/scope.js → dist/builtin-skills/
|
|
10
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
11
|
+
const coreDir = dirname(thisFile);
|
|
12
|
+
const pkgDir = dirname(coreDir); // src/ or dist/
|
|
13
|
+
return join(pkgDir, 'builtin-skills');
|
|
14
|
+
}
|
|
7
15
|
export function userScopeRoot() {
|
|
8
16
|
return join(homedir(), CRTR_DIR_NAME);
|
|
9
17
|
}
|
|
@@ -37,6 +45,8 @@ export function projectScopeRoot(startDir) {
|
|
|
37
45
|
return findProjectScopeRoot(startDir);
|
|
38
46
|
}
|
|
39
47
|
export function scopeRoot(scope) {
|
|
48
|
+
if (scope === 'builtin')
|
|
49
|
+
return builtinSkillsRoot();
|
|
40
50
|
return scope === 'user' ? userScopeRoot() : projectScopeRoot();
|
|
41
51
|
}
|
|
42
52
|
export function requireScopeRoot(scope) {
|
|
@@ -71,9 +81,9 @@ export function resolveScopeArg(scopeArg) {
|
|
|
71
81
|
if (scopeArg === undefined)
|
|
72
82
|
return 'all';
|
|
73
83
|
const value = scopeArg.toLowerCase();
|
|
74
|
-
if (value === 'user' || value === 'project' || value === 'all')
|
|
84
|
+
if (value === 'user' || value === 'project' || value === 'builtin' || value === 'all')
|
|
75
85
|
return value;
|
|
76
|
-
throw usage(`invalid --scope: ${scopeArg} (expected user|project|all)`);
|
|
86
|
+
throw usage(`invalid --scope: ${scopeArg} (expected user|project|builtin|all)`);
|
|
77
87
|
}
|
|
78
88
|
export function listScopes(scopeArg) {
|
|
79
89
|
const v = resolveScopeArg(scopeArg);
|
|
@@ -82,6 +92,7 @@ export function listScopes(scopeArg) {
|
|
|
82
92
|
if (projectScopeRoot())
|
|
83
93
|
out.push('project');
|
|
84
94
|
out.push('user');
|
|
95
|
+
out.push('builtin');
|
|
85
96
|
return out;
|
|
86
97
|
}
|
|
87
98
|
return [v];
|