cli4ai 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.js +105 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +335 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.js +464 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.js +382 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +121 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +125 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +458 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +76 -0
- package/dist/commands/mcp-config.d.ts +10 -0
- package/dist/commands/mcp-config.js +49 -0
- package/dist/commands/remotes.d.ts +22 -0
- package/dist/commands/remotes.js +196 -0
- package/dist/commands/remove.d.ts +8 -0
- package/dist/commands/remove.js +61 -0
- package/dist/commands/routines.d.ts +29 -0
- package/dist/commands/routines.js +363 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +104 -0
- package/dist/commands/scheduler.d.ts +27 -0
- package/dist/commands/scheduler.js +350 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.js +162 -0
- package/dist/commands/secrets.d.ts +28 -0
- package/dist/commands/secrets.js +236 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +27 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +210 -0
- package/dist/core/config.d.ts +91 -0
- package/dist/core/config.js +738 -0
- package/dist/core/execute.d.ts +51 -0
- package/dist/core/execute.js +475 -0
- package/dist/core/link.d.ts +39 -0
- package/dist/core/link.js +214 -0
- package/dist/core/lockfile.d.ts +63 -0
- package/dist/core/lockfile.js +140 -0
- package/dist/core/manifest.d.ts +96 -0
- package/dist/core/manifest.js +224 -0
- package/dist/core/registry.d.ts +74 -0
- package/dist/core/registry.js +116 -0
- package/dist/core/remote-client.d.ts +98 -0
- package/dist/core/remote-client.js +252 -0
- package/dist/core/remotes.d.ts +88 -0
- package/dist/core/remotes.js +206 -0
- package/dist/core/routine-engine.d.ts +124 -0
- package/dist/core/routine-engine.js +699 -0
- package/dist/core/routines.d.ts +36 -0
- package/dist/core/routines.js +132 -0
- package/dist/core/scheduler-daemon.d.ts +10 -0
- package/dist/core/scheduler-daemon.js +77 -0
- package/dist/core/scheduler.d.ts +131 -0
- package/dist/core/scheduler.js +492 -0
- package/dist/core/secrets.d.ts +48 -0
- package/dist/core/secrets.js +384 -0
- package/dist/lib/cli.d.ts +84 -0
- package/dist/lib/cli.js +216 -0
- package/dist/mcp/adapter.d.ts +35 -0
- package/dist/mcp/adapter.js +94 -0
- package/dist/mcp/config-gen.d.ts +31 -0
- package/dist/mcp/config-gen.js +75 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.js +296 -0
- package/dist/server/service.d.ts +85 -0
- package/dist/server/service.js +304 -0
- package/package.json +6 -3
- package/src/bin.ts +0 -118
- package/src/cli.ts +0 -412
- package/src/commands/add.ts +0 -562
- package/src/commands/browse.ts +0 -449
- package/src/commands/config.ts +0 -154
- package/src/commands/info.ts +0 -133
- package/src/commands/init.ts +0 -514
- package/src/commands/list.ts +0 -95
- package/src/commands/mcp-config.ts +0 -69
- package/src/commands/remotes.ts +0 -253
- package/src/commands/remove.ts +0 -78
- package/src/commands/routines.ts +0 -427
- package/src/commands/run.ts +0 -127
- package/src/commands/scheduler.ts +0 -438
- package/src/commands/search.ts +0 -185
- package/src/commands/secrets.ts +0 -292
- package/src/commands/serve.ts +0 -66
- package/src/commands/start.ts +0 -40
- package/src/commands/update.ts +0 -252
- package/src/core/config.ts +0 -845
- package/src/core/execute.ts +0 -569
- package/src/core/link.ts +0 -246
- package/src/core/lockfile.ts +0 -187
- package/src/core/manifest.ts +0 -327
- package/src/core/registry.ts +0 -165
- package/src/core/remote-client.ts +0 -419
- package/src/core/remotes.ts +0 -268
- package/src/core/routine-engine.ts +0 -895
- package/src/core/routines.ts +0 -171
- package/src/core/scheduler-daemon.ts +0 -94
- package/src/core/scheduler.ts +0 -606
- package/src/core/secrets.ts +0 -430
- package/src/lib/cli.ts +0 -261
- package/src/mcp/adapter.ts +0 -131
- package/src/mcp/config-gen.ts +0 -106
- package/src/mcp/server.ts +0 -365
- package/src/server/service.ts +0 -434
|
@@ -1,895 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured routine runner (v1).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
6
|
-
import { spawn } from 'child_process';
|
|
7
|
-
import { parse as parseYaml } from 'yaml';
|
|
8
|
-
import { executeTool, ExecuteToolError } from './execute.js';
|
|
9
|
-
import { remoteRunTool, RemoteConnectionError, RemoteApiError } from './remote-client.js';
|
|
10
|
-
import { getRemote } from './remotes.js';
|
|
11
|
-
|
|
12
|
-
export class RoutineParseError extends Error {
|
|
13
|
-
constructor(
|
|
14
|
-
public path: string,
|
|
15
|
-
message: string
|
|
16
|
-
) {
|
|
17
|
-
super(message);
|
|
18
|
-
this.name = 'RoutineParseError';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class RoutineValidationError extends Error {
|
|
23
|
-
constructor(
|
|
24
|
-
message: string,
|
|
25
|
-
public details?: Record<string, unknown>
|
|
26
|
-
) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = 'RoutineValidationError';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class RoutineTemplateError extends Error {
|
|
33
|
-
constructor(
|
|
34
|
-
message: string,
|
|
35
|
-
public details?: Record<string, unknown>
|
|
36
|
-
) {
|
|
37
|
-
super(message);
|
|
38
|
-
this.name = 'RoutineTemplateError';
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type StepCapture = 'inherit' | 'text' | 'json';
|
|
43
|
-
|
|
44
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
45
|
-
// SCHEDULE TYPES
|
|
46
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
47
|
-
|
|
48
|
-
export interface RoutineSchedule {
|
|
49
|
-
/** Cron expression (e.g., "0 9 * * *" for 9am daily) */
|
|
50
|
-
cron?: string;
|
|
51
|
-
/** Simple interval (e.g., "30s", "5m", "1h", "1d") */
|
|
52
|
-
interval?: string;
|
|
53
|
-
/** IANA timezone (e.g., "Pacific/Auckland"). Defaults to system timezone */
|
|
54
|
-
timezone?: string;
|
|
55
|
-
/** Whether this schedule is active. Defaults to true */
|
|
56
|
-
enabled?: boolean;
|
|
57
|
-
/** Number of retry attempts on failure. Defaults to 0 */
|
|
58
|
-
retries?: number;
|
|
59
|
-
/** Delay between retries in milliseconds. Defaults to 60000 */
|
|
60
|
-
retryDelayMs?: number;
|
|
61
|
-
/** What to do if previous run is still executing. Defaults to 'skip' */
|
|
62
|
-
concurrency?: 'skip' | 'queue';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface RoutineVarDef {
|
|
66
|
-
default?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface RoutineBaseStep {
|
|
70
|
-
id: string;
|
|
71
|
-
type: 'cli4ai' | 'set' | 'exec';
|
|
72
|
-
continueOnError?: boolean;
|
|
73
|
-
timeout?: number;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface RoutineC4aiStep extends RoutineBaseStep {
|
|
77
|
-
type: 'cli4ai';
|
|
78
|
-
package: string;
|
|
79
|
-
command?: string;
|
|
80
|
-
args?: string[];
|
|
81
|
-
env?: Record<string, string>;
|
|
82
|
-
stdin?: string;
|
|
83
|
-
capture?: StepCapture;
|
|
84
|
-
/** Name of a configured remote to execute on (optional) */
|
|
85
|
-
remote?: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface RoutineSetStep extends RoutineBaseStep {
|
|
89
|
-
type: 'set';
|
|
90
|
-
vars: Record<string, string>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface RoutineExecStep extends RoutineBaseStep {
|
|
94
|
-
type: 'exec';
|
|
95
|
-
cmd: string;
|
|
96
|
-
args?: string[];
|
|
97
|
-
env?: Record<string, string>;
|
|
98
|
-
stdin?: string;
|
|
99
|
-
capture?: Exclude<StepCapture, 'inherit'>;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export type RoutineStep = RoutineC4aiStep | RoutineSetStep | RoutineExecStep;
|
|
103
|
-
|
|
104
|
-
export interface RoutineDefinition {
|
|
105
|
-
version: 1;
|
|
106
|
-
name: string;
|
|
107
|
-
description?: string;
|
|
108
|
-
mcp?: { expose?: boolean; description?: string };
|
|
109
|
-
vars?: Record<string, RoutineVarDef>;
|
|
110
|
-
/** Schedule configuration for automatic execution */
|
|
111
|
-
schedule?: RoutineSchedule;
|
|
112
|
-
steps: RoutineStep[];
|
|
113
|
-
result?: unknown;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface StepRunResult {
|
|
117
|
-
id: string;
|
|
118
|
-
type: RoutineStep['type'];
|
|
119
|
-
status: 'success' | 'failed' | 'skipped';
|
|
120
|
-
exitCode?: number;
|
|
121
|
-
durationMs?: number;
|
|
122
|
-
stdout?: string;
|
|
123
|
-
stderr?: string;
|
|
124
|
-
json?: unknown;
|
|
125
|
-
error?: { code: string; message: string; details?: Record<string, unknown> };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface RoutineRunSummary {
|
|
129
|
-
routine: string;
|
|
130
|
-
version: number;
|
|
131
|
-
status: 'success' | 'failed';
|
|
132
|
-
exitCode: number;
|
|
133
|
-
durationMs: number;
|
|
134
|
-
vars: Record<string, unknown>;
|
|
135
|
-
steps: StepRunResult[];
|
|
136
|
-
result?: unknown;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface RoutineDryRunStep {
|
|
140
|
-
id: string;
|
|
141
|
-
type: RoutineStep['type'];
|
|
142
|
-
rendered: Record<string, unknown>;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export interface RoutineDryRunPlan {
|
|
146
|
-
routine: string;
|
|
147
|
-
version: number;
|
|
148
|
-
vars: Record<string, unknown>;
|
|
149
|
-
steps: RoutineDryRunStep[];
|
|
150
|
-
result?: unknown;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function loadRoutineDefinition(path: string): RoutineDefinition {
|
|
154
|
-
let content: string;
|
|
155
|
-
try {
|
|
156
|
-
content = readFileSync(path, 'utf-8');
|
|
157
|
-
} catch (err) {
|
|
158
|
-
throw new RoutineParseError(path, `Failed to read: ${err instanceof Error ? err.message : String(err)}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const isYaml = path.endsWith('.yaml') || path.endsWith('.yml');
|
|
162
|
-
let data: unknown;
|
|
163
|
-
try {
|
|
164
|
-
data = isYaml ? parseYaml(content) : JSON.parse(content);
|
|
165
|
-
} catch (err) {
|
|
166
|
-
const format = isYaml ? 'YAML' : 'JSON';
|
|
167
|
-
throw new RoutineParseError(path, `Invalid ${format}: ${err instanceof Error ? err.message : String(err)}`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return validateRoutineDefinition(data, path);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const INTERVAL_PATTERN = /^(\d+)(s|m|h|d)$/;
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Validate a schedule configuration.
|
|
177
|
-
* Exported for use by scheduler and tests.
|
|
178
|
-
*/
|
|
179
|
-
export function validateScheduleConfig(schedule: unknown, source?: string): RoutineSchedule {
|
|
180
|
-
if (!schedule || typeof schedule !== 'object') {
|
|
181
|
-
throw new RoutineValidationError('Schedule must be an object', { source });
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const s = schedule as Record<string, unknown>;
|
|
185
|
-
|
|
186
|
-
// Must have either cron or interval (or both)
|
|
187
|
-
if (s.cron === undefined && s.interval === undefined) {
|
|
188
|
-
throw new RoutineValidationError('Schedule must have "cron" or "interval"', { source });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Validate cron (basic format check - full validation happens at runtime with cron-parser)
|
|
192
|
-
if (s.cron !== undefined) {
|
|
193
|
-
if (typeof s.cron !== 'string' || s.cron.trim().length === 0) {
|
|
194
|
-
throw new RoutineValidationError('Schedule "cron" must be a non-empty string', { source, got: s.cron });
|
|
195
|
-
}
|
|
196
|
-
// Basic cron format: should have 5 space-separated parts
|
|
197
|
-
const parts = s.cron.trim().split(/\s+/);
|
|
198
|
-
if (parts.length < 5 || parts.length > 6) {
|
|
199
|
-
throw new RoutineValidationError('Schedule "cron" must have 5 or 6 fields', { source, got: s.cron });
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Validate interval
|
|
204
|
-
if (s.interval !== undefined) {
|
|
205
|
-
if (typeof s.interval !== 'string') {
|
|
206
|
-
throw new RoutineValidationError('Schedule "interval" must be a string', { source, got: s.interval });
|
|
207
|
-
}
|
|
208
|
-
if (!INTERVAL_PATTERN.test(s.interval)) {
|
|
209
|
-
throw new RoutineValidationError('Schedule "interval" must be like "30s", "5m", "1h", or "1d"', { source, got: s.interval });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Validate timezone (just check it's a string - actual validation happens at runtime)
|
|
214
|
-
if (s.timezone !== undefined && typeof s.timezone !== 'string') {
|
|
215
|
-
throw new RoutineValidationError('Schedule "timezone" must be a string', { source, got: s.timezone });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Validate enabled
|
|
219
|
-
if (s.enabled !== undefined && typeof s.enabled !== 'boolean') {
|
|
220
|
-
throw new RoutineValidationError('Schedule "enabled" must be a boolean', { source, got: s.enabled });
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Validate retries
|
|
224
|
-
if (s.retries !== undefined) {
|
|
225
|
-
if (typeof s.retries !== 'number' || !Number.isInteger(s.retries) || s.retries < 0) {
|
|
226
|
-
throw new RoutineValidationError('Schedule "retries" must be a non-negative integer', { source, got: s.retries });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Validate retryDelayMs
|
|
231
|
-
if (s.retryDelayMs !== undefined) {
|
|
232
|
-
if (typeof s.retryDelayMs !== 'number' || !Number.isInteger(s.retryDelayMs) || s.retryDelayMs < 0) {
|
|
233
|
-
throw new RoutineValidationError('Schedule "retryDelayMs" must be a non-negative integer', { source, got: s.retryDelayMs });
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Validate concurrency
|
|
238
|
-
if (s.concurrency !== undefined) {
|
|
239
|
-
if (s.concurrency !== 'skip' && s.concurrency !== 'queue') {
|
|
240
|
-
throw new RoutineValidationError('Schedule "concurrency" must be "skip" or "queue"', { source, got: s.concurrency });
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return s as RoutineSchedule;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function validateRoutineDefinition(value: unknown, source?: string): RoutineDefinition {
|
|
248
|
-
if (!value || typeof value !== 'object') {
|
|
249
|
-
throw new RoutineValidationError('Routine must be an object', { source });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const obj = value as Record<string, unknown>;
|
|
253
|
-
|
|
254
|
-
if (obj.version !== 1) {
|
|
255
|
-
throw new RoutineValidationError('Invalid or missing "version" (must be 1)', { source, got: obj.version });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (typeof obj.name !== 'string' || obj.name.length === 0) {
|
|
259
|
-
throw new RoutineValidationError('Invalid or missing "name"', { source, got: obj.name });
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Validate schedule if present
|
|
263
|
-
if (obj.schedule !== undefined) {
|
|
264
|
-
validateScheduleConfig(obj.schedule, source);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (!Array.isArray(obj.steps)) {
|
|
268
|
-
throw new RoutineValidationError('Invalid or missing "steps" (must be an array)', { source });
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const steps = obj.steps as unknown[];
|
|
272
|
-
const ids = new Set<string>();
|
|
273
|
-
|
|
274
|
-
for (const [idx, step] of steps.entries()) {
|
|
275
|
-
if (!step || typeof step !== 'object') {
|
|
276
|
-
throw new RoutineValidationError('Invalid step (must be an object)', { source, index: idx });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const s = step as Record<string, unknown>;
|
|
280
|
-
|
|
281
|
-
if (typeof s.id !== 'string' || s.id.length === 0) {
|
|
282
|
-
throw new RoutineValidationError('Step missing "id"', { source, index: idx });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (ids.has(s.id)) {
|
|
286
|
-
throw new RoutineValidationError(`Duplicate step id: ${s.id}`, { source, id: s.id });
|
|
287
|
-
}
|
|
288
|
-
ids.add(s.id);
|
|
289
|
-
|
|
290
|
-
if (typeof s.type !== 'string' || !['cli4ai', 'set', 'exec'].includes(s.type)) {
|
|
291
|
-
throw new RoutineValidationError(`Invalid step type for "${s.id}"`, { source, id: s.id, got: s.type });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (s.timeout !== undefined && typeof s.timeout !== 'number') {
|
|
295
|
-
throw new RoutineValidationError(`Invalid timeout for "${s.id}" (must be number ms)`, { source, id: s.id, got: s.timeout });
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (s.continueOnError !== undefined && typeof s.continueOnError !== 'boolean') {
|
|
299
|
-
throw new RoutineValidationError(`Invalid continueOnError for "${s.id}" (must be boolean)`, { source, id: s.id, got: s.continueOnError });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (s.type === 'cli4ai') {
|
|
303
|
-
if (typeof s.package !== 'string' || s.package.length === 0) {
|
|
304
|
-
throw new RoutineValidationError(`Step "${s.id}" missing "package"`, { source, id: s.id });
|
|
305
|
-
}
|
|
306
|
-
if (s.command !== undefined && typeof s.command !== 'string') {
|
|
307
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "command"`, { source, id: s.id });
|
|
308
|
-
}
|
|
309
|
-
if (s.args !== undefined && (!Array.isArray(s.args) || s.args.some(a => typeof a !== 'string'))) {
|
|
310
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "args" (must be string array)`, { source, id: s.id });
|
|
311
|
-
}
|
|
312
|
-
if (s.env !== undefined) {
|
|
313
|
-
if (typeof s.env !== 'object' || s.env === null) {
|
|
314
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "env" (must be object)`, { source, id: s.id });
|
|
315
|
-
}
|
|
316
|
-
for (const [k, v] of Object.entries(s.env as Record<string, unknown>)) {
|
|
317
|
-
if (typeof v !== 'string') {
|
|
318
|
-
throw new RoutineValidationError(`Step "${s.id}" env.${k} must be a string template`, { source, id: s.id });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (s.stdin !== undefined && typeof s.stdin !== 'string') {
|
|
323
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "stdin"`, { source, id: s.id });
|
|
324
|
-
}
|
|
325
|
-
if (s.capture !== undefined && !['inherit', 'text', 'json'].includes(String(s.capture))) {
|
|
326
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "capture"`, { source, id: s.id, got: s.capture });
|
|
327
|
-
}
|
|
328
|
-
if (s.remote !== undefined && typeof s.remote !== 'string') {
|
|
329
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "remote" (must be string)`, { source, id: s.id, got: s.remote });
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (s.type === 'set') {
|
|
334
|
-
if (typeof s.vars !== 'object' || s.vars === null) {
|
|
335
|
-
throw new RoutineValidationError(`Step "${s.id}" missing "vars"`, { source, id: s.id });
|
|
336
|
-
}
|
|
337
|
-
for (const [k, v] of Object.entries(s.vars as Record<string, unknown>)) {
|
|
338
|
-
if (typeof v !== 'string') {
|
|
339
|
-
throw new RoutineValidationError(`Step "${s.id}" vars.${k} must be a string template`, { source, id: s.id });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (s.type === 'exec') {
|
|
345
|
-
if (typeof s.cmd !== 'string' || s.cmd.length === 0) {
|
|
346
|
-
throw new RoutineValidationError(`Step "${s.id}" missing "cmd"`, { source, id: s.id });
|
|
347
|
-
}
|
|
348
|
-
if (s.args !== undefined && (!Array.isArray(s.args) || s.args.some(a => typeof a !== 'string'))) {
|
|
349
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "args" (must be string array)`, { source, id: s.id });
|
|
350
|
-
}
|
|
351
|
-
if (s.env !== undefined) {
|
|
352
|
-
if (typeof s.env !== 'object' || s.env === null) {
|
|
353
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "env" (must be object)`, { source, id: s.id });
|
|
354
|
-
}
|
|
355
|
-
for (const [k, v] of Object.entries(s.env as Record<string, unknown>)) {
|
|
356
|
-
if (typeof v !== 'string') {
|
|
357
|
-
throw new RoutineValidationError(`Step "${s.id}" env.${k} must be a string template`, { source, id: s.id });
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (s.stdin !== undefined && typeof s.stdin !== 'string') {
|
|
362
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "stdin"`, { source, id: s.id });
|
|
363
|
-
}
|
|
364
|
-
if (s.capture !== undefined && !['text', 'json'].includes(String(s.capture))) {
|
|
365
|
-
throw new RoutineValidationError(`Step "${s.id}" has invalid "capture"`, { source, id: s.id, got: s.capture });
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return obj as unknown as RoutineDefinition;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function getPath(root: unknown, segments: string[]): unknown {
|
|
374
|
-
let current: unknown = root;
|
|
375
|
-
for (const seg of segments) {
|
|
376
|
-
if (current === null || current === undefined) return undefined;
|
|
377
|
-
|
|
378
|
-
if (Array.isArray(current)) {
|
|
379
|
-
const idx = Number(seg);
|
|
380
|
-
if (!Number.isInteger(idx)) return undefined;
|
|
381
|
-
current = current[idx];
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (typeof current !== 'object') return undefined;
|
|
386
|
-
|
|
387
|
-
current = (current as Record<string, unknown>)[seg];
|
|
388
|
-
}
|
|
389
|
-
return current;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function renderTemplateString(
|
|
393
|
-
template: string,
|
|
394
|
-
ctx: Record<string, unknown>,
|
|
395
|
-
meta: { stepId?: string; field?: string; strict?: boolean }
|
|
396
|
-
): unknown {
|
|
397
|
-
const regex = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
398
|
-
const matches = [...template.matchAll(regex)];
|
|
399
|
-
if (matches.length === 0) return template;
|
|
400
|
-
|
|
401
|
-
const resolveExpr = (expr: string): unknown => {
|
|
402
|
-
const parts = expr.split('.').map(s => s.trim()).filter(Boolean);
|
|
403
|
-
const value = getPath(ctx, parts);
|
|
404
|
-
if (value === undefined) {
|
|
405
|
-
if (meta.strict === false) {
|
|
406
|
-
return `{{${expr}}}`;
|
|
407
|
-
}
|
|
408
|
-
throw new RoutineTemplateError(`Template error${meta.stepId ? ` in step "${meta.stepId}"` : ''}: path "${expr}" not found`, {
|
|
409
|
-
step: meta.stepId,
|
|
410
|
-
field: meta.field,
|
|
411
|
-
path: expr
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
return value;
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// If the template is exactly a single {{expr}}, return the raw value.
|
|
418
|
-
if (matches.length === 1 && matches[0].index === 0 && matches[0][0].length === template.length) {
|
|
419
|
-
return resolveExpr(matches[0][1]);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
let out = '';
|
|
423
|
-
let lastIndex = 0;
|
|
424
|
-
for (const m of matches) {
|
|
425
|
-
const full = m[0];
|
|
426
|
-
const expr = m[1];
|
|
427
|
-
const idx = m.index ?? 0;
|
|
428
|
-
out += template.slice(lastIndex, idx);
|
|
429
|
-
const value = resolveExpr(expr);
|
|
430
|
-
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
431
|
-
out += String(value);
|
|
432
|
-
} else {
|
|
433
|
-
out += JSON.stringify(value);
|
|
434
|
-
}
|
|
435
|
-
lastIndex = idx + full.length;
|
|
436
|
-
}
|
|
437
|
-
out += template.slice(lastIndex);
|
|
438
|
-
return out;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function renderString(value: string, ctx: Record<string, unknown>, meta: { stepId?: string; field?: string; strict?: boolean }): string {
|
|
442
|
-
const rendered = renderTemplateString(value, ctx, meta);
|
|
443
|
-
if (rendered === undefined || rendered === null) {
|
|
444
|
-
throw new RoutineTemplateError(`Template error${meta.stepId ? ` in step "${meta.stepId}"` : ''}: "${value}" resolved to empty`, {
|
|
445
|
-
step: meta.stepId,
|
|
446
|
-
field: meta.field
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
if (typeof rendered === 'string') return rendered;
|
|
450
|
-
if (typeof rendered === 'number' || typeof rendered === 'boolean') return String(rendered);
|
|
451
|
-
try {
|
|
452
|
-
return JSON.stringify(rendered);
|
|
453
|
-
} catch {
|
|
454
|
-
throw new RoutineTemplateError(`Template error${meta.stepId ? ` in step "${meta.stepId}"` : ''}: "${value}" resolved to non-serializable value`, {
|
|
455
|
-
step: meta.stepId,
|
|
456
|
-
field: meta.field
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function renderScalarString(value: string, ctx: Record<string, unknown>, meta: { stepId?: string; field?: string; strict?: boolean }): string {
|
|
462
|
-
const rendered = renderTemplateString(value, ctx, meta);
|
|
463
|
-
if (rendered === undefined || rendered === null) {
|
|
464
|
-
throw new RoutineTemplateError(`Template error${meta.stepId ? ` in step "${meta.stepId}"` : ''}: "${value}" resolved to empty`, {
|
|
465
|
-
step: meta.stepId,
|
|
466
|
-
field: meta.field
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
if (typeof rendered === 'string' || typeof rendered === 'number' || typeof rendered === 'boolean') {
|
|
470
|
-
return String(rendered);
|
|
471
|
-
}
|
|
472
|
-
throw new RoutineTemplateError(
|
|
473
|
-
`Template error${meta.stepId ? ` in step "${meta.stepId}"` : ''}: "${value}" did not resolve to a string`,
|
|
474
|
-
{
|
|
475
|
-
step: meta.stepId,
|
|
476
|
-
field: meta.field,
|
|
477
|
-
gotType: Array.isArray(rendered) ? 'array' : typeof rendered
|
|
478
|
-
}
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function renderStringArray(values: string[] | undefined, ctx: Record<string, unknown>, meta: { stepId?: string; field?: string; strict?: boolean }): string[] {
|
|
483
|
-
if (!values) return [];
|
|
484
|
-
return values.map((v, i) => renderString(v, ctx, { ...meta, field: `${meta.field ?? 'args'}.${i}` }));
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function renderEnv(env: Record<string, string> | undefined, ctx: Record<string, unknown>, meta: { stepId?: string; field?: string; strict?: boolean }): Record<string, string> {
|
|
488
|
-
if (!env) return {};
|
|
489
|
-
const out: Record<string, string> = {};
|
|
490
|
-
for (const [k, v] of Object.entries(env)) {
|
|
491
|
-
out[k] = renderString(v, ctx, { ...meta, field: `${meta.field ?? 'env'}.${k}` });
|
|
492
|
-
}
|
|
493
|
-
return out;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function renderTemplateValue(value: unknown, ctx: Record<string, unknown>, meta: { stepId?: string; field?: string; strict?: boolean }): unknown {
|
|
497
|
-
if (typeof value === 'string') {
|
|
498
|
-
return renderTemplateString(value, ctx, meta);
|
|
499
|
-
}
|
|
500
|
-
if (Array.isArray(value)) {
|
|
501
|
-
return value.map((item, idx) => renderTemplateValue(item, ctx, { ...meta, field: `${meta.field ?? 'value'}.${idx}` }));
|
|
502
|
-
}
|
|
503
|
-
if (value && typeof value === 'object') {
|
|
504
|
-
const out: Record<string, unknown> = {};
|
|
505
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
506
|
-
out[k] = renderTemplateValue(v, ctx, { ...meta, field: `${meta.field ?? 'value'}.${k}` });
|
|
507
|
-
}
|
|
508
|
-
return out;
|
|
509
|
-
}
|
|
510
|
-
return value;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function collectStream(stream: NodeJS.ReadableStream): Promise<string> {
|
|
514
|
-
return new Promise((resolve, reject) => {
|
|
515
|
-
const chunks: Buffer[] = [];
|
|
516
|
-
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
517
|
-
stream.on('error', reject);
|
|
518
|
-
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
async function runExecStep(step: RoutineExecStep, ctx: Record<string, unknown>, invocationDir: string): Promise<StepRunResult> {
|
|
523
|
-
const startTime = Date.now();
|
|
524
|
-
|
|
525
|
-
const cmd = renderScalarString(step.cmd, ctx, { stepId: step.id, field: 'cmd' });
|
|
526
|
-
const args = renderStringArray(step.args, ctx, { stepId: step.id, field: 'args' });
|
|
527
|
-
const env = renderEnv(step.env, ctx, { stepId: step.id, field: 'env' });
|
|
528
|
-
const stdin = step.stdin !== undefined ? renderString(step.stdin, ctx, { stepId: step.id, field: 'stdin' }) : undefined;
|
|
529
|
-
|
|
530
|
-
const child = spawn(cmd, args, {
|
|
531
|
-
cwd: invocationDir,
|
|
532
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
533
|
-
env: {
|
|
534
|
-
...process.env,
|
|
535
|
-
...(env ?? {})
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
if (stdin !== undefined && child.stdin) {
|
|
540
|
-
child.stdin.write(stdin);
|
|
541
|
-
}
|
|
542
|
-
if (child.stdin) child.stdin.end();
|
|
543
|
-
|
|
544
|
-
const stdoutPromise = child.stdout ? collectStream(child.stdout) : Promise.resolve('');
|
|
545
|
-
const stderrPromise = child.stderr ? collectStream(child.stderr) : Promise.resolve('');
|
|
546
|
-
|
|
547
|
-
if (child.stderr) {
|
|
548
|
-
child.stderr.on('data', (chunk) => process.stderr.write(chunk));
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
let timeout: NodeJS.Timeout | undefined;
|
|
552
|
-
if (step.timeout && step.timeout > 0) {
|
|
553
|
-
timeout = setTimeout(() => {
|
|
554
|
-
try {
|
|
555
|
-
child.kill('SIGTERM');
|
|
556
|
-
} catch {}
|
|
557
|
-
setTimeout(() => {
|
|
558
|
-
try {
|
|
559
|
-
child.kill('SIGKILL');
|
|
560
|
-
} catch {}
|
|
561
|
-
}, 250);
|
|
562
|
-
}, step.timeout);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
566
|
-
child.on('close', (code) => resolve(code ?? 0));
|
|
567
|
-
child.on('error', (err) => reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`)));
|
|
568
|
-
}).finally(() => {
|
|
569
|
-
if (timeout) clearTimeout(timeout);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
573
|
-
|
|
574
|
-
const capture = step.capture ?? 'text';
|
|
575
|
-
let json: unknown | undefined;
|
|
576
|
-
if (capture === 'json') {
|
|
577
|
-
try {
|
|
578
|
-
json = JSON.parse(stdout);
|
|
579
|
-
} catch {
|
|
580
|
-
throw new RoutineTemplateError(`JSON parse error in step "${step.id}": stdout is not valid JSON`, {
|
|
581
|
-
step: step.id,
|
|
582
|
-
stdout: stdout.slice(0, 200)
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
return {
|
|
588
|
-
id: step.id,
|
|
589
|
-
type: step.type,
|
|
590
|
-
status: exitCode === 0 ? 'success' : 'failed',
|
|
591
|
-
exitCode,
|
|
592
|
-
durationMs: Date.now() - startTime,
|
|
593
|
-
stdout,
|
|
594
|
-
stderr,
|
|
595
|
-
json
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
export async function dryRunRoutine(def: RoutineDefinition, vars: Record<string, string>, invocationDir: string): Promise<RoutineDryRunPlan> {
|
|
600
|
-
const ctxVars: Record<string, unknown> = {};
|
|
601
|
-
|
|
602
|
-
for (const [k, v] of Object.entries(def.vars ?? {})) {
|
|
603
|
-
if (v.default !== undefined) ctxVars[k] = v.default;
|
|
604
|
-
}
|
|
605
|
-
for (const [k, v] of Object.entries(vars)) ctxVars[k] = v;
|
|
606
|
-
|
|
607
|
-
const ctx: Record<string, unknown> = {
|
|
608
|
-
vars: ctxVars,
|
|
609
|
-
steps: {}
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
const steps: RoutineDryRunStep[] = [];
|
|
613
|
-
|
|
614
|
-
for (const step of def.steps) {
|
|
615
|
-
if (step.type === 'set') {
|
|
616
|
-
const renderedVars: Record<string, unknown> = {};
|
|
617
|
-
for (const [k, v] of Object.entries(step.vars)) {
|
|
618
|
-
renderedVars[k] = renderTemplateString(v, ctx, { stepId: step.id, field: `vars.${k}`, strict: false });
|
|
619
|
-
}
|
|
620
|
-
// Apply to context for subsequent steps
|
|
621
|
-
for (const [k, v] of Object.entries(renderedVars)) ctxVars[k] = v;
|
|
622
|
-
|
|
623
|
-
steps.push({ id: step.id, type: step.type, rendered: { vars: renderedVars } });
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (step.type === 'cli4ai') {
|
|
628
|
-
const rendered = {
|
|
629
|
-
package: renderScalarString(step.package, ctx, { stepId: step.id, field: 'package', strict: false }),
|
|
630
|
-
command: step.command ? renderScalarString(step.command, ctx, { stepId: step.id, field: 'command', strict: false }) : undefined,
|
|
631
|
-
args: renderStringArray(step.args, ctx, { stepId: step.id, field: 'args', strict: false }),
|
|
632
|
-
env: renderEnv(step.env, ctx, { stepId: step.id, field: 'env', strict: false }),
|
|
633
|
-
stdin: step.stdin ? renderString(step.stdin, ctx, { stepId: step.id, field: 'stdin', strict: false }) : undefined,
|
|
634
|
-
timeout: step.timeout
|
|
635
|
-
};
|
|
636
|
-
steps.push({ id: step.id, type: step.type, rendered });
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (step.type === 'exec') {
|
|
641
|
-
const rendered = {
|
|
642
|
-
cmd: renderScalarString(step.cmd, ctx, { stepId: step.id, field: 'cmd', strict: false }),
|
|
643
|
-
args: renderStringArray(step.args, ctx, { stepId: step.id, field: 'args', strict: false }),
|
|
644
|
-
env: renderEnv(step.env, ctx, { stepId: step.id, field: 'env', strict: false }),
|
|
645
|
-
stdin: step.stdin ? renderString(step.stdin, ctx, { stepId: step.id, field: 'stdin', strict: false }) : undefined,
|
|
646
|
-
timeout: step.timeout
|
|
647
|
-
};
|
|
648
|
-
steps.push({ id: step.id, type: step.type, rendered });
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const result = def.result !== undefined ? renderTemplateValue(def.result, ctx, { field: 'result', strict: false }) : undefined;
|
|
654
|
-
|
|
655
|
-
return {
|
|
656
|
-
routine: def.name,
|
|
657
|
-
version: def.version,
|
|
658
|
-
vars: ctxVars,
|
|
659
|
-
steps,
|
|
660
|
-
result
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export async function runRoutine(def: RoutineDefinition, vars: Record<string, string>, invocationDir: string): Promise<RoutineRunSummary> {
|
|
665
|
-
const startTime = Date.now();
|
|
666
|
-
|
|
667
|
-
const ctxVars: Record<string, unknown> = {};
|
|
668
|
-
for (const [k, v] of Object.entries(def.vars ?? {})) {
|
|
669
|
-
if (v.default !== undefined) ctxVars[k] = v.default;
|
|
670
|
-
}
|
|
671
|
-
for (const [k, v] of Object.entries(vars)) ctxVars[k] = v;
|
|
672
|
-
|
|
673
|
-
const stepsById: Record<string, StepRunResult> = {};
|
|
674
|
-
|
|
675
|
-
const ctx: Record<string, unknown> = {
|
|
676
|
-
vars: ctxVars,
|
|
677
|
-
steps: stepsById
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
const steps: StepRunResult[] = [];
|
|
681
|
-
|
|
682
|
-
for (const step of def.steps) {
|
|
683
|
-
if (step.type === 'set') {
|
|
684
|
-
const assigned: Record<string, unknown> = {};
|
|
685
|
-
for (const [k, v] of Object.entries(step.vars)) {
|
|
686
|
-
assigned[k] = renderTemplateString(v, ctx, { stepId: step.id, field: `vars.${k}` });
|
|
687
|
-
}
|
|
688
|
-
for (const [k, v] of Object.entries(assigned)) ctxVars[k] = v;
|
|
689
|
-
|
|
690
|
-
const res: StepRunResult = {
|
|
691
|
-
id: step.id,
|
|
692
|
-
type: step.type,
|
|
693
|
-
status: 'success',
|
|
694
|
-
exitCode: 0,
|
|
695
|
-
durationMs: 0,
|
|
696
|
-
json: assigned
|
|
697
|
-
};
|
|
698
|
-
stepsById[step.id] = res;
|
|
699
|
-
steps.push(res);
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (step.type === 'exec') {
|
|
704
|
-
try {
|
|
705
|
-
const res = await runExecStep(step, ctx, invocationDir);
|
|
706
|
-
stepsById[step.id] = res;
|
|
707
|
-
steps.push(res);
|
|
708
|
-
|
|
709
|
-
if (res.exitCode !== 0 && !step.continueOnError) {
|
|
710
|
-
return finalizeFailure(def, ctxVars, steps, Date.now() - startTime, res.exitCode ?? 1);
|
|
711
|
-
}
|
|
712
|
-
continue;
|
|
713
|
-
} catch (err) {
|
|
714
|
-
const error = normalizeError(err);
|
|
715
|
-
const res: StepRunResult = { id: step.id, type: step.type, status: 'failed', exitCode: 1, error };
|
|
716
|
-
stepsById[step.id] = res;
|
|
717
|
-
steps.push(res);
|
|
718
|
-
if (!step.continueOnError) {
|
|
719
|
-
return finalizeFailure(def, ctxVars, steps, Date.now() - startTime, 1);
|
|
720
|
-
}
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
if (step.type === 'cli4ai') {
|
|
726
|
-
const pkg = renderScalarString(step.package, ctx, { stepId: step.id, field: 'package' });
|
|
727
|
-
const cmd = step.command !== undefined ? renderScalarString(step.command, ctx, { stepId: step.id, field: 'command' }) : undefined;
|
|
728
|
-
const args = renderStringArray(step.args, ctx, { stepId: step.id, field: 'args' });
|
|
729
|
-
const env = renderEnv(step.env, ctx, { stepId: step.id, field: 'env' });
|
|
730
|
-
const stdin = step.stdin !== undefined ? renderString(step.stdin, ctx, { stepId: step.id, field: 'stdin' }) : undefined;
|
|
731
|
-
const remoteName = step.remote !== undefined ? renderScalarString(step.remote, ctx, { stepId: step.id, field: 'remote' }) : undefined;
|
|
732
|
-
|
|
733
|
-
const capture = step.capture ?? 'text';
|
|
734
|
-
|
|
735
|
-
try {
|
|
736
|
-
let execResult: { exitCode: number; durationMs: number; stdout?: string; stderr?: string };
|
|
737
|
-
|
|
738
|
-
if (remoteName) {
|
|
739
|
-
// Remote execution
|
|
740
|
-
const remote = getRemote(remoteName);
|
|
741
|
-
if (!remote) {
|
|
742
|
-
throw new ExecuteToolError('NOT_FOUND', `Remote "${remoteName}" not found`, {
|
|
743
|
-
step: step.id,
|
|
744
|
-
hint: 'Use "cli4ai remotes add <name> <url>" to configure a remote'
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const remoteRes = await remoteRunTool(remoteName, {
|
|
749
|
-
package: pkg,
|
|
750
|
-
command: cmd,
|
|
751
|
-
args,
|
|
752
|
-
env: Object.keys(env).length > 0 ? env : undefined,
|
|
753
|
-
stdin,
|
|
754
|
-
timeout: step.timeout
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
execResult = {
|
|
758
|
-
exitCode: remoteRes.exitCode,
|
|
759
|
-
durationMs: remoteRes.durationMs,
|
|
760
|
-
stdout: remoteRes.stdout,
|
|
761
|
-
stderr: remoteRes.stderr
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
// Log stderr from remote
|
|
765
|
-
if (remoteRes.stderr) {
|
|
766
|
-
process.stderr.write(remoteRes.stderr);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// Handle remote-level errors
|
|
770
|
-
if (remoteRes.error && !remoteRes.success) {
|
|
771
|
-
throw new ExecuteToolError(
|
|
772
|
-
remoteRes.error.code,
|
|
773
|
-
remoteRes.error.message,
|
|
774
|
-
remoteRes.error.details
|
|
775
|
-
);
|
|
776
|
-
}
|
|
777
|
-
} else {
|
|
778
|
-
// Local execution
|
|
779
|
-
const localRes = await executeTool({
|
|
780
|
-
packageName: pkg,
|
|
781
|
-
command: cmd,
|
|
782
|
-
args,
|
|
783
|
-
cwd: invocationDir,
|
|
784
|
-
env,
|
|
785
|
-
stdin,
|
|
786
|
-
capture: 'pipe',
|
|
787
|
-
timeoutMs: step.timeout,
|
|
788
|
-
teeStderr: true
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
execResult = {
|
|
792
|
-
exitCode: localRes.exitCode,
|
|
793
|
-
durationMs: localRes.durationMs,
|
|
794
|
-
stdout: localRes.stdout,
|
|
795
|
-
stderr: localRes.stderr
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const res: StepRunResult = {
|
|
800
|
-
id: step.id,
|
|
801
|
-
type: step.type,
|
|
802
|
-
status: execResult.exitCode === 0 ? 'success' : 'failed',
|
|
803
|
-
exitCode: execResult.exitCode,
|
|
804
|
-
durationMs: execResult.durationMs,
|
|
805
|
-
stdout: execResult.stdout,
|
|
806
|
-
stderr: execResult.stderr
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
if (capture === 'json') {
|
|
810
|
-
try {
|
|
811
|
-
res.json = execResult.stdout ? JSON.parse(execResult.stdout) : null;
|
|
812
|
-
} catch {
|
|
813
|
-
throw new RoutineTemplateError(`JSON parse error in step "${step.id}": stdout is not valid JSON`, {
|
|
814
|
-
step: step.id,
|
|
815
|
-
stdout: (execResult.stdout ?? '').slice(0, 200)
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
stepsById[step.id] = res;
|
|
821
|
-
steps.push(res);
|
|
822
|
-
|
|
823
|
-
if (execResult.exitCode !== 0 && !step.continueOnError) {
|
|
824
|
-
return finalizeFailure(def, ctxVars, steps, Date.now() - startTime, execResult.exitCode);
|
|
825
|
-
}
|
|
826
|
-
continue;
|
|
827
|
-
} catch (err) {
|
|
828
|
-
const error = normalizeError(err);
|
|
829
|
-
const res: StepRunResult = {
|
|
830
|
-
id: step.id,
|
|
831
|
-
type: step.type,
|
|
832
|
-
status: 'failed',
|
|
833
|
-
exitCode: error.code === 'INTEGRITY_ERROR' ? 1 : 1,
|
|
834
|
-
error
|
|
835
|
-
};
|
|
836
|
-
stepsById[step.id] = res;
|
|
837
|
-
steps.push(res);
|
|
838
|
-
if (!step.continueOnError) {
|
|
839
|
-
return finalizeFailure(def, ctxVars, steps, Date.now() - startTime, 1);
|
|
840
|
-
}
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
const result = def.result !== undefined ? renderTemplateValue(def.result, ctx, { field: 'result' }) : undefined;
|
|
847
|
-
|
|
848
|
-
return {
|
|
849
|
-
routine: def.name,
|
|
850
|
-
version: def.version,
|
|
851
|
-
status: 'success',
|
|
852
|
-
exitCode: 0,
|
|
853
|
-
durationMs: Date.now() - startTime,
|
|
854
|
-
vars: ctxVars,
|
|
855
|
-
steps,
|
|
856
|
-
result
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function normalizeError(err: unknown): { code: string; message: string; details?: Record<string, unknown> } {
|
|
861
|
-
if (err instanceof ExecuteToolError) {
|
|
862
|
-
return { code: err.code, message: err.message, details: err.details };
|
|
863
|
-
}
|
|
864
|
-
if (err instanceof RemoteConnectionError) {
|
|
865
|
-
return { code: 'NETWORK_ERROR', message: err.message, details: { remote: err.remoteName, url: err.url } };
|
|
866
|
-
}
|
|
867
|
-
if (err instanceof RemoteApiError) {
|
|
868
|
-
return { code: err.code, message: err.message, details: err.details };
|
|
869
|
-
}
|
|
870
|
-
if (err instanceof RoutineTemplateError) {
|
|
871
|
-
return { code: 'INVALID_INPUT', message: err.message, details: err.details };
|
|
872
|
-
}
|
|
873
|
-
if (err instanceof RoutineValidationError) {
|
|
874
|
-
return { code: 'INVALID_INPUT', message: err.message, details: err.details };
|
|
875
|
-
}
|
|
876
|
-
if (err instanceof RoutineParseError) {
|
|
877
|
-
return { code: 'PARSE_ERROR', message: err.message, details: { path: err.path } };
|
|
878
|
-
}
|
|
879
|
-
if (err instanceof Error) {
|
|
880
|
-
return { code: 'API_ERROR', message: err.message };
|
|
881
|
-
}
|
|
882
|
-
return { code: 'API_ERROR', message: String(err) };
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
function finalizeFailure(def: RoutineDefinition, vars: Record<string, unknown>, steps: StepRunResult[], durationMs: number, exitCode: number): RoutineRunSummary {
|
|
886
|
-
return {
|
|
887
|
-
routine: def.name,
|
|
888
|
-
version: def.version,
|
|
889
|
-
status: 'failed',
|
|
890
|
-
exitCode,
|
|
891
|
-
durationMs,
|
|
892
|
-
vars,
|
|
893
|
-
steps
|
|
894
|
-
};
|
|
895
|
-
}
|