@sanity/workflow-cli 0.9.0 → 0.11.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 (71) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +71 -19
  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 +41 -47
  12. package/dist/commands/definition/show.d.ts +35 -3
  13. package/dist/commands/definition/show.js +44 -24
  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 +60 -56
  18. package/dist/commands/fire-action.d.ts +2 -4
  19. package/dist/commands/fire-action.js +39 -62
  20. package/dist/commands/list.d.ts +18 -10
  21. package/dist/commands/list.js +77 -69
  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 -28
  28. package/dist/commands/start.js +19 -80
  29. package/dist/commands/tail.d.ts +2 -2
  30. package/dist/commands/tail.js +34 -36
  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 +8 -0
  34. package/dist/hooks/prerun/telemetry.js +9 -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.d.ts +10 -0
  42. package/dist/lib/definitions.js +11 -24
  43. package/dist/lib/diff.d.ts +5 -2
  44. package/dist/lib/diff.js +61 -27
  45. package/dist/lib/env.d.ts +4 -0
  46. package/dist/lib/env.js +4 -3
  47. package/dist/lib/fail.d.ts +19 -6
  48. package/dist/lib/fail.js +26 -26
  49. package/dist/lib/flags.d.ts +0 -7
  50. package/dist/lib/flags.js +0 -17
  51. package/dist/lib/load-config.d.ts +11 -7
  52. package/dist/lib/load-config.js +40 -20
  53. package/dist/lib/operation-args.d.ts +14 -23
  54. package/dist/lib/operation-args.js +6 -38
  55. package/dist/lib/ops-report.d.ts +18 -10
  56. package/dist/lib/ops-report.js +20 -16
  57. package/dist/lib/params.js +0 -8
  58. package/dist/lib/read-fanout.d.ts +22 -0
  59. package/dist/lib/read-fanout.js +28 -0
  60. package/dist/lib/select-deployment.js +0 -21
  61. package/dist/lib/share-definitions.d.ts +120 -0
  62. package/dist/lib/share-definitions.js +187 -0
  63. package/dist/lib/stub.js +0 -7
  64. package/dist/lib/telemetry-setup.d.ts +70 -0
  65. package/dist/lib/telemetry-setup.js +99 -0
  66. package/dist/lib/telemetry.d.ts +131 -0
  67. package/dist/lib/telemetry.js +94 -0
  68. package/dist/lib/ui.d.ts +33 -1
  69. package/dist/lib/ui.js +32 -22
  70. package/oclif.manifest.json +21 -52
  71. package/package.json +14 -8
