letmecook 0.0.21 → 0.0.22

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 CHANGED
@@ -1,50 +1,73 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import pkg from "./package.json";
4
- import { parseRepoSpec, type RepoSpec } from "./src/types";
5
- import {
6
- listSessions,
7
- getSession,
8
- updateLastAccessed,
9
- deleteAllSessions,
10
- deleteSession,
11
- } from "./src/sessions";
12
- import { createRenderer, destroyRenderer } from "./src/ui/renderer";
13
- import { showNewSessionPrompt } from "./src/ui/new-session";
14
- import { showSessionList } from "./src/ui/list";
15
- import { showNukeConfirm } from "./src/ui/confirm-nuke";
16
- import { createNewSession, resumeSession } from "./src/flows";
17
4
  import { handleTUIMode } from "./src/tui-mode";
5
+ import { handleCLIMode } from "./src/cli-mode";
6
+
7
+ const block = (r: number, g: number, b: number): string => `\x1b[48;2;${r};${g};${b}m \x1b[0m`;
18
8
 
19
9
  function printUsage(): void {
20
10
  console.log(`
21
11
  letmecook - Multi-repo workspace manager for AI coding sessions
22
12
 
23
13
  Usage:
24
- letmecook Launch interactive TUI (recommended)
14
+ letmecook Launch interactive TUI (default)
25
15
  letmecook --tui Launch interactive TUI explicitly
26
- letmecook <owner/repo> [owner/repo:branch...] Create or resume a session (CLI)
27
- letmecook --list List all sessions
28
- letmecook --resume <session-name> Resume a session
29
- letmecook --delete <session-name> Delete a session
30
- letmecook --nuke [--yes] Nuke everything
16
+ letmecook --cli <owner/repo> [...] Create or resume a session (CLI mode)
17
+ letmecook --cli --list List all sessions
18
+ letmecook --cli --resume <session-name> Resume a session
19
+ letmecook --cli --delete <session-name> Delete a session
20
+ letmecook --cli --nuke [--yes] Nuke everything
31
21
  letmecook --why Show why this tool exists
32
22
  letmecook --help Show this help
33
23
  letmecook --version Show version
34
24
 
35
25
  Examples:
36
- # Interactive mode (new - recommended)
26
+ # Interactive mode (default, recommended)
37
27
  letmecook
38
28
 
39
- # CLI mode
40
- letmecook microsoft/playwright
41
- letmecook facebook/react openai/agents
42
- letmecook --resume playwright-agent-tests
29
+ # CLI mode (requires --cli prefix)
30
+ letmecook --cli microsoft/playwright
31
+ letmecook --cli facebook/react openai/agents
32
+ letmecook --cli --resume playwright-agent-tests
43
33
  `);
44
34
  }
45
35
 
46
36
  function printWhy(): void {
37
+ const palette: Record<string, string> = {
38
+ ".": " ",
39
+ s: block(200, 200, 200), // steam
40
+ d: block(88, 62, 52), // dark brown
41
+ l: block(121, 85, 72), // light brown (crust)
42
+ r: block(214, 64, 36), // red (pepperoni)
43
+ o: block(245, 124, 0), // orange
44
+ y: block(255, 193, 7), // yellow (cheese)
45
+ Y: block(255, 220, 80), // bright yellow (cheese highlight)
46
+ g: block(76, 175, 80), // green (basil)
47
+ k: block(20, 20, 20), // black (nori)
48
+ w: block(236, 236, 236), // white (rice)
49
+ b: block(79, 118, 170), // blue (fish)
50
+ };
51
+ const arts = [
52
+ // Pizza slice (7x7)
53
+ ["..yy...", ".yyyy..", ".yryry.", "yyyyyyy", "yygyrry", "lllllll", "ddddddd"],
54
+ // Burger (7x7)
55
+ [".yyyyy.", "ggggggg", "rrrrrrr", "ddddddd", "ggggggg", "ooooooo", ".yyyyy."],
56
+ // Fried egg (7x7)
57
+ ["..www..", ".wwwww.", "wwwyyww", "wwyyyyw", "wwwyyww", ".wwwww.", "..www.."],
58
+ ];
59
+ const artRows = arts[Math.floor(Math.random() * arts.length)]!;
60
+ const art = artRows
61
+ .map((row) =>
62
+ row
63
+ .split("")
64
+ .map((cell) => palette[cell] ?? " ")
65
+ .join(""),
66
+ )
67
+ .join("\n");
47
68
  console.log(`
69
+ ${art}
70
+
48
71
  Your dev folder is full of traps: outdated code, abandoned experiments, naming
49
72
  collisions that send agents down rabbit holes until they hit context limits.
50
73
 
@@ -54,257 +77,63 @@ Agents are good at problem-solving when you give them what they need and let the
54
77
  `);
55
78
  }
