@agentled/cli 0.6.5 → 0.6.7

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 (50) hide show
  1. package/README.md +25 -0
  2. package/dist/commands/agents.js +7 -2
  3. package/dist/commands/agents.js.map +1 -1
  4. package/dist/commands/auth.js +18 -0
  5. package/dist/commands/auth.js.map +1 -1
  6. package/dist/commands/dryrun.d.ts +23 -0
  7. package/dist/commands/dryrun.js +641 -0
  8. package/dist/commands/dryrun.js.map +1 -0
  9. package/dist/commands/executions.js +67 -0
  10. package/dist/commands/executions.js.map +1 -1
  11. package/dist/commands/fixture.d.ts +10 -0
  12. package/dist/commands/fixture.js +155 -0
  13. package/dist/commands/fixture.js.map +1 -0
  14. package/dist/commands/group-manifest.d.ts +15 -0
  15. package/dist/commands/group-manifest.js +488 -0
  16. package/dist/commands/group-manifest.js.map +1 -0
  17. package/dist/commands/init.d.ts +17 -0
  18. package/dist/commands/init.js +83 -0
  19. package/dist/commands/init.js.map +1 -0
  20. package/dist/commands/onboarding.js +70 -3
  21. package/dist/commands/onboarding.js.map +1 -1
  22. package/dist/commands/test.d.ts +15 -0
  23. package/dist/commands/test.js +98 -0
  24. package/dist/commands/test.js.map +1 -0
  25. package/dist/commands/workflows.js +104 -0
  26. package/dist/commands/workflows.js.map +1 -1
  27. package/dist/commands/workspace.js +65 -0
  28. package/dist/commands/workspace.js.map +1 -1
  29. package/dist/context-schema.js +3 -4
  30. package/dist/context-schema.js.map +1 -1
  31. package/dist/index.js +10 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/utils/browser-auth.js +6 -2
  34. package/dist/utils/browser-auth.js.map +1 -1
  35. package/dist/utils/pipeline-lint.d.ts +20 -0
  36. package/dist/utils/pipeline-lint.js +348 -0
  37. package/dist/utils/pipeline-lint.js.map +1 -0
  38. package/dist/utils/preflight.js +1 -1
  39. package/dist/utils/test-runner.d.ts +85 -0
  40. package/dist/utils/test-runner.js +283 -0
  41. package/dist/utils/test-runner.js.map +1 -0
  42. package/dist/utils/workspace-folder.d.ts +46 -0
  43. package/dist/utils/workspace-folder.js +340 -0
  44. package/dist/utils/workspace-folder.js.map +1 -0
  45. package/llms-full.txt +13 -0
  46. package/package.json +1 -1
  47. package/patterns/v1/12-event-driven-workflow-groups.md +288 -0
  48. package/scaffolds/extract-threshold-alert.json +3 -3
  49. package/skills/agentled/SKILL.md +102 -26
  50. package/skills/agentled/WHY-AGENTLED.md +15 -0
