bet-cli 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/config.ts CHANGED
@@ -68,11 +68,25 @@ function normalizeIgnoredPaths(parsed: unknown): string[] | undefined {
68
68
  return list.length === 0 ? undefined : list;
69
69
  }
70
70
 
71
+ function normalizeEditor(parsed: unknown): string | undefined {
72
+ if (typeof parsed !== "string") return undefined;
73
+ const trimmed = parsed.trim();
74
+ return trimmed.length === 0 ? undefined : trimmed;
75
+ }
76
+
71
77
  async function readAppConfig(): Promise<AppConfig> {
72
78
  try {
73
79
  const raw = await fs.readFile(CONFIG_PATH, "utf8");
74
- const parsed = JSON.parse(raw) as { version?: number; roots?: unknown; ignores?: unknown; ignoredPaths?: unknown; slugParentFolders?: unknown };
80
+ const parsed = JSON.parse(raw) as {
81
+ version?: number;
82
+ roots?: unknown;
83
+ editor?: unknown;
84
+ ignores?: unknown;
85
+ ignoredPaths?: unknown;
86
+ slugParentFolders?: unknown;
87
+ };
75
88
  const roots = normalizeRoots(parsed.roots ?? []);
89
+ const editor = normalizeEditor(parsed.editor);
76
90
  const ignores = normalizeIgnores(parsed.ignores);
77
91
  const ignoredPaths = normalizeIgnoredPaths(parsed.ignoredPaths);
78
92
  const slugParentFolders = normalizeSlugParentFolders(parsed.slugParentFolders);
@@ -80,6 +94,7 @@ async function readAppConfig(): Promise<AppConfig> {
80
94
  ...DEFAULT_APP_CONFIG,
81
95
  version: parsed.version ?? 1,
82
96
  roots,
97
+ ...(editor !== undefined && { editor }),
83
98
  ...(ignores !== undefined && { ignores }),
84
99
  ...(ignoredPaths !== undefined && { ignoredPaths }),
85
100
  ...(slugParentFolders !== undefined && { slugParentFolders }),
@@ -95,6 +110,7 @@ async function readProjectsConfig(): Promise<ProjectsConfig> {
95
110
  const parsed = JSON.parse(raw) as ProjectsConfig;
96
111
  return {
97
112
  projects: parsed.projects ?? {},
113
+ ...(parsed.updatedAt != null && typeof parsed.updatedAt === "string" && { updatedAt: parsed.updatedAt }),
98
114
  };
99
115
  } catch (error) {
100
116
  return { ...DEFAULT_PROJECTS_CONFIG };
@@ -123,6 +139,7 @@ export async function readConfig(): Promise<Config> {
123
139
  return {
124
140
  ...appConfig,
125
141
  projects: normalizedProjects,
142
+ ...(projectsConfig.updatedAt != null && { updatedAt: projectsConfig.updatedAt }),
126
143
  };
127
144
  }
128
145
 
@@ -144,12 +161,14 @@ export async function writeConfig(config: Config): Promise<void> {
144
161
  const appConfig: AppConfig = {
145
162
  version: config.version,
146
163
  roots: config.roots,
164
+ ...(config.editor !== undefined && { editor: config.editor }),
147
165
  ...(config.ignores !== undefined && { ignores: config.ignores }),
148
166
  ...(config.ignoredPaths !== undefined && { ignoredPaths: config.ignoredPaths }),
149
167
  ...(config.slugParentFolders !== undefined && { slugParentFolders: config.slugParentFolders }),
150
168
  };
151
169
  const projectsConfig: ProjectsConfig = {
152
170
  projects: config.projects,
171
+ ...(config.updatedAt !== undefined && { updatedAt: config.updatedAt }),
153
172
  };
154
173
  await Promise.all([
155
174
  writeAppConfig(appConfig),
@@ -0,0 +1,131 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export type LaunchCommand = {
4
+ command: string;
5
+ args: string[];
6
+ };
7
+
8
+ function tokenizeCommand(input: string): string[] {
9
+ const tokens: string[] = [];
10
+ let current = "";
11
+ let inSingle = false;
12
+ let inDouble = false;
13
+ let escaped = false;
14
+
15
+ for (const char of input) {
16
+ if (escaped) {
17
+ current += char;
18
+ escaped = false;
19
+ continue;
20
+ }
21
+
22
+ if (char === "\\") {
23
+ escaped = true;
24
+ continue;
25
+ }
26
+
27
+ if (char === "'" && !inDouble) {
28
+ inSingle = !inSingle;
29
+ continue;
30
+ }
31
+
32
+ if (char === '"' && !inSingle) {
33
+ inDouble = !inDouble;
34
+ continue;
35
+ }
36
+
37
+ if (!inSingle && !inDouble && /\s/.test(char)) {
38
+ if (current.length > 0) {
39
+ tokens.push(current);
40
+ current = "";
41
+ }
42
+ continue;
43
+ }
44
+
45
+ current += char;
46
+ }
47
+
48
+ if (escaped || inSingle || inDouble) {
49
+ throw new Error("Invalid editor command in config.");
50
+ }
51
+
52
+ if (current.length > 0) {
53
+ tokens.push(current);
54
+ }
55
+
56
+ return tokens;
57
+ }
58
+
59
+ export function parseEditorCommand(editor: string): LaunchCommand {
60
+ const trimmed = editor.trim();
61
+ if (!trimmed) {
62
+ throw new Error("Config editor must not be empty.");
63
+ }
64
+
65
+ const tokens = tokenizeCommand(trimmed);
66
+ if (tokens.length === 0) {
67
+ throw new Error("Config editor must not be empty.");
68
+ }
69
+
70
+ const [command, ...args] = tokens;
71
+ return { command, args };
72
+ }
73
+
74
+ export function getSystemOpenCommand(
75
+ targetPath: string,
76
+ platform: NodeJS.Platform = process.platform,
77
+ ): LaunchCommand {
78
+ if (platform === "darwin") {
79
+ return { command: "open", args: [targetPath] };
80
+ }
81
+
82
+ if (platform === "win32") {
83
+ return { command: "cmd", args: ["/c", "start", "", targetPath] };
84
+ }
85
+
86
+ return { command: "xdg-open", args: [targetPath] };
87
+ }
88
+
89
+ function getEnvEditor(env: NodeJS.ProcessEnv): string | undefined {
90
+ const visual = env.VISUAL?.trim();
91
+ if (visual) return visual;
92
+
93
+ const editor = env.EDITOR?.trim();
94
+ if (editor) return editor;
95
+
96
+ return undefined;
97
+ }
98
+
99
+ function spawnDetached(command: string, args: string[]): Promise<void> {
100
+ return new Promise((resolve, reject) => {
101
+ const child = spawn(command, args, {
102
+ detached: true,
103
+ stdio: "ignore",
104
+ });
105
+
106
+ child.once("error", (error) => {
107
+ reject(error);
108
+ });
109
+
110
+ child.once("spawn", () => {
111
+ child.unref();
112
+ resolve();
113
+ });
114
+ });
115
+ }
116
+
117
+ export async function openProjectInEditor(
118
+ projectPath: string,
119
+ configuredEditor?: string,
120
+ env: NodeJS.ProcessEnv = process.env,
121
+ ): Promise<void> {
122
+ const preferredEditor = configuredEditor?.trim() || getEnvEditor(env);
123
+ if (preferredEditor) {
124
+ const parsed = parseEditorCommand(preferredEditor);
125
+ await spawnDetached(parsed.command, [...parsed.args, projectPath]);
126
+ return;
127
+ }
128
+
129
+ const fallback = getSystemOpenCommand(projectPath);
130
+ await spawnDetached(fallback.command, fallback.args);
131
+ }
package/src/lib/git.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
3
 
4
4
  const execFileAsync = promisify(execFile);
5
5
 
6
6
  async function runGit(cwd: string, args: string[]): Promise<string | null> {
7
7
  try {
8
- const { stdout } = await execFileAsync('git', ['-C', cwd, ...args], {
9
- encoding: 'utf8',
8
+ const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
9
+ encoding: "utf8",
10
10
  });
11
11
  return stdout.trim();
12
12
  } catch {
@@ -14,18 +14,28 @@ async function runGit(cwd: string, args: string[]): Promise<string | null> {
14
14
  }
15
15
  }
16
16
 
17
- export async function getFirstCommitDate(cwd: string): Promise<string | undefined> {
18
- const output = await runGit(cwd, ['log', '--reverse', '--format=%cI', '-n', '1']);
17
+ export async function getFirstCommitDate(
18
+ cwd: string,
19
+ ): Promise<string | undefined> {
20
+ const output = await runGit(cwd, [
21
+ "log",
22
+ "--max-parents=0",
23
+ "--format=%cd",
24
+ "--date=iso-strict",
25
+ "HEAD",
26
+ ]);
19
27
  return output || undefined;
20
28
  }
21
29
 
22
- export async function getDirtyStatus(cwd: string): Promise<boolean | undefined> {
23
- const output = await runGit(cwd, ['status', '--porcelain']);
30
+ export async function getDirtyStatus(
31
+ cwd: string,
32
+ ): Promise<boolean | undefined> {
33
+ const output = await runGit(cwd, ["status", "--porcelain"]);
24
34
  if (output === null) return undefined;
25
35
  return output.length > 0;
26
36
  }
27
37
 
28
38
  export async function isInsideGitRepo(cwd: string): Promise<boolean> {
29
- const output = await runGit(cwd, ['rev-parse', '--is-inside-work-tree']);
30
- return output === 'true';
39
+ const output = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
40
+ return output === "true";
31
41
  }
package/src/lib/help.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Help } from "commander";
2
2
  import type { Command } from "commander";
3
3
 
4
- const GROUP_1: string[] = ["list", "search", "info", "go", "path"];
4
+ const GROUP_1: string[] = ["list", "search", "info", "go", "edit", "path"];
5
5
  const GROUP_2: string[] = ["shell", "completion"];
6
6
  const GROUP_3: string[] = ["update", "ignore", "help"];
7
7
 
@@ -55,23 +55,24 @@ export class GroupedHelp extends Help {
55
55
  }
56
56
 
57
57
  // Arguments
58
- const argumentList = helper.visibleArguments(cmd).map((argument) =>
59
- formatItem(
60
- helper.argumentTerm(argument),
61
- helper.argumentDescription(argument),
62
- ),
63
- );
58
+ const argumentList = helper
59
+ .visibleArguments(cmd)
60
+ .map((argument) =>
61
+ formatItem(
62
+ helper.argumentTerm(argument),
63
+ helper.argumentDescription(argument),
64
+ ),
65
+ );
64
66
  if (argumentList.length > 0) {
65
67
  output.push("Arguments:", formatList(argumentList), "");
66
68
  }
67
69
 
68
70
  // Options
69
- const optionList = helper.visibleOptions(cmd).map((option) =>
70
- formatItem(
71
- helper.optionTerm(option),
72
- helper.optionDescription(option),
73
- ),
74
- );
71
+ const optionList = helper
72
+ .visibleOptions(cmd)
73
+ .map((option) =>
74
+ formatItem(helper.optionTerm(option), helper.optionDescription(option)),
75
+ );
75
76
  if (optionList.length > 0) {
76
77
  output.push("Options:", formatList(optionList), "");
77
78
  }
@@ -105,8 +106,7 @@ export class GroupedHelp extends Help {
105
106
  const groupIndices = [...byGroup.keys()].sort((a, b) => a - b);
106
107
  for (const idx of groupIndices) {
107
108
  const commands = byGroup.get(idx)!;
108
- const heading =
109
- idx < GROUPS.length ? GROUPS[idx].heading : "Commands";
109
+ const heading = idx < GROUPS.length ? GROUPS[idx].heading : "Commands";
110
110
  const commandList = commands.map((sub) =>
111
111
  formatItem(
112
112
  helper.subcommandTerm(sub),
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
@@ -33,6 +33,7 @@ export type RootConfig = {
33
33
  export type AppConfig = {
34
34
  version: number;
35
35
  roots: RootConfig[];
36
+ editor?: string;
36
37
  ignores?: string[];
37
38
  ignoredPaths?: string[];
38
39
  slugParentFolders?: string[];
@@ -40,15 +41,18 @@ export type AppConfig = {
40
41
 
41
42
  export type ProjectsConfig = {
42
43
  projects: Record<string, Project>;
44
+ updatedAt?: string;
43
45
  };
44
46
 
45
47
  export type Config = {
46
48
  version: number;
47
49
  roots: RootConfig[];
50
+ editor?: string;
48
51
  ignores?: string[];
49
52
  ignoredPaths?: string[];
50
53
  slugParentFolders?: string[];
51
54
  projects: Record<string, Project>;
55
+ updatedAt?: string;
52
56
  };
53
57
 
54
58
  export type ProjectCandidate = {
package/src/main.ts ADDED
@@ -0,0 +1,57 @@
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
+ import { registerEdit } from "./commands/edit.js";
13
+
14
+ const ASCII_HEADER = `
15
+ ░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
16
+ ░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
17
+ ░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
18
+ ░░░░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
19
+ ░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
20
+ ░░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
21
+ ░░░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░░░
22
+ ░░░░░░░░░░░░░░░░ ░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
23
+ ░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
24
+ ░░░░░░░░░░░░░░ ░░░░░░ ░░░░░░
25
+ ░░░░░░░░░ ░░░░░░ ░░░░░░
26
+ ░░░░░ ░░░░░░ ░░░░░░
27
+ ░░░░░ ░░░░░░ ░░░░░░
28
+ ░░░░░ ░░░░░░ ░░░░░░
29
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
30
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░
31
+ `;
32
+
33
+ const program = new Command();
34
+
35
+ program.createHelp = function createHelp(this: Command) {
36
+ return Object.assign(new GroupedHelp(), this.configureHelp());
37
+ };
38
+
39
+ program
40
+ .name("bet")
41
+ .description("Explore and jump between local projects.")
42
+ .version("0.3.0");
43
+
44
+ registerUpdate(program);
45
+ registerList(program);
46
+ registerSearch(program);
47
+ registerInfo(program);
48
+ registerGo(program);
49
+ registerEdit(program);
50
+ registerPath(program);
51
+ registerShell(program);
52
+ registerCompletion(program);
53
+ registerIgnore(program);
54
+
55
+ program.addHelpText("before", ASCII_HEADER);
56
+
57
+ 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
  );