@flumecode/runner 0.21.0-beta.1 → 0.23.0
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/dist/cli.js +1021 -116
- package/package.json +7 -1
- package/skills-plugin/skills/create-release/SKILL.md +25 -80
- package/skills-plugin/skills/preview-ui/SKILL.md +127 -0
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,652 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/version.ts
|
|
13
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
function readVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const path = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
18
|
+
const pkg = JSON.parse(readFileSync2(path, "utf8"));
|
|
19
|
+
return pkg.version ?? "unknown";
|
|
20
|
+
} catch {
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function compareVersions(a, b) {
|
|
25
|
+
const parse = (v) => (v.split("-")[0] ?? v).split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
26
|
+
const pa = parse(a);
|
|
27
|
+
const pb = parse(b);
|
|
28
|
+
const len = Math.max(pa.length, pb.length);
|
|
29
|
+
for (let i = 0; i < len; i++) {
|
|
30
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
31
|
+
if (diff !== 0) return diff;
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
var RUNNER_VERSION, RUNNER_VERSION_HEADER, RUNNER_MIN_VERSION_HEADER, RUNNER_LATEST_VERSION_HEADER;
|
|
36
|
+
var init_version = __esm({
|
|
37
|
+
"src/version.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
RUNNER_VERSION = readVersion();
|
|
40
|
+
RUNNER_VERSION_HEADER = "x-flumecode-runner-version";
|
|
41
|
+
RUNNER_MIN_VERSION_HEADER = "x-flumecode-min-runner-version";
|
|
42
|
+
RUNNER_LATEST_VERSION_HEADER = "x-flumecode-latest-runner-version";
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/tui-api.ts
|
|
47
|
+
function authHeaders(cfg) {
|
|
48
|
+
return {
|
|
49
|
+
Authorization: `Bearer ${cfg.apiToken ?? ""}`,
|
|
50
|
+
"x-flumecode-runner-version": RUNNER_VERSION
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function safeText2(res) {
|
|
54
|
+
try {
|
|
55
|
+
return await res.text();
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function get(cfg, path) {
|
|
61
|
+
const res = await fetch(cfg.serverUrl + path, { headers: authHeaders(cfg) });
|
|
62
|
+
if (!res.ok) throw new Error(await safeText2(res));
|
|
63
|
+
return res.json();
|
|
64
|
+
}
|
|
65
|
+
async function post(cfg, path, body) {
|
|
66
|
+
const res = await fetch(cfg.serverUrl + path, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { ...authHeaders(cfg), "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify(body)
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) throw new Error(await safeText2(res));
|
|
72
|
+
return res.json();
|
|
73
|
+
}
|
|
74
|
+
var listRepositories, listAgents, listRequests, getRequest, createRequest, answerWidgets, acceptPlan, startImplementing, listJobs, listRunners;
|
|
75
|
+
var init_tui_api = __esm({
|
|
76
|
+
"src/tui-api.ts"() {
|
|
77
|
+
"use strict";
|
|
78
|
+
init_version();
|
|
79
|
+
listRepositories = (cfg) => get(cfg, "/api/cli/repositories").then((r) => r.repositories);
|
|
80
|
+
listAgents = (cfg) => get(cfg, "/api/cli/agents").then((r) => r.agents);
|
|
81
|
+
listRequests = (cfg, repoId) => get(cfg, `/api/cli/repositories/${repoId}/requests`).then(
|
|
82
|
+
(r) => r.requests
|
|
83
|
+
);
|
|
84
|
+
getRequest = (cfg, id) => get(cfg, `/api/cli/requests/${id}`);
|
|
85
|
+
createRequest = (cfg, input) => post(cfg, "/api/cli/requests", input);
|
|
86
|
+
answerWidgets = (cfg, messageId, payload) => post(cfg, `/api/cli/messages/${messageId}/answer`, payload);
|
|
87
|
+
acceptPlan = (cfg, messageId, target, mergeBranch) => post(cfg, `/api/cli/plans/${messageId}/accept`, { target, mergeBranch });
|
|
88
|
+
startImplementing = (cfg, sessionId) => post(cfg, `/api/cli/sessions/${sessionId}/implement`, {});
|
|
89
|
+
listJobs = (cfg) => get(cfg, "/api/cli/jobs").then((r) => r.jobs);
|
|
90
|
+
listRunners = (cfg) => get(cfg, "/api/cli/runners").then((r) => r.runners);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// src/tui/usePoll.ts
|
|
95
|
+
import { useState, useEffect, useRef } from "react";
|
|
96
|
+
function usePoll(fetcher, intervalMs) {
|
|
97
|
+
const [data, setData] = useState(null);
|
|
98
|
+
const fetcherRef = useRef(fetcher);
|
|
99
|
+
fetcherRef.current = fetcher;
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
let cancelled = false;
|
|
102
|
+
function poll() {
|
|
103
|
+
fetcherRef.current().then((result) => {
|
|
104
|
+
if (!cancelled) setData(result);
|
|
105
|
+
}).catch(() => {
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
poll();
|
|
109
|
+
const id = setInterval(poll, intervalMs);
|
|
110
|
+
return () => {
|
|
111
|
+
cancelled = true;
|
|
112
|
+
clearInterval(id);
|
|
113
|
+
};
|
|
114
|
+
}, [intervalMs]);
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
var init_usePoll = __esm({
|
|
118
|
+
"src/tui/usePoll.ts"() {
|
|
119
|
+
"use strict";
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// src/tui/StatusBar.tsx
|
|
124
|
+
import { Box, Text } from "ink";
|
|
125
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
126
|
+
function StatusBar({ jobs, runners }) {
|
|
127
|
+
const runningJobs = jobs?.filter((j) => j.status === "running").length ?? 0;
|
|
128
|
+
const onlineRunners = runners?.filter((r) => r.status === "online").length ?? 0;
|
|
129
|
+
return /* @__PURE__ */ jsx(Box, { borderStyle: "single", paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
130
|
+
"Jobs running: ",
|
|
131
|
+
runningJobs,
|
|
132
|
+
" | Runners online: ",
|
|
133
|
+
onlineRunners
|
|
134
|
+
] }) });
|
|
135
|
+
}
|
|
136
|
+
var init_StatusBar = __esm({
|
|
137
|
+
"src/tui/StatusBar.tsx"() {
|
|
138
|
+
"use strict";
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// src/tui/RepoListScreen.tsx
|
|
143
|
+
import { useState as useState2 } from "react";
|
|
144
|
+
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
145
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
146
|
+
function RepoListScreen({ config, onSelect }) {
|
|
147
|
+
const repos = usePoll(() => listRepositories(config), 1e4);
|
|
148
|
+
const [cursor, setCursor] = useState2(0);
|
|
149
|
+
useInput((input, key) => {
|
|
150
|
+
if (!repos) return;
|
|
151
|
+
if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
|
|
152
|
+
else if (key.downArrow) setCursor((c) => Math.min(repos.length - 1, c + 1));
|
|
153
|
+
else if (key.return) {
|
|
154
|
+
const repo = repos[cursor];
|
|
155
|
+
if (repo) onSelect(repo);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
if (!repos) return /* @__PURE__ */ jsx2(Text2, { children: "Loading repositories\u2026" });
|
|
159
|
+
if (repos.length === 0) return /* @__PURE__ */ jsx2(Text2, { children: "No repositories found." });
|
|
160
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
161
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: "Select a repository (\u2191\u2193 Enter):" }),
|
|
162
|
+
repos.map((repo, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === cursor ? "cyan" : void 0, children: [
|
|
163
|
+
i === cursor ? "\u25B6 " : " ",
|
|
164
|
+
repo.fullName
|
|
165
|
+
] }, repo.id))
|
|
166
|
+
] });
|
|
167
|
+
}
|
|
168
|
+
var init_RepoListScreen = __esm({
|
|
169
|
+
"src/tui/RepoListScreen.tsx"() {
|
|
170
|
+
"use strict";
|
|
171
|
+
init_usePoll();
|
|
172
|
+
init_tui_api();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// src/tui/RequestListScreen.tsx
|
|
177
|
+
import { useState as useState3 } from "react";
|
|
178
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
179
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
180
|
+
function RequestListScreen({ config, repo, onOpen, onNew, onBack }) {
|
|
181
|
+
const requests = usePoll(() => listRequests(config, repo.id), 5e3);
|
|
182
|
+
const [cursor, setCursor] = useState3(0);
|
|
183
|
+
useInput2((input, key) => {
|
|
184
|
+
if (key.escape) {
|
|
185
|
+
onBack();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!requests) return;
|
|
189
|
+
if (key.upArrow) setCursor((c) => Math.max(0, c - 1));
|
|
190
|
+
else if (key.downArrow) setCursor((c) => Math.min(requests.length - 1, c + 1));
|
|
191
|
+
else if (key.return) {
|
|
192
|
+
const req = requests[cursor];
|
|
193
|
+
if (req) onOpen(req);
|
|
194
|
+
} else if (input === "n") {
|
|
195
|
+
onNew();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (!requests) return /* @__PURE__ */ jsx3(Text3, { children: "Loading requests\u2026" });
|
|
199
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
200
|
+
/* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
|
|
201
|
+
repo.fullName,
|
|
202
|
+
" \u2014 Requests (\u2191\u2193 Enter open, n new, Esc back):"
|
|
203
|
+
] }),
|
|
204
|
+
requests.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No requests yet." }),
|
|
205
|
+
requests.map((req, i) => /* @__PURE__ */ jsxs3(Text3, { color: i === cursor ? "cyan" : void 0, children: [
|
|
206
|
+
i === cursor ? "\u25B6 " : " ",
|
|
207
|
+
"[",
|
|
208
|
+
req.status,
|
|
209
|
+
"] ",
|
|
210
|
+
req.title
|
|
211
|
+
] }, req.id))
|
|
212
|
+
] });
|
|
213
|
+
}
|
|
214
|
+
var init_RequestListScreen = __esm({
|
|
215
|
+
"src/tui/RequestListScreen.tsx"() {
|
|
216
|
+
"use strict";
|
|
217
|
+
init_usePoll();
|
|
218
|
+
init_tui_api();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// src/tui/NewRequestScreen.tsx
|
|
223
|
+
import { useState as useState4 } from "react";
|
|
224
|
+
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
225
|
+
import TextInput from "ink-text-input";
|
|
226
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
227
|
+
function NewRequestScreen({ config, repo, onCreated, onBack }) {
|
|
228
|
+
const agents = usePoll(() => listAgents(config), 3e4);
|
|
229
|
+
const [field, setField] = useState4("title");
|
|
230
|
+
const [title, setTitle] = useState4("");
|
|
231
|
+
const [body, setBody] = useState4("");
|
|
232
|
+
const [agentCursor, setAgentCursor] = useState4(0);
|
|
233
|
+
const [branch, setBranch] = useState4("");
|
|
234
|
+
const [error, setError] = useState4(null);
|
|
235
|
+
const [submitting, setSubmitting] = useState4(false);
|
|
236
|
+
useInput3((_input, key) => {
|
|
237
|
+
if (key.escape) {
|
|
238
|
+
onBack();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (field === "title" && key.tab) {
|
|
242
|
+
setField("body");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (field === "body" && key.tab) {
|
|
246
|
+
setField("agent");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (field === "agent") {
|
|
250
|
+
if (key.upArrow && agents) setAgentCursor((c) => Math.max(0, c - 1));
|
|
251
|
+
if (key.downArrow && agents) setAgentCursor((c) => Math.min((agents.length || 1) - 1, c + 1));
|
|
252
|
+
if (key.tab) setField("branch");
|
|
253
|
+
}
|
|
254
|
+
if (field === "branch" && key.return && !submitting) {
|
|
255
|
+
void submit();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
async function submit() {
|
|
259
|
+
if (!title.trim() || !agents?.[agentCursor]) return;
|
|
260
|
+
setSubmitting(true);
|
|
261
|
+
setError(null);
|
|
262
|
+
try {
|
|
263
|
+
const result = await createRequest(config, {
|
|
264
|
+
title: title.trim(),
|
|
265
|
+
body: body.trim(),
|
|
266
|
+
agentId: agents[agentCursor].id,
|
|
267
|
+
repoId: repo.id,
|
|
268
|
+
checkoutBranch: branch.trim() || void 0
|
|
269
|
+
});
|
|
270
|
+
onCreated(result.requestId);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
273
|
+
setSubmitting(false);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
|
|
277
|
+
/* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
|
|
278
|
+
"New Request in ",
|
|
279
|
+
repo.fullName,
|
|
280
|
+
" (Tab to advance, Esc back)"
|
|
281
|
+
] }),
|
|
282
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
283
|
+
/* @__PURE__ */ jsx4(Text4, { children: "Title: " }),
|
|
284
|
+
/* @__PURE__ */ jsx4(TextInput, { value: title, onChange: setTitle, focus: field === "title" })
|
|
285
|
+
] }),
|
|
286
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
287
|
+
/* @__PURE__ */ jsx4(Text4, { children: "Body: " }),
|
|
288
|
+
/* @__PURE__ */ jsx4(TextInput, { value: body, onChange: setBody, focus: field === "body" })
|
|
289
|
+
] }),
|
|
290
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
291
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
292
|
+
"Agent",
|
|
293
|
+
field === "agent" ? " (\u2191\u2193 to pick, Tab next)" : "",
|
|
294
|
+
":"
|
|
295
|
+
] }),
|
|
296
|
+
(agents ?? []).map((a, i) => /* @__PURE__ */ jsxs4(Text4, { color: field === "agent" && i === agentCursor ? "cyan" : void 0, children: [
|
|
297
|
+
field === "agent" && i === agentCursor ? "\u25B6 " : " ",
|
|
298
|
+
a.name
|
|
299
|
+
] }, a.id))
|
|
300
|
+
] }),
|
|
301
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
302
|
+
/* @__PURE__ */ jsx4(Text4, { children: "Checkout branch (optional): " }),
|
|
303
|
+
/* @__PURE__ */ jsx4(TextInput, { value: branch, onChange: setBranch, focus: field === "branch" })
|
|
304
|
+
] }),
|
|
305
|
+
submitting && /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "Creating\u2026" }),
|
|
306
|
+
error && /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
|
|
307
|
+
"Error: ",
|
|
308
|
+
error
|
|
309
|
+
] })
|
|
310
|
+
] });
|
|
311
|
+
}
|
|
312
|
+
var init_NewRequestScreen = __esm({
|
|
313
|
+
"src/tui/NewRequestScreen.tsx"() {
|
|
314
|
+
"use strict";
|
|
315
|
+
init_usePoll();
|
|
316
|
+
init_tui_api();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// src/tui/WidgetAnswerScreen.tsx
|
|
321
|
+
import { useState as useState5 } from "react";
|
|
322
|
+
import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
|
|
323
|
+
import TextInput2 from "ink-text-input";
|
|
324
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
325
|
+
function renderOptionLabel(opt, i, cursor, isSingle, checked) {
|
|
326
|
+
const pointer = i === cursor ? "\u25B6 " : " ";
|
|
327
|
+
if (isSingle) {
|
|
328
|
+
return `${pointer}${opt.label}`;
|
|
329
|
+
}
|
|
330
|
+
return `${pointer}[${checked.has(opt.id) ? "x" : " "}] ${opt.label}`;
|
|
331
|
+
}
|
|
332
|
+
function WidgetAnswerScreen({ config, message, onDone }) {
|
|
333
|
+
const [widgetIndex, setWidgetIndex] = useState5(0);
|
|
334
|
+
const [cursor, setCursor] = useState5(0);
|
|
335
|
+
const [checked, setChecked] = useState5(/* @__PURE__ */ new Set());
|
|
336
|
+
const [customText, setCustomText] = useState5("");
|
|
337
|
+
const [showCustom, setShowCustom] = useState5(false);
|
|
338
|
+
const [error, setError] = useState5(null);
|
|
339
|
+
const [submitting, setSubmitting] = useState5(false);
|
|
340
|
+
const widget = message.widgets[widgetIndex];
|
|
341
|
+
function handleSingleInput(input, key, w) {
|
|
342
|
+
const opts2 = [...w.options, { id: "__other__", label: "Other" }];
|
|
343
|
+
if (key.upArrow) {
|
|
344
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
345
|
+
} else if (key.downArrow) {
|
|
346
|
+
setCursor((c) => Math.min(opts2.length - 1, c + 1));
|
|
347
|
+
} else if (key.return) {
|
|
348
|
+
const picked = opts2[cursor];
|
|
349
|
+
if (picked?.id === "__other__") {
|
|
350
|
+
setShowCustom(true);
|
|
351
|
+
} else {
|
|
352
|
+
void advanceOrSubmit(w, picked?.id, void 0, void 0);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function handleMultiInput(input, key, w) {
|
|
357
|
+
const opts2 = [...w.options, { id: "__other__", label: "Other" }];
|
|
358
|
+
if (key.upArrow) {
|
|
359
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
360
|
+
} else if (key.downArrow) {
|
|
361
|
+
setCursor((c) => Math.min(opts2.length - 1, c + 1));
|
|
362
|
+
} else if (input === " ") {
|
|
363
|
+
const picked = opts2[cursor];
|
|
364
|
+
if (!picked) return;
|
|
365
|
+
if (picked.id === "__other__") {
|
|
366
|
+
setShowCustom((s) => !s);
|
|
367
|
+
} else {
|
|
368
|
+
const next = new Set(checked);
|
|
369
|
+
if (next.has(picked.id)) next.delete(picked.id);
|
|
370
|
+
else next.add(picked.id);
|
|
371
|
+
setChecked(next);
|
|
372
|
+
}
|
|
373
|
+
} else if (key.return) {
|
|
374
|
+
void advanceOrSubmit(w, void 0, [...checked], customText || void 0);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
useInput4((input, key) => {
|
|
378
|
+
if (!widget) return;
|
|
379
|
+
if (widget.type === "single_select") {
|
|
380
|
+
handleSingleInput(input, key, widget);
|
|
381
|
+
} else {
|
|
382
|
+
handleMultiInput(input, key, widget);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
async function advanceOrSubmit(w, optionId, optionIds, custom) {
|
|
386
|
+
const answers = [
|
|
387
|
+
{
|
|
388
|
+
widgetId: w.id,
|
|
389
|
+
...optionId !== void 0 ? { optionId } : {},
|
|
390
|
+
...optionIds !== void 0 ? { optionIds } : {},
|
|
391
|
+
...custom ? { customText: custom } : {}
|
|
392
|
+
}
|
|
393
|
+
];
|
|
394
|
+
if (widgetIndex < message.widgets.length - 1) {
|
|
395
|
+
setWidgetIndex((i) => i + 1);
|
|
396
|
+
setCursor(0);
|
|
397
|
+
setChecked(/* @__PURE__ */ new Set());
|
|
398
|
+
setCustomText("");
|
|
399
|
+
setShowCustom(false);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
setSubmitting(true);
|
|
403
|
+
setError(null);
|
|
404
|
+
try {
|
|
405
|
+
await answerWidgets(config, message.id, { requestId: message.requestId, answers });
|
|
406
|
+
onDone();
|
|
407
|
+
} catch (err) {
|
|
408
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
409
|
+
setSubmitting(false);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (!widget) return /* @__PURE__ */ jsx5(Text5, { children: "No widgets to answer." });
|
|
413
|
+
const isSingle = widget.type === "single_select";
|
|
414
|
+
const opts = [...widget.options, { id: "__other__", label: "Other" }];
|
|
415
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
|
|
416
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
417
|
+
"Widget ",
|
|
418
|
+
widgetIndex + 1,
|
|
419
|
+
"/",
|
|
420
|
+
message.widgets.length,
|
|
421
|
+
": ",
|
|
422
|
+
widget.question
|
|
423
|
+
] }),
|
|
424
|
+
widget.body && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: widget.body }),
|
|
425
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: isSingle ? "\u2191\u2193 to navigate, Enter to select" : "\u2191\u2193 to navigate, Space to toggle, Enter to confirm" }),
|
|
426
|
+
opts.map((opt, i) => /* @__PURE__ */ jsx5(Text5, { color: i === cursor ? "cyan" : void 0, children: renderOptionLabel(opt, i, cursor, isSingle, checked) }, opt.id)),
|
|
427
|
+
showCustom && /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
428
|
+
/* @__PURE__ */ jsx5(Text5, { children: "Custom answer: " }),
|
|
429
|
+
/* @__PURE__ */ jsx5(TextInput2, { value: customText, onChange: setCustomText, focus: showCustom })
|
|
430
|
+
] }),
|
|
431
|
+
submitting && /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Submitting\u2026" }),
|
|
432
|
+
error && /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
|
|
433
|
+
"Error: ",
|
|
434
|
+
error
|
|
435
|
+
] })
|
|
436
|
+
] });
|
|
437
|
+
}
|
|
438
|
+
var init_WidgetAnswerScreen = __esm({
|
|
439
|
+
"src/tui/WidgetAnswerScreen.tsx"() {
|
|
440
|
+
"use strict";
|
|
441
|
+
init_tui_api();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// src/tui/ThreadScreen.tsx
|
|
446
|
+
import { useState as useState6 } from "react";
|
|
447
|
+
import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
|
|
448
|
+
import TextInput3 from "ink-text-input";
|
|
449
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
450
|
+
function ThreadScreen({ config, requestId, onBack }) {
|
|
451
|
+
const data = usePoll(() => getRequest(config, requestId), 3e3);
|
|
452
|
+
const [cursor, setCursor] = useState6(0);
|
|
453
|
+
const [overlay, setOverlay] = useState6({ kind: "none" });
|
|
454
|
+
const [targetBranch, setTargetBranch] = useState6("main");
|
|
455
|
+
const [mergeBranch, setMergeBranch] = useState6("");
|
|
456
|
+
const [acceptStep, setAcceptStep] = useState6("target");
|
|
457
|
+
const [error, setError] = useState6(null);
|
|
458
|
+
const messages = data?.messages ?? [];
|
|
459
|
+
const sessions = data?.sessions ?? [];
|
|
460
|
+
const selectedMsg = messages[cursor];
|
|
461
|
+
const implementableSession = sessions.find((s) => s.status === "planned" || s.status === "ready");
|
|
462
|
+
useInput5((_input, key) => {
|
|
463
|
+
if (overlay.kind !== "none") return;
|
|
464
|
+
if (key.escape) {
|
|
465
|
+
onBack();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (key.upArrow) {
|
|
469
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (key.downArrow) {
|
|
473
|
+
setCursor((c) => Math.min(messages.length - 1, c + 1));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (key.return && selectedMsg) {
|
|
477
|
+
if (selectedMsg.widgets?.length) {
|
|
478
|
+
const widgets = selectedMsg.widgets;
|
|
479
|
+
setOverlay({ kind: "widget", message: { ...selectedMsg, widgets, requestId } });
|
|
480
|
+
} else if (selectedMsg.type === "plan") {
|
|
481
|
+
setTargetBranch("main");
|
|
482
|
+
setMergeBranch("");
|
|
483
|
+
setAcceptStep("target");
|
|
484
|
+
setOverlay({ kind: "accept", messageId: selectedMsg.id });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (_input === "i" && implementableSession) {
|
|
488
|
+
void doImplement(implementableSession.id);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
async function doAcceptPlan() {
|
|
492
|
+
if (overlay.kind !== "accept") return;
|
|
493
|
+
try {
|
|
494
|
+
await acceptPlan(
|
|
495
|
+
config,
|
|
496
|
+
overlay.messageId,
|
|
497
|
+
targetBranch.trim() || "main",
|
|
498
|
+
mergeBranch.trim() || "flumecode/session"
|
|
499
|
+
);
|
|
500
|
+
setOverlay({ kind: "accepted" });
|
|
501
|
+
} catch (err) {
|
|
502
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function doImplement(sessionId) {
|
|
506
|
+
try {
|
|
507
|
+
await startImplementing(config, sessionId);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (overlay.kind === "widget") {
|
|
513
|
+
return /* @__PURE__ */ jsx6(
|
|
514
|
+
WidgetAnswerScreen,
|
|
515
|
+
{
|
|
516
|
+
config,
|
|
517
|
+
message: overlay.message,
|
|
518
|
+
onDone: () => setOverlay({ kind: "none" })
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (overlay.kind === "accept") {
|
|
523
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
|
|
524
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: "Accept Plan" }),
|
|
525
|
+
/* @__PURE__ */ jsx6(Text6, { children: "Enter target branch (base):" }),
|
|
526
|
+
/* @__PURE__ */ jsx6(
|
|
527
|
+
TextInput3,
|
|
528
|
+
{
|
|
529
|
+
value: targetBranch,
|
|
530
|
+
onChange: setTargetBranch,
|
|
531
|
+
onSubmit: () => setAcceptStep("merge"),
|
|
532
|
+
focus: acceptStep === "target"
|
|
533
|
+
}
|
|
534
|
+
),
|
|
535
|
+
acceptStep === "merge" && /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
536
|
+
/* @__PURE__ */ jsx6(Text6, { children: "Enter merge branch name:" }),
|
|
537
|
+
/* @__PURE__ */ jsx6(
|
|
538
|
+
TextInput3,
|
|
539
|
+
{
|
|
540
|
+
value: mergeBranch,
|
|
541
|
+
onChange: setMergeBranch,
|
|
542
|
+
onSubmit: () => void doAcceptPlan(),
|
|
543
|
+
focus: true
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
] }),
|
|
547
|
+
error && /* @__PURE__ */ jsx6(Text6, { color: "red", children: error })
|
|
548
|
+
] });
|
|
549
|
+
}
|
|
550
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
551
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: "Thread (\u2191\u2193 navigate, Enter act on selected, i implement, Esc back)" }),
|
|
552
|
+
!data && /* @__PURE__ */ jsx6(Text6, { children: "Loading\u2026" }),
|
|
553
|
+
overlay.kind === "accepted" && /* @__PURE__ */ jsx6(Text6, { color: "green", children: "Plan accepted! Job queued." }),
|
|
554
|
+
messages.map((msg, i) => /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", marginY: 0, children: /* @__PURE__ */ jsxs6(Text6, { color: i === cursor ? "cyan" : void 0, children: [
|
|
555
|
+
i === cursor ? "\u25B6 " : " ",
|
|
556
|
+
"[",
|
|
557
|
+
msg.type,
|
|
558
|
+
"] ",
|
|
559
|
+
msg.body?.slice(0, 80) ?? "",
|
|
560
|
+
msg.widgets?.length ? ` (${msg.widgets.length} widget${msg.widgets.length > 1 ? "s" : ""})` : ""
|
|
561
|
+
] }) }, msg.id)),
|
|
562
|
+
implementableSession && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
563
|
+
"Press i to start implementing session ",
|
|
564
|
+
implementableSession.id.slice(0, 8),
|
|
565
|
+
"\u2026"
|
|
566
|
+
] }),
|
|
567
|
+
error && /* @__PURE__ */ jsx6(Text6, { color: "red", children: error })
|
|
568
|
+
] });
|
|
569
|
+
}
|
|
570
|
+
var init_ThreadScreen = __esm({
|
|
571
|
+
"src/tui/ThreadScreen.tsx"() {
|
|
572
|
+
"use strict";
|
|
573
|
+
init_usePoll();
|
|
574
|
+
init_tui_api();
|
|
575
|
+
init_WidgetAnswerScreen();
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// src/tui/App.tsx
|
|
580
|
+
var App_exports = {};
|
|
581
|
+
__export(App_exports, {
|
|
582
|
+
App: () => App
|
|
583
|
+
});
|
|
584
|
+
import { useState as useState7 } from "react";
|
|
585
|
+
import { Box as Box7 } from "ink";
|
|
586
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
587
|
+
function renderScreen(screen, setScreen, config) {
|
|
588
|
+
switch (screen.name) {
|
|
589
|
+
case "repos":
|
|
590
|
+
return /* @__PURE__ */ jsx7(
|
|
591
|
+
RepoListScreen,
|
|
592
|
+
{
|
|
593
|
+
config,
|
|
594
|
+
onSelect: (repo) => setScreen({ name: "requests", repo })
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
case "requests":
|
|
598
|
+
return /* @__PURE__ */ jsx7(
|
|
599
|
+
RequestListScreen,
|
|
600
|
+
{
|
|
601
|
+
config,
|
|
602
|
+
repo: screen.repo,
|
|
603
|
+
onOpen: (req) => setScreen({ name: "thread", repo: screen.repo, requestId: req.id }),
|
|
604
|
+
onNew: () => setScreen({ name: "new-request", repo: screen.repo }),
|
|
605
|
+
onBack: () => setScreen({ name: "repos" })
|
|
606
|
+
}
|
|
607
|
+
);
|
|
608
|
+
case "new-request":
|
|
609
|
+
return /* @__PURE__ */ jsx7(
|
|
610
|
+
NewRequestScreen,
|
|
611
|
+
{
|
|
612
|
+
config,
|
|
613
|
+
repo: screen.repo,
|
|
614
|
+
onCreated: (requestId) => setScreen({ name: "thread", repo: screen.repo, requestId }),
|
|
615
|
+
onBack: () => setScreen({ name: "requests", repo: screen.repo })
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
case "thread":
|
|
619
|
+
return /* @__PURE__ */ jsx7(
|
|
620
|
+
ThreadScreen,
|
|
621
|
+
{
|
|
622
|
+
config,
|
|
623
|
+
requestId: screen.requestId,
|
|
624
|
+
onBack: () => setScreen({ name: "requests", repo: screen.repo })
|
|
625
|
+
}
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function App({ config }) {
|
|
630
|
+
const [screen, setScreen] = useState7({ name: "repos" });
|
|
631
|
+
const jobs = usePoll(() => listJobs(config), 3e3);
|
|
632
|
+
const runners = usePoll(() => listRunners(config), 3e3);
|
|
633
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
634
|
+
renderScreen(screen, setScreen, config),
|
|
635
|
+
/* @__PURE__ */ jsx7(StatusBar, { jobs, runners })
|
|
636
|
+
] });
|
|
637
|
+
}
|
|
638
|
+
var init_App = __esm({
|
|
639
|
+
"src/tui/App.tsx"() {
|
|
640
|
+
"use strict";
|
|
641
|
+
init_tui_api();
|
|
642
|
+
init_usePoll();
|
|
643
|
+
init_StatusBar();
|
|
644
|
+
init_RepoListScreen();
|
|
645
|
+
init_RequestListScreen();
|
|
646
|
+
init_NewRequestScreen();
|
|
647
|
+
init_ThreadScreen();
|
|
648
|
+
}
|
|
649
|
+
});
|
|
2
650
|
|
|
3
651
|
// src/cli.ts
|
|
4
652
|
import { createInterface } from "node:readline/promises";
|
|
@@ -15,7 +663,9 @@ function readConfig() {
|
|
|
15
663
|
try {
|
|
16
664
|
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
17
665
|
if (!parsed.serverUrl || !parsed.token) return null;
|
|
18
|
-
|
|
666
|
+
const config = { serverUrl: parsed.serverUrl, token: parsed.token };
|
|
667
|
+
if (parsed.apiToken) config.apiToken = parsed.apiToken;
|
|
668
|
+
return config;
|
|
19
669
|
} catch {
|
|
20
670
|
return null;
|
|
21
671
|
}
|
|
@@ -26,38 +676,11 @@ function writeConfig(config) {
|
|
|
26
676
|
}
|
|
27
677
|
|
|
28
678
|
// src/run.ts
|
|
29
|
-
import { existsSync as
|
|
30
|
-
import { join as
|
|
31
|
-
|
|
32
|
-
// src/version.ts
|
|
33
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
34
|
-
import { fileURLToPath } from "node:url";
|
|
35
|
-
function readVersion() {
|
|
36
|
-
try {
|
|
37
|
-
const path = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
38
|
-
const pkg = JSON.parse(readFileSync2(path, "utf8"));
|
|
39
|
-
return pkg.version ?? "unknown";
|
|
40
|
-
} catch {
|
|
41
|
-
return "unknown";
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
var RUNNER_VERSION = readVersion();
|
|
45
|
-
var RUNNER_VERSION_HEADER = "x-flumecode-runner-version";
|
|
46
|
-
var RUNNER_MIN_VERSION_HEADER = "x-flumecode-min-runner-version";
|
|
47
|
-
var RUNNER_LATEST_VERSION_HEADER = "x-flumecode-latest-runner-version";
|
|
48
|
-
function compareVersions(a, b) {
|
|
49
|
-
const parse = (v) => (v.split("-")[0] ?? v).split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
50
|
-
const pa = parse(a);
|
|
51
|
-
const pb = parse(b);
|
|
52
|
-
const len = Math.max(pa.length, pb.length);
|
|
53
|
-
for (let i = 0; i < len; i++) {
|
|
54
|
-
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
55
|
-
if (diff !== 0) return diff;
|
|
56
|
-
}
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
679
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
680
|
+
import { join as join6 } from "node:path";
|
|
59
681
|
|
|
60
682
|
// src/api.ts
|
|
683
|
+
init_version();
|
|
61
684
|
var OUTDATED_WARN_INTERVAL_MS = 60 * 6e4;
|
|
62
685
|
var lastOutdatedWarnAt = 0;
|
|
63
686
|
var UPDATE_NUDGE_INTERVAL_MS = 60 * 6e4;
|
|
@@ -172,6 +795,20 @@ async function fetchRelatedSessions(config, params) {
|
|
|
172
795
|
return [];
|
|
173
796
|
}
|
|
174
797
|
}
|
|
798
|
+
async function mintUploadUrls(config, jobId, keys) {
|
|
799
|
+
const res = await fetch(`${config.serverUrl}/api/runner/previews/mint-upload-urls`, {
|
|
800
|
+
method: "POST",
|
|
801
|
+
headers: {
|
|
802
|
+
authorization: `Bearer ${config.token}`,
|
|
803
|
+
"content-type": "application/json",
|
|
804
|
+
[RUNNER_VERSION_HEADER]: RUNNER_VERSION
|
|
805
|
+
},
|
|
806
|
+
body: JSON.stringify({ jobId, keys })
|
|
807
|
+
});
|
|
808
|
+
noteServerVersion(res);
|
|
809
|
+
if (!res.ok) throw new Error(`mint-upload-urls failed: ${res.status} ${await safeText(res)}`);
|
|
810
|
+
return res.json();
|
|
811
|
+
}
|
|
175
812
|
async function safeText(res) {
|
|
176
813
|
try {
|
|
177
814
|
return await res.text();
|
|
@@ -1065,56 +1702,35 @@ function buildRepairPrompt(ctx, hookLog) {
|
|
|
1065
1702
|
];
|
|
1066
1703
|
return lines2.join("\n");
|
|
1067
1704
|
}
|
|
1068
|
-
function buildReleasePrompt(ctx
|
|
1069
|
-
const task = `Use the \`flumecode:create-release\` skill to handle this turn. You are driving a release: first analyse commits since the last tag, propose version bumps, and ask the user to confirm via widgets (Phase 1); once the user's widget answers appear in the thread,
|
|
1705
|
+
function buildReleasePrompt(ctx) {
|
|
1706
|
+
const task = `Use the \`flumecode:create-release\` skill to handle this turn. You are driving a release: first analyse commits since the last tag, propose version bumps, and ask the user to confirm via widgets (Phase 1); once the user's widget answers appear in the thread, emit the final report with the confirmed versions (Phase 2). Do NOT edit package.json or CHANGELOG.md, do NOT commit, push, or open a PR.`;
|
|
1070
1707
|
const orient = `Before investigating raw source, check for a FlumeCode wiki at \`.flumecode/wiki/\`. If it exists, read \`.flumecode/wiki/README.md\` first \u2014 it is the index \u2014 and follow its links to the pages and source paths relevant to this release. If there is no wiki, work from the code directly.`;
|
|
1071
1708
|
const widgets = `When you need the user to choose, ask it as a widget rather than writing the options as prose: call \`single_select\` for a one-of-N choice (radio buttons) or \`multi_select\` for a "select all that apply" choice (checkboxes). Don't add your own "Other" option \u2014 the UI always provides one. After calling a widget tool, end your turn \u2014 the user's answer comes back as their next message and starts a fresh run.`;
|
|
1072
1709
|
const lines2 = [
|
|
1073
1710
|
`You are "${ctx.agentName}", an autonomous coding agent driving a FlumeCode release.`,
|
|
1074
|
-
`The repository ${ctx.repo.fullName} is checked out in your current working directory
|
|
1711
|
+
`The repository ${ctx.repo.fullName} is checked out in your current working directory at the frozen release commit (branch "${ctx.repo.checkoutBranch}").`,
|
|
1075
1712
|
task,
|
|
1076
1713
|
orient,
|
|
1077
1714
|
widgets,
|
|
1078
1715
|
LANGUAGE_DIRECTIVE,
|
|
1079
1716
|
"",
|
|
1080
|
-
"These coding guidelines apply to all code produced in this run:",
|
|
1081
|
-
"",
|
|
1082
|
-
loadRule("coding-guideline"),
|
|
1083
|
-
"",
|
|
1084
1717
|
`# Release: ${ctx.request?.title ?? ""}`
|
|
1085
1718
|
];
|
|
1086
1719
|
if (ctx.request?.body) {
|
|
1087
1720
|
lines2.push("", ctx.request.body);
|
|
1088
1721
|
}
|
|
1089
|
-
if (baseChecks && !baseChecks.ok) {
|
|
1090
|
-
lines2.push(
|
|
1091
|
-
"",
|
|
1092
|
-
"# Pre-release check status",
|
|
1093
|
-
"",
|
|
1094
|
-
"\u26A0\uFE0F The repository's pre-commit checks (lint / typecheck / tests) are currently FAILING on the base branch, independently of any version bump. A release must not ship a broken base:",
|
|
1095
|
-
"",
|
|
1096
|
-
"- **Phase 1 (propose):** tell the user, in your reply, that the base currently fails these checks and that the release will fix them as part of the bump.",
|
|
1097
|
-
"- **Phase 2 (apply):** fix the failing code at its root so the checks pass, THEN apply the version bumps and CHANGELOG. Do NOT delete/skip tests or weaken assertions. The fixes ship in the same bump PR. Still do NOT commit or push \u2014 the runner does.",
|
|
1098
|
-
"",
|
|
1099
|
-
"Failing check output:",
|
|
1100
|
-
"",
|
|
1101
|
-
"```",
|
|
1102
|
-
baseChecks.log,
|
|
1103
|
-
"```"
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
1722
|
if (ctx.prerelease) {
|
|
1107
1723
|
lines2.push(
|
|
1108
1724
|
"",
|
|
1109
1725
|
"# Pre-release",
|
|
1110
1726
|
"",
|
|
1111
|
-
"This is a PRE-RELEASE. When proposing
|
|
1727
|
+
"This is a PRE-RELEASE. When proposing versions, use a semver pre-release version string (e.g. `0.9.0-beta.1`): take the next stable version you would otherwise pick and append `-beta.N`, where N is the next unused beta number for that version (check existing `v<version>-beta.*` tags). Offer these pre-release strings in the version-confirmation widgets, and include them in the `flumecode:versions` comment as usual."
|
|
1112
1728
|
);
|
|
1113
1729
|
}
|
|
1114
1730
|
appendThread(lines2, ctx);
|
|
1115
1731
|
lines2.push(
|
|
1116
1732
|
"",
|
|
1117
|
-
"Your final reply is posted verbatim as your comment in the release thread \u2014 if you called widgets (Phase 1), your reply text accompanies the questions; if you
|
|
1733
|
+
"Your final reply is posted verbatim as your comment in the release thread \u2014 if you called widgets (Phase 1), your reply text accompanies the questions; if you emitted the final report (Phase 2), make it the report the skill produced."
|
|
1118
1734
|
);
|
|
1119
1735
|
return lines2.join("\n");
|
|
1120
1736
|
}
|
|
@@ -1124,6 +1740,36 @@ function buildCodexPrompt(ctx) {
|
|
|
1124
1740
|
"\n"
|
|
1125
1741
|
);
|
|
1126
1742
|
}
|
|
1743
|
+
function buildPreviewPrompt(ctx, committedFileNames, tmpRoute) {
|
|
1744
|
+
const lines2 = [
|
|
1745
|
+
`You are "${ctx.agentName}" authoring an ephemeral UI showcase for ${ctx.repo.fullName}.`,
|
|
1746
|
+
`An implementation just ran and committed UI changes to branch "${ctx.repo.checkoutBranch}". Use the \`flumecode:preview-ui\` skill to create a temporary showcase page that imports the changed components and fills them with realistic fake data.`,
|
|
1747
|
+
`Write your showcase files under \`${tmpRoute}\` and write the URL path to \`${tmpRoute}/.showcase-path\`.`,
|
|
1748
|
+
`These files are ephemeral and git-excluded \u2014 do NOT commit or push them.`,
|
|
1749
|
+
"",
|
|
1750
|
+
"These coding guidelines apply to all code produced in this run:",
|
|
1751
|
+
"",
|
|
1752
|
+
loadRule("coding-guideline"),
|
|
1753
|
+
"",
|
|
1754
|
+
`# Request: ${ctx.request?.title ?? ""}`
|
|
1755
|
+
];
|
|
1756
|
+
if (ctx.request?.body) {
|
|
1757
|
+
lines2.push("", ctx.request.body);
|
|
1758
|
+
}
|
|
1759
|
+
if (committedFileNames.trim()) {
|
|
1760
|
+
lines2.push(
|
|
1761
|
+
"",
|
|
1762
|
+
"UI files changed by this implementation (showcase these components):",
|
|
1763
|
+
"",
|
|
1764
|
+
committedFileNames.trim()
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
lines2.push(
|
|
1768
|
+
"",
|
|
1769
|
+
"When done, reply with one short sentence confirming what showcase file you created."
|
|
1770
|
+
);
|
|
1771
|
+
return lines2.join("\n");
|
|
1772
|
+
}
|
|
1127
1773
|
function buildInitPrompt(ctx) {
|
|
1128
1774
|
return [
|
|
1129
1775
|
`You are "${ctx.agentName}" initializing FlumeCode for the repository ${ctx.repo.fullName}, checked out in your current working directory.`,
|
|
@@ -1311,27 +1957,6 @@ function commitFailureLog(err) {
|
|
|
1311
1957
|
const parts = [e.stdout, e.stderr].map((s) => typeof s === "string" ? s.trim() : "").filter((s) => s.length > 0);
|
|
1312
1958
|
return parts.length > 0 ? parts.join("\n") : e.message ?? String(err);
|
|
1313
1959
|
}
|
|
1314
|
-
function isUnsupportedGitSubcommand(err) {
|
|
1315
|
-
const e = err;
|
|
1316
|
-
const text = `${typeof e.stderr === "string" ? e.stderr : ""}
|
|
1317
|
-
${e.message ?? ""}`;
|
|
1318
|
-
return /is not a git command|unknown subcommand|usage: git hook/i.test(text);
|
|
1319
|
-
}
|
|
1320
|
-
async function runRepoChecks(dir) {
|
|
1321
|
-
try {
|
|
1322
|
-
await git(["-C", dir, "hook", "run", "pre-commit"]);
|
|
1323
|
-
logEvent("checks", "pre-commit hook passed");
|
|
1324
|
-
return { ok: true, log: "", skipped: false };
|
|
1325
|
-
} catch (err) {
|
|
1326
|
-
if (isUnsupportedGitSubcommand(err)) {
|
|
1327
|
-
logEvent("checks", "pre-commit hook skipped (git too old)");
|
|
1328
|
-
return { ok: true, log: "", skipped: true };
|
|
1329
|
-
}
|
|
1330
|
-
const log = commitFailureLog(err);
|
|
1331
|
-
logEvent("checks:err", log);
|
|
1332
|
-
return { ok: false, log, skipped: false };
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
1960
|
async function commitChanges(ctx, dir) {
|
|
1336
1961
|
if (!await hasChanges(dir)) return false;
|
|
1337
1962
|
try {
|
|
@@ -1428,10 +2053,9 @@ async function openPullRequest(ctx) {
|
|
|
1428
2053
|
return { number: data.number, url: data.html_url };
|
|
1429
2054
|
}
|
|
1430
2055
|
if (res.status === 422) {
|
|
1431
|
-
const list = await fetch(
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
);
|
|
2056
|
+
const list = await fetch(`${apiBase}/pulls?state=open&head=${owner}:${checkoutBranch}`, {
|
|
2057
|
+
headers
|
|
2058
|
+
});
|
|
1435
2059
|
if (list.ok) {
|
|
1436
2060
|
const open = await list.json();
|
|
1437
2061
|
if (open[0]) return { number: open[0].number, url: open[0].html_url };
|
|
@@ -1440,6 +2064,18 @@ async function openPullRequest(ctx) {
|
|
|
1440
2064
|
}
|
|
1441
2065
|
throw new Error(`PR creation failed: ${res.status} ${await res.text()}`);
|
|
1442
2066
|
}
|
|
2067
|
+
async function gitCommittedFiles(dir) {
|
|
2068
|
+
const { stdout: stdout2 } = await git([
|
|
2069
|
+
"-C",
|
|
2070
|
+
dir,
|
|
2071
|
+
"--no-pager",
|
|
2072
|
+
"show",
|
|
2073
|
+
"--name-only",
|
|
2074
|
+
"--format=",
|
|
2075
|
+
"HEAD"
|
|
2076
|
+
]);
|
|
2077
|
+
return stdout2;
|
|
2078
|
+
}
|
|
1443
2079
|
async function cleanup(dir) {
|
|
1444
2080
|
await rm(dir, { recursive: true, force: true });
|
|
1445
2081
|
}
|
|
@@ -1499,6 +2135,250 @@ async function prNumbersForCommit(ctx, sha) {
|
|
|
1499
2135
|
}
|
|
1500
2136
|
}
|
|
1501
2137
|
|
|
2138
|
+
// src/preview.ts
|
|
2139
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync } from "node:fs";
|
|
2140
|
+
import { createServer } from "node:net";
|
|
2141
|
+
import { join as join5, relative } from "node:path";
|
|
2142
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2143
|
+
var PREVIEW_MAX_TURNS = 60;
|
|
2144
|
+
var CAPTURE_TIMEOUT_MS = 3e4;
|
|
2145
|
+
var DEV_SERVER_READY_TIMEOUT_MS = 6e4;
|
|
2146
|
+
var VIEWPORTS = [
|
|
2147
|
+
{ name: "mobile", width: 375, height: 812 },
|
|
2148
|
+
{ name: "tablet", width: 768, height: 1024 },
|
|
2149
|
+
{ name: "desktop", width: 1280, height: 800 }
|
|
2150
|
+
];
|
|
2151
|
+
var UI_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2152
|
+
".tsx",
|
|
2153
|
+
".jsx",
|
|
2154
|
+
".vue",
|
|
2155
|
+
".svelte",
|
|
2156
|
+
".astro",
|
|
2157
|
+
".css",
|
|
2158
|
+
".scss",
|
|
2159
|
+
".sass",
|
|
2160
|
+
".less"
|
|
2161
|
+
]);
|
|
2162
|
+
function changedFilesTouchUi(stat) {
|
|
2163
|
+
for (const line of stat.split("\n")) {
|
|
2164
|
+
const trimmed = line.trim();
|
|
2165
|
+
if (!trimmed) continue;
|
|
2166
|
+
const dot = trimmed.lastIndexOf(".");
|
|
2167
|
+
if (dot === -1) continue;
|
|
2168
|
+
const ext = trimmed.slice(dot);
|
|
2169
|
+
if (UI_EXTENSIONS.has(ext)) return true;
|
|
2170
|
+
}
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
function detectFramework(dir) {
|
|
2174
|
+
const pm = detectPackageManager(dir);
|
|
2175
|
+
const pkgPath = join5(dir, "package.json");
|
|
2176
|
+
if (!existsSync4(pkgPath)) return { pm, family: null, devCmd: null };
|
|
2177
|
+
let pkg;
|
|
2178
|
+
try {
|
|
2179
|
+
pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
2180
|
+
} catch {
|
|
2181
|
+
return { pm, family: null, devCmd: null };
|
|
2182
|
+
}
|
|
2183
|
+
const deps = {
|
|
2184
|
+
...pkg.dependencies,
|
|
2185
|
+
...pkg.devDependencies
|
|
2186
|
+
};
|
|
2187
|
+
const family = pickFamily(deps);
|
|
2188
|
+
const port = 0;
|
|
2189
|
+
const devCmd = family ? pickDevCmd(family, port) : null;
|
|
2190
|
+
return { pm, family, devCmd };
|
|
2191
|
+
}
|
|
2192
|
+
async function runPreviewPass(ctx, dir, committedFileNames, config, abort) {
|
|
2193
|
+
const tmpRoute = mkTempRouteDir(dir);
|
|
2194
|
+
try {
|
|
2195
|
+
gitExclude(dir, tmpRoute);
|
|
2196
|
+
await runClaudeCode({
|
|
2197
|
+
cwd: dir,
|
|
2198
|
+
prompt: buildPreviewPrompt(ctx, committedFileNames, tmpRoute),
|
|
2199
|
+
permissionMode: ctx.permissionMode,
|
|
2200
|
+
abortController: abort,
|
|
2201
|
+
maxTurns: PREVIEW_MAX_TURNS
|
|
2202
|
+
});
|
|
2203
|
+
const showcasePath = readShowcasePath(tmpRoute);
|
|
2204
|
+
if (!showcasePath) {
|
|
2205
|
+
throw new Error("preview-ui skill did not write .showcase-path \u2014 skipping preview");
|
|
2206
|
+
}
|
|
2207
|
+
const framework = detectFramework(dir);
|
|
2208
|
+
if (!framework.family) {
|
|
2209
|
+
throw new Error("unsupported framework \u2014 skipping preview");
|
|
2210
|
+
}
|
|
2211
|
+
const port = await findFreePort();
|
|
2212
|
+
const devServer = await startDevServer(dir, framework, port);
|
|
2213
|
+
try {
|
|
2214
|
+
const url = `http://localhost:${port}${showcasePath}`;
|
|
2215
|
+
const shots = await captureWithChromium(url, VIEWPORTS);
|
|
2216
|
+
const { previewId, urls } = await mintUploadUrls(config, ctx.jobId, shots.keys);
|
|
2217
|
+
await uploadAll(urls, shots.keys, shots.bytes);
|
|
2218
|
+
return { previewId, entrypoint: shots.entrypointKey };
|
|
2219
|
+
} finally {
|
|
2220
|
+
await devServer.stop();
|
|
2221
|
+
}
|
|
2222
|
+
} finally {
|
|
2223
|
+
rmSync(tmpRoute, { recursive: true, force: true });
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
function pickFamily(deps) {
|
|
2227
|
+
if (deps["next"]) return "next";
|
|
2228
|
+
if (deps["@sveltejs/kit"]) return "svelte-kit";
|
|
2229
|
+
if (deps["nuxt"]) return "nuxt";
|
|
2230
|
+
if (deps["astro"]) return "astro";
|
|
2231
|
+
if (deps["react-scripts"]) return "cra";
|
|
2232
|
+
if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
|
|
2233
|
+
if (deps["vite"]) {
|
|
2234
|
+
if (deps["vue"]) return "vite-vue";
|
|
2235
|
+
if (deps["svelte"]) return "vite-svelte";
|
|
2236
|
+
if (deps["react"]) return "vite-react";
|
|
2237
|
+
return "vite-react";
|
|
2238
|
+
}
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2241
|
+
function pickDevCmd(family, port) {
|
|
2242
|
+
switch (family) {
|
|
2243
|
+
case "next":
|
|
2244
|
+
return ["next", "dev", "-p", String(port)];
|
|
2245
|
+
case "vite-react":
|
|
2246
|
+
case "vite-vue":
|
|
2247
|
+
case "vite-svelte":
|
|
2248
|
+
return ["vite", "--port", String(port)];
|
|
2249
|
+
case "nuxt":
|
|
2250
|
+
return ["nuxt", "dev", "--port", String(port)];
|
|
2251
|
+
case "svelte-kit":
|
|
2252
|
+
return ["vite", "--port", String(port)];
|
|
2253
|
+
case "astro":
|
|
2254
|
+
return ["astro", "dev", "--port", String(port)];
|
|
2255
|
+
case "cra":
|
|
2256
|
+
return ["react-scripts", "start"];
|
|
2257
|
+
case "remix":
|
|
2258
|
+
return ["remix", "dev", "--port", String(port)];
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
function mkTempRouteDir(dir) {
|
|
2262
|
+
const suffix = Math.random().toString(36).slice(2, 10);
|
|
2263
|
+
const tmpRoute = join5(dir, `__flumecode_preview_${suffix}`);
|
|
2264
|
+
mkdirSync2(tmpRoute, { recursive: true });
|
|
2265
|
+
return tmpRoute;
|
|
2266
|
+
}
|
|
2267
|
+
function gitExclude(dir, tmpRoute) {
|
|
2268
|
+
const excludePath = join5(dir, ".git", "info", "exclude");
|
|
2269
|
+
const rel = relative(dir, tmpRoute);
|
|
2270
|
+
appendFileSync(excludePath, `
|
|
2271
|
+
${rel}
|
|
2272
|
+
`);
|
|
2273
|
+
}
|
|
2274
|
+
function readShowcasePath(tmpRoute) {
|
|
2275
|
+
const sentinelPath = join5(tmpRoute, ".showcase-path");
|
|
2276
|
+
if (!existsSync4(sentinelPath)) return null;
|
|
2277
|
+
const content = readFileSync5(sentinelPath, "utf8").trim();
|
|
2278
|
+
return content.length > 0 ? content : null;
|
|
2279
|
+
}
|
|
2280
|
+
function findFreePort() {
|
|
2281
|
+
return new Promise((resolve, reject) => {
|
|
2282
|
+
const server = createServer();
|
|
2283
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2284
|
+
const addr = server.address();
|
|
2285
|
+
if (!addr || typeof addr === "string") {
|
|
2286
|
+
server.close();
|
|
2287
|
+
reject(new Error("Could not determine free port"));
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
const port = addr.port;
|
|
2291
|
+
server.close((err) => {
|
|
2292
|
+
if (err) reject(err);
|
|
2293
|
+
else resolve(port);
|
|
2294
|
+
});
|
|
2295
|
+
});
|
|
2296
|
+
server.on("error", reject);
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
function startDevServer(dir, framework, port) {
|
|
2300
|
+
return new Promise((resolve, reject) => {
|
|
2301
|
+
const pm = framework.pm ?? "npm";
|
|
2302
|
+
const devCmd = pickDevCmd(framework.family, port);
|
|
2303
|
+
const argv = [pm === "npm" ? "npx" : "exec", ...devCmd];
|
|
2304
|
+
const cmd = pm === "npm" ? "npx" : pm;
|
|
2305
|
+
const args = pm === "npm" ? devCmd : ["exec", ...devCmd];
|
|
2306
|
+
const proc = spawn2(cmd, args, {
|
|
2307
|
+
cwd: dir,
|
|
2308
|
+
env: { ...process.env, PORT: String(port) },
|
|
2309
|
+
stdio: "ignore"
|
|
2310
|
+
});
|
|
2311
|
+
proc.on("error", reject);
|
|
2312
|
+
const baseUrl = `http://localhost:${port}`;
|
|
2313
|
+
const deadline = Date.now() + DEV_SERVER_READY_TIMEOUT_MS;
|
|
2314
|
+
const poll = () => {
|
|
2315
|
+
fetch(baseUrl).then((res) => {
|
|
2316
|
+
if (res.ok || res.status < 500) {
|
|
2317
|
+
resolve({
|
|
2318
|
+
stop: () => new Promise((res2) => {
|
|
2319
|
+
proc.kill();
|
|
2320
|
+
proc.on("close", () => res2());
|
|
2321
|
+
setTimeout(res2, 500);
|
|
2322
|
+
})
|
|
2323
|
+
});
|
|
2324
|
+
} else {
|
|
2325
|
+
scheduleNext();
|
|
2326
|
+
}
|
|
2327
|
+
}).catch(() => scheduleNext());
|
|
2328
|
+
};
|
|
2329
|
+
const scheduleNext = () => {
|
|
2330
|
+
if (Date.now() > deadline) {
|
|
2331
|
+
proc.kill();
|
|
2332
|
+
reject(
|
|
2333
|
+
new Error(`Dev server did not become ready within ${DEV_SERVER_READY_TIMEOUT_MS}ms`)
|
|
2334
|
+
);
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
setTimeout(poll, 1e3);
|
|
2338
|
+
};
|
|
2339
|
+
setTimeout(poll, 2e3);
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
async function captureWithChromium(url, viewports) {
|
|
2343
|
+
const { chromium } = await import("playwright");
|
|
2344
|
+
const browser = await chromium.launch({ headless: true });
|
|
2345
|
+
const keys = [];
|
|
2346
|
+
const bytes = [];
|
|
2347
|
+
try {
|
|
2348
|
+
for (const vp of viewports) {
|
|
2349
|
+
const context = await browser.newContext({
|
|
2350
|
+
viewport: { width: vp.width, height: vp.height }
|
|
2351
|
+
});
|
|
2352
|
+
const page = await context.newPage();
|
|
2353
|
+
await page.goto(url, { timeout: CAPTURE_TIMEOUT_MS, waitUntil: "networkidle" });
|
|
2354
|
+
const screenshot = await page.screenshot({ fullPage: true, type: "png" });
|
|
2355
|
+
keys.push(`${vp.name}.png`);
|
|
2356
|
+
bytes.push(Buffer.from(screenshot));
|
|
2357
|
+
await context.close();
|
|
2358
|
+
}
|
|
2359
|
+
} finally {
|
|
2360
|
+
await browser.close();
|
|
2361
|
+
}
|
|
2362
|
+
return {
|
|
2363
|
+
keys,
|
|
2364
|
+
bytes,
|
|
2365
|
+
entrypointKey: keys[keys.length - 1] ?? "desktop.png"
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
async function uploadAll(urls, keys, bytes) {
|
|
2369
|
+
await Promise.all(
|
|
2370
|
+
keys.map((key, i) => {
|
|
2371
|
+
const url = urls[key];
|
|
2372
|
+
if (!url) return Promise.resolve();
|
|
2373
|
+
return fetch(url, {
|
|
2374
|
+
method: "PUT",
|
|
2375
|
+
headers: { "content-type": "image/png" },
|
|
2376
|
+
body: bytes[i]
|
|
2377
|
+
});
|
|
2378
|
+
})
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
1502
2382
|
// src/run.ts
|
|
1503
2383
|
var IDLE_MS = 5e3;
|
|
1504
2384
|
var CANCEL_POLL_MS = 2500;
|
|
@@ -1693,7 +2573,7 @@ async function processChatJob(ctx, dir, config, abort) {
|
|
|
1693
2573
|
console.log(` \u2026job ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
|
|
1694
2574
|
return { text: reply, widgets: result.widgets };
|
|
1695
2575
|
}
|
|
1696
|
-
const wikiExists =
|
|
2576
|
+
const wikiExists = existsSync5(join6(dir, ".flumecode", "wiki"));
|
|
1697
2577
|
let documented = false;
|
|
1698
2578
|
if (ctx.permissionMode !== "plan" && wikiExists && await hasChanges(dir)) {
|
|
1699
2579
|
try {
|
|
@@ -1782,7 +2662,7 @@ ${reply}`;
|
|
|
1782
2662
|
|
|
1783
2663
|
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
1784
2664
|
}
|
|
1785
|
-
const wikiExists =
|
|
2665
|
+
const wikiExists = existsSync5(join6(dir, ".flumecode", "wiki"));
|
|
1786
2666
|
let documented = false;
|
|
1787
2667
|
if (wikiExists && await hasChanges(dir)) {
|
|
1788
2668
|
try {
|
|
@@ -1807,11 +2687,24 @@ ${reply}`;
|
|
|
1807
2687
|
});
|
|
1808
2688
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
|
|
1809
2689
|
const finalReport = report && conflictResolution ? { ...report, conflictResolution } : report;
|
|
2690
|
+
let preview;
|
|
2691
|
+
if (outcome.kind !== "none") {
|
|
2692
|
+
const committedFileNames = await gitCommittedFiles(dir);
|
|
2693
|
+
if (changedFilesTouchUi(committedFileNames)) {
|
|
2694
|
+
try {
|
|
2695
|
+
console.log(` \u2026generating UI preview for implement ${ctx.jobId}`);
|
|
2696
|
+
preview = await runPreviewPass(ctx, dir, committedFileNames, config, abort);
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
console.warn(` preview skipped: ${errorMessage2(err)}`);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
1810
2702
|
return {
|
|
1811
2703
|
text: reply,
|
|
1812
2704
|
widgets: [],
|
|
1813
2705
|
...finalReport ? { report: finalReport } : {},
|
|
1814
|
-
...outcome.kind === "pr" ? { pr: outcome.pr } : {}
|
|
2706
|
+
...outcome.kind === "pr" ? { pr: outcome.pr } : {},
|
|
2707
|
+
...preview ? { preview } : {}
|
|
1815
2708
|
};
|
|
1816
2709
|
}
|
|
1817
2710
|
async function processReviseJob(ctx, dir, resumed, config, abort) {
|
|
@@ -1838,7 +2731,7 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
|
|
|
1838
2731
|
console.log(` \u2026revise ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`);
|
|
1839
2732
|
return { text: reply, widgets: result.widgets };
|
|
1840
2733
|
}
|
|
1841
|
-
const wikiExists =
|
|
2734
|
+
const wikiExists = existsSync5(join6(dir, ".flumecode", "wiki"));
|
|
1842
2735
|
let documented = false;
|
|
1843
2736
|
if (wikiExists && await hasChanges(dir)) {
|
|
1844
2737
|
try {
|
|
@@ -1865,12 +2758,25 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
|
|
|
1865
2758
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, documented, autoMerged });
|
|
1866
2759
|
}
|
|
1867
2760
|
const finalReport = report && conflictResolution ? { ...report, conflictResolution } : report;
|
|
2761
|
+
let preview;
|
|
2762
|
+
if (outcome.kind !== "none") {
|
|
2763
|
+
const committedFileNames = await gitCommittedFiles(dir);
|
|
2764
|
+
if (changedFilesTouchUi(committedFileNames)) {
|
|
2765
|
+
try {
|
|
2766
|
+
console.log(` \u2026generating UI preview for revise ${ctx.jobId}`);
|
|
2767
|
+
preview = await runPreviewPass(ctx, dir, committedFileNames, config, abort);
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
console.warn(` preview skipped: ${errorMessage2(err)}`);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
1868
2773
|
return {
|
|
1869
2774
|
text: reply,
|
|
1870
2775
|
widgets: [],
|
|
1871
2776
|
...finalReport ? { report: finalReport } : {},
|
|
1872
2777
|
...outcome.kind === "pr" ? { pr: outcome.pr } : {},
|
|
1873
|
-
...result.plans?.length ? { plans: result.plans } : {}
|
|
2778
|
+
...result.plans?.length ? { plans: result.plans } : {},
|
|
2779
|
+
...preview ? { preview } : {}
|
|
1874
2780
|
};
|
|
1875
2781
|
}
|
|
1876
2782
|
async function processResolveJob(ctx, dir, config, abort) {
|
|
@@ -1897,48 +2803,26 @@ async function processResolveJob(ctx, dir, config, abort) {
|
|
|
1897
2803
|
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch });
|
|
1898
2804
|
return { text: reply, widgets: [], ...report ? { report } : {}, ...pr ? { pr } : {} };
|
|
1899
2805
|
}
|
|
1900
|
-
async function processReleaseJob(ctx, dir,
|
|
2806
|
+
async function processReleaseJob(ctx, dir, _resumed, _config, abort) {
|
|
1901
2807
|
console.log(`
|
|
1902
2808
|
\u25B6 Release ${ctx.jobId} \u2014 ${ctx.repo.fullName}: "${jobTitle(ctx)}"`);
|
|
1903
|
-
|
|
1904
|
-
const checks = await runRepoChecks(dir);
|
|
1905
|
-
if (checks.skipped) {
|
|
1906
|
-
console.log(` \u2026release ${ctx.jobId}: pre-release checks skipped (git too old for 'hook run')`);
|
|
1907
|
-
} else {
|
|
1908
|
-
console.log(` \u2026release ${ctx.jobId}: pre-release checks ${checks.ok ? "passed" : "FAILED"}`);
|
|
1909
|
-
}
|
|
1910
|
-
const baseChecks = checks.ok ? void 0 : { ok: false, log: trimHookLog(checks.log) };
|
|
2809
|
+
await installDependencies(dir);
|
|
1911
2810
|
const result = await runClaudeCode({
|
|
1912
2811
|
cwd: dir,
|
|
1913
|
-
prompt: buildReleasePrompt(ctx
|
|
2812
|
+
prompt: buildReleasePrompt(ctx),
|
|
1914
2813
|
permissionMode: ctx.permissionMode,
|
|
1915
2814
|
model: orchestratorModel(ctx),
|
|
1916
2815
|
maxTurns: ORCHESTRATOR_MAX_TURNS,
|
|
1917
2816
|
abortController: abort
|
|
1918
2817
|
});
|
|
1919
|
-
|
|
1920
|
-
if (installResult.status === "failed") {
|
|
1921
|
-
reply += `
|
|
1922
|
-
|
|
1923
|
-
> \u26A0\uFE0F Dependencies failed to install (\`${installResult.manager}\`); tests may not have run.`;
|
|
1924
|
-
}
|
|
2818
|
+
const reply = result.text.trim() || "(the agent produced no reply)";
|
|
1925
2819
|
if (result.widgets.length > 0) {
|
|
1926
2820
|
console.log(
|
|
1927
2821
|
` \u2026release ${ctx.jobId} posted ${result.widgets.length} widget(s); awaiting reply`
|
|
1928
2822
|
);
|
|
1929
2823
|
return { text: reply, widgets: result.widgets };
|
|
1930
2824
|
}
|
|
1931
|
-
|
|
1932
|
-
rebase: !resumed
|
|
1933
|
-
});
|
|
1934
|
-
if (outcome.kind !== "none") {
|
|
1935
|
-
reply += outcomeBanner(outcome, { branch: ctx.repo.checkoutBranch, autoMerged });
|
|
1936
|
-
}
|
|
1937
|
-
return {
|
|
1938
|
-
text: reply,
|
|
1939
|
-
widgets: [],
|
|
1940
|
-
...outcome.kind === "pr" ? { pr: outcome.pr } : {}
|
|
1941
|
-
};
|
|
2825
|
+
return { text: reply, widgets: [] };
|
|
1942
2826
|
}
|
|
1943
2827
|
async function heartbeat(config) {
|
|
1944
2828
|
const health = await checkClaudeCode();
|
|
@@ -2004,7 +2888,7 @@ async function pollLoop(config) {
|
|
|
2004
2888
|
scheduleCancelPoll();
|
|
2005
2889
|
try {
|
|
2006
2890
|
resetUsage();
|
|
2007
|
-
const { text, widgets, pr, plans, report } = await processJob(ctx, config, abort);
|
|
2891
|
+
const { text, widgets, pr, plans, report, preview } = await processJob(ctx, config, abort);
|
|
2008
2892
|
const usage = getUsage();
|
|
2009
2893
|
await reportJob(config, ctx.jobId, {
|
|
2010
2894
|
status: "done",
|
|
@@ -2013,6 +2897,7 @@ async function pollLoop(config) {
|
|
|
2013
2897
|
pr,
|
|
2014
2898
|
...plans?.length ? { plans } : {},
|
|
2015
2899
|
...report ? { report } : {},
|
|
2900
|
+
...preview ? { preview } : {},
|
|
2016
2901
|
...usage.totalTokens > 0 ? { usage } : {}
|
|
2017
2902
|
});
|
|
2018
2903
|
console.log(`\u2713 Job ${ctx.jobId} done`);
|
|
@@ -2075,7 +2960,7 @@ ${trimmed}` : trimmed;
|
|
|
2075
2960
|
}
|
|
2076
2961
|
function formatJobError(ctx, err) {
|
|
2077
2962
|
if (!(err instanceof PreCommitError)) return errorMessage2(err);
|
|
2078
|
-
const nextStep = ctx.kind === "release" ? `These checks are failing on \`${ctx.repo.mergeBranch}\` independently of
|
|
2963
|
+
const nextStep = ctx.kind === "release" ? `These checks are failing on \`${ctx.repo.mergeBranch}\` independently of this release, and the release couldn't fix them after ${MAX_COMMIT_REPAIRS} automatic attempts. Open a request on **${ctx.repo.fullName}** to fix the failing checks above, then start the release again once that fix has merged.` : `The agent couldn't get its change past these checks after ${MAX_COMMIT_REPAIRS} automatic repair attempts. Open a request on **${ctx.repo.fullName}** describing the failing checks above so the agent can fix them at their root, then try again.`;
|
|
2079
2964
|
return [
|
|
2080
2965
|
"\u274C **Blocked by failing pre-commit checks.**",
|
|
2081
2966
|
"",
|
|
@@ -2092,6 +2977,8 @@ function formatJobError(ctx, err) {
|
|
|
2092
2977
|
}
|
|
2093
2978
|
|
|
2094
2979
|
// src/cli.ts
|
|
2980
|
+
init_version();
|
|
2981
|
+
import React7 from "react";
|
|
2095
2982
|
var DEFAULT_SERVER = process.env.FLUME_SERVER || "http://localhost:3000";
|
|
2096
2983
|
async function login(args) {
|
|
2097
2984
|
const flags = parseFlags(args);
|
|
@@ -2126,6 +3013,21 @@ async function start() {
|
|
|
2126
3013
|
}
|
|
2127
3014
|
await pollLoop(config);
|
|
2128
3015
|
}
|
|
3016
|
+
async function runTui(args) {
|
|
3017
|
+
const flags = parseFlags(args);
|
|
3018
|
+
let config = readConfig() ?? { serverUrl: flags.server ?? DEFAULT_SERVER, token: "" };
|
|
3019
|
+
if (flags.server) config.serverUrl = flags.server.replace(/\/+$/, "");
|
|
3020
|
+
if (flags.token) config.apiToken = flags.token;
|
|
3021
|
+
if (!config.apiToken) {
|
|
3022
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
3023
|
+
config.apiToken = (await rl.question("CLI token (flcli_\u2026): ")).trim();
|
|
3024
|
+
rl.close();
|
|
3025
|
+
}
|
|
3026
|
+
writeConfig(config);
|
|
3027
|
+
const { render } = await import("ink");
|
|
3028
|
+
const { App: App2 } = await Promise.resolve().then(() => (init_App(), App_exports));
|
|
3029
|
+
render(React7.createElement(App2, { config }));
|
|
3030
|
+
}
|
|
2129
3031
|
function parseFlags(args) {
|
|
2130
3032
|
const out = {};
|
|
2131
3033
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -2143,11 +3045,14 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
2143
3045
|
void login(rest);
|
|
2144
3046
|
} else if (command === "start") {
|
|
2145
3047
|
void start();
|
|
3048
|
+
} else if (command === "tui") {
|
|
3049
|
+
void runTui(rest);
|
|
2146
3050
|
} else {
|
|
2147
3051
|
console.log(`FlumeCode runner v${RUNNER_VERSION}`);
|
|
2148
3052
|
console.log("Usage:");
|
|
2149
3053
|
console.log(" flumecode login # save server URL + token");
|
|
2150
3054
|
console.log(" flumecode start # poll for and run jobs");
|
|
3055
|
+
console.log(" flumecode tui # open the interactive TUI");
|
|
2151
3056
|
console.log(" flumecode --version # print the runner version");
|
|
2152
3057
|
process.exit(command ? 1 : 0);
|
|
2153
3058
|
}
|