letmecook 0.0.1 → 0.0.2

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,107 @@
1
+ import type { Session, RepoSpec } from "../types";
2
+ import { updateSessionRepos, updateSessionSkills, deleteSession } from "../sessions";
3
+ import { cloneAllRepos } from "../git";
4
+ import { writeAgentsMd } from "../agents-md";
5
+ import { recordRepoHistory } from "../repo-history";
6
+ import { removeSkillFromSession } from "../skills";
7
+ import { addSkillToSession } from "../skills";
8
+
9
+ export interface EditSessionParams {
10
+ session: Session;
11
+ updates: {
12
+ repos?: RepoSpec[];
13
+ goal?: string;
14
+ skills?: string[];
15
+ };
16
+ }
17
+
18
+ export async function editSession(params: EditSessionParams): Promise<Session | null> {
19
+ const { session, updates } = params;
20
+
21
+ if (!updates.repos && !updates.goal && !updates.skills) {
22
+ return session;
23
+ }
24
+
25
+ let currentSession = session;
26
+
27
+ if (updates.repos) {
28
+ const existingSpecs = new Set(session.repos.map((r) => r.spec));
29
+ const newRepos = updates.repos.filter((r) => !existingSpecs.has(r.spec));
30
+
31
+ if (newRepos.length > 0) {
32
+ console.log(`\nCloning ${newRepos.length} new repository(ies)...`);
33
+
34
+ await cloneAllRepos(newRepos, currentSession.path, (repoIndex, status) => {
35
+ const repo = newRepos[repoIndex];
36
+ if (repo) {
37
+ if (status === "done") {
38
+ console.log(` ✓ ${repo.owner}/${repo.name}`);
39
+ } else if (status === "error") {
40
+ console.log(` ✗ ${repo.owner}/${repo.name} (failed)`);
41
+ }
42
+ }
43
+ });
44
+
45
+ await recordRepoHistory(newRepos);
46
+ await writeAgentsMd(currentSession);
47
+
48
+ console.log("\n✅ Repositories added.\n");
49
+ }
50
+
51
+ const updatedSession = await updateSessionRepos(session.name, updates.repos);
52
+ if (updatedSession) {
53
+ currentSession = updatedSession;
54
+ }
55
+ }
56
+
57
+ if (updates.skills) {
58
+ const existingSkills = new Set(session.skills || []);
59
+ const newSkills = updates.skills.filter((s) => !existingSkills.has(s));
60
+
61
+ if (newSkills.length > 0) {
62
+ console.log(`\nAdding ${newSkills.length} skill package(s)...`);
63
+
64
+ for (const skill of newSkills) {
65
+ console.log(` Adding ${skill}...`);
66
+ const { success } = await addSkillToSession(currentSession, skill, (output) => {
67
+ console.log(` ${output}`);
68
+ });
69
+
70
+ if (success) {
71
+ console.log(` ✓ ${skill}`);
72
+ } else {
73
+ console.log(` ✗ ${skill} (addition failed)`);
74
+ }
75
+ }
76
+
77
+ const allSkills = [...(session.skills || []), ...newSkills];
78
+ const updatedSession = await updateSessionSkills(currentSession.name, allSkills);
79
+
80
+ if (updatedSession) {
81
+ currentSession = updatedSession;
82
+ await writeAgentsMd(currentSession);
83
+ console.log("\n✅ Skills added.\n");
84
+ }
85
+ }
86
+ }
87
+
88
+ if (updates.goal !== undefined) {
89
+ const sessionGoal = updates.goal || undefined;
90
+ const updatedSession = { ...currentSession, goal: sessionGoal };
91
+
92
+ await writeAgentsMd(updatedSession);
93
+ currentSession = updatedSession as Session;
94
+ }
95
+
96
+ return currentSession;
97
+ }
98
+
99
+ export async function nukeSession(session: Session): Promise<boolean> {
100
+ const success = await deleteSession(session.name);
101
+ return success;
102
+ }
103
+
104
+ export async function removeSkill(session: Session, skillString: string): Promise<Session> {
105
+ const updatedSession = await removeSkillFromSession(session, skillString);
106
+ return updatedSession || session;
107
+ }
@@ -0,0 +1,5 @@
1
+ export { createNewSession, type NewSessionParams, type NewSessionResult } from "./new-session";
2
+ export { resumeSession, type ResumeSessionParams, type ResumeResult } from "./resume-session";
3
+ export { editSession, nukeSession, removeSkill, type EditSessionParams } from "./edit-session";
4
+ export { addSkillsFlow, type AddSkillsParams, type AddSkillsResult } from "./add-skills";
5
+ export { addReposFlow, type AddReposParams, type AddReposResult } from "./add-repos";
@@ -0,0 +1,182 @@
1
+ import type { CliRenderer } from "@opentui/core";
2
+ import type { Session, RepoSpec } from "../types";
3
+ import { findMatchingSession, createSession, deleteSession } from "../sessions";
4
+ import { cloneAllRepos } from "../git";
5
+ import { generateSessionName } from "../naming";
6
+ import { writeAgentsMd, createClaudeMdSymlink } from "../agents-md";
7
+ import { addSkillToSession } from "../skills";
8
+ import { recordRepoHistory } from "../repo-history";
9
+ import { showProgress, updateProgress, hideProgress } from "../ui/progress";
10
+ import { showAgentProposal } from "../ui/agent-proposal";
11
+ import { showConflictPrompt } from "../ui/conflict";
12
+
13
+ export interface NewSessionParams {
14
+ repos: RepoSpec[];
15
+ goal?: string;
16
+ skills?: string[];
17
+ mode: "cli" | "tui";
18
+ skipConflictCheck?: boolean;
19
+ }
20
+
21
+ export interface NewSessionResult {
22
+ session: Session;
23
+ skipped?: boolean;
24
+ }
25
+
26
+ export async function createNewSession(
27
+ renderer: CliRenderer,
28
+ params: NewSessionParams,
29
+ ): Promise<NewSessionResult | null> {
30
+ const { repos, goal, skills, mode, skipConflictCheck = false } = params;
31
+
32
+ try {
33
+ if (!skipConflictCheck) {
34
+ const existingSession = await findMatchingSession(repos);
35
+
36
+ if (existingSession) {
37
+ const choice = await showConflictPrompt(renderer, existingSession);
38
+
39
+ switch (choice) {
40
+ case "resume":
41
+ return { session: existingSession, skipped: true };
42
+
43
+ case "nuke":
44
+ await deleteSession(existingSession.name);
45
+ break;
46
+
47
+ case "new":
48
+ break;
49
+
50
+ case "cancel":
51
+ return null;
52
+ }
53
+ }
54
+ }
55
+
56
+ const progressState = showProgress(renderer, repos);
57
+
58
+ progressState.phase = "naming";
59
+ updateProgress(renderer, progressState);
60
+
61
+ const sessionName = await generateSessionName(repos, goal);
62
+ progressState.sessionName = sessionName;
63
+ progressState.phase = "cloning";
64
+ updateProgress(renderer, progressState);
65
+
66
+ if (mode === "tui") {
67
+ progressState.phase = "proposal";
68
+ updateProgress(renderer, progressState);
69
+
70
+ showAgentProposal(renderer, {
71
+ sessionName,
72
+ repos,
73
+ goal,
74
+ });
75
+
76
+ await new Promise((resolve) => setTimeout(resolve, 3000));
77
+
78
+ const cloneProgressState = showProgress(renderer, repos);
79
+ cloneProgressState.sessionName = sessionName;
80
+ cloneProgressState.phase = "cloning";
81
+ updateProgress(renderer, cloneProgressState);
82
+
83
+ const session = await createSession(
84
+ sessionName,
85
+ repos,
86
+ goal,
87
+ skills?.length ? skills : undefined,
88
+ );
89
+
90
+ await cloneAllRepos(repos, session.path, (repoIndex, status, outputLines) => {
91
+ const repoState = cloneProgressState.repos[repoIndex];
92
+ if (repoState) {
93
+ repoState.status = status;
94
+ if (outputLines) {
95
+ cloneProgressState.currentOutput = outputLines;
96
+ }
97
+ updateProgress(renderer, cloneProgressState);
98
+ }
99
+ });
100
+
101
+ if (skills && skills.length > 0) {
102
+ cloneProgressState.phase = "installing-skills";
103
+ updateProgress(renderer, cloneProgressState);
104
+
105
+ for (const skill of skills) {
106
+ console.log(`Installing skill: ${skill}...`);
107
+ const { success } = await addSkillToSession(session, skill, (output) => {
108
+ console.log(` ${output}`);
109
+ });
110
+
111
+ if (success) {
112
+ console.log(`✓ ${skill}`);
113
+ } else {
114
+ console.log(`✗ ${skill} (installation failed)`);
115
+ }
116
+ }
117
+ }
118
+
119
+ await recordRepoHistory(repos);
120
+
121
+ cloneProgressState.phase = "done";
122
+ updateProgress(renderer, cloneProgressState);
123
+
124
+ await new Promise((resolve) => setTimeout(resolve, 1000));
125
+
126
+ hideProgress(renderer);
127
+
128
+ await writeAgentsMd(session);
129
+ await createClaudeMdSymlink(session.path);
130
+
131
+ return { session };
132
+ } else {
133
+ const session = await createSession(
134
+ sessionName,
135
+ repos,
136
+ goal,
137
+ skills?.length ? skills : undefined,
138
+ );
139
+
140
+ await cloneAllRepos(repos, session.path, (repoIndex, status) => {
141
+ const repoState = progressState.repos[repoIndex];
142
+ if (repoState) {
143
+ repoState.status = status;
144
+ updateProgress(renderer, progressState);
145
+ }
146
+ });
147
+
148
+ if (skills && skills.length > 0) {
149
+ progressState.phase = "installing-skills";
150
+ updateProgress(renderer, progressState);
151
+
152
+ for (const skill of skills) {
153
+ console.log(`Installing skill: ${skill}...`);
154
+ const { success } = await addSkillToSession(session, skill, (output) => {
155
+ console.log(` ${output}`);
156
+ });
157
+
158
+ if (success) {
159
+ console.log(`✓ ${skill}`);
160
+ } else {
161
+ console.log(`✗ ${skill} (installation failed)`);
162
+ }
163
+ }
164
+ }
165
+
166
+ await writeAgentsMd(session);
167
+ await createClaudeMdSymlink(session.path);
168
+
169
+ progressState.phase = "done";
170
+ updateProgress(renderer, progressState);
171
+
172
+ await new Promise((resolve) => setTimeout(resolve, 500));
173
+
174
+ hideProgress(renderer);
175
+
176
+ return { session };
177
+ }
178
+ } catch (error) {
179
+ hideProgress(renderer);
180
+ throw error;
181
+ }
182
+ }
@@ -0,0 +1,231 @@
1
+ import type { CliRenderer } from "@opentui/core";
2
+ import type { Session } from "../types";
3
+ import { updateSessionSettings } from "../sessions";
4
+ import { refreshLatestRepos } from "../git";
5
+ import { updateSkills } from "../skills";
6
+ import { handleSmartExit } from "../ui/exit";
7
+ import { showSessionSettings } from "../ui/session-settings";
8
+ import { showDeleteConfirm } from "../ui/confirm-delete";
9
+ import { showProgress, updateProgress, hideProgress } from "../ui/progress";
10
+ import { showReclonePrompt } from "../ui/reclone-prompt";
11
+ import { deleteSession } from "../sessions";
12
+ import { recloneRepo } from "../git";
13
+ import { writeAgentsMd } from "../agents-md";
14
+ import { createRenderer, destroyRenderer } from "../ui/renderer";
15
+
16
+ export interface ResumeSessionParams {
17
+ session: Session;
18
+ mode: "cli" | "tui";
19
+ initialRefresh?: boolean;
20
+ }
21
+
22
+ export type ResumeResult = "resume" | "home";
23
+
24
+ export async function resumeSession(
25
+ renderer: CliRenderer,
26
+ params: ResumeSessionParams,
27
+ ): Promise<ResumeResult> {
28
+ const { session, mode, initialRefresh = true } = params;
29
+
30
+ let currentSession = session;
31
+ let shouldRefresh = initialRefresh;
32
+
33
+ while (true) {
34
+ if (mode === "tui" && shouldRefresh) {
35
+ renderer = await createRenderer();
36
+ await refreshLatestBeforeResume(renderer, currentSession);
37
+ destroyRenderer();
38
+ } else if (shouldRefresh) {
39
+ await refreshLatestBeforeResumeSimple(currentSession);
40
+ }
41
+
42
+ await runOpencodeMode(mode, currentSession.path);
43
+ shouldRefresh = true;
44
+
45
+ if (mode === "tui") {
46
+ renderer = await createRenderer();
47
+ }
48
+
49
+ while (true) {
50
+ const exitResult = await handleSmartExit(renderer, currentSession);
51
+
52
+ if (exitResult.action === "resume") {
53
+ break;
54
+ }
55
+
56
+ if (exitResult.action === "home") {
57
+ destroyRenderer();
58
+ return "home";
59
+ }
60
+
61
+ if (exitResult.action === "delete") {
62
+ const choice = await showDeleteConfirm(renderer, currentSession);
63
+ if (choice === "confirm") {
64
+ await deleteSession(currentSession.name);
65
+ }
66
+ destroyRenderer();
67
+ return "home";
68
+ }
69
+
70
+ if (exitResult.action === "edit") {
71
+ const editResult = await showSessionSettings(renderer, currentSession);
72
+
73
+ if (
74
+ editResult.action === "saved" ||
75
+ editResult.action === "add-repos" ||
76
+ editResult.action === "add-skills"
77
+ ) {
78
+ const saved = await updateSessionSettings(editResult.session.name, {
79
+ repos: editResult.session.repos,
80
+ goal: editResult.session.goal,
81
+ skills: editResult.session.skills,
82
+ });
83
+
84
+ if (saved) {
85
+ currentSession = saved;
86
+ await writeAgentsMd(currentSession);
87
+ }
88
+ }
89
+
90
+ if (editResult.action === "add-repos") {
91
+ console.log("[TODO] Add repos flow - needs to be implemented");
92
+ }
93
+
94
+ if (editResult.action === "add-skills") {
95
+ console.log("[TODO] Add skills flow - needs to be implemented");
96
+ }
97
+ }
98
+ }
99
+
100
+ destroyRenderer();
101
+ }
102
+ }
103
+
104
+ async function refreshLatestBeforeResume(renderer: CliRenderer, session: Session): Promise<void> {
105
+ const latestRepos = session.repos.filter((repo) => repo.latest);
106
+ if (latestRepos.length === 0) return;
107
+
108
+ const refreshProgressState = showProgress(renderer, latestRepos, {
109
+ title: "Refreshing repositories",
110
+ label: "Refreshing latest repositories:",
111
+ initialPhase: "refreshing",
112
+ });
113
+ refreshProgressState.sessionName = session.name;
114
+ updateProgress(renderer, refreshProgressState);
115
+
116
+ const refreshResults = await refreshLatestRepos(
117
+ latestRepos,
118
+ session.path,
119
+ (repoIndex, status, outputLines) => {
120
+ const repoState = refreshProgressState.repos[repoIndex];
121
+ if (repoState) {
122
+ repoState.status = status;
123
+ if (outputLines) {
124
+ refreshProgressState.currentOutput = outputLines;
125
+ }
126
+ updateProgress(renderer, refreshProgressState);
127
+ }
128
+ },
129
+ );
130
+
131
+ refreshProgressState.phase = "done";
132
+ updateProgress(renderer, refreshProgressState);
133
+
134
+ await new Promise((resolve) => setTimeout(resolve, 700));
135
+ hideProgress(renderer);
136
+
137
+ const recloneTargets = refreshResults.filter(
138
+ (result) => result.status === "error" && result.repo.readOnly,
139
+ );
140
+
141
+ for (const result of recloneTargets) {
142
+ const choice = await showReclonePrompt(renderer, result.repo);
143
+
144
+ if (choice === "reclone") {
145
+ const recloneProgressState = showProgress(renderer, [result.repo], {
146
+ title: "Recloning repository",
147
+ label: "Recloning:",
148
+ initialPhase: "cloning",
149
+ });
150
+ recloneProgressState.sessionName = session.name;
151
+ updateProgress(renderer, recloneProgressState);
152
+
153
+ try {
154
+ await recloneRepo(result.repo, session.path, (status, outputLines) => {
155
+ const repoState = recloneProgressState.repos[0];
156
+ if (repoState) {
157
+ repoState.status = status;
158
+ if (outputLines) {
159
+ recloneProgressState.currentOutput = outputLines;
160
+ }
161
+ updateProgress(renderer, recloneProgressState);
162
+ }
163
+ });
164
+ } catch (error) {
165
+ const repoState = recloneProgressState.repos[0];
166
+ if (repoState) {
167
+ repoState.status = "error";
168
+ }
169
+ recloneProgressState.currentOutput = [
170
+ error instanceof Error ? error.message : String(error),
171
+ ];
172
+ updateProgress(renderer, recloneProgressState);
173
+ }
174
+
175
+ recloneProgressState.phase = "done";
176
+ updateProgress(renderer, recloneProgressState);
177
+ await new Promise((resolve) => setTimeout(resolve, 700));
178
+ hideProgress(renderer);
179
+ }
180
+ }
181
+ }
182
+
183
+ async function refreshLatestBeforeResumeSimple(session: Session): Promise<void> {
184
+ const latestRepos = session.repos.filter((repo) => repo.latest);
185
+ if (latestRepos.length === 0) return;
186
+
187
+ console.log("\nRefreshing latest repositories...");
188
+
189
+ const results = await refreshLatestRepos(latestRepos, session.path);
190
+
191
+ if (results.length === 0) return;
192
+
193
+ results.forEach((result) => {
194
+ const icon =
195
+ result.status === "updated"
196
+ ? "✓"
197
+ : result.status === "up-to-date"
198
+ ? "•"
199
+ : result.status === "skipped"
200
+ ? "↷"
201
+ : "✗";
202
+ const reason = result.reason ? ` (${result.reason})` : "";
203
+ console.log(` ${icon} ${result.repo.owner}/${result.repo.name}${reason}`);
204
+ });
205
+
206
+ await updateSkillsSimple(session);
207
+ }
208
+
209
+ async function updateSkillsSimple(session: Session): Promise<void> {
210
+ if (!session.skills || session.skills.length === 0) return;
211
+
212
+ console.log("\nUpdating skills...");
213
+ const { success } = await updateSkills(session, (output) => {
214
+ console.log(` ${output}`);
215
+ });
216
+
217
+ if (!success) {
218
+ console.log(" ⚠️ Skills update failed (continuing anyway)");
219
+ }
220
+ }
221
+
222
+ async function runOpencodeMode(mode: "cli" | "tui", sessionPath: string): Promise<void> {
223
+ const proc = Bun.spawn(["claude", "--dangerously-skip-permissions"], {
224
+ cwd: sessionPath,
225
+ stdin: "inherit",
226
+ stdout: "inherit",
227
+ stderr: "inherit",
228
+ env: { ...process.env, IS_DEMO: mode === "tui" ? "1" : undefined },
229
+ });
230
+ await proc.exited;
231
+ }