UNPKG

@agentled/cli

Version:

CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.

1,231 lines 56.1 kB
/* eslint-disable no-console */
import { AgentledClient } from '../client.js';
import { printOutput, printError } from '../utils/output.js';
import { preflightPipeline, formatPreflightIssue } from '../utils/preflight.js';
import { registerScaffoldCommand } from './scaffold.js';
import * as fs from 'node:fs';
import { join, resolve } from 'node:path';
import { findWorkspaceDir } from '../utils/workspace-folder.js';
import { lintPipeline } from '../utils/pipeline-lint.js';
import { makeTestSkeleton } from '../utils/test-runner.js';
/**
 * Exit code used when a workflow is created/updated but server-side validation
 * reports errors. Distinct from `1` (user/network error) so CI pipelines can
 * detect "the pipeline JSON is broken" vs "the CLI itself failed".
 */
const VALIDATION_EXIT_CODE = 2;
function formatIssueLine(issue) {
    const scope = issue.stepId ? `[${issue.stepId}] ` : '';
    const code = issue.code ? ` (${issue.code})` : '';
    const msg = typeof issue.message === 'string' ? issue.message : JSON.stringify(issue);
    const fix = issue.suggestedFix ? `\n      fix: ${issue.suggestedFix}` : '';
    return `    - ${scope}${msg}${code}${fix}`;
}
/**
 * Calls validate_workflow on a freshly created/updated workflow. Returns a
 * tagged outcome instead of a boolean so callers can distinguish "broken
 * pipeline" (exit 2) from "CLI/network failure" (exit 1).
 *
 * In pretty formats this function prints the human-readable report directly.
 * In JSON format it is silent — the caller is responsible for emitting the
 * combined envelope so fields from the create/update mutation response
 * (name, pathname, status, workspaceId, …) are preserved for script consumers.
 *
 * When called from `workflows create`, the caller sets `createdId` so the
 * fix-flow hint can tell the user how to clean up.
 */
async function runAutoValidate(client, workflowId, opts) {
    let result;
    try {
        let pipeline;
        let validationSource = 'live';
        if (opts.source === 'draft' || opts.source === 'auto') {
            try {
                const draft = await client.getDraft(workflowId);
                if (draft?.draft?.config) {
                    pipeline = draft.draft.config;
                    validationSource = 'draft';
                }
                else if (opts.source === 'draft') {
                    throw new Error(`No draft exists for workflow ${workflowId}`);
                }
            }
            catch (e) {
                if (opts.source === 'draft')
                    throw e;
            }
        }
        result = await client.validateWorkflow(workflowId, pipeline);
        result = { ...result, source: validationSource };
    }
    catch (e) {
        // The workflow was saved; only the post-hoc validate call failed.
        // Surface as a structured warning but do not treat it as a pipeline
        // defect — callers map this outcome to exit code 1, not 2.
        console.error(`\nValidation request failed: ${e.message}`);
        console.error(`  The workflow was saved as ${workflowId}, but we could not validate it automatically.`);
        console.error(`  Run: agentled workflows validate ${workflowId}`);
        return { status: 'request-failed' };
    }
    if (opts.format !== 'json') {
        const errorCount = result.errors?.length ?? 0;
        const warnCount = result.warnings?.length ?? 0;
        if (result.source && result.source !== 'live') {
            console.log(`  Source: ${result.source}`);
        }
        if (errorCount === 0 && warnCount === 0) {
            console.log(`  ✓ Validated (0 errors, 0 warnings) — ${workflowId}`);
        }
        else {
            if (errorCount > 0) {
                console.error(`\n  ✗ Validation failed: ${errorCount} error${errorCount === 1 ? '' : 's'}`);
                for (const e of result.errors)
                    console.error(formatIssueLine(e));
            }
            if (warnCount > 0) {
                console.log(`\n  ⚠ ${warnCount} warning${warnCount === 1 ? '' : 's'}`);
                for (const w of result.warnings)
                    console.log(formatIssueLine(w));
            }
        }
    }
    if (!result.valid) {
        const id = opts.createdId ?? workflowId;
        console.error(`\n  Workflow ${id} was saved but is broken. Fix flow:`);
        console.error(`    1. agentled workflows export ${id} --output pipeline.json`);
        console.error(`    2. edit pipeline.json (use \`agentled workflows scaffold <pattern>\` or \`agentled examples <pattern>\` for reference)`);
        console.error(`    3. agentled workflows validate --file pipeline.json   # local preflight, no API call`);
        console.error(`    4. agentled workflows update ${id} --file pipeline.json`);
        console.error(`    (or) agentled workflows delete ${id}`);
    }
    return { status: result.valid ? 'valid' : 'invalid', result };
}
/**
 * Map a ValidateOutcome to its CLI exit code. `valid` returns 0 (caller does
 * not exit), `invalid` returns 2 (pipeline broken), `request-failed` returns
 * 1 (CLI/network problem).
 */
