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.
Files changed (55) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +268 -237
  3. package/bin/ghfind.js +98 -89
  4. package/completions/ghfind.bash +1 -1
  5. package/completions/ghfind.fish +3 -1
  6. package/completions/ghfind.zsh +6 -0
  7. package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
  8. package/dist/cli.js +1179 -403
  9. package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
  10. package/package.json +16 -5
  11. package/release-notes/9.3.0.md +32 -29
  12. package/scripts/build.ts +34 -19
  13. package/scripts/generate-svg-demo.js +218 -168
  14. package/scripts/postinstall.js +176 -176
  15. package/src/bookmarks.ts +69 -69
  16. package/src/cli.ts +650 -566
  17. package/src/compare.ts +69 -47
  18. package/src/config.ts +70 -65
  19. package/src/deepdive.ts +149 -118
  20. package/src/doctor.ts +287 -287
  21. package/src/error-report.ts +8 -12
  22. package/src/errors.ts +62 -59
  23. package/src/explore.ts +34 -29
  24. package/src/export.ts +73 -63
  25. package/src/help.ts +157 -157
  26. package/src/history.ts +69 -55
  27. package/src/init.ts +60 -55
  28. package/src/landing.ts +106 -104
  29. package/src/login.ts +186 -0
  30. package/src/markdown-render.ts +1 -6
  31. package/src/notifications.ts +75 -59
  32. package/src/open-url.ts +16 -16
  33. package/src/org.ts +299 -0
  34. package/src/output.ts +153 -137
  35. package/src/package-browser.ts +317 -305
  36. package/src/package.ts +149 -137
  37. package/src/pipe.ts +60 -54
  38. package/src/releases.ts +182 -144
  39. package/src/saved-searches.ts +72 -50
  40. package/src/search-state.ts +283 -229
  41. package/src/search.ts +678 -653
  42. package/src/session.ts +26 -24
  43. package/src/share.ts +61 -54
  44. package/src/status.ts +68 -61
  45. package/src/storage.ts +136 -106
  46. package/src/themes.ts +259 -116
  47. package/src/tips.ts +42 -42
  48. package/src/toast.ts +70 -69
  49. package/src/trending-parser.ts +104 -0
  50. package/src/trending.ts +481 -462
  51. package/src/tui.ts +3015 -2867
  52. package/src/types.ts +160 -158
  53. package/src/update-check.ts +231 -215
  54. package/src/version.ts +33 -0
  55. 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?: { final?: number; detail?: { quality?: number; popularity?: number; maintenance?: number } };
