github-search-cli 9.4.2 → 9.4.4
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/CHANGELOG.md +38 -4
- package/README.md +8 -3
- package/bin/ghfind.js +98 -89
- package/completions/ghfind.bash +1 -1
- package/completions/ghfind.fish +3 -1
- package/completions/ghfind.zsh +6 -0
- package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
- package/dist/cli.js +761 -342
- package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
- package/package.json +6 -3
- package/release-notes/9.3.0.md +32 -29
- package/scripts/build.ts +34 -19
- package/scripts/generate-svg-demo.js +218 -168
- package/scripts/postinstall.js +176 -176
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +17 -1
- package/src/compare.ts +69 -47
- package/src/config.ts +70 -65
- package/src/deepdive.ts +149 -118
- package/src/doctor.ts +287 -287
- package/src/errors.ts +62 -59
- package/src/explore.ts +34 -29
- package/src/export.ts +73 -63
- package/src/help.ts +157 -157
- package/src/history.ts +69 -55
- package/src/init.ts +60 -55
- package/src/landing.ts +106 -104
- package/src/login.ts +186 -0
- package/src/markdown-render.ts +1 -6
- package/src/notifications.ts +75 -59
- package/src/open-url.ts +16 -16
- package/src/org.ts +43 -15
- package/src/output.ts +153 -137
- package/src/package-browser.ts +317 -305
- package/src/package.ts +149 -137
- package/src/pipe.ts +60 -54
- package/src/releases.ts +182 -144
- package/src/saved-searches.ts +72 -50
- package/src/search-state.ts +283 -229
- package/src/search.ts +678 -653
- package/src/session.ts +26 -24
- package/src/share.ts +61 -54
- package/src/status.ts +68 -61
- package/src/storage.ts +136 -106
- package/src/themes.ts +259 -116
- package/src/tips.ts +42 -42
- package/src/toast.ts +70 -69
- package/src/trending-parser.ts +104 -0
- package/src/trending.ts +481 -462
- package/src/tui.ts +149 -153
- package/src/types.ts +160 -158
- package/src/watch.ts +95 -74
package/src/doctor.ts
CHANGED
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `ghfind --doctor` — environment diagnostics for troubleshooting
|
|
3
|
-
* installation / launch problems (missing Bun, wrong libc build, bad config).
|
|
4
|
-
*
|
|
5
|
-
* Runs a battery of checks and prints a PASS / WARN / FAIL report, then exits
|
|
6
|
-
* non-zero if any FAIL check was detected.
|
|
7
|
-
*/
|
|
8
|
-
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
9
|
-
import { spawnSync } from "child_process";
|
|
10
|
-
import { join, dirname } from "path";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
12
|
-
import { homedir, platform, arch } from "os";
|
|
13
|
-
import { loadConfig, configPath, stateDir, cacheDir } from "./config";
|
|
14
|
-
|
|
15
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const PKG_DIR = join(__dirname, "..");
|
|
17
|
-
|
|
18
|
-
type CheckStatus = "PASS" | "WARN" | "FAIL";
|
|
19
|
-
interface CheckResult {
|
|
20
|
-
status: CheckStatus;
|
|
21
|
-
label: string;
|
|
22
|
-
detail: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const results: CheckResult[] = [];
|
|
26
|
-
|
|
27
|
-
function add(status: CheckStatus, label: string, detail: string): void {
|
|
28
|
-
results.push({ status, label, detail });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function run(
|
|
32
|
-
cmd: string,
|
|
33
|
-
args: string[],
|
|
34
|
-
): { status: number | null; stdout: string; stderr: string } {
|
|
35
|
-
try {
|
|
36
|
-
const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 15000 });
|
|
37
|
-
return {
|
|
38
|
-
status: res.status,
|
|
39
|
-
stdout: res.stdout ?? "",
|
|
40
|
-
stderr: res.stderr ?? "",
|
|
41
|
-
};
|
|
42
|
-
} catch {
|
|
43
|
-
return { status: null, stdout: "", stderr: "" };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function hasSystemBun(): { ok: boolean; version?: string } {
|
|
48
|
-
const res = run("bun", ["--version"]);
|
|
49
|
-
return res.status === 0
|
|
50
|
-
? { ok: true, version: res.stdout.trim() }
|
|
51
|
-
: { ok: false };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function bundledBunPath(): string {
|
|
55
|
-
return join(PKG_DIR, "vendor", platform() === "win32" ? "bun.exe" : "bun");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Verify a bun binary actually runs (wrong libc flavor → ENOENT / segfault). */
|
|
59
|
-
function bunRuns(bin: string): { ok: boolean; version?: string } {
|
|
60
|
-
const res = run(bin, ["--version"]);
|
|
61
|
-
if (res.status !== 0) return { ok: false };
|
|
62
|
-
return { ok: true, version: res.stdout.trim() };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function detectLibc(): "musl" | "glibc" | "unknown" {
|
|
66
|
-
if (platform() !== "linux") return "unknown";
|
|
67
|
-
try {
|
|
68
|
-
const osRelease = readFileSync("/etc/os-release", "utf8");
|
|
69
|
-
if (/alpine|musl/i.test(osRelease)) return "musl";
|
|
70
|
-
} catch {
|
|
71
|
-
// ignore
|
|
72
|
-
}
|
|
73
|
-
const ldd = run("ldd", ["--version"]);
|
|
74
|
-
const out = `${ldd.stdout}${ldd.stderr}`;
|
|
75
|
-
if (/musl/i.test(out)) return "musl";
|
|
76
|
-
if (/glibc|GNU libc/i.test(out)) return "glibc";
|
|
77
|
-
return "unknown";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function checkRuntime(): Promise<void> {
|
|
81
|
-
const runningOnBun = process.versions.bun !== undefined;
|
|
82
|
-
const runtime = runningOnBun
|
|
83
|
-
? `Bun ${process.versions.bun}`
|
|
84
|
-
: `Node ${process.version}`;
|
|
85
|
-
add(
|
|
86
|
-
runningOnBun ? "PASS" : "WARN",
|
|
87
|
-
"Runtime",
|
|
88
|
-
`Running on ${runtime}. The interactive TUI needs Bun.`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function checkBundledBun(): { ok: boolean } {
|
|
93
|
-
const path = bundledBunPath();
|
|
94
|
-
if (!existsSync(path)) {
|
|
95
|
-
add(
|
|
96
|
-
"WARN",
|
|
97
|
-
"Bundled Bun",
|
|
98
|
-
`Not found (${path}). Run \`npm install\` or install Bun manually.`,
|
|
99
|
-
);
|
|
100
|
-
return { ok: false };
|
|
101
|
-
}
|
|
102
|
-
const check = bunRuns(path);
|
|
103
|
-
if (check.ok) {
|
|
104
|
-
add("PASS", "Bundled Bun", `${path} (v${check.version})`);
|
|
105
|
-
return { ok: true };
|
|
106
|
-
}
|
|
107
|
-
const libc = detectLibc();
|
|
108
|
-
const hint =
|
|
109
|
-
libc === "musl"
|
|
110
|
-
? "Installed Bun is the glibc build, but this system uses musl."
|
|
111
|
-
: libc === "glibc"
|
|
112
|
-
? "Installed Bun is the musl build, but this system uses glibc."
|
|
113
|
-
: "The bundled binary does not launch.";
|
|
114
|
-
add("FAIL", "Bundled Bun", `${path} does not launch. ${hint}`);
|
|
115
|
-
return { ok: false };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function checkSystemBun(bundledOk: boolean): void {
|
|
119
|
-
const sys = hasSystemBun();
|
|
120
|
-
if (sys.ok) {
|
|
121
|
-
add("PASS", "System Bun", `Found \`bun\` on PATH (v${sys.version}).`);
|
|
122
|
-
} else if (bundledOk) {
|
|
123
|
-
add(
|
|
124
|
-
"WARN",
|
|
125
|
-
"System Bun",
|
|
126
|
-
"No \`bun\` on PATH. Bundled Bun is working, so this is only a fallback.",
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
add(
|
|
130
|
-
"FAIL",
|
|
131
|
-
"System Bun",
|
|
132
|
-
"No \`bun\` on PATH. Install it: curl -fsSL https://bun.sh/install | bash",
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function checkNodeVersion(): void {
|
|
138
|
-
const major = parseInt(process.version.replace(/^v/, "").split(".")[0], 10);
|
|
139
|
-
if (Number.isNaN(major) || major >= 20) {
|
|
140
|
-
add("PASS", "Node version", `Node ${process.version} (>= 20 required)`);
|
|
141
|
-
} else {
|
|
142
|
-
add(
|
|
143
|
-
"FAIL",
|
|
144
|
-
"Node version",
|
|
145
|
-
`Node ${process.version} is too old (>= 20 required)`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function checkBuildOutput(): void {
|
|
151
|
-
const dist = join(PKG_DIR, "dist", "cli.js");
|
|
152
|
-
if (existsSync(dist)) {
|
|
153
|
-
add("PASS", "Build output", `${dist}`);
|
|
154
|
-
} else {
|
|
155
|
-
add(
|
|
156
|
-
"WARN",
|
|
157
|
-
"Build output",
|
|
158
|
-
"dist/cli.js not found. Run `npm run build` (Node-only fallback needs it).",
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function checkSource(): void {
|
|
164
|
-
const src = join(PKG_DIR, "src", "cli.ts");
|
|
165
|
-
if (existsSync(src)) {
|
|
166
|
-
add("PASS", "Source", `${src}`);
|
|
167
|
-
} else {
|
|
168
|
-
add("FAIL", "Source", "src/cli.ts not found. Reinstall the package.");
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function checkTuiBackend(): void {
|
|
173
|
-
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
174
|
-
add(
|
|
175
|
-
isTTY ? "PASS" : "WARN",
|
|
176
|
-
"Terminal (TTY)",
|
|
177
|
-
isTTY
|
|
178
|
-
? "stdin/stdout are TTYs — TUI can render."
|
|
179
|
-
: "stdin/stdout are not TTYs — the TUI cannot render (piped/non-interactive).",
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function checkConfig(): void {
|
|
184
|
-
const path = configPath();
|
|
185
|
-
if (!existsSync(path)) {
|
|
186
|
-
add(
|
|
187
|
-
"WARN",
|
|
188
|
-
"Config",
|
|
189
|
-
`Not found (${path}). Run \`ghfind init\` or it will use defaults.`,
|
|
190
|
-
);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
try {
|
|
194
|
-
const raw = readFileSync(path, "utf-8");
|
|
195
|
-
JSON.parse(raw);
|
|
196
|
-
add("PASS", "Config", `${path} (valid JSON)`);
|
|
197
|
-
} catch (err) {
|
|
198
|
-
add(
|
|
199
|
-
"FAIL",
|
|
200
|
-
"Config",
|
|
201
|
-
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function checkGithubToken(): void {
|
|
207
|
-
const config = loadConfig();
|
|
208
|
-
const token = config.githubToken || process.env.GITHUB_TOKEN;
|
|
209
|
-
if (token) {
|
|
210
|
-
const preview = token.slice(0, 7);
|
|
211
|
-
add("PASS", "GitHub token", `Configured (${preview}…).`);
|
|
212
|
-
} else {
|
|
213
|
-
add(
|
|
214
|
-
"WARN",
|
|
215
|
-
"GitHub token",
|
|
216
|
-
"No token found. Unauthenticated requests hit lower rate limits.",
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function checkDirs(): void {
|
|
222
|
-
for (const [label, dir] of [
|
|
223
|
-
["State dir", stateDir()],
|
|
224
|
-
["Cache dir", cacheDir()],
|
|
225
|
-
] as const) {
|
|
226
|
-
try {
|
|
227
|
-
mkdirSync(dir, { recursive: true });
|
|
228
|
-
add("PASS", label, dir);
|
|
229
|
-
} catch (err) {
|
|
230
|
-
add(
|
|
231
|
-
"FAIL",
|
|
232
|
-
label,
|
|
233
|
-
`${dir} not writable: ${err instanceof Error ? err.message : String(err)}`,
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function printReport(): void {
|
|
240
|
-
const colors = (s: string, n: CheckStatus) => {
|
|
241
|
-
const code =
|
|
242
|
-
n === "PASS" ? "\x1b[32m" : n === "WARN" ? "\x1b[33m" : "\x1b[31m";
|
|
243
|
-
return `${code}${s}\x1b[0m`;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
console.log("\n ghfind doctor — environment diagnostics");
|
|
247
|
-
console.log(` ${platform()} ${arch} · ${homedir()}`);
|
|
248
|
-
console.log(" ─────────────────────────────────────────");
|
|
249
|
-
for (const r of results) {
|
|
250
|
-
console.log(
|
|
251
|
-
` ${colors(r.status.padEnd(4), r.status)} ${r.label}: ${r.detail}`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
console.log(" ─────────────────────────────────────────");
|
|
255
|
-
|
|
256
|
-
const fails = results.filter((r) => r.status === "FAIL").length;
|
|
257
|
-
const warns = results.filter((r) => r.status === "WARN").length;
|
|
258
|
-
if (fails === 0 && warns === 0) {
|
|
259
|
-
console.log(" All checks passed.");
|
|
260
|
-
} else if (fails === 0) {
|
|
261
|
-
console.log(
|
|
262
|
-
` ${warns} warning(s) — ghfind should work, but review the items above.`,
|
|
263
|
-
);
|
|
264
|
-
} else {
|
|
265
|
-
console.log(
|
|
266
|
-
` ${fails} failed check(s). Resolve these before running the TUI.`,
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
console.log("");
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export async function runDoctor(): Promise<void> {
|
|
273
|
-
await checkRuntime();
|
|
274
|
-
const bundled = checkBundledBun();
|
|
275
|
-
checkSystemBun(bundled.ok);
|
|
276
|
-
checkNodeVersion();
|
|
277
|
-
checkBuildOutput();
|
|
278
|
-
checkSource();
|
|
279
|
-
checkTuiBackend();
|
|
280
|
-
checkConfig();
|
|
281
|
-
checkGithubToken();
|
|
282
|
-
checkDirs();
|
|
283
|
-
printReport();
|
|
284
|
-
|
|
285
|
-
const hasFail = results.some((r) => r.status === "FAIL");
|
|
286
|
-
process.exitCode = hasFail ? 1 : 0;
|
|
287
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `ghfind --doctor` — environment diagnostics for troubleshooting
|
|
3
|
+
* installation / launch problems (missing Bun, wrong libc build, bad config).
|
|
4
|
+
*
|
|
5
|
+
* Runs a battery of checks and prints a PASS / WARN / FAIL report, then exits
|
|
6
|
+
* non-zero if any FAIL check was detected.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
9
|
+
import { spawnSync } from "child_process";
|
|
10
|
+
import { join, dirname } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { homedir, platform, arch } from "os";
|
|
13
|
+
import { loadConfig, configPath, stateDir, cacheDir } from "./config";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const PKG_DIR = join(__dirname, "..");
|
|
17
|
+
|
|
18
|
+
type CheckStatus = "PASS" | "WARN" | "FAIL";
|
|
19
|
+
interface CheckResult {
|
|
20
|
+
status: CheckStatus;
|
|
21
|
+
label: string;
|
|
22
|
+
detail: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const results: CheckResult[] = [];
|
|
26
|
+
|
|
27
|
+
function add(status: CheckStatus, label: string, detail: string): void {
|
|
28
|
+
results.push({ status, label, detail });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function run(
|
|
32
|
+
cmd: string,
|
|
33
|
+
args: string[],
|
|
34
|
+
): { status: number | null; stdout: string; stderr: string } {
|
|
35
|
+
try {
|
|
36
|
+
const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 15000 });
|
|
37
|
+
return {
|
|
38
|
+
status: res.status,
|
|
39
|
+
stdout: res.stdout ?? "",
|
|
40
|
+
stderr: res.stderr ?? "",
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return { status: null, stdout: "", stderr: "" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasSystemBun(): { ok: boolean; version?: string } {
|
|
48
|
+
const res = run("bun", ["--version"]);
|
|
49
|
+
return res.status === 0
|
|
50
|
+
? { ok: true, version: res.stdout.trim() }
|
|
51
|
+
: { ok: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function bundledBunPath(): string {
|
|
55
|
+
return join(PKG_DIR, "vendor", platform() === "win32" ? "bun.exe" : "bun");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Verify a bun binary actually runs (wrong libc flavor → ENOENT / segfault). */
|
|
59
|
+
function bunRuns(bin: string): { ok: boolean; version?: string } {
|
|
60
|
+
const res = run(bin, ["--version"]);
|
|
61
|
+
if (res.status !== 0) return { ok: false };
|
|
62
|
+
return { ok: true, version: res.stdout.trim() };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function detectLibc(): "musl" | "glibc" | "unknown" {
|
|
66
|
+
if (platform() !== "linux") return "unknown";
|
|
67
|
+
try {
|
|
68
|
+
const osRelease = readFileSync("/etc/os-release", "utf8");
|
|
69
|
+
if (/alpine|musl/i.test(osRelease)) return "musl";
|
|
70
|
+
} catch {
|
|
71
|
+
// ignore
|
|
72
|
+
}
|
|
73
|
+
const ldd = run("ldd", ["--version"]);
|
|
74
|
+
const out = `${ldd.stdout}${ldd.stderr}`;
|
|
75
|
+
if (/musl/i.test(out)) return "musl";
|
|
76
|
+
if (/glibc|GNU libc/i.test(out)) return "glibc";
|
|
77
|
+
return "unknown";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function checkRuntime(): Promise<void> {
|
|
81
|
+
const runningOnBun = process.versions.bun !== undefined;
|
|
82
|
+
const runtime = runningOnBun
|
|
83
|
+
? `Bun ${process.versions.bun}`
|
|
84
|
+
: `Node ${process.version}`;
|
|
85
|
+
add(
|
|
86
|
+
runningOnBun ? "PASS" : "WARN",
|
|
87
|
+
"Runtime",
|
|
88
|
+
`Running on ${runtime}. The interactive TUI needs Bun.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function checkBundledBun(): { ok: boolean } {
|
|
93
|
+
const path = bundledBunPath();
|
|
94
|
+
if (!existsSync(path)) {
|
|
95
|
+
add(
|
|
96
|
+
"WARN",
|
|
97
|
+
"Bundled Bun",
|
|
98
|
+
`Not found (${path}). Run \`npm install\` or install Bun manually.`,
|
|
99
|
+
);
|
|
100
|
+
return { ok: false };
|
|
101
|
+
}
|
|
102
|
+
const check = bunRuns(path);
|
|
103
|
+
if (check.ok) {
|
|
104
|
+
add("PASS", "Bundled Bun", `${path} (v${check.version})`);
|
|
105
|
+
return { ok: true };
|
|
106
|
+
}
|
|
107
|
+
const libc = detectLibc();
|
|
108
|
+
const hint =
|
|
109
|
+
libc === "musl"
|
|
110
|
+
? "Installed Bun is the glibc build, but this system uses musl."
|
|
111
|
+
: libc === "glibc"
|
|
112
|
+
? "Installed Bun is the musl build, but this system uses glibc."
|
|
113
|
+
: "The bundled binary does not launch.";
|
|
114
|
+
add("FAIL", "Bundled Bun", `${path} does not launch. ${hint}`);
|
|
115
|
+
return { ok: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function checkSystemBun(bundledOk: boolean): void {
|
|
119
|
+
const sys = hasSystemBun();
|
|
120
|
+
if (sys.ok) {
|
|
121
|
+
add("PASS", "System Bun", `Found \`bun\` on PATH (v${sys.version}).`);
|
|
122
|
+
} else if (bundledOk) {
|
|
123
|
+
add(
|
|
124
|
+
"WARN",
|
|
125
|
+
"System Bun",
|
|
126
|
+
"No \`bun\` on PATH. Bundled Bun is working, so this is only a fallback.",
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
add(
|
|
130
|
+
"FAIL",
|
|
131
|
+
"System Bun",
|
|
132
|
+
"No \`bun\` on PATH. Install it: curl -fsSL https://bun.sh/install | bash",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function checkNodeVersion(): void {
|
|
138
|
+
const major = parseInt(process.version.replace(/^v/, "").split(".")[0], 10);
|
|
139
|
+
if (Number.isNaN(major) || major >= 20) {
|
|
140
|
+
add("PASS", "Node version", `Node ${process.version} (>= 20 required)`);
|
|
141
|
+
} else {
|
|
142
|
+
add(
|
|
143
|
+
"FAIL",
|
|
144
|
+
"Node version",
|
|
145
|
+
`Node ${process.version} is too old (>= 20 required)`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function checkBuildOutput(): void {
|
|
151
|
+
const dist = join(PKG_DIR, "dist", "cli.js");
|
|
152
|
+
if (existsSync(dist)) {
|
|
153
|
+
add("PASS", "Build output", `${dist}`);
|
|
154
|
+
} else {
|
|
155
|
+
add(
|
|
156
|
+
"WARN",
|
|
157
|
+
"Build output",
|
|
158
|
+
"dist/cli.js not found. Run `npm run build` (Node-only fallback needs it).",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function checkSource(): void {
|
|
164
|
+
const src = join(PKG_DIR, "src", "cli.ts");
|
|
165
|
+
if (existsSync(src)) {
|
|
166
|
+
add("PASS", "Source", `${src}`);
|
|
167
|
+
} else {
|
|
168
|
+
add("FAIL", "Source", "src/cli.ts not found. Reinstall the package.");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkTuiBackend(): void {
|
|
173
|
+
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
174
|
+
add(
|
|
175
|
+
isTTY ? "PASS" : "WARN",
|
|
176
|
+
"Terminal (TTY)",
|
|
177
|
+
isTTY
|
|
178
|
+
? "stdin/stdout are TTYs — TUI can render."
|
|
179
|
+
: "stdin/stdout are not TTYs — the TUI cannot render (piped/non-interactive).",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function checkConfig(): void {
|
|
184
|
+
const path = configPath();
|
|
185
|
+
if (!existsSync(path)) {
|
|
186
|
+
add(
|
|
187
|
+
"WARN",
|
|
188
|
+
"Config",
|
|
189
|
+
`Not found (${path}). Run \`ghfind init\` or it will use defaults.`,
|
|
190
|
+
);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const raw = readFileSync(path, "utf-8");
|
|
195
|
+
JSON.parse(raw);
|
|
196
|
+
add("PASS", "Config", `${path} (valid JSON)`);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
add(
|
|
199
|
+
"FAIL",
|
|
200
|
+
"Config",
|
|
201
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkGithubToken(): void {
|
|
207
|
+
const config = loadConfig();
|
|
208
|
+
const token = config.githubToken || process.env.GITHUB_TOKEN;
|
|
209
|
+
if (token) {
|
|
210
|
+
const preview = token.slice(0, 7);
|
|
211
|
+
add("PASS", "GitHub token", `Configured (${preview}…).`);
|
|
212
|
+
} else {
|
|
213
|
+
add(
|
|
214
|
+
"WARN",
|
|
215
|
+
"GitHub token",
|
|
216
|
+
"No token found. Unauthenticated requests hit lower rate limits.",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function checkDirs(): void {
|
|
222
|
+
for (const [label, dir] of [
|
|
223
|
+
["State dir", stateDir()],
|
|
224
|
+
["Cache dir", cacheDir()],
|
|
225
|
+
] as const) {
|
|
226
|
+
try {
|
|
227
|
+
mkdirSync(dir, { recursive: true });
|
|
228
|
+
add("PASS", label, dir);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
add(
|
|
231
|
+
"FAIL",
|
|
232
|
+
label,
|
|
233
|
+
`${dir} not writable: ${err instanceof Error ? err.message : String(err)}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function printReport(): void {
|
|
240
|
+
const colors = (s: string, n: CheckStatus) => {
|
|
241
|
+
const code =
|
|
242
|
+
n === "PASS" ? "\x1b[32m" : n === "WARN" ? "\x1b[33m" : "\x1b[31m";
|
|
243
|
+
return `${code}${s}\x1b[0m`;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
console.log("\n ghfind doctor — environment diagnostics");
|
|
247
|
+
console.log(` ${platform()} ${arch} · ${homedir()}`);
|
|
248
|
+
console.log(" ─────────────────────────────────────────");
|
|
249
|
+
for (const r of results) {
|
|
250
|
+
console.log(
|
|
251
|
+
` ${colors(r.status.padEnd(4), r.status)} ${r.label}: ${r.detail}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
console.log(" ─────────────────────────────────────────");
|
|
255
|
+
|
|
256
|
+
const fails = results.filter((r) => r.status === "FAIL").length;
|
|
257
|
+
const warns = results.filter((r) => r.status === "WARN").length;
|
|
258
|
+
if (fails === 0 && warns === 0) {
|
|
259
|
+
console.log(" All checks passed.");
|
|
260
|
+
} else if (fails === 0) {
|
|
261
|
+
console.log(
|
|
262
|
+
` ${warns} warning(s) — ghfind should work, but review the items above.`,
|
|
263
|
+
);
|
|
264
|
+
} else {
|
|
265
|
+
console.log(
|
|
266
|
+
` ${fails} failed check(s). Resolve these before running the TUI.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
console.log("");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function runDoctor(): Promise<void> {
|
|
273
|
+
await checkRuntime();
|
|
274
|
+
const bundled = checkBundledBun();
|
|
275
|
+
checkSystemBun(bundled.ok);
|
|
276
|
+
checkNodeVersion();
|
|
277
|
+
checkBuildOutput();
|
|
278
|
+
checkSource();
|
|
279
|
+
checkTuiBackend();
|
|
280
|
+
checkConfig();
|
|
281
|
+
checkGithubToken();
|
|
282
|
+
checkDirs();
|
|
283
|
+
printReport();
|
|
284
|
+
|
|
285
|
+
const hasFail = results.some((r) => r.status === "FAIL");
|
|
286
|
+
process.exitCode = hasFail ? 1 : 0;
|
|
287
|
+
}
|