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.
@@ -0,0 +1,266 @@
1
+ import { parseRepoSpec, type RepoSpec } from "./types";
2
+ import {
3
+ listSessions,
4
+ getSession,
5
+ updateLastAccessed,
6
+ deleteAllSessions,
7
+ deleteSession,
8
+ } from "./sessions";
9
+ import { createRenderer, destroyRenderer } from "./ui/renderer";
10
+ import { showNewSessionPrompt } from "./ui/new-session";
11
+ import { showSessionList } from "./ui/list";
12
+ import { showNukeConfirm } from "./ui/confirm-nuke";
13
+ import { createNewSession, resumeSession } from "./flows";
14
+
15
+ export function printCLIUsage(): void {
16
+ console.log(`
17
+ letmecook CLI mode
18
+
19
+ Usage:
20
+ letmecook --cli <owner/repo> [owner/repo:branch...] Create or resume a session
21
+ letmecook --cli --list List all sessions
22
+ letmecook --cli --resume <session-name> Resume a session
23
+ letmecook --cli --delete <session-name> Delete a session
24
+ letmecook --cli --nuke [--yes] Nuke everything
25
+
26
+ Examples:
27
+ letmecook --cli microsoft/playwright
28
+ letmecook --cli facebook/react openai/agents
29
+ letmecook --cli --resume playwright-agent-tests
30
+ `);
31
+ }
32
+
33
+ export async function handleNewSessionCLI(repos: RepoSpec[]): Promise<void> {
34
+ const renderer = await createRenderer();
35
+
36
+ try {
37
+ const { goal, cancelled } = await showNewSessionPrompt(renderer, repos);
38
+
39
+ if (cancelled) {
40
+ destroyRenderer();
41
+ console.log("\nCancelled.");
42
+ return;
43
+ }
44
+
45
+ const result = await createNewSession(renderer, {
46
+ repos,
47
+ goal,
48
+ mode: "cli",
49
+ });
50
+
51
+ if (!result) {
52
+ destroyRenderer();
53
+ console.log("\nCancelled.");
54
+ return;
55
+ }
56
+
57
+ const { session, skipped } = result;
58
+
59
+ if (skipped) {
60
+ destroyRenderer();
61
+ console.log(`\nResuming existing session: ${session.name}\n`);
62
+ await resumeSession(renderer, {
63
+ session,
64
+ mode: "cli",
65
+ initialRefresh: true,
66
+ });
67
+ return;
68
+ }
69
+
70
+ destroyRenderer();
71
+ console.log(`\nSession created: ${session.name}`);
72
+ console.log(`Path: ${session.path}\n`);
73
+
74
+ await resumeSession(renderer, {
75
+ session,
76
+ mode: "cli",
77
+ initialRefresh: false,
78
+ });
79
+ } catch (error) {
80
+ destroyRenderer();
81
+ console.error("\nError:", error instanceof Error ? error.message : error);
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ export async function handleList(): Promise<void> {
87
+ const renderer = await createRenderer();
88
+
89
+ try {
90
+ while (true) {
91
+ const sessions = await listSessions();
92
+ const action = await showSessionList(renderer, sessions);
93
+
94
+ switch (action.type) {
95
+ case "resume":
96
+ destroyRenderer();
97
+ await updateLastAccessed(action.session.name);
98
+ console.log(`\nResuming session: ${action.session.name}\n`);
99
+ await resumeSession(renderer, {
100
+ session: action.session,
101
+ mode: "cli",
102
+ initialRefresh: true,
103
+ });
104
+ return;
105
+
106
+ case "delete":
107
+ console.log("[TODO] Delete session flow");
108
+ break;
109
+
110
+ case "nuke": {
111
+ const choice = await showNukeConfirm(renderer, sessions.length);
112
+ if (choice === "confirm") {
113
+ const count = await deleteAllSessions();
114
+ destroyRenderer();
115
+ console.log(`\nNuked ${count} session(s) and all data.`);
116
+ return;
117
+ }
118
+ break;
119
+ }
120
+
121
+ case "quit":
122
+ destroyRenderer();
123
+ return;
124
+ }
125
+ }
126
+ } catch (error) {
127
+ destroyRenderer();
128
+ console.error("\nError:", error instanceof Error ? error.message : error);
129
+ process.exit(1);
130
+ }
131
+ }
132
+
133
+ export async function handleResume(sessionName: string): Promise<void> {
134
+ const session = await getSession(sessionName);
135
+
136
+ if (!session) {
137
+ console.error(`Session not found: ${sessionName}`);
138
+ console.log("\nAvailable sessions:");
139
+ const sessions = await listSessions();
140
+ if (sessions.length === 0) {
141
+ console.log(" (none)");
142
+ } else {
143
+ sessions.forEach((s) => console.log(` - ${s.name}`));
144
+ }
145
+ process.exit(1);
146
+ }
147
+
148
+ await updateLastAccessed(session.name);
149
+ console.log(`\nResuming session: ${session.name}\n`);
150
+
151
+ const renderer = await createRenderer();
152
+ await resumeSession(renderer, {
153
+ session,
154
+ mode: "cli",
155
+ initialRefresh: true,
156
+ });
157
+ }
158
+
159
+ export async function handleDelete(sessionName: string): Promise<void> {
160
+ const session = await getSession(sessionName);
161
+
162
+ if (!session) {
163
+ console.error(`Session not found: ${sessionName}`);
164
+ const sessions = await listSessions();
165
+ if (sessions.length > 0) {
166
+ console.log("\nAvailable sessions:");
167
+ sessions.forEach((s) => console.log(` - ${s.name}`));
168
+ }
169
+ process.exit(1);
170
+ }
171
+
172
+ const deleted = await deleteSession(sessionName);
173
+ if (deleted) {
174
+ console.log(`Deleted session: ${sessionName}`);
175
+ } else {
176
+ console.error(`Failed to delete session: ${sessionName}`);
177
+ process.exit(1);
178
+ }
179
+ }
180
+
181
+ export async function handleNuke(skipConfirm = false): Promise<void> {
182
+ const sessions = await listSessions();
183
+ if (sessions.length === 0) {
184
+ console.log("Nothing to nuke.");
185
+ return;
186
+ }
187
+
188
+ // Skip confirmation if --yes flag or non-interactive (piped input)
189
+ if (skipConfirm || !process.stdin.isTTY) {
190
+ const count = await deleteAllSessions();
191
+ console.log(`Nuked ${count} session(s) and all data.`);
192
+ return;
193
+ }
194
+
195
+ const renderer = await createRenderer();
196
+ const choice = await showNukeConfirm(renderer, sessions.length);
197
+ destroyRenderer();
198
+
199
+ if (choice === "confirm") {
200
+ const count = await deleteAllSessions();
201
+ console.log(`Nuked ${count} session(s) and all data.`);
202
+ } else {
203
+ console.log("Cancelled.");
204
+ }
205
+ }
206
+
207
+ export function parseRepos(args: string[]): RepoSpec[] {
208
+ const repos: RepoSpec[] = [];
209
+
210
+ for (const arg of args) {
211
+ if (!arg || arg.startsWith("-")) continue;
212
+
213
+ if (!arg.includes("/")) {
214
+ throw new Error(`Invalid repo format: ${arg} (expected owner/repo)`);
215
+ }
216
+
217
+ const repo = parseRepoSpec(arg);
218
+ repos.push(repo);
219
+ }
220
+
221
+ return repos;
222
+ }
223
+
224
+ export async function handleCLIMode(args: string[]): Promise<void> {
225
+ const firstArg = args[0];
226
+
227
+ if (firstArg === "--list" || firstArg === "-l") {
228
+ await handleList();
229
+ } else if (firstArg === "--resume" || firstArg === "-r") {
230
+ const sessionName = args[1];
231
+ if (!sessionName) {
232
+ console.error("Missing session name. Usage: letmecook --cli --resume <session-name>");
233
+ process.exit(1);
234
+ }
235
+ await handleResume(sessionName);
236
+ } else if (firstArg === "--delete" || firstArg === "-d") {
237
+ const sessionName = args[1];
238
+ if (!sessionName) {
239
+ console.error("Missing session name. Usage: letmecook --cli --delete <session-name>");
240
+ process.exit(1);
241
+ }
242
+ await handleDelete(sessionName);
243
+ } else if (firstArg === "--nuke") {
244
+ const hasYes = args.includes("--yes") || args.includes("-y");
245
+ await handleNuke(hasYes);
246
+ } else if (!firstArg || firstArg.startsWith("-")) {
247
+ // No args or unknown flag after --cli
248
+ if (firstArg?.startsWith("-")) {
249
+ console.error(`Unknown CLI option: ${firstArg}`);
250
+ }
251
+ printCLIUsage();
252
+ process.exit(firstArg ? 1 : 0);
253
+ } else {
254
+ try {
255
+ const repos = parseRepos(args);
256
+ if (repos.length === 0) {
257
+ printCLIUsage();
258
+ process.exit(1);
259
+ }
260
+ await handleNewSessionCLI(repos);
261
+ } catch (error) {
262
+ console.error("Error:", error instanceof Error ? error.message : error);
263
+ process.exit(1);
264
+ }
265
+ }
266
+ }
@@ -6,7 +6,6 @@ import { writeAgentsMd } from "../agents-md";
6
6
  import { recordRepoHistory } from "../repo-history";
7
7
  import { showAddReposPrompt } from "../ui/add-repos";
8
8
  import { runCommands, hideCommandRunner, type CommandTask } from "../ui/common/command-runner";
9
- import { ensureReferenceRepo, linkReferenceRepo, ensureReferencesDir } from "../reference-repo";
10
9
  import { rm } from "node:fs/promises";
11
10
 
12
11
  export interface AddReposParams {
@@ -20,10 +19,7 @@ export interface AddReposResult {
20
19
  }
21
20
 
22
21
  function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTask[] {
23
- // Only create command tasks for non-reference repos
24
- const regularRepos = repos.filter((repo) => !repo.reference);
25
-
26
- return regularRepos.map((repo) => {
22
+ return repos.map((repo) => {
27
23
  const url = `https://github.com/${repo.owner}/${repo.name}.git`;
28
24
  const targetDir = join(sessionPath, repo.dir);
29
25
  const args = repo.branch
@@ -49,34 +45,6 @@ function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTas
49
45
  });
50
46
  }
51
47
 
52
- async function setupReferenceRepos(
53
- repos: RepoSpec[],
54
- sessionPath: string,
55
- ): Promise<{ successful: RepoSpec[]; failed: Array<{ repo: RepoSpec; error: string }> }> {
56
- const referenceRepos = repos.filter((repo) => repo.reference);
57
- const successful: RepoSpec[] = [];
58
- const failed: Array<{ repo: RepoSpec; error: string }> = [];
59
-
60
- if (referenceRepos.length === 0) {
61
- return { successful, failed };
62
- }
63
-
64
- await ensureReferencesDir();
65
-
66
- for (const repo of referenceRepos) {
67
- try {
68
- await ensureReferenceRepo(repo);
69
- await linkReferenceRepo(repo, sessionPath);
70
- successful.push(repo);
71
- } catch (error) {
72
- const errorMsg = error instanceof Error ? error.message : String(error);
73
- failed.push({ repo, error: errorMsg });
74
- }
75
- }
76
-
77
- return { successful, failed };
78
- }
79
-
80
48
  export async function addReposFlow(params: AddReposParams): Promise<AddReposResult> {
81
49
  const { renderer, session } = params;
82
50
 
@@ -87,102 +55,70 @@ export async function addReposFlow(params: AddReposParams): Promise<AddReposResu
87
55
  const newRepos = addResult.repos.filter((r) => !existingSpecs.has(r.spec));
88
56
 
89
57
  if (newRepos.length > 0) {
90
- // Setup reference repos first (symlinks from cache)
91
- const { successful: successfulRefs, failed: failedRefs } = await setupReferenceRepos(
92
- newRepos,
93
- session.path,
94
- );
95
-
96
- // Report reference repo failures
97
- if (failedRefs.length > 0) {
98
- console.error(`\n⚠️ ${failedRefs.length} reference repo(s) failed:`);
99
- failedRefs.forEach(({ repo, error }) => {
100
- console.error(` ✗ ${repo.owner}/${repo.name}: ${error}`);
101
- });
102
- }
103
-
104
- // Clone regular repos
105
58
  const tasks = reposToCommandTasks(newRepos, session.path);
106
- let results: Awaited<ReturnType<typeof runCommands>> = [];
107
-
108
- if (tasks.length > 0) {
109
- results = await runCommands(renderer, {
110
- title: "Adding repositories",
111
- tasks,
112
- showOutput: true,
113
- outputLines: 5,
114
- allowAbort: true,
115
- allowSkip: tasks.length > 1,
116
- allowBackground: true,
117
- sessionName: session.name,
118
- });
119
59
 
120
- // Handle aborted operation - return without changes
121
- const wasAborted = results.some((r) => r.outcome === "aborted");
122
- if (wasAborted) {
123
- hideCommandRunner(renderer);
124
- // Clean up any partially cloned repos (non-reference only)
125
- const regularRepos = newRepos.filter((r) => !r.reference);
126
- for (const repo of regularRepos) {
127
- const repoPath = join(session.path, repo.dir);
128
- try {
129
- await rm(repoPath, { recursive: true, force: true });
130
- } catch {
131
- // Ignore cleanup errors
132
- }
133
- }
134
- // Also clean up reference symlinks
135
- for (const repo of successfulRefs) {
136
- const repoPath = join(session.path, repo.dir);
137
- try {
138
- await rm(repoPath, { recursive: true, force: true });
139
- } catch {
140
- // Ignore cleanup errors
141
- }
142
- }
143
- return { session, cancelled: true };
144
- }
60
+ const results = await runCommands(renderer, {
61
+ title: "Adding repositories",
62
+ tasks,
63
+ showOutput: true,
64
+ outputLines: 5,
65
+ allowAbort: true,
66
+ allowSkip: tasks.length > 1,
67
+ allowBackground: true,
68
+ sessionName: session.name,
69
+ });
145
70
 
146
- // Clean up skipped repo directories
147
- const skippedResults = results.filter((r) => r.outcome === "skipped");
148
- for (const skipped of skippedResults) {
149
- const repoSpec = skipped.task.label.replace("Cloning ", "");
150
- const repo = newRepos.find((r) => `${r.owner}/${r.name}` === repoSpec);
151
- if (repo) {
152
- const repoPath = join(session.path, repo.dir);
153
- try {
154
- await rm(repoPath, { recursive: true, force: true });
155
- } catch {
156
- // Ignore cleanup errors
157
- }
71
+ // Handle aborted operation - return without changes
72
+ const wasAborted = results.some((r) => r.outcome === "aborted");
73
+ if (wasAborted) {
74
+ hideCommandRunner(renderer);
75
+ // Clean up any partially cloned repos
76
+ for (const repo of newRepos) {
77
+ const repoPath = join(session.path, repo.dir);
78
+ try {
79
+ await rm(repoPath, { recursive: true, force: true });
80
+ } catch {
81
+ // Ignore cleanup errors
158
82
  }
159
83
  }
84
+ return { session, cancelled: true };
85
+ }
160
86
 
161
- // Check for errors (not skipped/aborted)
162
- const errors = results.filter((r) => r.outcome === "error");
163
- if (errors.length > 0) {
164
- console.error(`\n⚠️ ${errors.length} repository(ies) failed to clone:`);
165
- errors.forEach((err) => {
166
- console.error(` ✗ ${err.task.label}`);
167
- if (err.error) {
168
- console.error(` ${err.error}`);
169
- }
170
- });
87
+ // Clean up skipped repo directories
88
+ const skippedResults = results.filter((r) => r.outcome === "skipped");
89
+ for (const skipped of skippedResults) {
90
+ const repoSpec = skipped.task.label.replace("Cloning ", "");
91
+ const repo = newRepos.find((r) => `${r.owner}/${r.name}` === repoSpec);
92
+ if (repo) {
93
+ const repoPath = join(session.path, repo.dir);
94
+ try {
95
+ await rm(repoPath, { recursive: true, force: true });
96
+ } catch {
97
+ // Ignore cleanup errors
98
+ }
171
99
  }
172
100
  }
173
101
 
174
- await new Promise((resolve) => setTimeout(resolve, 700));
175
- hideCommandRunner(renderer);
176
-
177
102
  // Filter to only successfully cloned repos
178
- const successfulCloned = newRepos.filter((repo) => {
179
- if (repo.reference) return false; // Reference repos handled separately
103
+ const successfulRepos = newRepos.filter((repo) => {
180
104
  const result = results.find((r) => r.task.label === `Cloning ${repo.owner}/${repo.name}`);
181
105
  return result?.outcome === "completed";
182
106
  });
183
107
 
184
- // Combine successful reference and cloned repos
185
- const successfulRepos = [...successfulRefs, ...successfulCloned];
108
+ // Check for errors (not skipped/aborted)
109
+ const errors = results.filter((r) => r.outcome === "error");
110
+ if (errors.length > 0) {
111
+ console.error(`\n⚠️ ${errors.length} repository(ies) failed to clone:`);
112
+ errors.forEach((err) => {
113
+ console.error(` ✗ ${err.task.label}`);
114
+ if (err.error) {
115
+ console.error(` ${err.error}`);
116
+ }
117
+ });
118
+ }
119
+
120
+ await new Promise((resolve) => setTimeout(resolve, 700));
121
+ hideCommandRunner(renderer);
186
122
 
187
123
  if (successfulRepos.length > 0) {
188
124
  const allRepos = [...session.repos, ...successfulRepos];