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.
Files changed (149) hide show
  1. package/.env.example +28 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LEGAL_NOTICE.md +39 -0
  4. package/LICENSE +21 -0
  5. package/README.md +524 -0
  6. package/package.json +90 -0
  7. package/packages/cli/package.json +13 -0
  8. package/packages/cli/src/commands/analytics.ts +157 -0
  9. package/packages/cli/src/commands/attach.ts +299 -0
  10. package/packages/cli/src/commands/audit.ts +50 -0
  11. package/packages/cli/src/commands/auth.ts +261 -0
  12. package/packages/cli/src/commands/daemon.ts +162 -0
  13. package/packages/cli/src/commands/doctor.ts +303 -0
  14. package/packages/cli/src/commands/env-set.ts +162 -0
  15. package/packages/cli/src/commands/hook-forward.ts +268 -0
  16. package/packages/cli/src/commands/keys.ts +77 -0
  17. package/packages/cli/src/commands/kill.ts +19 -0
  18. package/packages/cli/src/commands/logs.ts +419 -0
  19. package/packages/cli/src/commands/model.ts +172 -0
  20. package/packages/cli/src/commands/policy-set.ts +185 -0
  21. package/packages/cli/src/commands/policy.ts +227 -0
  22. package/packages/cli/src/commands/providers.ts +114 -0
  23. package/packages/cli/src/commands/resize.ts +34 -0
  24. package/packages/cli/src/commands/send.ts +184 -0
  25. package/packages/cli/src/commands/sessions.ts +202 -0
  26. package/packages/cli/src/commands/slash.ts +92 -0
  27. package/packages/cli/src/commands/start.ts +243 -0
  28. package/packages/cli/src/commands/stop.ts +19 -0
  29. package/packages/cli/src/commands/tail.ts +137 -0
  30. package/packages/cli/src/commands/workflow.ts +786 -0
  31. package/packages/cli/src/commands/workspace.ts +127 -0
  32. package/packages/cli/src/lib/api.ts +78 -0
  33. package/packages/cli/src/lib/args.ts +72 -0
  34. package/packages/cli/src/lib/format.ts +93 -0
  35. package/packages/cli/src/main.ts +563 -0
  36. package/packages/core/package.json +7 -0
  37. package/packages/core/src/config.ts +30 -0
  38. package/packages/core/src/errors.ts +38 -0
  39. package/packages/core/src/eventBus.ts +56 -0
  40. package/packages/core/src/eventBusAdapter.ts +143 -0
  41. package/packages/core/src/index.ts +10 -0
  42. package/packages/core/src/sqliteEventBus.ts +336 -0
  43. package/packages/core/src/types.ts +63 -0
  44. package/packages/daemon/package.json +11 -0
  45. package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
  46. package/packages/daemon/src/api/authRoutes.ts +344 -0
  47. package/packages/daemon/src/api/bodyLimit.ts +133 -0
  48. package/packages/daemon/src/api/envSetRoutes.ts +170 -0
  49. package/packages/daemon/src/api/gitRoutes.ts +409 -0
  50. package/packages/daemon/src/api/hooks.ts +588 -0
  51. package/packages/daemon/src/api/inputPolicy.ts +249 -0
  52. package/packages/daemon/src/api/notificationRoutes.ts +532 -0
  53. package/packages/daemon/src/api/policyRoutes.ts +234 -0
  54. package/packages/daemon/src/api/policySetRoutes.ts +445 -0
  55. package/packages/daemon/src/api/routeUtils.ts +28 -0
  56. package/packages/daemon/src/api/routes.ts +1004 -0
  57. package/packages/daemon/src/api/server.ts +1388 -0
  58. package/packages/daemon/src/api/settingsRoutes.ts +367 -0
  59. package/packages/daemon/src/api/sqliteErrors.ts +47 -0
  60. package/packages/daemon/src/api/stt.ts +143 -0
  61. package/packages/daemon/src/api/terminalRoutes.ts +200 -0
  62. package/packages/daemon/src/api/validation.ts +287 -0
  63. package/packages/daemon/src/api/validationRoutes.ts +174 -0
  64. package/packages/daemon/src/api/workflowRoutes.ts +567 -0
  65. package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
  66. package/packages/daemon/src/api/ws.ts +1588 -0
  67. package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
  68. package/packages/daemon/src/auth/authMiddleware.ts +305 -0
  69. package/packages/daemon/src/auth/authService.ts +496 -0
  70. package/packages/daemon/src/auth/rateLimiter.ts +137 -0
  71. package/packages/daemon/src/config/pricing.ts +79 -0
  72. package/packages/daemon/src/crypto/encryption.ts +196 -0
  73. package/packages/daemon/src/db/db.ts +2745 -0
  74. package/packages/daemon/src/db/index.ts +16 -0
  75. package/packages/daemon/src/db/migrations.ts +182 -0
  76. package/packages/daemon/src/db/policyDb.ts +349 -0
  77. package/packages/daemon/src/db/schema.sql +408 -0
  78. package/packages/daemon/src/db/workflowDb.ts +464 -0
  79. package/packages/daemon/src/git/gitUtils.ts +544 -0
  80. package/packages/daemon/src/index.ts +6 -0
  81. package/packages/daemon/src/main.ts +525 -0
  82. package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
  83. package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
  84. package/packages/daemon/src/providers/registry.ts +111 -0
  85. package/packages/daemon/src/providers/types.ts +82 -0
  86. package/packages/daemon/src/sessions/auditLogger.ts +103 -0
  87. package/packages/daemon/src/sessions/policyEngine.ts +165 -0
  88. package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
  89. package/packages/daemon/src/sessions/policyTypes.ts +94 -0
  90. package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
  91. package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
  92. package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
  93. package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
  94. package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
  95. package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
  96. package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
  97. package/packages/daemon/src/workflows/contextManager.ts +83 -0
  98. package/packages/daemon/src/workflows/index.ts +31 -0
  99. package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
  100. package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
  101. package/packages/daemon/src/workflows/workflowParser.ts +217 -0
  102. package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
  103. package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
  104. package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
  105. package/packages/providers/claude-code/package.json +11 -0
  106. package/packages/providers/claude-code/src/index.ts +7 -0
  107. package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
  108. package/packages/providers/claude-code/src/provider.ts +311 -0
  109. package/packages/web/dist/android-chrome-192x192.png +0 -0
  110. package/packages/web/dist/android-chrome-512x512.png +0 -0
  111. package/packages/web/dist/apple-touch-icon.png +0 -0
  112. package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
  113. package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
  114. package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
  115. package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
  116. package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
  117. package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
  118. package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  119. package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
  120. package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
  121. package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
  122. package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
  123. package/packages/web/dist/assets/index-hgphORiw.js +204 -0
  124. package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
  125. package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
  126. package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
  127. package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
  128. package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
  129. package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
  130. package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
  131. package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
  132. package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
  133. package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
  134. package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
  135. package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
  136. package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
  137. package/packages/web/dist/favicon.ico +0 -0
  138. package/packages/web/dist/icon.svg +1 -0
  139. package/packages/web/dist/index.html +29 -0
  140. package/packages/web/dist/manifest.json +29 -0
  141. package/packages/web/dist/og-image.png +0 -0
  142. package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
  143. package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
  144. package/packages/web/dist/originals/apple-touch-icon.png +0 -0
  145. package/packages/web/dist/originals/favicon.ico +0 -0
  146. package/packages/web/dist/piper.svg +1 -0
  147. package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
  148. package/packages/web/dist/sw.js +257 -0
  149. package/scripts/postinstall-link-workspaces.mjs +58 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Workflow types and definitions
