rush-ai 0.19.0 → 0.21.0
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/README.md +18 -9
- package/dist/{_codex-content-loader-Q7KBWZSB.js → _codex-content-loader-WAPA56U3.js} +2 -2
- package/dist/{chunk-X45FKY3L.js → chunk-22YQT6XF.js} +18 -7
- package/dist/chunk-22YQT6XF.js.map +1 -0
- package/dist/chunk-C7KJUHEF.js +830 -0
- package/dist/chunk-C7KJUHEF.js.map +1 -0
- package/dist/{chunk-RLKEUPBP.js → chunk-GPOA7SAG.js} +18 -3
- package/dist/chunk-GPOA7SAG.js.map +1 -0
- package/dist/index.js +1521 -346
- package/dist/index.js.map +1 -1
- package/dist/{mcp-UZYID3GG.js → mcp-TROKQRBF.js} +2 -2
- package/dist/{mirror-IHLOSPAS.js → mirror-SQ6GNVP5.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-2AICQRQP.js +0 -447
- package/dist/chunk-2AICQRQP.js.map +0 -1
- package/dist/chunk-RLKEUPBP.js.map +0 -1
- package/dist/chunk-X45FKY3L.js.map +0 -1
- /package/dist/{_codex-content-loader-Q7KBWZSB.js.map → _codex-content-loader-WAPA56U3.js.map} +0 -0
- /package/dist/{mcp-UZYID3GG.js.map → mcp-TROKQRBF.js.map} +0 -0
- /package/dist/{mirror-IHLOSPAS.js.map → mirror-SQ6GNVP5.js.map} +0 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getAuthToken
|
|
4
|
+
} from "./chunk-OIKYNVKO.js";
|
|
5
|
+
import {
|
|
6
|
+
output
|
|
7
|
+
} from "./chunk-T5S6NCHZ.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/marketplace/_codex-content-loader.ts
|
|
10
|
+
import { constants as fsConstants } from "fs";
|
|
11
|
+
import { access, readFile, stat as stat2 } from "fs/promises";
|
|
12
|
+
import { resolve as pathResolve2, sep } from "path";
|
|
13
|
+
|
|
14
|
+
// src/marketplaces/rush.ts
|
|
15
|
+
import { execFileSync, spawn } from "child_process";
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
17
|
+
import { mkdir, rename, rm, stat, writeFile } from "fs/promises";
|
|
18
|
+
import { resolve as pathResolve, resolve } from "path";
|
|
19
|
+
var RUSH_API_FETCH_TIMEOUT_MS = 3e4;
|
|
20
|
+
function fetchInitWithTimeout(headers) {
|
|
21
|
+
return {
|
|
22
|
+
headers,
|
|
23
|
+
signal: AbortSignal.timeout(RUSH_API_FETCH_TIMEOUT_MS)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function fetchRushMarketplace(source, opts = {}) {
|
|
27
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
28
|
+
const url = `${inferProtocol(source.host)}${source.host}/api/marketplace`;
|
|
29
|
+
const headers = {
|
|
30
|
+
Accept: "application/json"
|
|
31
|
+
};
|
|
32
|
+
if (opts.token) {
|
|
33
|
+
headers.Authorization = `Bearer ${opts.token}`;
|
|
34
|
+
}
|
|
35
|
+
const response = await fetchFn(url, fetchInitWithTimeout(headers));
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Failed to fetch rush marketplace from ${url}: ${response.status} ${response.statusText}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
if (!data.plugins || !Array.isArray(data.plugins)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Invalid marketplace response from ${url}: missing 'plugins' array`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
async function fetchRushPlugin(source, pluginSlug, opts = {}) {
|
|
50
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
51
|
+
const url = `${inferProtocol(source.host)}${source.host}/api/marketplace/${encodeURIComponent(pluginSlug)}`;
|
|
52
|
+
const headers = {
|
|
53
|
+
Accept: "application/json"
|
|
54
|
+
};
|
|
55
|
+
if (opts.token) {
|
|
56
|
+
headers.Authorization = `Bearer ${opts.token}`;
|
|
57
|
+
}
|
|
58
|
+
const response = await fetchFn(url, fetchInitWithTimeout(headers));
|
|
59
|
+
if (response.status === 401) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Plugin '${pluginSlug}' requires authentication. Run 'rush-ai login' first.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (response.status === 404) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Plugin '${pluginSlug}' not found in rush marketplace at ${source.host}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Failed to fetch plugin '${pluginSlug}' from ${url}: ${response.status} ${response.statusText}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return await response.json();
|
|
75
|
+
}
|
|
76
|
+
async function fetchRushOfficialFile(source, pluginSlug, filePath, opts = {}) {
|
|
77
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
78
|
+
const encoded = filePath.split("/").filter((s) => s.length > 0).map((s) => encodeURIComponent(s)).join("/");
|
|
79
|
+
const url = `${inferProtocol(source.host)}${source.host}/api/marketplace/${encodeURIComponent(pluginSlug)}/files/${encoded}`;
|
|
80
|
+
const headers = { Accept: "application/json" };
|
|
81
|
+
if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
|
|
82
|
+
const response = await fetchFn(url, fetchInitWithTimeout(headers));
|
|
83
|
+
if (response.status === 404) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`File '${filePath}' not found in official plugin '${pluginSlug}'`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Failed to fetch file '${filePath}' from ${url}: ${response.status} ${response.statusText}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return await response.json();
|
|
94
|
+
}
|
|
95
|
+
async function materializeRushPlugin(source, pluginSlug, targetDir, opts = {}) {
|
|
96
|
+
validateSlug(pluginSlug, "plugin slug");
|
|
97
|
+
const manifest = await fetchRushPlugin(source, pluginSlug, opts);
|
|
98
|
+
if (manifest.source === "claude-plugins-official") {
|
|
99
|
+
if (manifest.redirectTo) {
|
|
100
|
+
await materializeOfficialExternal(manifest, targetDir);
|
|
101
|
+
return manifest;
|
|
102
|
+
}
|
|
103
|
+
await materializeOfficialLocal(
|
|
104
|
+
source,
|
|
105
|
+
pluginSlug,
|
|
106
|
+
manifest,
|
|
107
|
+
targetDir,
|
|
108
|
+
opts
|
|
109
|
+
);
|
|
110
|
+
return manifest;
|
|
111
|
+
}
|
|
112
|
+
if (manifest.source === "rush-git-import") {
|
|
113
|
+
await materializeOfficialLocal(
|
|
114
|
+
source,
|
|
115
|
+
pluginSlug,
|
|
116
|
+
manifest,
|
|
117
|
+
targetDir,
|
|
118
|
+
opts
|
|
119
|
+
);
|
|
120
|
+
return manifest;
|
|
121
|
+
}
|
|
122
|
+
return materializeRushUserPlugin(
|
|
123
|
+
source,
|
|
124
|
+
pluginSlug,
|
|
125
|
+
manifest,
|
|
126
|
+
targetDir,
|
|
127
|
+
opts
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
async function materializeRushUserPlugin(source, pluginSlug, manifest, targetDir, opts) {
|
|
131
|
+
let mcpServers = {};
|
|
132
|
+
if (manifest.mcpServers && Object.keys(manifest.mcpServers).length > 0) {
|
|
133
|
+
mcpServers = { ...manifest.mcpServers };
|
|
134
|
+
if (opts.secrets && Object.keys(opts.secrets).length > 0) {
|
|
135
|
+
mcpServers = substituteMcpSecrets(mcpServers, opts.secrets);
|
|
136
|
+
}
|
|
137
|
+
mcpServers = normalizeMcpServerDefaults(mcpServers);
|
|
138
|
+
}
|
|
139
|
+
const pluginJson = buildPluginJsonShape(manifest, {
|
|
140
|
+
mcpServers
|
|
141
|
+
});
|
|
142
|
+
const tmpDir = `${targetDir}.${randomUUID()}.tmp`;
|
|
143
|
+
try {
|
|
144
|
+
const pluginJsonDir = resolve(tmpDir, ".claude-plugin");
|
|
145
|
+
await mkdir(pluginJsonDir, { recursive: true });
|
|
146
|
+
await writeFile(
|
|
147
|
+
resolve(pluginJsonDir, "plugin.json"),
|
|
148
|
+
`${JSON.stringify(pluginJson, null, 2)}
|
|
149
|
+
`,
|
|
150
|
+
"utf8"
|
|
151
|
+
);
|
|
152
|
+
if (manifest.skills && manifest.skills.length > 0) {
|
|
153
|
+
await writeFile(
|
|
154
|
+
resolve(tmpDir, "skills.json"),
|
|
155
|
+
'{"skills":{}}\n',
|
|
156
|
+
"utf8"
|
|
157
|
+
);
|
|
158
|
+
const reskillHomeDir = resolve(tmpDir, ".reskill-home");
|
|
159
|
+
await mkdir(reskillHomeDir, { recursive: true });
|
|
160
|
+
const registryUrl = `${inferProtocol(source.host)}${source.host}`;
|
|
161
|
+
for (const [index, skill] of manifest.skills.entries()) {
|
|
162
|
+
try {
|
|
163
|
+
output.dim(
|
|
164
|
+
` Fetching skill ${index + 1}/${manifest.skills.length}: ${skill.name}`
|
|
165
|
+
);
|
|
166
|
+
const args = [
|
|
167
|
+
"-y",
|
|
168
|
+
"reskill@latest",
|
|
169
|
+
"install",
|
|
170
|
+
skill.name,
|
|
171
|
+
"--agent",
|
|
172
|
+
"claude-code",
|
|
173
|
+
"--mode",
|
|
174
|
+
"copy",
|
|
175
|
+
"--no-save",
|
|
176
|
+
"--skip-manifest",
|
|
177
|
+
"-y",
|
|
178
|
+
"-f",
|
|
179
|
+
"-r",
|
|
180
|
+
registryUrl,
|
|
181
|
+
...opts.token ? ["-t", opts.token] : []
|
|
182
|
+
];
|
|
183
|
+
execFileSync("npx", args, {
|
|
184
|
+
cwd: tmpDir,
|
|
185
|
+
// 3 分钟 — 覆盖 npx 首次下载 reskill 包的冷启动场景
|
|
186
|
+
// (warm cache 时 reskill 实际运行只需 ~7s)
|
|
187
|
+
timeout: 18e4,
|
|
188
|
+
stdio: "pipe",
|
|
189
|
+
env: {
|
|
190
|
+
...process.env,
|
|
191
|
+
HOME: reskillHomeDir,
|
|
192
|
+
npm_config_cache: resolve(reskillHomeDir, ".npm")
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const stderr = err && typeof err === "object" && "stderr" in err ? err.stderr?.toString() ?? "" : "";
|
|
197
|
+
if (stderr.includes("401") || stderr.includes("403") || stderr.includes("Unauthorized") || stderr.includes("Forbidden")) {
|
|
198
|
+
throw new SkillAuthError(skill.name, 401);
|
|
199
|
+
}
|
|
200
|
+
output.warn(
|
|
201
|
+
` Warning: failed to install skill '${skill.name}': ${err instanceof Error ? err.message : String(err)}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const dotSkillsDir = resolve(tmpDir, ".skills");
|
|
207
|
+
const claudeSkillsDir = resolve(tmpDir, ".claude", "skills");
|
|
208
|
+
const skillsDir = resolve(tmpDir, "skills");
|
|
209
|
+
if (await stat(dotSkillsDir).then((s) => s.isDirectory()).catch(() => false)) {
|
|
210
|
+
if (!await stat(skillsDir).then((s) => s.isDirectory()).catch(() => false)) {
|
|
211
|
+
await rename(dotSkillsDir, skillsDir);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (await stat(claudeSkillsDir).then((s) => s.isDirectory()).catch(() => false)) {
|
|
215
|
+
if (!await stat(skillsDir).then((s) => s.isDirectory()).catch(() => false)) {
|
|
216
|
+
await rename(claudeSkillsDir, skillsDir);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
await rm(resolve(tmpDir, "skills.json"), { force: true });
|
|
220
|
+
await rm(resolve(tmpDir, ".reskill-home"), {
|
|
221
|
+
recursive: true,
|
|
222
|
+
force: true
|
|
223
|
+
});
|
|
224
|
+
await rm(resolve(tmpDir, ".cursor"), { recursive: true, force: true });
|
|
225
|
+
await rm(resolve(tmpDir, ".claude"), { recursive: true, force: true });
|
|
226
|
+
await rm(resolve(tmpDir, ".codex"), { recursive: true, force: true });
|
|
227
|
+
await rm(resolve(tmpDir, ".github"), { recursive: true, force: true });
|
|
228
|
+
await rm(resolve(tmpDir, ".opencode"), { recursive: true, force: true });
|
|
229
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
230
|
+
await mkdir(resolve(targetDir, ".."), { recursive: true });
|
|
231
|
+
await rename(tmpDir, targetDir);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
234
|
+
});
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
return manifest;
|
|
238
|
+
}
|
|
239
|
+
async function materializeOfficialLocal(source, pluginSlug, manifest, targetDir, opts) {
|
|
240
|
+
let mcpServers = {};
|
|
241
|
+
if (manifest.mcpServers && Object.keys(manifest.mcpServers).length > 0) {
|
|
242
|
+
mcpServers = { ...manifest.mcpServers };
|
|
243
|
+
if (opts.secrets && Object.keys(opts.secrets).length > 0) {
|
|
244
|
+
mcpServers = substituteMcpSecrets(mcpServers, opts.secrets);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const pluginJson = buildPluginJsonShape(manifest, {
|
|
248
|
+
mcpServers
|
|
249
|
+
});
|
|
250
|
+
const tmpDir = `${targetDir}.${randomUUID()}.tmp`;
|
|
251
|
+
try {
|
|
252
|
+
await mkdir(resolve(tmpDir, ".claude-plugin"), { recursive: true });
|
|
253
|
+
await writeFile(
|
|
254
|
+
resolve(tmpDir, ".claude-plugin", "plugin.json"),
|
|
255
|
+
`${JSON.stringify(pluginJson, null, 2)}
|
|
256
|
+
`,
|
|
257
|
+
"utf8"
|
|
258
|
+
);
|
|
259
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
260
|
+
await writeFile(
|
|
261
|
+
resolve(tmpDir, ".mcp.json"),
|
|
262
|
+
`${JSON.stringify({ mcpServers }, null, 2)}
|
|
263
|
+
`,
|
|
264
|
+
"utf8"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
await fetchAndWriteOfficialFiles(
|
|
268
|
+
source,
|
|
269
|
+
pluginSlug,
|
|
270
|
+
manifest,
|
|
271
|
+
tmpDir,
|
|
272
|
+
opts
|
|
273
|
+
);
|
|
274
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
275
|
+
await mkdir(resolve(targetDir, ".."), { recursive: true });
|
|
276
|
+
await rename(tmpDir, targetDir);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
279
|
+
});
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function fetchAndWriteOfficialFiles(source, pluginSlug, manifest, rootDir, opts) {
|
|
284
|
+
const entries = [];
|
|
285
|
+
for (const s of manifest.skills ?? []) {
|
|
286
|
+
if (s.path) {
|
|
287
|
+
const skillFilePath = s.path.endsWith(".md") || s.path.endsWith(".MD") ? s.path : `${s.path.endsWith("/") ? s.path.slice(0, -1) : s.path}/SKILL.md`;
|
|
288
|
+
entries.push({ kind: "skill", name: s.name, path: skillFilePath });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for (const a of manifest.agents ?? []) {
|
|
292
|
+
entries.push({ kind: "agent", name: a.name, path: a.path });
|
|
293
|
+
}
|
|
294
|
+
for (const h of manifest.hooks ?? []) {
|
|
295
|
+
entries.push({ kind: "hook", name: h.name, path: h.path });
|
|
296
|
+
}
|
|
297
|
+
for (const c of manifest.commands ?? []) {
|
|
298
|
+
entries.push({ kind: "command", name: c.name, path: c.path });
|
|
299
|
+
}
|
|
300
|
+
if (entries.length === 0) return;
|
|
301
|
+
const fetchOpts = {};
|
|
302
|
+
if (opts.fetchFn !== void 0) fetchOpts.fetchFn = opts.fetchFn;
|
|
303
|
+
if (opts.token !== void 0 && opts.token !== null) {
|
|
304
|
+
fetchOpts.token = opts.token;
|
|
305
|
+
}
|
|
306
|
+
for (const e of entries) {
|
|
307
|
+
try {
|
|
308
|
+
const file = await fetchRushOfficialFile(
|
|
309
|
+
source,
|
|
310
|
+
pluginSlug,
|
|
311
|
+
e.path,
|
|
312
|
+
fetchOpts
|
|
313
|
+
);
|
|
314
|
+
if (file.truncated) {
|
|
315
|
+
output.warn(
|
|
316
|
+
` Warning: ${e.kind} '${e.name}' file '${e.path}' was truncated during sync; please view on web`
|
|
317
|
+
);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
let body = null;
|
|
321
|
+
if (typeof file.content === "string") {
|
|
322
|
+
body = file.content;
|
|
323
|
+
} else if (typeof file.signedUrl === "string") {
|
|
324
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
325
|
+
const res = await fetchFn(file.signedUrl);
|
|
326
|
+
if (res.ok) {
|
|
327
|
+
const arr = await res.arrayBuffer();
|
|
328
|
+
body = Buffer.from(arr);
|
|
329
|
+
} else {
|
|
330
|
+
output.warn(
|
|
331
|
+
` Warning: failed to download signed URL for ${e.kind} '${e.name}': ${res.status}`
|
|
332
|
+
);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
output.warn(
|
|
337
|
+
` Warning: ${e.kind} '${e.name}' has no content or signedUrl`
|
|
338
|
+
);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const dest = resolve(rootDir, e.path);
|
|
342
|
+
const root = pathResolve(rootDir);
|
|
343
|
+
const rootWithSep = root.endsWith("/") ? root : `${root}/`;
|
|
344
|
+
if (dest !== root && !dest.startsWith(rootWithSep)) {
|
|
345
|
+
output.warn(
|
|
346
|
+
` Warning: refusing to write ${e.kind} '${e.name}' outside plugin dir: ${e.path}`
|
|
347
|
+
);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
await mkdir(resolve(dest, ".."), { recursive: true });
|
|
351
|
+
await writeFile(dest, body);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
output.warn(
|
|
354
|
+
` Warning: failed to fetch ${e.kind} '${e.name}' (${e.path}): ${err instanceof Error ? err.message : String(err)}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function materializeOfficialExternal(manifest, targetDir) {
|
|
360
|
+
const r = manifest.redirectTo;
|
|
361
|
+
if (!r || r.kind !== "github") {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Plugin '${manifest.name}' has unsupported redirectTo (kind=${r?.kind ?? "undefined"}). External plugins must redirect to GitHub.`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (!r.owner || !r.repo) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`Plugin '${manifest.name}' has invalid redirectTo: missing owner/repo`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const url = `https://github.com/${r.owner}/${r.repo}.git`;
|
|
372
|
+
const cloneTmp = `${targetDir}.${randomUUID()}.clone.tmp`;
|
|
373
|
+
const args = ["clone", "--depth", "1"];
|
|
374
|
+
if (r.ref) args.push("--branch", r.ref);
|
|
375
|
+
args.push("--", url, cloneTmp);
|
|
376
|
+
try {
|
|
377
|
+
await runGitClone(args);
|
|
378
|
+
let sourceDir;
|
|
379
|
+
if (r.path && r.path.length > 0) {
|
|
380
|
+
const candidate = resolve(cloneTmp, r.path);
|
|
381
|
+
const cloneTmpAbs = pathResolve(cloneTmp);
|
|
382
|
+
const cloneTmpWithSep = cloneTmpAbs.endsWith("/") ? cloneTmpAbs : `${cloneTmpAbs}/`;
|
|
383
|
+
if (candidate !== cloneTmpAbs && !candidate.startsWith(cloneTmpWithSep)) {
|
|
384
|
+
throw new Error(`redirectTo.path escapes repo root: '${r.path}'`);
|
|
385
|
+
}
|
|
386
|
+
const st = await stat(candidate).catch(() => null);
|
|
387
|
+
if (!st || !st.isDirectory()) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`redirectTo.path '${r.path}' is not a directory in ${url}`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
sourceDir = candidate;
|
|
393
|
+
} else {
|
|
394
|
+
sourceDir = cloneTmp;
|
|
395
|
+
}
|
|
396
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
397
|
+
await mkdir(resolve(targetDir, ".."), { recursive: true });
|
|
398
|
+
await rename(sourceDir, targetDir);
|
|
399
|
+
await enrichExternalPluginJson(targetDir, manifest);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
await rm(cloneTmp, { recursive: true, force: true }).catch(() => {
|
|
402
|
+
});
|
|
403
|
+
throw err;
|
|
404
|
+
} finally {
|
|
405
|
+
await rm(cloneTmp, { recursive: true, force: true }).catch(() => {
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function enrichExternalPluginJson(pluginDir, manifest) {
|
|
410
|
+
const pjPath = resolve(pluginDir, ".claude-plugin", "plugin.json");
|
|
411
|
+
let existing = {};
|
|
412
|
+
try {
|
|
413
|
+
const raw = await import("fs/promises").then(
|
|
414
|
+
(m) => m.readFile(pjPath, "utf8")
|
|
415
|
+
);
|
|
416
|
+
const parsed = JSON.parse(raw);
|
|
417
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
418
|
+
existing = parsed;
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
await mkdir(resolve(pluginDir, ".claude-plugin"), { recursive: true });
|
|
422
|
+
existing = {
|
|
423
|
+
name: manifest.name,
|
|
424
|
+
version: manifest.version || "1.0.0",
|
|
425
|
+
description: manifest.description || ""
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
let derivedHomepage = manifest.homepage;
|
|
429
|
+
if (!derivedHomepage && manifest.redirectTo?.kind === "github") {
|
|
430
|
+
const r = manifest.redirectTo;
|
|
431
|
+
derivedHomepage = `https://github.com/${r.owner}/${r.repo}`;
|
|
432
|
+
if (r.path) derivedHomepage += `/tree/${r.ref ?? "main"}/${r.path}`;
|
|
433
|
+
}
|
|
434
|
+
if (!existing.author && manifest.author) {
|
|
435
|
+
existing.author = { ...manifest.author };
|
|
436
|
+
}
|
|
437
|
+
if (!existing.homepage && derivedHomepage) {
|
|
438
|
+
existing.homepage = derivedHomepage;
|
|
439
|
+
}
|
|
440
|
+
const ifaceExisting = existing.interface ?? {};
|
|
441
|
+
const ifaceOut = { ...ifaceExisting };
|
|
442
|
+
if (!ifaceOut.developerName && manifest.author?.name) {
|
|
443
|
+
ifaceOut.developerName = manifest.author.name;
|
|
444
|
+
}
|
|
445
|
+
if (!ifaceOut.websiteURL && derivedHomepage) {
|
|
446
|
+
ifaceOut.websiteURL = derivedHomepage;
|
|
447
|
+
}
|
|
448
|
+
if (Object.keys(ifaceOut).length > 0) {
|
|
449
|
+
existing.interface = ifaceOut;
|
|
450
|
+
}
|
|
451
|
+
await writeFile(pjPath, `${JSON.stringify(existing, null, 2)}
|
|
452
|
+
`, "utf8");
|
|
453
|
+
}
|
|
454
|
+
async function runGitClone(args) {
|
|
455
|
+
await new Promise((res, rej) => {
|
|
456
|
+
const child = spawn("git", args, {
|
|
457
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
458
|
+
});
|
|
459
|
+
let stderr = "";
|
|
460
|
+
child.stderr?.on("data", (chunk) => {
|
|
461
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString();
|
|
462
|
+
});
|
|
463
|
+
child.on("error", (err) => rej(err));
|
|
464
|
+
child.on("close", (code) => {
|
|
465
|
+
if (code === 0) res();
|
|
466
|
+
else
|
|
467
|
+
rej(
|
|
468
|
+
new Error(`git ${args.join(" ")} exited ${code}: ${stderr.trim()}`)
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function buildPluginJsonShape(manifest, context) {
|
|
474
|
+
const out = {
|
|
475
|
+
name: manifest.name,
|
|
476
|
+
version: manifest.version || "1.0.0",
|
|
477
|
+
description: manifest.description
|
|
478
|
+
};
|
|
479
|
+
if (Object.keys(context.mcpServers).length > 0) {
|
|
480
|
+
out.mcpServers = context.mcpServers;
|
|
481
|
+
}
|
|
482
|
+
if (manifest.author && Object.keys(manifest.author).length > 0) {
|
|
483
|
+
out.author = { ...manifest.author };
|
|
484
|
+
}
|
|
485
|
+
let homepage = manifest.homepage;
|
|
486
|
+
if (!homepage && manifest.redirectTo?.kind === "github") {
|
|
487
|
+
const r = manifest.redirectTo;
|
|
488
|
+
homepage = `https://github.com/${r.owner}/${r.repo}`;
|
|
489
|
+
if (r.path) homepage += `/tree/${r.ref ?? "main"}/${r.path}`;
|
|
490
|
+
}
|
|
491
|
+
if (homepage) out.homepage = homepage;
|
|
492
|
+
const ifaceOut = {};
|
|
493
|
+
if (manifest.author?.name) ifaceOut.developerName = manifest.author.name;
|
|
494
|
+
if (homepage) ifaceOut.websiteURL = homepage;
|
|
495
|
+
if (Object.keys(ifaceOut).length > 0) out.interface = ifaceOut;
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
var SkillAuthError = class extends Error {
|
|
499
|
+
constructor(skillName, status) {
|
|
500
|
+
super(
|
|
501
|
+
`Skill '${skillName}' requires authentication (HTTP ${status}). Run 'rush-ai auth login' first.`
|
|
502
|
+
);
|
|
503
|
+
this.name = "SkillAuthError";
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
function substituteMcpSecrets(mcpServers, secrets) {
|
|
507
|
+
const result = {};
|
|
508
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
509
|
+
if (!serverConfig || typeof serverConfig !== "object") {
|
|
510
|
+
result[serverName] = serverConfig;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const cfg = { ...serverConfig };
|
|
514
|
+
if (cfg.env && typeof cfg.env === "object") {
|
|
515
|
+
const env = {};
|
|
516
|
+
for (const [k, v] of Object.entries(cfg.env)) {
|
|
517
|
+
env[k] = substituteValue(v, secrets);
|
|
518
|
+
}
|
|
519
|
+
cfg.env = env;
|
|
520
|
+
}
|
|
521
|
+
if (cfg.headers && typeof cfg.headers === "object") {
|
|
522
|
+
const headers = {};
|
|
523
|
+
for (const [k, v] of Object.entries(
|
|
524
|
+
cfg.headers
|
|
525
|
+
)) {
|
|
526
|
+
headers[k] = substituteValue(v, secrets);
|
|
527
|
+
}
|
|
528
|
+
cfg.headers = headers;
|
|
529
|
+
}
|
|
530
|
+
result[serverName] = cfg;
|
|
531
|
+
}
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
function normalizeMcpServerDefaults(mcpServers) {
|
|
535
|
+
const result = {};
|
|
536
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
537
|
+
if (!serverConfig || typeof serverConfig !== "object") {
|
|
538
|
+
result[serverName] = serverConfig;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const cfg = { ...serverConfig };
|
|
542
|
+
if (typeof cfg.url === "string" && cfg.url.length > 0 && typeof cfg.command !== "string" && typeof cfg.type !== "string") {
|
|
543
|
+
cfg.type = "http";
|
|
544
|
+
}
|
|
545
|
+
result[serverName] = cfg;
|
|
546
|
+
}
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
function substituteValue(value, secrets) {
|
|
550
|
+
return value.replace(/\$\{([^}]+)\}/g, (match, key) => {
|
|
551
|
+
return key in secrets ? secrets[key] : match;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function validateSlug(value, label) {
|
|
555
|
+
if (!value || value.includes("..") || value.startsWith("/") || value.includes("\0")) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`Invalid ${label} '${value}': must not be empty, contain '..', start with '/', or contain null bytes.`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function inferProtocol(host) {
|
|
562
|
+
const hostname = host.split(":")[0];
|
|
563
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0") {
|
|
564
|
+
return "http://";
|
|
565
|
+
}
|
|
566
|
+
return "https://";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/commands/marketplace/_codex-content-loader.ts
|
|
570
|
+
function pickContentLoader(resolved, opts = {}) {
|
|
571
|
+
switch (resolved.source.kind) {
|
|
572
|
+
case "rush":
|
|
573
|
+
return buildRushPluginContentLoader(resolved.source, opts);
|
|
574
|
+
case "directory":
|
|
575
|
+
case "github":
|
|
576
|
+
return buildLocalCachePluginContentLoader(resolved);
|
|
577
|
+
default:
|
|
578
|
+
return async () => ({});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function buildRushPluginContentLoader(source, opts = {}) {
|
|
582
|
+
const token = opts.token ?? getAuthToken();
|
|
583
|
+
const fetchOpts = {};
|
|
584
|
+
if (opts.fetchFn !== void 0) fetchOpts.fetchFn = opts.fetchFn;
|
|
585
|
+
if (token !== void 0 && token !== null) fetchOpts.token = token;
|
|
586
|
+
const webBase = inferWebBase(source.host);
|
|
587
|
+
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
588
|
+
return async (entry) => {
|
|
589
|
+
if (typeof entry.name !== "string" || entry.name.length === 0) return {};
|
|
590
|
+
try {
|
|
591
|
+
const detail = await fetchRushPlugin(source, entry.name, fetchOpts);
|
|
592
|
+
const manifest = pickManifestFields(
|
|
593
|
+
detail
|
|
594
|
+
);
|
|
595
|
+
const hasMcp = detail.mcpServers && Object.keys(detail.mcpServers).length > 0;
|
|
596
|
+
const pluginWebUrl = `${webBase}/next/plugins/${encodeURIComponent(
|
|
597
|
+
entry.name
|
|
598
|
+
)}`;
|
|
599
|
+
const isOfficial = detail.source === "claude-plugins-official";
|
|
600
|
+
const isGitImported = detail.source === "rush-git-import";
|
|
601
|
+
const usesOssProxy = isOfficial || isGitImported;
|
|
602
|
+
const isExternalRedirect = isOfficial && !!detail.redirectTo;
|
|
603
|
+
const skillEntries = Array.isArray(detail.skills) && detail.skills.length > 0 ? detail.skills.filter(
|
|
604
|
+
(s) => !!s && typeof s.name === "string" && s.name.length > 0
|
|
605
|
+
) : [];
|
|
606
|
+
const skillsPlaceholders = [];
|
|
607
|
+
for (const s of skillEntries) {
|
|
608
|
+
const url = `${webBase}/next/plugins/${encodeURIComponent(entry.name)}`;
|
|
609
|
+
let rawSkillMd;
|
|
610
|
+
if (isExternalRedirect) {
|
|
611
|
+
} else if (usesOssProxy && typeof s.path === "string" && s.path.length > 0) {
|
|
612
|
+
const skillMdPath = ensureSkillMdSuffix(s.path);
|
|
613
|
+
try {
|
|
614
|
+
const file = await fetchRushOfficialFile(
|
|
615
|
+
source,
|
|
616
|
+
entry.name,
|
|
617
|
+
skillMdPath,
|
|
618
|
+
fetchOpts
|
|
619
|
+
);
|
|
620
|
+
if (typeof file.content === "string" && file.content.length > 0) {
|
|
621
|
+
rawSkillMd = file.content;
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
const mdUrl = `${webBase}/next/skills/${encodeURIComponent(s.name)}.md`;
|
|
627
|
+
try {
|
|
628
|
+
const headers = {
|
|
629
|
+
Accept: "text/markdown,text/plain,*/*"
|
|
630
|
+
};
|
|
631
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
632
|
+
const res = await fetchFn(mdUrl, { headers });
|
|
633
|
+
if (res.ok) {
|
|
634
|
+
const text = await res.text();
|
|
635
|
+
if (text.length > 0) rawSkillMd = text;
|
|
636
|
+
}
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
skillsPlaceholders.push({
|
|
641
|
+
name: s.name,
|
|
642
|
+
url,
|
|
643
|
+
...rawSkillMd !== void 0 ? { rawSkillMd } : {}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
...manifest !== void 0 ? { manifest } : {},
|
|
648
|
+
...hasMcp ? { mcpServers: detail.mcpServers } : {},
|
|
649
|
+
...skillsPlaceholders.length > 0 ? { skillsPlaceholders } : {},
|
|
650
|
+
pluginWebUrl
|
|
651
|
+
};
|
|
652
|
+
} catch {
|
|
653
|
+
return {};
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function inferWebBase(host) {
|
|
658
|
+
const hostname = host.split(":")[0];
|
|
659
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0") {
|
|
660
|
+
return `http://${host}`;
|
|
661
|
+
}
|
|
662
|
+
return `https://${host}`;
|
|
663
|
+
}
|
|
664
|
+
function buildLocalCachePluginContentLoader(resolved) {
|
|
665
|
+
return async (entry) => {
|
|
666
|
+
const relPath = extractLocalPluginPath(entry);
|
|
667
|
+
if (relPath === null) return {};
|
|
668
|
+
const pluginDir = pathResolve2(resolved.rootDir, relPath);
|
|
669
|
+
const root = pathResolve2(resolved.rootDir);
|
|
670
|
+
const rootWithSep = root.endsWith(sep) ? root : root + sep;
|
|
671
|
+
const safe = pluginDir === root || pluginDir.startsWith(rootWithSep);
|
|
672
|
+
if (!safe) return {};
|
|
673
|
+
let manifest;
|
|
674
|
+
let mcpServers;
|
|
675
|
+
let skillsSourceDir;
|
|
676
|
+
try {
|
|
677
|
+
const pj = await readFile(
|
|
678
|
+
pathResolve2(pluginDir, ".claude-plugin", "plugin.json"),
|
|
679
|
+
"utf8"
|
|
680
|
+
);
|
|
681
|
+
const parsed = JSON.parse(pj);
|
|
682
|
+
manifest = pickManifestFields(parsed);
|
|
683
|
+
const inline = parsed.mcpServers;
|
|
684
|
+
if (inline && typeof inline === "object" && !Array.isArray(inline) && Object.keys(inline).length > 0) {
|
|
685
|
+
mcpServers = inline;
|
|
686
|
+
}
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const raw = await readFile(pathResolve2(pluginDir, ".mcp.json"), "utf8");
|
|
691
|
+
const parsed = JSON.parse(raw);
|
|
692
|
+
let servers = null;
|
|
693
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "mcpServers" in parsed && typeof parsed.mcpServers === "object") {
|
|
694
|
+
servers = parsed.mcpServers;
|
|
695
|
+
} else if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
696
|
+
servers = parsed;
|
|
697
|
+
}
|
|
698
|
+
if (servers && Object.keys(servers).length > 0) {
|
|
699
|
+
mcpServers = servers;
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
const skillsDir = pathResolve2(pluginDir, "skills");
|
|
704
|
+
if (await isDir(skillsDir)) {
|
|
705
|
+
skillsSourceDir = skillsDir;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
...manifest !== void 0 ? { manifest } : {},
|
|
709
|
+
...mcpServers !== void 0 ? { mcpServers } : {},
|
|
710
|
+
...skillsSourceDir !== void 0 ? { skillsSourceDir } : {}
|
|
711
|
+
};
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function extractLocalPluginPath(entry) {
|
|
715
|
+
if (typeof entry.source === "string") {
|
|
716
|
+
return entry.source;
|
|
717
|
+
}
|
|
718
|
+
if (entry.source && typeof entry.source === "object" && !Array.isArray(entry.source)) {
|
|
719
|
+
const obj = entry.source;
|
|
720
|
+
if (typeof obj.url === "string" && obj.url.length > 0) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
if (typeof obj.path === "string" && obj.path.length > 0) {
|
|
724
|
+
return obj.path;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (entry.source === void 0 && typeof entry.name === "string" && entry.name.length > 0) {
|
|
728
|
+
return `plugins/${entry.name}`;
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
async function isDir(p) {
|
|
733
|
+
try {
|
|
734
|
+
const s = await stat2(p);
|
|
735
|
+
return s.isDirectory();
|
|
736
|
+
} catch {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async function pathExists(p) {
|
|
741
|
+
try {
|
|
742
|
+
await access(p, fsConstants.F_OK);
|
|
743
|
+
return true;
|
|
744
|
+
} catch {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function pickManifestFields(raw) {
|
|
749
|
+
const m = {};
|
|
750
|
+
if (typeof raw.description === "string" && raw.description.length > 0) {
|
|
751
|
+
m.description = raw.description;
|
|
752
|
+
}
|
|
753
|
+
if (typeof raw.version === "string" && raw.version.length > 0) {
|
|
754
|
+
m.version = raw.version;
|
|
755
|
+
}
|
|
756
|
+
if (typeof raw.homepage === "string" && raw.homepage.length > 0) {
|
|
757
|
+
m.homepage = raw.homepage;
|
|
758
|
+
}
|
|
759
|
+
if (typeof raw.license === "string" && raw.license.length > 0) {
|
|
760
|
+
m.license = raw.license;
|
|
761
|
+
}
|
|
762
|
+
if (Array.isArray(raw.keywords)) {
|
|
763
|
+
const kw = raw.keywords.filter(
|
|
764
|
+
(x) => typeof x === "string" && x.length > 0
|
|
765
|
+
);
|
|
766
|
+
if (kw.length > 0) m.keywords = kw;
|
|
767
|
+
}
|
|
768
|
+
if (raw.author && typeof raw.author === "object" && !Array.isArray(raw.author)) {
|
|
769
|
+
const a = raw.author;
|
|
770
|
+
const author = {};
|
|
771
|
+
if (typeof a.name === "string") author.name = a.name;
|
|
772
|
+
if (typeof a.email === "string") author.email = a.email;
|
|
773
|
+
if (typeof a.url === "string") author.url = a.url;
|
|
774
|
+
if (Object.keys(author).length > 0) m.author = author;
|
|
775
|
+
}
|
|
776
|
+
if (raw.interface && typeof raw.interface === "object" && !Array.isArray(raw.interface)) {
|
|
777
|
+
const iface = pickInterfaceFields(raw.interface);
|
|
778
|
+
if (iface !== void 0) m.interface = iface;
|
|
779
|
+
}
|
|
780
|
+
return Object.keys(m).length > 0 ? m : void 0;
|
|
781
|
+
}
|
|
782
|
+
function pickInterfaceFields(raw) {
|
|
783
|
+
const out = {};
|
|
784
|
+
const stringKeys = [
|
|
785
|
+
"displayName",
|
|
786
|
+
"shortDescription",
|
|
787
|
+
"longDescription",
|
|
788
|
+
"developerName",
|
|
789
|
+
"category",
|
|
790
|
+
"websiteURL",
|
|
791
|
+
"privacyPolicyURL",
|
|
792
|
+
"termsOfServiceURL",
|
|
793
|
+
"brandColor",
|
|
794
|
+
"composerIcon",
|
|
795
|
+
"logo"
|
|
796
|
+
];
|
|
797
|
+
for (const key of stringKeys) {
|
|
798
|
+
const v = raw[key];
|
|
799
|
+
if (typeof v === "string" && v.length > 0) {
|
|
800
|
+
out[key] = v;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
for (const key of ["capabilities", "defaultPrompt", "screenshots"]) {
|
|
804
|
+
const v = raw[key];
|
|
805
|
+
if (Array.isArray(v)) {
|
|
806
|
+
const arr = v.filter(
|
|
807
|
+
(x) => typeof x === "string" && x.length > 0
|
|
808
|
+
);
|
|
809
|
+
if (arr.length > 0) out[key] = arr;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
813
|
+
}
|
|
814
|
+
function ensureSkillMdSuffix(p) {
|
|
815
|
+
if (p.endsWith(".md") || p.endsWith(".MD")) return p;
|
|
816
|
+
const trimmed = p.endsWith("/") ? p.slice(0, -1) : p;
|
|
817
|
+
return `${trimmed}/SKILL.md`;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export {
|
|
821
|
+
fetchRushMarketplace,
|
|
822
|
+
fetchRushPlugin,
|
|
823
|
+
materializeRushPlugin,
|
|
824
|
+
SkillAuthError,
|
|
825
|
+
pickContentLoader,
|
|
826
|
+
buildRushPluginContentLoader,
|
|
827
|
+
buildLocalCachePluginContentLoader,
|
|
828
|
+
pathExists
|
|
829
|
+
};
|
|
830
|
+
//# sourceMappingURL=chunk-C7KJUHEF.js.map
|