github-search-cli 0.6.1 → 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/explore.ts CHANGED
@@ -1,45 +1,29 @@
1
- /**
2
- * Topic explorer — browse popular GitHub topics.
3
- *
4
- * ponytail: fetch flat topic list, no hierarchy. Selecting a topic
5
- * triggers a search. Hierarchy adds ~60 lines for marginal benefit.
6
- */
7
- import { ParseError } from "./errors.ts";
8
-
9
- export interface TopicItem {
10
- name: string;
11
- description: string;
12
- repoCount: number;
13
- }
14
-
15
- /** Fetch popular topics from github.com/topics. */
16
- export async function fetchTopics(): Promise<TopicItem[]> {
17
- const res = await fetch("https://github.com/topics", {
18
- headers: { "User-Agent": "search-cli/1.0" },
19
- });
20
- if (!res.ok) throw new ParseError("GitHub topics", `HTTP ${res.status}`);
21
- const html = await res.text();
22
-
23
- const topics: TopicItem[] = [];
24
- // Parse topic cards from the HTML
25
- const cards = html.split('<article class="border');
26
- for (let i = 1; i < cards.length; i++) {
27
- const block = cards[i];
28
-
29
- const nameMatch = block.match(/href="\/topics\/([^"]+)"/);
30
- if (!nameMatch) continue;
31
- const name = decodeURIComponent(nameMatch[1]);
32
-
33
- const descMatch = block.match(/<p[^>]*>([\s\S]*?)<\/p>/);
34
- const description = descMatch
35
- ? descMatch[1].replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim()
36
- : "";
37
-
38
- const countMatch = block.match(/([\d,]+)\s+repositories/);
39
- const repoCount = countMatch ? parseInt(countMatch[1].replace(/,/g, "")) : 0;
40
-
41
- topics.push({ name, description, repoCount });
42
- }
43
-
44
- return topics.slice(0, 30);
45
- }
1
+ /**
2
+ * Topic explorer — browse popular GitHub topics.
3
+ *
4
+ * Selecting a topic triggers a search.
5
+ */
6
+ export interface TopicItem {
7
+ name: string;
8
+ description: string;
9
+ repoCount: number;
10
+ }
11
+
12
+ /** Fetch popular topics from GitHub search API.
13
+ * Requires the `mercy-preview` media type for the /search/topics endpoint.
14
+ */
15
+ export async function fetchTopics(): Promise<TopicItem[]> {
16
+ const res = await fetch("https://api.github.com/search/topics?q=popular&per_page=30", {
17
+ headers: {
18
+ Accept: "application/vnd.github.mercy-preview+json",
19
+ "User-Agent": "search-cli",
20
+ },
21
+ });
22
+ if (!res.ok) throw new Error(`Topics API: HTTP ${res.status}`);
23
+ const data = await res.json() as { items?: Array<{ name: string; description?: string; score?: number }> };
24
+ return (data.items ?? []).map((item) => ({
25
+ name: item.name,
26
+ description: item.description || "",
27
+ repoCount: item.score || 0,
28
+ }));
29
+ }
package/src/export.ts CHANGED
@@ -1,56 +1,63 @@
1
- /**
2
- * Export — format repos as JSON, CSV, Markdown, or plain text.
3
- */
4
- import { writeFileSync } from "fs";
5
- import type { Repo } from "./types.ts";
6
-
7
- export type ExportFormat = "json" | "csv" | "markdown" | "text";
8
-
9
- /** Format repos as JSON string. */
10
- export function formatJson(repos: Repo[]): string {
11
- return JSON.stringify(repos, null, 2);
12
- }
13
-
14
- /** Format repos as CSV string. */
15
- export function formatCsv(repos: Repo[]): string {
16
- const header = "rank,full_name,stars,forks,language,url";
17
- const rows = repos.map((r, i) =>
18
- `${i + 1},"${r.fullName}",${r.stars},${r.forks},"${r.language ?? ""}","${r.url}"`,
19
- );
20
- return [header, ...rows].join("\n");
21
- }
22
-
23
- /** Format repos as Markdown table. */
24
- export function formatMarkdown(repos: Repo[]): string {
25
- const header = "| # | Repo | Stars | Forks | Language |";
26
- const sep = "|---|------|-------|-------|----------|";
27
- const rows = repos.map(
28
- (r, i) =>
29
- `| ${i + 1} | [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "—"} |`,
30
- );
31
- return [header, sep, ...rows].join("\n");
32
- }
33
-
34
- /** Format repos as plain text, one per line. */
35
- export function formatText(repos: Repo[]): string {
36
- return repos.map((r, i) => `${i + 1}. ${r.fullName} ★ ${r.stars.toLocaleString()} ${r.url}`).join("\n");
37
- }
38
-
39
- /** Format repos in the given format. */
40
- export function formatRepos(repos: Repo[], format: ExportFormat): string {
41
- switch (format) {
42
- case "json": return formatJson(repos);
43
- case "csv": return formatCsv(repos);
44
- case "markdown": return formatMarkdown(repos);
45
- case "text": return formatText(repos);
46
- }
47
- }
48
-
49
- /** Export repos to a file. Returns the file path. */
50
- export function exportToFile(repos: Repo[], format: ExportFormat): string {
51
- const ext = format === "markdown" ? "md" : format;
52
- const ts = Date.now();
53
- const path = `./search-cli-export-${ts}.${ext}`;
54
- writeFileSync(path, formatRepos(repos, format), "utf-8");
55
- return path;
56
- }
1
+ /**
2
+ * Export — format repos as JSON, CSV, Markdown, or plain text.
3
+ */
4
+ import { writeFileSync } from "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((r, i) =>
25
+ `${i + 1},${csvEscape(r.fullName)},${r.stars},${r.forks},${csvEscape(r.language ?? "")},${csvEscape(r.url)}`,
26
+ );
27
+ return [header, ...rows].join("\n");
28
+ }
29
+
30
+ /** Format repos as Markdown table. */
31
+ export function formatMarkdown(repos: Repo[]): string {
32
+ const header = "| # | Repo | Stars | Forks | Language |";
33
+ const sep = "|---|------|-------|-------|----------|";
34
+ const rows = repos.map(
35
+ (r, i) =>
36
+ `| ${i + 1} | [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "—"} |`,
37
+ );
38
+ return [header, sep, ...rows].join("\n");
39
+ }
40
+
41
+ /** Format repos as plain text, one per line. */
42
+ export function formatText(repos: Repo[]): string {
43
+ return repos.map((r, i) => `${i + 1}. ${r.fullName} ★ ${r.stars.toLocaleString()} ${r.url}`).join("\n");
44
+ }
45
+
46
+ /** Format repos in the given format. */
47
+ export function formatRepos(repos: Repo[], format: ExportFormat): string {
48
+ switch (format) {
49
+ case "json": return formatJson(repos);
50
+ case "csv": return formatCsv(repos);
51
+ case "markdown": return formatMarkdown(repos);
52
+ case "text": return formatText(repos);
53
+ }
54
+ }
55
+
56
+ /** Export repos to a file. Returns the file path. */
57
+ export function exportToFile(repos: Repo[], format: ExportFormat): string {
58
+ const ext = format === "markdown" ? "md" : format;
59
+ const ts = Date.now();
60
+ const path = `./ghfind-export-${ts}.${ext}`;
61
+ writeFileSync(path, formatRepos(repos, format), "utf-8");
62
+ return path;
63
+ }
package/src/help.ts CHANGED
@@ -1,46 +1,112 @@
1
- /**
2
- * Help screen: full keyboard shortcut reference for the TUI.
3
- */
4
- export function buildHelpText(): string {
5
- return [
6
- "┌─ search-cli Help ──────────────────────────── [q] close ─┐",
7
- "│ │",
8
- "│ NAVIGATION │",
9
- "│ ↑ ↓ / j k Move selection │",
10
- "│ Enter / o Open repo in browser │",
11
- "│ / Focus search input │",
12
- "│ t Toggle search / trending │",
13
- "│ ? / Ctrl+H Toggle this help screen │",
14
- "│ q Quit │",
15
- "│ │",
16
- "│ SEARCH MODE │",
17
- "│ s Cycle sort: best-match → stars → ... │",
18
- "│ l Cycle limit: 10 → 25 → 50 → 100 │",
19
- "│ r Refresh / re-run last search │",
20
- "│ g Toggle activity graph fullscreen │",
21
- "│ Tab Auto-complete qualifier │",
22
- "│ │",
23
- "│ TRENDING MODE │",
24
- "│ 1-5 Switch tab: Today → Week → Month → ...│",
25
- "│ ← → / h l Navigate tabs │",
26
- "│ r Refresh │",
27
- "│ │",
28
- "│ SEARCH SYNTAX │",
29
- "│ keywords Free-text search │",
30
- "│ key:value Qualifier filter │",
31
- "│ -key:value Negated qualifier │",
32
- "│ \"phrase\" Exact phrase match │",
33
- "│ │",
34
- "│ QUALIFIERS │",
35
- "│ language: Stars: Fork: Archived: Topic: │",
36
- "│ user: Org: Repo: Updated: Pushed: │",
37
- "│ visibility: License: Created: Size: In: │",
38
- "│ │",
39
- "│ RATE LIMITS │",
40
- "│ No token 10 req/min │",
41
- "│ GITHUB_TOKEN 30 req/min (or your plan limit) │",
42
- "│ │",
43
- "│ Set GITHUB_TOKEN in your environment or add to config. │",
44
- "└───────────────────────────────────────────────────────────┘",
45
- ].join("\n");
46
- }
1
+ /** Help screen — structured sections rendered by OpenTUI components. */
2
+
3
+ export interface HelpRow {
4
+ keys: string;
5
+ action: string;
6
+ }
7
+
8
+ export interface HelpSection {
9
+ title: string;
10
+ /** Theme color key or hex — used for Box title color */
11
+ titleColor: string;
12
+ rows: HelpRow[];
13
+ note?: string;
14
+ }
15
+
16
+ export function buildHelpSections(): HelpSection[] {
17
+ return [
18
+ {
19
+ title: " Navigation ",
20
+ titleColor: "blue",
21
+ rows: [
22
+ { keys: "↑ ↓ / j k", action: "Move selection" },
23
+ { keys: "Enter", action: "Open repo in browser" },
24
+ { keys: "/", action: "Focus search input" },
25
+ { keys: "Space", action: "Open command menu" },
26
+ { keys: "? / Ctrl+H", action: "Toggle this help screen" },
27
+ { keys: "q", action: "Quit" },
28
+ ],
29
+ },
30
+ {
31
+ title: " Main View ",
32
+ titleColor: "green",
33
+ rows: [
34
+ { keys: "Tab", action: "Auto-complete qualifier" },
35
+ { keys: "PageDown", action: "Next page of results" },
36
+ { keys: "PageUp", action: "Scroll to top of results" },
37
+ ],
38
+ },
39
+ {
40
+ title: " Trending Mode ",
41
+ titleColor: "yellow",
42
+ rows: [
43
+ { keys: "1-5", action: "Switch tab: Today → Week → Month → …" },
44
+ { keys: "← → / h l", action: "Navigate tabs" },
45
+ ],
46
+ },
47
+ {
48
+ title: " Command Menu (Space)",
49
+ titleColor: "purple",
50
+ rows: [
51
+ { keys: "Sort", action: "Cycle sort: best-match → stars → …" },
52
+ { keys: "Limit", action: "Cycle limit: 10 → 25 → 50 → 100" },
53
+ { keys: "Refresh", action: "Re-run current search" },
54
+ { keys: "Graph", action: "Toggle commit activity chart" },
55
+ { keys: "Readme", action: "Full README viewer" },
56
+ { keys: "Deep-dive", action: "Languages + contributors + excerpt" },
57
+ { keys: "Bookmark", action: "Save / unsave selected repo" },
58
+ { keys: "Compare", action: "Add to / remove from comparison" },
59
+ { keys: "Trending", action: "Switch to trending browser" },
60
+ ],
61
+ note: "Also: Open, History, Export, Share, Topics, Saved, Notifs, Help",
62
+ },
63
+ {
64
+ title: " Overlay Keys ",
65
+ titleColor: "teal",
66
+ rows: [
67
+ { keys: "Esc / q", action: "Close any overlay" },
68
+ { keys: "Enter", action: "Confirm selection / run action" },
69
+ { keys: "d", action: "Delete entry (history, bookmarks, saved)" },
70
+ { keys: "↑ ↓ / j k", action: "Navigate overlay list" },
71
+ ],
72
+ },
73
+ {
74
+ title: " Search Syntax ",
75
+ titleColor: "teal",
76
+ rows: [
77
+ { keys: "keywords", action: "Free-text search" },
78
+ { keys: "key:value", action: "Qualifier filter" },
79
+ { keys: "-key:value", action: "Negated qualifier" },
80
+ { keys: '"phrase"', action: "Exact phrase match" },
81
+ ],
82
+ },
83
+ {
84
+ title: " Qualifiers ",
85
+ titleColor: "muted",
86
+ rows: [
87
+ { keys: "language:", action: "Filter by language" },
88
+ { keys: "stars:>N", action: "Minimum stars" },
89
+ { keys: "fork:true", action: "Include forks" },
90
+ { keys: "user:", action: "User's repos" },
91
+ { keys: "org:", action: "Organization repos" },
92
+ { keys: "repo:", action: "Specific repo" },
93
+ { keys: "topic:", action: "Filter by topic" },
94
+ { keys: "license:", action: "License type" },
95
+ { keys: "pushed:>YYYY-MM-DD", action: "Last push date" },
96
+ { keys: "archived:false", action: "Exclude archived" },
97
+ ],
98
+ },
99
+ {
100
+ title: " Rate Limits ",
101
+ titleColor: "orange",
102
+ rows: [
103
+ { keys: "No token", action: "10 req / min" },
104
+ { keys: "GITHUB_TOKEN", action: "30 req / min (or your plan limit)" },
105
+ ],
106
+ note: "Set GITHUB_TOKEN in your environment or add to config.",
107
+ },
108
+ ];
109
+ }
110
+
111
+ /** Column at which the action description starts (right-aligns keys). */
112
+ export const HELP_KEYS_COLUMN = 24;
package/src/history.ts CHANGED
@@ -1,91 +1,91 @@
1
- /**
2
- * Persistent search history — append-only JSONL with rotation.
3
- */
4
- import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, unlinkSync } from "fs";
5
- import { join } from "path";
6
- import type { HistoryEntry } from "./types.ts";
7
- import { stateDir } from "./config.ts";
8
-
9
- const HISTORY_FILE = join(stateDir(), "history.jsonl");
10
- const MAX_ENTRIES = 500;
11
-
12
- function ensureDir() {
13
- mkdirSync(stateDir(), { recursive: true });
14
- }
15
-
16
- /** Append one entry to the history log. */
17
- export function appendHistory(entry: HistoryEntry): void {
18
- ensureDir();
19
- const line = JSON.stringify(entry) + "\n";
20
- try {
21
- appendFileSync(HISTORY_FILE, line, "utf-8");
22
- } catch {
23
- // non-critical
24
- }
25
- }
26
-
27
- /** Read all history entries, newest first, deduplicating consecutive identical queries. */
28
- export function readHistory(): HistoryEntry[] {
29
- try {
30
- const raw = readFileSync(HISTORY_FILE, "utf-8").trim();
31
- if (!raw) return [];
32
- const lines = raw.split("\n").filter(Boolean);
33
- const entries: HistoryEntry[] = lines.map((l) => JSON.parse(l) as HistoryEntry);
34
-
35
- // Sorted oldest-first in file; reverse to newest-first
36
- entries.reverse();
37
-
38
- // Deduplicate consecutive identical queries (keep newest)
39
- const deduped: HistoryEntry[] = [];
40
- let lastQuery: string | undefined;
41
- for (const e of entries) {
42
- if (e.query !== lastQuery) {
43
- deduped.push(e);
44
- lastQuery = e.query;
45
- }
46
- }
47
- return deduped;
48
- } catch {
49
- return [];
50
- }
51
- }
52
-
53
- /** Delete a single history entry by index (0 = newest). */
54
- export function deleteHistoryEntry(index: number): void {
55
- const entries = readHistory();
56
- if (index < 0 || index >= entries.length) return;
57
- const removed = entries[index];
58
- // Rewrite file without the removed entry
59
- const remaining = entries.filter((e) => e.timestamp !== removed.timestamp || e.query !== removed.query);
60
- ensureDir();
61
- // Reverse back to oldest-first for file order
62
- writeFileSync(HISTORY_FILE, remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
63
- }
64
-
65
- /** Clear all history. */
66
- export function clearHistory(): void {
67
- try {
68
- writeFileSync(HISTORY_FILE, "");
69
- } catch {
70
- // non-critical
71
- }
72
- }
73
-
74
- /** Rotate history to max entries (trim oldest). */
75
- export function rotateHistory(): void {
76
- const entries = readHistory();
77
- if (entries.length <= MAX_ENTRIES) return;
78
- const trimmed = entries.slice(0, MAX_ENTRIES);
79
- ensureDir();
80
- writeFileSync(HISTORY_FILE, trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
81
- }
82
-
83
- /** Count entries in history file. */
84
- export function historyCount(): number {
85
- try {
86
- const raw = readFileSync(HISTORY_FILE, "utf-8").trim();
87
- return raw ? raw.split("\n").length : 0;
88
- } catch {
89
- return 0;
90
- }
91
- }
1
+ /**
2
+ * Persistent search history — append-only JSONL with rotation.
3
+ */
4
+ import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, unlinkSync } from "fs";
5
+ import { join } from "path";
6
+ import type { HistoryEntry } from "./types";
7
+ import { stateDir } from "./config";
8
+
9
+ const HISTORY_FILE = join(stateDir(), "history.jsonl");
10
+ const MAX_ENTRIES = 500;
11
+
12
+ function ensureDir() {
13
+ mkdirSync(stateDir(), { recursive: true });
14
+ }
15
+
16
+ /** Append one entry to the history log. */
17
+ export function appendHistory(entry: HistoryEntry): void {
18
+ ensureDir();
19
+ const line = JSON.stringify(entry) + "\n";
20
+ try {
21
+ appendFileSync(HISTORY_FILE, line, "utf-8");
22
+ } catch {
23
+ // non-critical
24
+ }
25
+ }
26
+
27
+ /** Read all history entries, newest first, deduplicating consecutive identical queries. */
28
+ export function readHistory(): HistoryEntry[] {
29
+ try {
30
+ const raw = readFileSync(HISTORY_FILE, "utf-8").trim();
31
+ if (!raw) return [];
32
+ const lines = raw.split("\n").filter(Boolean);
33
+ const entries: HistoryEntry[] = lines.map((l) => JSON.parse(l) as HistoryEntry);
34
+
35
+ // Sorted oldest-first in file; reverse to newest-first
36
+ entries.reverse();
37
+
38
+ // Deduplicate consecutive identical queries (keep newest)
39
+ const deduped: HistoryEntry[] = [];
40
+ let lastQuery: string | undefined;
41
+ for (const e of entries) {
42
+ if (e.query !== lastQuery) {
43
+ deduped.push(e);
44
+ lastQuery = e.query;
45
+ }
46
+ }
47
+ return deduped;
48
+ } catch {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ /** Delete a single history entry by index (0 = newest). */
54
+ export function deleteHistoryEntry(index: number): void {
55
+ const entries = readHistory();
56
+ if (index < 0 || index >= entries.length) return;
57
+ const removed = entries[index];
58
+ // Rewrite file without the removed entry
59
+ const remaining = entries.filter((e) => e.timestamp !== removed.timestamp || e.query !== removed.query);
60
+ ensureDir();
61
+ // Reverse back to oldest-first for file order
62
+ writeFileSync(HISTORY_FILE, remaining.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
63
+ }
64
+
65
+ /** Clear all history. */
66
+ export function clearHistory(): void {
67
+ try {
68
+ writeFileSync(HISTORY_FILE, "");
69
+ } catch {
70
+ // non-critical
71
+ }
72
+ }
73
+
74
+ /** Rotate history to max entries (trim oldest). */
75
+ export function rotateHistory(): void {
76
+ const entries = readHistory();
77
+ if (entries.length <= MAX_ENTRIES) return;
78
+ const trimmed = entries.slice(0, MAX_ENTRIES);
79
+ ensureDir();
80
+ writeFileSync(HISTORY_FILE, trimmed.reverse().map((e) => JSON.stringify(e)).join("\n") + "\n");
81
+ }
82
+
83
+ /** Count entries in history file. */
84
+ export function historyCount(): number {
85
+ try {
86
+ const raw = readFileSync(HISTORY_FILE, "utf-8").trim();
87
+ return raw ? raw.split("\n").length : 0;
88
+ } catch {
89
+ return 0;
90
+ }
91
+ }
package/src/init.ts CHANGED
@@ -1,47 +1,55 @@
1
- /**
2
- * `search-cli init` wizard — interactive one-time config setup.
3
- *
4
- * ponytail: simple stdin prompts, no curses/readline UI.
5
- */
6
- import { createInterface } from "readline";
7
- import { saveConfig } from "./config.ts";
8
- import type { Config, SortStrategy } from "./types.ts";
9
-
10
- function ask(query: string): Promise<string> {
11
- const rl = createInterface({ input: process.stdin, output: process.stdout });
12
- return new Promise((resolve) => {
13
- rl.question(query, (answer) => {
14
- rl.close();
15
- resolve(answer.trim());
16
- });
17
- });
18
- }
19
-
20
- export async function runInitWizard(): Promise<void> {
21
- console.log("");
22
- console.log(" search-cli setup wizard");
23
- console.log(" ──────────────────────");
24
- console.log("");
25
-
26
- const token = await ask(" GitHub token (optional, press Enter to skip): ");
27
- const sortStr = await ask(" Default sort [best-match/stars/updated/forks] (best-match): ");
28
- const limitStr = await ask(" Default result limit [10-100] (50): ");
29
-
30
- const config: Config = {
31
- defaultSort: (["best-match", "stars", "updated", "forks"].includes(sortStr)
32
- ? sortStr
33
- : "best-match") as SortStrategy,
34
- defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
35
- theme: "tokyo-night",
36
- cacheTtlSeconds: 300,
37
- defaultTab: "search",
38
- };
39
-
40
- if (token) config.githubToken = token;
41
-
42
- saveConfig(config);
43
- const { configPath } = await import("./config.ts");
44
- console.log(` ✓ Config saved to ${configPath()}`);
45
- console.log(" Run 'search-cli' to start browsing!");
46
- console.log("");
47
- }
1
+ /**
2
+ * `ghfind init` wizard — interactive one-time config setup.
3
+ *
4
+ * ponytail: simple stdin prompts, no curses/readline UI.
5
+ */
6
+ import { createInterface } from "readline";
7
+ import { saveConfig } from "./config";
8
+ import { listThemes } from "./themes";
9
+ import type { Config, SortStrategy } from "./types";
10
+
11
+ function ask(query: string): Promise<string> {
12
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
13
+ return new Promise((resolve) => {
14
+ rl.question(query, (answer) => {
15
+ rl.close();
16
+ resolve(answer.trim());
17
+ });
18
+ });
19
+ }
20
+
21
+ export async function runInitWizard(): Promise<void> {
22
+ console.log("");
23
+ console.log(" ghfind setup wizard");
24
+ console.log(" ──────────────────────");
25
+ console.log("");
26
+
27
+ const token = await ask(" GitHub token (optional, press Enter to skip): ");
28
+ if (token && !token.startsWith("ghp_") && !token.startsWith("github_pat_") && !token.startsWith("gho_")) {
29
+ console.log(" ⚠ Token doesn't look like a GitHub token (should start with ghp_, gho_, or github_pat_)");
30
+ }
31
+
32
+ const sortStr = await ask(" Default sort [best-match/stars/updated/forks] (best-match): ");
33
+ const limitStr = await ask(" Default result limit [10-100] (50): ");
34
+
35
+ const themes = listThemes();
36
+ const themeStr = await ask(` Theme [${themes.join("/")}] (tokyo-night): `);
37
+
38
+ const config: Config = {
39
+ defaultSort: (["best-match", "stars", "updated", "forks"].includes(sortStr)
40
+ ? sortStr
41
+ : "best-match") as SortStrategy,
42
+ defaultLimit: Math.min(100, Math.max(10, parseInt(limitStr) || 50)),
43
+ theme: themes.includes(themeStr) ? themeStr : "tokyo-night",
44
+ cacheTtlSeconds: 300,
45
+ defaultTab: "search",
46
+ };
47
+
48
+ if (token) config.githubToken = token;
49
+
50
+ saveConfig(config);
51
+ const { configPath } = await import("./config.ts");
52
+ console.log(` ✓ Config saved to ${configPath()}`);
53
+ console.log(" Run 'ghfind' to start browsing!");
54
+ console.log("");
55
+ }