github-search-cli 9.4.2 → 9.4.4
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 +38 -4
- package/README.md +8 -3
- 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 +761 -342
- package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
- package/package.json +6 -3
- 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 +17 -1
- 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/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 +43 -15
- 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 +149 -153
- package/src/types.ts +160 -158
- package/src/watch.ts +95 -74
package/src/notifications.ts
CHANGED
|
@@ -1,59 +1,75 @@
|
|
|
1
|
-
/** Notification center — ephemeral alerts for rate limits, watch changes, etc. */
|
|
2
|
-
import { readJSON, writeJSON } from "./storage";
|
|
3
|
-
|
|
4
|
-
export interface Notification {
|
|
5
|
-
id: number;
|
|
6
|
-
type:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
}
|
|
1
|
+
/** Notification center — ephemeral alerts for rate limits, watch changes, etc. */
|
|
2
|
+
import { readJSON, writeJSON } from "./storage";
|
|
3
|
+
|
|
4
|
+
export interface Notification {
|
|
5
|
+
id: number;
|
|
6
|
+
type:
|
|
7
|
+
"info" | "warning" | "rate-limit" | "watch-change" | "upgrade" | "release";
|
|
8
|
+
icon: string;
|
|
9
|
+
message: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let nextId = 1;
|
|
14
|
+
let notifs: Notification[] = [];
|
|
15
|
+
const MAX_NOTIFS = 50;
|
|
16
|
+
const FILE = "notifications.json";
|
|
17
|
+
|
|
18
|
+
function load(): void {
|
|
19
|
+
notifs = readJSON<Notification[]>(FILE, []);
|
|
20
|
+
nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function save(): void {
|
|
24
|
+
writeJSON(FILE, notifs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function addNotification(
|
|
28
|
+
type: Notification["type"],
|
|
29
|
+
message: string,
|
|
30
|
+
): Notification {
|
|
31
|
+
if (notifs.length === 0) load();
|
|
32
|
+
const n: Notification = {
|
|
33
|
+
id: nextId++,
|
|
34
|
+
type,
|
|
35
|
+
icon: iconFor(type),
|
|
36
|
+
message,
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
};
|
|
39
|
+
notifs.unshift(n);
|
|
40
|
+
if (notifs.length > MAX_NOTIFS) notifs = notifs.slice(0, MAX_NOTIFS);
|
|
41
|
+
save();
|
|
42
|
+
return n;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function iconFor(type: Notification["type"]): string {
|
|
46
|
+
switch (type) {
|
|
47
|
+
case "info":
|
|
48
|
+
return "ℹ";
|
|
49
|
+
case "warning":
|
|
50
|
+
return "⚠";
|
|
51
|
+
case "rate-limit":
|
|
52
|
+
return "🚫";
|
|
53
|
+
case "watch-change":
|
|
54
|
+
return "🔔";
|
|
55
|
+
case "upgrade":
|
|
56
|
+
return "📦";
|
|
57
|
+
case "release":
|
|
58
|
+
return "🚀";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getNotifications(): Notification[] {
|
|
63
|
+
if (notifs.length === 0) load();
|
|
64
|
+
return [...notifs];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function dismissNotification(id: number): void {
|
|
68
|
+
notifs = notifs.filter((n) => n.id !== id);
|
|
69
|
+
save();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function dismissAll(): void {
|
|
73
|
+
notifs = [];
|
|
74
|
+
save();
|
|
75
|
+
}
|
package/src/open-url.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Open a URL in the user's default browser — platform-aware.
|
|
3
|
-
*
|
|
4
|
-
* only if the open/xdg-open pattern fails somewhere.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export function openUrl(url: string): void {
|
|
8
|
-
try {
|
|
9
|
-
const p = process.platform;
|
|
10
|
-
if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
|
|
11
|
-
else if (p === "darwin") Bun.spawn(["open", url]);
|
|
12
|
-
else Bun.spawn(["xdg-open", url]);
|
|
13
|
-
} catch {
|
|
14
|
-
// non-critical — user can copy the URL manually
|
|
15
|
-
}
|
|
16
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Open a URL in the user's default browser — platform-aware.
|
|
3
|
+
*
|
|
4
|
+
* only if the open/xdg-open pattern fails somewhere.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function openUrl(url: string): void {
|
|
8
|
+
try {
|
|
9
|
+
const p = process.platform;
|
|
10
|
+
if (p === "win32") Bun.spawn(["cmd", "/c", "start", "", url]);
|
|
11
|
+
else if (p === "darwin") Bun.spawn(["open", url]);
|
|
12
|
+
else Bun.spawn(["xdg-open", url]);
|
|
13
|
+
} catch {
|
|
14
|
+
// non-critical — user can copy the URL manually
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/org.ts
CHANGED
|
@@ -144,9 +144,12 @@ export async function fetchOrgProfile(
|
|
|
144
144
|
|
|
145
145
|
let orgPayload: GitHubOrgPayload;
|
|
146
146
|
try {
|
|
147
|
-
const res = await fetch(
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
const res = await fetch(
|
|
148
|
+
`https://api.github.com/orgs/${encodeURIComponent(name)}`,
|
|
149
|
+
{
|
|
150
|
+
headers: apiHeaders(token),
|
|
151
|
+
},
|
|
152
|
+
);
|
|
150
153
|
if (res.status === 404) {
|
|
151
154
|
throw new Error(`Organization "${name}" not found.`);
|
|
152
155
|
}
|
|
@@ -154,10 +157,13 @@ export async function fetchOrgProfile(
|
|
|
154
157
|
const body = await res.text().catch(() => "");
|
|
155
158
|
throw new Error(`GitHub API error ${res.status}: ${body.slice(0, 200)}`);
|
|
156
159
|
}
|
|
157
|
-
orgPayload = await res.json() as GitHubOrgPayload;
|
|
160
|
+
orgPayload = (await res.json()) as GitHubOrgPayload;
|
|
158
161
|
} catch (err) {
|
|
159
|
-
if (err instanceof Error && err.message.includes(`Organization "${name}"`))
|
|
160
|
-
|
|
162
|
+
if (err instanceof Error && err.message.includes(`Organization "${name}"`))
|
|
163
|
+
throw err;
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Failed to fetch org "${name}": ${err instanceof Error ? err.message : String(err)}`,
|
|
166
|
+
);
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
// Orgs with no public repos don't need the repos request.
|
|
@@ -198,9 +204,15 @@ export function formatOrgText(p: OrgProfile): string {
|
|
|
198
204
|
if (p.createdAt) meta.push(`Created ${p.createdAt.slice(0, 10)}`);
|
|
199
205
|
if (meta.length > 0) lines.push(meta.join("\n"));
|
|
200
206
|
|
|
201
|
-
lines.push(
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
lines.push(
|
|
208
|
+
`Repos ${p.publicRepoCount.toLocaleString()} public${p.fetchedRepoCount < p.publicRepoCount ? ` (aggregated ${p.fetchedRepoCount})` : ""}`,
|
|
209
|
+
);
|
|
210
|
+
lines.push(
|
|
211
|
+
`Stars ${p.totalStars.toLocaleString()} (sum of aggregated repos)`,
|
|
212
|
+
);
|
|
213
|
+
lines.push(
|
|
214
|
+
`Forks ${p.totalForks.toLocaleString()} (sum of aggregated repos)`,
|
|
215
|
+
);
|
|
204
216
|
lines.push(`URL ${p.url}`);
|
|
205
217
|
|
|
206
218
|
if (p.topLanguages.length > 0) {
|
|
@@ -214,7 +226,9 @@ export function formatOrgText(p: OrgProfile): string {
|
|
|
214
226
|
lines.push("");
|
|
215
227
|
lines.push("Top repos by stars:");
|
|
216
228
|
for (const r of p.topRepos) {
|
|
217
|
-
lines.push(
|
|
229
|
+
lines.push(
|
|
230
|
+
` ${r.fullName} ★ ${r.stars.toLocaleString()} ${r.language ?? ""}`,
|
|
231
|
+
);
|
|
218
232
|
}
|
|
219
233
|
}
|
|
220
234
|
if (p.activeRepos.length > 0) {
|
|
@@ -232,7 +246,8 @@ export function formatOrgJson(p: OrgProfile): string {
|
|
|
232
246
|
}
|
|
233
247
|
|
|
234
248
|
export function formatOrgCsv(p: OrgProfile): string {
|
|
235
|
-
const esc = (v: string) =>
|
|
249
|
+
const esc = (v: string) =>
|
|
250
|
+
/[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
236
251
|
const row = [
|
|
237
252
|
p.login,
|
|
238
253
|
p.name ?? "",
|
|
@@ -243,7 +258,10 @@ export function formatOrgCsv(p: OrgProfile): string {
|
|
|
243
258
|
p.topLanguages.map((l) => l.language).join(";"),
|
|
244
259
|
p.createdAt,
|
|
245
260
|
].map(esc);
|
|
246
|
-
return [
|
|
261
|
+
return [
|
|
262
|
+
"login,name,url,public_repos,total_stars,total_forks,top_languages,created_at",
|
|
263
|
+
row.join(","),
|
|
264
|
+
].join("\n");
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
export function formatOrgMarkdown(p: OrgProfile): string {
|
|
@@ -259,12 +277,22 @@ export function formatOrgMarkdown(p: OrgProfile): string {
|
|
|
259
277
|
`| Total forks (aggregated) | ${p.totalForks.toLocaleString()} |`,
|
|
260
278
|
);
|
|
261
279
|
if (p.topLanguages.length > 0) {
|
|
262
|
-
lines.push(
|
|
280
|
+
lines.push(
|
|
281
|
+
`| Top languages | ${p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ")} |`,
|
|
282
|
+
);
|
|
263
283
|
}
|
|
264
284
|
if (p.topRepos.length > 0) {
|
|
265
|
-
lines.push(
|
|
285
|
+
lines.push(
|
|
286
|
+
"",
|
|
287
|
+
"## Top repos by stars",
|
|
288
|
+
"",
|
|
289
|
+
"| Repo | Stars | Language |",
|
|
290
|
+
"| --- | --- | --- |",
|
|
291
|
+
);
|
|
266
292
|
for (const r of p.topRepos) {
|
|
267
|
-
lines.push(
|
|
293
|
+
lines.push(
|
|
294
|
+
`| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.language ?? "—"} |`,
|
|
295
|
+
);
|
|
268
296
|
}
|
|
269
297
|
}
|
|
270
298
|
return lines.join("\n");
|
package/src/output.ts
CHANGED
|
@@ -1,137 +1,153 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output module — unified formatting, export, and pipe execution.
|
|
3
|
-
*
|
|
4
|
-
* All format functions (JSON, CSV, Markdown, text, urls, names, ssh-urls,
|
|
5
|
-
* clone-commands, ids) register in a single registry. One `format()` function
|
|
6
|
-
* dispatches. `exportToFile` and `pipeExec` are methods on the same module.
|
|
7
|
-
*/
|
|
8
|
-
import { writeFileSync } from "fs";
|
|
9
|
-
import type { Repo } from "./types";
|
|
10
|
-
import { openUrl } from "./open-url";
|
|
11
|
-
|
|
12
|
-
export type Format =
|
|
13
|
-
| "json"
|
|
14
|
-
| "
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
["
|
|
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
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
* Output module — unified formatting, export, and pipe execution.
|
|
3
|
+
*
|
|
4
|
+
* All format functions (JSON, CSV, Markdown, text, urls, names, ssh-urls,
|
|
5
|
+
* clone-commands, ids) register in a single registry. One `format()` function
|
|
6
|
+
* dispatches. `exportToFile` and `pipeExec` are methods on the same module.
|
|
7
|
+
*/
|
|
8
|
+
import { writeFileSync } from "fs";
|
|
9
|
+
import type { Repo } from "./types";
|
|
10
|
+
import { openUrl } from "./open-url";
|
|
11
|
+
|
|
12
|
+
export type Format =
|
|
13
|
+
| "json"
|
|
14
|
+
| "csv"
|
|
15
|
+
| "markdown"
|
|
16
|
+
| "text"
|
|
17
|
+
| "urls"
|
|
18
|
+
| "names"
|
|
19
|
+
| "ssh-urls"
|
|
20
|
+
| "clone-commands"
|
|
21
|
+
| "ids";
|
|
22
|
+
|
|
23
|
+
export type FormatFn = (repos: Repo[]) => string;
|
|
24
|
+
|
|
25
|
+
/** Registry of all format functions. Adding a format = 1 line here. */
|
|
26
|
+
const FORMATS: Map<Format, FormatFn> = new Map([
|
|
27
|
+
// Export formats
|
|
28
|
+
["json", formatJson],
|
|
29
|
+
["csv", formatCsv],
|
|
30
|
+
["markdown", formatMarkdown],
|
|
31
|
+
["text", formatText],
|
|
32
|
+
// Pipe formats
|
|
33
|
+
["urls", formatUrls],
|
|
34
|
+
["names", formatNames],
|
|
35
|
+
["ssh-urls", formatSshUrls],
|
|
36
|
+
["clone-commands", formatCloneCommands],
|
|
37
|
+
["ids", formatIds],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// ── Export format functions ────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export function formatJson(repos: Repo[]): string {
|
|
43
|
+
return JSON.stringify(repos, null, 2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function csvEscape(val: unknown): string {
|
|
47
|
+
const str = String(val ?? "");
|
|
48
|
+
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
49
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
50
|
+
}
|
|
51
|
+
return str;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatCsv(repos: Repo[]): string {
|
|
55
|
+
const header = "rank,full_name,stars,forks,language,url";
|
|
56
|
+
const rows = repos.map(
|
|
57
|
+
(r, i) =>
|
|
58
|
+
`${i + 1},${csvEscape(r.fullName)},${r.stars},${r.forks},${csvEscape(r.language ?? "")},${csvEscape(r.url)}`,
|
|
59
|
+
);
|
|
60
|
+
return [header, ...rows].join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatMarkdown(repos: Repo[]): string {
|
|
64
|
+
const header = "| # | Repo | Stars | Forks | Language |";
|
|
65
|
+
const sep = "|---|------|-------|-------|----------|";
|
|
66
|
+
const rows = repos.map(
|
|
67
|
+
(r, i) =>
|
|
68
|
+
`| ${i + 1} | [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "—"} |`,
|
|
69
|
+
);
|
|
70
|
+
return [header, sep, ...rows].join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatText(repos: Repo[]): string {
|
|
74
|
+
return repos
|
|
75
|
+
.map(
|
|
76
|
+
(r, i) =>
|
|
77
|
+
`${i + 1}. ${r.fullName} ★ ${r.stars.toLocaleString()} ${r.url}`,
|
|
78
|
+
)
|
|
79
|
+
.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Pipe format functions ──────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export function formatUrls(repos: Repo[]): string {
|
|
85
|
+
return repos.map((r) => r.url).join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatNames(repos: Repo[]): string {
|
|
89
|
+
return repos.map((r) => r.fullName).join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatSshUrls(repos: Repo[]): string {
|
|
93
|
+
return repos.map((r) => `git@github.com:${r.fullName}.git`).join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatCloneCommands(repos: Repo[]): string {
|
|
97
|
+
return repos
|
|
98
|
+
.map((r) => `git clone git@github.com:${r.fullName}.git`)
|
|
99
|
+
.join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatIds(repos: Repo[]): string {
|
|
103
|
+
return repos.map((r) => String(r.id)).join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/** Format repos using the registered format function. */
|
|
109
|
+
export function format(repos: Repo[], format: Format): string {
|
|
110
|
+
const fn = FORMATS.get(format);
|
|
111
|
+
if (!fn) throw new Error(`Unknown format: ${format}`);
|
|
112
|
+
return fn(repos);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Export repos to a file. Returns the file path. */
|
|
116
|
+
export function exportToFile(repos: Repo[], fmt: Format): string {
|
|
117
|
+
const ext = fmt === "markdown" ? "md" : fmt;
|
|
118
|
+
const ts = Date.now();
|
|
119
|
+
const path = `./ghfind-export-${ts}.${ext}`;
|
|
120
|
+
writeFileSync(path, format(repos, fmt), "utf-8");
|
|
121
|
+
return path;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Execute a pipe target (clone, open) on repos. */
|
|
125
|
+
export async function pipeExec(repos: Repo[], target: string): Promise<void> {
|
|
126
|
+
for (const repo of repos) {
|
|
127
|
+
const url = repo.url;
|
|
128
|
+
switch (target) {
|
|
129
|
+
case "open":
|
|
130
|
+
await openUrl(url);
|
|
131
|
+
break;
|
|
132
|
+
case "clone":
|
|
133
|
+
console.log(`git clone git@github.com:${repo.fullName}.git`);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Backward-compatible re-exports ─────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export type ExportFormat = "json" | "csv" | "markdown" | "text";
|
|
142
|
+
export type FormatLine =
|
|
143
|
+
"urls" | "names" | "ssh-urls" | "clone-commands" | "ids";
|
|
144
|
+
|
|
145
|
+
/** @deprecated Use `format()` instead. */
|
|
146
|
+
export function formatRepos(repos: Repo[], fmt: ExportFormat): string {
|
|
147
|
+
return format(repos, fmt);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @deprecated Use `format()` instead. */
|
|
151
|
+
export function formatLines(repos: Repo[], fmt: FormatLine): string {
|
|
152
|
+
return format(repos, fmt);
|
|
153
|
+
}
|