@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.
- package/LICENSE +21 -0
- package/README.en.md +86 -0
- package/README.md +132 -0
- package/docs/01-installation.en.md +102 -0
- package/docs/01-installation.md +101 -0
- package/docs/02-login-auth.en.md +121 -0
- package/docs/02-login-auth.md +121 -0
- package/docs/03-network-proxy.en.md +96 -0
- package/docs/03-network-proxy.md +96 -0
- package/docs/04-config.en.md +118 -0
- package/docs/04-config.md +120 -0
- package/docs/05-models-limits.en.md +51 -0
- package/docs/05-models-limits.md +51 -0
- package/docs/06-sandbox-windows.en.md +74 -0
- package/docs/06-sandbox-windows.md +74 -0
- package/docs/07-mcp.en.md +55 -0
- package/docs/07-mcp.md +57 -0
- package/docs/08-errors-quickref.en.md +53 -0
- package/docs/08-errors-quickref.md +55 -0
- package/docs/09-maintenance.en.md +77 -0
- package/docs/09-maintenance.md +78 -0
- package/docs/10-ide-vscode.en.md +63 -0
- package/docs/10-ide-vscode.md +63 -0
- package/docs/11-tips.en.md +51 -0
- package/docs/11-tips.md +51 -0
- package/docs/12-walkthrough.en.md +79 -0
- package/docs/12-walkthrough.md +79 -0
- package/docs/13-codex-doctor.en.md +63 -0
- package/docs/13-codex-doctor.md +63 -0
- package/docs/releases.md +59 -0
- package/package.json +25 -0
- package/tool/checks.mjs +335 -0
- package/tool/clean.mjs +72 -0
- package/tool/cli.mjs +162 -0
- package/tool/ops.mjs +179 -0
- package/tool/util.mjs +55 -0
package/tool/ops.mjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// ops:备份/恢复、auth 重置、版本追踪、归档管理、更新检查
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { BACKUP_DIR, CODEX_DIR, ensureDir, exists, timestamp, confirm, dirBytes } from './util.mjs';
|
|
5
|
+
|
|
6
|
+
const CRITICAL_FILES = ['config.toml', 'auth.json'];
|
|
7
|
+
|
|
8
|
+
export function backupConfig(out) {
|
|
9
|
+
if (!exists(CODEX_DIR)) return { ok: false, lines: ['~/.codex 不存在,无需备份'] };
|
|
10
|
+
const dir = out ? path.resolve(out) : path.join(BACKUP_DIR, timestamp());
|
|
11
|
+
ensureDir(dir);
|
|
12
|
+
const copied = [];
|
|
13
|
+
for (const name of CRITICAL_FILES) {
|
|
14
|
+
const src = path.join(CODEX_DIR, name);
|
|
15
|
+
if (exists(src)) {
|
|
16
|
+
fs.copyFileSync(src, path.join(dir, name));
|
|
17
|
+
copied.push(name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const lines = [];
|
|
21
|
+
if (copied.length === 0) {
|
|
22
|
+
lines.push('~/.codex 里没有 config.toml / auth.json 可备份');
|
|
23
|
+
return { ok: false, lines };
|
|
24
|
+
}
|
|
25
|
+
lines.push(`已备份 ${copied.join(', ')} → ${dir}`);
|
|
26
|
+
if (copied.includes('auth.json')) {
|
|
27
|
+
lines.push('注意:auth.json 等同密码——备份目录请勿提交仓库或上传网盘明文。');
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, lines, dir };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function restoreBackup(dir) {
|
|
33
|
+
const src = path.resolve(String(dir || ''));
|
|
34
|
+
if (!exists(src)) return { ok: false, lines: [`备份目录不存在: ${src}`] };
|
|
35
|
+
const lines = [];
|
|
36
|
+
const restored = [];
|
|
37
|
+
for (const name of CRITICAL_FILES) {
|
|
38
|
+
const from = path.join(src, name);
|
|
39
|
+
if (exists(from)) {
|
|
40
|
+
fs.copyFileSync(from, path.join(CODEX_DIR, name));
|
|
41
|
+
restored.push(name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (restored.length === 0) {
|
|
45
|
+
return { ok: false, lines: [`目录里没有可恢复的文件(需要 ${CRITICAL_FILES.join(' / ')}): ${src}`] };
|
|
46
|
+
}
|
|
47
|
+
lines.push(`已恢复 ${restored.join(', ')} → ${CODEX_DIR}`);
|
|
48
|
+
if (restored.includes('auth.json')) {
|
|
49
|
+
lines.push('若凭据较旧导致 401,重新 codex login 一次即可。');
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, lines };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function resetAuth(yes) {
|
|
55
|
+
const auth = path.join(CODEX_DIR, 'auth.json');
|
|
56
|
+
if (!exists(auth)) {
|
|
57
|
+
return { lines: ['auth.json 不存在,无需重置——直接运行 codex login 即可'] };
|
|
58
|
+
}
|
|
59
|
+
if (!yes) {
|
|
60
|
+
const go = await confirm('将备份并删除 auth.json(之后需要重新 codex login),继续?');
|
|
61
|
+
if (!go) return { lines: ['已取消'] };
|
|
62
|
+
}
|
|
63
|
+
const dir = path.join(BACKUP_DIR, timestamp());
|
|
64
|
+
ensureDir(dir);
|
|
65
|
+
fs.copyFileSync(auth, path.join(dir, 'auth.json'));
|
|
66
|
+
fs.rmSync(auth);
|
|
67
|
+
return {
|
|
68
|
+
lines: [
|
|
69
|
+
`已备份并删除 auth.json(备份在 ${dir})`,
|
|
70
|
+
'下一步:运行 codex login 重新登录(401 排障详见 docs/02-login-auth.md)',
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function listVersions(limit = 10) {
|
|
76
|
+
const headers = { Accept: 'application/vnd.github+json', 'User-Agent': 'codex-doctor-cli' };
|
|
77
|
+
if (process.env.GH_TOKEN) headers.Authorization = `Bearer ${process.env.GH_TOKEN}`;
|
|
78
|
+
const n = Math.min(Math.max(Number(limit) || 10, 1), 50);
|
|
79
|
+
const res = await fetch(`https://api.github.com/repos/openai/codex/releases?per_page=${n}`, { headers });
|
|
80
|
+
if (!res.ok) throw new Error(`GitHub API HTTP ${res.status}`);
|
|
81
|
+
const releases = (await res.json()).filter((r) => !r.draft);
|
|
82
|
+
return releases.slice(0, n).map((r) => ({
|
|
83
|
+
tag: r.tag_name,
|
|
84
|
+
date: (r.published_at || '').slice(0, 10) || '—',
|
|
85
|
+
prerelease: r.prerelease === true,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------- 归档管理 ----------
|
|
90
|
+
|
|
91
|
+
const ARCHIVE_ROOT = () => path.join(CODEX_DIR, 'archive');
|
|
92
|
+
|
|
93
|
+
export function listArchives() {
|
|
94
|
+
const root = ARCHIVE_ROOT();
|
|
95
|
+
if (!exists(root)) return { items: [], lines: [`暂无归档目录(${root})`] };
|
|
96
|
+
const items = [];
|
|
97
|
+
for (const name of fs.readdirSync(root)) {
|
|
98
|
+
const p = path.join(root, name);
|
|
99
|
+
if (!fs.statSync(p).isDirectory()) continue;
|
|
100
|
+
let files = 0;
|
|
101
|
+
(function w(x) {
|
|
102
|
+
for (const e of fs.readdirSync(x, { withFileTypes: true })) {
|
|
103
|
+
if (e.isDirectory()) w(path.join(x, e.name));
|
|
104
|
+
else files++;
|
|
105
|
+
}
|
|
106
|
+
})(p);
|
|
107
|
+
items.push({ name, bytes: dirBytes(p), files });
|
|
108
|
+
}
|
|
109
|
+
if (items.length === 0) return { items: [], lines: ['归档目录为空'] };
|
|
110
|
+
return { items, lines: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function deleteArchive(name, { all = false, yes = false } = {}) {
|
|
114
|
+
const root = path.resolve(ARCHIVE_ROOT());
|
|
115
|
+
const targets = [];
|
|
116
|
+
if (all) {
|
|
117
|
+
if (!exists(root)) return { lines: ['暂无归档可删除'] };
|
|
118
|
+
for (const n of fs.readdirSync(root)) {
|
|
119
|
+
const p = path.join(root, n);
|
|
120
|
+
if (fs.statSync(p).isDirectory()) targets.push({ name: n, p, bytes: dirBytes(p) });
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
if (!name) return { bad: true, lines: ['用法: codex-doctor archive delete <名称|--all>'] };
|
|
124
|
+
const p = path.resolve(root, String(name));
|
|
125
|
+
// 防目录穿越:目标必须仍在 archive 根内
|
|
126
|
+
if (!p.toLowerCase().startsWith(root.toLowerCase() + path.sep) || !exists(p) || !fs.statSync(p).isDirectory()) {
|
|
127
|
+
return { bad: true, lines: [`归档不存在: ${name}`] };
|
|
128
|
+
}
|
|
129
|
+
targets.push({ name: path.basename(p), p, bytes: dirBytes(p) });
|
|
130
|
+
}
|
|
131
|
+
if (targets.length === 0) return { lines: ['归档目录为空,无需删除'] };
|
|
132
|
+
|
|
133
|
+
const totalMB = targets.reduce((s, t) => s + t.bytes, 0) / 1024 / 1024;
|
|
134
|
+
const summary = `将删除 ${targets.length} 个归档目录(共约 ${totalMB.toFixed(1)} MB):${targets.map((t) => t.name).join(', ')}`;
|
|
135
|
+
if (!yes) {
|
|
136
|
+
const go = await confirm(`${summary},继续?`);
|
|
137
|
+
if (!go) return { lines: ['已取消'] };
|
|
138
|
+
}
|
|
139
|
+
const lines = [];
|
|
140
|
+
for (const t of targets) {
|
|
141
|
+
fs.rmSync(t.p, { recursive: true, force: true });
|
|
142
|
+
lines.push(`已删除 ${t.name}`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(`共释放约 ${totalMB.toFixed(1)} MB`);
|
|
145
|
+
return { lines };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------- 更新检查 ----------
|
|
149
|
+
|
|
150
|
+
function compareSemver(a, b) {
|
|
151
|
+
const pa = String(a).split('.').map(Number);
|
|
152
|
+
const pb = String(b).split('.').map(Number);
|
|
153
|
+
for (let i = 0; i < 3; i++) {
|
|
154
|
+
const x = pa[i] || 0;
|
|
155
|
+
const y = pb[i] || 0;
|
|
156
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function checkUpdate(currentVersion) {
|
|
162
|
+
const headers = { Accept: 'application/vnd.github+json', 'User-Agent': 'codex-doctor-cli' };
|
|
163
|
+
if (process.env.GH_TOKEN) headers.Authorization = `Bearer ${process.env.GH_TOKEN}`;
|
|
164
|
+
const res = await fetch('https://api.github.com/repos/qlw088697-ui/codex-troubleshooting/releases/latest', { headers });
|
|
165
|
+
if (!res.ok) throw new Error(`GitHub API HTTP ${res.status}`);
|
|
166
|
+
const r = await res.json();
|
|
167
|
+
const tag = r.tag_name || '';
|
|
168
|
+
const latest = tag.replace(/^v/, '');
|
|
169
|
+
const newer = latest ? compareSemver(currentVersion, latest) < 0 : false;
|
|
170
|
+
return {
|
|
171
|
+
lines: [
|
|
172
|
+
`当前工具版本: ${currentVersion}`,
|
|
173
|
+
`仓库最新发布: ${tag}(${(r.published_at || '').slice(0, 10)})`,
|
|
174
|
+
newer
|
|
175
|
+
? 'npx 直跑始终使用最新代码,无需操作;本地克隆请 git pull 后重跑。'
|
|
176
|
+
: '已是最新版本。',
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
package/tool/util.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 共享工具:路径、文件系统辅助、确认交互(零依赖,Node 标准库)
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
|
|
6
|
+
export const HOME = os.homedir();
|
|
7
|
+
export const CODEX_DIR = path.join(HOME, '.codex');
|
|
8
|
+
export const BACKUP_DIR = path.join(HOME, '.codex-backups');
|
|
9
|
+
|
|
10
|
+
export function exists(p) {
|
|
11
|
+
try {
|
|
12
|
+
fs.statSync(p);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ensureDir(p) {
|
|
20
|
+
fs.mkdirSync(p, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function timestamp() {
|
|
24
|
+
return new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function walkFiles(dir) {
|
|
28
|
+
const out = [];
|
|
29
|
+
if (!exists(dir)) return out;
|
|
30
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
31
|
+
const p = path.join(dir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) out.push(...walkFiles(p));
|
|
33
|
+
else if (entry.isFile()) out.push(p);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function dirBytes(dir) {
|
|
39
|
+
return walkFiles(dir).reduce((s, f) => s + fs.statSync(f).size, 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 破坏性操作的交互确认;非 TTY(如 CI)一律拒绝,必须显式 --yes
|
|
43
|
+
export async function confirm(question) {
|
|
44
|
+
if (!process.stdin.isTTY) return false;
|
|
45
|
+
const rl = (await import('node:readline/promises')).createInterface({
|
|
46
|
+
input: process.stdin,
|
|
47
|
+
output: process.stdout,
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
const ans = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
51
|
+
return ans === 'y' || ans === 'yes';
|
|
52
|
+
} finally {
|
|
53
|
+
rl.close();
|
|
54
|
+
}
|
|
55
|
+
}
|