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 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**
@@ -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
  }
@@ -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 Table from "../ui/table.js";
11
- const data = [];
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 = await readReadmeContent(project.path);
58
+ const readme = options.full
59
+ ? await readReadmeContent(project.path, { full: true })
60
+ : null;
58
61
  const markdown = readme ?? description;
59
- let Markdown = null;
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
- process.stdout.write(`${chalk.bold("Description:")} ${description}\n`);
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`);
@@ -1,11 +1,18 @@
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';
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 = ['#22d3ee', '#34d399', '#facc15', '#c084fc', '#60a5fa', '#f87171'];
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('list')
48
- .description('List projects')
49
- .option('--plain', 'Print a non-interactive list')
50
- .option('--json', 'Print JSON output')
51
- .option('--print', 'Print selected path only')
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('\n');
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('No projects indexed. Run bet update.\n');
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({ type: 'group', label: rootName, color: groupColor(groupIndex++) });
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: 'item',
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, { title: 'Projects', maxRows: MAX_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 });
@@ -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
- import { Command } from "commander";
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 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 {};
@@ -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),
@@ -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
+ }
@@ -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;
@@ -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
- // only graph the first 300 characters, and add ... if there are more
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);
@@ -1,10 +1,9 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Text } from 'ink';
3
- import { marked } from 'marked';
4
- import TerminalRenderer from 'marked-terminal';
5
- const renderer = new TerminalRenderer();
6
- export function Markdown({ content }) {
7
- marked.setOptions({ renderer });
8
- const output = marked.parse(content).trim();
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 && _jsx(Text, { children: chalk.bold(title) }), _jsx(Text, { children: `Search: ${query}` }), _jsx(Text, { children: "No results." })] }));
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 && _jsx(Text, { children: chalk.bold(title) }), _jsx(Text, { children: `Search: ${query}` }), showCount && _jsx(Text, { children: chalk.dim(`${items.length} result(s)`) }), windowed.map((row, idx) => {
49
- const absoluteIndex = windowStart + idx;
50
- const selected = absoluteIndex === selectedRowIndex;
51
- return (_jsxs(Box, { children: [_jsxs(Text, { children: [selected ? chalk.cyan.bold('› ') : ' ', selected ? chalk.cyan.bold(row.label) : row.label] }), row.hint ? _jsx(Text, { children: chalk.dim(` ${row.hint}`) }) : null] }, `item-${absoluteIndex}`));
52
- }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: chalk.dim('Type to filter. Use ↑/↓ or j/k. Enter to select. Esc to cancel.') }) })] }));
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 && _jsx(Text, { children: chalk.bold(title) }), _jsx(Text, { children: "No results." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: chalk.dim('Press Esc to exit.') }) })] }));
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 && _jsx(Text, { children: chalk.bold(title) }), windowed.map((row, idx) => {
43
- const absoluteIndex = windowStart + idx;
44
- const selected = row.type === 'item' && absoluteIndex === selectedRowIndex;
45
- if (row.type === 'group') {
46
- const colored = row.color ? chalk.hex(row.color)(`[${row.label}]`) : `[${row.label}]`;
47
- return (_jsx(Box, { marginTop: idx === 0 ? 0 : 1, children: _jsx(Text, { children: chalk.bold(colored) }) }, `group-${absoluteIndex}`));
48
- }
49
- return (_jsxs(Box, { children: [_jsxs(Text, { children: [selected ? chalk.cyan.bold('› ') : ' ', selected ? chalk.cyan.bold(row.label) : row.label] }), row.hint ? _jsx(Text, { children: chalk.dim(` ${row.hint}`) }) : null] }, `item-${absoluteIndex}`));
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.1.3",
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
  },
@@ -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;