29
- searchScore?: number;
30
- downloads?: { npm?: number; total?: number; monthly?: number };
31
- }
32
-
33
- export function normalizeNpmObject(obj: NpmSearchObject): Package {
34
- const p = obj.package;
35
- const name = p.name ?? "";
36
- return {
37
- name,
38
- version: p.version ?? "",
39
- description: p.description ?? null,
40
- url: p.links?.npm ?? `https://www.npmjs.com/package/${name}`,
41
- downloads: obj.downloads?.monthly ?? 0,
42
- score: obj.score?.final ?? obj.searchScore ?? 0,
43
- publisher: p.publisher?.username ?? p.author?.name ?? null,
44
- keywords: p.keywords ?? [],
45
- registry: "npm",
46
- };
47
- }
48
-
49
- export function createPackageSearch() {
50
- return {
51
- async searchPackage(rawQuery: string, limit: number): Promise<PackageSearchResult> {
52
- const size = Math.min(Math.max(Math.trunc(limit) || 50, 1), 250); // npm caps size at 250
53
- const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(rawQuery)}&size=${size}`;
54
- const res = await fetch(url, {
55
- headers: { "User-Agent": "ghfind", Accept: "application/json" },
56
- });
57
- if (!res.ok) {
58
- throw new Error(`npm search failed: HTTP ${res.status}`);
59
- }
60
- const data = (await res.json()) as { objects?: NpmSearchObject[]; total?: number };
61
- const packages = (data.objects ?? []).map(normalizeNpmObject);
62
- return { totalCount: data.total ?? packages.length, packages };
63
- },
64
- };
65
- }
66
-
67
- /** Sort strategies for package results (npm registry order IS best-match). */
68
- export type PackageSortMode = "best-match" | "score" | "downloads" | "name";
69
-
70
- /** Sort modes in cycle order for the TUI Sort menu. */
71
- export const PACKAGE_SORT_MODES: { key: PackageSortMode; label: string }[] = [
72
- { key: "best-match", label: "best-match" },
73
- { key: "downloads", label: "downloads" },
74
- { key: "score", label: "score" },
75
- { key: "name", label: "name" },
76
- ];
77
-
78
- /**
79
- * Sort fetched packages. "best-match" is a no-op: it preserves the registry's
80
- * relevance order, which is the canonical npm ranking.
81
- */
82
- export function sortPackages(pkgs: Package[], sort: PackageSortMode): Package[] {
83
- if (sort === "best-match" || pkgs.length < 2) return pkgs;
84
- const arr = [...pkgs];
85
- if (sort === "score") {
86
- arr.sort((a, b) => b.score - a.score);
87
- } else if (sort === "downloads") {
88
- arr.sort((a, b) => b.downloads - a.downloads);
89
- } else {
90
- arr.sort((a, b) => a.name.localeCompare(b.name));
91
- }
92
- return arr;
93
- }
94
-
95
- // ── Package output formatters (parallel to output.ts for repo) ─────────
96
- // Entity model (ticket 002-007): json/text/count/limit are shared generics
97
- // (json uses JSON.stringify on Package[]); csv/markdown/urls/names get package
98
- // variants here. git-only formats (ssh-urls, clone-commands, ids) stay repo-only.
99
-
100
- function csvEscape(val: unknown): string {
101
- const str = String(val ?? "");
102
- if (str.includes('"') || str.includes(",") || str.includes("\n")) {
103
- return `"${str.replace(/"/g, '""')}"`;
104
- }
105
- return str;
106
- }
107
-
108
- export function formatPackagesJson(pkgs: Package[]): string {
109
- return JSON.stringify(pkgs, null, 2);
110
- }
111
-
112
- export function formatPackagesCsv(pkgs: Package[]): string {
113
- const header = "rank,name,version,downloads,score,url";
114
- const rows = pkgs.map(
115
- (p, i) =>
116
- `${i + 1},${csvEscape(p.name)},${p.version},${p.downloads},${p.score.toFixed(2)},${csvEscape(p.url)}`,
117
- );
118
- return [header, ...rows].join("\n");
119
- }
120
-
121
- export function formatPackagesMarkdown(pkgs: Package[]): string {
122
- const header = "| # | Package | Version | Downloads | Score |";
123
- const sep = "|---|---------|---------|-----------|-------|";
124
- const rows = pkgs.map(
125
- (p, i) =>
126
- `| ${i + 1} | [${p.name}@${p.version}](${p.url}) | ${p.version} | ${p.downloads.toLocaleString()} | ${p.score.toFixed(2)} |`,
127
- );
128
- return [header, sep, ...rows].join("\n");
129
- }
130
-
131
- export function formatPackagesUrls(pkgs: Package[]): string {
132
- return pkgs.map((p) => p.url).join("\n");
133
- }
134
-
135
- export function formatPackagesNames(pkgs: Package[]): string {
136
- return pkgs.map((p) => p.name).join("\n");
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 = "urls" | "names" | "ssh-urls" | "clone-commands" | "ids";
8
-
9
- export function formatUrls(repos: Repo[]): string {
10
- return repos.map((r) => r.url).join("\n");
11
- }
12
-
13
- export function formatNames(repos: Repo[]): string {
14
- return repos.map((r) => r.fullName).join("\n");
15
- }
16
-
17
- export function formatSshUrls(repos: Repo[]): string {
18
- return repos.map((r) => `git@github.com:${r.fullName}.git`).join("\n");
19
- }
20
-
21
- export function formatCloneCommands(repos: Repo[]): string {
22
- return repos.map((r) => `git clone git@github.com:${r.fullName}.git`).join("\n");
23
- }
24
-
25
- export function formatIds(repos: Repo[]): string {
26
- return repos.map((r) => String(r.id)).join("\n");
27
- }
28
-
29
- export function formatLines(repos: Repo[], format: FormatLine): string {
30
- switch (format) {
31
- case "urls": return formatUrls(repos);
32
- case "names": return formatNames(repos);
33
- case "ssh-urls": return formatSshUrls(repos);
34
- case "clone-commands": return formatCloneCommands(repos);
35
- case "ids": return formatIds(repos);
36
- }
37
- }
38
-
39
- /** Execute a pipe target (clone, open) on repos. */
40
- export async function pipeExec(repos: Repo[], target: string): Promise<void> {
41
- for (const repo of repos) {
42
- const url = repo.url;
43
- switch (target) {
44
- case "open":
45
- await openUrl(url);
46
- break;
47
- case "clone":
48
- console.log(`git clone git@github.com:${repo.fullName}.git`);
49
- break;
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
+ }