stageflow 0.1.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/agent/activity.d.ts +76 -0
- package/dist/agent/activity.js +89 -0
- package/dist/agent/activityObserver.d.ts +17 -0
- package/dist/agent/activityObserver.js +66 -0
- package/dist/agent/cursorExtension.d.ts +12 -0
- package/dist/agent/cursorExtension.js +88 -0
- package/dist/agent/cursorProvider.d.ts +14 -0
- package/dist/agent/cursorProvider.js +139 -0
- package/dist/agent/fakeAgent.d.ts +29 -0
- package/dist/agent/fakeAgent.js +295 -0
- package/dist/agent/oauthSessionManager.d.ts +33 -0
- package/dist/agent/oauthSessionManager.js +214 -0
- package/dist/agent/piAdapter.d.ts +128 -0
- package/dist/agent/piAdapter.js +967 -0
- package/dist/agent/port.d.ts +63 -0
- package/dist/agent/port.js +46 -0
- package/dist/agent/providerAuth.d.ts +101 -0
- package/dist/agent/providerAuth.js +282 -0
- package/dist/agent/providerSupport.d.ts +28 -0
- package/dist/agent/providerSupport.js +19 -0
- package/dist/cli/providersCommand.d.ts +11 -0
- package/dist/cli/providersCommand.js +275 -0
- package/dist/cli/terminalAuthInteraction.d.ts +8 -0
- package/dist/cli/terminalAuthInteraction.js +65 -0
- package/dist/cli/terminalSecret.d.ts +2 -0
- package/dist/cli/terminalSecret.js +67 -0
- package/dist/cli/validateCommand.d.ts +11 -0
- package/dist/cli/validateCommand.js +81 -0
- package/dist/cli/validateOutput.d.ts +8 -0
- package/dist/cli/validateOutput.js +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +273 -0
- package/dist/config/createPipeline.d.ts +31 -0
- package/dist/config/createPipeline.js +255 -0
- package/dist/config/createStage.d.ts +25 -0
- package/dist/config/createStage.js +181 -0
- package/dist/config/listConfig.d.ts +33 -0
- package/dist/config/listConfig.js +195 -0
- package/dist/config/listExtensions.d.ts +26 -0
- package/dist/config/listExtensions.js +50 -0
- package/dist/config/listSkills.d.ts +29 -0
- package/dist/config/listSkills.js +55 -0
- package/dist/config/loadOutcome.d.ts +27 -0
- package/dist/config/loadOutcome.js +6 -0
- package/dist/config/loadPipeline.d.ts +25 -0
- package/dist/config/loadPipeline.js +196 -0
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +127 -0
- package/dist/config/loadTask.d.ts +7 -0
- package/dist/config/loadTask.js +69 -0
- package/dist/config/readYamlObject.d.ts +1 -0
- package/dist/config/readYamlObject.js +5 -0
- package/dist/config/resolvePipelineDag.d.ts +12 -0
- package/dist/config/resolvePipelineDag.js +282 -0
- package/dist/config/validateCatalog.d.ts +36 -0
- package/dist/config/validateCatalog.js +322 -0
- package/dist/envelope/check.d.ts +3 -0
- package/dist/envelope/check.js +52 -0
- package/dist/envelope/payloadSchema.d.ts +10 -0
- package/dist/envelope/payloadSchema.js +91 -0
- package/dist/hitl/qaTrail.d.ts +31 -0
- package/dist/hitl/qaTrail.js +65 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7 -0
- package/dist/mcp/projectRun.d.ts +37 -0
- package/dist/mcp/projectRun.js +57 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +46 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +26 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +96 -0
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -0
- package/dist/prompt/priorEnvelope.js +5 -0
- package/dist/runstore/catalog.d.ts +8 -0
- package/dist/runstore/catalog.js +16 -0
- package/dist/runstore/createStore.d.ts +15 -0
- package/dist/runstore/createStore.js +23 -0
- package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
- package/dist/runstore/disk/DiskRunStore.js +238 -0
- package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
- package/dist/runstore/disk/catalogHelpers.js +102 -0
- package/dist/runstore/layout.d.ts +22 -0
- package/dist/runstore/layout.js +58 -0
- package/dist/runstore/paths.d.ts +7 -0
- package/dist/runstore/paths.js +60 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
- package/dist/runstore/pipelineDagSnapshot.js +43 -0
- package/dist/runstore/port.d.ts +146 -0
- package/dist/runstore/port.js +28 -0
- package/dist/runstore/runProjection.d.ts +6 -0
- package/dist/runstore/runProjection.js +102 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
- package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
- package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
- package/dist/runstore/sqlite/schema.d.ts +1 -0
- package/dist/runstore/sqlite/schema.js +59 -0
- package/dist/runstore/stageExecution.d.ts +13 -0
- package/dist/runstore/stageExecution.js +22 -0
- package/dist/runstore/stageSnapshot.d.ts +2 -0
- package/dist/runstore/stageSnapshot.js +46 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
- package/dist/runstore/syntheticStageSnapshot.js +10 -0
- package/dist/runstore/trackProjection.d.ts +7 -0
- package/dist/runstore/trackProjection.js +94 -0
- package/dist/runstore/workspaceLayout.d.ts +31 -0
- package/dist/runstore/workspaceLayout.js +172 -0
- package/dist/runtime/answerResume.d.ts +16 -0
- package/dist/runtime/answerResume.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +16 -0
- package/dist/runtime/credentialBinding.js +80 -0
- package/dist/runtime/dagTraversal.d.ts +3 -0
- package/dist/runtime/dagTraversal.js +26 -0
- package/dist/runtime/envelopeRouting.d.ts +20 -0
- package/dist/runtime/envelopeRouting.js +59 -0
- package/dist/runtime/hitlSeams.d.ts +38 -0
- package/dist/runtime/hitlSeams.js +2 -0
- package/dist/runtime/pipelineRunner.d.ts +77 -0
- package/dist/runtime/pipelineRunner.js +141 -0
- package/dist/runtime/pipelineScheduler.d.ts +90 -0
- package/dist/runtime/pipelineScheduler.js +494 -0
- package/dist/runtime/pipelineValidationError.d.ts +6 -0
- package/dist/runtime/pipelineValidationError.js +8 -0
- package/dist/runtime/resumeReconstruct.d.ts +33 -0
- package/dist/runtime/resumeReconstruct.js +144 -0
- package/dist/runtime/runManager.d.ts +129 -0
- package/dist/runtime/runManager.js +818 -0
- package/dist/runtime/runRetryCoordinator.d.ts +85 -0
- package/dist/runtime/runRetryCoordinator.js +372 -0
- package/dist/runtime/settingsFile.d.ts +17 -0
- package/dist/runtime/settingsFile.js +88 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
- package/dist/runtime/stageAttemptBootstrap.js +82 -0
- package/dist/runtime/stageAttemptContext.d.ts +11 -0
- package/dist/runtime/stageAttemptContext.js +26 -0
- package/dist/runtime/stageConcurrency.d.ts +10 -0
- package/dist/runtime/stageConcurrency.js +51 -0
- package/dist/runtime/stageHitl.d.ts +95 -0
- package/dist/runtime/stageHitl.js +198 -0
- package/dist/runtime/stageProcessLauncher.d.ts +45 -0
- package/dist/runtime/stageProcessLauncher.js +199 -0
- package/dist/runtime/stageRecovery.d.ts +10 -0
- package/dist/runtime/stageRecovery.js +43 -0
- package/dist/runtime/stageRoots.d.ts +22 -0
- package/dist/runtime/stageRoots.js +93 -0
- package/dist/runtime/stageRunner.d.ts +59 -0
- package/dist/runtime/stageRunner.js +221 -0
- package/dist/runtime/stageWorker.d.ts +6 -0
- package/dist/runtime/stageWorker.js +109 -0
- package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
- package/dist/runtime/stageWorkerProtocol.js +16 -0
- package/dist/runtime/taskInput.d.ts +15 -0
- package/dist/runtime/taskInput.js +38 -0
- package/dist/server/http.d.ts +31 -0
- package/dist/server/http.js +561 -0
- package/dist/server/providerRoutes.d.ts +15 -0
- package/dist/server/providerRoutes.js +186 -0
- package/dist/tools/askOperator.d.ts +185 -0
- package/dist/tools/askOperator.js +409 -0
- package/dist/tools/emitStageEnvelope.d.ts +42 -0
- package/dist/tools/emitStageEnvelope.js +61 -0
- package/dist/tools/writeStageArtifact.d.ts +26 -0
- package/dist/tools/writeStageArtifact.js +103 -0
- package/dist/types/envelope.d.ts +12 -0
- package/dist/types/envelope.js +6 -0
- package/dist/types/pipeline.d.ts +25 -0
- package/dist/types/pipeline.js +1 -0
- package/dist/types/stage.d.ts +12 -0
- package/dist/types/stage.js +6 -0
- package/dist/types/task.d.ts +7 -0
- package/dist/types/task.js +1 -0
- package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
- package/dist/ui/assets/index-DCsDopak.css +1 -0
- package/dist/ui/index.html +21 -0
- package/package.json +72 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { PiAgentAdapter } from "./agent/piAdapter.js";
|
|
5
|
+
import { PROVIDERS_USAGE, runProvidersCommand } from "./cli/providersCommand.js";
|
|
6
|
+
import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
|
|
7
|
+
import { exitCodeForValidation, formatValidationHuman, } from "./cli/validateOutput.js";
|
|
8
|
+
import { createRunStore } from "./runstore/createStore.js";
|
|
9
|
+
import { PipelineValidationError } from "./runtime/pipelineRunner.js";
|
|
10
|
+
import { RunManager } from "./runtime/runManager.js";
|
|
11
|
+
import { readStageExecutionMode } from "./runtime/stageConcurrency.js";
|
|
12
|
+
import { exitForOutcome, runStageWorker } from "./runtime/stageWorker.js";
|
|
13
|
+
import { SF_STAGE_WORKER } from "./runtime/stageWorkerProtocol.js";
|
|
14
|
+
import { DEFAULT_PORT, startUiServer } from "./server/http.js";
|
|
15
|
+
const USAGE = `Usage:
|
|
16
|
+
sf run --task <path> --pipeline <name-or-path> [--checkout <path>]
|
|
17
|
+
sf validate [--pipeline <name-or-path>] [--strict] [--json]
|
|
18
|
+
sf ui [--port ${DEFAULT_PORT}]
|
|
19
|
+
sf providers list
|
|
20
|
+
sf providers status [--provider <id>]
|
|
21
|
+
sf providers detect
|
|
22
|
+
sf providers source [get | set <pi_home|sf_owned>]
|
|
23
|
+
sf providers login <providerId> [--type api_key|oauth] [--api-key-env <VAR>]
|
|
24
|
+
sf providers logout <providerId>
|
|
25
|
+
sf --help
|
|
26
|
+
|
|
27
|
+
Stageflow (sf) runs YAML-defined automatic stage pipelines.
|
|
28
|
+
|
|
29
|
+
Store backend: SF_STORE=sqlite only. SF_STORE=disk is rejected; disk-era .stageflow/runs trees import when the SQLite store is empty. Data under .stageflow/.
|
|
30
|
+
|
|
31
|
+
${VALIDATE_USAGE}
|
|
32
|
+
|
|
33
|
+
${PROVIDERS_USAGE}`;
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const args = argv.slice(2);
|
|
36
|
+
if (args.length === 0) {
|
|
37
|
+
return { help: true };
|
|
38
|
+
}
|
|
39
|
+
if ((args[0] === "--help" || args[0] === "-h") &&
|
|
40
|
+
args.length === 1) {
|
|
41
|
+
return { help: true };
|
|
42
|
+
}
|
|
43
|
+
const command = args[0];
|
|
44
|
+
if (command === "providers" || command === "validate") {
|
|
45
|
+
return { help: false, command };
|
|
46
|
+
}
|
|
47
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
48
|
+
return { help: true };
|
|
49
|
+
}
|
|
50
|
+
let task;
|
|
51
|
+
let pipeline;
|
|
52
|
+
let port;
|
|
53
|
+
let checkout;
|
|
54
|
+
for (let i = 1; i < args.length; i++) {
|
|
55
|
+
if (args[i] === "--task") {
|
|
56
|
+
task = args[++i];
|
|
57
|
+
}
|
|
58
|
+
else if (args[i] === "--pipeline") {
|
|
59
|
+
pipeline = args[++i];
|
|
60
|
+
}
|
|
61
|
+
else if (args[i] === "--checkout") {
|
|
62
|
+
const raw = args[++i];
|
|
63
|
+
if (raw === undefined || raw.length === 0) {
|
|
64
|
+
throw new Error("Missing value for --checkout");
|
|
65
|
+
}
|
|
66
|
+
checkout = raw;
|
|
67
|
+
}
|
|
68
|
+
else if (args[i] === "--port") {
|
|
69
|
+
const raw = args[++i];
|
|
70
|
+
port = Number(raw);
|
|
71
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
72
|
+
throw new Error(`Invalid --port: ${raw}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { help: false, command, task, pipeline, port, checkout };
|
|
77
|
+
}
|
|
78
|
+
function parseRunStageArgs(argv) {
|
|
79
|
+
let runId;
|
|
80
|
+
let stageId;
|
|
81
|
+
let mode;
|
|
82
|
+
let resumeAnswer;
|
|
83
|
+
let attempt;
|
|
84
|
+
let sessionFilePath;
|
|
85
|
+
let operatorAgentDir;
|
|
86
|
+
for (let i = 0; i < argv.length; i++) {
|
|
87
|
+
if (argv[i] === "--run-id") {
|
|
88
|
+
runId = argv[++i];
|
|
89
|
+
}
|
|
90
|
+
else if (argv[i] === "--stage-id") {
|
|
91
|
+
stageId = argv[++i];
|
|
92
|
+
}
|
|
93
|
+
else if (argv[i] === "--mode") {
|
|
94
|
+
const raw = argv[++i];
|
|
95
|
+
if (raw !== "run" && raw !== "resume") {
|
|
96
|
+
throw new Error("--mode must be run or resume");
|
|
97
|
+
}
|
|
98
|
+
mode = raw;
|
|
99
|
+
}
|
|
100
|
+
else if (argv[i] === "--resume-answer") {
|
|
101
|
+
const raw = argv[++i];
|
|
102
|
+
if (raw === undefined) {
|
|
103
|
+
throw new Error("Missing value for --resume-answer");
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
resumeAnswer = JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error("--resume-answer must be valid JSON");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (argv[i] === "--attempt") {
|
|
113
|
+
const raw = argv[++i];
|
|
114
|
+
if (raw === undefined) {
|
|
115
|
+
throw new Error("Missing value for --attempt");
|
|
116
|
+
}
|
|
117
|
+
const parsed = Number.parseInt(raw, 10);
|
|
118
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
119
|
+
throw new Error("--attempt must be a positive integer");
|
|
120
|
+
}
|
|
121
|
+
attempt = parsed;
|
|
122
|
+
}
|
|
123
|
+
else if (argv[i] === "--session-file") {
|
|
124
|
+
sessionFilePath = argv[++i];
|
|
125
|
+
if (sessionFilePath === undefined) {
|
|
126
|
+
throw new Error("Missing value for --session-file");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (argv[i] === "--operator-agent-dir") {
|
|
130
|
+
operatorAgentDir = argv[++i];
|
|
131
|
+
if (operatorAgentDir === undefined) {
|
|
132
|
+
throw new Error("Missing value for --operator-agent-dir");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!runId || !stageId) {
|
|
137
|
+
throw new Error("Missing --run-id and/or --stage-id");
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
runId,
|
|
141
|
+
stageId,
|
|
142
|
+
mode,
|
|
143
|
+
resumeAnswer,
|
|
144
|
+
attempt,
|
|
145
|
+
sessionFilePath,
|
|
146
|
+
...(operatorAgentDir !== undefined
|
|
147
|
+
? { operatorCatalog: { cwd: process.cwd(), agentDir: operatorAgentDir } }
|
|
148
|
+
: {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async function handleInternalRunStage(argv) {
|
|
152
|
+
if (process.env[SF_STAGE_WORKER] !== "1") {
|
|
153
|
+
process.env[SF_STAGE_WORKER] = "1";
|
|
154
|
+
}
|
|
155
|
+
const parsed = parseRunStageArgs(argv);
|
|
156
|
+
const outcome = await runStageWorker({
|
|
157
|
+
runId: parsed.runId,
|
|
158
|
+
stageId: parsed.stageId,
|
|
159
|
+
rootDir: process.cwd(),
|
|
160
|
+
mode: parsed.mode ??
|
|
161
|
+
(parsed.resumeAnswer !== undefined ? "resume" : "run"),
|
|
162
|
+
resumeAnswer: parsed.resumeAnswer,
|
|
163
|
+
attempt: parsed.attempt,
|
|
164
|
+
sessionFilePath: parsed.sessionFilePath,
|
|
165
|
+
operatorCatalog: parsed.operatorCatalog,
|
|
166
|
+
});
|
|
167
|
+
exitForOutcome(outcome);
|
|
168
|
+
}
|
|
169
|
+
function openBrowser(url) {
|
|
170
|
+
const platform = process.platform;
|
|
171
|
+
if (platform === "darwin") {
|
|
172
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
173
|
+
}
|
|
174
|
+
else if (platform === "win32") {
|
|
175
|
+
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function main(argv) {
|
|
182
|
+
try {
|
|
183
|
+
const parsed = parseArgs(argv);
|
|
184
|
+
if (parsed.help) {
|
|
185
|
+
console.log(USAGE);
|
|
186
|
+
return argv.slice(2).length === 0 ? 1 : 0;
|
|
187
|
+
}
|
|
188
|
+
const rootDir = process.cwd();
|
|
189
|
+
if (parsed.command === "validate") {
|
|
190
|
+
return runValidateCommand(argv.slice(3), { cwd: rootDir });
|
|
191
|
+
}
|
|
192
|
+
if (parsed.command === "providers") {
|
|
193
|
+
return runProvidersCommand(argv.slice(3), rootDir);
|
|
194
|
+
}
|
|
195
|
+
const store = createRunStore({ rootDir });
|
|
196
|
+
if (parsed.command === "ui") {
|
|
197
|
+
const { url, mcpUrl } = await startUiServer({
|
|
198
|
+
agent: new PiAgentAdapter(),
|
|
199
|
+
store,
|
|
200
|
+
port: parsed.port,
|
|
201
|
+
});
|
|
202
|
+
console.log(`Operator console: ${url}`);
|
|
203
|
+
console.log(`MCP endpoint: ${mcpUrl}`);
|
|
204
|
+
openBrowser(url);
|
|
205
|
+
await new Promise(() => undefined);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
if (parsed.command === "internal") {
|
|
209
|
+
const subArgs = argv.slice(3);
|
|
210
|
+
const subcommand = subArgs[0];
|
|
211
|
+
if (subcommand !== "run-stage") {
|
|
212
|
+
console.error(`Unknown internal command: ${subcommand ?? "(none)"}`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
return handleInternalRunStage(subArgs.slice(1));
|
|
216
|
+
}
|
|
217
|
+
if (parsed.command !== "run") {
|
|
218
|
+
console.error(`Unknown command: ${parsed.command}`);
|
|
219
|
+
console.error(USAGE);
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
if (!parsed.task || !parsed.pipeline) {
|
|
223
|
+
console.error("Missing --task and/or --pipeline");
|
|
224
|
+
console.error(USAGE);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
const manager = new RunManager({
|
|
228
|
+
agent: new PiAgentAdapter(),
|
|
229
|
+
store,
|
|
230
|
+
cwd: rootDir,
|
|
231
|
+
operatorCatalog: { cwd: rootDir, agentDir: getAgentDir() },
|
|
232
|
+
executionMode: readStageExecutionMode(process.env, "process"),
|
|
233
|
+
});
|
|
234
|
+
const started = await manager.startRun({
|
|
235
|
+
task: parsed.task,
|
|
236
|
+
pipeline: parsed.pipeline,
|
|
237
|
+
checkoutOverride: parsed.checkout,
|
|
238
|
+
});
|
|
239
|
+
if (!started.ok) {
|
|
240
|
+
const code = started.code ?? "error";
|
|
241
|
+
console.error(`${code}: ${started.reason}`);
|
|
242
|
+
if (started.conflictingRunId !== undefined) {
|
|
243
|
+
console.error(`conflictingRunId: ${started.conflictingRunId}`);
|
|
244
|
+
}
|
|
245
|
+
if (started.conflictingCheckout !== undefined) {
|
|
246
|
+
console.error(`conflictingCheckout: ${started.conflictingCheckout}`);
|
|
247
|
+
}
|
|
248
|
+
return started.status ?? 1;
|
|
249
|
+
}
|
|
250
|
+
const result = await started.done;
|
|
251
|
+
if (result.ok) {
|
|
252
|
+
console.log(`Pipeline succeeded. Run folder: ${result.runDir}`);
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
console.error(`Pipeline failed: ${result.reason}`);
|
|
256
|
+
console.error(`Run folder: ${result.runDir}`);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
if (err instanceof PipelineValidationError) {
|
|
261
|
+
console.error(formatValidationHuman(err.result));
|
|
262
|
+
return exitCodeForValidation(err.result);
|
|
263
|
+
}
|
|
264
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
main(process.argv)
|
|
269
|
+
.then((code) => process.exit(code))
|
|
270
|
+
.catch((err) => {
|
|
271
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PipelineListing } from "./listConfig.js";
|
|
2
|
+
export type CreatePipelineStageRef = {
|
|
3
|
+
id: string;
|
|
4
|
+
needs?: string;
|
|
5
|
+
};
|
|
6
|
+
export type CreatePipelineInput = {
|
|
7
|
+
id: string;
|
|
8
|
+
stages: CreatePipelineStageRef[] | string[];
|
|
9
|
+
};
|
|
10
|
+
export type CreatePipelineParseError = {
|
|
11
|
+
ok: false;
|
|
12
|
+
status: 400;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
export type CreatePipelineResult = {
|
|
16
|
+
ok: true;
|
|
17
|
+
pipeline: PipelineListing;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
status: 400 | 409 | 422 | 500;
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function normalizeCreatePipelineStages(stages: CreatePipelineStageRef[] | string[]): CreatePipelineStageRef[];
|
|
24
|
+
export declare function parseCreatePipelineBody(body: unknown): CreatePipelineInput | CreatePipelineParseError;
|
|
25
|
+
export declare function pipelineConfigToYaml(pipeline: {
|
|
26
|
+
id: string;
|
|
27
|
+
stages: CreatePipelineStageRef[];
|
|
28
|
+
}, options: {
|
|
29
|
+
format: "linear" | "dag";
|
|
30
|
+
}): string;
|
|
31
|
+
export declare function createPipeline(cwd: string, input: CreatePipelineInput): Promise<CreatePipelineResult>;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { access, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { STAGE_ID_PATTERN } from "./createStage.js";
|
|
4
|
+
import { loadPipeline } from "./loadPipeline.js";
|
|
5
|
+
import { loadStage } from "./loadStage.js";
|
|
6
|
+
import { readYamlObject } from "./readYamlObject.js";
|
|
7
|
+
import { resolvePipelineDag } from "./resolvePipelineDag.js";
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function validatePipelineId(id) {
|
|
12
|
+
if (id.length === 0 || id.length > 64) {
|
|
13
|
+
return "id must be 1-64 characters";
|
|
14
|
+
}
|
|
15
|
+
if (!STAGE_ID_PATTERN.test(id)) {
|
|
16
|
+
return "id must be lowercase kebab-case";
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function normalizeCreatePipelineStages(stages) {
|
|
21
|
+
if (stages.length > 0 && typeof stages[0] === "string") {
|
|
22
|
+
return stages.map((id) => ({ id }));
|
|
23
|
+
}
|
|
24
|
+
return stages;
|
|
25
|
+
}
|
|
26
|
+
function parseStageEntry(entry, index) {
|
|
27
|
+
if (!isPlainObject(entry)) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
status: 400,
|
|
31
|
+
error: `stages[${index}] must be a string or object with id`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (typeof entry.id !== "string") {
|
|
35
|
+
return { ok: false, status: 400, error: `stages[${index}].id is required` };
|
|
36
|
+
}
|
|
37
|
+
const ref = { id: entry.id };
|
|
38
|
+
if (entry.needs !== undefined) {
|
|
39
|
+
if (typeof entry.needs !== "string") {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
status: 400,
|
|
43
|
+
error: `stages[${index}].needs must be a string`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
ref.needs = entry.needs;
|
|
47
|
+
}
|
|
48
|
+
return ref;
|
|
49
|
+
}
|
|
50
|
+
export function parseCreatePipelineBody(body) {
|
|
51
|
+
if (!isPlainObject(body)) {
|
|
52
|
+
return { ok: false, status: 400, error: "Request body must be an object" };
|
|
53
|
+
}
|
|
54
|
+
if (typeof body.id !== "string") {
|
|
55
|
+
return { ok: false, status: 400, error: "id is required" };
|
|
56
|
+
}
|
|
57
|
+
const idError = validatePipelineId(body.id);
|
|
58
|
+
if (idError) {
|
|
59
|
+
return { ok: false, status: 400, error: idError };
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(body.stages)) {
|
|
62
|
+
return { ok: false, status: 400, error: "stages must be an array" };
|
|
63
|
+
}
|
|
64
|
+
if (body.stages.length === 0) {
|
|
65
|
+
return { ok: false, status: 400, error: "stages must be a non-empty array" };
|
|
66
|
+
}
|
|
67
|
+
let hasString = false;
|
|
68
|
+
let hasObject = false;
|
|
69
|
+
for (const entry of body.stages) {
|
|
70
|
+
if (typeof entry === "string") {
|
|
71
|
+
hasString = true;
|
|
72
|
+
}
|
|
73
|
+
else if (isPlainObject(entry)) {
|
|
74
|
+
hasObject = true;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return { ok: false, status: 400, error: "stages must be an array of strings" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (hasString && hasObject) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
status: 400,
|
|
84
|
+
error: "stages must be either all strings or all objects",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (hasString) {
|
|
88
|
+
return {
|
|
89
|
+
id: body.id,
|
|
90
|
+
stages: body.stages,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const refs = [];
|
|
94
|
+
for (let index = 0; index < body.stages.length; index++) {
|
|
95
|
+
const parsed = parseStageEntry(body.stages[index], index);
|
|
96
|
+
if ("ok" in parsed) {
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
refs.push(parsed);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
id: body.id,
|
|
103
|
+
stages: refs,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function pipelineConfigToYaml(pipeline, options) {
|
|
107
|
+
const lines = [`id: ${pipeline.id}`, "stages:"];
|
|
108
|
+
if (options.format === "linear") {
|
|
109
|
+
for (const stage of pipeline.stages) {
|
|
110
|
+
lines.push(` - ${stage.id}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
for (const stage of pipeline.stages) {
|
|
115
|
+
lines.push(` - id: ${stage.id}`);
|
|
116
|
+
if (stage.needs) {
|
|
117
|
+
lines.push(` needs: ${stage.needs}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
lines.push("");
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
async function fileExists(filePath) {
|
|
125
|
+
try {
|
|
126
|
+
await access(filePath);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function findPipelineIdCollision(cwd, id, targetPath) {
|
|
134
|
+
if (await fileExists(targetPath)) {
|
|
135
|
+
return path.relative(cwd, targetPath);
|
|
136
|
+
}
|
|
137
|
+
const dir = path.join(cwd, "pipelines");
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (!entry.isFile() || !entry.name.endsWith(".yaml"))
|
|
147
|
+
continue;
|
|
148
|
+
const filePath = path.join(dir, entry.name);
|
|
149
|
+
if (filePath === targetPath)
|
|
150
|
+
continue;
|
|
151
|
+
try {
|
|
152
|
+
const raw = await readYamlObject(filePath);
|
|
153
|
+
if (raw?.id === id) {
|
|
154
|
+
return path.relative(cwd, filePath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// ignore unreadable files for collision scan
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
async function validateStageReferences(cwd, stages) {
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
for (const stage of stages) {
|
|
166
|
+
if (seen.has(stage.id)) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
status: 422,
|
|
170
|
+
error: "Pipeline stages must not contain duplicates",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
seen.add(stage.id);
|
|
174
|
+
}
|
|
175
|
+
const stagesDir = path.join(cwd, "stages");
|
|
176
|
+
for (const stage of stages) {
|
|
177
|
+
const stagePath = path.join(stagesDir, `${stage.id}.yaml`);
|
|
178
|
+
try {
|
|
179
|
+
await loadStage(stagePath);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
status: 422,
|
|
185
|
+
error: "one or more selected Stages no longer exist",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { ok: true };
|
|
190
|
+
}
|
|
191
|
+
function buildResolverInput(input, normalized) {
|
|
192
|
+
if (input.stages.length > 0 && typeof input.stages[0] === "string") {
|
|
193
|
+
return input.stages;
|
|
194
|
+
}
|
|
195
|
+
return normalized.map((ref) => ref.needs ? { id: ref.id, needs: ref.needs } : { id: ref.id });
|
|
196
|
+
}
|
|
197
|
+
function usesDagYaml(input, normalized) {
|
|
198
|
+
if (input.stages.length > 0 && typeof input.stages[0] !== "string") {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return normalized.some((ref) => ref.needs !== undefined);
|
|
202
|
+
}
|
|
203
|
+
export async function createPipeline(cwd, input) {
|
|
204
|
+
const normalized = normalizeCreatePipelineStages(input.stages);
|
|
205
|
+
const stageValidation = await validateStageReferences(cwd, normalized);
|
|
206
|
+
if (!stageValidation.ok) {
|
|
207
|
+
return stageValidation;
|
|
208
|
+
}
|
|
209
|
+
const pipelinesDir = path.join(cwd, "pipelines");
|
|
210
|
+
const filePath = path.join(pipelinesDir, `${input.id}.yaml`);
|
|
211
|
+
const relPath = path.relative(cwd, filePath);
|
|
212
|
+
const collision = await findPipelineIdCollision(cwd, input.id, filePath);
|
|
213
|
+
if (collision) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
status: 409,
|
|
217
|
+
error: `Pipeline id already exists (${collision})`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const resolverInput = buildResolverInput(input, normalized);
|
|
221
|
+
try {
|
|
222
|
+
resolvePipelineDag(resolverInput, { pipelineId: input.id, path: relPath });
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
226
|
+
return { ok: false, status: 422, error: message };
|
|
227
|
+
}
|
|
228
|
+
const yamlFormat = usesDagYaml(input, normalized) ? "dag" : "linear";
|
|
229
|
+
try {
|
|
230
|
+
await mkdir(pipelinesDir, { recursive: true });
|
|
231
|
+
await writeFile(filePath, pipelineConfigToYaml({ id: input.id, stages: normalized }, { format: yamlFormat }), "utf8");
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
235
|
+
return { ok: false, status: 500, error: message };
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const loaded = await loadPipeline(filePath, { cwd });
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
pipeline: {
|
|
242
|
+
path: relPath,
|
|
243
|
+
id: loaded.pipeline.id,
|
|
244
|
+
stages: loaded.stages.map((stage) => ({
|
|
245
|
+
id: stage.id,
|
|
246
|
+
...(stage.gate_kinds ? { gate_kinds: stage.gate_kinds } : {}),
|
|
247
|
+
})),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
253
|
+
return { ok: false, status: 500, error: message };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type StageGateKind } from "../types/stage.js";
|
|
2
|
+
import type { ValidStageListing } from "./listConfig.js";
|
|
3
|
+
export declare const STAGE_ID_PATTERN: RegExp;
|
|
4
|
+
export type CreateStageInput = {
|
|
5
|
+
id: string;
|
|
6
|
+
system_prompt: string;
|
|
7
|
+
model: string;
|
|
8
|
+
gate_kinds?: StageGateKind[];
|
|
9
|
+
};
|
|
10
|
+
export type CreateStageParseError = {
|
|
11
|
+
ok: false;
|
|
12
|
+
status: 400;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
export type CreateStageResult = {
|
|
16
|
+
ok: true;
|
|
17
|
+
stage: ValidStageListing;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
status: 400 | 409 | 500;
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function parseCreateStageBody(body: unknown): CreateStageInput | CreateStageParseError;
|
|
24
|
+
export declare function stageConfigToYaml(stage: CreateStageInput): string;
|
|
25
|
+
export declare function createStage(cwd: string, input: CreateStageInput): Promise<CreateStageResult>;
|