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/src/ui/skills.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CliRenderer,
|
|
3
|
+
TextRenderable,
|
|
4
|
+
InputRenderable,
|
|
5
|
+
SelectRenderable,
|
|
6
|
+
SelectRenderableEvents,
|
|
7
|
+
type KeyEvent,
|
|
8
|
+
} from "@opentui/core";
|
|
9
|
+
import { createBaseLayout, clearLayout } from "./renderer";
|
|
10
|
+
|
|
11
|
+
export interface SkillsPromptResult {
|
|
12
|
+
skills: string[];
|
|
13
|
+
cancelled: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function showSkillsPrompt(
|
|
17
|
+
renderer: CliRenderer,
|
|
18
|
+
_existingSkills: string[] = [],
|
|
19
|
+
): Promise<SkillsPromptResult> {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
clearLayout(renderer);
|
|
22
|
+
|
|
23
|
+
const { content } = createBaseLayout(renderer, "Add skills to session");
|
|
24
|
+
|
|
25
|
+
const description = new TextRenderable(renderer, {
|
|
26
|
+
id: "description",
|
|
27
|
+
content:
|
|
28
|
+
"Enter skill packages to install (e.g., vercel-labs/agent-skills).\nSkills will be installed in the session directory.",
|
|
29
|
+
fg: "#64748b",
|
|
30
|
+
marginBottom: 1,
|
|
31
|
+
});
|
|
32
|
+
content.add(description);
|
|
33
|
+
|
|
34
|
+
const input = new InputRenderable(renderer, {
|
|
35
|
+
id: "skill-input",
|
|
36
|
+
width: 60,
|
|
37
|
+
height: 1,
|
|
38
|
+
placeholder: "Enter skill package",
|
|
39
|
+
placeholderColor: "#64748b",
|
|
40
|
+
backgroundColor: "#334155",
|
|
41
|
+
textColor: "#f8fafc",
|
|
42
|
+
cursorColor: "#38bdf8",
|
|
43
|
+
marginTop: 1,
|
|
44
|
+
});
|
|
45
|
+
content.add(input);
|
|
46
|
+
|
|
47
|
+
const existingSkillsText = new TextRenderable(renderer, {
|
|
48
|
+
id: "existing-skills",
|
|
49
|
+
content: "",
|
|
50
|
+
fg: "#94a3b8",
|
|
51
|
+
marginTop: 1,
|
|
52
|
+
marginBottom: 0,
|
|
53
|
+
});
|
|
54
|
+
content.add(existingSkillsText);
|
|
55
|
+
|
|
56
|
+
const confirmation = new SelectRenderable(renderer, {
|
|
57
|
+
id: "confirmation-select",
|
|
58
|
+
width: 40,
|
|
59
|
+
height: 1,
|
|
60
|
+
options: [
|
|
61
|
+
{ name: "Add another skill", description: "", value: "yes" },
|
|
62
|
+
{ name: "Done adding skills", description: "", value: "no" },
|
|
63
|
+
],
|
|
64
|
+
showDescription: false,
|
|
65
|
+
backgroundColor: "transparent",
|
|
66
|
+
focusedBackgroundColor: "transparent",
|
|
67
|
+
selectedBackgroundColor: "#334155",
|
|
68
|
+
textColor: "#e2e8f0",
|
|
69
|
+
selectedTextColor: "#38bdf8",
|
|
70
|
+
descriptionColor: "#64748b",
|
|
71
|
+
selectedDescriptionColor: "#94a3b8",
|
|
72
|
+
marginTop: 1,
|
|
73
|
+
});
|
|
74
|
+
content.add(confirmation);
|
|
75
|
+
|
|
76
|
+
const instructions = new TextRenderable(renderer, {
|
|
77
|
+
id: "instructions",
|
|
78
|
+
content: "\n[Enter] Add skill [Ctrl+D] Done [Esc] Cancel",
|
|
79
|
+
fg: "#64748b",
|
|
80
|
+
marginTop: 1,
|
|
81
|
+
});
|
|
82
|
+
content.add(instructions);
|
|
83
|
+
|
|
84
|
+
const skills: string[] = [];
|
|
85
|
+
let showingConfirmation = false;
|
|
86
|
+
|
|
87
|
+
const updateExistingSkills = () => {
|
|
88
|
+
if (skills.length === 0) {
|
|
89
|
+
existingSkillsText.content = "";
|
|
90
|
+
} else {
|
|
91
|
+
const text = skills.map((skill) => ` ✓ ${skill}`).join("\n");
|
|
92
|
+
existingSkillsText.content = `Added skills:\n${text}`;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const cleanup = () => {
|
|
97
|
+
renderer.keyInput.off("keypress", handleKeypress);
|
|
98
|
+
input.blur();
|
|
99
|
+
confirmation.off(SelectRenderableEvents.ITEM_SELECTED, handleConfirm);
|
|
100
|
+
confirmation.blur();
|
|
101
|
+
clearLayout(renderer);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleKeypress = (key: KeyEvent) => {
|
|
105
|
+
if (showingConfirmation) {
|
|
106
|
+
if (key.name === "return" || key.name === "enter") {
|
|
107
|
+
cleanup();
|
|
108
|
+
resolve({
|
|
109
|
+
skills,
|
|
110
|
+
cancelled: false,
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (key.name === "escape") {
|
|
115
|
+
cleanup();
|
|
116
|
+
resolve({
|
|
117
|
+
skills,
|
|
118
|
+
cancelled: false,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (key.name === "d" && key.ctrl) {
|
|
123
|
+
cleanup();
|
|
124
|
+
resolve({
|
|
125
|
+
skills,
|
|
126
|
+
cancelled: false,
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (key.name === "escape") {
|
|
134
|
+
cleanup();
|
|
135
|
+
resolve({
|
|
136
|
+
skills: [],
|
|
137
|
+
cancelled: true,
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (key.name === "d" && key.ctrl) {
|
|
143
|
+
cleanup();
|
|
144
|
+
resolve({
|
|
145
|
+
skills,
|
|
146
|
+
cancelled: false,
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (key.name === "return" || key.name === "enter") {
|
|
152
|
+
const skill = input.value.trim();
|
|
153
|
+
if (skill) {
|
|
154
|
+
skills.push(skill);
|
|
155
|
+
updateExistingSkills();
|
|
156
|
+
input.value = "";
|
|
157
|
+
showingConfirmation = true;
|
|
158
|
+
confirmation.focus();
|
|
159
|
+
instructions.content = "\n[Enter] Add another [Ctrl+D] Done [Esc] Cancel";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleConfirm = (_index: number, option: { value: string }) => {
|
|
165
|
+
if (option.value === "yes") {
|
|
166
|
+
showingConfirmation = false;
|
|
167
|
+
input.focus();
|
|
168
|
+
instructions.content = "\n[Enter] Add skill [Esc] Cancel";
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
cleanup();
|
|
173
|
+
resolve({
|
|
174
|
+
skills,
|
|
175
|
+
cancelled: false,
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
input.focus();
|
|
180
|
+
confirmation.off(SelectRenderableEvents.ITEM_SELECTED, handleConfirm);
|
|
181
|
+
renderer.keyInput.off("keypress", handleKeypress);
|
|
182
|
+
renderer.keyInput.on("keypress", handleKeypress);
|
|
183
|
+
confirmation.on(SelectRenderableEvents.ITEM_SELECTED, handleConfirm);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export async function readProcessOutput(
|
|
2
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
3
|
+
onOutput?: (line: string) => void,
|
|
4
|
+
): Promise<{ success: boolean; output: string[] }> {
|
|
5
|
+
const outputBuffer: string[] = [];
|
|
6
|
+
const addLine = (line: string) => {
|
|
7
|
+
const trimmed = line.trim();
|
|
8
|
+
if (trimmed) {
|
|
9
|
+
outputBuffer.push(trimmed);
|
|
10
|
+
onOutput?.(trimmed);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const readStream = async (stream: ReadableStream<Uint8Array> | number | undefined) => {
|
|
15
|
+
if (!stream || typeof stream === "number") return;
|
|
16
|
+
|
|
17
|
+
const reader = stream.getReader();
|
|
18
|
+
const decoder = new TextDecoder();
|
|
19
|
+
let buffer = "";
|
|
20
|
+
|
|
21
|
+
while (true) {
|
|
22
|
+
const { done, value } = await reader.read();
|
|
23
|
+
if (done) break;
|
|
24
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
25
|
+
buffer += chunk;
|
|
26
|
+
|
|
27
|
+
const lines = buffer.split(/[\r\n]+/);
|
|
28
|
+
buffer = lines.pop() || "";
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
addLine(line);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (buffer.trim()) {
|
|
36
|
+
addLine(buffer);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
await Promise.all([readStream(proc.stdout), readStream(proc.stderr)]);
|
|
41
|
+
const exitCode = await proc.exited;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
success: exitCode === 0,
|
|
45
|
+
output: outputBuffer,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ReadProcessOutputWithBufferOptions {
|
|
50
|
+
maxBufferLines?: number;
|
|
51
|
+
onBufferUpdate?: (buffer: string[]) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readProcessOutputWithBuffer(
|
|
55
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
56
|
+
options?: ReadProcessOutputWithBufferOptions,
|
|
57
|
+
): Promise<{ success: boolean; output: string[]; fullOutput: string }> {
|
|
58
|
+
const { maxBufferLines, onBufferUpdate } = options || {};
|
|
59
|
+
const outputBuffer: string[] = [];
|
|
60
|
+
const addLine = (line: string) => {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (trimmed) {
|
|
63
|
+
outputBuffer.push(trimmed);
|
|
64
|
+
if (maxBufferLines && outputBuffer.length > maxBufferLines) {
|
|
65
|
+
outputBuffer.shift();
|
|
66
|
+
}
|
|
67
|
+
onBufferUpdate?.([...outputBuffer]);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const fullOutputParts: string[] = [];
|
|
72
|
+
|
|
73
|
+
const readStream = async (stream: ReadableStream<Uint8Array> | number | undefined) => {
|
|
74
|
+
if (!stream || typeof stream === "number") return;
|
|
75
|
+
|
|
76
|
+
const reader = stream.getReader();
|
|
77
|
+
const decoder = new TextDecoder();
|
|
78
|
+
let buffer = "";
|
|
79
|
+
|
|
80
|
+
while (true) {
|
|
81
|
+
const { done, value } = await reader.read();
|
|
82
|
+
if (done) break;
|
|
83
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
84
|
+
fullOutputParts.push(chunk);
|
|
85
|
+
buffer += chunk;
|
|
86
|
+
|
|
87
|
+
const lines = buffer.split(/[\r\n]+/);
|
|
88
|
+
buffer = lines.pop() || "";
|
|
89
|
+
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
addLine(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (buffer.trim()) {
|
|
96
|
+
addLine(buffer);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
await Promise.all([readStream(proc.stdout), readStream(proc.stderr)]);
|
|
101
|
+
const exitCode = await proc.exited;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
success: exitCode === 0,
|
|
105
|
+
output: outputBuffer,
|
|
106
|
+
fullOutput: fullOutputParts.join(""),
|
|
107
|
+
};
|
|
108
|
+
}
|