@crouton-kit/crouter 0.2.6 → 0.3.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/skills/crouter-development/marketplaces/SKILL.md +9 -9
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
- package/dist/cli.js +42 -37
- package/dist/commands/__tests__/human.test.d.ts +1 -0
- package/dist/commands/__tests__/human.test.js +214 -0
- package/dist/commands/__tests__/skill.test.d.ts +1 -0
- package/dist/commands/__tests__/skill.test.js +294 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +179 -0
- package/dist/commands/flow.d.ts +2 -0
- package/dist/commands/flow.js +24 -0
- package/dist/commands/human.d.ts +2 -0
- package/dist/commands/human.js +480 -0
- package/dist/commands/job.d.ts +2 -0
- package/dist/commands/job.js +669 -0
- package/dist/commands/pkg.d.ts +2 -0
- package/dist/commands/pkg.js +1021 -0
- package/dist/commands/plan.d.ts +4 -2
- package/dist/commands/plan.js +306 -22
- package/dist/commands/skill.d.ts +2 -2
- package/dist/commands/skill.js +613 -456
- package/dist/commands/spec.d.ts +3 -2
- package/dist/commands/spec.js +283 -10
- package/dist/commands/sys.d.ts +2 -0
- package/dist/commands/sys.js +712 -0
- package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
- package/dist/core/__tests__/argv-parser.test.js +199 -0
- package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
- package/dist/core/__tests__/flow-leaves.test.js +248 -0
- package/dist/core/__tests__/job.test.d.ts +1 -0
- package/dist/core/__tests__/job.test.js +346 -0
- package/dist/core/__tests__/pkg.test.d.ts +1 -0
- package/dist/core/__tests__/pkg.test.js +218 -0
- package/dist/core/__tests__/sys.test.d.ts +1 -0
- package/dist/core/__tests__/sys.test.js +208 -0
- package/dist/core/artifact.d.ts +29 -18
- package/dist/core/artifact.js +78 -221
- package/dist/core/auto-update.js +11 -4
- package/dist/core/command.d.ts +36 -0
- package/dist/core/command.js +287 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +5 -0
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +4 -0
- package/dist/core/help.d.ts +98 -0
- package/dist/core/help.js +163 -0
- package/dist/core/io.d.ts +29 -0
- package/dist/core/io.js +83 -0
- package/dist/core/jobs.d.ts +87 -0
- package/dist/core/jobs.js +353 -0
- package/dist/core/pagination.d.ts +33 -0
- package/dist/core/pagination.js +89 -0
- package/dist/core/self-update.d.ts +21 -0
- package/dist/core/self-update.js +105 -0
- package/dist/core/spawn.d.ts +47 -65
- package/dist/core/spawn.js +78 -228
- package/dist/prompts/agent.d.ts +10 -5
- package/dist/prompts/agent.js +51 -74
- package/dist/prompts/debug.d.ts +8 -0
- package/dist/prompts/debug.js +37 -0
- package/dist/prompts/review.js +4 -11
- package/dist/prompts/skill.d.ts +0 -1
- package/dist/prompts/skill.js +95 -149
- package/package.json +4 -2
- package/dist/commands/agent.d.ts +0 -2
- package/dist/commands/agent.js +0 -265
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -146
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -268
- package/dist/commands/marketplace.d.ts +0 -2
- package/dist/commands/marketplace.js +0 -365
- package/dist/commands/plugin.d.ts +0 -2
- package/dist/commands/plugin.js +0 -367
- package/dist/commands/update.d.ts +0 -4
- package/dist/commands/update.js +0 -140
- package/dist/prompts/plan.d.ts +0 -1
- package/dist/prompts/plan.js +0 -175
- package/dist/prompts/spec.d.ts +0 -1
- package/dist/prompts/spec.js +0 -153
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
// `crtr pkg` subtree: replaces plugin.ts + marketplace.ts for the agent-first CLI.
|
|
2
|
+
// Sub-branches: plugin {manage {install,remove,enable,disable,update}, inspect {list,show}}
|
|
3
|
+
// market {manage {add,remove,update,install}, inspect {list,browse}}
|
|
4
|
+
import { join, isAbsolute } from 'node:path';
|
|
5
|
+
import { renameSync } from 'node:fs';
|
|
6
|
+
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
7
|
+
import { notFound, usage, general } from '../core/errors.js';
|
|
8
|
+
import { createJob, appendEvent, writeResult } from '../core/jobs.js';
|
|
9
|
+
import { paginate } from '../core/pagination.js';
|
|
10
|
+
import { listInstalledPlugins, listAllPlugins, findPluginByName, listSkillsInPlugin, listInstalledMarketplaces, listAllMarketplaces, findMarketplaceByName, } from '../core/resolver.js';
|
|
11
|
+
import { pluginsDir, ensureProjectScopeRoot, resolveScopeArg, userScopeRoot, requireScopeRoot, projectScopeRoot, } from '../core/scope.js';
|
|
12
|
+
import { updateConfig, updateState, ensureScopeInitialized, readConfig, } from '../core/config.js';
|
|
13
|
+
import { pathExists, ensureDir, removePath, linkOrCopy, isSymlink, nowIso } from '../core/fs-utils.js';
|
|
14
|
+
import { clone, pull, deriveNameFromUrl, currentSha } from '../core/git.js';
|
|
15
|
+
import { readPluginManifest, readMarketplaceManifest } from '../core/manifest.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const GIT_URL_RE = /^(https?:\/\/|git@|ssh:\/\/|file:\/\/)/;
|
|
20
|
+
function isGitUrl(arg) {
|
|
21
|
+
return GIT_URL_RE.test(arg) || arg.endsWith('.git');
|
|
22
|
+
}
|
|
23
|
+
function resolveInstallScope(scopeInput) {
|
|
24
|
+
if (scopeInput !== undefined) {
|
|
25
|
+
const resolved = resolveScopeArg(scopeInput);
|
|
26
|
+
if (resolved === 'all' || resolved === 'builtin') {
|
|
27
|
+
throw usage('scope must be "user" or "project"');
|
|
28
|
+
}
|
|
29
|
+
return resolved;
|
|
30
|
+
}
|
|
31
|
+
// Default: project if available, else user
|
|
32
|
+
return projectScopeRoot() !== null ? 'project' : 'user';
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// plugin.manage.install
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
const pluginInstall = defineLeaf({
|
|
38
|
+
name: 'install',
|
|
39
|
+
help: {
|
|
40
|
+
name: 'pkg plugin manage install',
|
|
41
|
+
summary: 'install a plugin from a git URL into the given scope',
|
|
42
|
+
params: [
|
|
43
|
+
{ kind: 'positional', name: 'source', type: 'string', required: true, constraint: 'Git URL or relative path to the plugin directory.' },
|
|
44
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'One of: user, project. Default: project if available, else user.' },
|
|
45
|
+
{ kind: 'flag', name: 'ref', type: 'string', required: false, constraint: 'Git ref (branch/tag) to clone. Default: default branch.' },
|
|
46
|
+
],
|
|
47
|
+
output: [
|
|
48
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Plugin name as declared in plugin.json.' },
|
|
49
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the plugin was installed into.' },
|
|
50
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the installed plugin directory.' },
|
|
51
|
+
],
|
|
52
|
+
outputKind: 'object',
|
|
53
|
+
effects: ['Clones or copies the plugin into the scope plugins directory. Registers the plugin in config.json.'],
|
|
54
|
+
},
|
|
55
|
+
run: async (input) => {
|
|
56
|
+
const source = input['source'];
|
|
57
|
+
const scopeInput = input['scope'];
|
|
58
|
+
const ref = input['ref'];
|
|
59
|
+
if (!isGitUrl(source)) {
|
|
60
|
+
throw usage(`"${source}" is not a git URL. Use \`pkg market manage install\` to install from a marketplace.`);
|
|
61
|
+
}
|
|
62
|
+
const scope = resolveInstallScope(scopeInput);
|
|
63
|
+
let scopeRootPath;
|
|
64
|
+
if (scope === 'project') {
|
|
65
|
+
scopeRootPath = ensureProjectScopeRoot();
|
|
66
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
scopeRootPath = userScopeRoot();
|
|
70
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
71
|
+
}
|
|
72
|
+
const pDir = join(scopeRootPath, 'plugins');
|
|
73
|
+
ensureDir(pDir);
|
|
74
|
+
const tempName = deriveNameFromUrl(source);
|
|
75
|
+
const tempDir = join(pDir, tempName);
|
|
76
|
+
if (pathExists(tempDir)) {
|
|
77
|
+
throw general(`plugin directory already exists: ${tempDir}\n` +
|
|
78
|
+
`Uninstall the existing plugin first with \`pkg plugin manage remove ${tempName}\``);
|
|
79
|
+
}
|
|
80
|
+
clone(source, tempDir, { ref: ref !== undefined ? ref : undefined, depth: 1 });
|
|
81
|
+
const manifest = readPluginManifest(tempDir);
|
|
82
|
+
if (manifest === null) {
|
|
83
|
+
removePath(tempDir);
|
|
84
|
+
throw general(`cloned repo does not contain a valid .crouter-plugin/plugin.json: ${source}`);
|
|
85
|
+
}
|
|
86
|
+
const finalName = manifest.name;
|
|
87
|
+
let finalDir = tempDir;
|
|
88
|
+
if (finalName !== tempName) {
|
|
89
|
+
const candidateDir = join(pDir, finalName);
|
|
90
|
+
if (pathExists(candidateDir)) {
|
|
91
|
+
removePath(tempDir);
|
|
92
|
+
throw general(`plugin "${finalName}" is already installed at ${candidateDir}`);
|
|
93
|
+
}
|
|
94
|
+
renameSync(tempDir, candidateDir);
|
|
95
|
+
finalDir = candidateDir;
|
|
96
|
+
}
|
|
97
|
+
updateConfig(scope, (cfg) => {
|
|
98
|
+
cfg.plugins[finalName] = {
|
|
99
|
+
enabled: true,
|
|
100
|
+
version: manifest.version,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
return { name: finalName, scope, path: finalDir };
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// plugin.manage.remove
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
const pluginRemove = defineLeaf({
|
|
110
|
+
name: 'remove',
|
|
111
|
+
help: {
|
|
112
|
+
name: 'pkg plugin manage remove',
|
|
113
|
+
summary: 'remove a plugin and its directory from the given scope',
|
|
114
|
+
params: [
|
|
115
|
+
{ kind: 'positional', name: 'name', type: 'string', required: true, constraint: 'Plugin name to remove.' },
|
|
116
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'One of: user, project. Default: searches all scopes.' },
|
|
117
|
+
],
|
|
118
|
+
output: [
|
|
119
|
+
{ name: 'removed', type: 'boolean', required: true, constraint: 'True if removed from at least one scope.' },
|
|
120
|
+
{ name: 'scopes', type: 'string[]', required: true, constraint: 'Scopes the plugin was removed from.' },
|
|
121
|
+
],
|
|
122
|
+
outputKind: 'object',
|
|
123
|
+
effects: ['Deletes the plugin directory. Removes the plugin entry from config.json.'],
|
|
124
|
+
},
|
|
125
|
+
run: async (input) => {
|
|
126
|
+
const name = input['name'];
|
|
127
|
+
const scopeInput = input['scope'];
|
|
128
|
+
if (name === 'crtr') {
|
|
129
|
+
throw usage('cannot remove builtin plugin "crtr" — it ships with the binary');
|
|
130
|
+
}
|
|
131
|
+
let scopes;
|
|
132
|
+
if (scopeInput !== undefined) {
|
|
133
|
+
const resolved = resolveScopeArg(scopeInput);
|
|
134
|
+
if (resolved === 'builtin')
|
|
135
|
+
throw usage('cannot remove plugins from builtin scope');
|
|
136
|
+
scopes = resolved === 'all' ? ['project', 'user'].filter((s) => s !== 'project' || projectScopeRoot() !== null) : [resolved];
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
scopes = ['project', 'user'].filter((s) => s !== 'project' || projectScopeRoot() !== null);
|
|
140
|
+
}
|
|
141
|
+
const removedFrom = [];
|
|
142
|
+
for (const scope of scopes) {
|
|
143
|
+
const pDir = pluginsDir(scope);
|
|
144
|
+
if (pDir === null)
|
|
145
|
+
continue;
|
|
146
|
+
const pluginDir = join(pDir, name);
|
|
147
|
+
if (!pathExists(pluginDir))
|
|
148
|
+
continue;
|
|
149
|
+
removePath(pluginDir);
|
|
150
|
+
updateConfig(scope, (cfg) => {
|
|
151
|
+
delete cfg.plugins[name];
|
|
152
|
+
});
|
|
153
|
+
removedFrom.push(scope);
|
|
154
|
+
}
|
|
155
|
+
if (removedFrom.length === 0) {
|
|
156
|
+
throw notFound(`plugin not found: ${name}`);
|
|
157
|
+
}
|
|
158
|
+
return { removed: true, scopes: removedFrom };
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// plugin.manage.enable / disable (shared helper)
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
async function setPluginEnabled(input, enabled) {
|
|
165
|
+
const name = input['name'];
|
|
166
|
+
const scopeInput = input['scope'];
|
|
167
|
+
let scopes;
|
|
168
|
+
if (scopeInput !== undefined) {
|
|
169
|
+
const resolved = resolveScopeArg(scopeInput);
|
|
170
|
+
if (resolved === 'builtin')
|
|
171
|
+
throw usage('cannot enable/disable plugins in builtin scope');
|
|
172
|
+
scopes = resolved === 'all' ? ['project', 'user'].filter((s) => s !== 'project' || projectScopeRoot() !== null) : [resolved];
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
scopes = ['project', 'user'].filter((s) => s !== 'project' || projectScopeRoot() !== null);
|
|
176
|
+
}
|
|
177
|
+
let actedScope;
|
|
178
|
+
for (const scope of scopes) {
|
|
179
|
+
const pDir = pluginsDir(scope);
|
|
180
|
+
if (pDir === null)
|
|
181
|
+
continue;
|
|
182
|
+
const pluginDir = join(pDir, name);
|
|
183
|
+
if (!pathExists(pluginDir))
|
|
184
|
+
continue;
|
|
185
|
+
updateConfig(scope, (cfg) => {
|
|
186
|
+
const entry = cfg.plugins[name];
|
|
187
|
+
if (entry !== undefined) {
|
|
188
|
+
entry.enabled = enabled;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
cfg.plugins[name] = { enabled };
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
actedScope = scope;
|
|
195
|
+
break; // only act on first found scope
|
|
196
|
+
}
|
|
197
|
+
if (actedScope === undefined) {
|
|
198
|
+
throw notFound(`plugin not found: ${name}`);
|
|
199
|
+
}
|
|
200
|
+
return { name, scope: actedScope, enabled };
|
|
201
|
+
}
|
|
202
|
+
const pluginEnable = defineLeaf({
|
|
203
|
+
name: 'enable',
|
|
204
|
+
help: {
|
|
205
|
+
name: 'pkg plugin manage enable',
|
|
206
|
+
summary: 'enable a plugin in the given scope',
|
|
207
|
+
params: [
|
|
208
|
+
{ kind: 'positional', name: 'name', type: 'string', required: true, constraint: 'Plugin name to enable.' },
|
|
209
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'One of: user, project. Default: scope where the plugin is installed.' },
|
|
210
|
+
],
|
|
211
|
+
output: [
|
|
212
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Plugin name.' },
|
|
213
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the change was applied in.' },
|
|
214
|
+
{ name: 'enabled', type: 'boolean', required: true, constraint: 'Always true.' },
|
|
215
|
+
],
|
|
216
|
+
outputKind: 'object',
|
|
217
|
+
effects: ['Sets plugin enabled=true in config.json.'],
|
|
218
|
+
},
|
|
219
|
+
run: async (input) => setPluginEnabled(input, true),
|
|
220
|
+
});
|
|
221
|
+
const pluginDisable = defineLeaf({
|
|
222
|
+
name: 'disable',
|
|
223
|
+
help: {
|
|
224
|
+
name: 'pkg plugin manage disable',
|
|
225
|
+
summary: 'disable a plugin (keeps files, hides from resolution)',
|
|
226
|
+
params: [
|
|
227
|
+
{ kind: 'positional', name: 'name', type: 'string', required: true, constraint: 'Plugin name to disable.' },
|
|
228
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'One of: user, project. Default: scope where the plugin is installed.' },
|
|
229
|
+
],
|
|
230
|
+
output: [
|
|
231
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Plugin name.' },
|
|
232
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the change was applied in.' },
|
|
233
|
+
{ name: 'enabled', type: 'boolean', required: true, constraint: 'Always false.' },
|
|
234
|
+
],
|
|
235
|
+
outputKind: 'object',
|
|
236
|
+
effects: ['Sets plugin enabled=false in config.json.'],
|
|
237
|
+
},
|
|
238
|
+
run: async (input) => setPluginEnabled(input, false),
|
|
239
|
+
});
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// plugin.manage.update
|
|
242
|
+
// Single plugin: blocking (bounded). All plugins: job handle (unbounded network).
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
const pluginUpdate = defineLeaf({
|
|
245
|
+
name: 'update',
|
|
246
|
+
help: {
|
|
247
|
+
name: 'pkg plugin manage update',
|
|
248
|
+
summary: 'git pull updates for one or all installed plugins',
|
|
249
|
+
params: [
|
|
250
|
+
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Plugin name to update. Omit to update all (returns a job handle).' },
|
|
251
|
+
],
|
|
252
|
+
output: [
|
|
253
|
+
{ name: 'updated', type: 'object[]', required: false, constraint: 'Present for single-plugin (blocking) path: [{name, updated, sha}].' },
|
|
254
|
+
{ name: 'job_id', type: 'string', required: false, constraint: 'Present for all-plugins (async) path.' },
|
|
255
|
+
{ name: 'follow_up', type: 'string', required: false, constraint: 'Instruction for retrieving async result.' },
|
|
256
|
+
],
|
|
257
|
+
outputKind: 'object',
|
|
258
|
+
effects: ['Runs git pull in plugin directories. Updates version in config.json.'],
|
|
259
|
+
},
|
|
260
|
+
run: async (input) => {
|
|
261
|
+
const name = input['name'];
|
|
262
|
+
if (name !== undefined) {
|
|
263
|
+
// Single plugin — blocking (bounded network op, one repo)
|
|
264
|
+
const found = findPluginByName(name);
|
|
265
|
+
if (found === null) {
|
|
266
|
+
throw notFound(`plugin not found: ${name}`);
|
|
267
|
+
}
|
|
268
|
+
const shaBefore = currentSha(found.root);
|
|
269
|
+
const res = pull(found.root);
|
|
270
|
+
if (res.status !== 0) {
|
|
271
|
+
throw general(`git pull failed for "${name}": ${res.stderr.trim()}`);
|
|
272
|
+
}
|
|
273
|
+
const shaAfter = currentSha(found.root);
|
|
274
|
+
const updated = shaBefore !== shaAfter;
|
|
275
|
+
const manifest = readPluginManifest(found.root);
|
|
276
|
+
if (manifest !== null) {
|
|
277
|
+
updateConfig(found.scope, (cfg) => {
|
|
278
|
+
const entry = cfg.plugins[found.name];
|
|
279
|
+
if (entry !== undefined) {
|
|
280
|
+
entry.version = manifest.version;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
cfg.plugins[found.name] = { enabled: true, version: manifest.version };
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
updateState(found.scope, (s) => {
|
|
287
|
+
if (s.plugins[found.name] === undefined) {
|
|
288
|
+
s.plugins[found.name] = {};
|
|
289
|
+
}
|
|
290
|
+
s.plugins[found.name].last_updated = nowIso();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
updated: [
|
|
295
|
+
{
|
|
296
|
+
name: found.name,
|
|
297
|
+
updated,
|
|
298
|
+
sha: shaAfter !== null ? shaAfter : '',
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// All plugins — async job (unbounded network, N repos)
|
|
304
|
+
const all = listAllPlugins();
|
|
305
|
+
const targets = all
|
|
306
|
+
.filter((p) => p.enabled && !p.sourceMarketplace && p.scope !== 'builtin')
|
|
307
|
+
.map((p) => ({ name: p.name, scope: p.scope, root: p.root }));
|
|
308
|
+
const { jobId } = createJob('pkg-update', { cwd: process.cwd(), pid: process.pid });
|
|
309
|
+
// Fire-and-forget: run updates in background after returning job handle.
|
|
310
|
+
// Using setImmediate so the job handle is returned before the work starts.
|
|
311
|
+
setImmediate(() => {
|
|
312
|
+
void (async () => {
|
|
313
|
+
const results = [];
|
|
314
|
+
for (const target of targets) {
|
|
315
|
+
appendEvent(jobId, { level: 'info', event: 'updating', message: `updating ${target.name}` });
|
|
316
|
+
const shaBefore = currentSha(target.root);
|
|
317
|
+
const res = pull(target.root);
|
|
318
|
+
if (res.status !== 0) {
|
|
319
|
+
appendEvent(jobId, { level: 'error', event: 'pull_failed', message: `git pull failed for "${target.name}": ${res.stderr.trim()}` });
|
|
320
|
+
results.push({ name: target.name, updated: false, sha: shaBefore !== null ? shaBefore : '' });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const shaAfter = currentSha(target.root);
|
|
324
|
+
const updated = shaBefore !== shaAfter;
|
|
325
|
+
const manifest = readPluginManifest(target.root);
|
|
326
|
+
if (manifest !== null) {
|
|
327
|
+
updateConfig(target.scope, (cfg) => {
|
|
328
|
+
const entry = cfg.plugins[target.name];
|
|
329
|
+
if (entry !== undefined) {
|
|
330
|
+
entry.version = manifest.version;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
cfg.plugins[target.name] = { enabled: true, version: manifest.version };
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
updateState(target.scope, (s) => {
|
|
337
|
+
if (s.plugins[target.name] === undefined) {
|
|
338
|
+
s.plugins[target.name] = {};
|
|
339
|
+
}
|
|
340
|
+
s.plugins[target.name].last_updated = nowIso();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
results.push({ name: target.name, updated, sha: shaAfter !== null ? shaAfter : '' });
|
|
344
|
+
appendEvent(jobId, { level: 'info', event: 'updated', message: `${target.name} updated=${updated}` });
|
|
345
|
+
}
|
|
346
|
+
writeResult(jobId, { updated: results }, 'done');
|
|
347
|
+
})();
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
job_id: jobId,
|
|
351
|
+
follow_up: `crtr job read result ${jobId} --wait`,
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
const pluginManageBranch = defineBranch({
|
|
356
|
+
name: 'manage',
|
|
357
|
+
help: {
|
|
358
|
+
name: 'pkg plugin manage',
|
|
359
|
+
summary: 'install, remove, enable, disable, or update plugins',
|
|
360
|
+
children: [
|
|
361
|
+
{ name: 'install', desc: 'install from a git URL', useWhen: 'adding a new plugin' },
|
|
362
|
+
{ name: 'remove', desc: 'remove plugin and directory', useWhen: 'uninstalling a plugin' },
|
|
363
|
+
{ name: 'enable', desc: 'enable a plugin', useWhen: 'activating a disabled plugin' },
|
|
364
|
+
{ name: 'disable', desc: 'disable without removing', useWhen: 'temporarily hiding a plugin' },
|
|
365
|
+
{ name: 'update', desc: 'pull latest from git', useWhen: 'updating a plugin to its latest version' },
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
children: [pluginInstall, pluginRemove, pluginEnable, pluginDisable, pluginUpdate],
|
|
369
|
+
});
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// plugin.inspect.list
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
const pluginList = defineLeaf({
|
|
374
|
+
name: 'list',
|
|
375
|
+
help: {
|
|
376
|
+
name: 'pkg plugin inspect list',
|
|
377
|
+
summary: 'paginated list of installed plugins',
|
|
378
|
+
params: [
|
|
379
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
|
|
380
|
+
{ kind: 'flag', name: 'include-disabled', type: 'bool', required: false, constraint: 'Include disabled plugins. Default: false.' },
|
|
381
|
+
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 50, constraint: 'Default 50, max 200.' },
|
|
382
|
+
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
383
|
+
],
|
|
384
|
+
output: [
|
|
385
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {name, scope, version?, enabled, source_marketplace?, path}. Sorted by scope then name ascending.' },
|
|
386
|
+
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'null means no more items.' },
|
|
387
|
+
{ name: 'total', type: 'integer | null', required: true, constraint: 'Exact when cheap; null otherwise.' },
|
|
388
|
+
],
|
|
389
|
+
outputKind: 'object',
|
|
390
|
+
effects: ['None. Read-only.'],
|
|
391
|
+
},
|
|
392
|
+
run: async (input) => {
|
|
393
|
+
const scopeInput = input['scope'];
|
|
394
|
+
const includeDisabled = input['includeDisabled'] ?? false;
|
|
395
|
+
const limitRaw = input['limit'];
|
|
396
|
+
const limit = limitRaw !== undefined ? Math.min(Math.max(1, limitRaw), 200) : 50;
|
|
397
|
+
const cursor = input['cursor'];
|
|
398
|
+
let scopesToScan;
|
|
399
|
+
if (scopeInput !== undefined) {
|
|
400
|
+
const resolved = resolveScopeArg(scopeInput);
|
|
401
|
+
scopesToScan = resolved === 'all'
|
|
402
|
+
? ['project', 'user'].filter((s) => s !== 'project' || projectScopeRoot() !== null)
|
|
403
|
+
: [resolved];
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
scopesToScan = ['project', 'user'].filter((s) => s !== 'project' || projectScopeRoot() !== null);
|
|
407
|
+
}
|
|
408
|
+
const all = scopesToScan
|
|
409
|
+
.flatMap((s) => listInstalledPlugins(s))
|
|
410
|
+
.filter((p) => includeDisabled || p.enabled)
|
|
411
|
+
.sort((a, b) => {
|
|
412
|
+
if (a.scope === 'project' && b.scope !== 'project')
|
|
413
|
+
return -1;
|
|
414
|
+
if (a.scope !== 'project' && b.scope === 'project')
|
|
415
|
+
return 1;
|
|
416
|
+
return a.name.localeCompare(b.name);
|
|
417
|
+
});
|
|
418
|
+
const result = paginate(all, { limit, cursor: cursor !== undefined ? cursor : undefined }, {
|
|
419
|
+
defaultLimit: 50,
|
|
420
|
+
maxLimit: 200,
|
|
421
|
+
keyOf: (p) => `${p.scope}:${p.name}`,
|
|
422
|
+
total: 'count',
|
|
423
|
+
});
|
|
424
|
+
return {
|
|
425
|
+
items: result.items.map((p) => ({
|
|
426
|
+
name: p.name,
|
|
427
|
+
scope: p.scope,
|
|
428
|
+
version: p.version,
|
|
429
|
+
enabled: p.enabled,
|
|
430
|
+
source_marketplace: p.sourceMarketplace,
|
|
431
|
+
path: p.root,
|
|
432
|
+
})),
|
|
433
|
+
next_cursor: result.next_cursor,
|
|
434
|
+
total: result.total,
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// plugin.inspect.show
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
const pluginShow = defineLeaf({
|
|
442
|
+
name: 'show',
|
|
443
|
+
help: {
|
|
444
|
+
name: 'pkg plugin inspect show',
|
|
445
|
+
summary: 'read plugin manifest and metadata by name',
|
|
446
|
+
params: [
|
|
447
|
+
{ kind: 'positional', name: 'name', type: 'string', required: true, constraint: 'Plugin name.' },
|
|
448
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'One of: user, project. Narrows resolution.' },
|
|
449
|
+
],
|
|
450
|
+
output: [
|
|
451
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Plugin name.' },
|
|
452
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the plugin is installed in.' },
|
|
453
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the plugin directory.' },
|
|
454
|
+
{ name: 'enabled', type: 'boolean', required: true, constraint: 'Whether the plugin is active.' },
|
|
455
|
+
{ name: 'manifest', type: 'object', required: true, constraint: 'Full plugin.json contents.' },
|
|
456
|
+
{ name: 'skills', type: 'object[]', required: true, constraint: 'Each: {name, path, enabled}. Skills provided by the plugin.' },
|
|
457
|
+
],
|
|
458
|
+
outputKind: 'object',
|
|
459
|
+
effects: ['None. Read-only.'],
|
|
460
|
+
},
|
|
461
|
+
run: async (input) => {
|
|
462
|
+
const name = input['name'];
|
|
463
|
+
const scopeInput = input['scope'];
|
|
464
|
+
let found;
|
|
465
|
+
if (scopeInput !== undefined) {
|
|
466
|
+
const resolved = resolveScopeArg(scopeInput);
|
|
467
|
+
if (resolved === 'all' || resolved === 'builtin') {
|
|
468
|
+
found = findPluginByName(name);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
found = findPluginByName(name, resolved);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
found = findPluginByName(name);
|
|
476
|
+
}
|
|
477
|
+
if (found === null) {
|
|
478
|
+
throw notFound(`plugin not found: ${name}`);
|
|
479
|
+
}
|
|
480
|
+
const skills = listSkillsInPlugin(found);
|
|
481
|
+
return {
|
|
482
|
+
name: found.name,
|
|
483
|
+
scope: found.scope,
|
|
484
|
+
path: found.root,
|
|
485
|
+
enabled: found.enabled,
|
|
486
|
+
manifest: found.manifest,
|
|
487
|
+
skills: skills.map((s) => ({
|
|
488
|
+
name: s.name,
|
|
489
|
+
path: s.path,
|
|
490
|
+
enabled: s.enabled,
|
|
491
|
+
})),
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
const pluginInspectBranch = defineBranch({
|
|
496
|
+
name: 'inspect',
|
|
497
|
+
help: {
|
|
498
|
+
name: 'pkg plugin inspect',
|
|
499
|
+
summary: 'read plugin metadata without modifying state',
|
|
500
|
+
children: [
|
|
501
|
+
{ name: 'list', desc: 'paginated list of installed plugins', useWhen: 'enumerating what plugins are installed' },
|
|
502
|
+
{ name: 'show', desc: 'read plugin manifest and skill inventory', useWhen: 'inspecting a specific plugin\'s details' },
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
children: [pluginList, pluginShow],
|
|
506
|
+
});
|
|
507
|
+
const pluginBranch = defineBranch({
|
|
508
|
+
name: 'plugin',
|
|
509
|
+
help: {
|
|
510
|
+
name: 'pkg plugin',
|
|
511
|
+
summary: 'install and manage plugins that extend crtr with skills',
|
|
512
|
+
model: 'Plugins are git repos or local directories containing a .crouter-plugin/plugin.json manifest and a skills/ directory.',
|
|
513
|
+
children: [
|
|
514
|
+
{ name: 'manage', desc: 'install, remove, enable, disable, update', useWhen: 'changing plugin state' },
|
|
515
|
+
{ name: 'inspect', desc: 'list or show installed plugins', useWhen: 'reading plugin metadata' },
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
children: [pluginManageBranch, pluginInspectBranch],
|
|
519
|
+
});
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// market.manage.add
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
const marketAdd = defineLeaf({
|
|
524
|
+
name: 'add',
|
|
525
|
+
help: {
|
|
526
|
+
name: 'pkg market manage add',
|
|
527
|
+
summary: 'add a marketplace by git URL',
|
|
528
|
+
params: [
|
|
529
|
+
{ kind: 'flag', name: 'url', type: 'string', required: true, constraint: 'Git URL of the marketplace repo.' },
|
|
530
|
+
{ kind: 'flag', name: 'ref', type: 'string', required: false, constraint: 'Git ref to track. Default: main.' },
|
|
531
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'One of: user, project. Default: project if available, else user.' },
|
|
532
|
+
],
|
|
533
|
+
output: [
|
|
534
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Marketplace name as declared in marketplace.json.' },
|
|
535
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the marketplace was added into.' },
|
|
536
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the cloned marketplace directory.' },
|
|
537
|
+
],
|
|
538
|
+
outputKind: 'object',
|
|
539
|
+
effects: ['Clones the marketplace repo. Registers the marketplace in config.json.'],
|
|
540
|
+
},
|
|
541
|
+
run: async (input) => {
|
|
542
|
+
const url = input['url'];
|
|
543
|
+
const ref = input['ref'];
|
|
544
|
+
const scopeInput = input['scope'];
|
|
545
|
+
const scope = resolveInstallScope(scopeInput);
|
|
546
|
+
const root = requireScopeRoot(scope);
|
|
547
|
+
ensureScopeInitialized(scope, root);
|
|
548
|
+
const tempName = deriveNameFromUrl(url);
|
|
549
|
+
const mktsDir = join(root, 'marketplaces');
|
|
550
|
+
ensureDir(mktsDir);
|
|
551
|
+
const tempDest = join(mktsDir, tempName);
|
|
552
|
+
if (pathExists(tempDest)) {
|
|
553
|
+
removePath(tempDest);
|
|
554
|
+
}
|
|
555
|
+
clone(url, tempDest, { depth: 1, ref: ref !== undefined ? ref : undefined });
|
|
556
|
+
const manifest = readMarketplaceManifest(tempDest);
|
|
557
|
+
if (!manifest) {
|
|
558
|
+
removePath(tempDest);
|
|
559
|
+
throw notFound(`marketplace manifest not found at ${tempDest}/.crouter-marketplace/marketplace.json — not a valid marketplace`);
|
|
560
|
+
}
|
|
561
|
+
const finalName = manifest.name;
|
|
562
|
+
const finalDest = join(mktsDir, finalName);
|
|
563
|
+
if (finalName !== tempName) {
|
|
564
|
+
if (pathExists(finalDest)) {
|
|
565
|
+
removePath(finalDest);
|
|
566
|
+
}
|
|
567
|
+
renameSync(tempDest, finalDest);
|
|
568
|
+
}
|
|
569
|
+
const effectiveRef = ref !== undefined ? ref : 'main';
|
|
570
|
+
updateConfig(scope, (cfg) => {
|
|
571
|
+
cfg.marketplaces[finalName] = {
|
|
572
|
+
url,
|
|
573
|
+
ref: effectiveRef,
|
|
574
|
+
installed_at: nowIso(),
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
return { name: finalName, scope, path: finalDest };
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// market.manage.remove
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
const marketRemove = defineLeaf({
|
|
584
|
+
name: 'remove',
|
|
585
|
+
help: {
|
|
586
|
+
name: 'pkg market manage remove',
|
|
587
|
+
summary: 'remove a marketplace and its directory',
|
|
588
|
+
params: [
|
|
589
|
+
{ kind: 'positional', name: 'name', type: 'string', required: true, constraint: 'Marketplace name to remove.' },
|
|
590
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'One of: user, project.' },
|
|
591
|
+
],
|
|
592
|
+
output: [
|
|
593
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Removed marketplace name.' },
|
|
594
|
+
{ name: 'removed', type: 'boolean', required: true, constraint: 'Always true on success.' },
|
|
595
|
+
],
|
|
596
|
+
outputKind: 'object',
|
|
597
|
+
effects: ['Deletes the marketplace directory. Removes the entry from config.json.'],
|
|
598
|
+
},
|
|
599
|
+
run: async (input) => {
|
|
600
|
+
const name = input['name'];
|
|
601
|
+
const scopeInput = input['scope'];
|
|
602
|
+
let targetScope;
|
|
603
|
+
let mktRoot;
|
|
604
|
+
if (scopeInput !== undefined) {
|
|
605
|
+
const resolved = resolveScopeArg(scopeInput);
|
|
606
|
+
if (resolved !== 'all' && resolved !== 'builtin') {
|
|
607
|
+
const s = resolved;
|
|
608
|
+
const found = findMarketplaceByName(name, s);
|
|
609
|
+
if (!found)
|
|
610
|
+
throw notFound(`marketplace not found: ${name} (scope: ${s})`);
|
|
611
|
+
targetScope = s;
|
|
612
|
+
mktRoot = found.root;
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
const found = findMarketplaceByName(name);
|
|
616
|
+
if (!found)
|
|
617
|
+
throw notFound(`marketplace not found: ${name}`);
|
|
618
|
+
targetScope = found.scope;
|
|
619
|
+
mktRoot = found.root;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
const found = findMarketplaceByName(name);
|
|
624
|
+
if (!found)
|
|
625
|
+
throw notFound(`marketplace not found: ${name}`);
|
|
626
|
+
targetScope = found.scope;
|
|
627
|
+
mktRoot = found.root;
|
|
628
|
+
}
|
|
629
|
+
const scope = targetScope;
|
|
630
|
+
const scopeRootPath = requireScopeRoot(scope);
|
|
631
|
+
const plgDir = join(scopeRootPath, 'plugins');
|
|
632
|
+
const cfg = readConfig(scope);
|
|
633
|
+
const pluginsToRemove = [];
|
|
634
|
+
for (const [pluginName, entry] of Object.entries(cfg.plugins)) {
|
|
635
|
+
if (entry.source_marketplace === name) {
|
|
636
|
+
pluginsToRemove.push(pluginName);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (const pluginName of pluginsToRemove) {
|
|
640
|
+
const pluginPath = join(plgDir, pluginName);
|
|
641
|
+
removePath(pluginPath);
|
|
642
|
+
}
|
|
643
|
+
removePath(mktRoot);
|
|
644
|
+
updateConfig(scope, (c) => {
|
|
645
|
+
delete c.marketplaces[name];
|
|
646
|
+
for (const pluginName of pluginsToRemove) {
|
|
647
|
+
delete c.plugins[pluginName];
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return { name, removed: true };
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
// market.manage.update
|
|
655
|
+
// Single marketplace: blocking. All marketplaces: job handle.
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
const marketUpdate = defineLeaf({
|
|
658
|
+
name: 'update',
|
|
659
|
+
help: {
|
|
660
|
+
name: 'pkg market manage update',
|
|
661
|
+
summary: 'git pull updates for one or all registered marketplaces',
|
|
662
|
+
params: [
|
|
663
|
+
{ kind: 'flag', name: 'marketplace', type: 'string', required: false, constraint: 'Marketplace name to update. Omit to update all (returns a job handle).' },
|
|
664
|
+
],
|
|
665
|
+
output: [
|
|
666
|
+
{ name: 'updated', type: 'object[]', required: false, constraint: 'Present for single (blocking) path: [{name, updated, sha}].' },
|
|
667
|
+
{ name: 'job_id', type: 'string', required: false, constraint: 'Present for all (async) path.' },
|
|
668
|
+
{ name: 'follow_up', type: 'string', required: false, constraint: 'Instruction for retrieving async result.' },
|
|
669
|
+
],
|
|
670
|
+
outputKind: 'object',
|
|
671
|
+
effects: ['Runs git pull in marketplace directories.'],
|
|
672
|
+
},
|
|
673
|
+
run: async (input) => {
|
|
674
|
+
const name = input['marketplace'];
|
|
675
|
+
async function doUpdate(targets) {
|
|
676
|
+
const results = [];
|
|
677
|
+
for (const target of targets) {
|
|
678
|
+
const shaBefore = currentSha(target.root);
|
|
679
|
+
const res = pull(target.root);
|
|
680
|
+
if (res.status !== 0) {
|
|
681
|
+
results.push({ name: target.name, updated: false, sha: shaBefore !== null ? shaBefore : '' });
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const shaAfter = currentSha(target.root);
|
|
685
|
+
const updated = shaBefore !== shaAfter;
|
|
686
|
+
const freshManifest = readMarketplaceManifest(target.root);
|
|
687
|
+
updateState(target.scope, (s) => {
|
|
688
|
+
if (s.marketplaces[target.name] === undefined) {
|
|
689
|
+
s.marketplaces[target.name] = {};
|
|
690
|
+
}
|
|
691
|
+
s.marketplaces[target.name].last_updated = nowIso();
|
|
692
|
+
});
|
|
693
|
+
const cfg = readConfig(target.scope);
|
|
694
|
+
for (const [pluginName, entry] of Object.entries(cfg.plugins)) {
|
|
695
|
+
if (entry.source_marketplace !== target.name)
|
|
696
|
+
continue;
|
|
697
|
+
const scopeRootPath = requireScopeRoot(target.scope);
|
|
698
|
+
const pluginPath = join(scopeRootPath, 'plugins', pluginName);
|
|
699
|
+
if (isSymlink(pluginPath)) {
|
|
700
|
+
// content updated by marketplace pull above
|
|
701
|
+
}
|
|
702
|
+
else if (pathExists(pluginPath)) {
|
|
703
|
+
pull(pluginPath);
|
|
704
|
+
}
|
|
705
|
+
if (freshManifest !== null) {
|
|
706
|
+
const pluginEntry = freshManifest.plugins.find((p) => p.name === pluginName);
|
|
707
|
+
if (pluginEntry !== undefined && pluginEntry.version !== undefined) {
|
|
708
|
+
updateConfig(target.scope, (c) => {
|
|
709
|
+
if (c.plugins[pluginName] !== undefined) {
|
|
710
|
+
c.plugins[pluginName].version = pluginEntry.version;
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
updateState(target.scope, (s) => {
|
|
716
|
+
if (s.plugins[pluginName] === undefined) {
|
|
717
|
+
s.plugins[pluginName] = {};
|
|
718
|
+
}
|
|
719
|
+
s.plugins[pluginName].last_updated = nowIso();
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
results.push({ name: target.name, updated, sha: shaAfter !== null ? shaAfter : '' });
|
|
723
|
+
}
|
|
724
|
+
return results;
|
|
725
|
+
}
|
|
726
|
+
if (name !== undefined) {
|
|
727
|
+
// Single marketplace — blocking
|
|
728
|
+
const found = findMarketplaceByName(name);
|
|
729
|
+
if (!found)
|
|
730
|
+
throw notFound(`marketplace not found: ${name}`);
|
|
731
|
+
const results = await doUpdate([{ name: found.name, scope: found.scope, root: found.root }]);
|
|
732
|
+
return { updated: results };
|
|
733
|
+
}
|
|
734
|
+
// All marketplaces — async job
|
|
735
|
+
const all = listAllMarketplaces();
|
|
736
|
+
const targets = all.map((m) => ({ name: m.name, scope: m.scope, root: m.root }));
|
|
737
|
+
const { jobId } = createJob('pkg-market-update', { cwd: process.cwd(), pid: process.pid });
|
|
738
|
+
setImmediate(() => {
|
|
739
|
+
void (async () => {
|
|
740
|
+
appendEvent(jobId, { level: 'info', event: 'start', message: `updating ${targets.length} marketplace(s)` });
|
|
741
|
+
try {
|
|
742
|
+
const results = await doUpdate(targets);
|
|
743
|
+
writeResult(jobId, { updated: results }, 'done');
|
|
744
|
+
}
|
|
745
|
+
catch (e) {
|
|
746
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
747
|
+
appendEvent(jobId, { level: 'error', event: 'failed', message: msg });
|
|
748
|
+
writeResult(jobId, { error: msg }, 'failed');
|
|
749
|
+
}
|
|
750
|
+
})();
|
|
751
|
+
});
|
|
752
|
+
return {
|
|
753
|
+
job_id: jobId,
|
|
754
|
+
follow_up: `crtr job read result ${jobId} --wait`,
|
|
755
|
+
};
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
// market.manage.install
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
const marketInstall = defineLeaf({
|
|
762
|
+
name: 'install',
|
|
763
|
+
help: {
|
|
764
|
+
name: 'pkg market manage install',
|
|
765
|
+
summary: 'install a plugin from an added marketplace by plugin name',
|
|
766
|
+
params: [
|
|
767
|
+
{ kind: 'flag', name: 'marketplace', type: 'string', required: true, constraint: 'Marketplace name (must already be added via `pkg market manage add`).' },
|
|
768
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: true, constraint: 'Plugin name as listed in the marketplace manifest.' },
|
|
769
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'One of: user, project. Default: project if available, else user.' },
|
|
770
|
+
],
|
|
771
|
+
output: [
|
|
772
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Installed plugin name.' },
|
|
773
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the plugin was installed into.' },
|
|
774
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the installed plugin directory.' },
|
|
775
|
+
],
|
|
776
|
+
outputKind: 'object',
|
|
777
|
+
effects: ['Clones or copies the plugin from the marketplace into the scope plugins directory. Registers the plugin in config.json with source_marketplace set.'],
|
|
778
|
+
},
|
|
779
|
+
run: async (input) => {
|
|
780
|
+
const mktName = input['marketplace'];
|
|
781
|
+
const pluginName = input['plugin'];
|
|
782
|
+
const scopeInput = input['scope'];
|
|
783
|
+
const mkt = findMarketplaceByName(mktName);
|
|
784
|
+
if (!mkt)
|
|
785
|
+
throw notFound(`marketplace not found: ${mktName}`);
|
|
786
|
+
const entry = mkt.manifest.plugins.find((p) => p.name === pluginName);
|
|
787
|
+
if (!entry)
|
|
788
|
+
throw notFound(`plugin "${pluginName}" not found in marketplace "${mktName}"`);
|
|
789
|
+
const destScope = resolveInstallScope(scopeInput);
|
|
790
|
+
const destRoot = requireScopeRoot(destScope);
|
|
791
|
+
ensureScopeInitialized(destScope, destRoot);
|
|
792
|
+
const destPluginDir = join(destRoot, 'plugins', pluginName);
|
|
793
|
+
const source = entry.source;
|
|
794
|
+
const isRelativePath = source.startsWith('./') ||
|
|
795
|
+
source.startsWith('../') ||
|
|
796
|
+
(!source.includes('://') && !isAbsolute(source));
|
|
797
|
+
if (isRelativePath) {
|
|
798
|
+
const sourcePath = join(mkt.root, source);
|
|
799
|
+
if (!pathExists(sourcePath)) {
|
|
800
|
+
throw notFound(`plugin source path does not exist: ${sourcePath}`);
|
|
801
|
+
}
|
|
802
|
+
linkOrCopy(sourcePath, destPluginDir);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
if (pathExists(destPluginDir)) {
|
|
806
|
+
removePath(destPluginDir);
|
|
807
|
+
}
|
|
808
|
+
clone(source, destPluginDir, { depth: 1 });
|
|
809
|
+
}
|
|
810
|
+
const pluginManifest = readPluginManifest(destPluginDir);
|
|
811
|
+
if (!pluginManifest) {
|
|
812
|
+
removePath(destPluginDir);
|
|
813
|
+
throw notFound(`plugin manifest not found at ${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
814
|
+
}
|
|
815
|
+
const version = entry.version !== undefined ? entry.version : pluginManifest.version;
|
|
816
|
+
updateConfig(destScope, (cfg) => {
|
|
817
|
+
const pluginCfg = {
|
|
818
|
+
enabled: true,
|
|
819
|
+
source_marketplace: mktName,
|
|
820
|
+
};
|
|
821
|
+
if (version !== undefined) {
|
|
822
|
+
pluginCfg.version = version;
|
|
823
|
+
}
|
|
824
|
+
cfg.plugins[pluginName] = pluginCfg;
|
|
825
|
+
});
|
|
826
|
+
return { name: pluginName, scope: destScope, path: destPluginDir };
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
const marketManageBranch = defineBranch({
|
|
830
|
+
name: 'manage',
|
|
831
|
+
help: {
|
|
832
|
+
name: 'pkg market manage',
|
|
833
|
+
summary: 'add, remove, update, or install from marketplaces',
|
|
834
|
+
children: [
|
|
835
|
+
{ name: 'add', desc: 'add a marketplace by git URL', useWhen: 'registering a new marketplace source' },
|
|
836
|
+
{ name: 'remove', desc: 'remove a marketplace', useWhen: 'unregistering a marketplace' },
|
|
837
|
+
{ name: 'update', desc: 'pull latest marketplace index', useWhen: 'refreshing the marketplace plugin list' },
|
|
838
|
+
{ name: 'install', desc: 'install a plugin from a marketplace', useWhen: 'adding a plugin sourced from a registered marketplace' },
|
|
839
|
+
],
|
|
840
|
+
},
|
|
841
|
+
children: [marketAdd, marketRemove, marketUpdate, marketInstall],
|
|
842
|
+
});
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// market.inspect.list
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
const marketList = defineLeaf({
|
|
847
|
+
name: 'list',
|
|
848
|
+
help: {
|
|
849
|
+
name: 'pkg market inspect list',
|
|
850
|
+
summary: 'list registered marketplaces',
|
|
851
|
+
params: [
|
|
852
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'One of: user, project, all. Default: all.' },
|
|
853
|
+
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 50, constraint: 'Default 50, max 200.' },
|
|
854
|
+
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
855
|
+
],
|
|
856
|
+
output: [
|
|
857
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {name, scope, url, ref, path, last_updated?}. Sorted by scope then name ascending.' },
|
|
858
|
+
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'null means no more items.' },
|
|
859
|
+
{ name: 'total', type: 'integer | null', required: true, constraint: 'Total count.' },
|
|
860
|
+
],
|
|
861
|
+
outputKind: 'object',
|
|
862
|
+
effects: ['None. Read-only.'],
|
|
863
|
+
},
|
|
864
|
+
run: async (input) => {
|
|
865
|
+
const scopeInput = input['scope'];
|
|
866
|
+
const limitRaw = input['limit'];
|
|
867
|
+
const limit = limitRaw !== undefined ? Math.min(Math.max(1, limitRaw), 200) : 50;
|
|
868
|
+
const cursor = input['cursor'];
|
|
869
|
+
let all;
|
|
870
|
+
if (scopeInput !== undefined) {
|
|
871
|
+
const resolved = resolveScopeArg(scopeInput);
|
|
872
|
+
if (resolved === 'all') {
|
|
873
|
+
all = listAllMarketplaces();
|
|
874
|
+
}
|
|
875
|
+
else if (resolved === 'builtin') {
|
|
876
|
+
all = [];
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
all = listInstalledMarketplaces(resolved);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
all = listAllMarketplaces();
|
|
884
|
+
}
|
|
885
|
+
const sorted = [...all].sort((a, b) => {
|
|
886
|
+
if (a.scope === 'project' && b.scope !== 'project')
|
|
887
|
+
return -1;
|
|
888
|
+
if (a.scope !== 'project' && b.scope === 'project')
|
|
889
|
+
return 1;
|
|
890
|
+
return a.name.localeCompare(b.name);
|
|
891
|
+
});
|
|
892
|
+
const result = paginate(sorted, { limit, cursor: cursor !== undefined ? cursor : undefined }, {
|
|
893
|
+
defaultLimit: 50,
|
|
894
|
+
maxLimit: 200,
|
|
895
|
+
keyOf: (m) => `${m.scope}:${m.name}`,
|
|
896
|
+
total: 'count',
|
|
897
|
+
});
|
|
898
|
+
return {
|
|
899
|
+
items: result.items.map((m) => ({
|
|
900
|
+
name: m.name,
|
|
901
|
+
scope: m.scope,
|
|
902
|
+
url: m.url,
|
|
903
|
+
ref: m.ref,
|
|
904
|
+
path: m.root,
|
|
905
|
+
})),
|
|
906
|
+
next_cursor: result.next_cursor,
|
|
907
|
+
total: result.total,
|
|
908
|
+
};
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
// ---------------------------------------------------------------------------
|
|
912
|
+
// market.inspect.browse
|
|
913
|
+
// ---------------------------------------------------------------------------
|
|
914
|
+
const marketBrowse = defineLeaf({
|
|
915
|
+
name: 'browse',
|
|
916
|
+
help: {
|
|
917
|
+
name: 'pkg market inspect browse',
|
|
918
|
+
summary: 'list plugins available in a marketplace',
|
|
919
|
+
params: [
|
|
920
|
+
{ kind: 'flag', name: 'marketplace', type: 'string', required: false, constraint: 'Marketplace name. Omit to browse all registered marketplaces.' },
|
|
921
|
+
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 50, constraint: 'Default 50, max 200.' },
|
|
922
|
+
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
923
|
+
],
|
|
924
|
+
output: [
|
|
925
|
+
{ name: 'marketplace', type: 'string', required: true, constraint: 'Echo of the input marketplace name.' },
|
|
926
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {name, source, version?, description?, keywords?, installed, installed_scope?}. Sorted by name ascending.' },
|
|
927
|
+
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'null means no more items.' },
|
|
928
|
+
{ name: 'total', type: 'integer | null', required: true, constraint: 'Total plugins in the marketplace; null if unavailable.' },
|
|
929
|
+
],
|
|
930
|
+
outputKind: 'object',
|
|
931
|
+
effects: ['None. Read-only.'],
|
|
932
|
+
},
|
|
933
|
+
run: async (input) => {
|
|
934
|
+
const mktName = input['marketplace'];
|
|
935
|
+
const limitRaw = input['limit'];
|
|
936
|
+
const limit = limitRaw !== undefined ? Math.min(Math.max(1, limitRaw), 200) : 50;
|
|
937
|
+
const cursor = input['cursor'];
|
|
938
|
+
if (mktName === undefined) {
|
|
939
|
+
throw usage('--marketplace is required for browse. Use `pkg market inspect list` to see registered marketplaces.');
|
|
940
|
+
}
|
|
941
|
+
const mkt = findMarketplaceByName(mktName);
|
|
942
|
+
if (!mkt)
|
|
943
|
+
throw notFound(`marketplace not found: ${mktName}`);
|
|
944
|
+
const allScopes = ['project', 'user'].filter((s) => s !== 'project' || projectScopeRoot() !== null);
|
|
945
|
+
const installedMap = new Map();
|
|
946
|
+
for (const scope of allScopes) {
|
|
947
|
+
for (const p of listInstalledPlugins(scope)) {
|
|
948
|
+
if (!installedMap.has(p.name)) {
|
|
949
|
+
installedMap.set(p.name, p.scope);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const pluginsSorted = [...mkt.manifest.plugins].sort((a, b) => a.name.localeCompare(b.name));
|
|
954
|
+
const result = paginate(pluginsSorted, { limit, cursor: cursor !== undefined ? cursor : undefined }, {
|
|
955
|
+
defaultLimit: 50,
|
|
956
|
+
maxLimit: 200,
|
|
957
|
+
keyOf: (p) => p.name,
|
|
958
|
+
total: 'count',
|
|
959
|
+
});
|
|
960
|
+
return {
|
|
961
|
+
marketplace: mkt.name,
|
|
962
|
+
items: result.items.map((entry) => {
|
|
963
|
+
const installedScope = installedMap.get(entry.name);
|
|
964
|
+
const installed = installedScope !== undefined;
|
|
965
|
+
const item = {
|
|
966
|
+
name: entry.name,
|
|
967
|
+
source: entry.source,
|
|
968
|
+
version: entry.version,
|
|
969
|
+
description: entry.description,
|
|
970
|
+
keywords: entry.keywords,
|
|
971
|
+
installed,
|
|
972
|
+
};
|
|
973
|
+
if (installed) {
|
|
974
|
+
item.installed_scope = installedScope;
|
|
975
|
+
}
|
|
976
|
+
return item;
|
|
977
|
+
}),
|
|
978
|
+
next_cursor: result.next_cursor,
|
|
979
|
+
total: result.total,
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
const marketInspectBranch = defineBranch({
|
|
984
|
+
name: 'inspect',
|
|
985
|
+
help: {
|
|
986
|
+
name: 'pkg market inspect',
|
|
987
|
+
summary: 'read marketplace metadata without modifying state',
|
|
988
|
+
children: [
|
|
989
|
+
{ name: 'list', desc: 'list registered marketplaces', useWhen: 'seeing which marketplaces are configured' },
|
|
990
|
+
{ name: 'browse', desc: 'list plugins available in a marketplace', useWhen: 'exploring what a marketplace offers before installing' },
|
|
991
|
+
],
|
|
992
|
+
},
|
|
993
|
+
children: [marketList, marketBrowse],
|
|
994
|
+
});
|
|
995
|
+
const marketBranch = defineBranch({
|
|
996
|
+
name: 'market',
|
|
997
|
+
help: {
|
|
998
|
+
name: 'pkg market',
|
|
999
|
+
summary: 'manage and browse plugin marketplaces',
|
|
1000
|
+
model: 'Marketplaces are git repos containing a .crouter-marketplace/marketplace.json index of plugins.',
|
|
1001
|
+
children: [
|
|
1002
|
+
{ name: 'manage', desc: 'add, remove, update, install', useWhen: 'changing marketplace or marketplace-sourced plugin state' },
|
|
1003
|
+
{ name: 'inspect', desc: 'list or browse marketplaces', useWhen: 'reading marketplace metadata' },
|
|
1004
|
+
],
|
|
1005
|
+
},
|
|
1006
|
+
children: [marketManageBranch, marketInspectBranch],
|
|
1007
|
+
});
|
|
1008
|
+
export function registerPkg() {
|
|
1009
|
+
return defineBranch({
|
|
1010
|
+
name: 'pkg',
|
|
1011
|
+
help: {
|
|
1012
|
+
name: 'pkg',
|
|
1013
|
+
summary: 'manage plugins and plugin marketplaces',
|
|
1014
|
+
children: [
|
|
1015
|
+
{ name: 'plugin', desc: 'install and manage plugins', useWhen: 'working with individual plugins directly' },
|
|
1016
|
+
{ name: 'market', desc: 'manage marketplace sources and install from them', useWhen: 'using curated plugin collections' },
|
|
1017
|
+
],
|
|
1018
|
+
},
|
|
1019
|
+
children: [pluginBranch, marketBranch],
|
|
1020
|
+
});
|
|
1021
|
+
}
|