bet-cli 0.1.4 → 0.2.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/README.md +21 -10
- package/dist/commands/ignore.js +1 -1
- package/dist/commands/info.js +12 -15
- package/dist/commands/list.js +32 -17
- package/dist/commands/update.js +1 -0
- package/dist/index.js +8 -29
- package/dist/lib/config.js +3 -0
- package/dist/lib/readme.js +5 -2
- package/dist/main.js +48 -0
- package/dist/ui/markdown.js +8 -9
- package/dist/ui/search.js +6 -7
- package/dist/ui/select.js +9 -11
- package/package.json +4 -1
- package/src/commands/ignore.ts +1 -1
- package/src/commands/info.tsx +118 -54
- package/src/commands/list.ts +81 -53
- package/src/commands/update.ts +1 -0
- package/src/index.ts +8 -34
- package/src/lib/config.ts +3 -0
- package/src/lib/readme.ts +6 -1
- package/src/lib/types.ts +2 -0
- package/src/main.ts +55 -0
- package/src/ui/markdown.tsx +14 -0
- package/src/ui/search.tsx +55 -24
- package/src/ui/select.tsx +48 -27
package/README.md
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
```
|
|
2
|
-
██
|
|
3
|
-
██
|
|
4
|
-
████████████
|
|
5
|
-
██
|
|
6
|
-
████████
|
|
7
|
-
╲╲
|
|
8
|
-
╲╲
|
|
9
|
-
```
|
|
10
|
-
|
|
11
1
|
# bet
|
|
12
2
|
|
|
13
3
|
Keep your house in order. Explore and jump between local projects.
|
|
@@ -16,6 +6,27 @@ Keep your house in order. Explore and jump between local projects.
|
|
|
16
6
|
|
|
17
7
|
If your `~/code` folder is chaos, **bet turns it into a map**.
|
|
18
8
|
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
12
|
+
░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
13
|
+
░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
14
|
+
░░░░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
15
|
+
░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
16
|
+
░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
17
|
+
░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
18
|
+
░░░░░░░░░░░░░░░░ ░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
19
|
+
░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
20
|
+
░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
21
|
+
░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
22
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
23
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
24
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
25
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
26
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
|
|
19
30
|
## Why bet?
|
|
20
31
|
|
|
21
32
|
- **No scrolling through 300 folders**
|
package/dist/commands/ignore.js
CHANGED
|
@@ -30,7 +30,7 @@ export function registerIgnore(program) {
|
|
|
30
30
|
}
|
|
31
31
|
const rootPaths = config.roots.map((r) => r.path);
|
|
32
32
|
if (!isPathUnderAnyRoot(normalized, rootPaths)) {
|
|
33
|
-
process.stderr.write(`Error: Path must be under a configured root.\n Path: ${normalized}\n Roots: ${rootPaths.join(", ")}\n`);
|
|
33
|
+
process.stderr.write(`Error: Path must be under a configured root. Use an absolute path (e.g. /path/to/project), not a project slug.\n Path: ${normalized}\n Roots: ${rootPaths.join(", ")}\n`);
|
|
34
34
|
process.exitCode = 1;
|
|
35
35
|
return;
|
|
36
36
|
}
|
package/dist/commands/info.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { readConfig } from "../lib/config.js";
|
|
4
3
|
import { render, Box, Text } from "ink";
|
|
4
|
+
import { readConfig } from "../lib/config.js";
|
|
5
5
|
import { findBySlug, listProjects, projectLabel } from "../lib/projects.js";
|
|
6
6
|
import { getDirtyStatus, isInsideGitRepo } from "../lib/git.js";
|
|
7
7
|
import { formatDate } from "../utils/format.js";
|
|
8
8
|
import { promptSelect } from "../ui/prompt.js";
|
|
9
9
|
import { readReadmeContent } from "../lib/readme.js";
|
|
10
|
-
import
|
|
11
|
-
const
|
|
10
|
+
import Markdown from "../ui/markdown.js";
|
|
11
|
+
const MetaRow = ({ label, value, valueColor }) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "gray", children: `${label}: ` }), _jsx(Text, { color: valueColor, bold: !!valueColor, children: value })] }));
|
|
12
12
|
export function registerInfo(program) {
|
|
13
13
|
program
|
|
14
14
|
.command("info <slug>")
|
|
15
15
|
.description("Show project details")
|
|
16
16
|
.option("--json", "Print JSON output")
|
|
17
|
+
.option("--full", "Show full README content")
|
|
17
18
|
.action(async (slug, options) => {
|
|
18
19
|
const config = await readConfig();
|
|
19
20
|
const projects = listProjects(config);
|
|
@@ -54,18 +55,11 @@ export function registerInfo(program) {
|
|
|
54
55
|
const hasGit = await isInsideGitRepo(project.path);
|
|
55
56
|
const dirty = hasGit ? await getDirtyStatus(project.path) : undefined;
|
|
56
57
|
if (process.stdin.isTTY) {
|
|
57
|
-
const readme =
|
|
58
|
+
const readme = options.full
|
|
59
|
+
? await readReadmeContent(project.path, { full: true })
|
|
60
|
+
: null;
|
|
58
61
|
const markdown = readme ?? description;
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const markdownModule = await import("ink-markdown");
|
|
62
|
-
Markdown = (markdownModule.default ??
|
|
63
|
-
markdownModule);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
Markdown = null;
|
|
67
|
-
}
|
|
68
|
-
const view = (_jsxs(Box, { flexDirection: "column", children: [_jsx(Table, { data: data }), _jsx(Text, { color: "green", bold: true, children: project.slug }), _jsx(Text, { dimColor: true, children: project.path }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: `Root: ${project.rootName}` }), _jsx(Text, { bold: true, children: `Root path: ${project.root}` }), _jsx(Text, { bold: true, children: `Git: ${hasGit ? "yes" : "no"}` }), _jsx(Text, { bold: true, children: `README: ${project.hasReadme ? "yes" : "no"}` }), _jsx(Text, { bold: true, children: `Started: ${formatDate(project.auto.startedAt)}` }), _jsx(Text, { bold: true, children: `Last modified: ${formatDate(project.auto.lastModifiedAt)}` }), _jsx(Text, { bold: true, children: `Last indexed: ${formatDate(project.auto.lastIndexedAt)}` }), _jsx(Text, { bold: true, children: `Dirty: ${dirty === undefined ? "unknown" : dirty ? "yes" : "no"}` }), project.user?.tags?.length ? (_jsx(Text, { children: `Tags: ${project.user.tags.join(", ")}` })) : null, project.user?.onEnter ? (_jsx(Text, { children: `On enter: ${project.user.onEnter}` })) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: chalk.bold("Description") }), Markdown ? (_jsx(Markdown, { children: markdown })) : (_jsx(Text, { children: markdown }))] })] }));
|
|
62
|
+
const view = (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { width: "100%", borderStyle: "single", borderColor: "green", paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: "green", bold: true, children: project.slug }), _jsx(Text, { color: "cyan", children: project.path })] }), _jsxs(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "magenta", children: "Details" }) }), _jsxs(Box, { flexDirection: "column", children: [_jsx(MetaRow, { label: "Root", value: project.rootName }), _jsx(MetaRow, { label: "Root path", value: project.root }), _jsx(MetaRow, { label: "Git", value: hasGit ? "yes" : "no", valueColor: hasGit ? "green" : "yellow" }), _jsx(MetaRow, { label: "README", value: project.hasReadme ? "yes" : "no", valueColor: project.hasReadme ? "green" : "yellow" }), _jsx(MetaRow, { label: "Started", value: formatDate(project.auto.startedAt) }), _jsx(MetaRow, { label: "Last modified", value: formatDate(project.auto.lastModifiedAt) }), _jsx(MetaRow, { label: "Last indexed", value: formatDate(project.auto.lastIndexedAt) }), _jsx(MetaRow, { label: "Dirty", value: dirty === undefined ? "unknown" : dirty ? "yes" : "no", valueColor: dirty === undefined ? "yellow" : dirty ? "red" : "green" }), project.user?.tags?.length ? (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "gray", children: `Tags: ` }), _jsx(Text, { color: "magenta", children: project.user.tags.join(", ") })] })) : null, project.user?.onEnter ? (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "gray", children: `On enter: ` }), _jsx(Text, { color: "blue", children: project.user.onEnter })] })) : null] })] }), _jsxs(Box, { borderStyle: "round", borderColor: "magenta", padding: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "magenta", children: "Description" }) }), _jsx(Markdown, { children: markdown })] })] }));
|
|
69
63
|
const { unmount } = render(view, { stdout: process.stdout });
|
|
70
64
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
71
65
|
unmount();
|
|
@@ -77,7 +71,10 @@ export function registerInfo(program) {
|
|
|
77
71
|
process.stdout.write(`${chalk.bold("Root path:")} ${project.root}\n`);
|
|
78
72
|
process.stdout.write(`${chalk.bold("Git:")} ${hasGit ? "yes" : "no"}\n`);
|
|
79
73
|
process.stdout.write(`${chalk.bold("README:")} ${project.hasReadme ? "yes" : "no"}\n\n`);
|
|
80
|
-
|
|
74
|
+
const descToShow = options.full && project.hasReadme
|
|
75
|
+
? (await readReadmeContent(project.path, { full: true })) ?? description
|
|
76
|
+
: description;
|
|
77
|
+
process.stdout.write(`${chalk.bold("Description:")} ${descToShow}\n`);
|
|
81
78
|
process.stdout.write(`${chalk.bold("Started:")} ${formatDate(project.auto.startedAt)}\n`);
|
|
82
79
|
process.stdout.write(`${chalk.bold("Last modified:")} ${formatDate(project.auto.lastModifiedAt)}\n`);
|
|
83
80
|
process.stdout.write(`${chalk.bold("Last indexed:")} ${formatDate(project.auto.lastIndexedAt)}\n`);
|
package/dist/commands/list.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
import { readConfig } from
|
|
3
|
-
import path from
|
|
4
|
-
import { listProjects } from
|
|
5
|
-
import { emitSelection } from
|
|
6
|
-
import { promptSelect } from
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { readConfig } from "../lib/config.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { listProjects } from "../lib/projects.js";
|
|
5
|
+
import { emitSelection } from "../utils/output.js";
|
|
6
|
+
import { promptSelect } from "../ui/prompt.js";
|
|
7
7
|
const MAX_ROWS = 18;
|
|
8
|
-
const GROUP_COLORS = [
|
|
8
|
+
const GROUP_COLORS = [
|
|
9
|
+
"#22d3ee",
|
|
10
|
+
"#34d399",
|
|
11
|
+
"#facc15",
|
|
12
|
+
"#c084fc",
|
|
13
|
+
"#60a5fa",
|
|
14
|
+
"#f87171",
|
|
15
|
+
];
|
|
9
16
|
function groupColor(index) {
|
|
10
17
|
return GROUP_COLORS[index % GROUP_COLORS.length];
|
|
11
18
|
}
|
|
@@ -44,22 +51,23 @@ function orderedGroupsByConfigRoots(projects, roots) {
|
|
|
44
51
|
}
|
|
45
52
|
export function registerList(program) {
|
|
46
53
|
program
|
|
47
|
-
.command(
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
.option(
|
|
51
|
-
.option(
|
|
54
|
+
.command("list")
|
|
55
|
+
.alias("ls")
|
|
56
|
+
.description("List projects")
|
|
57
|
+
.option("--plain", "Print a non-interactive list")
|
|
58
|
+
.option("--json", "Print JSON output")
|
|
59
|
+
.option("--print", "Print selected path only")
|
|
52
60
|
.action(async (options) => {
|
|
53
61
|
const config = await readConfig();
|
|
54
62
|
const projects = listProjects(config);
|
|
55
63
|
if (options.json) {
|
|
56
64
|
process.stdout.write(JSON.stringify(projects, null, 2));
|
|
57
|
-
process.stdout.write(
|
|
65
|
+
process.stdout.write("\n");
|
|
58
66
|
return;
|
|
59
67
|
}
|
|
60
68
|
if (!process.stdin.isTTY || options.plain) {
|
|
61
69
|
if (projects.length === 0) {
|
|
62
|
-
process.stdout.write(
|
|
70
|
+
process.stdout.write("No projects indexed. Run bet update.\n");
|
|
63
71
|
return;
|
|
64
72
|
}
|
|
65
73
|
const orderedGroups = orderedGroupsByConfigRoots(projects, config.roots);
|
|
@@ -79,17 +87,24 @@ export function registerList(program) {
|
|
|
79
87
|
const rows = [];
|
|
80
88
|
let groupIndex = 0;
|
|
81
89
|
for (const { rootName, items } of orderedGroups) {
|
|
82
|
-
rows.push({
|
|
90
|
+
rows.push({
|
|
91
|
+
type: "group",
|
|
92
|
+
label: rootName,
|
|
93
|
+
color: groupColor(groupIndex++),
|
|
94
|
+
});
|
|
83
95
|
for (const project of items) {
|
|
84
96
|
const rel = relativePath(project.path, project.root);
|
|
85
97
|
rows.push({
|
|
86
|
-
type:
|
|
98
|
+
type: "item",
|
|
87
99
|
label: formatLabel(project.slug, project.rootName, rel),
|
|
88
100
|
value: project,
|
|
89
101
|
});
|
|
90
102
|
}
|
|
91
103
|
}
|
|
92
|
-
const selected = await promptSelect(rows, {
|
|
104
|
+
const selected = await promptSelect(rows, {
|
|
105
|
+
title: "Projects",
|
|
106
|
+
maxRows: MAX_ROWS,
|
|
107
|
+
});
|
|
93
108
|
if (!selected)
|
|
94
109
|
return;
|
|
95
110
|
emitSelection(selected.value, { printOnly: options.print });
|
package/dist/commands/update.js
CHANGED
|
@@ -134,6 +134,7 @@ export function registerUpdate(program) {
|
|
|
134
134
|
version: config.version ?? 1,
|
|
135
135
|
roots: rootsResolved,
|
|
136
136
|
projects,
|
|
137
|
+
updatedAt: new Date().toISOString(),
|
|
137
138
|
...(config.ignores !== undefined && { ignores: config.ignores }),
|
|
138
139
|
...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
|
|
139
140
|
...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
import { registerShell } from "./commands/shell.js";
|
|
11
|
-
import { registerCompletion } from "./commands/completion.js";
|
|
12
|
-
import { registerIgnore } from "./commands/ignore.js";
|
|
13
|
-
const program = new Command();
|
|
14
|
-
program.createHelp = function createHelp() {
|
|
15
|
-
return Object.assign(new GroupedHelp(), this.configureHelp());
|
|
16
|
-
};
|
|
17
|
-
program
|
|
18
|
-
.name("bet")
|
|
19
|
-
.description("Explore and jump between local projects.")
|
|
20
|
-
.version("0.1.3");
|
|
21
|
-
registerUpdate(program);
|
|
22
|
-
registerList(program);
|
|
23
|
-
registerSearch(program);
|
|
24
|
-
registerInfo(program);
|
|
25
|
-
registerGo(program);
|
|
26
|
-
registerPath(program);
|
|
27
|
-
registerShell(program);
|
|
28
|
-
registerCompletion(program);
|
|
29
|
-
registerIgnore(program);
|
|
30
|
-
program.parseAsync(process.argv);
|
|
2
|
+
/**
|
|
3
|
+
* Set FORCE_COLOR before any module (including Ink/chalk) is loaded so that
|
|
4
|
+
* list/search/info render with colors. ESM hoists static imports, so we must
|
|
5
|
+
* use a dynamic import here.
|
|
6
|
+
*/
|
|
7
|
+
process.env.FORCE_COLOR = "1";
|
|
8
|
+
await import("./main.js");
|
|
9
|
+
export {};
|
package/dist/lib/config.js
CHANGED
|
@@ -87,6 +87,7 @@ async function readProjectsConfig() {
|
|
|
87
87
|
const parsed = JSON.parse(raw);
|
|
88
88
|
return {
|
|
89
89
|
projects: parsed.projects ?? {},
|
|
90
|
+
...(parsed.updatedAt != null && typeof parsed.updatedAt === "string" && { updatedAt: parsed.updatedAt }),
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
catch (error) {
|
|
@@ -116,6 +117,7 @@ export async function readConfig() {
|
|
|
116
117
|
return {
|
|
117
118
|
...appConfig,
|
|
118
119
|
projects: normalizedProjects,
|
|
120
|
+
...(projectsConfig.updatedAt != null && { updatedAt: projectsConfig.updatedAt }),
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
async function writeAppConfig(appConfig) {
|
|
@@ -138,6 +140,7 @@ export async function writeConfig(config) {
|
|
|
138
140
|
};
|
|
139
141
|
const projectsConfig = {
|
|
140
142
|
projects: config.projects,
|
|
143
|
+
...(config.updatedAt !== undefined && { updatedAt: config.updatedAt }),
|
|
141
144
|
};
|
|
142
145
|
await Promise.all([
|
|
143
146
|
writeAppConfig(appConfig),
|
package/dist/lib/readme.js
CHANGED
|
@@ -14,14 +14,17 @@ export async function readReadmePath(projectPath) {
|
|
|
14
14
|
}
|
|
15
15
|
return undefined;
|
|
16
16
|
}
|
|
17
|
-
export async function readReadmeContent(projectPath) {
|
|
17
|
+
export async function readReadmeContent(projectPath, options) {
|
|
18
18
|
const readmePath = await readReadmePath(projectPath);
|
|
19
19
|
if (!readmePath)
|
|
20
20
|
return undefined;
|
|
21
21
|
let content = await fs.readFile(readmePath, "utf8");
|
|
22
22
|
// remove html tags
|
|
23
23
|
content = content.replace(/<[^>]*>?/g, "");
|
|
24
|
-
|
|
24
|
+
if (options?.full) {
|
|
25
|
+
return content;
|
|
26
|
+
}
|
|
27
|
+
// only grab the first 500 characters, and add ... if there are more
|
|
25
28
|
const truncated = content.slice(0, 500);
|
|
26
29
|
if (content.length > 500) {
|
|
27
30
|
return `${truncated}...`;
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { GroupedHelp } from "./lib/help.js";
|
|
3
|
+
import { registerUpdate } from "./commands/update.js";
|
|
4
|
+
import { registerList } from "./commands/list.js";
|
|
5
|
+
import { registerSearch } from "./commands/search.js";
|
|
6
|
+
import { registerInfo } from "./commands/info.js";
|
|
7
|
+
import { registerGo } from "./commands/go.js";
|
|
8
|
+
import { registerPath } from "./commands/path.js";
|
|
9
|
+
import { registerShell } from "./commands/shell.js";
|
|
10
|
+
import { registerCompletion } from "./commands/completion.js";
|
|
11
|
+
import { registerIgnore } from "./commands/ignore.js";
|
|
12
|
+
const ASCII_HEADER = `
|
|
13
|
+
░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
14
|
+
░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
15
|
+
░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
16
|
+
░░░░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
17
|
+
░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
18
|
+
░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
19
|
+
░░░░░░░░░░░░ ░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
20
|
+
░░░░░░░░░░░░░░░░ ░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
21
|
+
░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
22
|
+
░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
23
|
+
░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
24
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
25
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
26
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
27
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
28
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
29
|
+
`;
|
|
30
|
+
const program = new Command();
|
|
31
|
+
program.createHelp = function createHelp() {
|
|
32
|
+
return Object.assign(new GroupedHelp(), this.configureHelp());
|
|
33
|
+
};
|
|
34
|
+
program
|
|
35
|
+
.name("bet")
|
|
36
|
+
.description("Explore and jump between local projects.")
|
|
37
|
+
.version("0.2.0");
|
|
38
|
+
registerUpdate(program);
|
|
39
|
+
registerList(program);
|
|
40
|
+
registerSearch(program);
|
|
41
|
+
registerInfo(program);
|
|
42
|
+
registerGo(program);
|
|
43
|
+
registerPath(program);
|
|
44
|
+
registerShell(program);
|
|
45
|
+
registerCompletion(program);
|
|
46
|
+
registerIgnore(program);
|
|
47
|
+
program.addHelpText("before", ASCII_HEADER);
|
|
48
|
+
program.parseAsync(process.argv);
|
package/dist/ui/markdown.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
marked.
|
|
8
|
-
|
|
9
|
-
return _jsx(Text, { children: output });
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import { markedTerminal } from "marked-terminal";
|
|
4
|
+
import React from "react";
|
|
5
|
+
export default function Markdown({ children, ...options }) {
|
|
6
|
+
marked.use(markedTerminal(options));
|
|
7
|
+
const parsedMarkdown = marked.parse(children, { async: false });
|
|
8
|
+
return React.createElement(Text, null, parsedMarkdown.trim());
|
|
10
9
|
}
|
package/dist/ui/search.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
3
|
import { Box, Text, useInput } from 'ink';
|
|
5
4
|
const DEFAULT_MAX_ROWS = 18;
|
|
6
5
|
export function SearchSelect({ title, allItems, filter, onSelect, onCancel, maxRows = DEFAULT_MAX_ROWS, initialQuery = '', showCount = true, }) {
|
|
@@ -37,7 +36,7 @@ export function SearchSelect({ title, allItems, filter, onSelect, onCancel, maxR
|
|
|
37
36
|
}
|
|
38
37
|
});
|
|
39
38
|
if (items.length === 0) {
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column", children: [title
|
|
39
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", padding: 1, children: [title ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: title }) })) : null, _jsxs(Box, { marginBottom: 1, flexDirection: "row", children: [_jsx(Text, { bold: true, color: "yellow", children: "Search: " }), _jsx(Text, { color: "cyan", children: query || '…' })] }), _jsx(Text, { color: "yellow", children: "No results." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Press Esc to exit." }) })] }));
|
|
41
40
|
}
|
|
42
41
|
const selectedRowIndex = Math.min(cursor, items.length - 1);
|
|
43
42
|
const totalRows = items.length;
|
|
@@ -45,9 +44,9 @@ export function SearchSelect({ title, allItems, filter, onSelect, onCancel, maxR
|
|
|
45
44
|
const windowStart = Math.min(Math.max(0, selectedRowIndex - Math.floor(effectiveMaxRows / 2)), Math.max(0, totalRows - effectiveMaxRows));
|
|
46
45
|
const windowEnd = Math.min(totalRows, windowStart + effectiveMaxRows);
|
|
47
46
|
const windowed = items.slice(windowStart, windowEnd);
|
|
48
|
-
return (_jsxs(Box, { flexDirection: "column", children: [title
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", padding: 1, children: [title ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: title }) })) : null, _jsxs(Box, { marginBottom: 1, flexDirection: "row", children: [_jsx(Text, { bold: true, color: "yellow", children: "Search: " }), _jsx(Text, { color: "green", children: query || '…' }), showCount ? (_jsxs(Text, { color: "cyan", children: [" \u00B7 ", items.length, " result", items.length !== 1 ? 's' : ''] })) : null] }), _jsx(Box, { flexDirection: "column", children: windowed.map((row, idx) => {
|
|
48
|
+
const absoluteIndex = windowStart + idx;
|
|
49
|
+
const selected = absoluteIndex === selectedRowIndex;
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: selected ? 'green' : undefined, bold: selected, children: [selected ? '› ' : ' ', row.label] }), row.hint ? _jsxs(Text, { color: "gray", children: [" ", row.hint] }) : null] }, `item-${absoluteIndex}`));
|
|
51
|
+
}) }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "yellow", children: "Type to filter" }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: "yellow", children: "\u2191/\u2193 or j/k" }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: "green", children: "Enter" }), _jsx(Text, { color: "gray", children: " to select \u00B7 " }), _jsx(Text, { color: "red", children: "Esc" }), _jsx(Text, { color: "gray", children: " to cancel" })] })] }));
|
|
53
52
|
}
|
package/dist/ui/select.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
3
|
import { Box, Text, useInput } from 'ink';
|
|
5
4
|
const DEFAULT_MAX_ROWS = 18;
|
|
6
5
|
export function SelectList({ title, items, onSelect, onCancel, maxRows = DEFAULT_MAX_ROWS, }) {
|
|
@@ -31,7 +30,7 @@ export function SelectList({ title, items, onSelect, onCancel, maxRows = DEFAULT
|
|
|
31
30
|
}
|
|
32
31
|
});
|
|
33
32
|
if (items.length === 0) {
|
|
34
|
-
return (_jsxs(Box, { flexDirection: "column", children: [title
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", padding: 1, children: [title ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: title }) })) : null, _jsx(Text, { color: "yellow", children: "No results." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Press Esc to exit." }) })] }));
|
|
35
34
|
}
|
|
36
35
|
const selectedRowIndex = selectableIndices[cursor] ?? 0;
|
|
37
36
|
const totalRows = items.length;
|
|
@@ -39,13 +38,12 @@ export function SelectList({ title, items, onSelect, onCancel, maxRows = DEFAULT
|
|
|
39
38
|
const windowStart = Math.min(Math.max(0, selectedRowIndex - Math.floor(effectiveMaxRows / 2)), Math.max(0, totalRows - effectiveMaxRows));
|
|
40
39
|
const windowEnd = Math.min(totalRows, windowStart + effectiveMaxRows);
|
|
41
40
|
const windowed = items.slice(windowStart, windowEnd);
|
|
42
|
-
return (_jsxs(Box, { flexDirection: "column", children: [title
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: chalk.dim('Use ↑/↓ or j/k. Enter to select. Esc to cancel.') }) })] }));
|
|
41
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", padding: 1, children: [title ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: title }) })) : null, _jsx(Box, { flexDirection: "column", children: windowed.map((row, idx) => {
|
|
42
|
+
const absoluteIndex = windowStart + idx;
|
|
43
|
+
const selected = row.type === 'item' && absoluteIndex === selectedRowIndex;
|
|
44
|
+
if (row.type === 'group') {
|
|
45
|
+
return (_jsx(Box, { marginTop: idx === 0 ? 0 : 1, children: _jsxs(Text, { bold: true, color: row.color ?? 'cyan', children: ["[", row.label, "]"] }) }, `group-${absoluteIndex}`));
|
|
46
|
+
}
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: selected ? 'green' : undefined, bold: selected, children: [selected ? '› ' : ' ', row.label] }), row.hint ? (_jsxs(Text, { color: "gray", children: [" ", row.hint] })) : null] }, `item-${absoluteIndex}`));
|
|
48
|
+
}) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u2191/\u2193 or j/k" }), _jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: "green", children: "Enter" }), _jsx(Text, { color: "gray", children: " to select \u00B7 " }), _jsx(Text, { color: "red", children: "Esc" }), _jsx(Text, { color: "gray", children: " to cancel" })] })] }));
|
|
51
49
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bet-cli",
|
|
3
3
|
"description": "Explore and jump between local projects.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"author": "Chris Mckenzie",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"node": ">=20"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@types/marked-terminal": "^6.1.1",
|
|
26
27
|
"@types/object-hash": "^3.0.6",
|
|
27
28
|
"chalk": "^5.3.0",
|
|
28
29
|
"commander": "^12.0.0",
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
"ink": "^6.6.0",
|
|
32
33
|
"ink-markdown": "^1.0.4",
|
|
33
34
|
"ink-table": "^3.1.0",
|
|
35
|
+
"marked": "^17.0.3",
|
|
36
|
+
"marked-terminal": "^7.3.0",
|
|
34
37
|
"object-hash": "^3.0.0",
|
|
35
38
|
"react": "^19.2.4"
|
|
36
39
|
},
|
package/src/commands/ignore.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function registerIgnore(program: Command): void {
|
|
|
38
38
|
const rootPaths = config.roots.map((r) => r.path);
|
|
39
39
|
if (!isPathUnderAnyRoot(normalized, rootPaths)) {
|
|
40
40
|
process.stderr.write(
|
|
41
|
-
`Error: Path must be under a configured root.\n Path: ${normalized}\n Roots: ${rootPaths.join(", ")}\n`,
|
|
41
|
+
`Error: Path must be under a configured root. Use an absolute path (e.g. /path/to/project), not a project slug.\n Path: ${normalized}\n Roots: ${rootPaths.join(", ")}\n`,
|
|
42
42
|
);
|
|
43
43
|
process.exitCode = 1;
|
|
44
44
|
return;
|
package/src/commands/info.tsx
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import
|
|
3
|
+
import React from "react";
|
|
4
4
|
import { render, Box, Text } from "ink";
|
|
5
|
+
import { readConfig } from "../lib/config.js";
|
|
5
6
|
import { findBySlug, listProjects, projectLabel } from "../lib/projects.js";
|
|
6
7
|
import { getDirtyStatus, isInsideGitRepo } from "../lib/git.js";
|
|
7
8
|
import { formatDate } from "../utils/format.js";
|
|
8
9
|
import { promptSelect } from "../ui/prompt.js";
|
|
9
10
|
import { SelectEntry } from "../ui/select.js";
|
|
10
11
|
import { readReadmeContent } from "../lib/readme.js";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import Markdown from "../ui/markdown.js";
|
|
13
|
+
|
|
14
|
+
type MetaRowProps = {
|
|
15
|
+
label: string;
|
|
16
|
+
value: string;
|
|
17
|
+
valueColor?:
|
|
18
|
+
| "green"
|
|
19
|
+
| "red"
|
|
20
|
+
| "yellow"
|
|
21
|
+
| "blue"
|
|
22
|
+
| "cyan"
|
|
23
|
+
| "magenta"
|
|
24
|
+
| "gray";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const MetaRow: React.FC<MetaRowProps> = ({ label, value, valueColor }) => (
|
|
28
|
+
<Box>
|
|
29
|
+
<Text bold color="gray">{`${label}: `}</Text>
|
|
30
|
+
<Text color={valueColor} bold={!!valueColor}>
|
|
31
|
+
{value}
|
|
32
|
+
</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
14
35
|
|
|
15
36
|
export function registerInfo(program: Command): void {
|
|
16
37
|
program
|
|
17
38
|
.command("info <slug>")
|
|
18
39
|
.description("Show project details")
|
|
19
40
|
.option("--json", "Print JSON output")
|
|
20
|
-
.
|
|
41
|
+
.option("--full", "Show full README content")
|
|
42
|
+
.action(async (slug: string, options: { json?: boolean; full?: boolean }) => {
|
|
21
43
|
const config = await readConfig();
|
|
22
44
|
const projects = listProjects(config);
|
|
23
45
|
const matches = findBySlug(projects, slug);
|
|
@@ -67,58 +89,96 @@ export function registerInfo(program: Command): void {
|
|
|
67
89
|
const dirty = hasGit ? await getDirtyStatus(project.path) : undefined;
|
|
68
90
|
|
|
69
91
|
if (process.stdin.isTTY) {
|
|
70
|
-
const readme =
|
|
92
|
+
const readme = options.full
|
|
93
|
+
? await readReadmeContent(project.path, { full: true })
|
|
94
|
+
: null;
|
|
71
95
|
const markdown = readme ?? description;
|
|
72
96
|
|
|
73
|
-
let Markdown: React.FC<{ children: string }> | null = null;
|
|
74
|
-
try {
|
|
75
|
-
const markdownModule = await import("ink-markdown");
|
|
76
|
-
Markdown = (markdownModule.default ??
|
|
77
|
-
markdownModule) as unknown as React.FC<{
|
|
78
|
-
children: string;
|
|
79
|
-
}>;
|
|
80
|
-
} catch {
|
|
81
|
-
Markdown = null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
97
|
const view = (
|
|
85
|
-
<Box flexDirection="column">
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<Text
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
98
|
+
<Box flexDirection="column" width="100%">
|
|
99
|
+
<Box
|
|
100
|
+
width="100%"
|
|
101
|
+
borderStyle="single"
|
|
102
|
+
borderColor="green"
|
|
103
|
+
paddingX={1}
|
|
104
|
+
paddingY={1}
|
|
105
|
+
marginBottom={1}
|
|
106
|
+
>
|
|
107
|
+
<Text color="green" bold>
|
|
108
|
+
{project.slug}
|
|
109
|
+
</Text>
|
|
110
|
+
<Text color="cyan">{project.path}</Text>
|
|
111
|
+
</Box>
|
|
112
|
+
<Box
|
|
113
|
+
borderStyle="round"
|
|
114
|
+
borderColor="cyan"
|
|
115
|
+
padding={1}
|
|
116
|
+
flexDirection="column"
|
|
117
|
+
marginBottom={1}
|
|
118
|
+
>
|
|
119
|
+
<Box marginBottom={1}>
|
|
120
|
+
<Text bold color="magenta">
|
|
121
|
+
Details
|
|
122
|
+
</Text>
|
|
123
|
+
</Box>
|
|
124
|
+
<Box flexDirection="column">
|
|
125
|
+
<MetaRow label="Root" value={project.rootName} />
|
|
126
|
+
<MetaRow label="Root path" value={project.root} />
|
|
127
|
+
<MetaRow
|
|
128
|
+
label="Git"
|
|
129
|
+
value={hasGit ? "yes" : "no"}
|
|
130
|
+
valueColor={hasGit ? "green" : "yellow"}
|
|
131
|
+
/>
|
|
132
|
+
<MetaRow
|
|
133
|
+
label="README"
|
|
134
|
+
value={project.hasReadme ? "yes" : "no"}
|
|
135
|
+
valueColor={project.hasReadme ? "green" : "yellow"}
|
|
136
|
+
/>
|
|
137
|
+
<MetaRow
|
|
138
|
+
label="Started"
|
|
139
|
+
value={formatDate(project.auto.startedAt)}
|
|
140
|
+
/>
|
|
141
|
+
<MetaRow
|
|
142
|
+
label="Last modified"
|
|
143
|
+
value={formatDate(project.auto.lastModifiedAt)}
|
|
144
|
+
/>
|
|
145
|
+
<MetaRow
|
|
146
|
+
label="Last indexed"
|
|
147
|
+
value={formatDate(project.auto.lastIndexedAt)}
|
|
148
|
+
/>
|
|
149
|
+
<MetaRow
|
|
150
|
+
label="Dirty"
|
|
151
|
+
value={dirty === undefined ? "unknown" : dirty ? "yes" : "no"}
|
|
152
|
+
valueColor={
|
|
153
|
+
dirty === undefined ? "yellow" : dirty ? "red" : "green"
|
|
154
|
+
}
|
|
155
|
+
/>
|
|
156
|
+
{project.user?.tags?.length ? (
|
|
157
|
+
<Box>
|
|
158
|
+
<Text bold color="gray">{`Tags: `}</Text>
|
|
159
|
+
<Text color="magenta">{project.user.tags.join(", ")}</Text>
|
|
160
|
+
</Box>
|
|
161
|
+
) : null}
|
|
162
|
+
{project.user?.onEnter ? (
|
|
163
|
+
<Box>
|
|
164
|
+
<Text bold color="gray">{`On enter: `}</Text>
|
|
165
|
+
<Text color="blue">{project.user.onEnter}</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
) : null}
|
|
168
|
+
</Box>
|
|
114
169
|
</Box>
|
|
115
|
-
<Box
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
170
|
+
<Box
|
|
171
|
+
borderStyle="round"
|
|
172
|
+
borderColor="magenta"
|
|
173
|
+
padding={1}
|
|
174
|
+
flexDirection="column"
|
|
175
|
+
>
|
|
176
|
+
<Box marginBottom={1}>
|
|
177
|
+
<Text bold color="magenta">
|
|
178
|
+
Description
|
|
179
|
+
</Text>
|
|
180
|
+
</Box>
|
|
181
|
+
<Markdown>{markdown}</Markdown>
|
|
122
182
|
</Box>
|
|
123
183
|
</Box>
|
|
124
184
|
);
|
|
@@ -139,7 +199,11 @@ export function registerInfo(program: Command): void {
|
|
|
139
199
|
`${chalk.bold("README:")} ${project.hasReadme ? "yes" : "no"}\n\n`,
|
|
140
200
|
);
|
|
141
201
|
|
|
142
|
-
|
|
202
|
+
const descToShow =
|
|
203
|
+
options.full && project.hasReadme
|
|
204
|
+
? (await readReadmeContent(project.path, { full: true })) ?? description
|
|
205
|
+
: description;
|
|
206
|
+
process.stdout.write(`${chalk.bold("Description:")} ${descToShow}\n`);
|
|
143
207
|
process.stdout.write(
|
|
144
208
|
`${chalk.bold("Started:")} ${formatDate(project.auto.startedAt)}\n`,
|
|
145
209
|
);
|
package/src/commands/list.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
import { Command } from
|
|
3
|
-
import { readConfig } from
|
|
4
|
-
import path from
|
|
5
|
-
import { listProjects } from
|
|
6
|
-
import type { Project, RootConfig } from
|
|
7
|
-
import { emitSelection } from
|
|
8
|
-
import { promptSelect } from
|
|
9
|
-
import { SelectRow } from
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { readConfig } from "../lib/config.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { listProjects } from "../lib/projects.js";
|
|
6
|
+
import type { Project, RootConfig } from "../lib/types.js";
|
|
7
|
+
import { emitSelection } from "../utils/output.js";
|
|
8
|
+
import { promptSelect } from "../ui/prompt.js";
|
|
9
|
+
import { SelectRow } from "../ui/select.js";
|
|
10
10
|
|
|
11
11
|
const MAX_ROWS = 18;
|
|
12
|
-
const GROUP_COLORS = [
|
|
12
|
+
const GROUP_COLORS = [
|
|
13
|
+
"#22d3ee",
|
|
14
|
+
"#34d399",
|
|
15
|
+
"#facc15",
|
|
16
|
+
"#c084fc",
|
|
17
|
+
"#60a5fa",
|
|
18
|
+
"#f87171",
|
|
19
|
+
];
|
|
13
20
|
|
|
14
21
|
function groupColor(index: number): string {
|
|
15
22
|
return GROUP_COLORS[index % GROUP_COLORS.length];
|
|
@@ -28,7 +35,10 @@ function orderedGroupsByConfigRoots(
|
|
|
28
35
|
projects: Project[],
|
|
29
36
|
roots: RootConfig[],
|
|
30
37
|
): { rootName: string; items: Project[] }[] {
|
|
31
|
-
const groupsByRoot = new Map<
|
|
38
|
+
const groupsByRoot = new Map<
|
|
39
|
+
string,
|
|
40
|
+
{ rootName: string; items: Project[] }
|
|
41
|
+
>();
|
|
32
42
|
for (const project of projects) {
|
|
33
43
|
const key = project.root;
|
|
34
44
|
if (!groupsByRoot.has(key)) {
|
|
@@ -60,58 +70,76 @@ function orderedGroupsByConfigRoots(
|
|
|
60
70
|
|
|
61
71
|
export function registerList(program: Command): void {
|
|
62
72
|
program
|
|
63
|
-
.command(
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
.option(
|
|
67
|
-
.option(
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
.command("list")
|
|
74
|
+
.alias("ls")
|
|
75
|
+
.description("List projects")
|
|
76
|
+
.option("--plain", "Print a non-interactive list")
|
|
77
|
+
.option("--json", "Print JSON output")
|
|
78
|
+
.option("--print", "Print selected path only")
|
|
79
|
+
.action(
|
|
80
|
+
async (options: { plain?: boolean; json?: boolean; print?: boolean }) => {
|
|
81
|
+
const config = await readConfig();
|
|
82
|
+
const projects = listProjects(config);
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
if (options.json) {
|
|
85
|
+
process.stdout.write(JSON.stringify(projects, null, 2));
|
|
86
|
+
process.stdout.write("\n");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
if (!process.stdin.isTTY || options.plain) {
|
|
91
|
+
if (projects.length === 0) {
|
|
92
|
+
process.stdout.write("No projects indexed. Run bet update.\n");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const orderedGroups = orderedGroupsByConfigRoots(
|
|
96
|
+
projects,
|
|
97
|
+
config.roots,
|
|
98
|
+
);
|
|
99
|
+
let groupIndex = 0;
|
|
100
|
+
for (const { rootName, items } of orderedGroups) {
|
|
101
|
+
const color = groupColor(groupIndex++);
|
|
102
|
+
const label = chalk.hex(color).bold(`[${rootName}]`);
|
|
103
|
+
process.stdout.write(`${label}\n`);
|
|
104
|
+
for (const project of items) {
|
|
105
|
+
const rel = relativePath(project.path, project.root);
|
|
106
|
+
process.stdout.write(
|
|
107
|
+
` ${chalk.reset(formatLabel(project.slug, project.rootName, rel))}\n`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
81
111
|
return;
|
|
82
112
|
}
|
|
83
|
-
|
|
113
|
+
|
|
114
|
+
const orderedGroups = orderedGroupsByConfigRoots(
|
|
115
|
+
projects,
|
|
116
|
+
config.roots,
|
|
117
|
+
);
|
|
118
|
+
const rows: SelectRow<Project>[] = [];
|
|
84
119
|
let groupIndex = 0;
|
|
85
120
|
for (const { rootName, items } of orderedGroups) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
121
|
+
rows.push({
|
|
122
|
+
type: "group",
|
|
123
|
+
label: rootName,
|
|
124
|
+
color: groupColor(groupIndex++),
|
|
125
|
+
});
|
|
89
126
|
for (const project of items) {
|
|
90
127
|
const rel = relativePath(project.path, project.root);
|
|
91
|
-
|
|
128
|
+
rows.push({
|
|
129
|
+
type: "item",
|
|
130
|
+
label: formatLabel(project.slug, project.rootName, rel),
|
|
131
|
+
value: project,
|
|
132
|
+
});
|
|
92
133
|
}
|
|
93
134
|
}
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const orderedGroups = orderedGroupsByConfigRoots(projects, config.roots);
|
|
98
|
-
const rows: SelectRow<Project>[] = [];
|
|
99
|
-
let groupIndex = 0;
|
|
100
|
-
for (const { rootName, items } of orderedGroups) {
|
|
101
|
-
rows.push({ type: 'group', label: rootName, color: groupColor(groupIndex++) });
|
|
102
|
-
for (const project of items) {
|
|
103
|
-
const rel = relativePath(project.path, project.root);
|
|
104
|
-
rows.push({
|
|
105
|
-
type: 'item',
|
|
106
|
-
label: formatLabel(project.slug, project.rootName, rel),
|
|
107
|
-
value: project,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
135
|
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
const selected = await promptSelect(rows, {
|
|
137
|
+
title: "Projects",
|
|
138
|
+
maxRows: MAX_ROWS,
|
|
139
|
+
});
|
|
140
|
+
if (!selected) return;
|
|
114
141
|
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
emitSelection(selected.value, { printOnly: options.print });
|
|
143
|
+
},
|
|
144
|
+
);
|
|
117
145
|
}
|
package/src/commands/update.ts
CHANGED
|
@@ -162,6 +162,7 @@ export function registerUpdate(program: Command): void {
|
|
|
162
162
|
version: config.version ?? 1,
|
|
163
163
|
roots: rootsResolved,
|
|
164
164
|
projects,
|
|
165
|
+
updatedAt: new Date().toISOString(),
|
|
165
166
|
...(config.ignores !== undefined && { ignores: config.ignores }),
|
|
166
167
|
...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
|
|
167
168
|
...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
|
package/src/index.ts
CHANGED
|
@@ -1,35 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
import { registerShell } from "./commands/shell.js";
|
|
11
|
-
import { registerCompletion } from "./commands/completion.js";
|
|
12
|
-
import { registerIgnore } from "./commands/ignore.js";
|
|
13
|
-
|
|
14
|
-
const program = new Command();
|
|
15
|
-
|
|
16
|
-
program.createHelp = function createHelp(this: Command) {
|
|
17
|
-
return Object.assign(new GroupedHelp(), this.configureHelp());
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
program
|
|
21
|
-
.name("bet")
|
|
22
|
-
.description("Explore and jump between local projects.")
|
|
23
|
-
.version("0.1.4");
|
|
24
|
-
|
|
25
|
-
registerUpdate(program);
|
|
26
|
-
registerList(program);
|
|
27
|
-
registerSearch(program);
|
|
28
|
-
registerInfo(program);
|
|
29
|
-
registerGo(program);
|
|
30
|
-
registerPath(program);
|
|
31
|
-
registerShell(program);
|
|
32
|
-
registerCompletion(program);
|
|
33
|
-
registerIgnore(program);
|
|
34
|
-
|
|
35
|
-
program.parseAsync(process.argv);
|
|
2
|
+
/**
|
|
3
|
+
* Set FORCE_COLOR before any module (including Ink/chalk) is loaded so that
|
|
4
|
+
* list/search/info render with colors. ESM hoists static imports, so we must
|
|
5
|
+
* use a dynamic import here.
|
|
6
|
+
*/
|
|
7
|
+
process.env.FORCE_COLOR = "1";
|
|
8
|
+
|
|
9
|
+
await import("./main.js");
|
package/src/lib/config.ts
CHANGED
|
@@ -95,6 +95,7 @@ async function readProjectsConfig(): Promise<ProjectsConfig> {
|
|
|
95
95
|
const parsed = JSON.parse(raw) as ProjectsConfig;
|
|
96
96
|
return {
|
|
97
97
|
projects: parsed.projects ?? {},
|
|
98
|
+
...(parsed.updatedAt != null && typeof parsed.updatedAt === "string" && { updatedAt: parsed.updatedAt }),
|
|
98
99
|
};
|
|
99
100
|
} catch (error) {
|
|
100
101
|
return { ...DEFAULT_PROJECTS_CONFIG };
|
|
@@ -123,6 +124,7 @@ export async function readConfig(): Promise<Config> {
|
|
|
123
124
|
return {
|
|
124
125
|
...appConfig,
|
|
125
126
|
projects: normalizedProjects,
|
|
127
|
+
...(projectsConfig.updatedAt != null && { updatedAt: projectsConfig.updatedAt }),
|
|
126
128
|
};
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -150,6 +152,7 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
150
152
|
};
|
|
151
153
|
const projectsConfig: ProjectsConfig = {
|
|
152
154
|
projects: config.projects,
|
|
155
|
+
...(config.updatedAt !== undefined && { updatedAt: config.updatedAt }),
|
|
153
156
|
};
|
|
154
157
|
await Promise.all([
|
|
155
158
|
writeAppConfig(appConfig),
|
package/src/lib/readme.ts
CHANGED
|
@@ -20,6 +20,7 @@ export async function readReadmePath(
|
|
|
20
20
|
|
|
21
21
|
export async function readReadmeContent(
|
|
22
22
|
projectPath: string,
|
|
23
|
+
options?: { full?: boolean },
|
|
23
24
|
): Promise<string | undefined> {
|
|
24
25
|
const readmePath = await readReadmePath(projectPath);
|
|
25
26
|
if (!readmePath) return undefined;
|
|
@@ -28,7 +29,11 @@ export async function readReadmeContent(
|
|
|
28
29
|
// remove html tags
|
|
29
30
|
content = content.replace(/<[^>]*>?/g, "");
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
if (options?.full) {
|
|
33
|
+
return content;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// only grab the first 500 characters, and add ... if there are more
|
|
32
37
|
const truncated = content.slice(0, 500);
|
|
33
38
|
if (content.length > 500) {
|
|
34
39
|
return `${truncated}...`;
|
package/src/lib/types.ts
CHANGED
|
@@ -40,6 +40,7 @@ export type AppConfig = {
|
|
|
40
40
|
|
|
41
41
|
export type ProjectsConfig = {
|
|
42
42
|
projects: Record<string, Project>;
|
|
43
|
+
updatedAt?: string;
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
export type Config = {
|
|
@@ -49,6 +50,7 @@ export type Config = {
|
|
|
49
50
|
ignoredPaths?: string[];
|
|
50
51
|
slugParentFolders?: string[];
|
|
51
52
|
projects: Record<string, Project>;
|
|
53
|
+
updatedAt?: string;
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
export type ProjectCandidate = {
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { GroupedHelp } from "./lib/help.js";
|
|
3
|
+
import { registerUpdate } from "./commands/update.js";
|
|
4
|
+
import { registerList } from "./commands/list.js";
|
|
5
|
+
import { registerSearch } from "./commands/search.js";
|
|
6
|
+
import { registerInfo } from "./commands/info.js";
|
|
7
|
+
import { registerGo } from "./commands/go.js";
|
|
8
|
+
import { registerPath } from "./commands/path.js";
|
|
9
|
+
import { registerShell } from "./commands/shell.js";
|
|
10
|
+
import { registerCompletion } from "./commands/completion.js";
|
|
11
|
+
import { registerIgnore } from "./commands/ignore.js";
|
|
12
|
+
|
|
13
|
+
const ASCII_HEADER = `
|
|
14
|
+
░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
15
|
+
░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
16
|
+
░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
17
|
+
░░░░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
18
|
+
░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
19
|
+
░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
20
|
+
░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
|
|
21
|
+
░░░░░░░░░░░░░░░░ ░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
22
|
+
░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
23
|
+
░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
24
|
+
░░░░░░░░░ ░░░░░░ ░░░░░░
|
|
25
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
26
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
27
|
+
░░░░░ ░░░░░░ ░░░░░░
|
|
28
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
29
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const program = new Command();
|
|
33
|
+
|
|
34
|
+
program.createHelp = function createHelp(this: Command) {
|
|
35
|
+
return Object.assign(new GroupedHelp(), this.configureHelp());
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.name("bet")
|
|
40
|
+
.description("Explore and jump between local projects.")
|
|
41
|
+
.version("0.2.0");
|
|
42
|
+
|
|
43
|
+
registerUpdate(program);
|
|
44
|
+
registerList(program);
|
|
45
|
+
registerSearch(program);
|
|
46
|
+
registerInfo(program);
|
|
47
|
+
registerGo(program);
|
|
48
|
+
registerPath(program);
|
|
49
|
+
registerShell(program);
|
|
50
|
+
registerCompletion(program);
|
|
51
|
+
registerIgnore(program);
|
|
52
|
+
|
|
53
|
+
program.addHelpText("before", ASCII_HEADER);
|
|
54
|
+
|
|
55
|
+
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import { marked, type MarkedExtension } from "marked";
|
|
3
|
+
import { markedTerminal, type TerminalRendererOptions } from "marked-terminal";
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
|
+
export type Props = TerminalRendererOptions & {
|
|
7
|
+
children: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function Markdown({ children, ...options }: Props) {
|
|
11
|
+
marked.use(markedTerminal(options) as MarkedExtension);
|
|
12
|
+
const parsedMarkdown = marked.parse(children, { async: false }) as string;
|
|
13
|
+
return React.createElement(Text, null, parsedMarkdown.trim());
|
|
14
|
+
}
|
package/src/ui/search.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useMemo, useState } from 'react';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { Box, Text, useInput } from 'ink';
|
|
4
3
|
import { SelectEntry } from './select.js';
|
|
5
4
|
|
|
@@ -67,10 +66,22 @@ export function SearchSelect<T>({
|
|
|
67
66
|
|
|
68
67
|
if (items.length === 0) {
|
|
69
68
|
return (
|
|
70
|
-
<Box flexDirection="column">
|
|
71
|
-
{title
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
70
|
+
{title ? (
|
|
71
|
+
<Box marginBottom={1}>
|
|
72
|
+
<Text bold color="cyan">
|
|
73
|
+
{title}
|
|
74
|
+
</Text>
|
|
75
|
+
</Box>
|
|
76
|
+
) : null}
|
|
77
|
+
<Box marginBottom={1} flexDirection="row">
|
|
78
|
+
<Text bold color="yellow">Search: </Text>
|
|
79
|
+
<Text color="cyan">{query || '…'}</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
<Text color="yellow">No results.</Text>
|
|
82
|
+
<Box marginTop={1}>
|
|
83
|
+
<Text color="gray">Press Esc to exit.</Text>
|
|
84
|
+
</Box>
|
|
74
85
|
</Box>
|
|
75
86
|
);
|
|
76
87
|
}
|
|
@@ -86,25 +97,45 @@ export function SearchSelect<T>({
|
|
|
86
97
|
const windowed = items.slice(windowStart, windowEnd);
|
|
87
98
|
|
|
88
99
|
return (
|
|
89
|
-
<Box flexDirection="column">
|
|
90
|
-
{title
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
101
|
+
{title ? (
|
|
102
|
+
<Box marginBottom={1}>
|
|
103
|
+
<Text bold color="cyan">
|
|
104
|
+
{title}
|
|
105
|
+
</Text>
|
|
106
|
+
</Box>
|
|
107
|
+
) : null}
|
|
108
|
+
<Box marginBottom={1} flexDirection="row">
|
|
109
|
+
<Text bold color="yellow">Search: </Text>
|
|
110
|
+
<Text color="green">{query || '…'}</Text>
|
|
111
|
+
{showCount ? (
|
|
112
|
+
<Text color="cyan"> · {items.length} result{items.length !== 1 ? 's' : ''}</Text>
|
|
113
|
+
) : null}
|
|
114
|
+
</Box>
|
|
115
|
+
<Box flexDirection="column">
|
|
116
|
+
{windowed.map((row, idx) => {
|
|
117
|
+
const absoluteIndex = windowStart + idx;
|
|
118
|
+
const selected = absoluteIndex === selectedRowIndex;
|
|
119
|
+
return (
|
|
120
|
+
<Box key={`item-${absoluteIndex}`} flexDirection="row">
|
|
121
|
+
<Text color={selected ? 'green' : undefined} bold={selected}>
|
|
122
|
+
{selected ? '› ' : ' '}
|
|
123
|
+
{row.label}
|
|
124
|
+
</Text>
|
|
125
|
+
{row.hint ? <Text color="gray"> {row.hint}</Text> : null}
|
|
126
|
+
</Box>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
</Box>
|
|
130
|
+
<Box marginTop={1} flexDirection="row">
|
|
131
|
+
<Text color="yellow">Type to filter</Text>
|
|
132
|
+
<Text color="gray"> · </Text>
|
|
133
|
+
<Text color="yellow">↑/↓ or j/k</Text>
|
|
134
|
+
<Text color="gray"> · </Text>
|
|
135
|
+
<Text color="green">Enter</Text>
|
|
136
|
+
<Text color="gray"> to select · </Text>
|
|
137
|
+
<Text color="red">Esc</Text>
|
|
138
|
+
<Text color="gray"> to cancel</Text>
|
|
108
139
|
</Box>
|
|
109
140
|
</Box>
|
|
110
141
|
);
|
package/src/ui/select.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useMemo, useState } from 'react';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { Box, Text, useInput } from 'ink';
|
|
4
3
|
|
|
5
4
|
export type SelectGroup = {
|
|
@@ -65,11 +64,17 @@ export function SelectList<T>({
|
|
|
65
64
|
|
|
66
65
|
if (items.length === 0) {
|
|
67
66
|
return (
|
|
68
|
-
<Box flexDirection="column">
|
|
69
|
-
{title
|
|
70
|
-
|
|
67
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
68
|
+
{title ? (
|
|
69
|
+
<Box marginBottom={1}>
|
|
70
|
+
<Text bold color="cyan">
|
|
71
|
+
{title}
|
|
72
|
+
</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
) : null}
|
|
75
|
+
<Text color="yellow">No results.</Text>
|
|
71
76
|
<Box marginTop={1}>
|
|
72
|
-
|
|
77
|
+
<Text color="gray">Press Esc to exit.</Text>
|
|
73
78
|
</Box>
|
|
74
79
|
</Box>
|
|
75
80
|
);
|
|
@@ -86,33 +91,49 @@ export function SelectList<T>({
|
|
|
86
91
|
const windowed = items.slice(windowStart, windowEnd);
|
|
87
92
|
|
|
88
93
|
return (
|
|
89
|
-
<Box flexDirection="column">
|
|
90
|
-
{title
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
<Box flexDirection="column" borderStyle="round" borderColor="magenta" padding={1}>
|
|
95
|
+
{title ? (
|
|
96
|
+
<Box marginBottom={1}>
|
|
97
|
+
<Text bold color="cyan">
|
|
98
|
+
{title}
|
|
99
|
+
</Text>
|
|
100
|
+
</Box>
|
|
101
|
+
) : null}
|
|
102
|
+
<Box flexDirection="column">
|
|
103
|
+
{windowed.map((row, idx) => {
|
|
104
|
+
const absoluteIndex = windowStart + idx;
|
|
105
|
+
const selected = row.type === 'item' && absoluteIndex === selectedRowIndex;
|
|
106
|
+
|
|
107
|
+
if (row.type === 'group') {
|
|
108
|
+
return (
|
|
109
|
+
<Box key={`group-${absoluteIndex}`} marginTop={idx === 0 ? 0 : 1}>
|
|
110
|
+
<Text bold color={row.color ?? 'cyan'}>
|
|
111
|
+
[{row.label}]
|
|
112
|
+
</Text>
|
|
113
|
+
</Box>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
94
116
|
|
|
95
|
-
if (row.type === 'group') {
|
|
96
|
-
const colored = row.color ? chalk.hex(row.color)(`[${row.label}]`) : `[${row.label}]`;
|
|
97
117
|
return (
|
|
98
|
-
<Box key={`
|
|
99
|
-
<Text
|
|
118
|
+
<Box key={`item-${absoluteIndex}`} flexDirection="row">
|
|
119
|
+
<Text color={selected ? 'green' : undefined} bold={selected}>
|
|
120
|
+
{selected ? '› ' : ' '}
|
|
121
|
+
{row.label}
|
|
122
|
+
</Text>
|
|
123
|
+
{row.hint ? (
|
|
124
|
+
<Text color="gray"> {row.hint}</Text>
|
|
125
|
+
) : null}
|
|
100
126
|
</Box>
|
|
101
127
|
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<Box key={`item-${absoluteIndex}`}>
|
|
106
|
-
<Text>
|
|
107
|
-
{selected ? chalk.cyan.bold('› ') : ' '}
|
|
108
|
-
{selected ? chalk.cyan.bold(row.label) : row.label}
|
|
109
|
-
</Text>
|
|
110
|
-
{row.hint ? <Text>{chalk.dim(` ${row.hint}`)}</Text> : null}
|
|
111
|
-
</Box>
|
|
112
|
-
);
|
|
113
|
-
})}
|
|
128
|
+
})}
|
|
129
|
+
</Box>
|
|
114
130
|
<Box marginTop={1}>
|
|
115
|
-
<Text
|
|
131
|
+
<Text color="yellow">↑/↓ or j/k</Text>
|
|
132
|
+
<Text color="gray"> · </Text>
|
|
133
|
+
<Text color="green">Enter</Text>
|
|
134
|
+
<Text color="gray"> to select · </Text>
|
|
135
|
+
<Text color="red">Esc</Text>
|
|
136
|
+
<Text color="gray"> to cancel</Text>
|
|
116
137
|
</Box>
|
|
117
138
|
</Box>
|
|
118
139
|
);
|