github-search-cli 9.4.2 → 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 (52) hide show
  1. package/CHANGELOG.md +26 -4
  2. package/README.md +8 -3
  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 +761 -342
  9. package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
  10. package/package.json +6 -3
  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 +17 -1
  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/errors.ts +62 -59
  22. package/src/explore.ts +34 -29
  23. package/src/export.ts +73 -63
  24. package/src/help.ts +157 -157
  25. package/src/history.ts +69 -55
  26. package/src/init.ts +60 -55
  27. package/src/landing.ts +106 -104
  28. package/src/login.ts +186 -0
  29. package/src/markdown-render.ts +1 -6
  30. package/src/notifications.ts +75 -59
  31. package/src/open-url.ts +16 -16
  32. package/src/org.ts +43 -15
  33. package/src/output.ts +153 -137
  34. package/src/package-browser.ts +317 -305
  35. package/src/package.ts +149 -137
  36. package/src/pipe.ts +60 -54
  37. package/src/releases.ts +182 -144
  38. package/src/saved-searches.ts +72 -50
  39. package/src/search-state.ts +283 -229
  40. package/src/search.ts +678 -653
  41. package/src/session.ts +26 -24
  42. package/src/share.ts +61 -54
  43. package/src/status.ts +68 -61
  44. package/src/storage.ts +136 -106
  45. package/src/themes.ts +259 -116
  46. package/src/tips.ts +42 -42
  47. package/src/toast.ts +70 -69
  48. package/src/trending-parser.ts +104 -0
  49. package/src/trending.ts +481 -462
  50. package/src/tui.ts +149 -153
  51. package/src/types.ts +160 -158
  52. 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
+ }