@sanity/workflow-cli 0.8.1 → 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 (79) hide show
  1. package/CHANGELOG.md +485 -0
  2. package/README.md +145 -63
  3. package/bin/run.js +0 -4
  4. package/dist/commands/abort.d.ts +8 -12
  5. package/dist/commands/abort.js +24 -34
  6. package/dist/commands/definition/delete.d.ts +6 -6
  7. package/dist/commands/definition/delete.js +17 -33
  8. package/dist/commands/definition/diff.d.ts +10 -3
  9. package/dist/commands/definition/diff.js +21 -29
  10. package/dist/commands/definition/list.d.ts +5 -6
  11. package/dist/commands/definition/list.js +44 -50
  12. package/dist/commands/definition/show.d.ts +22 -13
  13. package/dist/commands/definition/show.js +25 -36
  14. package/dist/commands/deploy.d.ts +67 -8
  15. package/dist/commands/deploy.js +136 -32
  16. package/dist/commands/diagnose.d.ts +22 -4
  17. package/dist/commands/diagnose.js +45 -61
  18. package/dist/commands/fire-action.d.ts +7 -30
  19. package/dist/commands/fire-action.js +42 -115
  20. package/dist/commands/list.d.ts +9 -10
  21. package/dist/commands/list.js +49 -62
  22. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  23. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -3
  24. package/dist/commands/set-stage.d.ts +27 -3
  25. package/dist/commands/set-stage.js +63 -12
  26. package/dist/commands/show.d.ts +3 -2
  27. package/dist/commands/show.js +15 -21
  28. package/dist/commands/start.d.ts +56 -0
  29. package/dist/commands/start.js +133 -0
  30. package/dist/commands/tail.d.ts +5 -2
  31. package/dist/commands/tail.js +25 -26
  32. package/dist/hooks/finally/telemetry.d.ts +8 -0
  33. package/dist/hooks/finally/telemetry.js +13 -0
  34. package/dist/hooks/prerun/telemetry.d.ts +5 -0
  35. package/dist/hooks/prerun/telemetry.js +5 -0
  36. package/dist/index.js +0 -3
  37. package/dist/lib/base-command.d.ts +14 -0
  38. package/dist/lib/base-command.js +10 -0
  39. package/dist/lib/client.d.ts +11 -14
  40. package/dist/lib/client.js +29 -34
  41. package/dist/lib/context.d.ts +98 -0
  42. package/dist/lib/context.js +83 -0
  43. package/dist/lib/definitions.d.ts +38 -29
  44. package/dist/lib/definitions.js +34 -93
  45. package/dist/lib/diff.d.ts +5 -2
  46. package/dist/lib/diff.js +62 -20
  47. package/dist/lib/env.d.ts +4 -6
  48. package/dist/lib/env.js +4 -9
  49. package/dist/lib/fail.d.ts +12 -0
  50. package/dist/lib/fail.js +25 -16
  51. package/dist/lib/flags.d.ts +7 -2
  52. package/dist/lib/flags.js +7 -3
  53. package/dist/lib/load-config.d.ts +15 -0
  54. package/dist/lib/load-config.js +89 -0
  55. package/dist/lib/operation-args.d.ts +20 -15
  56. package/dist/lib/operation-args.js +9 -40
  57. package/dist/lib/ops-report.d.ts +29 -13
  58. package/dist/lib/ops-report.js +20 -17
  59. package/dist/lib/params.d.ts +7 -0
  60. package/dist/lib/params.js +18 -0
  61. package/dist/lib/read-fanout.d.ts +22 -0
  62. package/dist/lib/read-fanout.js +28 -0
  63. package/dist/lib/select-deployment.d.ts +31 -0
  64. package/dist/lib/select-deployment.js +37 -0
  65. package/dist/lib/share-definitions.d.ts +86 -0
  66. package/dist/lib/share-definitions.js +106 -0
  67. package/dist/lib/stub.js +0 -7
  68. package/dist/lib/telemetry-setup.d.ts +66 -0
  69. package/dist/lib/telemetry-setup.js +92 -0
  70. package/dist/lib/telemetry.d.ts +100 -0
  71. package/dist/lib/telemetry.js +89 -0
  72. package/dist/lib/ui.d.ts +33 -1
  73. package/dist/lib/ui.js +32 -21
  74. package/oclif.manifest.json +133 -113
  75. package/package.json +16 -8
  76. package/dist/commands/move-stage.d.ts +0 -52
  77. package/dist/commands/move-stage.js +0 -86
  78. package/dist/lib/config.d.ts +0 -18
  79. package/dist/lib/config.js +0 -50
