@sanity/workflow-cli 0.8.0 → 0.9.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 (60) hide show
  1. package/README.md +99 -61
  2. package/dist/commands/abort.d.ts +5 -7
  3. package/dist/commands/abort.js +15 -14
  4. package/dist/commands/definition/delete.d.ts +10 -6
  5. package/dist/commands/definition/delete.js +8 -11
  6. package/dist/commands/definition/diff.d.ts +8 -1
  7. package/dist/commands/definition/diff.js +30 -24
  8. package/dist/commands/definition/list.d.ts +4 -4
  9. package/dist/commands/definition/list.js +8 -8
  10. package/dist/commands/definition/show.d.ts +0 -10
  11. package/dist/commands/definition/show.js +11 -20
  12. package/dist/commands/deploy.d.ts +58 -5
  13. package/dist/commands/deploy.js +134 -27
  14. package/dist/commands/diagnose.d.ts +5 -1
  15. package/dist/commands/diagnose.js +18 -28
  16. package/dist/commands/fire-action.d.ts +15 -28
  17. package/dist/commands/fire-action.js +37 -72
  18. package/dist/commands/list.d.ts +2 -2
  19. package/dist/commands/list.js +8 -8
  20. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  21. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -2
  22. package/dist/commands/set-stage.d.ts +27 -3
  23. package/dist/commands/set-stage.js +80 -13
  24. package/dist/commands/show.d.ts +1 -0
  25. package/dist/commands/show.js +14 -8
  26. package/dist/commands/start.d.ts +59 -0
  27. package/dist/commands/start.js +169 -0
  28. package/dist/commands/tail.d.ts +3 -0
  29. package/dist/commands/tail.js +9 -5
  30. package/dist/lib/client.d.ts +11 -14
  31. package/dist/lib/client.js +45 -33
  32. package/dist/lib/context.d.ts +62 -0
  33. package/dist/lib/context.js +82 -0
  34. package/dist/lib/definitions.d.ts +38 -29
  35. package/dist/lib/definitions.js +45 -85
  36. package/dist/lib/diff.js +8 -0
  37. package/dist/lib/env.d.ts +0 -6
  38. package/dist/lib/env.js +3 -9
  39. package/dist/lib/fail.d.ts +6 -0
  40. package/dist/lib/fail.js +11 -1
  41. package/dist/lib/flags.d.ts +14 -2
  42. package/dist/lib/flags.js +24 -3
  43. package/dist/lib/load-config.d.ts +11 -0
  44. package/dist/lib/load-config.js +69 -0
  45. package/dist/lib/operation-args.d.ts +25 -7
  46. package/dist/lib/operation-args.js +12 -11
  47. package/dist/lib/ops-report.d.ts +19 -7
  48. package/dist/lib/ops-report.js +6 -7
  49. package/dist/lib/params.d.ts +7 -0
  50. package/dist/lib/params.js +26 -0
  51. package/dist/lib/select-deployment.d.ts +31 -0
  52. package/dist/lib/select-deployment.js +58 -0
  53. package/dist/lib/ui.d.ts +3 -1
  54. package/dist/lib/ui.js +1 -3
  55. package/oclif.manifest.json +145 -86
  56. package/package.json +6 -4
  57. package/dist/commands/move-stage.d.ts +0 -47
  58. package/dist/commands/move-stage.js +0 -77
  59. package/dist/lib/config.d.ts +0 -18
  60. package/dist/lib/config.js +0 -50
@@ -1,45 +1,15 @@
1
1
  import { styleText } from 'node:util';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
