xxt-skills-client 0.1.1

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,1972 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import * as tar from 'tar';
8
+ const DEFAULT_PLUGINS = ['llmdoc', 'xxt-service-query', 'xxt-component', 'xxt-backend'];
9
+ const DEFAULT_CLIENTS = ['claude', 'codex'];
10
+ const DEFAULT_MCP_SERVERS = ['api-registry', 'code-search', 'dependency-graph', 'llmdoc-registry', 'service-discovery', 'component-registry'];
11
+ const DEFAULT_PROJECT_CODEX_MARKETPLACE = 'xxt-coder-project';
12
+ const DEFAULT_GLOBAL_CODEX_MARKETPLACE = 'xxt-coder-local';
13
+ class BootstrapError extends Error {
14
+ exitCode;
15
+ details;
16
+ constructor(message, exitCode = 1, details) {
17
+ super(message);
18
+ this.name = 'BootstrapError';
19
+ this.exitCode = exitCode;
20
+ this.details = details;
21
+ }
22
+ }
23
+ function printJson(value) {
24
+ console.log(JSON.stringify(value, null, 2));
25
+ }
26
+ function printHuman(value) {
27
+ console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
28
+ }
29
+ function requireYes(yes) {
30
+ if (yes)
31
+ return;
32
+ throw new BootstrapError([
33
+ '⚠️ 危险操作检测!',
34
+ '操作类型:客户端插件生命周期操作',
35
+ '影响范围:可能写入当前项目的 .claude/.codex/.agents,以及本机 Claude/Codex 配置目录和插件缓存',
36
+ '风险评估:install/update/remove 会创建、覆盖或删除 xxtcoder 受管插件目录;pause/resume 会修改插件启用状态。',
37
+ '',
38
+ '请确认是否继续?[需要明确的"是"、"确认"、"继续"]',
39
+ '',
40
+ '本 CLI 使用 --yes 作为确认开关;请在命令末尾添加 --yes。',
41
+ ].join('\n'), 2);
42
+ }
43
+ async function exists(absPath) {
44
+ try {
45
+ await fs.stat(absPath);
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ async function isDirectory(absPath) {
53
+ try {
54
+ const st = await fs.stat(absPath);
55
+ return st.isDirectory();
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ async function readJsonLoose(absPath) {
62
+ const text = await fs.readFile(absPath, 'utf8');
63
+ return JSON.parse(text.replace(/^\uFEFF/, ''));
64
+ }
65
+ async function safeReadJsonLoose(absPath) {
66
+ if (!(await exists(absPath)))
67
+ return null;
68
+ try {
69
+ return await readJsonLoose(absPath);
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ async function writeJsonIfChanged(absPath, value) {
76
+ const next = `${JSON.stringify(value, null, 2)}\n`;
77
+ if (await exists(absPath)) {
78
+ const prev = (await fs.readFile(absPath, 'utf8')).replace(/^\uFEFF/, '');
79
+ if (prev === next)
80
+ return false;
81
+ }
82
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
83
+ await fs.writeFile(absPath, next, 'utf8');
84
+ return true;
85
+ }
86
+ async function copyDir(srcAbs, dstAbs) {
87
+ await fs.mkdir(dstAbs, { recursive: true });
88
+ await fs.cp(srcAbs, dstAbs, { recursive: true, force: true });
89
+ }
90
+ async function removeIfExists(absPath) {
91
+ if (!(await exists(absPath)))
92
+ return false;
93
+ await fs.rm(absPath, { recursive: true, force: true });
94
+ return true;
95
+ }
96
+ async function moveDirIfExists(srcAbs, dstAbs, protectedRootAbs, label) {
97
+ assertInside(protectedRootAbs, srcAbs, `${label} source`);
98
+ assertInside(protectedRootAbs, dstAbs, `${label} target`);
99
+ if (!(await exists(srcAbs)))
100
+ return 'missing';
101
+ if (await exists(dstAbs))
102
+ return 'target-exists';
103
+ await fs.mkdir(path.dirname(dstAbs), { recursive: true });
104
+ await fs.rename(srcAbs, dstAbs);
105
+ return 'moved';
106
+ }
107
+ function assertInside(parentAbs, childAbs, label) {
108
+ const parent = path.resolve(parentAbs);
109
+ const child = path.resolve(childAbs);
110
+ const rel = path.relative(parent, child);
111
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
112
+ throw new BootstrapError(`${label} 不在允许目录内:${child}`, 3, { parent, child });
113
+ }
114
+ }
115
+ async function copyManagedDir(params) {
116
+ assertInside(params.protectedRootAbs, params.dstAbs, params.label);
117
+ const targetExists = await exists(params.dstAbs);
118
+ if (targetExists && !params.update) {
119
+ throw new BootstrapError(`${params.label} 已存在,新增模式不会覆盖:${params.dstAbs};如需更新请执行 xxtcoder plugin update。`, 2);
120
+ }
121
+ const removedExisting = params.update ? await removeIfExists(params.dstAbs) : false;
122
+ await copyDir(params.srcAbs, params.dstAbs);
123
+ return { copied: true, removedExisting };
124
+ }
125
+ function normalizeBaseUrl(input, label) {
126
+ const raw = String(input ?? '').trim().replace(/\/+$/, '');
127
+ if (!raw)
128
+ throw new BootstrapError(`缺少 ${label}`, 2);
129
+ try {
130
+ const url = new URL(raw);
131
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
132
+ throw new Error('protocol');
133
+ return url.toString().replace(/\/+$/, '');
134
+ }
135
+ catch {
136
+ throw new BootstrapError(`${label} 必须是 http(s) URL:${raw}`, 2);
137
+ }
138
+ }
139
+ function parseClients(raw, fallback = DEFAULT_CLIENTS) {
140
+ const text = String(raw ?? '').trim();
141
+ const values = text ? text.split(',') : fallback;
142
+ const out = [];
143
+ for (const item of values) {
144
+ const value = String(item).trim();
145
+ if (!value)
146
+ continue;
147
+ if (value !== 'claude' && value !== 'codex')
148
+ throw new BootstrapError(`非法 client:${value}`, 2);
149
+ if (!out.includes(value))
150
+ out.push(value);
151
+ }
152
+ if (!out.length)
153
+ throw new BootstrapError('clients 不能为空', 2);
154
+ return out;
155
+ }
156
+ function parsePlugins(raw, fallback = DEFAULT_PLUGINS) {
157
+ const text = String(raw ?? '').trim();
158
+ const values = text ? text.split(',') : fallback;
159
+ const out = [];
160
+ for (const item of values) {
161
+ const value = String(item).trim();
162
+ if (!value)
163
+ continue;
164
+ if (value !== 'llmdoc' && value !== 'xxt-service-query' && value !== 'xxt-component' && value !== 'xxt-backend') {
165
+ throw new BootstrapError(`非法 plugin:${value}`, 2);
166
+ }
167
+ if (!out.includes(value))
168
+ out.push(value);
169
+ }
170
+ if (!out.length)
171
+ throw new BootstrapError('plugins 不能为空', 2);
172
+ return out;
173
+ }
174
+ function parseProfile(raw) {
175
+ const profile = String(raw ?? 'remote').trim();
176
+ if (profile === 'remote' || profile === 'hybrid')
177
+ return profile;
178
+ if (profile === 'local') {
179
+ throw new BootstrapError('最小客户端 bootstrap 暂只支持 remote/hybrid profile;local profile 需要完整服务端 CLI。', 2);
180
+ }
181
+ throw new BootstrapError(`非法 profile:${profile}`, 2);
182
+ }
183
+ function pluginFolder(kind) {
184
+ if (kind === 'llmdoc')
185
+ return 'llmdoc-plugin';
186
+ if (kind === 'xxt-component')
187
+ return 'xxt-component-plugin';
188
+ if (kind === 'xxt-backend')
189
+ return 'xxt-backend-plugin';
190
+ return 'xxt-service-query-plugin';
191
+ }
192
+ function pluginName(kind) {
193
+ if (kind === 'llmdoc')
194
+ return 'llmdoc';
195
+ if (kind === 'xxt-component')
196
+ return 'xxt-component-plugin';
197
+ if (kind === 'xxt-backend')
198
+ return 'xxt-backend-plugin';
199
+ return 'xxt-service-query-plugin';
200
+ }
201
+ function marketplaceName(kind, platform) {
202
+ if (platform === 'claude') {
203
+ if (kind === 'llmdoc')
204
+ return 'llmdoc-cc-plugin';
205
+ if (kind === 'xxt-component')
206
+ return 'xxt-component-plugin-local';
207
+ if (kind === 'xxt-backend')
208
+ return 'xxt-backend-local';
209
+ return 'xxt-service-query-local';
210
+ }
211
+ if (kind === 'llmdoc')
212
+ return 'local-personal';
213
+ if (kind === 'xxt-component')
214
+ return 'xxt-component-local';
215
+ if (kind === 'xxt-backend')
216
+ return 'xxt-backend-local';
217
+ return 'xxt-service-query-local';
218
+ }
219
+ function bootstrapCodexMarketplaceName(scope) {
220
+ return scope === 'project' ? DEFAULT_PROJECT_CODEX_MARKETPLACE : DEFAULT_GLOBAL_CODEX_MARKETPLACE;
221
+ }
222
+ function bootstrapCodexMarketplaceDisplayName(scope) {
223
+ return scope === 'project' ? 'XXT Coder Project Plugins' : 'XXT Coder Local Plugins';
224
+ }
225
+ function slugifyName(value) {
226
+ const slug = value
227
+ .toLowerCase()
228
+ .replace(/[^a-z0-9]+/g, '-')
229
+ .replace(/^-+|-+$/g, '')
230
+ .slice(0, 32);
231
+ return slug || 'project';
232
+ }
233
+ function shortHash(value) {
234
+ return createHash('sha1').update(value).digest('hex').slice(0, 8);
235
+ }
236
+ function defaultProjectCodexMarketplaceName(projectDir) {
237
+ const name = slugifyName(path.basename(path.resolve(projectDir)));
238
+ const hash = shortHash(path.resolve(projectDir).toLowerCase());
239
+ return `xxt-coder-${name}-${hash}`;
240
+ }
241
+ function codexEnv(params) {
242
+ return {
243
+ ...process.env,
244
+ CODEX_HOME: params.codexDir,
245
+ AGENTS_HOME: params.agentsDir,
246
+ };
247
+ }
248
+ function commandLineForLog(command, args) {
249
+ return [command, ...args].map((item) => item.includes(' ') ? `"${item}"` : item).join(' ');
250
+ }
251
+ async function runProcessJson(params) {
252
+ return new Promise((resolve, reject) => {
253
+ const commandLine = commandLineForLog(params.command, params.args);
254
+ const child = process.platform === 'win32'
255
+ ? spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', params.command, ...params.args], {
256
+ cwd: params.cwd,
257
+ env: params.env,
258
+ windowsHide: true,
259
+ stdio: ['ignore', 'pipe', 'pipe'],
260
+ })
261
+ : spawn(params.command, params.args, {
262
+ cwd: params.cwd,
263
+ env: params.env,
264
+ windowsHide: true,
265
+ stdio: ['ignore', 'pipe', 'pipe'],
266
+ });
267
+ let stdout = '';
268
+ let stderr = '';
269
+ child.stdout.setEncoding('utf8');
270
+ child.stderr.setEncoding('utf8');
271
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
272
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
273
+ child.on('error', reject);
274
+ child.on('close', (status) => {
275
+ let parsed = null;
276
+ if (stdout.trim()) {
277
+ try {
278
+ parsed = JSON.parse(stdout);
279
+ }
280
+ catch {
281
+ parsed = null;
282
+ }
283
+ }
284
+ const ok = status === 0;
285
+ const result = {
286
+ ok,
287
+ status,
288
+ stdout,
289
+ stderr,
290
+ json: parsed,
291
+ commandLine,
292
+ };
293
+ if (!ok && !params.allowFailure) {
294
+ reject(new BootstrapError(`命令执行失败:${result.commandLine}\n${stderr || stdout}`, 3, result));
295
+ return;
296
+ }
297
+ resolve(result);
298
+ });
299
+ });
300
+ }
301
+ async function resolveCodexMarketplaceName(params) {
302
+ const data = await safeReadJsonLoose(params.marketplaceAbsPath);
303
+ const existingName = String(data?.name ?? '').trim();
304
+ const existingDisplayName = String(data?.interface?.displayName ?? '').trim();
305
+ const fallbackName = params.overrideName?.trim()
306
+ || (params.scope === 'project' && params.projectDir ? defaultProjectCodexMarketplaceName(params.projectDir) : bootstrapCodexMarketplaceName(params.scope));
307
+ return {
308
+ name: existingName || fallbackName,
309
+ displayName: existingDisplayName || bootstrapCodexMarketplaceDisplayName(params.scope),
310
+ };
311
+ }
312
+ async function ensureGitInfoExclude(projectRootAbs, relPaths) {
313
+ const gitDirAbs = path.join(projectRootAbs, '.git');
314
+ if (!(await isDirectory(gitDirAbs)))
315
+ return false;
316
+ const excludeAbs = path.join(gitDirAbs, 'info', 'exclude');
317
+ let text = (await exists(excludeAbs)) ? await fs.readFile(excludeAbs, 'utf8') : '';
318
+ const normalized = text.replace(/\r\n/g, '\n');
319
+ const lines = new Set(normalized.split('\n').map((line) => line.trim()).filter(Boolean));
320
+ let changed = false;
321
+ for (const rel of relPaths) {
322
+ const value = rel.replace(/\\/g, '/').replace(/^\/+/, '');
323
+ if (!lines.has(value)) {
324
+ text = `${text}${text.endsWith('\n') || !text ? '' : '\n'}${value}\n`;
325
+ lines.add(value);
326
+ changed = true;
327
+ }
328
+ }
329
+ if (!changed)
330
+ return false;
331
+ await fs.mkdir(path.dirname(excludeAbs), { recursive: true });
332
+ await fs.writeFile(excludeAbs, text, 'utf8');
333
+ return true;
334
+ }
335
+ async function resolveExistingAssetRoot(kind, platform) {
336
+ const here = path.dirname(fileURLToPath(import.meta.url));
337
+ const candidates = [
338
+ path.resolve(here, 'client', pluginFolder(kind), platform),
339
+ path.resolve(here, '..', 'client', pluginFolder(kind), platform),
340
+ path.resolve(here, '..', '..', '..', 'client', pluginFolder(kind), platform),
341
+ ];
342
+ for (const candidate of candidates) {
343
+ const marker = platform === 'claude'
344
+ ? path.join(candidate, '.claude-plugin', 'plugin.json')
345
+ : path.join(candidate, '.codex-plugin', 'plugin.json');
346
+ if (await exists(marker))
347
+ return candidate;
348
+ }
349
+ throw new BootstrapError(`未找到 ${kind} ${platform} 插件资产`, 3, { candidates });
350
+ }
351
+ function serverCacheKey(serverUrl) {
352
+ return createHash('sha1').update(serverUrl).digest('hex').slice(0, 12);
353
+ }
354
+ function safePathSegment(value) {
355
+ return String(value || 'unknown').replace(/[^A-Za-z0-9._-]/g, '_');
356
+ }
357
+ async function sha256File(absPath) {
358
+ const buf = await fs.readFile(absPath);
359
+ return createHash('sha256').update(buf).digest('hex');
360
+ }
361
+ function findManifestAsset(manifest, kind, platform) {
362
+ const bundles = Array.isArray(manifest.assetBundles) ? manifest.assetBundles : [];
363
+ const item = bundles.find((bundle) => String(bundle?.id ?? '').trim() === kind);
364
+ const asset = item?.assets?.[platform];
365
+ const url = String(asset?.url ?? '').trim();
366
+ if (!url)
367
+ return null;
368
+ return {
369
+ url,
370
+ sha256: String(asset?.sha256 ?? '').trim() || undefined,
371
+ version: String(item?.version ?? '').trim() || undefined,
372
+ };
373
+ }
374
+ async function validatePluginAssetRoot(rootAbs, platform, label) {
375
+ const marker = platform === 'claude'
376
+ ? path.join(rootAbs, '.claude-plugin', 'plugin.json')
377
+ : path.join(rootAbs, '.codex-plugin', 'plugin.json');
378
+ if (!(await exists(marker))) {
379
+ throw new BootstrapError(`${label} 缺少插件 manifest:${marker}`, 3);
380
+ }
381
+ }
382
+ async function downloadFile(url, targetAbs) {
383
+ const response = await fetch(url);
384
+ if (!response.ok) {
385
+ throw new BootstrapError(`下载插件资产失败:HTTP ${response.status} ${url}`, 3);
386
+ }
387
+ const data = Buffer.from(await response.arrayBuffer());
388
+ await fs.mkdir(path.dirname(targetAbs), { recursive: true });
389
+ await fs.writeFile(targetAbs, data);
390
+ }
391
+ async function resolveAssetRoot(params) {
392
+ const asset = findManifestAsset(params.manifest, params.kind, params.platform);
393
+ if (!asset) {
394
+ return resolveExistingAssetRoot(params.kind, params.platform);
395
+ }
396
+ const version = safePathSegment(asset.version || '0.0.0');
397
+ const rootAbs = path.join(os.homedir(), '.xxtcoder', 'client-assets', serverCacheKey(params.serverUrl), pluginFolder(params.kind), params.platform, version);
398
+ const marker = params.platform === 'claude'
399
+ ? path.join(rootAbs, '.claude-plugin', 'plugin.json')
400
+ : path.join(rootAbs, '.codex-plugin', 'plugin.json');
401
+ if (!params.update && await exists(marker))
402
+ return rootAbs;
403
+ if (params.dryRun && await exists(marker))
404
+ return rootAbs;
405
+ const tgzAbs = path.join(path.dirname(rootAbs), `${version}.tgz`);
406
+ if (params.update || !(await exists(tgzAbs))) {
407
+ await downloadFile(asset.url, tgzAbs);
408
+ }
409
+ if (asset.sha256) {
410
+ const actual = await sha256File(tgzAbs);
411
+ if (actual !== asset.sha256) {
412
+ await removeIfExists(tgzAbs);
413
+ throw new BootstrapError(`插件资产校验失败:${params.kind}/${params.platform}`, 3, {
414
+ expected: asset.sha256,
415
+ actual,
416
+ url: asset.url,
417
+ });
418
+ }
419
+ }
420
+ await removeIfExists(rootAbs);
421
+ await fs.mkdir(rootAbs, { recursive: true });
422
+ await tar.x({ file: tgzAbs, cwd: rootAbs });
423
+ await validatePluginAssetRoot(rootAbs, params.platform, `${params.kind} ${params.platform} 插件资产`);
424
+ return rootAbs;
425
+ }
426
+ async function pluginVersion(srcRootAbs, platform) {
427
+ const manifestAbs = path.join(srcRootAbs, platform === 'claude' ? '.claude-plugin' : '.codex-plugin', 'plugin.json');
428
+ const manifest = await readJsonLoose(manifestAbs);
429
+ return String(manifest?.version ?? '').trim() || '0.0.0';
430
+ }
431
+ function escapeToml(value) {
432
+ return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
433
+ }
434
+ function escapeRegExp(value) {
435
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
436
+ }
437
+ function upsertTomlSection(text, header, blockBody) {
438
+ const normalized = text.replace(/^\uFEFF/, '');
439
+ const headerRe = new RegExp(`^${escapeRegExp(header)}\\s*$`, 'm');
440
+ const match = headerRe.exec(normalized);
441
+ const block = `${header}\n${blockBody.trimEnd()}\n`;
442
+ if (!match) {
443
+ const prefix = normalized.trimEnd();
444
+ return `${prefix ? `${prefix}\n\n` : ''}${block}`;
445
+ }
446
+ const start = match.index;
447
+ const nextHeaderRel = normalized.slice(start + match[0].length).search(/\n\[/);
448
+ const end = nextHeaderRel >= 0 ? start + match[0].length + nextHeaderRel + 1 : normalized.length;
449
+ return `${normalized.slice(0, start)}${block}${normalized.slice(end)}`;
450
+ }
451
+ function tomlMcpServerSectionKeys(server) {
452
+ const bare = /^[A-Za-z0-9_-]+$/.test(server) ? `mcp_servers.${server}` : null;
453
+ const quoted = `mcp_servers."${escapeToml(server)}"`;
454
+ const key = bare ?? quoted;
455
+ return {
456
+ header: `[${key}]`,
457
+ key,
458
+ aliases: [key, ...(bare && bare !== quoted ? [quoted] : [])],
459
+ };
460
+ }
461
+ function removeTomlSections(text, predicate) {
462
+ const normalized = text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
463
+ const lines = normalized.split('\n');
464
+ const out = [];
465
+ let skip = false;
466
+ for (const line of lines) {
467
+ const match = /^\s*\[([^\]]+)\]\s*$/.exec(line);
468
+ if (match)
469
+ skip = predicate(match[1].trim());
470
+ if (!skip)
471
+ out.push(line);
472
+ }
473
+ return out.join('\n').trimEnd();
474
+ }
475
+ function upsertTomlMcpServer(text, server, blockBody) {
476
+ const section = tomlMcpServerSectionKeys(server);
477
+ const aliases = section.aliases;
478
+ const withoutOld = removeTomlSections(text, (key) => aliases.some((alias) => key === alias || key.startsWith(`${alias}.`)));
479
+ const block = `${section.header}\n${blockBody.trimEnd()}\n`;
480
+ return `${withoutOld ? `${withoutOld}\n\n` : ''}${block}`;
481
+ }
482
+ async function writeTextIfChanged(absPath, next) {
483
+ const prev = (await exists(absPath)) ? await fs.readFile(absPath, 'utf8') : '';
484
+ if (prev.replace(/^\uFEFF/, '') === next.replace(/^\uFEFF/, ''))
485
+ return false;
486
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
487
+ await fs.writeFile(absPath, next, 'utf8');
488
+ return true;
489
+ }
490
+ function codexPluginSectionKey(pluginKey) {
491
+ return `plugins."${escapeToml(pluginKey)}"`;
492
+ }
493
+ function codexMarketplaceSectionAliases(marketplaceName) {
494
+ const bare = /^[A-Za-z0-9_-]+$/.test(marketplaceName) ? `marketplaces.${marketplaceName}` : null;
495
+ const quoted = `marketplaces."${escapeToml(marketplaceName)}"`;
496
+ return [quoted, ...(bare && bare !== quoted ? [bare] : [])];
497
+ }
498
+ async function setCodexPluginEnabled(configAbsPath, pluginKey, enabled) {
499
+ let text = (await exists(configAbsPath)) ? await fs.readFile(configAbsPath, 'utf8') : '';
500
+ text = upsertTomlSection(text, `[${codexPluginSectionKey(pluginKey)}]`, `enabled = ${enabled ? 'true' : 'false'}`);
501
+ return writeTextIfChanged(configAbsPath, text);
502
+ }
503
+ async function removeCodexPluginSections(configAbsPath, pluginKeys) {
504
+ if (!(await exists(configAbsPath)))
505
+ return false;
506
+ const keys = new Set(pluginKeys.map(codexPluginSectionKey));
507
+ const text = await fs.readFile(configAbsPath, 'utf8');
508
+ const next = removeTomlSections(text, (key) => keys.has(key));
509
+ return writeTextIfChanged(configAbsPath, next ? `${next}\n` : '');
510
+ }
511
+ async function removeCodexMarketplaceSection(configAbsPath, marketplaceName) {
512
+ if (!(await exists(configAbsPath)))
513
+ return false;
514
+ const aliases = codexMarketplaceSectionAliases(marketplaceName);
515
+ const text = await fs.readFile(configAbsPath, 'utf8');
516
+ const next = removeTomlSections(text, (key) => aliases.includes(key));
517
+ return writeTextIfChanged(configAbsPath, next ? `${next}\n` : '');
518
+ }
519
+ async function removeCodexMarketplacePlugins(marketplaceAbsPath, pluginNames) {
520
+ const data = await safeReadJsonLoose(marketplaceAbsPath);
521
+ if (!data)
522
+ return false;
523
+ const names = new Set(pluginNames);
524
+ const plugins = Array.isArray(data?.plugins) ? data.plugins : [];
525
+ const nextPlugins = plugins.filter((item) => !names.has(String(item?.name ?? '')));
526
+ if (nextPlugins.length === plugins.length)
527
+ return false;
528
+ return writeJsonIfChanged(marketplaceAbsPath, { ...data, plugins: nextPlugins });
529
+ }
530
+ async function writeCodexConfig(params) {
531
+ await fs.mkdir(path.dirname(params.configAbsPath), { recursive: true });
532
+ let text = (await exists(params.configAbsPath)) ? await fs.readFile(params.configAbsPath, 'utf8') : '';
533
+ for (const kind of params.plugins) {
534
+ const key = `${pluginName(kind)}@${params.marketplaceName}`;
535
+ text = upsertTomlSection(text, `[plugins."${escapeToml(key)}"]`, 'enabled = true');
536
+ }
537
+ for (const server of params.mcpServers) {
538
+ const section = tomlMcpServerSectionKeys(server);
539
+ const urlLine = `url = "${escapeToml(`${params.remoteMcpUrl.replace(/\/+$/, '')}/mcp/${server}`)}"`;
540
+ const body = params.auth.mode === 'env'
541
+ ? [urlLine, `bearer_token_env_var = "${escapeToml(params.auth.tokenEnv)}"`].join('\n')
542
+ : [
543
+ urlLine,
544
+ '',
545
+ `[${section.key}.http_headers]`,
546
+ `Authorization = "Bearer ${escapeToml(params.auth.token)}"`,
547
+ ].join('\n');
548
+ text = upsertTomlMcpServer(text, server, body);
549
+ }
550
+ const prev = (await exists(params.configAbsPath)) ? await fs.readFile(params.configAbsPath, 'utf8') : '';
551
+ if (prev.replace(/^\uFEFF/, '') === text.replace(/^\uFEFF/, ''))
552
+ return false;
553
+ await fs.writeFile(params.configAbsPath, text, 'utf8');
554
+ return true;
555
+ }
556
+ function claudeMcpJson(params) {
557
+ const base = params.remoteMcpUrl.replace(/\/+$/, '');
558
+ const servers = {};
559
+ for (const server of params.mcpServers) {
560
+ servers[server] = {
561
+ type: 'http',
562
+ url: `${base}/mcp/${server}`,
563
+ headers: {
564
+ Authorization: `Bearer ${params.token}`,
565
+ },
566
+ };
567
+ }
568
+ return `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`;
569
+ }
570
+ function codexMcpJsonForAuth(params) {
571
+ const base = params.remoteMcpUrl.replace(/\/+$/, '');
572
+ const servers = {};
573
+ for (const server of params.mcpServers) {
574
+ servers[server] = params.auth.mode === 'env'
575
+ ? {
576
+ type: 'http',
577
+ url: `${base}/mcp/${server}`,
578
+ bearerTokenEnvVar: params.auth.tokenEnv,
579
+ }
580
+ : {
581
+ type: 'http',
582
+ url: `${base}/mcp/${server}`,
583
+ headers: {
584
+ Authorization: `Bearer ${params.auth.token}`,
585
+ },
586
+ };
587
+ }
588
+ return `${JSON.stringify(servers, null, 2)}\n`;
589
+ }
590
+ async function codexMarketplaceHasPlugin(marketplaceAbsPath, name) {
591
+ const data = await safeReadJsonLoose(marketplaceAbsPath);
592
+ const plugins = Array.isArray(data?.plugins) ? data.plugins : [];
593
+ return plugins.some((item) => String(item?.name ?? '') === name);
594
+ }
595
+ async function claudeSettingsHasPlugin(settingsAbsPath, pluginSpec) {
596
+ const data = await safeReadJsonLoose(settingsAbsPath);
597
+ return data?.enabledPlugins?.[pluginSpec] === true;
598
+ }
599
+ async function claudeInstalledHasPlugin(installedAbsPath, pluginSpec) {
600
+ const data = await safeReadJsonLoose(installedAbsPath);
601
+ const list = data?.plugins?.[pluginSpec];
602
+ return Array.isArray(list) && list.length > 0;
603
+ }
604
+ async function codexConfigHasPlugin(configAbsPath, pluginKey) {
605
+ if (!(await exists(configAbsPath)))
606
+ return false;
607
+ const text = (await fs.readFile(configAbsPath, 'utf8')).replace(/^\uFEFF/, '');
608
+ const header = `\\[plugins\\."${escapeRegExp(pluginKey)}"\\]`;
609
+ return new RegExp(`^${header}\\s*$`, 'm').test(text);
610
+ }
611
+ async function codexConfigPluginEnabled(configAbsPath, pluginKey) {
612
+ if (!(await exists(configAbsPath)))
613
+ return null;
614
+ const text = (await fs.readFile(configAbsPath, 'utf8')).replace(/^\uFEFF/, '');
615
+ const key = codexPluginSectionKey(pluginKey);
616
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
617
+ let inSection = false;
618
+ let seenSection = false;
619
+ for (const line of lines) {
620
+ const match = /^\s*\[([^\]]+)\]\s*$/.exec(line);
621
+ if (match) {
622
+ inSection = match[1].trim() === key;
623
+ if (inSection)
624
+ seenSection = true;
625
+ continue;
626
+ }
627
+ if (!inSection)
628
+ continue;
629
+ const enabledMatch = /^\s*enabled\s*=\s*(true|false)\s*$/i.exec(line);
630
+ if (enabledMatch)
631
+ return enabledMatch[1].toLowerCase() === 'true';
632
+ }
633
+ return seenSection ? null : null;
634
+ }
635
+ async function codexConfigHasMcpServer(configAbsPath, server) {
636
+ if (!(await exists(configAbsPath)))
637
+ return false;
638
+ const text = (await fs.readFile(configAbsPath, 'utf8')).replace(/^\uFEFF/, '');
639
+ return tomlMcpServerSectionKeys(server).aliases.some((key) => {
640
+ const header = `\\[${escapeRegExp(key)}\\]`;
641
+ return new RegExp(`^${header}\\s*$`, 'm').test(text);
642
+ });
643
+ }
644
+ async function assertNoCodexMcpConflicts(configAbsPath, servers, update) {
645
+ if (update)
646
+ return;
647
+ const conflicts = [];
648
+ for (const server of servers) {
649
+ if (await codexConfigHasMcpServer(configAbsPath, server))
650
+ conflicts.push(server);
651
+ }
652
+ if (conflicts.length) {
653
+ throw new BootstrapError(`Codex MCP 配置已存在,新增模式不会覆盖:${conflicts.join(', ')};如需更新请执行 xxtcoder plugin update。`, 2, { configAbsPath, conflicts });
654
+ }
655
+ }
656
+ async function assertNoCodexPluginConflict(params) {
657
+ if (params.update)
658
+ return;
659
+ const conflicts = [];
660
+ if (await exists(params.pluginDirAbs))
661
+ conflicts.push(params.pluginDirAbs);
662
+ if (params.pluginCacheParentAbs && await exists(params.pluginCacheParentAbs))
663
+ conflicts.push(params.pluginCacheParentAbs);
664
+ if (await codexMarketplaceHasPlugin(params.marketplaceAbsPath, params.pluginNameValue)) {
665
+ conflicts.push(`${params.marketplaceAbsPath}:${params.pluginNameValue}`);
666
+ }
667
+ if (await codexConfigHasPlugin(params.configAbsPath, params.pluginKey))
668
+ conflicts.push(`${params.configAbsPath}:${params.pluginKey}`);
669
+ if (conflicts.length) {
670
+ throw new BootstrapError(`Codex 插件已存在,新增模式不会覆盖:${params.pluginKey};如需更新请执行 xxtcoder plugin update。`, 2, { conflicts });
671
+ }
672
+ }
673
+ async function assertNoClaudePluginConflict(params) {
674
+ if (params.update)
675
+ return;
676
+ const conflicts = [];
677
+ if (params.pluginDirAbs && await exists(params.pluginDirAbs))
678
+ conflicts.push(params.pluginDirAbs);
679
+ if (params.pluginCacheParentAbs && await exists(params.pluginCacheParentAbs))
680
+ conflicts.push(params.pluginCacheParentAbs);
681
+ if (params.marketplaceDirAbs && await exists(params.marketplaceDirAbs))
682
+ conflicts.push(params.marketplaceDirAbs);
683
+ if (params.installedAbsPath && await claudeInstalledHasPlugin(params.installedAbsPath, params.pluginSpec)) {
684
+ conflicts.push(`${params.installedAbsPath}:${params.pluginSpec}`);
685
+ }
686
+ if (params.settingsAbsPath && await claudeSettingsHasPlugin(params.settingsAbsPath, params.pluginSpec)) {
687
+ conflicts.push(`${params.settingsAbsPath}:${params.pluginSpec}`);
688
+ }
689
+ if (conflicts.length) {
690
+ throw new BootstrapError(`Claude 插件已存在,新增模式不会覆盖:${params.pluginSpec};如需更新请执行 xxtcoder plugin update。`, 2, { conflicts });
691
+ }
692
+ }
693
+ async function installClaudePlugin(params) {
694
+ const srcRootAbs = params.sourceRootAbs;
695
+ const version = await pluginVersion(srcRootAbs, 'claude');
696
+ const name = pluginName(params.kind);
697
+ const marketplace = marketplaceName(params.kind, 'claude');
698
+ const spec = `${name}@${marketplace}`;
699
+ const cacheParentDir = path.join(params.claudeDir, 'plugins', 'cache', marketplace, name);
700
+ const cacheDir = path.join(cacheParentDir, version);
701
+ const marketplaceDir = path.join(params.claudeDir, 'plugins', 'marketplaces', marketplace);
702
+ const installedAbs = path.join(params.claudeDir, 'plugins', 'installed_plugins.json');
703
+ const knownAbs = path.join(params.claudeDir, 'plugins', 'known_marketplaces.json');
704
+ const settingsAbs = path.join(params.claudeDir, 'settings.json');
705
+ const wrote = {
706
+ pluginCacheCopied: false,
707
+ marketplaceCopied: false,
708
+ removedExisting: false,
709
+ mcpJsonWritten: false,
710
+ installedPluginsUpdated: false,
711
+ knownMarketplacesUpdated: false,
712
+ settingsUpdated: false,
713
+ };
714
+ if (!params.dryRun) {
715
+ await assertNoClaudePluginConflict({
716
+ pluginSpec: spec,
717
+ pluginCacheParentAbs: cacheParentDir,
718
+ marketplaceDirAbs: marketplaceDir,
719
+ installedAbsPath: installedAbs,
720
+ settingsAbsPath: settingsAbs,
721
+ update: params.update,
722
+ });
723
+ if (params.update) {
724
+ assertInside(params.claudeDir, cacheParentDir, 'Claude plugin cache parent');
725
+ wrote.removedExisting = (await removeIfExists(cacheParentDir)) || wrote.removedExisting;
726
+ }
727
+ const cacheCopy = await copyManagedDir({
728
+ srcAbs: srcRootAbs,
729
+ dstAbs: cacheDir,
730
+ update: params.update,
731
+ protectedRootAbs: params.claudeDir,
732
+ label: 'Claude plugin cache',
733
+ });
734
+ const marketplaceCopy = await copyManagedDir({
735
+ srcAbs: srcRootAbs,
736
+ dstAbs: marketplaceDir,
737
+ update: params.update,
738
+ protectedRootAbs: params.claudeDir,
739
+ label: 'Claude marketplace copy',
740
+ });
741
+ wrote.pluginCacheCopied = cacheCopy.copied;
742
+ wrote.marketplaceCopied = marketplaceCopy.copied;
743
+ wrote.removedExisting = wrote.removedExisting || cacheCopy.removedExisting || marketplaceCopy.removedExisting;
744
+ await fs.writeFile(path.join(cacheDir, '.mcp.json'), params.mcpJsonText, 'utf8');
745
+ await fs.writeFile(path.join(marketplaceDir, '.mcp.json'), params.mcpJsonText, 'utf8');
746
+ wrote.mcpJsonWritten = true;
747
+ const installed = (await exists(installedAbs)) ? await readJsonLoose(installedAbs) : { version: 2, plugins: {} };
748
+ const plugins = installed?.plugins && typeof installed.plugins === 'object' && !Array.isArray(installed.plugins) ? installed.plugins : {};
749
+ const now = new Date().toISOString();
750
+ const list = Array.isArray(plugins[spec]) ? plugins[spec] : [];
751
+ const prevUser = list.find((item) => String(item?.scope ?? 'user') === 'user');
752
+ const other = list.filter((item) => String(item?.scope ?? 'user') !== 'user');
753
+ plugins[spec] = [
754
+ ...other,
755
+ {
756
+ scope: 'user',
757
+ installPath: cacheDir,
758
+ version,
759
+ installedAt: String(prevUser?.installedAt ?? '').trim() || now,
760
+ lastUpdated: now,
761
+ },
762
+ ];
763
+ wrote.installedPluginsUpdated = await writeJsonIfChanged(installedAbs, { version: 2, plugins });
764
+ const known = (await exists(knownAbs)) ? await readJsonLoose(knownAbs) : {};
765
+ known[marketplace] = {
766
+ source: { source: 'directory', path: marketplaceDir },
767
+ installLocation: marketplaceDir,
768
+ lastUpdated: now,
769
+ };
770
+ wrote.knownMarketplacesUpdated = await writeJsonIfChanged(knownAbs, known);
771
+ const settings = (await exists(settingsAbs)) ? await readJsonLoose(settingsAbs) : {};
772
+ const enabledPlugins = settings?.enabledPlugins && typeof settings.enabledPlugins === 'object' ? { ...settings.enabledPlugins } : {};
773
+ enabledPlugins[spec] = true;
774
+ const extraKnownMarketplaces = settings?.extraKnownMarketplaces && typeof settings.extraKnownMarketplaces === 'object'
775
+ ? { ...settings.extraKnownMarketplaces }
776
+ : {};
777
+ extraKnownMarketplaces[marketplace] = { source: { source: 'directory', path: marketplaceDir } };
778
+ wrote.settingsUpdated = await writeJsonIfChanged(settingsAbs, { ...settings, extraKnownMarketplaces, enabledPlugins });
779
+ }
780
+ return { kind: params.kind, platform: 'claude', sourceRootAbs: srcRootAbs, version, target: cacheDir, wrote };
781
+ }
782
+ async function upsertCodexMarketplace(params) {
783
+ const data = (await exists(params.marketplaceAbsPath))
784
+ ? await readJsonLoose(params.marketplaceAbsPath)
785
+ : { name: params.marketplaceName, interface: { displayName: params.marketplaceDisplayName }, plugins: [] };
786
+ const plugins = Array.isArray(data?.plugins) ? data.plugins.slice() : [];
787
+ const entry = {
788
+ name: params.pluginName,
789
+ source: { source: 'local', path: params.pluginPathRel },
790
+ policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
791
+ category: 'Productivity',
792
+ };
793
+ const index = plugins.findIndex((item) => String(item?.name ?? '') === params.pluginName);
794
+ if (index >= 0)
795
+ plugins[index] = entry;
796
+ else
797
+ plugins.push(entry);
798
+ return writeJsonIfChanged(params.marketplaceAbsPath, {
799
+ ...data,
800
+ name: data?.name || params.marketplaceName,
801
+ interface: data?.interface || { displayName: params.marketplaceDisplayName },
802
+ plugins,
803
+ });
804
+ }
805
+ async function installCodexPlugin(params) {
806
+ const srcRootAbs = params.sourceRootAbs;
807
+ const version = await pluginVersion(srcRootAbs, 'codex');
808
+ const name = pluginName(params.kind);
809
+ const pluginDir = path.join(params.codexDir, 'plugins', name);
810
+ const cacheParentDir = path.join(params.codexDir, 'plugins', 'cache', params.marketplaceName, name);
811
+ const cacheDir = path.join(cacheParentDir, version);
812
+ const marketplaceAbs = path.join(params.agentsDir, 'plugins', 'marketplace.json');
813
+ const configAbs = path.join(params.codexDir, 'config.toml');
814
+ const pluginKey = `${name}@${params.marketplaceName}`;
815
+ const wrote = {
816
+ pluginFilesCopied: false,
817
+ pluginCacheCopied: false,
818
+ removedExisting: false,
819
+ mcpJsonWritten: false,
820
+ marketplaceUpdated: false,
821
+ };
822
+ if (!params.dryRun) {
823
+ await assertNoCodexPluginConflict({
824
+ pluginNameValue: name,
825
+ pluginKey,
826
+ pluginDirAbs: pluginDir,
827
+ pluginCacheParentAbs: cacheParentDir,
828
+ marketplaceAbsPath: marketplaceAbs,
829
+ configAbsPath: configAbs,
830
+ update: params.update,
831
+ });
832
+ if (params.update) {
833
+ assertInside(params.codexDir, cacheParentDir, 'Codex plugin cache parent');
834
+ wrote.removedExisting = (await removeIfExists(cacheParentDir)) || wrote.removedExisting;
835
+ }
836
+ const pluginCopy = await copyManagedDir({
837
+ srcAbs: srcRootAbs,
838
+ dstAbs: pluginDir,
839
+ update: params.update,
840
+ protectedRootAbs: params.codexDir,
841
+ label: 'Codex plugin working copy',
842
+ });
843
+ const cacheCopy = await copyManagedDir({
844
+ srcAbs: srcRootAbs,
845
+ dstAbs: cacheDir,
846
+ update: params.update,
847
+ protectedRootAbs: params.codexDir,
848
+ label: 'Codex plugin cache',
849
+ });
850
+ wrote.pluginFilesCopied = pluginCopy.copied;
851
+ wrote.pluginCacheCopied = cacheCopy.copied;
852
+ wrote.removedExisting = wrote.removedExisting || pluginCopy.removedExisting || cacheCopy.removedExisting;
853
+ await fs.writeFile(path.join(pluginDir, '.mcp.json'), params.mcpJsonText, 'utf8');
854
+ await fs.writeFile(path.join(cacheDir, '.mcp.json'), params.mcpJsonText, 'utf8');
855
+ wrote.mcpJsonWritten = true;
856
+ wrote.marketplaceUpdated = await upsertCodexMarketplace({
857
+ marketplaceAbsPath: marketplaceAbs,
858
+ marketplaceName: params.marketplaceName,
859
+ marketplaceDisplayName: params.marketplaceDisplayName,
860
+ pluginName: name,
861
+ pluginPathRel: `./.codex/plugins/${name}`,
862
+ });
863
+ }
864
+ return { kind: params.kind, platform: 'codex', sourceRootAbs: srcRootAbs, version, marketplace: params.marketplaceName, target: pluginDir, wrote };
865
+ }
866
+ async function installClaudeProjectPlugin(params) {
867
+ const srcRootAbs = params.sourceRootAbs;
868
+ const version = await pluginVersion(srcRootAbs, 'claude');
869
+ const name = pluginName(params.kind);
870
+ const targetDir = path.join(params.projectDir, '.claude', 'skills', name);
871
+ const pausedDir = path.join(params.projectDir, '.claude', '.xxtcoder-paused', 'skills', name);
872
+ const spec = `${name}@skills-dir`;
873
+ const mcpRel = path.join('.claude', 'skills', name, '.mcp.json');
874
+ const wrote = {
875
+ pluginFilesCopied: false,
876
+ removedExisting: false,
877
+ mcpJsonWritten: false,
878
+ gitInfoExcludeUpdated: false,
879
+ };
880
+ if (!params.dryRun) {
881
+ if (!params.update && await exists(pausedDir)) {
882
+ throw new BootstrapError(`Claude 插件已暂停存在,新增模式不会覆盖:${pausedDir};如需更新请执行 xxtcoder plugin update。`, 2);
883
+ }
884
+ if (params.update) {
885
+ assertInside(params.projectDir, pausedDir, 'Claude paused project plugin update');
886
+ wrote.removedExisting = (await removeIfExists(pausedDir)) || wrote.removedExisting;
887
+ }
888
+ await assertNoClaudePluginConflict({
889
+ pluginSpec: spec,
890
+ pluginDirAbs: targetDir,
891
+ update: params.update,
892
+ });
893
+ const copied = await copyManagedDir({
894
+ srcAbs: srcRootAbs,
895
+ dstAbs: targetDir,
896
+ update: params.update,
897
+ protectedRootAbs: params.projectDir,
898
+ label: 'Claude project plugin',
899
+ });
900
+ wrote.pluginFilesCopied = copied.copied;
901
+ wrote.removedExisting = copied.removedExisting;
902
+ await fs.writeFile(path.join(targetDir, '.mcp.json'), params.mcpJsonText, 'utf8');
903
+ wrote.mcpJsonWritten = true;
904
+ wrote.gitInfoExcludeUpdated = await ensureGitInfoExclude(params.projectDir, [mcpRel]);
905
+ }
906
+ return { kind: params.kind, platform: 'claude', scope: 'project', sourceRootAbs: srcRootAbs, version, target: targetDir, wrote };
907
+ }
908
+ async function installCodexProjectPlugin(params) {
909
+ const srcRootAbs = params.sourceRootAbs;
910
+ const version = await pluginVersion(srcRootAbs, 'codex');
911
+ const name = pluginName(params.kind);
912
+ const pluginDir = path.join(params.projectDir, '.codex', 'plugins', name);
913
+ const marketplaceAbs = path.join(params.projectDir, '.agents', 'plugins', 'marketplace.json');
914
+ const configAbs = path.join(params.projectDir, '.codex', 'config.toml');
915
+ const pluginKey = `${name}@${params.marketplaceName}`;
916
+ const mcpRel = path.join('.codex', 'plugins', name, '.mcp.json');
917
+ const wrote = {
918
+ pluginFilesCopied: false,
919
+ removedExisting: false,
920
+ mcpJsonWritten: false,
921
+ marketplaceUpdated: false,
922
+ gitInfoExcludeUpdated: false,
923
+ };
924
+ if (!params.dryRun) {
925
+ await assertNoCodexPluginConflict({
926
+ pluginNameValue: name,
927
+ pluginKey,
928
+ pluginDirAbs: pluginDir,
929
+ marketplaceAbsPath: marketplaceAbs,
930
+ configAbsPath: configAbs,
931
+ update: params.update,
932
+ });
933
+ const copied = await copyManagedDir({
934
+ srcAbs: srcRootAbs,
935
+ dstAbs: pluginDir,
936
+ update: params.update,
937
+ protectedRootAbs: params.projectDir,
938
+ label: 'Codex project plugin',
939
+ });
940
+ wrote.pluginFilesCopied = copied.copied;
941
+ wrote.removedExisting = copied.removedExisting;
942
+ await fs.writeFile(path.join(pluginDir, '.mcp.json'), params.mcpJsonText, 'utf8');
943
+ wrote.mcpJsonWritten = true;
944
+ wrote.marketplaceUpdated = await upsertCodexMarketplace({
945
+ marketplaceAbsPath: marketplaceAbs,
946
+ marketplaceName: params.marketplaceName,
947
+ marketplaceDisplayName: params.marketplaceDisplayName,
948
+ pluginName: name,
949
+ pluginPathRel: `./.codex/plugins/${name}`,
950
+ });
951
+ wrote.gitInfoExcludeUpdated = await ensureGitInfoExclude(params.projectDir, [mcpRel, '.codex/config.toml']);
952
+ }
953
+ return {
954
+ kind: params.kind,
955
+ platform: 'codex',
956
+ scope: 'project',
957
+ sourceRootAbs: srcRootAbs,
958
+ version,
959
+ marketplace: params.marketplaceName,
960
+ target: pluginDir,
961
+ wrote,
962
+ };
963
+ }
964
+ async function codexCliMarketplaceAdd(params) {
965
+ if (params.dryRun) {
966
+ return { skipped: true, commandLine: commandLineForLog('codex', ['plugin', 'marketplace', 'add', params.projectDir, '--json']) };
967
+ }
968
+ await fs.mkdir(params.codexDir, { recursive: true });
969
+ await fs.mkdir(params.agentsDir, { recursive: true });
970
+ const result = await runProcessJson({
971
+ command: 'codex',
972
+ args: ['plugin', 'marketplace', 'add', params.projectDir, '--json'],
973
+ cwd: params.projectDir,
974
+ env: codexEnv(params),
975
+ });
976
+ return { skipped: false, ...result };
977
+ }
978
+ async function codexCliPluginAdd(params) {
979
+ if (params.dryRun) {
980
+ return { skipped: true, commandLine: commandLineForLog('codex', ['plugin', 'add', params.pluginKey, '--json']) };
981
+ }
982
+ await fs.mkdir(params.codexDir, { recursive: true });
983
+ await fs.mkdir(params.agentsDir, { recursive: true });
984
+ const result = await runProcessJson({
985
+ command: 'codex',
986
+ args: ['plugin', 'add', params.pluginKey, '--json'],
987
+ cwd: params.projectDir,
988
+ env: codexEnv(params),
989
+ });
990
+ return { skipped: false, ...result };
991
+ }
992
+ async function codexCliPluginRemove(params) {
993
+ if (params.dryRun) {
994
+ return { skipped: true, commandLine: commandLineForLog('codex', ['plugin', 'remove', params.pluginKey, '--json']) };
995
+ }
996
+ await fs.mkdir(params.codexDir, { recursive: true });
997
+ await fs.mkdir(params.agentsDir, { recursive: true });
998
+ const result = await runProcessJson({
999
+ command: 'codex',
1000
+ args: ['plugin', 'remove', params.pluginKey, '--json'],
1001
+ cwd: params.projectDir,
1002
+ env: codexEnv(params),
1003
+ allowFailure: true,
1004
+ });
1005
+ return { skipped: false, ...result };
1006
+ }
1007
+ async function codexCliMarketplaceRemove(params) {
1008
+ if (params.dryRun) {
1009
+ return { skipped: true, commandLine: commandLineForLog('codex', ['plugin', 'marketplace', 'remove', params.marketplaceName, '--json']) };
1010
+ }
1011
+ await fs.mkdir(params.codexDir, { recursive: true });
1012
+ await fs.mkdir(params.agentsDir, { recursive: true });
1013
+ const result = await runProcessJson({
1014
+ command: 'codex',
1015
+ args: ['plugin', 'marketplace', 'remove', params.marketplaceName, '--json'],
1016
+ cwd: params.projectDir,
1017
+ env: codexEnv(params),
1018
+ allowFailure: true,
1019
+ });
1020
+ return { skipped: false, ...result };
1021
+ }
1022
+ async function codexCliPluginList(params) {
1023
+ await fs.mkdir(params.codexDir, { recursive: true });
1024
+ await fs.mkdir(params.agentsDir, { recursive: true });
1025
+ return runProcessJson({
1026
+ command: 'codex',
1027
+ args: ['plugin', 'list', '--available', '--json', '--marketplace', params.marketplaceName],
1028
+ cwd: params.projectDir,
1029
+ env: codexEnv(params),
1030
+ allowFailure: true,
1031
+ });
1032
+ }
1033
+ function codexListPluginStatus(data, pluginKey) {
1034
+ const installed = Array.isArray(data?.installed) ? data.installed : [];
1035
+ const available = Array.isArray(data?.available) ? data.available : [];
1036
+ const item = [...installed, ...available].find((entry) => String(entry?.pluginId ?? '') === pluginKey);
1037
+ if (!item)
1038
+ return { installed: false, enabled: false, found: false };
1039
+ return {
1040
+ installed: Boolean(item.installed),
1041
+ enabled: Boolean(item.enabled),
1042
+ found: true,
1043
+ };
1044
+ }
1045
+ async function activateCodexProjectPlugins(params) {
1046
+ const actions = [];
1047
+ actions.push({
1048
+ action: 'codex-marketplace-add',
1049
+ marketplace: params.marketplaceName,
1050
+ result: await codexCliMarketplaceAdd(params),
1051
+ });
1052
+ for (const kind of params.plugins) {
1053
+ const pluginKey = `${pluginName(kind)}@${params.marketplaceName}`;
1054
+ if (params.reinstall) {
1055
+ actions.push({
1056
+ action: 'codex-plugin-remove-before-add',
1057
+ pluginKey,
1058
+ result: await codexCliPluginRemove({ ...params, pluginKey }),
1059
+ });
1060
+ }
1061
+ actions.push({
1062
+ action: 'codex-plugin-add',
1063
+ pluginKey,
1064
+ result: await codexCliPluginAdd({ ...params, pluginKey }),
1065
+ });
1066
+ }
1067
+ if (!params.dryRun) {
1068
+ const list = await codexCliPluginList(params);
1069
+ for (const kind of params.plugins) {
1070
+ const pluginKey = `${pluginName(kind)}@${params.marketplaceName}`;
1071
+ const status = codexListPluginStatus(list.json, pluginKey);
1072
+ if (!status.installed || !status.enabled) {
1073
+ throw new BootstrapError(`Codex 插件安装后未处于 enabled 状态:${pluginKey}`, 3, { list, status });
1074
+ }
1075
+ }
1076
+ actions.push({ action: 'codex-plugin-list-verify', marketplace: params.marketplaceName, result: list });
1077
+ }
1078
+ return actions;
1079
+ }
1080
+ async function pluginKindsFromProjectMarketplace(projectDir) {
1081
+ const data = await safeReadJsonLoose(path.join(projectDir, '.agents', 'plugins', 'marketplace.json'));
1082
+ const plugins = Array.isArray(data?.plugins) ? data.plugins : [];
1083
+ const kinds = [];
1084
+ for (const item of plugins) {
1085
+ const name = String(item?.name ?? '').trim();
1086
+ const kind = DEFAULT_PLUGINS.find((candidate) => pluginName(candidate) === name);
1087
+ if (kind && !kinds.includes(kind))
1088
+ kinds.push(kind);
1089
+ }
1090
+ return kinds.length ? kinds : null;
1091
+ }
1092
+ async function resolveLifecyclePlugins(raw, projectDir, fallback = DEFAULT_PLUGINS) {
1093
+ const text = String(raw ?? '').trim();
1094
+ if (text)
1095
+ return parsePlugins(text, fallback);
1096
+ return (await pluginKindsFromProjectMarketplace(projectDir)) ?? fallback;
1097
+ }
1098
+ async function removeClaudeProjectPlugins(params) {
1099
+ const results = [];
1100
+ for (const kind of params.plugins) {
1101
+ const name = pluginName(kind);
1102
+ const target = path.join(params.projectDir, '.claude', 'skills', name);
1103
+ const paused = path.join(params.projectDir, '.claude', '.xxtcoder-paused', 'skills', name);
1104
+ assertInside(path.join(params.projectDir, '.claude', 'skills'), target, 'Claude project plugin remove');
1105
+ assertInside(path.join(params.projectDir, '.claude', '.xxtcoder-paused', 'skills'), paused, 'Claude paused project plugin remove');
1106
+ const removed = params.dryRun ? false : await removeIfExists(target);
1107
+ const pausedRemoved = params.dryRun ? false : await removeIfExists(paused);
1108
+ results.push({ platform: 'claude', action: 'remove-project-plugin', kind, target, paused, removed, pausedRemoved });
1109
+ }
1110
+ return results;
1111
+ }
1112
+ async function setClaudeProjectPluginsEnabled(params) {
1113
+ const results = [];
1114
+ for (const kind of params.plugins) {
1115
+ const name = pluginName(kind);
1116
+ const active = path.join(params.projectDir, '.claude', 'skills', name);
1117
+ const paused = path.join(params.projectDir, '.claude', '.xxtcoder-paused', 'skills', name);
1118
+ const result = params.dryRun
1119
+ ? 'dry-run'
1120
+ : params.enabled
1121
+ ? await moveDirIfExists(paused, active, params.projectDir, 'Claude project plugin resume')
1122
+ : await moveDirIfExists(active, paused, params.projectDir, 'Claude project plugin pause');
1123
+ results.push({
1124
+ platform: 'claude',
1125
+ action: params.enabled ? 'resume-project-plugin' : 'pause-project-plugin',
1126
+ kind,
1127
+ active,
1128
+ paused,
1129
+ result,
1130
+ });
1131
+ }
1132
+ return results;
1133
+ }
1134
+ async function removeCodexProjectPlugins(params) {
1135
+ const results = [];
1136
+ const pluginNames = params.plugins.map(pluginName);
1137
+ for (const kind of params.plugins) {
1138
+ const name = pluginName(kind);
1139
+ const pluginKey = `${name}@${params.marketplaceName}`;
1140
+ const target = path.join(params.projectDir, '.codex', 'plugins', name);
1141
+ assertInside(path.join(params.projectDir, '.codex', 'plugins'), target, 'Codex project plugin remove');
1142
+ const cliRemove = await codexCliPluginRemove({ ...params, pluginKey });
1143
+ const removed = params.dryRun ? false : await removeIfExists(target);
1144
+ results.push({ platform: 'codex', action: 'remove-project-plugin', kind, pluginKey, target, removed, cliRemove });
1145
+ }
1146
+ const marketplaceAbs = path.join(params.projectDir, '.agents', 'plugins', 'marketplace.json');
1147
+ const configAbs = path.join(params.projectDir, '.codex', 'config.toml');
1148
+ const marketplaceUpdated = params.dryRun ? false : await removeCodexMarketplacePlugins(marketplaceAbs, pluginNames);
1149
+ const projectConfigUpdated = params.dryRun ? false : await removeCodexPluginSections(configAbs, pluginNames.map((name) => `${name}@${params.marketplaceName}`));
1150
+ const userConfigAbs = path.join(params.codexDir, 'config.toml');
1151
+ const marketplaceRemove = await codexCliMarketplaceRemove(params);
1152
+ const userMarketplaceSectionRemoved = params.dryRun ? false : await removeCodexMarketplaceSection(userConfigAbs, params.marketplaceName);
1153
+ results.push({
1154
+ platform: 'codex',
1155
+ action: 'remove-project-marketplace',
1156
+ marketplace: params.marketplaceName,
1157
+ marketplaceAbs,
1158
+ marketplaceUpdated,
1159
+ projectConfigUpdated,
1160
+ userMarketplaceSectionRemoved,
1161
+ marketplaceRemove,
1162
+ });
1163
+ return results;
1164
+ }
1165
+ async function setCodexProjectPluginsEnabled(params) {
1166
+ const results = [];
1167
+ const userConfigAbs = path.join(params.codexDir, 'config.toml');
1168
+ const projectConfigAbs = path.join(params.projectDir, '.codex', 'config.toml');
1169
+ if (params.enabled && !params.dryRun) {
1170
+ const list = await codexCliPluginList(params);
1171
+ const missing = params.plugins.filter((kind) => {
1172
+ const pluginKey = `${pluginName(kind)}@${params.marketplaceName}`;
1173
+ return !codexListPluginStatus(list.json, pluginKey).installed;
1174
+ });
1175
+ if (missing.length) {
1176
+ results.push(...await activateCodexProjectPlugins({ ...params, plugins: missing }));
1177
+ }
1178
+ }
1179
+ for (const kind of params.plugins) {
1180
+ const pluginKey = `${pluginName(kind)}@${params.marketplaceName}`;
1181
+ const userConfigUpdated = params.dryRun ? false : await setCodexPluginEnabled(userConfigAbs, pluginKey, params.enabled);
1182
+ const projectConfigUpdated = params.dryRun ? false : await setCodexPluginEnabled(projectConfigAbs, pluginKey, params.enabled);
1183
+ results.push({
1184
+ platform: 'codex',
1185
+ action: params.enabled ? 'resume-plugin' : 'pause-plugin',
1186
+ kind,
1187
+ pluginKey,
1188
+ userConfigAbs,
1189
+ projectConfigAbs,
1190
+ userConfigUpdated,
1191
+ projectConfigUpdated,
1192
+ });
1193
+ }
1194
+ return results;
1195
+ }
1196
+ async function setCodexGlobalPluginsEnabled(params) {
1197
+ const results = [];
1198
+ const configAbs = path.join(params.codexDir, 'config.toml');
1199
+ for (const kind of params.plugins) {
1200
+ const pluginKey = `${pluginName(kind)}@${params.marketplaceName}`;
1201
+ const updated = params.dryRun ? false : await setCodexPluginEnabled(configAbs, pluginKey, params.enabled);
1202
+ results.push({
1203
+ platform: 'codex',
1204
+ action: params.enabled ? 'resume-plugin' : 'pause-plugin',
1205
+ scope: 'global',
1206
+ kind,
1207
+ pluginKey,
1208
+ configAbs,
1209
+ updated,
1210
+ });
1211
+ }
1212
+ return results;
1213
+ }
1214
+ async function setClaudeGlobalPluginsEnabled(params) {
1215
+ const settingsAbs = path.join(params.claudeDir, 'settings.json');
1216
+ const settings = (await exists(settingsAbs)) ? await readJsonLoose(settingsAbs) : {};
1217
+ const enabledPlugins = settings?.enabledPlugins && typeof settings.enabledPlugins === 'object' ? { ...settings.enabledPlugins } : {};
1218
+ const results = [];
1219
+ for (const kind of params.plugins) {
1220
+ const spec = `${pluginName(kind)}@${marketplaceName(kind, 'claude')}`;
1221
+ enabledPlugins[spec] = params.enabled;
1222
+ results.push({ platform: 'claude', action: params.enabled ? 'resume-plugin' : 'pause-plugin', scope: 'global', kind, spec, settingsAbs });
1223
+ }
1224
+ const updated = params.dryRun ? false : await writeJsonIfChanged(settingsAbs, { ...settings, enabledPlugins });
1225
+ return results.map((item) => ({ ...item, updated }));
1226
+ }
1227
+ async function removeClaudeGlobalPlugins(params) {
1228
+ const results = [];
1229
+ const installedAbs = path.join(params.claudeDir, 'plugins', 'installed_plugins.json');
1230
+ const knownAbs = path.join(params.claudeDir, 'plugins', 'known_marketplaces.json');
1231
+ const settingsAbs = path.join(params.claudeDir, 'settings.json');
1232
+ const installed = (await exists(installedAbs)) ? await readJsonLoose(installedAbs) : { version: 2, plugins: {} };
1233
+ const installedPlugins = installed?.plugins && typeof installed.plugins === 'object' && !Array.isArray(installed.plugins) ? { ...installed.plugins } : {};
1234
+ const known = (await exists(knownAbs)) ? await readJsonLoose(knownAbs) : {};
1235
+ const settings = (await exists(settingsAbs)) ? await readJsonLoose(settingsAbs) : {};
1236
+ const enabledPlugins = settings?.enabledPlugins && typeof settings.enabledPlugins === 'object' ? { ...settings.enabledPlugins } : {};
1237
+ const extraKnownMarketplaces = settings?.extraKnownMarketplaces && typeof settings.extraKnownMarketplaces === 'object'
1238
+ ? { ...settings.extraKnownMarketplaces }
1239
+ : {};
1240
+ for (const kind of params.plugins) {
1241
+ const name = pluginName(kind);
1242
+ const marketplace = marketplaceName(kind, 'claude');
1243
+ const spec = `${name}@${marketplace}`;
1244
+ const cacheParentDir = path.join(params.claudeDir, 'plugins', 'cache', marketplace, name);
1245
+ const marketplaceDir = path.join(params.claudeDir, 'plugins', 'marketplaces', marketplace);
1246
+ assertInside(params.claudeDir, cacheParentDir, 'Claude global plugin cache remove');
1247
+ assertInside(params.claudeDir, marketplaceDir, 'Claude global marketplace remove');
1248
+ const cacheRemoved = params.dryRun ? false : await removeIfExists(cacheParentDir);
1249
+ const marketplaceRemoved = params.dryRun ? false : await removeIfExists(marketplaceDir);
1250
+ delete installedPlugins[spec];
1251
+ delete known[marketplace];
1252
+ delete enabledPlugins[spec];
1253
+ delete extraKnownMarketplaces[marketplace];
1254
+ results.push({ platform: 'claude', action: 'remove-global-plugin', kind, spec, cacheParentDir, marketplaceDir, cacheRemoved, marketplaceRemoved });
1255
+ }
1256
+ const installedUpdated = params.dryRun ? false : await writeJsonIfChanged(installedAbs, { version: 2, plugins: installedPlugins });
1257
+ const knownUpdated = params.dryRun ? false : await writeJsonIfChanged(knownAbs, known);
1258
+ const settingsUpdated = params.dryRun ? false : await writeJsonIfChanged(settingsAbs, { ...settings, enabledPlugins, extraKnownMarketplaces });
1259
+ return results.map((item) => ({ ...item, installedUpdated, knownUpdated, settingsUpdated }));
1260
+ }
1261
+ async function removeCodexGlobalPlugins(params) {
1262
+ const results = [];
1263
+ const marketplaceAbs = path.join(params.agentsDir, 'plugins', 'marketplace.json');
1264
+ const configAbs = path.join(params.codexDir, 'config.toml');
1265
+ const pluginNames = params.plugins.map(pluginName);
1266
+ for (const kind of params.plugins) {
1267
+ const name = pluginName(kind);
1268
+ const pluginKey = `${name}@${params.marketplaceName}`;
1269
+ const pluginDir = path.join(params.codexDir, 'plugins', name);
1270
+ const cacheParentDir = path.join(params.codexDir, 'plugins', 'cache', params.marketplaceName, name);
1271
+ assertInside(params.codexDir, pluginDir, 'Codex global plugin remove');
1272
+ assertInside(params.codexDir, cacheParentDir, 'Codex global plugin cache remove');
1273
+ const cliRemove = await codexCliPluginRemove({ ...params, pluginKey });
1274
+ const pluginDirRemoved = params.dryRun ? false : await removeIfExists(pluginDir);
1275
+ const cacheRemoved = params.dryRun ? false : await removeIfExists(cacheParentDir);
1276
+ results.push({ platform: 'codex', action: 'remove-global-plugin', kind, pluginKey, pluginDir, cacheParentDir, pluginDirRemoved, cacheRemoved, cliRemove });
1277
+ }
1278
+ const marketplaceUpdated = params.dryRun ? false : await removeCodexMarketplacePlugins(marketplaceAbs, pluginNames);
1279
+ const configUpdated = params.dryRun ? false : await removeCodexPluginSections(configAbs, pluginNames.map((name) => `${name}@${params.marketplaceName}`));
1280
+ results.push({ platform: 'codex', action: 'remove-global-marketplace-entries', marketplaceAbs, configAbs, marketplaceUpdated, configUpdated });
1281
+ return results;
1282
+ }
1283
+ async function getProjectLifecycleStatus(params) {
1284
+ const codexList = params.clients.includes('codex') ? await codexCliPluginList(params) : null;
1285
+ const userConfigAbs = path.join(params.codexDir, 'config.toml');
1286
+ const projectConfigAbs = path.join(params.projectDir, '.codex', 'config.toml');
1287
+ const items = [];
1288
+ for (const kind of params.plugins) {
1289
+ const name = pluginName(kind);
1290
+ if (params.clients.includes('claude')) {
1291
+ const claudeDir = path.join(params.projectDir, '.claude', 'skills', name);
1292
+ const pausedDir = path.join(params.projectDir, '.claude', '.xxtcoder-paused', 'skills', name);
1293
+ items.push({
1294
+ platform: 'claude',
1295
+ kind,
1296
+ plugin: name,
1297
+ projectFilesPresent: await exists(path.join(claudeDir, '.claude-plugin', 'plugin.json')),
1298
+ pausedFilesPresent: await exists(path.join(pausedDir, '.claude-plugin', 'plugin.json')),
1299
+ mcpConfigPresent: await exists(path.join(claudeDir, '.mcp.json')),
1300
+ target: claudeDir,
1301
+ pausedTarget: pausedDir,
1302
+ });
1303
+ }
1304
+ if (params.clients.includes('codex')) {
1305
+ const pluginKey = `${name}@${params.marketplaceName}`;
1306
+ const codexDir = path.join(params.projectDir, '.codex', 'plugins', name);
1307
+ const cliStatus = codexList ? codexListPluginStatus(codexList.json, pluginKey) : { found: false, installed: false, enabled: false };
1308
+ items.push({
1309
+ platform: 'codex',
1310
+ kind,
1311
+ plugin: name,
1312
+ pluginKey,
1313
+ marketplace: params.marketplaceName,
1314
+ projectFilesPresent: await exists(path.join(codexDir, '.codex-plugin', 'plugin.json')),
1315
+ mcpConfigPresent: await exists(path.join(codexDir, '.mcp.json')),
1316
+ marketplaceEntryPresent: await codexMarketplaceHasPlugin(path.join(params.projectDir, '.agents', 'plugins', 'marketplace.json'), name),
1317
+ userConfigEnabled: await codexConfigPluginEnabled(userConfigAbs, pluginKey),
1318
+ projectConfigEnabled: await codexConfigPluginEnabled(projectConfigAbs, pluginKey),
1319
+ cli: cliStatus,
1320
+ target: codexDir,
1321
+ });
1322
+ }
1323
+ }
1324
+ return {
1325
+ projectDir: params.projectDir,
1326
+ codexDir: params.codexDir,
1327
+ agentsDir: params.agentsDir,
1328
+ marketplace: params.marketplaceName,
1329
+ items,
1330
+ };
1331
+ }
1332
+ async function getGlobalLifecycleStatus(params) {
1333
+ const codexList = params.clients.includes('codex') ? await codexCliPluginList({
1334
+ marketplaceName: params.marketplaceName,
1335
+ projectDir: params.projectDir,
1336
+ codexDir: params.codexDir,
1337
+ agentsDir: params.agentsDir,
1338
+ }) : null;
1339
+ const codexConfigAbs = path.join(params.codexDir, 'config.toml');
1340
+ const claudeSettingsAbs = path.join(params.claudeDir, 'settings.json');
1341
+ const claudeSettings = await safeReadJsonLoose(claudeSettingsAbs);
1342
+ const items = [];
1343
+ for (const kind of params.plugins) {
1344
+ const name = pluginName(kind);
1345
+ if (params.clients.includes('claude')) {
1346
+ const marketplace = marketplaceName(kind, 'claude');
1347
+ const spec = `${name}@${marketplace}`;
1348
+ const cacheParent = path.join(params.claudeDir, 'plugins', 'cache', marketplace, name);
1349
+ items.push({
1350
+ platform: 'claude',
1351
+ kind,
1352
+ plugin: name,
1353
+ pluginKey: spec,
1354
+ cachePresent: await exists(cacheParent),
1355
+ enabled: claudeSettings?.enabledPlugins?.[spec] === true,
1356
+ target: cacheParent,
1357
+ });
1358
+ }
1359
+ if (params.clients.includes('codex')) {
1360
+ const pluginKey = `${name}@${params.marketplaceName}`;
1361
+ const cacheParent = path.join(params.codexDir, 'plugins', 'cache', params.marketplaceName, name);
1362
+ items.push({
1363
+ platform: 'codex',
1364
+ kind,
1365
+ plugin: name,
1366
+ pluginKey,
1367
+ marketplace: params.marketplaceName,
1368
+ cachePresent: await exists(cacheParent),
1369
+ configEnabled: await codexConfigPluginEnabled(codexConfigAbs, pluginKey),
1370
+ cli: codexList ? codexListPluginStatus(codexList.json, pluginKey) : { found: false, installed: false, enabled: false },
1371
+ target: cacheParent,
1372
+ });
1373
+ }
1374
+ }
1375
+ return {
1376
+ claudeDir: params.claudeDir,
1377
+ codexDir: params.codexDir,
1378
+ agentsDir: params.agentsDir,
1379
+ marketplace: params.marketplaceName,
1380
+ items,
1381
+ };
1382
+ }
1383
+ async function preflightBootstrapWrites(params) {
1384
+ if (params.update)
1385
+ return;
1386
+ if (params.clients.includes('claude')) {
1387
+ for (const kind of params.plugins) {
1388
+ const name = pluginName(kind);
1389
+ if (params.scope === 'project') {
1390
+ await assertNoClaudePluginConflict({
1391
+ pluginSpec: `${name}@skills-dir`,
1392
+ pluginDirAbs: path.join(params.projectDir, '.claude', 'skills', name),
1393
+ update: false,
1394
+ });
1395
+ }
1396
+ else {
1397
+ const marketplace = marketplaceName(kind, 'claude');
1398
+ await assertNoClaudePluginConflict({
1399
+ pluginSpec: `${name}@${marketplace}`,
1400
+ pluginCacheParentAbs: path.join(params.claudeDir, 'plugins', 'cache', marketplace, name),
1401
+ marketplaceDirAbs: path.join(params.claudeDir, 'plugins', 'marketplaces', marketplace),
1402
+ installedAbsPath: path.join(params.claudeDir, 'plugins', 'installed_plugins.json'),
1403
+ settingsAbsPath: path.join(params.claudeDir, 'settings.json'),
1404
+ update: false,
1405
+ });
1406
+ }
1407
+ }
1408
+ }
1409
+ if (params.clients.includes('codex')) {
1410
+ const marketplaceAbs = params.scope === 'project'
1411
+ ? path.join(params.projectDir, '.agents', 'plugins', 'marketplace.json')
1412
+ : path.join(params.agentsDir, 'plugins', 'marketplace.json');
1413
+ const configAbs = params.scope === 'project'
1414
+ ? path.join(params.projectDir, '.codex', 'config.toml')
1415
+ : path.join(params.codexDir, 'config.toml');
1416
+ await assertNoCodexMcpConflicts(configAbs, params.mcpServers, false);
1417
+ for (const kind of params.plugins) {
1418
+ const name = pluginName(kind);
1419
+ const pluginKey = `${name}@${params.codexMarketplaceName}`;
1420
+ await assertNoCodexPluginConflict({
1421
+ pluginNameValue: name,
1422
+ pluginKey,
1423
+ pluginDirAbs: params.scope === 'project'
1424
+ ? path.join(params.projectDir, '.codex', 'plugins', name)
1425
+ : path.join(params.codexDir, 'plugins', name),
1426
+ pluginCacheParentAbs: params.scope === 'project'
1427
+ ? undefined
1428
+ : path.join(params.codexDir, 'plugins', 'cache', params.codexMarketplaceName, name),
1429
+ marketplaceAbsPath: marketplaceAbs,
1430
+ configAbsPath: configAbs,
1431
+ update: false,
1432
+ });
1433
+ }
1434
+ }
1435
+ }
1436
+ async function fetchManifest(serverUrl) {
1437
+ const response = await fetch(`${serverUrl}/v1/client-access/manifest`);
1438
+ const data = await response.json().catch(() => null);
1439
+ if (!response.ok || !data?.ok) {
1440
+ const message = data?.error?.message || `获取客户端接入 manifest 失败:HTTP ${response.status}`;
1441
+ throw new BootstrapError(message, 3, data?.error?.details);
1442
+ }
1443
+ return data.data;
1444
+ }
1445
+ async function fetchBootstrapConfig(serverUrl) {
1446
+ const response = await fetch(`${serverUrl}/v1/client-access/bootstrap-config`);
1447
+ const data = await response.json().catch(() => null);
1448
+ if (!response.ok || !data?.ok) {
1449
+ const message = data?.error?.message || `获取客户端 bootstrap 配置失败:HTTP ${response.status}`;
1450
+ throw new BootstrapError(message, 3, data?.error?.details);
1451
+ }
1452
+ return data.data;
1453
+ }
1454
+ async function checkRemoteMcp(remoteMcpUrl) {
1455
+ try {
1456
+ const [health, ready] = await Promise.all([
1457
+ fetch(`${remoteMcpUrl.replace(/\/+$/, '')}/health`).then((r) => r.ok).catch(() => false),
1458
+ fetch(`${remoteMcpUrl.replace(/\/+$/, '')}/ready`).then((r) => r.ok).catch(() => false),
1459
+ ]);
1460
+ return { health, ready, error: null };
1461
+ }
1462
+ catch (e) {
1463
+ return { health: false, ready: false, error: e instanceof Error ? e.message : String(e) };
1464
+ }
1465
+ }
1466
+ async function runBootstrap(options, globals) {
1467
+ const serverUrl = normalizeBaseUrl(options.serverUrl, '--server-url');
1468
+ const manifest = await fetchManifest(serverUrl);
1469
+ const manifestPlugins = parsePlugins(manifest.plugins, DEFAULT_PLUGINS);
1470
+ const manifestClients = parseClients(manifest.clients, DEFAULT_CLIENTS);
1471
+ const clients = parseClients(options.clients, manifestClients);
1472
+ const plugins = parsePlugins(options.plugins, manifestPlugins);
1473
+ const profile = parseProfile(options.profile || manifest.defaultProfile || 'remote');
1474
+ const scope = options.global ? 'global' : 'project';
1475
+ const update = Boolean(options.update);
1476
+ const projectDir = path.resolve(String(options.projectDir ?? process.cwd()));
1477
+ const tokenEnv = String(options.tokenEnv ?? '').trim();
1478
+ const remoteMcpUrl = normalizeBaseUrl(options.mcpUrl || manifest.remoteMcpUrl, '--mcp-url 或 manifest.remoteMcpUrl');
1479
+ const dryRun = Boolean(options.dryRun);
1480
+ const json = Boolean(options.json || globals?.json);
1481
+ if (!dryRun)
1482
+ requireYes(options.yes);
1483
+ let resolvedAuth;
1484
+ if (tokenEnv) {
1485
+ resolvedAuth = { mode: 'env', tokenEnv, token: String(process.env[tokenEnv] ?? '').trim() };
1486
+ }
1487
+ else if (dryRun) {
1488
+ resolvedAuth = { mode: 'inline', token: '<server-managed-token>' };
1489
+ }
1490
+ else {
1491
+ const bootstrapConfig = await fetchBootstrapConfig(serverUrl);
1492
+ const token = String(bootstrapConfig.token?.value ?? '').trim();
1493
+ if (!token)
1494
+ throw new BootstrapError('服务端未配置 Remote MCP Auth Token,无法执行隐藏 bootstrap。', 2);
1495
+ resolvedAuth = { mode: 'inline', token };
1496
+ }
1497
+ if (!dryRun && resolvedAuth.mode === 'env' && clients.includes('claude') && !resolvedAuth.token) {
1498
+ throw new BootstrapError(`环境变量 ${resolvedAuth.tokenEnv} 未设置,无法为 Claude .mcp.json 写入 Authorization header`, 2);
1499
+ }
1500
+ if (scope === 'project' && !(await isDirectory(projectDir))) {
1501
+ throw new BootstrapError(`--project-dir 不是有效目录:${projectDir}`, 2);
1502
+ }
1503
+ const claudeDir = path.resolve(String(options.claudeDir ?? process.env.CLAUDE_DIR ?? path.join(os.homedir(), '.claude')));
1504
+ const codexDir = path.resolve(String(options.codexDir ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex')));
1505
+ const agentsDir = path.resolve(String(options.agentsDir ?? process.env.AGENTS_HOME ?? path.join(os.homedir(), '.agents')));
1506
+ const mcpServers = Array.isArray(manifest.mcpServers) && manifest.mcpServers.length
1507
+ ? manifest.mcpServers.map((item) => String(item).trim()).filter(Boolean)
1508
+ : DEFAULT_MCP_SERVERS;
1509
+ const codexMarketplaceAbs = scope === 'project'
1510
+ ? path.join(projectDir, '.agents', 'plugins', 'marketplace.json')
1511
+ : path.join(agentsDir, 'plugins', 'marketplace.json');
1512
+ const codexMarketplace = await resolveCodexMarketplaceName({
1513
+ marketplaceAbsPath: codexMarketplaceAbs,
1514
+ scope,
1515
+ projectDir,
1516
+ overrideName: options.marketplace,
1517
+ });
1518
+ const plan = {
1519
+ serverUrl,
1520
+ remoteMcpUrl,
1521
+ profile,
1522
+ scope,
1523
+ update,
1524
+ authMode: resolvedAuth.mode,
1525
+ tokenEnv: resolvedAuth.mode === 'env' ? resolvedAuth.tokenEnv : null,
1526
+ clients,
1527
+ plugins,
1528
+ targets: {
1529
+ projectDir: scope === 'project' ? projectDir : null,
1530
+ claudeDir: scope === 'global' && clients.includes('claude') ? claudeDir : null,
1531
+ codexDir: scope === 'global' && clients.includes('codex') ? codexDir : null,
1532
+ agentsDir: scope === 'global' && clients.includes('codex') ? agentsDir : null,
1533
+ codexMarketplace: clients.includes('codex') ? codexMarketplaceAbs : null,
1534
+ },
1535
+ };
1536
+ const results = [];
1537
+ const claudeToken = resolvedAuth.mode === 'inline' ? resolvedAuth.token : (resolvedAuth.token || '<token-from-env>');
1538
+ const claudeMcp = claudeMcpJson({ remoteMcpUrl, token: claudeToken, mcpServers });
1539
+ const codexMcp = codexMcpJsonForAuth({ remoteMcpUrl, auth: resolvedAuth, mcpServers });
1540
+ if (!dryRun) {
1541
+ await preflightBootstrapWrites({
1542
+ scope,
1543
+ clients,
1544
+ plugins,
1545
+ update,
1546
+ projectDir,
1547
+ claudeDir,
1548
+ codexDir,
1549
+ agentsDir,
1550
+ codexMarketplaceName: codexMarketplace.name,
1551
+ mcpServers,
1552
+ });
1553
+ }
1554
+ const assetRoots = new Map();
1555
+ for (const kind of plugins) {
1556
+ for (const platform of clients) {
1557
+ const sourceRootAbs = await resolveAssetRoot({
1558
+ manifest,
1559
+ serverUrl,
1560
+ kind,
1561
+ platform,
1562
+ update,
1563
+ dryRun,
1564
+ });
1565
+ assetRoots.set(`${kind}:${platform}`, sourceRootAbs);
1566
+ }
1567
+ }
1568
+ const assetRootFor = (kind, platform) => {
1569
+ const value = assetRoots.get(`${kind}:${platform}`);
1570
+ if (!value)
1571
+ throw new BootstrapError(`插件资产未准备:${kind}/${platform}`, 3);
1572
+ return value;
1573
+ };
1574
+ if (clients.includes('claude')) {
1575
+ for (const kind of plugins) {
1576
+ results.push(scope === 'project'
1577
+ ? await installClaudeProjectPlugin({ kind, sourceRootAbs: assetRootFor(kind, 'claude'), projectDir, mcpJsonText: claudeMcp, dryRun, update })
1578
+ : await installClaudePlugin({ kind, sourceRootAbs: assetRootFor(kind, 'claude'), claudeDir, mcpJsonText: claudeMcp, dryRun, update }));
1579
+ }
1580
+ }
1581
+ if (clients.includes('codex')) {
1582
+ const codexConfigAbs = scope === 'project' ? path.join(projectDir, '.codex', 'config.toml') : path.join(codexDir, 'config.toml');
1583
+ for (const kind of plugins) {
1584
+ results.push(scope === 'project'
1585
+ ? await installCodexProjectPlugin({
1586
+ kind,
1587
+ sourceRootAbs: assetRootFor(kind, 'codex'),
1588
+ projectDir,
1589
+ marketplaceName: codexMarketplace.name,
1590
+ marketplaceDisplayName: codexMarketplace.displayName,
1591
+ mcpJsonText: codexMcp,
1592
+ dryRun,
1593
+ update,
1594
+ })
1595
+ : await installCodexPlugin({
1596
+ kind,
1597
+ sourceRootAbs: assetRootFor(kind, 'codex'),
1598
+ codexDir,
1599
+ agentsDir,
1600
+ marketplaceName: codexMarketplace.name,
1601
+ marketplaceDisplayName: codexMarketplace.displayName,
1602
+ mcpJsonText: codexMcp,
1603
+ dryRun,
1604
+ update,
1605
+ }));
1606
+ }
1607
+ if (!dryRun) {
1608
+ const updated = await writeCodexConfig({
1609
+ configAbsPath: codexConfigAbs,
1610
+ plugins,
1611
+ marketplaceName: codexMarketplace.name,
1612
+ remoteMcpUrl,
1613
+ auth: resolvedAuth,
1614
+ mcpServers,
1615
+ });
1616
+ const gitInfoExcludeUpdated = scope === 'project'
1617
+ ? await ensureGitInfoExclude(projectDir, ['.codex/config.toml'])
1618
+ : false;
1619
+ results.push({ platform: 'codex', kind: 'config', scope, marketplace: codexMarketplace.name, target: codexConfigAbs, wrote: { configUpdated: updated, gitInfoExcludeUpdated } });
1620
+ }
1621
+ else {
1622
+ results.push({ platform: 'codex', kind: 'config', scope, marketplace: codexMarketplace.name, target: codexConfigAbs, wrote: { configUpdated: false, gitInfoExcludeUpdated: false } });
1623
+ }
1624
+ if (scope === 'project') {
1625
+ results.push({
1626
+ platform: 'codex',
1627
+ kind: 'activation',
1628
+ scope,
1629
+ marketplace: codexMarketplace.name,
1630
+ actions: await activateCodexProjectPlugins({
1631
+ projectDir,
1632
+ codexDir,
1633
+ agentsDir,
1634
+ marketplaceName: codexMarketplace.name,
1635
+ plugins,
1636
+ dryRun,
1637
+ reinstall: update,
1638
+ }),
1639
+ });
1640
+ }
1641
+ }
1642
+ const remoteCheck = dryRun ? null : await checkRemoteMcp(remoteMcpUrl);
1643
+ const output = {
1644
+ ok: true,
1645
+ dryRun,
1646
+ manifest: {
1647
+ serverVersion: manifest.serverVersion ?? null,
1648
+ recommendedSuite: manifest.recommendedSuite ?? null,
1649
+ warnings: manifest.warnings ?? [],
1650
+ },
1651
+ plan,
1652
+ results,
1653
+ checks: {
1654
+ remoteMcp: remoteCheck,
1655
+ },
1656
+ };
1657
+ if (json)
1658
+ printJson(output);
1659
+ else
1660
+ printHuman(output);
1661
+ }
1662
+ async function resolveLifecycleContext(options, globals, requireServer) {
1663
+ const projectDir = path.resolve(String(options.projectDir ?? process.cwd()));
1664
+ const scope = options.global ? 'global' : 'project';
1665
+ const dryRun = Boolean(options.dryRun);
1666
+ const json = Boolean(options.json || globals?.json);
1667
+ const claudeDir = path.resolve(String(options.claudeDir ?? process.env.CLAUDE_DIR ?? path.join(os.homedir(), '.claude')));
1668
+ const codexDir = path.resolve(String(options.codexDir ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex')));
1669
+ const agentsDir = path.resolve(String(options.agentsDir ?? process.env.AGENTS_HOME ?? path.join(os.homedir(), '.agents')));
1670
+ if (scope === 'project' && !(await isDirectory(projectDir))) {
1671
+ throw new BootstrapError(`--project-dir 不是有效目录:${projectDir}`, 2);
1672
+ }
1673
+ let manifest = null;
1674
+ let serverUrl = null;
1675
+ if (requireServer) {
1676
+ serverUrl = normalizeBaseUrl(options.serverUrl, '--server-url');
1677
+ manifest = await fetchManifest(serverUrl);
1678
+ }
1679
+ const manifestClients = manifest ? parseClients(manifest.clients, DEFAULT_CLIENTS) : DEFAULT_CLIENTS;
1680
+ const clients = parseClients(options.clients, manifestClients);
1681
+ const manifestPlugins = manifest ? parsePlugins(manifest.plugins, DEFAULT_PLUGINS) : DEFAULT_PLUGINS;
1682
+ const plugins = requireServer
1683
+ ? parsePlugins(options.plugins, manifestPlugins)
1684
+ : await resolveLifecyclePlugins(options.plugins, projectDir, manifestPlugins);
1685
+ const codexMarketplaceAbs = scope === 'project'
1686
+ ? path.join(projectDir, '.agents', 'plugins', 'marketplace.json')
1687
+ : path.join(agentsDir, 'plugins', 'marketplace.json');
1688
+ const codexMarketplace = await resolveCodexMarketplaceName({
1689
+ marketplaceAbsPath: codexMarketplaceAbs,
1690
+ scope,
1691
+ projectDir,
1692
+ overrideName: options.marketplace,
1693
+ });
1694
+ return {
1695
+ projectDir,
1696
+ scope,
1697
+ dryRun,
1698
+ json,
1699
+ claudeDir,
1700
+ codexDir,
1701
+ agentsDir,
1702
+ clients,
1703
+ plugins,
1704
+ codexMarketplace,
1705
+ codexMarketplaceAbs,
1706
+ manifest,
1707
+ serverUrl,
1708
+ };
1709
+ }
1710
+ async function runPluginInstall(options, globals, update) {
1711
+ const merged = { ...options, update };
1712
+ await runBootstrap(merged, globals);
1713
+ }
1714
+ async function runPluginStatus(options, globals) {
1715
+ const ctx = await resolveLifecycleContext(options, globals, false);
1716
+ const status = ctx.scope === 'project'
1717
+ ? await getProjectLifecycleStatus({
1718
+ projectDir: ctx.projectDir,
1719
+ codexDir: ctx.codexDir,
1720
+ agentsDir: ctx.agentsDir,
1721
+ marketplaceName: ctx.codexMarketplace.name,
1722
+ plugins: ctx.plugins,
1723
+ clients: ctx.clients,
1724
+ })
1725
+ : await getGlobalLifecycleStatus({
1726
+ claudeDir: ctx.claudeDir,
1727
+ codexDir: ctx.codexDir,
1728
+ agentsDir: ctx.agentsDir,
1729
+ projectDir: ctx.projectDir,
1730
+ marketplaceName: ctx.codexMarketplace.name,
1731
+ plugins: ctx.plugins,
1732
+ clients: ctx.clients,
1733
+ });
1734
+ const output = { ok: true, action: 'status', scope: ctx.scope, status };
1735
+ if (ctx.json)
1736
+ printJson(output);
1737
+ else
1738
+ printHuman(output);
1739
+ }
1740
+ async function runPluginPauseResume(options, globals, enabled) {
1741
+ const ctx = await resolveLifecycleContext(options, globals, false);
1742
+ if (!ctx.dryRun)
1743
+ requireYes(options.yes);
1744
+ const results = [];
1745
+ if (ctx.scope === 'project') {
1746
+ if (ctx.clients.includes('claude')) {
1747
+ results.push(...await setClaudeProjectPluginsEnabled({
1748
+ projectDir: ctx.projectDir,
1749
+ plugins: ctx.plugins,
1750
+ enabled,
1751
+ dryRun: ctx.dryRun,
1752
+ }));
1753
+ }
1754
+ if (ctx.clients.includes('codex')) {
1755
+ results.push(...await setCodexProjectPluginsEnabled({
1756
+ projectDir: ctx.projectDir,
1757
+ codexDir: ctx.codexDir,
1758
+ agentsDir: ctx.agentsDir,
1759
+ marketplaceName: ctx.codexMarketplace.name,
1760
+ plugins: ctx.plugins,
1761
+ enabled,
1762
+ dryRun: ctx.dryRun,
1763
+ }));
1764
+ }
1765
+ }
1766
+ else {
1767
+ if (ctx.clients.includes('claude')) {
1768
+ results.push(...await setClaudeGlobalPluginsEnabled({
1769
+ claudeDir: ctx.claudeDir,
1770
+ plugins: ctx.plugins,
1771
+ enabled,
1772
+ dryRun: ctx.dryRun,
1773
+ }));
1774
+ }
1775
+ if (ctx.clients.includes('codex')) {
1776
+ results.push(...await setCodexGlobalPluginsEnabled({
1777
+ codexDir: ctx.codexDir,
1778
+ marketplaceName: ctx.codexMarketplace.name,
1779
+ plugins: ctx.plugins,
1780
+ enabled,
1781
+ dryRun: ctx.dryRun,
1782
+ }));
1783
+ }
1784
+ }
1785
+ const output = { ok: true, action: enabled ? 'resume' : 'pause', dryRun: ctx.dryRun, scope: ctx.scope, marketplace: ctx.codexMarketplace.name, results };
1786
+ if (ctx.json)
1787
+ printJson(output);
1788
+ else
1789
+ printHuman(output);
1790
+ }
1791
+ async function runPluginRemove(options, globals) {
1792
+ const ctx = await resolveLifecycleContext(options, globals, false);
1793
+ if (!ctx.dryRun)
1794
+ requireYes(options.yes);
1795
+ const results = [];
1796
+ if (ctx.scope === 'project') {
1797
+ if (ctx.clients.includes('claude')) {
1798
+ results.push(...await removeClaudeProjectPlugins({
1799
+ projectDir: ctx.projectDir,
1800
+ plugins: ctx.plugins,
1801
+ dryRun: ctx.dryRun,
1802
+ }));
1803
+ }
1804
+ if (ctx.clients.includes('codex')) {
1805
+ results.push(...await removeCodexProjectPlugins({
1806
+ projectDir: ctx.projectDir,
1807
+ codexDir: ctx.codexDir,
1808
+ agentsDir: ctx.agentsDir,
1809
+ marketplaceName: ctx.codexMarketplace.name,
1810
+ plugins: ctx.plugins,
1811
+ dryRun: ctx.dryRun,
1812
+ }));
1813
+ }
1814
+ }
1815
+ else {
1816
+ if (ctx.clients.includes('claude')) {
1817
+ results.push(...await removeClaudeGlobalPlugins({
1818
+ claudeDir: ctx.claudeDir,
1819
+ plugins: ctx.plugins,
1820
+ dryRun: ctx.dryRun,
1821
+ }));
1822
+ }
1823
+ if (ctx.clients.includes('codex')) {
1824
+ results.push(...await removeCodexGlobalPlugins({
1825
+ projectDir: ctx.projectDir,
1826
+ codexDir: ctx.codexDir,
1827
+ agentsDir: ctx.agentsDir,
1828
+ marketplaceName: ctx.codexMarketplace.name,
1829
+ plugins: ctx.plugins,
1830
+ dryRun: ctx.dryRun,
1831
+ }));
1832
+ }
1833
+ }
1834
+ const output = { ok: true, action: 'remove', dryRun: ctx.dryRun, scope: ctx.scope, marketplace: ctx.codexMarketplace.name, results };
1835
+ if (ctx.json)
1836
+ printJson(output);
1837
+ else
1838
+ printHuman(output);
1839
+ }
1840
+ function addInstallOptions(command) {
1841
+ return command
1842
+ .requiredOption('--server-url <url>', 'XXT Coder WebUI 地址,例如 http://xxt-coder:8100')
1843
+ .option('--clients <list>', '客户端列表:claude,codex(默认使用服务端推荐)')
1844
+ .option('--profile <remote|hybrid>', 'MCP profile(默认 remote)')
1845
+ .option('--token-env <name>', '高级用法:改为让 Codex 运行时从环境变量读取 Remote MCP token')
1846
+ .option('--mcp-url <url>', '覆盖服务端 manifest 中的 Remote MCP URL')
1847
+ .option('--plugins <list>', '插件列表:llmdoc,xxt-service-query,xxt-component,xxt-backend')
1848
+ .option('--project-dir <path>', '项目级安装目录(默认当前执行目录)')
1849
+ .option('-g, --global', '安装到用户级 Claude/Codex 配置目录(默认不写全局)', false)
1850
+ .option('--marketplace <name>', '覆盖 Codex marketplace 名称(默认项目级自动生成稳定名称)')
1851
+ .option('--claude-dir <path>', '全局模式 Claude 配置目录(默认 ~/.claude;可指向临时目录验证)')
1852
+ .option('--codex-dir <path>', 'Codex 配置目录(默认 ~/.codex;可指向临时目录验证)')
1853
+ .option('--agents-dir <path>', 'Agents 配置目录(默认 ~/.agents;可指向临时目录验证)')
1854
+ .option('--dry-run', '只输出计划,不写入文件', false)
1855
+ .option('--yes', '确认写入本机客户端配置', false)
1856
+ .option('--json', '输出 JSON', false);
1857
+ }
1858
+ function addLifecycleOptions(command) {
1859
+ return command
1860
+ .option('--clients <list>', '客户端列表:claude,codex(默认 claude,codex)')
1861
+ .option('--plugins <list>', '插件列表:llmdoc,xxt-service-query,xxt-component,xxt-backend;默认从项目 marketplace 识别,识别不到则使用全部')
1862
+ .option('--project-dir <path>', '项目级安装目录(默认当前执行目录)')
1863
+ .option('-g, --global', '操作用户级 Claude/Codex 配置目录(默认项目级)', false)
1864
+ .option('--marketplace <name>', '覆盖 Codex marketplace 名称')
1865
+ .option('--claude-dir <path>', '全局模式 Claude 配置目录(默认 ~/.claude;可指向临时目录验证)')
1866
+ .option('--codex-dir <path>', 'Codex 配置目录(默认 ~/.codex;可指向临时目录验证)')
1867
+ .option('--agents-dir <path>', 'Agents 配置目录(默认 ~/.agents;可指向临时目录验证)')
1868
+ .option('--dry-run', '只输出计划,不写入文件', false)
1869
+ .option('--yes', '确认写入本机客户端配置', false)
1870
+ .option('--json', '输出 JSON', false);
1871
+ }
1872
+ async function runCommandWithJsonError(options, globals, fn) {
1873
+ try {
1874
+ await fn();
1875
+ }
1876
+ catch (e) {
1877
+ if (options.json || globals.json) {
1878
+ printJson({
1879
+ ok: false,
1880
+ error: {
1881
+ message: e instanceof Error ? e.message : String(e),
1882
+ details: e instanceof BootstrapError ? e.details : undefined,
1883
+ },
1884
+ });
1885
+ }
1886
+ throw e;
1887
+ }
1888
+ }
1889
+ export function registerBootstrapCommands(program) {
1890
+ const plugin = program
1891
+ .command('plugin')
1892
+ .description('管理 XXT Coder 客户端插件生命周期');
1893
+ addInstallOptions(plugin
1894
+ .command('install')
1895
+ .description('安装并启用 Claude/Codex 插件和 Remote MCP 配置'))
1896
+ .action(async function (options) {
1897
+ const globals = this.optsWithGlobals?.() ?? {};
1898
+ await runCommandWithJsonError(options, globals, () => runPluginInstall(options, globals, false));
1899
+ });
1900
+ addInstallOptions(plugin
1901
+ .command('update')
1902
+ .description('更新并重新启用 Claude/Codex 插件和 Remote MCP 配置'))
1903
+ .action(async function (options) {
1904
+ const globals = this.optsWithGlobals?.() ?? {};
1905
+ await runCommandWithJsonError(options, globals, () => runPluginInstall(options, globals, true));
1906
+ });
1907
+ addLifecycleOptions(plugin
1908
+ .command('status')
1909
+ .description('查看 Claude/Codex 插件真实安装与启用状态'))
1910
+ .action(async function (options) {
1911
+ const globals = this.optsWithGlobals?.() ?? {};
1912
+ await runCommandWithJsonError(options, globals, () => runPluginStatus(options, globals));
1913
+ });
1914
+ addLifecycleOptions(plugin
1915
+ .command('pause')
1916
+ .description('暂停 Claude/Codex 插件,不删除受管插件文件'))
1917
+ .action(async function (options) {
1918
+ const globals = this.optsWithGlobals?.() ?? {};
1919
+ await runCommandWithJsonError(options, globals, () => runPluginPauseResume(options, globals, false));
1920
+ });
1921
+ addLifecycleOptions(plugin
1922
+ .command('resume')
1923
+ .description('恢复 Claude/Codex 插件;Codex cache 缺失时会重新安装'))
1924
+ .action(async function (options) {
1925
+ const globals = this.optsWithGlobals?.() ?? {};
1926
+ await runCommandWithJsonError(options, globals, () => runPluginPauseResume(options, globals, true));
1927
+ });
1928
+ addLifecycleOptions(plugin
1929
+ .command('remove')
1930
+ .description('移除 Claude/Codex 插件注册、缓存和 xxtcoder 受管项目文件'))
1931
+ .action(async function (options) {
1932
+ const globals = this.optsWithGlobals?.() ?? {};
1933
+ await runCommandWithJsonError(options, globals, () => runPluginRemove(options, globals));
1934
+ });
1935
+ program
1936
+ .command('bootstrap')
1937
+ .description('兼容入口:等价于 xxtcoder plugin install')
1938
+ .requiredOption('--server-url <url>', 'XXT Coder WebUI 地址,例如 http://xxt-coder:8100')
1939
+ .option('--clients <list>', '客户端列表:claude,codex(默认使用服务端推荐)')
1940
+ .option('--profile <remote|hybrid>', 'MCP profile(默认 remote)')
1941
+ .option('--token-env <name>', '高级用法:改为让 Codex 运行时从环境变量读取 Remote MCP token')
1942
+ .option('--mcp-url <url>', '覆盖服务端 manifest 中的 Remote MCP URL')
1943
+ .option('--plugins <list>', '插件列表:llmdoc,xxt-service-query,xxt-component,xxt-backend')
1944
+ .option('--project-dir <path>', '项目级安装目录(默认当前执行目录)')
1945
+ .option('-g, --global', '安装到用户级 Claude/Codex 配置目录(默认不写全局)', false)
1946
+ .option('--marketplace <name>', '覆盖 Codex marketplace 名称(默认项目级自动生成稳定名称)')
1947
+ .option('--update', '更新已有安装;不加时发现同名插件或 MCP 配置会报错', false)
1948
+ .option('--claude-dir <path>', '全局模式 Claude 配置目录(默认 ~/.claude;可指向临时目录验证)')
1949
+ .option('--codex-dir <path>', '全局模式 Codex 配置目录(默认 ~/.codex;可指向临时目录验证)')
1950
+ .option('--agents-dir <path>', '全局模式 Agents 配置目录(默认 ~/.agents;可指向临时目录验证)')
1951
+ .option('--dry-run', '只输出计划,不写入文件', false)
1952
+ .option('--yes', '确认写入本机客户端配置', false)
1953
+ .option('--json', '输出 JSON', false)
1954
+ .action(async function (options) {
1955
+ const globals = this.optsWithGlobals?.() ?? {};
1956
+ try {
1957
+ await runBootstrap(options, globals);
1958
+ }
1959
+ catch (e) {
1960
+ if (options.json || globals.json) {
1961
+ printJson({
1962
+ ok: false,
1963
+ error: {
1964
+ message: e instanceof Error ? e.message : String(e),
1965
+ details: e instanceof BootstrapError ? e.details : undefined,
1966
+ },
1967
+ });
1968
+ }
1969
+ throw e;
1970
+ }
1971
+ });
1972
+ }