github-search-cli 9.4.1 → 9.4.3
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 +65 -1
- package/README.md +268 -237
- 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 +1179 -403
- package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
- package/package.json +16 -5
- 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 +650 -566
- 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/error-report.ts +8 -12
- 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 +299 -0
- 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 +3015 -2867
- package/src/types.ts +160 -158
- package/src/update-check.ts +231 -215
- package/src/version.ts +33 -0
- package/src/watch.ts +95 -74
package/src/package.ts
CHANGED
|
@@ -1,137 +1,149 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* package.ts — package-registry search provider (prototype for ticket 002-008).
|
|
3
|
-
*
|
|
4
|
-
* npm-only for the MVP (ticket 002-005). Query is passed through verbatim to npm's
|
|
5
|
-
* native `/-/v1/search` (ticket 002-006) and the response normalized to the parallel
|
|
6
|
-
* `Package` shape (ticket 002-007). Rough on purpose — this is the artifact to react to.
|
|
7
|
-
*/
|
|
8
|
-
import type { Package } from "./types";
|
|
9
|
-
|
|
10
|
-
export interface PackageSearchResult {
|
|
11
|
-
totalCount: number;
|
|
12
|
-
packages: Package[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** The npm `/-/v1/search` object shape we actually consume (research 002-001). */
|
|
16
|
-
interface NpmSearchObject {
|
|
17
|
-
package: {
|
|
18
|
-
name?: string;
|
|
19
|
-
scope?: string;
|
|
20
|
-
version?: string;
|
|
21
|
-
description?: string | null;
|
|
22
|
-
keywords?: string[];
|
|
23
|
-
date?: string;
|
|
24
|
-
links?: { npm?: string; homepage?: string; repository?: string };
|
|
25
|
-
publisher?: { username?: string };
|
|
26
|
-
author?: { name?: string };
|
|
27
|
-
};
|
|
28
|
-
score?: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
1
|
+
/**
|
|
2
|
+
* package.ts — package-registry search provider (prototype for ticket 002-008).
|
|
3
|
+
*
|
|
4
|
+
* npm-only for the MVP (ticket 002-005). Query is passed through verbatim to npm's
|
|
5
|
+
* native `/-/v1/search` (ticket 002-006) and the response normalized to the parallel
|
|
6
|
+
* `Package` shape (ticket 002-007). Rough on purpose — this is the artifact to react to.
|
|
7
|
+
*/
|
|
8
|
+
import type { Package } from "./types";
|
|
9
|
+
|
|
10
|
+
export interface PackageSearchResult {
|
|
11
|
+
totalCount: number;
|
|
12
|
+
packages: Package[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The npm `/-/v1/search` object shape we actually consume (research 002-001). */
|
|
16
|
+
interface NpmSearchObject {
|
|
17
|
+
package: {
|
|
18
|
+
name?: string;
|
|
19
|
+
scope?: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
description?: string | null;
|
|
22
|
+
keywords?: string[];
|
|
23
|
+
date?: string;
|
|
24
|
+
links?: { npm?: string; homepage?: string; repository?: string };
|
|
25
|
+
publisher?: { username?: string };
|
|
26
|
+
author?: { name?: string };
|
|
27
|
+
};
|
|
28
|
+
score?: {
|
|
29
|
+
final?: number;
|
|
30
|
+
detail?: { quality?: number; popularity?: number; maintenance?: number };
|
|
31
|
+
};
|
|
32
|
+
searchScore?: number;
|
|
33
|
+
downloads?: { npm?: number; total?: number; monthly?: number };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeNpmObject(obj: NpmSearchObject): Package {
|
|
37
|
+
const p = obj.package;
|
|
38
|
+
const name = p.name ?? "";
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
version: p.version ?? "",
|
|
42
|
+
description: p.description ?? null,
|
|
43
|
+
url: p.links?.npm ?? `https://www.npmjs.com/package/${name}`,
|
|
44
|
+
downloads: obj.downloads?.monthly ?? 0,
|
|
45
|
+
score: obj.score?.final ?? obj.searchScore ?? 0,
|
|
46
|
+
publisher: p.publisher?.username ?? p.author?.name ?? null,
|
|
47
|
+
keywords: p.keywords ?? [],
|
|
48
|
+
registry: "npm",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createPackageSearch() {
|
|
53
|
+
return {
|
|
54
|
+
async searchPackage(
|
|
55
|
+
rawQuery: string,
|
|
56
|
+
limit: number,
|
|
57
|
+
): Promise<PackageSearchResult> {
|
|
58
|
+
const size = Math.min(Math.max(Math.trunc(limit) || 50, 1), 250); // npm caps size at 250
|
|
59
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(rawQuery)}&size=${size}`;
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
headers: { "User-Agent": "ghfind", Accept: "application/json" },
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
throw new Error(`npm search failed: HTTP ${res.status}`);
|
|
65
|
+
}
|
|
66
|
+
const data = (await res.json()) as {
|
|
67
|
+
objects?: NpmSearchObject[];
|
|
68
|
+
total?: number;
|
|
69
|
+
};
|
|
70
|
+
const packages = (data.objects ?? []).map(normalizeNpmObject);
|
|
71
|
+
return { totalCount: data.total ?? packages.length, packages };
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Sort strategies for package results (npm registry order IS best-match). */
|
|
77
|
+
export type PackageSortMode = "best-match" | "score" | "downloads" | "name";
|
|
78
|
+
|
|
79
|
+
/** Sort modes in cycle order for the TUI Sort menu. */
|
|
80
|
+
export const PACKAGE_SORT_MODES: { key: PackageSortMode; label: string }[] = [
|
|
81
|
+
{ key: "best-match", label: "best-match" },
|
|
82
|
+
{ key: "downloads", label: "downloads" },
|
|
83
|
+
{ key: "score", label: "score" },
|
|
84
|
+
{ key: "name", label: "name" },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sort fetched packages. "best-match" is a no-op: it preserves the registry's
|
|
89
|
+
* relevance order, which is the canonical npm ranking.
|
|
90
|
+
*/
|
|
91
|
+
export function sortPackages(
|
|
92
|
+
pkgs: Package[],
|
|
93
|
+
sort: PackageSortMode,
|
|
94
|
+
): Package[] {
|
|
95
|
+
if (sort === "best-match" || pkgs.length < 2) return pkgs;
|
|
96
|
+
const arr = [...pkgs];
|
|
97
|
+
if (sort === "score") {
|
|
98
|
+
arr.sort((a, b) => b.score - a.score);
|
|
99
|
+
} else if (sort === "downloads") {
|
|
100
|
+
arr.sort((a, b) => b.downloads - a.downloads);
|
|
101
|
+
} else {
|
|
102
|
+
arr.sort((a, b) => a.name.localeCompare(b.name));
|
|
103
|
+
}
|
|
104
|
+
return arr;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Package output formatters (parallel to output.ts for repo) ─────────
|
|
108
|
+
// Entity model (ticket 002-007): json/text/count/limit are shared generics
|
|
109
|
+
// (json uses JSON.stringify on Package[]); csv/markdown/urls/names get package
|
|
110
|
+
// variants here. git-only formats (ssh-urls, clone-commands, ids) stay repo-only.
|
|
111
|
+
|
|
112
|
+
function csvEscape(val: unknown): string {
|
|
113
|
+
const str = String(val ?? "");
|
|
114
|
+
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
115
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
116
|
+
}
|
|
117
|
+
return str;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatPackagesJson(pkgs: Package[]): string {
|
|
121
|
+
return JSON.stringify(pkgs, null, 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatPackagesCsv(pkgs: Package[]): string {
|
|
125
|
+
const header = "rank,name,version,downloads,score,url";
|
|
126
|
+
const rows = pkgs.map(
|
|
127
|
+
(p, i) =>
|
|
128
|
+
`${i + 1},${csvEscape(p.name)},${p.version},${p.downloads},${p.score.toFixed(2)},${csvEscape(p.url)}`,
|
|
129
|
+
);
|
|
130
|
+
return [header, ...rows].join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatPackagesMarkdown(pkgs: Package[]): string {
|
|
134
|
+
const header = "| # | Package | Version | Downloads | Score |";
|
|
135
|
+
const sep = "|---|---------|---------|-----------|-------|";
|
|
136
|
+
const rows = pkgs.map(
|
|
137
|
+
(p, i) =>
|
|
138
|
+
`| ${i + 1} | [${p.name}@${p.version}](${p.url}) | ${p.version} | ${p.downloads.toLocaleString()} | ${p.score.toFixed(2)} |`,
|
|
139
|
+
);
|
|
140
|
+
return [header, sep, ...rows].join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatPackagesUrls(pkgs: Package[]): string {
|
|
144
|
+
return pkgs.map((p) => p.url).join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function formatPackagesNames(pkgs: Package[]): string {
|
|
148
|
+
return pkgs.map((p) => p.name).join("\n");
|
|
149
|
+
}
|
package/src/pipe.ts
CHANGED
|
@@ -1,54 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pipe-friendly format modifiers — output repo data as lines for shell piping.
|
|
3
|
-
*/
|
|
4
|
-
import type { Repo } from "./types";
|
|
5
|
-
import { openUrl } from "./open-url";
|
|
6
|
-
|
|
7
|
-
export type FormatLine =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
case "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Pipe-friendly format modifiers — output repo data as lines for shell piping.
|
|
3
|
+
*/
|
|
4
|
+
import type { Repo } from "./types";
|
|
5
|
+
import { openUrl } from "./open-url";
|
|
6
|
+
|
|
7
|
+
export type FormatLine =
|
|
8
|
+
"urls" | "names" | "ssh-urls" | "clone-commands" | "ids";
|
|
9
|
+
|
|
10
|
+
export function formatUrls(repos: Repo[]): string {
|
|
11
|
+
return repos.map((r) => r.url).join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatNames(repos: Repo[]): string {
|
|
15
|
+
return repos.map((r) => r.fullName).join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatSshUrls(repos: Repo[]): string {
|
|
19
|
+
return repos.map((r) => `git@github.com:${r.fullName}.git`).join("\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatCloneCommands(repos: Repo[]): string {
|
|
23
|
+
return repos
|
|
24
|
+
.map((r) => `git clone git@github.com:${r.fullName}.git`)
|
|
25
|
+
.join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatIds(repos: Repo[]): string {
|
|
29
|
+
return repos.map((r) => String(r.id)).join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatLines(repos: Repo[], format: FormatLine): string {
|
|
33
|
+
switch (format) {
|
|
34
|
+
case "urls":
|
|
35
|
+
return formatUrls(repos);
|
|
36
|
+
case "names":
|
|
37
|
+
return formatNames(repos);
|
|
38
|
+
case "ssh-urls":
|
|
39
|
+
return formatSshUrls(repos);
|
|
40
|
+
case "clone-commands":
|
|
41
|
+
return formatCloneCommands(repos);
|
|
42
|
+
case "ids":
|
|
43
|
+
return formatIds(repos);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Execute a pipe target (clone, open) on repos. */
|
|
48
|
+
export async function pipeExec(repos: Repo[], target: string): Promise<void> {
|
|
49
|
+
for (const repo of repos) {
|
|
50
|
+
const url = repo.url;
|
|
51
|
+
switch (target) {
|
|
52
|
+
case "open":
|
|
53
|
+
await openUrl(url);
|
|
54
|
+
break;
|
|
55
|
+
case "clone":
|
|
56
|
+
console.log(`git clone git@github.com:${repo.fullName}.git`);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|