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.
Files changed (52) hide show
  1. package/CHANGELOG.md +38 -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
@@ -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: "info" | "warning" | "rate-limit" | "watch-change" | "upgrade" | "release";
7
- icon: string;
8
- message: string;
9
- timestamp: number;
10
- }
11
-
12
- let nextId = 1;
13
- let notifs: Notification[] = [];
14
- const MAX_NOTIFS = 50;
15
- const FILE = "notifications.json";
16
-
17
- function load(): void {
18
- notifs = readJSON<Notification[]>(FILE, []);
19
- nextId = Math.max(...notifs.map((n) => n.id), 0) + 1;
20
- }
21
-
22
- function save(): void {
23
- writeJSON(FILE, notifs);
24
- }
25
-
26
- export function addNotification(type: Notification["type"], message: string): Notification {
27
- if (notifs.length === 0) load();
28
- const n: Notification = { id: nextId++, type, icon: iconFor(type), message, timestamp: Date.now() };
29
- notifs.unshift(n);
30
- if (notifs.length > MAX_NOTIFS) notifs = notifs.slice(0, MAX_NOTIFS);
31
- save();
32
- return n;
33
- }
34
-
35
- function iconFor(type: Notification["type"]): string {
36
- switch (type) {
37
- case "info": return "ℹ";
38
- case "warning": return "⚠";
39
- case "rate-limit": return "🚫";
40
- case "watch-change": return "🔔";
41
- case "upgrade": return "📦";
42
- case "release": return "🚀";
43
- }
44
- }
45
-
46
- export function getNotifications(): Notification[] {
47
- if (notifs.length === 0) load();
48
- return [...notifs];
49
- }
50
-
51
- export function dismissNotification(id: number): void {
52
- notifs = notifs.filter((n) => n.id !== id);
53
- save();
54
- }
55
-
56
- export function dismissAll(): void {
57
- notifs = [];
58
- save();
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(`https://api.github.com/orgs/${encodeURIComponent(name)}`, {
148
- headers: apiHeaders(token),
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}"`)) throw err;
160
- throw new Error(`Failed to fetch org "${name}": ${err instanceof Error ? err.message : String(err)}`);
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(`Repos ${p.publicRepoCount.toLocaleString()} public${p.fetchedRepoCount < p.publicRepoCount ? ` (aggregated ${p.fetchedRepoCount})` : ""}`);
202
- lines.push(`Stars ${p.totalStars.toLocaleString()} (sum of aggregated repos)`);
203
- lines.push(`Forks ${p.totalForks.toLocaleString()} (sum of aggregated repos)`);
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(` ${r.fullName} ★ ${r.stars.toLocaleString()} ${r.language ?? ""}`);
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) => (/[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);
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 ["login,name,url,public_repos,total_stars,total_forks,top_languages,created_at", row.join(",")].join("\n");
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(`| Top languages | ${p.topLanguages.map((l) => `${l.language} (${l.repos})`).join(", ")} |`);
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("", "## Top repos by stars", "", "| Repo | Stars | Language |", "| --- | --- | --- |");
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(`| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.language ?? "—"} |`);
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" | "csv" | "markdown" | "text"
14
- | "urls" | "names" | "ssh-urls" | "clone-commands" | "ids";
15
-
16
- export type FormatFn = (repos: Repo[]) => string;
17
-
18
- /** Registry of all format functions. Adding a format = 1 line here. */
19
- const FORMATS: Map<Format, FormatFn> = new Map([
20
- // Export formats
21
- ["json", formatJson],
22
- ["csv", formatCsv],
23
- ["markdown", formatMarkdown],
24
- ["text", formatText],
25
- // Pipe formats
26
- ["urls", formatUrls],
27
- ["names", formatNames],
28
- ["ssh-urls", formatSshUrls],
29
- ["clone-commands", formatCloneCommands],
30
- ["ids", formatIds],
31
- ]);
32
-
33
- // ── Export format functions ────────────────────────────────────────────
34
-
35
- export function formatJson(repos: Repo[]): string {
36
- return JSON.stringify(repos, null, 2);
37
- }
38
-
39
- function csvEscape(val: unknown): string {
40
- const str = String(val ?? "");
41
- if (str.includes('"') || str.includes(",") || str.includes("\n")) {
42
- return `"${str.replace(/"/g, '""')}"`;
43
- }
44
- return str;
45
- }
46
-
47
- export function formatCsv(repos: Repo[]): string {
48
- const header = "rank,full_name,stars,forks,language,url";
49
- const rows = repos.map((r, i) =>
50
- `${i + 1},${csvEscape(r.fullName)},${r.stars},${r.forks},${csvEscape(r.language ?? "")},${csvEscape(r.url)}`,
51
- );
52
- return [header, ...rows].join("\n");
53
- }
54
-
55
- export function formatMarkdown(repos: Repo[]): string {
56
- const header = "| # | Repo | Stars | Forks | Language |";
57
- const sep = "|---|------|-------|-------|----------|";
58
- const rows = repos.map(
59
- (r, i) =>
60
- `| ${i + 1} | [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "—"} |`,
61
- );
62
- return [header, sep, ...rows].join("\n");
63
- }
64
-
65
- export function formatText(repos: Repo[]): string {
66
- return repos.map((r, i) => `${i + 1}. ${r.fullName} ★ ${r.stars.toLocaleString()} ${r.url}`).join("\n");
67
- }
68
-
69
- // ── Pipe format functions ──────────────────────────────────────────────
70
-
71
- export function formatUrls(repos: Repo[]): string {
72
- return repos.map((r) => r.url).join("\n");
73
- }
74
-
75
- export function formatNames(repos: Repo[]): string {
76
- return repos.map((r) => r.fullName).join("\n");
77
- }
78
-
79
- export function formatSshUrls(repos: Repo[]): string {
80
- return repos.map((r) => `git@github.com:${r.fullName}.git`).join("\n");
81
- }
82
-
83
- export function formatCloneCommands(repos: Repo[]): string {
84
- return repos.map((r) => `git clone git@github.com:${r.fullName}.git`).join("\n");
85
- }
86
-
87
- export function formatIds(repos: Repo[]): string {
88
- return repos.map((r) => String(r.id)).join("\n");
89
- }
90
-
91
- // ── Public API ─────────────────────────────────────────────────────────
92
-
93
- /** Format repos using the registered format function. */
94
- export function format(repos: Repo[], format: Format): string {
95
- const fn = FORMATS.get(format);
96
- if (!fn) throw new Error(`Unknown format: ${format}`);
97
- return fn(repos);
98
- }
99
-
100
- /** Export repos to a file. Returns the file path. */
101
- export function exportToFile(repos: Repo[], fmt: Format): string {
102
- const ext = fmt === "markdown" ? "md" : fmt;
103
- const ts = Date.now();
104
- const path = `./ghfind-export-${ts}.${ext}`;
105
- writeFileSync(path, format(repos, fmt), "utf-8");
106
- return path;
107
- }
108
-
109
- /** Execute a pipe target (clone, open) on repos. */
110
- export async function pipeExec(repos: Repo[], target: string): Promise<void> {
111
- for (const repo of repos) {
112
- const url = repo.url;
113
- switch (target) {
114
- case "open":
115
- await openUrl(url);
116
- break;
117
- case "clone":
118
- console.log(`git clone git@github.com:${repo.fullName}.git`);
119
- break;
120
- }
121
- }
122
- }
123
-
124
- // ─── Backward-compatible re-exports ─────────────────────────────────────
125
-
126
- export type ExportFormat = "json" | "csv" | "markdown" | "text";
127
- export type FormatLine = "urls" | "names" | "ssh-urls" | "clone-commands" | "ids";
128
-
129
- /** @deprecated Use `format()` instead. */
130
- export function formatRepos(repos: Repo[], fmt: ExportFormat): string {
131
- return format(repos, fmt);
132
- }
133
-
134
- /** @deprecated Use `format()` instead. */
135
- export function formatLines(repos: Repo[], fmt: FormatLine): string {
136
- return format(repos, fmt);
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
+ }