@qqq123456789/codex-doctor 0.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.
@@ -0,0 +1,335 @@
1
+ // doctor 自检:与 scripts/codex-doctor.ps1/.sh 检查项一致,跨平台单一实现
2
+ import { execFileSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { HOME, CODEX_DIR, exists, dirBytes } from './util.mjs';
6
+
7
+ const ROOT_KEYS = /^(model|model_provider|approval_policy|sandbox_mode)\s*=/;
8
+
9
+ async function probe(url) {
10
+ const ctl = new AbortController();
11
+ const timer = setTimeout(() => ctl.abort(), 8000);
12
+ try {
13
+ const res = await fetch(url, { method: 'HEAD', signal: ctl.signal });
14
+ return { ok: true, code: res.status };
15
+ } catch (err) {
16
+ return { ok: false, err: err?.cause?.code || err?.name || 'error' };
17
+ } finally {
18
+ clearTimeout(timer);
19
+ }
20
+ }
21
+
22
+ async function probeWithRetry(url, tries = 2) {
23
+ let last = null;
24
+ for (let i = 0; i < tries; i++) {
25
+ const r = await probe(url);
26
+ if (r.ok) return r;
27
+ last = r;
28
+ }
29
+ return last;
30
+ }
31
+
32
+ function safeHost(u) {
33
+ try {
34
+ return new URL(u).host;
35
+ } catch {
36
+ return '(无效 URL)';
37
+ }
38
+ }
39
+
40
+ function runCmd(cmd, args) {
41
+ try {
42
+ return execFileSync(cmd, args, {
43
+ encoding: 'utf8',
44
+ timeout: 15000,
45
+ stdio: ['ignore', 'pipe', 'pipe'],
46
+ }).trim();
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // Windows 上 codex 是 .cmd,必须经 shell 启动;命令整串传递以避免参数转义告警
53
+ function runShell(cmdString) {
54
+ try {
55
+ return execFileSync(cmdString, {
56
+ encoding: 'utf8',
57
+ timeout: 15000,
58
+ stdio: ['ignore', 'pipe', 'pipe'],
59
+ shell: true,
60
+ }).trim();
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function compareSemver(a, b) {
67
+ const pa = String(a).split('.').map(Number);
68
+ const pb = String(b).split('.').map(Number);
69
+ for (let i = 0; i < 3; i++) {
70
+ const x = pa[i] || 0;
71
+ const y = pb[i] || 0;
72
+ if (x !== y) return x < y ? -1 : 1;
73
+ }
74
+ return 0;
75
+ }
76
+
77
+ // 查询 openai/codex 最新稳定版(rust-v0.151.0 → 0.151.0);失败返回 null 静默跳过
78
+ async function fetchLatestCodexVersion() {
79
+ try {
80
+ const headers = { Accept: 'application/vnd.github+json', 'User-Agent': 'codex-doctor-cli' };
81
+ if (process.env.GH_TOKEN) headers.Authorization = `Bearer ${process.env.GH_TOKEN}`;
82
+ const res = await fetch('https://api.github.com/repos/openai/codex/releases/latest', { headers });
83
+ if (!res.ok) return null;
84
+ const tag = (await res.json()).tag_name || '';
85
+ return (tag.match(/(\d+\.\d+\.\d+)/) || [])[1] || null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ export async function collectChecks({ network = true } = {}) {
92
+ const results = [];
93
+ const add = (id, status, detail, doc) => results.push({ id, status, detail, doc });
94
+
95
+ // 1. codex 本体
96
+ const codexVer = runShell('codex --version');
97
+ add(
98
+ 'codex',
99
+ codexVer ? 'ok' : 'fail',
100
+ codexVer ? `codex 已安装: ${codexVer}` : 'codex 不在 PATH 中',
101
+ 'docs/01-installation.md'
102
+ );
103
+
104
+ // 2. node / npm
105
+ const nodeVer = runCmd('node', ['-v']);
106
+ if (nodeVer) {
107
+ const major = parseInt(String(nodeVer).replace(/^v(\d+).*/, '$1'), 10);
108
+ add('node', major >= 20 ? 'ok' : 'warn', `node ${nodeVer}${major >= 20 ? '' : '(建议 20 LTS+)'}`, 'docs/01-installation.md');
109
+ } else {
110
+ add('node', 'info', '未检测到 node(brew/二进制方式安装 codex 则无妨)');
111
+ }
112
+
113
+ // 3. 配置目录与 config.toml
114
+ if (exists(CODEX_DIR)) {
115
+ add('codexdir', 'ok', `~/.codex 存在: ${CODEX_DIR}`);
116
+ } else {
117
+ add('codexdir', 'warn', '~/.codex 不存在(从未运行过 codex,或已被完全重置)');
118
+ }
119
+
120
+ let relayUrl = null;
121
+ const cfg = path.join(CODEX_DIR, 'config.toml');
122
+ if (exists(cfg)) {
123
+ add('config', 'ok', 'config.toml 存在');
124
+ const content = fs.readFileSync(cfg, 'utf8');
125
+ let inTable = false;
126
+ const suspects = [];
127
+ content.split(/\r?\n/).forEach((line, i) => {
128
+ const l = line.trim();
129
+ if (/^\[.+\]/.test(l)) {
130
+ inTable = true;
131
+ return;
132
+ }
133
+ if (!l || l.startsWith('#')) return;
134
+ if (inTable && ROOT_KEYS.test(l)) suspects.push(`第${i + 1}行: ${l}`);
135
+ });
136
+ if (suspects.length > 0) {
137
+ add(
138
+ 'config-roots',
139
+ 'warn',
140
+ `有 ${suspects.length} 处赋值出现在 [表] 之后,若本意是根级配置则不生效([profiles.*] 内属正常):${suspects.slice(0, 3).join(';')}`,
141
+ 'docs/04-config.md'
142
+ );
143
+ } else {
144
+ add('config-roots', 'ok', '未发现根级键位置问题');
145
+ }
146
+ if (/^\[model_providers\./m.test(content)) {
147
+ add('providers', 'warn', '检测到第三方 provider 配置——用官方账号报 401 时先核对它', 'docs/04-config.md');
148
+ // 提取第一个中转 base_url(供网络探测感知中转模式)
149
+ let inProvider = false;
150
+ for (const raw of content.split(/\r?\n/)) {
151
+ const l = raw.trim();
152
+ if (/^\[model_providers\./.test(l)) {
153
+ inProvider = true;
154
+ continue;
155
+ }
156
+ if (/^\[/.test(l)) {
157
+ inProvider = false;
158
+ continue;
159
+ }
160
+ if (inProvider) {
161
+ const m = l.match(/^base_url\s*=\s*["']([^"']+)["']/);
162
+ if (m && !relayUrl) relayUrl = m[1];
163
+ }
164
+ }
165
+ }
166
+ if (relayUrl && !network) {
167
+ add('relay', 'info', `检测到第三方中转端点 ${safeHost(relayUrl)}(--no-network 未探测)`, 'docs/04-config.md');
168
+ }
169
+ } else {
170
+ add('config', 'info', 'config.toml 不存在(使用默认配置,不一定是问题)');
171
+ }
172
+
173
+ // 4. 凭据
174
+ const auth = path.join(CODEX_DIR, 'auth.json');
175
+ add(
176
+ 'auth',
177
+ exists(auth) ? 'ok' : 'warn',
178
+ exists(auth) ? 'auth.json 存在(内容不读取)' : 'auth.json 不存在——尚未登录或已清除,运行 codex login',
179
+ 'docs/02-login-auth.md'
180
+ );
181
+
182
+ // 5. 环境变量
183
+ if (process.env.OPENAI_API_KEY) add('env-key', 'ok', 'OPENAI_API_KEY 已设置(值不显示)');
184
+ else add('env-key', 'info', 'OPENAI_API_KEY 未设置(ChatGPT 登录方式无需设置)');
185
+ if (process.env.OPENAI_BASE_URL) {
186
+ add('env-url', 'warn', `OPENAI_BASE_URL=${process.env.OPENAI_BASE_URL}——会改变请求端点,401 排障重点`, 'docs/02-login-auth.md');
187
+ }
188
+ const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
189
+ add(
190
+ 'proxy',
191
+ proxy ? 'ok' : 'info',
192
+ proxy ? `代理已设置: ${proxy}` : '未设置 HTTP(S)_PROXY(直连网络;受限网络用户报断流先看代理)',
193
+ 'docs/03-network-proxy.md'
194
+ );
195
+
196
+ // 5b. Windows 系统代理(解释「系统有代理但终端不通」类现象)
197
+ if (process.platform === 'win32') {
198
+ const q = runShell('reg query "HKCU\\Internet Settings" /v ProxyEnable');
199
+ if (q && /0x1\b/.test(q)) {
200
+ const ps = runShell('reg query "HKCU\\Internet Settings" /v ProxyServer');
201
+ const server = ps ? (ps.split(/\r?\n/).find((l) => /ProxyServer/i.test(l)) || '').trim().split(/\s+/).pop() : '';
202
+ add(
203
+ 'sysproxy',
204
+ 'info',
205
+ `Windows 系统代理已开启(${server || '已启用'})——命令行程序不一定走系统代理,终端报断流先设置 HTTP(S)_PROXY`,
206
+ 'docs/03-network-proxy.md'
207
+ );
208
+ }
209
+ }
210
+
211
+ // 5c. Windows PowerShell 执行策略(npm 方式安装的常见拦截点)
212
+ if (process.platform === 'win32') {
213
+ const pol = runShell('powershell -NoProfile -Command Get-ExecutionPolicy');
214
+ if (pol && /restricted/i.test(pol)) {
215
+ add('ps-policy', 'warn', `PowerShell 执行策略为 ${pol.trim()}——npm 方式的 codex.ps1 会被拦截`, 'docs/01-installation.md');
216
+ }
217
+ }
218
+
219
+ // 6. 网络连通性(中转模式感知)+ codex 版本过期检测
220
+ if (network) {
221
+ if (relayUrl) {
222
+ // 中转用户:探测配置里的中转端点;官方端点降级为参考信息
223
+ const rr = await probeWithRetry(relayUrl);
224
+ add(
225
+ 'relay',
226
+ rr.ok ? 'ok' : 'fail',
227
+ rr.ok
228
+ ? `中转端点可达: ${safeHost(relayUrl)} → HTTP ${rr.code}`
229
+ : `中转端点不可达: ${safeHost(relayUrl)}(${rr.err})——中转用户断流先查这里`,
230
+ 'docs/04-config.md'
231
+ );
232
+ const rs = await Promise.all(['https://chatgpt.com', 'https://api.openai.com'].map((u) => probe(u).then((r) => ({ u, r }))));
233
+ for (const { u, r } of rs) {
234
+ add(
235
+ 'net',
236
+ 'info',
237
+ `官方端点 ${u} → ${r.ok ? `HTTP ${r.code}` : '不可达'}(中转模式下属预期,仅供参考)`,
238
+ 'docs/03-network-proxy.md'
239
+ );
240
+ }
241
+ } else {
242
+ const targets = ['https://chatgpt.com', 'https://api.openai.com'];
243
+ const rs = await Promise.all(targets.map((u) => probeWithRetry(u).then((r) => ({ u, r }))));
244
+ for (const { u, r } of rs) {
245
+ add('net', r.ok ? 'ok' : 'fail', r.ok ? `${u} → HTTP ${r.code}` : `${u} → 不通(${r.err})`, 'docs/03-network-proxy.md');
246
+ }
247
+ }
248
+
249
+ if (codexVer) {
250
+ const local = (codexVer.match(/(\d+\.\d+\.\d+)/) || [])[1];
251
+ const latest = await fetchLatestCodexVersion();
252
+ if (local && latest) {
253
+ if (compareSemver(local, latest) < 0) {
254
+ add('codex-newer', 'warn', `codex 版本偏旧:本地 ${local},最新稳定版 ${latest}——建议升级`, 'docs/01-installation.md');
255
+ } else {
256
+ add('codex-newer', 'ok', `codex 版本为最新稳定版(${local})`);
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ // 7. 磁盘空间
263
+ try {
264
+ if (typeof fs.statfsSync === 'function') {
265
+ const s = fs.statfsSync(HOME);
266
+ const freeGB = (s.bsize * s.bfree) / 1024 ** 3;
267
+ add('disk', freeGB >= 5 ? 'ok' : 'warn', `HOME 所在分区剩余约 ${freeGB.toFixed(1)} GB`);
268
+ }
269
+ } catch {
270
+ /* 平台不支持则跳过 */
271
+ }
272
+
273
+ // 8. OneDrive 已知坑
274
+ const oneDrive = process.env.OneDrive;
275
+ if (oneDrive && process.platform === 'win32') {
276
+ const norm = (p) => path.resolve(String(p)).toLowerCase();
277
+ if (norm(CODEX_DIR).startsWith(norm(oneDrive))) {
278
+ add('onedrive', 'fail', '~/.codex 在 OneDrive 同步范围内——凭据/配置被同步盘接管,务必移出', 'docs/09-maintenance.md');
279
+ } else if (norm(process.cwd()).startsWith(norm(oneDrive))) {
280
+ add('onedrive', 'warn', '当前目录在 OneDrive 内——同步盘文件锁是 stream disconnected 的高发原因', 'docs/03-network-proxy.md');
281
+ } else {
282
+ add('onedrive', 'info', `OneDrive 存在(${oneDrive}):项目与 ~/.codex 请勿放入其中`, 'docs/03-network-proxy.md');
283
+ }
284
+ }
285
+
286
+ // 9. WSL 运行开关(Windows 特有状态文件)
287
+ const gs = path.join(CODEX_DIR, 'codex-global-state.json');
288
+ if (exists(gs)) {
289
+ try {
290
+ const j = JSON.parse(fs.readFileSync(gs, 'utf8'));
291
+ if (j.runCodexInWindowsSubsystemForLinux === true) {
292
+ add('wsl-state', 'warn', 'runCodexInWindowsSubsystemForLinux=true(CLI 跑在 WSL);IDE 进不去/崩溃可改回 false', 'docs/10-ide-vscode.md');
293
+ } else {
294
+ add('wsl-state', 'ok', 'codex-global-state.json 正常(未启用 WSL 运行模式)');
295
+ }
296
+ } catch {
297
+ add('wsl-state', 'warn', 'codex-global-state.json 无法解析');
298
+ }
299
+ }
300
+
301
+ // 10. sessions 体积
302
+ const sess = path.join(CODEX_DIR, 'sessions');
303
+ if (exists(sess)) {
304
+ const mb = dirBytes(sess) / 1024 / 1024;
305
+ add(
306
+ 'sessions',
307
+ mb >= 500 ? 'warn' : 'ok',
308
+ `sessions 目录约 ${mb.toFixed(1)} MB${mb >= 500 ? '——可运行 codex-doctor clean sessions 归档' : ''}`,
309
+ 'docs/09-maintenance.md'
310
+ );
311
+ }
312
+
313
+ return results;
314
+ }
315
+
316
+ export function summarize(results) {
317
+ const c = { pass: 0, warn: 0, fail: 0, info: 0 };
318
+ for (const r of results) {
319
+ if (r.status === 'ok') c.pass++;
320
+ else c[r.status]++;
321
+ }
322
+ return c;
323
+ }
324
+
325
+ export function renderHuman(results, summary) {
326
+ const sym = { ok: '[OK] ', warn: '[WARN]', fail: '[FAIL]', info: '[--] ' };
327
+ const lines = ['codex-doctor - Codex 环境自检', ''];
328
+ for (const r of results) {
329
+ lines.push(`${sym[r.status]} ${r.detail}${r.doc ? ` → ${r.doc}` : ''}`);
330
+ }
331
+ lines.push('');
332
+ lines.push(`======== 汇总:通过 ${summary.pass} 警告 ${summary.warn} 失败 ${summary.fail} ========`);
333
+ lines.push('WARN/FAIL 项请对照 docs/ 下对应文档处理;提 Issue 时请附完整输出(脱敏后)。');
334
+ return lines.join('\n');
335
+ }
package/tool/clean.mjs ADDED
@@ -0,0 +1,72 @@
1
+ // clean:把超过 N 天的会话/日志文件归档到 ~/.codex/archive/(移动而非删除)
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { CODEX_DIR, ensureDir, exists, timestamp, walkFiles } from './util.mjs';
5
+
6
+ function pruneEmptyDirs(root) {
7
+ if (!exists(root)) return;
8
+ for (const e of fs.readdirSync(root, { withFileTypes: true })) {
9
+ if (e.isDirectory()) {
10
+ const p = path.join(root, e.name);
11
+ pruneEmptyDirs(p);
12
+ try {
13
+ fs.rmdirSync(p); // 仅当已空才生效
14
+ } catch {
15
+ /* 非空则保留 */
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ export function cleanTarget({ target, days, yes }) {
22
+ const dir =
23
+ target === 'sessions'
24
+ ? path.join(CODEX_DIR, 'sessions')
25
+ : path.join(CODEX_DIR, 'log');
26
+
27
+ if (!exists(dir)) {
28
+ return { lines: [`${dir} 不存在,无需清理`], moved: 0 };
29
+ }
30
+
31
+ const cutoff = Date.now() - days * 86400e3;
32
+ const victims = [];
33
+ for (const f of walkFiles(dir)) {
34
+ const st = fs.statSync(f);
35
+ if (st.mtimeMs < cutoff) {
36
+ victims.push({ f, rel: path.relative(dir, f), bytes: st.size });
37
+ }
38
+ }
39
+
40
+ const lines = [];
41
+ if (victims.length === 0) {
42
+ lines.push(`${days} 天内没有可归档的文件(${dir})`);
43
+ return { lines, moved: 0 };
44
+ }
45
+
46
+ const kb = victims.reduce((s, v) => s + v.bytes, 0) / 1024;
47
+ lines.push(`发现 ${victims.length} 个超过 ${days} 天的文件(约 ${kb.toFixed(1)} KB):`);
48
+ for (const v of victims.slice(0, 10)) lines.push(` - ${v.rel}`);
49
+ if (victims.length > 10) lines.push(` ...等共 ${victims.length} 个文件`);
50
+
51
+ if (!yes) {
52
+ lines.push('');
53
+ lines.push('(预演模式:未做任何改动。确认无误请加 --yes 执行)');
54
+ return { lines, moved: 0, dryRun: true };
55
+ }
56
+
57
+ const dest = path.join(CODEX_DIR, 'archive', `${target}-${timestamp()}`);
58
+ let moved = 0;
59
+ for (const v of victims) {
60
+ const to = path.join(dest, v.rel);
61
+ try {
62
+ ensureDir(path.dirname(to));
63
+ fs.renameSync(v.f, to);
64
+ moved++;
65
+ } catch (e) {
66
+ lines.push(` 跳过 ${v.rel}: ${e.code || e.message}(文件可能正被 codex 使用,关闭 codex 后重试)`);
67
+ }
68
+ }
69
+ pruneEmptyDirs(dir);
70
+ lines.push(`已归档 ${moved} 个文件 → ${dest}`);
71
+ return { lines, moved };
72
+ }
package/tool/cli.mjs ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ // codex-doctor CLI 入口:codex-doctor <command> [options]
3
+ import { collectChecks, summarize, renderHuman } from './checks.mjs';
4
+ import { cleanTarget } from './clean.mjs';
5
+ import { backupConfig, restoreBackup, resetAuth, listVersions, listArchives, deleteArchive, checkUpdate } from './ops.mjs';
6
+
7
+ const VERSION = '0.2.0';
8
+
9
+ const HELP = `codex-doctor v${VERSION} — Codex CLI 维护与排障工具(零依赖)
10
+
11
+ 用法: codex-doctor <command> [options]
12
+
13
+ 命令:
14
+ doctor 全套环境自检
15
+ --no-network 跳过网络探测
16
+ --json 输出 JSON(供脚本消费)
17
+ --strict 有 WARN 也返回非零退出码
18
+ clean <sessions|logs> 归档超过 N 天的会话/日志(默认预演,--yes 才执行)
19
+ --days N 阈值天数(sessions 默认 30,logs 默认 14)
20
+ --yes 真正执行(否则仅预演)
21
+ backup [--out DIR] 备份 config.toml + auth.json 到带时间戳目录
22
+ restore <dir> 从备份目录恢复
23
+ auth reset 备份并删除 auth.json,引导重新登录(401 终极大招)
24
+ archive list 查看归档目录与体积
25
+ archive delete <名称|--all> 删除归档(需 --yes 或交互确认)
26
+ versions [-n N] 查看 openai/codex 最近 N 个版本(默认 10)
27
+ update 查询仓库最新发布与更新方式
28
+ help 显示本帮助
29
+
30
+ 全局: --yes 跳过交互确认(非 TTY 环境必须显式提供)。文档: docs/13-codex-doctor.md`;
31
+
32
+ function parseFlags(args) {
33
+ const flags = {};
34
+ const rest = [];
35
+ for (let i = 0; i < args.length; i++) {
36
+ const a = args[i];
37
+ if (a === '--yes') flags.yes = true;
38
+ else if (a === '--json') flags.json = true;
39
+ else if (a === '--no-network') flags.network = false;
40
+ else if (a === '--strict') flags.strict = true;
41
+ else if (a === '--days') flags.days = Number(args[++i]);
42
+ else if (a === '-n' || a === '--limit') flags.limit = Number(args[++i]);
43
+ else if (a === '--out') flags.out = args[++i];
44
+ else rest.push(a);
45
+ }
46
+ return { flags, rest };
47
+ }
48
+
49
+ function print(lines) {
50
+ for (const l of lines) console.log(l);
51
+ }
52
+
53
+ async function main() {
54
+ const cmd = process.argv[2] || 'help';
55
+ const { flags, rest } = parseFlags(process.argv.slice(3));
56
+
57
+ switch (cmd) {
58
+ case 'doctor': {
59
+ const results = await collectAndRun(flags);
60
+ break;
61
+ }
62
+ case 'clean': {
63
+ const target = rest[0];
64
+ if (target !== 'sessions' && target !== 'logs') {
65
+ console.error('用法: codex-doctor clean <sessions|logs> [--days N] [--yes]');
66
+ process.exitCode = 1;
67
+ break;
68
+ }
69
+ const defaultDays = target === 'logs' ? 14 : 30;
70
+ const days = Number.isFinite(flags.days) && flags.days > 0 ? flags.days : defaultDays;
71
+ const r = cleanTarget({ target, days, yes: flags.yes === true });
72
+ print(r.lines);
73
+ break;
74
+ }
75
+ case 'backup': {
76
+ const r = backupConfig(flags.out);
77
+ print(r.lines);
78
+ if (!r.ok) process.exitCode = 1;
79
+ break;
80
+ }
81
+ case 'restore': {
82
+ const r = restoreBackup(rest[0]);
83
+ print(r.lines);
84
+ if (!r.ok) process.exitCode = 1;
85
+ break;
86
+ }
87
+ case 'auth': {
88
+ if (rest[0] !== 'reset') {
89
+ console.error('用法: codex-doctor auth reset [--yes]');
90
+ process.exitCode = 1;
91
+ break;
92
+ }
93
+ const r = await resetAuth(flags.yes === true);
94
+ print(r.lines);
95
+ break;
96
+ }
97
+ case 'versions': {
98
+ const rels = await listVersions(flags.limit);
99
+ console.log('版本 日期 预发布');
100
+ for (const r of rels) {
101
+ console.log(`${r.tag.padEnd(24)} ${r.date} ${r.prerelease ? '是' : ''}`);
102
+ }
103
+ break;
104
+ }
105
+ case 'archive': {
106
+ const sub = rest[0];
107
+ if (sub === 'list') {
108
+ const r = listArchives();
109
+ if (r.items.length > 0) {
110
+ for (const it of r.items) {
111
+ console.log(`${it.name.padEnd(36)} ${(it.bytes / 1024 / 1024).toFixed(1).padStart(8)} MB ${it.files} 个文件`);
112
+ }
113
+ } else {
114
+ print(r.lines);
115
+ }
116
+ break;
117
+ }
118
+ if (sub === 'delete') {
119
+ const r = await deleteArchive(rest[1], { all: rest[1] === '--all', yes: flags.yes === true });
120
+ print(r.lines);
121
+ if (r.bad) process.exitCode = 1;
122
+ break;
123
+ }
124
+ console.error('用法: codex-doctor archive <list|delete <名称|--all>> [--yes]');
125
+ process.exitCode = 1;
126
+ break;
127
+ }
128
+ case 'update': {
129
+ const r = await checkUpdate(VERSION);
130
+ print(r.lines);
131
+ break;
132
+ }
133
+ case 'help':
134
+ case '--help':
135
+ case '-h':
136
+ case '-v':
137
+ case '--version':
138
+ console.log(HELP);
139
+ break;
140
+ default:
141
+ console.error(`未知命令: ${cmd}\n`);
142
+ console.log(HELP);
143
+ process.exitCode = 1;
144
+ }
145
+ }
146
+
147
+ async function collectAndRun(flags) {
148
+ const results = await collectChecks({ network: flags.network !== false });
149
+ const summary = summarize(results);
150
+ if (flags.json) {
151
+ console.log(JSON.stringify({ results, summary }, null, 2));
152
+ } else {
153
+ console.log(renderHuman(results, summary));
154
+ }
155
+ if (summary.fail > 0 || (flags.strict && summary.warn > 0)) process.exitCode = 1;
156
+ return { results, summary };
157
+ }
158
+
159
+ main().catch((e) => {
160
+ console.error(`出错: ${e?.message || e}`);
161
+ process.exitCode = 1;
162
+ });