letmecook 0.0.21 → 0.0.23

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,78 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { loadEnv } from "./src/env";
4
+
5
+ // Load env files from ~/.letmecook/.env and ./.env
6
+ loadEnv();
7
+
3
8
  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
9
  import { handleTUIMode } from "./src/tui-mode";
10
+ import { handleCLIMode } from "./src/cli-mode";
11
+
12
+ const block = (r: number, g: number, b: number): string => `\x1b[48;2;${r};${g};${b}m \x1b[0m`;
18
13
 
19
14
  function printUsage(): void {
20
15
  console.log(`
21
16
  letmecook - Multi-repo workspace manager for AI coding sessions
22
17
 
23
18
  Usage:
24
- letmecook Launch interactive TUI (recommended)
19
+ letmecook Launch interactive TUI (default)
25
20
  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
21
+ letmecook --cli <owner/repo> [...] Create or resume a session (CLI mode)
22
+ letmecook --cli --list List all sessions
23
+ letmecook --cli --resume <session-name> Resume a session
24
+ letmecook --cli --delete <session-name> Delete a session
25
+ letmecook --cli --nuke [--yes] Nuke everything
31
26
  letmecook --why Show why this tool exists
32
27
  letmecook --help Show this help
33
28
  letmecook --version Show version
34
29
 
35
30
  Examples:
36
- # Interactive mode (new - recommended)
31
+ # Interactive mode (default, recommended)
37
32
  letmecook
38
33
 
39
- # CLI mode
40
- letmecook microsoft/playwright
41
- letmecook facebook/react openai/agents
42
- letmecook --resume playwright-agent-tests
34
+ # CLI mode (requires --cli prefix)
35
+ letmecook --cli microsoft/playwright
36
+ letmecook --cli facebook/react openai/agents
37
+ letmecook --cli --resume playwright-agent-tests
43
38
  `);
44
39
  }
45
40
 
46
41
  function printWhy(): void {
42
+ const palette: Record<string, string> = {
43
+ ".": " ",
44
+ s: block(200, 200, 200), // steam
45
+ d: block(88, 62, 52), // dark brown
46
+ l: block(121, 85, 72), // light brown (crust)
47
+ r: block(214, 64, 36), // red (pepperoni)
48
+ o: block(245, 124, 0), // orange
49
+ y: block(255, 193, 7), // yellow (cheese)
50
+ Y: block(255, 220, 80), // bright yellow (cheese highlight)
51
+ g: block(76, 175, 80), // green (basil)
52
+ k: block(20, 20, 20), // black (nori)
53
+ w: block(236, 236, 236), // white (rice)
54
+ b: block(79, 118, 170), // blue (fish)
55
+ };
56
+ const arts = [
57
+ // Pizza slice (7x7)
58
+ ["..yy...", ".yyyy..", ".yryry.", "yyyyyyy", "yygyrry", "lllllll", "ddddddd"],
59
+ // Burger (7x7)
60
+ [".yyyyy.", "ggggggg", "rrrrrrr", "ddddddd", "ggggggg", "ooooooo", ".yyyyy."],
61
+ // Fried egg (7x7)
62
+ ["..www..", ".wwwww.", "wwwyyww", "wwyyyyw", "wwwyyww", ".wwwww.", "..www.."],
63
+ ];
64
+ const artRows = arts[Math.floor(Math.random() * arts.length)]!;
65
+ const art = artRows
66
+ .map((row) =>
67
+ row
68
+ .split("")
69
+ .map((cell) => palette[cell] ?? " ")
70
+ .join(""),
71
+ )
72
+ .join("\n");
47
73
  console.log(`
74
+ ${art}
75
+
48
76
  Your dev folder is full of traps: outdated code, abandoned experiments, naming
49
77
  collisions that send agents down rabbit holes until they hit context limits.
50
78
 
@@ -54,257 +82,63 @@ Agents are good at problem-solving when you give them what they need and let the
54
82
  `);
55
83
  }
56
84
 
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
85
  console.clear();
286
86
  const args = process.argv.slice(2);
287
87
  const firstArg = args[0];
288
88
 
89
+ // Version flag
289
90
  if (firstArg === "--version" || firstArg === "-v") {
290
91
  console.log(`letmecook v${pkg.version}`);
291
92
  process.exit(0);
292
93
  }
293
94
 
95
+ // Why flag
294
96
  if (firstArg === "--why") {
295
97
  printWhy();
296
98
  process.exit(0);
297
99
  }
298
100
 
299
- if (args.length === 0 || firstArg === "--help" || firstArg === "-h") {
101
+ // Help flag
102
+ if (firstArg === "--help" || firstArg === "-h") {
300
103
  printUsage();
301
104
  process.exit(0);
302
105
  }
303
106
 
107
+ // CLI mode with --cli prefix
108
+ if (firstArg === "--cli") {
109
+ const cliArgs = args.slice(1);
110
+ await handleCLIMode(cliArgs);
111
+ process.exit(0);
112
+ }
113
+
114
+ // Explicit TUI mode
304
115
  if (firstArg === "--tui") {
305
116
  await handleTUIMode();
306
- } else if (args.length === 0) {
117
+ process.exit(0);
118
+ }
119
+
120
+ // Default: TUI mode (no args)
121
+ if (args.length === 0) {
307
122
  await handleTUIMode();
308
- } else {
309
- await handleCLIMode(args);
123
+ process.exit(0);
124
+ }
125
+
126
+ // Catch bare repo args without --cli prefix
127
+ if (firstArg && !firstArg.startsWith("-") && firstArg.includes("/")) {
128
+ console.error(`Error: Repo arguments require --cli prefix.`);
129
+ console.log(`\nUsage: letmecook --cli ${args.join(" ")}`);
130
+ console.log(`\nOr launch the interactive TUI: letmecook`);
131
+ process.exit(1);
310
132
  }
133
+
134
+ // Unknown flag
135
+ if (firstArg?.startsWith("-")) {
136
+ console.error(`Unknown option: ${firstArg}`);
137
+ printUsage();
138
+ process.exit(1);
139
+ }
140
+
141
+ // Unknown positional argument that's not a repo
142
+ console.error(`Unknown argument: ${firstArg}`);
143
+ printUsage();
144
+ 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.23",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/rustydotwtf/letmecook.git"
@@ -28,13 +28,18 @@
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"
34
38
  },
35
39
  "dependencies": {
36
40
  "@opentui/core": "^0.1.63",
37
- "ai": "^6.0.3"
41
+ "ai": "^6.0.3",
42
+ "zod": "^4.3.6"
38
43
  },
39
44
  "devDependencies": {
40
45
  "@types/bun": "latest",
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