UNPKG

@swell/cli

Version:

Swell's command line interface/utility

320 lines (317 loc) 12.6 kB
import { Args, Flags } from '@oclif/core'; import { InspectResourceCommand, inspectResourceBaseFlags, } from '../../inspect-resource-command.js'; import { classifyIdentifier, } from '../../lib/apps/inspect-scope.js'; import { buildAppSlugMap } from '../../lib/apps/resolve.js'; import { default as localConfig } from '../../lib/config.js'; import { renderKeyMetaTable } from '../../lib/inspect/table.js'; import { isWorkflowInstanceIdentifier, redactWorkflowPayload, resolveWorkflowOperationRef, resolveWorkflowScope, workflowRunDate, workflowDisplayName, workflowRunMeta, WORKFLOW_STATUSES, } from '../../lib/workflows/operations.js'; export default class WorkflowRuns extends InspectResourceCommand { static summary = 'Workflow runtime instances.'; static description = `Lists recent workflow runtime instances, or shows one workflow run by instance id. Identifier forms: (none) - recent runs across installed apps, or --app scope wf_inst_<id> - single run detail Use --workflow=<workflow> to filter list mode by workflow name, app.<app>.<workflow>, or manifest id. `; static args = { identifier: Args.string({ description: 'wf_inst_* workflow run instance id.', required: false, }), }; static flags = { ...inspectResourceBaseFlags, limit: Flags.integer({ default: 10, description: 'Maximum runs to show.', }), status: Flags.string({ description: 'Workflow run status.', options: WORKFLOW_STATUSES, }), workflow: Flags.string({ description: 'Filter by workflow name, app.<app>.<workflow>, or manifest id.', }), }; static examples = [ 'swell inspect workflow-runs', 'swell inspect workflow-runs --app=my-app --status active', 'swell inspect workflow-runs --workflow run-import --app=my-app', 'swell inspect workflow-runs --workflow app.my-app.run-import --status active', 'swell inspect workflow-runs wf_inst_abc123 --app=my-app', 'swell inspect workflow-runs wf_inst_abc123', ]; resourceLabel = 'Workflow runs'; resourceLabelSingular = 'workflow run'; adminPath = '/data/:workflows/runs'; commandName = 'workflow-runs'; detailContext; async run() { const { args, flags } = await this.parse(WorkflowRuns); if (args.identifier && !isWorkflowInstanceIdentifier(args.identifier)) { this.error(`Invalid workflow run identifier '${args.identifier}'. Expected wf_inst_* instance id. Use --workflow=<workflow> to filter run lists.`, { exit: 1 }); } if (args.identifier && flags.workflow) { this.error('--workflow is only valid in list mode.', { exit: 1 }); } if (!flags.live) { await this.api.setEnv('test'); } if (!args.identifier) { if (flags.json) { this.error('--json is only valid when inspecting a single wf_inst_* run.', { exit: 1 }); } await this.showRecentRunList(flags); return; } if (isWorkflowInstanceIdentifier(args.identifier)) { await this.showRunDetail(args.identifier, flags); } } hints() { if (!this.detailContext) { return []; } const { appId, appPart, live, run, workflowName } = this.detailContext; const envFlag = live ? ' --live' : ''; const lines = [ `swell logs --type workflow --app ${appPart} -s ${workflowName}${envFlag}`, ]; if (run?.status === 'active' && run.workflow_instance_id) { lines.unshift(buildTerminateApiHint({ appId: appId || run.app_id || appPart, instanceId: run.workflow_instance_id, live, workflowName, })); } return lines; } async showRecentRunList(flags) { const { runs, hasMore, skippedApps } = flags.app ? await this.getRecentRunsForScopedApp(flags) : await this.getRecentRunsForInstalledApps(flags); const limitedRuns = sortRunsByDateDesc(runs).slice(0, flags.limit); this.log(`Workflow runs in '${localConfig.getDefaultStore()}' ${flags.live ? '[live]' : '[test]'}`); if (limitedRuns.length === 0) { this.log(); this.log(flags.status ? ` (no ${flags.status} workflow runs found)` : ' (no workflow runs found)'); this.log(); return; } const rows = limitedRuns.map((run) => ({ key: run.workflow_instance_id || '-', meta: workflowRunMetaWithContext(run), })); this.log(); for (const line of renderKeyMetaTable(rows)) { this.log(line); } if (skippedApps.length > 0) { this.log(); this.log(`Skipped inaccessible apps: ${skippedApps.join(', ')}`); } if (hasMore || runs.length > flags.limit) { this.log(); this.log(`Showing first ${flags.limit} runs. Increase --limit to see more.`); } this.log(); this.log('Run "swell inspect workflow-runs <wf_inst_id>" to view a run.'); this.log(); } async showRunDetail(instanceId, flags) { const detail = await this.getRunDetail(instanceId, flags); const redacted = redactWorkflowPayload(detail); const manifest = redacted.manifest || {}; const appPart = this.detailContext?.appPart || manifest.app_id || ''; const workflowName = workflowDisplayName(manifest); this.detailContext = { appId: manifest.app_id, appPart, live: flags.live, run: redacted.run, workflowName, }; this.emitDetail(redacted, flags); } async getRunDetail(instanceId, flags) { if (flags.app) { const scope = await this.resolveWorkflowRunScope(flags); this.detailContext = { appPart: scope.appSlug || scope.appId || flags.app, workflowName: '', }; return this.getRunDetailForApp(instanceId, scope.appId || ''); } const details = await Promise.all(Object.entries(await buildAppSlugMap(this.api)).map(async ([appId, appSlug]) => { try { return { appPart: appSlug || appId, detail: await this.getRunDetailForApp(instanceId, appId), }; } catch { return null; } })); const match = details.find(Boolean); if (match) { this.detailContext = { appPart: match.appPart, workflowName: '', }; return match.detail; } throw new Error(`No workflow run found for '${instanceId}'. Pass --app=<slug> if the app is not listed in this store session.`); } async resolveWorkflowRunScope(flags) { const scope = await resolveWorkflowScope(this.api, { app: flags.app }); if (!scope.appId) { throw new Error('Workflow run detail requires --app=<slug> or --app=.'); } return scope; } async getRunDetailForApp(instanceId, appId) { return this.api.get({ adminPath: `/data/:workflows/runs/${instanceId}` }, { query: { app_id: appId } }); } async getRecentRunsForScopedApp(flags) { const [target] = await this.resolveRunListTargets(flags); if (!target) { throw new Error('Workflow run list requires --app=<slug> or --app=.'); } const response = await this.getRunListForApp(target.appId, flags, target.workflowQuery); const runs = (response?.results || []).map((run) => ({ ...run, })); return { hasMore: Boolean(response?.has_more), runs, skippedApps: [], }; } async getRecentRunsForInstalledApps(flags) { const appSlugById = await buildAppSlugMap(this.api); const targets = await this.resolveRunListTargets(flags, appSlugById); const skippedApps = []; let hasMore = false; const results = await Promise.all(targets.map(async ({ appId, appSlug, workflowQuery }) => { try { const response = await this.getRunListForApp(appId, flags, workflowQuery); hasMore = hasMore || Boolean(response?.has_more); return (response?.results || []).map((run) => ({ ...run, app_slug: appSlug || appSlugById[appId], })); } catch { skippedApps.push(appSlug || appSlugById[appId] || appId); return []; } })); return { hasMore, runs: results.flat(), skippedApps, }; } async resolveRunListTargets(flags, appSlugById) { if (flags.app) { const scope = await resolveWorkflowScope(this.api, { app: flags.app }); if (!scope.appId) { throw new Error('Workflow run list requires --app=<slug> or --app=.'); } if (!flags.workflow) { return [{ appId: scope.appId, appSlug: scope.appSlug }]; } const kind = classifyIdentifier(flags.workflow); if (kind.kind === 'name') { return [ { appId: scope.appId, appSlug: scope.appSlug, workflowQuery: { workflow_name: flags.workflow }, }, ]; } const ref = await resolveWorkflowOperationRef(this.api, flags.workflow, { app: flags.app, }); if (ref.appId !== scope.appId) { throw new Error(`--workflow=${flags.workflow} belongs to a different app than --app=${flags.app}.`); } return [ { appId: ref.appId, appSlug: scope.appSlug || ref.appSlug, workflowQuery: ref.query, }, ]; } if (flags.workflow) { const kind = classifyIdentifier(flags.workflow); if (kind.kind === 'name') { const slugMap = appSlugById || (await buildAppSlugMap(this.api)); return Object.entries(slugMap).map(([appId, appSlug]) => ({ appId, appSlug, workflowQuery: { workflow_name: flags.workflow }, })); } const ref = await resolveWorkflowOperationRef(this.api, flags.workflow, {}); return [ { appId: ref.appId, appSlug: ref.appSlug, workflowQuery: ref.query, }, ]; } const slugMap = appSlugById || (await buildAppSlugMap(this.api)); return Object.entries(slugMap).map(([appId, appSlug]) => ({ appId, appSlug, })); } async getRunListForApp(appId, flags, workflowQuery = {}) { const query = { app_id: appId, ...workflowQuery, limit: flags.limit, }; if (flags.status) { query.status = flags.status; } return this.api.get({ adminPath: '/data/:workflows/instances' }, { query, }); } } function sortRunsByDateDesc(runs) { return [...runs].sort((a, b) => { const aDate = Date.parse(workflowRunDate(a) || '') || 0; const bDate = Date.parse(workflowRunDate(b) || '') || 0; return bDate - aDate; }); } function workflowRunMetaWithContext(run) { const context = [ run.app_slug ? `app.${run.app_slug}` : null, run.workflow_name, ] .filter(Boolean) .join('.'); const meta = workflowRunMeta(run); return [context, meta].filter(Boolean).join(' · '); } function buildTerminateApiHint({ appId, instanceId, live, workflowName, }) { const body = JSON.stringify({ app_id: appId, instance_id: instanceId, source: 'cli', workflow_name: workflowName, }); const envFlag = live ? ' --live' : ''; return `swell api post '/:workflows/instances/terminate' --body '${body}'${envFlag}`; }