@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,364 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { renameSync } from 'node:fs';
|
|
3
|
+
import { notFound, usage, general } from '../core/errors.js';
|
|
4
|
+
import { out, hint, info, jsonOut, handleError, isTTY } from '../core/output.js';
|
|
5
|
+
import { pluginsDir, ensureProjectScopeRoot, resolveScopeArg, listScopes, userScopeRoot, } from '../core/scope.js';
|
|
6
|
+
import { updateConfig, updateState, ensureScopeInitialized } from '../core/config.js';
|
|
7
|
+
import { listInstalledPlugins, listAllPlugins, findPluginByName, listSkillsInPlugin, } from '../core/resolver.js';
|
|
8
|
+
import { pathExists, ensureDir, removePath, nowIso } from '../core/fs-utils.js';
|
|
9
|
+
import { clone, pull, deriveNameFromUrl } from '../core/git.js';
|
|
10
|
+
import { readPluginManifest } from '../core/manifest.js';
|
|
11
|
+
const KNOWN_VERBS = new Set([
|
|
12
|
+
'list',
|
|
13
|
+
'show',
|
|
14
|
+
'install',
|
|
15
|
+
'uninstall',
|
|
16
|
+
'enable',
|
|
17
|
+
'disable',
|
|
18
|
+
'update',
|
|
19
|
+
]);
|
|
20
|
+
const GIT_URL_RE = /^(https?:\/\/|git@|ssh:\/\/|file:\/\/)/;
|
|
21
|
+
function isGitUrl(arg) {
|
|
22
|
+
return GIT_URL_RE.test(arg) || arg.endsWith('.git');
|
|
23
|
+
}
|
|
24
|
+
export function registerPluginCommands(program) {
|
|
25
|
+
const plugin = program
|
|
26
|
+
.command('plugin [nameOrVerb] [rest...]')
|
|
27
|
+
.description('manage plugins')
|
|
28
|
+
.action(async (nameOrVerb, rest) => {
|
|
29
|
+
if (nameOrVerb === undefined) {
|
|
30
|
+
plugin.help();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!KNOWN_VERBS.has(nameOrVerb)) {
|
|
34
|
+
try {
|
|
35
|
+
await showPlugin(nameOrVerb, { json: false });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
handleError(e);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Known verbs dispatched by commander subcommands; nothing to do here.
|
|
43
|
+
void rest;
|
|
44
|
+
});
|
|
45
|
+
// list
|
|
46
|
+
plugin
|
|
47
|
+
.command('list')
|
|
48
|
+
.description('list installed plugins')
|
|
49
|
+
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
50
|
+
.option('--json', 'emit JSON')
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
try {
|
|
53
|
+
const scopes = listScopes(opts.scope);
|
|
54
|
+
const plugins = scopes
|
|
55
|
+
.flatMap((s) => listInstalledPlugins(s))
|
|
56
|
+
.sort((a, b) => {
|
|
57
|
+
if (a.scope === 'project' && b.scope !== 'project')
|
|
58
|
+
return -1;
|
|
59
|
+
if (a.scope !== 'project' && b.scope === 'project')
|
|
60
|
+
return 1;
|
|
61
|
+
return a.name.localeCompare(b.name);
|
|
62
|
+
});
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
jsonOut({
|
|
65
|
+
plugins: plugins.map((p) => ({
|
|
66
|
+
name: p.name,
|
|
67
|
+
scope: p.scope,
|
|
68
|
+
version: p.version,
|
|
69
|
+
source_marketplace: p.sourceMarketplace,
|
|
70
|
+
description: p.manifest.description,
|
|
71
|
+
enabled: p.enabled,
|
|
72
|
+
root: p.root,
|
|
73
|
+
})),
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const p of plugins) {
|
|
78
|
+
const version = p.version !== undefined ? `@${p.version}` : '';
|
|
79
|
+
const mkt = p.sourceMarketplace !== undefined ? ` [${p.sourceMarketplace}]` : '';
|
|
80
|
+
const desc = p.manifest.description !== undefined ? ` ${p.manifest.description}` : '';
|
|
81
|
+
out(`${p.scope}:${p.name}${version}${mkt}${desc}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
handleError(e, { json: opts.json });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// show
|
|
89
|
+
plugin
|
|
90
|
+
.command('show <name>')
|
|
91
|
+
.description('print plugin.json and skill index (default verb)')
|
|
92
|
+
.option('--json', 'emit JSON')
|
|
93
|
+
.action(async (name, opts) => {
|
|
94
|
+
try {
|
|
95
|
+
await showPlugin(name, opts);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
handleError(e, { json: opts.json });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// install
|
|
102
|
+
plugin
|
|
103
|
+
.command('install <gitUrlOrName>')
|
|
104
|
+
.description('install a plugin from a git URL or marketplace name')
|
|
105
|
+
.option('--scope <scope>', 'user|project (default: user)')
|
|
106
|
+
.option('--ref <branch>', 'git branch/tag/ref to clone')
|
|
107
|
+
.action(async (gitUrlOrName, opts) => {
|
|
108
|
+
try {
|
|
109
|
+
if (!isGitUrl(gitUrlOrName)) {
|
|
110
|
+
throw usage(`"${gitUrlOrName}" is not a git URL and no matching marketplace plugin was found.\n` +
|
|
111
|
+
`Use \`crtr marketplace install <mkt>:<name>\` to install from a marketplace.`, { code: 'USAGE' });
|
|
112
|
+
}
|
|
113
|
+
const url = gitUrlOrName;
|
|
114
|
+
let scope = 'user';
|
|
115
|
+
if (opts.scope !== undefined) {
|
|
116
|
+
const resolved = resolveScopeArg(opts.scope);
|
|
117
|
+
if (resolved === 'all') {
|
|
118
|
+
throw usage('--scope must be user or project, not all');
|
|
119
|
+
}
|
|
120
|
+
scope = resolved;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
hint('No --scope provided; defaulting to user scope (~/.crouter/plugins/).' +
|
|
124
|
+
' Pass --scope project to install into the project scope.');
|
|
125
|
+
}
|
|
126
|
+
let scopeRootPath;
|
|
127
|
+
if (scope === 'project') {
|
|
128
|
+
scopeRootPath = ensureProjectScopeRoot();
|
|
129
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
scopeRootPath = userScopeRoot();
|
|
133
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
134
|
+
}
|
|
135
|
+
const pDir = join(scopeRootPath, 'plugins');
|
|
136
|
+
ensureDir(pDir);
|
|
137
|
+
const tempName = deriveNameFromUrl(url);
|
|
138
|
+
const tempDir = join(pDir, tempName);
|
|
139
|
+
if (pathExists(tempDir)) {
|
|
140
|
+
throw general(`plugin directory already exists: ${tempDir}\n` +
|
|
141
|
+
`Uninstall the existing plugin first with \`crtr plugin uninstall ${tempName}\`.`);
|
|
142
|
+
}
|
|
143
|
+
clone(url, tempDir, { ref: opts.ref, depth: 1 });
|
|
144
|
+
const manifest = readPluginManifest(tempDir);
|
|
145
|
+
if (manifest === null) {
|
|
146
|
+
removePath(tempDir);
|
|
147
|
+
throw general(`cloned repo does not contain a valid .crouter-plugin/plugin.json: ${url}`);
|
|
148
|
+
}
|
|
149
|
+
const finalName = manifest.name;
|
|
150
|
+
let finalDir = tempDir;
|
|
151
|
+
if (finalName !== tempName) {
|
|
152
|
+
const candidateDir = join(pDir, finalName);
|
|
153
|
+
if (pathExists(candidateDir)) {
|
|
154
|
+
removePath(tempDir);
|
|
155
|
+
throw general(`plugin "${finalName}" is already installed at ${candidateDir}`);
|
|
156
|
+
}
|
|
157
|
+
renameSync(tempDir, candidateDir);
|
|
158
|
+
finalDir = candidateDir;
|
|
159
|
+
}
|
|
160
|
+
updateConfig(scope, (cfg) => {
|
|
161
|
+
cfg.plugins[finalName] = {
|
|
162
|
+
enabled: true,
|
|
163
|
+
version: manifest.version,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
info(`installed plugin "${finalName}"${manifest.version !== undefined ? ` v${manifest.version}` : ''} (${scope} scope)`);
|
|
167
|
+
out(finalDir);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
handleError(e);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// uninstall
|
|
174
|
+
plugin
|
|
175
|
+
.command('uninstall <name>')
|
|
176
|
+
.description('remove a plugin and its config entry')
|
|
177
|
+
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
178
|
+
.option('--yes', 'skip confirmation in non-TTY mode')
|
|
179
|
+
.action(async (name, opts) => {
|
|
180
|
+
try {
|
|
181
|
+
if (!isTTY() && !opts.yes) {
|
|
182
|
+
throw usage(`uninstall requires --yes in non-TTY mode: crtr plugin uninstall ${name} --yes`);
|
|
183
|
+
}
|
|
184
|
+
const scopes = listScopes(opts.scope);
|
|
185
|
+
let removed = false;
|
|
186
|
+
for (const scope of scopes) {
|
|
187
|
+
const pDir = pluginsDir(scope);
|
|
188
|
+
if (pDir === null)
|
|
189
|
+
continue;
|
|
190
|
+
const pluginDir = join(pDir, name);
|
|
191
|
+
if (!pathExists(pluginDir))
|
|
192
|
+
continue;
|
|
193
|
+
removePath(pluginDir);
|
|
194
|
+
updateConfig(scope, (cfg) => {
|
|
195
|
+
delete cfg.plugins[name];
|
|
196
|
+
});
|
|
197
|
+
info(`uninstalled plugin "${name}" from ${scope} scope`);
|
|
198
|
+
removed = true;
|
|
199
|
+
}
|
|
200
|
+
if (!removed) {
|
|
201
|
+
throw notFound(`plugin not found: ${name}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
handleError(e);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// enable
|
|
209
|
+
plugin
|
|
210
|
+
.command('enable <name>')
|
|
211
|
+
.description('enable a plugin')
|
|
212
|
+
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
213
|
+
.action(async (name, opts) => {
|
|
214
|
+
try {
|
|
215
|
+
await setEnabled(name, true, opts.scope);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
handleError(e);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
// disable
|
|
222
|
+
plugin
|
|
223
|
+
.command('disable <name>')
|
|
224
|
+
.description('disable a plugin without removing it')
|
|
225
|
+
.option('--scope <scope>', 'user|project|all (default: all)')
|
|
226
|
+
.action(async (name, opts) => {
|
|
227
|
+
try {
|
|
228
|
+
await setEnabled(name, false, opts.scope);
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
handleError(e);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// update
|
|
235
|
+
plugin
|
|
236
|
+
.command('update [name]')
|
|
237
|
+
.description('git pull one or all enabled non-marketplace plugins')
|
|
238
|
+
.action(async (name) => {
|
|
239
|
+
try {
|
|
240
|
+
let targets;
|
|
241
|
+
if (name !== undefined) {
|
|
242
|
+
const found = findPluginByName(name);
|
|
243
|
+
if (found === null) {
|
|
244
|
+
throw notFound(`plugin not found: ${name}`);
|
|
245
|
+
}
|
|
246
|
+
targets = [{ name: found.name, scope: found.scope, root: found.root }];
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const all = listAllPlugins();
|
|
250
|
+
targets = all
|
|
251
|
+
.filter((p) => p.enabled && !p.sourceMarketplace)
|
|
252
|
+
.map((p) => ({ name: p.name, scope: p.scope, root: p.root }));
|
|
253
|
+
}
|
|
254
|
+
if (targets.length === 0) {
|
|
255
|
+
info('no plugins to update');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
for (const target of targets) {
|
|
259
|
+
const res = pull(target.root);
|
|
260
|
+
if (res.status !== 0) {
|
|
261
|
+
info(`failed to update "${target.name}": ${res.stderr.trim()}`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const manifest = readPluginManifest(target.root);
|
|
265
|
+
if (manifest !== null) {
|
|
266
|
+
updateConfig(target.scope, (cfg) => {
|
|
267
|
+
const entry = cfg.plugins[target.name];
|
|
268
|
+
if (entry !== undefined) {
|
|
269
|
+
entry.version = manifest.version;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
cfg.plugins[target.name] = {
|
|
273
|
+
enabled: true,
|
|
274
|
+
version: manifest.version,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
updateState(target.scope, (s) => {
|
|
279
|
+
if (s.plugins[target.name] === undefined) {
|
|
280
|
+
s.plugins[target.name] = {};
|
|
281
|
+
}
|
|
282
|
+
s.plugins[target.name].last_updated = nowIso();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const version = manifest !== null && manifest.version !== undefined
|
|
286
|
+
? ` → v${manifest.version}`
|
|
287
|
+
: '';
|
|
288
|
+
info(`updated "${target.name}"${version}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
handleError(e);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async function showPlugin(name, opts) {
|
|
297
|
+
const found = findPluginByName(name);
|
|
298
|
+
if (found === null) {
|
|
299
|
+
throw notFound(`plugin not found: ${name}`);
|
|
300
|
+
}
|
|
301
|
+
const skills = listSkillsInPlugin(found);
|
|
302
|
+
if (opts.json) {
|
|
303
|
+
jsonOut({
|
|
304
|
+
plugin: {
|
|
305
|
+
name: found.manifest.name,
|
|
306
|
+
version: found.manifest.version,
|
|
307
|
+
description: found.manifest.description,
|
|
308
|
+
source: found.manifest.source,
|
|
309
|
+
owner: found.manifest.owner,
|
|
310
|
+
scope: found.scope,
|
|
311
|
+
root: found.root,
|
|
312
|
+
enabled: found.enabled,
|
|
313
|
+
},
|
|
314
|
+
skills: skills.map((s) => ({
|
|
315
|
+
name: s.name,
|
|
316
|
+
description: s.frontmatter.description,
|
|
317
|
+
path: s.path,
|
|
318
|
+
})),
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const manifest = found.manifest;
|
|
323
|
+
const version = manifest.version !== undefined ? ` v${manifest.version}` : '';
|
|
324
|
+
const desc = manifest.description !== undefined ? `\n ${manifest.description}` : '';
|
|
325
|
+
const source = manifest.source !== undefined ? `\n source: ${manifest.source}` : '';
|
|
326
|
+
out(`${manifest.name}${version} (${found.scope})${desc}${source}`);
|
|
327
|
+
if (skills.length > 0) {
|
|
328
|
+
out('');
|
|
329
|
+
out('Skills:');
|
|
330
|
+
for (const s of skills) {
|
|
331
|
+
const skillDesc = s.frontmatter.description !== undefined
|
|
332
|
+
? ` — ${s.frontmatter.description}`
|
|
333
|
+
: '';
|
|
334
|
+
out(` ${s.name}${skillDesc}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
hint(`crtr: update with \`crtr plugin update ${name}\``);
|
|
338
|
+
}
|
|
339
|
+
async function setEnabled(name, enabled, scopeOpt) {
|
|
340
|
+
const scopes = listScopes(scopeOpt);
|
|
341
|
+
let acted = false;
|
|
342
|
+
for (const scope of scopes) {
|
|
343
|
+
const pDir = pluginsDir(scope);
|
|
344
|
+
if (pDir === null)
|
|
345
|
+
continue;
|
|
346
|
+
const pluginDir = join(pDir, name);
|
|
347
|
+
if (!pathExists(pluginDir))
|
|
348
|
+
continue;
|
|
349
|
+
updateConfig(scope, (cfg) => {
|
|
350
|
+
const entry = cfg.plugins[name];
|
|
351
|
+
if (entry !== undefined) {
|
|
352
|
+
entry.enabled = enabled;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
cfg.plugins[name] = { enabled };
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
info(`plugin "${name}" ${enabled ? 'enabled' : 'disabled'} in ${scope} scope`);
|
|
359
|
+
acted = true;
|
|
360
|
+
}
|
|
361
|
+
if (!acted) {
|
|
362
|
+
throw notFound(`plugin not found: ${name}`);
|
|
363
|
+
}
|
|
364
|
+
}
|