@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.
Files changed (50) hide show
  1. package/bin/crouter +2 -0
  2. package/bin/crtr +2 -0
  3. package/dist/cli.js +36 -4
  4. package/dist/commands/config.d.ts +2 -0
  5. package/dist/commands/config.js +134 -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 +405 -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 +4 -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/auto-update.d.ts +1 -0
  23. package/dist/core/auto-update.js +86 -0
  24. package/dist/core/config.d.ts +10 -0
  25. package/dist/core/config.js +96 -0
  26. package/dist/core/errors.d.ts +12 -0
  27. package/dist/core/errors.js +28 -0
  28. package/dist/core/frontmatter.d.ts +8 -0
  29. package/dist/core/frontmatter.js +156 -0
  30. package/dist/core/fs-utils.d.ts +18 -0
  31. package/dist/core/fs-utils.js +115 -0
  32. package/dist/core/git.d.ts +18 -0
  33. package/dist/core/git.js +71 -0
  34. package/dist/core/manifest.d.ts +5 -0
  35. package/dist/core/manifest.js +15 -0
  36. package/dist/core/output.d.ts +35 -0
  37. package/dist/core/output.js +99 -0
  38. package/dist/core/resolver.d.ts +28 -0
  39. package/dist/core/resolver.js +228 -0
  40. package/dist/core/scope.d.ts +12 -0
  41. package/dist/core/scope.js +87 -0
  42. package/dist/prompts/plan.d.ts +1 -0
  43. package/dist/prompts/plan.js +106 -0
  44. package/dist/prompts/skill.d.ts +1 -0
  45. package/dist/prompts/skill.js +49 -0
  46. package/dist/prompts/spec.d.ts +1 -0
  47. package/dist/prompts/spec.js +113 -0
  48. package/dist/types.d.ts +115 -0
  49. package/dist/types.js +33 -0
  50. package/package.json +8 -5
