github-search-cli 9.8.3 → 9.10.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/CHANGELOG.md +123 -0
- package/README.md +68 -40
- package/dist/cli.js +992 -172
- package/docs/HOW-TO.md +255 -0
- package/package.json +3 -2
- package/release-notes/9.10.0.md +137 -0
- package/release-notes/9.9.0.md +193 -0
- package/src/cli.ts +13 -11
- package/src/errors.ts +35 -11
- package/src/help.ts +21 -4
- package/src/mcp-server.ts +18 -2
- package/src/search-state.ts +8 -10
- package/src/search.ts +683 -11
- package/src/trending.ts +2 -6
- package/src/tui/mouse.ts +349 -0
- package/src/tui.ts +433 -123
- package/src/types.ts +11 -1
- package/src/update-check.ts +72 -6
- package/src/watch.ts +16 -11
package/src/types.ts
CHANGED
|
@@ -10,14 +10,24 @@ export interface Qualifier {
|
|
|
10
10
|
negated: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** A normalization applied to a qualifier while parsing (typo fix, `10k` → `>=10000`). */
|
|
14
|
+
export interface QueryCorrection {
|
|
15
|
+
/** What the user typed, e.g. `langauge:rust`. */
|
|
16
|
+
from: string;
|
|
17
|
+
/** What it was normalized to, e.g. `language:Rust`. */
|
|
18
|
+
to: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
/** The fully parsed, normalized representation of a user's search request. */
|
|
14
22
|
export interface ParsedQuery {
|
|
15
23
|
/** Free-text keywords (everything that is not a qualifier). */
|
|
16
24
|
keywords: string[];
|
|
17
|
-
/** Structured qualifiers extracted from the query string. */
|
|
25
|
+
/** Structured qualifiers extracted from the query string, normalized. */
|
|
18
26
|
qualifiers: Qualifier[];
|
|
19
27
|
/** The original, untouched query string (preserved for debugging/display). */
|
|
20
28
|
raw: string;
|
|
29
|
+
/** Normalizations applied while parsing. Empty when the query was already canonical. */
|
|
30
|
+
corrections: QueryCorrection[];
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
/** A repository as returned by a search provider, normalized to a stable shape. */
|
package/src/update-check.ts
CHANGED
|
@@ -59,6 +59,32 @@ export function shouldCheckUpdate(): boolean {
|
|
|
59
59
|
return true;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Split a semver string into comparable numeric parts.
|
|
64
|
+
*
|
|
65
|
+
* A leading `v` is tolerated. Pre-release (`-beta.1`) and build (`+build.5`)
|
|
66
|
+
* suffixes are both stripped first, so `9.9.0+build.5` and `9.9.0` compare as
|
|
67
|
+
* the same release — leaving them in makes `Number("0+build")` NaN.
|
|
68
|
+
*/
|
|
69
|
+
function semverParts(v: string): number[] {
|
|
70
|
+
const core = v.trim().replace(/^v/i, "").split(/[-+]/)[0];
|
|
71
|
+
return core.split(".").map((s) => {
|
|
72
|
+
const n = Number(s);
|
|
73
|
+
return Number.isFinite(n) ? n : 0;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* True when `v` carries a pre-release tag (`-beta.1`, `-rc.2`, …).
|
|
79
|
+
* npm semantics: a pre-release is OLDER than the matching release.
|
|
80
|
+
*/
|
|
81
|
+
function isPrerelease(v: string): boolean {
|
|
82
|
+
const core = v.trim().replace(/^v/i, "");
|
|
83
|
+
const plus = core.indexOf("+");
|
|
84
|
+
const coreOnly = plus === -1 ? core : core.slice(0, plus);
|
|
85
|
+
return coreOnly.includes("-");
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
/**
|
|
63
89
|
* Compare two semver strings. Returns true if `latest` is newer than `current`.
|
|
64
90
|
* Uses Bun's built-in semver if available, falls back to simple comparison.
|
|
@@ -68,16 +94,27 @@ export function isNewerVersion(current: string, latest: string): boolean {
|
|
|
68
94
|
if (typeof Bun !== "undefined" && Bun.semver) {
|
|
69
95
|
return Bun.semver.order(current, latest) < 0;
|
|
70
96
|
}
|
|
71
|
-
// Fallback
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
// Fallback for Node/compiled binaries. npm orders `1.0.0-beta.1` BEFORE
|
|
98
|
+
// `1.0.0`, so a prerelease only counts as an upgrade when its numeric core
|
|
99
|
+
// is genuinely ahead — never for the same core, which would be a downgrade.
|
|
100
|
+
const curParts = semverParts(current);
|
|
101
|
+
const newParts = semverParts(latest);
|
|
102
|
+
let order = 0;
|
|
74
103
|
for (let i = 0; i < Math.max(curParts.length, newParts.length); i++) {
|
|
75
104
|
const c = curParts[i] ?? 0;
|
|
76
105
|
const n = newParts[i] ?? 0;
|
|
77
|
-
if (n > c)
|
|
78
|
-
|
|
106
|
+
if (n > c) {
|
|
107
|
+
order = 1;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (n < c) {
|
|
111
|
+
order = -1;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
79
114
|
}
|
|
80
|
-
return
|
|
115
|
+
if (order !== 0) return order > 0;
|
|
116
|
+
// Same numeric core: only a prerelease -> release move is an upgrade.
|
|
117
|
+
return isPrerelease(current) && !isPrerelease(latest);
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
/**
|
|
@@ -257,6 +294,35 @@ export function recordPreUpdateState(currentVersion: string): void {
|
|
|
257
294
|
});
|
|
258
295
|
}
|
|
259
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Acknowledge that the post-upgrade panel for `shownVersion` has been seen.
|
|
299
|
+
*
|
|
300
|
+
* The panel's trigger is `lastInstalledVersion !== currentVersion`, so without
|
|
301
|
+
* this write the state stays "upgraded" forever and the panel reappears on
|
|
302
|
+
* every single launch — including after the user dismissed it with "later" or
|
|
303
|
+
* "never", since neither touches `lastInstalledVersion`. Writing the running
|
|
304
|
+
* version makes the condition false exactly once.
|
|
305
|
+
*
|
|
306
|
+
* Best-effort: a failed write must not block the panel from being shown.
|
|
307
|
+
*/
|
|
308
|
+
export function markPostUpgradeSeen(currentVersion: string): void {
|
|
309
|
+
try {
|
|
310
|
+
const state = readUpdateState();
|
|
311
|
+
// Already acknowledged — nothing to do.
|
|
312
|
+
if (state.lastInstalledVersion === currentVersion) return;
|
|
313
|
+
// Only acknowledge a recorded version that is genuinely OLDER than the
|
|
314
|
+
// running one (the upgrade we just showed notes for). If the recorded
|
|
315
|
+
// version is NEWER — e.g. an update landed mid-session, or the install was
|
|
316
|
+
// rolled back — leave it alone so the next launch still reports something.
|
|
317
|
+
if (isNewerVersion(currentVersion, state.lastInstalledVersion ?? "")) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
writeUpdateState({ ...state, lastInstalledVersion: currentVersion });
|
|
321
|
+
} catch {
|
|
322
|
+
// non-critical
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
260
326
|
/**
|
|
261
327
|
* Fetch release notes for a given version.
|
|
262
328
|
* Tries release-notes/<version>.md first, then falls back to CHANGELOG.md section.
|
package/src/watch.ts
CHANGED
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
applyFlagFilters,
|
|
9
9
|
rankRepos,
|
|
10
10
|
createGitHubSearch,
|
|
11
|
+
validateQuery,
|
|
11
12
|
} from "./search";
|
|
12
|
-
import { createTrendingSearch } from "./search";
|
|
13
|
+
import { createTrendingSearch, trendingQuery } from "./search";
|
|
13
14
|
|
|
14
15
|
export interface WatchOptions {
|
|
15
16
|
query: string;
|
|
@@ -32,6 +33,13 @@ export async function runWatch(
|
|
|
32
33
|
let failCount = 0;
|
|
33
34
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
34
35
|
|
|
36
|
+
// Validate once, before the poll loop starts. A malformed query is not
|
|
37
|
+
// transient, so letting it reach the tick would burn the retry budget and
|
|
38
|
+
// report the same error five times instead of failing fast.
|
|
39
|
+
if (!opts.trending) {
|
|
40
|
+
validateQuery(applyFlagFilters(parseQuery(opts.query), {}));
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
const tick = async () => {
|
|
36
44
|
try {
|
|
37
45
|
let repos: Repo[];
|
|
@@ -43,16 +51,13 @@ export async function runWatch(
|
|
|
43
51
|
? "monthly"
|
|
44
52
|
: "daily";
|
|
45
53
|
const trendingSearch = createTrendingSearch();
|
|
46
|
-
const response = await trendingSearch.search(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
trendingSince: since,
|
|
54
|
-
},
|
|
55
|
-
);
|
|
54
|
+
const response = await trendingSearch.search(trendingQuery(), {
|
|
55
|
+
limit: opts.limit,
|
|
56
|
+
sort: opts.sort,
|
|
57
|
+
json: false,
|
|
58
|
+
verbose: false,
|
|
59
|
+
trendingSince: since,
|
|
60
|
+
});
|
|
56
61
|
repos = response.repos;
|
|
57
62
|
} else {
|
|
58
63
|
const parsed = applyFlagFilters(parseQuery(opts.query), {});
|