@crouton-kit/crouter 0.1.1 → 0.1.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.
Files changed (46) hide show
  1. package/bin/crouter +2 -0
  2. package/bin/crtr +2 -0
  3. package/dist/cli.js +34 -4
  4. package/dist/commands/config.d.ts +2 -0
  5. package/dist/commands/config.js +126 -0
  6. package/dist/commands/doctor.d.ts +2 -0
  7. package/dist/commands/doctor.js +216 -0
  8. package/dist/commands/marketplace.d.ts +2 -0
  9. package/dist/commands/marketplace.js +365 -0
  10. package/dist/commands/plan.d.ts +2 -0
  11. package/dist/commands/plan.js +9 -0
  12. package/dist/commands/plugin.d.ts +2 -0
  13. package/dist/commands/plugin.js +364 -0
  14. package/dist/commands/skill.d.ts +2 -0
  15. package/dist/commands/skill.js +404 -0
  16. package/dist/commands/spec.d.ts +2 -0
  17. package/dist/commands/spec.js +9 -0
  18. package/dist/commands/update.d.ts +2 -0
  19. package/dist/commands/update.js +140 -0
  20. package/dist/core/artifact.d.ts +14 -0
  21. package/dist/core/artifact.js +187 -0
  22. package/dist/core/config.d.ts +10 -0
  23. package/dist/core/config.js +83 -0
  24. package/dist/core/errors.d.ts +12 -0
  25. package/dist/core/errors.js +28 -0
  26. package/dist/core/frontmatter.d.ts +8 -0
  27. package/dist/core/frontmatter.js +84 -0
  28. package/dist/core/fs-utils.d.ts +18 -0
  29. package/dist/core/fs-utils.js +115 -0
  30. package/dist/core/git.d.ts +18 -0
  31. package/dist/core/git.js +71 -0
  32. package/dist/core/manifest.d.ts +5 -0
  33. package/dist/core/manifest.js +15 -0
  34. package/dist/core/output.d.ts +35 -0
  35. package/dist/core/output.js +99 -0
  36. package/dist/core/resolver.d.ts +28 -0
  37. package/dist/core/resolver.js +228 -0
  38. package/dist/core/scope.d.ts +12 -0
  39. package/dist/core/scope.js +87 -0
  40. package/dist/prompts/plan.d.ts +1 -0
  41. package/dist/prompts/plan.js +99 -0
  42. package/dist/prompts/spec.d.ts +1 -0
  43. package/dist/prompts/spec.js +106 -0
  44. package/dist/types.d.ts +114 -0
  45. package/dist/types.js +33 -0
  46. 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
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSkillCommands(program: Command): void;