@@ -1,5 +1,20 @@
1
- import { Command } from '@oclif/core';
2
- import { type Diagnosis, type DiagnoseInput, type SuggestedRemediation } from '@sanity/workflow-engine';
1
+ import { type Diagnosis, type DiagnoseInput, type SuggestedRemediation, type WorkflowEvaluation } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ /** The `--json` transitions payload: raw derived state (atom GROQ, negation,
4
+ * pivotality, solvable requirement) — structure for scripts, never baked
5
+ * English. Pure so the shape is a testable contract. */
6
+ export declare function rawTransitionProjection(evaluation: WorkflowEvaluation): {
7
+ name: string;
8
+ to: string;
9
+ filterSatisfied: boolean;
10
+ unevaluable: boolean;
11
+ blockedBy: {
12
+ requirement?: import("@sanity/groq-condition-describe").AtomRequirement;
13
+ groq: string;
14
+ negated: boolean;
15
+ pivotal: boolean;
16
+ }[];
17
+ }[];
3
18
  /**
4
19
  * The diagnosis body: the verdict line, the current stage's activities +
5
20
  * transitions as evidence (for any in-flight state), and the cause block with
@@ -8,12 +23,15 @@ import { type Diagnosis, type DiagnoseInput, type SuggestedRemediation } from '@
8
23
  * {@link SuggestedRemediation}s rather than re-deriving them, so the rendered
9
24
  * fix block and the `--json` output stay one computation.
10
25
  */