56
79
 
57
- async function handleNewSessionCLI(repos: RepoSpec[]): Promise<void> {
58
- const renderer = await createRenderer();
59
-
60
- try {
61
- const { goal, cancelled } = await showNewSessionPrompt(renderer, repos);
62
-
63
- if (cancelled) {
64
- destroyRenderer();
65
- console.log("\nCancelled.");
66
- return;
67
- }
68
-
69
- const result = await createNewSession(renderer, {
70
- repos,
71
- goal,
72
- mode: "cli",
73
- });
74
-
75
- if (!result) {
76
- destroyRenderer();
77
- console.log("\nCancelled.");
78
- return;
79
- }
80
-
81
- const { session, skipped } = result;
82
-
83
- if (skipped) {
84
- destroyRenderer();
85
- console.log(`\nResuming existing session: ${session.name}\n`);
86
- await resumeSession(renderer, {
87
- session,
88
- mode: "cli",
89
- initialRefresh: true,
90
- });
91
- return;
92
- }
93
-
94
- destroyRenderer();
95
- console.log(`\nSession created: ${session.name}`);
96
- console.log(`Path: ${session.path}\n`);
97
-
98
- await resumeSession(renderer, {
99
- session,
100
- mode: "cli",
101
- initialRefresh: false,
102
- });
103
- } catch (error) {
104
- destroyRenderer();
105
- console.error("\nError:", error instanceof Error ? error.message : error);
106
- process.exit(1);
107
- }
108
- }
109
-
110
- async function handleList(): Promise<void> {
111
- const renderer = await createRenderer();
112
-
113
- try {
114
- while (true) {
115
- const sessions = await listSessions();
116
- const action = await showSessionList(renderer, sessions);
117
-
118
- switch (action.type) {
119
- case "resume":
120
- destroyRenderer();
121
- await updateLastAccessed(action.session.name);
122
- console.log(`\nResuming session: ${action.session.name}\n`);
123
- await resumeSession(renderer, {
124
- session: action.session,
125
- mode: "cli",
126
- initialRefresh: true,
127
- });
128
- return;
129
-
130
- case "delete":
131
- console.log("[TODO] Delete session flow");
132
- break;
133
-
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
- }
144
-
145
- case "quit":
146
- destroyRenderer();
147
- return;
148
- }
149
- }
150
- } catch (error) {
151
- destroyRenderer();
152
- console.error("\nError:", error instanceof Error ? error.message : error);
153
- process.exit(1);
154
- }
155
- }
156
-
157
- async function handleResume(sessionName: string): Promise<void> {
158
- const session = await getSession(sessionName);
159
-
160
- if (!session) {
161
- console.error(`Session not found: ${sessionName}`);
162
- console.log("\nAvailable sessions:");
163
- const sessions = await listSessions();
164
- if (sessions.length === 0) {
165
- console.log(" (none)");
166
- } else {
167
- sessions.forEach((s) => console.log(` - ${s.name}`));
168
- }
169
- process.exit(1);
170
- }
171
-
172
- await updateLastAccessed(session.name);
173
- console.log(`\nResuming session: ${session.name}\n`);
174
-
175
- const renderer = await createRenderer();
176
- await resumeSession(renderer, {
177
- session,
178
- mode: "cli",
179
- initialRefresh: true,
180
- });
181
- }
182
-
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
- }
203
- }
204
-
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
- }
229
- }
230
-
231
- async function handleCLIMode(args: string[]): Promise<void> {
232
- const firstArg = args[0];
233
-
234
- if (firstArg === "--list" || firstArg === "-l") {
235
- await handleList();
236
- } else if (firstArg === "--resume" || firstArg === "-r") {
237
- const sessionName = args[1];
238
- if (!sessionName) {
239
- console.error("Missing session name. Usage: letmecook --resume <session-name>");
240
- process.exit(1);
241
- }
242
- await handleResume(sessionName);
243
- } else if (firstArg === "--delete" || firstArg === "-d") {
244
- const sessionName = args[1];
245
- if (!sessionName) {
246
- console.error("Missing session name. Usage: letmecook --delete <session-name>");
247
- process.exit(1);
248
- }
249
- await handleDelete(sessionName);
250
- } else if (firstArg === "--nuke") {
251
- const hasYes = args.includes("--yes") || args.includes("-y");
252
- await handleNuke(hasYes);
253
- } else if (firstArg?.startsWith("-")) {
254
- console.error(`Unknown option: ${firstArg}`);
255
- printUsage();
256
- process.exit(1);
257
- } else {
258
- try {
259
- const repos = parseRepos(args);
260
- await handleNewSessionCLI(repos);
261
- } catch (error) {
262
- console.error("Error:", error instanceof Error ? error.message : error);
263
- process.exit(1);
264
- }
265
- }
266
- }
267
-
268
- function parseRepos(args: string[]): RepoSpec[] {
269
- const repos: RepoSpec[] = [];
270
-
271
- for (const arg of args) {
272
- if (!arg || arg.startsWith("-")) continue;
273
-
274
- if (!arg.includes("/")) {
275
- throw new Error(`Invalid repo format: ${arg} (expected owner/repo)`);
276
- }
277
-
278
- const repo = parseRepoSpec(arg);
279
- repos.push(repo);
280
- }
281
-
282
- return repos;
283
- }
284
-
285
80
  console.clear();
