stageflow 0.19.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 +5 -0
- package/dist/agent/activityObserver.d.ts +3 -0
- package/dist/agent/activityObserver.js +6 -1
- package/dist/agent/piAdapter.js +7 -3
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/providerInspect.d.ts +19 -0
- package/dist/agent/providerInspect.js +35 -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/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 +3 -7
- package/dist/cli/runsCommand.js +33 -94
- package/dist/cli.js +1 -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 +77 -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/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/pipelineRunner.d.ts +3 -3
- package/dist/runtime/pipelineRunner.js +3 -1
- package/dist/runtime/resumeReconstruct.js +2 -2
- package/dist/runtime/runManager.d.ts +2 -1
- package/dist/runtime/runManager.js +36 -16
- 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/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 +450 -402
- 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/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DMVdl1qB.js → index-Da5EHU6M.js} +5 -5
- package/dist/ui/index.html +1 -1
- 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/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",
|
|
@@ -178,480 +180,526 @@ export function defaultUiDistDir() {
|
|
|
178
180
|
}
|
|
179
181
|
return path.resolve(here, "../ui");
|
|
180
182
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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;
|
|
201
206
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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" });
|
|
205
213
|
return true;
|
|
206
214
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
|
214
224
|
}
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
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);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
const content = await readRunArtifact(store, runId, artifactPath);
|
|
227
|
-
textPlain(res, 200, content);
|
|
228
|
-
}
|
|
225
|
+
else {
|
|
226
|
+
const content = await readRunArtifact(store, runId, artifactPath);
|
|
227
|
+
textPlain(res, 200, content);
|
|
229
228
|
}
|
|
230
|
-
catch (err) {
|
|
231
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
232
|
-
if (message === "Artifact path denied") {
|
|
233
|
-
json(res, 403, { error: message });
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
const notFound = message.startsWith("Run not found") ||
|
|
237
|
-
message.startsWith("Artifact not found") ||
|
|
238
|
-
/no such|not found/i.test(message);
|
|
239
|
-
json(res, notFound ? 404 : 400, { error: message });
|
|
240
|
-
}
|
|
241
|
-
return true;
|
|
242
229
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
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("/")) {
|
|
247
253
|
try {
|
|
248
|
-
json(res, 200, await
|
|
254
|
+
json(res, 200, await store.readRun(decodeURIComponent(rest)));
|
|
249
255
|
}
|
|
250
256
|
catch (err) {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
json(res, notFound ? 404 : 500, { error: message });
|
|
257
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
258
|
+
json(res, 404, { error: mapped.error });
|
|
254
259
|
}
|
|
255
260
|
return true;
|
|
256
261
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
error: err instanceof Error ? err.message : String(err),
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
return true;
|
|
269
|
-
}
|
|
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;
|
|
270
270
|
}
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
!body.pipeline.trim() ||
|
|
275
|
-
body.task === undefined) {
|
|
276
|
-
json(res, 400, { error: "task and pipeline are required" });
|
|
277
|
-
return true;
|
|
278
|
-
}
|
|
279
|
-
if (typeof body.task !== "string" && !isTaskFile(body.task)) {
|
|
280
|
-
json(res, 400, {
|
|
281
|
-
error: "task must be a path string or TaskFile object (id and goal required)",
|
|
282
|
-
});
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
const result = await manager.startRun({
|
|
286
|
-
task: body.task,
|
|
287
|
-
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)",
|
|
288
274
|
});
|
|
289
|
-
if (!result.ok) {
|
|
290
|
-
json(res, result.status ?? 500, mapStartFailure(result));
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
json(res, 202, { runId: result.runId });
|
|
294
275
|
return true;
|
|
295
276
|
}
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (!result.ok) {
|
|
300
|
-
json(res, result.status ?? 500, mapStartFailure(result));
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
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" });
|
|
304
280
|
return true;
|
|
305
281
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const runId = decodeURIComponent(answerMatch[1] ?? "");
|
|
309
|
-
const stageId = decodeURIComponent(answerMatch[2] ?? "");
|
|
310
|
-
let answer;
|
|
311
|
-
try {
|
|
312
|
-
answer = parseAskOperatorAnswer(await readJsonBody(req));
|
|
313
|
-
}
|
|
314
|
-
catch (err) {
|
|
315
|
-
json(res, 400, {
|
|
316
|
-
error: err instanceof Error ? err.message : String(err),
|
|
317
|
-
});
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
const result = await manager.deliverAnswer(runId, stageId, answer);
|
|
321
|
-
if (!result.ok) {
|
|
322
|
-
json(res, result.status, { error: result.reason });
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
json(res, 202, { ok: true });
|
|
282
|
+
if (body.skipGates !== undefined && typeof body.skipGates !== "boolean") {
|
|
283
|
+
json(res, 400, { error: "skipGates must be a boolean" });
|
|
326
284
|
return true;
|
|
327
285
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
|
|
332
|
-
let body;
|
|
333
|
-
try {
|
|
334
|
-
body = await readJsonBody(req);
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
338
|
-
return true;
|
|
339
|
-
}
|
|
340
|
-
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
341
|
-
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` });
|
|
342
289
|
return true;
|
|
343
290
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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) {
|
|
348
308
|
json(res, 400, {
|
|
349
|
-
error: "
|
|
309
|
+
error: "Pipeline validation failed",
|
|
310
|
+
validation: err.result,
|
|
350
311
|
});
|
|
351
312
|
return true;
|
|
352
313
|
}
|
|
353
|
-
|
|
354
|
-
json(res, 400, { error: "loopId must be a string" });
|
|
355
|
-
return true;
|
|
356
|
-
}
|
|
357
|
-
if (raw.reason !== undefined && typeof raw.reason !== "string") {
|
|
358
|
-
json(res, 400, { error: "reason must be a string" });
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
const result = await manager.decideFeedbackLoop(runId, stageId, {
|
|
362
|
-
decision: raw.decision,
|
|
363
|
-
...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
|
|
364
|
-
...(raw.reason !== undefined ? { reason: raw.reason } : {}),
|
|
365
|
-
});
|
|
366
|
-
if (!result.ok) {
|
|
367
|
-
json(res, result.status ?? 500, { error: result.reason });
|
|
368
|
-
return true;
|
|
369
|
-
}
|
|
370
|
-
json(res, 202, {
|
|
371
|
-
ok: true,
|
|
372
|
-
effect: result.effect,
|
|
373
|
-
loopId: result.loopId,
|
|
374
|
-
});
|
|
375
|
-
return true;
|
|
314
|
+
throw err;
|
|
376
315
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
await readJsonBody(req);
|
|
380
|
-
const runId = decodeURIComponent(retryMatch[1] ?? "");
|
|
381
|
-
const stageId = decodeURIComponent(retryMatch[2] ?? "");
|
|
382
|
-
const result = await manager.retryStage(runId, stageId);
|
|
383
|
-
if (!result.ok) {
|
|
384
|
-
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
json(res, 202, {
|
|
388
|
-
runId: result.runId,
|
|
389
|
-
stageId: result.stageId,
|
|
390
|
-
attemptIndex: result.attemptIndex,
|
|
391
|
-
});
|
|
316
|
+
if (!result.ok) {
|
|
317
|
+
json(res, result.status ?? 500, mapStartFailure(result));
|
|
392
318
|
return true;
|
|
393
319
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
json(res, 202, {
|
|
405
|
-
runId: result.runId,
|
|
406
|
-
stageId: result.stageId,
|
|
407
|
-
attemptIndex: result.attemptIndex,
|
|
408
|
-
});
|
|
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));
|
|
409
328
|
return true;
|
|
410
329
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
json(res, 400, { error: "guidance must be a string" });
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
const runId = decodeURIComponent(recoverMatch[1] ?? "");
|
|
429
|
-
const stageId = decodeURIComponent(recoverMatch[2] ?? "");
|
|
430
|
-
const result = await manager.recoverManualStage(runId, stageId, guidance);
|
|
431
|
-
if (!result.ok) {
|
|
432
|
-
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
json(res, 202, {
|
|
436
|
-
runId: result.runId,
|
|
437
|
-
stageId: result.stageId,
|
|
438
|
-
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),
|
|
439
344
|
});
|
|
440
345
|
return true;
|
|
441
346
|
}
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
|
|
446
|
-
const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
|
|
447
|
-
const result = await manager.stopManualRecovery(runId, stageId);
|
|
448
|
-
if (!result.ok) {
|
|
449
|
-
json(res, result.status ?? 500, { error: result.reason });
|
|
450
|
-
return true;
|
|
451
|
-
}
|
|
452
|
-
json(res, 202, result);
|
|
453
|
-
return true;
|
|
454
|
-
}
|
|
455
|
-
const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
|
|
456
|
-
if (method === "POST" && abandonMatch) {
|
|
457
|
-
await readJsonBody(req);
|
|
458
|
-
const runId = decodeURIComponent(abandonMatch[1] ?? "");
|
|
459
|
-
const stageId = decodeURIComponent(abandonMatch[2] ?? "");
|
|
460
|
-
const result = await manager.abandonStage(runId, stageId);
|
|
461
|
-
if (!result.ok) {
|
|
462
|
-
json(res, result.status ?? 500, {
|
|
463
|
-
error: result.reason,
|
|
464
|
-
});
|
|
465
|
-
return true;
|
|
466
|
-
}
|
|
467
|
-
json(res, 202, {
|
|
468
|
-
ok: true,
|
|
469
|
-
runId: result.runId,
|
|
470
|
-
stageId: result.stageId,
|
|
471
|
-
});
|
|
347
|
+
const result = await manager.deliverAnswer(runId, stageId, answer);
|
|
348
|
+
if (!result.ok) {
|
|
349
|
+
json(res, result.status, { error: result.reason });
|
|
472
350
|
return true;
|
|
473
351
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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" });
|
|
477
365
|
return true;
|
|
478
366
|
}
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
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" });
|
|
482
369
|
return true;
|
|
483
370
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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",
|
|
487
377
|
});
|
|
488
378
|
return true;
|
|
489
379
|
}
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
try {
|
|
493
|
-
body = await readJsonBody(req);
|
|
494
|
-
}
|
|
495
|
-
catch {
|
|
496
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
497
|
-
return true;
|
|
498
|
-
}
|
|
499
|
-
const parsed = parseCreateStageBody(body);
|
|
500
|
-
if ("ok" in parsed) {
|
|
501
|
-
json(res, parsed.status, { error: parsed.error });
|
|
502
|
-
return true;
|
|
503
|
-
}
|
|
504
|
-
const ctx = await resolveStageflowContext(cwd);
|
|
505
|
-
if (!ctx.isGitProject) {
|
|
506
|
-
json(res, 400, {
|
|
507
|
-
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
508
|
-
});
|
|
509
|
-
return true;
|
|
510
|
-
}
|
|
511
|
-
const result = await createStage(ctx.projectRoot, parsed);
|
|
512
|
-
if (!result.ok) {
|
|
513
|
-
json(res, result.status, { error: result.error });
|
|
514
|
-
return true;
|
|
515
|
-
}
|
|
516
|
-
json(res, 201, result.stage);
|
|
380
|
+
if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
|
|
381
|
+
json(res, 400, { error: "loopId must be a string" });
|
|
517
382
|
return true;
|
|
518
383
|
}
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
body = await readJsonBody(req);
|
|
523
|
-
}
|
|
524
|
-
catch {
|
|
525
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
526
|
-
return true;
|
|
527
|
-
}
|
|
528
|
-
const parsed = parseCreatePipelineBody(body);
|
|
529
|
-
if ("ok" in parsed) {
|
|
530
|
-
json(res, parsed.status, { error: parsed.error });
|
|
531
|
-
return true;
|
|
532
|
-
}
|
|
533
|
-
const ctx = await resolveStageflowContext(cwd);
|
|
534
|
-
if (!ctx.isGitProject) {
|
|
535
|
-
json(res, 400, {
|
|
536
|
-
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
537
|
-
});
|
|
538
|
-
return true;
|
|
539
|
-
}
|
|
540
|
-
const result = await createPipeline(ctx.projectRoot, parsed);
|
|
541
|
-
if (!result.ok) {
|
|
542
|
-
json(res, result.status, { error: result.error });
|
|
543
|
-
return true;
|
|
544
|
-
}
|
|
545
|
-
json(res, 201, result.pipeline);
|
|
384
|
+
if (raw.reason !== undefined && typeof raw.reason !== "string") {
|
|
385
|
+
json(res, 400, { error: "reason must be a string" });
|
|
546
386
|
return true;
|
|
547
387
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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));
|
|
551
412
|
return true;
|
|
552
413
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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));
|
|
559
429
|
return true;
|
|
560
430
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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" });
|
|
565
446
|
return true;
|
|
566
447
|
}
|
|
567
|
-
|
|
568
|
-
|
|
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" });
|
|
569
453
|
return true;
|
|
570
454
|
}
|
|
571
|
-
|
|
572
|
-
|
|
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));
|
|
573
460
|
return true;
|
|
574
461
|
}
|
|
575
|
-
|
|
576
|
-
|
|
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 });
|
|
577
477
|
return true;
|
|
578
478
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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,
|
|
585
491
|
});
|
|
586
492
|
return true;
|
|
587
493
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
...health,
|
|
630
|
-
...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",
|
|
631
535
|
});
|
|
632
536
|
return true;
|
|
633
537
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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",
|
|
640
564
|
});
|
|
641
565
|
return true;
|
|
642
566
|
}
|
|
643
|
-
|
|
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);
|
|
644
573
|
return true;
|
|
645
574
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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",
|
|
650
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
|
+
}
|
|
651
654
|
}
|
|
655
|
+
json(res, 200, {
|
|
656
|
+
...health,
|
|
657
|
+
...credential,
|
|
658
|
+
});
|
|
652
659
|
return true;
|
|
653
660
|
}
|
|
654
|
-
|
|
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
|
+
}),
|
|
655
703
|
});
|
|
656
704
|
}
|
|
657
705
|
export { DEFAULT_PORT };
|