@@ -0,0 +1,641 @@
1
+ /**
2
+ * `agentled dryrun <file>` — zero-credit variable-resolution walk over a pipeline.
3
+ *
4
+ * Walks the step graph from the trigger, synthesizes mock outputs for each step
5
+ * (from responseStructure / app output schemas / fixture data), substitutes
6
+ * template variables, and reports:
7
+ *
8
+ * - Unresolved `{{steps.X.field}}` / `{{input.Y}}` / `{{currentItem.Z}}` refs
9
+ * - Always-skip / always-stop branches based on entryConditions
10
+ * - Credit estimate (sum of step.creditCost or per-action defaults)
11
+ *
12
+ * Inputs (in priority order, latest wins):
13
+ * 1. Synthetic defaults from context.executionInputConfig.fields
14
+ * 2. --input '<json>' or --input-file <path>
15
+ * 3. --mocks <path> — full {stepId: output} object overriding any synthetic
16
+ * 4. --from-execution <execId> — load each step's output from
17
+ * fixtures/step-outputs/<execId>/<stepId>.json (most realistic)
18
+ *
19
+ * Exit codes: 0 clean, 1 warnings (always-skip branches, no terminal step),
20
+ * 2 unresolved refs.
21
+ */
22
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
23
+ import { join, resolve } from 'node:path';
24
+ import { printOutput, printError } from '../utils/output.js';
25
+ import { findWorkspaceDir } from '../utils/workspace-folder.js';
26
+ const VALIDATION_EXIT_CODE = 2;
27
+ const WARNING_EXIT_CODE = 1;
28
+ // ---------------------------------------------------------------------------
29
+ // Pipeline helpers
30
+ // ---------------------------------------------------------------------------
31
+ function steps(p) {
32
+ return Array.isArray(p.steps) ? p.steps : [];
33
+ }
34
+ function findStepById(allSteps, id) {
35
+ return allSteps.find((s) => String(s.id ?? '') === id);
36
+ }
37
+ function findTrigger(allSteps) {
38
+ return allSteps.find((s) => s.type === 'trigger') ?? allSteps[0];
39
+ }
40
+ function getNextStepId(step) {
41
+ const next = step.next;
42
+ if (!next)
43
+ return undefined;
44
+ const id = next.stepId;
45
+ return typeof id === 'string' && id ? id : undefined;
46
+ }
47
+ // ---------------------------------------------------------------------------
48
+ // Template substitution
49
+ // ---------------------------------------------------------------------------
50
+ const TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
51
+ function getDeep(obj, path) {
52
+ let cur = obj;
53
+ for (const part of path) {
54
+ if (cur === null || cur === undefined)
55
+ return { found: false, value: undefined };
56
+ if (Array.isArray(cur)) {
57
+ const idx = parseInt(part, 10);
58
+ if (isNaN(idx))
59
+ return { found: false, value: undefined };
60
+ cur = cur[idx];
61
+ continue;
62
+ }
63
+ if (typeof cur === 'object') {
64
+ const rec = cur;
65
+ if (!(part in rec))
66
+ return { found: false, value: undefined };
67
+ cur = rec[part];
68
+ continue;
69
+ }
70
+ return { found: false, value: undefined };
71
+ }
72
+ return { found: true, value: cur };
73
+ }
74
+ function resolveExpression(expression, state) {
75
+ // Strip filter pipes (`| filter`) and fallback (`|| 'default'`) before
76
+ // resolving the leftmost path. This matches docs/TEMPLATE_SYNTAX.md —
77
+ // both `{{a || 'fb'}}` and `{{array | where:x | first}}` should resolve
78
+ // to the leftmost identifier (`a` / `array`). We still report unresolved
79
+ // refs on the left side (catches typos in step IDs) without false-failing
80
+ // on legitimate fallbacks/filters.
81
+ const leftmost = expression.split(/\s*\|\|?\s*/)[0].trim();
82
+ if (!leftmost)
83
+ return { resolved: true, value: undefined };
84
+ const parts = leftmost.split('.');
85
+ const root = parts[0];
86
+ const rest = parts.slice(1);
87
+ if (root === 'input') {
88
+ return resolveAndReport({ found: true, value: state.input }, rest, root);
89
+ }
90
+ if (root === 'currentItem') {
91
+ if (state.currentItem === undefined)
92
+ return { resolved: false, value: undefined, reason: 'currentItem set only inside loops' };
93
+ return resolveAndReport({ found: true, value: state.currentItem }, rest, root);
94
+ }
95
+ if (root === 'context') {
96
+ return resolveAndReport({ found: true, value: state.context }, rest, root);
97
+ }
98
+ if (root === 'steps') {
99
+ if (rest.length === 0)
100
+ return { resolved: false, value: undefined, reason: 'expression "steps" needs a step ID' };
101
+ const stepId = rest[0];
102
+ if (!(stepId in state.steps)) {
103
+ return { resolved: false, value: undefined, reason: `step "${stepId}" has no recorded output yet (forward ref or missing step)` };
104
+ }
105
+ return resolveAndReport({ found: true, value: state.steps[stepId] }, rest.slice(1), `steps.${stepId}`);
106
+ }
107
+ if (root === 'execution' || root === 'workspace') {
108
+ // Always resolvable at runtime, treat as known
109
+ return { resolved: true, value: `<${root}>` };
110
+ }
111
+ return { resolved: false, value: undefined, reason: `unknown root identifier "${root}"` };
112
+ }
113
+ function resolveAndReport(start, rest, rootLabel) {
114
+ if (rest.length === 0)
115
+ return { resolved: true, value: start.value };
116
+ const got = getDeep(start.value, rest);
117
+ if (!got.found) {
118
+ return {
119
+ resolved: false,
120
+ value: undefined,
121
+ reason: `path "${rest.join('.')}" not found on ${rootLabel} (mock output may be missing this field)`,
122
+ };
123
+ }
124
+ return { resolved: true, value: got.value };
125
+ }
126
+ function findExpressionsAndCheck(value, state, stepId, fieldPath, unresolved) {
127
+ if (value === null || value === undefined)
128
+ return;
129
+ if (typeof value === 'string') {
130
+ const matches = value.matchAll(TEMPLATE_RE);
131
+ for (const m of matches) {
132
+ const expr = m[1];
133
+ const result = resolveExpression(expr, state);
134
+ if (!result.resolved) {
135
+ // If the expression has a fallback (`||`), runtime will use the
136
+ // fallback value when the leftmost path is missing. Downgrade
137
+ // to warning — still surfaces typos but doesn't fail the dry-run.
138
+ const hasFallback = /\|\|/.test(expr);
139
+ unresolved.push({
140
+ stepId,
141
+ field: fieldPath,
142
+ expression: m[0],
143
+ reason: result.reason ?? 'unresolved',
144
+ severity: hasFallback ? 'warning' : 'error',
145
+ });
146
+ }
147
+ }
148
+ return;
149
+ }
150
+ if (Array.isArray(value)) {
151
+ value.forEach((item, idx) => findExpressionsAndCheck(item, state, stepId, `${fieldPath}[${idx}]`, unresolved));
152
+ return;
153
+ }
154
+ if (typeof value === 'object') {
155
+ for (const [k, v] of Object.entries(value)) {
156
+ findExpressionsAndCheck(v, state, stepId, fieldPath ? `${fieldPath}.${k}` : k, unresolved);
157
+ }
158
+ }
159
+ }
160
+ // ---------------------------------------------------------------------------
161
+ // Mock output synthesizer
162
+ // ---------------------------------------------------------------------------
163
+ function synthesizeMockOutput(step, appsCache) {
164
+ const type = String(step.type ?? '');
165
+ if (type === 'aiAction' || type === 'aiActionWithTools') {
166
+ const prompt = step.pipelineStepPrompt;
167
+ const responseStructure = prompt?.responseStructure;
168
+ return shapeFromResponseStructure(responseStructure);
169
+ }
170
+ if (type === 'appAction') {
171
+ const app = step.app;
172
+ if (app && appsCache) {
173
+ const schema = lookupAppOutputSchema(appsCache, String(app.id ?? ''), String(app.actionId ?? ''));
174
+ if (schema)
175
+ return shapeFromOutputSchema(schema);
176
+ }
177
+ return {};
178
+ }
179
+ if (type === 'code') {
180
+ // Without --live, we don't run the code. Empty object signals "any shape".
181
+ return {};
182
+ }
183
+ if (type === 'setVariables') {
184
+ const cfg = step.setVariablesConfig;
185
+ const vars = cfg?.variables;
186
+ const obj = {};
187
+ if (Array.isArray(vars)) {
188
+ for (const v of vars) {
189
+ const name = v?.name;
190
+ if (typeof name === 'string')
191
+ obj[name] = null;
192
+ }
193
+ }
194
+ return obj;
195
+ }
196
+ if (type === 'knowledgeSync') {
197
+ return { addedCount: 0, listKey: null };
198
+ }
199
+ return {};
200
+ }
201
+ function shapeFromResponseStructure(rs) {
202
+ if (!rs || typeof rs !== 'object')
203
+ return {};
204
+ const out = {};
205
+ for (const [k, v] of Object.entries(rs)) {
206
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
207
+ out[k] = shapeFromResponseStructure(v);
208
+ continue;
209
+ }
210
+ const desc = String(v ?? '').toLowerCase();
211
+ if (desc.includes('array') || desc.includes('list'))
212
+ out[k] = [];
213
+ else if (desc.includes('number') || desc.includes('int') || desc.includes('float') || desc.includes('score'))
214
+ out[k] = 0;
215
+ else if (desc.includes('bool'))
216
+ out[k] = false;
217
+ else if (desc.includes('object'))
218
+ out[k] = {};
219
+ else
220
+ out[k] = '';
221
+ }
222
+ return out;
223
+ }
224
+ function lookupAppOutputSchema(appsCache, appId, actionId) {
225
+ const apps = (appsCache.apps ?? appsCache);
226
+ if (!Array.isArray(apps))
227
+ return null;
228
+ const app = apps.find((a) => a.id === appId);
229
+ const actions = app?.actions;
230
+ if (!Array.isArray(actions))
231
+ return null;
232
+ const action = actions.find((a) => a.id === actionId || a.actionId === actionId);
233
+ return action?.outputSchema ?? action?.output ?? null;
234
+ }
235
+ function shapeFromOutputSchema(schema) {
236
+ if (schema.type === 'object' || schema.properties) {
237
+ const props = (schema.properties ?? {});
238
+ const out = {};
239
+ for (const [k, v] of Object.entries(props)) {
240
+ out[k] = shapeFromOutputSchema(v);
241
+ }
242
+ return out;
243
+ }
244
+ if (schema.type === 'array')
245
+ return [];
246
+ if (schema.type === 'number' || schema.type === 'integer')
247
+ return 0;
248
+ if (schema.type === 'boolean')
249
+ return false;
250
+ return '';
251
+ }
252
+ // ---------------------------------------------------------------------------
253
+ // Synthetic input from executionInputConfig.fields
254
+ // ---------------------------------------------------------------------------
255
+ function synthesizeInput(pipeline) {
256
+ const ctx = pipeline.context;
257
+ const cfg = ctx?.executionInputConfig;
258
+ const fields = cfg?.fields;
259
+ if (!Array.isArray(fields))
260
+ return {};
261
+ const input = {};
262
+ for (const f of fields) {
263
+ const name = String(f.name ?? '');
264
+ if (!name)
265
+ continue;
266
+ const type = String(f.type ?? 'text').toLowerCase();
267
+ if (type.includes('number'))
268
+ input[name] = 1;
269
+ else if (type.includes('bool'))
270
+ input[name] = true;
271
+ else if (type.includes('array') || type.includes('multiple'))
272
+ input[name] = [];
273
+ else if (type.includes('url'))
274
+ input[name] = 'https://example.com';
275
+ else if (type.includes('email'))
276
+ input[name] = 'sample@example.com';
277
+ else
278
+ input[name] = `sample_${name}`;
279
+ }
280
+ return input;
281
+ }
282
+ function synthesizeContext(pipeline) {
283
+ const ctx = pipeline.context;
284
+ const inputPages = ctx?.inputPages;
285
+ if (!Array.isArray(inputPages))
286
+ return {};
287
+ const out = {};
288
+ for (const page of inputPages) {
289
+ const config = page.configuration;
290
+ const contextKey = config?.contextKey;
291
+ if (typeof contextKey !== 'string')
292
+ continue;
293
+ const fields = config?.fields;
294
+ const synth = {};
295
+ if (Array.isArray(fields)) {
296
+ for (const f of fields) {
297
+ const name = String(f.name ?? '');
298
+ if (name)
299
+ synth[name] = `<${contextKey}.${name}>`;
300
+ }
301
+ }
302
+ out[contextKey] = synth;
303
+ }
304
+ return out;
305
+ }
306
+ // ---------------------------------------------------------------------------
307
+ // Entry condition simulation
308
+ // ---------------------------------------------------------------------------
309
+ function simulateEntryConditions(step, state) {
310
+ const ec = step.entryConditions;
311
+ if (!ec)
312
+ return { run: true };
313
+ const criteria = ec.criteria;
314
+ if (!Array.isArray(criteria) || criteria.length === 0)
315
+ return { run: true };
316
+ const onFail = String(ec.onCriteriaFail ?? 'skip');
317
+ let allPass = true;
318
+ let allFailDueToUnresolved = true;
319
+ for (const c of criteria) {
320
+ if (c.type === 'loop_completion') {
321
+ // Can't simulate without loop tracking; treat as resolvable at runtime
322
+ continue;
323
+ }
324
+ const variable = c.variable;
325
+ if (typeof variable !== 'string') {
326
+ allFailDueToUnresolved = false;
327
+ continue;
328
+ }
329
+ const exprs = [...variable.matchAll(TEMPLATE_RE)];
330
+ if (exprs.length === 0) {
331
+ allFailDueToUnresolved = false;
332
+ continue;
333
+ }
334
+ const expr = exprs[0][1];
335
+ const res = resolveExpression(expr, state);
336
+ if (!res.resolved) {
337
+ // Unresolvable means we can't evaluate; assume runtime determines
338
+ continue;
339
+ }
340
+ allFailDueToUnresolved = false;
341
+ const operator = String(c.operator ?? '==');
342
+ const passes = evaluateOperator(res.value, operator, c.value);
343
+ if (!passes) {
344
+ allPass = false;
345
+ }
346
+ }
347
+ if (allPass)
348
+ return { run: true };
349
+ if (allFailDueToUnresolved)
350
+ return { run: true };
351
+ return {
352
+ run: false,
353
+ kind: onFail === 'stop' ? 'always-stop' : onFail === 'wait' ? 'always-wait' : 'always-skip',
354
+ reason: `entryConditions.criteria evaluated to false; onCriteriaFail=${onFail}`,
355
+ };
356
+ }
357
+ function evaluateOperator(left, op, right) {
358
+ switch (op) {
359
+ case '==':
360
+ case 'eq': return left === right;
361
+ case '!=':
362
+ case 'ne': return left !== right;
363
+ case '>': return Number(left) > Number(right);
364
+ case '<': return Number(left) < Number(right);
365
+ case '>=': return Number(left) >= Number(right);
366
+ case '<=': return Number(left) <= Number(right);
367
+ case 'isNull': return left === null || left === undefined;
368
+ case 'isNotNull': return left !== null && left !== undefined;
369
+ case 'isEmpty':
370
+ return left === '' || (Array.isArray(left) && left.length === 0)
371
+ || (typeof left === 'object' && left !== null && Object.keys(left).length === 0);
372
+ case 'notEmpty':
373
+ return !(left === '' || (Array.isArray(left) && left.length === 0)
374
+ || (typeof left === 'object' && left !== null && Object.keys(left).length === 0));
375
+ case 'contains':
376
+ return typeof left === 'string' && typeof right === 'string' && left.includes(right);
377
+ default:
378
+ return true; // unknown operator → assume runtime decides
379
+ }
380
+ }
381
+ // ---------------------------------------------------------------------------
382
+ // Credit estimate
383
+ // ---------------------------------------------------------------------------
384
+ const DEFAULT_CREDITS_BY_TYPE = {
385
+ aiAction: 10,
386
+ aiActionWithTools: 15,
387
+ appAction: 1,
388
+ code: 0,
389
+ setVariables: 0,
390
+ knowledgeSync: 1,
391
+ trigger: 0,
392
+ return: 0,
393
+ milestone: 0,
394
+ };
395
+ function creditsForStep(step) {
396
+ const explicit = Number(step.creditCost);
397
+ if (Number.isFinite(explicit) && explicit >= 0)
398
+ return explicit;
399
+ return DEFAULT_CREDITS_BY_TYPE[String(step.type ?? '')] ?? 0;
400
+ }
401
+ // ---------------------------------------------------------------------------
402
+ // Mock loaders
403
+ // ---------------------------------------------------------------------------
404
+ function loadMocksFromExecution(executionId) {
405
+ const wsDir = findWorkspaceDir();
406
+ const dir = wsDir
407
+ ? join(wsDir, 'fixtures', 'step-outputs', executionId)
408
+ : resolve('fixtures', 'step-outputs', executionId);
409
+ if (!existsSync(dir)) {
410
+ throw new Error(`Fixture directory not found: ${dir}. Run "agentled fixture capture ${executionId} --wf <wfId>" first.`);
411
+ }
412
+ const result = {};
413
+ for (const file of readdirSync(dir).filter((f) => f.endsWith('.json'))) {
414
+ const stepId = file.replace(/\.json$/, '');
415
+ try {
416
+ const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
417
+ result[stepId] = data.output ?? data.stepOutput ?? data.result ?? data;
418
+ }
419
+ catch { /* skip malformed file */ }
420
+ }
421
+ return result;
422
+ }
423
+ function loadAppsCache() {
424
+ const wsDir = findWorkspaceDir();
425
+ if (!wsDir)
426
+ return null;
427
+ const cachePath = join(wsDir, '.agentled', 'cache', 'apps.json');
428
+ if (!existsSync(cachePath))
429
+ return null;
430
+ try {
431
+ return JSON.parse(readFileSync(cachePath, 'utf-8'));
432
+ }
433
+ catch {
434
+ return null;
435
+ }
436
+ }
437
+ // ---------------------------------------------------------------------------
438
+ // Walker
439
+ // ---------------------------------------------------------------------------
440
+ function walkPipeline(pipeline, overrides) {
441
+ const allSteps = steps(pipeline);
442
+ const trigger = findTrigger(allSteps);
443
+ if (!trigger)
444
+ return { steps: [], terminal: { reached: false } };
445
+ const state = {
446
+ input: overrides.input,
447
+ steps: { ...overrides.mocks },
448
+ context: synthesizeContext(pipeline),
449
+ };
450
+ const visited = new Set();
451
+ const trace = [];
452
+ let cursorId = String(trigger.id ?? '');
453
+ let terminalType;
454
+ let terminalStepId;
455
+ while (cursorId) {
456
+ if (visited.has(cursorId))
457
+ break; // cycle safety
458
+ visited.add(cursorId);
459
+ const step = findStepById(allSteps, cursorId);
460
+ if (!step)
461
+ break;
462
+ const type = String(step.type ?? '');
463
+ const ec = simulateEntryConditions(step, state);
464
+ const credit = creditsForStep(step);
465
+ let mock;
466
+ const unresolved = [];
467
+ if (cursorId in state.steps) {
468
+ // Caller-provided mock takes precedence
469
+ mock = state.steps[cursorId];
470
+ }
471
+ else {
472
+ mock = synthesizeMockOutput(step, overrides.appsCache);
473
+ }
474
+ // Check templates in stepInputData / pipelineStepPrompt.template / setVariables / knowledgeSync
475
+ const fieldsToCheck = [];
476
+ if (step.stepInputData)
477
+ fieldsToCheck.push({ value: step.stepInputData, field: 'stepInputData' });
478
+ const prompt = step.pipelineStepPrompt;
479
+ if (prompt?.template)
480
+ fieldsToCheck.push({ value: prompt.template, field: 'pipelineStepPrompt.template' });
481
+ if (step.setVariablesConfig)
482
+ fieldsToCheck.push({ value: step.setVariablesConfig, field: 'setVariablesConfig' });
483
+ if (step.knowledgeSync)
484
+ fieldsToCheck.push({ value: step.knowledgeSync, field: 'knowledgeSync' });
485
+ if (step.returnConfig)
486
+ fieldsToCheck.push({ value: step.returnConfig, field: 'returnConfig' });
487
+ for (const f of fieldsToCheck) {
488
+ findExpressionsAndCheck(f.value, state, cursorId, f.field, unresolved);
489
+ }
490
+ let status = 'executed';
491
+ let skipReason;
492
+ if (!ec.run) {
493
+ status = ec.kind === 'always-stop' ? 'stop' : ec.kind === 'always-wait' ? 'wait' : 'skipped';
494
+ skipReason = ec.reason;
495
+ }
496
+ // Record the synthesized output into state for downstream resolution
497
+ if (status === 'executed' || status === 'wait') {
498
+ state.steps[cursorId] = mock;
499
+ }
500
+ trace.push({
501
+ stepId: cursorId,
502
+ type,
503
+ name: step.name,
504
+ status,
505
+ creditCost: status === 'executed' ? credit : 0,
506
+ mockOutput: mock,
507
+ unresolved,
508
+ skipReason,
509
+ });
510
+ if (type === 'milestone' || type === 'return') {
511
+ terminalType = type;
512
+ terminalStepId = cursorId;
513
+ break;
514
+ }
515
+ if (status === 'stop' || status === 'wait')
516
+ break;
517
+ cursorId = getNextStepId(step);
518
+ }
519
+ return {
520
+ steps: trace,
521
+ terminal: { reached: !!terminalType, type: terminalType, stepId: terminalStepId },
522
+ };
523
+ }
524
+ // ---------------------------------------------------------------------------
525
+ // Command
526
+ // ---------------------------------------------------------------------------
527
+ export function registerDryRunCommand(program) {
528
+ program
529
+ .command('dryrun <file>')
530
+ .alias('dry-run')
531
+ .description('Zero-credit variable-resolution walk over a pipeline JSON. Synthesizes mock outputs per step (or uses --mocks / --from-execution), resolves all {{...}} references, and reports unresolved refs, always-skip branches, and a credit estimate. Run before `workflows start` to catch data-flow bugs without spending credits.')
532
+ .option('--input <json>', 'Inline JSON object for {{input.*}} resolution')
533
+ .option('--input-file <path>', 'Path to a JSON file containing the input object')
534
+ .option('--mocks <path>', 'Path to a JSON file mapping {stepId: <output>} to override synthetic mocks')
535
+ .option('--from-execution <execId>', 'Load step outputs from fixtures/step-outputs/<execId>/ (most realistic — pair with `agentled fixture capture`)')
536
+ .option('--format <fmt>', 'Output format', 'json')
537
+ .action((file, opts) => {
538
+ try {
539
+ const abs = resolve(file);
540
+ if (!existsSync(abs)) {
541
+ printError(`File not found: ${abs}`);
542
+ process.exitCode = VALIDATION_EXIT_CODE;
543
+ return;
544
+ }
545
+ let pipeline;
546
+ try {
547
+ pipeline = JSON.parse(readFileSync(abs, 'utf-8'));
548
+ }
549
+ catch (err) {
550
+ printError(`Failed to parse pipeline JSON: ${err instanceof Error ? err.message : String(err)}`);
551
+ process.exitCode = VALIDATION_EXIT_CODE;
552
+ return;
553
+ }
554
+ // Resolve input
555
+ let input = synthesizeInput(pipeline);
556
+ let inputProvided = false;
557
+ if (opts.input) {
558
+ try {
559
+ input = { ...input, ...JSON.parse(opts.input) };
560
+ inputProvided = true;
561
+ }
562
+ catch (err) {
563
+ printError(`Failed to parse --input JSON: ${err instanceof Error ? err.message : String(err)}`);
564
+ process.exitCode = VALIDATION_EXIT_CODE;
565
+ return;
566
+ }
567
+ }
568
+ if (opts.inputFile) {
569
+ try {
570
+ const raw = readFileSync(resolve(opts.inputFile), 'utf-8');
571
+ input = { ...input, ...JSON.parse(raw) };
572
+ inputProvided = true;
573
+ }
574
+ catch (err) {
575
+ printError(`Failed to read --input-file: ${err instanceof Error ? err.message : String(err)}`);
576
+ process.exitCode = VALIDATION_EXIT_CODE;
577
+ return;
578
+ }
579
+ }
580
+ // Resolve mocks
581
+ let mocks = {};
582
+ if (opts.mocks) {
583
+ try {
584
+ mocks = JSON.parse(readFileSync(resolve(opts.mocks), 'utf-8'));
585
+ }
586
+ catch (err) {
587
+ printError(`Failed to read --mocks file: ${err instanceof Error ? err.message : String(err)}`);
588
+ process.exitCode = VALIDATION_EXIT_CODE;
589
+ return;
590
+ }
591
+ }
592
+ if (opts.fromExecution) {
593
+ try {
594
+ const execMocks = loadMocksFromExecution(opts.fromExecution);
595
+ mocks = { ...mocks, ...execMocks };
596
+ }
597
+ catch (err) {
598
+ printError(err instanceof Error ? err.message : String(err));
599
+ process.exitCode = VALIDATION_EXIT_CODE;
600
+ return;
601
+ }
602
+ }
603
+ const appsCache = loadAppsCache();
604
+ const walked = walkPipeline(pipeline, { input, mocks, appsCache });
605
+ const unresolvedRefs = walked.steps.flatMap((s) => s.unresolved);
606
+ const branchIssues = walked.steps
607
+ .filter((s) => s.status !== 'executed')
608
+ .map((s) => ({
609
+ stepId: s.stepId,
610
+ kind: s.status === 'stop' ? 'always-stop'
611
+ : s.status === 'wait' ? 'always-wait'
612
+ : 'always-skip',
613
+ onCriteriaFail: s.skipReason,
614
+ }));
615
+ const creditEstimate = walked.steps.reduce((acc, s) => acc + s.creditCost, 0);
616
+ const result = {
617
+ file: abs,
618
+ pipeline: { name: pipeline.name, stepCount: steps(pipeline).length },
619
+ inputProvided,
620
+ mocksProvided: Object.keys(mocks).length,
621
+ fromExecution: opts.fromExecution,
622
+ steps: walked.steps,
623
+ unresolvedRefs,
624
+ branchIssues,
625
+ creditEstimate,
626
+ terminal: walked.terminal,
627
+ };
628
+ printOutput(result, (opts.format ?? 'json'));
629
+ const unresolvedErrors = unresolvedRefs.filter((r) => r.severity === 'error');
630
+ const unresolvedWarnings = unresolvedRefs.filter((r) => r.severity === 'warning');
631
+ if (unresolvedErrors.length > 0)
632
+ process.exitCode = VALIDATION_EXIT_CODE;
633
+ else if (unresolvedWarnings.length > 0 || branchIssues.length > 0 || !walked.terminal.reached)
634
+ process.exitCode = WARNING_EXIT_CODE;
635
+ }
636
+ catch (e) {
637
+ printError(e instanceof Error ? e.message : String(e));
638
+ }
639
+ });
640
+ }
641
+ //# sourceMappingURL=dryrun.js.map