3
+ */
4
+
5
+ import type { ProviderId } from "@codepiper/core";
6
+
7
+ /**
8
+ * Wait condition types
9
+ */
10
+ export type WaitConditionType = "idle_prompt" | "permission_prompt" | "stop" | "event" | "timeout";
11
+
12
+ /**
13
+ * Wait condition definition
14
+ */
15
+ export interface WaitCondition {
16
+ type: WaitConditionType;
17
+ eventType?: string; // For type=event
18
+ timeout?: number; // Max wait time in ms
19
+ }
20
+
21
+ /**
22
+ * Result extraction types
23
+ */
24
+ export type ExtractType = "regex" | "jsonpath" | "xpath";
25
+
26
+ /**
27
+ * Result extraction configuration
28
+ */
29
+ export interface ExtractConfig {
30
+ type: ExtractType;
31
+ pattern?: string; // For regex
32
+ path?: string; // For jsonpath/xpath
33
+ }
34
+
35
+ /**
36
+ * Step types
37
+ */
38
+ export type StepType = "session" | "if" | "parallel" | "foreach" | "log";
39
+
40
+ /**
41
+ * Error handling strategy
42
+ */
43
+ export type ErrorStrategy = "continue" | "fail" | "retry";
44
+
45
+ /**
46
+ * Retry configuration
47
+ */
48
+ export interface RetryConfig {
49
+ maxAttempts: number;
50
+ delay: number; // ms
51
+ }
52
+
53
+ /**
54
+ * Base step definition
55
+ */
56
+ export interface BaseStep {
57
+ name: string;
58
+ type: StepType;
59
+ }
60
+
61
+ /**
62
+ * Session step - spawns a new session
63
+ */
64
+ export interface SessionStep extends BaseStep {
65
+ type: "session";
66
+ provider: ProviderId;
67
+ cwd: string;
68
+ args?: string[];
69
+ env?: Record<string, string>;
70
+ prompt?: string;
71
+ wait?: WaitCondition[];
72
+ extract?: Record<string, ExtractConfig>;
73
+ onError?: ErrorStrategy;
74
+ retry?: RetryConfig;
75
+ }
76
+
77
+ /**
78
+ * Conditional step
79
+ */
80
+ export interface ConditionalStep extends BaseStep {
81
+ type: "if";
82
+ condition: string;
83
+ then: WorkflowStep[];
84
+ else?: WorkflowStep[];
85
+ }
86
+
87
+ /**
88
+ * Parallel step
89
+ */
90
+ export interface ParallelStep extends BaseStep {
91
+ type: "parallel";
92
+ steps: WorkflowStep[];
93
+ waitFor: "all" | "any" | "none";
94
+ }
95
+
96
+ /**
97
+ * Loop step
98
+ */
99
+ export interface LoopStep extends BaseStep {
100
+ type: "foreach";
101
+ items: string; // Variable reference like "${files}"
102
+ step: WorkflowStep;
103
+ }
104
+
105
+ /**
106
+ * Log step - outputs a message
107
+ */
108
+ export interface LogStep extends BaseStep {
109
+ type: "log";
110
+ message: string;
111
+ }
112
+
113
+ /**
114
+ * Union of all step types
115
+ */
116
+ export type WorkflowStep = SessionStep | ConditionalStep | ParallelStep | LoopStep | LogStep;
117
+
118
+ /**
119
+ * Workflow definition
120
+ */
121
+ export interface WorkflowDefinition {
122
+ name: string;
123
+ description?: string;
124
+ steps: WorkflowStep[];
125
+ env?: Record<string, string>;
126
+ cwd?: string;
127
+ }
128
+
129
+ /**
130
+ * Step execution status
131
+ */
132
+ export type StepStatus = "pending" | "running" | "completed" | "failed" | "skipped";
133
+
134
+ /**
135
+ * Step execution result
136
+ */
137
+ export interface StepResult {
138
+ status: StepStatus;
139
+ sessionId?: string;
140
+ extractedData?: Record<string, any>;
141
+ error?: string;
142
+ startedAt?: Date;
143
+ completedAt?: Date;
144
+ }
145
+
146
+ /**
147
+ * Workflow execution status
148
+ */
149
+ export type WorkflowStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
150
+
151
+ /**
152
+ * Workflow execution state
153
+ */
154
+ export interface WorkflowExecution {
155
+ id: string;
156
+ workflowId: string;
157
+ status: WorkflowStatus;
158
+ startedAt: Date;
159
+ completedAt?: Date;
160
+ error?: string;
161
+ context: Record<string, any>;
162
+ stepResults: Map<string, StepResult>;
163
+ }
164
+
165
+ /**
166
+ * Workflow context - variables passed between steps
167
+ */
168
+ export interface WorkflowContext {
169
+ /** Step results indexed by step name */
170
+ steps: Record<string, StepResult>;
171
+
172
+ /** Global variables */
173
+ variables: Record<string, unknown>;
174
+ }
175
+
176
+ /**
177
+ * Workflow validation error
178
+ */
179
+ export interface WorkflowValidationError {
180
+ /** Error message */
181
+ message: string;
182
+
183
+ /** Path to the error in the workflow definition */
184
+ path?: string;
185
+
186
+ /** Step name where error occurred */
187
+ stepName?: string;
188
+ }
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Workflow Validator
3
+ *
4
+ * Validates workflow structure, dependencies, and configurations
5
+ */
6
+
7
+ import { type KnownProviderId, SUPPORTED_PROVIDERS } from "@codepiper/core";
8
+ import { getProviderDefinition } from "../providers/registry";
9
+ import type {
10
+ ConditionalStep,
11
+ ExtractConfig,
12
+ LoopStep,
13
+ ParallelStep,
14
+ SessionStep,
15
+ WaitCondition,
16
+ WorkflowDefinition,
17
+ WorkflowStep,
18
+ WorkflowValidationError,
19
+ } from "./workflowTypes";
20
+
21
+ const VALID_PROVIDERS: readonly KnownProviderId[] = [...SUPPORTED_PROVIDERS];
22
+ const VALID_WAIT_TYPES = ["idle_prompt", "permission_prompt", "stop", "event", "timeout"];
23
+ const VALID_EXTRACT_TYPES = ["regex", "jsonpath", "xpath"];
24
+ const VALID_ERROR_STRATEGIES = ["continue", "fail", "retry"];
25
+ const WAIT_TYPES_REQUIRING_HOOKS = new Set<WaitCondition["type"]>([
26
+ "idle_prompt",
27
+ "permission_prompt",
28
+ "stop",
29
+ "event",
30
+ ]);
31
+
32
+ function isKnownProviderId(value: string): value is KnownProviderId {
33
+ return VALID_PROVIDERS.includes(value as KnownProviderId);
34
+ }
35
+
36
+ /**
37
+ * Validate a workflow definition
38
+ *
39
+ * Checks:
40
+ * - Workflow structure
41
+ * - Step configurations
42
+ * - Variable references
43
+ * - Circular dependencies
44
+ * - Required fields
45
+ *
46
+ * @param workflow - Workflow to validate
47
+ * @returns Array of validation errors (empty if valid)
48
+ */
49
+ export function validateWorkflow(workflow: WorkflowDefinition): WorkflowValidationError[] {
50
+ const errors: WorkflowValidationError[] = [];
51
+
52
+ // Validate top-level structure
53
+ if (!workflow.steps || workflow.steps.length === 0) {
54
+ errors.push({
55
+ message: "Workflow must have at least one step",
56
+ path: "steps",
57
+ });
58
+ return errors;
59
+ }
60
+
61
+ // Check for duplicate step names
62
+ const stepNames = new Set<string>();
63
+ const duplicates = new Set<string>();
64
+
65
+ for (const step of workflow.steps) {
66
+ if (stepNames.has(step.name)) {
67
+ duplicates.add(step.name);
68
+ }
69
+ stepNames.add(step.name);
70
+ }
71
+
72
+ for (const name of duplicates) {
73
+ errors.push({
74
+ message: `Duplicate step name: ${name}`,
75
+ path: "steps",
76
+ stepName: name,
77
+ });
78
+ }
79
+
80
+ // Validate each step
81
+ for (const step of workflow.steps) {
82
+ errors.push(...validateStep(step, `steps.${step.name}`));
83
+ }
84
+
85
+ // Check for circular dependencies
86
+ const circularErrors = detectCircularDependencies(workflow.steps);
87
+ errors.push(...circularErrors);
88
+
89
+ return errors;
90
+ }
91
+
92
+ /**
93
+ * Validate a single step
94
+ */
95
+ function validateStep(step: WorkflowStep, path: string): WorkflowValidationError[] {
96
+ const errors: WorkflowValidationError[] = [];
97
+
98
+ // Validate based on step type
99
+ switch (step.type) {
100
+ case "session":
101
+ errors.push(...validateSessionStep(step, path));
102
+ break;
103
+ case "if":
104
+ errors.push(...validateConditionalStep(step, path));
105
+ break;
106
+ case "parallel":
107
+ errors.push(...validateParallelStep(step, path));
108
+ break;
109
+ case "foreach":
110
+ errors.push(...validateLoopStep(step, path));
111
+ break;
112
+ case "log":
113
+ errors.push(...validateLogStep(step, path));
114
+ break;
115
+ default: {
116
+ // Keeps runtime safety if parser types drift in the future.
117
+ // Compile-time exhaustiveness guard for WorkflowStep unions.
118
+ const unknownStep = step as { type?: unknown };
119
+ const _exhaustive: never = step;
120
+ void _exhaustive;
121
+ errors.push({
122
+ message: `Unknown step type: ${String(unknownStep.type)}`,
123
+ path,
124
+ });
125
+ }
126
+ }
127
+
128
+ return errors;
129
+ }
130
+
131
+ /**
132
+ * Validate session step
133
+ */
134
+ function validateSessionStep(step: SessionStep, path: string): WorkflowValidationError[] {
135
+ const errors: WorkflowValidationError[] = [];
136
+ const providerCapabilities =
137
+ step.provider && isKnownProviderId(step.provider)
138
+ ? getProviderDefinition(step.provider).capabilities
139
+ : undefined;
140
+
141
+ // Required fields
142
+ if (!step.provider) {
143
+ errors.push({
144
+ message: "Session step requires 'provider' field",
145
+ path: `${path}.provider`,
146
+ stepName: step.name,
147
+ });
148
+ } else if (!isKnownProviderId(step.provider)) {
149
+ errors.push({
150
+ message: `Invalid provider: ${step.provider}. Must be one of: ${VALID_PROVIDERS.join(", ")}`,
151
+ path: `${path}.provider`,
152
+ stepName: step.name,
153
+ });
154
+ }
155
+
156
+ if (!step.cwd) {
157
+ errors.push({
158
+ message: "Session step requires 'cwd' field",
159
+ path: `${path}.cwd`,
160
+ stepName: step.name,
161
+ });
162
+ }
163
+
164
+ // Validate wait conditions
165
+ if (step.wait) {
166
+ for (let i = 0; i < step.wait.length; i++) {
167
+ const waitCondition = step.wait[i];
168
+ if (!waitCondition) {
169
+ continue;
170
+ }
171
+ errors.push(...validateWaitCondition(waitCondition, `${path}.wait[${i}]`, step.name));
172
+ if (
173
+ providerCapabilities &&
174
+ !providerCapabilities.nativeHooks &&
175
+ WAIT_TYPES_REQUIRING_HOOKS.has(waitCondition.type)
176
+ ) {
177
+ errors.push({
178
+ message: `Provider ${step.provider} does not support wait condition type '${waitCondition.type}' (requires native hooks)`,
179
+ path: `${path}.wait[${i}].type`,
180
+ stepName: step.name,
181
+ });
182
+ }
183
+ }
184
+ }
185
+
186
+ // Validate extract config
187
+ if (step.extract) {
188
+ for (const [key, config] of Object.entries(step.extract)) {
189
+ errors.push(...validateExtractConfig(config, `${path}.extract.${key}`, step.name));
190
+ if (providerCapabilities && !providerCapabilities.supportsTranscriptTailing) {
191
+ errors.push({
192
+ message: `Provider ${step.provider} does not support extract config '${key}' (requires transcript tailing)`,
193
+ path: `${path}.extract.${key}`,
194
+ stepName: step.name,
195
+ });
196
+ }
197
+ }
198
+ }
199
+
200
+ // Validate error handling
201
+ if (step.onError && !VALID_ERROR_STRATEGIES.includes(step.onError)) {
202
+ errors.push({
203
+ message: `Invalid error strategy: ${step.onError}. Must be one of: ${VALID_ERROR_STRATEGIES.join(", ")}`,
204
+ path: `${path}.onError`,
205
+ stepName: step.name,
206
+ });
207
+ }
208
+
209
+ // Validate retry config
210
+ if (step.onError === "retry" && !step.retry) {
211
+ errors.push({
212
+ message: "Retry configuration required when onError is 'retry'",
213
+ path: `${path}.retry`,
214
+ stepName: step.name,
215
+ });
216
+ }
217
+
218
+ if (step.retry) {
219
+ if (!step.retry.maxAttempts || step.retry.maxAttempts < 1) {
220
+ errors.push({
221
+ message: "Retry maxAttempts must be at least 1",
222
+ path: `${path}.retry.maxAttempts`,
223
+ stepName: step.name,
224
+ });
225
+ }
226
+
227
+ if (step.retry.delay === undefined || step.retry.delay < 0) {
228
+ errors.push({
229
+ message: "Retry delay must be >= 0",
230
+ path: `${path}.retry.delay`,
231
+ stepName: step.name,
232
+ });
233
+ }
234
+ }
235
+
236
+ return errors;
237
+ }
238
+
239
+ /**
240
+ * Validate wait condition
241
+ */
242
+ function validateWaitCondition(
243
+ condition: WaitCondition,
244
+ path: string,
245
+ stepName: string
246
+ ): WorkflowValidationError[] {
247
+ const errors: WorkflowValidationError[] = [];
248
+
249
+ if (!VALID_WAIT_TYPES.includes(condition.type)) {
250
+ errors.push({
251
+ message: `Invalid wait condition type: ${condition.type}. Must be one of: ${VALID_WAIT_TYPES.join(", ")}`,
252
+ path: `${path}.type`,
253
+ stepName,
254
+ });
255
+ }
256
+
257
+ // Event type requires eventType field
258
+ if (condition.type === "event" && !condition.eventType) {
259
+ errors.push({
260
+ message: "Wait condition type 'event' requires 'eventType' field",
261
+ path: `${path}.eventType`,
262
+ stepName,
263
+ });
264
+ }
265
+
266
+ // Validate timeout
267
+ if (condition.timeout !== undefined && condition.timeout < 0) {
268
+ errors.push({
269
+ message: "Wait condition timeout must be >= 0",
270
+ path: `${path}.timeout`,
271
+ stepName,
272
+ });
273
+ }
274
+
275
+ return errors;
276
+ }
277
+
278
+ /**
279
+ * Validate extract configuration
280
+ */
281
+ function validateExtractConfig(
282
+ config: ExtractConfig,
283
+ path: string,
284
+ stepName: string
285
+ ): WorkflowValidationError[] {
286
+ const errors: WorkflowValidationError[] = [];
287
+
288
+ if (!VALID_EXTRACT_TYPES.includes(config.type)) {
289
+ errors.push({
290
+ message: `Invalid extract type: ${config.type}. Must be one of: ${VALID_EXTRACT_TYPES.join(", ")}`,
291
+ path: `${path}.type`,
292
+ stepName,
293
+ });
294
+ }
295
+
296
+ // Regex requires pattern
297
+ if (config.type === "regex" && !config.pattern) {
298
+ errors.push({
299
+ message: "Extract type 'regex' requires 'pattern' field",
300
+ path: `${path}.pattern`,
301
+ stepName,
302
+ });
303
+ }
304
+
305
+ // JSONPath and XPath require path
306
+ if ((config.type === "jsonpath" || config.type === "xpath") && !config.path) {
307
+ errors.push({
308
+ message: `Extract type '${config.type}' requires 'path' field`,
309
+ path: `${path}.path`,
310
+ stepName,
311
+ });
312
+ }
313
+
314
+ return errors;
315
+ }
316
+
317
+ /**
318
+ * Validate conditional step
319
+ */
320
+ function validateConditionalStep(step: ConditionalStep, path: string): WorkflowValidationError[] {
321
+ const errors: WorkflowValidationError[] = [];
322
+
323
+ // Require condition
324
+ if (!step.condition || step.condition.trim() === "") {
325
+ errors.push({
326
+ message: "Conditional step requires non-empty 'condition' field",
327
+ path: `${path}.condition`,
328
+ stepName: step.name,
329
+ });
330
+ }
331
+
332
+ // Require then steps
333
+ if (!step.then || step.then.length === 0) {
334
+ errors.push({
335
+ message: "Conditional step requires at least one step in 'then' branch",
336
+ path: `${path}.then`,
337
+ stepName: step.name,
338
+ });
339
+ }
340
+
341
+ // Validate then steps
342
+ if (step.then) {
343
+ for (let i = 0; i < step.then.length; i++) {
344
+ const thenStep = step.then[i];
345
+ if (!thenStep) {
346
+ continue;
347
+ }
348
+ errors.push(...validateStep(thenStep, `${path}.then[${i}]`));
349
+ }
350
+ }
351
+
352
+ // Validate else steps
353
+ if (step.else) {
354
+ for (let i = 0; i < step.else.length; i++) {
355
+ const elseStep = step.else[i];
356
+ if (!elseStep) {
357
+ continue;
358
+ }
359
+ errors.push(...validateStep(elseStep, `${path}.else[${i}]`));
360
+ }
361
+ }
362
+
363
+ return errors;
364
+ }
365
+
366
+ /**
367
+ * Validate parallel step
368
+ */
369
+ function validateParallelStep(step: ParallelStep, path: string): WorkflowValidationError[] {
370
+ const errors: WorkflowValidationError[] = [];
371
+
372
+ // Require steps
373
+ if (!step.steps || step.steps.length === 0) {
374
+ errors.push({
375
+ message: "Parallel step requires at least one step",
376
+ path: `${path}.steps`,
377
+ stepName: step.name,
378
+ });
379
+ }
380
+
381
+ // Validate each parallel step
382
+ if (step.steps) {
383
+ for (let i = 0; i < step.steps.length; i++) {
384
+ const parallelStep = step.steps[i];
385
+ if (!parallelStep) {
386
+ continue;
387
+ }
388
+ errors.push(...validateStep(parallelStep, `${path}.steps[${i}]`));
389
+ }
390
+ }
391
+
392
+ // Validate waitFor
393
+ if (step.waitFor && !["all", "any", "none"].includes(step.waitFor)) {
394
+ errors.push({
395
+ message: "Parallel step waitFor must be 'all', 'any', or 'none'",
396
+ path: `${path}.waitFor`,
397
+ stepName: step.name,
398
+ });
399
+ }
400
+
401
+ return errors;
402
+ }
403
+
404
+ /**
405
+ * Validate loop step
406
+ */
407
+ function validateLoopStep(step: LoopStep, path: string): WorkflowValidationError[] {
408
+ const errors: WorkflowValidationError[] = [];
409
+
410
+ // Require items
411
+ if (!step.items || step.items.trim() === "") {
412
+ errors.push({
413
+ message: "Foreach step requires non-empty 'items' field",
414
+ path: `${path}.items`,
415
+ stepName: step.name,
416
+ });
417
+ }
418
+
419
+ // Validate step template
420
+ if (!step.step) {
421
+ errors.push({
422
+ message: "Foreach step requires 'step' field",
423
+ path: `${path}.step`,
424
+ stepName: step.name,
425
+ });
426
+ } else {
427
+ errors.push(...validateStep(step.step, `${path}.step`));
428
+ }
429
+
430
+ return errors;
431
+ }
432
+
433
+ /**
434
+ * Validate log step
435
+ */
436
+ function validateLogStep(
437
+ step: { name: string; type: "log"; message: string },
438
+ path: string
439
+ ): WorkflowValidationError[] {
440
+ const errors: WorkflowValidationError[] = [];
441
+
442
+ if (!step.message || step.message.trim() === "") {
443
+ errors.push({
444
+ message: "Log step requires non-empty 'message' field",
445
+ path: `${path}.message`,
446
+ stepName: step.name,
447
+ });
448
+ }
449
+
450
+ return errors;
451
+ }
452
+
453
+ /**
454
+ * Detect circular dependencies in variable references
455
+ *
456
+ * Checks if step A references step B and step B references step A
457
+ */
458
+ function detectCircularDependencies(steps: WorkflowStep[]): WorkflowValidationError[] {
459
+ const errors: WorkflowValidationError[] = [];
460
+
461
+ // Build dependency graph
462
+ const dependencies = new Map<string, Set<string>>();
463
+
464
+ for (const step of steps) {
465
+ const deps = extractStepDependencies(step);
466
+ dependencies.set(step.name, deps);
467
+ }
468
+
469
+ // Check for cycles using DFS
470
+ const visited = new Set<string>();
471
+ const recursionStack = new Set<string>();
472
+
473
+ function hasCycle(stepName: string): boolean {
474
+ if (recursionStack.has(stepName)) {
475
+ return true; // Found cycle
476
+ }
477
+
478
+ if (visited.has(stepName)) {
479
+ return false; // Already checked
480
+ }
481
+
482
+ visited.add(stepName);
483
+ recursionStack.add(stepName);
484
+
485
+ const deps = dependencies.get(stepName) || new Set();
486
+ for (const dep of deps) {
487
+ if (hasCycle(dep)) {
488
+ return true;
489
+ }
490
+ }
491
+
492
+ recursionStack.delete(stepName);
493
+ return false;
494
+ }
495
+
496
+ for (const step of steps) {
497
+ visited.clear();
498
+ recursionStack.clear();
499
+
500
+ if (hasCycle(step.name)) {
501
+ errors.push({
502
+ message: `Circular dependency detected involving step: ${step.name}`,
503
+ stepName: step.name,
504
+ });
505
+ }
506
+ }
507
+
508
+ return errors;
509
+ }
510
+
511
+ /**
512
+ * Extract step dependencies from variable references
513
+ *
514
+ * Looks for ${steps.stepName.*} patterns
515
+ */
516
+ function extractStepDependencies(step: WorkflowStep): Set<string> {
517
+ const dependencies = new Set<string>();
518
+ const variablePattern = /\$\{steps\.([^.}]+)/g;
519
+
520
+ // Convert step to JSON to search all string fields
521
+ const stepJson = JSON.stringify(step);
522
+ let match: RegExpExecArray | null;
523
+
524
+ // biome-ignore lint/suspicious/noAssignInExpressions: Standard regex pattern matching
525
+ while ((match = variablePattern.exec(stepJson)) !== null) {
526
+ const dependency = match[1];
527
+ if (dependency) {
528
+ dependencies.add(dependency);
529
+ }
530
+ }
531
+
532
+ return dependencies;
533
+ }