rush-ai 0.18.1 → 0.19.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.
@@ -0,0 +1,992 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ output
4
+ } from "./chunk-T5S6NCHZ.js";
5
+
6
+ // src/installers/codex/mirror.ts
7
+ import { randomUUID as randomUUID2 } from "crypto";
8
+ import { constants as fsConstants2 } from "fs";
9
+ import {
10
+ access as access2,
11
+ cp,
12
+ mkdir as mkdir2,
13
+ mkdtemp,
14
+ readdir,
15
+ readFile as readFile2,
16
+ rename as rename2,
17
+ rm as rm2,
18
+ stat as stat2,
19
+ writeFile as writeFile2
20
+ } from "fs/promises";
21
+ import { basename, dirname as dirname2, resolve } from "path";
22
+
23
+ // src/installers/codex/paths.ts
24
+ import { resolve as pathResolve } from "path";
25
+ function codexHomeDir(home) {
26
+ return pathResolve(home, ".codex");
27
+ }
28
+ function codexConfigTomlPath(home) {
29
+ return pathResolve(codexHomeDir(home), "config.toml");
30
+ }
31
+ function codexPluginsCacheDir(home) {
32
+ return pathResolve(codexHomeDir(home), "plugins", "cache");
33
+ }
34
+ function codexPluginsMarketplacesDir(home) {
35
+ return pathResolve(codexHomeDir(home), "plugins", "marketplaces");
36
+ }
37
+ function codexMarketplaceDir(home, marketplace) {
38
+ return pathResolve(codexPluginsMarketplacesDir(home), marketplace);
39
+ }
40
+ function codexMarketplaceManifestPath(marketplaceDir) {
41
+ return pathResolve(marketplaceDir, ".agents", "plugins", "marketplace.json");
42
+ }
43
+ function codexMarketplacePluginDir(marketplaceDir, pluginName) {
44
+ return pathResolve(marketplaceDir, "plugins", pluginName);
45
+ }
46
+ function codexPluginVersionDir(home, ref, version) {
47
+ return pathResolve(
48
+ codexPluginsCacheDir(home),
49
+ ref.marketplace,
50
+ ref.name,
51
+ version
52
+ );
53
+ }
54
+ function codexPluginManifestPath(versionDir) {
55
+ return pathResolve(versionDir, ".codex-plugin", "plugin.json");
56
+ }
57
+ function codexPluginMcpPath(versionDir) {
58
+ return pathResolve(versionDir, ".mcp.json");
59
+ }
60
+ function codexPluginSkillsDir(versionDir) {
61
+ return pathResolve(versionDir, "skills");
62
+ }
63
+ function codexConfigBackupPath(home, now) {
64
+ const ts = formatBackupTimestamp(now);
65
+ return `${codexConfigTomlPath(home)}.bak.${ts}`;
66
+ }
67
+ function formatBackupTimestamp(date) {
68
+ const pad = (n) => String(n).padStart(2, "0");
69
+ const yyyy = date.getUTCFullYear();
70
+ const mm = pad(date.getUTCMonth() + 1);
71
+ const dd = pad(date.getUTCDate());
72
+ const hh = pad(date.getUTCHours());
73
+ const mi = pad(date.getUTCMinutes());
74
+ const ss = pad(date.getUTCSeconds());
75
+ return `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
76
+ }
77
+ function pluginSectionKey(ref) {
78
+ return `${ref.name}@${ref.marketplace}`;
79
+ }
80
+
81
+ // src/installers/codex/toml.ts
82
+ import { randomUUID } from "crypto";
83
+ import { constants as fsConstants } from "fs";
84
+ import {
85
+ access,
86
+ copyFile,
87
+ mkdir,
88
+ readFile,
89
+ rename,
90
+ rm,
91
+ stat,
92
+ writeFile
93
+ } from "fs/promises";
94
+ import { dirname } from "path";
95
+ import tomlModule from "@iarna/toml";
96
+ var { parse: tomlParse, stringify: tomlStringify } = tomlModule;
97
+ var CodexConfigTomlError = class extends Error {
98
+ constructor(message) {
99
+ super(message);
100
+ this.name = "CodexConfigTomlError";
101
+ }
102
+ };
103
+ var CodexConfigTomlCorruptError = class extends CodexConfigTomlError {
104
+ constructor(filePath, cause) {
105
+ super(
106
+ `Codex config.toml \u89E3\u6790\u5931\u8D25\uFF08${filePath}\uFF09\u3002\u8BF7\u5907\u4EFD\u540E\u624B\u5DE5\u4FEE\u590D\u3002\u539F\u56E0\uFF1A${String(
107
+ cause?.message ?? cause
108
+ )}`
109
+ );
110
+ this.filePath = filePath;
111
+ this.cause = cause;
112
+ this.name = "CodexConfigTomlCorruptError";
113
+ }
114
+ };
115
+ var CodexConfigTomlConflictError = class extends CodexConfigTomlError {
116
+ constructor(filePath) {
117
+ super(
118
+ `Codex config.toml \u5E76\u53D1\u5199\u51B2\u7A81\uFF1A\u8BFB\u53D6\u548C\u5199\u5165\u4E4B\u95F4 ${filePath} \u88AB\u5176\u4ED6\u8FDB\u7A0B\u4FEE\u6539\u3002\u8BF7\u91CD\u8BD5\u3002`
119
+ );
120
+ this.filePath = filePath;
121
+ this.name = "CodexConfigTomlConflictError";
122
+ }
123
+ };
124
+ async function readCodexConfig(filePath) {
125
+ if (!await pathExists(filePath)) {
126
+ return { data: {}, mtimeMs: null };
127
+ }
128
+ const stats = await stat(filePath);
129
+ const raw = await readFile(filePath, "utf8");
130
+ try {
131
+ const parsed = tomlParse(raw);
132
+ return { data: parsed, mtimeMs: stats.mtimeMs };
133
+ } catch (err) {
134
+ throw new CodexConfigTomlCorruptError(filePath, err);
135
+ }
136
+ }
137
+ async function backupCodexConfig(filePath, backupPath) {
138
+ if (!await pathExists(filePath)) {
139
+ return null;
140
+ }
141
+ let finalBackup = backupPath;
142
+ if (await pathExists(finalBackup)) {
143
+ finalBackup = `${backupPath}.${randomUUID().slice(0, 8)}`;
144
+ }
145
+ await copyFile(filePath, finalBackup);
146
+ return finalBackup;
147
+ }
148
+ async function writeCodexConfig(filePath, data, expectedMtimeMs) {
149
+ await assertNoMtimeDrift(filePath, expectedMtimeMs);
150
+ const serialized = tomlStringify(data);
151
+ await atomicWrite(filePath, serialized);
152
+ const afterStats = await stat(filePath);
153
+ return { mtimeMs: afterStats.mtimeMs };
154
+ }
155
+ async function restoreCodexConfigFromBackup(filePath, backupPath) {
156
+ if (!backupPath || !await pathExists(backupPath)) {
157
+ if (await pathExists(filePath)) {
158
+ await rm(filePath, { force: true });
159
+ }
160
+ return;
161
+ }
162
+ const raw = await readFile(backupPath, "utf8");
163
+ await atomicWrite(filePath, raw);
164
+ await rm(backupPath, { force: true }).catch(() => {
165
+ });
166
+ }
167
+ function setMarketplaceSection(config, name, entry) {
168
+ const mkts = ensureObjectSection(config, "marketplaces");
169
+ mkts[name] = { ...entry };
170
+ }
171
+ function removeMarketplaceSection(config, name) {
172
+ const mkts = config.marketplaces;
173
+ if (!mkts || typeof mkts !== "object") return;
174
+ delete mkts[name];
175
+ if (Object.keys(mkts).length === 0) {
176
+ delete config.marketplaces;
177
+ }
178
+ }
179
+ function setPluginEntry(config, key, entry) {
180
+ const plugins = ensureObjectSection(config, "plugins");
181
+ plugins[key] = { ...entry };
182
+ }
183
+ function removePluginEntry(config, key) {
184
+ const plugins = config.plugins;
185
+ if (!plugins || typeof plugins !== "object") return;
186
+ delete plugins[key];
187
+ if (Object.keys(plugins).length === 0) {
188
+ delete config.plugins;
189
+ }
190
+ }
191
+ var MCP_OWNER_MARKER_KEY = "_rush_ai_owner";
192
+ function setMcpServerSection(config, serverName, ownerKey, entry) {
193
+ const servers = ensureObjectSection(config, "mcp_servers");
194
+ servers[serverName] = { ...entry, [MCP_OWNER_MARKER_KEY]: ownerKey };
195
+ }
196
+ function removeMcpServerSection(config, serverName, expectedOwner) {
197
+ const servers = config.mcp_servers;
198
+ if (!servers || typeof servers !== "object") return "absent";
199
+ const entry = servers[serverName];
200
+ if (!entry || typeof entry !== "object") return "absent";
201
+ const owner = entry[MCP_OWNER_MARKER_KEY];
202
+ if (owner !== expectedOwner) return "foreign";
203
+ delete servers[serverName];
204
+ if (Object.keys(servers).length === 0) {
205
+ delete config.mcp_servers;
206
+ }
207
+ return "removed";
208
+ }
209
+ function getMcpServerOwner(config, serverName) {
210
+ const servers = config.mcp_servers;
211
+ if (!servers || typeof servers !== "object") return null;
212
+ const entry = servers[serverName];
213
+ if (!entry || typeof entry !== "object") return null;
214
+ const owner = entry[MCP_OWNER_MARKER_KEY];
215
+ return typeof owner === "string" ? owner : void 0;
216
+ }
217
+ async function pathExists(p) {
218
+ try {
219
+ await access(p, fsConstants.F_OK);
220
+ return true;
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ async function atomicWrite(filePath, content) {
226
+ await mkdir(dirname(filePath), { recursive: true });
227
+ const tmp = `${filePath}.${randomUUID()}.tmp`;
228
+ try {
229
+ await writeFile(tmp, content, { encoding: "utf8", flag: "w" });
230
+ await rename(tmp, filePath);
231
+ } catch (err) {
232
+ await rm(tmp, { force: true }).catch(() => {
233
+ });
234
+ throw err;
235
+ }
236
+ }
237
+ async function assertNoMtimeDrift(filePath, expectedMtimeMs) {
238
+ if (expectedMtimeMs === null) {
239
+ if (await pathExists(filePath)) {
240
+ throw new CodexConfigTomlConflictError(filePath);
241
+ }
242
+ return;
243
+ }
244
+ const current = await stat(filePath).catch(() => null);
245
+ if (current === null) {
246
+ throw new CodexConfigTomlConflictError(filePath);
247
+ }
248
+ if (current.mtimeMs !== expectedMtimeMs) {
249
+ throw new CodexConfigTomlConflictError(filePath);
250
+ }
251
+ }
252
+ function ensureObjectSection(config, key) {
253
+ const existing = config[key];
254
+ if (existing === void 0) {
255
+ const fresh = {};
256
+ config[key] = fresh;
257
+ return fresh;
258
+ }
259
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
260
+ throw new CodexConfigTomlError(
261
+ `config.toml \u9876\u5C42 '${key}' \u5DF2\u5B58\u5728\u4F46\u4E0D\u662F object\uFF08type=${Array.isArray(existing) ? "array" : typeof existing}\uFF09\u3002\u8BF7\u624B\u5DE5\u5907\u4EFD\u5E76\u6574\u7406\u8BE5\u5B57\u6BB5\u540E\u91CD\u8BD5\u3002`
262
+ );
263
+ }
264
+ return existing;
265
+ }
266
+
267
+ // src/installers/codex/mirror.ts
268
+ function isSafePluginName(name) {
269
+ if (typeof name !== "string" || name.length === 0) return false;
270
+ if (name === "." || name === "..") return false;
271
+ if (name.startsWith(".")) return false;
272
+ if (name.includes("/") || name.includes("\\")) return false;
273
+ if (name.includes("..")) return false;
274
+ return /^[A-Za-z0-9_@][A-Za-z0-9._@-]*$/.test(name);
275
+ }
276
+ var DEFAULT_CATEGORY = "Engineering";
277
+ var DEFAULT_CAPABILITIES = ["Read", "Write"];
278
+ async function syncCodexMarketplaceMirror(home, resolved, opts = {}) {
279
+ const result = {
280
+ skipped: false,
281
+ added: [],
282
+ refreshed: [],
283
+ preservedFull: [],
284
+ removed: [],
285
+ warnings: []
286
+ };
287
+ if (!await isDir(codexHomeDir(home))) {
288
+ result.skipped = true;
289
+ return result;
290
+ }
291
+ const marketplaceDir = codexMarketplaceDir(home, resolved.name);
292
+ const manifestPath = codexMarketplaceManifestPath(marketplaceDir);
293
+ const contentLoader = opts.contentLoader ?? (async () => ({}));
294
+ const fallback = {
295
+ name: resolved.name,
296
+ interface: { displayName: resolved.name },
297
+ plugins: []
298
+ };
299
+ const existing = await readCodexMarketplaceCatalog(manifestPath, fallback);
300
+ const existingByName = new Map(existing.plugins.map((p) => [p.name, p]));
301
+ const remotePlugins = [];
302
+ const remoteNames = /* @__PURE__ */ new Set();
303
+ for (const entry of resolved.manifest.plugins) {
304
+ if (typeof entry?.name !== "string" || !isSafePluginName(entry.name)) {
305
+ const display = typeof entry?.name === "string" ? entry.name : "<unnamed>";
306
+ result.warnings.push(`skipped plugin with unsafe name: ${display}`);
307
+ output.warn(
308
+ `[codex] marketplace '${resolved.name}': skipped plugin with unsafe name: ${display}`
309
+ );
310
+ continue;
311
+ }
312
+ if (remoteNames.has(entry.name)) {
313
+ result.warnings.push(`duplicate plugin name in manifest: ${entry.name}`);
314
+ continue;
315
+ }
316
+ remotePlugins.push(entry);
317
+ remoteNames.add(entry.name);
318
+ }
319
+ const nextEntries = [];
320
+ for (const entry of remotePlugins) {
321
+ const pluginDir = codexMarketplacePluginDir(marketplaceDir, entry.name);
322
+ const fullState = await detectFullPluginDir(pluginDir);
323
+ if (fullState === "full") {
324
+ const prior = existingByName.get(entry.name);
325
+ const base = prior ?? buildStubCatalogEntry(entry);
326
+ const fullEntry = {
327
+ ...base,
328
+ policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }
329
+ };
330
+ nextEntries.push(fullEntry);
331
+ result.preservedFull.push(entry.name);
332
+ continue;
333
+ }
334
+ let content = {};
335
+ try {
336
+ content = await contentLoader(entry);
337
+ } catch (err) {
338
+ result.warnings.push(
339
+ `contentLoader failed for '${entry.name}': ${err.message}`
340
+ );
341
+ output.warn(
342
+ `[codex] marketplace '${resolved.name}': contentLoader failed for '${entry.name}': ${err.message}`
343
+ );
344
+ content = {};
345
+ }
346
+ try {
347
+ await writePluginMaterials(pluginDir, entry, content, resolved.name);
348
+ } catch (err) {
349
+ result.warnings.push(
350
+ `failed to write stub for '${entry.name}': ${err.message}`
351
+ );
352
+ output.warn(
353
+ `[codex] marketplace '${resolved.name}': failed to write stub for '${entry.name}': ${err.message}`
354
+ );
355
+ continue;
356
+ }
357
+ nextEntries.push(buildStubCatalogEntry(entry, content));
358
+ if (existingByName.has(entry.name)) {
359
+ result.refreshed.push(entry.name);
360
+ } else {
361
+ result.added.push(entry.name);
362
+ }
363
+ }
364
+ for (const prior of existing.plugins) {
365
+ if (remoteNames.has(prior.name)) continue;
366
+ if (!isSafePluginName(prior.name)) {
367
+ result.warnings.push(
368
+ `dropped catalog entry with unsafe name: ${prior.name}`
369
+ );
370
+ output.warn(
371
+ `[codex] marketplace '${resolved.name}': dropped catalog entry with unsafe name: ${prior.name}`
372
+ );
373
+ result.removed.push(prior.name);
374
+ continue;
375
+ }
376
+ const pluginDir = codexMarketplacePluginDir(marketplaceDir, prior.name);
377
+ if (await detectFullPluginDir(pluginDir) === "full") {
378
+ nextEntries.push(prior);
379
+ output.warn(
380
+ `[codex] marketplace '${resolved.name}': installed plugin '${prior.name}' no longer in remote catalog; keeping entry. Run 'rush-ai plugin uninstall ${prior.name}@${resolved.name}' if intended.`
381
+ );
382
+ continue;
383
+ }
384
+ await rm2(pluginDir, { recursive: true, force: true }).catch(() => {
385
+ });
386
+ result.removed.push(prior.name);
387
+ }
388
+ nextEntries.sort((a, b) => a.name.localeCompare(b.name));
389
+ const nextCatalog = {
390
+ name: existing.name || resolved.name,
391
+ interface: existing.interface ?? fallback.interface,
392
+ plugins: nextEntries
393
+ };
394
+ await writeFileAtomic(
395
+ manifestPath,
396
+ `${JSON.stringify(nextCatalog, null, 2)}
397
+ `
398
+ );
399
+ try {
400
+ await registerMarketplaceInCodexConfig({
401
+ home,
402
+ marketplaceName: resolved.name,
403
+ marketplaceDir,
404
+ now: opts.now ?? (() => /* @__PURE__ */ new Date())
405
+ });
406
+ } catch (err) {
407
+ const msg = err instanceof Error ? err.message : String(err);
408
+ result.warnings.push(
409
+ `failed to register [marketplaces.${resolved.name}] in config.toml: ${msg}`
410
+ );
411
+ output.warn(
412
+ `[codex] marketplace '${resolved.name}': failed to register in config.toml: ${msg}`
413
+ );
414
+ }
415
+ return result;
416
+ }
417
+ async function registerMarketplaceInCodexConfig(args) {
418
+ const cfgPath = codexConfigTomlPath(args.home);
419
+ await backupCodexConfig(
420
+ cfgPath,
421
+ codexConfigBackupPath(args.home, args.now())
422
+ );
423
+ const { data, mtimeMs } = await readCodexConfig(cfgPath);
424
+ setMarketplaceSection(data, args.marketplaceName, {
425
+ last_updated: args.now().toISOString(),
426
+ source_type: "local",
427
+ source: args.marketplaceDir
428
+ });
429
+ await writeCodexConfig(cfgPath, data, mtimeMs);
430
+ }
431
+ async function writeCodexMarketplacePluginMirror(input) {
432
+ const pluginParent = dirname2(input.pluginDir);
433
+ const pluginBase = basename(input.pluginDir);
434
+ await mkdir2(pluginParent, { recursive: true });
435
+ const tmpPluginDir = await mkdtemp(
436
+ resolve(pluginParent, `.${pluginBase}.tmp-`)
437
+ );
438
+ const backupPluginDir = await mkdtemp(
439
+ resolve(pluginParent, `.${pluginBase}.bak-`)
440
+ );
441
+ let hadExistingPluginDir = false;
442
+ let swappedPluginDir = false;
443
+ try {
444
+ await rm2(tmpPluginDir, { recursive: true, force: true });
445
+ await rm2(backupPluginDir, { recursive: true, force: true }).catch(() => {
446
+ });
447
+ await cp(input.versionDir, tmpPluginDir, {
448
+ recursive: true,
449
+ dereference: false,
450
+ preserveTimestamps: true,
451
+ verbatimSymlinks: true
452
+ });
453
+ hadExistingPluginDir = await pathExists2(input.pluginDir);
454
+ if (hadExistingPluginDir) {
455
+ await rename2(input.pluginDir, backupPluginDir);
456
+ }
457
+ await rename2(tmpPluginDir, input.pluginDir);
458
+ swappedPluginDir = true;
459
+ await upsertCodexCatalogPluginFull(input.marketplaceDir, input.plugin);
460
+ if (hadExistingPluginDir) {
461
+ await rm2(backupPluginDir, { recursive: true, force: true });
462
+ }
463
+ } catch (err) {
464
+ await rm2(tmpPluginDir, { recursive: true, force: true }).catch(() => {
465
+ });
466
+ let restoredBackup = false;
467
+ if (hadExistingPluginDir && await pathExists2(backupPluginDir)) {
468
+ let displacedPluginDir = null;
469
+ try {
470
+ if (await pathExists2(input.pluginDir)) {
471
+ displacedPluginDir = await mkdtemp(
472
+ resolve(pluginParent, `.${pluginBase}.failed-`)
473
+ );
474
+ await rm2(displacedPluginDir, { recursive: true, force: true });
475
+ await rename2(input.pluginDir, displacedPluginDir);
476
+ }
477
+ await rename2(backupPluginDir, input.pluginDir);
478
+ restoredBackup = true;
479
+ if (displacedPluginDir) {
480
+ await rm2(displacedPluginDir, { recursive: true, force: true }).catch(
481
+ () => {
482
+ }
483
+ );
484
+ }
485
+ } catch {
486
+ if (displacedPluginDir && !await pathExists2(input.pluginDir) && await pathExists2(displacedPluginDir)) {
487
+ await rename2(displacedPluginDir, input.pluginDir).catch(() => {
488
+ });
489
+ }
490
+ }
491
+ } else if (swappedPluginDir) {
492
+ await rm2(input.pluginDir, { recursive: true, force: true }).catch(
493
+ () => {
494
+ }
495
+ );
496
+ }
497
+ if (restoredBackup || !hadExistingPluginDir) {
498
+ await rm2(backupPluginDir, { recursive: true, force: true }).catch(
499
+ () => {
500
+ }
501
+ );
502
+ }
503
+ throw err;
504
+ }
505
+ }
506
+ async function upsertCodexCatalogPluginFull(marketplaceDir, plugin) {
507
+ const manifestPath = codexMarketplaceManifestPath(marketplaceDir);
508
+ const fallback = {
509
+ name: plugin.ref.marketplace,
510
+ interface: { displayName: plugin.ref.marketplace },
511
+ plugins: []
512
+ };
513
+ const catalog = await readCodexMarketplaceCatalog(manifestPath, fallback);
514
+ const nextEntry = {
515
+ name: plugin.ref.name,
516
+ source: { source: "local", path: `./plugins/${plugin.ref.name}` },
517
+ policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
518
+ category: buildInterfaceCategory(plugin.manifest)
519
+ };
520
+ if (plugin.manifest.description !== void 0) {
521
+ nextEntry.description = plugin.manifest.description;
522
+ }
523
+ if (typeof plugin.manifest.version === "string") {
524
+ nextEntry.version = plugin.manifest.version;
525
+ }
526
+ const next = [
527
+ ...catalog.plugins.filter((entry) => entry.name !== plugin.ref.name),
528
+ nextEntry
529
+ ].sort((a, b) => a.name.localeCompare(b.name));
530
+ await writeFileAtomic(
531
+ manifestPath,
532
+ `${JSON.stringify({ ...catalog, plugins: next }, null, 2)}
533
+ `
534
+ );
535
+ }
536
+ async function downgradeCodexCatalogEntryToStub(marketplaceDir, pluginName) {
537
+ const manifestPath = codexMarketplaceManifestPath(marketplaceDir);
538
+ const pluginDir = codexMarketplacePluginDir(marketplaceDir, pluginName);
539
+ await rm2(pluginDir, { recursive: true, force: true }).catch(() => {
540
+ });
541
+ if (!await pathExists2(manifestPath)) return;
542
+ try {
543
+ const fallback = {
544
+ name: "",
545
+ interface: {},
546
+ plugins: []
547
+ };
548
+ const catalog = await readCodexMarketplaceCatalog(manifestPath, fallback);
549
+ const next = catalog.plugins.map((entry) => {
550
+ if (entry.name !== pluginName) return entry;
551
+ return {
552
+ ...entry,
553
+ policy: {
554
+ installation: "NOT_AVAILABLE",
555
+ authentication: "ON_INSTALL"
556
+ }
557
+ };
558
+ });
559
+ await writeFileAtomic(
560
+ manifestPath,
561
+ `${JSON.stringify({ ...catalog, plugins: next }, null, 2)}
562
+ `
563
+ );
564
+ } catch {
565
+ }
566
+ }
567
+ async function readCodexMarketplaceCatalog(manifestPath, fallback) {
568
+ if (!await pathExists2(manifestPath)) return fallback;
569
+ try {
570
+ const parsed = JSON.parse(await readFile2(manifestPath, "utf8"));
571
+ return {
572
+ name: typeof parsed.name === "string" ? parsed.name : fallback.name,
573
+ interface: parsed.interface && typeof parsed.interface === "object" && !Array.isArray(parsed.interface) ? parsed.interface : fallback.interface,
574
+ plugins: Array.isArray(parsed.plugins) ? parsed.plugins.filter(isCatalogPluginEntry) : fallback.plugins
575
+ };
576
+ } catch {
577
+ return fallback;
578
+ }
579
+ }
580
+ function isCatalogPluginEntry(value) {
581
+ return !!value && typeof value === "object" && !Array.isArray(value) && typeof value.name === "string";
582
+ }
583
+ function buildStubCatalogEntry(entry, content = {}) {
584
+ const description = pickNonEmptyString(entry.description) ?? pickNonEmptyString(content.manifest?.description);
585
+ const version = pickNonEmptyString(entry.version) ?? pickNonEmptyString(content.manifest?.version);
586
+ const m = content.manifest ?? void 0;
587
+ const ifaceIn = m?.interface ?? {};
588
+ const entryCategory = pickNonEmptyString(
589
+ entry.category
590
+ );
591
+ const category = pickNonEmptyString(ifaceIn.category) ?? normalizeCategory(entryCategory) ?? DEFAULT_CATEGORY;
592
+ const out = {
593
+ name: entry.name,
594
+ source: { source: "local", path: `./plugins/${entry.name}` },
595
+ // stub 入口 → 标记为 NOT_AVAILABLE,让 Codex UI 禁用"连接"按钮,
596
+ // 因为点击连接只会写一份没有 MCP key 的伪安装。真正安装走
597
+ // `npx rush-ai@latest plugin install <name>@<marketplace>`。
598
+ // 已装 full 路径在 upsertCodexCatalogPluginFull 中覆写为 AVAILABLE。
599
+ policy: { installation: "NOT_AVAILABLE", authentication: "ON_INSTALL" },
600
+ category
601
+ };
602
+ if (description !== void 0) out.description = description;
603
+ if (version !== void 0) out.version = version;
604
+ return out;
605
+ }
606
+ function buildPluginJson(entry, content, marketplaceName) {
607
+ const m = content.manifest ?? void 0;
608
+ const author = m?.author ?? (entry.author && typeof entry.author === "object" ? entry.author : void 0);
609
+ const homepage = pickNonEmptyString(m?.homepage) ?? pickNonEmptyString(entry.homepage);
610
+ const license = pickNonEmptyString(m?.license) ?? pickNonEmptyString(entry.license);
611
+ const keywords = Array.isArray(m?.keywords) && m.keywords.length > 0 ? m.keywords : Array.isArray(entry.keywords) && entry.keywords.length > 0 ? entry.keywords : void 0;
612
+ const entryCategory = pickNonEmptyString(
613
+ entry.category
614
+ );
615
+ const sourceUrl = extractSourceUrl(entry.source);
616
+ const description = pickNonEmptyString(m?.description) ?? pickNonEmptyString(entry.description) ?? entry.name;
617
+ const version = pickNonEmptyString(m?.version) ?? pickNonEmptyString(entry.version) ?? "0.0.0";
618
+ const ifaceIn = m?.interface ?? {};
619
+ const developerName = pickNonEmptyString(ifaceIn.developerName) ?? pickNonEmptyString(author?.name);
620
+ const websiteURL = pickNonEmptyString(ifaceIn.websiteURL) ?? sourceUrl ?? homepage;
621
+ const category = pickNonEmptyString(ifaceIn.category) ?? normalizeCategory(entryCategory) ?? DEFAULT_CATEGORY;
622
+ const capabilities = Array.isArray(ifaceIn.capabilities) && ifaceIn.capabilities.length > 0 ? [...ifaceIn.capabilities] : [...DEFAULT_CAPABILITIES];
623
+ const installCmd = `npx rush-ai@latest plugin install ${entry.name}@${marketplaceName} --target codex`;
624
+ const shortHintPrefix = "\u26A0 \u9700\u5728\u7EC8\u7AEF\u5B89\u88C5 \uFF5C ";
625
+ const authorShort = pickNonEmptyString(ifaceIn.shortDescription) ?? description;
626
+ const baseLongDescription = pickNonEmptyString(ifaceIn.longDescription) ?? pickNonEmptyString(ifaceIn.shortDescription) ?? description;
627
+ const extraLinks = [];
628
+ if (homepage !== void 0 && homepage !== websiteURL) {
629
+ extraLinks.push(`\u5B98\u7F51\uFF1A${homepage}`);
630
+ }
631
+ if (sourceUrl !== void 0 && sourceUrl !== websiteURL && sourceUrl !== homepage) {
632
+ extraLinks.push(`\u6E90\u4EE3\u7801\uFF1A${sourceUrl}`);
633
+ }
634
+ const extraLinksSection = extraLinks.length > 0 ? `
635
+
636
+ ${extraLinks.join("\n")}` : "";
637
+ const installSection = `\u2014\u2014\u2014
638
+ \u26A0 Codex \u684C\u9762\u7AEF\u7684"\u8FDE\u63A5"\u6309\u94AE\u65E0\u6CD5\u5B8C\u6574\u5B89\u88C5\u672C\u63D2\u4EF6
639
+ \u8BF7\u5728\u7EC8\u7AEF\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\u5B8C\u6210\u5B89\u88C5\uFF1A
640
+ ${installCmd}`;
641
+ const longDescription = `${baseLongDescription}${extraLinksSection}
642
+
643
+ ${installSection}`;
644
+ const defaultPrompt = Array.isArray(ifaceIn.defaultPrompt) ? [...ifaceIn.defaultPrompt] : [];
645
+ if (defaultPrompt.length === 0) {
646
+ defaultPrompt.push(installCmd);
647
+ }
648
+ const ifaceOut = {
649
+ displayName: pickNonEmptyString(ifaceIn.displayName) ?? entry.name,
650
+ shortDescription: `${shortHintPrefix}${authorShort}`,
651
+ longDescription,
652
+ category,
653
+ capabilities,
654
+ defaultPrompt
655
+ };
656
+ if (developerName !== void 0) ifaceOut.developerName = developerName;
657
+ if (websiteURL !== void 0) ifaceOut.websiteURL = websiteURL;
658
+ if (pickNonEmptyString(ifaceIn.privacyPolicyURL) !== void 0)
659
+ ifaceOut.privacyPolicyURL = ifaceIn.privacyPolicyURL;
660
+ if (pickNonEmptyString(ifaceIn.termsOfServiceURL) !== void 0)
661
+ ifaceOut.termsOfServiceURL = ifaceIn.termsOfServiceURL;
662
+ if (pickNonEmptyString(ifaceIn.brandColor) !== void 0)
663
+ ifaceOut.brandColor = ifaceIn.brandColor;
664
+ if (pickNonEmptyString(ifaceIn.composerIcon) !== void 0)
665
+ ifaceOut.composerIcon = ifaceIn.composerIcon;
666
+ if (pickNonEmptyString(ifaceIn.logo) !== void 0)
667
+ ifaceOut.logo = ifaceIn.logo;
668
+ if (Array.isArray(ifaceIn.screenshots) && ifaceIn.screenshots.length > 0)
669
+ ifaceOut.screenshots = [...ifaceIn.screenshots];
670
+ const out = {
671
+ // marker:让 detectFullPluginDir 可靠区分 sync vs install 写出的目录。
672
+ // Codex 不知道这个字段,会被忽略;marker 必须是合法 JSON 字段名才能被 plugin.json 接受。
673
+ [STUB_MARKER_KEY]: STUB_MARKER_VALUE,
674
+ name: entry.name,
675
+ version,
676
+ description,
677
+ skills: "./skills/",
678
+ interface: ifaceOut
679
+ };
680
+ if (author && typeof author === "object") {
681
+ out.author = { ...author };
682
+ }
683
+ if (homepage !== void 0) {
684
+ out.homepage = homepage;
685
+ }
686
+ if (sourceUrl !== void 0) {
687
+ out.repository = sourceUrl;
688
+ }
689
+ if (license !== void 0) {
690
+ out.license = license;
691
+ }
692
+ if (keywords !== void 0) {
693
+ out.keywords = [...keywords];
694
+ }
695
+ if (hasMcpServers(content)) {
696
+ out.mcpServers = "./.mcp.json";
697
+ }
698
+ return out;
699
+ }
700
+ function extractSourceUrl(source) {
701
+ if (!source || typeof source === "string") return void 0;
702
+ const obj = source;
703
+ const url = obj.url;
704
+ if (typeof url !== "string" || url.length === 0) return void 0;
705
+ if (!/^https?:\/\//i.test(url)) return void 0;
706
+ return url.replace(/\.git$/, "");
707
+ }
708
+ function normalizeCategory(raw) {
709
+ if (raw === void 0) return void 0;
710
+ const lower = raw.toLowerCase().trim();
711
+ const map = {
712
+ development: "Engineering",
713
+ data: "Engineering",
714
+ devops: "Engineering",
715
+ design: "Design",
716
+ productivity: "Productivity",
717
+ security: "Security",
718
+ research: "Research",
719
+ automation: "Productivity",
720
+ integration: "Engineering"
721
+ };
722
+ if (lower in map) return map[lower];
723
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
724
+ }
725
+ async function writePluginMaterials(pluginDir, entry, content, marketplaceName) {
726
+ const manifestPath = codexPluginManifestPath(pluginDir);
727
+ await mkdir2(dirname2(manifestPath), { recursive: true });
728
+ await rm2(codexPluginMcpPath(pluginDir), { force: true }).catch(() => {
729
+ });
730
+ await rm2(codexPluginSkillsDir(pluginDir), {
731
+ recursive: true,
732
+ force: true
733
+ }).catch(() => {
734
+ });
735
+ const pluginJson = buildPluginJson(entry, content, marketplaceName);
736
+ await writeFileAtomic(
737
+ manifestPath,
738
+ `${JSON.stringify(pluginJson, null, 2)}
739
+ `
740
+ );
741
+ if (hasMcpServers(content)) {
742
+ const mcpPath = codexPluginMcpPath(pluginDir);
743
+ await writeFileAtomic(
744
+ mcpPath,
745
+ `${JSON.stringify({ mcpServers: content.mcpServers }, null, 2)}
746
+ `
747
+ );
748
+ }
749
+ if (content.skillsSourceDir && await isDir(content.skillsSourceDir)) {
750
+ const skillsDir = codexPluginSkillsDir(pluginDir);
751
+ await mkdir2(skillsDir, { recursive: true });
752
+ await cp(content.skillsSourceDir, skillsDir, {
753
+ recursive: true,
754
+ dereference: false,
755
+ preserveTimestamps: true,
756
+ verbatimSymlinks: true
757
+ });
758
+ } else if (Array.isArray(content.skillsPlaceholders) && content.skillsPlaceholders.length > 0) {
759
+ const installCmd = pickNonEmptyString(content.installCommand) ?? `npx rush-ai@latest plugin install ${entry.name}@${marketplaceName} --target codex`;
760
+ const pluginWebUrl = pickNonEmptyString(content.pluginWebUrl);
761
+ const skillsDir = codexPluginSkillsDir(pluginDir);
762
+ const usedDirs = /* @__PURE__ */ new Set();
763
+ for (const skill of content.skillsPlaceholders) {
764
+ if (!isSafeSkillName(skill.name)) continue;
765
+ const dirName = uniqueDirName(slugifySkillName(skill.name), usedDirs);
766
+ usedDirs.add(dirName);
767
+ const skillDir = resolve(skillsDir, dirName);
768
+ await mkdir2(skillDir, { recursive: true });
769
+ const rawSkillMd = pickNonEmptyString(skill.rawSkillMd);
770
+ if (rawSkillMd !== void 0) {
771
+ const rewritten = rewriteSkillMdName(rawSkillMd, dirName);
772
+ await writeFileAtomic(resolve(skillDir, "SKILL.md"), rewritten);
773
+ continue;
774
+ }
775
+ const desc = pickNonEmptyString(skill.description) ?? `Skill ${skill.name}`;
776
+ const fullDesc = skill.name === dirName ? desc : `${skill.name} \u2014 ${desc}`;
777
+ const skillUrl = pickNonEmptyString(skill.url);
778
+ const body = buildSkillPlaceholderBody({
779
+ name: skill.name,
780
+ description: fullDesc,
781
+ skillUrl,
782
+ pluginWebUrl,
783
+ installCmd
784
+ });
785
+ const md = `---
786
+ name: ${jsonQuote(dirName)}
787
+ description: ${jsonQuote(fullDesc)}
788
+ ---
789
+
790
+ ${body}
791
+ `;
792
+ await writeFileAtomic(resolve(skillDir, "SKILL.md"), md);
793
+ }
794
+ }
795
+ }
796
+ function rewriteSkillMdName(raw, dirName) {
797
+ const text = raw.replace(/\r\n/g, "\n");
798
+ if (!text.startsWith("---\n")) {
799
+ return `---
800
+ name: ${jsonQuote(dirName)}
801
+ description: ${jsonQuote(dirName)}
802
+ ---
803
+
804
+ ` + text;
805
+ }
806
+ const lines = text.split("\n");
807
+ let closeIdx = -1;
808
+ for (let i = 1; i < lines.length; i++) {
809
+ if (lines[i] === "---") {
810
+ closeIdx = i;
811
+ break;
812
+ }
813
+ }
814
+ if (closeIdx === -1) {
815
+ return `---
816
+ name: ${jsonQuote(dirName)}
817
+ description: ${jsonQuote(dirName)}
818
+ ---
819
+
820
+ ` + text;
821
+ }
822
+ let foundName = false;
823
+ for (let i = 1; i < closeIdx; i++) {
824
+ const line = lines[i] ?? "";
825
+ if (/^name\s*:/.test(line)) {
826
+ lines[i] = `name: ${jsonQuote(dirName)}`;
827
+ foundName = true;
828
+ break;
829
+ }
830
+ }
831
+ if (!foundName) {
832
+ lines.splice(1, 0, `name: ${jsonQuote(dirName)}`);
833
+ }
834
+ return lines.join("\n");
835
+ }
836
+ function buildSkillPlaceholderBody(input) {
837
+ const lines = [];
838
+ lines.push(`# ${input.name}`);
839
+ lines.push("");
840
+ lines.push(input.description);
841
+ lines.push("");
842
+ lines.push(
843
+ "> \u8FD9\u662F rush-ai marketplace \u540C\u6B65\u51FA\u7684\u5360\u4F4D skill \u2014\u2014 skill \u5B8C\u6574\u5185\u5BB9\uFF08trigger / \u64CD\u4F5C\u6B65\u9AA4 / \u63D0\u793A\u8BCD\uFF09\u5C1A\u672A\u4E0B\u8F7D\u3002"
844
+ );
845
+ lines.push(">");
846
+ lines.push("> \u5728\u7EC8\u7AEF\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\u4EE5\u5B8C\u6574\u5B89\u88C5\uFF1A");
847
+ lines.push(">");
848
+ lines.push("> ```bash");
849
+ lines.push(`> ${input.installCmd}`);
850
+ lines.push("> ```");
851
+ if (input.skillUrl !== void 0) {
852
+ lines.push(">");
853
+ lines.push(`> \u4E5F\u53EF\u5728 web \u7AD9\u70B9\u67E5\u770B\uFF1A[${input.name}](${input.skillUrl})`);
854
+ }
855
+ if (input.pluginWebUrl !== void 0) {
856
+ lines.push(">");
857
+ lines.push(`> \u6D4F\u89C8\u6240\u5C5E plugin\uFF1A${input.pluginWebUrl}`);
858
+ }
859
+ return lines.join("\n");
860
+ }
861
+ function jsonQuote(s) {
862
+ return JSON.stringify(s);
863
+ }
864
+ function slugifySkillName(name) {
865
+ return name.replace(/^@/, "").replace(/[\\/.]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "") || "skill";
866
+ }
867
+ function uniqueDirName(base, used) {
868
+ if (!used.has(base)) return base;
869
+ let i = 2;
870
+ while (used.has(`${base}-${i}`)) i += 1;
871
+ return `${base}-${i}`;
872
+ }
873
+ function isSafeSkillName(name) {
874
+ if (typeof name !== "string" || name.length === 0) return false;
875
+ if (name === "." || name === "..") return false;
876
+ if (name.startsWith(".")) return false;
877
+ if (name.includes("\\")) return false;
878
+ if (name.includes("..")) return false;
879
+ const slashCount = (name.match(/\//g) ?? []).length;
880
+ if (slashCount > 1) return false;
881
+ if (slashCount === 1 && !name.startsWith("@")) return false;
882
+ return /^@?[A-Za-z0-9_][A-Za-z0-9._@/-]*$/.test(name);
883
+ }
884
+ function hasMcpServers(content) {
885
+ return !!content.mcpServers && typeof content.mcpServers === "object" && Object.keys(content.mcpServers).length > 0;
886
+ }
887
+ function pickNonEmptyString(value) {
888
+ if (typeof value === "string" && value.length > 0) return value;
889
+ return void 0;
890
+ }
891
+ var STUB_MARKER_KEY = "x-rush-mirror";
892
+ var STUB_MARKER_VALUE = "stub";
893
+ async function detectFullPluginDir(pluginDir) {
894
+ if (!await isDir(pluginDir)) return "stub";
895
+ const manifestPath = codexPluginManifestPath(pluginDir);
896
+ if (await pathExists2(manifestPath)) {
897
+ try {
898
+ const raw = await readFile2(manifestPath, "utf8");
899
+ const parsed = JSON.parse(raw);
900
+ if (parsed[STUB_MARKER_KEY] === STUB_MARKER_VALUE) {
901
+ return "stub";
902
+ }
903
+ } catch {
904
+ }
905
+ }
906
+ if (await dirHasEntries(codexPluginSkillsDir(pluginDir))) {
907
+ return "full";
908
+ }
909
+ const mcpPath = codexPluginMcpPath(pluginDir);
910
+ if (await pathExists2(mcpPath)) {
911
+ try {
912
+ const raw = await readFile2(mcpPath, "utf8");
913
+ if (!/\$\{[^}]+\}/.test(raw)) {
914
+ return "full";
915
+ }
916
+ } catch {
917
+ }
918
+ }
919
+ return "stub";
920
+ }
921
+ async function dirHasEntries(dir) {
922
+ if (!await isDir(dir)) return false;
923
+ let entries;
924
+ try {
925
+ entries = await readdir(dir, { withFileTypes: true });
926
+ } catch {
927
+ return false;
928
+ }
929
+ return entries.length > 0;
930
+ }
931
+ function buildInterfaceCategory(manifest) {
932
+ return manifest.interface?.category ?? DEFAULT_CATEGORY;
933
+ }
934
+ async function writeFileAtomic(filePath, content) {
935
+ await mkdir2(dirname2(filePath), { recursive: true });
936
+ const tmp = `${filePath}.${randomUUID2()}.tmp`;
937
+ try {
938
+ await writeFile2(tmp, content, { encoding: "utf8", flag: "w" });
939
+ await rename2(tmp, filePath);
940
+ } catch (err) {
941
+ await rm2(tmp, { force: true }).catch(() => {
942
+ });
943
+ throw err;
944
+ }
945
+ }
946
+ async function pathExists2(p) {
947
+ try {
948
+ await access2(p, fsConstants2.F_OK);
949
+ return true;
950
+ } catch {
951
+ return false;
952
+ }
953
+ }
954
+ async function isDir(p) {
955
+ try {
956
+ const s = await stat2(p);
957
+ return s.isDirectory();
958
+ } catch {
959
+ return false;
960
+ }
961
+ }
962
+
963
+ export {
964
+ codexHomeDir,
965
+ codexConfigTomlPath,
966
+ codexMarketplaceDir,
967
+ codexMarketplaceManifestPath,
968
+ codexMarketplacePluginDir,
969
+ codexPluginVersionDir,
970
+ codexPluginManifestPath,
971
+ codexPluginMcpPath,
972
+ codexPluginSkillsDir,
973
+ codexConfigBackupPath,
974
+ pluginSectionKey,
975
+ readCodexConfig,
976
+ backupCodexConfig,
977
+ writeCodexConfig,
978
+ restoreCodexConfigFromBackup,
979
+ setMarketplaceSection,
980
+ removeMarketplaceSection,
981
+ setPluginEntry,
982
+ removePluginEntry,
983
+ setMcpServerSection,
984
+ removeMcpServerSection,
985
+ getMcpServerOwner,
986
+ syncCodexMarketplaceMirror,
987
+ writeCodexMarketplacePluginMirror,
988
+ upsertCodexCatalogPluginFull,
989
+ downgradeCodexCatalogEntryToStub,
990
+ readCodexMarketplaceCatalog
991
+ };
992
+ //# sourceMappingURL=chunk-RLKEUPBP.js.map