11
- export declare function renderDiagnosis({ diagnosis, input, remediations, }: {
26
+ export declare function renderDiagnosis({ diagnosis, input, remediations, explanations, }: {
12
27
  diagnosis: Diagnosis;
13
28
  input: DiagnoseInput;
14
29
  remediations: SuggestedRemediation[];
30
+ /** Insight summaries per unsatisfied transition name — rendered under the
31
+ * transition line so "stuck" reads as "stuck until X". */
32
+ explanations?: ReadonlyMap<string, string> | undefined;
15
33
  }): string[];
16
- export default class Diagnose extends Command {
34
+ export default class Diagnose extends WorkflowCommand {
17
35
  static description: string;
18
36
  static examples: string[];
19
37
  static args: {
@@ -1,11 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } 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 { loadClient } from "../lib/client.js";
6
- import { loadWorkflowConfig } from "../lib/config.js";
7
- import { fail } from "../lib/fail.js";
8
- import { tagFlags } from "../lib/flags.js";
5
+ import { WorkflowCommand } from "../lib/base-command.js";
6
+ import { resolveInstanceContext } from "../lib/context.js";
7
+ import { fail, failureDetail } from "../lib/fail.js";
8
+ import { jsonFlags, tagFlags } from "../lib/flags.js";
9
+ import { baseEngineArgs } from "../lib/operation-args.js";
9
10
  import { formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
10
11
  import { instanceHeader } from "./show.js";
11
12
  function formatAssignee(a) {
@@ -17,11 +18,12 @@ function activityLine(t, assignees) {
17
18
  : '';
18
19
  return ` ${activityIcon[t.status]} ${t.activity.name} ${styleText('dim', `[${t.status}]`)}${who}`;
19
20
  }
20
- function transitionLine(tr) {
21
+ function transitionLine(tr, explanation) {
21
22
  const mark = tr.filterSatisfied ? styleText('green', 'ready') : styleText('red', 'false');
22
- return ` → ${tr.transition.to} ${styleText('dim', tr.transition.filter ?? 'true')} ${mark}`;
23
+ const line = ` → ${tr.transition.to} ${styleText('dim', tr.transition.filter ?? 'true')} ${mark}`;
24
+ return explanation !== undefined ? `${line}\n ${styleText('dim', `↳ ${explanation}`)}` : line;
23
25
  }
24
- function currentStageLines(input) {
26
+ function currentStageLines(input, explanations) {
25
27
  const lines = [
26
28
  `${sectionHeader('Stage')} ${input.instance.currentStage}`,
27
29
  sectionHeader('Activities'),
@@ -32,7 +34,7 @@ function currentStageLines(input) {
32
34
  if (input.transitions.length > 0) {
33
35
  lines.push('', sectionHeader('Exit transitions'));
34
36
  for (const tr of input.transitions) {
35
- lines.push(transitionLine(tr));
37
+ lines.push(transitionLine(tr, explanations?.get(tr.transition.name)));
36
38
  }
37
39
  }
38
40
  return lines;
@@ -108,11 +110,6 @@ function noTransitionDetail() {
108
110
  ],
109
111
  };
110
112
  }
111
- /** An unevaluable exit filter is a *recoverable* hold, not a dead end: it
112
- * clears itself once the operand becomes readable, and no engine verb
113
- * unsticks it (so the engine emits no remediation). The "what to do" —
114
- * make the operand readable — lives here in the why, since there is no
115
- * runnable fix to render. */
116
113
  function transitionUnevaluableDetail(transitions) {
117
114
  return {
118
115
  headline: `an exit transition's filter could not be evaluated`,
@@ -121,23 +118,12 @@ function transitionUnevaluableDetail(transitions) {
121
118
  `that is missing or unreadable (GROQ null), so routing is held rather than`,
122
119
  `falling through. Make the data the filter reads readable — publish the`,
123
120
  `subject (or fill the field) — and the instance advances on its own; no`,
124
- `move-stage needed.`,
121
+ `set-stage needed.`,
125
122
  ],
126
123
  };
127
124
  }
128
- const REMEDIATION_LABEL = {
129
- 'retry-effect': 'retry-effect',
130
- 'drain-effects': 're-run the effect drainer',
131
- 'reset-activity': 'reset-activity',
132
- 'set-stage': 'move-stage',
133
- abort: 'abort',
134
- };
135
- /** Render the engine's structured remediations as the "suggested fix" block.
136
- * Only the runnable ones reach here — {@link renderDiagnosis} filters out
137
- * verbs the CLI can't yet perform so it never advertises a fix you can't run
138
- * (the full set, planned verbs included, still rides `--json`). */
139
125
  function remediationLines(remediations) {
140
- return remediations.map((r) => ` • ${REMEDIATION_LABEL[r.verb]} — ${r.rationale}`);
126
+ return remediations.map((r) => ` • ${r.verb} — ${r.rationale}`);
141
127
  }
142
128
  function causeDetail(cause) {
143
129
  switch (cause.kind) {
@@ -153,6 +139,11 @@ function causeDetail(cause) {
153
139
  return transitionUnevaluableDetail(cause.transitions);
154
140
  }
155
141
  }
142
+ function liveChildrenTail(diagnosis) {
143
+ if (diagnosis.liveChildren === undefined)
144
+ return '';
145
+ return ` ${styleText('yellow', `${diagnosis.liveChildren} spawned child workflow(s) still running.`)}`;
146
+ }
156
147
  function statusLine(diagnosis) {
157
148
  switch (diagnosis.state) {
158
149
  case 'progressing':
@@ -162,31 +153,37 @@ function statusLine(diagnosis) {
162
153
  case 'blocked':
163
154
  return `${logSymbols.info} ${styleText('cyan', 'blocked')} — ${blockedHeadline(diagnosis)}`;
164
155
  case 'completed':
165
- return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}.`;
156
+ return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
166
157
  case 'aborted': {
167
158
  const tail = diagnosis.reason !== undefined ? ` — ${diagnosis.reason}` : '';
168
- return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}.`;
159
+ return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
169
160
  }
170
161
  case 'stuck':
171
162
  return `${logSymbols.warning} ${styleText('yellow', 'STUCK')} — ${causeDetail(diagnosis.cause).headline}`;
172
163
  }
173
164
  }
174
- /**
175
- * The diagnosis body: the verdict line, the current stage's activities +
176
- * transitions as evidence (for any in-flight state), and the cause block with
177
- * the engine's suggested fix when stuck. Pure so every verdict permutation is
178
- * assertable without driving a live evaluation. Takes the engine's
179
- * {@link SuggestedRemediation}s rather than re-deriving them, so the rendered
180
- * fix block and the `--json` output stay one computation.
181
- */
182
- export function renderDiagnosis({ diagnosis, input, remediations, }) {
165
+ export function rawTransitionProjection(evaluation) {
166
+ return evaluation.currentStage.transitions.map((t) => ({
167
+ name: t.transition.name,
168
+ to: t.transition.to,
169
+ filterSatisfied: t.filterSatisfied,
170
+ unevaluable: t.unevaluable,
171
+ blockedBy: t.insight.blockedBy.map((atom) => ({
172
+ groq: atom.atom.groq,
173
+ negated: atom.atom.negated,
174
+ pivotal: atom.pivotal,
175
+ ...(atom.requirement !== undefined ? { requirement: atom.requirement } : {}),
176
+ })),
177
+ }));
178
+ }
179
+ export function renderDiagnosis({ diagnosis, input, remediations, explanations, }) {
183
180
  const lines = [statusLine(diagnosis)];
184
181
  const inFlight = diagnosis.state === 'progressing' ||
185
182
  diagnosis.state === 'waiting' ||
186
183
  diagnosis.state === 'blocked' ||
187
184
  diagnosis.state === 'stuck';
188
185
  if (inFlight) {
189
- lines.push('', ...currentStageLines(input));
186
+ lines.push('', ...currentStageLines(input, explanations));
190
187
  }
191
188
  if (diagnosis.state === 'waiting') {
192
189
  lines.push('', ...waitingLines(diagnosis));
@@ -198,16 +195,13 @@ export function renderDiagnosis({ diagnosis, input, remediations, }) {
198
195
  const detail = causeDetail(diagnosis.cause);
199
196
  const runnable = remediations.filter((r) => r.available);
200
197
  lines.push('', sectionHeader("Why it's stuck"), ...detail.why);
201
- // A recoverable hold (e.g. an unevaluable transition filter) has no
202
- // runnable remediation — its "what to do" is in the why above, so skip
203
- // the empty fix header rather than dangle it.
204
198
  if (runnable.length > 0) {
205
199
  lines.push('', sectionHeader('Suggested fix'), ...remediationLines(runnable));
206
200
  }
207
201
  }
208
202
  return lines;
209
203
  }
210
- export default class Diagnose extends Command {
204
+ export default class Diagnose extends WorkflowCommand {
211
205
  static description = "Explain why a workflow instance is or isn't progressing, and what would unstick it.";
212
206
  static examples = [
213
207
  '<%= config.bin %> diagnose wf-instance.abc123',
@@ -219,30 +213,18 @@ export default class Diagnose extends Command {
219
213
  };
220
214
  static flags = {
221
215
  ...tagFlags,
222
- json: Flags.boolean({
223
- description: 'Emit the structured diagnosis as JSON instead of rendered output.',
224
- default: false,
225
- }),
216
+ ...jsonFlags,
226
217
  };
227
218
  async run() {
228
219
  const { args, flags } = await this.parse(Diagnose);
229
- const config = loadWorkflowConfig(flags.tag);
230
- const client = loadClient();
220
+ const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
231
221
  const { evaluation, diagnosis, remediations } = await workflow
232
222
  .diagnose({
233
223
  client,
234
- tag: config.tag,
235
- workflowResource: config.workflowResource,
224
+ ...baseEngineArgs(scope),
236
225
  instanceId: args.instanceId,
237
- // Token-auth CLI can't hit /users/me; pin the same system actor the
238
- // write verbs use. Transition filters and activity status are actor-
239
- // independent. A requirement that reads $actor/$assigned/$can is the
240
- // exception — it's evaluated against this system actor, so a `blocked`
241
- // verdict on such a requirement reflects the engine's view, not a
242
- // specific assignee's.
243
- access: { actor: { kind: 'system', id: 'workflow-cli' } },
244
226
  })
245
- .catch((error) => fail('diagnose failed:', error instanceof Error ? error.message : String(error)));
227
+ .catch((error) => fail('diagnose failed:', failureDetail(error)));
246
228
  const input = diagnoseInputFromEvaluation(evaluation);
247
229
  if (flags.json) {
248
230
  this.log(JSON.stringify({
@@ -250,12 +232,14 @@ export default class Diagnose extends Command {
250
232
  stage: evaluation.instance.currentStage,
251
233
  diagnosis,
252
234
  remediations,
235
+ transitions: rawTransitionProjection(evaluation),
253
236
  }, null, 2));
254
237
  return;
255
238
  }
256
239
  this.log(instanceHeader(evaluation.instance));
257
240
  this.log('');
258
- for (const line of renderDiagnosis({ diagnosis, input, remediations })) {
241
+ const explanations = new Map(unsatisfiedTransitionSummaries(evaluation).map((entry) => [entry.transition, entry.summary]));
242
+ for (const line of renderDiagnosis({ diagnosis, input, remediations, explanations })) {
259
243
  this.log(line);
260
244
  }
261
245
  }
@@ -1,13 +1,6 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type AvailableAction } from '@sanity/workflow-engine';
3
- import { type RanOp } from '../lib/ops-report.ts';
4
- /**
5
- * Parse repeated `--param key=value` flags into a params object. Each
6
- * value is JSON-parsed so numbers/booleans/objects type correctly,
7
- * falling back to the raw string. The engine validates the result against
8
- * the action's declared param types.
9
- */
10
- export declare function parseParams(pairs: string[]): Record<string, unknown>;
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
11
4
  /**
12
5
  * The list-mode body: every action on the current stage's activities, marked
13
6
  * fireable or not (with the disabling reason), plus a hint showing the
@@ -19,53 +12,37 @@ export declare function renderAvailableActions({ actions, stage, instanceId, }:
19
12
  stage: string;
20
13
  instanceId: string;
21
14
  }): string[];
22
- interface FireOutcome {
23
- fired: boolean;
24
- cascaded: number;
25
- instance: {
26
- currentStage: string;
27
- };
28
- ranOps?: RanOp[];
29
- }
30
15
  /** The `--json` payload for a fired action — the structured counterpart of
31
16
  * {@link fireActionReport}. Pure so its shape is asserted directly. */
32
- export declare function fireResultJson(result: FireOutcome, ids: {
17
+ export declare function fireResultJson(result: WriteOutcome, ids: {
33
18
  instanceId: string;
34
19
  activity: string;
35
20
  action: string;
36
21
  }): Record<string, unknown>;
37
- export interface FireActionReport {
38
- fired: boolean;
39
- message: string;
40
- opsLines: string[];
41
- }
42
22
  /**
43
23
  * Turn a `fireAction` result into the spinner message + ops block. Pure so
44
24
  * the fired / no-op / cascaded / ops permutations assert without a live
45
25
  * fire.
46
26
  */
47
27
  export declare function fireActionReport({ result, activity, action, }: {
48
- result: FireOutcome;
28
+ result: WriteOutcome;
49
29
  activity: string;
50
30
  action: string;
51
- }): FireActionReport;
52
- export default class FireAction extends Command {
31
+ }): WriteReport;
32
+ export default class FireAction extends WorkflowCommand {
53
33
  static description: string;
54
34
  static examples: string[];
55
35
  static args: {
56
36
  instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
57
37
  };
58
38
  static flags: {
39
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
59
40
  activity: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
60
41
  action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
61
42
  param: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
62
- as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
63
- 'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
64
- json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
65
43
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
66
44
  };
67
45
  run(): Promise<void>;
68
46
  private listActions;
69
47
  private fireOne;
70
48
  }
71
- export {};
@@ -1,60 +1,20 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
3
- import { 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';
6
- import { loadClient } from "../lib/client.js";
7
- import { loadWorkflowConfig } from "../lib/config.js";
8
- import { fail, failOnThrow } from "../lib/fail.js";
9
- import { tagFlags } from "../lib/flags.js";
10
- import { resolveActor } from "../lib/operation-args.js";
11
- import { emitWriteReport, opsAppliedLines } from "../lib/ops-report.js";
5
+ import { WorkflowCommand } from "../lib/base-command.js";
6
+ import { resolveInstanceContext } from "../lib/context.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
+ import { parseParams } from "../lib/params.js";
12
12
  import { instanceHeader } from "./show.js";
13
- function errorMessage(error) {
14
- return error instanceof Error ? error.message : String(error);
15
- }
16
- function coerceParamValue(raw) {
17
- try {
18
- return JSON.parse(raw);
19
- }
20
- catch {
21
- // Not valid JSON — treat it as a plain string so `--param note=hi`
22
- // needs no quoting. The engine validates against the action's
23
- // declared param type either way.
24
- return raw;
25
- }
26
- }
27
- function parseParam(pair) {
28
- const eq = pair.indexOf('=');
29
- if (eq < 1) {
30
- throw new Error(`expected key=value, got "${pair}"`);
31
- }
32
- return [pair.slice(0, eq), coerceParamValue(pair.slice(eq + 1))];
33
- }
34
- /**
35
- * Parse repeated `--param key=value` flags into a params object. Each
36
- * value is JSON-parsed so numbers/booleans/objects type correctly,
37
- * falling back to the raw string. The engine validates the result against
38
- * the action's declared param types.
39
- */
40
- export function parseParams(pairs) {
41
- return Object.fromEntries(pairs.map(parseParam));
42
- }
43
13
  function reasonText(reason) {
44
- switch (reason.kind) {
45
- case 'filter-failed':
46
- return `action filter not satisfied${reason.detail ? ` (${reason.detail})` : ''}`;
47
- case 'activity-not-active':
48
- return `activity is ${reason.status}`;
49
- case 'stage-terminal':
50
- return `stage '${reason.stage}' is terminal`;
51
- case 'mutation-guard-denied':
52
- return `blocked by mutation guard(s) ${reason.guardIds.join(', ')}${reason.detail ? ` (${reason.detail})` : ''}`;
53
- case 'instance-completed':
54
- return `instance completed at ${reason.completedAt}`;
55
- case 'requirements-unmet':
56
- 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(', ')}`;
57
16
  }
17
+ return actionDisabledDetail(reason);
58
18
  }
59
19
  function paramLabel(p) {
60
20
  return `${p.name}${p.required === true ? '' : '?'}:${p.type}`;
@@ -73,12 +33,6 @@ function actionLines(a) {
73
33
  }
74
34
  return lines;
75
35
  }
76
- /**
77
- * The list-mode body: every action on the current stage's activities, marked
78
- * fireable or not (with the disabling reason), plus a hint showing the
79
- * flags to fire one. Pure so the populated/empty permutations assert
80
- * without a live evaluation.
81
- */
82
36
  export function renderAvailableActions({ actions, stage, instanceId, }) {
83
37
  if (actions.length === 0) {
84
38
  return [`No actions on the activities of the current stage ('${stage}').`];
@@ -90,46 +44,37 @@ export function renderAvailableActions({ actions, stage, instanceId, }) {
90
44
  lines.push('', styleText('dim', 'Fire one with:'), styleText('dim', ` fire-action ${instanceId} --activity <activity> --action <action>`));
91
45
  return lines;
92
46
  }
93
- /** The `--json` payload for a fired action — the structured counterpart of
94
- * {@link fireActionReport}. Pure so its shape is asserted directly. */
95
47
  export function fireResultJson(result, ids) {
96
48
  return {
97
49
  instanceId: ids.instanceId,
98
50
  activity: ids.activity,
99
51
  action: ids.action,
100
- fired: result.fired,
52
+ changed: result.changed,
101
53
  cascaded: result.cascaded,
102
54
  currentStage: result.instance.currentStage,
103
55
  ...(result.ranOps !== undefined ? { ranOps: result.ranOps } : {}),
104
56
  };
105
57
  }
106
- /**
107
- * Turn a `fireAction` result into the spinner message + ops block. Pure so
108
- * the fired / no-op / cascaded / ops permutations assert without a live
109
- * fire.
110
- */
111
58
  export function fireActionReport({ result, activity, action, }) {
112
59
  const stage = styleText('bold', result.instance.currentStage);
113
- if (!result.fired) {
60
+ if (!result.changed) {
114
61
  return {
115
- fired: false,
62
+ changed: false,
116
63
  message: `fireAction returned without firing — activity '${activity}' is no longer active`,
117
64
  opsLines: [],
118
65
  };
119
66
  }
120
- const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
121
67
  return {
122
- fired: true,
123
- message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${tail}`,
68
+ changed: true,
69
+ message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${cascadeTail(result.cascaded)}`,
124
70
  opsLines: opsAppliedLines(result.ranOps),
125
71
  };
126
72
  }
127
- export default class FireAction extends Command {
128
- 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.';
129
75
  static examples = [
130
76
  '<%= config.bin %> fire-action wf-instance.abc123',
131
77
  '<%= config.bin %> fire-action wf-instance.abc123 --activity approve --action approve',
132
- '<%= config.bin %> fire-action wf-instance.abc123 --activity approve --action approve --as user.xyz --as-role editor',
133
78
  '<%= config.bin %> fire-action wf-instance.abc123 --activity publish --action publish --param note=shipping',
134
79
  ];
135
80
  static args = {
@@ -148,26 +93,18 @@ export default class FireAction extends Command {
148
93
  multiple: true,
149
94
  default: [],
150
95
  }),
151
- as: Flags.string({
152
- description: 'Attribute the fire to this Sanity user id (default: a workflow-cli system actor).',
153
- }),
154
- 'as-role': Flags.string({
155
- description: 'Role to attribute to the --as user (repeatable), so role-gated filters pass.',
156
- multiple: true,
157
- default: [],
158
- }),
159
- json: Flags.boolean({
160
- description: 'Emit structured JSON instead of rendered output.',
161
- default: false,
162
- }),
96
+ ...jsonFlags,
163
97
  };
164
98
  async run() {
165
99
  const { args, flags } = await this.parse(FireAction);
166
- const config = loadWorkflowConfig(flags.tag);
167
- const client = loadClient();
168
- const actor = failOnThrow('fire-action usage error:', () => resolveActor(flags.as, flags['as-role']));
100
+ const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
169
101
  if (flags.action === undefined) {
170
- await this.listActions({ client, config, instanceId: args.instanceId, actor, json: flags.json });
102
+ await this.listActions({
103
+ client,
104
+ scope,
105
+ instanceId: args.instanceId,
106
+ json: flags.json,
107
+ });
171
108
  return;
172
109
  }
173
110
  if (flags.activity === undefined) {
@@ -175,25 +112,22 @@ export default class FireAction extends Command {
175
112
  }
176
113
  await this.fireOne({
177
114
  client,
178
- config,
115
+ scope,
179
116
  instanceId: args.instanceId,
180
- actor,
181
117
  activity: flags.activity,
182
118
  action: flags.action,
183
119
  params: flags.param,
184
120
  json: flags.json,
185
121
  });
186
122
  }
187
- async listActions({ client, config, instanceId, actor, json, }) {
123
+ async listActions({ client, scope, instanceId, json, }) {
188
124
  const { evaluation, actions } = await workflow
189
125
  .availableActions({
190
126
  client,
191
- tag: config.tag,
192
- workflowResource: config.workflowResource,
127
+ ...baseEngineArgs(scope),
193
128
  instanceId,
194
- access: { actor },
195
129
  })
196
- .catch((error) => fail('fire-action failed:', errorMessage(error)));
130
+ .catch((error) => fail('fire-action failed:', failureDetail(error)));
197
131
  const stage = evaluation.instance.currentStage;
198
132
  if (json) {
199
133
  this.log(JSON.stringify({ instanceId, stage, actions }, null, 2));
@@ -206,37 +140,30 @@ export default class FireAction extends Command {
206
140
  }
207
141
  }
208
142
  async fireOne(opts) {
209
- const { client, config, instanceId, actor, activity, action, json } = opts;
143
+ const { client, scope, instanceId, activity, action, json } = opts;
210
144
  const params = failOnThrow('Invalid --param:', () => parseParams(opts.params));
211
145
  const fireArgs = {
212
146
  client,
213
- tag: config.tag,
214
- workflowResource: config.workflowResource,
147
+ ...baseEngineArgs(scope),
215
148
  instanceId,
216
149
  activity,
217
150
  action,
218
- access: { actor },
219
151
  params,
220
152
  };
221
153
  if (json) {
222
154
  const result = await workflow
223
155
  .fireAction(fireArgs)
224
- .catch((error) => fail('fireAction error:', errorMessage(error)));
156
+ .catch((error) => fail('fire-action error:', failureDetail(error)));
225
157
  this.log(JSON.stringify(fireResultJson(result, { instanceId, activity, action }), null, 2));
226
158
  return;
227
159
  }
228
- const spinner = ora(`Firing ${action} on ${activity}…`).start();
229
- try {
230
- const result = await workflow.fireAction(fireArgs);
231
- emitWriteReport({
232
- spinner,
233
- report: fireActionReport({ result, activity, action }),
234
- log: (line) => this.log(line),
235
- });
236
- }
237
- catch (error) {
238
- spinner.fail('Action rejected');
239
- fail('fireAction error:', errorMessage(error));
240
- }
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
+ });
241
168
  }
242
169
  }
@@ -1,18 +1,18 @@
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;
15
- 'workflow-name'?: string | undefined;
15
+ definition?: string | undefined;
16
16
  tag?: string | undefined;
17
17
  limit: number;
18
18
  }
@@ -25,23 +25,22 @@ 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: {
42
41
  'in-flight': import("@oclif/core/interfaces").BooleanFlag<boolean>;
43
42
  failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
44
- 'workflow-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
+ definition: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
45
44
  limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
46
45
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
47
46
  };