stageflow 0.27.0 → 0.30.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 +4 -0
- package/builtin-skills/browser/SKILL.md +42 -0
- package/dist/agent/claudeAdapter.js +2 -2
- package/dist/agent/cursorProvider.d.ts +2 -0
- package/dist/agent/cursorProvider.js +30 -5
- package/dist/agent/fakeAgent.d.ts +1 -0
- package/dist/agent/fakeAgent.js +13 -0
- package/dist/agent/piAdapter.d.ts +3 -24
- package/dist/agent/piAdapter.js +49 -47
- package/dist/agent/piSessionFactory.d.ts +38 -0
- package/dist/agent/piSessionFactory.js +60 -0
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/streamLogRedact.d.ts +6 -0
- package/dist/agent/streamLogRedact.js +23 -1
- package/dist/browser/anchor.d.ts +27 -0
- package/dist/browser/anchor.js +127 -0
- package/dist/browser/auditSink.d.ts +38 -0
- package/dist/browser/auditSink.js +34 -0
- package/dist/browser/browserHost.d.ts +86 -0
- package/dist/browser/browserHost.js +7 -0
- package/dist/browser/browserSweep.d.ts +14 -0
- package/dist/browser/browserSweep.js +54 -0
- package/dist/browser/browserTeardown.d.ts +56 -0
- package/dist/browser/browserTeardown.js +235 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
- package/dist/browser/fakeRemoteBrowserHost.js +34 -0
- package/dist/browser/gateHandoff.d.ts +25 -0
- package/dist/browser/gateHandoff.js +83 -0
- package/dist/browser/humanLogin.d.ts +18 -0
- package/dist/browser/humanLogin.js +34 -0
- package/dist/browser/keyProvider.d.ts +10 -0
- package/dist/browser/keyProvider.js +39 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +152 -0
- package/dist/browser/localProfileLock.d.ts +2 -0
- package/dist/browser/localProfileLock.js +218 -0
- package/dist/browser/localProfileStore.d.ts +5 -0
- package/dist/browser/localProfileStore.js +91 -0
- package/dist/browser/loginCheck.d.ts +53 -0
- package/dist/browser/loginCheck.js +147 -0
- package/dist/browser/memoryProfileLock.d.ts +2 -0
- package/dist/browser/memoryProfileLock.js +53 -0
- package/dist/browser/memoryProfileStore.d.ts +2 -0
- package/dist/browser/memoryProfileStore.js +31 -0
- package/dist/browser/navigationAudit.d.ts +16 -0
- package/dist/browser/navigationAudit.js +53 -0
- package/dist/browser/persistedEnv.d.ts +3 -0
- package/dist/browser/persistedEnv.js +15 -0
- package/dist/browser/profileLock.d.ts +40 -0
- package/dist/browser/profileLock.js +10 -0
- package/dist/browser/profileStore.d.ts +23 -0
- package/dist/browser/profileStore.js +39 -0
- package/dist/browser/runLiveness.d.ts +13 -0
- package/dist/browser/runLiveness.js +39 -0
- package/dist/browser/sitePolicy.d.ts +10 -0
- package/dist/browser/sitePolicy.js +61 -0
- package/dist/browser/stageBrowserEnv.d.ts +22 -0
- package/dist/browser/stageBrowserEnv.js +147 -0
- package/dist/browser/stageProfileLock.d.ts +17 -0
- package/dist/browser/stageProfileLock.js +39 -0
- package/dist/cli/browserCommand.d.ts +44 -0
- package/dist/cli/browserCommand.js +472 -0
- package/dist/cli/hostClient.d.ts +20 -0
- package/dist/cli/hostClient.js +36 -2
- package/dist/cli/initCommand.d.ts +1 -0
- package/dist/cli/initCommand.js +23 -7
- package/dist/cli/initTemplates.d.ts +5 -2
- package/dist/cli/initTemplates.js +25 -2
- package/dist/cli/runsCommand.js +6 -2
- package/dist/cli/triggerCommand.d.ts +12 -0
- package/dist/cli/triggerCommand.js +183 -0
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +23 -6
- package/dist/cli.js +31 -2
- package/dist/config/browseCatalog.d.ts +1 -0
- package/dist/config/browseCatalog.js +5 -4
- package/dist/config/builtinSkills.d.ts +4 -0
- package/dist/config/builtinSkills.js +23 -0
- package/dist/config/catalogRelativePath.d.ts +1 -0
- package/dist/config/catalogRelativePath.js +10 -2
- package/dist/config/createTrigger.d.ts +27 -0
- package/dist/config/createTrigger.js +232 -0
- package/dist/config/draftPackage.d.ts +117 -0
- package/dist/config/draftPackage.js +550 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
- package/dist/config/ensureCatalogScanRoot.js +73 -0
- package/dist/config/hostConfig.d.ts +2 -0
- package/dist/config/hostConfig.js +27 -0
- package/dist/config/listSkills.d.ts +2 -2
- package/dist/config/listSkills.js +17 -3
- package/dist/config/loadOutcome.d.ts +6 -0
- package/dist/config/loadPipeline.js +16 -0
- package/dist/config/loadStage.js +7 -0
- package/dist/config/loadStageflowManifest.js +25 -0
- package/dist/config/loadTrigger.d.ts +9 -0
- package/dist/config/loadTrigger.js +120 -0
- package/dist/config/multiProjectCatalog.d.ts +2 -0
- package/dist/config/multiProjectCatalog.js +26 -18
- package/dist/config/normalizePipelineStageEntry.js +8 -1
- package/dist/config/parseCompletionContract.js +6 -0
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/config/publishDraftPackage.d.ts +7 -0
- package/dist/config/publishDraftPackage.js +25 -0
- package/dist/config/resolveCatalogContext.js +1 -1
- package/dist/config/resolvePipelineDag.js +1 -1
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +6 -2
- package/dist/config/stageBrowser.d.ts +12 -0
- package/dist/config/stageBrowser.js +118 -0
- package/dist/config/triggerCatalog.d.ts +10 -0
- package/dist/config/triggerCatalog.js +37 -0
- package/dist/config/validateCatalog.d.ts +13 -3
- package/dist/config/validateCatalog.js +83 -2
- package/dist/config/validateCompletionContract.js +8 -0
- package/dist/envelope/preEmitChecks.d.ts +1 -1
- package/dist/envelope/preEmitChecks.js +8 -0
- package/dist/logging/redact.d.ts +4 -0
- package/dist/logging/redact.js +17 -0
- package/dist/mcp/catalogTools.js +43 -8
- package/dist/mcp/controlTools.js +3 -1
- package/dist/mcp/deps.d.ts +2 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +2 -0
- package/dist/mcp/triggerTools.d.ts +3 -0
- package/dist/mcp/triggerTools.js +176 -0
- package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
- package/dist/operatorAgent/docsRetrieval.js +269 -0
- package/dist/operatorAgent/draftContext.d.ts +16 -0
- package/dist/operatorAgent/draftContext.js +68 -0
- package/dist/operatorAgent/fakeHost.d.ts +39 -0
- package/dist/operatorAgent/fakeHost.js +92 -0
- package/dist/operatorAgent/host.d.ts +40 -0
- package/dist/operatorAgent/host.js +179 -0
- package/dist/operatorAgent/index.d.ts +9 -0
- package/dist/operatorAgent/index.js +8 -0
- package/dist/operatorAgent/piModel.d.ts +80 -0
- package/dist/operatorAgent/piModel.js +528 -0
- package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
- package/dist/operatorAgent/proposals.d.ts +32 -0
- package/dist/operatorAgent/proposals.js +144 -0
- package/dist/operatorAgent/types.d.ts +128 -0
- package/dist/operatorAgent/types.js +1 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/project/findManifestRoot.d.ts +3 -0
- package/dist/project/findManifestRoot.js +35 -0
- package/dist/project/resolveStageflowContext.d.ts +7 -0
- package/dist/project/resolveStageflowContext.js +44 -28
- package/dist/runstore/assertTmpdir.js +6 -2
- package/dist/runstore/port.d.ts +28 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
- package/dist/runstore/sqlite/migrations/index.js +6 -0
- package/dist/runtime/answerResume.d.ts +1 -0
- package/dist/runtime/answerResume.js +3 -1
- package/dist/runtime/completionCheckRunner.d.ts +12 -1
- package/dist/runtime/completionCheckRunner.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +2 -0
- package/dist/runtime/credentialBinding.js +3 -0
- package/dist/runtime/emailSource.d.ts +72 -0
- package/dist/runtime/emailSource.js +304 -0
- package/dist/runtime/githubPollSource.d.ts +86 -0
- package/dist/runtime/githubPollSource.js +284 -0
- package/dist/runtime/pipelineRunner.d.ts +4 -0
- package/dist/runtime/pipelineRunner.js +3 -0
- package/dist/runtime/pipelineScheduler.d.ts +2 -0
- package/dist/runtime/pipelineScheduler.js +85 -1
- package/dist/runtime/repositoryMaterialize.d.ts +1 -1
- package/dist/runtime/runManager.d.ts +11 -0
- package/dist/runtime/runManager.js +149 -1
- package/dist/runtime/runRetryCoordinator.d.ts +2 -0
- package/dist/runtime/runRetryCoordinator.js +1 -0
- package/dist/runtime/scheduleSource.d.ts +38 -0
- package/dist/runtime/scheduleSource.js +119 -0
- package/dist/runtime/settingsFile.d.ts +5 -0
- package/dist/runtime/settingsFile.js +61 -54
- package/dist/runtime/stageAttemptBootstrap.js +55 -3
- package/dist/runtime/stageEnvironment.js +1 -0
- package/dist/runtime/stageHitl.d.ts +1 -0
- package/dist/runtime/stageHitl.js +5 -1
- package/dist/runtime/stageProcessLauncher.d.ts +2 -0
- package/dist/runtime/stageProcessLauncher.js +44 -5
- package/dist/runtime/stageRunner.d.ts +5 -0
- package/dist/runtime/stageRunner.js +30 -6
- package/dist/runtime/stageStreamLog.d.ts +1 -0
- package/dist/runtime/stageStreamLog.js +2 -1
- package/dist/runtime/triggerCatalog.d.ts +6 -0
- package/dist/runtime/triggerCatalog.js +24 -0
- package/dist/runtime/triggerEventMatch.d.ts +2 -0
- package/dist/runtime/triggerEventMatch.js +6 -0
- package/dist/runtime/triggerPort.d.ts +14 -0
- package/dist/runtime/triggerPort.js +1 -0
- package/dist/runtime/triggerRunner.d.ts +17 -0
- package/dist/runtime/triggerRunner.js +89 -0
- package/dist/runtime/verifiedStageExecution.d.ts +7 -0
- package/dist/runtime/verifiedStageExecution.js +14 -0
- package/dist/runtime/webhookSignature.d.ts +1 -0
- package/dist/runtime/webhookSignature.js +12 -0
- package/dist/server/bootstrap.d.ts +6 -0
- package/dist/server/bootstrap.js +64 -0
- package/dist/server/controlToken.js +13 -0
- package/dist/server/createHttpHost.js +3 -0
- package/dist/server/healthSurfaces.js +31 -6
- package/dist/server/http.d.ts +15 -0
- package/dist/server/http.js +1251 -74
- package/dist/server/requestAuthContext.d.ts +1 -1
- package/dist/server/triggerRoutes.d.ts +15 -0
- package/dist/server/triggerRoutes.js +268 -0
- package/dist/tools/askOperator.d.ts +3 -2
- package/dist/types/completion.d.ts +3 -0
- package/dist/types/pipeline.d.ts +3 -1
- package/dist/types/preEmitCheck.d.ts +7 -0
- package/dist/types/stage.d.ts +14 -0
- package/dist/types/stageflowManifest.d.ts +3 -0
- package/dist/types/trigger.d.ts +18 -0
- package/dist/types/trigger.js +1 -0
- package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
- package/dist/ui/assets/index-CntB4bZR.js +133 -0
- package/dist/ui/index.html +2 -2
- package/dist/workshop/autosave.d.ts +58 -0
- package/dist/workshop/autosave.js +222 -0
- package/dist/workshop/buildStore.d.ts +79 -0
- package/dist/workshop/buildStore.js +326 -0
- package/dist/workshop/chatTurn.d.ts +232 -0
- package/dist/workshop/chatTurn.js +475 -0
- package/dist/workshop/modelSettings.d.ts +18 -0
- package/dist/workshop/modelSettings.js +44 -0
- package/dist/workshop/sessionStore.d.ts +61 -0
- package/dist/workshop/sessionStore.js +254 -0
- package/dist/workshop/workshopPin.d.ts +72 -0
- package/dist/workshop/workshopPin.js +193 -0
- package/examples/README.md +2 -1
- package/examples/browser-session/README.md +77 -0
- package/examples/browser-session/browser-session.pipeline.yaml +50 -0
- package/examples/browser-session/browser-session.task.yaml +6 -0
- package/examples/browser-session/check-login.yaml +22 -0
- package/examples/browser-session/fixture-server.mjs +64 -0
- package/examples/browser-session/human-login.yaml +31 -0
- package/examples/browser-session/merge.yaml +36 -0
- package/examples/browser-session/work-a.yaml +41 -0
- package/examples/browser-session/work-b.yaml +41 -0
- package/examples/stage-mcp/README.md +3 -2
- package/examples/stageflow.yaml +51 -0
- package/package.json +5 -1
- package/skills/stageflow/references/control-surface.md +3 -1
- package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
- package/skills/stageflow-run/SKILL.md +2 -0
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
- package/skills/stageflow-setup/SKILL.md +11 -0
- package/dist/ui/assets/index-B6hlSXXn.js +0 -120
package/dist/server/http.js
CHANGED
|
@@ -6,8 +6,17 @@ import { getCredentialSourceSettings, setCredentialSource, } from "../agent/prov
|
|
|
6
6
|
import { mapProviderAuthError } from "../agent/providerInspect.js";
|
|
7
7
|
import { handleProviderRoutes } from "./providerRoutes.js";
|
|
8
8
|
import { handleProjectMcpRoutes } from "./projectMcpRoutes.js";
|
|
9
|
+
import { handleTriggerRoutes } from "./triggerRoutes.js";
|
|
9
10
|
import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
|
|
10
11
|
import { createStage, parseCreateStageBody } from "../config/createStage.js";
|
|
12
|
+
import { loadDraftPackage, loadTaskArtifact, parseAttachTaskBody, parseCreateDraftPackageBody, parseDraftPackageBody, parseOpenDraftPackageBody, parseOverwriteDraftPackageBody, validateDraftPackage, } from "../config/draftPackage.js";
|
|
13
|
+
import { publishDraftPackage } from "../config/publishDraftPackage.js";
|
|
14
|
+
import { clearWorkshopAutosave, detectDiskChange, draftPackageDiskRelativePaths, fingerprintPackageFiles, parseWorkshopAutosaveRecord, readWorkshopAutosave, resolveWorkshopAutosaveStoreRoot, workshopAutosaveSlotKey, writeWorkshopAutosave, } from "../workshop/autosave.js";
|
|
15
|
+
import { acceptWorkshopSessionMutation, iterateWorkshopChatStreamFrames, runWorkshopChatTurn, stopWorkshopChatTurn, undoWorkshopSessionMutation, WorkshopChatSessionRegistry, WorkshopSessionStoreError, } from "../workshop/chatTurn.js";
|
|
16
|
+
import { readWorkshopOpenModel, resolveWorkshopModel, } from "../workshop/modelSettings.js";
|
|
17
|
+
import { createWorkshopSession, getWorkshopSession, listWorkshopSessions, resolveWorkshopSessionStoreRoot, updateWorkshopSessionActiveBuildId, } from "../workshop/sessionStore.js";
|
|
18
|
+
import { createWorkshopBuild, focusUnboundWorkshopBuild, getWorkshopBuild, listWorkshopBuilds, listWorkshopPickerRows, resolvePickerCatalogPipelines, resolveWorkshopBuildStoreRoot, updateWorkshopBuild, WorkshopBuildStoreError, } from "../workshop/buildStore.js";
|
|
19
|
+
import { createLiveWorkshopOperatorHost, } from "../operatorAgent/index.js";
|
|
11
20
|
import { listModelsMultiProject, listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
|
|
12
21
|
import { catalogPathErrorBody, CatalogPathError, resolveCatalogRelativePath, resolveCatalogStartInput, resolveWritableCatalogRoot, } from "../config/catalogRelativePath.js";
|
|
13
22
|
import { listExtensions } from "../config/listExtensions.js";
|
|
@@ -25,7 +34,7 @@ import { globalStageflowHome } from "../project/globalHome.js";
|
|
|
25
34
|
import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
|
|
26
35
|
import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
|
|
27
36
|
import { PipelinePreflightError } from "../runtime/pipelineRunner.js";
|
|
28
|
-
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
|
|
37
|
+
import { INVALID_SLOT_COUNT_MESSAGE, INVALID_WORKSHOP_MODEL_MESSAGE, parseSlotCount, parseWorkshopModelSetting, readFactorySettings, writeFactorySettings, } from "../runtime/settingsFile.js";
|
|
29
38
|
import { isTaskFile } from "../runtime/taskInput.js";
|
|
30
39
|
import { findTokenShapedField, tokenRejectedPayload, } from "../runtime/startPayload.js";
|
|
31
40
|
import { parseAskOperatorAnswer } from "../tools/askOperator.js";
|
|
@@ -56,6 +65,28 @@ async function readJsonBody(req) {
|
|
|
56
65
|
return {};
|
|
57
66
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
58
67
|
}
|
|
68
|
+
function isPlainObject(value) {
|
|
69
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
function readBuildTieField(body, field) {
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(body, field)) {
|
|
73
|
+
return { ok: true, present: false };
|
|
74
|
+
}
|
|
75
|
+
const value = body[field];
|
|
76
|
+
if (value === null)
|
|
77
|
+
return { ok: true, present: true, value: null };
|
|
78
|
+
if (typeof value === "string")
|
|
79
|
+
return { ok: true, present: true, value };
|
|
80
|
+
return { ok: false, error: `${field} must be a string or null` };
|
|
81
|
+
}
|
|
82
|
+
/** Raw bytes, unparsed — for callers (webhook signature verification) that need the exact wire body. */
|
|
83
|
+
async function readRawBody(req) {
|
|
84
|
+
const chunks = [];
|
|
85
|
+
for await (const chunk of req) {
|
|
86
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
87
|
+
}
|
|
88
|
+
return Buffer.concat(chunks);
|
|
89
|
+
}
|
|
59
90
|
function contentTypeFor(filePath) {
|
|
60
91
|
const ext = path.extname(filePath).toLowerCase();
|
|
61
92
|
switch (ext) {
|
|
@@ -125,7 +156,15 @@ export function defaultUiDistDir() {
|
|
|
125
156
|
* API; only the former needs the UI files.
|
|
126
157
|
*/
|
|
127
158
|
export function createOperatorRoutes(deps) {
|
|
128
|
-
const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir } = deps;
|
|
159
|
+
const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir, workshopOperatorHost, } = deps;
|
|
160
|
+
const workshopChatRegistry = deps.workshopChatRegistry ??
|
|
161
|
+
new WorkshopChatSessionRegistry(workshopOperatorHost ??
|
|
162
|
+
createLiveWorkshopOperatorHost({
|
|
163
|
+
cwd,
|
|
164
|
+
projectRoot: cwd,
|
|
165
|
+
}));
|
|
166
|
+
const workshopSessionStoreRoot = () => resolveWorkshopSessionStoreRoot();
|
|
167
|
+
const workshopBuildStoreRoot = () => resolveWorkshopBuildStoreRoot();
|
|
129
168
|
const allowedHosts = deps.allowedHosts ?? resolveAllowedHosts();
|
|
130
169
|
const controlTokens = deps.controlTokens ?? loadControlTokens();
|
|
131
170
|
return async ({ req, res, url, pathname, method, boot }) => {
|
|
@@ -780,7 +819,12 @@ export function createOperatorRoutes(deps) {
|
|
|
780
819
|
json(res, result.status, { error: result.reason });
|
|
781
820
|
return true;
|
|
782
821
|
}
|
|
783
|
-
json(res, 202, {
|
|
822
|
+
json(res, 202, {
|
|
823
|
+
ok: true,
|
|
824
|
+
...(result.verification !== undefined
|
|
825
|
+
? { verification: result.verification }
|
|
826
|
+
: {}),
|
|
827
|
+
});
|
|
784
828
|
return true;
|
|
785
829
|
}
|
|
786
830
|
const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
|
|
@@ -1007,6 +1051,7 @@ export function createOperatorRoutes(deps) {
|
|
|
1007
1051
|
json(res, 200, {
|
|
1008
1052
|
tasks: result.items,
|
|
1009
1053
|
root_errors: result.root_errors,
|
|
1054
|
+
...(result.tip !== undefined ? { tip: result.tip } : {}),
|
|
1010
1055
|
});
|
|
1011
1056
|
return true;
|
|
1012
1057
|
}
|
|
@@ -1029,6 +1074,7 @@ export function createOperatorRoutes(deps) {
|
|
|
1029
1074
|
json(res, 200, {
|
|
1030
1075
|
pipelines: result.items,
|
|
1031
1076
|
root_errors: result.root_errors,
|
|
1077
|
+
...(result.tip !== undefined ? { tip: result.tip } : {}),
|
|
1032
1078
|
});
|
|
1033
1079
|
return true;
|
|
1034
1080
|
}
|
|
@@ -1071,9 +1117,9 @@ export function createOperatorRoutes(deps) {
|
|
|
1071
1117
|
return true;
|
|
1072
1118
|
}
|
|
1073
1119
|
const ctx = await resolveStageflowContext(stageWriteRoot);
|
|
1074
|
-
if (
|
|
1120
|
+
if (ctx.manifestStatus !== "ok") {
|
|
1075
1121
|
json(res, 400, {
|
|
1076
|
-
error: "Project root not found; initialize stageflow.yaml
|
|
1122
|
+
error: "Project root not found; initialize stageflow.yaml",
|
|
1077
1123
|
});
|
|
1078
1124
|
return true;
|
|
1079
1125
|
}
|
|
@@ -1118,9 +1164,9 @@ export function createOperatorRoutes(deps) {
|
|
|
1118
1164
|
return true;
|
|
1119
1165
|
}
|
|
1120
1166
|
const ctx = await resolveStageflowContext(pipelineWriteRoot);
|
|
1121
|
-
if (
|
|
1167
|
+
if (ctx.manifestStatus !== "ok") {
|
|
1122
1168
|
json(res, 400, {
|
|
1123
|
-
error: "Project root not found; initialize stageflow.yaml
|
|
1169
|
+
error: "Project root not found; initialize stageflow.yaml",
|
|
1124
1170
|
});
|
|
1125
1171
|
return true;
|
|
1126
1172
|
}
|
|
@@ -1132,65 +1178,233 @@ export function createOperatorRoutes(deps) {
|
|
|
1132
1178
|
json(res, 201, result.pipeline);
|
|
1133
1179
|
return true;
|
|
1134
1180
|
}
|
|
1135
|
-
if (method === "
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1181
|
+
if (method === "POST" && pathname === "/api/drafts/validate") {
|
|
1182
|
+
let body;
|
|
1183
|
+
try {
|
|
1184
|
+
body = await readJsonBody(req);
|
|
1185
|
+
}
|
|
1186
|
+
catch {
|
|
1187
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
1191
|
+
if ("ok" in draftParsed) {
|
|
1192
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
const writeRoot = body !== null &&
|
|
1196
|
+
typeof body === "object" &&
|
|
1197
|
+
!Array.isArray(body) &&
|
|
1198
|
+
typeof body.project_root === "string"
|
|
1199
|
+
? body.project_root
|
|
1200
|
+
: undefined;
|
|
1201
|
+
let validateRoot = cwd;
|
|
1202
|
+
if (writeRoot) {
|
|
1203
|
+
try {
|
|
1204
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1205
|
+
validateRoot = selected.path;
|
|
1206
|
+
}
|
|
1207
|
+
catch (err) {
|
|
1208
|
+
if (err instanceof CatalogPathError) {
|
|
1209
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1210
|
+
return true;
|
|
1211
|
+
}
|
|
1212
|
+
throw err;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
const ctx = await resolveStageflowContext(validateRoot);
|
|
1216
|
+
const result = await validateDraftPackage(draftParsed, {
|
|
1217
|
+
cwd: ctx.projectRoot,
|
|
1218
|
+
projectRoot: ctx.projectRoot,
|
|
1219
|
+
strict: true,
|
|
1141
1220
|
});
|
|
1142
|
-
|
|
1143
|
-
|
|
1221
|
+
json(res, 200, result);
|
|
1222
|
+
return true;
|
|
1223
|
+
}
|
|
1224
|
+
if (method === "POST" && pathname === "/api/drafts/create") {
|
|
1225
|
+
let body;
|
|
1226
|
+
try {
|
|
1227
|
+
body = await readJsonBody(req);
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
const writeRoot = body !== null &&
|
|
1234
|
+
typeof body === "object" &&
|
|
1235
|
+
!Array.isArray(body) &&
|
|
1236
|
+
typeof body.project_root === "string"
|
|
1237
|
+
? body.project_root
|
|
1238
|
+
: undefined;
|
|
1239
|
+
let draftWriteRoot;
|
|
1240
|
+
try {
|
|
1241
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1242
|
+
draftWriteRoot = selected.path;
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
if (err instanceof CatalogPathError) {
|
|
1246
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
throw err;
|
|
1250
|
+
}
|
|
1251
|
+
const parsed = parseCreateDraftPackageBody(body);
|
|
1252
|
+
if ("ok" in parsed) {
|
|
1253
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
const ctx = await resolveStageflowContext(draftWriteRoot);
|
|
1257
|
+
if (!ctx.isGitProject) {
|
|
1144
1258
|
json(res, 400, {
|
|
1145
|
-
error:
|
|
1146
|
-
code: "unknown_project_root",
|
|
1147
|
-
root_errors: result.root_errors,
|
|
1259
|
+
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
1148
1260
|
});
|
|
1149
1261
|
return true;
|
|
1150
1262
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1263
|
+
const result = (await publishDraftPackage(ctx.projectRoot, parsed, "create")).write;
|
|
1264
|
+
if (!result.ok) {
|
|
1265
|
+
json(res, result.status, {
|
|
1266
|
+
error: result.error,
|
|
1267
|
+
...(result.findings ? { findings: result.findings } : {}),
|
|
1268
|
+
});
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
json(res, 201, {
|
|
1272
|
+
pipeline: result.pipeline,
|
|
1273
|
+
pipelinePath: result.pipelinePath,
|
|
1274
|
+
stagePaths: result.stagePaths,
|
|
1275
|
+
...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
|
|
1155
1276
|
});
|
|
1156
1277
|
return true;
|
|
1157
1278
|
}
|
|
1158
|
-
if (
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1279
|
+
if (method === "POST" && pathname === "/api/drafts/overwrite") {
|
|
1280
|
+
let body;
|
|
1281
|
+
try {
|
|
1282
|
+
body = await readJsonBody(req);
|
|
1283
|
+
}
|
|
1284
|
+
catch {
|
|
1285
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
const writeRoot = body !== null &&
|
|
1289
|
+
typeof body === "object" &&
|
|
1290
|
+
!Array.isArray(body) &&
|
|
1291
|
+
typeof body.project_root === "string"
|
|
1292
|
+
? body.project_root
|
|
1293
|
+
: undefined;
|
|
1294
|
+
let draftWriteRoot;
|
|
1295
|
+
try {
|
|
1296
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1297
|
+
draftWriteRoot = selected.path;
|
|
1298
|
+
}
|
|
1299
|
+
catch (err) {
|
|
1300
|
+
if (err instanceof CatalogPathError) {
|
|
1301
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
throw err;
|
|
1305
|
+
}
|
|
1306
|
+
const parsed = parseOverwriteDraftPackageBody(body);
|
|
1307
|
+
if ("ok" in parsed) {
|
|
1308
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
const ctx = await resolveStageflowContext(draftWriteRoot);
|
|
1312
|
+
if (!ctx.isGitProject) {
|
|
1313
|
+
json(res, 400, {
|
|
1314
|
+
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
1315
|
+
});
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
const result = (await publishDraftPackage(ctx.projectRoot, parsed, "overwrite")).write;
|
|
1319
|
+
if (!result.ok) {
|
|
1320
|
+
json(res, result.status, {
|
|
1321
|
+
error: result.error,
|
|
1322
|
+
...(result.findings ? { findings: result.findings } : {}),
|
|
1323
|
+
});
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
json(res, 200, {
|
|
1327
|
+
pipeline: result.pipeline,
|
|
1328
|
+
pipelinePath: result.pipelinePath,
|
|
1329
|
+
stagePaths: result.stagePaths,
|
|
1330
|
+
...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
|
|
1331
|
+
});
|
|
1182
1332
|
return true;
|
|
1183
1333
|
}
|
|
1184
|
-
if (method === "
|
|
1185
|
-
|
|
1186
|
-
|
|
1334
|
+
if (method === "POST" && pathname === "/api/drafts/open") {
|
|
1335
|
+
let body;
|
|
1336
|
+
try {
|
|
1337
|
+
body = await readJsonBody(req);
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
const parsed = parseOpenDraftPackageBody(body);
|
|
1344
|
+
if ("ok" in parsed) {
|
|
1345
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1346
|
+
return true;
|
|
1347
|
+
}
|
|
1348
|
+
const writeRoot = body !== null &&
|
|
1349
|
+
typeof body === "object" &&
|
|
1350
|
+
!Array.isArray(body) &&
|
|
1351
|
+
typeof body.project_root === "string"
|
|
1352
|
+
? body.project_root
|
|
1353
|
+
: undefined;
|
|
1354
|
+
let wireRoot;
|
|
1355
|
+
let roots;
|
|
1356
|
+
try {
|
|
1357
|
+
({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot));
|
|
1358
|
+
}
|
|
1359
|
+
catch (err) {
|
|
1360
|
+
if (err instanceof CatalogPathError) {
|
|
1361
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
throw err;
|
|
1365
|
+
}
|
|
1366
|
+
let pipelineRel;
|
|
1367
|
+
let taskRel;
|
|
1368
|
+
try {
|
|
1369
|
+
pipelineRel = resolveCatalogRelativePath({
|
|
1370
|
+
inputPath: parsed.path,
|
|
1371
|
+
projectRoot: writeRoot,
|
|
1372
|
+
roots,
|
|
1373
|
+
fieldName: "path",
|
|
1374
|
+
}).relativePath;
|
|
1375
|
+
if (parsed.taskPath) {
|
|
1376
|
+
taskRel = resolveCatalogRelativePath({
|
|
1377
|
+
inputPath: parsed.taskPath,
|
|
1378
|
+
projectRoot: writeRoot,
|
|
1379
|
+
roots,
|
|
1380
|
+
fieldName: "task",
|
|
1381
|
+
}).relativePath;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
catch (err) {
|
|
1385
|
+
if (err instanceof CatalogPathError) {
|
|
1386
|
+
json(res, 400, catalogPathErrorBody(err));
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
throw err;
|
|
1390
|
+
}
|
|
1391
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1392
|
+
const result = await loadDraftPackage(ctx.projectRoot, pipelineRel, {
|
|
1393
|
+
...(taskRel !== undefined ? { taskPath: taskRel } : {}),
|
|
1394
|
+
});
|
|
1395
|
+
if (!result.ok) {
|
|
1396
|
+
json(res, result.status, { error: result.error });
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1187
1399
|
json(res, 200, {
|
|
1188
|
-
|
|
1189
|
-
|
|
1400
|
+
draft: result.draft,
|
|
1401
|
+
destination: result.destination,
|
|
1402
|
+
pipelinePath: result.pipelinePath,
|
|
1403
|
+
...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
|
|
1190
1404
|
});
|
|
1191
1405
|
return true;
|
|
1192
1406
|
}
|
|
1193
|
-
if (method === "POST" && pathname === "/api/
|
|
1407
|
+
if (method === "POST" && pathname === "/api/drafts/attach-task") {
|
|
1194
1408
|
let body;
|
|
1195
1409
|
try {
|
|
1196
1410
|
body = await readJsonBody(req);
|
|
@@ -1199,40 +1413,1000 @@ export function createOperatorRoutes(deps) {
|
|
|
1199
1413
|
json(res, 400, { error: "Invalid JSON body" });
|
|
1200
1414
|
return true;
|
|
1201
1415
|
}
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
:
|
|
1205
|
-
const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
|
|
1206
|
-
const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
|
|
1207
|
-
if (!hasMax && !hasCredentialSource) {
|
|
1208
|
-
json(res, 400, {
|
|
1209
|
-
error: "maxConcurrent or credentialSource is required",
|
|
1210
|
-
});
|
|
1416
|
+
const parsed = parseAttachTaskBody(body);
|
|
1417
|
+
if ("ok" in parsed) {
|
|
1418
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1211
1419
|
return true;
|
|
1212
1420
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1421
|
+
const writeRoot = body !== null &&
|
|
1422
|
+
typeof body === "object" &&
|
|
1423
|
+
!Array.isArray(body) &&
|
|
1424
|
+
typeof body.project_root === "string"
|
|
1425
|
+
? body.project_root
|
|
1426
|
+
: undefined;
|
|
1427
|
+
let wireRoot;
|
|
1428
|
+
let roots;
|
|
1429
|
+
try {
|
|
1430
|
+
({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot));
|
|
1431
|
+
}
|
|
1432
|
+
catch (err) {
|
|
1433
|
+
if (err instanceof CatalogPathError) {
|
|
1434
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1218
1435
|
return true;
|
|
1219
1436
|
}
|
|
1220
|
-
|
|
1437
|
+
throw err;
|
|
1221
1438
|
}
|
|
1222
|
-
let
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1439
|
+
let taskRel;
|
|
1440
|
+
try {
|
|
1441
|
+
taskRel = resolveCatalogRelativePath({
|
|
1442
|
+
inputPath: parsed.taskPath,
|
|
1443
|
+
projectRoot: writeRoot,
|
|
1444
|
+
roots,
|
|
1445
|
+
fieldName: "task",
|
|
1446
|
+
}).relativePath;
|
|
1447
|
+
}
|
|
1448
|
+
catch (err) {
|
|
1449
|
+
if (err instanceof CatalogPathError) {
|
|
1450
|
+
json(res, 400, catalogPathErrorBody(err));
|
|
1451
|
+
return true;
|
|
1226
1452
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1453
|
+
throw err;
|
|
1454
|
+
}
|
|
1455
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1456
|
+
const result = await loadTaskArtifact(ctx.projectRoot, taskRel);
|
|
1457
|
+
if (!result.ok) {
|
|
1458
|
+
json(res, result.status, { error: result.error });
|
|
1459
|
+
return true;
|
|
1460
|
+
}
|
|
1461
|
+
json(res, 200, {
|
|
1462
|
+
task: result.task,
|
|
1463
|
+
taskPath: result.taskPath,
|
|
1464
|
+
});
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
if (method === "GET" &&
|
|
1468
|
+
pathname === "/api/workshop/autosave") {
|
|
1469
|
+
const keyParam = url.searchParams.get("key");
|
|
1470
|
+
const key = workshopAutosaveSlotKey(keyParam);
|
|
1471
|
+
const writeRoot = url.searchParams.get("project_root") ?? undefined;
|
|
1472
|
+
let storeRoot;
|
|
1473
|
+
try {
|
|
1474
|
+
if (writeRoot) {
|
|
1475
|
+
const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot);
|
|
1476
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1477
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1478
|
+
projectRoot: ctx.projectRoot,
|
|
1479
|
+
isGitProject: ctx.isGitProject,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
1484
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1485
|
+
projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
|
|
1486
|
+
isGitProject: ctx.isGitProject,
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
catch (err) {
|
|
1491
|
+
if (err instanceof CatalogPathError) {
|
|
1492
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
throw err;
|
|
1496
|
+
}
|
|
1497
|
+
const record = readWorkshopAutosave(storeRoot, key);
|
|
1498
|
+
json(res, 200, { key, autosave: record });
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
if (method === "PUT" &&
|
|
1502
|
+
pathname === "/api/workshop/autosave") {
|
|
1503
|
+
let body;
|
|
1504
|
+
try {
|
|
1505
|
+
body = await readJsonBody(req);
|
|
1506
|
+
}
|
|
1507
|
+
catch {
|
|
1508
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
if (!isPlainObject(body)) {
|
|
1512
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1513
|
+
return true;
|
|
1514
|
+
}
|
|
1515
|
+
const key = workshopAutosaveSlotKey(typeof body.key === "string" ? body.key : undefined);
|
|
1516
|
+
const writeRoot = typeof body.project_root === "string"
|
|
1517
|
+
? body.project_root
|
|
1518
|
+
: undefined;
|
|
1519
|
+
let storeRoot;
|
|
1520
|
+
try {
|
|
1521
|
+
if (writeRoot) {
|
|
1522
|
+
const { wireRoot } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1523
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1524
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1525
|
+
projectRoot: ctx.projectRoot,
|
|
1526
|
+
isGitProject: ctx.isGitProject,
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
1531
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1532
|
+
projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
|
|
1533
|
+
isGitProject: ctx.isGitProject,
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
catch (err) {
|
|
1538
|
+
if (err instanceof CatalogPathError) {
|
|
1539
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1540
|
+
return true;
|
|
1541
|
+
}
|
|
1542
|
+
throw err;
|
|
1543
|
+
}
|
|
1544
|
+
const candidate = {
|
|
1545
|
+
version: 1,
|
|
1546
|
+
key,
|
|
1547
|
+
updatedAt: typeof body.updatedAt === "string" && body.updatedAt
|
|
1548
|
+
? body.updatedAt
|
|
1549
|
+
: new Date().toISOString(),
|
|
1550
|
+
draft: body.draft,
|
|
1551
|
+
messages: body.messages,
|
|
1552
|
+
autoApply: Boolean(body.autoApply),
|
|
1553
|
+
...(body.sessionModelOverride === null ||
|
|
1554
|
+
typeof body.sessionModelOverride === "string"
|
|
1555
|
+
? { sessionModelOverride: body.sessionModelOverride }
|
|
1556
|
+
: {}),
|
|
1557
|
+
...(body.destination !== undefined
|
|
1558
|
+
? {
|
|
1559
|
+
destination: body.destination,
|
|
1560
|
+
}
|
|
1561
|
+
: {}),
|
|
1562
|
+
...(body.savedPath === null || typeof body.savedPath === "string"
|
|
1563
|
+
? { savedPath: body.savedPath }
|
|
1564
|
+
: {}),
|
|
1565
|
+
...(body.savedTaskPath === null ||
|
|
1566
|
+
typeof body.savedTaskPath === "string"
|
|
1567
|
+
? { savedTaskPath: body.savedTaskPath }
|
|
1568
|
+
: {}),
|
|
1569
|
+
...(isPlainObject(body.diskFingerprints)
|
|
1570
|
+
? {
|
|
1571
|
+
diskFingerprints: body.diskFingerprints,
|
|
1572
|
+
}
|
|
1573
|
+
: {}),
|
|
1574
|
+
};
|
|
1575
|
+
const parsed = parseWorkshopAutosaveRecord(candidate);
|
|
1576
|
+
if (!parsed) {
|
|
1577
|
+
json(res, 400, { error: "Invalid workshop autosave payload" });
|
|
1578
|
+
return true;
|
|
1579
|
+
}
|
|
1580
|
+
const written = writeWorkshopAutosave(storeRoot, parsed);
|
|
1581
|
+
json(res, 200, { autosave: written });
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
if (method === "DELETE" &&
|
|
1585
|
+
pathname === "/api/workshop/autosave") {
|
|
1586
|
+
let body;
|
|
1587
|
+
try {
|
|
1588
|
+
body = await readJsonBody(req);
|
|
1589
|
+
}
|
|
1590
|
+
catch {
|
|
1591
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
if (!isPlainObject(body)) {
|
|
1595
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1596
|
+
return true;
|
|
1597
|
+
}
|
|
1598
|
+
const key = workshopAutosaveSlotKey(typeof body.key === "string" ? body.key : undefined);
|
|
1599
|
+
const writeRoot = typeof body.project_root === "string"
|
|
1600
|
+
? body.project_root
|
|
1601
|
+
: undefined;
|
|
1602
|
+
let storeRoot;
|
|
1603
|
+
try {
|
|
1604
|
+
if (writeRoot) {
|
|
1605
|
+
const { wireRoot } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1606
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1607
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1608
|
+
projectRoot: ctx.projectRoot,
|
|
1609
|
+
isGitProject: ctx.isGitProject,
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
else {
|
|
1613
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
1614
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1615
|
+
projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
|
|
1616
|
+
isGitProject: ctx.isGitProject,
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
catch (err) {
|
|
1621
|
+
if (err instanceof CatalogPathError) {
|
|
1622
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
throw err;
|
|
1626
|
+
}
|
|
1627
|
+
const cleared = clearWorkshopAutosave(storeRoot, key);
|
|
1628
|
+
json(res, 200, { key, cleared });
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
if (method === "GET" && pathname === "/api/workshop/picker") {
|
|
1632
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
1633
|
+
const result = await listPipelinesMultiProject({
|
|
1634
|
+
store,
|
|
1635
|
+
bootCwd: cwd,
|
|
1636
|
+
projectRootFilter: filter,
|
|
1637
|
+
});
|
|
1638
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
1639
|
+
result.items.length === 0) {
|
|
1640
|
+
json(res, 400, {
|
|
1641
|
+
error: result.root_errors[0].message,
|
|
1642
|
+
code: "unknown_project_root",
|
|
1643
|
+
root_errors: result.root_errors,
|
|
1644
|
+
});
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
json(res, 200, {
|
|
1648
|
+
rows: listWorkshopPickerRows(workshopBuildStoreRoot(), resolvePickerCatalogPipelines(result.items, result.roots), result.roots),
|
|
1649
|
+
root_errors: result.root_errors,
|
|
1650
|
+
...(result.tip !== undefined ? { tip: result.tip } : {}),
|
|
1651
|
+
});
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
if (method === "POST" && pathname === "/api/workshop/focus") {
|
|
1655
|
+
let body;
|
|
1656
|
+
try {
|
|
1657
|
+
body = await readJsonBody(req);
|
|
1658
|
+
}
|
|
1659
|
+
catch {
|
|
1660
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
if (!isPlainObject(body)) {
|
|
1664
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
if (typeof body.projectRoot !== "string" ||
|
|
1668
|
+
typeof body.relativePath !== "string") {
|
|
1669
|
+
json(res, 400, { error: "project root and path are required" });
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
let projectRoot = body.projectRoot;
|
|
1673
|
+
try {
|
|
1674
|
+
const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, body.projectRoot);
|
|
1675
|
+
projectRoot = wireRoot.path;
|
|
1676
|
+
}
|
|
1677
|
+
catch (err) {
|
|
1678
|
+
if (err instanceof CatalogPathError) {
|
|
1679
|
+
json(res, 400, { error: err.message });
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1682
|
+
throw err;
|
|
1683
|
+
}
|
|
1684
|
+
const focused = await focusUnboundWorkshopBuild(workshopBuildStoreRoot(), {
|
|
1685
|
+
projectRoot,
|
|
1686
|
+
relativePath: body.relativePath,
|
|
1687
|
+
});
|
|
1688
|
+
if (!focused.ok) {
|
|
1689
|
+
json(res, focused.status, { error: focused.error });
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
json(res, focused.created ? 201 : 200, { build: focused.build });
|
|
1693
|
+
return true;
|
|
1694
|
+
}
|
|
1695
|
+
if (method === "GET" &&
|
|
1696
|
+
pathname === "/api/workshop/builds") {
|
|
1697
|
+
json(res, 200, {
|
|
1698
|
+
builds: listWorkshopBuilds(workshopBuildStoreRoot()),
|
|
1699
|
+
});
|
|
1700
|
+
return true;
|
|
1701
|
+
}
|
|
1702
|
+
if (method === "POST" &&
|
|
1703
|
+
pathname === "/api/workshop/builds") {
|
|
1704
|
+
let body = {};
|
|
1705
|
+
try {
|
|
1706
|
+
body = await readJsonBody(req);
|
|
1707
|
+
}
|
|
1708
|
+
catch {
|
|
1709
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
if (!isPlainObject(body)) {
|
|
1713
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1716
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
1717
|
+
if ("ok" in draftParsed) {
|
|
1718
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
const projectRoot = readBuildTieField(body, "projectRoot");
|
|
1722
|
+
if (!projectRoot.ok) {
|
|
1723
|
+
json(res, 400, { error: projectRoot.error });
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
const relativePath = readBuildTieField(body, "relativePath");
|
|
1727
|
+
if (!relativePath.ok) {
|
|
1728
|
+
json(res, 400, { error: relativePath.error });
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
const build = createWorkshopBuild(workshopBuildStoreRoot(), {
|
|
1732
|
+
draft: draftParsed,
|
|
1733
|
+
...(typeof body.id === "string" ? { id: body.id } : {}),
|
|
1734
|
+
...(projectRoot.present ? { projectRoot: projectRoot.value } : {}),
|
|
1735
|
+
...(relativePath.present
|
|
1736
|
+
? { relativePath: relativePath.value }
|
|
1737
|
+
: {}),
|
|
1738
|
+
});
|
|
1739
|
+
json(res, 201, { build });
|
|
1740
|
+
return true;
|
|
1741
|
+
}
|
|
1742
|
+
const workshopBuildMatch = pathname.match(/^\/api\/workshop\/builds\/([^/]+)$/);
|
|
1743
|
+
if (method === "GET" && workshopBuildMatch) {
|
|
1744
|
+
const buildId = decodeURIComponent(workshopBuildMatch[1] ?? "");
|
|
1745
|
+
try {
|
|
1746
|
+
const build = getWorkshopBuild(workshopBuildStoreRoot(), buildId);
|
|
1747
|
+
json(res, 200, { build });
|
|
1748
|
+
}
|
|
1749
|
+
catch (err) {
|
|
1750
|
+
if (err instanceof WorkshopBuildStoreError &&
|
|
1751
|
+
err.code === "workshop_build_not_found") {
|
|
1752
|
+
json(res, 404, {
|
|
1753
|
+
error: err.message,
|
|
1754
|
+
code: err.code,
|
|
1755
|
+
buildId: err.buildId,
|
|
1756
|
+
});
|
|
1757
|
+
return true;
|
|
1758
|
+
}
|
|
1759
|
+
throw err;
|
|
1760
|
+
}
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
if (method === "PUT" && workshopBuildMatch) {
|
|
1764
|
+
const buildId = decodeURIComponent(workshopBuildMatch[1] ?? "");
|
|
1765
|
+
let body = {};
|
|
1766
|
+
try {
|
|
1767
|
+
body = await readJsonBody(req);
|
|
1768
|
+
}
|
|
1769
|
+
catch {
|
|
1770
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
if (!isPlainObject(body)) {
|
|
1774
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1775
|
+
return true;
|
|
1776
|
+
}
|
|
1777
|
+
let draft;
|
|
1778
|
+
if (body.draft !== undefined) {
|
|
1779
|
+
const draftParsed = parseDraftPackageBody({ draft: body.draft });
|
|
1780
|
+
if ("ok" in draftParsed) {
|
|
1781
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
draft = draftParsed;
|
|
1785
|
+
}
|
|
1786
|
+
const projectRoot = readBuildTieField(body, "projectRoot");
|
|
1787
|
+
if (!projectRoot.ok) {
|
|
1788
|
+
json(res, 400, { error: projectRoot.error });
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
const relativePath = readBuildTieField(body, "relativePath");
|
|
1792
|
+
if (!relativePath.ok) {
|
|
1793
|
+
json(res, 400, { error: relativePath.error });
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1796
|
+
try {
|
|
1797
|
+
const build = updateWorkshopBuild(workshopBuildStoreRoot(), buildId, {
|
|
1798
|
+
...(draft ? { draft } : {}),
|
|
1799
|
+
...(projectRoot.present
|
|
1800
|
+
? { projectRoot: projectRoot.value }
|
|
1801
|
+
: {}),
|
|
1802
|
+
...(relativePath.present
|
|
1803
|
+
? { relativePath: relativePath.value }
|
|
1804
|
+
: {}),
|
|
1805
|
+
});
|
|
1806
|
+
json(res, 200, { build });
|
|
1807
|
+
}
|
|
1808
|
+
catch (err) {
|
|
1809
|
+
if (err instanceof WorkshopBuildStoreError &&
|
|
1810
|
+
err.code === "workshop_build_not_found") {
|
|
1811
|
+
json(res, 404, {
|
|
1812
|
+
error: err.message,
|
|
1813
|
+
code: err.code,
|
|
1814
|
+
buildId: err.buildId,
|
|
1815
|
+
});
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
throw err;
|
|
1819
|
+
}
|
|
1820
|
+
return true;
|
|
1821
|
+
}
|
|
1822
|
+
if (method === "GET" &&
|
|
1823
|
+
pathname === "/api/workshop/sessions") {
|
|
1824
|
+
const sessions = listWorkshopSessions(workshopSessionStoreRoot());
|
|
1825
|
+
json(res, 200, { sessions });
|
|
1826
|
+
return true;
|
|
1827
|
+
}
|
|
1828
|
+
if (method === "POST" &&
|
|
1829
|
+
pathname === "/api/workshop/sessions") {
|
|
1830
|
+
let body = {};
|
|
1831
|
+
try {
|
|
1832
|
+
body = await readJsonBody(req);
|
|
1833
|
+
}
|
|
1834
|
+
catch {
|
|
1835
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
if (!isPlainObject(body)) {
|
|
1839
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1840
|
+
return true;
|
|
1841
|
+
}
|
|
1842
|
+
const record = createWorkshopSession(workshopSessionStoreRoot(), {
|
|
1843
|
+
...(typeof body.id === "string" ? { id: body.id } : {}),
|
|
1844
|
+
});
|
|
1845
|
+
json(res, 201, { session: record });
|
|
1846
|
+
return true;
|
|
1847
|
+
}
|
|
1848
|
+
const workshopSessionGetMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)$/);
|
|
1849
|
+
if (method === "GET" && workshopSessionGetMatch) {
|
|
1850
|
+
const sessionId = decodeURIComponent(workshopSessionGetMatch[1] ?? "");
|
|
1851
|
+
try {
|
|
1852
|
+
const session = getWorkshopSession(workshopSessionStoreRoot(), sessionId);
|
|
1853
|
+
json(res, 200, { session });
|
|
1854
|
+
}
|
|
1855
|
+
catch (err) {
|
|
1856
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
1857
|
+
err.code === "workshop_session_not_found") {
|
|
1858
|
+
json(res, 404, {
|
|
1859
|
+
error: err.message,
|
|
1860
|
+
code: err.code,
|
|
1861
|
+
sessionId: err.sessionId,
|
|
1862
|
+
});
|
|
1863
|
+
return true;
|
|
1864
|
+
}
|
|
1865
|
+
throw err;
|
|
1866
|
+
}
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
const workshopSessionPatchMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)$/);
|
|
1870
|
+
if (method === "PATCH" && workshopSessionPatchMatch) {
|
|
1871
|
+
const sessionId = decodeURIComponent(workshopSessionPatchMatch[1] ?? "");
|
|
1872
|
+
let body = {};
|
|
1873
|
+
try {
|
|
1874
|
+
body = await readJsonBody(req);
|
|
1875
|
+
}
|
|
1876
|
+
catch {
|
|
1877
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1878
|
+
return true;
|
|
1879
|
+
}
|
|
1880
|
+
if (!isPlainObject(body)) {
|
|
1881
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
if (!Object.prototype.hasOwnProperty.call(body, "activeBuildId")) {
|
|
1885
|
+
json(res, 400, { error: "activeBuildId is required" });
|
|
1886
|
+
return true;
|
|
1887
|
+
}
|
|
1888
|
+
const activeBuildId = body.activeBuildId;
|
|
1889
|
+
if (activeBuildId !== null && typeof activeBuildId !== "string") {
|
|
1890
|
+
json(res, 400, { error: "activeBuildId must be a string or null" });
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
const session = updateWorkshopSessionActiveBuildId(workshopSessionStoreRoot(), sessionId, activeBuildId);
|
|
1895
|
+
json(res, 200, { session });
|
|
1896
|
+
}
|
|
1897
|
+
catch (err) {
|
|
1898
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
1899
|
+
err.code === "workshop_session_not_found") {
|
|
1900
|
+
json(res, 404, {
|
|
1901
|
+
error: err.message,
|
|
1902
|
+
code: err.code,
|
|
1903
|
+
sessionId: err.sessionId,
|
|
1904
|
+
});
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
throw err;
|
|
1908
|
+
}
|
|
1909
|
+
return true;
|
|
1910
|
+
}
|
|
1911
|
+
const workshopSessionUndoMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)\/undo$/);
|
|
1912
|
+
if (method === "POST" && workshopSessionUndoMatch) {
|
|
1913
|
+
const sessionId = decodeURIComponent(workshopSessionUndoMatch[1] ?? "");
|
|
1914
|
+
let body = {};
|
|
1915
|
+
try {
|
|
1916
|
+
body = await readJsonBody(req);
|
|
1917
|
+
}
|
|
1918
|
+
catch {
|
|
1919
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
if (!isPlainObject(body)) {
|
|
1923
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1924
|
+
return true;
|
|
1925
|
+
}
|
|
1926
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
1927
|
+
if ("ok" in draftParsed) {
|
|
1928
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1929
|
+
return true;
|
|
1930
|
+
}
|
|
1931
|
+
try {
|
|
1932
|
+
const result = await undoWorkshopSessionMutation({
|
|
1933
|
+
sessionId,
|
|
1934
|
+
draft: draftParsed,
|
|
1935
|
+
...(typeof body.mutationId === "string"
|
|
1936
|
+
? { mutationId: body.mutationId }
|
|
1937
|
+
: {}),
|
|
1938
|
+
registry: workshopChatRegistry,
|
|
1939
|
+
storeRoot: workshopSessionStoreRoot(),
|
|
1940
|
+
});
|
|
1941
|
+
json(res, result.ok ? 200 : 409, result);
|
|
1942
|
+
}
|
|
1943
|
+
catch (err) {
|
|
1944
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
1945
|
+
err.code === "workshop_session_not_found") {
|
|
1946
|
+
json(res, 404, {
|
|
1947
|
+
error: err.message,
|
|
1948
|
+
code: err.code,
|
|
1949
|
+
sessionId: err.sessionId,
|
|
1950
|
+
});
|
|
1951
|
+
return true;
|
|
1952
|
+
}
|
|
1953
|
+
json(res, 400, {
|
|
1954
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
return true;
|
|
1958
|
+
}
|
|
1959
|
+
const workshopSessionAcceptMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)\/accept$/);
|
|
1960
|
+
if (method === "POST" && workshopSessionAcceptMatch) {
|
|
1961
|
+
const sessionId = decodeURIComponent(workshopSessionAcceptMatch[1] ?? "");
|
|
1962
|
+
let body = {};
|
|
1963
|
+
try {
|
|
1964
|
+
body = await readJsonBody(req);
|
|
1965
|
+
}
|
|
1966
|
+
catch {
|
|
1967
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1970
|
+
if (!isPlainObject(body)) {
|
|
1971
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1972
|
+
return true;
|
|
1973
|
+
}
|
|
1974
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
1975
|
+
if ("ok" in draftParsed) {
|
|
1976
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1979
|
+
try {
|
|
1980
|
+
const result = await acceptWorkshopSessionMutation({
|
|
1981
|
+
sessionId,
|
|
1982
|
+
draft: draftParsed,
|
|
1983
|
+
...(typeof body.mutationId === "string"
|
|
1984
|
+
? { mutationId: body.mutationId }
|
|
1985
|
+
: {}),
|
|
1986
|
+
registry: workshopChatRegistry,
|
|
1987
|
+
storeRoot: workshopSessionStoreRoot(),
|
|
1988
|
+
});
|
|
1989
|
+
json(res, result.ok ? 200 : 409, result);
|
|
1990
|
+
}
|
|
1991
|
+
catch (err) {
|
|
1992
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
1993
|
+
err.code === "workshop_session_not_found") {
|
|
1994
|
+
json(res, 404, {
|
|
1995
|
+
error: err.message,
|
|
1996
|
+
code: err.code,
|
|
1997
|
+
sessionId: err.sessionId,
|
|
1998
|
+
});
|
|
1999
|
+
return true;
|
|
2000
|
+
}
|
|
2001
|
+
json(res, 400, {
|
|
2002
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
return true;
|
|
2006
|
+
}
|
|
2007
|
+
if (method === "POST" && pathname === "/api/workshop/chat/stop") {
|
|
2008
|
+
let body;
|
|
2009
|
+
try {
|
|
2010
|
+
body = await readJsonBody(req);
|
|
2011
|
+
}
|
|
2012
|
+
catch {
|
|
2013
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2014
|
+
return true;
|
|
2015
|
+
}
|
|
2016
|
+
if (!isPlainObject(body)) {
|
|
2017
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2018
|
+
return true;
|
|
2019
|
+
}
|
|
2020
|
+
if (typeof body.sessionId !== "string" || !body.sessionId.trim()) {
|
|
2021
|
+
json(res, 400, { error: "sessionId is required" });
|
|
2022
|
+
return true;
|
|
2023
|
+
}
|
|
2024
|
+
const stopped = await stopWorkshopChatTurn(workshopChatRegistry, body.sessionId);
|
|
2025
|
+
json(res, 200, { ok: true, ...stopped });
|
|
2026
|
+
return true;
|
|
2027
|
+
}
|
|
2028
|
+
if (method === "POST" && pathname === "/api/workshop/chat") {
|
|
2029
|
+
let body;
|
|
2030
|
+
try {
|
|
2031
|
+
body = await readJsonBody(req);
|
|
2032
|
+
}
|
|
2033
|
+
catch {
|
|
2034
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2035
|
+
return true;
|
|
2036
|
+
}
|
|
2037
|
+
if (!isPlainObject(body)) {
|
|
2038
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
if (typeof body.sessionId !== "string" || !body.sessionId.trim()) {
|
|
2042
|
+
json(res, 400, { error: "sessionId is required" });
|
|
2043
|
+
return true;
|
|
2044
|
+
}
|
|
2045
|
+
if (typeof body.message !== "string" || !body.message.trim()) {
|
|
2046
|
+
json(res, 400, { error: "message is required" });
|
|
2047
|
+
return true;
|
|
2048
|
+
}
|
|
2049
|
+
const sessionId = body.sessionId;
|
|
2050
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
2051
|
+
if ("ok" in draftParsed) {
|
|
2052
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
2053
|
+
return true;
|
|
2054
|
+
}
|
|
2055
|
+
const settingsDefault = await readWorkshopOpenModel(cwd);
|
|
2056
|
+
const accept = String(req.headers.accept ?? "");
|
|
2057
|
+
const wantsStream = body.stream === true ||
|
|
2058
|
+
accept.includes("application/x-ndjson") ||
|
|
2059
|
+
accept.includes("text/event-stream");
|
|
2060
|
+
const chatTurnBase = {
|
|
2061
|
+
sessionId: body.sessionId,
|
|
2062
|
+
draft: draftParsed,
|
|
2063
|
+
message: body.message,
|
|
2064
|
+
autoApply: body.autoApply === true,
|
|
2065
|
+
model: typeof body.model === "string" || body.model === null
|
|
2066
|
+
? body.model
|
|
2067
|
+
: undefined,
|
|
2068
|
+
settingsDefault: settingsDefault ?? null,
|
|
2069
|
+
registry: workshopChatRegistry,
|
|
2070
|
+
storeRoot: workshopSessionStoreRoot(),
|
|
2071
|
+
};
|
|
2072
|
+
// Fail closed on missing sessions before opening an NDJSON body.
|
|
2073
|
+
try {
|
|
2074
|
+
getWorkshopSession(workshopSessionStoreRoot(), body.sessionId);
|
|
2075
|
+
}
|
|
2076
|
+
catch (err) {
|
|
2077
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
2078
|
+
err.code === "workshop_session_not_found") {
|
|
2079
|
+
json(res, 404, {
|
|
2080
|
+
error: err.message,
|
|
2081
|
+
code: err.code,
|
|
2082
|
+
sessionId: err.sessionId,
|
|
2083
|
+
});
|
|
2084
|
+
return true;
|
|
2085
|
+
}
|
|
2086
|
+
throw err;
|
|
2087
|
+
}
|
|
2088
|
+
if (wantsStream) {
|
|
2089
|
+
res.writeHead(200, {
|
|
2090
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
2091
|
+
"Cache-Control": "no-store",
|
|
2092
|
+
});
|
|
2093
|
+
const resolvedModel = resolveWorkshopModel({
|
|
2094
|
+
sessionOverride: typeof body.model === "string" || body.model === null
|
|
2095
|
+
? body.model
|
|
2096
|
+
: undefined,
|
|
2097
|
+
settingsDefault: settingsDefault ?? null,
|
|
2098
|
+
});
|
|
2099
|
+
const writeFrame = async (frame) => {
|
|
2100
|
+
if (req.aborted || res.writableEnded || res.destroyed)
|
|
2101
|
+
return;
|
|
2102
|
+
const line = `${JSON.stringify(frame)}\n`;
|
|
2103
|
+
if (!res.write(line)) {
|
|
2104
|
+
await new Promise((resolve) => {
|
|
2105
|
+
const onDrain = () => {
|
|
2106
|
+
cleanup();
|
|
2107
|
+
resolve();
|
|
2108
|
+
};
|
|
2109
|
+
const onAbort = () => {
|
|
2110
|
+
cleanup();
|
|
2111
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
2112
|
+
res.destroy();
|
|
2113
|
+
}
|
|
2114
|
+
resolve();
|
|
2115
|
+
};
|
|
2116
|
+
const cleanup = () => {
|
|
2117
|
+
res.off("drain", onDrain);
|
|
2118
|
+
req.off("aborted", onAbort);
|
|
2119
|
+
req.off("close", onAbort);
|
|
2120
|
+
res.off("close", onAbort);
|
|
2121
|
+
res.off("error", onAbort);
|
|
2122
|
+
};
|
|
2123
|
+
res.once("drain", onDrain);
|
|
2124
|
+
req.once("aborted", onAbort);
|
|
2125
|
+
req.once("close", onAbort);
|
|
2126
|
+
res.once("close", onAbort);
|
|
2127
|
+
res.once("error", onAbort);
|
|
2128
|
+
if (req.aborted || res.writableEnded || res.destroyed) {
|
|
2129
|
+
onAbort();
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
let streamedDelta = false;
|
|
2135
|
+
let writeChain = Promise.resolve();
|
|
2136
|
+
let turnFinished = false;
|
|
2137
|
+
const enqueueFrame = (frame) => {
|
|
2138
|
+
writeChain = writeChain.then(() => writeFrame(frame));
|
|
2139
|
+
};
|
|
2140
|
+
const stopIfClientLeft = () => {
|
|
2141
|
+
if (turnFinished || res.writableEnded)
|
|
2142
|
+
return;
|
|
2143
|
+
void workshopChatRegistry.abortTurn(sessionId);
|
|
2144
|
+
};
|
|
2145
|
+
res.once("close", stopIfClientLeft);
|
|
2146
|
+
try {
|
|
2147
|
+
const turn = await runWorkshopChatTurn({
|
|
2148
|
+
...chatTurnBase,
|
|
2149
|
+
onDelta: (text) => {
|
|
2150
|
+
streamedDelta = true;
|
|
2151
|
+
enqueueFrame({ type: "delta", text });
|
|
2152
|
+
},
|
|
2153
|
+
onActivity: (update) => {
|
|
2154
|
+
enqueueFrame({ type: "activity", ...update });
|
|
2155
|
+
},
|
|
2156
|
+
onPointerChange: (frame) => {
|
|
2157
|
+
enqueueFrame({
|
|
2158
|
+
type: "pointer-change",
|
|
2159
|
+
buildId: frame.buildId,
|
|
2160
|
+
draft: frame.draft,
|
|
2161
|
+
});
|
|
2162
|
+
},
|
|
2163
|
+
});
|
|
2164
|
+
turnFinished = true;
|
|
2165
|
+
await writeChain;
|
|
2166
|
+
for (const frame of iterateWorkshopChatStreamFrames(turn, {
|
|
2167
|
+
chunkAssistantText: !streamedDelta,
|
|
2168
|
+
})) {
|
|
2169
|
+
await writeFrame(frame);
|
|
2170
|
+
}
|
|
2171
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
2172
|
+
res.end();
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
catch (err) {
|
|
2176
|
+
turnFinished = true;
|
|
2177
|
+
await writeChain.catch(() => undefined);
|
|
2178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2179
|
+
const errorEvent = { type: "error", message };
|
|
2180
|
+
await writeFrame({
|
|
2181
|
+
type: "event",
|
|
2182
|
+
event: errorEvent,
|
|
2183
|
+
});
|
|
2184
|
+
await writeFrame({
|
|
2185
|
+
type: "done",
|
|
2186
|
+
sessionId: body.sessionId,
|
|
2187
|
+
events: [errorEvent],
|
|
2188
|
+
draft: draftParsed,
|
|
2189
|
+
pending: null,
|
|
2190
|
+
autoApply: false,
|
|
2191
|
+
model: resolvedModel,
|
|
2192
|
+
buildId: workshopChatRegistry.getPinnedBuildId(body.sessionId),
|
|
2193
|
+
});
|
|
2194
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
2195
|
+
res.end();
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
return true;
|
|
2199
|
+
}
|
|
2200
|
+
let turn;
|
|
2201
|
+
try {
|
|
2202
|
+
turn = await runWorkshopChatTurn(chatTurnBase);
|
|
2203
|
+
}
|
|
2204
|
+
catch (err) {
|
|
2205
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
2206
|
+
err.code === "workshop_session_not_found") {
|
|
2207
|
+
json(res, 404, {
|
|
2208
|
+
error: err.message,
|
|
2209
|
+
code: err.code,
|
|
2210
|
+
sessionId: err.sessionId,
|
|
2211
|
+
});
|
|
2212
|
+
return true;
|
|
2213
|
+
}
|
|
2214
|
+
json(res, 400, {
|
|
2215
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2216
|
+
});
|
|
2217
|
+
return true;
|
|
2218
|
+
}
|
|
2219
|
+
json(res, 200, turn);
|
|
2220
|
+
return true;
|
|
2221
|
+
}
|
|
2222
|
+
if (method === "POST" &&
|
|
2223
|
+
pathname === "/api/workshop/disk-change") {
|
|
2224
|
+
let body;
|
|
2225
|
+
try {
|
|
2226
|
+
body = await readJsonBody(req);
|
|
2227
|
+
}
|
|
2228
|
+
catch {
|
|
2229
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
if (!isPlainObject(body)) {
|
|
2233
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2234
|
+
return true;
|
|
2235
|
+
}
|
|
2236
|
+
if (typeof body.pipelinePath !== "string" || !body.pipelinePath.trim()) {
|
|
2237
|
+
json(res, 400, { error: "pipelinePath is required" });
|
|
2238
|
+
return true;
|
|
2239
|
+
}
|
|
2240
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
2241
|
+
if ("ok" in draftParsed) {
|
|
2242
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
2243
|
+
return true;
|
|
2244
|
+
}
|
|
2245
|
+
const writeRoot = typeof body.project_root === "string"
|
|
2246
|
+
? body.project_root
|
|
2247
|
+
: undefined;
|
|
2248
|
+
let projectRoot;
|
|
2249
|
+
try {
|
|
2250
|
+
const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot);
|
|
2251
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
2252
|
+
projectRoot = ctx.projectRoot;
|
|
2253
|
+
}
|
|
2254
|
+
catch (err) {
|
|
2255
|
+
if (err instanceof CatalogPathError) {
|
|
2256
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
2257
|
+
return true;
|
|
2258
|
+
}
|
|
2259
|
+
throw err;
|
|
2260
|
+
}
|
|
2261
|
+
const relativePaths = draftPackageDiskRelativePaths({
|
|
2262
|
+
pipelinePath: body.pipelinePath,
|
|
2263
|
+
draft: draftParsed,
|
|
2264
|
+
taskPath: typeof body.taskPath === "string" ? body.taskPath : null,
|
|
2265
|
+
});
|
|
2266
|
+
const current = fingerprintPackageFiles(projectRoot, relativePaths);
|
|
2267
|
+
const baseline = isPlainObject(body.baseline) &&
|
|
2268
|
+
Object.values(body.baseline).every((v) => typeof v === "string")
|
|
2269
|
+
? body.baseline
|
|
2270
|
+
: null;
|
|
2271
|
+
const detection = detectDiskChange({ baseline, current });
|
|
2272
|
+
json(res, 200, {
|
|
2273
|
+
fingerprints: current,
|
|
2274
|
+
changed: detection.changed,
|
|
2275
|
+
changedPaths: detection.changedPaths,
|
|
2276
|
+
});
|
|
2277
|
+
return true;
|
|
2278
|
+
}
|
|
2279
|
+
if (method === "GET" && pathname === "/api/models") {
|
|
2280
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
2281
|
+
const result = await listModelsMultiProject({
|
|
2282
|
+
store,
|
|
2283
|
+
bootCwd: cwd,
|
|
2284
|
+
projectRootFilter: filter,
|
|
2285
|
+
});
|
|
2286
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
2287
|
+
result.items.length === 0) {
|
|
2288
|
+
json(res, 400, {
|
|
2289
|
+
error: result.root_errors[0].message,
|
|
2290
|
+
code: "unknown_project_root",
|
|
2291
|
+
root_errors: result.root_errors,
|
|
2292
|
+
});
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
json(res, 200, {
|
|
2296
|
+
models: result.models,
|
|
2297
|
+
entries: result.items,
|
|
2298
|
+
root_errors: result.root_errors,
|
|
2299
|
+
});
|
|
2300
|
+
return true;
|
|
2301
|
+
}
|
|
2302
|
+
if (await handleProviderRoutes(req, res, {
|
|
2303
|
+
cwd,
|
|
2304
|
+
readJsonBody,
|
|
2305
|
+
json,
|
|
2306
|
+
providerAuthContext,
|
|
2307
|
+
})) {
|
|
2308
|
+
return true;
|
|
2309
|
+
}
|
|
2310
|
+
if (await handleProjectMcpRoutes(req, res, {
|
|
2311
|
+
projectRoot: rootDir,
|
|
2312
|
+
json,
|
|
2313
|
+
})) {
|
|
2314
|
+
return true;
|
|
2315
|
+
}
|
|
2316
|
+
if (await handleTriggerRoutes(req, res, {
|
|
2317
|
+
cwd,
|
|
2318
|
+
manager,
|
|
2319
|
+
store,
|
|
2320
|
+
json,
|
|
2321
|
+
readJsonBody,
|
|
2322
|
+
readRawBody,
|
|
2323
|
+
auditLog,
|
|
2324
|
+
})) {
|
|
2325
|
+
return true;
|
|
2326
|
+
}
|
|
2327
|
+
if (method === "GET" && pathname === "/api/skills") {
|
|
2328
|
+
json(res, 200, await listSkills({ cwd, agentDir }));
|
|
2329
|
+
return true;
|
|
2330
|
+
}
|
|
2331
|
+
if (method === "GET" && pathname === "/api/extensions") {
|
|
2332
|
+
json(res, 200, await listExtensions({ cwd, agentDir }));
|
|
2333
|
+
return true;
|
|
2334
|
+
}
|
|
2335
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
2336
|
+
await handleApiHealth(req, res, boot);
|
|
2337
|
+
return true;
|
|
2338
|
+
}
|
|
2339
|
+
if (method === "GET" && pathname === "/api/settings") {
|
|
2340
|
+
const health = manager.getHealth();
|
|
2341
|
+
const credential = getCredentialSourceSettings(cwd);
|
|
2342
|
+
const factory = readFactorySettings(cwd);
|
|
2343
|
+
const defaultModel = await readWorkshopOpenModel(cwd);
|
|
2344
|
+
json(res, 200, {
|
|
2345
|
+
maxConcurrent: health.maxConcurrent,
|
|
2346
|
+
...credential,
|
|
2347
|
+
...(factory.workshopModel !== undefined
|
|
2348
|
+
? { workshopModel: factory.workshopModel }
|
|
2349
|
+
: {}),
|
|
2350
|
+
defaultModel,
|
|
2351
|
+
});
|
|
2352
|
+
return true;
|
|
2353
|
+
}
|
|
2354
|
+
if (method === "POST" && pathname === "/api/settings") {
|
|
2355
|
+
let body;
|
|
2356
|
+
try {
|
|
2357
|
+
body = await readJsonBody(req);
|
|
2358
|
+
}
|
|
2359
|
+
catch {
|
|
2360
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2361
|
+
return true;
|
|
2362
|
+
}
|
|
2363
|
+
const record = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
2364
|
+
? body
|
|
2365
|
+
: {};
|
|
2366
|
+
const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
|
|
2367
|
+
const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
|
|
2368
|
+
const hasWorkshopModel = Object.prototype.hasOwnProperty.call(record, "workshopModel");
|
|
2369
|
+
if (!hasMax && !hasCredentialSource && !hasWorkshopModel) {
|
|
2370
|
+
json(res, 400, {
|
|
2371
|
+
error: "maxConcurrent, credentialSource, or workshopModel is required",
|
|
2372
|
+
});
|
|
2373
|
+
return true;
|
|
2374
|
+
}
|
|
2375
|
+
let health = manager.getHealth();
|
|
2376
|
+
if (hasMax) {
|
|
2377
|
+
const n = parseSlotCount(record.maxConcurrent);
|
|
2378
|
+
if (n === undefined) {
|
|
2379
|
+
json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
|
|
2380
|
+
return true;
|
|
2381
|
+
}
|
|
2382
|
+
health = manager.setMaxConcurrent(n);
|
|
2383
|
+
}
|
|
2384
|
+
let credential = getCredentialSourceSettings(cwd);
|
|
2385
|
+
if (hasCredentialSource) {
|
|
2386
|
+
try {
|
|
2387
|
+
credential = setCredentialSource(cwd, record.credentialSource);
|
|
2388
|
+
}
|
|
2389
|
+
catch (err) {
|
|
2390
|
+
const mapped = mapProviderAuthError(err);
|
|
2391
|
+
json(res, mapped.status, mapped.body);
|
|
2392
|
+
return true;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
let workshopModel = readFactorySettings(cwd).workshopModel;
|
|
2396
|
+
if (hasWorkshopModel) {
|
|
2397
|
+
const parsed = parseWorkshopModelSetting(record.workshopModel);
|
|
2398
|
+
if (parsed === undefined) {
|
|
2399
|
+
json(res, 400, { error: INVALID_WORKSHOP_MODEL_MESSAGE });
|
|
1230
2400
|
return true;
|
|
1231
2401
|
}
|
|
2402
|
+
writeFactorySettings(cwd, { workshopModel: parsed });
|
|
2403
|
+
workshopModel = parsed;
|
|
1232
2404
|
}
|
|
1233
2405
|
json(res, 200, {
|
|
1234
2406
|
...health,
|
|
1235
2407
|
...credential,
|
|
2408
|
+
...(workshopModel !== undefined ? { workshopModel } : {}),
|
|
2409
|
+
defaultModel: await readWorkshopOpenModel(cwd),
|
|
1236
2410
|
});
|
|
1237
2411
|
return true;
|
|
1238
2412
|
}
|
|
@@ -1284,6 +2458,9 @@ export async function startUiServer(options) {
|
|
|
1284
2458
|
allowedHosts,
|
|
1285
2459
|
controlTokens,
|
|
1286
2460
|
getShutdown: () => shutdown,
|
|
2461
|
+
...(options.workshopOperatorHost
|
|
2462
|
+
? { workshopOperatorHost: options.workshopOperatorHost }
|
|
2463
|
+
: {}),
|
|
1287
2464
|
});
|
|
1288
2465
|
const envelope = await createHttpHost({
|
|
1289
2466
|
boot,
|