@swell/cli
Version:
Swell's command line interface/utility
320 lines (317 loc) • 12.6 kB
JavaScript
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}`;
}