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.
- package/README.md +120 -0
- package/index.ts +234 -0
- package/package.json +41 -5
- package/src/agents-md.ts +115 -0
- package/src/flows/add-repos.ts +57 -0
- package/src/flows/add-skills.ts +57 -0
- package/src/flows/edit-session.ts +107 -0
- package/src/flows/index.ts +5 -0
- package/src/flows/new-session.ts +182 -0
- package/src/flows/resume-session.ts +231 -0
- package/src/git.ts +256 -0
- package/src/naming.ts +57 -0
- package/src/opencode-integration.ts +20 -0
- package/src/repo-history.ts +82 -0
- package/src/sessions.ts +217 -0
- package/src/skills.ts +49 -0
- package/src/tui-mode.ts +184 -0
- package/src/types.ts +80 -0
- package/src/ui/add-repos.ts +396 -0
- package/src/ui/agent-proposal.ts +80 -0
- package/src/ui/common/repo-formatter.ts +45 -0
- package/src/ui/confirm-delete.ts +95 -0
- package/src/ui/conflict.ts +121 -0
- package/src/ui/exit.ts +175 -0
- package/src/ui/list.ts +112 -0
- package/src/ui/main-menu.ts +155 -0
- package/src/ui/new-session.ts +99 -0
- package/src/ui/progress.ts +191 -0
- package/src/ui/reclone-prompt.ts +93 -0
- package/src/ui/renderer.ts +108 -0
- package/src/ui/session-actions.ts +109 -0
- package/src/ui/session-details.ts +77 -0
- package/src/ui/session-options.ts +41 -0
- package/src/ui/session-settings.ts +363 -0
- package/src/ui/skills.ts +185 -0
- package/src/utils/stream.ts +108 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# letmecook
|
|
2
|
+
|
|
3
|
+
Multi-repo workspace manager for AI coding sessions. Clone multiple GitHub repos into a persistent session and launch opencode with an interactive TUI.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Interactive TUI** - User-friendly guided experience built with [OpenTUI](https://github.com/sst/opentui)
|
|
8
|
+
- **Agent transparency** - See exactly what the agent plans to do before execution
|
|
9
|
+
- **Real-time progress** - Live updates showing cloning and preparation
|
|
10
|
+
- **Manual Setup Prompt** - Run custom setup commands (e.g., `npm install`) before starting
|
|
11
|
+
- **Session-based workflows** - Workspaces persist until you explicitly nuke them
|
|
12
|
+
- **AI-generated session names** - Memorable names based on repos and your goal
|
|
13
|
+
- **Multi-repo support** - Clone multiple repos with optional branch specification
|
|
14
|
+
- **AGENTS.md generation** - Auto-generated context file for AI agents
|
|
15
|
+
- **100% backward compatible** - All existing CLI commands continue to work
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun install
|
|
21
|
+
bun link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
Create a `.env` file with your AI Gateway API key (for session naming):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
AI_GATEWAY_API_KEY=your-key-here
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Interactive Mode (Recommended)
|
|
35
|
+
|
|
36
|
+
Launch the user-friendly TUI interface:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
letmecook
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The TUI guides you through:
|
|
43
|
+
|
|
44
|
+
1. **Adding repositories** - Interactive repo collection
|
|
45
|
+
2. **Session goal** - Describe what you want to work on
|
|
46
|
+
3. **Agent proposal transparency** - See exactly what the agent plans to do
|
|
47
|
+
4. **Real-time progress** - Live updates showing cloning progress
|
|
48
|
+
5. **Manual Setup** - Option to run commands like `npm install` before launching
|
|
49
|
+
6. **Interactive CLI** - Enter opencode with full context
|
|
50
|
+
|
|
51
|
+
### CLI Mode (Backward Compatible)
|
|
52
|
+
|
|
53
|
+
All existing commands continue to work exactly as before:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Single repo
|
|
57
|
+
letmecook microsoft/playwright
|
|
58
|
+
|
|
59
|
+
# Multiple repos
|
|
60
|
+
letmecook microsoft/playwright openai/agents
|
|
61
|
+
|
|
62
|
+
# With specific branches
|
|
63
|
+
letmecook facebook/react:experimental vercel/next.js:canary
|
|
64
|
+
|
|
65
|
+
# Explicit TUI mode
|
|
66
|
+
letmecook --tui
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Manage sessions
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# List all sessions (interactive)
|
|
73
|
+
letmecook --list
|
|
74
|
+
|
|
75
|
+
# Resume a specific session
|
|
76
|
+
letmecook --resume <session-name>
|
|
77
|
+
|
|
78
|
+
# Delete a session
|
|
79
|
+
letmecook --nuke <session-name>
|
|
80
|
+
|
|
81
|
+
# Delete all sessions
|
|
82
|
+
letmecook --nuke-all
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Session persistence
|
|
86
|
+
|
|
87
|
+
When you exit opencode, you're prompted to keep or nuke the session. Sessions are kept by default - just press Enter.
|
|
88
|
+
|
|
89
|
+
To resume later:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
letmecook --resume <session-name>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Session Structure
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
~/.letmecook/sessions/<session-name>/
|
|
99
|
+
├── manifest.json # Session metadata
|
|
100
|
+
├── AGENTS.md # Context for AI agents
|
|
101
|
+
├── repo1/ # Cloned repository
|
|
102
|
+
└── repo2/ # Another cloned repository
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Requirements
|
|
106
|
+
|
|
107
|
+
- [Bun](https://bun.sh)
|
|
108
|
+
- [opencode](https://opencode.ai) - must be in PATH
|
|
109
|
+
- [Zig](https://ziglang.org) - required by OpenTUI
|
|
110
|
+
- `AI_GATEWAY_API_KEY` environment variable (for AI naming)
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Run directly
|
|
116
|
+
bun run index.ts microsoft/playwright
|
|
117
|
+
|
|
118
|
+
# Type check
|
|
119
|
+
bun run tsc --noEmit
|
|
120
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { parseRepoSpec, type RepoSpec } from "./src/types";
|
|
4
|
+
import { listSessions, getSession, updateLastAccessed, deleteAllSessions } from "./src/sessions";
|
|
5
|
+
import { createRenderer, destroyRenderer } from "./src/ui/renderer";
|
|
6
|
+
import { showNewSessionPrompt } from "./src/ui/new-session";
|
|
7
|
+
import { showSessionList } from "./src/ui/list";
|
|
8
|
+
import { createNewSession, resumeSession } from "./src/flows";
|
|
9
|
+
import { handleTUIMode } from "./src/tui-mode";
|
|
10
|
+
|
|
11
|
+
function printUsage(): void {
|
|
12
|
+
console.log(`
|
|
13
|
+
letmecook - Multi-repo workspace manager for AI coding sessions
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
letmecook Launch interactive TUI (recommended)
|
|
17
|
+
letmecook --tui Launch interactive TUI explicitly
|
|
18
|
+
letmecook <owner/repo> [owner/repo:branch...] Create or resume a session (CLI)
|
|
19
|
+
letmecook --list List all sessions
|
|
20
|
+
letmecook --resume <session-name> Resume a session
|
|
21
|
+
letmecook --nuke <session-name> Delete a session
|
|
22
|
+
letmecook --nuke-all Delete all sessions
|
|
23
|
+
letmecook --help Show this help
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
# Interactive mode (new - recommended)
|
|
27
|
+
letmecook
|
|
28
|
+
|
|
29
|
+
# CLI mode
|
|
30
|
+
letmecook microsoft/playwright
|
|
31
|
+
letmecook facebook/react openai/agents
|
|
32
|
+
letmecook --resume playwright-agent-tests
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function handleNewSessionCLI(repos: RepoSpec[]): Promise<void> {
|
|
37
|
+
const renderer = await createRenderer();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const { goal, cancelled } = await showNewSessionPrompt(renderer, repos);
|
|
41
|
+
|
|
42
|
+
if (cancelled) {
|
|
43
|
+
destroyRenderer();
|
|
44
|
+
console.log("\nCancelled.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await createNewSession(renderer, {
|
|
49
|
+
repos,
|
|
50
|
+
goal,
|
|
51
|
+
mode: "cli",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!result) {
|
|
55
|
+
destroyRenderer();
|
|
56
|
+
console.log("\nCancelled.");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { session, skipped } = result;
|
|
61
|
+
|
|
62
|
+
if (skipped) {
|
|
63
|
+
destroyRenderer();
|
|
64
|
+
console.log(`\nResuming existing session: ${session.name}\n`);
|
|
65
|
+
await resumeSession(renderer, {
|
|
66
|
+
session,
|
|
67
|
+
mode: "cli",
|
|
68
|
+
initialRefresh: true,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
destroyRenderer();
|
|
74
|
+
console.log(`\nSession created: ${session.name}`);
|
|
75
|
+
console.log(`Path: ${session.path}\n`);
|
|
76
|
+
|
|
77
|
+
await resumeSession(renderer, {
|
|
78
|
+
session,
|
|
79
|
+
mode: "cli",
|
|
80
|
+
initialRefresh: false,
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
destroyRenderer();
|
|
84
|
+
console.error("\nError:", error instanceof Error ? error.message : error);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function handleList(): Promise<void> {
|
|
90
|
+
const renderer = await createRenderer();
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
while (true) {
|
|
94
|
+
const sessions = await listSessions();
|
|
95
|
+
const action = await showSessionList(renderer, sessions);
|
|
96
|
+
|
|
97
|
+
switch (action.type) {
|
|
98
|
+
case "resume":
|
|
99
|
+
destroyRenderer();
|
|
100
|
+
await updateLastAccessed(action.session.name);
|
|
101
|
+
console.log(`\nResuming session: ${action.session.name}\n`);
|
|
102
|
+
await resumeSession(renderer, {
|
|
103
|
+
session: action.session,
|
|
104
|
+
mode: "cli",
|
|
105
|
+
initialRefresh: true,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
|
|
109
|
+
case "delete":
|
|
110
|
+
console.log("[TODO] Delete session flow");
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case "nuke-all":
|
|
114
|
+
const count = await deleteAllSessions();
|
|
115
|
+
destroyRenderer();
|
|
116
|
+
console.log(`\nNuked ${count} session(s).`);
|
|
117
|
+
return;
|
|
118
|
+
|
|
119
|
+
case "quit":
|
|
120
|
+
destroyRenderer();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
destroyRenderer();
|
|
126
|
+
console.error("\nError:", error instanceof Error ? error.message : error);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function handleResume(sessionName: string): Promise<void> {
|
|
132
|
+
const session = await getSession(sessionName);
|
|
133
|
+
|
|
134
|
+
if (!session) {
|
|
135
|
+
console.error(`Session not found: ${sessionName}`);
|
|
136
|
+
console.log("\nAvailable sessions:");
|
|
137
|
+
const sessions = await listSessions();
|
|
138
|
+
if (sessions.length === 0) {
|
|
139
|
+
console.log(" (none)");
|
|
140
|
+
} else {
|
|
141
|
+
sessions.forEach((s) => console.log(` - ${s.name}`));
|
|
142
|
+
}
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await updateLastAccessed(session.name);
|
|
147
|
+
console.log(`\nResuming session: ${session.name}\n`);
|
|
148
|
+
|
|
149
|
+
const renderer = await createRenderer();
|
|
150
|
+
await resumeSession(renderer, {
|
|
151
|
+
session,
|
|
152
|
+
mode: "cli",
|
|
153
|
+
initialRefresh: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function handleNuke(_sessionName: string): Promise<void> {
|
|
158
|
+
console.log("[TODO] Nuke session flow");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function handleNukeAll(): Promise<void> {
|
|
162
|
+
const count = await deleteAllSessions();
|
|
163
|
+
console.log(`Nuked ${count} session(s).`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleCLIMode(args: string[]): Promise<void> {
|
|
167
|
+
const firstArg = args[0];
|
|
168
|
+
|
|
169
|
+
if (firstArg === "--list" || firstArg === "-l") {
|
|
170
|
+
await handleList();
|
|
171
|
+
} else if (firstArg === "--resume" || firstArg === "-r") {
|
|
172
|
+
const sessionName = args[1];
|
|
173
|
+
if (!sessionName) {
|
|
174
|
+
console.error("Missing session name. Usage: letmecook --resume <session-name>");
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
await handleResume(sessionName);
|
|
178
|
+
} else if (firstArg === "--nuke") {
|
|
179
|
+
const sessionName = args[1];
|
|
180
|
+
if (!sessionName) {
|
|
181
|
+
console.error("Missing session name. Usage: letmecook --nuke <session-name>");
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
await handleNuke(sessionName);
|
|
185
|
+
} else if (firstArg === "--nuke-all") {
|
|
186
|
+
await handleNukeAll();
|
|
187
|
+
} else if (firstArg?.startsWith("-")) {
|
|
188
|
+
console.error(`Unknown option: ${firstArg}`);
|
|
189
|
+
printUsage();
|
|
190
|
+
process.exit(1);
|
|
191
|
+
} else {
|
|
192
|
+
try {
|
|
193
|
+
const repos = parseRepos(args);
|
|
194
|
+
await handleNewSessionCLI(repos);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseRepos(args: string[]): RepoSpec[] {
|
|
203
|
+
const repos: RepoSpec[] = [];
|
|
204
|
+
|
|
205
|
+
for (const arg of args) {
|
|
206
|
+
if (!arg || arg.startsWith("-")) continue;
|
|
207
|
+
|
|
208
|
+
if (!arg.includes("/")) {
|
|
209
|
+
throw new Error(`Invalid repo format: ${arg} (expected owner/repo)`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const repo = parseRepoSpec(arg);
|
|
213
|
+
repos.push(repo);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return repos;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.clear();
|
|
220
|
+
const args = process.argv.slice(2);
|
|
221
|
+
const firstArg = args[0];
|
|
222
|
+
|
|
223
|
+
if (args.length === 0 || firstArg === "--help" || firstArg === "-h") {
|
|
224
|
+
printUsage();
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (firstArg === "--tui") {
|
|
229
|
+
await handleTUIMode();
|
|
230
|
+
} else if (args.length === 0) {
|
|
231
|
+
await handleTUIMode();
|
|
232
|
+
} else {
|
|
233
|
+
await handleCLIMode(args);
|
|
234
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecook",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"bin": {
|
|
5
|
+
"letmecook": "./index.ts"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"module": "index.ts",
|
|
5
9
|
"repository": {
|
|
6
10
|
"type": "git",
|
|
7
|
-
"url": "
|
|
11
|
+
"url": "https://github.com/rustydotwtf/letmecook.git"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"tag": "latest"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.ts",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"bun": ">=1.0.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"prepare": "husky",
|
|
26
|
+
"lint": "oxlint",
|
|
27
|
+
"lint:fix": "oxlint --fix",
|
|
28
|
+
"lint:ci": "oxlint --type-aware --deny-warnings --tsconfig ./tsconfig.json",
|
|
29
|
+
"format": "oxfmt --write .",
|
|
30
|
+
"format:check": "oxfmt --check .",
|
|
31
|
+
"check": "bun run lint && bun run format:check",
|
|
32
|
+
"fix": "bun run lint:fix && bun run format"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@opentui/core": "^0.1.63",
|
|
36
|
+
"ai": "^6.0.3"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/bun": "latest",
|
|
40
|
+
"husky": "^9.1.7",
|
|
41
|
+
"oxfmt": "^0.26.0",
|
|
42
|
+
"oxlint": "^1.41.0"
|
|
8
43
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"typescript": "^5"
|
|
46
|
+
}
|
|
11
47
|
}
|
package/src/agents-md.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { symlink } from "node:fs/promises";
|
|
3
|
+
import type { Session } from "./types";
|
|
4
|
+
|
|
5
|
+
export function generateAgentsMd(session: Session): string {
|
|
6
|
+
const createdDate = new Date(session.created).toLocaleDateString("en-US", {
|
|
7
|
+
year: "numeric",
|
|
8
|
+
month: "long",
|
|
9
|
+
day: "numeric",
|
|
10
|
+
hour: "numeric",
|
|
11
|
+
minute: "2-digit",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const hasReadOnlyRepos = session.repos.some((repo) => repo.readOnly);
|
|
15
|
+
const hasLatestRepos = session.repos.some((repo) => repo.latest);
|
|
16
|
+
const hasSkills = session.skills && session.skills.length > 0;
|
|
17
|
+
|
|
18
|
+
const repoRows = session.repos
|
|
19
|
+
.map((repo) => {
|
|
20
|
+
const branch = repo.branch || "default";
|
|
21
|
+
const url = `https://github.com/${repo.owner}/${repo.name}`;
|
|
22
|
+
const readOnlyStatus = repo.readOnly ? "**YES**" : "no";
|
|
23
|
+
const latestStatus = repo.latest ? "**YES**" : "no";
|
|
24
|
+
return `| \`${repo.dir}/\` | [${repo.owner}/${repo.name}](${url}) | ${branch} | ${readOnlyStatus} | ${latestStatus} |`;
|
|
25
|
+
})
|
|
26
|
+
.join("\n");
|
|
27
|
+
|
|
28
|
+
const readOnlyRepos = session.repos.filter((repo) => repo.readOnly);
|
|
29
|
+
const latestRepos = session.repos.filter((repo) => repo.latest);
|
|
30
|
+
|
|
31
|
+
const skillsSection = hasSkills
|
|
32
|
+
? `
|
|
33
|
+
## 🎯 Skills
|
|
34
|
+
|
|
35
|
+
Installed skill packages (managed by bunx skills):
|
|
36
|
+
|
|
37
|
+
${(session.skills || []).map((skill) => `- \`${skill}\``).join("\n")}
|
|
38
|
+
|
|
39
|
+
These skills are available for use in this session and are automatically updated before launching.
|
|
40
|
+
`
|
|
41
|
+
: "";
|
|
42
|
+
|
|
43
|
+
const readOnlyWarning = hasReadOnlyRepos
|
|
44
|
+
? `
|
|
45
|
+
## ⚠️ Read-Only Repositories
|
|
46
|
+
|
|
47
|
+
**WARNING: The following repositories are marked as READ-ONLY:**
|
|
48
|
+
|
|
49
|
+
${readOnlyRepos.map((repo) => `- \`${repo.dir}/\` (${repo.owner}/${repo.name})`).join("\n")}
|
|
50
|
+
|
|
51
|
+
**AI agents must NOT:**
|
|
52
|
+
- Create, modify, or delete any files in these directories
|
|
53
|
+
- Make commits affecting these repositories
|
|
54
|
+
- Use bash commands to circumvent file permissions
|
|
55
|
+
|
|
56
|
+
**Why are these read-only?**
|
|
57
|
+
These repositories are included for reference only. The user wants to read and understand the code without risk of accidental modifications.
|
|
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")}
|
|
68
|
+
`
|
|
69
|
+
: "";
|
|
70
|
+
|
|
71
|
+
return `# letmecook Session: ${session.name}
|
|
72
|
+
|
|
73
|
+
${session.goal ? `> ${session.goal}\n` : ""}
|
|
74
|
+
## Session Info
|
|
75
|
+
- **Created**: ${createdDate}
|
|
76
|
+
|
|
77
|
+
## Repositories
|
|
78
|
+
|
|
79
|
+
| Directory | Repository | Branch | Read-Only | Latest |
|
|
80
|
+
|-----------|------------|--------|-----------|--------|
|
|
81
|
+
${repoRows}
|
|
82
|
+
${readOnlyWarning}
|
|
83
|
+
${latestNotice}
|
|
84
|
+
${skillsSection}
|
|
85
|
+
## Important Notes
|
|
86
|
+
|
|
87
|
+
- This is a **multi-repo workspace** - each subdirectory is a separate git repository
|
|
88
|
+
- Make commits within individual repo directories, not from the workspace root
|
|
89
|
+
- This workspace root is NOT a git repository
|
|
90
|
+
- Your changes persist until you explicitly nuke the session
|
|
91
|
+
|
|
92
|
+
## Resume This Session
|
|
93
|
+
|
|
94
|
+
\`\`\`bash
|
|
95
|
+
letmecook --resume ${session.name}
|
|
96
|
+
\`\`\`
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function writeAgentsMd(session: Session): Promise<void> {
|
|
101
|
+
const content = generateAgentsMd(session);
|
|
102
|
+
const path = join(session.path, "AGENTS.md");
|
|
103
|
+
await Bun.write(path, content);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function createClaudeMdSymlink(sessionPath: string): Promise<void> {
|
|
107
|
+
const symlinkPath = join(sessionPath, "CLAUDE.md");
|
|
108
|
+
try {
|
|
109
|
+
await symlink("AGENTS.md", symlinkPath);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
112
|
+
console.warn(`Could not create CLAUDE.md symlink: ${error}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CliRenderer } from "@opentui/core";
|
|
2
|
+
import type { Session } from "../types";
|
|
3
|
+
import { updateSessionRepos } from "../sessions";
|
|
4
|
+
import { cloneAllRepos } from "../git";
|
|
5
|
+
import { writeAgentsMd } from "../agents-md";
|
|
6
|
+
import { recordRepoHistory } from "../repo-history";
|
|
7
|
+
import { showAddReposPrompt } from "../ui/add-repos";
|
|
8
|
+
|
|
9
|
+
export interface AddReposParams {
|
|
10
|
+
renderer: CliRenderer;
|
|
11
|
+
session: Session;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AddReposResult {
|
|
15
|
+
session: Session;
|
|
16
|
+
cancelled: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function addReposFlow(params: AddReposParams): Promise<AddReposResult> {
|
|
20
|
+
const { renderer, session } = params;
|
|
21
|
+
|
|
22
|
+
const addResult = await showAddReposPrompt(renderer);
|
|
23
|
+
|
|
24
|
+
if (!addResult.cancelled && addResult.repos.length > 0) {
|
|
25
|
+
const existingSpecs = new Set(session.repos.map((r) => r.spec));
|
|
26
|
+
const newRepos = addResult.repos.filter((r) => !existingSpecs.has(r.spec));
|
|
27
|
+
|
|
28
|
+
if (newRepos.length > 0) {
|
|
29
|
+
console.log(`\nCloning ${newRepos.length} new repository(ies)...`);
|
|
30
|
+
|
|
31
|
+
await cloneAllRepos(newRepos, session.path, (repoIndex, status) => {
|
|
32
|
+
const repo = newRepos[repoIndex];
|
|
33
|
+
if (repo) {
|
|
34
|
+
if (status === "done") {
|
|
35
|
+
console.log(` ✓ ${repo.owner}/${repo.name}`);
|
|
36
|
+
} else if (status === "error") {
|
|
37
|
+
console.log(` ✗ ${repo.owner}/${repo.name} (failed)`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const allRepos = [...session.repos, ...newRepos];
|
|
43
|
+
const updatedSession = await updateSessionRepos(session.name, allRepos);
|
|
44
|
+
const nextSession = updatedSession ?? session;
|
|
45
|
+
|
|
46
|
+
await recordRepoHistory(newRepos);
|
|
47
|
+
await writeAgentsMd(nextSession);
|
|
48
|
+
|
|
49
|
+
console.log("\n✅ Repositories added.\n");
|
|
50
|
+
return { session: nextSession, cancelled: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log("\nNo new repositories to add (all already in session).\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { session, cancelled: addResult.cancelled };
|
|
57
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CliRenderer } from "@opentui/core";
|
|
2
|
+
import type { Session } from "../types";
|
|
3
|
+
import { updateSessionSkills } from "../sessions";
|
|
4
|
+
import { writeAgentsMd } from "../agents-md";
|
|
5
|
+
import { addSkillToSession } from "../skills";
|
|
6
|
+
import { showSkillsPrompt } from "../ui/skills";
|
|
7
|
+
|
|
8
|
+
export interface AddSkillsParams {
|
|
9
|
+
renderer: CliRenderer;
|
|
10
|
+
session: Session;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AddSkillsResult {
|
|
14
|
+
session: Session;
|
|
15
|
+
cancelled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function addSkillsFlow(params: AddSkillsParams): Promise<AddSkillsResult> {
|
|
19
|
+
const { renderer, session } = params;
|
|
20
|
+
|
|
21
|
+
const { skills, cancelled } = await showSkillsPrompt(renderer, session.skills || []);
|
|
22
|
+
|
|
23
|
+
if (!cancelled && skills.length > 0) {
|
|
24
|
+
const existingSkills = new Set(session.skills || []);
|
|
25
|
+
const newSkills = skills.filter((s) => !existingSkills.has(s));
|
|
26
|
+
|
|
27
|
+
if (newSkills.length > 0) {
|
|
28
|
+
console.log(`\nAdding ${newSkills.length} skill package(s)...`);
|
|
29
|
+
|
|
30
|
+
for (const skill of newSkills) {
|
|
31
|
+
console.log(` Adding ${skill}...`);
|
|
32
|
+
const { success } = await addSkillToSession(session, skill, (output) => {
|
|
33
|
+
console.log(` ${output}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (success) {
|
|
37
|
+
console.log(` ✓ ${skill}`);
|
|
38
|
+
} else {
|
|
39
|
+
console.log(` ✗ ${skill} (addition failed)`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const allSkills = [...(session.skills || []), ...newSkills];
|
|
44
|
+
const updatedSession = await updateSessionSkills(session.name, allSkills);
|
|
45
|
+
|
|
46
|
+
if (updatedSession) {
|
|
47
|
+
await writeAgentsMd(updatedSession);
|
|
48
|
+
console.log("\n✅ Skills added.\n");
|
|
49
|
+
return { session: updatedSession, cancelled: false };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log("\nNo new skills to add (all already in session).\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { session, cancelled };
|
|
57
|
+
}
|