github-search-cli 9.10.0 → 9.10.1
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 +43 -0
- package/README.md +13 -21
- package/dist/cli.js +114 -17
- package/package.json +1 -1
- package/release-notes/9.10.1.md +59 -0
- package/src/cli.ts +77 -7
- package/src/history.ts +4 -5
- package/src/mcp-server.ts +4 -2
- package/src/output.ts +11 -1
- package/src/package-browser.ts +2 -0
- package/src/status.ts +40 -0
- package/src/trending.ts +3 -1
- package/src/tui.ts +51 -9
- package/src/watch.ts +18 -6
- package/src/export.ts +0 -73
- package/src/pipe.ts +0 -60
- package/src/search-state.ts +0 -278
package/src/output.ts
CHANGED
|
@@ -22,8 +22,11 @@ export type Format =
|
|
|
22
22
|
|
|
23
23
|
export type FormatFn = (repos: Repo[]) => string;
|
|
24
24
|
|
|
25
|
+
/** Accepted `--pipe` targets — the cases `pipeExec` actually handles. */
|
|
26
|
+
export const PIPE_TARGETS: readonly string[] = ["open", "clone"];
|
|
27
|
+
|
|
25
28
|
/** Registry of all format functions. Adding a format = 1 line here. */
|
|
26
|
-
const FORMATS: Map<Format, FormatFn> = new Map([
|
|
29
|
+
const FORMATS: Map<Format, FormatFn> = new Map<Format, FormatFn>([
|
|
27
30
|
// Export formats
|
|
28
31
|
["json", formatJson],
|
|
29
32
|
["csv", formatCsv],
|
|
@@ -37,6 +40,13 @@ const FORMATS: Map<Format, FormatFn> = new Map([
|
|
|
37
40
|
["ids", formatIds],
|
|
38
41
|
]);
|
|
39
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Every accepted `--format` value, derived from the registry above so the CLI
|
|
45
|
+
* can validate up front instead of throwing `Unknown format` mid-run (a plain
|
|
46
|
+
* typo used to route into the crash-report flow).
|
|
47
|
+
*/
|
|
48
|
+
export const FORMAT_VALUES: readonly string[] = [...FORMATS.keys()];
|
|
49
|
+
|
|
40
50
|
// ── Export format functions ────────────────────────────────────────────
|
|
41
51
|
|
|
42
52
|
export function formatJson(repos: Repo[]): string {
|
package/src/package-browser.ts
CHANGED
|
@@ -265,6 +265,8 @@ export async function launchPackageBrowser(): Promise<void> {
|
|
|
265
265
|
renderer.keyInput.on("keypress", (key: import("@opentui/core").KeyEvent) => {
|
|
266
266
|
const name = key.name || "";
|
|
267
267
|
if (name === "q" || name === "escape") {
|
|
268
|
+
// Release the terminal before the hard exit, same as the main TUI.
|
|
269
|
+
renderer.destroy();
|
|
268
270
|
process.exit(0);
|
|
269
271
|
return;
|
|
270
272
|
}
|
package/src/status.ts
CHANGED
|
@@ -12,12 +12,16 @@ export class StatusManager {
|
|
|
12
12
|
private type: StatusType = "idle";
|
|
13
13
|
private message = "";
|
|
14
14
|
private onUpdate: (text: string) => void;
|
|
15
|
+
private disposed = false;
|
|
15
16
|
|
|
16
17
|
constructor(onUpdate: (text: string) => void) {
|
|
17
18
|
this.onUpdate = onUpdate;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
set(type: StatusType, message: string): void {
|
|
22
|
+
// Late async continuations (a search resolving after quit) can call
|
|
23
|
+
// set() long after the UI is gone; a disposed manager is inert.
|
|
24
|
+
if (this.disposed) return;
|
|
21
25
|
this.stopSpinner();
|
|
22
26
|
this.type = type;
|
|
23
27
|
this.message = message;
|
|
@@ -32,6 +36,7 @@ export class StatusManager {
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
private startSpinner(frames: string[]): void {
|
|
39
|
+
if (this.disposed) return;
|
|
35
40
|
this.spinnerIndex = 0;
|
|
36
41
|
this.emit(frames[0]);
|
|
37
42
|
this.spinnerInterval = setInterval(() => {
|
|
@@ -48,6 +53,8 @@ export class StatusManager {
|
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
private emit(spinner?: string): void {
|
|
56
|
+
// A tick can already be queued when dispose() clears the interval.
|
|
57
|
+
if (this.disposed) return;
|
|
51
58
|
const icon =
|
|
52
59
|
this.type === "searching"
|
|
53
60
|
? "🔍"
|
|
@@ -63,6 +70,39 @@ export class StatusManager {
|
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
dispose(): void {
|
|
73
|
+
this.disposed = true;
|
|
66
74
|
this.stopSpinner();
|
|
67
75
|
}
|
|
68
76
|
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The status-bar renderable, narrowed to what the manager needs. TextRenderable
|
|
80
|
+
* satisfies this structurally (isDestroyed is a public getter on Renderable).
|
|
81
|
+
*/
|
|
82
|
+
export interface StatusBarTarget {
|
|
83
|
+
readonly isDestroyed: boolean;
|
|
84
|
+
// Accessor pair, not `content: string`: TextRenderable's getter widens to
|
|
85
|
+
// StyledText while its setter accepts a plain string. `unknown` on the read
|
|
86
|
+
// side keeps the renderable assignable; we only ever write strings.
|
|
87
|
+
get content(): unknown;
|
|
88
|
+
set content(value: string);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a StatusManager wired to a status bar, plus a hook to request a frame.
|
|
93
|
+
*
|
|
94
|
+
* Writes are dropped once the renderable is destroyed. The renderer tears its
|
|
95
|
+
* tree down on Ctrl+C / SIGTERM / terminal close but leaves the event loop
|
|
96
|
+
* running, so an unguarded spinner tick would assign to a destroyed TextBuffer
|
|
97
|
+
* and throw out of a timer callback, taking the whole process with it.
|
|
98
|
+
*/
|
|
99
|
+
export function createStatusManager(
|
|
100
|
+
bar: StatusBarTarget,
|
|
101
|
+
onFrame: () => void,
|
|
102
|
+
): StatusManager {
|
|
103
|
+
return new StatusManager((text) => {
|
|
104
|
+
if (bar.isDestroyed) return;
|
|
105
|
+
bar.content = ` ${text}`;
|
|
106
|
+
onFrame();
|
|
107
|
+
});
|
|
108
|
+
}
|
package/src/trending.ts
CHANGED
|
@@ -398,7 +398,7 @@ export async function launchTrending(): Promise<void> {
|
|
|
398
398
|
paddingX: 1,
|
|
399
399
|
});
|
|
400
400
|
const footerText = new TextRenderable(renderer, {
|
|
401
|
-
content: " ↑↓ navigate ↵ open /
|
|
401
|
+
content: " ↑↓ navigate ↵ open / top 1-3 tab q quit r refresh",
|
|
402
402
|
fg: C.muted,
|
|
403
403
|
height: 1,
|
|
404
404
|
});
|
|
@@ -430,6 +430,8 @@ export async function launchTrending(): Promise<void> {
|
|
|
430
430
|
renderer.keyInput.on("keypress", (key) => {
|
|
431
431
|
const name = key.name || "";
|
|
432
432
|
if (name === "q") {
|
|
433
|
+
// Release the terminal before the hard exit, same as the main TUI.
|
|
434
|
+
renderer.destroy();
|
|
433
435
|
process.exit(0);
|
|
434
436
|
return;
|
|
435
437
|
}
|
package/src/tui.ts
CHANGED
|
@@ -108,7 +108,7 @@ import {
|
|
|
108
108
|
PACKAGE_SORT_MODES,
|
|
109
109
|
type PackageSortMode,
|
|
110
110
|
} from "./package";
|
|
111
|
-
import { StatusManager } from "./status";
|
|
111
|
+
import { type StatusManager, createStatusManager } from "./status";
|
|
112
112
|
import {
|
|
113
113
|
getNotifications,
|
|
114
114
|
dismissNotification,
|
|
@@ -199,6 +199,15 @@ function createTuiLogger(): Logger {
|
|
|
199
199
|
|
|
200
200
|
const logger: Logger = createTuiLogger();
|
|
201
201
|
|
|
202
|
+
// The live StatusManager for the current renderer. Kept at module scope so the
|
|
203
|
+
// hoisted cleanup() can dispose the spinner interval on the quit path; a stale
|
|
204
|
+
// interval would otherwise outlive the destroyed renderable it writes to.
|
|
205
|
+
let activeStatusMgr: StatusManager | null = null;
|
|
206
|
+
// The live renderer, so the hoisted cleanup() can release the terminal
|
|
207
|
+
// (raw mode, alternate screen) before process.exit() — same reasoning as
|
|
208
|
+
// activeStatusMgr above.
|
|
209
|
+
let activeRenderer: CliRenderer | null = null;
|
|
210
|
+
|
|
202
211
|
// ─── Sort cycle ───────────────────────────────────────────────────────
|
|
203
212
|
const SORT_MODES: { key: SortStrategy; label: string }[] = [
|
|
204
213
|
{ key: "best-match", label: "best-match" },
|
|
@@ -208,7 +217,10 @@ const SORT_MODES: { key: SortStrategy; label: string }[] = [
|
|
|
208
217
|
];
|
|
209
218
|
|
|
210
219
|
// ─── Main ─────────────────────────────────────────────────────────────
|
|
211
|
-
export async function launchBrowser(
|
|
220
|
+
export async function launchBrowser(
|
|
221
|
+
theme_override?: string,
|
|
222
|
+
startMode?: "trending",
|
|
223
|
+
): Promise<void> {
|
|
212
224
|
// Ask the terminal for its real background color (OSC 11) BEFORE the
|
|
213
225
|
// renderer takes over stdin — after that, reading the reply gets messy.
|
|
214
226
|
// Used below so the app backdrop matches the user's terminal theme.
|
|
@@ -247,6 +259,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
247
259
|
}
|
|
248
260
|
|
|
249
261
|
const root = renderer.root;
|
|
262
|
+
activeRenderer = renderer;
|
|
250
263
|
root.flexDirection = "column";
|
|
251
264
|
|
|
252
265
|
// ── Config ──
|
|
@@ -692,6 +705,10 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
692
705
|
leaderBox.visible = false;
|
|
693
706
|
leaderDim.visible = false;
|
|
694
707
|
readmeBox.visible = false;
|
|
708
|
+
// The org panel is a flexGrow sibling of `body` just like every other
|
|
709
|
+
// overlay — omitting it here meant it stayed visible after Esc, permanently
|
|
710
|
+
// stealing half the content height and every click in the lower half.
|
|
711
|
+
orgBox.visible = false;
|
|
695
712
|
updateBox.visible = false;
|
|
696
713
|
updateDim.visible = false;
|
|
697
714
|
if (type === "none") {
|
|
@@ -1929,6 +1946,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1929
1946
|
},
|
|
1930
1947
|
});
|
|
1931
1948
|
const orgText = new TextRenderable(renderer, {
|
|
1949
|
+
...NON_INTERACTIVE_TEXT,
|
|
1932
1950
|
content: "",
|
|
1933
1951
|
fg: colors.text,
|
|
1934
1952
|
bg: colors.bg,
|
|
@@ -1936,6 +1954,7 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
1936
1954
|
orgScroll.add(orgText);
|
|
1937
1955
|
orgBox.add(orgScroll);
|
|
1938
1956
|
const orgFooter = new TextRenderable(renderer, {
|
|
1957
|
+
...NON_INTERACTIVE_TEXT,
|
|
1939
1958
|
content: " ↑↓/jk scroll Esc/q close",
|
|
1940
1959
|
fg: colors.muted,
|
|
1941
1960
|
bg: colors.bg,
|
|
@@ -2130,10 +2149,16 @@ export async function launchBrowser(theme_override?: string): Promise<void> {
|
|
|
2130
2149
|
renderer.requestRender();
|
|
2131
2150
|
};
|
|
2132
2151
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2152
|
+
// Held at module scope so cleanup() (a hoisted function declaration, called
|
|
2153
|
+
// from key handlers defined above the manager's creation) can still reach it.
|
|
2154
|
+
activeStatusMgr = createStatusManager(statusBar, () =>
|
|
2155
|
+
renderer.requestRender(),
|
|
2156
|
+
);
|
|
2157
|
+
const statusMgr = activeStatusMgr;
|
|
2158
|
+
// The renderer tears down its renderables on Ctrl+C / SIGTERM / terminal
|
|
2159
|
+
// close, but the 120ms spinner interval is ours. Without this, the next tick
|
|
2160
|
+
// assigns to a destroyed TextBuffer and throws out of a timer callback.
|
|
2161
|
+
renderer.on("destroy", () => statusMgr.dispose());
|
|
2137
2162
|
setTimeout(() => {
|
|
2138
2163
|
if (currentOverlay === "none") {
|
|
2139
2164
|
setStatus(nextTip());
|
|
@@ -3518,12 +3543,18 @@ ${pack.description ?? ""}`;
|
|
|
3518
3543
|
// ── Update check (before start) ──────────────────────────────────────
|
|
3519
3544
|
const _currentVersion = getVersion();
|
|
3520
3545
|
// ── Start ──────────────────────────────────────────────────────────
|
|
3521
|
-
//
|
|
3546
|
+
// Default is the landing menu. Saved sessions restore preferences
|
|
3522
3547
|
// (sort/limit/trending tab) and pre-fill the query once a mode is picked,
|
|
3523
3548
|
// but never auto-run a search — a stale or corrupted session (e.g. an MCP
|
|
3524
3549
|
// handshake blob saved as a query) used to hijack startup and skip the
|
|
3525
|
-
// menu on every launch.
|
|
3526
|
-
|
|
3550
|
+
// menu on every launch. An explicit `ghfind --trending` is not a stale
|
|
3551
|
+
// session though: it is the mode the user asked for, so it skips the menu.
|
|
3552
|
+
if (startMode === "trending") {
|
|
3553
|
+
showLanding(false);
|
|
3554
|
+
loadTrending();
|
|
3555
|
+
} else {
|
|
3556
|
+
showLanding(true);
|
|
3557
|
+
}
|
|
3527
3558
|
checkForUpdateAndShow();
|
|
3528
3559
|
checkPostUpgradePanel();
|
|
3529
3560
|
renderer.start();
|
|
@@ -3549,11 +3580,22 @@ function formatToolbar(
|
|
|
3549
3580
|
}
|
|
3550
3581
|
|
|
3551
3582
|
function cleanup(): void {
|
|
3583
|
+
// Stop the spinner before exiting: process.exit() would kill the process
|
|
3584
|
+
// anyway, but a tick already queued between here and the exit can still fire.
|
|
3585
|
+
activeStatusMgr?.dispose();
|
|
3586
|
+
activeStatusMgr = null;
|
|
3552
3587
|
try {
|
|
3553
3588
|
rotateHistory();
|
|
3554
3589
|
} catch {
|
|
3555
3590
|
/* non-critical */
|
|
3556
3591
|
}
|
|
3592
|
+
// Release the terminal (raw mode, alternate screen) before the hard exit.
|
|
3593
|
+
try {
|
|
3594
|
+
activeRenderer?.destroy();
|
|
3595
|
+
} catch {
|
|
3596
|
+
/* non-critical */
|
|
3597
|
+
}
|
|
3598
|
+
activeRenderer = null;
|
|
3557
3599
|
process.exit(0);
|
|
3558
3600
|
}
|
|
3559
3601
|
|
package/src/watch.ts
CHANGED
|
@@ -19,6 +19,23 @@ export interface WatchOptions {
|
|
|
19
19
|
token?: string;
|
|
20
20
|
intervalMs: number;
|
|
21
21
|
trending?: boolean;
|
|
22
|
+
/** Trending period (`--since`); falls back to the query-string convention. */
|
|
23
|
+
since?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the trending period for a watch tick. `--since` wins when it names a
|
|
28
|
+
* real period; otherwise fall back to the legacy `ghfind --watch weekly`
|
|
29
|
+
* convention where the query string itself carried the period.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveTrendingSince(
|
|
32
|
+
opts: Pick<WatchOptions, "query" | "since">,
|
|
33
|
+
): "daily" | "weekly" | "monthly" {
|
|
34
|
+
if (opts.since === "weekly" || opts.since === "monthly") return opts.since;
|
|
35
|
+
if (opts.since === "daily") return "daily";
|
|
36
|
+
if (opts.query === "weekly") return "weekly";
|
|
37
|
+
if (opts.query === "monthly") return "monthly";
|
|
38
|
+
return "daily";
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
const MAX_FAILS = 5;
|
|
@@ -44,12 +61,7 @@ export async function runWatch(
|
|
|
44
61
|
try {
|
|
45
62
|
let repos: Repo[];
|
|
46
63
|
if (opts.trending) {
|
|
47
|
-
const since =
|
|
48
|
-
opts.query === "weekly"
|
|
49
|
-
? "weekly"
|
|
50
|
-
: opts.query === "monthly"
|
|
51
|
-
? "monthly"
|
|
52
|
-
: "daily";
|
|
64
|
+
const since = resolveTrendingSince(opts);
|
|
53
65
|
const trendingSearch = createTrendingSearch();
|
|
54
66
|
const response = await trendingSearch.search(trendingQuery(), {
|
|
55
67
|
limit: opts.limit,
|
package/src/export.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Export — format repos as JSON, CSV, Markdown, or plain text.
|
|
3
|
-
*/
|
|
4
|
-
import { writeFileSync } from "node:fs";
|
|
5
|
-
import type { Repo } from "./types";
|
|
6
|
-
|
|
7
|
-
export type ExportFormat = "json" | "csv" | "markdown" | "text";
|
|
8
|
-
|
|
9
|
-
export function formatJson(repos: Repo[]): string {
|
|
10
|
-
return JSON.stringify(repos, null, 2);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function csvEscape(val: unknown): string {
|
|
14
|
-
const str = String(val ?? "");
|
|
15
|
-
if (str.includes('"') || str.includes(",") || str.includes("\n")) {
|
|
16
|
-
return `"${str.replace(/"/g, '""')}"`;
|
|
17
|
-
}
|
|
18
|
-
return str;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Format repos as CSV string. */
|
|
22
|
-
export function formatCsv(repos: Repo[]): string {
|
|
23
|
-
const header = "rank,full_name,stars,forks,language,url";
|
|
24
|
-
const rows = repos.map(
|
|
25
|
-
(r, i) =>
|
|
26
|
-
`${i + 1},${csvEscape(r.fullName)},${r.stars},${r.forks},${csvEscape(r.language ?? "")},${csvEscape(r.url)}`,
|
|
27
|
-
);
|
|
28
|
-
return [header, ...rows].join("\n");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Format repos as Markdown table. */
|
|
32
|
-
export function formatMarkdown(repos: Repo[]): string {
|
|
33
|
-
const header = "| # | Repo | Stars | Forks | Language |";
|
|
34
|
-
const sep = "|---|------|-------|-------|----------|";
|
|
35
|
-
const rows = repos.map(
|
|
36
|
-
(r, i) =>
|
|
37
|
-
`| ${i + 1} | [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "—"} |`,
|
|
38
|
-
);
|
|
39
|
-
return [header, sep, ...rows].join("\n");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Format repos as plain text, one per line. */
|
|
43
|
-
export function formatText(repos: Repo[]): string {
|
|
44
|
-
return repos
|
|
45
|
-
.map(
|
|
46
|
-
(r, i) =>
|
|
47
|
-
`${i + 1}. ${r.fullName} ★ ${r.stars.toLocaleString()} ${r.url}`,
|
|
48
|
-
)
|
|
49
|
-
.join("\n");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Format repos in the given format. */
|
|
53
|
-
export function formatRepos(repos: Repo[], format: ExportFormat): string {
|
|
54
|
-
switch (format) {
|
|
55
|
-
case "json":
|
|
56
|
-
return formatJson(repos);
|
|
57
|
-
case "csv":
|
|
58
|
-
return formatCsv(repos);
|
|
59
|
-
case "markdown":
|
|
60
|
-
return formatMarkdown(repos);
|
|
61
|
-
case "text":
|
|
62
|
-
return formatText(repos);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Export repos to a file. Returns the file path. */
|
|
67
|
-
export function exportToFile(repos: Repo[], format: ExportFormat): string {
|
|
68
|
-
const ext = format === "markdown" ? "md" : format;
|
|
69
|
-
const ts = Date.now();
|
|
70
|
-
const path = `./ghfind-export-${ts}.${ext}`;
|
|
71
|
-
writeFileSync(path, formatRepos(repos, format), "utf-8");
|
|
72
|
-
return path;
|
|
73
|
-
}
|
package/src/pipe.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|