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,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
|
+
}
|