codepiper 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/.env.example +28 -0
- package/CHANGELOG.md +10 -0
- package/LEGAL_NOTICE.md +39 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/package.json +90 -0
- package/packages/cli/package.json +13 -0
- package/packages/cli/src/commands/analytics.ts +157 -0
- package/packages/cli/src/commands/attach.ts +299 -0
- package/packages/cli/src/commands/audit.ts +50 -0
- package/packages/cli/src/commands/auth.ts +261 -0
- package/packages/cli/src/commands/daemon.ts +162 -0
- package/packages/cli/src/commands/doctor.ts +303 -0
- package/packages/cli/src/commands/env-set.ts +162 -0
- package/packages/cli/src/commands/hook-forward.ts +268 -0
- package/packages/cli/src/commands/keys.ts +77 -0
- package/packages/cli/src/commands/kill.ts +19 -0
- package/packages/cli/src/commands/logs.ts +419 -0
- package/packages/cli/src/commands/model.ts +172 -0
- package/packages/cli/src/commands/policy-set.ts +185 -0
- package/packages/cli/src/commands/policy.ts +227 -0
- package/packages/cli/src/commands/providers.ts +114 -0
- package/packages/cli/src/commands/resize.ts +34 -0
- package/packages/cli/src/commands/send.ts +184 -0
- package/packages/cli/src/commands/sessions.ts +202 -0
- package/packages/cli/src/commands/slash.ts +92 -0
- package/packages/cli/src/commands/start.ts +243 -0
- package/packages/cli/src/commands/stop.ts +19 -0
- package/packages/cli/src/commands/tail.ts +137 -0
- package/packages/cli/src/commands/workflow.ts +786 -0
- package/packages/cli/src/commands/workspace.ts +127 -0
- package/packages/cli/src/lib/api.ts +78 -0
- package/packages/cli/src/lib/args.ts +72 -0
- package/packages/cli/src/lib/format.ts +93 -0
- package/packages/cli/src/main.ts +563 -0
- package/packages/core/package.json +7 -0
- package/packages/core/src/config.ts +30 -0
- package/packages/core/src/errors.ts +38 -0
- package/packages/core/src/eventBus.ts +56 -0
- package/packages/core/src/eventBusAdapter.ts +143 -0
- package/packages/core/src/index.ts +10 -0
- package/packages/core/src/sqliteEventBus.ts +336 -0
- package/packages/core/src/types.ts +63 -0
- package/packages/daemon/package.json +11 -0
- package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
- package/packages/daemon/src/api/authRoutes.ts +344 -0
- package/packages/daemon/src/api/bodyLimit.ts +133 -0
- package/packages/daemon/src/api/envSetRoutes.ts +170 -0
- package/packages/daemon/src/api/gitRoutes.ts +409 -0
- package/packages/daemon/src/api/hooks.ts +588 -0
- package/packages/daemon/src/api/inputPolicy.ts +249 -0
- package/packages/daemon/src/api/notificationRoutes.ts +532 -0
- package/packages/daemon/src/api/policyRoutes.ts +234 -0
- package/packages/daemon/src/api/policySetRoutes.ts +445 -0
- package/packages/daemon/src/api/routeUtils.ts +28 -0
- package/packages/daemon/src/api/routes.ts +1004 -0
- package/packages/daemon/src/api/server.ts +1388 -0
- package/packages/daemon/src/api/settingsRoutes.ts +367 -0
- package/packages/daemon/src/api/sqliteErrors.ts +47 -0
- package/packages/daemon/src/api/stt.ts +143 -0
- package/packages/daemon/src/api/terminalRoutes.ts +200 -0
- package/packages/daemon/src/api/validation.ts +287 -0
- package/packages/daemon/src/api/validationRoutes.ts +174 -0
- package/packages/daemon/src/api/workflowRoutes.ts +567 -0
- package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
- package/packages/daemon/src/api/ws.ts +1588 -0
- package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
- package/packages/daemon/src/auth/authMiddleware.ts +305 -0
- package/packages/daemon/src/auth/authService.ts +496 -0
- package/packages/daemon/src/auth/rateLimiter.ts +137 -0
- package/packages/daemon/src/config/pricing.ts +79 -0
- package/packages/daemon/src/crypto/encryption.ts +196 -0
- package/packages/daemon/src/db/db.ts +2745 -0
- package/packages/daemon/src/db/index.ts +16 -0
- package/packages/daemon/src/db/migrations.ts +182 -0
- package/packages/daemon/src/db/policyDb.ts +349 -0
- package/packages/daemon/src/db/schema.sql +408 -0
- package/packages/daemon/src/db/workflowDb.ts +464 -0
- package/packages/daemon/src/git/gitUtils.ts +544 -0
- package/packages/daemon/src/index.ts +6 -0
- package/packages/daemon/src/main.ts +525 -0
- package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
- package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
- package/packages/daemon/src/providers/registry.ts +111 -0
- package/packages/daemon/src/providers/types.ts +82 -0
- package/packages/daemon/src/sessions/auditLogger.ts +103 -0
- package/packages/daemon/src/sessions/policyEngine.ts +165 -0
- package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
- package/packages/daemon/src/sessions/policyTypes.ts +94 -0
- package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
- package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
- package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
- package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
- package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
- package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
- package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
- package/packages/daemon/src/workflows/contextManager.ts +83 -0
- package/packages/daemon/src/workflows/index.ts +31 -0
- package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
- package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
- package/packages/daemon/src/workflows/workflowParser.ts +217 -0
- package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
- package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
- package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
- package/packages/providers/claude-code/package.json +11 -0
- package/packages/providers/claude-code/src/index.ts +7 -0
- package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
- package/packages/providers/claude-code/src/provider.ts +311 -0
- package/packages/web/dist/android-chrome-192x192.png +0 -0
- package/packages/web/dist/android-chrome-512x512.png +0 -0
- package/packages/web/dist/apple-touch-icon.png +0 -0
- package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
- package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
- package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
- package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
- package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
- package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
- package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
- package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
- package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
- package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
- package/packages/web/dist/assets/index-hgphORiw.js +204 -0
- package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
- package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
- package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
- package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
- package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
- package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
- package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
- package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
- package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
- package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
- package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
- package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
- package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/icon.svg +1 -0
- package/packages/web/dist/index.html +29 -0
- package/packages/web/dist/manifest.json +29 -0
- package/packages/web/dist/og-image.png +0 -0
- package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
- package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
- package/packages/web/dist/originals/apple-touch-icon.png +0 -0
- package/packages/web/dist/originals/favicon.ico +0 -0
- package/packages/web/dist/piper.svg +1 -0
- package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
- package/packages/web/dist/sw.js +257 -0
- package/scripts/postinstall-link-workspaces.mjs +58 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowRunner - executes workflow definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EventBus } from "@codepiper/core";
|
|
6
|
+
import type { Database } from "../db/db";
|
|
7
|
+
import { createWorkflowDb } from "../db/workflowDb";
|
|
8
|
+
import type { SessionManager } from "../sessions/sessionManager";
|
|
9
|
+
import { ContextManager } from "./contextManager";
|
|
10
|
+
import { ResultExtractor } from "./resultExtractor";
|
|
11
|
+
import { WaitConditionPoller } from "./waitConditionPoller";
|
|
12
|
+
import type {
|
|
13
|
+
ConditionalStep,
|
|
14
|
+
LogStep,
|
|
15
|
+
LoopStep,
|
|
16
|
+
ParallelStep,
|
|
17
|
+
SessionStep,
|
|
18
|
+
StepResult,
|
|
19
|
+
WorkflowDefinition,
|
|
20
|
+
WorkflowExecution,
|
|
21
|
+
WorkflowStep,
|
|
22
|
+
} from "./workflowTypes";
|
|
23
|
+
|
|
24
|
+
export interface WorkflowRunnerOptions {
|
|
25
|
+
sessionManager: SessionManager;
|
|
26
|
+
database: Database;
|
|
27
|
+
eventBus: EventBus;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class WorkflowRunner {
|
|
31
|
+
private sessionManager: SessionManager;
|
|
32
|
+
private database: Database;
|
|
33
|
+
private eventBus: EventBus;
|
|
34
|
+
private executions: Map<string, WorkflowExecution>;
|
|
35
|
+
private poller: WaitConditionPoller;
|
|
36
|
+
private extractor: ResultExtractor;
|
|
37
|
+
|
|
38
|
+
constructor(options: WorkflowRunnerOptions) {
|
|
39
|
+
this.sessionManager = options.sessionManager;
|
|
40
|
+
this.database = options.database;
|
|
41
|
+
this.eventBus = options.eventBus;
|
|
42
|
+
this.executions = new Map();
|
|
43
|
+
this.poller = new WaitConditionPoller(this.database);
|
|
44
|
+
this.extractor = new ResultExtractor(this.database);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Start workflow execution
|
|
49
|
+
* @param workflow - Workflow definition
|
|
50
|
+
* @param executionId - Optional execution ID (if not provided, generates new one)
|
|
51
|
+
* @param variables - Initial variables for context
|
|
52
|
+
* @returns Execution ID
|
|
53
|
+
*/
|
|
54
|
+
async start(
|
|
55
|
+
workflow: WorkflowDefinition,
|
|
56
|
+
executionId?: string,
|
|
57
|
+
variables: Record<string, any> = {}
|
|
58
|
+
): Promise<string> {
|
|
59
|
+
const execId = executionId || crypto.randomUUID();
|
|
60
|
+
const isNewExecution = !executionId;
|
|
61
|
+
|
|
62
|
+
const execution: WorkflowExecution = {
|
|
63
|
+
id: execId,
|
|
64
|
+
workflowId: workflow.name,
|
|
65
|
+
status: "running",
|
|
66
|
+
startedAt: new Date(),
|
|
67
|
+
context: { ...variables },
|
|
68
|
+
stepResults: new Map(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this.executions.set(execId, execution);
|
|
72
|
+
|
|
73
|
+
// Create execution in database if this is a new execution (not passed from API)
|
|
74
|
+
if (isNewExecution) {
|
|
75
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
76
|
+
workflowDb.createExecution({
|
|
77
|
+
id: execId,
|
|
78
|
+
workflowId: workflow.name,
|
|
79
|
+
status: "running",
|
|
80
|
+
context: variables,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Execute workflow asynchronously
|
|
85
|
+
this.executeWorkflow(execId, workflow).catch((error) => {
|
|
86
|
+
const exec = this.executions.get(execId);
|
|
87
|
+
if (exec && exec.status === "running") {
|
|
88
|
+
exec.status = "failed";
|
|
89
|
+
exec.error = error instanceof Error ? error.message : String(error);
|
|
90
|
+
exec.completedAt = new Date();
|
|
91
|
+
|
|
92
|
+
// Update database
|
|
93
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
94
|
+
workflowDb.updateExecution(execId, {
|
|
95
|
+
status: "failed",
|
|
96
|
+
errorMessage: exec.error,
|
|
97
|
+
completedAt: exec.completedAt,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return execId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get execution by ID
|
|
107
|
+
*/
|
|
108
|
+
getExecution(executionId: string): WorkflowExecution | undefined {
|
|
109
|
+
return this.executions.get(executionId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* List all executions
|
|
114
|
+
*/
|
|
115
|
+
listExecutions(): WorkflowExecution[] {
|
|
116
|
+
return Array.from(this.executions.values());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Cancel running execution
|
|
121
|
+
*/
|
|
122
|
+
async cancel(executionId: string): Promise<void> {
|
|
123
|
+
const execution = this.executions.get(executionId);
|
|
124
|
+
if (!execution) {
|
|
125
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (execution.status !== "running") {
|
|
129
|
+
throw new Error(`Execution ${executionId} is not running`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
execution.status = "cancelled";
|
|
133
|
+
execution.completedAt = new Date();
|
|
134
|
+
|
|
135
|
+
// Update database
|
|
136
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
137
|
+
workflowDb.updateExecution(executionId, {
|
|
138
|
+
status: "cancelled",
|
|
139
|
+
completedAt: execution.completedAt,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Stop any running sessions
|
|
143
|
+
for (const [_stepName, stepResult] of execution.stepResults) {
|
|
144
|
+
if (stepResult.sessionId && stepResult.status === "running") {
|
|
145
|
+
try {
|
|
146
|
+
await this.sessionManager.stopSession(stepResult.sessionId);
|
|
147
|
+
} catch {
|
|
148
|
+
// Ignore errors during cleanup
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Wait for execution to complete
|
|
156
|
+
* @param executionId - Execution ID
|
|
157
|
+
* @param timeout - Max wait time in ms
|
|
158
|
+
*/
|
|
159
|
+
async waitForCompletion(executionId: string, timeout: number): Promise<void> {
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
|
|
162
|
+
while (true) {
|
|
163
|
+
const execution = this.executions.get(executionId);
|
|
164
|
+
if (!execution) {
|
|
165
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (execution.status !== "running") {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (Date.now() - startTime > timeout) {
|
|
173
|
+
throw new Error(`Workflow execution timeout after ${timeout}ms`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Execute workflow steps
|
|
182
|
+
*/
|
|
183
|
+
private async executeWorkflow(executionId: string, workflow: WorkflowDefinition): Promise<void> {
|
|
184
|
+
const execution = this.executions.get(executionId);
|
|
185
|
+
if (!execution) {
|
|
186
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
190
|
+
const contextManager = new ContextManager();
|
|
191
|
+
|
|
192
|
+
// Initialize context with variables
|
|
193
|
+
for (const [key, value] of Object.entries(execution.context)) {
|
|
194
|
+
contextManager.set(key, value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Update execution status to running in database
|
|
198
|
+
workflowDb.updateExecution(executionId, {
|
|
199
|
+
status: "running",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Execute steps sequentially
|
|
204
|
+
for (const step of workflow.steps) {
|
|
205
|
+
// Check if execution was cancelled
|
|
206
|
+
if (execution.status === "cancelled") {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await this.executeStepWithErrorHandling(executionId, step, contextManager);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Workflow completed successfully
|
|
214
|
+
execution.status = "completed";
|
|
215
|
+
execution.completedAt = new Date();
|
|
216
|
+
|
|
217
|
+
// Update database
|
|
218
|
+
workflowDb.updateExecution(executionId, {
|
|
219
|
+
status: "completed",
|
|
220
|
+
completedAt: execution.completedAt,
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (execution.status !== "cancelled") {
|
|
224
|
+
execution.status = "failed";
|
|
225
|
+
execution.error = error instanceof Error ? error.message : String(error);
|
|
226
|
+
execution.completedAt = new Date();
|
|
227
|
+
|
|
228
|
+
// Update database
|
|
229
|
+
workflowDb.updateExecution(executionId, {
|
|
230
|
+
status: "failed",
|
|
231
|
+
errorMessage: execution.error,
|
|
232
|
+
completedAt: execution.completedAt,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Execute a workflow step and apply session error strategy when configured.
|
|
240
|
+
*/
|
|
241
|
+
private async executeStepWithErrorHandling(
|
|
242
|
+
executionId: string,
|
|
243
|
+
step: WorkflowStep,
|
|
244
|
+
contextManager: ContextManager
|
|
245
|
+
): Promise<StepResult> {
|
|
246
|
+
if (step.type !== "session") {
|
|
247
|
+
const result = await this.executeStep(executionId, step, contextManager);
|
|
248
|
+
if (result.status === "failed") {
|
|
249
|
+
throw new Error(`Step ${step.name} failed: ${result.error}`);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const maxAttempts = step.onError === "retry" ? Math.max(1, step.retry?.maxAttempts ?? 1) : 1;
|
|
255
|
+
const retryDelayMs = Math.max(0, step.retry?.delay ?? 0);
|
|
256
|
+
|
|
257
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
258
|
+
const execution = this.executions.get(executionId);
|
|
259
|
+
if (!execution || execution.status === "cancelled") {
|
|
260
|
+
return {
|
|
261
|
+
status: "skipped",
|
|
262
|
+
startedAt: new Date(),
|
|
263
|
+
completedAt: new Date(),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const stepResult = await this.executeSessionStep(executionId, step, contextManager);
|
|
268
|
+
if (stepResult.status !== "failed") {
|
|
269
|
+
return stepResult;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (step.onError === "continue") {
|
|
273
|
+
return stepResult;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (step.onError === "retry" && attempt < maxAttempts) {
|
|
277
|
+
if (retryDelayMs > 0) {
|
|
278
|
+
await this.sleep(retryDelayMs);
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (step.onError === "retry") {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Step ${step.name} failed after ${maxAttempts} attempt(s): ${stepResult.error}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
throw new Error(`Step ${step.name} failed: ${stepResult.error}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw new Error(`Step ${step.name} failed unexpectedly`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Execute a workflow step by type.
|
|
297
|
+
*/
|
|
298
|
+
private async executeStep(
|
|
299
|
+
executionId: string,
|
|
300
|
+
step: WorkflowStep,
|
|
301
|
+
contextManager: ContextManager
|
|
302
|
+
): Promise<StepResult> {
|
|
303
|
+
switch (step.type) {
|
|
304
|
+
case "session":
|
|
305
|
+
return this.executeSessionStep(executionId, step, contextManager);
|
|
306
|
+
case "if":
|
|
307
|
+
return this.executeConditionalStep(executionId, step, contextManager);
|
|
308
|
+
case "parallel":
|
|
309
|
+
return this.executeParallelStep(executionId, step, contextManager);
|
|
310
|
+
case "foreach":
|
|
311
|
+
return this.executeLoopStep(executionId, step, contextManager);
|
|
312
|
+
case "log":
|
|
313
|
+
return this.executeLogStep(executionId, step, contextManager);
|
|
314
|
+
default:
|
|
315
|
+
return {
|
|
316
|
+
status: "failed",
|
|
317
|
+
error: `Unsupported step type: ${(step as { type: string }).type}`,
|
|
318
|
+
startedAt: new Date(),
|
|
319
|
+
completedAt: new Date(),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Execute a session step.
|
|
326
|
+
*/
|
|
327
|
+
private async executeSessionStep(
|
|
328
|
+
executionId: string,
|
|
329
|
+
step: SessionStep,
|
|
330
|
+
contextManager: ContextManager
|
|
331
|
+
): Promise<StepResult> {
|
|
332
|
+
const execution = this.executions.get(executionId);
|
|
333
|
+
if (!execution) {
|
|
334
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
338
|
+
|
|
339
|
+
// Initialize step result
|
|
340
|
+
const stepResult: StepResult = {
|
|
341
|
+
status: "running",
|
|
342
|
+
startedAt: new Date(),
|
|
343
|
+
};
|
|
344
|
+
execution.stepResults.set(step.name, stepResult);
|
|
345
|
+
|
|
346
|
+
// Create step record in database
|
|
347
|
+
const stepId = workflowDb.createStep({
|
|
348
|
+
executionId,
|
|
349
|
+
stepName: step.name,
|
|
350
|
+
status: "running",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
// Substitute variables in cwd and prompt
|
|
355
|
+
const cwd = contextManager.substitute(step.cwd);
|
|
356
|
+
const prompt = step.prompt ? contextManager.substitute(step.prompt) : undefined;
|
|
357
|
+
|
|
358
|
+
// Create session
|
|
359
|
+
const session = await this.sessionManager.createSession({
|
|
360
|
+
provider: step.provider,
|
|
361
|
+
cwd,
|
|
362
|
+
env: step.env,
|
|
363
|
+
args: step.args,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
stepResult.sessionId = session.id;
|
|
367
|
+
|
|
368
|
+
// Update step with sessionId
|
|
369
|
+
workflowDb.updateStep(stepId, {
|
|
370
|
+
sessionId: session.id,
|
|
371
|
+
startedAt: stepResult.startedAt,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Send prompt if provided
|
|
375
|
+
if (prompt) {
|
|
376
|
+
await this.sessionManager.sendText(session.id, prompt);
|
|
377
|
+
await this.sessionManager.sendKeys(session.id, ["enter"]);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Wait for conditions if provided
|
|
381
|
+
if (step.wait && step.wait.length > 0) {
|
|
382
|
+
await this.poller.wait(session.id, step.wait);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Extract results if configured
|
|
386
|
+
if (step.extract) {
|
|
387
|
+
const extractedData: Record<string, any> = {};
|
|
388
|
+
|
|
389
|
+
for (const [key, config] of Object.entries(step.extract)) {
|
|
390
|
+
const value = await this.extractor.extract(session.id, config);
|
|
391
|
+
extractedData[key] = value;
|
|
392
|
+
|
|
393
|
+
this.setStepContextValue(contextManager, execution, step.name, key, value);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
stepResult.extractedData = extractedData;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Mark step as completed
|
|
400
|
+
stepResult.status = "completed";
|
|
401
|
+
stepResult.completedAt = new Date();
|
|
402
|
+
|
|
403
|
+
// Update step in database
|
|
404
|
+
workflowDb.updateStep(stepId, {
|
|
405
|
+
status: "completed",
|
|
406
|
+
completedAt: stepResult.completedAt,
|
|
407
|
+
result: stepResult.extractedData ? JSON.stringify(stepResult.extractedData) : undefined,
|
|
408
|
+
});
|
|
409
|
+
return stepResult;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
stepResult.status = "failed";
|
|
412
|
+
stepResult.error = error instanceof Error ? error.message : String(error);
|
|
413
|
+
stepResult.completedAt = new Date();
|
|
414
|
+
|
|
415
|
+
// Update step in database
|
|
416
|
+
workflowDb.updateStep(stepId, {
|
|
417
|
+
status: "failed",
|
|
418
|
+
errorMessage: stepResult.error,
|
|
419
|
+
completedAt: stepResult.completedAt,
|
|
420
|
+
});
|
|
421
|
+
return stepResult;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private async executeConditionalStep(
|
|
426
|
+
executionId: string,
|
|
427
|
+
step: ConditionalStep,
|
|
428
|
+
contextManager: ContextManager
|
|
429
|
+
): Promise<StepResult> {
|
|
430
|
+
const execution = this.executions.get(executionId);
|
|
431
|
+
if (!execution) {
|
|
432
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
436
|
+
const stepResult: StepResult = {
|
|
437
|
+
status: "running",
|
|
438
|
+
startedAt: new Date(),
|
|
439
|
+
};
|
|
440
|
+
execution.stepResults.set(step.name, stepResult);
|
|
441
|
+
|
|
442
|
+
const stepId = workflowDb.createStep({
|
|
443
|
+
executionId,
|
|
444
|
+
stepName: step.name,
|
|
445
|
+
status: "running",
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const conditionMatched = this.evaluateCondition(step.condition, contextManager);
|
|
450
|
+
const selectedBranch = conditionMatched ? step.then : (step.else ?? []);
|
|
451
|
+
const branchName = conditionMatched ? "then" : "else";
|
|
452
|
+
|
|
453
|
+
for (const childStep of selectedBranch) {
|
|
454
|
+
await this.executeStepWithErrorHandling(executionId, childStep, contextManager);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
stepResult.status = "completed";
|
|
458
|
+
stepResult.completedAt = new Date();
|
|
459
|
+
stepResult.extractedData = {
|
|
460
|
+
branch: branchName,
|
|
461
|
+
matched: conditionMatched,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
workflowDb.updateStep(stepId, {
|
|
465
|
+
status: "completed",
|
|
466
|
+
completedAt: stepResult.completedAt,
|
|
467
|
+
result: JSON.stringify(stepResult.extractedData),
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
return stepResult;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
stepResult.status = "failed";
|
|
473
|
+
stepResult.error = error instanceof Error ? error.message : String(error);
|
|
474
|
+
stepResult.completedAt = new Date();
|
|
475
|
+
|
|
476
|
+
workflowDb.updateStep(stepId, {
|
|
477
|
+
status: "failed",
|
|
478
|
+
errorMessage: stepResult.error,
|
|
479
|
+
completedAt: stepResult.completedAt,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return stepResult;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private async executeParallelStep(
|
|
487
|
+
executionId: string,
|
|
488
|
+
step: ParallelStep,
|
|
489
|
+
contextManager: ContextManager
|
|
490
|
+
): Promise<StepResult> {
|
|
491
|
+
const execution = this.executions.get(executionId);
|
|
492
|
+
if (!execution) {
|
|
493
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
497
|
+
const stepResult: StepResult = {
|
|
498
|
+
status: "running",
|
|
499
|
+
startedAt: new Date(),
|
|
500
|
+
};
|
|
501
|
+
execution.stepResults.set(step.name, stepResult);
|
|
502
|
+
|
|
503
|
+
const stepId = workflowDb.createStep({
|
|
504
|
+
executionId,
|
|
505
|
+
stepName: step.name,
|
|
506
|
+
status: "running",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const waitFor = step.waitFor ?? "all";
|
|
511
|
+
const branchPromises = step.steps.map((childStep) =>
|
|
512
|
+
this.executeParallelBranch(executionId, childStep, contextManager)
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
if (waitFor === "none") {
|
|
516
|
+
// Fire-and-forget branch execution: workflow continues immediately.
|
|
517
|
+
for (const promise of branchPromises) {
|
|
518
|
+
void promise.catch(() => {
|
|
519
|
+
// Branch errors are intentionally decoupled from parent step completion.
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
stepResult.status = "completed";
|
|
524
|
+
stepResult.completedAt = new Date();
|
|
525
|
+
stepResult.extractedData = {
|
|
526
|
+
waitFor,
|
|
527
|
+
launched: step.steps.length,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
workflowDb.updateStep(stepId, {
|
|
531
|
+
status: "completed",
|
|
532
|
+
completedAt: stepResult.completedAt,
|
|
533
|
+
result: JSON.stringify(stepResult.extractedData),
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
return stepResult;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (waitFor === "any") {
|
|
540
|
+
try {
|
|
541
|
+
const winner = await Promise.any(
|
|
542
|
+
branchPromises.map(async (promise) => {
|
|
543
|
+
const result = await promise;
|
|
544
|
+
if (result.status === "completed") {
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
throw new Error(result.error ?? `${result.stepName} failed`);
|
|
548
|
+
})
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
stepResult.status = "completed";
|
|
552
|
+
stepResult.completedAt = new Date();
|
|
553
|
+
stepResult.extractedData = {
|
|
554
|
+
waitFor,
|
|
555
|
+
winner: winner.stepName,
|
|
556
|
+
launched: step.steps.length,
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
workflowDb.updateStep(stepId, {
|
|
560
|
+
status: "completed",
|
|
561
|
+
completedAt: stepResult.completedAt,
|
|
562
|
+
result: JSON.stringify(stepResult.extractedData),
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
return stepResult;
|
|
566
|
+
} catch {
|
|
567
|
+
const settled = await Promise.all(branchPromises);
|
|
568
|
+
const failed = settled.filter((result) => result.status === "failed");
|
|
569
|
+
throw new Error(
|
|
570
|
+
`Parallel step ${step.name} failed: no branch completed successfully (${failed
|
|
571
|
+
.map((branch) => branch.stepName)
|
|
572
|
+
.join(", ")})`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// waitFor === "all" (default)
|
|
578
|
+
const childResults = await Promise.all(branchPromises);
|
|
579
|
+
const completedCount = childResults.filter((result) => result.status === "completed").length;
|
|
580
|
+
const failed = childResults.filter((result) => result.status === "failed");
|
|
581
|
+
|
|
582
|
+
if (failed.length > 0) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
`Parallel branches failed: ${failed
|
|
585
|
+
.map((branch) => `${branch.stepName} (${branch.error ?? "unknown error"})`)
|
|
586
|
+
.join(", ")}`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
stepResult.status = "completed";
|
|
591
|
+
stepResult.completedAt = new Date();
|
|
592
|
+
stepResult.extractedData = {
|
|
593
|
+
waitFor,
|
|
594
|
+
completed: completedCount,
|
|
595
|
+
failed: failed.length,
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
workflowDb.updateStep(stepId, {
|
|
599
|
+
status: "completed",
|
|
600
|
+
completedAt: stepResult.completedAt,
|
|
601
|
+
result: JSON.stringify(stepResult.extractedData),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return stepResult;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
stepResult.status = "failed";
|
|
607
|
+
stepResult.error = error instanceof Error ? error.message : String(error);
|
|
608
|
+
stepResult.completedAt = new Date();
|
|
609
|
+
|
|
610
|
+
workflowDb.updateStep(stepId, {
|
|
611
|
+
status: "failed",
|
|
612
|
+
errorMessage: stepResult.error,
|
|
613
|
+
completedAt: stepResult.completedAt,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return stepResult;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private async executeParallelBranch(
|
|
621
|
+
executionId: string,
|
|
622
|
+
childStep: WorkflowStep,
|
|
623
|
+
contextManager: ContextManager
|
|
624
|
+
): Promise<{ stepName: string; status: "completed" | "failed"; error?: string }> {
|
|
625
|
+
try {
|
|
626
|
+
await this.executeStepWithErrorHandling(executionId, childStep, contextManager);
|
|
627
|
+
return { stepName: childStep.name, status: "completed" };
|
|
628
|
+
} catch (error) {
|
|
629
|
+
return {
|
|
630
|
+
stepName: childStep.name,
|
|
631
|
+
status: "failed",
|
|
632
|
+
error: error instanceof Error ? error.message : String(error),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private async executeLoopStep(
|
|
638
|
+
executionId: string,
|
|
639
|
+
step: LoopStep,
|
|
640
|
+
contextManager: ContextManager
|
|
641
|
+
): Promise<StepResult> {
|
|
642
|
+
const execution = this.executions.get(executionId);
|
|
643
|
+
if (!execution) {
|
|
644
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
648
|
+
const stepResult: StepResult = {
|
|
649
|
+
status: "running",
|
|
650
|
+
startedAt: new Date(),
|
|
651
|
+
};
|
|
652
|
+
execution.stepResults.set(step.name, stepResult);
|
|
653
|
+
|
|
654
|
+
const stepId = workflowDb.createStep({
|
|
655
|
+
executionId,
|
|
656
|
+
stepName: step.name,
|
|
657
|
+
status: "running",
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const previousContext = contextManager.getAllContext();
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
const items = this.resolveLoopItems(step.items, contextManager);
|
|
664
|
+
const iterationStepNames: string[] = [];
|
|
665
|
+
|
|
666
|
+
for (let index = 0; index < items.length; index++) {
|
|
667
|
+
const item = items[index];
|
|
668
|
+
contextManager.set("item", item);
|
|
669
|
+
contextManager.set("index", index);
|
|
670
|
+
|
|
671
|
+
const iterationStep: WorkflowStep = {
|
|
672
|
+
...step.step,
|
|
673
|
+
name: `${step.step.name}[${index}]`,
|
|
674
|
+
};
|
|
675
|
+
iterationStepNames.push(iterationStep.name);
|
|
676
|
+
|
|
677
|
+
await this.executeStepWithErrorHandling(executionId, iterationStep, contextManager);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
this.setStepContextValue(contextManager, execution, step.name, "results", items);
|
|
681
|
+
|
|
682
|
+
stepResult.status = "completed";
|
|
683
|
+
stepResult.completedAt = new Date();
|
|
684
|
+
stepResult.extractedData = {
|
|
685
|
+
iterations: items.length,
|
|
686
|
+
stepNames: iterationStepNames,
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
workflowDb.updateStep(stepId, {
|
|
690
|
+
status: "completed",
|
|
691
|
+
completedAt: stepResult.completedAt,
|
|
692
|
+
result: JSON.stringify(stepResult.extractedData),
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
return stepResult;
|
|
696
|
+
} catch (error) {
|
|
697
|
+
stepResult.status = "failed";
|
|
698
|
+
stepResult.error = error instanceof Error ? error.message : String(error);
|
|
699
|
+
stepResult.completedAt = new Date();
|
|
700
|
+
|
|
701
|
+
workflowDb.updateStep(stepId, {
|
|
702
|
+
status: "failed",
|
|
703
|
+
errorMessage: stepResult.error,
|
|
704
|
+
completedAt: stepResult.completedAt,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
return stepResult;
|
|
708
|
+
} finally {
|
|
709
|
+
if ("item" in previousContext) {
|
|
710
|
+
contextManager.set("item", previousContext.item);
|
|
711
|
+
} else {
|
|
712
|
+
contextManager.set("item", undefined);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if ("index" in previousContext) {
|
|
716
|
+
contextManager.set("index", previousContext.index);
|
|
717
|
+
} else {
|
|
718
|
+
contextManager.set("index", undefined);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private async executeLogStep(
|
|
724
|
+
executionId: string,
|
|
725
|
+
step: LogStep,
|
|
726
|
+
contextManager: ContextManager
|
|
727
|
+
): Promise<StepResult> {
|
|
728
|
+
const execution = this.executions.get(executionId);
|
|
729
|
+
if (!execution) {
|
|
730
|
+
throw new Error(`Execution ${executionId} not found`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const workflowDb = createWorkflowDb(this.database);
|
|
734
|
+
const stepResult: StepResult = {
|
|
735
|
+
status: "running",
|
|
736
|
+
startedAt: new Date(),
|
|
737
|
+
};
|
|
738
|
+
execution.stepResults.set(step.name, stepResult);
|
|
739
|
+
|
|
740
|
+
const stepId = workflowDb.createStep({
|
|
741
|
+
executionId,
|
|
742
|
+
stepName: step.name,
|
|
743
|
+
status: "running",
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
try {
|
|
747
|
+
const message = contextManager.substitute(step.message);
|
|
748
|
+
|
|
749
|
+
stepResult.status = "completed";
|
|
750
|
+
stepResult.completedAt = new Date();
|
|
751
|
+
stepResult.extractedData = { message };
|
|
752
|
+
|
|
753
|
+
workflowDb.updateStep(stepId, {
|
|
754
|
+
status: "completed",
|
|
755
|
+
completedAt: stepResult.completedAt,
|
|
756
|
+
result: JSON.stringify(stepResult.extractedData),
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
return stepResult;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
stepResult.status = "failed";
|
|
762
|
+
stepResult.error = error instanceof Error ? error.message : String(error);
|
|
763
|
+
stepResult.completedAt = new Date();
|
|
764
|
+
|
|
765
|
+
workflowDb.updateStep(stepId, {
|
|
766
|
+
status: "failed",
|
|
767
|
+
errorMessage: stepResult.error,
|
|
768
|
+
completedAt: stepResult.completedAt,
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
return stepResult;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private evaluateCondition(condition: string, contextManager: ContextManager): boolean {
|
|
776
|
+
const substituted = contextManager.substitute(condition).trim();
|
|
777
|
+
if (!substituted) {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (substituted.includes("${")) {
|
|
782
|
+
throw new Error(`Unresolved variable in condition: ${condition}`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const match = substituted.match(/^(.*?)(===|!==|==|!=|>=|<=|>|<)(.*)$/);
|
|
786
|
+
if (!match) {
|
|
787
|
+
return this.toBoolean(this.parseConditionValue(substituted));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const leftRaw = match[1];
|
|
791
|
+
const operator = match[2];
|
|
792
|
+
const rightRaw = match[3];
|
|
793
|
+
if (leftRaw === undefined || operator === undefined || rightRaw === undefined) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
const left = this.parseConditionValue(leftRaw);
|
|
797
|
+
const right = this.parseConditionValue(rightRaw);
|
|
798
|
+
|
|
799
|
+
switch (operator) {
|
|
800
|
+
case "===":
|
|
801
|
+
return left === right;
|
|
802
|
+
case "!==":
|
|
803
|
+
return left !== right;
|
|
804
|
+
case "==":
|
|
805
|
+
return this.areConditionValuesEqual(left, right);
|
|
806
|
+
case "!=":
|
|
807
|
+
return !this.areConditionValuesEqual(left, right);
|
|
808
|
+
case ">":
|
|
809
|
+
return this.compareValues(left, right) > 0;
|
|
810
|
+
case "<":
|
|
811
|
+
return this.compareValues(left, right) < 0;
|
|
812
|
+
case ">=":
|
|
813
|
+
return this.compareValues(left, right) >= 0;
|
|
814
|
+
case "<=":
|
|
815
|
+
return this.compareValues(left, right) <= 0;
|
|
816
|
+
default:
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private parseConditionValue(raw: string): unknown {
|
|
822
|
+
const value = raw.trim();
|
|
823
|
+
if (value === "") {
|
|
824
|
+
return "";
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const quotedMatch = value.match(/^(['"])(.*)\1$/);
|
|
828
|
+
if (quotedMatch) {
|
|
829
|
+
return quotedMatch[2];
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (value === "true") return true;
|
|
833
|
+
if (value === "false") return false;
|
|
834
|
+
if (value === "null") return null;
|
|
835
|
+
if (value === "undefined") return undefined;
|
|
836
|
+
|
|
837
|
+
const num = Number(value);
|
|
838
|
+
if (!Number.isNaN(num)) {
|
|
839
|
+
return num;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
private compareValues(left: unknown, right: unknown): number {
|
|
846
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
847
|
+
return left - right;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return String(left).localeCompare(String(right));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private areConditionValuesEqual(left: unknown, right: unknown): boolean {
|
|
854
|
+
if (left === right) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (typeof left === "number" && typeof right === "string") {
|
|
859
|
+
const parsed = Number(right);
|
|
860
|
+
return !Number.isNaN(parsed) && left === parsed;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (typeof left === "string" && typeof right === "number") {
|
|
864
|
+
const parsed = Number(left);
|
|
865
|
+
return !Number.isNaN(parsed) && parsed === right;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return String(left) === String(right);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private toBoolean(value: unknown): boolean {
|
|
872
|
+
if (typeof value === "boolean") return value;
|
|
873
|
+
if (typeof value === "number") return value !== 0;
|
|
874
|
+
if (typeof value === "string") {
|
|
875
|
+
const normalized = value.trim().toLowerCase();
|
|
876
|
+
return normalized !== "" && normalized !== "false" && normalized !== "0";
|
|
877
|
+
}
|
|
878
|
+
return Boolean(value);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
private resolveLoopItems(itemsExpr: string, contextManager: ContextManager): unknown[] {
|
|
882
|
+
const exactVariableMatch = itemsExpr.trim().match(/^\$\{([^}]+)\}$/);
|
|
883
|
+
let resolved: unknown;
|
|
884
|
+
|
|
885
|
+
if (exactVariableMatch) {
|
|
886
|
+
const variablePath = exactVariableMatch[1];
|
|
887
|
+
resolved = variablePath ? contextManager.get(variablePath.trim()) : undefined;
|
|
888
|
+
} else {
|
|
889
|
+
resolved = contextManager.substitute(itemsExpr);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (Array.isArray(resolved)) {
|
|
893
|
+
return resolved;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (typeof resolved === "string") {
|
|
897
|
+
const trimmed = resolved.trim();
|
|
898
|
+
if (!trimmed) {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
const parsed = JSON.parse(trimmed);
|
|
904
|
+
if (Array.isArray(parsed)) {
|
|
905
|
+
return parsed;
|
|
906
|
+
}
|
|
907
|
+
} catch {
|
|
908
|
+
// Fall through to comma-delimited values.
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (trimmed.includes(",")) {
|
|
912
|
+
return trimmed
|
|
913
|
+
.split(",")
|
|
914
|
+
.map((item) => item.trim())
|
|
915
|
+
.filter((item) => item.length > 0);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return [trimmed];
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
throw new Error(`Foreach step expected array-like items, got ${typeof resolved}`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private setStepContextValue(
|
|
925
|
+
contextManager: ContextManager,
|
|
926
|
+
execution: WorkflowExecution,
|
|
927
|
+
stepName: string,
|
|
928
|
+
key: string,
|
|
929
|
+
value: unknown
|
|
930
|
+
): void {
|
|
931
|
+
const steps = contextManager.get("steps");
|
|
932
|
+
const stepsObj: Record<string, any> =
|
|
933
|
+
steps && typeof steps === "object" && !Array.isArray(steps) ? steps : {};
|
|
934
|
+
const stepObj: Record<string, any> =
|
|
935
|
+
stepsObj[stepName] &&
|
|
936
|
+
typeof stepsObj[stepName] === "object" &&
|
|
937
|
+
!Array.isArray(stepsObj[stepName])
|
|
938
|
+
? stepsObj[stepName]
|
|
939
|
+
: {};
|
|
940
|
+
|
|
941
|
+
stepObj[key] = value;
|
|
942
|
+
stepsObj[stepName] = stepObj;
|
|
943
|
+
contextManager.set("steps", stepsObj);
|
|
944
|
+
|
|
945
|
+
const executionSteps: Record<string, any> =
|
|
946
|
+
execution.context.steps &&
|
|
947
|
+
typeof execution.context.steps === "object" &&
|
|
948
|
+
!Array.isArray(execution.context.steps)
|
|
949
|
+
? execution.context.steps
|
|
950
|
+
: {};
|
|
951
|
+
const executionStepObj: Record<string, any> =
|
|
952
|
+
executionSteps[stepName] &&
|
|
953
|
+
typeof executionSteps[stepName] === "object" &&
|
|
954
|
+
!Array.isArray(executionSteps[stepName])
|
|
955
|
+
? executionSteps[stepName]
|
|
956
|
+
: {};
|
|
957
|
+
|
|
958
|
+
executionStepObj[key] = value;
|
|
959
|
+
executionSteps[stepName] = executionStepObj;
|
|
960
|
+
execution.context.steps = executionSteps;
|
|
961
|
+
|
|
962
|
+
// Keep dotted-path compatibility for older callers.
|
|
963
|
+
execution.context[`steps.${stepName}.${key}`] = value;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private sleep(ms: number): Promise<void> {
|
|
967
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
968
|
+
}
|
|
969
|
+
}
|