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