@puzzmo/cli 1.0.33 → 1.0.34
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/lib/agents/claude.d.ts +5 -0
- package/lib/agents/claude.js +69 -0
- package/lib/agents/claude.js.map +1 -0
- package/lib/agents/codex.d.ts +4 -0
- package/lib/agents/codex.js +90 -0
- package/lib/agents/codex.js.map +1 -0
- package/lib/agents/copilot.d.ts +4 -0
- package/lib/agents/copilot.js +123 -0
- package/lib/agents/copilot.js.map +1 -0
- package/lib/agents/gemini.d.ts +5 -0
- package/lib/agents/gemini.js +69 -0
- package/lib/agents/gemini.js.map +1 -0
- package/lib/agents/index.d.ts +5 -0
- package/lib/agents/index.js +15 -0
- package/lib/agents/index.js.map +1 -0
- package/lib/agents/opencode.d.ts +5 -0
- package/lib/agents/opencode.js +159 -0
- package/lib/agents/opencode.js.map +1 -0
- package/lib/agents/stream-json-cli.d.ts +12 -0
- package/lib/agents/stream-json-cli.js +75 -0
- package/lib/agents/stream-json-cli.js.map +1 -0
- package/lib/agents/types.d.ts +37 -0
- package/lib/agents/types.js +2 -0
- package/lib/agents/types.js.map +1 -0
- package/lib/commands/agent-test.d.ts +3 -0
- package/lib/commands/agent-test.js +79 -0
- package/lib/commands/agent-test.js.map +1 -0
- package/lib/commands/game/create.js +6 -4
- package/lib/commands/game/create.js.map +1 -1
- package/lib/index.js +5 -0
- package/lib/index.js.map +1 -1
- package/lib/tui/pipeline.d.ts +1 -2
- package/lib/tui/pipeline.js +160 -267
- package/lib/tui/pipeline.js.map +1 -1
- package/package.json +4 -1
- package/src/agents/claude.ts +72 -0
- package/src/agents/codex.ts +82 -0
- package/src/agents/copilot.ts +118 -0
- package/src/agents/gemini.ts +74 -0
- package/src/agents/index.ts +20 -0
- package/src/agents/opencode.ts +154 -0
- package/src/agents/stream-json-cli.ts +86 -0
- package/src/agents/types.ts +22 -0
- package/src/commands/agent-test.ts +81 -0
- package/src/commands/game/create.ts +6 -4
- package/src/index.ts +5 -0
- package/src/tui/pipeline.ts +151 -268
package/lib/tui/pipeline.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { getAgent } from "../agents/index.js";
|
|
5
5
|
import { skillsPipeline } from "../skills/registry.js";
|
|
6
6
|
import { fetchSkillPrompt } from "../skills/mcp-client.js";
|
|
7
|
-
// ANSI escape helpers
|
|
8
7
|
const ESC = "\x1b";
|
|
9
8
|
const CSI = `${ESC}[`;
|
|
10
9
|
const moveTo = (row, col) => `${CSI}${row};${col}H`;
|
|
11
10
|
const clearScreen = () => `${CSI}2J`;
|
|
12
11
|
const hideCursor = () => `${CSI}?25l`;
|
|
13
12
|
const showCursor = () => `${CSI}?25h`;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
13
|
+
const reset = () => `${CSI}0m`;
|
|
14
|
+
const bold = (s) => `${CSI}1m${s}${reset()}`;
|
|
15
|
+
const dim = (s) => `${CSI}2m${s}${reset()}`;
|
|
16
|
+
const fg = (code, s) => `${CSI}${code}m${s}${reset()}`;
|
|
17
17
|
const green = (s) => fg(32, s);
|
|
18
18
|
const yellow = (s) => fg(33, s);
|
|
19
19
|
const red = (s) => fg(31, s);
|
|
@@ -40,32 +40,44 @@ const phaseColor = {
|
|
|
40
40
|
commit: magenta,
|
|
41
41
|
idle: gray,
|
|
42
42
|
};
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
if (agent === "claude")
|
|
46
|
-
return { cmd: "claude", args: ["-p", "--verbose", "--output-format", "stream-json", prompt] };
|
|
47
|
-
if (agent === "codex")
|
|
48
|
-
return { cmd: "codex", args: ["--quiet", prompt] };
|
|
49
|
-
return { cmd: agent, args: [prompt] };
|
|
50
|
-
};
|
|
51
|
-
/** Fetches step instructions from the MCP server and wraps them as an agent prompt */
|
|
43
|
+
const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
44
|
+
const stripAnsi = (s) => s.replace(ANSI_RE, "");
|
|
52
45
|
const buildPrompt = async (stepName, gameDir, repoContext) => {
|
|
53
46
|
const instructions = await fetchSkillPrompt(stepName, gameDir);
|
|
54
47
|
return `Follow these instructions. The game source is in the current directory.\n\n${repoContext}\n\n${instructions}`;
|
|
55
48
|
};
|
|
49
|
+
/** Format a non-streaming event into discrete lines. text/thinking are handled
|
|
50
|
+
* separately by appendText so token deltas merge into one in-progress line. */
|
|
51
|
+
const formatDiscreteEvent = (e) => {
|
|
52
|
+
if (e.type === "tool_use")
|
|
53
|
+
return [`${yellow(e.name)} ${dim(e.summary)}`];
|
|
54
|
+
if (e.type === "tool_result")
|
|
55
|
+
return [(e.ok ? green(" ✓ ") : red(" ✗ ")) + dim(e.summary)];
|
|
56
|
+
if (e.type === "system")
|
|
57
|
+
return [dim(e.text)];
|
|
58
|
+
if (e.type === "result") {
|
|
59
|
+
const cost = e.costUSD != null ? ` ($${e.costUSD.toFixed(4)})` : "";
|
|
60
|
+
return [(e.ok ? green : red)(`Done${cost}`)];
|
|
61
|
+
}
|
|
62
|
+
if (e.type === "error")
|
|
63
|
+
return [red(`Error: ${e.message}`)];
|
|
64
|
+
return [];
|
|
65
|
+
};
|
|
56
66
|
class PipelineTUI {
|
|
57
67
|
agent;
|
|
58
68
|
gameDir;
|
|
59
69
|
repoContext;
|
|
60
70
|
states;
|
|
61
71
|
currentStep = 0;
|
|
62
|
-
outputLines = [];
|
|
63
|
-
chatLines = [];
|
|
64
72
|
phase = "idle";
|
|
65
73
|
done = false;
|
|
66
|
-
|
|
74
|
+
outputLines = [];
|
|
75
|
+
currentLine = "";
|
|
76
|
+
chatLines = [];
|
|
77
|
+
renderScheduled = false;
|
|
67
78
|
sidebarWidth = 30;
|
|
68
79
|
logsDir;
|
|
80
|
+
abortController = null;
|
|
69
81
|
constructor(agent, gameDir, repoContext) {
|
|
70
82
|
this.agent = agent;
|
|
71
83
|
this.gameDir = gameDir;
|
|
@@ -80,37 +92,79 @@ class PipelineTUI {
|
|
|
80
92
|
get cols() {
|
|
81
93
|
return process.stdout.columns || 80;
|
|
82
94
|
}
|
|
83
|
-
get
|
|
95
|
+
get outputWidth() {
|
|
96
|
+
return Math.max(this.cols - this.sidebarWidth - 1, 20);
|
|
97
|
+
}
|
|
98
|
+
get contentRows() {
|
|
84
99
|
return Math.max(this.rows - 4, 8);
|
|
85
100
|
}
|
|
86
101
|
write(s) {
|
|
87
102
|
process.stdout.write(s);
|
|
88
103
|
}
|
|
89
|
-
|
|
104
|
+
scheduleRender() {
|
|
105
|
+
if (this.renderScheduled)
|
|
106
|
+
return;
|
|
107
|
+
this.renderScheduled = true;
|
|
108
|
+
setImmediate(() => {
|
|
109
|
+
this.renderScheduled = false;
|
|
110
|
+
this.render();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Lock the in-progress streaming line as a complete entry. */
|
|
114
|
+
lockCurrent() {
|
|
115
|
+
if (this.currentLine.length === 0)
|
|
116
|
+
return;
|
|
117
|
+
this.outputLines.push(this.currentLine);
|
|
118
|
+
this.chatLines.push(stripAnsi(this.currentLine));
|
|
119
|
+
this.currentLine = "";
|
|
120
|
+
}
|
|
121
|
+
/** Append streaming text. Newlines lock the in-progress line; the trailing
|
|
122
|
+
* fragment continues as the in-progress line. Optional styler wraps each
|
|
123
|
+
* fragment in ANSI codes (e.g. gray() for thinking). */
|
|
124
|
+
appendText(text, styler) {
|
|
125
|
+
if (text.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
const parts = text.split("\n");
|
|
128
|
+
this.currentLine += styler ? styler(parts[0]) : parts[0];
|
|
129
|
+
for (let i = 1; i < parts.length; i++) {
|
|
130
|
+
this.lockCurrent();
|
|
131
|
+
this.currentLine = styler ? styler(parts[i]) : parts[i];
|
|
132
|
+
}
|
|
133
|
+
if (this.outputLines.length > 1000)
|
|
134
|
+
this.outputLines = this.outputLines.slice(-this.contentRows * 4);
|
|
135
|
+
this.scheduleRender();
|
|
136
|
+
}
|
|
137
|
+
/** Append discrete pre-formatted lines (each becomes its own row). Locks any
|
|
138
|
+
* in-progress streaming line first so token streams don't bleed into a
|
|
139
|
+
* following tool_use marker. */
|
|
140
|
+
appendLines(lines) {
|
|
141
|
+
if (lines.length === 0)
|
|
142
|
+
return;
|
|
143
|
+
this.lockCurrent();
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
this.outputLines.push(line);
|
|
146
|
+
this.chatLines.push(stripAnsi(line));
|
|
147
|
+
}
|
|
148
|
+
if (this.outputLines.length > 1000)
|
|
149
|
+
this.outputLines = this.outputLines.slice(-this.contentRows * 4);
|
|
150
|
+
this.scheduleRender();
|
|
151
|
+
}
|
|
90
152
|
render() {
|
|
91
|
-
const { rows,
|
|
92
|
-
const outputWidth = cols - sidebarWidth - 1;
|
|
153
|
+
const { rows, sidebarWidth, outputWidth, contentRows } = this;
|
|
93
154
|
let buf = "";
|
|
94
155
|
buf += clearScreen();
|
|
95
156
|
buf += moveTo(1, 1);
|
|
96
|
-
// Top border
|
|
97
157
|
buf += "╭" + "─".repeat(sidebarWidth - 2) + "┬" + "─".repeat(outputWidth) + "╮";
|
|
98
|
-
// Sidebar header
|
|
99
158
|
buf += moveTo(2, 1);
|
|
100
159
|
buf += "│ " + bold("Migration Steps") + " ".repeat(Math.max(0, sidebarWidth - 18)) + "│";
|
|
101
|
-
// Output panel header
|
|
102
160
|
const headerLabel = this.done ? "Done" : `${skillsPipeline[this.currentStep]?.name ?? ""} ${dim(`(${this.phase})`)}`;
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
buf += " ".repeat(Math.max(0, outputWidth -
|
|
106
|
-
// Separator
|
|
161
|
+
const styledHeader = phaseColor[this.phase](bold(headerLabel));
|
|
162
|
+
const headerVisible = stripAnsi(headerLabel).length;
|
|
163
|
+
buf += " " + styledHeader + " ".repeat(Math.max(0, outputWidth - headerVisible - 1)) + "│";
|
|
107
164
|
buf += moveTo(3, 1);
|
|
108
165
|
buf += "├" + "─".repeat(sidebarWidth - 2) + "┼" + "─".repeat(outputWidth) + "┤";
|
|
109
|
-
// Content rows
|
|
110
|
-
const contentRows = rows - 4; // top border + header + separator + bottom border
|
|
111
166
|
for (let row = 0; row < contentRows; row++) {
|
|
112
167
|
buf += moveTo(row + 4, 1);
|
|
113
|
-
// Sidebar column
|
|
114
168
|
const skillIndex = row;
|
|
115
169
|
if (skillIndex < skillsPipeline.length) {
|
|
116
170
|
const skill = skillsPipeline[skillIndex];
|
|
@@ -120,11 +174,10 @@ class PipelineTUI {
|
|
|
120
174
|
const colorFn = stateColor[state];
|
|
121
175
|
const label = `${icon} ${skill.name}`;
|
|
122
176
|
const styled = isActive ? bold(colorFn(label)) : colorFn(label);
|
|
123
|
-
const rawLen =
|
|
177
|
+
const rawLen = stripAnsi(styled).length;
|
|
124
178
|
buf += "│ " + styled + " ".repeat(Math.max(0, sidebarWidth - rawLen - 3)) + "│";
|
|
125
179
|
}
|
|
126
180
|
else if (skillIndex === skillsPipeline.length + 1) {
|
|
127
|
-
// Status line
|
|
128
181
|
const completedCount = this.states.filter((s) => s === "success").length;
|
|
129
182
|
const failedCount = this.states.filter((s) => s === "failed").length;
|
|
130
183
|
const status = this.done
|
|
@@ -132,248 +185,78 @@ class PipelineTUI {
|
|
|
132
185
|
? red(`${failedCount} failure(s)`)
|
|
133
186
|
: green("All complete!")
|
|
134
187
|
: dim(`${completedCount}/${skillsPipeline.length} done`);
|
|
135
|
-
const rawLen =
|
|
188
|
+
const rawLen = stripAnsi(status).length;
|
|
136
189
|
buf += "│ " + status + " ".repeat(Math.max(0, sidebarWidth - rawLen - 3)) + "│";
|
|
137
190
|
}
|
|
138
191
|
else {
|
|
139
192
|
buf += "│" + " ".repeat(sidebarWidth - 2) + "│";
|
|
140
193
|
}
|
|
141
|
-
|
|
142
|
-
const lineIndex =
|
|
143
|
-
const
|
|
144
|
-
const truncated =
|
|
145
|
-
const visibleLen =
|
|
194
|
+
const visibleCount = this.outputLines.length + (this.currentLine.length > 0 ? 1 : 0);
|
|
195
|
+
const lineIndex = visibleCount - contentRows + row;
|
|
196
|
+
const rawLine = lineIndex < 0 ? "" : lineIndex < this.outputLines.length ? (this.outputLines[lineIndex] ?? "") : this.currentLine;
|
|
197
|
+
const truncated = truncateVisible(rawLine, outputWidth - 2);
|
|
198
|
+
const visibleLen = stripAnsi(truncated).length;
|
|
146
199
|
buf += " " + truncated + " ".repeat(Math.max(0, outputWidth - visibleLen - 1)) + "│";
|
|
147
200
|
}
|
|
148
|
-
// Bottom border
|
|
149
201
|
buf += moveTo(rows, 1);
|
|
150
202
|
buf += "╰" + "─".repeat(sidebarWidth - 2) + "┴" + "─".repeat(outputWidth) + "╯";
|
|
151
203
|
this.write(buf);
|
|
152
204
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (end !== -1) {
|
|
166
|
-
i = end + 1;
|
|
205
|
+
async runAgent(prompt) {
|
|
206
|
+
this.abortController = new AbortController();
|
|
207
|
+
let sawError = false;
|
|
208
|
+
let resultOk = null;
|
|
209
|
+
try {
|
|
210
|
+
for await (const event of this.agent.run({ prompt, cwd: this.gameDir, signal: this.abortController.signal })) {
|
|
211
|
+
if (event.type === "error")
|
|
212
|
+
sawError = true;
|
|
213
|
+
if (event.type === "result")
|
|
214
|
+
resultOk = event.ok;
|
|
215
|
+
if (event.type === "text") {
|
|
216
|
+
this.appendText(event.text);
|
|
167
217
|
continue;
|
|
168
218
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
// Include any trailing ANSI sequences (like reset codes) right after the cut point
|
|
174
|
-
while (i < s.length && s[i] === "\x1b" && s[i + 1] === "[") {
|
|
175
|
-
const end = s.indexOf("m", i);
|
|
176
|
-
if (end !== -1) {
|
|
177
|
-
i = end + 1;
|
|
178
|
-
}
|
|
179
|
-
else
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
return s.slice(0, i);
|
|
183
|
-
}
|
|
184
|
-
/** Parse stream-json output from claude, or plain text from other agents */
|
|
185
|
-
appendOutput(text) {
|
|
186
|
-
const rawLines = text.split("\n").filter((l) => l.trim());
|
|
187
|
-
for (const line of rawLines) {
|
|
188
|
-
const parsed = this.parseStreamLine(line);
|
|
189
|
-
if (parsed) {
|
|
190
|
-
for (const displayLine of parsed.split("\n")) {
|
|
191
|
-
this.outputLines.push(displayLine);
|
|
192
|
-
this.chatLines.push(this.stripAnsi(displayLine));
|
|
219
|
+
if (event.type === "thinking") {
|
|
220
|
+
this.appendText(event.text, gray);
|
|
221
|
+
continue;
|
|
193
222
|
}
|
|
223
|
+
const lines = formatDiscreteEvent(event);
|
|
224
|
+
if (lines.length > 0)
|
|
225
|
+
this.appendLines(lines);
|
|
194
226
|
}
|
|
195
227
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
this.render();
|
|
201
|
-
}
|
|
202
|
-
/** Format a tool use block into a display line */
|
|
203
|
-
formatToolUse(t) {
|
|
204
|
-
const name = t.name ?? "unknown";
|
|
205
|
-
if (name === "Edit")
|
|
206
|
-
return `${yellow("Edit")} ${t.input?.file_path ?? ""}`;
|
|
207
|
-
if (name === "Write")
|
|
208
|
-
return `${yellow("Write")} ${t.input?.file_path ?? ""}`;
|
|
209
|
-
if (name === "Read")
|
|
210
|
-
return `${yellow("Read")} ${t.input?.file_path ?? ""}`;
|
|
211
|
-
if (name === "Bash")
|
|
212
|
-
return `${yellow("Bash")} ${t.input?.command ?? ""}`;
|
|
213
|
-
if (name === "Glob")
|
|
214
|
-
return `${yellow("Glob")} ${t.input?.pattern ?? ""}`;
|
|
215
|
-
if (name === "Grep")
|
|
216
|
-
return `${yellow("Grep")} ${t.input?.pattern ?? ""}`;
|
|
217
|
-
return `${yellow(name)}`;
|
|
218
|
-
}
|
|
219
|
-
/** Extract human-readable text from a stream-json line, or pass through raw text */
|
|
220
|
-
parseStreamLine(line) {
|
|
221
|
-
try {
|
|
222
|
-
const obj = JSON.parse(line);
|
|
223
|
-
// Assistant messages - may contain text, tool_use, thinking, or a mix
|
|
224
|
-
if (obj.type === "assistant" && obj.message?.content) {
|
|
225
|
-
const content = obj.message.content;
|
|
226
|
-
const parts = [];
|
|
227
|
-
const texts = content.filter((c) => c.type === "text").map((c) => c.text);
|
|
228
|
-
if (texts.length > 0)
|
|
229
|
-
parts.push(texts.join(""));
|
|
230
|
-
const toolUses = content.filter((c) => c.type === "tool_use");
|
|
231
|
-
if (toolUses.length > 0)
|
|
232
|
-
parts.push(toolUses.map((t) => this.formatToolUse(t)).join("\n"));
|
|
233
|
-
// Thinking blocks - skip silently, they're just internal reasoning
|
|
234
|
-
if (parts.length > 0)
|
|
235
|
-
return parts.join("\n");
|
|
236
|
-
if (content.some((c) => c.type === "thinking"))
|
|
237
|
-
return null;
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
// Content block delta (streaming text chunks)
|
|
241
|
-
if (obj.type === "content_block_delta" && obj.delta?.text) {
|
|
242
|
-
return obj.delta.text;
|
|
243
|
-
}
|
|
244
|
-
// Standalone tool use event
|
|
245
|
-
if (obj.type === "tool_use") {
|
|
246
|
-
return this.formatToolUse(obj);
|
|
247
|
-
}
|
|
248
|
-
// User messages (tool results coming back)
|
|
249
|
-
if (obj.type === "user") {
|
|
250
|
-
const content = obj.message?.content;
|
|
251
|
-
if (!content)
|
|
252
|
-
return null;
|
|
253
|
-
// Show file create/update results
|
|
254
|
-
const result = obj.tool_use_result;
|
|
255
|
-
if (result?.type === "create")
|
|
256
|
-
return dim(`Created ${result.filePath}`);
|
|
257
|
-
if (result?.type === "update")
|
|
258
|
-
return dim(`Updated ${result.filePath}`);
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
// Rate limit events - suppress
|
|
262
|
-
if (obj.type === "rate_limit_event")
|
|
263
|
-
return null;
|
|
264
|
-
// Tool results
|
|
265
|
-
if (obj.type === "result") {
|
|
266
|
-
const cost = obj.cost_usd ? ` ($${obj.cost_usd.toFixed(4)})` : "";
|
|
267
|
-
return green(`Done${cost}`);
|
|
268
|
-
}
|
|
269
|
-
// System messages
|
|
270
|
-
if (obj.type === "system") {
|
|
271
|
-
if (obj.subtype === "init")
|
|
272
|
-
return dim(`Session started`);
|
|
273
|
-
if (obj.subtype === "hook_started")
|
|
274
|
-
return null;
|
|
275
|
-
if (obj.subtype === "hook_response")
|
|
276
|
-
return null;
|
|
277
|
-
if (obj.subtype === "task_started")
|
|
278
|
-
return dim(`Subagent: ${obj.description ?? "started"}`);
|
|
279
|
-
if (obj.subtype === "task_progress")
|
|
280
|
-
return dim(` ${obj.last_tool_name ?? "working"}${obj.description ? ": " + obj.description : ""}`);
|
|
281
|
-
if (obj.subtype === "task_notification")
|
|
282
|
-
return obj.status === "completed"
|
|
283
|
-
? dim(`Subagent done: ${obj.summary ?? ""}`)
|
|
284
|
-
: dim(`Subagent ${obj.status ?? "update"}: ${obj.summary ?? ""}`);
|
|
285
|
-
return dim(`[system:${obj.subtype}]`);
|
|
286
|
-
}
|
|
287
|
-
// Show unrecognized types so we can add support
|
|
288
|
-
return dim(`[${obj.type}${obj.subtype ? ":" + obj.subtype : ""}]`);
|
|
228
|
+
catch (e) {
|
|
229
|
+
this.appendLines([red(`Error: ${e.message ?? e}`)]);
|
|
230
|
+
return false;
|
|
289
231
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return line;
|
|
232
|
+
finally {
|
|
233
|
+
this.abortController = null;
|
|
293
234
|
}
|
|
235
|
+
this.lockCurrent();
|
|
236
|
+
if (resultOk != null)
|
|
237
|
+
return resultOk;
|
|
238
|
+
return !sawError;
|
|
294
239
|
}
|
|
295
|
-
/** Run a shell command, capturing output into the TUI panel */
|
|
296
240
|
runCmd(cmd) {
|
|
297
241
|
try {
|
|
298
242
|
const output = execSync(cmd, { cwd: this.gameDir, encoding: "utf-8", stdio: "pipe" });
|
|
299
243
|
if (output.trim())
|
|
300
|
-
this.
|
|
244
|
+
this.appendLines(output.trim().split("\n"));
|
|
301
245
|
return { success: true, output };
|
|
302
246
|
}
|
|
303
247
|
catch (e) {
|
|
304
248
|
const output = (e.stdout || "") + (e.stderr || "") || e.message;
|
|
305
249
|
if (output.trim())
|
|
306
|
-
this.
|
|
250
|
+
this.appendLines(output.trim().split("\n"));
|
|
307
251
|
return { success: false, output };
|
|
308
252
|
}
|
|
309
253
|
}
|
|
310
|
-
/** Spawn agent and stream output */
|
|
311
|
-
runAgent(prompt) {
|
|
312
|
-
return new Promise((resolve) => {
|
|
313
|
-
const { cmd, args } = buildAgentCmd(this.agent, prompt);
|
|
314
|
-
const env = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
|
|
315
|
-
// Remove env vars that prevent nested claude sessions
|
|
316
|
-
delete env.CLAUDECODE;
|
|
317
|
-
const proc = spawn(cmd, args, {
|
|
318
|
-
cwd: this.gameDir,
|
|
319
|
-
env,
|
|
320
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
321
|
-
});
|
|
322
|
-
this.activeProc = proc;
|
|
323
|
-
let resolved = false;
|
|
324
|
-
let gotResult = false;
|
|
325
|
-
const debugLog = path.join(this.gameDir, "pipeline-debug.log");
|
|
326
|
-
const finish = (success) => {
|
|
327
|
-
if (resolved)
|
|
328
|
-
return;
|
|
329
|
-
resolved = true;
|
|
330
|
-
this.activeProc = null;
|
|
331
|
-
resolve(success);
|
|
332
|
-
};
|
|
333
|
-
proc.stdout?.on("data", (data) => {
|
|
334
|
-
const text = data.toString();
|
|
335
|
-
fs.appendFileSync(debugLog, text);
|
|
336
|
-
this.appendOutput(text);
|
|
337
|
-
// Detect the result event in stream-json - the agent is done
|
|
338
|
-
if (!gotResult) {
|
|
339
|
-
for (const line of text.split("\n")) {
|
|
340
|
-
try {
|
|
341
|
-
const obj = JSON.parse(line);
|
|
342
|
-
if (obj.type === "result") {
|
|
343
|
-
gotResult = true;
|
|
344
|
-
// Give the process a moment to exit cleanly, then force-resolve
|
|
345
|
-
setTimeout(() => {
|
|
346
|
-
if (!resolved) {
|
|
347
|
-
proc.kill();
|
|
348
|
-
finish(true);
|
|
349
|
-
}
|
|
350
|
-
}, 3000);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
catch { }
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
proc.stderr?.on("data", (data) => {
|
|
358
|
-
fs.appendFileSync(debugLog, `[stderr] ${data.toString()}`);
|
|
359
|
-
this.appendOutput(data.toString());
|
|
360
|
-
});
|
|
361
|
-
proc.on("close", (code) => {
|
|
362
|
-
fs.appendFileSync(debugLog, `\n[close] code=${code}\n`);
|
|
363
|
-
finish(gotResult || code === 0);
|
|
364
|
-
});
|
|
365
|
-
proc.on("error", (err) => {
|
|
366
|
-
this.appendOutput(`Error: ${err.message}`);
|
|
367
|
-
finish(false);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
/** Run one pipeline step */
|
|
372
254
|
async runStep(stepIndex) {
|
|
373
255
|
const skill = skillsPipeline[stepIndex];
|
|
374
256
|
this.states[stepIndex] = "running";
|
|
375
257
|
this.currentStep = stepIndex;
|
|
376
258
|
this.outputLines = [];
|
|
259
|
+
this.currentLine = "";
|
|
377
260
|
this.chatLines = [];
|
|
378
261
|
this.phase = "agent";
|
|
379
262
|
this.render();
|
|
@@ -382,71 +265,58 @@ class PipelineTUI {
|
|
|
382
265
|
prompt = await buildPrompt(skill.name, this.gameDir, this.repoContext);
|
|
383
266
|
}
|
|
384
267
|
catch (e) {
|
|
385
|
-
this.
|
|
268
|
+
this.appendLines([red(`Failed to fetch instructions: ${e.message}`)]);
|
|
386
269
|
this.writeSkillLog(skill.name);
|
|
387
270
|
this.states[stepIndex] = "failed";
|
|
388
|
-
this.render();
|
|
389
271
|
return false;
|
|
390
272
|
}
|
|
391
273
|
let success = await this.runAgent(prompt);
|
|
392
274
|
if (!success) {
|
|
393
|
-
this.
|
|
275
|
+
this.appendLines(["", dim("--- Retrying ---")]);
|
|
394
276
|
success = await this.runAgent(prompt);
|
|
395
277
|
if (!success) {
|
|
396
278
|
this.writeSkillLog(skill.name);
|
|
397
279
|
this.states[stepIndex] = "failed";
|
|
398
|
-
this.render();
|
|
399
280
|
return false;
|
|
400
281
|
}
|
|
401
282
|
}
|
|
402
|
-
// Verify build
|
|
403
283
|
this.phase = "build";
|
|
404
284
|
this.render();
|
|
405
|
-
this.
|
|
285
|
+
this.appendLines([dim("Verifying build...")]);
|
|
406
286
|
const buildResult = this.runCmd("npx vite build");
|
|
407
287
|
if (!buildResult.success) {
|
|
408
|
-
this.
|
|
288
|
+
this.appendLines([dim("Asking agent to fix...")]);
|
|
409
289
|
const fixPrompt = `The vite build failed after the "${skill.name}" step. Fix the build errors:\n\n${buildResult.output}`;
|
|
410
290
|
await this.runAgent(fixPrompt);
|
|
411
291
|
const retry = this.runCmd("npx vite build");
|
|
412
292
|
if (!retry.success) {
|
|
413
293
|
this.writeSkillLog(skill.name);
|
|
414
294
|
this.states[stepIndex] = "failed";
|
|
415
|
-
this.render();
|
|
416
295
|
return false;
|
|
417
296
|
}
|
|
418
297
|
}
|
|
419
|
-
// Commit
|
|
420
298
|
this.phase = "commit";
|
|
421
299
|
this.render();
|
|
422
300
|
this.runCmd("git add -A");
|
|
423
301
|
const commitResult = this.runCmd(`git commit -m "step: ${skill.name}"`);
|
|
424
|
-
|
|
425
|
-
this.appendOutput("Committed.");
|
|
426
|
-
else
|
|
427
|
-
this.appendOutput("No changes to commit.");
|
|
428
|
-
// Write chat log for this skill
|
|
302
|
+
this.appendLines([dim(commitResult.success ? "Committed." : "No changes to commit.")]);
|
|
429
303
|
this.writeSkillLog(skill.name);
|
|
430
304
|
this.states[stepIndex] = "success";
|
|
431
305
|
this.render();
|
|
432
306
|
return true;
|
|
433
307
|
}
|
|
434
|
-
/** Writes the collected chat lines for a skill to a log file */
|
|
435
308
|
writeSkillLog(skillName) {
|
|
436
309
|
const logPath = path.join(this.logsDir, `${skillName}.txt`);
|
|
437
310
|
fs.writeFileSync(logPath, this.chatLines.join("\n") + "\n");
|
|
438
311
|
}
|
|
439
|
-
/** Set up keyboard handling (Ctrl+C to quit) */
|
|
440
312
|
setupInput() {
|
|
441
|
-
if (process.stdin.isTTY)
|
|
313
|
+
if (process.stdin.isTTY)
|
|
442
314
|
process.stdin.setRawMode(true);
|
|
443
|
-
}
|
|
444
315
|
process.stdin.resume();
|
|
445
316
|
process.stdin.on("data", (data) => {
|
|
446
317
|
// Ctrl+C
|
|
447
318
|
if (data[0] === 3) {
|
|
448
|
-
|
|
449
|
-
this.activeProc.kill();
|
|
319
|
+
this.abortController?.abort();
|
|
450
320
|
this.cleanup();
|
|
451
321
|
process.exit(0);
|
|
452
322
|
}
|
|
@@ -454,22 +324,19 @@ class PipelineTUI {
|
|
|
454
324
|
}
|
|
455
325
|
cleanup() {
|
|
456
326
|
this.write(showCursor());
|
|
457
|
-
if (process.stdin.isTTY)
|
|
327
|
+
if (process.stdin.isTTY)
|
|
458
328
|
process.stdin.setRawMode(false);
|
|
459
|
-
}
|
|
460
329
|
process.stdin.pause();
|
|
461
330
|
}
|
|
462
|
-
/** Run the full pipeline */
|
|
463
331
|
async run() {
|
|
464
332
|
this.write(hideCursor());
|
|
465
333
|
this.setupInput();
|
|
466
334
|
this.render();
|
|
467
|
-
// Redraw on terminal resize
|
|
468
335
|
process.stdout.on("resize", () => this.render());
|
|
469
336
|
for (let i = 0; i < skillsPipeline.length; i++) {
|
|
470
337
|
const ok = await this.runStep(i);
|
|
471
338
|
if (!ok) {
|
|
472
|
-
this.
|
|
339
|
+
this.appendLines(["", red("Pipeline stopped due to failure.")]);
|
|
473
340
|
break;
|
|
474
341
|
}
|
|
475
342
|
}
|
|
@@ -479,8 +346,34 @@ class PipelineTUI {
|
|
|
479
346
|
this.cleanup();
|
|
480
347
|
}
|
|
481
348
|
}
|
|
482
|
-
/**
|
|
483
|
-
|
|
349
|
+
/** Truncate a string to a visible width, preserving ANSI codes. */
|
|
350
|
+
const truncateVisible = (s, maxWidth) => {
|
|
351
|
+
let visible = 0;
|
|
352
|
+
let i = 0;
|
|
353
|
+
while (i < s.length && visible < maxWidth) {
|
|
354
|
+
if (s[i] === "\x1b" && s[i + 1] === "[") {
|
|
355
|
+
const end = s.indexOf("m", i);
|
|
356
|
+
if (end !== -1) {
|
|
357
|
+
i = end + 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
visible++;
|
|
362
|
+
i++;
|
|
363
|
+
}
|
|
364
|
+
while (i < s.length && s[i] === "\x1b" && s[i + 1] === "[") {
|
|
365
|
+
const end = s.indexOf("m", i);
|
|
366
|
+
if (end !== -1)
|
|
367
|
+
i = end + 1;
|
|
368
|
+
else
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
return s.slice(0, i);
|
|
372
|
+
};
|
|
373
|
+
export const runPipelineTUI = async (agentName, gameDir, repoContext) => {
|
|
374
|
+
const agent = getAgent(agentName);
|
|
375
|
+
if (!agent)
|
|
376
|
+
throw new Error(`Unknown agent: ${agentName}`);
|
|
484
377
|
const tui = new PipelineTUI(agent, gameDir, repoContext);
|
|
485
378
|
await tui.run();
|
|
486
379
|
};
|