@sanity/workflow-cli 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +485 -0
  2. package/README.md +62 -18
  3. package/bin/run.js +0 -4
  4. package/dist/commands/abort.d.ts +4 -6
  5. package/dist/commands/abort.js +13 -24
  6. package/dist/commands/definition/delete.d.ts +2 -2
  7. package/dist/commands/definition/delete.js +14 -27
  8. package/dist/commands/definition/diff.d.ts +2 -2
  9. package/dist/commands/definition/diff.js +3 -12
  10. package/dist/commands/definition/list.d.ts +3 -4
  11. package/dist/commands/definition/list.js +40 -46
  12. package/dist/commands/definition/show.d.ts +22 -3
  13. package/dist/commands/definition/show.js +23 -21
  14. package/dist/commands/deploy.d.ts +11 -5
  15. package/dist/commands/deploy.js +54 -57
  16. package/dist/commands/diagnose.d.ts +22 -4
  17. package/dist/commands/diagnose.js +39 -45
  18. package/dist/commands/fire-action.d.ts +2 -4
  19. package/dist/commands/fire-action.js +27 -61
  20. package/dist/commands/list.d.ts +7 -8
  21. package/dist/commands/list.js +43 -56
  22. package/dist/commands/reset-activity.js +0 -1
  23. package/dist/commands/set-stage.d.ts +2 -2
  24. package/dist/commands/set-stage.js +14 -30
  25. package/dist/commands/show.d.ts +2 -2
  26. package/dist/commands/show.js +12 -24
  27. package/dist/commands/start.d.ts +4 -7
  28. package/dist/commands/start.js +18 -54
  29. package/dist/commands/tail.d.ts +2 -2
  30. package/dist/commands/tail.js +20 -25
  31. package/dist/hooks/finally/telemetry.d.ts +8 -0
  32. package/dist/hooks/finally/telemetry.js +13 -0
  33. package/dist/hooks/prerun/telemetry.d.ts +5 -0
  34. package/dist/hooks/prerun/telemetry.js +5 -0
  35. package/dist/index.js +0 -3
  36. package/dist/lib/base-command.d.ts +14 -0
  37. package/dist/lib/base-command.js +10 -0
  38. package/dist/lib/client.js +13 -30
  39. package/dist/lib/context.d.ts +49 -13
  40. package/dist/lib/context.js +49 -48
  41. package/dist/lib/definitions.js +5 -24
  42. package/dist/lib/diff.d.ts +5 -2
  43. package/dist/lib/diff.js +61 -27
  44. package/dist/lib/env.d.ts +4 -0
  45. package/dist/lib/env.js +4 -3
  46. package/dist/lib/fail.d.ts +12 -6
  47. package/dist/lib/fail.js +24 -25
  48. package/dist/lib/flags.d.ts +0 -7
  49. package/dist/lib/flags.js +0 -17
  50. package/dist/lib/load-config.d.ts +11 -7
  51. package/dist/lib/load-config.js +40 -20
  52. package/dist/lib/operation-args.d.ts +14 -23
  53. package/dist/lib/operation-args.js +6 -38
  54. package/dist/lib/ops-report.d.ts +18 -10
  55. package/dist/lib/ops-report.js +20 -16
  56. package/dist/lib/params.js +0 -8
  57. package/dist/lib/read-fanout.d.ts +22 -0
  58. package/dist/lib/read-fanout.js +28 -0
  59. package/dist/lib/select-deployment.js +0 -21
  60. package/dist/lib/share-definitions.d.ts +86 -0
  61. package/dist/lib/share-definitions.js +106 -0
  62. package/dist/lib/stub.js +0 -7
  63. package/dist/lib/telemetry-setup.d.ts +66 -0
  64. package/dist/lib/telemetry-setup.js +92 -0
  65. package/dist/lib/telemetry.d.ts +100 -0
  66. package/dist/lib/telemetry.js +89 -0
  67. package/dist/lib/ui.d.ts +33 -1
  68. package/dist/lib/ui.js +32 -21
  69. package/oclif.manifest.json +8 -47
  70. package/package.json +14 -8
