github-search-cli 0.8.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-search-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Terminal-native GitHub search — search, trending, bookmarks, history, deep-dive, compare, export, and more. All keyboard-driven, no browser needed.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "bin/",
11
11
  "src/",
12
+ "vendor/",
12
13
  "completions/",
13
14
  "demo.svg",
14
15
  "LICENSE",
@@ -16,10 +17,12 @@
16
17
  ],
17
18
  "scripts": {
18
19
  "start": "bun run src/cli.ts",
19
- "build": "tsup src/cli.ts --format esm --target node20 --out-dir dist --external @opentui/core",
20
+ "build": "tsup",
20
21
  "prepublishOnly": "npm run build",
21
22
  "test": "vitest run",
23
+ "test:bun": "bun test",
22
24
  "dev": "bun run --watch src/tui.ts",
25
+ "postinstall": "node scripts/postinstall.js",
23
26
  "format:check": "prettier --check .",
24
27
  "format": "prettier --write ."
25
28
  },
package/src/bookmarks.ts CHANGED
@@ -1,69 +1,69 @@
1
- /**
2
- * Bookmarks — saved repos with tags, persisted as JSON.
3
- */
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
- import { join } from "path";
6
- import type { Bookmark, Repo } from "./types";
7
- import { stateDir } from "./config";
8
-
9
- const BOOKMARK_FILE = join(stateDir(), "bookmarks.json");
10
-
11
- function ensureDir() {
12
- mkdirSync(stateDir(), { recursive: true });
13
- }
14
-
15
- function loadRaw(): Bookmark[] {
16
- ensureDir();
17
- try {
18
- const raw = readFileSync(BOOKMARK_FILE, "utf-8");
19
- return JSON.parse(raw) as Bookmark[];
20
- } catch {
21
- return [];
22
- }
23
- }
24
-
25
- function saveRaw(bookmarks: Bookmark[]): void {
26
- ensureDir();
27
- writeFileSync(BOOKMARK_FILE, JSON.stringify(bookmarks, null, 2));
28
- }
29
-
30
- /** Get all bookmarks, newest first. */
31
- export function getBookmarks(): Bookmark[] {
32
- return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
33
- }
34
-
35
- /** Check if a repo is bookmarked by fullName. */
36
- export function isBookmarked(fullName: string): boolean {
37
- return loadRaw().some((b) => b.repo.fullName === fullName);
38
- }
39
-
40
- /** Toggle bookmark: add if not present, remove if present. Returns new state. */
41
- export function toggleBookmark(repo: Repo, tags: string[] = []): boolean {
42
- const bookmarks = loadRaw();
43
- const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
44
- if (idx >= 0) {
45
- bookmarks.splice(idx, 1);
46
- saveRaw(bookmarks);
47
- return false; // removed
48
- }
49
- bookmarks.push({ repo, savedAt: Date.now(), tags });
50
- saveRaw(bookmarks);
51
- return true; // added
52
- }
53
-
54
- /** Remove a bookmark by fullName. */
55
- export function removeBookmark(fullName: string): void {
56
- const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
57
- saveRaw(bookmarks);
58
- }
59
-
60
- /** Search bookmarks by text (matches name, description, tags). */
61
- export function searchBookmarks(query: string): Bookmark[] {
62
- const q = query.toLowerCase();
63
- return getBookmarks().filter(
64
- (b) =>
65
- b.repo.fullName.toLowerCase().includes(q) ||
66
- (b.repo.description ?? "").toLowerCase().includes(q) ||
67
- b.tags.some((t) => t.toLowerCase().includes(q)),
68
- );
69
- }
1
+ /**
2
+ * Bookmarks — saved repos with tags, persisted as JSON.
3
+ */
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ import type { Bookmark, Repo } from "./types";
7
+ import { stateDir } from "./config";
8
+
9
+ const BOOKMARK_FILE = join(stateDir(), "bookmarks.json");
10
+
11
+ function ensureDir() {
12
+ mkdirSync(stateDir(), { recursive: true });
13
+ }
14
+
15
+ function loadRaw(): Bookmark[] {
16
+ ensureDir();
17
+ try {
18
+ const raw = readFileSync(BOOKMARK_FILE, "utf-8");
19
+ return JSON.parse(raw) as Bookmark[];
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ function saveRaw(bookmarks: Bookmark[]): void {
26
+ ensureDir();
27
+ writeFileSync(BOOKMARK_FILE, JSON.stringify(bookmarks, null, 2));
28
+ }
29
+
30
+ /** Get all bookmarks, newest first. */
31
+ export function getBookmarks(): Bookmark[] {
32
+ return loadRaw().sort((a, b) => b.savedAt - a.savedAt);
33
+ }
34
+
35
+ /** Check if a repo is bookmarked by fullName. */
36
+ export function isBookmarked(fullName: string): boolean {
37
+ return loadRaw().some((b) => b.repo.fullName === fullName);
38
+ }
39
+
40
+ /** Toggle bookmark: add if not present, remove if present. Returns new state. */
41
+ export function toggleBookmark(repo: Repo, tags: string[] = []): boolean {
42
+ const bookmarks = loadRaw();
43
+ const idx = bookmarks.findIndex((b) => b.repo.fullName === repo.fullName);
44
+ if (idx >= 0) {
45
+ bookmarks.splice(idx, 1);
46
+ saveRaw(bookmarks);
47
+ return false; // removed
48
+ }
49
+ bookmarks.push({ repo, savedAt: Date.now(), tags });
50
+ saveRaw(bookmarks);
51
+ return true; // added
52
+ }
53
+
54
+ /** Remove a bookmark by fullName. */
55
+ export function removeBookmark(fullName: string): void {
56
+ const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
57
+ saveRaw(bookmarks);
58
+ }
59
+
60
+ /** Search bookmarks by text (matches name, description, tags). */
61
+ export function searchBookmarks(query: string): Bookmark[] {
62
+ const q = query.toLowerCase();
63
+ return getBookmarks().filter(
64
+ (b) =>
65
+ b.repo.fullName.toLowerCase().includes(q) ||
66
+ (b.repo.description ?? "").toLowerCase().includes(q) ||
67
+ b.tags.some((t) => t.toLowerCase().includes(q)),
68
+ );
69
+ }
package/src/cli.ts CHANGED
@@ -1,239 +1,239 @@
1
- #!/usr/bin/env node
2
- /** CLI entry point — routes between TUI browser and non-interactive modes. */
3
- import { parseQuery, applyFlagFilters, rankRepos, createGitHubSearch } from "./search";
4
- import { formatRepos, type ExportFormat } from "./export";
5
- import { formatLines, pipeExec, type FormatLine } from "./pipe";
6
- import { runWatch } from "./watch";
7
- import { runInitWizard } from "./init";
8
- import { readFileSync } from "fs";
9
- import { join, dirname } from "path";
10
- import { fileURLToPath } from "url";
11
- import { launchBrowser } from "./tui";
12
- import type { SearchOptions } from "./types";
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const PKG_DIR = join(__dirname, "..");
16
-
17
- interface CLIFlags {
18
- query: string;
19
- json: boolean;
20
- csv: boolean;
21
- markdown: boolean;
22
- count: boolean;
23
- limit: number;
24
- sort: SearchOptions["sort"];
25
- token?: string;
26
- trending: boolean;
27
- since: string;
28
- pipe?: string;
29
- format?: string;
30
- watch: boolean;
31
- interval: number;
32
- completion?: string;
33
- init: boolean;
34
- version: boolean;
35
- help: boolean;
36
- }
37
-
38
- function parseArgs(args: string[]): CLIFlags {
39
- const flags: CLIFlags = {
40
- query: "",
41
- json: false,
42
- csv: false,
43
- markdown: false,
44
- count: false,
45
- limit: 50,
46
- sort: "best-match",
47
- token: process.env.GITHUB_TOKEN,
48
- trending: false,
49
- since: "daily",
50
- watch: false,
51
- interval: 300,
52
- init: false,
53
- version: false,
54
- help: false,
55
- };
56
-
57
- let queryParts: string[] = [];
58
- for (let i = 0; i < args.length; i++) {
59
- const arg = args[i];
60
- switch (arg) {
61
- case "--json": flags.json = true; break;
62
- case "--csv": flags.csv = true; break;
63
- case "--markdown": flags.markdown = true; break;
64
- case "--count": flags.count = true; break;
65
- case "--trending": flags.trending = true; break;
66
- case "--watch": flags.watch = true; break;
67
- case "--version": case "-v": flags.version = true; break;
68
- case "--help": case "-h": flags.help = true; break;
69
- case "init": flags.init = true; break;
70
- case "--limit": {
71
- const parsed = parseInt(args[++i]);
72
- if (isNaN(parsed) || parsed < 1 || parsed > 100) {
73
- console.error(`Invalid limit: must be 1-100. Using default 50.`);
74
- flags.limit = 50;
75
- } else {
76
- flags.limit = parsed;
77
- }
78
- break;
79
- }
80
- case "--sort": flags.sort = args[++i] as SearchOptions["sort"]; break;
81
- case "--token": flags.token = args[++i]; break;
82
- case "--since": flags.since = args[++i]; break;
83
- case "--pipe": flags.pipe = args[++i]; break;
84
- case "--format": flags.format = args[++i]; break;
85
- case "--interval": flags.interval = parseInt(args[++i]) || 300; break;
86
- case "--completion": flags.completion = args[++i]; break;
87
- default:
88
- if (!arg.startsWith("-")) queryParts.push(arg);
89
- }
90
- }
91
- flags.query = queryParts.join(" ");
92
- return flags;
93
- }
94
-
95
- async function main() {
96
- const flags = parseArgs(process.argv.slice(2));
97
-
98
- // Version
99
- if (flags.version) {
100
- const pkg = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"));
101
- console.log(`ghfind v${pkg.version}`);
102
- return;
103
- }
104
-
105
- // Help
106
- if (flags.help) {
107
- console.log(`
108
- ghfind — Interactive GitHub repository browser
109
-
110
- Usage:
111
- ghfind Launch interactive TUI
112
- ghfind <query> --json Search, output JSON
113
- ghfind <query> --csv Search, output CSV
114
- ghfind <query> --markdown Search, output Markdown table
115
- ghfind <query> --count Just the result count
116
- ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
117
- ghfind <query> --pipe <target> Pipe to clone/open
118
- ghfind --trending --json Trending repos as JSON
119
- ghfind --watch <query> Watch mode (poll every Ns)
120
- ghfind init Run setup wizard
121
- ghfind --completion <shell> Print completion script (bash|zsh|fish)
122
- ghfind --version Print version
123
- ghfind --help Print this help
124
-
125
- Options:
126
- --json, --csv, --markdown, --count Output format
127
- --limit <n> Max results (default: 50)
128
- --sort <strategy> Sort: best-match|stars|updated|forks
129
- --token <token> GitHub API token
130
- --trending Trending mode
131
- --since <period> Trending period: daily|weekly|monthly
132
- --pipe <target> Pipe target: clone|open
133
- --format <fmt> Line format: urls|names|ssh-urls|...
134
- --watch Watch mode (re-run periodically)
135
- --interval <s> Watch interval in seconds (default: 300)
136
- --completion <shell> Generate completions
137
- `);
138
- return;
139
- }
140
-
141
- // Shell completions
142
- if (flags.completion) {
143
- const shell = flags.completion;
144
- try {
145
- const content = readFileSync(join(PKG_DIR, "completions", `ghfind.${shell}`), "utf-8");
146
- console.log(content);
147
- } catch {
148
- console.error(`Completions not available for shell: ${shell}`);
149
- process.exit(1);
150
- }
151
- return;
152
- }
153
-
154
- // Init wizard
155
- if (flags.init) {
156
- await runInitWizard();
157
- return;
158
- }
159
-
160
- // Determine output format
161
- const format: ExportFormat | undefined =
162
- flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : undefined;
163
- const isNonInteractive = format || flags.count || flags.format || flags.pipe || flags.watch;
164
-
165
- // Non-interactive mode
166
- if (isNonInteractive) {
167
- await runNonInteractive(flags, format);
168
- return;
169
- }
170
-
171
- // Default: launch interactive TUI
172
- await launchBrowser();
173
- }
174
-
175
- async function runNonInteractive(flags: CLIFlags, format?: ExportFormat) {
176
- const query = flags.query;
177
- const provider = createGitHubSearch(undefined, flags.token ? [flags.token] : []);
178
-
179
- // --watch mode
180
- if (flags.watch) {
181
- let tick = 0;
182
- await runWatch(
183
- { query, sort: flags.sort, limit: flags.limit, token: flags.token, intervalMs: flags.interval * 1000, trending: flags.trending },
184
- (repos, delta) => {
185
- tick++;
186
- if (format) {
187
- console.log(`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`);
188
- console.log(formatRepos(repos, format));
189
- } else {
190
- console.log(`[Watch #${tick}] ${repos.length} results — ${delta >= 0 ? "+" : ""}${delta} since last check`);
191
- }
192
- },
193
- (err) => console.error(`[Watch error] ${err.message}`),
194
- );
195
- return;
196
- }
197
-
198
- // --pipe mode
199
- if (flags.pipe) {
200
- const parsed = applyFlagFilters(parseQuery(query), {});
201
- const response = await provider.search(parsed, { limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token });
202
- const repos = rankRepos(response.repos, flags.sort);
203
- await pipeExec(repos, flags.pipe);
204
- return;
205
- }
206
-
207
- // --format mode
208
- if (flags.format) {
209
- const parsed = applyFlagFilters(parseQuery(query), {});
210
- const response = await provider.search(parsed, { limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token });
211
- const repos = rankRepos(response.repos, flags.sort);
212
- console.log(formatLines(repos, flags.format as FormatLine));
213
- return;
214
- }
215
-
216
- // Standard search with format
217
- const parsed = applyFlagFilters(parseQuery(query), {});
218
- const response = await provider.search(parsed, {
219
- limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
220
- });
221
- const repos = rankRepos(response.repos, flags.sort);
222
-
223
- // --count
224
- if (flags.count) {
225
- console.log(response.totalCount);
226
- return;
227
- }
228
-
229
- // --json, --csv, --markdown
230
- if (format) {
231
- console.log(formatRepos(repos, format));
232
- return;
233
- }
234
- }
235
-
236
- main().catch((err) => {
237
- console.error(err instanceof Error ? err.message : String(err));
238
- process.exit(1);
239
- });
1
+ #!/usr/bin/env node
2
+ /** CLI entry point — routes between TUI browser and non-interactive modes. */
3
+ import { parseQuery, applyFlagFilters, rankRepos, createGitHubSearch } from "./search";
4
+ import { formatRepos, type ExportFormat } from "./export";
5
+ import { formatLines, pipeExec, type FormatLine } from "./pipe";
6
+ import { runWatch } from "./watch";
7
+ import { runInitWizard } from "./init";
8
+ import { readFileSync } from "fs";
9
+ import { join, dirname } from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { launchBrowser } from "./tui";
12
+ import type { SearchOptions } from "./types";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const PKG_DIR = join(__dirname, "..");
16
+
17
+ interface CLIFlags {
18
+ query: string;
19
+ json: boolean;
20
+ csv: boolean;
21
+ markdown: boolean;
22
+ count: boolean;
23
+ limit: number;
24
+ sort: SearchOptions["sort"];
25
+ token?: string;
26
+ trending: boolean;
27
+ since: string;
28
+ pipe?: string;
29
+ format?: string;
30
+ watch: boolean;
31
+ interval: number;
32
+ completion?: string;
33
+ init: boolean;
34
+ version: boolean;
35
+ help: boolean;
36
+ }
37
+
38
+ function parseArgs(args: string[]): CLIFlags {
39
+ const flags: CLIFlags = {
40
+ query: "",
41
+ json: false,
42
+ csv: false,
43
+ markdown: false,
44
+ count: false,
45
+ limit: 50,
46
+ sort: "best-match",
47
+ token: process.env.GITHUB_TOKEN,
48
+ trending: false,
49
+ since: "daily",
50
+ watch: false,
51
+ interval: 300,
52
+ init: false,
53
+ version: false,
54
+ help: false,
55
+ };
56
+
57
+ let queryParts: string[] = [];
58
+ for (let i = 0; i < args.length; i++) {
59
+ const arg = args[i];
60
+ switch (arg) {
61
+ case "--json": flags.json = true; break;
62
+ case "--csv": flags.csv = true; break;
63
+ case "--markdown": flags.markdown = true; break;
64
+ case "--count": flags.count = true; break;
65
+ case "--trending": flags.trending = true; break;
66
+ case "--watch": flags.watch = true; break;
67
+ case "--version": case "-v": flags.version = true; break;
68
+ case "--help": case "-h": flags.help = true; break;
69
+ case "init": flags.init = true; break;
70
+ case "--limit": {
71
+ const parsed = parseInt(args[++i]);
72
+ if (isNaN(parsed) || parsed < 1 || parsed > 100) {
73
+ console.error(`Invalid limit: must be 1-100. Using default 50.`);
74
+ flags.limit = 50;
75
+ } else {
76
+ flags.limit = parsed;
77
+ }
78
+ break;
79
+ }
80
+ case "--sort": flags.sort = args[++i] as SearchOptions["sort"]; break;
81
+ case "--token": flags.token = args[++i]; break;
82
+ case "--since": flags.since = args[++i]; break;
83
+ case "--pipe": flags.pipe = args[++i]; break;
84
+ case "--format": flags.format = args[++i]; break;
85
+ case "--interval": flags.interval = parseInt(args[++i]) || 300; break;
86
+ case "--completion": flags.completion = args[++i]; break;
87
+ default:
88
+ if (!arg.startsWith("-")) queryParts.push(arg);
89
+ }
90
+ }
91
+ flags.query = queryParts.join(" ");
92
+ return flags;
93
+ }
94
+
95
+ async function main() {
96
+ const flags = parseArgs(process.argv.slice(2));
97
+
98
+ // Version
99
+ if (flags.version) {
100
+ const pkg = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"));
101
+ console.log(`ghfind v${pkg.version}`);
102
+ return;
103
+ }
104
+
105
+ // Help
106
+ if (flags.help) {
107
+ console.log(`
108
+ ghfind — Interactive GitHub repository browser
109
+
110
+ Usage:
111
+ ghfind Launch interactive TUI
112
+ ghfind <query> --json Search, output JSON
113
+ ghfind <query> --csv Search, output CSV
114
+ ghfind <query> --markdown Search, output Markdown table
115
+ ghfind <query> --count Just the result count
116
+ ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
117
+ ghfind <query> --pipe <target> Pipe to clone/open
118
+ ghfind --trending --json Trending repos as JSON
119
+ ghfind --watch <query> Watch mode (poll every Ns)
120
+ ghfind init Run setup wizard
121
+ ghfind --completion <shell> Print completion script (bash|zsh|fish)
122
+ ghfind --version Print version
123
+ ghfind --help Print this help
124
+
125
+ Options:
126
+ --json, --csv, --markdown, --count Output format
127
+ --limit <n> Max results (default: 50)
128
+ --sort <strategy> Sort: best-match|stars|updated|forks
129
+ --token <token> GitHub API token
130
+ --trending Trending mode
131
+ --since <period> Trending period: daily|weekly|monthly
132
+ --pipe <target> Pipe target: clone|open
133
+ --format <fmt> Line format: urls|names|ssh-urls|...
134
+ --watch Watch mode (re-run periodically)
135
+ --interval <s> Watch interval in seconds (default: 300)
136
+ --completion <shell> Generate completions
137
+ `);
138
+ return;
139
+ }
140
+
141
+ // Shell completions
142
+ if (flags.completion) {
143
+ const shell = flags.completion;
144
+ try {
145
+ const content = readFileSync(join(PKG_DIR, "completions", `ghfind.${shell}`), "utf-8");
146
+ console.log(content);
147
+ } catch {
148
+ console.error(`Completions not available for shell: ${shell}`);
149
+ process.exit(1);
150
+ }
151
+ return;
152
+ }
153
+
154
+ // Init wizard
155
+ if (flags.init) {
156
+ await runInitWizard();
157
+ return;
158
+ }
159
+
160
+ // Determine output format
161
+ const format: ExportFormat | undefined =
162
+ flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : undefined;
163
+ const isNonInteractive = format || flags.count || flags.format || flags.pipe || flags.watch;
164
+
165
+ // Non-interactive mode
166
+ if (isNonInteractive) {
167
+ await runNonInteractive(flags, format);
168
+ return;
169
+ }
170
+
171
+ // Default: launch interactive TUI
172
+ await launchBrowser();
173
+ }
174
+
175
+ async function runNonInteractive(flags: CLIFlags, format?: ExportFormat) {
176
+ const query = flags.query;
177
+ const provider = createGitHubSearch(undefined, flags.token ? [flags.token] : []);
178
+
179
+ // --watch mode
180
+ if (flags.watch) {
181
+ let tick = 0;
182
+ await runWatch(
183
+ { query, sort: flags.sort, limit: flags.limit, token: flags.token, intervalMs: flags.interval * 1000, trending: flags.trending },
184
+ (repos, delta) => {
185
+ tick++;
186
+ if (format) {
187
+ console.log(`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`);
188
+ console.log(formatRepos(repos, format));
189
+ } else {
190
+ console.log(`[Watch #${tick}] ${repos.length} results — ${delta >= 0 ? "+" : ""}${delta} since last check`);
191
+ }
192
+ },
193
+ (err) => console.error(`[Watch error] ${err.message}`),
194
+ );
195
+ return;
196
+ }
197
+
198
+ // --pipe mode
199
+ if (flags.pipe) {
200
+ const parsed = applyFlagFilters(parseQuery(query), {});
201
+ const response = await provider.search(parsed, { limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token });
202
+ const repos = rankRepos(response.repos, flags.sort);
203
+ await pipeExec(repos, flags.pipe);
204
+ return;
205
+ }
206
+
207
+ // --format mode
208
+ if (flags.format) {
209
+ const parsed = applyFlagFilters(parseQuery(query), {});
210
+ const response = await provider.search(parsed, { limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token });
211
+ const repos = rankRepos(response.repos, flags.sort);
212
+ console.log(formatLines(repos, flags.format as FormatLine));
213
+ return;
214
+ }
215
+
216
+ // Standard search with format
217
+ const parsed = applyFlagFilters(parseQuery(query), {});
218
+ const response = await provider.search(parsed, {
219
+ limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
220
+ });
221
+ const repos = rankRepos(response.repos, flags.sort);
222
+
223
+ // --count
224
+ if (flags.count) {
225
+ console.log(response.totalCount);
226
+ return;
227
+ }
228
+
229
+ // --json, --csv, --markdown
230
+ if (format) {
231
+ console.log(formatRepos(repos, format));
232
+ return;
233
+ }
234
+ }
235
+
236
+ main().catch((err) => {
237
+ console.error(err instanceof Error ? err.message : String(err));
238
+ process.exit(1);
239
+ });