ccbot-cli 2.0.1 → 2.2.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/bin/adapters/claude.js +150 -0
  3. package/bin/adapters/codex.js +439 -0
  4. package/bin/install.js +583 -349
  5. package/bin/lib/ccline.js +82 -0
  6. package/bin/lib/utils.js +87 -34
  7. package/bin/uninstall.js +48 -0
  8. package/config/AGENTS.md +630 -0
  9. package/config/CLAUDE.md +229 -20
  10. package/config/ccline/config.toml +161 -0
  11. package/config/codex-config.example.toml +22 -0
  12. package/config/settings.example.json +32 -0
  13. package/output-styles/abyss-cultivator.md +399 -0
  14. package/package.json +14 -5
  15. package/skills/SKILL.md +159 -0
  16. package/skills/domains/ai/SKILL.md +34 -0
  17. package/skills/domains/ai/agent-dev.md +242 -0
  18. package/skills/domains/ai/llm-security.md +288 -0
  19. package/skills/domains/ai/prompt-and-eval.md +279 -0
  20. package/skills/domains/ai/rag-system.md +542 -0
  21. package/skills/domains/architecture/SKILL.md +42 -0
  22. package/skills/domains/architecture/api-design.md +225 -0
  23. package/skills/domains/architecture/caching.md +299 -0
  24. package/skills/domains/architecture/cloud-native.md +285 -0
  25. package/skills/domains/architecture/message-queue.md +329 -0
  26. package/skills/domains/architecture/security-arch.md +297 -0
  27. package/skills/domains/data-engineering/SKILL.md +207 -0
  28. package/skills/domains/development/SKILL.md +46 -0
  29. package/skills/domains/development/cpp.md +246 -0
  30. package/skills/domains/development/go.md +323 -0
  31. package/skills/domains/development/java.md +277 -0
  32. package/skills/domains/development/python.md +288 -0
  33. package/skills/domains/development/rust.md +313 -0
  34. package/skills/domains/development/shell.md +313 -0
  35. package/skills/domains/development/typescript.md +277 -0
  36. package/skills/domains/devops/SKILL.md +39 -0
  37. package/skills/domains/devops/cost-optimization.md +272 -0
  38. package/skills/domains/devops/database.md +217 -0
  39. package/skills/domains/devops/devsecops.md +198 -0
  40. package/skills/domains/devops/git-workflow.md +181 -0
  41. package/skills/domains/devops/observability.md +280 -0
  42. package/skills/domains/devops/performance.md +336 -0
  43. package/skills/domains/devops/testing.md +283 -0
  44. package/skills/domains/frontend-design/SKILL.md +38 -0
  45. package/skills/domains/frontend-design/claymorphism/SKILL.md +119 -0
  46. package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
  47. package/skills/domains/frontend-design/component-patterns.md +202 -0
  48. package/skills/domains/frontend-design/engineering.md +287 -0
  49. package/skills/domains/frontend-design/glassmorphism/SKILL.md +140 -0
  50. package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
  51. package/skills/domains/frontend-design/liquid-glass/SKILL.md +137 -0
  52. package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
  53. package/skills/domains/frontend-design/neubrutalism/SKILL.md +143 -0
  54. package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
  55. package/skills/domains/frontend-design/state-management.md +680 -0
  56. package/skills/domains/frontend-design/ui-aesthetics.md +110 -0
  57. package/skills/domains/frontend-design/ux-principles.md +156 -0
  58. package/skills/domains/infrastructure/SKILL.md +200 -0
  59. package/skills/domains/mobile/SKILL.md +224 -0
  60. package/skills/domains/orchestration/SKILL.md +29 -0
  61. package/skills/domains/orchestration/multi-agent.md +263 -0
  62. package/skills/domains/security/SKILL.md +54 -0
  63. package/skills/domains/security/blue-team.md +436 -0
  64. package/skills/domains/security/code-audit.md +265 -0
  65. package/skills/domains/security/pentest.md +226 -0
  66. package/skills/domains/security/red-team.md +375 -0
  67. package/skills/domains/security/threat-intel.md +372 -0
  68. package/skills/domains/security/vuln-research.md +369 -0
  69. package/skills/orchestration/multi-agent/SKILL.md +493 -0
  70. package/skills/run_skill.js +129 -0
  71. package/skills/tools/gen-docs/SKILL.md +116 -0
  72. package/skills/tools/gen-docs/scripts/doc_generator.js +435 -0
  73. package/skills/tools/lib/shared.js +98 -0
  74. package/skills/tools/verify-change/SKILL.md +140 -0
  75. package/skills/tools/verify-change/scripts/change_analyzer.js +289 -0
  76. package/skills/tools/verify-module/SKILL.md +127 -0
  77. package/skills/tools/verify-module/scripts/module_scanner.js +171 -0
  78. package/skills/tools/verify-quality/SKILL.md +160 -0
  79. package/skills/tools/verify-quality/scripts/quality_checker.js +337 -0
  80. package/skills/tools/verify-security/SKILL.md +143 -0
  81. package/skills/tools/verify-security/scripts/security_scanner.js +283 -0
  82. package/bin/lib/registry.js +0 -61
  83. package/config/.claudeignore +0 -11
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 telagod
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const SETTINGS_TEMPLATE = {
7
+ $schema: 'https://json.schemastore.org/claude-code-settings.json',
8
+ env: {
9
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
10
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
11
+ },
12
+ alwaysThinkingEnabled: true,
13
+ model: 'opus',
14
+ outputStyle: 'abyss-cultivator',
15
+ attribution: { commit: '', pr: '' },
16
+ permissions: {
17
+ allow: [
18
+ 'Bash', 'LS', 'Read', 'Agent', 'Write', 'Edit', 'MultiEdit',
19
+ 'Glob', 'Grep', 'WebFetch', 'WebSearch', 'TodoWrite',
20
+ 'NotebookRead', 'NotebookEdit'
21
+ ]
22
+ }
23
+ };
24
+
25
+ const CCLINE_CMD = process.platform === 'win32' ? 'ccline' : '~/.claude/ccline/ccline';
26
+ const CCLINE_STATUS_LINE = {
27
+ statusLine: {
28
+ type: 'command',
29
+ command: CCLINE_CMD,
30
+ padding: 0
31
+ }
32
+ };
33
+
34
+ function getClaudeCoreFiles() {
35
+ return [
36
+ { src: 'config/CLAUDE.md', dest: 'CLAUDE.md' },
37
+ { src: 'output-styles', dest: 'output-styles' },
38
+ { src: 'skills', dest: 'skills' },
39
+ ];
40
+ }
41
+
42
+ function detectClaudeAuth({
43
+ settings = {},
44
+ HOME,
45
+ env = process.env,
46
+ warn = () => {}
47
+ }) {
48
+ const settingsEnv = settings.env || {};
49
+ if (settingsEnv.ANTHROPIC_BASE_URL && settingsEnv.ANTHROPIC_AUTH_TOKEN) {
50
+ return { type: 'custom', detail: settingsEnv.ANTHROPIC_BASE_URL };
51
+ }
52
+ if (env.ANTHROPIC_API_KEY) return { type: 'env', detail: 'ANTHROPIC_API_KEY' };
53
+ if (env.ANTHROPIC_BASE_URL && env.ANTHROPIC_AUTH_TOKEN) {
54
+ return { type: 'env-custom', detail: env.ANTHROPIC_BASE_URL };
55
+ }
56
+
57
+ const cred = path.join(HOME, '.claude', '.credentials.json');
58
+ if (fs.existsSync(cred)) {
59
+ try {
60
+ const cc = JSON.parse(fs.readFileSync(cred, 'utf8'));
61
+ if (cc.claudeAiOauth || cc.apiKey) return { type: 'login', detail: 'claude login' };
62
+ } catch (e) {
63
+ warn(`凭证文件损坏: ${cred}`);
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ async function configureCustomProvider(ctx, { ok }) {
71
+ const { confirm, input } = await import('@inquirer/prompts');
72
+ const doCfg = await confirm({ message: '配置自定义 provider?', default: false });
73
+ if (!doCfg) return;
74
+
75
+ if (!ctx.settings.env) ctx.settings.env = {};
76
+ const url = await input({ message: 'ANTHROPIC_BASE_URL:' });
77
+ const token = await input({ message: 'ANTHROPIC_AUTH_TOKEN:' });
78
+ if (url) ctx.settings.env.ANTHROPIC_BASE_URL = url;
79
+ if (token) ctx.settings.env.ANTHROPIC_AUTH_TOKEN = token;
80
+
81
+ fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
82
+ ok('provider 已配置');
83
+ }
84
+
85
+ function mergeSettings(ctx, { deepMergeNew, printMergeLog, c, ok }) {
86
+ const log = [];
87
+ deepMergeNew(ctx.settings, SETTINGS_TEMPLATE, '', log);
88
+ printMergeLog(log, c);
89
+ fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
90
+ ok('settings.json 合并完成');
91
+ }
92
+
93
+ async function postClaude({
94
+ ctx,
95
+ autoYes,
96
+ HOME,
97
+ PKG_ROOT,
98
+ step,
99
+ ok,
100
+ warn,
101
+ info,
102
+ c,
103
+ deepMergeNew,
104
+ printMergeLog,
105
+ installCcline
106
+ }) {
107
+ step(2, 3, '认证检测');
108
+ const auth = detectClaudeAuth({ settings: ctx.settings, HOME, warn });
109
+ if (auth) {
110
+ ok(`${c.b(auth.type)} → ${auth.detail}`);
111
+ } else {
112
+ warn('未检测到 API 认证');
113
+ info(`支持: ${c.cyn('claude login')} | ${c.cyn('ANTHROPIC_API_KEY')} | ${c.cyn('自定义 provider')}`);
114
+ if (!autoYes) await configureCustomProvider(ctx, { ok });
115
+ }
116
+
117
+ step(3, 3, '可选配置');
118
+ if (autoYes) {
119
+ info('自动模式: 合并推荐配置');
120
+ mergeSettings(ctx, { deepMergeNew, printMergeLog, c, ok });
121
+ await installCcline(ctx, { HOME, PKG_ROOT, CCLINE_STATUS_LINE, ok, warn, info, c });
122
+ return;
123
+ }
124
+
125
+ const { checkbox } = await import('@inquirer/prompts');
126
+ const choices = await checkbox({
127
+ message: '选择要安装的配置 (空格选择, 回车确认)',
128
+ choices: [
129
+ { name: '精细合并推荐 settings.json (保留现有配置)', value: 'settings', checked: true },
130
+ { name: '安装 ccline 状态栏 (需要 Nerd Font)', value: 'ccline', checked: true },
131
+ ],
132
+ });
133
+
134
+ if (choices.includes('settings')) {
135
+ mergeSettings(ctx, { deepMergeNew, printMergeLog, c, ok });
136
+ }
137
+ if (choices.includes('ccline')) {
138
+ await installCcline(ctx, { HOME, PKG_ROOT, CCLINE_STATUS_LINE, ok, warn, info, c });
139
+ }
140
+ }
141
+
142
+ module.exports = {
143
+ SETTINGS_TEMPLATE,
144
+ CCLINE_STATUS_LINE,
145
+ getClaudeCoreFiles,
146
+ detectClaudeAuth,
147
+ configureCustomProvider,
148
+ mergeSettings,
149
+ postClaude,
150
+ };
@@ -0,0 +1,439 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const CODEX_DEFAULTS = {
7
+ approvalPolicy: 'on-request',
8
+ sandboxMode: 'workspace-write',
9
+ featureFlag: 'multi_agent',
10
+ };
11
+
12
+ const LEGACY_FEATURES = {
13
+ removed: [
14
+ 'search_tool',
15
+ 'request_rule',
16
+ 'experimental_windows_sandbox',
17
+ 'elevated_windows_sandbox',
18
+ 'remote_models',
19
+ 'collaboration_modes',
20
+ 'steer',
21
+ ],
22
+ deprecated: [
23
+ 'web_search_request',
24
+ 'web_search_cached',
25
+ ],
26
+ };
27
+
28
+ function escapeRegExp(input) {
29
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ }
31
+
32
+ function isTableHeader(line) {
33
+ return /^\s*\[[^\]]+\]\s*$/.test(line);
34
+ }
35
+
36
+ function isProjectTableHeader(line) {
37
+ return /^\s*\[projects\."[^"]+"\]\s*$/.test(line);
38
+ }
39
+
40
+ function isAssignmentForKey(line, key) {
41
+ const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
42
+ return re.test(line);
43
+ }
44
+
45
+ function hasRootKey(content, key) {
46
+ const lines = content.split(/\r?\n/);
47
+ let inRoot = true;
48
+
49
+ for (const line of lines) {
50
+ if (isTableHeader(line)) {
51
+ inRoot = false;
52
+ continue;
53
+ }
54
+ if (inRoot && isAssignmentForKey(line, key)) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function readRootStringKey(content, key) {
62
+ const lines = content.split(/\r?\n/);
63
+ const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"`);
64
+ let inRoot = true;
65
+
66
+ for (const line of lines) {
67
+ if (isTableHeader(line)) {
68
+ inRoot = false;
69
+ continue;
70
+ }
71
+ if (!inRoot) continue;
72
+ const m = line.match(re);
73
+ if (m) return m[1];
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function hasSection(content, sectionName) {
79
+ const re = new RegExp(`^\\s*\\[${escapeRegExp(sectionName)}\\]\\s*$`, 'm');
80
+ return re.test(content);
81
+ }
82
+
83
+ function hasKeyInSection(content, sectionName, key) {
84
+ const lines = content.split(/\r?\n/);
85
+ const sectionRe = new RegExp(`^\\s*\\[${escapeRegExp(sectionName)}\\]\\s*$`);
86
+ const anySectionRe = /^\s*\[[^\]]+\]\s*$/;
87
+ const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
88
+ let inSection = false;
89
+
90
+ for (const line of lines) {
91
+ if (sectionRe.test(line)) {
92
+ inSection = true;
93
+ continue;
94
+ }
95
+ if (inSection && anySectionRe.test(line)) {
96
+ return false;
97
+ }
98
+ if (inSection && keyRe.test(line)) {
99
+ return true;
100
+ }
101
+ }
102
+ return false;
103
+ }
104
+
105
+ function appendLine(content, line, eol) {
106
+ if (!content) return `${line}${eol}`;
107
+ const normalized = content.endsWith('\n') ? content : `${content}${eol}`;
108
+ return `${normalized}${line}${eol}`;
109
+ }
110
+
111
+ function insertRootLine(content, line, eol) {
112
+ if (!content) return `${line}${eol}`;
113
+ const lines = content.split(/\r?\n/);
114
+ const firstSection = lines.findIndex((l) => isTableHeader(l));
115
+ const idx = firstSection === -1 ? lines.length : firstSection;
116
+ lines.splice(idx, 0, line);
117
+ return lines.join(eol);
118
+ }
119
+
120
+ function ensureRootKey(content, key, valueLiteral, eol) {
121
+ if (hasRootKey(content, key)) return { merged: content, added: false };
122
+ return { merged: insertRootLine(content, `${key} = ${valueLiteral}`, eol), added: true };
123
+ }
124
+
125
+ function insertLineAfterSectionHeader(content, sectionName, line, eol) {
126
+ const lines = content.split(/\r?\n/);
127
+ const sectionRe = new RegExp(`^\\s*\\[${escapeRegExp(sectionName)}\\]\\s*$`);
128
+ const idx = lines.findIndex((l) => sectionRe.test(l));
129
+ if (idx === -1) return appendLine(content, line, eol);
130
+ lines.splice(idx + 1, 0, line);
131
+ return lines.join(eol);
132
+ }
133
+
134
+ function ensureKeyInSection(content, sectionName, key, valueLiteral, eol) {
135
+ let merged = content;
136
+ let added = false;
137
+
138
+ if (!hasSection(merged, sectionName)) {
139
+ merged = appendLine(merged, `[${sectionName}]`, eol);
140
+ merged = appendLine(merged, `${key} = ${valueLiteral}`, eol);
141
+ added = true;
142
+ } else if (!hasKeyInSection(merged, sectionName, key)) {
143
+ merged = insertLineAfterSectionHeader(merged, sectionName, `${key} = ${valueLiteral}`, eol);
144
+ added = true;
145
+ }
146
+
147
+ return { merged, added };
148
+ }
149
+
150
+ function parseTomlBooleanAssignment(line) {
151
+ const m = line.match(/=\s*(true|false)\b/i);
152
+ if (!m) return null;
153
+ return m[1].toLowerCase() === 'true';
154
+ }
155
+
156
+ function removeKeyAssignmentsInNonRootSections(content, key) {
157
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
158
+ const lines = content.split(/\r?\n/);
159
+ const kept = [];
160
+ let inRoot = true;
161
+ let removed = false;
162
+
163
+ for (const line of lines) {
164
+ if (isTableHeader(line)) {
165
+ inRoot = false;
166
+ kept.push(line);
167
+ continue;
168
+ }
169
+ if (!inRoot && isAssignmentForKey(line, key)) {
170
+ removed = true;
171
+ continue;
172
+ }
173
+ kept.push(line);
174
+ }
175
+
176
+ return { merged: kept.join(eol), removed };
177
+ }
178
+
179
+ function removeProjectTrustSectionsForFullAccess(content) {
180
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
181
+ const sandboxMode = readRootStringKey(content, 'sandbox_mode');
182
+ if (sandboxMode !== 'danger-full-access') {
183
+ return { merged: content, removed: false };
184
+ }
185
+
186
+ const lines = content.split(/\r?\n/);
187
+ const kept = [];
188
+ let inProjectSection = false;
189
+ let removed = false;
190
+
191
+ for (const line of lines) {
192
+ if (isTableHeader(line)) {
193
+ if (isProjectTableHeader(line)) {
194
+ inProjectSection = true;
195
+ removed = true;
196
+ continue;
197
+ }
198
+ inProjectSection = false;
199
+ kept.push(line);
200
+ continue;
201
+ }
202
+ if (inProjectSection) continue;
203
+ kept.push(line);
204
+ }
205
+
206
+ return { merged: kept.join(eol), removed };
207
+ }
208
+
209
+ function removeFeatureFlagsFromFeaturesSection(content, featureNames) {
210
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
211
+ const lines = content.split(/\r?\n/);
212
+ const sectionRe = /^\s*\[features\]\s*$/;
213
+ const anySectionRe = /^\s*\[[^\]]+\]\s*$/;
214
+ const assignRe = /^\s*([A-Za-z0-9_.-]+)\s*=/;
215
+ const featureSet = new Set(featureNames);
216
+ const removedEntries = [];
217
+
218
+ let inFeatures = false;
219
+ const kept = [];
220
+
221
+ for (const line of lines) {
222
+ if (sectionRe.test(line)) {
223
+ inFeatures = true;
224
+ kept.push(line);
225
+ continue;
226
+ }
227
+ if (inFeatures && anySectionRe.test(line)) {
228
+ inFeatures = false;
229
+ kept.push(line);
230
+ continue;
231
+ }
232
+ if (inFeatures) {
233
+ const m = line.match(assignRe);
234
+ if (m && featureSet.has(m[1])) {
235
+ removedEntries.push({ key: m[1], enabled: parseTomlBooleanAssignment(line) });
236
+ continue;
237
+ }
238
+ }
239
+ kept.push(line);
240
+ }
241
+
242
+ return { merged: kept.join(eol), removedEntries };
243
+ }
244
+
245
+ function uniq(values) {
246
+ return [...new Set(values)];
247
+ }
248
+
249
+ function cleanupLegacyCodexConfig(content) {
250
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
251
+ const toRemove = [...LEGACY_FEATURES.removed, ...LEGACY_FEATURES.deprecated];
252
+ const { merged: pruned, removedEntries } = removeFeatureFlagsFromFeaturesSection(content, toRemove);
253
+ let merged = pruned;
254
+ const removed = uniq(removedEntries.map((x) => x.key));
255
+ const migrated = [];
256
+
257
+ const deprecatedRemoved = removedEntries.filter((x) => LEGACY_FEATURES.deprecated.includes(x.key));
258
+ if (deprecatedRemoved.length > 0) {
259
+ const shouldEnableWebSearch = deprecatedRemoved.some((x) => x.enabled !== false);
260
+ const { merged: withTools, added } = ensureKeyInSection(
261
+ merged,
262
+ 'tools',
263
+ 'web_search',
264
+ shouldEnableWebSearch ? 'true' : 'false',
265
+ eol
266
+ );
267
+ merged = withTools;
268
+ if (added) migrated.push(`tools.web_search=${shouldEnableWebSearch ? 'true' : 'false'}`);
269
+ }
270
+
271
+ return { merged, removed, migrated };
272
+ }
273
+
274
+ function mergeCodexConfigDefaults(content) {
275
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
276
+ let merged = content;
277
+ const added = [];
278
+
279
+ const rootKeys = [
280
+ 'approval_policy',
281
+ 'sandbox_mode',
282
+ 'model_reasoning_effort',
283
+ 'disable_response_storage',
284
+ 'personality',
285
+ ];
286
+
287
+ for (const key of rootKeys) {
288
+ const cleaned = removeKeyAssignmentsInNonRootSections(merged, key);
289
+ merged = cleaned.merged;
290
+ }
291
+
292
+ const approval = ensureRootKey(merged, 'approval_policy', `"${CODEX_DEFAULTS.approvalPolicy}"`, eol);
293
+ merged = approval.merged;
294
+ if (approval.added) {
295
+ added.push('approval_policy');
296
+ }
297
+
298
+ const sandbox = ensureRootKey(merged, 'sandbox_mode', `"${CODEX_DEFAULTS.sandboxMode}"`, eol);
299
+ merged = sandbox.merged;
300
+ if (sandbox.added) {
301
+ added.push('sandbox_mode');
302
+ }
303
+
304
+ if (!hasSection(merged, 'features')) {
305
+ merged = appendLine(merged, '[features]', eol);
306
+ merged = appendLine(merged, `${CODEX_DEFAULTS.featureFlag} = true`, eol);
307
+ added.push('features.multi_agent');
308
+ } else if (!hasKeyInSection(merged, 'features', CODEX_DEFAULTS.featureFlag)) {
309
+ merged = insertLineAfterSectionHeader(merged, 'features', `${CODEX_DEFAULTS.featureFlag} = true`, eol);
310
+ added.push('features.multi_agent');
311
+ }
312
+
313
+ return { merged, added };
314
+ }
315
+
316
+ function patchCodexConfig(cfgPath) {
317
+ const raw = fs.readFileSync(cfgPath, 'utf8');
318
+ const { merged: cleaned, removed, migrated } = cleanupLegacyCodexConfig(raw);
319
+ const { merged: mergedDefaults, added } = mergeCodexConfigDefaults(cleaned);
320
+ const { merged, removed: removedProjectTrust } = removeProjectTrustSectionsForFullAccess(mergedDefaults);
321
+ const removedAll = removedProjectTrust ? [...removed, 'projects.*.trust_level'] : removed;
322
+
323
+ if (merged !== raw) fs.writeFileSync(cfgPath, merged);
324
+ return { added, removed: removedAll, migrated };
325
+ }
326
+
327
+ function patchCodexConfigDefaults(cfgPath) {
328
+ return patchCodexConfig(cfgPath).added;
329
+ }
330
+
331
+ function patchAndReportCodexDefaults({ cfgPath, ok, warn }) {
332
+ try {
333
+ const { added, removed, migrated } = patchCodexConfig(cfgPath);
334
+ const changes = [];
335
+ if (added.length > 0) changes.push(`补全默认项: ${added.join(', ')}`);
336
+ if (removed.length > 0) changes.push(`清理过时项: ${removed.join(', ')}`);
337
+ if (migrated.length > 0) changes.push(`迁移配置: ${migrated.join(', ')}`);
338
+ if (changes.length > 0) ok(`config.toml 已存在,${changes.join(';')}`);
339
+ else ok('config.toml 已存在');
340
+ } catch (e) {
341
+ warn(`config.toml 读取失败,跳过补全: ${e.message}`);
342
+ }
343
+ }
344
+
345
+ function detectCodexAuth({
346
+ HOME,
347
+ env = process.env,
348
+ warn = () => {}
349
+ }) {
350
+ if (env.OPENAI_API_KEY) return { type: 'env', detail: 'OPENAI_API_KEY' };
351
+
352
+ const auth = path.join(HOME, '.codex', 'auth.json');
353
+ if (fs.existsSync(auth)) {
354
+ try {
355
+ const a = JSON.parse(fs.readFileSync(auth, 'utf8'));
356
+ if (a.token || a.api_key) return { type: 'login', detail: 'codex login' };
357
+ } catch (e) {
358
+ warn(`凭证文件损坏: ${auth}`);
359
+ }
360
+ }
361
+
362
+ const cfg = path.join(HOME, '.codex', 'config.toml');
363
+ if (fs.existsSync(cfg) && fs.readFileSync(cfg, 'utf8').includes('base_url')) {
364
+ return { type: 'custom', detail: 'config.toml' };
365
+ }
366
+
367
+ return null;
368
+ }
369
+
370
+ function getCodexCoreFiles() {
371
+ return [
372
+ { src: 'config/AGENTS.md', dest: 'AGENTS.md' },
373
+ { src: 'skills', dest: 'skills' },
374
+ ];
375
+ }
376
+
377
+ async function postCodex({
378
+ autoYes,
379
+ HOME,
380
+ PKG_ROOT,
381
+ step,
382
+ ok,
383
+ warn,
384
+ info,
385
+ c
386
+ }) {
387
+ const { confirm } = await import('@inquirer/prompts');
388
+ const cfgPath = path.join(HOME, '.codex', 'config.toml');
389
+ const exists = fs.existsSync(cfgPath);
390
+
391
+ step(2, 3, '认证检测');
392
+ const auth = detectCodexAuth({ HOME, warn });
393
+ if (auth) {
394
+ ok(`${c.b(auth.type)} → ${auth.detail}`);
395
+ } else {
396
+ warn('未检测到 API 认证');
397
+ info(`支持: ${c.cyn('codex login')} | ${c.cyn('OPENAI_API_KEY')} | ${c.cyn('自定义 provider')}`);
398
+ }
399
+
400
+ step(3, 3, '可选配置');
401
+ if (autoYes) {
402
+ if (!exists) {
403
+ const src = path.join(PKG_ROOT, 'config', 'codex-config.example.toml');
404
+ if (fs.existsSync(src)) {
405
+ fs.copyFileSync(src, cfgPath);
406
+ ok('写入: ~/.codex/config.toml (模板)');
407
+ warn('请编辑 base_url 和 model');
408
+ }
409
+ } else {
410
+ patchAndReportCodexDefaults({ cfgPath, ok, warn });
411
+ }
412
+ return;
413
+ }
414
+
415
+ if (!exists) {
416
+ warn('未检测到 ~/.codex/config.toml');
417
+ const doWrite = await confirm({ message: '写入推荐 config.toml (含自定义 provider 模板)?', default: true });
418
+ if (doWrite) {
419
+ const src = path.join(PKG_ROOT, 'config', 'codex-config.example.toml');
420
+ if (fs.existsSync(src)) {
421
+ fs.copyFileSync(src, cfgPath);
422
+ ok('写入: ~/.codex/config.toml');
423
+ warn('请编辑 base_url 和 model');
424
+ }
425
+ }
426
+ } else {
427
+ patchAndReportCodexDefaults({ cfgPath, ok, warn });
428
+ }
429
+ }
430
+
431
+ module.exports = {
432
+ cleanupLegacyCodexConfig,
433
+ mergeCodexConfigDefaults,
434
+ patchCodexConfig,
435
+ patchCodexConfigDefaults,
436
+ detectCodexAuth,
437
+ getCodexCoreFiles,
438
+ postCodex,
439
+ };