letmecook 0.0.15 → 0.0.17
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/package.json +1 -1
- package/src/agents-md.ts +16 -27
- package/src/flows/add-repos.ts +132 -22
- package/src/flows/edit-session.ts +86 -15
- package/src/flows/new-session.ts +159 -34
- package/src/flows/resume-session.ts +53 -33
- package/src/git.ts +77 -39
- package/src/process-registry.ts +179 -0
- package/src/reference-repo.ts +288 -0
- package/src/tui-mode.ts +14 -1
- package/src/types.ts +2 -4
- package/src/ui/add-repos.ts +26 -70
- package/src/ui/background-warning.ts +196 -0
- package/src/ui/common/command-runner.ts +270 -69
- package/src/ui/common/keyboard.ts +26 -0
- package/src/ui/common/repo-formatter.ts +4 -9
- package/src/ui/new-session.ts +2 -3
- package/src/ui/progress.ts +1 -1
- package/src/ui/session-settings.ts +2 -17
- package/src/utils/stream.ts +89 -0
package/package.json
CHANGED
package/src/agents-md.ts
CHANGED
|
@@ -11,22 +11,19 @@ export function generateAgentsMd(session: Session): string {
|
|
|
11
11
|
minute: "2-digit",
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
const hasLatestRepos = session.repos.some((repo) => repo.latest);
|
|
14
|
+
const hasReferenceRepos = session.repos.some((repo) => repo.reference);
|
|
16
15
|
const hasSkills = session.skills && session.skills.length > 0;
|
|
17
16
|
|
|
18
17
|
const repoRows = session.repos
|
|
19
18
|
.map((repo) => {
|
|
20
19
|
const branch = repo.branch || "default";
|
|
21
20
|
const url = `https://github.com/${repo.owner}/${repo.name}`;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
return `| \`${repo.dir}/\` | [${repo.owner}/${repo.name}](${url}) | ${branch} | ${readOnlyStatus} | ${latestStatus} |`;
|
|
21
|
+
const referenceStatus = repo.reference ? "**YES**" : "no";
|
|
22
|
+
return `| \`${repo.dir}/\` | [${repo.owner}/${repo.name}](${url}) | ${branch} | ${referenceStatus} |`;
|
|
25
23
|
})
|
|
26
24
|
.join("\n");
|
|
27
25
|
|
|
28
|
-
const
|
|
29
|
-
const latestRepos = session.repos.filter((repo) => repo.latest);
|
|
26
|
+
const referenceRepos = session.repos.filter((repo) => repo.reference);
|
|
30
27
|
|
|
31
28
|
const skillsSection = hasSkills
|
|
32
29
|
? `
|
|
@@ -40,31 +37,24 @@ These skills are available for use in this session and are automatically updated
|
|
|
40
37
|
`
|
|
41
38
|
: "";
|
|
42
39
|
|
|
43
|
-
const
|
|
40
|
+
const referenceWarning = hasReferenceRepos
|
|
44
41
|
? `
|
|
45
|
-
## ⚠️
|
|
42
|
+
## ⚠️ Reference Repositories
|
|
46
43
|
|
|
47
|
-
**WARNING: The following repositories are
|
|
44
|
+
**WARNING: The following repositories are REFERENCES (symlinked from shared cache):**
|
|
48
45
|
|
|
49
|
-
${
|
|
46
|
+
${referenceRepos.map((repo) => `- \`${repo.dir}/\` (${repo.owner}/${repo.name})`).join("\n")}
|
|
50
47
|
|
|
51
48
|
**AI agents must NOT:**
|
|
52
49
|
- Create, modify, or delete any files in these directories
|
|
53
50
|
- Make commits affecting these repositories
|
|
54
51
|
- Use bash commands to circumvent file permissions
|
|
55
52
|
|
|
56
|
-
**Why are these
|
|
57
|
-
These
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const latestNotice = hasLatestRepos
|
|
62
|
-
? `
|
|
63
|
-
## 🔄 Latest Repositories
|
|
64
|
-
|
|
65
|
-
These repositories are pinned to **Latest** and will be refreshed before resuming the session (only if clean).
|
|
66
|
-
|
|
67
|
-
${latestRepos.map((repo) => `- \`${repo.dir}/\` (${repo.owner}/${repo.name})`).join("\n")}
|
|
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
|
|
68
58
|
`
|
|
69
59
|
: "";
|
|
70
60
|
|
|
@@ -76,11 +66,10 @@ ${session.goal ? `> ${session.goal}\n` : ""}
|
|
|
76
66
|
|
|
77
67
|
## Repositories
|
|
78
68
|
|
|
79
|
-
| Directory | Repository | Branch |
|
|
80
|
-
|
|
69
|
+
| Directory | Repository | Branch | Reference |
|
|
70
|
+
|-----------|------------|--------|-----------|
|
|
81
71
|
${repoRows}
|
|
82
|
-
${
|
|
83
|
-
${latestNotice}
|
|
72
|
+
${referenceWarning}
|
|
84
73
|
${skillsSection}
|
|
85
74
|
## Important Notes
|
|
86
75
|
|
package/src/flows/add-repos.ts
CHANGED
|
@@ -6,6 +6,8 @@ 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
|
+
import { rm } from "node:fs/promises";
|
|
9
11
|
|
|
10
12
|
export interface AddReposParams {
|
|
11
13
|
renderer: CliRenderer;
|
|
@@ -18,7 +20,10 @@ export interface AddReposResult {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTask[] {
|
|
21
|
-
|
|
23
|
+
// Only create command tasks for non-reference repos
|
|
24
|
+
const regularRepos = repos.filter((repo) => !repo.reference);
|
|
25
|
+
|
|
26
|
+
return regularRepos.map((repo) => {
|
|
22
27
|
const url = `https://github.com/${repo.owner}/${repo.name}.git`;
|
|
23
28
|
const targetDir = join(sessionPath, repo.dir);
|
|
24
29
|
const args = repo.branch
|
|
@@ -44,6 +49,34 @@ function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTas
|
|
|
44
49
|
});
|
|
45
50
|
}
|
|
46
51
|
|
|
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
|
+
|
|
47
80
|
export async function addReposFlow(params: AddReposParams): Promise<AddReposResult> {
|
|
48
81
|
const { renderer, session } = params;
|
|
49
82
|
|
|
@@ -54,38 +87,115 @@ export async function addReposFlow(params: AddReposParams): Promise<AddReposResu
|
|
|
54
87
|
const newRepos = addResult.repos.filter((r) => !existingSpecs.has(r.spec));
|
|
55
88
|
|
|
56
89
|
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
|
|
57
105
|
const tasks = reposToCommandTasks(newRepos, session.path);
|
|
106
|
+
let results: Awaited<ReturnType<typeof runCommands>> = [];
|
|
58
107
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
});
|
|
65
119
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
133
|
}
|
|
75
|
-
|
|
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
|
+
}
|
|
145
|
+
|
|
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
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
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
|
+
});
|
|
171
|
+
}
|
|
76
172
|
}
|
|
77
173
|
|
|
78
174
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
79
175
|
hideCommandRunner(renderer);
|
|
80
176
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
177
|
+
// Filter to only successfully cloned repos
|
|
178
|
+
const successfulCloned = newRepos.filter((repo) => {
|
|
179
|
+
if (repo.reference) return false; // Reference repos handled separately
|
|
180
|
+
const result = results.find((r) => r.task.label === `Cloning ${repo.owner}/${repo.name}`);
|
|
181
|
+
return result?.outcome === "completed";
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Combine successful reference and cloned repos
|
|
185
|
+
const successfulRepos = [...successfulRefs, ...successfulCloned];
|
|
84
186
|
|
|
85
|
-
|
|
86
|
-
|
|
187
|
+
if (successfulRepos.length > 0) {
|
|
188
|
+
const allRepos = [...session.repos, ...successfulRepos];
|
|
189
|
+
const updatedSession = await updateSessionRepos(session.name, allRepos);
|
|
190
|
+
const nextSession = updatedSession ?? session;
|
|
191
|
+
|
|
192
|
+
await recordRepoHistory(successfulRepos);
|
|
193
|
+
await writeAgentsMd(nextSession);
|
|
194
|
+
|
|
195
|
+
return { session: nextSession, cancelled: false };
|
|
196
|
+
}
|
|
87
197
|
|
|
88
|
-
return { session
|
|
198
|
+
return { session, cancelled: false };
|
|
89
199
|
}
|
|
90
200
|
}
|
|
91
201
|
|
|
@@ -6,6 +6,7 @@ import { recordRepoHistory } from "../repo-history";
|
|
|
6
6
|
import { removeSkillFromSession } from "../skills";
|
|
7
7
|
import { createRenderer, destroyRenderer } from "../ui/renderer";
|
|
8
8
|
import { runCommands, hideCommandRunner, type CommandTask } from "../ui/common/command-runner";
|
|
9
|
+
import { rm } from "node:fs/promises";
|
|
9
10
|
|
|
10
11
|
export interface EditSessionParams {
|
|
11
12
|
session: Session;
|
|
@@ -74,10 +75,51 @@ export async function editSession(params: EditSessionParams): Promise<Session |
|
|
|
74
75
|
tasks,
|
|
75
76
|
showOutput: true,
|
|
76
77
|
outputLines: 5,
|
|
78
|
+
allowAbort: true,
|
|
79
|
+
allowSkip: tasks.length > 1,
|
|
80
|
+
allowBackground: true,
|
|
77
81
|
});
|
|
78
82
|
|
|
79
|
-
//
|
|
80
|
-
const
|
|
83
|
+
// Handle aborted operation
|
|
84
|
+
const wasAborted = results.some((r) => r.outcome === "aborted");
|
|
85
|
+
if (wasAborted) {
|
|
86
|
+
hideCommandRunner(renderer);
|
|
87
|
+
// Clean up any partially cloned repos
|
|
88
|
+
for (const repo of newRepos) {
|
|
89
|
+
const repoPath = join(currentSession.path, repo.dir);
|
|
90
|
+
try {
|
|
91
|
+
await rm(repoPath, { recursive: true, force: true });
|
|
92
|
+
} catch {
|
|
93
|
+
// Ignore cleanup errors
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
destroyRenderer();
|
|
97
|
+
return session; // Return unchanged session
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Clean up skipped repo directories
|
|
101
|
+
const skippedResults = results.filter((r) => r.outcome === "skipped");
|
|
102
|
+
for (const skipped of skippedResults) {
|
|
103
|
+
const repoSpec = skipped.task.label.replace("Cloning ", "");
|
|
104
|
+
const repo = newRepos.find((r) => `${r.owner}/${r.name}` === repoSpec);
|
|
105
|
+
if (repo) {
|
|
106
|
+
const repoPath = join(currentSession.path, repo.dir);
|
|
107
|
+
try {
|
|
108
|
+
await rm(repoPath, { recursive: true, force: true });
|
|
109
|
+
} catch {
|
|
110
|
+
// Ignore cleanup errors
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Filter to only successfully cloned repos
|
|
116
|
+
const successfulRepos = newRepos.filter((repo) => {
|
|
117
|
+
const result = results.find((r) => r.task.label === `Cloning ${repo.owner}/${repo.name}`);
|
|
118
|
+
return result?.outcome === "completed";
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Check for errors (not skipped/aborted)
|
|
122
|
+
const errors = results.filter((r) => r.outcome === "error");
|
|
81
123
|
if (errors.length > 0) {
|
|
82
124
|
console.error(`\n⚠️ ${errors.length} repository(ies) failed to clone:`);
|
|
83
125
|
errors.forEach((err) => {
|
|
@@ -91,13 +133,23 @@ export async function editSession(params: EditSessionParams): Promise<Session |
|
|
|
91
133
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
92
134
|
hideCommandRunner(renderer);
|
|
93
135
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
if (successfulRepos.length > 0) {
|
|
137
|
+
await recordRepoHistory(successfulRepos);
|
|
138
|
+
await writeAgentsMd(currentSession);
|
|
139
|
+
}
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
141
|
+
// Update session with existing + successful repos only
|
|
142
|
+
const allRepos = [...session.repos, ...successfulRepos];
|
|
143
|
+
const updatedSession = await updateSessionRepos(session.name, allRepos);
|
|
144
|
+
if (updatedSession) {
|
|
145
|
+
currentSession = updatedSession;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// No new repos, just update with the provided list
|
|
149
|
+
const updatedSession = await updateSessionRepos(session.name, updates.repos);
|
|
150
|
+
if (updatedSession) {
|
|
151
|
+
currentSession = updatedSession;
|
|
152
|
+
}
|
|
101
153
|
}
|
|
102
154
|
}
|
|
103
155
|
|
|
@@ -113,10 +165,27 @@ export async function editSession(params: EditSessionParams): Promise<Session |
|
|
|
113
165
|
tasks,
|
|
114
166
|
showOutput: true,
|
|
115
167
|
outputLines: 5,
|
|
168
|
+
allowAbort: true,
|
|
169
|
+
allowSkip: tasks.length > 1,
|
|
170
|
+
allowBackground: true,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Handle aborted operation
|
|
174
|
+
const wasAborted = results.some((r) => r.outcome === "aborted");
|
|
175
|
+
if (wasAborted) {
|
|
176
|
+
hideCommandRunner(renderer);
|
|
177
|
+
destroyRenderer();
|
|
178
|
+
return currentSession; // Return current session state
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Filter to only successfully installed skills
|
|
182
|
+
const successfulSkills = newSkills.filter((skill) => {
|
|
183
|
+
const result = results.find((r) => r.task.label === `Installing ${skill}`);
|
|
184
|
+
return result?.outcome === "completed";
|
|
116
185
|
});
|
|
117
186
|
|
|
118
|
-
// Check for errors
|
|
119
|
-
const errors = results.filter((r) =>
|
|
187
|
+
// Check for errors (not skipped/aborted)
|
|
188
|
+
const errors = results.filter((r) => r.outcome === "error");
|
|
120
189
|
if (errors.length > 0) {
|
|
121
190
|
console.error(`\n⚠️ ${errors.length} skill(s) failed to install:`);
|
|
122
191
|
errors.forEach((err) => {
|
|
@@ -130,12 +199,14 @@ export async function editSession(params: EditSessionParams): Promise<Session |
|
|
|
130
199
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
131
200
|
hideCommandRunner(renderer);
|
|
132
201
|
|
|
133
|
-
|
|
134
|
-
|
|
202
|
+
if (successfulSkills.length > 0) {
|
|
203
|
+
const allSkills = [...(currentSession.skills || []), ...successfulSkills];
|
|
204
|
+
const updatedSession = await updateSessionSkills(currentSession.name, allSkills);
|
|
135
205
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
206
|
+
if (updatedSession) {
|
|
207
|
+
currentSession = updatedSession;
|
|
208
|
+
await writeAgentsMd(currentSession);
|
|
209
|
+
}
|
|
139
210
|
}
|
|
140
211
|
}
|
|
141
212
|
}
|
package/src/flows/new-session.ts
CHANGED
|
@@ -8,6 +8,8 @@ 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
|
+
import { rm } from "node:fs/promises";
|
|
11
13
|
|
|
12
14
|
export interface NewSessionParams {
|
|
13
15
|
repos: RepoSpec[];
|
|
@@ -23,7 +25,11 @@ export interface NewSessionResult {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTask[] {
|
|
26
|
-
|
|
28
|
+
// Only create command tasks for non-reference repos
|
|
29
|
+
// Reference repos are handled separately with ensureReferenceRepo + symlink
|
|
30
|
+
const regularRepos = repos.filter((repo) => !repo.reference);
|
|
31
|
+
|
|
32
|
+
return regularRepos.map((repo) => {
|
|
27
33
|
const url = `https://github.com/${repo.owner}/${repo.name}.git`;
|
|
28
34
|
const targetDir = join(sessionPath, repo.dir);
|
|
29
35
|
const args = repo.branch
|
|
@@ -49,6 +55,41 @@ function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTas
|
|
|
49
55
|
});
|
|
50
56
|
}
|
|
51
57
|
|
|
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
|
+
|
|
52
93
|
function skillsToCommandTasks(skills: string[], sessionPath: string): CommandTask[] {
|
|
53
94
|
return skills.map((skill) => ({
|
|
54
95
|
label: `Installing ${skill}`,
|
|
@@ -105,32 +146,94 @@ export async function createNewSession(
|
|
|
105
146
|
skills?.length ? skills : undefined,
|
|
106
147
|
);
|
|
107
148
|
|
|
108
|
-
//
|
|
149
|
+
// Setup reference repos first (symlinks from cache)
|
|
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
|
|
109
164
|
const tasks: CommandTask[] = [
|
|
110
165
|
...reposToCommandTasks(repos, session.path),
|
|
111
166
|
...(skills && skills.length > 0 ? skillsToCommandTasks(skills, session.path) : []),
|
|
112
167
|
];
|
|
113
168
|
|
|
114
|
-
|
|
115
|
-
title: "Setting up session",
|
|
116
|
-
tasks,
|
|
117
|
-
showOutput: true,
|
|
118
|
-
outputLines: 5,
|
|
119
|
-
});
|
|
169
|
+
let results: Awaited<ReturnType<typeof runCommands>> = [];
|
|
120
170
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
171
|
+
if (tasks.length > 0) {
|
|
172
|
+
results = await runCommands(renderer, {
|
|
173
|
+
title: "Setting up session",
|
|
174
|
+
tasks,
|
|
175
|
+
showOutput: true,
|
|
176
|
+
outputLines: 5,
|
|
177
|
+
allowAbort: true,
|
|
178
|
+
allowSkip: tasks.length > 1,
|
|
179
|
+
allowBackground: true,
|
|
180
|
+
sessionName,
|
|
130
181
|
});
|
|
182
|
+
|
|
183
|
+
// Handle aborted operation - clean up and return null
|
|
184
|
+
const wasAborted = results.some((r) => r.outcome === "aborted");
|
|
185
|
+
if (wasAborted) {
|
|
186
|
+
hideCommandRunner(renderer);
|
|
187
|
+
// Clean up the partial session
|
|
188
|
+
await deleteSession(session.name);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Clean up skipped repo directories
|
|
193
|
+
const skippedResults = results.filter((r) => r.outcome === "skipped");
|
|
194
|
+
for (const skipped of skippedResults) {
|
|
195
|
+
const repoName = skipped.task.label.replace("Cloning ", "").split("/")[1];
|
|
196
|
+
if (repoName) {
|
|
197
|
+
const repoPath = join(session.path, repoName);
|
|
198
|
+
try {
|
|
199
|
+
await rm(repoPath, { recursive: true, force: true });
|
|
200
|
+
} catch {
|
|
201
|
+
// Ignore cleanup errors
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
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
|
+
}
|
|
131
217
|
}
|
|
132
218
|
|
|
133
|
-
|
|
219
|
+
// Filter out skipped repos from the session
|
|
220
|
+
const successfulRepoResults = results.filter(
|
|
221
|
+
(r) => r.outcome === "completed" && r.task.label.startsWith("Cloning "),
|
|
222
|
+
);
|
|
223
|
+
const successfulClonedSpecs = successfulRepoResults
|
|
224
|
+
.map((r) => {
|
|
225
|
+
const repoSpec = r.task.label.replace("Cloning ", "");
|
|
226
|
+
return repos.find((repo) => `${repo.owner}/${repo.name}` === repoSpec);
|
|
227
|
+
})
|
|
228
|
+
.filter((r): r is RepoSpec => r !== undefined);
|
|
229
|
+
|
|
230
|
+
// Combine successful reference repos and cloned repos
|
|
231
|
+
const successfulRepoSpecs = [...successfulRefs, ...successfulClonedSpecs];
|
|
232
|
+
|
|
233
|
+
// Record only successful repos
|
|
234
|
+
if (successfulRepoSpecs.length > 0) {
|
|
235
|
+
await recordRepoHistory(successfulRepoSpecs);
|
|
236
|
+
}
|
|
134
237
|
|
|
135
238
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
136
239
|
hideCommandRunner(renderer);
|
|
@@ -148,30 +251,52 @@ export async function createNewSession(
|
|
|
148
251
|
skills?.length ? skills : undefined,
|
|
149
252
|
);
|
|
150
253
|
|
|
151
|
-
|
|
254
|
+
// Setup reference repos first (symlinks from cache)
|
|
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
|
+
}
|
|
152
275
|
|
|
153
276
|
const tasks: CommandTask[] = [
|
|
154
277
|
...reposToCommandTasks(repos, session.path),
|
|
155
278
|
...(skills && skills.length > 0 ? skillsToCommandTasks(skills, session.path) : []),
|
|
156
279
|
];
|
|
157
280
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
281
|
+
if (tasks.length > 0) {
|
|
282
|
+
const results = await runCommands(renderer, {
|
|
283
|
+
title: "Setting up session",
|
|
284
|
+
tasks,
|
|
285
|
+
showOutput: false, // CLI mode doesn't need visual output
|
|
286
|
+
});
|
|
163
287
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
288
|
+
// Print results
|
|
289
|
+
results.forEach((result) => {
|
|
290
|
+
if (result.success) {
|
|
291
|
+
console.log(` ✓ ${result.task.label}`);
|
|
292
|
+
} else {
|
|
293
|
+
console.log(` ✗ ${result.task.label}`);
|
|
294
|
+
if (result.error) {
|
|
295
|
+
console.log(` ${result.error}`);
|
|
296
|
+
}
|
|
172
297
|
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
175
300
|
|
|
176
301
|
await writeAgentsMd(session);
|
|
177
302
|
await createClaudeMdSymlink(session.path);
|