bet-cli 0.1.3 → 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 -25
- package/dist/lib/config.js +3 -0
- package/dist/lib/help.js +81 -0
- package/dist/lib/ignore.js +36 -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 -29
- package/src/lib/config.ts +3 -0
- package/src/lib/help.ts +122 -0
- package/src/lib/ignore.ts +40 -1
- 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,26 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
import { registerCompletion } from "./commands/completion.js";
|
|
11
|
-
import { registerIgnore } from "./commands/ignore.js";
|
|
12
|
-
const program = new Command();
|
|
13
|
-
program
|
|
14
|
-
.name("bet")
|
|
15
|
-
.description("Explore and jump between local projects.")
|
|
16
|
-
.version("0.1.2");
|
|
17
|
-
registerUpdate(program);
|
|
18
|
-
registerList(program);
|
|
19
|
-
registerSearch(program);
|
|
20
|
-
registerInfo(program);
|
|
21
|
-
registerGo(program);
|
|
22
|
-
registerPath(program);
|
|
23
|
-
registerShell(program);
|
|
24
|
-
registerCompletion(program);
|
|
25
|
-
registerIgnore(program);
|
|
26
|
-
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/help.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Help } from "commander";
|
|
2
|
+
const GROUP_1 = ["list", "search", "info", "go", "path"];
|
|
3
|
+
const GROUP_2 = ["shell", "completion"];
|
|
4
|
+
const GROUP_3 = ["update", "ignore", "help"];
|
|
5
|
+
const GROUPS = [
|
|
6
|
+
{ heading: "Projects", names: GROUP_1 },
|
|
7
|
+
{ heading: "Shell integration", names: GROUP_2 },
|
|
8
|
+
{ heading: "Index & config", names: GROUP_3 },
|
|
9
|
+
];
|
|
10
|
+
function getGroupIndex(cmdName) {
|
|
11
|
+
for (let i = 0; i < GROUPS.length; i++) {
|
|
12
|
+
if (GROUPS[i].names.includes(cmdName))
|
|
13
|
+
return i;
|
|
14
|
+
}
|
|
15
|
+
return GROUPS.length; // unlisted commands go last
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Custom Help that renders top-level commands in three groups with headings.
|
|
19
|
+
*/
|
|
20
|
+
export class GroupedHelp extends Help {
|
|
21
|
+
formatHelp(cmd, helper) {
|
|
22
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
23
|
+
const helpWidth = helper.helpWidth ?? 80;
|
|
24
|
+
const itemIndentWidth = 2;
|
|
25
|
+
const itemSeparatorWidth = 2;
|
|
26
|
+
const formatItem = (term, description) => {
|
|
27
|
+
if (description) {
|
|
28
|
+
const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
|
|
29
|
+
return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth);
|
|
30
|
+
}
|
|
31
|
+
return term;
|
|
32
|
+
};
|
|
33
|
+
const formatList = (textArray) => textArray.join("\n").replace(/^/gm, " ".repeat(itemIndentWidth));
|
|
34
|
+
const output = [];
|
|
35
|
+
// Usage
|
|
36
|
+
output.push(`Usage: ${helper.commandUsage(cmd)}`, "");
|
|
37
|
+
// Description
|
|
38
|
+
const commandDescription = helper.commandDescription(cmd);
|
|
39
|
+
if (commandDescription.length > 0) {
|
|
40
|
+
output.push(helper.wrap(commandDescription, helpWidth, 0), "");
|
|
41
|
+
}
|
|
42
|
+
// Arguments
|
|
43
|
+
const argumentList = helper.visibleArguments(cmd).map((argument) => formatItem(helper.argumentTerm(argument), helper.argumentDescription(argument)));
|
|
44
|
+
if (argumentList.length > 0) {
|
|
45
|
+
output.push("Arguments:", formatList(argumentList), "");
|
|
46
|
+
}
|
|
47
|
+
// Options
|
|
48
|
+
const optionList = helper.visibleOptions(cmd).map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option)));
|
|
49
|
+
if (optionList.length > 0) {
|
|
50
|
+
output.push("Options:", formatList(optionList), "");
|
|
51
|
+
}
|
|
52
|
+
if (this.showGlobalOptions) {
|
|
53
|
+
const globalOptionList = helper
|
|
54
|
+
.visibleGlobalOptions(cmd)
|
|
55
|
+
.map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option)));
|
|
56
|
+
if (globalOptionList.length > 0) {
|
|
57
|
+
output.push("Global Options:", formatList(globalOptionList), "");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Commands (grouped)
|
|
61
|
+
const visibleCommands = helper.visibleCommands(cmd);
|
|
62
|
+
if (visibleCommands.length > 0) {
|
|
63
|
+
const byGroup = new Map();
|
|
64
|
+
for (const sub of visibleCommands) {
|
|
65
|
+
const name = sub.name();
|
|
66
|
+
const idx = getGroupIndex(name);
|
|
67
|
+
const list = byGroup.get(idx) ?? [];
|
|
68
|
+
list.push(sub);
|
|
69
|
+
byGroup.set(idx, list);
|
|
70
|
+
}
|
|
71
|
+
const groupIndices = [...byGroup.keys()].sort((a, b) => a - b);
|
|
72
|
+
for (const idx of groupIndices) {
|
|
73
|
+
const commands = byGroup.get(idx);
|
|
74
|
+
const heading = idx < GROUPS.length ? GROUPS[idx].heading : "Commands";
|
|
75
|
+
const commandList = commands.map((sub) => formatItem(helper.subcommandTerm(sub), helper.subcommandDescription(sub)));
|
|
76
|
+
output.push(`${heading}:`, formatList(commandList), "");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return output.join("\n");
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/lib/ignore.js
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
import { isSubpath } from "../utils/paths.js";
|
|
2
2
|
export const DEFAULT_IGNORES = [
|
|
3
|
+
// JS/TS/Node
|
|
3
4
|
"**/node_modules/**",
|
|
4
5
|
"**/.git/**",
|
|
5
6
|
"**/dist/**",
|
|
6
7
|
"**/build/**",
|
|
7
8
|
"**/.next/**",
|
|
9
|
+
// Rust, Scala, Java (Maven)
|
|
8
10
|
"**/target/**",
|
|
11
|
+
// PHP, Ruby, Go
|
|
9
12
|
"**/vendor/**",
|
|
13
|
+
// Python
|
|
10
14
|
"**/.venv/**",
|
|
11
15
|
"**/venv/**",
|
|
16
|
+
"**/__pycache__/**",
|
|
17
|
+
"**/.mypy_cache/**",
|
|
18
|
+
"**/.pytest_cache/**",
|
|
19
|
+
"**/.eggs/**",
|
|
20
|
+
"**/*.egg-info/**",
|
|
21
|
+
"**/*.egg",
|
|
22
|
+
// Ruby
|
|
23
|
+
"**/.bundle/**",
|
|
24
|
+
"**/vendor/bundle/**",
|
|
25
|
+
// PHP (Symfony, Laravel cache/log)
|
|
26
|
+
"**/var/cache/**",
|
|
27
|
+
"**/var/log/**",
|
|
28
|
+
// Java/Kotlin (Gradle, IntelliJ output)
|
|
29
|
+
"**/.gradle/**",
|
|
30
|
+
"**/out/**",
|
|
31
|
+
// Elixir
|
|
32
|
+
"**/deps/**",
|
|
33
|
+
"**/_build/**",
|
|
34
|
+
// Swift
|
|
35
|
+
"**/.build/**",
|
|
36
|
+
"**/DerivedData/**",
|
|
37
|
+
// Dart/Flutter
|
|
38
|
+
"**/.dart_tool/**",
|
|
39
|
+
"**/.packages",
|
|
40
|
+
// C# / .NET
|
|
41
|
+
"**/obj/**",
|
|
42
|
+
// Haskell
|
|
43
|
+
"**/.stack-work/**",
|
|
44
|
+
"**/.cabal-sandbox/**",
|
|
45
|
+
// Scala (Metals)
|
|
46
|
+
"**/.metals/**",
|
|
47
|
+
"**/.bloop/**",
|
|
12
48
|
];
|
|
13
49
|
export function getEffectiveIgnores(config) {
|
|
14
50
|
return config.ignores !== undefined ? config.ignores : DEFAULT_IGNORES;
|
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;
|