@@ -1,30 +1,20 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
3
- import { deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { actionDisabledDetail, deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import ora from 'ora';
5
+ import { WorkflowCommand } from "../lib/base-command.js";
6
6
  import { resolveInstanceContext } from "../lib/context.js";
7
- import { errorMessage, fail, failOnThrow } from "../lib/fail.js";
8
- import { actorFlags, jsonFlags, tagFlags } from "../lib/flags.js";
9
- import { baseEngineArgs, resolveActor } from "../lib/operation-args.js";
10
- import { emitWriteReport, opsAppliedLines, } from "../lib/ops-report.js";
7
+ import { fail, failOnThrow, failureDetail } from "../lib/fail.js";
8
+ import { jsonFlags, tagFlags } from "../lib/flags.js";
9
+ import { baseEngineArgs } from "../lib/operation-args.js";
10
+ import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../lib/ops-report.js";
11
11
  import { parseParams } from "../lib/params.js";
12
12
  import { instanceHeader } from "./show.js";
13
13
  function reasonText(reason) {
14
- switch (reason.kind) {
15
- case 'filter-failed':
16
- return `action filter not satisfied${reason.detail ? ` (${reason.detail})` : ''}`;
17
- case 'activity-not-active':
18
- return `activity is ${reason.status}`;
19
- case 'stage-terminal':
20
- return `stage '${reason.stage}' is terminal`;
21
- case 'mutation-guard-denied':
22
- return `blocked by mutation guard(s) ${deniedGuardLabels(reason.denied).join(', ')}`;
23
- case 'instance-completed':
24
- return `instance completed at ${reason.completedAt}`;
25
- case 'requirements-unmet':
26
- return `unmet requirement(s): ${reason.unmetRequirements.join(', ')}`;
14
+ if (reason.kind === 'mutation-guard-denied') {
15
+ return `blocked by mutation guard(s) ${deniedGuardLabels(reason.denied).join(', ')}`;
27
16
  }
17
+ return actionDisabledDetail(reason);
28
18
  }
29
19
  function paramLabel(p) {
30
20
  return `${p.name}${p.required === true ? '' : '?'}:${p.type}`;
@@ -43,12 +33,6 @@ function actionLines(a) {
43
33
  }
44
34
  return lines;
45
35
  }
46
- /**
47
- * The list-mode body: every action on the current stage's activities, marked
48
- * fireable or not (with the disabling reason), plus a hint showing the
49
- * flags to fire one. Pure so the populated/empty permutations assert
50
- * without a live evaluation.
51
- */
52
36
  export function renderAvailableActions({ actions, stage, instanceId, }) {
53
37
  if (actions.length === 0) {
54
38
  return [`No actions on the activities of the current stage ('${stage}').`];
@@ -60,8 +44,6 @@ export function renderAvailableActions({ actions, stage, instanceId, }) {
60
44
  lines.push('', styleText('dim', 'Fire one with:'), styleText('dim', ` fire-action ${instanceId} --activity <activity> --action <action>`));
61
45
  return lines;
62
46
  }
63
- /** The `--json` payload for a fired action — the structured counterpart of
64
- * {@link fireActionReport}. Pure so its shape is asserted directly. */
65
47
  export function fireResultJson(result, ids) {
66
48
  return {
67
49
  instanceId: ids.instanceId,
@@ -73,11 +55,6 @@ export function fireResultJson(result, ids) {
73
55
  ...(result.ranOps !== undefined ? { ranOps: result.ranOps } : {}),
74
56
  };
75
57
  }
76
- /**
77
- * Turn a `fireAction` result into the spinner message + ops block. Pure so
78
- * the fired / no-op / cascaded / ops permutations assert without a live
79
- * fire.
80
- */
81
58
  export function fireActionReport({ result, activity, action, }) {
82
59
  const stage = styleText('bold', result.instance.currentStage);
83
60
  if (!result.changed) {
@@ -87,19 +64,17 @@ export function fireActionReport({ result, activity, action, }) {
87
64
  opsLines: [],
88
65
  };
89
66
  }
90
- const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
91
67
  return {
92
68
  changed: true,
93
- message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${tail}`,
69
+ message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${cascadeTail(result.cascaded)}`,
94
70
  opsLines: opsAppliedLines(result.ranOps),
95
71
  };
96
72
  }
97
- export default class FireAction extends Command {
98
- static description = "Fire an action on an instance act on a user's behalf to unstick a waiting activity. Omit --action to list what can be fired.";
73
+ export default class FireAction extends WorkflowCommand {
74
+ static description = 'Fire an action on an instance to unstick a waiting activity the write acts as the configured token. Omit --action to list what can be fired.';
99
75
  static examples = [
100
76
  '<%= config.bin %> fire-action wf-instance.abc123',
101
77
  '<%= config.bin %> fire-action wf-instance.abc123 --activity approve --action approve',
102
- '<%= config.bin %> fire-action wf-instance.abc123 --activity approve --action approve --as user.xyz --as-role editor',
103
78
  '<%= config.bin %> fire-action wf-instance.abc123 --activity publish --action publish --param note=shipping',
104
79
  ];
105
80
  static args = {
@@ -118,19 +93,16 @@ export default class FireAction extends Command {
118
93
  multiple: true,
119
94
  default: [],
120
95
  }),
121
- ...actorFlags,
122
96
  ...jsonFlags,
123
97
  };
124
98
  async run() {
125
99
  const { args, flags } = await this.parse(FireAction);
126
100
  const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
127
- const actor = failOnThrow('fire-action usage error:', () => resolveActor(flags.as, flags['as-role']));
128
101
  if (flags.action === undefined) {
129
102
  await this.listActions({
130
103
  client,
131
104
  scope,
132
105
  instanceId: args.instanceId,
133
- actor,
134
106
  json: flags.json,
135
107
  });
136
108
  return;
@@ -142,21 +114,20 @@ export default class FireAction extends Command {
142
114
  client,
143
115
  scope,
144
116
  instanceId: args.instanceId,
145
- actor,
146
117
  activity: flags.activity,
147
118
  action: flags.action,
148
119
  params: flags.param,
149
120
  json: flags.json,
150
121
  });
151
122
  }
152
- async listActions({ client, scope, instanceId, actor, json, }) {
123
+ async listActions({ client, scope, instanceId, json, }) {
153
124
  const { evaluation, actions } = await workflow
154
125
  .availableActions({
155
126
  client,
156
- ...baseEngineArgs(scope, actor),
127
+ ...baseEngineArgs(scope),
157
128
  instanceId,
158
129
  })
159
- .catch((error) => fail('fire-action failed:', errorMessage(error)));
130
+ .catch((error) => fail('fire-action failed:', failureDetail(error)));
160
131
  const stage = evaluation.instance.currentStage;
161
132
  if (json) {
162
133
  this.log(JSON.stringify({ instanceId, stage, actions }, null, 2));
@@ -169,11 +140,11 @@ export default class FireAction extends Command {
169
140
  }
170
141
  }
171
142
  async fireOne(opts) {
172
- const { client, scope, instanceId, actor, activity, action, json } = opts;
143
+ const { client, scope, instanceId, activity, action, json } = opts;
173
144
  const params = failOnThrow('Invalid --param:', () => parseParams(opts.params));
174
145
  const fireArgs = {
175
146
  client,
176
- ...baseEngineArgs(scope, actor),
147
+ ...baseEngineArgs(scope),
177
148
  instanceId,
178
149
  activity,
179
150
  action,
@@ -182,22 +153,17 @@ export default class FireAction extends Command {
182
153
  if (json) {
183
154
  const result = await workflow
184
155
  .fireAction(fireArgs)
185
- .catch((error) => fail('fire-action error:', errorMessage(error)));
156
+ .catch((error) => fail('fire-action error:', failureDetail(error)));
186
157
  this.log(JSON.stringify(fireResultJson(result, { instanceId, activity, action }), null, 2));
187
158
  return;
188
159
  }
189
- const spinner = ora(`Firing ${action} on ${activity}…`).start();
190
- try {
191
- const result = await workflow.fireAction(fireArgs);
192
- emitWriteReport({
193
- spinner,
194
- report: fireActionReport({ result, activity, action }),
195
- log: (line) => this.log(line),
196
- });
197
- }
198
- catch (error) {
199
- spinner.fail('Action rejected');
200
- fail('fire-action error:', errorMessage(error));
201
- }
160
+ await runWriteVerb({
161
+ startLabel: `Firing ${action} on ${activity}…`,
162
+ failLabel: 'Action rejected',
163
+ failHeadline: 'fire-action error:',
164
+ run: () => workflow.fireAction(fireArgs),
165
+ report: (result) => fireActionReport({ result, activity, action }),
166
+ log: (line) => this.log(line),
167
+ });
202
168
  }
203
169
  }
@@ -1,14 +1,14 @@
1
- import { Command } from '@oclif/core';
1
+ import { type TerminalState } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
2
3
  interface InstanceRow {
3
4
  _id: string;
4
5
  definition: string;
5
6
  tag: string;
6
7
  currentStage: string;
7
- completedAt?: string;
8
- abortedAt?: string;
8
+ completedAt?: string | null;
9
+ abortedAt?: string | null;
9
10
  lastChangedAt: string;
10
11
  }
11
- type InstanceStatus = 'aborted' | 'completed' | 'in-flight';
12
12
  interface ListFlags {
13
13
  'in-flight': boolean;
14
14
  failed: boolean;
@@ -25,17 +25,16 @@ export declare function buildListQuery(flags: ListFlags): {
25
25
  groq: string;
26
26
  params: Record<string, unknown>;
27
27
  };
28
- /** Map a queried instance to its display row (derives the status column).
29
- * Pure — status is the semantic value; the table colors it at render. */
28
+ /** Map a queried instance to its display row (derives the status column). */
30
29
  export declare function instanceRow(r: InstanceRow): {
31
30
  _id: string;
32
31
  definition: string;
33
32
  tag: string;
34
33
  currentStage: string;
35
- status: InstanceStatus;
34
+ status: TerminalState;
36
35
  lastChangedAt: string;
37
36
  };
38
- export default class List extends Command {
37
+ export default class List extends WorkflowCommand {
39
38
  static description: string;
40
39
  static examples: string[];
41
40
  static flags: {
@@ -1,23 +1,17 @@
1
- import { styleText } from 'node:util';
2
- import { Command, Flags } from '@oclif/core';
3
- import { WORKFLOW_INSTANCE_TYPE, tagScopeFilter } from '@sanity/workflow-engine';
4
- import { Table } from 'console-table-printer';
1
+ import { Flags } from '@oclif/core';
2
+ import { WORKFLOW_INSTANCE_TYPE, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
5
3
  import logSymbols from 'log-symbols';
6
- import { resolveReadContext } from "../lib/context.js";
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
+ import { resolveReadTargets } from "../lib/context.js";
7
6
  import { tagFlags } from "../lib/flags.js";
8
- import { formatAge } from "../lib/ui.js";
9
- /**
10
- * Build the GROQ query + params for `list` from its flags. Pure so the
11
- * filter combinations can be asserted without a live dataset. `--tag` is an
12
- * optional filter: omit it and the listing spans every partition.
13
- */
7
+ import { runReadAcrossTargets } from "../lib/read-fanout.js";
8
+ import { clipToLimit, formatAge, formatTable } from "../lib/ui.js";
14
9
  export function buildListQuery(flags) {
15
10
  const filters = [`_type == "${WORKFLOW_INSTANCE_TYPE}"`];
16
11
  if (flags['in-flight'])
17
12
  filters.push('!defined(completedAt)');
18
13
  if (flags.definition)
19
14
  filters.push('definition == $definition');
20
- // Cheap approximation of --failed: any activity in any stage with status "failed".
21
15
  if (flags.failed)
22
16
  filters.push('count(stages[].activities[status == "failed"]) > 0');
23
17
  if (flags.tag)
@@ -31,39 +25,27 @@ export function buildListQuery(flags) {
31
25
  abortedAt,
32
26
  lastChangedAt
33
27
  }`;
34
- const params = { limit: flags.limit };
28
+ const params = { limit: flags.limit + 1 };
35
29
  if (flags.definition)
36
30
  params['definition'] = flags.definition;
37
31
  if (flags.tag)
38
32
  params['tag'] = flags.tag;
39
33
  return { groq, params };
40
34
  }
41
- /** Aborted instances also carry `completedAt`, so check `abortedAt` first. */
42
- function deriveStatus(r) {
43
- if (r.abortedAt)
44
- return 'aborted';
45
- if (r.completedAt)
46
- return 'completed';
47
- return 'in-flight';
48
- }
49
- /** Map a queried instance to its display row (derives the status column).
50
- * Pure — status is the semantic value; the table colors it at render. */
51
35
  export function instanceRow(r) {
52
36
  return {
53
37
  _id: r._id,
54
38
  definition: r.definition,
55
39
  tag: r.tag,
56
40
  currentStage: r.currentStage,
57
- status: deriveStatus(r),
41
+ status: terminalState({
42
+ ...(r.completedAt != null ? { completedAt: r.completedAt } : {}),
43
+ ...(r.abortedAt != null ? { abortedAt: r.abortedAt } : {}),
44
+ }),
58
45
  lastChangedAt: r.lastChangedAt,
59
46
  };
60
47
  }
61
- const STATUS_COLOR = {
62
- completed: 'green',
63
- aborted: 'dim',
64
- 'in-flight': 'cyan',
65
- };
66
- export default class List extends Command {
48
+ export default class List extends WorkflowCommand {
67
49
  static description = 'List workflow instances in the configured dataset.';
68
50
  static examples = [
69
51
  '<%= config.bin %> list',
@@ -91,34 +73,39 @@ export default class List extends Command {
91
73
  };
92
74
  async run() {
93
75
  const { flags } = await this.parse(List);
94
- const { client } = await resolveReadContext(flags);
76
+ const targets = await resolveReadTargets(flags);
95
77
  const { groq, params } = buildListQuery(flags);
96
- const rows = await client.fetch(groq, params);
97
- if (rows.length === 0) {
98
- this.log(`${logSymbols.info} no instances match`);
99
- return;
100
- }
101
- const table = new Table({
102
- columns: [
103
- { name: '_id', title: 'Instance', alignment: 'left' },
104
- { name: 'definition', title: 'Workflow', alignment: 'left' },
105
- { name: 'tag', title: 'Tag', alignment: 'left' },
106
- { name: 'currentStage', title: 'Stage', alignment: 'left' },
107
- { name: 'status', title: 'Status', alignment: 'left' },
108
- { name: 'updated', title: 'Updated', alignment: 'left' },
109
- ],
78
+ const failures = await runReadAcrossTargets({
79
+ targets,
80
+ log: (line) => this.log(line),
81
+ run: async ({ client }) => {
82
+ const fetched = await client.fetch(groq, params, { tag: 'list' });
83
+ if (fetched.length === 0) {
84
+ this.log(`${logSymbols.info} no instances match`);
85
+ return;
86
+ }
87
+ const { rows, note } = clipToLimit(fetched, flags.limit);
88
+ const lines = formatTable(['instance', 'workflow', 'tag', 'stage', 'status', 'updated'], rows.map((r) => {
89
+ const row = instanceRow(r);
90
+ return [
91
+ row._id,
92
+ row.definition,
93
+ row.tag,
94
+ row.currentStage,
95
+ row.status,
96
+ formatAge(row.lastChangedAt),
97
+ ];
98
+ }));
99
+ for (const line of lines) {
100
+ this.log(line);
101
+ }
102
+ if (note) {
103
+ this.log(note);
104
+ }
105
+ },
110
106
  });
111
- for (const r of rows) {
112
- const row = instanceRow(r);
113
- table.addRow({
114
- _id: row._id,
115
- definition: row.definition,
116
- tag: row.tag,
117
- currentStage: row.currentStage,
118
- status: styleText(STATUS_COLOR[row.status], row.status),
119
- updated: formatAge(row.lastChangedAt),
120
- });
107
+ if (failures.length > 0) {
108
+ this.exit(1);
121
109
  }
122
- table.printTable();
123
110
  }
124
111
  }
@@ -1,7 +1,6 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { StubCommand } from "../lib/stub.js";
3
3
  export default class ResetActivity extends StubCommand {
4
- // Stubbed, so kept off the help + published surface until it's wired.
5
4
  static hidden = true;
6
5
  static description = 'Reset a failed activity on an in-flight instance — back to pending (re-run) or to skipped (bypass).';
7
6
  static args = {
@@ -1,8 +1,8 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type SetStageArgs, type WorkflowInstance } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
3
  import { type EngineScope } from '../lib/operation-args.ts';
4
4
  import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
5
- export default class SetStage extends Command {
5
+ export default class SetStage extends WorkflowCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static args: {
@@ -1,13 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
2
+ import { Args, Flags } from '@oclif/core';
3
3
  import { workflow } from '@sanity/workflow-engine';
4
- import ora from 'ora';
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
5
  import { resolveInstanceContext } from "../lib/context.js";
6
- import { errorMessage, fail } from "../lib/fail.js";
7
6
  import { tagFlags } from "../lib/flags.js";
8
7
  import { buildOperationArgs } from "../lib/operation-args.js";
9
- import { emitWriteReport, opsAppliedLines, } from "../lib/ops-report.js";
10
- export default class SetStage extends Command {
8
+ import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../lib/ops-report.js";
9
+ export default class SetStage extends WorkflowCommand {
11
10
  static description = "Force an instance into a stage, regardless of its declared transitions and filters — the engine's setStage admin override. The target stage's enter lifecycle still runs (auto-activities start, stage guards reconcile), and the post-move cascade can immediately auto-transition the instance onward.";
12
11
  static examples = [
13
12
  '<%= config.bin %> set-stage wf-instance.abc123 --to ready',
@@ -32,9 +31,11 @@ export default class SetStage extends Command {
32
31
  async run() {
33
32
  const { args, flags } = await this.parse(SetStage);
34
33
  const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
35
- const spinner = ora(`Setting ${args.instanceId} → ${flags.to}…`).start();
36
- try {
37
- const result = await workflow.setStage({
34
+ await runWriteVerb({
35
+ startLabel: `Setting ${args.instanceId} → ${flags.to}…`,
36
+ failLabel: 'Stage move rejected',
37
+ failHeadline: 'set-stage error:',
38
+ run: () => workflow.setStage({
38
39
  client,
39
40
  ...buildSetStageArgs({
40
41
  scope,
@@ -42,44 +43,27 @@ export default class SetStage extends Command {
42
43
  targetStage: flags.to,
43
44
  reason: flags.reason,
44
45
  }),
45
- });
46
- emitWriteReport({
47
- spinner,
48
- report: setStageReport(result, flags.to),
49
- log: (line) => this.log(line),
50
- });
51
- }
52
- catch (error) {
53
- spinner.fail('Stage move rejected');
54
- fail('set-stage error:', errorMessage(error));
55
- }
46
+ }),
47
+ report: (result) => setStageReport(result, flags.to),
48
+ log: (line) => this.log(line),
49
+ });
56
50
  }
57
51
  }
58
- /** The shared operation args ({@link buildOperationArgs}) plus the
59
- * setStage-specific target. */
60
52
  export function buildSetStageArgs({ scope, instanceId, targetStage, reason, }) {
61
53
  return { ...buildOperationArgs({ scope, instanceId, reason }), targetStage };
62
54
  }
63
- /**
64
- * Turn a `setStage` result into the spinner message + ops block. Pure so
65
- * the changed / terminal / already-at-target / cascaded / ops permutations
66
- * can be asserted without driving a real move.
67
- */
68
55
  export function setStageReport(result, to) {
69
56
  const stage = styleText('bold', result.instance.currentStage);
70
57
  if (!result.changed) {
71
- // The engine no-ops in two distinct cases: a terminal instance never
72
- // moves again, and a same-stage move has nothing to do.
73
58
  const { completedAt } = result.instance;
74
59
  const message = completedAt !== undefined
75
60
  ? `setStage changed nothing — instance is terminal (at ${stage} since ${completedAt})`
76
61
  : `setStage changed nothing — instance is already at ${stage}`;
77
62
  return { changed: false, message, opsLines: [] };
78
63
  }
79
- const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
80
64
  return {
81
65
  changed: true,
82
- message: `Now at ${stage} (was → ${to}${tail})`,
66
+ message: `Now at ${stage} (was → ${to}${cascadeTail(result.cascaded)})`,
83
67
  opsLines: opsAppliedLines(result.ranOps),
84
68
  };
85
69
  }
@@ -1,6 +1,6 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type WorkflowInstance } from '@sanity/workflow-engine';
3
- export default class Show extends Command {
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ export default class Show extends WorkflowCommand {
4
4
  static description: string;
5
5
  static examples: string[];
6
6
  static args: {
@@ -1,11 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
3
- import { abortReason } from '@sanity/workflow-engine';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { abortReason, displayTitle, terminalState, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { resolveReadContext } from "../lib/context.js";
5
+ import { WorkflowCommand } from "../lib/base-command.js";
6
+ import { findInstance, resolveReadTargets } from "../lib/context.js";
6
7
  import { tagFlags } from "../lib/flags.js";
7
8
  import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
8
- export default class Show extends Command {
9
+ export default class Show extends WorkflowCommand {
9
10
  static description = 'Show the state, activities, and effects of a workflow instance.';
10
11
  static examples = [
11
12
  '<%= config.bin %> show wf-instance.abc123',
@@ -28,11 +29,12 @@ export default class Show extends Command {
28
29
  };
29
30
  async run() {
30
31
  const { args, flags } = await this.parse(Show);
31
- const { client } = await resolveReadContext(flags);
32
- const instance = await client.getDocument(args.instanceId);
33
- if (!instance) {
32
+ const targets = await resolveReadTargets(flags);
33
+ const hit = await findInstance({ targets, instanceId: args.instanceId, requestTag: 'show' });
34
+ if (!hit) {
34
35
  this.error(`${logSymbols.error} no instance with id "${args.instanceId}"`, { exit: 1 });
35
36
  }
37
+ const { instance } = hit;
36
38
  this.log(instanceHeader(instance));
37
39
  this.log('');
38
40
  for (const line of describeInstance(instance, {
@@ -42,19 +44,14 @@ export default class Show extends Command {
42
44
  }
43
45
  }
44
46
  }
45
- const HEADER_PAD = 9; // "Completed" — the longest label in the block
46
- /** The summary block at the top of `show` (and reused by `diagnose` /
47
- * `fire-action`): a bold workflow + id title, then aligned detail rows. */
47
+ const HEADER_PAD = 9;
48
48
  export function instanceHeader(instance) {
49
49
  const title = `${styleText('bold', `${instance.definition} v${instance.pinnedVersion}`)} ${styleText('cyan', instance._id)}`;
50
50
  const rows = [
51
51
  formatKeyValue({ key: 'Stage', value: instance.currentStage, padTo: HEADER_PAD }),
52
52
  formatKeyValue({ key: 'Started', value: formatTimestamp(instance.startedAt), padTo: HEADER_PAD }),
53
53
  ];
54
- // Aborted instances also carry a completedAt stamped at the abort, so the
55
- // abort line stands in for completion — showing a green "Completed" too would
56
- // misread as success.
57
- if (instance.abortedAt) {
54
+ if (terminalState(instance) === 'aborted' && instance.abortedAt !== undefined) {
58
55
  const reason = abortReason(instance);
59
56
  const tail = reason ? ` — ${reason}` : '';
60
57
  const value = styleText('yellow', `${formatTimestamp(instance.abortedAt)}${tail}`);
@@ -69,11 +66,6 @@ export function instanceHeader(instance) {
69
66
  rows.push(formatKeyValue({ key: 'Tag', value: instance.tag, padTo: HEADER_PAD }));
70
67
  return [title, ...rows].join('\n');
71
68
  }
72
- /**
73
- * The per-instance body lines: every stage with its activities, any pending
74
- * effects, and (optionally) the history log. Pure so it can be asserted
75
- * without driving the oclif command.
76
- */
77
69
  export function describeInstance(instance, options) {
78
70
  const lines = [sectionHeader('Stages')];
79
71
  for (const stage of instance.stages) {
@@ -81,10 +73,6 @@ export function describeInstance(instance, options) {
81
73
  ? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
82
74
  : styleText('cyan', ' (current)');
83
75
  lines.push(` • ${stage.name}${marker}`);
84
- // A persisted stage may carry no activities array — an activity-less stage,
85
- // or an older-shape document — so treat an absent list as none rather than
86
- // crash. This renders whatever is in the lake, not only freshly-stamped
87
- // instances; it does not reconstruct activity state from history.
88
76
  for (const activity of stage.activities ?? []) {
89
77
  lines.push(` ${activityIcon[activity.status]} ${activity.name} ${styleText('dim', `[${activity.status}]`)}`);
90
78
  }
@@ -98,7 +86,7 @@ export function describeInstance(instance, options) {
98
86
  if (options.includeHistory) {
99
87
  lines.push('', sectionHeader('History'));
100
88
  for (const entry of instance.history) {
101
- lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${entry._type}`);
89
+ lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${displayTitle(entry._type)}`);
102
90
  }
103
91
  }
104
92
  return lines;
@@ -1,7 +1,7 @@
1
- import { Command } from '@oclif/core';
2
- import { type Actor, type FieldEntry, type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance, type WorkflowLifecycle } from '@sanity/workflow-engine';
1
+ import { type FieldEntry, type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance, type WorkflowLifecycle } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
3
  import { type EngineScope } from '../lib/operation-args.ts';
4
- export default class Start extends Command {
4
+ export default class Start extends WorkflowCommand {
5
5
  static description: string;
6
6
  static examples: string[];
7
7
  static args: {
@@ -9,8 +9,6 @@ export default class Start extends Command {
9
9
  };
10
10
  static flags: {
11
11
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
- as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
- 'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
14
12
  version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
13
  field: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
16
14
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -43,12 +41,11 @@ export declare function buildInitialFields({ declared, values, }: {
43
41
  * fields. Optionals spread conditionally because the engine's optional args
44
42
  * reject an explicit `undefined` under `exactOptionalPropertyTypes`.
45
43
  */
46
- export declare function buildStartArgs({ scope, name, version, initialFields, actor, }: {
44
+ export declare function buildStartArgs({ scope, name, version, initialFields, }: {
47
45
  scope: EngineScope;
48
46
  name: string;
49
47
  version: number | undefined;
50
48
  initialFields: InitialFieldValue[];
51
- actor: Actor;
52
49
  }): StartInstanceArgs & EngineScope;
53
50
  /** The success spinner message. Pure so the in-flight / immediately-terminal
54
51
  * permutations can be asserted without driving a real start. */