stageflow 0.18.0 → 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/README.md +2 -1
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/activityObserver.d.ts +3 -0
- package/dist/agent/activityObserver.js +6 -1
- package/dist/agent/claudeAdapter.js +38 -0
- package/dist/agent/fakeAgent.js +3 -1
- package/dist/agent/piAdapter.js +40 -4
- package/dist/agent/port.d.ts +3 -1
- package/dist/agent/providerInspect.d.ts +19 -0
- package/dist/agent/providerInspect.js +35 -0
- package/dist/agent/stageTimeout.d.ts +9 -0
- package/dist/agent/stageTimeout.js +24 -0
- package/dist/agent/streamLogRedact.d.ts +2 -0
- package/dist/agent/streamLogRedact.js +13 -0
- package/dist/cli/artifactCommand.js +2 -2
- package/dist/cli/envelopeCommand.js +2 -2
- package/dist/cli/exportRunCommand.js +2 -2
- package/dist/cli/graphCommand.d.ts +15 -0
- package/dist/cli/graphCommand.js +79 -0
- package/dist/cli/graphRender.d.ts +2 -0
- package/dist/cli/graphRender.js +85 -0
- package/dist/cli/hostClient.d.ts +43 -0
- package/dist/cli/hostClient.js +182 -0
- package/dist/cli/providersCommand.js +5 -3
- package/dist/cli/runCommand.d.ts +6 -5
- package/dist/cli/runCommand.js +23 -31
- package/dist/cli/runOutput.d.ts +1 -1
- package/dist/cli/runsCommand.d.ts +4 -8
- package/dist/cli/runsCommand.js +65 -89
- package/dist/cli.js +13 -5
- package/dist/config/describePipeline.d.ts +26 -0
- package/dist/config/describePipeline.js +37 -0
- package/dist/config/loadPipeline.d.ts +13 -1
- package/dist/config/loadPipeline.js +34 -4
- package/dist/config/mergePipelineIncludes.d.ts +11 -0
- package/dist/config/mergePipelineIncludes.js +75 -0
- package/dist/config/validateCatalog.d.ts +2 -2
- package/dist/config/validateCatalog.js +24 -17
- package/dist/mcp/catalogTools.js +146 -52
- package/dist/mcp/controlTools.js +94 -19
- package/dist/mcp/deps.d.ts +3 -0
- package/dist/mcp/projectMcpTools.d.ts +3 -0
- package/dist/mcp/projectMcpTools.js +25 -0
- package/dist/mcp/providerTools.d.ts +3 -0
- package/dist/mcp/providerTools.js +17 -0
- package/dist/mcp/readArtifact.d.ts +9 -0
- package/dist/mcp/readArtifact.js +11 -0
- package/dist/mcp/server.js +9 -1
- package/dist/mcp/tailStreamLog.d.ts +8 -0
- package/dist/mcp/tailStreamLog.js +28 -0
- package/dist/mcp/toolResults.d.ts +17 -0
- package/dist/mcp/toolResults.js +15 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/mcp/waitRun.js +4 -4
- package/dist/mcp/waitingGates.d.ts +1 -0
- package/dist/mcp/waitingGates.js +3 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +3 -0
- package/dist/projection/projectRun.js +7 -0
- package/dist/runstore/port.d.ts +3 -1
- package/dist/runstore/port.js +2 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +15 -1
- package/dist/runstore/workspaceLayout.d.ts +1 -0
- package/dist/runstore/workspaceLayout.js +3 -0
- package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
- package/dist/runtime/feedbackLoopCoordinator.js +50 -20
- package/dist/runtime/feedbackLoopDecision.js +33 -1
- package/dist/runtime/forkGeneration.d.ts +4 -0
- package/dist/runtime/forkGeneration.js +16 -0
- package/dist/runtime/pipelineRunner.d.ts +3 -3
- package/dist/runtime/pipelineRunner.js +3 -1
- package/dist/runtime/pipelineScheduler.js +23 -1
- package/dist/runtime/reopenSkipped.d.ts +13 -0
- package/dist/runtime/reopenSkipped.js +56 -0
- package/dist/runtime/resumeReconstruct.js +2 -2
- package/dist/runtime/resumeTimedOut.d.ts +33 -0
- package/dist/runtime/resumeTimedOut.js +165 -0
- package/dist/runtime/runManager.d.ts +3 -1
- package/dist/runtime/runManager.js +110 -14
- package/dist/runtime/settingsFile.d.ts +2 -0
- package/dist/runtime/settingsFile.js +19 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
- package/dist/runtime/stageAttemptBootstrap.js +8 -2
- package/dist/runtime/stageRecovery.js +2 -2
- package/dist/runtime/stageRunner.js +6 -2
- package/dist/runtime/stageStreamLog.d.ts +17 -0
- package/dist/runtime/stageStreamLog.js +89 -0
- package/dist/runtime/stageWorker.js +2 -1
- package/dist/server/bootstrap.js +9 -2
- package/dist/server/ensureGlobalService.d.ts +35 -0
- package/dist/server/ensureGlobalService.js +138 -0
- package/dist/server/http.d.ts +20 -1
- package/dist/server/http.js +452 -386
- package/dist/server/mcpHost.js +7 -8
- package/dist/server/operatorResults.d.ts +10 -0
- package/dist/server/operatorResults.js +24 -0
- package/dist/server/providerRoutes.d.ts +0 -6
- package/dist/server/providerRoutes.js +10 -18
- package/dist/tools/askOperator.d.ts +1 -0
- package/dist/tools/askOperator.js +14 -11
- package/dist/tools/emitStageEnvelope.d.ts +1 -0
- package/dist/tools/emitStageEnvelope.js +3 -1
- package/dist/tools/prepareToolArguments.d.ts +4 -0
- package/dist/tools/prepareToolArguments.js +79 -0
- package/dist/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DsgVjLUS.css → index-CSsDued1.css} +1 -1
- package/dist/ui/assets/index-Da5EHU6M.js +119 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/skills/stageflow/references/control-surface.md +2 -2
- package/skills/stageflow-run/SKILL.md +27 -9
- package/skills/stageflow-run/references/mcp-call.md +1 -1
- package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
- package/dist/ui/assets/index-DwKGaQvQ.js +0 -119
package/dist/server/http.js
CHANGED
|
@@ -2,7 +2,8 @@ import { readFile, access } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { getCredentialSourceSettings, setCredentialSource, } from "../agent/providerAuth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { mapProviderAuthError } from "../agent/providerInspect.js";
|
|
6
|
+
import { handleProviderRoutes } from "./providerRoutes.js";
|
|
6
7
|
import { handleProjectMcpRoutes } from "./projectMcpRoutes.js";
|
|
7
8
|
import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
|
|
8
9
|
import { createStage, parseCreateStageBody } from "../config/createStage.js";
|
|
@@ -12,12 +13,13 @@ import { listSkills } from "../config/listSkills.js";
|
|
|
12
13
|
import { artifactMediaType, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
|
|
13
14
|
import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
|
|
14
15
|
import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
|
|
16
|
+
import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
|
|
15
17
|
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
|
|
16
18
|
import { isTaskFile } from "../runtime/taskInput.js";
|
|
17
19
|
import { parseAskOperatorAnswer } from "../tools/askOperator.js";
|
|
18
20
|
import { bootstrapStageflowHost, } from "./bootstrap.js";
|
|
19
21
|
import { createHttpHost, DEFAULT_PORT, json, } from "./createHttpHost.js";
|
|
20
|
-
import { mapRetryStageFailure, mapStartFailure } from "./operatorResults.js";
|
|
22
|
+
import { mapRetryStageFailure, mapStartFailure, mapStoreLookupError, } from "./operatorResults.js";
|
|
21
23
|
function textPlain(res, status, body) {
|
|
22
24
|
res.writeHead(status, {
|
|
23
25
|
"Content-Type": "text/plain; charset=utf-8",
|
|
@@ -91,6 +93,7 @@ function isMutatingApi(method, pathname) {
|
|
|
91
93
|
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/answer$/.test(pathname) ||
|
|
92
94
|
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/feedback-decision$/.test(pathname) ||
|
|
93
95
|
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/retry$/.test(pathname) ||
|
|
96
|
+
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/resume$/.test(pathname) ||
|
|
94
97
|
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/recovery$/.test(pathname) ||
|
|
95
98
|
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/recovery\/stop$/.test(pathname) ||
|
|
96
99
|
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/abandon$/.test(pathname) ||
|
|
@@ -177,463 +180,526 @@ export function defaultUiDistDir() {
|
|
|
177
180
|
}
|
|
178
181
|
return path.resolve(here, "../ui");
|
|
179
182
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
183
|
+
/**
|
|
184
|
+
* The full operator-console route surface: REST API (runs, stages,
|
|
185
|
+
* catalog, settings, providers) plus, when `uiDistDir` is given, static
|
|
186
|
+
* console UI file serving. Shared by `startUiServer` (browser-facing) and
|
|
187
|
+
* `startMcpServer` (headless global-service daemon) — both need the REST
|
|
188
|
+
* API; only the former needs the UI files.
|
|
189
|
+
*/
|
|
190
|
+
export function createOperatorRoutes(deps) {
|
|
191
|
+
const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir } = deps;
|
|
192
|
+
return async ({ req, res, url, pathname, method }) => {
|
|
193
|
+
if (isMutatingApi(method, pathname)) {
|
|
194
|
+
if (!assertLoopbackHttpAccess(req, res)) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
if (isCredentialMutatingApi(method, pathname) &&
|
|
198
|
+
!assertCredentialMutatingOrigin(req, res)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
if (method === "GET" && pathname === "/api/runs") {
|
|
204
|
+
json(res, 200, { runs: await store.listRuns() });
|
|
205
|
+
return true;
|
|
200
206
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
|
|
208
|
+
if (method === "GET" && artifactMatch) {
|
|
209
|
+
const runId = decodeURIComponent(artifactMatch[1] ?? "");
|
|
210
|
+
const artifactPath = url.searchParams.get("path");
|
|
211
|
+
if (artifactPath === null || artifactPath.trim().length === 0) {
|
|
212
|
+
json(res, 400, { error: "path query parameter is required" });
|
|
204
213
|
return true;
|
|
205
214
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
try {
|
|
216
|
+
const mediaType = artifactMediaType(artifactPath);
|
|
217
|
+
if (mediaType !== undefined) {
|
|
218
|
+
const bytes = await readRunArtifactBytes(store, runId, artifactPath);
|
|
219
|
+
res.writeHead(200, {
|
|
220
|
+
"Content-Type": mediaType,
|
|
221
|
+
"Content-Length": bytes.length,
|
|
222
|
+
});
|
|
223
|
+
res.end(bytes);
|
|
213
224
|
}
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
const bytes = await readRunArtifactBytes(store, runId, artifactPath);
|
|
218
|
-
res.writeHead(200, {
|
|
219
|
-
"Content-Type": mediaType,
|
|
220
|
-
"Content-Length": bytes.length,
|
|
221
|
-
});
|
|
222
|
-
res.end(bytes);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
const content = await readRunArtifact(store, runId, artifactPath);
|
|
226
|
-
textPlain(res, 200, content);
|
|
227
|
-
}
|
|
225
|
+
else {
|
|
226
|
+
const content = await readRunArtifact(store, runId, artifactPath);
|
|
227
|
+
textPlain(res, 200, content);
|
|
228
228
|
}
|
|
229
|
-
catch (err) {
|
|
230
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
231
|
-
if (message === "Artifact path denied") {
|
|
232
|
-
json(res, 403, { error: message });
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
const notFound = message.startsWith("Run not found") ||
|
|
236
|
-
message.startsWith("Artifact not found") ||
|
|
237
|
-
/no such|not found/i.test(message);
|
|
238
|
-
json(res, notFound ? 404 : 400, { error: message });
|
|
239
|
-
}
|
|
240
|
-
return true;
|
|
241
229
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
230
|
+
catch (err) {
|
|
231
|
+
const mapped = mapStoreLookupError(err, { policy: "artifact" });
|
|
232
|
+
const status = mapped.kind === "denied" ? 403 : mapped.status;
|
|
233
|
+
json(res, status, { error: mapped.error });
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
const verificationMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/verification$/);
|
|
238
|
+
if (method === "GET" && verificationMatch) {
|
|
239
|
+
const runId = decodeURIComponent(verificationMatch[1] ?? "");
|
|
240
|
+
const stageId = decodeURIComponent(verificationMatch[2] ?? "");
|
|
241
|
+
try {
|
|
242
|
+
json(res, 200, await readStageVerificationHistory(store, runId, stageId));
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
246
|
+
json(res, mapped.status, { error: mapped.error });
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
if (method === "GET" && pathname.startsWith("/api/runs/")) {
|
|
251
|
+
const rest = pathname.slice("/api/runs/".length);
|
|
252
|
+
if (rest && !rest.includes("/")) {
|
|
246
253
|
try {
|
|
247
|
-
json(res, 200, await
|
|
254
|
+
json(res, 200, await store.readRun(decodeURIComponent(rest)));
|
|
248
255
|
}
|
|
249
256
|
catch (err) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
json(res, notFound ? 404 : 500, { error: message });
|
|
257
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
258
|
+
json(res, 404, { error: mapped.error });
|
|
253
259
|
}
|
|
254
260
|
return true;
|
|
255
261
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
error: err instanceof Error ? err.message : String(err),
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
262
|
+
}
|
|
263
|
+
if (method === "POST" && pathname === "/api/runs") {
|
|
264
|
+
const body = (await readJsonBody(req));
|
|
265
|
+
if (typeof body.pipeline !== "string" ||
|
|
266
|
+
!body.pipeline.trim() ||
|
|
267
|
+
body.task === undefined) {
|
|
268
|
+
json(res, 400, { error: "task and pipeline are required" });
|
|
269
|
+
return true;
|
|
269
270
|
}
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
!body.pipeline.trim() ||
|
|
274
|
-
body.task === undefined) {
|
|
275
|
-
json(res, 400, { error: "task and pipeline are required" });
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
if (typeof body.task !== "string" && !isTaskFile(body.task)) {
|
|
279
|
-
json(res, 400, {
|
|
280
|
-
error: "task must be a path string or TaskFile object (id and goal required)",
|
|
281
|
-
});
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
const result = await manager.startRun({
|
|
285
|
-
task: body.task,
|
|
286
|
-
pipeline: body.pipeline.trim(),
|
|
271
|
+
if (typeof body.task !== "string" && !isTaskFile(body.task)) {
|
|
272
|
+
json(res, 400, {
|
|
273
|
+
error: "task must be a path string or TaskFile object (id and goal required)",
|
|
287
274
|
});
|
|
288
|
-
if (!result.ok) {
|
|
289
|
-
json(res, result.status ?? 500, mapStartFailure(result));
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
json(res, 202, { runId: result.runId });
|
|
293
275
|
return true;
|
|
294
276
|
}
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (!result.ok) {
|
|
299
|
-
json(res, result.status ?? 500, mapStartFailure(result));
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
json(res, 202, { runId: result.runId });
|
|
277
|
+
if (body.checkoutOverride !== undefined &&
|
|
278
|
+
typeof body.checkoutOverride !== "string") {
|
|
279
|
+
json(res, 400, { error: "checkoutOverride must be a string" });
|
|
303
280
|
return true;
|
|
304
281
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const runId = decodeURIComponent(answerMatch[1] ?? "");
|
|
308
|
-
const stageId = decodeURIComponent(answerMatch[2] ?? "");
|
|
309
|
-
let answer;
|
|
310
|
-
try {
|
|
311
|
-
answer = parseAskOperatorAnswer(await readJsonBody(req));
|
|
312
|
-
}
|
|
313
|
-
catch (err) {
|
|
314
|
-
json(res, 400, {
|
|
315
|
-
error: err instanceof Error ? err.message : String(err),
|
|
316
|
-
});
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
const result = await manager.deliverAnswer(runId, stageId, answer);
|
|
320
|
-
if (!result.ok) {
|
|
321
|
-
json(res, result.status, { error: result.reason });
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
json(res, 202, { ok: true });
|
|
282
|
+
if (body.skipGates !== undefined && typeof body.skipGates !== "boolean") {
|
|
283
|
+
json(res, 400, { error: "skipGates must be a boolean" });
|
|
325
284
|
return true;
|
|
326
285
|
}
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
|
|
331
|
-
let body;
|
|
332
|
-
try {
|
|
333
|
-
body = await readJsonBody(req);
|
|
334
|
-
}
|
|
335
|
-
catch {
|
|
336
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
340
|
-
json(res, 400, { error: "Body must be a JSON object" });
|
|
286
|
+
for (const field of ["gitSha", "ciPrUrl", "ciJobUrl"]) {
|
|
287
|
+
if (body[field] !== undefined && typeof body[field] !== "string") {
|
|
288
|
+
json(res, 400, { error: `${field} must be a string` });
|
|
341
289
|
return true;
|
|
342
290
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
291
|
+
}
|
|
292
|
+
let result;
|
|
293
|
+
try {
|
|
294
|
+
result = await manager.startRun({
|
|
295
|
+
task: body.task,
|
|
296
|
+
pipeline: body.pipeline.trim(),
|
|
297
|
+
...(body.checkoutOverride !== undefined
|
|
298
|
+
? { checkoutOverride: body.checkoutOverride }
|
|
299
|
+
: {}),
|
|
300
|
+
...(body.skipGates !== undefined ? { skipGates: body.skipGates } : {}),
|
|
301
|
+
...(body.gitSha !== undefined ? { gitSha: body.gitSha } : {}),
|
|
302
|
+
...(body.ciPrUrl !== undefined ? { ciPrUrl: body.ciPrUrl } : {}),
|
|
303
|
+
...(body.ciJobUrl !== undefined ? { ciJobUrl: body.ciJobUrl } : {}),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (err instanceof PipelineValidationError) {
|
|
347
308
|
json(res, 400, {
|
|
348
|
-
error: "
|
|
309
|
+
error: "Pipeline validation failed",
|
|
310
|
+
validation: err.result,
|
|
349
311
|
});
|
|
350
312
|
return true;
|
|
351
313
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (raw.reason !== undefined && typeof raw.reason !== "string") {
|
|
357
|
-
json(res, 400, { error: "reason must be a string" });
|
|
358
|
-
return true;
|
|
359
|
-
}
|
|
360
|
-
const result = await manager.decideFeedbackLoop(runId, stageId, {
|
|
361
|
-
decision: raw.decision,
|
|
362
|
-
...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
|
|
363
|
-
...(raw.reason !== undefined ? { reason: raw.reason } : {}),
|
|
364
|
-
});
|
|
365
|
-
if (!result.ok) {
|
|
366
|
-
json(res, result.status ?? 500, { error: result.reason });
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
json(res, 202, {
|
|
370
|
-
ok: true,
|
|
371
|
-
effect: result.effect,
|
|
372
|
-
loopId: result.loopId,
|
|
373
|
-
});
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
if (!result.ok) {
|
|
317
|
+
json(res, result.status ?? 500, mapStartFailure(result));
|
|
374
318
|
return true;
|
|
375
319
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return true;
|
|
385
|
-
}
|
|
386
|
-
json(res, 202, {
|
|
387
|
-
runId: result.runId,
|
|
388
|
-
stageId: result.stageId,
|
|
389
|
-
attemptIndex: result.attemptIndex,
|
|
390
|
-
});
|
|
320
|
+
json(res, 202, { runId: result.runId });
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
|
|
324
|
+
const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
|
|
325
|
+
const result = await manager.rerun(runId);
|
|
326
|
+
if (!result.ok) {
|
|
327
|
+
json(res, result.status ?? 500, mapStartFailure(result));
|
|
391
328
|
return true;
|
|
392
329
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
json(res, 400, { error: "guidance must be a string" });
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
const runId = decodeURIComponent(recoverMatch[1] ?? "");
|
|
411
|
-
const stageId = decodeURIComponent(recoverMatch[2] ?? "");
|
|
412
|
-
const result = await manager.recoverManualStage(runId, stageId, guidance);
|
|
413
|
-
if (!result.ok) {
|
|
414
|
-
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
417
|
-
json(res, 202, {
|
|
418
|
-
runId: result.runId,
|
|
419
|
-
stageId: result.stageId,
|
|
420
|
-
attemptIndex: result.attemptIndex,
|
|
330
|
+
json(res, 202, { runId: result.runId });
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
|
|
334
|
+
if (method === "POST" && answerMatch) {
|
|
335
|
+
const runId = decodeURIComponent(answerMatch[1] ?? "");
|
|
336
|
+
const stageId = decodeURIComponent(answerMatch[2] ?? "");
|
|
337
|
+
let answer;
|
|
338
|
+
try {
|
|
339
|
+
answer = parseAskOperatorAnswer(await readJsonBody(req));
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
json(res, 400, {
|
|
343
|
+
error: err instanceof Error ? err.message : String(err),
|
|
421
344
|
});
|
|
422
345
|
return true;
|
|
423
346
|
}
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
|
|
428
|
-
const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
|
|
429
|
-
const result = await manager.stopManualRecovery(runId, stageId);
|
|
430
|
-
if (!result.ok) {
|
|
431
|
-
json(res, result.status ?? 500, { error: result.reason });
|
|
432
|
-
return true;
|
|
433
|
-
}
|
|
434
|
-
json(res, 202, result);
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
|
|
438
|
-
if (method === "POST" && abandonMatch) {
|
|
439
|
-
await readJsonBody(req);
|
|
440
|
-
const runId = decodeURIComponent(abandonMatch[1] ?? "");
|
|
441
|
-
const stageId = decodeURIComponent(abandonMatch[2] ?? "");
|
|
442
|
-
const result = await manager.abandonStage(runId, stageId);
|
|
443
|
-
if (!result.ok) {
|
|
444
|
-
json(res, result.status ?? 500, {
|
|
445
|
-
error: result.reason,
|
|
446
|
-
});
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
json(res, 202, {
|
|
450
|
-
ok: true,
|
|
451
|
-
runId: result.runId,
|
|
452
|
-
stageId: result.stageId,
|
|
453
|
-
});
|
|
347
|
+
const result = await manager.deliverAnswer(runId, stageId, answer);
|
|
348
|
+
if (!result.ok) {
|
|
349
|
+
json(res, result.status, { error: result.reason });
|
|
454
350
|
return true;
|
|
455
351
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
352
|
+
json(res, 202, { ok: true });
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
|
|
356
|
+
if (method === "POST" && feedbackDecisionMatch) {
|
|
357
|
+
const runId = decodeURIComponent(feedbackDecisionMatch[1] ?? "");
|
|
358
|
+
const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
|
|
359
|
+
let body;
|
|
360
|
+
try {
|
|
361
|
+
body = await readJsonBody(req);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
459
365
|
return true;
|
|
460
366
|
}
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
json(res, 200, { pipelines: catalog.pipelines });
|
|
367
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
368
|
+
json(res, 400, { error: "Body must be a JSON object" });
|
|
464
369
|
return true;
|
|
465
370
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
371
|
+
const raw = body;
|
|
372
|
+
if (raw.decision !== "extend" &&
|
|
373
|
+
raw.decision !== "continue" &&
|
|
374
|
+
raw.decision !== "abandon") {
|
|
375
|
+
json(res, 400, {
|
|
376
|
+
error: "decision must be extend, continue, or abandon",
|
|
469
377
|
});
|
|
470
378
|
return true;
|
|
471
379
|
}
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
body = await readJsonBody(req);
|
|
476
|
-
}
|
|
477
|
-
catch {
|
|
478
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
const parsed = parseCreateStageBody(body);
|
|
482
|
-
if ("ok" in parsed) {
|
|
483
|
-
json(res, parsed.status, { error: parsed.error });
|
|
484
|
-
return true;
|
|
485
|
-
}
|
|
486
|
-
const ctx = await resolveStageflowContext(cwd);
|
|
487
|
-
if (!ctx.isGitProject) {
|
|
488
|
-
json(res, 400, {
|
|
489
|
-
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
490
|
-
});
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
|
-
const result = await createStage(ctx.projectRoot, parsed);
|
|
494
|
-
if (!result.ok) {
|
|
495
|
-
json(res, result.status, { error: result.error });
|
|
496
|
-
return true;
|
|
497
|
-
}
|
|
498
|
-
json(res, 201, result.stage);
|
|
380
|
+
if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
|
|
381
|
+
json(res, 400, { error: "loopId must be a string" });
|
|
499
382
|
return true;
|
|
500
383
|
}
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
try {
|
|
504
|
-
body = await readJsonBody(req);
|
|
505
|
-
}
|
|
506
|
-
catch {
|
|
507
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
508
|
-
return true;
|
|
509
|
-
}
|
|
510
|
-
const parsed = parseCreatePipelineBody(body);
|
|
511
|
-
if ("ok" in parsed) {
|
|
512
|
-
json(res, parsed.status, { error: parsed.error });
|
|
513
|
-
return true;
|
|
514
|
-
}
|
|
515
|
-
const ctx = await resolveStageflowContext(cwd);
|
|
516
|
-
if (!ctx.isGitProject) {
|
|
517
|
-
json(res, 400, {
|
|
518
|
-
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
519
|
-
});
|
|
520
|
-
return true;
|
|
521
|
-
}
|
|
522
|
-
const result = await createPipeline(ctx.projectRoot, parsed);
|
|
523
|
-
if (!result.ok) {
|
|
524
|
-
json(res, result.status, { error: result.error });
|
|
525
|
-
return true;
|
|
526
|
-
}
|
|
527
|
-
json(res, 201, result.pipeline);
|
|
384
|
+
if (raw.reason !== undefined && typeof raw.reason !== "string") {
|
|
385
|
+
json(res, 400, { error: "reason must be a string" });
|
|
528
386
|
return true;
|
|
529
387
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
388
|
+
const result = await manager.decideFeedbackLoop(runId, stageId, {
|
|
389
|
+
decision: raw.decision,
|
|
390
|
+
...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
|
|
391
|
+
...(raw.reason !== undefined ? { reason: raw.reason } : {}),
|
|
392
|
+
});
|
|
393
|
+
if (!result.ok) {
|
|
394
|
+
json(res, result.status ?? 500, { error: result.reason });
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
json(res, 202, {
|
|
398
|
+
ok: true,
|
|
399
|
+
effect: result.effect,
|
|
400
|
+
loopId: result.loopId,
|
|
401
|
+
});
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
|
|
405
|
+
if (method === "POST" && retryMatch) {
|
|
406
|
+
await readJsonBody(req);
|
|
407
|
+
const runId = decodeURIComponent(retryMatch[1] ?? "");
|
|
408
|
+
const stageId = decodeURIComponent(retryMatch[2] ?? "");
|
|
409
|
+
const result = await manager.retryStage(runId, stageId);
|
|
410
|
+
if (!result.ok) {
|
|
411
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
533
412
|
return true;
|
|
534
413
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
414
|
+
json(res, 202, {
|
|
415
|
+
runId: result.runId,
|
|
416
|
+
stageId: result.stageId,
|
|
417
|
+
attemptIndex: result.attemptIndex,
|
|
418
|
+
});
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
const resumeMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/resume$/);
|
|
422
|
+
if (method === "POST" && resumeMatch) {
|
|
423
|
+
await readJsonBody(req);
|
|
424
|
+
const runId = decodeURIComponent(resumeMatch[1] ?? "");
|
|
425
|
+
const stageId = decodeURIComponent(resumeMatch[2] ?? "");
|
|
426
|
+
const result = await manager.resumeTimedOutStage(runId, stageId);
|
|
427
|
+
if (!result.ok) {
|
|
428
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
541
429
|
return true;
|
|
542
430
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
431
|
+
json(res, 202, {
|
|
432
|
+
runId: result.runId,
|
|
433
|
+
stageId: result.stageId,
|
|
434
|
+
attemptIndex: result.attemptIndex,
|
|
435
|
+
});
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
const recoverMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery$/);
|
|
439
|
+
if (method === "POST" && recoverMatch) {
|
|
440
|
+
let body;
|
|
441
|
+
try {
|
|
442
|
+
body = await readJsonBody(req);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
547
446
|
return true;
|
|
548
447
|
}
|
|
549
|
-
|
|
550
|
-
|
|
448
|
+
const guidance = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
449
|
+
? body.guidance
|
|
450
|
+
: undefined;
|
|
451
|
+
if (guidance !== undefined && typeof guidance !== "string") {
|
|
452
|
+
json(res, 400, { error: "guidance must be a string" });
|
|
551
453
|
return true;
|
|
552
454
|
}
|
|
553
|
-
|
|
554
|
-
|
|
455
|
+
const runId = decodeURIComponent(recoverMatch[1] ?? "");
|
|
456
|
+
const stageId = decodeURIComponent(recoverMatch[2] ?? "");
|
|
457
|
+
const result = await manager.recoverManualStage(runId, stageId, guidance);
|
|
458
|
+
if (!result.ok) {
|
|
459
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
555
460
|
return true;
|
|
556
461
|
}
|
|
557
|
-
|
|
558
|
-
|
|
462
|
+
json(res, 202, {
|
|
463
|
+
runId: result.runId,
|
|
464
|
+
stageId: result.stageId,
|
|
465
|
+
attemptIndex: result.attemptIndex,
|
|
466
|
+
});
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
const stopRecoveryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery\/stop$/);
|
|
470
|
+
if (method === "POST" && stopRecoveryMatch) {
|
|
471
|
+
await readJsonBody(req);
|
|
472
|
+
const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
|
|
473
|
+
const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
|
|
474
|
+
const result = await manager.stopManualRecovery(runId, stageId);
|
|
475
|
+
if (!result.ok) {
|
|
476
|
+
json(res, result.status ?? 500, { error: result.reason });
|
|
559
477
|
return true;
|
|
560
478
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
479
|
+
json(res, 202, result);
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
|
|
483
|
+
if (method === "POST" && abandonMatch) {
|
|
484
|
+
await readJsonBody(req);
|
|
485
|
+
const runId = decodeURIComponent(abandonMatch[1] ?? "");
|
|
486
|
+
const stageId = decodeURIComponent(abandonMatch[2] ?? "");
|
|
487
|
+
const result = await manager.abandonStage(runId, stageId);
|
|
488
|
+
if (!result.ok) {
|
|
489
|
+
json(res, result.status ?? 500, {
|
|
490
|
+
error: result.reason,
|
|
567
491
|
});
|
|
568
492
|
return true;
|
|
569
493
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
...health,
|
|
612
|
-
...credential,
|
|
494
|
+
json(res, 202, {
|
|
495
|
+
ok: true,
|
|
496
|
+
runId: result.runId,
|
|
497
|
+
stageId: result.stageId,
|
|
498
|
+
});
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
if (method === "GET" && pathname === "/api/tasks") {
|
|
502
|
+
const catalog = await browseCatalog(cwd);
|
|
503
|
+
json(res, 200, { tasks: catalog.tasks });
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
if (method === "GET" && pathname === "/api/pipelines") {
|
|
507
|
+
const catalog = await browseCatalog(cwd);
|
|
508
|
+
json(res, 200, { pipelines: catalog.pipelines });
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
if (method === "GET" && pathname === "/api/stages") {
|
|
512
|
+
json(res, 404, {
|
|
513
|
+
error: "Global stage library removed; stages are pipeline-scoped",
|
|
514
|
+
});
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
if (method === "POST" && pathname === "/api/stages") {
|
|
518
|
+
let body;
|
|
519
|
+
try {
|
|
520
|
+
body = await readJsonBody(req);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
const parsed = parseCreateStageBody(body);
|
|
527
|
+
if ("ok" in parsed) {
|
|
528
|
+
json(res, parsed.status, { error: parsed.error });
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
532
|
+
if (!ctx.isGitProject) {
|
|
533
|
+
json(res, 400, {
|
|
534
|
+
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
613
535
|
});
|
|
614
536
|
return true;
|
|
615
537
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
538
|
+
const result = await createStage(ctx.projectRoot, parsed);
|
|
539
|
+
if (!result.ok) {
|
|
540
|
+
json(res, result.status, { error: result.error });
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
json(res, 201, result.stage);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
if (method === "POST" && pathname === "/api/pipelines") {
|
|
547
|
+
let body;
|
|
548
|
+
try {
|
|
549
|
+
body = await readJsonBody(req);
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
const parsed = parseCreatePipelineBody(body);
|
|
556
|
+
if ("ok" in parsed) {
|
|
557
|
+
json(res, parsed.status, { error: parsed.error });
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
561
|
+
if (!ctx.isGitProject) {
|
|
562
|
+
json(res, 400, {
|
|
563
|
+
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
622
564
|
});
|
|
623
565
|
return true;
|
|
624
566
|
}
|
|
625
|
-
|
|
567
|
+
const result = await createPipeline(ctx.projectRoot, parsed);
|
|
568
|
+
if (!result.ok) {
|
|
569
|
+
json(res, result.status, { error: result.error });
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
json(res, 201, result.pipeline);
|
|
626
573
|
return true;
|
|
627
574
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
575
|
+
if (method === "GET" && pathname === "/api/models") {
|
|
576
|
+
const catalog = await browseCatalog(cwd);
|
|
577
|
+
json(res, 200, { models: catalog.models });
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
if (await handleProviderRoutes(req, res, {
|
|
581
|
+
cwd,
|
|
582
|
+
readJsonBody,
|
|
583
|
+
json,
|
|
584
|
+
providerAuthContext,
|
|
585
|
+
})) {
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
if (await handleProjectMcpRoutes(req, res, {
|
|
589
|
+
projectRoot: rootDir,
|
|
590
|
+
json,
|
|
591
|
+
})) {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
if (method === "GET" && pathname === "/api/skills") {
|
|
595
|
+
json(res, 200, await listSkills({ cwd, agentDir }));
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
if (method === "GET" && pathname === "/api/extensions") {
|
|
599
|
+
json(res, 200, await listExtensions({ cwd, agentDir }));
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
603
|
+
json(res, 200, manager.getHealth());
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
if (method === "GET" && pathname === "/api/settings") {
|
|
607
|
+
const health = manager.getHealth();
|
|
608
|
+
const credential = getCredentialSourceSettings(cwd);
|
|
609
|
+
json(res, 200, {
|
|
610
|
+
maxConcurrent: health.maxConcurrent,
|
|
611
|
+
...credential,
|
|
612
|
+
});
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
if (method === "POST" && pathname === "/api/settings") {
|
|
616
|
+
let body;
|
|
617
|
+
try {
|
|
618
|
+
body = await readJsonBody(req);
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
const record = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
625
|
+
? body
|
|
626
|
+
: {};
|
|
627
|
+
const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
|
|
628
|
+
const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
|
|
629
|
+
if (!hasMax && !hasCredentialSource) {
|
|
630
|
+
json(res, 400, {
|
|
631
|
+
error: "maxConcurrent or credentialSource is required",
|
|
632
632
|
});
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
let health = manager.getHealth();
|
|
636
|
+
if (hasMax) {
|
|
637
|
+
const n = parseSlotCount(record.maxConcurrent);
|
|
638
|
+
if (n === undefined) {
|
|
639
|
+
json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
health = manager.setMaxConcurrent(n);
|
|
643
|
+
}
|
|
644
|
+
let credential = getCredentialSourceSettings(cwd);
|
|
645
|
+
if (hasCredentialSource) {
|
|
646
|
+
try {
|
|
647
|
+
credential = setCredentialSource(cwd, record.credentialSource);
|
|
648
|
+
}
|
|
649
|
+
catch (err) {
|
|
650
|
+
const mapped = mapProviderAuthError(err);
|
|
651
|
+
json(res, mapped.status, mapped.body);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
633
654
|
}
|
|
655
|
+
json(res, 200, {
|
|
656
|
+
...health,
|
|
657
|
+
...credential,
|
|
658
|
+
});
|
|
634
659
|
return true;
|
|
635
660
|
}
|
|
636
|
-
|
|
661
|
+
if (method === "GET" && uiDistDir !== undefined) {
|
|
662
|
+
const served = await serveStatic(res, uiDistDir, pathname);
|
|
663
|
+
if (served)
|
|
664
|
+
return true;
|
|
665
|
+
json(res, 404, {
|
|
666
|
+
error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
|
|
667
|
+
});
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
json(res, 404, { error: "Not found" });
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
if (!res.headersSent) {
|
|
675
|
+
json(res, 500, {
|
|
676
|
+
error: err instanceof Error ? err.message : String(err),
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
export async function startUiServer(options) {
|
|
684
|
+
const host = options.host ?? "127.0.0.1";
|
|
685
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
686
|
+
const uiDistDir = options.uiDistDir ?? defaultUiDistDir();
|
|
687
|
+
const boot = await bootstrapStageflowHost(options);
|
|
688
|
+
const { manager, store, cwd, agentDir, rootDir } = boot;
|
|
689
|
+
const providerAuthContext = boot.providerAuthContext;
|
|
690
|
+
return createHttpHost({
|
|
691
|
+
boot,
|
|
692
|
+
host,
|
|
693
|
+
port,
|
|
694
|
+
routes: createOperatorRoutes({
|
|
695
|
+
manager,
|
|
696
|
+
store,
|
|
697
|
+
cwd,
|
|
698
|
+
agentDir,
|
|
699
|
+
rootDir,
|
|
700
|
+
providerAuthContext,
|
|
701
|
+
uiDistDir,
|
|
702
|
+
}),
|
|
637
703
|
});
|
|
638
704
|
}
|
|
639
705
|
export { DEFAULT_PORT };
|