286
81
  const args = process.argv.slice(2);
287
82
  const firstArg = args[0];
288
83
 
84
+ // Version flag
289
85
  if (firstArg === "--version" || firstArg === "-v") {
290
86
  console.log(`letmecook v${pkg.version}`);
291
87
  process.exit(0);
292
88
  }
293
89
 
90
+ // Why flag
294
91
  if (firstArg === "--why") {
295
92
  printWhy();
296
93
  process.exit(0);
297
94
  }
298
95
 
299
- if (args.length === 0 || firstArg === "--help" || firstArg === "-h") {
96
+ // Help flag
97
+ if (firstArg === "--help" || firstArg === "-h") {
300
98
  printUsage();
301
99
  process.exit(0);
302
100
  }
303
101
 
102
+ // CLI mode with --cli prefix
103
+ if (firstArg === "--cli") {
104
+ const cliArgs = args.slice(1);
105
+ await handleCLIMode(cliArgs);
106
+ process.exit(0);
107
+ }
108
+
109
+ // Explicit TUI mode
304
110
  if (firstArg === "--tui") {
305
111
  await handleTUIMode();
306
- } else if (args.length === 0) {
112
+ process.exit(0);
113
+ }
114
+
115
+ // Default: TUI mode (no args)
116
+ if (args.length === 0) {
307
117
  await handleTUIMode();
308
- } else {
309
- await handleCLIMode(args);
118
+ process.exit(0);
310
119
  }
120
+
121
+ // Catch bare repo args without --cli prefix
122
+ if (firstArg && !firstArg.startsWith("-") && firstArg.includes("/")) {
123
+ console.error(`Error: Repo arguments require --cli prefix.`);
124
+ console.log(`\nUsage: letmecook --cli ${args.join(" ")}`);
125
+ console.log(`\nOr launch the interactive TUI: letmecook`);
126
+ process.exit(1);
127
+ }
128
+
129
+ // Unknown flag
130
+ if (firstArg?.startsWith("-")) {
131
+ console.error(`Unknown option: ${firstArg}`);
132
+ printUsage();
133
+ process.exit(1);
134
+ }
135
+
136
+ // Unknown positional argument that's not a repo
137
+ console.error(`Unknown argument: ${firstArg}`);
138
+ printUsage();
139
+ process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecook",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/rustydotwtf/letmecook.git"
@@ -28,6 +28,10 @@
28
28
  "format:check": "oxfmt --check .",
29
29
  "check": "bun run typecheck && bun run lint && bun run format:check",
30
30
  "fix": "bun run lint:fix && bun run format",
31
+ "test": "bun test",
32
+ "test:unit": "bun test tests/unit",
33
+ "test:integration": "bun test tests/integration",
34
+ "test:tui": "bun test tests/tui",
31
35
  "docs": "cd docs && bun start",
32
36
  "docs:build": "cd docs && bun run build",
33
37
  "docs:serve": "cd docs && bun run serve"
package/src/agents-md.ts CHANGED
@@ -11,19 +11,19 @@ export function generateAgentsMd(session: Session): string {
11
11
  minute: "2-digit",
12
12
  });