@@ -1,10 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command } from '@oclif/core';
3
- import { diagnoseInputFromEvaluation, workflow, } from '@sanity/workflow-engine';
2
+ import { Args } from '@oclif/core';
3
+ import { diagnoseInputFromEvaluation, workflow, unsatisfiedTransitionSummaries, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
+ import { WorkflowCommand } from "../lib/base-command.js";
5
6
  import { resolveInstanceContext } from "../lib/context.js";
6
- import { errorMessage, fail } from "../lib/fail.js";
7
+ import { fail, failureDetail } from "../lib/fail.js";
7
8
  import { jsonFlags, tagFlags } from "../lib/flags.js";
9
+ import { baseEngineArgs } from "../lib/operation-args.js";
8
10
  import { formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
9
11
  import { instanceHeader } from "./show.js";
10
12
  function formatAssignee(a) {
@@ -16,22 +18,23 @@ function activityLine(t, assignees) {
16
18
  : '';
17
19
  return ` ${activityIcon[t.status]} ${t.activity.name} ${styleText('dim', `[${t.status}]`)}${who}`;
18
20
  }
19
- function transitionLine(tr) {
20
- const mark = tr.filterSatisfied ? styleText('green', 'ready') : styleText('red', 'false');
21
- return ` → ${tr.transition.to} ${styleText('dim', tr.transition.filter ?? 'true')} ${mark}`;
21
+ function transitionLine(tr, explanation) {
22
+ const mark = tr.whenSatisfied ? styleText('green', 'ready') : styleText('red', 'false');
23
+ const line = ` → ${tr.transition.to} ${styleText('dim', tr.transition.when)} ${mark}`;
24
+ return explanation !== undefined ? `${line}\n ${styleText('dim', `↳ ${explanation}`)}` : line;
22
25
  }
23
- function currentStageLines(input) {
26
+ function currentStageLines(input, explanations) {
24
27
  const lines = [
25
28
  `${sectionHeader('Stage')} ${input.instance.currentStage}`,
26
29
  sectionHeader('Activities'),
27
30
  ];
28
- for (const t of input.activities) {
31
+ for (const t of input.activities.filter((a) => !a.scopedOut)) {
29
32
  lines.push(activityLine(t, input.assignees[t.activity.name] ?? []));
30
33
  }
31
34
  if (input.transitions.length > 0) {
32
35
  lines.push('', sectionHeader('Exit transitions'));
33
36
  for (const tr of input.transitions) {
34
- lines.push(transitionLine(tr));
37
+ lines.push(transitionLine(tr, explanations?.get(tr.transition.name)));
35
38
  }
36
39
  }
37
40
  return lines;
@@ -39,14 +42,14 @@ function currentStageLines(input) {
39
42
  function failedEffectDetail(effect) {
40
43
  const ran = effect.durationMs !== undefined ? ` (after ${effect.durationMs}ms)` : '';
41
44
  return {
42
- headline: `a failed effect is blocking activity '${effect.origin.name}'`,
45
+ headline: `a failed effect from action '${effect.origin.name}' is blocking its activity`,
43
46
  why: [
44
47
  styleText('red', `${logSymbols.error} failed effect: ${effect.name}`),
45
- ` queued by activity '${effect.origin.name}', failed ${formatTimestamp(effect.ranAt)}${ran}`,
48
+ ` queued by action '${effect.origin.name}', failed ${formatTimestamp(effect.ranAt)}${ran}`,
46
49
  ...(effect.error !== undefined ? [` error: ${effect.error.message}`] : []),
47
50
  '',
48
- `Activity '${effect.origin.name}' is waiting on this effect. It failed against an`,
49
- `external system, so the activity never completes and the stage can't advance.`,
51
+ `The activity that fired '${effect.origin.name}' is waiting on this effect. It failed`,
52
+ `against an external system, so the activity never resolves and the stage can't advance.`,
50
53
  ],
51
54
  };
52
55
  }
@@ -71,12 +74,22 @@ function failedActivityDetail(activity) {
71
74
  }
72
75
  function waitingHeadline(w) {
73
76
  const who = w.assignees.length > 0 ? ` on ${w.assignees.map(formatAssignee).join(', ')}` : '';
77
+ if (w.actions.length === 0) {
78
+ return `waiting${who} on automation — a trigger fires on its own when its condition holds`;
79
+ }
74
80
  return `waiting${who} for action: ${w.actions.join(' or ')}`;
75
81
  }
76
82
  function waitingLines(w) {
77
83
  const who = w.assignees.length > 0
78
84
  ? `assigned to ${w.assignees.map(formatAssignee).join(', ')}`
79
85
  : 'unassigned — anyone with permission can act';
86
+ if (w.actions.length === 0) {
87
+ return [
88
+ `${logSymbols.info} activity '${w.activity}' is active (${who}),`,
89
+ `waiting on automation: its cascade-fired action(s) fire on their own when their`,
90
+ `trigger conditions hold — nothing for a caller to fire.`,
91
+ ];
92
+ }
80
93
  return [
81
94
  `${logSymbols.info} activity '${w.activity}' is active (${who}),`,
82
95
  `waiting for action: ${w.actions.join(' or ')}. This is the normal in-flight`,
@@ -100,36 +113,25 @@ function blockedLines(b) {
100
113
  }
101
114
  function noTransitionDetail() {
102
115
  return {
103
- headline: `no exit transition's filter is satisfied`,
116
+ headline: `no exit transition's trigger is satisfied`,
104
117
  why: [
105
- `${logSymbols.info} every activity is resolved, but no exit transition's filter is true.`,
106
- `Likely a routing state value a filter reads never got written.`,
118
+ `${logSymbols.info} every activity is resolved, but no exit transition's \`when\` is true.`,
119
+ `Likely a routing state value a trigger reads never got written.`,
107
120
  ],
108
121
  };
109
122
  }
110
- /** An unevaluable exit filter is a *recoverable* hold, not a dead end: it
111
- * clears itself once the operand becomes readable, and no engine verb
112
- * unsticks it (so the engine emits no remediation). The "what to do" —
113
- * make the operand readable — lives here in the why, since there is no
114
- * runnable fix to render. */
115
123
  function transitionUnevaluableDetail(transitions) {
116
124
  return {
117
- headline: `an exit transition's filter could not be evaluated`,
125
+ headline: `an exit transition's trigger could not be evaluated`,
118
126
  why: [
119
127
  `${logSymbols.info} every activity is resolved, but ${transitions.join(', ')} reads an operand`,
120
128
  `that is missing or unreadable (GROQ null), so routing is held rather than`,
121
- `falling through. Make the data the filter reads readable — publish the`,
129
+ `falling through. Make the data the trigger reads readable — publish the`,
122
130
  `subject (or fill the field) — and the instance advances on its own; no`,
123
131
  `set-stage needed.`,
124
132
  ],
125
133
  };
126
134
  }
127
- /** Render the engine's structured remediations as the "suggested fix" block.
128
- * Verbs render under their engine names — the same vocabulary `--json`
129
- * emits, so scripts and humans read one language. Only runnable ones reach
130
- * here: {@link renderDiagnosis} filters on the engine's `available` flag,
131
- * so verbs that aren't callable engine operations yet never render as a
132
- * fix (the full set, planned verbs included, still rides `--json`). */
133
135
  function remediationLines(remediations) {
134
136
  return remediations.map((r) => ` • ${r.verb} — ${r.rationale}`);
135
137
  }
@@ -147,6 +149,11 @@ function causeDetail(cause) {
147
149
  return transitionUnevaluableDetail(cause.transitions);
148
150
  }
149
151
  }
152
+ function liveChildrenTail(diagnosis) {
153
+ if (diagnosis.liveChildren === undefined)
154
+ return '';
155
+ return ` ${styleText('yellow', `${diagnosis.liveChildren} spawned child workflow(s) still running.`)}`;
156
+ }
150
157
  function statusLine(diagnosis) {
151
158
  switch (diagnosis.state) {
152
159
  case 'progressing':
@@ -156,31 +163,37 @@ function statusLine(diagnosis) {
156
163
  case 'blocked':
157
164
  return `${logSymbols.info} ${styleText('cyan', 'blocked')} — ${blockedHeadline(diagnosis)}`;
158
165
  case 'completed':
159
- return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}.`;
166
+ return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
160
167
  case 'aborted': {
161
168
  const tail = diagnosis.reason !== undefined ? ` — ${diagnosis.reason}` : '';
162
- return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}.`;
169
+ return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
163
170
  }
164
171
  case 'stuck':
165
172
  return `${logSymbols.warning} ${styleText('yellow', 'STUCK')} — ${causeDetail(diagnosis.cause).headline}`;
166
173
  }
167
174
  }
168
- /**
169
- * The diagnosis body: the verdict line, the current stage's activities +
170
- * transitions as evidence (for any in-flight state), and the cause block with
171
- * the engine's suggested fix when stuck. Pure so every verdict permutation is
172
- * assertable without driving a live evaluation. Takes the engine's
173
- * {@link SuggestedRemediation}s rather than re-deriving them, so the rendered
174
- * fix block and the `--json` output stay one computation.
175
- */
176
- export function renderDiagnosis({ diagnosis, input, remediations, }) {
175
+ export function rawTransitionProjection(evaluation) {
176
+ return evaluation.currentStage.transitions.map((t) => ({
177
+ name: t.transition.name,
178
+ to: t.transition.to,
179
+ whenSatisfied: t.whenSatisfied,
180
+ unevaluable: t.unevaluable,
181
+ blockedBy: t.insight.blockedBy.map((atom) => ({
182
+ groq: atom.atom.groq,
183
+ negated: atom.atom.negated,
184
+ pivotal: atom.pivotal,
185
+ ...(atom.requirement !== undefined ? { requirement: atom.requirement } : {}),
186
+ })),
187
+ }));
188
+ }
189
+ export function renderDiagnosis({ diagnosis, input, remediations, explanations, }) {
177
190
  const lines = [statusLine(diagnosis)];
178
191
  const inFlight = diagnosis.state === 'progressing' ||
179
192
  diagnosis.state === 'waiting' ||
180
193
  diagnosis.state === 'blocked' ||
181
194
  diagnosis.state === 'stuck';
182
195
  if (inFlight) {
183
- lines.push('', ...currentStageLines(input));
196
+ lines.push('', ...currentStageLines(input, explanations));
184
197
  }
185
198
  if (diagnosis.state === 'waiting') {
186
199
  lines.push('', ...waitingLines(diagnosis));
@@ -192,16 +205,13 @@ export function renderDiagnosis({ diagnosis, input, remediations, }) {
192
205
  const detail = causeDetail(diagnosis.cause);
193
206
  const runnable = remediations.filter((r) => r.available);
194
207
  lines.push('', sectionHeader("Why it's stuck"), ...detail.why);
195
- // A recoverable hold (e.g. an unevaluable transition filter) has no
196
- // runnable remediation — its "what to do" is in the why above, so skip
197
- // the empty fix header rather than dangle it.
198
208
  if (runnable.length > 0) {
199
209
  lines.push('', sectionHeader('Suggested fix'), ...remediationLines(runnable));
200
210
  }
201
211
  }
202
212
  return lines;
203
213
  }
204
- export default class Diagnose extends Command {
214
+ export default class Diagnose extends WorkflowCommand {
205
215
  static description = "Explain why a workflow instance is or isn't progressing, and what would unstick it.";
206
216
  static examples = [
207
217
  '<%= config.bin %> diagnose wf-instance.abc123',
@@ -221,18 +231,10 @@ export default class Diagnose extends Command {
221
231
  const { evaluation, diagnosis, remediations } = await workflow
222
232
  .diagnose({
223
233
  client,
224
- tag: scope.tag,
225
- workflowResource: scope.workflowResource,
234
+ ...baseEngineArgs(scope),
226
235
  instanceId: args.instanceId,
227
- // Token-auth CLI can't hit /users/me; pin the same system actor the
228
- // write verbs use. Transition filters and activity status are actor-
229
- // independent. A requirement that reads $actor/$assigned/$can is the
230
- // exception — it's evaluated against this system actor, so a `blocked`
231
- // verdict on such a requirement reflects the engine's view, not a
232
- // specific assignee's.
233
- access: { actor: { kind: 'system', id: 'workflow-cli' } },
234
236
  })
235
- .catch((error) => fail('diagnose failed:', errorMessage(error)));
237
+ .catch((error) => fail('diagnose failed:', failureDetail(error)));
236
238
  const input = diagnoseInputFromEvaluation(evaluation);
237
239
  if (flags.json) {
238
240
  this.log(JSON.stringify({
@@ -240,12 +242,14 @@ export default class Diagnose extends Command {
240
242
  stage: evaluation.instance.currentStage,
241
243
  diagnosis,
242
244
  remediations,
245
+ transitions: rawTransitionProjection(evaluation),
243
246
  }, null, 2));
244
247
  return;
245
248
  }
246
249
  this.log(instanceHeader(evaluation.instance));
247
250
  this.log('');
248
- for (const line of renderDiagnosis({ diagnosis, input, remediations })) {
251
+ const explanations = new Map(unsatisfiedTransitionSummaries(evaluation).map((entry) => [entry.transition, entry.summary]));
252
+ for (const line of renderDiagnosis({ diagnosis, input, remediations, explanations })) {
249
253
  this.log(line);
250
254
  }
251
255
  }
@@ -1,5 +1,5 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type AvailableAction } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
3
  import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
4
4
  /**
5
5
  * The list-mode body: every action on the current stage's activities, marked
@@ -29,7 +29,7 @@ export declare function fireActionReport({ result, activity, action, }: {
29
29
  activity: string;
30
30
  action: string;
31
31
  }): WriteReport;
32
- export default class FireAction extends Command {
32
+ export default class FireAction extends WorkflowCommand {
33
33
  static description: string;
34
34
  static examples: string[];
35
35
  static args: {
@@ -37,8 +37,6 @@ export default class FireAction extends Command {
37
37
  };
38
38
  static flags: {
39
39
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
40
- as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
41
- 'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
42
40
  activity: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
41
  action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
44
42
  param: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
@@ -1,37 +1,38 @@
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 { actionRendering, 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}`;
31
21
  }
32
22
  function actionLines(a) {
33
- const mark = a.allowed ? logSymbols.success : logSymbols.error;
34
23
  const title = a.title !== undefined ? styleText('dim', ` (${a.title})`) : '';
24
+ const rendering = actionRendering(a);
25
+ if (rendering === 'absent')
26
+ return [];
27
+ if (rendering === 'automation') {
28
+ return [
29
+ ` ${styleText('cyan', '⚡')} ${a.activity} → ${a.action}${title} ${styleText('dim', '[automation]')}`,
30
+ ...(a.disabledReason?.kind === 'cascade-fired'
31
+ ? [styleText('dim', ` ${actionDisabledDetail(a.disabledReason)}`)]
32
+ : []),
33
+ ];
34
+ }
35
+ const mark = a.allowed ? logSymbols.success : logSymbols.error;
35
36
  const lines = [
36
37
  ` ${mark} ${a.activity} → ${a.action}${title} ${styleText('dim', `[activity ${a.activityStatus}]`)}`,
37
38
  ];
@@ -43,12 +44,6 @@ function actionLines(a) {
43
44
  }
44
45
  return lines;
45
46
  }
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
47
  export function renderAvailableActions({ actions, stage, instanceId, }) {
53
48
  if (actions.length === 0) {
54
49
  return [`No actions on the activities of the current stage ('${stage}').`];
@@ -60,8 +55,6 @@ export function renderAvailableActions({ actions, stage, instanceId, }) {
60
55
  lines.push('', styleText('dim', 'Fire one with:'), styleText('dim', ` fire-action ${instanceId} --activity <activity> --action <action>`));
61
56
  return lines;
62
57
  }
63
- /** The `--json` payload for a fired action — the structured counterpart of
64
- * {@link fireActionReport}. Pure so its shape is asserted directly. */
65
58
  export function fireResultJson(result, ids) {
66
59
  return {
67
60
  instanceId: ids.instanceId,
@@ -73,11 +66,6 @@ export function fireResultJson(result, ids) {
73
66
  ...(result.ranOps !== undefined ? { ranOps: result.ranOps } : {}),
74
67
  };
75
68
  }
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
69
  export function fireActionReport({ result, activity, action, }) {
82
70
  const stage = styleText('bold', result.instance.currentStage);
83
71
  if (!result.changed) {
@@ -87,19 +75,17 @@ export function fireActionReport({ result, activity, action, }) {
87
75
  opsLines: [],
88
76
  };
89
77
  }
90
- const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
91
78
  return {
92
79
  changed: true,
93
- message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${tail}`,
80
+ message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${cascadeTail(result.cascaded)}`,
94
81
  opsLines: opsAppliedLines(result.ranOps),
95
82
  };
96
83
  }
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.";
84
+ export default class FireAction extends WorkflowCommand {
85
+ 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
86
  static examples = [
100
87
  '<%= config.bin %> fire-action wf-instance.abc123',
101
88
  '<%= 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
89
  '<%= config.bin %> fire-action wf-instance.abc123 --activity publish --action publish --param note=shipping',
104
90
  ];
105
91
  static args = {
@@ -118,19 +104,16 @@ export default class FireAction extends Command {
118
104
  multiple: true,
119
105
  default: [],
120
106
  }),
121
- ...actorFlags,
122
107
  ...jsonFlags,
123
108
  };
124
109
  async run() {
125
110
  const { args, flags } = await this.parse(FireAction);
126
111
  const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
127
- const actor = failOnThrow('fire-action usage error:', () => resolveActor(flags.as, flags['as-role']));
128
112
  if (flags.action === undefined) {
129
113
  await this.listActions({
130
114
  client,
131
115
  scope,
132
116
  instanceId: args.instanceId,
133
- actor,
134
117
  json: flags.json,
135
118
  });
136
119
  return;
@@ -142,21 +125,20 @@ export default class FireAction extends Command {
142
125
  client,
143
126
  scope,
144
127
  instanceId: args.instanceId,
145
- actor,
146
128
  activity: flags.activity,
147
129
  action: flags.action,
148
130
  params: flags.param,
149
131
  json: flags.json,
150
132
  });
151
133
  }
152
- async listActions({ client, scope, instanceId, actor, json, }) {
134
+ async listActions({ client, scope, instanceId, json, }) {
153
135
  const { evaluation, actions } = await workflow
154
136
  .availableActions({
155
137
  client,
156
- ...baseEngineArgs(scope, actor),
138
+ ...baseEngineArgs(scope),
157
139
  instanceId,
158
140
  })
159
- .catch((error) => fail('fire-action failed:', errorMessage(error)));
141
+ .catch((error) => fail('fire-action failed:', failureDetail(error)));
160
142
  const stage = evaluation.instance.currentStage;
161
143
  if (json) {
162
144
  this.log(JSON.stringify({ instanceId, stage, actions }, null, 2));
@@ -169,11 +151,11 @@ export default class FireAction extends Command {
169
151
  }
170
152
  }
171
153
  async fireOne(opts) {
172
- const { client, scope, instanceId, actor, activity, action, json } = opts;
154
+ const { client, scope, instanceId, activity, action, json } = opts;
173
155
  const params = failOnThrow('Invalid --param:', () => parseParams(opts.params));
174
156
  const fireArgs = {
175
157
  client,
176
- ...baseEngineArgs(scope, actor),
158
+ ...baseEngineArgs(scope),
177
159
  instanceId,
178
160
  activity,
179
161
  action,
@@ -182,22 +164,17 @@ export default class FireAction extends Command {
182
164
  if (json) {
183
165
  const result = await workflow
184
166
  .fireAction(fireArgs)
185
- .catch((error) => fail('fire-action error:', errorMessage(error)));
167
+ .catch((error) => fail('fire-action error:', failureDetail(error)));
186
168
  this.log(JSON.stringify(fireResultJson(result, { instanceId, activity, action }), null, 2));
187
169
  return;
188
170
  }
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
- }
171
+ await runWriteVerb({
172
+ startLabel: `Firing ${action} on ${activity}…`,
173
+ failLabel: 'Action rejected',
174
+ failHeadline: 'fire-action error:',
175
+ run: () => workflow.fireAction(fireArgs),
176
+ report: (result) => fireActionReport({ result, activity, action }),
177
+ log: (line) => this.log(line),
178
+ });
202
179
  }
203
180
  }
@@ -1,18 +1,19 @@
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
- 'in-flight': boolean;
13
+ 'include-completed': boolean;
14
14
  failed: boolean;
15
15
  definition?: string | undefined;
16
+ document?: string | undefined;
16
17
  tag?: string | undefined;
17
18
  limit: number;
18
19
  }
@@ -20,28 +21,35 @@ interface ListFlags {
20
21
  * Build the GROQ query + params for `list` from its flags. Pure so the
21
22
  * filter combinations can be asserted without a live dataset. `--tag` is an
22
23
  * optional filter: omit it and the listing spans every partition.
24
+ *
25
+ * With `--document` the lake filter is the engine's prefilter arm — a
26
+ * deliberate superset narrowed to the exact watch-set in JS
27
+ * ({@link instanceWatchesDocument}) — so the rows come back as full
28
+ * documents (the narrowing reads fields/stages/ancestors) and UNCAPPED: a
29
+ * capped superset could silently hide matches. The display limit applies
30
+ * after narrowing.
23
31
  */
24
32
  export declare function buildListQuery(flags: ListFlags): {
25
33
  groq: string;
26
34
  params: Record<string, unknown>;
27
35
  };
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. */
36
+ /** Map a queried instance to its display row (derives the status column). */
30
37
  export declare function instanceRow(r: InstanceRow): {
31
38
  _id: string;
32
39
  definition: string;
33
40
  tag: string;
34
41
  currentStage: string;
35
- status: InstanceStatus;
42
+ status: TerminalState;
36
43
  lastChangedAt: string;
37
44
  };
38
- export default class List extends Command {
45
+ export default class List extends WorkflowCommand {
39
46
  static description: string;
40
47
  static examples: string[];
41
48
  static flags: {
42
- 'in-flight': import("@oclif/core/interfaces").BooleanFlag<boolean>;
49
+ 'include-completed': import("@oclif/core/interfaces").BooleanFlag<boolean>;
43
50
  failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
44
51
  definition: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
52
+ document: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
45
53
  limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
46
54
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
47
55
  };