@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,23 +1,17 @@
1
- import { styleText } from 'node:util';
2
- import { Command, Flags } from '@oclif/core';
3
- import { WORKFLOW_INSTANCE_TYPE, tagScopeFilter } from '@sanity/workflow-engine';
4
- import { Table } from 'console-table-printer';
1
+ import { Flags } from '@oclif/core';
2
+ import { WORKFLOW_INSTANCE_TYPE, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
5
3
  import logSymbols from 'log-symbols';
6
- import { loadClient } from "../lib/client.js";
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
+ import { resolveReadTargets } from "../lib/context.js";
7
6
  import { tagFlags } from "../lib/flags.js";
8
- import { formatAge } from "../lib/ui.js";
9
- /**
10
- * Build the GROQ query + params for `list` from its flags. Pure so the
11
- * filter combinations can be asserted without a live dataset. `--tag` is an
12
- * optional filter: omit it and the listing spans every partition.
13
- */
7
+ import { runReadAcrossTargets } from "../lib/read-fanout.js";
8
+ import { clipToLimit, formatAge, formatTable } from "../lib/ui.js";
14
9
  export function buildListQuery(flags) {
15
10
  const filters = [`_type == "${WORKFLOW_INSTANCE_TYPE}"`];
16
11
  if (flags['in-flight'])
17
12
  filters.push('!defined(completedAt)');
18
- if (flags['workflow-name'])
13
+ if (flags.definition)
19
14
  filters.push('definition == $definition');
20
- // Cheap approximation of --failed: any activity in any stage with status "failed".
21
15
  if (flags.failed)
22
16
  filters.push('count(stages[].activities[status == "failed"]) > 0');
23
17
  if (flags.tag)
@@ -31,44 +25,32 @@ export function buildListQuery(flags) {
31
25
  abortedAt,
32
26
  lastChangedAt
33
27
  }`;
34
- const params = { limit: flags.limit };
35
- if (flags['workflow-name'])
36
- params['definition'] = flags['workflow-name'];
28
+ const params = { limit: flags.limit + 1 };
29
+ if (flags.definition)
30
+ params['definition'] = flags.definition;
37
31
  if (flags.tag)
38
32
  params['tag'] = flags.tag;
39
33
  return { groq, params };
40
34
  }
41
- /** Aborted instances also carry `completedAt`, so check `abortedAt` first. */
42
- function deriveStatus(r) {
43
- if (r.abortedAt)
44
- return 'aborted';
45
- if (r.completedAt)
46
- return 'completed';
47
- return 'in-flight';
48
- }
49
- /** Map a queried instance to its display row (derives the status column).
50
- * Pure — status is the semantic value; the table colors it at render. */
51
35
  export function instanceRow(r) {
52
36
  return {
53
37
  _id: r._id,
54
38
  definition: r.definition,
55
39
  tag: r.tag,
56
40
  currentStage: r.currentStage,
57
- status: deriveStatus(r),
41
+ status: terminalState({
42
+ ...(r.completedAt != null ? { completedAt: r.completedAt } : {}),
43
+ ...(r.abortedAt != null ? { abortedAt: r.abortedAt } : {}),
44
+ }),
58
45
  lastChangedAt: r.lastChangedAt,
59
46
  };
60
47
  }
61
- const STATUS_COLOR = {
62
- completed: 'green',
63
- aborted: 'dim',
64
- 'in-flight': 'cyan',
65
- };
66
- export default class List extends Command {
48
+ export default class List extends WorkflowCommand {
67
49
  static description = 'List workflow instances in the configured dataset.';
68
50
  static examples = [
69
51
  '<%= config.bin %> list',
70
52
  '<%= config.bin %> list --in-flight',
71
- '<%= config.bin %> list --workflow-name productLaunch',
53
+ '<%= config.bin %> list --definition productLaunch',
72
54
  '<%= config.bin %> list --tag prod',
73
55
  ];
74
56
  static flags = {
@@ -81,8 +63,8 @@ export default class List extends Command {
81
63
  description: 'Only instances with at least one failed activity.',
82
64
  default: false,
83
65
  }),
84
- 'workflow-name': Flags.string({
85
- description: 'Filter by workflow definition name.',
66
+ definition: Flags.string({
67
+ description: "Only instances of this workflow definition (its `name`; the instance's `definition` field).",
86
68
  }),
87
69
  limit: Flags.integer({
88
70
  description: 'Maximum rows to return.',
@@ -91,34 +73,39 @@ export default class List extends Command {
91
73
  };
92
74
  async run() {
93
75
  const { flags } = await this.parse(List);
76
+ const targets = await resolveReadTargets(flags);
94
77
  const { groq, params } = buildListQuery(flags);
95
- const client = loadClient();
96
- const rows = await client.fetch(groq, params);
97
- if (rows.length === 0) {
98
- this.log(`${logSymbols.info} no instances match`);
99
- return;
100
- }
101
- const table = new Table({
102
- columns: [
103
- { name: '_id', title: 'Instance', alignment: 'left' },
104
- { name: 'definition', title: 'Workflow', alignment: 'left' },
105
- { name: 'tag', title: 'Tag', alignment: 'left' },
106
- { name: 'currentStage', title: 'Stage', alignment: 'left' },
107
- { name: 'status', title: 'Status', alignment: 'left' },
108
- { name: 'updated', title: 'Updated', alignment: 'left' },
109
- ],
78
+ const failures = await runReadAcrossTargets({
79
+ targets,
80
+ log: (line) => this.log(line),
81
+ run: async ({ client }) => {
82
+ const fetched = await client.fetch(groq, params, { tag: 'list' });
83
+ if (fetched.length === 0) {
84
+ this.log(`${logSymbols.info} no instances match`);
85
+ return;
86
+ }
87
+ const { rows, note } = clipToLimit(fetched, flags.limit);
88
+ const lines = formatTable(['instance', 'workflow', 'tag', 'stage', 'status', 'updated'], rows.map((r) => {
89
+ const row = instanceRow(r);
90
+ return [
91
+ row._id,
92
+ row.definition,
93
+ row.tag,
94
+ row.currentStage,
95
+ row.status,
96
+ formatAge(row.lastChangedAt),
97
+ ];
98
+ }));
99
+ for (const line of lines) {
100
+ this.log(line);
101
+ }
102
+ if (note) {
103
+ this.log(note);
104
+ }
105
+ },
110
106
  });
111
- for (const r of rows) {
112
- const row = instanceRow(r);
113
- table.addRow({
114
- _id: row._id,
115
- definition: row.definition,
116
- tag: row.tag,
117
- currentStage: row.currentStage,
118
- status: styleText(STATUS_COLOR[row.status], row.status),
119
- updated: formatAge(row.lastChangedAt),
120
- });
107
+ if (failures.length > 0) {
108
+ this.exit(1);
121
109
  }
122
- table.printTable();
123
110
  }
124
111
  }
@@ -1,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,8 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { StubCommand } from "../lib/stub.js";
3
- export default class RetryActivity extends StubCommand {
4
- // Stubbed, so kept off the help + published surface until it's wired.
3
+ export default class ResetActivity extends StubCommand {
5
4
  static hidden = true;
6
- static description = 'Re-invoke a failed activity on an in-flight instance.';
5
+ static description = 'Reset a failed activity on an in-flight instance — back to pending (re-run) or to skipped (bypass).';
7
6
  static args = {
8
7
  instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
9
8
  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 { type SetStageArgs, type WorkflowInstance } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
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 WorkflowCommand {
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,69 @@
1
+ import { styleText } from 'node:util';
1
2
  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).";
3
+ import { workflow } from '@sanity/workflow-engine';
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
+ import { resolveInstanceContext } from "../lib/context.js";
6
+ import { tagFlags } from "../lib/flags.js";
7
+ import { buildOperationArgs } from "../lib/operation-args.js";
8
+ import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../lib/ops-report.js";
9
+ export default class SetStage extends WorkflowCommand {
10
+ static description = "Force an instance into a stage, regardless of its declared transitions and filters — the engine's setStage admin override. The target stage's enter lifecycle still runs (auto-activities start, stage guards reconcile), and the post-move cascade can immediately auto-transition the instance onward.";
11
+ static examples = [
12
+ '<%= config.bin %> set-stage wf-instance.abc123 --to ready',
13
+ "<%= config.bin %> set-stage wf-instance.abc123 --to ready --reason 'unblock for demo'",
14
+ ];
11
15
  static args = {
12
- instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
16
+ instanceId: Args.string({
17
+ required: true,
18
+ description: 'Workflow instance id to move.',
19
+ }),
13
20
  };
14
21
  static flags = {
15
- to: Flags.string({ required: true, description: 'Target stage name.' }),
16
- reason: Flags.string({ description: 'Reason for the manual override.' }),
22
+ ...tagFlags,
23
+ to: Flags.string({
24
+ required: true,
25
+ description: 'Target stage name.',
26
+ }),
27
+ reason: Flags.string({
28
+ description: 'Free-text reason — recorded on the history entry for audit.',
29
+ }),
30
+ };
31
+ async run() {
32
+ const { args, flags } = await this.parse(SetStage);
33
+ const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
34
+ await runWriteVerb({
35
+ startLabel: `Setting ${args.instanceId} → ${flags.to}…`,
36
+ failLabel: 'Stage move rejected',
37
+ failHeadline: 'set-stage error:',
38
+ run: () => workflow.setStage({
39
+ client,
40
+ ...buildSetStageArgs({
41
+ scope,
42
+ instanceId: args.instanceId,
43
+ targetStage: flags.to,
44
+ reason: flags.reason,
45
+ }),
46
+ }),
47
+ report: (result) => setStageReport(result, flags.to),
48
+ log: (line) => this.log(line),
49
+ });
50
+ }
51
+ }
52
+ export function buildSetStageArgs({ scope, instanceId, targetStage, reason, }) {
53
+ return { ...buildOperationArgs({ scope, instanceId, reason }), targetStage };
54
+ }
55
+ export function setStageReport(result, to) {
56
+ const stage = styleText('bold', result.instance.currentStage);
57
+ if (!result.changed) {
58
+ const { completedAt } = result.instance;
59
+ const message = completedAt !== undefined
60
+ ? `setStage changed nothing — instance is terminal (at ${stage} since ${completedAt})`
61
+ : `setStage changed nothing — instance is already at ${stage}`;
62
+ return { changed: false, message, opsLines: [] };
63
+ }
64
+ return {
65
+ changed: true,
66
+ message: `Now at ${stage} (was → ${to}${cascadeTail(result.cascaded)})`,
67
+ opsLines: opsAppliedLines(result.ranOps),
17
68
  };
18
69
  }
@@ -1,6 +1,6 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type WorkflowInstance } from '@sanity/workflow-engine';
3
- export default class Show extends Command {
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ export default class Show extends WorkflowCommand {
4
4
  static description: string;
5
5
  static examples: string[];
6
6
  static args: {
@@ -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
  }
@@ -1,10 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
3
- import { abortReason } from '@sanity/workflow-engine';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { abortReason, displayTitle, terminalState, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { loadClient } from "../lib/client.js";
5
+ import { WorkflowCommand } from "../lib/base-command.js";
6
+ import { findInstance, resolveReadTargets } from "../lib/context.js";
7
+ import { tagFlags } from "../lib/flags.js";
6
8
  import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
7
- export default class Show extends Command {
9
+ export default class Show extends WorkflowCommand {
8
10
  static description = 'Show the state, activities, and effects of a workflow instance.';
9
11
  static examples = [
10
12
  '<%= config.bin %> show wf-instance.abc123',
@@ -17,6 +19,7 @@ export default class Show extends Command {
17
19
  }),
18
20
  };
19
21
  static flags = {
22
+ ...tagFlags,
20
23
  include: Flags.string({
21
24
  description: 'Optional sections to include in output.',
22
25
  options: ['history'],
@@ -26,11 +29,12 @@ export default class Show extends Command {
26
29
  };
27
30
  async run() {
28
31
  const { args, flags } = await this.parse(Show);
29
- const client = loadClient();
30
- const instance = await client.getDocument(args.instanceId);
31
- if (!instance) {
32
+ const targets = await resolveReadTargets(flags);
33
+ const hit = await findInstance({ targets, instanceId: args.instanceId, requestTag: 'show' });
34
+ if (!hit) {
32
35
  this.error(`${logSymbols.error} no instance with id "${args.instanceId}"`, { exit: 1 });
33
36
  }
37
+ const { instance } = hit;
34
38
  this.log(instanceHeader(instance));
35
39
  this.log('');
36
40
  for (const line of describeInstance(instance, {
@@ -40,19 +44,14 @@ export default class Show extends Command {
40
44
  }
41
45
  }
42
46
  }
43
- const HEADER_PAD = 9; // "Completed" — the longest label in the block
44
- /** The summary block at the top of `show` (and reused by `diagnose` /
45
- * `fire-action`): a bold workflow + id title, then aligned detail rows. */
47
+ const HEADER_PAD = 9;
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
51
  formatKeyValue({ key: 'Stage', value: instance.currentStage, padTo: HEADER_PAD }),
50
52
  formatKeyValue({ key: 'Started', value: formatTimestamp(instance.startedAt), padTo: HEADER_PAD }),
51
53
  ];
52
- // Aborted instances also carry a completedAt stamped at the abort, so the
53
- // abort line stands in for completion — showing a green "Completed" too would
54
- // misread as success.
55
- if (instance.abortedAt) {
54
+ if (terminalState(instance) === 'aborted' && instance.abortedAt !== undefined) {
56
55
  const reason = abortReason(instance);
57
56
  const tail = reason ? ` — ${reason}` : '';
58
57
  const value = styleText('yellow', `${formatTimestamp(instance.abortedAt)}${tail}`);
@@ -67,11 +66,6 @@ export function instanceHeader(instance) {
67
66
  rows.push(formatKeyValue({ key: 'Tag', value: instance.tag, padTo: HEADER_PAD }));
68
67
  return [title, ...rows].join('\n');
69
68
  }
70
- /**
71
- * The per-instance body lines: every stage with its activities, any pending
72
- * effects, and (optionally) the history log. Pure so it can be asserted
73
- * without driving the oclif command.
74
- */
75
69
  export function describeInstance(instance, options) {
76
70
  const lines = [sectionHeader('Stages')];
77
71
  for (const stage of instance.stages) {
@@ -79,7 +73,7 @@ export function describeInstance(instance, options) {
79
73
  ? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
80
74
  : styleText('cyan', ' (current)');
81
75
  lines.push(` • ${stage.name}${marker}`);
82
- for (const activity of stage.activities) {
76
+ for (const activity of stage.activities ?? []) {
83
77
  lines.push(` ${activityIcon[activity.status]} ${activity.name} ${styleText('dim', `[${activity.status}]`)}`);
84
78
  }
85
79
  }
@@ -92,7 +86,7 @@ export function describeInstance(instance, options) {
92
86
  if (options.includeHistory) {
93
87
  lines.push('', sectionHeader('History'));
94
88
  for (const entry of instance.history) {
95
- lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${entry._type}`);
89
+ lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${displayTitle(entry._type)}`);
96
90
  }
97
91
  }
98
92
  return lines;
@@ -0,0 +1,56 @@
1
+ import { type FieldEntry, type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance, type WorkflowLifecycle } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ import { type EngineScope } from '../lib/operation-args.ts';
4
+ export default class Start extends WorkflowCommand {
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
+ version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ field: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
14
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
18
+ /**
19
+ * Why a definition can't be started standalone, or `undefined` when it can.
20
+ * Advisory, mirroring the Startable column in `definition list` — the engine
21
+ * itself does not refuse ({@link isStartableDefinition}), but the CLI has no
22
+ * way to supply the parent context a spawn-only child expects.
23
+ */
24
+ export declare function startRefusal(definition: {
25
+ lifecycle?: WorkflowLifecycle | undefined;
26
+ }): string | undefined;
27
+ /**
28
+ * Type each `--field name=value` against the workflow's declared field
29
+ * entries — the engine takes typed {@link InitialFieldValue}s, and a value's
30
+ * type IS its declared entry's kind. Only `input`-sourced entries read
31
+ * caller values (the engine silently ignores the rest), so anything else
32
+ * fails here, naming the fields that ARE settable; value validation stays
33
+ * in the engine.
34
+ */
35
+ export declare function buildInitialFields({ declared, values, }: {
36
+ declared: Pick<FieldEntry, 'type' | 'name' | 'initialValue'>[];
37
+ values: Record<string, unknown>;
38
+ }): InitialFieldValue[];
39
+ /**
40
+ * The shared engine args ({@link baseEngineArgs}) plus the start-specific
41
+ * fields. Optionals spread conditionally because the engine's optional args
42
+ * reject an explicit `undefined` under `exactOptionalPropertyTypes`.
43
+ */
44
+ export declare function buildStartArgs({ scope, name, version, initialFields, }: {
45
+ scope: EngineScope;
46
+ name: string;
47
+ version: number | undefined;
48
+ initialFields: InitialFieldValue[];
49
+ }): StartInstanceArgs & EngineScope;
50
+ /** The success spinner message. Pure so the in-flight / immediately-terminal
51
+ * permutations can be asserted without driving a real start. */
52
+ export declare function startReport(instance: Pick<WorkflowInstance, '_id' | 'currentStage' | 'completedAt'>): string;
53
+ /** The `--json` payload. Keys follow the CLI's `--json` vocabulary (matching
54
+ * `fire-action`): `instanceId` carries the document `_id`, `version` the
55
+ * pinned definition version; the rest mirror the instance fields. */
56
+ export declare function startResultJson(instance: Pick<WorkflowInstance, '_id' | 'definition' | 'pinnedVersion' | 'currentStage' | 'completedAt'>): Record<string, unknown>;
@@ -0,0 +1,133 @@
1
+ import { styleText } from 'node:util';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { isStartableDefinition, workflow, } from '@sanity/workflow-engine';
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
+ import { resolveContext } from "../lib/context.js";
6
+ import { fetchDeployedDefinition } from "../lib/definitions.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 { runWriteVerb } from "../lib/ops-report.js";
11
+ import { parseParams } from "../lib/params.js";
12
+ export default class Start extends WorkflowCommand {
13
+ static description = 'Start a workflow instance from a deployed definition. Supply values for the ' +
14
+ "workflow's input-sourced fields with --field (e.g. the subject document ref).";
15
+ static examples = [
16
+ '<%= config.bin %> start productLaunch',
17
+ '<%= config.bin %> start article-review --field subject=\'{"id":"dataset:proj:ds:article-1","type":"article"}\'',
18
+ '<%= config.bin %> start productLaunch --version 2 --tag prod',
19
+ ];
20
+ static args = {
21
+ name: Args.string({ required: true, description: 'Workflow definition name.' }),
22
+ };
23
+ static flags = {
24
+ ...tagFlags,
25
+ version: Flags.integer({
26
+ description: 'Definition version to start from (default: highest deployed).',
27
+ }),
28
+ field: Flags.string({
29
+ description: 'Initial value for a declared input-sourced field, as name=value (repeatable). Values ' +
30
+ 'are JSON-parsed, falling back to a string; ref kinds take a JSON object with a GDR ' +
31
+ '`id` and doc `type`.',
32
+ multiple: true,
33
+ default: [],
34
+ }),
35
+ ...jsonFlags,
36
+ };
37
+ async run() {
38
+ const { args, flags } = await this.parse(Start);
39
+ const { deployment, client } = await resolveContext(flags);
40
+ const values = failOnThrow('Invalid --field:', () => parseParams(flags.field));
41
+ const initialFields = await resolveStartInputs({
42
+ client,
43
+ name: args.name,
44
+ tag: deployment.tag,
45
+ version: flags.version,
46
+ values,
47
+ });
48
+ const startArgs = {
49
+ client,
50
+ ...buildStartArgs({
51
+ scope: deployment,
52
+ name: args.name,
53
+ version: flags.version,
54
+ initialFields,
55
+ }),
56
+ };
57
+ if (flags.json) {
58
+ const result = await workflow
59
+ .startInstance(startArgs)
60
+ .catch((error) => fail('start error:', failureDetail(error)));
61
+ this.log(JSON.stringify(startResultJson(result.instance), null, 2));
62
+ return;
63
+ }
64
+ await runWriteVerb({
65
+ startLabel: `Starting ${args.name}…`,
66
+ failLabel: 'Start rejected',
67
+ failHeadline: 'start error:',
68
+ run: () => workflow.startInstance(startArgs),
69
+ report: (result) => ({ changed: true, message: startReport(result.instance) }),
70
+ log: (line) => this.log(line),
71
+ });
72
+ }
73
+ }
74
+ async function resolveStartInputs({ client, name, tag, version, values, }) {
75
+ const deployed = await fetchDeployedDefinition({ client, name, tag, version });
76
+ if (deployed === undefined) {
77
+ fail(`No deployed definition "${name}".`);
78
+ }
79
+ const refusal = startRefusal(deployed);
80
+ if (refusal !== undefined) {
81
+ fail('start error:', refusal);
82
+ }
83
+ return failOnThrow('start usage error:', () => buildInitialFields({ declared: deployed.fields ?? [], values }));
84
+ }
85
+ export function startRefusal(definition) {
86
+ if (isStartableDefinition(definition)) {
87
+ return undefined;
88
+ }
89
+ return "definition is spawn-only (lifecycle: 'child') — instances of it are spawned by a parent workflow's activity, not started standalone";
90
+ }
91
+ export function buildInitialFields({ declared, values, }) {
92
+ const settable = declared.filter((f) => f.initialValue?.type === 'input');
93
+ const settableNames = settable.map((f) => f.name).join(', ');
94
+ return Object.entries(values).map(([name, value]) => {
95
+ const entry = declared.find((f) => f.name === name);
96
+ if (entry === undefined) {
97
+ throw new Error(settable.length > 0
98
+ ? `workflow declares no field "${name}" — input fields: ${settableNames}`
99
+ : `workflow declares no field "${name}" — it declares no input fields at all`);
100
+ }
101
+ if (entry.initialValue?.type !== 'input') {
102
+ throw new Error(`field "${name}" is not input-sourced — the engine would silently ignore the value. ` +
103
+ (settable.length > 0
104
+ ? `Input fields: ${settableNames}`
105
+ : 'This workflow has no input fields.'));
106
+ }
107
+ return { type: entry.type, name, value };
108
+ });
109
+ }
110
+ export function buildStartArgs({ scope, name, version, initialFields, }) {
111
+ return {
112
+ ...baseEngineArgs(scope),
113
+ definition: name,
114
+ ...(version !== undefined ? { version } : {}),
115
+ ...(initialFields.length > 0 ? { initialFields } : {}),
116
+ };
117
+ }
118
+ export function startReport(instance) {
119
+ const id = styleText('bold', instance._id);
120
+ const stage = styleText('bold', instance.currentStage);
121
+ return instance.completedAt !== undefined
122
+ ? `Started ${id} — completed immediately at ${stage}`
123
+ : `Started ${id} — now at ${stage}`;
124
+ }
125
+ export function startResultJson(instance) {
126
+ return {
127
+ instanceId: instance._id,
128
+ definition: instance.definition,
129
+ version: instance.pinnedVersion,
130
+ currentStage: instance.currentStage,
131
+ ...(instance.completedAt !== undefined ? { completedAt: instance.completedAt } : {}),
132
+ };
133
+ }
@@ -1,10 +1,13 @@
1
- import { Command } from '@oclif/core';
2
- export default class Tail extends Command {
1
+ import { WorkflowCommand } from '../lib/base-command.ts';
2
+ export default class Tail extends WorkflowCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static args: {
6
6
  instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
+ static flags: {
9
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
8
11
  run(): Promise<void>;
9
12
  /**
10
13
  * Fetch the current instance state, print history entries past the