13
13
 
14
- const hasReferenceRepos = session.repos.some((repo) => repo.reference);
14
+ const hasReadOnlyRepos = session.repos.some((repo) => repo.readOnly);
15
15
  const hasSkills = session.skills && session.skills.length > 0;
16
16
 
17
17
  const repoRows = session.repos
18
18
  .map((repo) => {
19
19
  const branch = repo.branch || "default";
20
20
  const url = `https://github.com/${repo.owner}/${repo.name}`;
21
- const referenceStatus = repo.reference ? "**YES**" : "no";
22
- return `| \`${repo.dir}/\` | [${repo.owner}/${repo.name}](${url}) | ${branch} | ${referenceStatus} |`;
21
+ const readOnlyStatus = repo.readOnly ? "**YES**" : "no";
22
+ return `| \`${repo.dir}/\` | [${repo.owner}/${repo.name}](${url}) | ${branch} | ${readOnlyStatus} |`;
23
23
  })
24
24
  .join("\n");
25
25
 
26
- const referenceRepos = session.repos.filter((repo) => repo.reference);
26
+ const readOnlyRepos = session.repos.filter((repo) => repo.readOnly);
27
27
 
28
28
  const skillsSection = hasSkills
29
29
  ? `
@@ -37,24 +37,21 @@ These skills are available for use in this session and are automatically updated
37
37
  `
38
38
  : "";
39
39
 
40
- const referenceWarning = hasReferenceRepos
40
+ const readOnlyWarning = hasReadOnlyRepos
41
41
  ? `
42
- ## ⚠️ Reference Repositories
42
+ ## ⚠️ Read-Only Repositories
43
43
 
44
- **WARNING: The following repositories are REFERENCES (symlinked from shared cache):**
44
+ **WARNING: The following repositories are marked as READ-ONLY:**
45
45
 
46
- ${referenceRepos.map((repo) => `- \`${repo.dir}/\` (${repo.owner}/${repo.name})`).join("\n")}
46
+ ${readOnlyRepos.map((repo) => `- \`${repo.dir}/\` (${repo.owner}/${repo.name})`).join("\n")}
47
47
 
48
48
  **AI agents must NOT:**
49
49
  - Create, modify, or delete any files in these directories
50
50
  - Make commits affecting these repositories
51
51
  - Use bash commands to circumvent file permissions
52
52
 
53
- **Why are these references?**
54
- - These directories are **symlinks** to a shared cache (~/.letmecook/references/)
55
- - Any modifications would affect ALL sessions using this repository
56
- - They are automatically refreshed to latest before each session resume
57
- - They are included for reference only - the user wants to read and understand the code without risk of accidental modifications
53
+ **Why are these read-only?**
54
+ These repositories are included for reference only. The user wants to read and understand the code without risk of accidental modifications.
58
55
  `
59
56
  : "";
60
57
 
@@ -66,10 +63,10 @@ ${session.goal ? `> ${session.goal}\n` : ""}
66
63
 
67
64
  ## Repositories
68
65
 
69
- | Directory | Repository | Branch | Reference |
66
+ | Directory | Repository | Branch | Read-Only |
70
67
  |-----------|------------|--------|-----------|
71
68
  ${repoRows}
72
- ${referenceWarning}
69
+ ${readOnlyWarning}
73
70
  ${skillsSection}
74
71
  ## Important Notes
75
72