letmecook 0.0.19 → 0.0.21
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/index.ts +70 -18
- package/package.json +1 -1
- package/src/sessions.ts +2 -1
- package/src/tui-mode.ts +19 -1
- package/src/ui/confirm-nuke.ts +95 -0
- package/src/ui/list.ts +3 -3
- package/src/ui/main-menu.ts +8 -1
package/index.ts
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import pkg from "./package.json";
|
|
4
4
|
import { parseRepoSpec, type RepoSpec } from "./src/types";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
listSessions,
|
|
7
|
+
getSession,
|
|
8
|
+
updateLastAccessed,
|
|
9
|
+
deleteAllSessions,
|
|
10
|
+
deleteSession,
|
|
11
|
+
} from "./src/sessions";
|
|
6
12
|
import { createRenderer, destroyRenderer } from "./src/ui/renderer";
|
|
7
13
|
import { showNewSessionPrompt } from "./src/ui/new-session";
|
|
8
14
|
import { showSessionList } from "./src/ui/list";
|
|
15
|
+
import { showNukeConfirm } from "./src/ui/confirm-nuke";
|
|
9
16
|
import { createNewSession, resumeSession } from "./src/flows";
|
|
10
17
|
import { handleTUIMode } from "./src/tui-mode";
|
|
11
18
|
|
|
@@ -19,8 +26,8 @@ Usage:
|
|
|
19
26
|
letmecook <owner/repo> [owner/repo:branch...] Create or resume a session (CLI)
|
|
20
27
|
letmecook --list List all sessions
|
|
21
28
|
letmecook --resume <session-name> Resume a session
|
|
22
|
-
letmecook --
|
|
23
|
-
letmecook --nuke
|
|
29
|
+
letmecook --delete <session-name> Delete a session
|
|
30
|
+
letmecook --nuke [--yes] Nuke everything
|
|
24
31
|
letmecook --why Show why this tool exists
|
|
25
32
|
letmecook --help Show this help
|
|
26
33
|
letmecook --version Show version
|
|
@@ -124,11 +131,16 @@ async function handleList(): Promise<void> {
|
|
|
124
131
|
console.log("[TODO] Delete session flow");
|
|
125
132
|
break;
|
|
126
133
|
|
|
127
|
-
case "nuke
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
case "nuke": {
|
|
135
|
+
const choice = await showNukeConfirm(renderer, sessions.length);
|
|
136
|
+
if (choice === "confirm") {
|
|
137
|
+
const count = await deleteAllSessions();
|
|
138
|
+
destroyRenderer();
|
|
139
|
+
console.log(`\nNuked ${count} session(s) and all data.`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
132
144
|
|
|
133
145
|
case "quit":
|
|
134
146
|
destroyRenderer();
|
|
@@ -168,13 +180,52 @@ async function handleResume(sessionName: string): Promise<void> {
|
|
|
168
180
|
});
|
|
169
181
|
}
|
|
170
182
|
|
|
171
|
-
async function
|
|
172
|
-
|
|
183
|
+
async function handleDelete(sessionName: string): Promise<void> {
|
|
184
|
+
const session = await getSession(sessionName);
|
|
185
|
+
|
|
186
|
+
if (!session) {
|
|
187
|
+
console.error(`Session not found: ${sessionName}`);
|
|
188
|
+
const sessions = await listSessions();
|
|
189
|
+
if (sessions.length > 0) {
|
|
190
|
+
console.log("\nAvailable sessions:");
|
|
191
|
+
sessions.forEach((s) => console.log(` - ${s.name}`));
|
|
192
|
+
}
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const deleted = await deleteSession(sessionName);
|
|
197
|
+
if (deleted) {
|
|
198
|
+
console.log(`Deleted session: ${sessionName}`);
|
|
199
|
+
} else {
|
|
200
|
+
console.error(`Failed to delete session: ${sessionName}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
173
203
|
}
|
|
174
204
|
|
|
175
|
-
async function
|
|
176
|
-
const
|
|
177
|
-
|
|
205
|
+
async function handleNuke(skipConfirm = false): Promise<void> {
|
|
206
|
+
const sessions = await listSessions();
|
|
207
|
+
if (sessions.length === 0) {
|
|
208
|
+
console.log("Nothing to nuke.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Skip confirmation if --yes flag or non-interactive (piped input)
|
|
213
|
+
if (skipConfirm || !process.stdin.isTTY) {
|
|
214
|
+
const count = await deleteAllSessions();
|
|
215
|
+
console.log(`Nuked ${count} session(s) and all data.`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const renderer = await createRenderer();
|
|
220
|
+
const choice = await showNukeConfirm(renderer, sessions.length);
|
|
221
|
+
destroyRenderer();
|
|
222
|
+
|
|
223
|
+
if (choice === "confirm") {
|
|
224
|
+
const count = await deleteAllSessions();
|
|
225
|
+
console.log(`Nuked ${count} session(s) and all data.`);
|
|
226
|
+
} else {
|
|
227
|
+
console.log("Cancelled.");
|
|
228
|
+
}
|
|
178
229
|
}
|
|
179
230
|
|
|
180
231
|
async function handleCLIMode(args: string[]): Promise<void> {
|
|
@@ -189,15 +240,16 @@ async function handleCLIMode(args: string[]): Promise<void> {
|
|
|
189
240
|
process.exit(1);
|
|
190
241
|
}
|
|
191
242
|
await handleResume(sessionName);
|
|
192
|
-
} else if (firstArg === "--
|
|
243
|
+
} else if (firstArg === "--delete" || firstArg === "-d") {
|
|
193
244
|
const sessionName = args[1];
|
|
194
245
|
if (!sessionName) {
|
|
195
|
-
console.error("Missing session name. Usage: letmecook --
|
|
246
|
+
console.error("Missing session name. Usage: letmecook --delete <session-name>");
|
|
196
247
|
process.exit(1);
|
|
197
248
|
}
|
|
198
|
-
await
|
|
199
|
-
} else if (firstArg === "--nuke
|
|
200
|
-
|
|
249
|
+
await handleDelete(sessionName);
|
|
250
|
+
} else if (firstArg === "--nuke") {
|
|
251
|
+
const hasYes = args.includes("--yes") || args.includes("-y");
|
|
252
|
+
await handleNuke(hasYes);
|
|
201
253
|
} else if (firstArg?.startsWith("-")) {
|
|
202
254
|
console.error(`Unknown option: ${firstArg}`);
|
|
203
255
|
printUsage();
|
package/package.json
CHANGED
package/src/sessions.ts
CHANGED
|
@@ -191,7 +191,8 @@ export async function deleteAllSessions(): Promise<number> {
|
|
|
191
191
|
const count = sessions.length;
|
|
192
192
|
|
|
193
193
|
try {
|
|
194
|
-
|
|
194
|
+
// Nuke entire .letmecook directory including SQLite history
|
|
195
|
+
await rm(LETMECOOK_DIR, { recursive: true, force: true });
|
|
195
196
|
return count;
|
|
196
197
|
} catch {
|
|
197
198
|
return 0;
|
package/src/tui-mode.ts
CHANGED
|
@@ -9,10 +9,17 @@ import { showMainMenu } from "./ui/main-menu";
|
|
|
9
9
|
import { showSessionDetails } from "./ui/session-details";
|
|
10
10
|
import { showSessionSettings } from "./ui/session-settings";
|
|
11
11
|
import { showDeleteConfirm } from "./ui/confirm-delete";
|
|
12
|
+
import { showNukeConfirm } from "./ui/confirm-nuke";
|
|
12
13
|
import { showQuitWarning } from "./ui/background-warning";
|
|
13
14
|
import type { Session } from "./types";
|
|
14
15
|
import { createNewSession, resumeSession } from "./flows";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
listSessions,
|
|
18
|
+
deleteSession,
|
|
19
|
+
deleteAllSessions,
|
|
20
|
+
updateLastAccessed,
|
|
21
|
+
updateSessionSettings,
|
|
22
|
+
} from "./sessions";
|
|
16
23
|
import { getRunningProcesses, killAllProcesses } from "./process-registry";
|
|
17
24
|
|
|
18
25
|
export async function handleTUIMode(): Promise<void> {
|
|
@@ -41,6 +48,17 @@ export async function handleTUIMode(): Promise<void> {
|
|
|
41
48
|
break;
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
case "nuke": {
|
|
52
|
+
const choice = await showNukeConfirm(renderer, sessions.length);
|
|
53
|
+
if (choice === "confirm") {
|
|
54
|
+
const count = await deleteAllSessions();
|
|
55
|
+
destroyRenderer();
|
|
56
|
+
console.log(`\nNuked ${count} session(s) and all data.`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
case "quit": {
|
|
45
63
|
const runningProcesses = await getRunningProcesses();
|
|
46
64
|
if (runningProcesses.length > 0) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CliRenderer,
|
|
3
|
+
TextRenderable,
|
|
4
|
+
SelectRenderable,
|
|
5
|
+
SelectRenderableEvents,
|
|
6
|
+
type KeyEvent,
|
|
7
|
+
} from "@opentui/core";
|
|
8
|
+
import { createBaseLayout, clearLayout } from "./renderer";
|
|
9
|
+
import { showFooter, hideFooter } from "./common/footer";
|
|
10
|
+
import { isEscape } from "./common/keyboard";
|
|
11
|
+
|
|
12
|
+
export type NukeConfirmChoice = "confirm" | "cancel";
|
|
13
|
+
|
|
14
|
+
export function showNukeConfirm(
|
|
15
|
+
renderer: CliRenderer,
|
|
16
|
+
sessionCount: number,
|
|
17
|
+
): Promise<NukeConfirmChoice> {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
clearLayout(renderer);
|
|
20
|
+
|
|
21
|
+
const { content } = createBaseLayout(renderer, "Delete all sessions");
|
|
22
|
+
|
|
23
|
+
const countInfo = new TextRenderable(renderer, {
|
|
24
|
+
id: "count-info",
|
|
25
|
+
content: `${sessionCount} session${sessionCount === 1 ? "" : "s"} will be deleted`,
|
|
26
|
+
fg: "#38bdf8",
|
|
27
|
+
marginBottom: 1,
|
|
28
|
+
});
|
|
29
|
+
content.add(countInfo);
|
|
30
|
+
|
|
31
|
+
const warning = new TextRenderable(renderer, {
|
|
32
|
+
id: "warning",
|
|
33
|
+
content: "This permanently deletes all sessions, history, and data.",
|
|
34
|
+
fg: "#f59e0b",
|
|
35
|
+
marginBottom: 1,
|
|
36
|
+
});
|
|
37
|
+
content.add(warning);
|
|
38
|
+
|
|
39
|
+
const question = new TextRenderable(renderer, {
|
|
40
|
+
id: "question",
|
|
41
|
+
content: "Are you sure you want to delete all sessions?",
|
|
42
|
+
fg: "#e2e8f0",
|
|
43
|
+
});
|
|
44
|
+
content.add(question);
|
|
45
|
+
|
|
46
|
+
const select = new SelectRenderable(renderer, {
|
|
47
|
+
id: "nuke-confirm-select",
|
|
48
|
+
width: 38,
|
|
49
|
+
height: 2,
|
|
50
|
+
options: [
|
|
51
|
+
{ name: "Cancel", description: "", value: "cancel" },
|
|
52
|
+
{ name: "Delete all sessions", description: "", value: "confirm" },
|
|
53
|
+
],
|
|
54
|
+
showDescription: false,
|
|
55
|
+
backgroundColor: "transparent",
|
|
56
|
+
focusedBackgroundColor: "transparent",
|
|
57
|
+
selectedBackgroundColor: "#334155",
|
|
58
|
+
textColor: "#e2e8f0",
|
|
59
|
+
selectedTextColor: "#38bdf8",
|
|
60
|
+
marginTop: 1,
|
|
61
|
+
});
|
|
62
|
+
content.add(select);
|
|
63
|
+
|
|
64
|
+
select.focus();
|
|
65
|
+
|
|
66
|
+
const handleSelect = (_index: number, option: { value: string }) => {
|
|
67
|
+
cleanup();
|
|
68
|
+
resolve(option.value as NukeConfirmChoice);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleKeypress = (key: KeyEvent) => {
|
|
72
|
+
if (isEscape(key)) {
|
|
73
|
+
cleanup();
|
|
74
|
+
resolve("cancel");
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const cleanup = () => {
|
|
79
|
+
select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
80
|
+
renderer.keyInput.off("keypress", handleKeypress);
|
|
81
|
+
select.blur();
|
|
82
|
+
hideFooter(renderer);
|
|
83
|
+
clearLayout(renderer);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
showFooter(renderer, content, {
|
|
87
|
+
navigate: true,
|
|
88
|
+
select: true,
|
|
89
|
+
back: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
93
|
+
renderer.keyInput.on("keypress", handleKeypress);
|
|
94
|
+
});
|
|
95
|
+
}
|
package/src/ui/list.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { isEscape, isArrowUp, isArrowDown } from "./common/keyboard";
|
|
|
14
14
|
export type ListAction =
|
|
15
15
|
| { type: "resume"; session: Session }
|
|
16
16
|
| { type: "delete"; session: Session }
|
|
17
|
-
| { type: "nuke
|
|
17
|
+
| { type: "nuke" }
|
|
18
18
|
| { type: "quit" };
|
|
19
19
|
|
|
20
20
|
export function showSessionList(renderer: CliRenderer, sessions: Session[]): Promise<ListAction> {
|
|
@@ -93,7 +93,7 @@ export function showSessionList(renderer: CliRenderer, sessions: Session[]): Pro
|
|
|
93
93
|
}
|
|
94
94
|
} else if (key.name === "a") {
|
|
95
95
|
cleanup();
|
|
96
|
-
resolve({ type: "nuke
|
|
96
|
+
resolve({ type: "nuke" });
|
|
97
97
|
} else if (key.name === "q" || isEscape(key)) {
|
|
98
98
|
cleanup();
|
|
99
99
|
resolve({ type: "quit" });
|
|
@@ -112,7 +112,7 @@ export function showSessionList(renderer: CliRenderer, sessions: Session[]): Pro
|
|
|
112
112
|
navigate: true,
|
|
113
113
|
select: false,
|
|
114
114
|
back: false,
|
|
115
|
-
custom: ["Enter Resume", "d Delete", "a Nuke
|
|
115
|
+
custom: ["Enter Resume", "d Delete", "a Nuke", "q Quit"],
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
package/src/ui/main-menu.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type MainMenuAction =
|
|
|
15
15
|
| { type: "new-session" }
|
|
16
16
|
| { type: "resume"; session: Session }
|
|
17
17
|
| { type: "delete"; session: Session }
|
|
18
|
+
| { type: "nuke" }
|
|
18
19
|
| { type: "quit" };
|
|
19
20
|
|
|
20
21
|
export function showMainMenu(renderer: CliRenderer, sessions: Session[]): Promise<MainMenuAction> {
|
|
@@ -94,6 +95,12 @@ export function showMainMenu(renderer: CliRenderer, sessions: Session[]): Promis
|
|
|
94
95
|
return;
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
if (key.name === "a" && sessions.length > 0) {
|
|
99
|
+
cleanup();
|
|
100
|
+
resolve({ type: "nuke" });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
97
104
|
if (key.name === "q" || isEscape(key)) {
|
|
98
105
|
cleanup();
|
|
99
106
|
resolve({ type: "quit" });
|
|
@@ -113,7 +120,7 @@ export function showMainMenu(renderer: CliRenderer, sessions: Session[]): Promis
|
|
|
113
120
|
// Show footer with context-aware actions
|
|
114
121
|
const footerActions: string[] = [];
|
|
115
122
|
if (sessions.length > 0) {
|
|
116
|
-
footerActions.push("Enter Open", "n New", "d Delete");
|
|
123
|
+
footerActions.push("Enter Open", "n New", "d Delete", "a Nuke");
|
|
117
124
|
} else {
|
|
118
125
|
footerActions.push("n New");
|
|
119
126
|
}
|