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 +90 -256
- package/package.json +7 -2
- package/src/agents-md.ts +12 -15
- package/src/chat-logger.ts +220 -0
- package/src/chat-mode.ts +465 -0
- package/src/cli-mode.ts +366 -0
- package/src/config-builder.ts +147 -0
- package/src/env.ts +76 -0
- package/src/flows/add-repos.ts +51 -115
- package/src/flows/chat-to-config.ts +373 -0
- package/src/flows/new-session.ts +69 -145
- package/src/flows/resume-session.ts +33 -37
- package/src/git.ts +39 -77
- package/src/naming.ts +2 -2
- package/src/prompts/chat-prompt.ts +143 -0
- package/src/schemas.ts +82 -0
- package/src/splash.ts +199 -0
- package/src/tui-mode.ts +41 -0
- package/src/types.ts +16 -78
- package/src/ui/add-repos.ts +34 -26
- package/src/ui/agent-proposal.ts +13 -1
- package/src/ui/chat-confirmation.ts +151 -0
- package/src/ui/chat-with-sidebar.ts +524 -0
- package/src/ui/common/clipboard.ts +105 -0
- package/src/ui/common/keyboard.ts +7 -0
- package/src/ui/common/repo-formatter.ts +4 -4
- package/src/ui/cooking-indicator.ts +88 -0
- package/src/ui/main-menu.ts +8 -0
- package/src/ui/new-session.ts +2 -2
- package/src/ui/progress.ts +1 -1
- package/src/ui/renderer.ts +7 -14
- package/src/ui/session-settings.ts +4 -3
- package/src/validation.ts +152 -0
- package/src/reference-repo.ts +0 -288
package/src/flows/new-session.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { recordRepoHistory } from "../repo-history";
|
|
|
8
8
|
import { showAgentProposal } from "../ui/agent-proposal";
|
|
9
9
|
import { showConflictPrompt } from "../ui/conflict";
|
|
10
10
|
import { runCommands, hideCommandRunner, type CommandTask } from "../ui/common/command-runner";
|
|
11
|
-
import { ensureReferenceRepo, linkReferenceRepo, ensureReferencesDir } from "../reference-repo";
|
|
12
11
|
import { rm } from "node:fs/promises";
|
|
13
12
|
|
|
14
13
|
export interface NewSessionParams {
|
|
@@ -25,11 +24,7 @@ export interface NewSessionResult {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTask[] {
|
|
28
|
-
|
|
29
|
-
// Reference repos are handled separately with ensureReferenceRepo + symlink
|
|
30
|
-
const regularRepos = repos.filter((repo) => !repo.reference);
|
|
31
|
-
|
|
32
|
-
return regularRepos.map((repo) => {
|
|
27
|
+
return repos.map((repo) => {
|
|
33
28
|
const url = `https://github.com/${repo.owner}/${repo.name}.git`;
|
|
34
29
|
const targetDir = join(sessionPath, repo.dir);
|
|
35
30
|
const args = repo.branch
|
|
@@ -55,41 +50,6 @@ function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTas
|
|
|
55
50
|
});
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
async function setupReferenceRepos(
|
|
59
|
-
repos: RepoSpec[],
|
|
60
|
-
sessionPath: string,
|
|
61
|
-
onProgress?: (repo: RepoSpec, status: "caching" | "linking" | "done" | "error") => void,
|
|
62
|
-
): Promise<{ successful: RepoSpec[]; failed: Array<{ repo: RepoSpec; error: string }> }> {
|
|
63
|
-
const referenceRepos = repos.filter((repo) => repo.reference);
|
|
64
|
-
const successful: RepoSpec[] = [];
|
|
65
|
-
const failed: Array<{ repo: RepoSpec; error: string }> = [];
|
|
66
|
-
|
|
67
|
-
if (referenceRepos.length === 0) {
|
|
68
|
-
return { successful, failed };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
await ensureReferencesDir();
|
|
72
|
-
|
|
73
|
-
for (const repo of referenceRepos) {
|
|
74
|
-
try {
|
|
75
|
-
onProgress?.(repo, "caching");
|
|
76
|
-
await ensureReferenceRepo(repo);
|
|
77
|
-
|
|
78
|
-
onProgress?.(repo, "linking");
|
|
79
|
-
await linkReferenceRepo(repo, sessionPath);
|
|
80
|
-
|
|
81
|
-
onProgress?.(repo, "done");
|
|
82
|
-
successful.push(repo);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
85
|
-
onProgress?.(repo, "error");
|
|
86
|
-
failed.push({ repo, error: errorMsg });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { successful, failed };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
53
|
function skillsToCommandTasks(skills: string[], sessionPath: string): CommandTask[] {
|
|
94
54
|
return skills.map((skill) => ({
|
|
95
55
|
label: `Installing ${skill}`,
|
|
@@ -128,15 +88,22 @@ export async function createNewSession(
|
|
|
128
88
|
}
|
|
129
89
|
}
|
|
130
90
|
|
|
131
|
-
|
|
132
|
-
|
|
91
|
+
// Generate session name (show UI feedback during generation in TUI mode)
|
|
92
|
+
let sessionName: string;
|
|
133
93
|
if (mode === "tui") {
|
|
134
|
-
showAgentProposal(renderer, {
|
|
135
|
-
sessionName,
|
|
94
|
+
const proposal = showAgentProposal(renderer, {
|
|
95
|
+
sessionName: "generating...",
|
|
136
96
|
repos,
|
|
137
97
|
goal,
|
|
138
98
|
});
|
|
99
|
+
sessionName = await generateSessionName(repos, goal);
|
|
100
|
+
proposal.sessionNameText.content = `Session: ${sessionName}`;
|
|
101
|
+
renderer.requestRender();
|
|
102
|
+
} else {
|
|
103
|
+
sessionName = await generateSessionName(repos, goal);
|
|
104
|
+
}
|
|
139
105
|
|
|
106
|
+
if (mode === "tui") {
|
|
140
107
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
141
108
|
|
|
142
109
|
const session = await createSession(
|
|
@@ -146,89 +113,68 @@ export async function createNewSession(
|
|
|
146
113
|
skills?.length ? skills : undefined,
|
|
147
114
|
);
|
|
148
115
|
|
|
149
|
-
//
|
|
150
|
-
const { successful: successfulRefs, failed: failedRefs } = await setupReferenceRepos(
|
|
151
|
-
repos,
|
|
152
|
-
session.path,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
// Report reference repo failures
|
|
156
|
-
if (failedRefs.length > 0) {
|
|
157
|
-
console.error(`\n⚠️ ${failedRefs.length} reference repo(s) failed:`);
|
|
158
|
-
failedRefs.forEach(({ repo, error }) => {
|
|
159
|
-
console.error(` ✗ ${repo.owner}/${repo.name}: ${error}`);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Build tasks for regular repos + skills
|
|
116
|
+
// Build all tasks (repos + skills)
|
|
164
117
|
const tasks: CommandTask[] = [
|
|
165
118
|
...reposToCommandTasks(repos, session.path),
|
|
166
119
|
...(skills && skills.length > 0 ? skillsToCommandTasks(skills, session.path) : []),
|
|
167
120
|
];
|
|
168
121
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
allowBackground: true,
|
|
180
|
-
sessionName,
|
|
181
|
-
});
|
|
122
|
+
const results = await runCommands(renderer, {
|
|
123
|
+
title: "Setting up session",
|
|
124
|
+
tasks,
|
|
125
|
+
showOutput: true,
|
|
126
|
+
outputLines: 5,
|
|
127
|
+
allowAbort: true,
|
|
128
|
+
allowSkip: tasks.length > 1,
|
|
129
|
+
allowBackground: true,
|
|
130
|
+
sessionName,
|
|
131
|
+
});
|
|
182
132
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
133
|
+
// Handle aborted operation - clean up and return null
|
|
134
|
+
const wasAborted = results.some((r) => r.outcome === "aborted");
|
|
135
|
+
if (wasAborted) {
|
|
136
|
+
hideCommandRunner(renderer);
|
|
137
|
+
// Clean up the partial session
|
|
138
|
+
await deleteSession(session.name);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
191
141
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
142
|
+
// Clean up skipped repo directories
|
|
143
|
+
const skippedResults = results.filter((r) => r.outcome === "skipped");
|
|
144
|
+
for (const skipped of skippedResults) {
|
|
145
|
+
const repoName = skipped.task.label.replace("Cloning ", "").split("/")[1];
|
|
146
|
+
if (repoName) {
|
|
147
|
+
const repoPath = join(session.path, repoName);
|
|
148
|
+
try {
|
|
149
|
+
await rm(repoPath, { recursive: true, force: true });
|
|
150
|
+
} catch {
|
|
151
|
+
// Ignore cleanup errors
|
|
203
152
|
}
|
|
204
153
|
}
|
|
205
|
-
|
|
206
|
-
// Check for errors (not skipped/aborted)
|
|
207
|
-
const errors = results.filter((r) => r.outcome === "error");
|
|
208
|
-
if (errors.length > 0) {
|
|
209
|
-
console.error(`\n⚠️ ${errors.length} task(s) failed:`);
|
|
210
|
-
errors.forEach((err) => {
|
|
211
|
-
console.error(` ✗ ${err.task.label}`);
|
|
212
|
-
if (err.error) {
|
|
213
|
-
console.error(` ${err.error}`);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
154
|
}
|
|
218
155
|
|
|
219
156
|
// Filter out skipped repos from the session
|
|
220
157
|
const successfulRepoResults = results.filter(
|
|
221
158
|
(r) => r.outcome === "completed" && r.task.label.startsWith("Cloning "),
|
|
222
159
|
);
|
|
223
|
-
const
|
|
160
|
+
const successfulRepoSpecs = successfulRepoResults
|
|
224
161
|
.map((r) => {
|
|
225
162
|
const repoSpec = r.task.label.replace("Cloning ", "");
|
|
226
163
|
return repos.find((repo) => `${repo.owner}/${repo.name}` === repoSpec);
|
|
227
164
|
})
|
|
228
165
|
.filter((r): r is RepoSpec => r !== undefined);
|
|
229
166
|
|
|
230
|
-
//
|
|
231
|
-
const
|
|
167
|
+
// Check for errors (not skipped/aborted)
|
|
168
|
+
const errors = results.filter((r) => r.outcome === "error");
|
|
169
|
+
if (errors.length > 0) {
|
|
170
|
+
console.error(`\n⚠️ ${errors.length} task(s) failed:`);
|
|
171
|
+
errors.forEach((err) => {
|
|
172
|
+
console.error(` ✗ ${err.task.label}`);
|
|
173
|
+
if (err.error) {
|
|
174
|
+
console.error(` ${err.error}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
232
178
|
|
|
233
179
|
// Record only successful repos
|
|
234
180
|
if (successfulRepoSpecs.length > 0) {
|
|
@@ -251,52 +197,30 @@ export async function createNewSession(
|
|
|
251
197
|
skills?.length ? skills : undefined,
|
|
252
198
|
);
|
|
253
199
|
|
|
254
|
-
|
|
255
|
-
const referenceRepos = repos.filter((r) => r.reference);
|
|
256
|
-
if (referenceRepos.length > 0) {
|
|
257
|
-
console.log(`\nSetting up ${referenceRepos.length} reference repo(s)...`);
|
|
258
|
-
const { successful: successfulRefs, failed: failedRefs } = await setupReferenceRepos(
|
|
259
|
-
repos,
|
|
260
|
-
session.path,
|
|
261
|
-
);
|
|
262
|
-
successfulRefs.forEach((repo) => {
|
|
263
|
-
console.log(` ✓ ${repo.owner}/${repo.name} (linked)`);
|
|
264
|
-
});
|
|
265
|
-
failedRefs.forEach(({ repo, error }) => {
|
|
266
|
-
console.log(` ✗ ${repo.owner}/${repo.name}: ${error}`);
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Clone regular repos
|
|
271
|
-
const regularRepos = repos.filter((r) => !r.reference);
|
|
272
|
-
if (regularRepos.length > 0) {
|
|
273
|
-
console.log(`\nCloning ${regularRepos.length} repository(ies)...`);
|
|
274
|
-
}
|
|
200
|
+
console.log(`\nCloning ${repos.length} repository(ies)...`);
|
|
275
201
|
|
|
276
202
|
const tasks: CommandTask[] = [
|
|
277
203
|
...reposToCommandTasks(repos, session.path),
|
|
278
204
|
...(skills && skills.length > 0 ? skillsToCommandTasks(skills, session.path) : []),
|
|
279
205
|
];
|
|
280
206
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
});
|
|
207
|
+
const results = await runCommands(renderer, {
|
|
208
|
+
title: "Setting up session",
|
|
209
|
+
tasks,
|
|
210
|
+
showOutput: false, // CLI mode doesn't need visual output
|
|
211
|
+
});
|
|
287
212
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
213
|
+
// Print results
|
|
214
|
+
results.forEach((result) => {
|
|
215
|
+
if (result.success) {
|
|
216
|
+
console.log(` ✓ ${result.task.label}`);
|
|
217
|
+
} else {
|
|
218
|
+
console.log(` ✗ ${result.task.label}`);
|
|
219
|
+
if (result.error) {
|
|
220
|
+
console.log(` ${result.error}`);
|
|
297
221
|
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
300
224
|
|
|
301
225
|
await writeAgentsMd(session);
|
|
302
226
|
await createClaudeMdSymlink(session.path);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CliRenderer } from "@opentui/core";
|
|
2
2
|
import type { Session } from "../types";
|
|
3
3
|
import { updateSessionSettings } from "../sessions";
|
|
4
|
-
import {
|
|
4
|
+
import { refreshLatestRepos } from "../git";
|
|
5
5
|
import { updateSkills } from "../skills";
|
|
6
6
|
import { handleSmartExit } from "../ui/exit";
|
|
7
7
|
import { showSessionSettings } from "../ui/session-settings";
|
|
@@ -10,10 +10,10 @@ import { showSessionStartWarning } from "../ui/background-warning";
|
|
|
10
10
|
import { showProgress, updateProgress, hideProgress } from "../ui/progress";
|
|
11
11
|
import { showReclonePrompt } from "../ui/reclone-prompt";
|
|
12
12
|
import { deleteSession } from "../sessions";
|
|
13
|
+
import { recloneRepo } from "../git";
|
|
13
14
|
import { writeAgentsMd } from "../agents-md";
|
|
14
15
|
import { createRenderer, destroyRenderer } from "../ui/renderer";
|
|
15
16
|
import { getProcessesForSession } from "../process-registry";
|
|
16
|
-
import { repairReferenceLink } from "../reference-repo";
|
|
17
17
|
|
|
18
18
|
export interface ResumeSessionParams {
|
|
19
19
|
session: Session;
|
|
@@ -49,10 +49,10 @@ export async function resumeSession(
|
|
|
49
49
|
while (true) {
|
|
50
50
|
if (mode === "tui" && shouldRefresh) {
|
|
51
51
|
renderer = await createRenderer();
|
|
52
|
-
await
|
|
52
|
+
await refreshLatestBeforeResume(renderer, currentSession);
|
|
53
53
|
destroyRenderer();
|
|
54
54
|
} else if (shouldRefresh) {
|
|
55
|
-
await
|
|
55
|
+
await refreshLatestBeforeResumeSimple(currentSession);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
await runOpencodeMode(mode, currentSession.path);
|
|
@@ -117,23 +117,20 @@ export async function resumeSession(
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
async function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
): Promise<void> {
|
|
124
|
-
const referenceRepos = session.repos.filter((repo) => repo.reference);
|
|
125
|
-
if (referenceRepos.length === 0) return;
|
|
120
|
+
async function refreshLatestBeforeResume(renderer: CliRenderer, session: Session): Promise<void> {
|
|
121
|
+
const readOnlyRepos = session.repos.filter((repo) => repo.readOnly);
|
|
122
|
+
if (readOnlyRepos.length === 0) return;
|
|
126
123
|
|
|
127
|
-
const refreshProgressState = showProgress(renderer,
|
|
124
|
+
const refreshProgressState = showProgress(renderer, readOnlyRepos, {
|
|
128
125
|
title: "Refreshing repositories",
|
|
129
|
-
label: "Refreshing
|
|
126
|
+
label: "Refreshing read-only repositories:",
|
|
130
127
|
initialPhase: "refreshing",
|
|
131
128
|
});
|
|
132
129
|
refreshProgressState.sessionName = session.name;
|
|
133
130
|
updateProgress(renderer, refreshProgressState);
|
|
134
131
|
|
|
135
|
-
const refreshResults = await
|
|
136
|
-
|
|
132
|
+
const refreshResults = await refreshLatestRepos(
|
|
133
|
+
readOnlyRepos,
|
|
137
134
|
session.path,
|
|
138
135
|
(repoIndex, status, outputLines) => {
|
|
139
136
|
const repoState = refreshProgressState.repos[repoIndex];
|
|
@@ -153,60 +150,59 @@ async function refreshReferenceBeforeResume(
|
|
|
153
150
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
154
151
|
hideProgress(renderer);
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
(result) => result.status === "error" && result.repo.reference,
|
|
153
|
+
const recloneTargets = refreshResults.filter(
|
|
154
|
+
(result) => result.status === "error" && result.repo.readOnly,
|
|
159
155
|
);
|
|
160
156
|
|
|
161
|
-
for (const result of
|
|
157
|
+
for (const result of recloneTargets) {
|
|
162
158
|
const choice = await showReclonePrompt(renderer, result.repo);
|
|
163
159
|
|
|
164
160
|
if (choice === "reclone") {
|
|
165
|
-
const
|
|
166
|
-
title: "
|
|
167
|
-
label: "
|
|
161
|
+
const recloneProgressState = showProgress(renderer, [result.repo], {
|
|
162
|
+
title: "Recloning repository",
|
|
163
|
+
label: "Recloning:",
|
|
168
164
|
initialPhase: "cloning",
|
|
169
165
|
});
|
|
170
|
-
|
|
171
|
-
updateProgress(renderer,
|
|
166
|
+
recloneProgressState.sessionName = session.name;
|
|
167
|
+
updateProgress(renderer, recloneProgressState);
|
|
172
168
|
|
|
173
169
|
try {
|
|
174
|
-
await
|
|
175
|
-
const repoState =
|
|
170
|
+
await recloneRepo(result.repo, session.path, (status, outputLines) => {
|
|
171
|
+
const repoState = recloneProgressState.repos[0];
|
|
176
172
|
if (repoState) {
|
|
177
173
|
repoState.status = status;
|
|
178
174
|
if (outputLines) {
|
|
179
|
-
|
|
175
|
+
recloneProgressState.currentOutput = outputLines;
|
|
180
176
|
}
|
|
181
|
-
updateProgress(renderer,
|
|
177
|
+
updateProgress(renderer, recloneProgressState);
|
|
182
178
|
}
|
|
183
179
|
});
|
|
184
180
|
} catch (error) {
|
|
185
|
-
const repoState =
|
|
181
|
+
const repoState = recloneProgressState.repos[0];
|
|
186
182
|
if (repoState) {
|
|
187
183
|
repoState.status = "error";
|
|
188
184
|
}
|
|
189
|
-
|
|
185
|
+
recloneProgressState.currentOutput = [
|
|
190
186
|
error instanceof Error ? error.message : String(error),
|
|
191
187
|
];
|
|
192
|
-
updateProgress(renderer,
|
|
188
|
+
updateProgress(renderer, recloneProgressState);
|
|
193
189
|
}
|
|
194
190
|
|
|
195
|
-
|
|
196
|
-
updateProgress(renderer,
|
|
191
|
+
recloneProgressState.phase = "done";
|
|
192
|
+
updateProgress(renderer, recloneProgressState);
|
|
197
193
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
198
194
|
hideProgress(renderer);
|
|
199
195
|
}
|
|
200
196
|
}
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
async function
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
199
|
+
async function refreshLatestBeforeResumeSimple(session: Session): Promise<void> {
|
|
200
|
+
const readOnlyRepos = session.repos.filter((repo) => repo.readOnly);
|
|
201
|
+
if (readOnlyRepos.length === 0) return;
|
|
206
202
|
|
|
207
|
-
console.log("\nRefreshing
|
|
203
|
+
console.log("\nRefreshing read-only repositories...");
|
|
208
204
|
|
|
209
|
-
const results = await
|
|
205
|
+
const results = await refreshLatestRepos(readOnlyRepos, session.path);
|
|
210
206
|
|
|
211
207
|
if (results.length === 0) return;
|
|
212
208
|
|
package/src/git.ts
CHANGED
|
@@ -193,101 +193,63 @@ export async function sessionHasUncommittedChanges(
|
|
|
193
193
|
return { hasChanges: reposWithChanges.length > 0, reposWithChanges };
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
export async function
|
|
196
|
+
export async function refreshLatestRepos(
|
|
197
197
|
repos: RepoSpec[],
|
|
198
198
|
sessionPath: string,
|
|
199
199
|
onProgress?: (repoIndex: number, status: RefreshProgressStatus, outputLines?: string[]) => void,
|
|
200
200
|
): Promise<RefreshResult[]> {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
await import("./reference-repo");
|
|
204
|
-
|
|
205
|
-
const referenceRepos = repos.filter((repo) => repo.reference);
|
|
206
|
-
if (referenceRepos.length === 0) return [];
|
|
201
|
+
const readOnlyRepos = repos.filter((repo) => repo.readOnly);
|
|
202
|
+
if (readOnlyRepos.length === 0) return [];
|
|
207
203
|
|
|
208
204
|
const results: RefreshResult[] = [];
|
|
209
205
|
|
|
210
|
-
for (const [repoIndex, repo] of
|
|
206
|
+
for (const [repoIndex, repo] of readOnlyRepos.entries()) {
|
|
211
207
|
const repoPath = join(sessionPath, repo.dir);
|
|
208
|
+
const dirty = await hasUncommittedChanges(repoPath);
|
|
212
209
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const isValid = await verifyReferenceLink(repo, sessionPath);
|
|
219
|
-
if (!isValid) {
|
|
220
|
-
onProgress?.(repoIndex, "refreshing", [`Repairing link to ${repo.owner}/${repo.name}...`]);
|
|
221
|
-
try {
|
|
222
|
-
await repairReferenceLink(repo, sessionPath, (status, lines) => {
|
|
223
|
-
if (status === "cloning") onProgress?.(repoIndex, "refreshing", lines);
|
|
224
|
-
});
|
|
225
|
-
} catch (error) {
|
|
226
|
-
results.push({
|
|
227
|
-
repo,
|
|
228
|
-
status: "error",
|
|
229
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
230
|
-
});
|
|
231
|
-
onProgress?.(repoIndex, "error", [
|
|
232
|
-
error instanceof Error ? error.message : String(error),
|
|
233
|
-
]);
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Refresh the cached reference repo
|
|
239
|
-
const result = await refreshReferenceRepo(repo, (status, lines) => {
|
|
240
|
-
onProgress?.(repoIndex, status, lines);
|
|
210
|
+
if (dirty) {
|
|
211
|
+
results.push({
|
|
212
|
+
repo,
|
|
213
|
+
status: "skipped",
|
|
214
|
+
reason: "uncommitted changes",
|
|
241
215
|
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const dirty = await hasUncommittedChanges(repoPath);
|
|
246
|
-
|
|
247
|
-
if (dirty) {
|
|
248
|
-
results.push({
|
|
249
|
-
repo,
|
|
250
|
-
status: "skipped",
|
|
251
|
-
reason: "uncommitted changes",
|
|
252
|
-
});
|
|
253
|
-
onProgress?.(repoIndex, "skipped", ["Skipped: uncommitted changes"]);
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
216
|
+
onProgress?.(repoIndex, "skipped", ["Skipped: uncommitted changes"]);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
256
219
|
|
|
257
|
-
|
|
220
|
+
onProgress?.(repoIndex, "refreshing", [`Pulling ${repo.owner}/${repo.name}...`]);
|
|
258
221
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const { success, output, fullOutput } = await readProcessOutputWithBuffer(proc, {
|
|
265
|
-
maxBufferLines: 5,
|
|
266
|
-
onBufferUpdate: (buffer) => onProgress?.(repoIndex, "refreshing", buffer),
|
|
267
|
-
});
|
|
268
|
-
const exitCode = success ? 0 : 1;
|
|
269
|
-
|
|
270
|
-
if (exitCode !== 0) {
|
|
271
|
-
const reason = fullOutput.trim() || `git pull exited with code ${exitCode}`;
|
|
272
|
-
results.push({
|
|
273
|
-
repo,
|
|
274
|
-
status: "error",
|
|
275
|
-
reason,
|
|
276
|
-
});
|
|
277
|
-
onProgress?.(repoIndex, "error", output.length > 0 ? [...output] : [reason]);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
222
|
+
const proc = Bun.spawn(["git", "-C", repoPath, "pull", "--ff-only", "--depth", "1"], {
|
|
223
|
+
stdout: "pipe",
|
|
224
|
+
stderr: "pipe",
|
|
225
|
+
});
|
|
280
226
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
227
|
+
const { success, output, fullOutput } = await readProcessOutputWithBuffer(proc, {
|
|
228
|
+
maxBufferLines: 5,
|
|
229
|
+
onBufferUpdate: (buffer) => onProgress?.(repoIndex, "refreshing", buffer),
|
|
230
|
+
});
|
|
231
|
+
const exitCode = success ? 0 : 1;
|
|
284
232
|
|
|
233
|
+
if (exitCode !== 0) {
|
|
234
|
+
const reason = fullOutput.trim() || `git pull exited with code ${exitCode}`;
|
|
285
235
|
results.push({
|
|
286
236
|
repo,
|
|
287
|
-
status:
|
|
237
|
+
status: "error",
|
|
238
|
+
reason,
|
|
288
239
|
});
|
|
289
|
-
onProgress?.(repoIndex,
|
|
240
|
+
onProgress?.(repoIndex, "error", output.length > 0 ? [...output] : [reason]);
|
|
241
|
+
continue;
|
|
290
242
|
}
|
|
243
|
+
|
|
244
|
+
const normalized = fullOutput.toLowerCase();
|
|
245
|
+
const upToDate =
|
|
246
|
+
normalized.includes("already up to date") || normalized.includes("already up-to-date");
|
|
247
|
+
|
|
248
|
+
results.push({
|
|
249
|
+
repo,
|
|
250
|
+
status: upToDate ? "up-to-date" : "updated",
|
|
251
|
+
});
|
|
252
|
+
onProgress?.(repoIndex, upToDate ? "up-to-date" : "updated", [...output]);
|
|
291
253
|
}
|
|
292
254
|
|
|
293
255
|
return results;
|
package/src/naming.ts
CHANGED
|
@@ -7,7 +7,7 @@ export async function generateSessionName(repos: RepoSpec[], goal?: string): Pro
|
|
|
7
7
|
|
|
8
8
|
try {
|
|
9
9
|
const { text } = await generateText({
|
|
10
|
-
model: "xai/grok-
|
|
10
|
+
model: "xai/grok-3-mini-fast",
|
|
11
11
|
temperature: 1.2, // Higher temperature for more creative/varied names
|
|
12
12
|
prompt: `Generate a creative, memorable session name for a coding workspace.
|
|
13
13
|
|
|
@@ -49,7 +49,7 @@ Reply with ONLY the name, nothing else. No quotes, no explanation.`,
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function generateFallbackName(repos: RepoSpec[]): string {
|
|
52
|
+
export function generateFallbackName(repos: RepoSpec[]): string {
|
|
53
53
|
// Simple fallback: first repo name + short hash
|
|
54
54
|
const base = repos[0]?.name || "session";
|
|
55
55
|
const hash = Math.random().toString(36).slice(2, 6);
|