- import { workflow, } from '@sanity/workflow-engine';
3
+ import { deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
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";
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";
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
14
  switch (reason.kind) {
45
15
  case 'filter-failed':
@@ -49,7 +19,7 @@ function reasonText(reason) {
49
19
  case 'stage-terminal':
50
20
  return `stage '${reason.stage}' is terminal`;
51
21
  case 'mutation-guard-denied':
52
- return `blocked by mutation guard(s) ${reason.guardIds.join(', ')}${reason.detail ? ` (${reason.detail})` : ''}`;
22
+ return `blocked by mutation guard(s) ${deniedGuardLabels(reason.denied).join(', ')}`;
53
23
  case 'instance-completed':
54
24
  return `instance completed at ${reason.completedAt}`;
55
25
  case 'requirements-unmet':
@@ -79,7 +49,7 @@ function actionLines(a) {
79
49
  * flags to fire one. Pure so the populated/empty permutations assert
80
50
  * without a live evaluation.
81
51
  */
82
- export function renderAvailableActions(actions, stage, instanceId) {
52
+ export function renderAvailableActions({ actions, stage, instanceId, }) {
83
53
  if (actions.length === 0) {
84
54
  return [`No actions on the activities of the current stage ('${stage}').`];
85
55
  }
@@ -97,7 +67,7 @@ export function fireResultJson(result, ids) {
97
67
  instanceId: ids.instanceId,
98
68
  activity: ids.activity,
99
69
  action: ids.action,
100
- fired: result.fired,
70
+ changed: result.changed,
101
71
  cascaded: result.cascaded,
102
72
  currentStage: result.instance.currentStage,
103
73
  ...(result.ranOps !== undefined ? { ranOps: result.ranOps } : {}),
@@ -108,18 +78,18 @@ export function fireResultJson(result, ids) {
108
78
  * the fired / no-op / cascaded / ops permutations assert without a live
109
79
  * fire.
110
80
  */
111
- export function fireActionReport(result, activity, action) {
81
+ export function fireActionReport({ result, activity, action, }) {
112
82
  const stage = styleText('bold', result.instance.currentStage);
113
- if (!result.fired) {
83
+ if (!result.changed) {
114
84
  return {
115
- fired: false,
85
+ changed: false,
116
86
  message: `fireAction returned without firing — activity '${activity}' is no longer active`,
117
87
  opsLines: [],
118
88
  };
119
89
  }
120
90
  const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
121
91
  return {
122
- fired: true,
92
+ changed: true,
123
93
  message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${tail}`,
124
94
  opsLines: opsAppliedLines(result.ranOps),
125
95
  };
@@ -148,26 +118,21 @@ export default class FireAction extends Command {
148
118
  multiple: true,
149
119
  default: [],
150
120
  }),
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
- }),
121
+ ...actorFlags,
122
+ ...jsonFlags,
163
123
  };
164
124
  async run() {
165
125
  const { args, flags } = await this.parse(FireAction);
166
- const config = loadWorkflowConfig(flags.tag);
167
- const client = loadClient();
126
+ const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
168
127
  const actor = failOnThrow('fire-action usage error:', () => resolveActor(flags.as, flags['as-role']));
169
128
  if (flags.action === undefined) {
170
- await this.listActions(client, config, args.instanceId, actor, flags.json);
129
+ await this.listActions({
130
+ client,
131
+ scope,
132
+ instanceId: args.instanceId,
133
+ actor,
134
+ json: flags.json,
135
+ });
171
136
  return;
172
137
  }
173
138
  if (flags.activity === undefined) {
@@ -175,7 +140,7 @@ export default class FireAction extends Command {
175
140
  }
176
141
  await this.fireOne({
177
142
  client,
178
- config,
143
+ scope,
179
144
  instanceId: args.instanceId,
180
145
  actor,
181
146
  activity: flags.activity,
@@ -184,14 +149,12 @@ export default class FireAction extends Command {
184
149
  json: flags.json,
185
150
  });
186
151
  }
187
- async listActions(client, config, instanceId, actor, json) {
152
+ async listActions({ client, scope, instanceId, actor, json, }) {
188
153
  const { evaluation, actions } = await workflow
189
154
  .availableActions({
190
155
  client,
191
- tag: config.tag,
192
- workflowResource: config.workflowResource,
156
+ ...baseEngineArgs(scope, actor),
193
157
  instanceId,
194
- access: { actor },
195
158
  })
196
159
  .catch((error) => fail('fire-action failed:', errorMessage(error)));
197
160
  const stage = evaluation.instance.currentStage;
@@ -201,38 +164,40 @@ export default class FireAction extends Command {
201
164
  }
202
165
  this.log(instanceHeader(evaluation.instance));
203
166
  this.log('');
204
- for (const line of renderAvailableActions(actions, stage, instanceId)) {
167
+ for (const line of renderAvailableActions({ actions, stage, instanceId })) {
205
168
  this.log(line);
206
169
  }
207
170
  }
208
171
  async fireOne(opts) {
209
- const { client, config, instanceId, actor, activity, action, json } = opts;
172
+ const { client, scope, instanceId, actor, activity, action, json } = opts;
210
173
  const params = failOnThrow('Invalid --param:', () => parseParams(opts.params));
211
174
  const fireArgs = {
212
175
  client,
213
- tag: config.tag,
214
- workflowResource: config.workflowResource,
176
+ ...baseEngineArgs(scope, actor),
215
177
  instanceId,
216
178
  activity,
217
179
  action,
218
- access: { actor },
219
180
  params,
220
181
  };
221
182
  if (json) {
222
183
  const result = await workflow
223
184
  .fireAction(fireArgs)
224
- .catch((error) => fail('fireAction error:', errorMessage(error)));
185
+ .catch((error) => fail('fire-action error:', errorMessage(error)));
225
186
  this.log(JSON.stringify(fireResultJson(result, { instanceId, activity, action }), null, 2));
226
187
  return;
227
188
  }
228
189
  const spinner = ora(`Firing ${action} on ${activity}…`).start();
229
190
  try {
230
191
  const result = await workflow.fireAction(fireArgs);
231
- emitWriteReport(spinner, fireActionReport(result, activity, action), (line) => this.log(line));
192
+ emitWriteReport({
193
+ spinner,
194
+ report: fireActionReport({ result, activity, action }),
195
+ log: (line) => this.log(line),
196
+ });
232
197
  }
233
198
  catch (error) {
234
199
  spinner.fail('Action rejected');
235
- fail('fireAction error:', errorMessage(error));
200
+ fail('fire-action error:', errorMessage(error));
236
201
  }
237
202
  }
238
203
  }
@@ -12,7 +12,7 @@ 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
  }
@@ -41,7 +41,7 @@ export default class List extends Command {
41
41
  static flags: {
42
42
  'in-flight': import("@oclif/core/interfaces").BooleanFlag<boolean>;
43
43
  failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
44
- 'workflow-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
44
+ definition: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
45
45
  limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
46
46
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
47
47
  };
@@ -3,7 +3,7 @@ import { Command, Flags } from '@oclif/core';
3
3
  import { WORKFLOW_INSTANCE_TYPE, tagScopeFilter } from '@sanity/workflow-engine';
4
4
  import { Table } from 'console-table-printer';
5
5
  import logSymbols from 'log-symbols';
6
- import { loadClient } from "../lib/client.js";
6
+ import { resolveReadContext } from "../lib/context.js";
7
7
  import { tagFlags } from "../lib/flags.js";
8
8
  import { formatAge } from "../lib/ui.js";
9
9
  /**
@@ -15,7 +15,7 @@ export function buildListQuery(flags) {
15
15
  const filters = [`_type == "${WORKFLOW_INSTANCE_TYPE}"`];
16
16
  if (flags['in-flight'])
17
17
  filters.push('!defined(completedAt)');
18
- if (flags['workflow-name'])
18
+ if (flags.definition)
19
19
  filters.push('definition == $definition');
20
20
  // Cheap approximation of --failed: any activity in any stage with status "failed".
21
21
  if (flags.failed)
@@ -32,8 +32,8 @@ export function buildListQuery(flags) {
32
32
  lastChangedAt
33
33
  }`;
34
34
  const params = { limit: flags.limit };
35
- if (flags['workflow-name'])
36
- params['definition'] = flags['workflow-name'];
35
+ if (flags.definition)
36
+ params['definition'] = flags.definition;
37
37
  if (flags.tag)
38
38
  params['tag'] = flags.tag;
39
39
  return { groq, params };
@@ -68,7 +68,7 @@ export default class List extends Command {
68
68
  static examples = [
69
69
  '<%= config.bin %> list',
70
70
  '<%= config.bin %> list --in-flight',
71
- '<%= config.bin %> list --workflow-name productLaunch',
71
+ '<%= config.bin %> list --definition productLaunch',
72
72
  '<%= config.bin %> list --tag prod',
73
73
  ];
74
74
  static flags = {
@@ -81,8 +81,8 @@ export default class List extends Command {
81
81
  description: 'Only instances with at least one failed activity.',
82
82
  default: false,
83
83
  }),
84
- 'workflow-name': Flags.string({
85
- description: 'Filter by workflow definition name.',
84
+ definition: Flags.string({
85
+ description: "Only instances of this workflow definition (its `name`; the instance's `definition` field).",
86
86
  }),
87
87
  limit: Flags.integer({
88
88
  description: 'Maximum rows to return.',
@@ -91,8 +91,8 @@ export default class List extends Command {
91
91
  };
92
92
  async run() {
93
93
  const { flags } = await this.parse(List);
94
+ const { client } = await resolveReadContext(flags);
94
95
  const { groq, params } = buildListQuery(flags);
95
- const client = loadClient();
96
96
  const rows = await client.fetch(groq, params);
97
97
  if (rows.length === 0) {
98
98
  this.log(`${logSymbols.info} no instances match`);
@@ -1,5 +1,5 @@
1
1
  import { StubCommand } from '../lib/stub.ts';
2
- export default class RetryActivity extends StubCommand {
2
+ export default class ResetActivity extends StubCommand {
3
3
  static hidden: boolean;
4
4
  static description: string;
5
5
  static args: {
@@ -1,9 +1,9 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { StubCommand } from "../lib/stub.js";
3
- export default class RetryActivity extends StubCommand {
3
+ export default class ResetActivity extends StubCommand {
4
4
  // Stubbed, so kept off the help + published surface until it's wired.
5
5
  static hidden = true;
6
- static description = 'Re-invoke a failed activity on an in-flight instance.';
6
+ static description = 'Reset a failed activity on an in-flight instance — back to pending (re-run) or to skipped (bypass).';
7
7
  static args = {
8
8
  instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
9
9
  activity: Args.string({ required: true, description: 'Activity name within the current stage.' }),
@@ -1,12 +1,36 @@
1
- import { StubCommand } from '../lib/stub.ts';
2
- export default class SetStage extends StubCommand {
3
- static hidden: boolean;
1
+ import { Command } from '@oclif/core';
2
+ import { type SetStageArgs, type WorkflowInstance } from '@sanity/workflow-engine';
3
+ import { type EngineScope } from '../lib/operation-args.ts';
4
+ import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
5
+ export default class SetStage extends Command {
4
6
  static description: string;
7
+ static examples: string[];
5
8
  static args: {
6
9
  instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
10
  };
8
11
  static flags: {
9
12
  to: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
13
  reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
15
  };
16
+ run(): Promise<void>;
12
17
  }
18
+ /** The shared operation args ({@link buildOperationArgs}) plus the
19
+ * setStage-specific target. */
20
+ export declare function buildSetStageArgs({ scope, instanceId, targetStage, reason, }: {
21
+ scope: EngineScope;
22
+ instanceId: string;
23
+ targetStage: string;
24
+ reason: string | undefined;
25
+ }): SetStageArgs & EngineScope;
26
+ /** {@link WriteOutcome} with the terminal stamp the no-op copy reads. */
27
+ interface SetStageOutcome extends WriteOutcome {
28
+ instance: Pick<WorkflowInstance, 'currentStage' | 'completedAt'>;
29
+ }
30
+ /**
31
+ * Turn a `setStage` result into the spinner message + ops block. Pure so
32
+ * the changed / terminal / already-at-target / cascaded / ops permutations
33
+ * can be asserted without driving a real move.
34
+ */
35
+ export declare function setStageReport(result: SetStageOutcome, to: string): WriteReport;
36
+ export {};
@@ -1,18 +1,85 @@
1
- import { Args, Flags } from '@oclif/core';
2
- import { StubCommand } from "../lib/stub.js";
3
- export default class SetStage extends StubCommand {
4
- // Stubbed, so kept off the help + published surface until it's wired.
5
- static hidden = true;
6
- // Engine note: workflow.setStage currently always runs guards + stage
7
- // entry (activity activation, effects). A "manual override" that bypasses
8
- // them is engine work that hasn't landed; this command stub captures
9
- // the intended surface only.
10
- static description = "Force-set an instance's current stage, bypassing guards + effects (manual override).";
1
+ import { styleText } from 'node:util';
2
+ import { Args, Command, Flags } from '@oclif/core';
3
+ import { workflow } from '@sanity/workflow-engine';
4
+ import ora from 'ora';
5
+ import { resolveInstanceContext } from "../lib/context.js";
6
+ import { errorMessage, fail } from "../lib/fail.js";
7
+ import { tagFlags } from "../lib/flags.js";
8
+ import { buildOperationArgs } from "../lib/operation-args.js";
9
+ import { emitWriteReport, opsAppliedLines, } from "../lib/ops-report.js";
10
+ export default class SetStage extends Command {
11
+ 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
+ static examples = [
13
+ '<%= config.bin %> set-stage wf-instance.abc123 --to ready',
14
+ "<%= config.bin %> set-stage wf-instance.abc123 --to ready --reason 'unblock for demo'",
15
+ ];
11
16
  static args = {
12
- instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
17
+ instanceId: Args.string({
18
+ required: true,
19
+ description: 'Workflow instance id to move.',
20
+ }),
13
21
  };
14
22
  static flags = {
15
- to: Flags.string({ required: true, description: 'Target stage name.' }),
16
- reason: Flags.string({ description: 'Reason for the manual override.' }),
23
+ ...tagFlags,
24
+ to: Flags.string({
25
+ required: true,
26
+ description: 'Target stage name.',
27
+ }),
28
+ reason: Flags.string({
29
+ description: 'Free-text reason — recorded on the history entry for audit.',
30
+ }),
31
+ };
32
+ async run() {
33
+ const { args, flags } = await this.parse(SetStage);
34
+ 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({
38
+ client,
39
+ ...buildSetStageArgs({
40
+ scope,
41
+ instanceId: args.instanceId,
42
+ targetStage: flags.to,
43
+ reason: flags.reason,
44
+ }),
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
+ }
56
+ }
57
+ }
58
+ /** The shared operation args ({@link buildOperationArgs}) plus the
59
+ * setStage-specific target. */
60
+ export function buildSetStageArgs({ scope, instanceId, targetStage, reason, }) {
61
+ return { ...buildOperationArgs({ scope, instanceId, reason }), targetStage };
62
+ }
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
+ export function setStageReport(result, to) {
69
+ const stage = styleText('bold', result.instance.currentStage);
70
+ 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
+ const { completedAt } = result.instance;
74
+ const message = completedAt !== undefined
75
+ ? `setStage changed nothing — instance is terminal (at ${stage} since ${completedAt})`
76
+ : `setStage changed nothing — instance is already at ${stage}`;
77
+ return { changed: false, message, opsLines: [] };
78
+ }
79
+ const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
80
+ return {
81
+ changed: true,
82
+ message: `Now at ${stage} (was → ${to}${tail})`,
83
+ opsLines: opsAppliedLines(result.ranOps),
17
84
  };
18
85
  }
@@ -8,6 +8,7 @@ export default class Show extends Command {
8
8
  };
9
9
  static flags: {
10
10
  include: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
11
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  };
12
13
  run(): Promise<void>;
13
14
  }
@@ -2,7 +2,8 @@ import { styleText } from 'node:util';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
3
  import { abortReason } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { loadClient } from "../lib/client.js";
5
+ import { resolveReadContext } from "../lib/context.js";
6
+ import { tagFlags } from "../lib/flags.js";
6
7
  import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
7
8
  export default class Show extends Command {
8
9
  static description = 'Show the state, activities, and effects of a workflow instance.';
@@ -17,6 +18,7 @@ export default class Show extends Command {
17
18
  }),
18
19
  };
19
20
  static flags = {
21
+ ...tagFlags,
20
22
  include: Flags.string({
21
23
  description: 'Optional sections to include in output.',
22
24
  options: ['history'],
@@ -26,7 +28,7 @@ export default class Show extends Command {
26
28
  };
27
29
  async run() {
28
30
  const { args, flags } = await this.parse(Show);
29
- const client = loadClient();
31
+ const { client } = await resolveReadContext(flags);
30
32
  const instance = await client.getDocument(args.instanceId);
31
33
  if (!instance) {
32
34
  this.error(`${logSymbols.error} no instance with id "${args.instanceId}"`, { exit: 1 });
@@ -46,8 +48,8 @@ const HEADER_PAD = 9; // "Completed" — the longest label in the block
46
48
  export function instanceHeader(instance) {
47
49
  const title = `${styleText('bold', `${instance.definition} v${instance.pinnedVersion}`)} ${styleText('cyan', instance._id)}`;
48
50
  const rows = [
49
- formatKeyValue('Stage', instance.currentStage, { padTo: HEADER_PAD }),
50
- formatKeyValue('Started', formatTimestamp(instance.startedAt), { padTo: HEADER_PAD }),
51
+ formatKeyValue({ key: 'Stage', value: instance.currentStage, padTo: HEADER_PAD }),
52
+ formatKeyValue({ key: 'Started', value: formatTimestamp(instance.startedAt), padTo: HEADER_PAD }),
51
53
  ];
52
54
  // Aborted instances also carry a completedAt stamped at the abort, so the
53
55
  // abort line stands in for completion — showing a green "Completed" too would
@@ -56,15 +58,15 @@ export function instanceHeader(instance) {
56
58
  const reason = abortReason(instance);
57
59
  const tail = reason ? ` — ${reason}` : '';
58
60
  const value = styleText('yellow', `${formatTimestamp(instance.abortedAt)}${tail}`);
59
- rows.push(formatKeyValue('Aborted', value, { padTo: HEADER_PAD }));
61
+ rows.push(formatKeyValue({ key: 'Aborted', value, padTo: HEADER_PAD }));
60
62
  }
61
63
  else {
62
64
  const completed = instance.completedAt
63
65
  ? styleText('green', formatTimestamp(instance.completedAt))
64
66
  : styleText('dim', '—');
65
- rows.push(formatKeyValue('Completed', completed, { padTo: HEADER_PAD }));
67
+ rows.push(formatKeyValue({ key: 'Completed', value: completed, padTo: HEADER_PAD }));
66
68
  }
67
- rows.push(formatKeyValue('Tag', instance.tag, { padTo: HEADER_PAD }));
69
+ rows.push(formatKeyValue({ key: 'Tag', value: instance.tag, padTo: HEADER_PAD }));
68
70
  return [title, ...rows].join('\n');
69
71
  }
70
72
  /**
@@ -79,7 +81,11 @@ export function describeInstance(instance, options) {
79
81
  ? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
80
82
  : styleText('cyan', ' (current)');
81
83
  lines.push(` • ${stage.name}${marker}`);
82
- for (const activity of stage.activities) {
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
+ for (const activity of stage.activities ?? []) {
83
89
  lines.push(` ${activityIcon[activity.status]} ${activity.name} ${styleText('dim', `[${activity.status}]`)}`);
84
90
  }
85
91
  }
@@ -0,0 +1,59 @@
1
+ import { Command } from '@oclif/core';
2
+ import { type Actor, type FieldEntry, type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance, type WorkflowLifecycle } from '@sanity/workflow-engine';
3
+ import { type EngineScope } from '../lib/operation-args.ts';
4
+ export default class Start extends Command {
5
+ static description: string;
6
+ static examples: string[];
7
+ static args: {
8
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
9
+ };
10
+ static flags: {
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
+ version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ field: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
16
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ };
18
+ run(): Promise<void>;
19
+ }
20
+ /**
21
+ * Why a definition can't be started standalone, or `undefined` when it can.
22
+ * Advisory, mirroring the Startable column in `definition list` — the engine
23
+ * itself does not refuse ({@link isStartableDefinition}), but the CLI has no
24
+ * way to supply the parent context a spawn-only child expects.
25
+ */
26
+ export declare function startRefusal(definition: {
27
+ lifecycle?: WorkflowLifecycle | undefined;
28
+ }): string | undefined;
29
+ /**
30
+ * Type each `--field name=value` against the workflow's declared field
31
+ * entries — the engine takes typed {@link InitialFieldValue}s, and a value's
32
+ * type IS its declared entry's kind. Only `input`-sourced entries read
33
+ * caller values (the engine silently ignores the rest), so anything else
34
+ * fails here, naming the fields that ARE settable; value validation stays
35
+ * in the engine.
36
+ */
37
+ export declare function buildInitialFields({ declared, values, }: {
38
+ declared: Pick<FieldEntry, 'type' | 'name' | 'initialValue'>[];
39
+ values: Record<string, unknown>;
40
+ }): InitialFieldValue[];
41
+ /**
42
+ * The shared engine args ({@link baseEngineArgs}) plus the start-specific
43
+ * fields. Optionals spread conditionally because the engine's optional args
44
+ * reject an explicit `undefined` under `exactOptionalPropertyTypes`.
45
+ */
46
+ export declare function buildStartArgs({ scope, name, version, initialFields, actor, }: {
47
+ scope: EngineScope;
48
+ name: string;
49
+ version: number | undefined;
50
+ initialFields: InitialFieldValue[];
51
+ actor: Actor;
52
+ }): StartInstanceArgs & EngineScope;
53
+ /** The success spinner message. Pure so the in-flight / immediately-terminal
54
+ * permutations can be asserted without driving a real start. */
55
+ export declare function startReport(instance: Pick<WorkflowInstance, '_id' | 'currentStage' | 'completedAt'>): string;
56
+ /** The `--json` payload. Keys follow the CLI's `--json` vocabulary (matching
57
+ * `fire-action`): `instanceId` carries the document `_id`, `version` the
58
+ * pinned definition version; the rest mirror the instance fields. */
59
+ export declare function startResultJson(instance: Pick<WorkflowInstance, '_id' | 'definition' | 'pinnedVersion' | 'currentStage' | 'completedAt'>): Record<string, unknown>;