github-search-cli 0.7.0 → 0.8.0

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/src/cli.ts CHANGED
@@ -1,252 +1,239 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * CLI entry point — routes between TUI browser and non-interactive modes.
4
- *
5
- * Usage:
6
- * search-cli Launch interactive TUI
7
- * search-cli "query" --json Search + JSON output
8
- * search-cli "query" --csv Search + CSV output
9
- * search-cli "query" --markdown Search + Markdown table
10
- * search-cli "query" --count Just the result count
11
- * search-cli --trending --json Trending repos as JSON
12
- * search-cli init Setup wizard
13
- * search-cli --watch "query" Periodic watch mode
14
- * search-cli --completion bash Print shell completion
15
- * search-cli --version Print version
16
- * search-cli --help Print help
17
- */
18
- import { parseQuery, applyFlagFilters } from "./query.ts";
19
- import { GitHubSearchProvider } from "./provider.ts";
20
- import { rankRepos } from "./ranking.ts";
21
- import { formatRepos, type ExportFormat } from "./export.ts";
22
- import { formatLines, pipeExec, type FormatLine } from "./pipe.ts";
23
- import { runWatch } from "./watch.ts";
24
- import { runInitWizard } from "./init.ts";
25
- import { readFileSync } from "fs";
26
- import { join } from "path";
27
- import { launchBrowser } from "./tui.ts";
28
- import type { SearchOptions } from "./types.ts";
29
-
30
- interface CLIFlags {
31
- query: string;
32
- json: boolean;
33
- csv: boolean;
34
- markdown: boolean;
35
- count: boolean;
36
- limit: number;
37
- sort: SearchOptions["sort"];
38
- token?: string;
39
- trending: boolean;
40
- since: string;
41
- pipe?: string;
42
- format?: string;
43
- watch: boolean;
44
- interval: number;
45
- completion?: string;
46
- init: boolean;
47
- version: boolean;
48
- help: boolean;
49
- }
50
-
51
- function parseArgs(args: string[]): CLIFlags {
52
- const flags: CLIFlags = {
53
- query: "",
54
- json: false,
55
- csv: false,
56
- markdown: false,
57
- count: false,
58
- limit: 50,
59
- sort: "best-match",
60
- token: process.env.GITHUB_TOKEN,
61
- trending: false,
62
- since: "daily",
63
- watch: false,
64
- interval: 300,
65
- init: false,
66
- version: false,
67
- help: false,
68
- };
69
-
70
- let queryParts: string[] = [];
71
- for (let i = 0; i < args.length; i++) {
72
- const arg = args[i];
73
- switch (arg) {
74
- case "--json": flags.json = true; break;
75
- case "--csv": flags.csv = true; break;
76
- case "--markdown": flags.markdown = true; break;
77
- case "--count": flags.count = true; break;
78
- case "--trending": flags.trending = true; break;
79
- case "--watch": flags.watch = true; break;
80
- case "--version": case "-v": flags.version = true; break;
81
- case "--help": case "-h": flags.help = true; break;
82
- case "init": flags.init = true; break;
83
- case "--limit": {
84
- const parsed = parseInt(args[++i]);
85
- if (isNaN(parsed) || parsed < 1 || parsed > 100) {
86
- console.error(`Invalid limit: must be 1-100. Using default 50.`);
87
- flags.limit = 50;
88
- } else {
89
- flags.limit = parsed;
90
- }
91
- break;
92
- }
93
- case "--sort": flags.sort = args[++i] as SearchOptions["sort"]; break;
94
- case "--token": flags.token = args[++i]; break;
95
- case "--since": flags.since = args[++i]; break;
96
- case "--pipe": flags.pipe = args[++i]; break;
97
- case "--format": flags.format = args[++i]; break;
98
- case "--interval": flags.interval = parseInt(args[++i]) || 300; break;
99
- case "--completion": flags.completion = args[++i]; break;
100
- default:
101
- if (!arg.startsWith("-")) queryParts.push(arg);
102
- }
103
- }
104
- flags.query = queryParts.join(" ");
105
- return flags;
106
- }
107
-
108
- async function main() {
109
- const flags = parseArgs(process.argv.slice(2));
110
-
111
- // Version
112
- if (flags.version) {
113
- const pkg = JSON.parse(readFileSync(join(import.meta.dir!, "..", "package.json"), "utf-8"));
114
- console.log(`search-cli v${pkg.version}`);
115
- return;
116
- }
117
-
118
- // Help
119
- if (flags.help) {
120
- console.log(`
121
- search-cli — Interactive GitHub repository browser
122
-
123
- Usage:
124
- search-cli Launch interactive TUI
125
- search-cli <query> --json Search, output JSON
126
- search-cli <query> --csv Search, output CSV
127
- search-cli <query> --markdown Search, output Markdown table
128
- search-cli <query> --count Just the result count
129
- search-cli <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
130
- search-cli <query> --pipe <target> Pipe to clone/open
131
- search-cli --trending --json Trending repos as JSON
132
- search-cli --watch <query> Watch mode (poll every Ns)
133
- search-cli init Run setup wizard
134
- search-cli --completion <shell> Print completion script (bash|zsh|fish)
135
- search-cli --version Print version
136
- search-cli --help Print this help
137
-
138
- Options:
139
- --json, --csv, --markdown, --count Output format
140
- --limit <n> Max results (default: 50)
141
- --sort <strategy> Sort: best-match|stars|updated|forks
142
- --token <token> GitHub API token
143
- --trending Trending mode
144
- --since <period> Trending period: daily|weekly|monthly
145
- --pipe <target> Pipe target: clone|open
146
- --format <fmt> Line format: urls|names|ssh-urls|...
147
- --watch Watch mode (re-run periodically)
148
- --interval <s> Watch interval in seconds (default: 300)
149
- --completion <shell> Generate completions
150
- `);
151
- return;
152
- }
153
-
154
- // Shell completions
155
- if (flags.completion) {
156
- const shell = flags.completion;
157
- try {
158
- const content = readFileSync(join(import.meta.dir!, "..", "completions", `search-cli.${shell}`), "utf-8");
159
- console.log(content);
160
- } catch {
161
- console.error(`Completions not available for shell: ${shell}`);
162
- process.exit(1);
163
- }
164
- return;
165
- }
166
-
167
- // Init wizard
168
- if (flags.init) {
169
- await runInitWizard();
170
- return;
171
- }
172
-
173
- // Determine output format
174
- const format: ExportFormat | undefined =
175
- flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : undefined;
176
- const isNonInteractive = format || flags.count || flags.format || flags.pipe || flags.watch;
177
-
178
- // Non-interactive mode
179
- if (isNonInteractive) {
180
- await runNonInteractive(flags, format);
181
- return;
182
- }
183
-
184
- // Default: launch interactive TUI
185
- await launchBrowser();
186
- }
187
-
188
- async function runNonInteractive(flags: CLIFlags, format?: ExportFormat) {
189
- const query = flags.query;
190
- const provider = new GitHubSearchProvider(undefined, flags.token ? [flags.token] : []);
191
-
192
- // --watch mode
193
- if (flags.watch) {
194
- let tick = 0;
195
- await runWatch(
196
- { query, sort: flags.sort, limit: flags.limit, token: flags.token, intervalMs: flags.interval * 1000, trending: flags.trending },
197
- (repos, delta) => {
198
- tick++;
199
- if (format) {
200
- console.log(`[Watch #${tick}] ${repos.length} results (${delta >= 0 ? "+" : ""}${delta} since last check)`);
201
- console.log(formatRepos(repos, format));
202
- } else {
203
- console.log(`[Watch #${tick}] ${repos.length} results — ${delta >= 0 ? "+" : ""}${delta} since last check`);
204
- }
205
- },
206
- (err) => console.error(`[Watch error] ${err.message}`),
207
- );
208
- return;
209
- }
210
-
211
- // --pipe mode
212
- if (flags.pipe) {
213
- const parsed = applyFlagFilters(parseQuery(query), {});
214
- const response = await provider.search(parsed, { limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token });
215
- const repos = rankRepos(response.repos, flags.sort);
216
- await pipeExec(repos, flags.pipe);
217
- return;
218
- }
219
-
220
- // --format mode
221
- if (flags.format) {
222
- const parsed = applyFlagFilters(parseQuery(query), {});
223
- const response = await provider.search(parsed, { limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token });
224
- const repos = rankRepos(response.repos, flags.sort);
225
- console.log(formatLines(repos, flags.format as FormatLine));
226
- return;
227
- }
228
-
229
- // Standard search with format
230
- const parsed = applyFlagFilters(parseQuery(query), {});
231
- const response = await provider.search(parsed, {
232
- limit: flags.limit, sort: flags.sort, json: false, verbose: false, token: flags.token,
233
- });
234
- const repos = rankRepos(response.repos, flags.sort);
235
-
236
- // --count
237
- if (flags.count) {
238
- console.log(response.totalCount);
239
- return;
240
- }
241
-
242
- // --json, --csv, --markdown
243
- if (format) {
244
- console.log(formatRepos(repos, format));
245
- return;
246
- }
247
- }
248
-
249
- main().catch((err) => {
250
- console.error(err instanceof Error ? err.message : String(err));
251
- process.exit(1);
252
- });
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
+ });
package/src/compare.ts CHANGED
@@ -1,46 +1,46 @@
1
- /**
2
- * Repo comparison — side-by-side table for 2+ repos.
3
- */
4
- import type { Repo } from "./types.ts";
5
-
6
- interface CompareRow {
7
- label: string;
8
- values: string[];
9
- }
10
-
11
- /** Build a comparison table as an array of text rows. */
12
- export function buildComparisonTable(repos: Repo[]): string {
13
- if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
14
-
15
- const rows: CompareRow[] = [];
16
- const names = repos.map((r) => r.fullName);
17
-
18
- rows.push({ label: "Stars", values: repos.map((r) => `★ ${r.stars.toLocaleString()}`) });
19
- rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
20
- rows.push({ label: "Language", values: repos.map((r) => r.language ?? "—") });
21
- rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "—") });
22
- rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "—") });
23
- rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 3).join(", ") ?? "—") || "—") });
24
-
25
- // Calculate the max width for each column
26
- const colW = Math.max(15, ...names.map((n) => n.length));
27
- const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
28
-
29
- // Header row
30
- const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
31
- const sep = ["".padEnd(14, "─"), ...names.map(() => "".padEnd(colW, "─"))];
32
-
33
- const lines: string[] = [
34
- "┌" + sep.map((s) => "─" + s + "─").join("┬") + "┐",
35
- "│" + header.map((h) => ` ${h} `).join("│") + "│",
36
- "├" + sep.map((s) => "─" + s + "─").join("┼") + "┤",
37
- ];
38
-
39
- for (const row of rows) {
40
- const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
41
- lines.push("│" + cells.map((c) => ` ${c} `).join("│") + "│");
42
- }
43
-
44
- lines.push("└" + sep.map((s) => "─" + s + "─").join("┴") + "┘");
45
- return lines.join("\n");
46
- }
1
+ /**
2
+ * Repo comparison — side-by-side table for 2+ repos.
3
+ */
4
+ import type { Repo } from "./types";
5
+
6
+ interface CompareRow {
7
+ label: string;
8
+ values: string[];
9
+ }
10
+
11
+ /** Build a comparison table as an array of text rows. */
12
+ export function buildComparisonTable(repos: Repo[]): string {
13
+ if (repos.length < 2) return "Select at least 2 repos to compare (press c to add).";
14
+
15
+ const rows: CompareRow[] = [];
16
+ const names = repos.map((r) => r.fullName);
17
+
18
+ rows.push({ label: "Stars", values: repos.map((r) => `★ ${r.stars.toLocaleString()}`) });
19
+ rows.push({ label: "Forks", values: repos.map((r) => r.forks.toLocaleString()) });
20
+ rows.push({ label: "Language", values: repos.map((r) => r.language ?? "—") });
21
+ rows.push({ label: "Created", values: repos.map((r) => r.createdAt?.slice(0, 7) ?? "—") });
22
+ rows.push({ label: "Updated", values: repos.map((r) => r.updatedAt?.slice(0, 10) ?? "—") });
23
+ rows.push({ label: "Topics", values: repos.map((r) => (r.topics?.slice(0, 3).join(", ") ?? "—") || "—") });
24
+
25
+ // Calculate the max width for each column
26
+ const colW = Math.max(15, ...names.map((n) => n.length));
27
+ const valW = Math.max(8, ...rows.flatMap((r) => r.values.map((v) => v.length)));
28
+
29
+ // Header row
30
+ const header = ["".padEnd(14), ...names.map((n) => n.padEnd(colW))];
31
+ const sep = ["".padEnd(14, "─"), ...names.map(() => "".padEnd(colW, "─"))];
32
+
33
+ const lines: string[] = [
34
+ "┌" + sep.map((s) => "─" + s + "─").join("┬") + "┐",
35
+ "│" + header.map((h) => ` ${h} `).join("│") + "│",
36
+ "├" + sep.map((s) => "─" + s + "─").join("┼") + "┤",
37
+ ];
38
+
39
+ for (const row of rows) {
40
+ const cells = [row.label.padEnd(14), ...row.values.map((v) => v.padEnd(colW))];
41
+ lines.push("│" + cells.map((c) => ` ${c} `).join("│") + "│");
42
+ }
43
+
44
+ lines.push("└" + sep.map((s) => "─" + s + "─").join("┴") + "┘");
45
+ return lines.join("\n");
46
+ }