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 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,30 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { GroupedHelp } from "./lib/help.js";
4
- import { registerUpdate } from "./commands/update.js";
5
- import { registerList } from "./commands/list.js";
6
- import { registerSearch } from "./commands/search.js";
7
- import { registerInfo } from "./commands/info.js";
8
- import { registerGo } from "./commands/go.js";
9
- import { registerPath } from "./commands/path.js";
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 {};
@@ -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),
@@ -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.4",
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;
@@ -1,23 +1,45 @@
1
1
  import chalk from "chalk";
2
2
  import { Command } from "commander";
3
- import { readConfig } from "../lib/config.js";
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 Table from "../ui/table.js";
12
-
13
- const data: { [key: string]: string }[] = [];
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
- .action(async (slug: string, options: { json?: boolean }) => {
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 = await readReadmeContent(project.path);
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
- <Table data={data} />
87
- <Text color="green" bold>
88
- {project.slug}
89
- </Text>
90
- <Text dimColor>{project.path}</Text>
91
- <Box marginTop={1} flexDirection="column">
92
- <Text bold>{`Root: ${project.rootName}`}</Text>
93
- <Text bold>{`Root path: ${project.root}`}</Text>
94
- <Text bold>{`Git: ${hasGit ? "yes" : "no"}`}</Text>
95
- <Text bold>{`README: ${project.hasReadme ? "yes" : "no"}`}</Text>
96
- <Text
97
- bold
98
- >{`Started: ${formatDate(project.auto.startedAt)}`}</Text>
99
- <Text
100
- bold
101
- >{`Last modified: ${formatDate(project.auto.lastModifiedAt)}`}</Text>
102
- <Text
103
- bold
104
- >{`Last indexed: ${formatDate(project.auto.lastIndexedAt)}`}</Text>
105
- <Text
106
- bold
107
- >{`Dirty: ${dirty === undefined ? "unknown" : dirty ? "yes" : "no"}`}</Text>
108
- {project.user?.tags?.length ? (
109
- <Text>{`Tags: ${project.user.tags.join(", ")}`}</Text>
110
- ) : null}
111
- {project.user?.onEnter ? (
112
- <Text>{`On enter: ${project.user.onEnter}`}</Text>
113
- ) : null}
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 marginTop={1} flexDirection="column">
116
- <Text>{chalk.bold("Description")}</Text>
117
- {Markdown ? (
118
- <Markdown>{markdown}</Markdown>
119
- ) : (
120
- <Text>{markdown}</Text>
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
- process.stdout.write(`${chalk.bold("Description:")} ${description}\n`);
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
  );
@@ -1,15 +1,22 @@
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';
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 = ['#22d3ee', '#34d399', '#facc15', '#c084fc', '#60a5fa', '#f87171'];
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<string, { rootName: string; items: Project[] }>();
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('list')
64
- .description('List projects')
65
- .option('--plain', 'Print a non-interactive list')
66
- .option('--json', 'Print JSON output')
67
- .option('--print', 'Print selected path only')
68
- .action(async (options: { plain?: boolean; json?: boolean; print?: boolean }) => {
69
- const config = await readConfig();
70
- const projects = listProjects(config);
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
- if (options.json) {
73
- process.stdout.write(JSON.stringify(projects, null, 2));
74
- process.stdout.write('\n');
75
- return;
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
- if (!process.stdin.isTTY || options.plain) {
79
- if (projects.length === 0) {
80
- process.stdout.write('No projects indexed. Run bet update.\n');
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
- const orderedGroups = orderedGroupsByConfigRoots(projects, config.roots);
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
- const color = groupColor(groupIndex++);
87
- const label = chalk.hex(color).bold(`[${rootName}]`);
88
- process.stdout.write(`${label}\n`);
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
- process.stdout.write(` ${chalk.reset(formatLabel(project.slug, project.rootName, rel))}\n`);
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
- const selected = await promptSelect(rows, { title: 'Projects', maxRows: MAX_ROWS });
113
- if (!selected) return;
136
+ const selected = await promptSelect(rows, {
137
+ title: "Projects",
138
+ maxRows: MAX_ROWS,
139
+ });
140
+ if (!selected) return;
114
141
 
115
- emitSelection(selected.value, { printOnly: options.print });
116
- });
142
+ emitSelection(selected.value, { printOnly: options.print });
143
+ },
144
+ );
117
145
  }
@@ -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
- import { Command } from "commander";
3
- import { GroupedHelp } from "./lib/help.js";
4
- import { registerUpdate } from "./commands/update.js";
5
- import { registerList } from "./commands/list.js";
6
- import { registerSearch } from "./commands/search.js";
7
- import { registerInfo } from "./commands/info.js";
8
- import { registerGo } from "./commands/go.js";
9
- import { registerPath } from "./commands/path.js";
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
- // only graph the first 300 characters, and add ... if there are more
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 && <Text>{chalk.bold(title)}</Text>}
72
- <Text>{`Search: ${query}`}</Text>
73
- <Text>No results.</Text>
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 && <Text>{chalk.bold(title)}</Text>}
91
- <Text>{`Search: ${query}`}</Text>
92
- {showCount && <Text>{chalk.dim(`${items.length} result(s)`)}</Text>}
93
- {windowed.map((row, idx) => {
94
- const absoluteIndex = windowStart + idx;
95
- const selected = absoluteIndex === selectedRowIndex;
96
- return (
97
- <Box key={`item-${absoluteIndex}`}>
98
- <Text>
99
- {selected ? chalk.cyan.bold('') : ' '}
100
- {selected ? chalk.cyan.bold(row.label) : row.label}
101
- </Text>
102
- {row.hint ? <Text>{chalk.dim(` ${row.hint}`)}</Text> : null}
103
- </Box>
104
- );
105
- })}
106
- <Box marginTop={1}>
107
- <Text>{chalk.dim('Type to filter. Use ↑/↓ or j/k. Enter to select. Esc to cancel.')}</Text>
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 && <Text>{chalk.bold(title)}</Text>}
70
- <Text>No results.</Text>
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
- <Text>{chalk.dim('Press Esc to exit.')}</Text>
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 && <Text>{chalk.bold(title)}</Text>}
91
- {windowed.map((row, idx) => {
92
- const absoluteIndex = windowStart + idx;
93
- const selected = row.type === 'item' && absoluteIndex === selectedRowIndex;
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={`group-${absoluteIndex}`} marginTop={idx === 0 ? 0 : 1}>
99
- <Text>{chalk.bold(colored)}</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>{chalk.dim('Use ↑/↓ or j/k. Enter to select. Esc to cancel.')}</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
  );