@agentled/cli 0.7.1 → 0.7.4
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 +18 -2
- package/dist/commands/executions.js +4 -1
- package/dist/commands/executions.js.map +1 -1
- package/dist/commands/init.js +10 -15
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/knowledge.js +3 -1
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/onboarding.d.ts +1 -1
- package/dist/commands/onboarding.js +30 -13
- package/dist/commands/onboarding.js.map +1 -1
- package/dist/commands/workflows.js +372 -14
- package/dist/commands/workflows.js.map +1 -1
- package/dist/context-schema.js +3 -4
- package/dist/context-schema.js.map +1 -1
- package/dist/utils/browser-auth.d.ts +1 -0
- package/dist/utils/browser-auth.js +5 -5
- package/dist/utils/browser-auth.js.map +1 -1
- package/dist/utils/preflight.js +3 -3
- package/dist/utils/preflight.js.map +1 -1
- package/dist/utils/workspace-folder.d.ts +2 -7
- package/dist/utils/workspace-folder.js +5 -0
- package/dist/utils/workspace-folder.js.map +1 -1
- package/dist/utils/workspace-meta.d.ts +27 -0
- package/dist/utils/workspace-meta.js +49 -0
- package/dist/utils/workspace-meta.js.map +1 -0
- package/llms-full.txt +17 -7
- package/package.json +2 -2
- package/patterns/v1/09-reports-and-knowledge-storage.md +331 -0
- package/scaffolds/extract-threshold-alert.json +2 -2
- package/scaffolds/lead-scoring-kg.json +1 -1
- package/scaffolds/list-match-email.json +1 -1
- package/skills/agentled/SKILL.md +47 -1
|
@@ -37,7 +37,26 @@ function formatIssueLine(issue) {
|
|
|
37
37
|
async function runAutoValidate(client, workflowId, opts) {
|
|
38
38
|
let result;
|
|
39
39
|
try {
|
|
40
|
-
|
|
40
|
+
let pipeline;
|
|
41
|
+
let validationSource = 'live';
|
|
42
|
+
if (opts.source === 'draft' || opts.source === 'auto') {
|
|
43
|
+
try {
|
|
44
|
+
const draft = await client.getDraft(workflowId);
|
|
45
|
+
if (draft?.draft?.config) {
|
|
46
|
+
pipeline = draft.draft.config;
|
|
47
|
+
validationSource = 'draft';
|
|
48
|
+
}
|
|
49
|
+
else if (opts.source === 'draft') {
|
|
50
|
+
throw new Error(`No draft exists for workflow ${workflowId}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (opts.source === 'draft')
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
result = await client.validateWorkflow(workflowId, pipeline);
|
|
59
|
+
result = { ...result, source: validationSource };
|
|
41
60
|
}
|
|
42
61
|
catch (e) {
|
|
43
62
|
// The workflow was saved; only the post-hoc validate call failed.
|
|
@@ -51,6 +70,9 @@ async function runAutoValidate(client, workflowId, opts) {
|
|
|
51
70
|
if (opts.format !== 'json') {
|
|
52
71
|
const errorCount = result.errors?.length ?? 0;
|
|
53
72
|
const warnCount = result.warnings?.length ?? 0;
|
|
73
|
+
if (result.source && result.source !== 'live') {
|
|
74
|
+
console.log(` Source: ${result.source}`);
|
|
75
|
+
}
|
|
54
76
|
if (errorCount === 0 && warnCount === 0) {
|
|
55
77
|
console.log(` ✓ Validated (0 errors, 0 warnings) — ${workflowId}`);
|
|
56
78
|
}
|
|
@@ -90,6 +112,145 @@ function exitCodeForOutcome(outcome) {
|
|
|
90
112
|
return VALIDATION_EXIT_CODE;
|
|
91
113
|
return 1;
|
|
92
114
|
}
|
|
115
|
+
const CONFIG_UPDATE_FIELDS = ['name', 'goal', 'description', 'steps', 'context', 'style', 'analyticsConfig'];
|
|
116
|
+
const READ_ONLY_WORKFLOW_FIELDS = new Set([
|
|
117
|
+
'_agentled',
|
|
118
|
+
'id',
|
|
119
|
+
'workspaceId',
|
|
120
|
+
'workspace',
|
|
121
|
+
'createdAt',
|
|
122
|
+
'updatedAt',
|
|
123
|
+
'owner',
|
|
124
|
+
'executions',
|
|
125
|
+
'executionInputs',
|
|
126
|
+
'computedMetrics',
|
|
127
|
+
'notificationBadge',
|
|
128
|
+
'agents',
|
|
129
|
+
'team',
|
|
130
|
+
'hasDraftSnapshot',
|
|
131
|
+
'draftSnapshot',
|
|
132
|
+
'urls',
|
|
133
|
+
]);
|
|
134
|
+
function normalizeRevision(value) {
|
|
135
|
+
const revision = typeof value === 'number'
|
|
136
|
+
? value
|
|
137
|
+
: typeof value === 'string'
|
|
138
|
+
? Number.parseInt(value, 10)
|
|
139
|
+
: Number.NaN;
|
|
140
|
+
return Number.isFinite(revision) && revision >= 0 ? revision : undefined;
|
|
141
|
+
}
|
|
142
|
+
function pickWorkflowConfig(workflow) {
|
|
143
|
+
const config = {};
|
|
144
|
+
for (const key of CONFIG_UPDATE_FIELDS) {
|
|
145
|
+
if (workflow[key] !== undefined)
|
|
146
|
+
config[key] = workflow[key];
|
|
147
|
+
}
|
|
148
|
+
return config;
|
|
149
|
+
}
|
|
150
|
+
function getWorkflowFromResponse(result) {
|
|
151
|
+
return (result?.workflow && typeof result.workflow === 'object') ? result.workflow : result;
|
|
152
|
+
}
|
|
153
|
+
function normalizeWorkflowUpdateFile(parsed) {
|
|
154
|
+
let source;
|
|
155
|
+
let updates = parsed;
|
|
156
|
+
if (parsed?.export?.pipeline) {
|
|
157
|
+
updates = parsed.export.pipeline;
|
|
158
|
+
source = parsed.export.sourceWorkflow;
|
|
159
|
+
}
|
|
160
|
+
else if (parsed?.exportVersion && parsed?.pipeline) {
|
|
161
|
+
updates = parsed.pipeline;
|
|
162
|
+
source = parsed.sourceWorkflow;
|
|
163
|
+
}
|
|
164
|
+
else if (parsed?.workflow) {
|
|
165
|
+
updates = parsed.workflow;
|
|
166
|
+
source = {
|
|
167
|
+
workflowId: parsed.workflow.id,
|
|
168
|
+
updatedAt: parsed.workflow.updatedAt,
|
|
169
|
+
revision: normalizeRevision(parsed.workflow.metadata?.revision),
|
|
170
|
+
status: parsed.workflow.status,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
else if (parsed?._agentled) {
|
|
174
|
+
source = {
|
|
175
|
+
workflowId: parsed._agentled.workflowId,
|
|
176
|
+
updatedAt: parsed._agentled.updatedAt,
|
|
177
|
+
revision: normalizeRevision(parsed._agentled.revision),
|
|
178
|
+
status: parsed._agentled.status,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const cleaned = {};
|
|
182
|
+
for (const [key, value] of Object.entries(updates || {})) {
|
|
183
|
+
if (!READ_ONLY_WORKFLOW_FIELDS.has(key))
|
|
184
|
+
cleaned[key] = value;
|
|
185
|
+
}
|
|
186
|
+
return { updates: cleaned, source };
|
|
187
|
+
}
|
|
188
|
+
function hasConfigUpdate(updates) {
|
|
189
|
+
return CONFIG_UPDATE_FIELDS.some((field) => Object.prototype.hasOwnProperty.call(updates, field));
|
|
190
|
+
}
|
|
191
|
+
function buildPulledWorkflowFile(workflow) {
|
|
192
|
+
return {
|
|
193
|
+
_agentled: {
|
|
194
|
+
kind: 'workflow-update-file',
|
|
195
|
+
workflowId: workflow.id,
|
|
196
|
+
updatedAt: workflow.updatedAt,
|
|
197
|
+
revision: normalizeRevision(workflow.metadata?.revision),
|
|
198
|
+
status: workflow.status,
|
|
199
|
+
pulledAt: new Date().toISOString(),
|
|
200
|
+
},
|
|
201
|
+
...pickWorkflowConfig(workflow),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function diffWorkflowConfig(remote, updates) {
|
|
205
|
+
const changedFields = CONFIG_UPDATE_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(updates, field)
|
|
206
|
+
&& JSON.stringify(remote[field] ?? null) !== JSON.stringify(updates[field] ?? null));
|
|
207
|
+
const remoteSteps = Array.isArray(remote.steps) ? remote.steps : [];
|
|
208
|
+
const nextSteps = Array.isArray(updates.steps) ? updates.steps : remoteSteps;
|
|
209
|
+
const remoteIds = new Set(remoteSteps.map((step) => step?.id).filter(Boolean));
|
|
210
|
+
const nextIds = new Set(nextSteps.map((step) => step?.id).filter(Boolean));
|
|
211
|
+
const addedSteps = Array.from(nextIds).filter((id) => !remoteIds.has(id));
|
|
212
|
+
const removedSteps = Array.from(remoteIds).filter((id) => !nextIds.has(id));
|
|
213
|
+
const changedSteps = nextSteps
|
|
214
|
+
.filter((step) => step?.id && remoteIds.has(step.id))
|
|
215
|
+
.filter((step) => {
|
|
216
|
+
const previous = remoteSteps.find((candidate) => candidate?.id === step.id);
|
|
217
|
+
return JSON.stringify(previous ?? null) !== JSON.stringify(step ?? null);
|
|
218
|
+
})
|
|
219
|
+
.map((step) => step.id);
|
|
220
|
+
return { changedFields, addedSteps, removedSteps, changedSteps };
|
|
221
|
+
}
|
|
222
|
+
async function verifySafeFileUpdate(client, workflowId, filePath, updates, source, opts) {
|
|
223
|
+
if (!hasConfigUpdate(updates) || opts.force)
|
|
224
|
+
return true;
|
|
225
|
+
const remote = getWorkflowFromResponse(await client.getWorkflow(workflowId));
|
|
226
|
+
const expectedUpdatedAt = opts.expectedUpdatedAt || source?.updatedAt;
|
|
227
|
+
const expectedRevision = normalizeRevision(source?.revision);
|
|
228
|
+
const remoteRevision = normalizeRevision(remote.metadata?.revision);
|
|
229
|
+
if (source?.workflowId && source.workflowId !== workflowId) {
|
|
230
|
+
printError(`File was pulled from workflow ${source.workflowId}, not ${workflowId}. Pull the target workflow first or use --force.`);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
if (expectedRevision !== undefined && remoteRevision !== undefined) {
|
|
234
|
+
if (expectedRevision !== remoteRevision) {
|
|
235
|
+
printError(`Refusing stale update. File is based on revision ${expectedRevision}, but remote is revision ${remoteRevision}. ` +
|
|
236
|
+
`Run: agentled workflows pull ${workflowId} --output ${filePath} --force, compare/re-apply your edits, then update or replace again.`);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (!expectedUpdatedAt) {
|
|
242
|
+
printError('Refusing config update from a file without a remote revision or updatedAt token. ' +
|
|
243
|
+
`Run: agentled workflows pull ${workflowId} --output ${filePath} --force, re-apply your edits, then run workflows diff/replace. ` +
|
|
244
|
+
'Use --force only when you intentionally want to overwrite remote config.');
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (expectedUpdatedAt !== remote.updatedAt) {
|
|
248
|
+
printError(`Refusing stale update. File is based on ${expectedUpdatedAt}, but remote is ${remote.updatedAt}. ` +
|
|
249
|
+
`Run: agentled workflows pull ${workflowId} --output ${filePath} --force, compare/re-apply your edits, then update or replace again.`);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
93
254
|
export function registerWorkflowCommands(program) {
|
|
94
255
|
const workflows = program
|
|
95
256
|
.command('workflows')
|
|
@@ -131,25 +292,39 @@ export function registerWorkflowCommands(program) {
|
|
|
131
292
|
});
|
|
132
293
|
workflows
|
|
133
294
|
.command('pull <id>')
|
|
134
|
-
.description('Pull a
|
|
295
|
+
.description('Pull a workflow into an update-safe local file with a remote updatedAt token. Use this before workflows update --file.')
|
|
296
|
+
.option('--output <path>', 'Write workflow config to a specific path')
|
|
135
297
|
.option('--force', 'Overwrite existing files', false)
|
|
136
298
|
.option('--format <fmt>', 'Output format', 'json')
|
|
137
299
|
.action(async (id, opts) => {
|
|
138
300
|
try {
|
|
139
301
|
const client = new AgentledClient();
|
|
140
|
-
const
|
|
141
|
-
const pipeline = (
|
|
302
|
+
const result = await client.getWorkflow(id);
|
|
303
|
+
const pipeline = getWorkflowFromResponse(result);
|
|
304
|
+
const updateFile = buildPulledWorkflowFile(pipeline);
|
|
142
305
|
const wsDir = findWorkspaceDir();
|
|
143
306
|
const name = String(pipeline.name ?? id).toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
144
307
|
const filename = `${name}-${id}.json`;
|
|
145
308
|
let savedPath;
|
|
146
309
|
let testPath = null;
|
|
147
310
|
let testSkippedReason = null;
|
|
148
|
-
if (
|
|
311
|
+
if (opts.output) {
|
|
312
|
+
savedPath = resolve(opts.output);
|
|
313
|
+
if (fs.existsSync(savedPath) && !opts.force) {
|
|
314
|
+
printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
|
|
318
|
+
}
|
|
319
|
+
else if (wsDir) {
|
|
149
320
|
const liveDir = join(wsDir, 'examples', 'live');
|
|
150
321
|
fs.mkdirSync(liveDir, { recursive: true });
|
|
151
322
|
savedPath = join(liveDir, filename);
|
|
152
|
-
fs.
|
|
323
|
+
if (fs.existsSync(savedPath) && !opts.force) {
|
|
324
|
+
printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
|
|
153
328
|
const testsDir = join(wsDir, 'tests');
|
|
154
329
|
fs.mkdirSync(testsDir, { recursive: true });
|
|
155
330
|
testPath = join(testsDir, `${id}.test.json`);
|
|
@@ -163,7 +338,11 @@ export function registerWorkflowCommands(program) {
|
|
|
163
338
|
}
|
|
164
339
|
else {
|
|
165
340
|
savedPath = resolve(filename);
|
|
166
|
-
fs.
|
|
341
|
+
if (fs.existsSync(savedPath) && !opts.force) {
|
|
342
|
+
printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
|
|
167
346
|
}
|
|
168
347
|
const allSteps = (Array.isArray(pipeline.steps) ? pipeline.steps : []);
|
|
169
348
|
const testableCount = allSteps.filter((s) => ['aiAction', 'aiActionWithTools', 'appAction', 'code'].includes(String(s.type ?? ''))).length;
|
|
@@ -177,6 +356,9 @@ export function registerWorkflowCommands(program) {
|
|
|
177
356
|
stepCount: allSteps.length,
|
|
178
357
|
testableSteps: testableCount,
|
|
179
358
|
nextSteps: [
|
|
359
|
+
`edit ${savedPath}`,
|
|
360
|
+
`agentled workflows diff ${id} --file ${savedPath}`,
|
|
361
|
+
`agentled workflows replace ${id} --file ${savedPath}`,
|
|
180
362
|
`agentled workflows start ${id} # run once to capture fixtures`,
|
|
181
363
|
`agentled fixture capture <execId> --wf ${id}`,
|
|
182
364
|
`agentled test ${id} # run assertions against fixtures`,
|
|
@@ -187,6 +369,52 @@ export function registerWorkflowCommands(program) {
|
|
|
187
369
|
printError(e instanceof Error ? e.message : String(e));
|
|
188
370
|
}
|
|
189
371
|
});
|
|
372
|
+
workflows
|
|
373
|
+
.command('diff <id>')
|
|
374
|
+
.description('Compare a local workflow update file with the current remote workflow before updating.')
|
|
375
|
+
.requiredOption('--file <path>', 'Path to workflow update JSON file')
|
|
376
|
+
.option('--format <fmt>', 'Output format', 'json')
|
|
377
|
+
.action(async (id, opts) => {
|
|
378
|
+
try {
|
|
379
|
+
const raw = fs.readFileSync(opts.file, 'utf-8');
|
|
380
|
+
const { updates, source } = normalizeWorkflowUpdateFile(JSON.parse(raw));
|
|
381
|
+
const client = new AgentledClient();
|
|
382
|
+
const remote = getWorkflowFromResponse(await client.getWorkflow(id));
|
|
383
|
+
const diff = diffWorkflowConfig(remote, updates);
|
|
384
|
+
const remoteRevision = normalizeRevision(remote.metadata?.revision);
|
|
385
|
+
const sourceRevision = normalizeRevision(source?.revision);
|
|
386
|
+
const sourceMatchesRemote = sourceRevision !== undefined && remoteRevision !== undefined
|
|
387
|
+
? sourceRevision === remoteRevision
|
|
388
|
+
: source?.updatedAt
|
|
389
|
+
? source.updatedAt === remote.updatedAt
|
|
390
|
+
: undefined;
|
|
391
|
+
printOutput({
|
|
392
|
+
workflowId: id,
|
|
393
|
+
file: resolve(opts.file),
|
|
394
|
+
remoteRevision,
|
|
395
|
+
fileSourceRevision: sourceRevision,
|
|
396
|
+
remoteUpdatedAt: remote.updatedAt,
|
|
397
|
+
fileSourceUpdatedAt: source?.updatedAt,
|
|
398
|
+
sourceMatchesRemote,
|
|
399
|
+
stale: sourceMatchesRemote === false,
|
|
400
|
+
...diff,
|
|
401
|
+
summary: {
|
|
402
|
+
changedFields: diff.changedFields.length,
|
|
403
|
+
addedSteps: diff.addedSteps.length,
|
|
404
|
+
removedSteps: diff.removedSteps.length,
|
|
405
|
+
changedSteps: diff.changedSteps.length,
|
|
406
|
+
},
|
|
407
|
+
nextStep: sourceMatchesRemote === false
|
|
408
|
+
? `Remote changed since this file was pulled. Run: agentled workflows pull ${id} --output ${opts.file} --force, then re-apply your edits.`
|
|
409
|
+
: `If this diff is intended, run: agentled workflows replace ${id} --file ${opts.file}`,
|
|
410
|
+
}, (opts.format ?? 'json'));
|
|
411
|
+
if (sourceMatchesRemote === false)
|
|
412
|
+
process.exitCode = 1;
|
|
413
|
+
}
|
|
414
|
+
catch (e) {
|
|
415
|
+
printError(e instanceof Error ? e.message : String(e));
|
|
416
|
+
}
|
|
417
|
+
});
|
|
190
418
|
workflows
|
|
191
419
|
.command('lint <file>')
|
|
192
420
|
.description('Static gotcha checks for a pipeline JSON file (no API). Catches 11 documented failure modes (criteria/conditions, Gmail label_id, missing tools, email shape, etc.). For platform validators (graph wiring, variable resolution), run `agentled workflows validate <id>` after creating the workflow.')
|
|
@@ -296,15 +524,20 @@ export function registerWorkflowCommands(program) {
|
|
|
296
524
|
.description('Update an existing workflow (auto-validates unless --skip-validate)')
|
|
297
525
|
.option('--file <path>', 'Path to updates JSON file')
|
|
298
526
|
.option('--updates <json>', 'Inline updates JSON')
|
|
527
|
+
.option('--expected-updated-at <timestamp>', 'Require the remote workflow updatedAt to match before applying --file')
|
|
528
|
+
.option('--force', 'Bypass the --file stale-update guard (advanced)')
|
|
299
529
|
.option('--locale <locale>', 'Locale')
|
|
300
530
|
.option('--format <fmt>', 'Output format', 'json')
|
|
301
531
|
.option('--skip-validate', 'Skip the post-update validate call (advanced)')
|
|
302
532
|
.action(async (id, opts) => {
|
|
303
533
|
try {
|
|
304
534
|
let updates;
|
|
535
|
+
let source;
|
|
305
536
|
if (opts.file) {
|
|
306
537
|
const raw = fs.readFileSync(opts.file, 'utf-8');
|
|
307
|
-
|
|
538
|
+
const normalized = normalizeWorkflowUpdateFile(JSON.parse(raw));
|
|
539
|
+
updates = normalized.updates;
|
|
540
|
+
source = normalized.source;
|
|
308
541
|
}
|
|
309
542
|
else if (opts.updates) {
|
|
310
543
|
updates = JSON.parse(opts.updates);
|
|
@@ -315,6 +548,14 @@ export function registerWorkflowCommands(program) {
|
|
|
315
548
|
}
|
|
316
549
|
const client = new AgentledClient();
|
|
317
550
|
const format = (opts.format ?? 'json');
|
|
551
|
+
if (opts.file) {
|
|
552
|
+
const safe = await verifySafeFileUpdate(client, id, opts.file, updates, source, {
|
|
553
|
+
force: opts.force,
|
|
554
|
+
expectedUpdatedAt: opts.expectedUpdatedAt,
|
|
555
|
+
});
|
|
556
|
+
if (!safe)
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
318
559
|
const result = await client.updateWorkflow(id, updates, opts.locale);
|
|
319
560
|
if (opts.skipValidate) {
|
|
320
561
|
printOutput(result, format);
|
|
@@ -324,7 +565,10 @@ export function registerWorkflowCommands(program) {
|
|
|
324
565
|
// Preserve the update mutation response fields in the JSON
|
|
325
566
|
// envelope — `result` may contain the refreshed workflow
|
|
326
567
|
// record, which script consumers parse.
|
|
327
|
-
const outcome = await runAutoValidate(client, id, {
|
|
568
|
+
const outcome = await runAutoValidate(client, id, {
|
|
569
|
+
format,
|
|
570
|
+
source: result?.editingDraft ? 'draft' : 'live',
|
|
571
|
+
});
|
|
328
572
|
const validation = outcome.status === 'request-failed'
|
|
329
573
|
? { requestFailed: true }
|
|
330
574
|
: outcome.result;
|
|
@@ -338,7 +582,64 @@ export function registerWorkflowCommands(program) {
|
|
|
338
582
|
return;
|
|
339
583
|
}
|
|
340
584
|
printOutput(result, format);
|
|
341
|
-
const outcome = await runAutoValidate(client, id, {
|
|
585
|
+
const outcome = await runAutoValidate(client, id, {
|
|
586
|
+
format,
|
|
587
|
+
source: result?.editingDraft ? 'draft' : 'live',
|
|
588
|
+
});
|
|
589
|
+
const code = exitCodeForOutcome(outcome);
|
|
590
|
+
if (code !== 0)
|
|
591
|
+
process.exit(code);
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
printError(e.message);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
workflows
|
|
598
|
+
.command('replace <id>')
|
|
599
|
+
.description('Replace workflow config fields from a pulled file. Uses true replace semantics for context and validates draft when editing live workflows.')
|
|
600
|
+
.requiredOption('--file <path>', 'Path to workflow JSON from workflows pull/export')
|
|
601
|
+
.option('--expected-updated-at <timestamp>', 'Require the remote workflow updatedAt to match before replacing')
|
|
602
|
+
.option('--force', 'Bypass the stale-file guard (advanced)')
|
|
603
|
+
.option('--locale <locale>', 'Locale')
|
|
604
|
+
.option('--format <fmt>', 'Output format', 'json')
|
|
605
|
+
.option('--skip-validate', 'Skip the post-replace validate call (advanced)')
|
|
606
|
+
.action(async (id, opts) => {
|
|
607
|
+
try {
|
|
608
|
+
const raw = fs.readFileSync(opts.file, 'utf-8');
|
|
609
|
+
const { updates, source } = normalizeWorkflowUpdateFile(JSON.parse(raw));
|
|
610
|
+
const client = new AgentledClient();
|
|
611
|
+
const format = (opts.format ?? 'json');
|
|
612
|
+
const safe = await verifySafeFileUpdate(client, id, opts.file, updates, source, {
|
|
613
|
+
force: opts.force,
|
|
614
|
+
expectedUpdatedAt: opts.expectedUpdatedAt,
|
|
615
|
+
});
|
|
616
|
+
if (!safe)
|
|
617
|
+
return;
|
|
618
|
+
const result = await client.replaceWorkflow(id, updates, opts.locale);
|
|
619
|
+
if (opts.skipValidate) {
|
|
620
|
+
printOutput(result, format);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const validateOpts = {
|
|
624
|
+
format,
|
|
625
|
+
source: result?.editingDraft ? 'draft' : 'live',
|
|
626
|
+
};
|
|
627
|
+
if (format === 'json') {
|
|
628
|
+
const outcome = await runAutoValidate(client, id, validateOpts);
|
|
629
|
+
const validation = outcome.status === 'request-failed'
|
|
630
|
+
? { requestFailed: true }
|
|
631
|
+
: outcome.result;
|
|
632
|
+
const envelope = result && typeof result === 'object'
|
|
633
|
+
? { ...result, validation }
|
|
634
|
+
: { workflowId: id, validation };
|
|
635
|
+
printOutput(envelope, 'json');
|
|
636
|
+
const code = exitCodeForOutcome(outcome);
|
|
637
|
+
if (code !== 0)
|
|
638
|
+
process.exit(code);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
printOutput(result, format);
|
|
642
|
+
const outcome = await runAutoValidate(client, id, validateOpts);
|
|
342
643
|
const code = exitCodeForOutcome(outcome);
|
|
343
644
|
if (code !== 0)
|
|
344
645
|
process.exit(code);
|
|
@@ -366,6 +667,9 @@ export function registerWorkflowCommands(program) {
|
|
|
366
667
|
.description('Validate a workflow. With <id>: server-side validation. With --file/--pipeline and no id: fast client-side preflight (no API call).')
|
|
367
668
|
.option('--file <path>', 'Path to pipeline JSON file (client-side preflight if no <id> is given)')
|
|
368
669
|
.option('--pipeline <json>', 'Inline pipeline JSON (overrides the saved pipeline when <id> is given; otherwise runs preflight)')
|
|
670
|
+
.option('--source <src>', 'Validation source when <id> is provided: auto (default) | live | draft', 'auto')
|
|
671
|
+
.option('--draft', 'Validate the current draft config for this workflow')
|
|
672
|
+
.option('--live', 'Validate the live stored workflow config')
|
|
369
673
|
.option('--format <fmt>', 'Output format', 'json')
|
|
370
674
|
.action(async (id, opts) => {
|
|
371
675
|
try {
|
|
@@ -411,14 +715,51 @@ export function registerWorkflowCommands(program) {
|
|
|
411
715
|
return;
|
|
412
716
|
}
|
|
413
717
|
const client = new AgentledClient();
|
|
718
|
+
if (opts.draft && opts.live) {
|
|
719
|
+
printError('Use either --draft or --live, not both.');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const source = opts.draft ? 'draft' : opts.live ? 'live' : String(opts.source ?? 'auto');
|
|
723
|
+
if (!['auto', 'live', 'draft'].includes(source)) {
|
|
724
|
+
printError(`Invalid --source "${source}". Must be one of: auto, live, draft.`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
// When source is specified, resolve which stored config to validate:
|
|
728
|
+
// - draft: validates the current draft pipeline and errors if none exists
|
|
729
|
+
// - auto: validates draft if present, otherwise live
|
|
730
|
+
// - live: validates the live stored workflow
|
|
731
|
+
let resolvedSource = 'live';
|
|
732
|
+
if (!pipeline && source !== 'live') {
|
|
733
|
+
try {
|
|
734
|
+
const draft = await client.getDraft(id);
|
|
735
|
+
if (draft?.draft?.config) {
|
|
736
|
+
pipeline = draft.draft.config;
|
|
737
|
+
resolvedSource = 'draft';
|
|
738
|
+
}
|
|
739
|
+
else if (source === 'draft') {
|
|
740
|
+
printError(`No draft exists for workflow ${id}.`);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
if (source === 'draft') {
|
|
746
|
+
printError(`No draft exists for workflow ${id}.`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
414
751
|
const result = await client.validateWorkflow(id, pipeline);
|
|
415
752
|
const format = opts.format;
|
|
753
|
+
const validationSource = opts.pipeline || opts.file
|
|
754
|
+
? 'inline'
|
|
755
|
+
: (pipeline ? resolvedSource : 'live');
|
|
416
756
|
if (format === 'json') {
|
|
417
|
-
printOutput(result, 'json');
|
|
757
|
+
printOutput({ ...result, source: validationSource }, 'json');
|
|
418
758
|
}
|
|
419
759
|
else {
|
|
420
760
|
const errorCount = result.errors?.length ?? 0;
|
|
421
761
|
const warnCount = result.warnings?.length ?? 0;
|
|
762
|
+
console.log(` Source: ${validationSource}`);
|
|
422
763
|
if (errorCount === 0 && warnCount === 0) {
|
|
423
764
|
console.log(` ✓ Validated (0 errors, 0 warnings) — ${id}`);
|
|
424
765
|
}
|
|
@@ -773,13 +1114,30 @@ export function registerWorkflowCommands(program) {
|
|
|
773
1114
|
}
|
|
774
1115
|
});
|
|
775
1116
|
workflows
|
|
776
|
-
.command('move-step <workflowId> <stepId>
|
|
777
|
-
.description('Move a step to a new location in a workflow')
|
|
1117
|
+
.command('move-step <workflowId> <stepId> [insertAfter]')
|
|
1118
|
+
.description('Move a step to a new location in a workflow. Use legacy positional <insertAfter>, --after <stepId>, or --position first|last.')
|
|
1119
|
+
.option('--after <stepId>', 'Insert after this step ID')
|
|
1120
|
+
.option('--position <pos>', 'Move to first or last position')
|
|
778
1121
|
.option('--format <fmt>', 'Output format', 'json')
|
|
779
1122
|
.action(async (workflowId, stepId, insertAfter, opts) => {
|
|
780
1123
|
try {
|
|
1124
|
+
const after = opts.after || insertAfter;
|
|
1125
|
+
const position = opts.position;
|
|
1126
|
+
if (!after && !position) {
|
|
1127
|
+
printError('Provide <insertAfter>, --after <stepId>, or --position first|last');
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (after && position) {
|
|
1131
|
+
printError('Provide either an insert-after target or --position, not both');
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (position && position !== 'first' && position !== 'last') {
|
|
1135
|
+
printError('--position must be "first" or "last"');
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
781
1138
|
const client = new AgentledClient();
|
|
782
|
-
const
|
|
1139
|
+
const target = position ? { position: position } : after;
|
|
1140
|
+
const result = await client.moveStep(workflowId, stepId, target);
|
|
783
1141
|
printOutput(result, opts.format);
|
|
784
1142
|
}
|
|
785
1143
|
catch (e) {
|