Skip to content

Commit ecd1df9

Browse files
committed
release: restore interactive shell in v0.1.3
1 parent cff5102 commit ecd1df9

File tree

9 files changed

+898
-1
lines changed

9 files changed

+898
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codecache-cli",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "Local-first CLI for storing and retrieving code snippets.",
55
"license": "MIT",
66
"bin": {

src/cli/interactive-shell.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { createPromptSession, createShellSession } from "./interactive";
2+
import { renderSuccess } from "./output";
3+
import { getSlashCommandSuggestions, resolveShellInput } from "./shell";
4+
import { renderSessionStatus } from "./session";
5+
import {
6+
formatErrorMessage,
7+
formatHelpPanel,
8+
formatShellFrame,
9+
formatSuccessMessage,
10+
formatSuggestionsBelowInput,
11+
} from "./ui";
12+
import { resolveVaultPath } from "../app/vault";
13+
import { isVaultInitialized } from "../storage/sqlite";
14+
import { runCli } from "./program";
15+
import { CacheError } from "../shared/errors";
16+
17+
function clampActivity(activity: string[], nextLine: string) {
18+
return [...activity, nextLine].slice(-20);
19+
}
20+
21+
function renderShell(shell: { write(message: string): void }, status: Awaited<ReturnType<typeof getInteractiveSessionStatus>>, input: string, activity: string[]) {
22+
const frame = formatShellFrame(input, status, activity);
23+
process.stdout.write("\x1Bc");
24+
shell.write(`${frame.join("\n")}\n\n`);
25+
}
26+
27+
function renderPrompt(shell: { write(message: string): void }, prompt: string, input: string) {
28+
shell.write(`${prompt}${input}`);
29+
}
30+
31+
function shouldConfirmDestructiveCommand(argv: string[]) {
32+
return (
33+
(argv[0] === "snippet" && argv[1] === "delete") ||
34+
(argv[0] === "attachment" && argv[1] === "delete")
35+
);
36+
}
37+
38+
function getDestructiveCommandLabel(argv: string[]) {
39+
return argv.join(" ");
40+
}
41+
42+
async function getInteractiveSessionStatus() {
43+
const vaultPath = await resolveVaultPath();
44+
45+
if (!vaultPath) {
46+
return {
47+
ready: false,
48+
vaultPath: null,
49+
};
50+
}
51+
52+
return {
53+
ready: isVaultInitialized(vaultPath),
54+
vaultPath,
55+
};
56+
}
57+
58+
export async function runInteractiveShell() {
59+
const shell = createShellSession();
60+
61+
try {
62+
let status = await getInteractiveSessionStatus();
63+
let activity: string[] = [formatSuccessMessage("Ready. Use /help to explore commands.")];
64+
renderShell(shell, status, "", activity);
65+
66+
while (true) {
67+
const line = await shell.readLine((input, completion) => {
68+
renderShell(shell, status, input, activity);
69+
renderPrompt(shell, "> ", input);
70+
const suggestions = formatSuggestionsBelowInput(input, completion?.selectedIndex);
71+
72+
if (suggestions.length > 0) {
73+
shell.write(`${suggestions.join("\n")}\n`);
74+
}
75+
}, getSlashCommandSuggestions);
76+
77+
if (!line) {
78+
continue;
79+
}
80+
81+
if (line === "/") {
82+
activity = formatHelpPanel();
83+
renderShell(shell, status, line, activity);
84+
continue;
85+
}
86+
87+
if (line === "/status") {
88+
status = await getInteractiveSessionStatus();
89+
activity = clampActivity(activity, formatSuccessMessage(renderSessionStatus(status)));
90+
renderShell(shell, status, line, activity);
91+
continue;
92+
}
93+
94+
const resolved = resolveShellInput(line);
95+
96+
if (resolved.kind === "builtin") {
97+
if (resolved.builtin === "exit") {
98+
break;
99+
}
100+
101+
if (resolved.builtin === "help") {
102+
activity = formatHelpPanel();
103+
renderShell(shell, status, line, activity);
104+
continue;
105+
}
106+
107+
if (resolved.builtin === "clear") {
108+
status = await getInteractiveSessionStatus();
109+
activity = [formatSuccessMessage("Cleared the screen.")];
110+
renderShell(shell, status, "", activity);
111+
continue;
112+
}
113+
}
114+
115+
if (resolved.kind === "command" && resolved.argv[0] === "snippet" && resolved.argv[1] === "create" && !resolved.argv[2]) {
116+
const prompt = createPromptSession();
117+
118+
try {
119+
const title = await prompt.ask("Title", "Untitled snippet");
120+
const language = await prompt.ask("Language", "text");
121+
const description = await prompt.ask("Description", "");
122+
const notes = await prompt.ask("Notes", "");
123+
const tagsValue = await prompt.ask("Tags (comma-separated)", "");
124+
const code = await prompt.askMultiline("Paste snippet code", ".");
125+
126+
const argv = [
127+
"snippet",
128+
"create",
129+
"-",
130+
"--title",
131+
title,
132+
"--language",
133+
language,
134+
"--description",
135+
description,
136+
"--notes",
137+
notes,
138+
];
139+
140+
tagsValue.split(",").map((tag) => tag.trim()).filter(Boolean).forEach((tag) => {
141+
argv.push("--tag", tag);
142+
});
143+
144+
const originalStdin = process.stdin;
145+
const { PassThrough } = await import("node:stream");
146+
const stdin = new PassThrough();
147+
Object.defineProperty(process, "stdin", { value: stdin, configurable: true });
148+
stdin.end(code);
149+
150+
try {
151+
const result = await runCli(argv);
152+
activity = clampActivity(activity, renderSuccess(result, "human").trim());
153+
} finally {
154+
Object.defineProperty(process, "stdin", { value: originalStdin, configurable: true });
155+
}
156+
} catch (error) {
157+
activity = clampActivity(activity, formatErrorMessage(error instanceof Error ? error.message : "Snippet create failed"));
158+
} finally {
159+
prompt.close();
160+
}
161+
162+
status = await getInteractiveSessionStatus();
163+
renderShell(shell, status, line, activity);
164+
continue;
165+
}
166+
167+
if (resolved.kind === "command" && shouldConfirmDestructiveCommand(resolved.argv)) {
168+
const confirmed = await shell.confirm(`Run destructive command: ${getDestructiveCommandLabel(resolved.argv)}?`, false);
169+
170+
if (!confirmed) {
171+
activity = clampActivity(activity, formatErrorMessage("Cancelled."));
172+
renderShell(shell, status, line, activity);
173+
continue;
174+
}
175+
176+
resolved.argv.push("--yes");
177+
}
178+
179+
try {
180+
const result = await runCli(resolved.kind === "command" ? resolved.argv : []);
181+
activity = clampActivity(activity, renderSuccess(result, "human").trim());
182+
status = await getInteractiveSessionStatus();
183+
renderShell(shell, status, line, activity);
184+
} catch (error) {
185+
const message = error instanceof CacheError || error instanceof Error ? error.message : "Unexpected error";
186+
activity = clampActivity(activity, formatErrorMessage(message));
187+
renderShell(shell, status, line, activity);
188+
}
189+
}
190+
} finally {
191+
shell.close();
192+
}
193+
}

0 commit comments

Comments
 (0)