function exitCodeForOutcome(outcome) {
    if (outcome.status === 'valid')
        return 0;
    if (outcome.status === 'invalid')
        return VALIDATION_EXIT_CODE;
    return 1;
}
const CONFIG_UPDATE_FIELDS = ['name', 'goal', 'description', 'steps', 'context', 'style', 'analyticsConfig'];
const READ_ONLY_WORKFLOW_FIELDS = new Set([
    '_agentled',
    'id',
    'workspaceId',
    'workspace',
    'createdAt',
    'updatedAt',
    'owner',
    'executions',
    'executionInputs',
    'computedMetrics',
    'notificationBadge',
    'agents',
    'team',
    'hasDraftSnapshot',
    'draftSnapshot',
    'urls',
]);
function normalizeRevision(value) {
    const revision = typeof value === 'number'
        ? value
        : typeof value === 'string'
            ? Number.parseInt(value, 10)
            : Number.NaN;
    return Number.isFinite(revision) && revision >= 0 ? revision : undefined;
}
function pickWorkflowConfig(workflow) {
    const config = {};
    for (const key of CONFIG_UPDATE_FIELDS) {
        if (workflow[key] !== undefined)
            config[key] = workflow[key];
    }
    return config;
}
function getWorkflowFromResponse(result) {
    return (result?.workflow && typeof result.workflow === 'object') ? result.workflow : result;
}
function normalizeWorkflowUpdateFile(parsed) {
    let source;
    let updates = parsed;
    if (parsed?.export?.pipeline) {
        updates = parsed.export.pipeline;
        source = parsed.export.sourceWorkflow;
    }
    else if (parsed?.exportVersion && parsed?.pipeline) {
        updates = parsed.pipeline;
        source = parsed.sourceWorkflow;
    }
    else if (parsed?.workflow) {
        updates = parsed.workflow;
        source = {
            workflowId: parsed.workflow.id,
            updatedAt: parsed.workflow.updatedAt,
            revision: normalizeRevision(parsed.workflow.metadata?.revision),
            status: parsed.workflow.status,
        };
    }
    else if (parsed?._agentled) {
        source = {
            workflowId: parsed._agentled.workflowId,
            updatedAt: parsed._agentled.updatedAt,
            revision: normalizeRevision(parsed._agentled.revision),
            status: parsed._agentled.status,
        };
    }
    const cleaned = {};
    for (const [key, value] of Object.entries(updates || {})) {
        if (!READ_ONLY_WORKFLOW_FIELDS.has(key))
            cleaned[key] = value;
    }
    return { updates: cleaned, source };
}
function hasConfigUpdate(updates) {
    return CONFIG_UPDATE_FIELDS.some((field) => Object.prototype.hasOwnProperty.call(updates, field));
}
function buildPulledWorkflowFile(workflow) {
    return {
        _agentled: {
            kind: 'workflow-update-file',
            workflowId: workflow.id,
            updatedAt: workflow.updatedAt,
            revision: normalizeRevision(workflow.metadata?.revision),
            status: workflow.status,
            pulledAt: new Date().toISOString(),
        },
        ...pickWorkflowConfig(workflow),
    };
}
function diffWorkflowConfig(remote, updates) {
    const changedFields = CONFIG_UPDATE_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(updates, field)
        && JSON.stringify(remote[field] ?? null) !== JSON.stringify(updates[field] ?? null));
    const remoteSteps = Array.isArray(remote.steps) ? remote.steps : [];
    const nextSteps = Array.isArray(updates.steps) ? updates.steps : remoteSteps;
    const remoteIds = new Set(remoteSteps.map((step) => step?.id).filter(Boolean));
    const nextIds = new Set(nextSteps.map((step) => step?.id).filter(Boolean));
    const addedSteps = Array.from(nextIds).filter((id) => !remoteIds.has(id));
    const removedSteps = Array.from(remoteIds).filter((id) => !nextIds.has(id));
    const changedSteps = nextSteps
        .filter((step) => step?.id && remoteIds.has(step.id))
        .filter((step) => {
        const previous = remoteSteps.find((candidate) => candidate?.id === step.id);
        return JSON.stringify(previous ?? null) !== JSON.stringify(step ?? null);
    })
        .map((step) => step.id);
    return { changedFields, addedSteps, removedSteps, changedSteps };
}
async function verifySafeFileUpdate(client, workflowId, filePath, updates, source, opts) {
    if (!hasConfigUpdate(updates) || opts.force)
        return true;
    const remote = getWorkflowFromResponse(await client.getWorkflow(workflowId));
    const expectedUpdatedAt = opts.expectedUpdatedAt || source?.updatedAt;
    const expectedRevision = normalizeRevision(source?.revision);
    const remoteRevision = normalizeRevision(remote.metadata?.revision);
    if (source?.workflowId && source.workflowId !== workflowId) {
        printError(`File was pulled from workflow ${source.workflowId}, not ${workflowId}. Pull the target workflow first or use --force.`);
        return false;
    }
    if (expectedRevision !== undefined && remoteRevision !== undefined) {
        if (expectedRevision !== remoteRevision) {
            printError(`Refusing stale update. File is based on revision ${expectedRevision}, but remote is revision ${remoteRevision}. ` +
                `Run: agentled workflows pull ${workflowId} --output ${filePath} --force, compare/re-apply your edits, then update or replace again.`);
            return false;
        }
        return true;
    }
    if (!expectedUpdatedAt) {
        printError('Refusing config update from a file without a remote revision or updatedAt token. ' +
            `Run: agentled workflows pull ${workflowId} --output ${filePath} --force, re-apply your edits, then run workflows diff/replace. ` +
            'Use --force only when you intentionally want to overwrite remote config.');
        return false;
    }
    if (expectedUpdatedAt !== remote.updatedAt) {
        printError(`Refusing stale update. File is based on ${expectedUpdatedAt}, but remote is ${remote.updatedAt}. ` +
            `Run: agentled workflows pull ${workflowId} --output ${filePath} --force, compare/re-apply your edits, then update or replace again.`);
        return false;
    }
    return true;
}
export function registerWorkflowCommands(program) {
    const workflows = program
        .command('workflows')
        .alias('wf')
        .description('Manage workflows');
    registerScaffoldCommand(workflows);
    workflows
        .command('list')
        .description('List all workflows in the workspace')
        .option('--status <status>', 'Filter by status (draft, live, paused, archived)')
        .option('--limit <n>', 'Max results', parseInt)
        .option('--format <fmt>', 'Output format: json, table, minimal', 'json')
        .action(async (opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.listWorkflows({
                status: opts.status,
                limit: opts.limit,
            });
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('get <id>')
        .description('Get full workflow details by ID, including useCaseContext and operating-guide warnings when available')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.getWorkflow(id);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('pull <id>')
        .description('Pull a workflow into an update-safe local file with a remote updatedAt token. Use this before workflows update --file.')
        .option('--output <path>', 'Write workflow config to a specific path')
        .option('--force', 'Overwrite existing files', false)
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.getWorkflow(id);
            const pipeline = getWorkflowFromResponse(result);
            const updateFile = buildPulledWorkflowFile(pipeline);
            const wsDir = findWorkspaceDir();
            const name = String(pipeline.name ?? id).toLowerCase().replace(/[^a-z0-9]+/g, '-');
            const filename = `${name}-${id}.json`;
            let savedPath;
            let testPath = null;
            let testSkippedReason = null;
            if (opts.output) {
                savedPath = resolve(opts.output);
                if (fs.existsSync(savedPath) && !opts.force) {
                    printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
                    return;
                }
                fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
            }
            else if (wsDir) {
                const liveDir = join(wsDir, 'examples', 'live');
                fs.mkdirSync(liveDir, { recursive: true });
                savedPath = join(liveDir, filename);
                if (fs.existsSync(savedPath) && !opts.force) {
                    printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
                    return;
                }
                fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
                const testsDir = join(wsDir, 'tests');
                fs.mkdirSync(testsDir, { recursive: true });
                testPath = join(testsDir, `${id}.test.json`);
                if (!fs.existsSync(testPath) || opts.force) {
                    const skeleton = makeTestSkeleton(id, pipeline);
                    fs.writeFileSync(testPath, JSON.stringify(skeleton, null, 2) + '\n');
                }
                else {
                    testSkippedReason = 'test file already exists (use --force to regenerate)';
                }
            }
            else {
                savedPath = resolve(filename);
                if (fs.existsSync(savedPath) && !opts.force) {
                    printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
                    return;
                }
                fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
            }
            const allSteps = (Array.isArray(pipeline.steps) ? pipeline.steps : []);
            const testableCount = allSteps.filter((s) => ['aiAction', 'aiActionWithTools', 'appAction', 'code'].includes(String(s.type ?? ''))).length;
            printOutput({
                workflowId: id,
                workflowName: pipeline.name,
                savedPath,
                testFilePath: testPath,
                testSkippedReason,
                workspaceDir: wsDir,
                stepCount: allSteps.length,
                testableSteps: testableCount,
                nextSteps: [
                    `edit ${savedPath}`,
                    `agentled workflows diff ${id} --file ${savedPath}`,
                    `agentled workflows replace ${id} --file ${savedPath}`,
                    `agentled workflows start ${id}     # run once to capture fixtures`,
                    `agentled fixture capture <execId> --wf ${id}`,
                    `agentled test ${id}                # run assertions against fixtures`,
                ],
            }, (opts.format ?? 'json'));
        }
        catch (e) {
            printError(e instanceof Error ? e.message : String(e));
        }
    });
    workflows
        .command('diff <id>')
        .description('Compare a local workflow update file with the current remote workflow before updating.')
        .requiredOption('--file <path>', 'Path to workflow update JSON file')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const raw = fs.readFileSync(opts.file, 'utf-8');
            const { updates, source } = normalizeWorkflowUpdateFile(JSON.parse(raw));
            const client = new AgentledClient();
            const remote = getWorkflowFromResponse(await client.getWorkflow(id));
            const diff = diffWorkflowConfig(remote, updates);
            const remoteRevision = normalizeRevision(remote.metadata?.revision);
            const sourceRevision = normalizeRevision(source?.revision);
            const sourceMatchesRemote = sourceRevision !== undefined && remoteRevision !== undefined
                ? sourceRevision === remoteRevision
                : source?.updatedAt
                    ? source.updatedAt === remote.updatedAt
                    : undefined;
            printOutput({
                workflowId: id,
                file: resolve(opts.file),
                remoteRevision,
                fileSourceRevision: sourceRevision,
                remoteUpdatedAt: remote.updatedAt,
                fileSourceUpdatedAt: source?.updatedAt,
                sourceMatchesRemote,
                stale: sourceMatchesRemote === false,
                ...diff,
                summary: {
                    changedFields: diff.changedFields.length,
                    addedSteps: diff.addedSteps.length,
                    removedSteps: diff.removedSteps.length,
                    changedSteps: diff.changedSteps.length,
                },
                nextStep: sourceMatchesRemote === false
                    ? `Remote changed since this file was pulled. Run: agentled workflows pull ${id} --output ${opts.file} --force, then re-apply your edits.`
                    : `If this diff is intended, run: agentled workflows replace ${id} --file ${opts.file}`,
            }, (opts.format ?? 'json'));
            if (sourceMatchesRemote === false)
                process.exitCode = 1;
        }
        catch (e) {
            printError(e instanceof Error ? e.message : String(e));
        }
    });
    workflows
        .command('lint <file>')
        .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.')
        .option('--format <fmt>', 'Output format', 'json')
        .action((file, opts) => {
        try {
            const abs = resolve(file);
            if (!fs.existsSync(abs)) {
                printError(`File not found: ${abs}`);
                process.exitCode = VALIDATION_EXIT_CODE;
                return;
            }
            let pipeline;
            try {
                pipeline = JSON.parse(fs.readFileSync(abs, 'utf-8'));
            }
            catch (err) {
                printError(`Failed to parse JSON: ${err instanceof Error ? err.message : String(err)}`);
                process.exitCode = VALIDATION_EXIT_CODE;
                return;
            }
            const issues = lintPipeline(pipeline);
            const errors = issues.filter((i) => i.severity === 'error');
            const warnings = issues.filter((i) => i.severity === 'warning');
            printOutput({
                file: abs,
                issues,
                summary: {
                    total: issues.length,
                    errors: errors.length,
                    warnings: warnings.length,
                },
            }, (opts.format ?? 'json'));
            if (errors.length > 0)
                process.exitCode = VALIDATION_EXIT_CODE;
            else if (warnings.length > 0)
                process.exitCode = 1;
        }
        catch (e) {
            printError(e instanceof Error ? e.message : String(e));
        }
    });
    workflows
        .command('create')
        .description('Create a new workflow. Recommended: create with just name+goal (--skip-validate), then add-step one at a time for per-step validation. Full pipeline JSON via --file is for imports/templates.')
        .option('--file <path>', 'Path to pipeline JSON file')
        .option('--pipeline <json>', 'Inline pipeline JSON')
        .option('--locale <locale>', 'Locale (default: en)')
        .option('--format <fmt>', 'Output format', 'json')
        .option('--skip-validate', 'Skip the post-create validate call (advanced — legacy raw create behavior)')
        .action(async (opts) => {
        try {
            let pipeline;
            if (opts.file) {
                const raw = fs.readFileSync(opts.file, 'utf-8');
                pipeline = JSON.parse(raw);
            }
            else if (opts.pipeline) {
                pipeline = JSON.parse(opts.pipeline);
            }
            else {
                printError('Provide --file or --pipeline');
                return;
            }
            const client = new AgentledClient();
            const format = (opts.format ?? 'json');
            const result = await client.createWorkflow(pipeline, opts.locale);
            const workflowId = (result?.id ?? result?.workflow?.id);
            if (opts.skipValidate || !workflowId) {
                printOutput(result, format);
                return;
            }
            if (format === 'json') {
                // Compound envelope: preserve every field from the create
                // mutation response (name, pathname, status, workspaceId, …)
                // AND include the validation report. Scripts that parse
                // top-level fields continue to work.
                const outcome = await runAutoValidate(client, workflowId, {
                    format,
                    createdId: workflowId,
                });
                const validation = outcome.status === 'request-failed'
                    ? { requestFailed: true }
                    : outcome.result;
                printOutput({ ...result, validation }, 'json');
                const code = exitCodeForOutcome(outcome);
                if (code !== 0)
                    process.exit(code);
                return;
            }
            // Pretty-print formats: emit the create response first, then validation.
            printOutput(result, format);
            const outcome = await runAutoValidate(client, workflowId, {
                format,
                createdId: workflowId,
            });
            const code = exitCodeForOutcome(outcome);
            if (code !== 0)
                process.exit(code);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('update <id>')
        .description('Update an existing workflow (auto-validates unless --skip-validate)')
        .option('--file <path>', 'Path to updates JSON file')
        .option('--updates <json>', 'Inline updates JSON')
        .option('--expected-updated-at <timestamp>', 'Require the remote workflow updatedAt to match before applying --file')
        .option('--force', 'Bypass the --file stale-update guard (advanced)')
        .option('--locale <locale>', 'Locale')
        .option('--format <fmt>', 'Output format', 'json')
        .option('--skip-validate', 'Skip the post-update validate call (advanced)')
        .action(async (id, opts) => {
        try {
            let updates;
            let source;
            if (opts.file) {
                const raw = fs.readFileSync(opts.file, 'utf-8');
                const normalized = normalizeWorkflowUpdateFile(JSON.parse(raw));
                updates = normalized.updates;
                source = normalized.source;
            }
            else if (opts.updates) {
                updates = JSON.parse(opts.updates);
            }
            else {
                printError('Provide --file or --updates');
                return;
            }
            const client = new AgentledClient();
            const format = (opts.format ?? 'json');
            if (opts.file) {
                const safe = await verifySafeFileUpdate(client, id, opts.file, updates, source, {
                    force: opts.force,
                    expectedUpdatedAt: opts.expectedUpdatedAt,
                });
                if (!safe)
                    return;
            }
            const result = await client.updateWorkflow(id, updates, opts.locale);
            if (opts.skipValidate) {
                printOutput(result, format);
                return;
            }
            if (format === 'json') {
                // Preserve the update mutation response fields in the JSON
                // envelope — `result` may contain the refreshed workflow
                // record, which script consumers parse.
                const outcome = await runAutoValidate(client, id, {
                    format,
                    source: result?.editingDraft ? 'draft' : 'live',
                });
                const validation = outcome.status === 'request-failed'
                    ? { requestFailed: true }
                    : outcome.result;
                const envelope = result && typeof result === 'object'
                    ? { ...result, validation }
                    : { workflowId: id, validation };
                printOutput(envelope, 'json');
                const code = exitCodeForOutcome(outcome);
                if (code !== 0)
                    process.exit(code);
                return;
            }
            printOutput(result, format);
            const outcome = await runAutoValidate(client, id, {
                format,
                source: result?.editingDraft ? 'draft' : 'live',
            });
            const code = exitCodeForOutcome(outcome);
            if (code !== 0)
                process.exit(code);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('replace <id>')
        .description('Replace workflow config fields from a pulled file. Uses true replace semantics for context and validates draft when editing live workflows.')
        .requiredOption('--file <path>', 'Path to workflow JSON from workflows pull/export')
        .option('--expected-updated-at <timestamp>', 'Require the remote workflow updatedAt to match before replacing')
        .option('--force', 'Bypass the stale-file guard (advanced)')
        .option('--locale <locale>', 'Locale')
        .option('--format <fmt>', 'Output format', 'json')
        .option('--skip-validate', 'Skip the post-replace validate call (advanced)')
        .action(async (id, opts) => {
        try {
            const raw = fs.readFileSync(opts.file, 'utf-8');
            const { updates, source } = normalizeWorkflowUpdateFile(JSON.parse(raw));
            const client = new AgentledClient();
            const format = (opts.format ?? 'json');
            const safe = await verifySafeFileUpdate(client, id, opts.file, updates, source, {
                force: opts.force,
                expectedUpdatedAt: opts.expectedUpdatedAt,
            });
            if (!safe)
                return;
            const result = await client.replaceWorkflow(id, updates, opts.locale);
            if (opts.skipValidate) {
                printOutput(result, format);
                return;
            }
            const validateOpts = {
                format,
                source: result?.editingDraft ? 'draft' : 'live',
            };
            if (format === 'json') {
                const outcome = await runAutoValidate(client, id, validateOpts);
                const validation = outcome.status === 'request-failed'
                    ? { requestFailed: true }
                    : outcome.result;
                const envelope = result && typeof result === 'object'
                    ? { ...result, validation }
                    : { workflowId: id, validation };
                printOutput(envelope, 'json');
                const code = exitCodeForOutcome(outcome);
                if (code !== 0)
                    process.exit(code);
                return;
            }
            printOutput(result, format);
            const outcome = await runAutoValidate(client, id, validateOpts);
            const code = exitCodeForOutcome(outcome);
            if (code !== 0)
                process.exit(code);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('delete <id>')
        .description('Preview or confirm permanent workflow deletion')
        .option('--confirm-token <token>', 'Confirm deletion with the token returned by the preview')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.deleteWorkflow(id, opts.confirmToken);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('validate [id]')
        .description('Validate a workflow. With <id>: server-side validation. With --file/--pipeline and no id: fast client-side preflight (no API call).')
        .option('--file <path>', 'Path to pipeline JSON file (client-side preflight if no <id> is given)')
        .option('--pipeline <json>', 'Inline pipeline JSON (overrides the saved pipeline when <id> is given; otherwise runs preflight)')
        .option('--source <src>', 'Validation source when <id> is provided: auto (default) | live | draft', 'auto')
        .option('--draft', 'Validate the current draft config for this workflow')
        .option('--live', 'Validate the live stored workflow config')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            let pipeline = undefined;
            if (opts.file) {
                pipeline = JSON.parse(fs.readFileSync(opts.file, 'utf-8'));
            }
            else if (opts.pipeline) {
                pipeline = JSON.parse(opts.pipeline);
            }
            // No workflow id → run a local preflight. This is the "pre-create"
            // path: catches invalid step types, duplicate ids, dangling next
            // references, stripped root fields, missing prompt templates —
            // without spending a create+validate+delete round trip.
            if (!id) {
                if (!pipeline) {
                    printError('Provide either <id> (server validate) or --file/--pipeline (client preflight).');
                    return;
                }
                const result = preflightPipeline(pipeline);
                const format = opts.format;
                if (format === 'json') {
                    printOutput({ preflight: true, ...result }, 'json');
                }
                else {
                    if (result.valid) {
                        console.log(`  ✓ Preflight OK (${result.stepCount} steps, ${result.warnings.length} warnings)`);
                    }
                    else {
                        console.error(`\n  ✗ Preflight failed: ${result.errors.length} error${result.errors.length === 1 ? '' : 's'}`);
                        for (const e of result.errors)
                            console.error(formatPreflightIssue(e));
                    }
                    if (result.warnings.length > 0) {
                        console.log(`\n  ⚠ ${result.warnings.length} warning${result.warnings.length === 1 ? '' : 's'}`);
                        for (const w of result.warnings)
                            console.log(formatPreflightIssue(w));
                    }
                    console.log('\n  Note: preflight is a fast structural check. Run `agentled workflows create` for full server-side validation.');
                }
                if (!result.valid)
                    process.exit(VALIDATION_EXIT_CODE);
                return;
            }
            const client = new AgentledClient();
            if (opts.draft && opts.live) {
                printError('Use either --draft or --live, not both.');
                return;
            }
            const source = opts.draft ? 'draft' : opts.live ? 'live' : String(opts.source ?? 'auto');
            if (!['auto', 'live', 'draft'].includes(source)) {
                printError(`Invalid --source "${source}". Must be one of: auto, live, draft.`);
                return;
            }
            // When source is specified, resolve which stored config to validate:
            // - draft: validates the current draft pipeline and errors if none exists
            // - auto: validates draft if present, otherwise live
            // - live: validates the live stored workflow
            let resolvedSource = 'live';
            if (!pipeline && source !== 'live') {
                try {
                    const draft = await client.getDraft(id);
                    if (draft?.draft?.config) {
                        pipeline = draft.draft.config;
                        resolvedSource = 'draft';
                    }
                    else if (source === 'draft') {
                        printError(`No draft exists for workflow ${id}.`);
                        return;
                    }
                }
                catch {
                    if (source === 'draft') {
                        printError(`No draft exists for workflow ${id}.`);
                        return;
                    }
                }
            }
            const result = await client.validateWorkflow(id, pipeline);
            const format = opts.format;
            const validationSource = opts.pipeline || opts.file
                ? 'inline'
                : (pipeline ? resolvedSource : 'live');
            if (format === 'json') {
                printOutput({ ...result, source: validationSource }, 'json');
            }
            else {
                const errorCount = result.errors?.length ?? 0;
                const warnCount = result.warnings?.length ?? 0;
                console.log(`  Source: ${validationSource}`);
                if (errorCount === 0 && warnCount === 0) {
                    console.log(`  ✓ Validated (0 errors, 0 warnings) — ${id}`);
                }
                else {
                    if (errorCount > 0) {
                        console.error(`\n  ✗ Validation failed: ${errorCount} error${errorCount === 1 ? '' : 's'}`);
                        for (const e of result.errors)
                            console.error(formatIssueLine(e));
                    }
                    if (warnCount > 0) {
                        console.log(`\n  ⚠ ${warnCount} warning${warnCount === 1 ? '' : 's'}`);
                        for (const w of result.warnings)
                            console.log(formatIssueLine(w));
                    }
                }
            }
            if (!result.valid) {
                console.error(`\n  Fix flow:`);
                console.error(`    1. agentled workflows export ${id} --output pipeline.json`);
                console.error(`    2. edit pipeline.json`);
                console.error(`    3. agentled workflows validate --file pipeline.json   # local preflight`);
                console.error(`    4. agentled workflows update ${id} --file pipeline.json`);
                console.error(`    (or) agentled workflows delete ${id}`);
                process.exit(VALIDATION_EXIT_CODE);
            }
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('publish <id>')
        .description('Change workflow status (live, paused, archived)')
        .requiredOption('--status <status>', 'Target status: live, paused, archived')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.publishWorkflow(id, opts.status);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('start <id>')
        .description('Start a workflow execution')
        .option('--input <json>', 'Input data as JSON')
        .option('--input-file <path>', 'Input data from JSON file')
        .option('--metadata <json>', 'Execution metadata as JSON')
        .option('--use-mocks', 'Honor per-step mock data (default behavior — zero credits for mocked steps)')
        .option('--no-mocks', 'Force a real run that ignores all step mocks (consumes real credits)')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            let input;
            if (opts.inputFile) {
                input = JSON.parse(fs.readFileSync(opts.inputFile, 'utf-8'));
            }
            else if (opts.input) {
                input = JSON.parse(opts.input);
            }
            let metadata = opts.metadata ? JSON.parse(opts.metadata) : undefined;
            // Commander turns --no-mocks into opts.mocks === false, and --use-mocks into opts.useMocks === true.
            // Only one should be set; if both are present --no-mocks wins (explicit opt-out).
            const wantsRealRun = opts.mocks === false;
            const wantsMocks = opts.useMocks === true && !wantsRealRun;
            if (wantsRealRun || wantsMocks) {
                metadata = {
                    ...(metadata ?? {}),
                    mockConfig: { ...(metadata?.mockConfig ?? {}), disabled: wantsRealRun },
                };
            }
            const client = new AgentledClient();
            const result = await client.startWorkflow(id, input, metadata);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('export <id>')
        .description('Export a workflow as portable JSON')
        .option('--output <path>', 'Write export to file instead of stdout')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.exportWorkflow(id);
            if (opts.output) {
                fs.writeFileSync(opts.output, JSON.stringify(result, null, 2));
                console.log(`Exported to ${opts.output}`);
            }
            else {
                printOutput(result, opts.format);
            }
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('import')
        .description('Import a workflow from export JSON')
        .option('--file <path>', 'Path to export JSON file')
        .option('--data <json>', 'Inline export JSON')
        .option('--locale <locale>', 'Locale')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (opts) => {
        try {
            let exportData;
            if (opts.file) {
                exportData = JSON.parse(fs.readFileSync(opts.file, 'utf-8'));
            }
            else if (opts.data) {
                exportData = JSON.parse(opts.data);
            }
            else {
                printError('Provide --file or --data');
                return;
            }
            const client = new AgentledClient();
            const result = await client.importWorkflow(exportData, opts.locale);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    // --- Snapshots ---
    workflows
        .command('snapshots <id>')
        .description('List config snapshots for a workflow')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.listSnapshots(id);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('snapshot-content <id> <snapshotId>')
        .description('Read a snapshot\'s full config without restoring it')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, snapshotId, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.getSnapshotContent(id, snapshotId);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('restore <id> <snapshotId>')
        .description('Restore a workflow to a previous config snapshot')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, snapshotId, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.restoreSnapshot(id, snapshotId);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('snapshot-create <id>')
        .description('Create a manual config snapshot for a workflow')
        .option('--label <label>', 'Snapshot label')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.createSnapshot(id, opts.label);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('snapshot-delete <id> <snapshotId>')
        .description('Delete a config snapshot')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, snapshotId, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.deleteSnapshot(id, snapshotId);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    // --- Draft Lifecycle ---
    const draft = workflows
        .command('draft')
        .description('Manage draft snapshots for live workflows');
    draft
        .command('get <id>')
        .description('Get the draft snapshot for a live workflow')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.getDraft(id);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    draft
        .command('promote <id>')
        .description('Promote draft to live (overwrites live config)')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.promoteDraft(id);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    draft
        .command('discard <id>')
        .description('Discard draft changes (live config stays unchanged)')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (id, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.discardDraft(id);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    // --- n8n Import ---
    workflows
        .command('import-n8n')
        .description('Import an n8n workflow JSON into Agentled')
        .option('--file <path>', 'Path to n8n JSON file')
        .option('--data <json>', 'Inline n8n JSON')
        .option('--name <name>', 'Workflow name override')
        .option('--goal <goal>', 'Workflow goal override')
        .option('--preview', 'Preview only — do not create workflow')
        .option('--locale <locale>', 'Locale')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (opts) => {
        try {
            let n8nJson;
            if (opts.file) {
                n8nJson = JSON.parse(fs.readFileSync(opts.file, 'utf-8'));
            }
            else if (opts.data) {
                n8nJson = JSON.parse(opts.data);
            }
            else {
                printError('Provide --file or --data with n8n JSON');
                return;
            }
            const workflow = (opts.name || opts.goal)
                ? { name: opts.name, goal: opts.goal }
                : undefined;
            const client = new AgentledClient();
            if (opts.preview) {
                const result = await client.previewN8nImport(n8nJson, undefined, workflow);
                printOutput(result, opts.format);
            }
            else {
                const result = await client.importN8nWorkflow(n8nJson, workflow, undefined, opts.locale);
                printOutput(result, opts.format);
            }
        }
        catch (e) {
            printError(e.message);
        }
    });
    // --- Step Operations ---
    workflows
        .command('add-step <workflowId>')
        .description('Add a step to a workflow (recommended authoring path). Create an empty workflow first, then add-step one at a time — each call validates the step immediately. Use --insert-after + --rewire-next to chain steps.')
        .requiredOption('--step <json>', 'Step definition as JSON')
        .option('--insert-after <stepId>', 'Insert after this step ID')
        .option('--rewire-next', 'Rewire the previous step\'s next pointer to the new step', false)
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (workflowId, opts) => {
        try {
            const step = JSON.parse(opts.step);
            const client = new AgentledClient();
            const result = await client.addStep(workflowId, step, opts.insertAfter, opts.rewireNext);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('update-step <workflowId> <stepId>')
        .description('Update a single step. --updates patches (one-level deep-merge); --replace overwrites paths wholesale (for dictionary-shaped fields like stepInputData.fieldUpdates); --unset deletes paths.')
        .option('--updates <json>', 'Partial step updates as JSON (deep-merged one level)')
        .option('--replace <paths>', 'Comma-separated dot-paths whose values in --updates should be assigned wholesale (skips deep-merge). Required for dictionary-shaped fields.', (val, prev = []) => prev.concat(val.split(',').map(s => s.trim()).filter(Boolean)), [])
        .option('--unset <paths>', 'Comma-separated dot-paths to delete from the step.', (val, prev = []) => prev.concat(val.split(',').map(s => s.trim()).filter(Boolean)), [])
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (workflowId, stepId, opts) => {
        try {
            const updates = opts.updates ? JSON.parse(opts.updates) : undefined;
            const replace = opts.replace && opts.replace.length > 0 ? opts.replace : undefined;
            const unset = opts.unset && opts.unset.length > 0 ? opts.unset : undefined;
            if (!updates && !replace && !unset) {
                printError('At least one of --updates, --replace, or --unset must be provided');
                return;
            }
            const client = new AgentledClient();
            const result = await client.updateStep(workflowId, stepId, { updates, replace, unset });
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('get-step <workflowId> <stepId>')
        .description('Read a single step (cheap; ~1KB). Use before editing dictionary-shaped fields so you can send the full new object back via update-step --replace.')
        .option('--source <src>', 'auto (default) | live | draft. auto returns draft if one exists, else live.', 'auto')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (workflowId, stepId, opts) => {
        try {
            const source = opts.source;
            if (!['auto', 'live', 'draft'].includes(source)) {
                printError(`Invalid --source "${source}". Must be one of: auto, live, draft.`);
                return;
            }
            const client = new AgentledClient();
            const result = await client.getStep(workflowId, stepId, source);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('move-step <workflowId> <stepId> [insertAfter]')
        .description('Move a step to a new location in a workflow. Use legacy positional <insertAfter>, --after <stepId>, or --position first|last.')
        .option('--after <stepId>', 'Insert after this step ID')
        .option('--position <pos>', 'Move to first or last position')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (workflowId, stepId, insertAfter, opts) => {
        try {
            const after = opts.after || insertAfter;
            const position = opts.position;
            if (!after && !position) {
                printError('Provide <insertAfter>, --after <stepId>, or --position first|last');
                return;
            }
            if (after && position) {
                printError('Provide either an insert-after target or --position, not both');
                return;
            }
            if (position && position !== 'first' && position !== 'last') {
                printError('--position must be "first" or "last"');
                return;
            }
            const client = new AgentledClient();
            const target = position ? { position: position } : after;
            const result = await client.moveStep(workflowId, stepId, target);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('remove-step <workflowId> <stepId>')
        .description('Remove a step from a workflow')
        .option('--rewire-next', 'Rewire the previous step to point to the removed step\'s next', false)
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (workflowId, stepId, opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.removeStep(workflowId, stepId, opts.rewireNext);
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('update-context <workflowId>')
        .description('Surgical edit of workflow.context and workflow.metadata. Three explicit verbs (--updates / --replace / --unset) on workflow-relative paths under context.inputPages, context.outputPages, context.executionInputConfig, and metadata. OR legacy --context-key/--value for wholesale per-key replacement.')
        .option('--updates <json>', 'Partial workflow patch as JSON. Top-level keys must be "context" and/or "metadata".')
        .option('--replace <paths>', 'Comma-separated workflow-relative dot-paths whose values in --updates should be assigned wholesale (skips deep-merge). Allowed prefixes: context.inputPages, context.outputPages, context.executionInputConfig, metadata.', (val, prev = []) => prev.concat(val.split(',').map(s => s.trim()).filter(Boolean)), [])
        .option('--unset <paths>', 'Comma-separated workflow-relative dot-paths to delete. Same allowed prefixes as --replace.', (val, prev = []) => prev.concat(val.split(',').map(s => s.trim()).filter(Boolean)), [])
        .option('--context-key <key>', 'Legacy per-key shape: one of inputPages | outputPages | executionInputConfig.')
        .option('--value <json>', 'Legacy per-key shape: new value for --context-key as JSON. Pass [] to clear a list.')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (workflowId, opts) => {
        try {
            const updates = opts.updates ? JSON.parse(opts.updates) : undefined;
            const replace = opts.replace && opts.replace.length > 0 ? opts.replace : undefined;
            const unset = opts.unset && opts.unset.length > 0 ? opts.unset : undefined;
            const usingOps = updates !== undefined || replace !== undefined || unset !== undefined;
            const usingLegacy = opts.contextKey !== undefined;
            if (usingOps && usingLegacy) {
                printError('Cannot mix --updates/--replace/--unset with --context-key/--value. Choose one shape.');
                process.exit(1);
            }
            if (!usingOps && !usingLegacy) {
                printError('At least one of --updates, --replace, --unset (ops shape) or --context-key/--value (legacy shape) must be provided');
                return;
            }
            const client = new AgentledClient();
            let result;
            if (usingOps) {
                result = await client.updateWorkflowContext(workflowId, { updates, replace, unset });
            }
            else {
                if (!['inputPages', 'outputPages', 'executionInputConfig'].includes(opts.contextKey)) {
                    printError(`Invalid --context-key "${opts.contextKey}". Must be one of: inputPages, outputPages, executionInputConfig.`);
                    process.exit(1);
                }
                if (opts.value === undefined) {
                    printError('--value is required with --context-key');
                    process.exit(1);
                }
                let value;
                try {
                    value = JSON.parse(opts.value);
                }
                catch {
                    value = opts.value;
                }
                result = await client.updateWorkflowContext(workflowId, opts.contextKey, value);
            }
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
    workflows
        .command('step-schema')
        .description('Get the full schema of allowed PipelineStep fields')
        .option('--format <fmt>', 'Output format', 'json')
        .action(async (opts) => {
        try {
            const client = new AgentledClient();
            const result = await client.getStepSchema();
            printOutput(result, opts.format);
        }
        catch (e) {
            printError(e.message);
        }
    });
}
//# sourceMappingURL=workflows.js.map