@@ -0,0 +1,365 @@
1
+ import { join, isAbsolute } from 'node:path';
2
+ import { renameSync } from 'node:fs';
3
+ import { notFound, usage } from '../core/errors.js';
4
+ import { out, err, hint, info, jsonOut, handleError } from '../core/output.js';
5
+ import { requireScopeRoot, resolveScopeArg, projectScopeRoot, } from '../core/scope.js';
6
+ import { readConfig, updateConfig, updateState, ensureScopeInitialized } from '../core/config.js';
7
+ import { listAllMarketplaces, findMarketplaceByName, listInstalledPlugins, } from '../core/resolver.js';
8
+ import { pathExists, ensureDir, removePath, linkOrCopy, isSymlink, nowIso } from '../core/fs-utils.js';
9
+ import { clone, pull, deriveNameFromUrl } from '../core/git.js';
10
+ import { readMarketplaceManifest, readPluginManifest } from '../core/manifest.js';
11
+ export function registerMarketplaceCommands(program) {
12
+ const marketplace = program
13
+ .command('marketplace')
14
+ .description('manage marketplaces');
15
+ // list
16
+ marketplace
17
+ .command('list')
18
+ .description('list installed marketplaces across all scopes')
19
+ .option('--json', 'emit JSON')
20
+ .action(async (opts) => {
21
+ try {
22
+ const all = listAllMarketplaces();
23
+ if (opts.json) {
24
+ jsonOut({
25
+ marketplaces: all.map((m) => ({
26
+ name: m.name,
27
+ scope: m.scope,
28
+ url: m.url,
29
+ ref: m.ref,
30
+ version: m.manifest.version,
31
+ plugins_count: m.manifest.plugins.length,
32
+ root: m.root,
33
+ })),
34
+ });
35
+ return;
36
+ }
37
+ for (const m of all) {
38
+ const version = m.manifest.version !== undefined ? m.manifest.version : 'unknown';
39
+ out(`${m.scope}:${m.name}@${version} ${m.url} (${m.manifest.plugins.length} plugins)`);
40
+ }
41
+ }
42
+ catch (e) {
43
+ handleError(e, { json: opts.json });
44
+ }
45
+ });
46
+ // add
47
+ marketplace
48
+ .command('add <git-url>')
49
+ .description('clone and register a marketplace')
50
+ .option('--scope <scope>', 'user|project (default: user)')
51
+ .option('--ref <branch>', 'branch/tag to clone')
52
+ .action(async (gitUrl, opts) => {
53
+ try {
54
+ const scope = opts.scope !== undefined
55
+ ? resolveScopeArg(opts.scope)
56
+ : 'user';
57
+ if (scope === 'all') {
58
+ throw usage('--scope must be user or project, not all');
59
+ }
60
+ hint(`default scope is user (private); pass --scope project to share with collaborators`);
61
+ const root = requireScopeRoot(scope);
62
+ ensureScopeInitialized(scope, root);
63
+ const tempName = deriveNameFromUrl(gitUrl);
64
+ const mktsDir = join(root, 'marketplaces');
65
+ ensureDir(mktsDir);
66
+ const tempDest = join(mktsDir, tempName);
67
+ if (pathExists(tempDest)) {
68
+ removePath(tempDest);
69
+ }
70
+ info(`cloning ${gitUrl}...`);
71
+ clone(gitUrl, tempDest, { depth: 1, ref: opts.ref });
72
+ const manifest = readMarketplaceManifest(tempDest);
73
+ if (!manifest) {
74
+ removePath(tempDest);
75
+ throw notFound(`marketplace manifest not found at ${tempDest}/.crouter-marketplace/marketplace.json — not a valid marketplace`);
76
+ }
77
+ const finalName = manifest.name;
78
+ const finalDest = join(mktsDir, finalName);
79
+ if (finalName !== tempName) {
80
+ if (pathExists(finalDest)) {
81
+ removePath(finalDest);
82
+ }
83
+ renameSync(tempDest, finalDest);
84
+ }
85
+ updateConfig(scope, (cfg) => {
86
+ const ref = opts.ref !== undefined ? opts.ref : 'main';
87
+ cfg.marketplaces[finalName] = {
88
+ url: gitUrl,
89
+ ref,
90
+ installed_at: nowIso(),
91
+ };
92
+ });
93
+ out(finalDest);
94
+ info(`marketplace "${finalName}" added to ${scope} scope`);
95
+ }
96
+ catch (e) {
97
+ handleError(e);
98
+ }
99
+ });
100
+ // remove
101
+ marketplace
102
+ .command('remove <name>')
103
+ .description('remove a marketplace and its sourced plugins')
104
+ .option('--scope <scope>', 'user|project')
105
+ .action(async (name, opts) => {
106
+ try {
107
+ const scopeArg = opts.scope !== undefined ? resolveScopeArg(opts.scope) : undefined;
108
+ let targetScope;
109
+ let mktRoot;
110
+ if (scopeArg !== undefined && scopeArg !== 'all') {
111
+ const s = scopeArg;
112
+ const found = findMarketplaceByName(name, s);
113
+ if (!found) {
114
+ throw notFound(`marketplace not found: ${name} (scope: ${s})`);
115
+ }
116
+ targetScope = s;
117
+ mktRoot = found.root;
118
+ }
119
+ else {
120
+ const found = findMarketplaceByName(name);
121
+ if (!found) {
122
+ throw notFound(`marketplace not found: ${name}`);
123
+ }
124
+ targetScope = found.scope;
125
+ mktRoot = found.root;
126
+ }
127
+ const scope = targetScope;
128
+ const root = requireScopeRoot(scope);
129
+ const plgDir = join(root, 'plugins');
130
+ const cfg = readConfig(scope);
131
+ const pluginsToRemove = [];
132
+ for (const [pluginName, entry] of Object.entries(cfg.plugins)) {
133
+ if (entry.source_marketplace === name) {
134
+ pluginsToRemove.push(pluginName);
135
+ }
136
+ }
137
+ for (const pluginName of pluginsToRemove) {
138
+ const pluginPath = join(plgDir, pluginName);
139
+ removePath(pluginPath);
140
+ info(`removed plugin "${pluginName}" (sourced from ${name})`);
141
+ }
142
+ removePath(mktRoot);
143
+ updateConfig(scope, (c) => {
144
+ delete c.marketplaces[name];
145
+ for (const pluginName of pluginsToRemove) {
146
+ delete c.plugins[pluginName];
147
+ }
148
+ });
149
+ info(`marketplace "${name}" removed from ${scope} scope`);
150
+ }
151
+ catch (e) {
152
+ handleError(e);
153
+ }
154
+ });
155
+ // browse
156
+ marketplace
157
+ .command('browse <name>')
158
+ .description('list available plugins in a marketplace, marking installed ones')
159
+ .option('--json', 'emit JSON')
160
+ .action(async (name, opts) => {
161
+ try {
162
+ const mkt = findMarketplaceByName(name);
163
+ if (!mkt) {
164
+ throw notFound(`marketplace not found: ${name}`);
165
+ }
166
+ const allPlugins = listInstalledPlugins('user').concat(projectScopeRoot() ? listInstalledPlugins('project') : []);
167
+ const installedMap = new Map();
168
+ for (const p of allPlugins) {
169
+ if (!installedMap.has(p.name)) {
170
+ installedMap.set(p.name, p.scope);
171
+ }
172
+ }
173
+ if (opts.json) {
174
+ jsonOut({
175
+ marketplace: mkt.name,
176
+ plugins: mkt.manifest.plugins.map((entry) => {
177
+ const installedScope = installedMap.get(entry.name);
178
+ const installed = installedScope !== undefined;
179
+ const result = {
180
+ name: entry.name,
181
+ version: entry.version,
182
+ description: entry.description,
183
+ keywords: entry.keywords,
184
+ installed,
185
+ };
186
+ if (installed) {
187
+ result.installed_scope = installedScope;
188
+ }
189
+ return result;
190
+ }),
191
+ });
192
+ return;
193
+ }
194
+ for (const entry of mkt.manifest.plugins) {
195
+ const installedScope = installedMap.get(entry.name);
196
+ const installed = installedScope !== undefined;
197
+ const mark = installed ? '[x]' : '[ ]';
198
+ const version = entry.version !== undefined ? `@${entry.version}` : '';
199
+ const desc = entry.description !== undefined ? ` ${entry.description}` : '';
200
+ out(`${mark} ${entry.name}${version}${desc}`);
201
+ }
202
+ }
203
+ catch (e) {
204
+ handleError(e, { json: opts.json });
205
+ }
206
+ });
207
+ // update
208
+ marketplace
209
+ .command('update [name]')
210
+ .description('git pull marketplace(s) and update their sourced plugins')
211
+ .action(async (name, _opts) => {
212
+ try {
213
+ const toUpdate = name !== undefined
214
+ ? (() => {
215
+ const found = findMarketplaceByName(name);
216
+ if (!found) {
217
+ throw notFound(`marketplace not found: ${name}`);
218
+ }
219
+ return [found];
220
+ })()
221
+ : listAllMarketplaces();
222
+ for (const mkt of toUpdate) {
223
+ info(`updating marketplace "${mkt.name}" (${mkt.scope})...`);
224
+ const pullResult = pull(mkt.root);
225
+ if (pullResult.status !== 0) {
226
+ err(`crtr: git pull failed for ${mkt.name}: ${pullResult.stderr.trim()}`);
227
+ continue;
228
+ }
229
+ const freshManifest = readMarketplaceManifest(mkt.root);
230
+ const newVersion = freshManifest !== null && freshManifest.version !== undefined
231
+ ? freshManifest.version
232
+ : undefined;
233
+ updateState(mkt.scope, (s) => {
234
+ if (!s.marketplaces[mkt.name]) {
235
+ s.marketplaces[mkt.name] = {};
236
+ }
237
+ s.marketplaces[mkt.name].last_updated = nowIso();
238
+ });
239
+ // newVersion is available but ConfigMarketplaceEntry has no version field; state tracks last_updated
240
+ const cfg = readConfig(mkt.scope);
241
+ for (const [pluginName, entry] of Object.entries(cfg.plugins)) {
242
+ if (entry.source_marketplace !== mkt.name)
243
+ continue;
244
+ const pluginPath = join(requireScopeRoot(mkt.scope), 'plugins', pluginName);
245
+ if (isSymlink(pluginPath)) {
246
+ // symlink points into marketplace; content is already updated by the pull above
247
+ info(`plugin "${pluginName}" updated via symlink (marketplace pull)`);
248
+ }
249
+ else if (pathExists(pluginPath)) {
250
+ // URL-sourced plugin — pull it too
251
+ const pluginPullResult = pull(pluginPath);
252
+ if (pluginPullResult.status !== 0) {
253
+ err(`crtr: git pull failed for plugin "${pluginName}": ${pluginPullResult.stderr.trim()}`);
254
+ continue;
255
+ }
256
+ info(`plugin "${pluginName}" updated`);
257
+ }
258
+ if (freshManifest !== null) {
259
+ const pluginEntry = freshManifest.plugins.find((p) => p.name === pluginName);
260
+ if (pluginEntry !== undefined && pluginEntry.version !== undefined) {
261
+ updateConfig(mkt.scope, (c) => {
262
+ if (c.plugins[pluginName]) {
263
+ c.plugins[pluginName].version = pluginEntry.version;
264
+ }
265
+ });
266
+ }
267
+ }
268
+ updateState(mkt.scope, (s) => {
269
+ if (!s.plugins[pluginName]) {
270
+ s.plugins[pluginName] = {};
271
+ }
272
+ s.plugins[pluginName].last_updated = nowIso();
273
+ });
274
+ }
275
+ info(`marketplace "${mkt.name}" up to date`);
276
+ }
277
+ }
278
+ catch (e) {
279
+ handleError(e);
280
+ }
281
+ });
282
+ // install
283
+ marketplace
284
+ .command('install <marketplace-plugin>')
285
+ .description('install a plugin from a marketplace using <marketplace>:<plugin> syntax')
286
+ .option('--scope <scope>', 'user|project')
287
+ .action(async (marketplacePlugin, opts) => {
288
+ try {
289
+ const colonIdx = marketplacePlugin.indexOf(':');
290
+ if (colonIdx === -1) {
291
+ throw usage(`argument must be in the form <marketplace>:<plugin> (e.g. crouton-kit:authoring)`);
292
+ }
293
+ const mktName = marketplacePlugin.slice(0, colonIdx);
294
+ const pluginName = marketplacePlugin.slice(colonIdx + 1);
295
+ if (!mktName || !pluginName) {
296
+ throw usage(`argument must be in the form <marketplace>:<plugin> (e.g. crouton-kit:authoring)`);
297
+ }
298
+ const scopeArg = opts.scope !== undefined ? resolveScopeArg(opts.scope) : undefined;
299
+ let mkt;
300
+ if (scopeArg !== undefined && scopeArg !== 'all') {
301
+ mkt = findMarketplaceByName(mktName, scopeArg);
302
+ }
303
+ else {
304
+ mkt = findMarketplaceByName(mktName);
305
+ }
306
+ if (!mkt) {
307
+ throw notFound(`marketplace not found: ${mktName}`);
308
+ }
309
+ const entry = mkt.manifest.plugins.find((p) => p.name === pluginName);
310
+ if (!entry) {
311
+ throw notFound(`plugin "${pluginName}" not found in marketplace "${mktName}"`);
312
+ }
313
+ let destScope;
314
+ if (opts.scope !== undefined && scopeArg !== 'all') {
315
+ destScope = scopeArg;
316
+ }
317
+ else {
318
+ destScope = mkt.scope;
319
+ }
320
+ const destRoot = requireScopeRoot(destScope);
321
+ ensureScopeInitialized(destScope, destRoot);
322
+ const destPluginDir = join(destRoot, 'plugins', pluginName);
323
+ const source = entry.source;
324
+ const isRelativePath = source.startsWith('./') || source.startsWith('../') ||
325
+ (!source.includes('://') && !isAbsolute(source));
326
+ if (isRelativePath) {
327
+ const sourcePath = join(mkt.root, source);
328
+ if (!pathExists(sourcePath)) {
329
+ throw notFound(`plugin source path does not exist: ${sourcePath}`);
330
+ }
331
+ linkOrCopy(sourcePath, destPluginDir);
332
+ info(`linked plugin "${pluginName}" → ${sourcePath}`);
333
+ }
334
+ else {
335
+ // URL source — clone directly
336
+ if (pathExists(destPluginDir)) {
337
+ removePath(destPluginDir);
338
+ }
339
+ info(`cloning plugin "${pluginName}" from ${source}...`);
340
+ clone(source, destPluginDir, { depth: 1 });
341
+ }
342
+ const pluginManifest = readPluginManifest(destPluginDir);
343
+ if (!pluginManifest) {
344
+ removePath(destPluginDir);
345
+ throw notFound(`plugin manifest not found at ${destPluginDir}/.crouter-plugin/plugin.json`);
346
+ }
347
+ const version = entry.version !== undefined ? entry.version : pluginManifest.version;
348
+ updateConfig(destScope, (cfg) => {
349
+ const pluginCfg = {
350
+ enabled: true,
351
+ source_marketplace: mktName,
352
+ };
353
+ if (version !== undefined) {
354
+ pluginCfg.version = version;
355
+ }
356
+ cfg.plugins[pluginName] = pluginCfg;
357
+ });
358
+ out(destPluginDir);
359
+ info(`plugin "${pluginName}" installed to ${destScope} scope`);
360
+ }
361
+ catch (e) {
362
+ handleError(e);
363
+ }
364
+ });
365
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerPlanCommand(program: Command): void;
@@ -0,0 +1,9 @@
1
+ import { registerArtifactCommand } from '../core/artifact.js';
2
+ import { planPrompt } from '../prompts/plan.js';
3
+ export function registerPlanCommand(program) {
4
+ registerArtifactCommand(program, {
5
+ command: 'plan',
6
+ kind: 'plans',
7
+ promptFn: planPrompt,
8
+ });
9
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerPluginCommands(program: Command): void;