github-search-cli 9.7.0 → 9.7.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.
- package/CHANGELOG.md +29 -0
- package/README.md +7 -5
- package/bin/ghfind.js +5 -5
- package/dist/cli.js +255 -110
- package/package.json +3 -2
- package/release-notes/9.7.1.md +62 -0
- package/scripts/build.ts +1 -1
- package/scripts/postinstall.js +8 -8
- package/src/cli.ts +21 -13
- package/src/compare.ts +5 -5
- package/src/config.ts +3 -3
- package/src/deepdive.ts +0 -1
- package/src/doctor.ts +144 -8
- package/src/error-report.ts +14 -8
- package/src/export.ts +1 -1
- package/src/history.ts +5 -5
- package/src/init.ts +2 -2
- package/src/login.ts +4 -4
- package/src/markdown-render.ts +13 -13
- package/src/mcp-server.ts +4 -3
- package/src/open-url.ts +16 -5
- package/src/output.ts +1 -1
- package/src/package-browser.ts +1 -1
- package/src/releases.ts +1 -14
- package/src/search-state.ts +1 -4
- package/src/search.ts +3 -4
- package/src/share.ts +2 -2
- package/src/storage.ts +4 -4
- package/src/themes.ts +1 -0
- package/src/trending-parser.ts +3 -4
- package/src/trending.ts +7 -7
- package/src/tui.ts +38 -42
- package/src/update-check.ts +96 -34
- package/src/version.ts +3 -3
- package/src/watch.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-search-cli",
|
|
3
|
-
"version": "9.7.
|
|
3
|
+
"version": "9.7.1",
|
|
4
4
|
"description": "Search GitHub from your terminal — repo search, trending repos, npm package search, bookmarks, deep-dive, compare, export. Keyboard-driven TUI + pipeable CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1",
|
|
@@ -37,13 +37,14 @@
|
|
|
37
37
|
"format:check": "prettier --check .",
|
|
38
38
|
"format": "prettier --write .",
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
|
-
"lint": "
|
|
40
|
+
"lint": "biome check .",
|
|
41
41
|
"check": "prettier --check . && bun test"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@opentui/core": "^0.4.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@biomejs/biome": "^2.5.14",
|
|
47
48
|
"@types/bun": "latest",
|
|
48
49
|
"@types/node": "^20.0.0",
|
|
49
50
|
"prettier": "^3.2.0",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# What's New in ghfind 9.7.1
|
|
2
|
+
|
|
3
|
+
The in-app update finally works — plus a Windows URL fix and a new doctor
|
|
4
|
+
check that names every stale install shadowing your updates.
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
**In-app update installed to the wrong location**
|
|
9
|
+
|
|
10
|
+
- When the TUI runs under Bun, `performUpdate()` ran `bun install -g
|
|
11
|
+
github-search-cli`, which targets Bun's own global dir (`~/.bun`) — not
|
|
12
|
+
the npm prefix where the running copy and its `ghfind` shim live. The
|
|
13
|
+
command exited 0, "update succeeded", and the running install never
|
|
14
|
+
changed.
|
|
15
|
+
- The updater now detects which package manager owns the running tree
|
|
16
|
+
(`detectInstallLocation`: npm global prefix vs `~/.bun` global) and
|
|
17
|
+
installs with `npm install -g --prefix <prefix>` for npm-owned installs,
|
|
18
|
+
so the update lands exactly where the running install is.
|
|
19
|
+
- Compiled binaries and dev checkouts (no `node_modules` ancestor) refuse
|
|
20
|
+
self-update honestly instead of npm-installing a copy you aren't running.
|
|
21
|
+
|
|
22
|
+
**Node fallback couldn't launch npm on Windows**
|
|
23
|
+
|
|
24
|
+
- `child_process.spawn` without a shell cannot execute `npm.cmd` — recent
|
|
25
|
+
Node rejects it outright with `EINVAL`. The update spawn now goes through
|
|
26
|
+
`node:child_process` on every runtime with `shell: true` on win32, so the
|
|
27
|
+
Node fallback path works on Windows too.
|
|
28
|
+
|
|
29
|
+
**Windows: URLs truncated at the first `&`**
|
|
30
|
+
|
|
31
|
+
- Opening URLs via `cmd /c start "" <url>` split the URL at the first
|
|
32
|
+
unquoted `&` — a query-string separator — and tried to execute the rest
|
|
33
|
+
as a command. Every pre-filled crash-report/issue link lost its body and
|
|
34
|
+
labels.
|
|
35
|
+
- Both URL openers now use `explorer.exe` on Windows, which receives the
|
|
36
|
+
URL as a single argument, so `&` is never interpreted.
|
|
37
|
+
|
|
38
|
+
**New `ghfind --doctor` check: stale/PATH-shadowing installs**
|
|
39
|
+
|
|
40
|
+
- Multiple `ghfind` copies on PATH resolving to different versions are the
|
|
41
|
+
classic "updater said OK but nothing changed" trap: the update lands in
|
|
42
|
+
one prefix while the shell keeps launching another. `ghfind --doctor` now
|
|
43
|
+
lists every install with its version and prints the exact
|
|
44
|
+
`npm uninstall -g github-search-cli --prefix <dir>` command to remove the
|
|
45
|
+
stale ones.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **CI enforces lint + typecheck** — the repo's first green Biome baseline
|
|
50
|
+
(198 diagnostics triaged, real `@biomejs/biome` 2.5.14 dependency replacing
|
|
51
|
+
the phantom `biome@2.5.13` pin) and a `.gitattributes` forcing LF so
|
|
52
|
+
`prettier --check` passes on Windows runners (main had been red on
|
|
53
|
+
windows-latest for weeks). Previously both gates were excluded as
|
|
54
|
+
known-broken; the typecheck-opentui-drift ticket is closed.
|
|
55
|
+
|
|
56
|
+
### Internal
|
|
57
|
+
|
|
58
|
+
- `open-url.ts` is runtime-agnostic with a detached Node fallback (it
|
|
59
|
+
previously crashed under Node via unconditional `Bun.spawn`).
|
|
60
|
+
- Housekeeping: `mcp-server.ts` assignment-in-expressions, `trending-parser.ts`
|
|
61
|
+
stateful regex loop → `matchAll`, typed casts replacing `as any`, and a
|
|
62
|
+
repaired unsafe-autofix regression in `TrendingAdapter`.
|
package/scripts/build.ts
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
chmodSync,
|
|
14
14
|
writeFileSync,
|
|
15
15
|
readFileSync,
|
|
16
|
-
} from "fs";
|
|
17
|
-
import { join, dirname } from "path";
|
|
18
|
-
import { fileURLToPath } from "url";
|
|
19
|
-
import { platform, arch } from "os";
|
|
20
|
-
import { spawnSync } from "child_process";
|
|
21
|
-
import { inflateRawSync } from "zlib";
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { platform, arch } from "node:os";
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { inflateRawSync } from "node:zlib";
|
|
22
22
|
|
|
23
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
const ROOT = join(__dirname, "..");
|
|
@@ -121,8 +121,8 @@ try {
|
|
|
121
121
|
let cdOffset = buffer.indexOf(cdSig, 0);
|
|
122
122
|
while (cdOffset !== -1) {
|
|
123
123
|
const nameLen = buffer.readUInt16LE(cdOffset + 28);
|
|
124
|
-
const
|
|
125
|
-
const
|
|
124
|
+
const _extraLen = buffer.readUInt16LE(cdOffset + 30);
|
|
125
|
+
const _commentLen = buffer.readUInt16LE(cdOffset + 32);
|
|
126
126
|
const localHeaderOffset = buffer.readUInt32LE(cdOffset + 42);
|
|
127
127
|
const compSize = buffer.readUInt32LE(cdOffset + 20);
|
|
128
128
|
const compMethod = buffer.readUInt16LE(cdOffset + 10);
|
package/src/cli.ts
CHANGED
|
@@ -22,11 +22,9 @@ import { runDoctor } from "./doctor";
|
|
|
22
22
|
import { reportError } from "./error-report";
|
|
23
23
|
import {
|
|
24
24
|
format as formatOutput,
|
|
25
|
-
exportToFile,
|
|
26
25
|
pipeExec,
|
|
27
26
|
type Format,
|
|
28
27
|
type ExportFormat,
|
|
29
|
-
type FormatLine,
|
|
30
28
|
} from "./output";
|
|
31
29
|
import { runWatch } from "./watch";
|
|
32
30
|
import {
|
|
@@ -61,9 +59,9 @@ import {
|
|
|
61
59
|
} from "./deepdive";
|
|
62
60
|
import { resolveTrendingLanguage } from "./search";
|
|
63
61
|
import type { Repo } from "./types";
|
|
64
|
-
import { readFileSync } from "fs";
|
|
65
|
-
import { join, dirname } from "path";
|
|
66
|
-
import { fileURLToPath } from "url";
|
|
62
|
+
import { readFileSync } from "node:fs";
|
|
63
|
+
import { join, dirname } from "node:path";
|
|
64
|
+
import { fileURLToPath } from "node:url";
|
|
67
65
|
import { launchBrowser } from "./tui";
|
|
68
66
|
import { getVersion } from "./version";
|
|
69
67
|
import { runMcpServer } from "./mcp-server";
|
|
@@ -139,7 +137,7 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
139
137
|
mcp: false,
|
|
140
138
|
};
|
|
141
139
|
|
|
142
|
-
|
|
140
|
+
const queryParts: string[] = [];
|
|
143
141
|
for (let i = 0; i < args.length; i++) {
|
|
144
142
|
const arg = args[i];
|
|
145
143
|
switch (arg) {
|
|
@@ -200,8 +198,8 @@ function parseArgs(args: string[]): CLIFlags {
|
|
|
200
198
|
flags.login = true;
|
|
201
199
|
break;
|
|
202
200
|
case "--limit": {
|
|
203
|
-
const parsed = parseInt(args[++i]);
|
|
204
|
-
if (isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
201
|
+
const parsed = parseInt(args[++i], 10);
|
|
202
|
+
if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
205
203
|
console.error(`Invalid limit: must be 1-100. Using default 50.`);
|
|
206
204
|
flags.limit = 50;
|
|
207
205
|
} else {
|
|
@@ -513,7 +511,12 @@ async function runUserProfile(
|
|
|
513
511
|
outputFormat?: ExportFormat,
|
|
514
512
|
): Promise<void> {
|
|
515
513
|
try {
|
|
516
|
-
const
|
|
514
|
+
const user = flags.user ?? "";
|
|
515
|
+
if (!user) {
|
|
516
|
+
console.error("Usage: ghfind user <name> [--json] [--token <t>]");
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
const profile = await fetchUserProfile(user, {
|
|
517
520
|
token: flags.token,
|
|
518
521
|
limit: flags.limit,
|
|
519
522
|
});
|
|
@@ -540,7 +543,12 @@ async function runOrgProfile(
|
|
|
540
543
|
outputFormat?: ExportFormat,
|
|
541
544
|
): Promise<void> {
|
|
542
545
|
try {
|
|
543
|
-
const
|
|
546
|
+
const org = flags.org ?? "";
|
|
547
|
+
if (!org) {
|
|
548
|
+
console.error("Usage: ghfind org <name> [--json] [--token <t>]");
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
const profile = await fetchOrgProfile(org, {
|
|
544
552
|
token: flags.token,
|
|
545
553
|
limit: flags.limit,
|
|
546
554
|
});
|
|
@@ -566,7 +574,7 @@ async function runDeepDive(
|
|
|
566
574
|
flags: CLIFlags,
|
|
567
575
|
outputFormat?: ExportFormat,
|
|
568
576
|
): Promise<void> {
|
|
569
|
-
const ref = flags.deepDive
|
|
577
|
+
const ref = flags.deepDive;
|
|
570
578
|
if (!ref) {
|
|
571
579
|
console.error(
|
|
572
580
|
"Usage: ghfind deep-dive <owner/repo> [--json] [--token <t>]",
|
|
@@ -684,7 +692,7 @@ async function runReleases(
|
|
|
684
692
|
async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
685
693
|
// Dispatch table: each handler receives shared context + flags, returns void.
|
|
686
694
|
const handlers: Record<string, (ctx: SearchContext) => Promise<void>> = {
|
|
687
|
-
trending: async (
|
|
695
|
+
trending: async (_ctx) => {
|
|
688
696
|
const since =
|
|
689
697
|
flags.since === "weekly"
|
|
690
698
|
? "weekly"
|
|
@@ -773,7 +781,7 @@ async function runNonInteractive(flags: CLIFlags, outputFormat?: ExportFormat) {
|
|
|
773
781
|
token: flags.token,
|
|
774
782
|
});
|
|
775
783
|
const repos = rankRepos(response.repos, flags.sort);
|
|
776
|
-
await pipeExec(repos, flags.pipe
|
|
784
|
+
if (flags.pipe) await pipeExec(repos, flags.pipe);
|
|
777
785
|
},
|
|
778
786
|
format: async (ctx) => {
|
|
779
787
|
const response = await ctx.provider.search(ctx.parsed, {
|
package/src/compare.ts
CHANGED
|
@@ -52,9 +52,9 @@ export function buildComparisonTable(repos: Repo[]): string {
|
|
|
52
52
|
const sep = ["".padEnd(14, "─"), ...names.map(() => "".padEnd(colW, "─"))];
|
|
53
53
|
|
|
54
54
|
const lines: string[] = [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
`┌${sep.map((s) => `─${s}─`).join("┬")}┐`,
|
|
56
|
+
`│${header.map((h) => ` ${h} `).join("│")}│`,
|
|
57
|
+
`├${sep.map((s) => `─${s}─`).join("┼")}┤`,
|
|
58
58
|
];
|
|
59
59
|
|
|
60
60
|
for (const row of rows) {
|
|
@@ -62,10 +62,10 @@ export function buildComparisonTable(repos: Repo[]): string {
|
|
|
62
62
|
row.label.padEnd(14),
|
|
63
63
|
...row.values.map((v) => v.padEnd(colW)),
|
|
64
64
|
];
|
|
65
|
-
lines.push(
|
|
65
|
+
lines.push(`│${cells.map((c) => ` ${c} `).join("│")}│`);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
lines.push(
|
|
68
|
+
lines.push(`└${sep.map((s) => `─${s}─`).join("┴")}┘`);
|
|
69
69
|
return lines.join("\n");
|
|
70
70
|
}
|
|
71
71
|
|
package/src/config.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* 3. ~/.config/ghfind/config.json
|
|
8
8
|
*/
|
|
9
9
|
import type { Config } from "./types";
|
|
10
|
-
import { homedir } from "os";
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
|
|
14
14
|
/** Directory for persistent state (history, bookmarks, session). */
|
|
15
15
|
export function stateDir(): string {
|
package/src/deepdive.ts
CHANGED
package/src/doctor.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* Runs a battery of checks and prints a PASS / WARN / FAIL report, then exits
|
|
6
6
|
* non-zero if any FAIL check was detected.
|
|
7
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";
|
|
8
|
+
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { join, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { homedir, platform, arch } from "node:os";
|
|
13
13
|
import { loadConfig, configPath, stateDir, cacheDir } from "./config";
|
|
14
14
|
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -31,9 +31,14 @@ function add(status: CheckStatus, label: string, detail: string): void {
|
|
|
31
31
|
function run(
|
|
32
32
|
cmd: string,
|
|
33
33
|
args: string[],
|
|
34
|
+
opts: { shell?: boolean; timeout?: number } = {},
|
|
34
35
|
): { status: number | null; stdout: string; stderr: string } {
|
|
35
36
|
try {
|
|
36
|
-
const res = spawnSync(cmd, args, {
|
|
37
|
+
const res = spawnSync(cmd, args, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
timeout: 15000,
|
|
40
|
+
...opts,
|
|
41
|
+
});
|
|
37
42
|
return {
|
|
38
43
|
status: res.status,
|
|
39
44
|
stdout: res.stdout ?? "",
|
|
@@ -123,13 +128,13 @@ function checkSystemBun(bundledOk: boolean): void {
|
|
|
123
128
|
add(
|
|
124
129
|
"WARN",
|
|
125
130
|
"System Bun",
|
|
126
|
-
"No
|
|
131
|
+
"No `bun` on PATH. Bundled Bun is working, so this is only a fallback.",
|
|
127
132
|
);
|
|
128
133
|
} else {
|
|
129
134
|
add(
|
|
130
135
|
"FAIL",
|
|
131
136
|
"System Bun",
|
|
132
|
-
"No
|
|
137
|
+
"No `bun` on PATH. Install it: curl -fsSL https://bun.sh/install | bash",
|
|
133
138
|
);
|
|
134
139
|
}
|
|
135
140
|
}
|
|
@@ -236,6 +241,136 @@ function checkDirs(): void {
|
|
|
236
241
|
}
|
|
237
242
|
}
|
|
238
243
|
|
|
244
|
+
const PKG_NAME = "github-search-cli";
|
|
245
|
+
const BIN_NAME = "ghfind";
|
|
246
|
+
|
|
247
|
+
function runningVersion(): string {
|
|
248
|
+
try {
|
|
249
|
+
const pkg = JSON.parse(
|
|
250
|
+
readFileSync(join(PKG_DIR, "package.json"), "utf8"),
|
|
251
|
+
) as { version?: string };
|
|
252
|
+
return pkg.version ?? "";
|
|
253
|
+
} catch {
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Best-effort version of the install that owns a `ghfind` shim on PATH.
|
|
260
|
+
* Prefers reading the sibling package.json (cheap, exact); falls back to
|
|
261
|
+
* executing the shim with `--version` (handles bun/unknown layouts).
|
|
262
|
+
*/
|
|
263
|
+
export function installVersionAt(
|
|
264
|
+
shimPath: string,
|
|
265
|
+
): { version: string; dir: string } | null {
|
|
266
|
+
const dir = dirname(shimPath);
|
|
267
|
+
// npm layouts: <prefix>/node_modules/<pkg> (win) or <prefix>/lib/node_modules/<pkg> (unix)
|
|
268
|
+
for (const ancestor of [dir, dirname(dir)]) {
|
|
269
|
+
for (const sub of ["node_modules", join("lib", "node_modules")]) {
|
|
270
|
+
const pkgJson = join(ancestor, sub, PKG_NAME, "package.json");
|
|
271
|
+
if (existsSync(pkgJson)) {
|
|
272
|
+
try {
|
|
273
|
+
const pkg = JSON.parse(readFileSync(pkgJson, "utf8")) as {
|
|
274
|
+
version?: string;
|
|
275
|
+
};
|
|
276
|
+
return { version: pkg.version ?? "", dir: dirname(pkgJson) };
|
|
277
|
+
} catch {
|
|
278
|
+
// fall through to shim execution
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// .cmd/.bat shims require shell:true under Node (EINVAL otherwise); the
|
|
284
|
+
// package.json read above already covered the common npm layouts.
|
|
285
|
+
const res = run(shimPath, ["--version"], {
|
|
286
|
+
shell: platform() === "win32",
|
|
287
|
+
timeout: 10000,
|
|
288
|
+
});
|
|
289
|
+
const m = /\d+\.\d+\.\d+/.exec(res.stdout);
|
|
290
|
+
return m ? { version: m[0], dir } : null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Detect stale/shadowing global installs: multiple `ghfind` binaries on PATH
|
|
295
|
+
* resolving to different versions means the shell may keep launching an old
|
|
296
|
+
* copy even after a successful update — the exact "updater says OK but
|
|
297
|
+
* nothing changed" trap. Warns and names the offending prefixes.
|
|
298
|
+
*/
|
|
299
|
+
function checkInstallShadowing(): void {
|
|
300
|
+
const isWin = platform() === "win32";
|
|
301
|
+
const res = isWin ? run("where", [BIN_NAME]) : run("which", ["-a", BIN_NAME]);
|
|
302
|
+
const shims = [
|
|
303
|
+
...new Set(
|
|
304
|
+
res.stdout
|
|
305
|
+
.split(/\r?\n/)
|
|
306
|
+
.map((l) => l.trim())
|
|
307
|
+
.filter((l) => l.length > 0),
|
|
308
|
+
),
|
|
309
|
+
];
|
|
310
|
+
if (shims.length === 0) {
|
|
311
|
+
add("WARN", "Global installs", `${BIN_NAME} not found on PATH.`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const running = runningVersion();
|
|
315
|
+
const norm = (p: string) => (isWin ? p.replace(/\\/g, "/").toLowerCase() : p);
|
|
316
|
+
const seenDirs = new Set<string>();
|
|
317
|
+
const stale: string[] = [];
|
|
318
|
+
let knownCount = 0;
|
|
319
|
+
let staleVersion = "";
|
|
320
|
+
let staleDir = "";
|
|
321
|
+
|
|
322
|
+
for (const shim of shims) {
|
|
323
|
+
const info = installVersionAt(shim);
|
|
324
|
+
// Group shims that live in the same install (ghfind + ghfind.cmd + ghfind.ps1)
|
|
325
|
+
const dirKey = norm(dirname(shim));
|
|
326
|
+
if (seenDirs.has(dirKey)) continue;
|
|
327
|
+
seenDirs.add(dirKey);
|
|
328
|
+
knownCount++;
|
|
329
|
+
const isRunningInstall = info !== null && norm(info.dir) === norm(PKG_DIR);
|
|
330
|
+
if (
|
|
331
|
+
!isRunningInstall &&
|
|
332
|
+
info !== null &&
|
|
333
|
+
running &&
|
|
334
|
+
info.version !== running
|
|
335
|
+
) {
|
|
336
|
+
stale.push(`${dirname(shim)} (v${info.version} ≠ running v${running})`);
|
|
337
|
+
staleVersion = info.version;
|
|
338
|
+
staleDir = dirname(shim);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (stale.length > 0 && knownCount > 1) {
|
|
343
|
+
// Shadowing needs ≥2 installs: the shell can keep launching an old copy
|
|
344
|
+
// while updates land in another prefix.
|
|
345
|
+
add(
|
|
346
|
+
"WARN",
|
|
347
|
+
"Global installs",
|
|
348
|
+
`${knownCount} install(s) on PATH; stale copies shadowing updates → ${stale.join("; ")}. Remove with: npm uninstall -g ${PKG_NAME} --prefix <dir>`,
|
|
349
|
+
);
|
|
350
|
+
} else if (stale.length > 0) {
|
|
351
|
+
// Single install whose version differs from the running copy — normal
|
|
352
|
+
// when developing from source (running vNEWER than the published one).
|
|
353
|
+
// Uninstalling it would be harmful advice.
|
|
354
|
+
add(
|
|
355
|
+
"PASS",
|
|
356
|
+
"Global installs",
|
|
357
|
+
`v${staleVersion} at ${staleDir} (running v${running || "?"} from source/build) — single install, nothing shadowed.`,
|
|
358
|
+
);
|
|
359
|
+
} else if (knownCount > 1) {
|
|
360
|
+
add(
|
|
361
|
+
"PASS",
|
|
362
|
+
"Global installs",
|
|
363
|
+
`${knownCount} install(s) on PATH, all v${running || "?"}.`,
|
|
364
|
+
);
|
|
365
|
+
} else {
|
|
366
|
+
add(
|
|
367
|
+
"PASS",
|
|
368
|
+
"Global installs",
|
|
369
|
+
`v${running || "?"} at ${dirname(shims[0])}.`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
239
374
|
function printReport(): void {
|
|
240
375
|
// Respect NO_COLOR (https://no-color.org/): plain text when set (any value,
|
|
241
376
|
// including the empty string) or when stdout is not a TTY.
|
|
@@ -284,6 +419,7 @@ export async function runDoctor(): Promise<void> {
|
|
|
284
419
|
checkConfig();
|
|
285
420
|
checkGithubToken();
|
|
286
421
|
checkDirs();
|
|
422
|
+
checkInstallShadowing();
|
|
287
423
|
printReport();
|
|
288
424
|
|
|
289
425
|
const hasFail = results.some((r) => r.status === "FAIL");
|
package/src/error-report.ts
CHANGED
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
arch as osArch,
|
|
11
11
|
version as nodeVersion,
|
|
12
12
|
versions as nodeVersions,
|
|
13
|
-
} from "process";
|
|
14
|
-
import { spawn } from "child_process";
|
|
13
|
+
} from "node:process";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
15
|
import { getVersion } from "./version";
|
|
16
16
|
|
|
17
17
|
// ponytail: in bun, process.platform is a string property; in node, same.
|
|
@@ -43,6 +43,7 @@ export function formatErrorBody(
|
|
|
43
43
|
context: { command: string; argv: string[] },
|
|
44
44
|
): string {
|
|
45
45
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
46
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: strips ANSI color escapes from stack traces
|
|
46
47
|
const stack = (e.stack ?? "(no stack)").replace(/\u001b\[[0-9;]*m/g, "");
|
|
47
48
|
const message = (e.message || "(no message)").slice(0, 500);
|
|
48
49
|
const cmd = `${context.command} ${context.argv.join(" ")}`.trim();
|
|
@@ -105,14 +106,19 @@ function tryOpenUrl(url: string): boolean {
|
|
|
105
106
|
try {
|
|
106
107
|
const p = osPlatform;
|
|
107
108
|
if (typeof Bun !== "undefined") {
|
|
108
|
-
if (p === "win32")
|
|
109
|
-
|
|
109
|
+
if (p === "win32") {
|
|
110
|
+
// explorer.exe, not `cmd /c start` — cmd splits the URL at the first
|
|
111
|
+
// unquoted `&` (query separator) and runs the rest as a command,
|
|
112
|
+
// truncating every pre-filled issue URL. See src/open-url.ts.
|
|
113
|
+
Bun.spawn(["explorer", url]);
|
|
114
|
+
} else if (p === "darwin") Bun.spawn(["open", url]);
|
|
110
115
|
else Bun.spawn(["xdg-open", url]);
|
|
111
116
|
return true;
|
|
112
117
|
}
|
|
113
118
|
// Node fallback — use child_process.spawn detached so it survives exit
|
|
114
|
-
const cmd =
|
|
115
|
-
|
|
119
|
+
const cmd =
|
|
120
|
+
p === "win32" ? "explorer" : p === "darwin" ? "open" : "xdg-open";
|
|
121
|
+
const args = [url];
|
|
116
122
|
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
117
123
|
child.unref();
|
|
118
124
|
return true;
|
|
@@ -142,11 +148,11 @@ export function reportError(
|
|
|
142
148
|
console.error("");
|
|
143
149
|
const opened = tryOpenUrl(url);
|
|
144
150
|
if (opened) {
|
|
145
|
-
console.error(
|
|
151
|
+
console.error(` → Opened: ${url}`);
|
|
146
152
|
} else {
|
|
147
153
|
console.error(" Copy this URL to file a bug report:");
|
|
148
154
|
console.error("");
|
|
149
|
-
console.error(
|
|
155
|
+
console.error(` ${url}`);
|
|
150
156
|
}
|
|
151
157
|
console.error("");
|
|
152
158
|
return url;
|
package/src/export.ts
CHANGED
package/src/history.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Persistent search history — append-only JSONL with rotation.
|
|
3
3
|
*/
|
|
4
4
|
import type { HistoryEntry } from "./types";
|
|
5
|
-
import { readJSONL, appendJSONL,
|
|
5
|
+
import { readJSONL, appendJSONL, writeRaw } from "./storage";
|
|
6
6
|
|
|
7
7
|
const HISTORY_FILE = "history.jsonl";
|
|
8
8
|
const MAX_ENTRIES = 500;
|
|
@@ -37,10 +37,10 @@ export function deleteHistoryEntry(index: number): void {
|
|
|
37
37
|
);
|
|
38
38
|
writeRaw(
|
|
39
39
|
HISTORY_FILE,
|
|
40
|
-
remaining
|
|
40
|
+
`${remaining
|
|
41
41
|
.reverse()
|
|
42
42
|
.map((e) => JSON.stringify(e))
|
|
43
|
-
.join("\n")
|
|
43
|
+
.join("\n")}\n`,
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -56,10 +56,10 @@ export function rotateHistory(): void {
|
|
|
56
56
|
const trimmed = entries.slice(0, MAX_ENTRIES);
|
|
57
57
|
writeRaw(
|
|
58
58
|
HISTORY_FILE,
|
|
59
|
-
trimmed
|
|
59
|
+
`${trimmed
|
|
60
60
|
.reverse()
|
|
61
61
|
.map((e) => JSON.stringify(e))
|
|
62
|
-
.join("\n")
|
|
62
|
+
.join("\n")}\n`,
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
|
package/src/init.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ponytail: simple stdin prompts, no curses/readline UI.
|
|
5
5
|
*/
|
|
6
|
-
import { createInterface } from "readline";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
7
|
import { saveConfig } from "./config";
|
|
8
8
|
import { isLikelyGithubToken } from "./login";
|
|
9
9
|
import { listThemes } from "./themes";
|
|
@@ -44,7 +44,7 @@ export async function runInitWizard(): Promise<void> {
|
|
|
44
44
|
defaultSort: (["best-match", "stars", "updated", "forks"].includes(sortStr)
|
|
45
45
|
? sortStr
|
|
46
46
|
: "best-match") as SortStrategy,
|
|
47
|
-
defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
|
|
47
|
+
defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr, 10) || 50)),
|
|
48
48
|
theme: themes.includes(themeStr) ? themeStr : themes[0],
|
|
49
49
|
cacheTtlSeconds: 300,
|
|
50
50
|
defaultTab: "search",
|
package/src/login.ts
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* used by `ghfind init` (~/.config/ghfind/config.json). `GITHUB_TOKEN` in the
|
|
7
7
|
* environment always takes precedence at runtime (see src/config.ts).
|
|
8
8
|
*/
|
|
9
|
-
import { createInterface } from "readline";
|
|
10
|
-
import { homedir } from "os";
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
13
|
import { loadConfig, saveConfig, configPath } from "./config";
|
|
14
14
|
import type { Config } from "./types";
|
|
15
15
|
|