@sanity/workflow-cli 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +71 -19
  3. package/bin/run.js +0 -4
  4. package/dist/commands/abort.d.ts +4 -6
  5. package/dist/commands/abort.js +13 -24
  6. package/dist/commands/definition/delete.d.ts +2 -2
  7. package/dist/commands/definition/delete.js +14 -27
  8. package/dist/commands/definition/diff.d.ts +2 -2
  9. package/dist/commands/definition/diff.js +3 -12
  10. package/dist/commands/definition/list.d.ts +3 -4
  11. package/dist/commands/definition/list.js +41 -47
  12. package/dist/commands/definition/show.d.ts +35 -3
  13. package/dist/commands/definition/show.js +44 -24
  14. package/dist/commands/deploy.d.ts +11 -5
  15. package/dist/commands/deploy.js +54 -57
  16. package/dist/commands/diagnose.d.ts +22 -4
  17. package/dist/commands/diagnose.js +60 -56
  18. package/dist/commands/fire-action.d.ts +2 -4
  19. package/dist/commands/fire-action.js +39 -62
  20. package/dist/commands/list.d.ts +18 -10
  21. package/dist/commands/list.js +77 -69
  22. package/dist/commands/reset-activity.js +0 -1
  23. package/dist/commands/set-stage.d.ts +2 -2
  24. package/dist/commands/set-stage.js +14 -30
  25. package/dist/commands/show.d.ts +2 -2
  26. package/dist/commands/show.js +12 -24
  27. package/dist/commands/start.d.ts +4 -28
  28. package/dist/commands/start.js +19 -80
  29. package/dist/commands/tail.d.ts +2 -2
  30. package/dist/commands/tail.js +34 -36
  31. package/dist/hooks/finally/telemetry.d.ts +8 -0
  32. package/dist/hooks/finally/telemetry.js +13 -0
  33. package/dist/hooks/prerun/telemetry.d.ts +8 -0
  34. package/dist/hooks/prerun/telemetry.js +9 -0
  35. package/dist/index.js +0 -3
  36. package/dist/lib/base-command.d.ts +14 -0
  37. package/dist/lib/base-command.js +10 -0
  38. package/dist/lib/client.js +13 -30
  39. package/dist/lib/context.d.ts +49 -13
  40. package/dist/lib/context.js +49 -48
  41. package/dist/lib/definitions.d.ts +10 -0
  42. package/dist/lib/definitions.js +11 -24
  43. package/dist/lib/diff.d.ts +5 -2
  44. package/dist/lib/diff.js +61 -27
  45. package/dist/lib/env.d.ts +4 -0
  46. package/dist/lib/env.js +4 -3
  47. package/dist/lib/fail.d.ts +19 -6
  48. package/dist/lib/fail.js +26 -26
  49. package/dist/lib/flags.d.ts +0 -7
  50. package/dist/lib/flags.js +0 -17
  51. package/dist/lib/load-config.d.ts +11 -7
  52. package/dist/lib/load-config.js +40 -20
  53. package/dist/lib/operation-args.d.ts +14 -23
  54. package/dist/lib/operation-args.js +6 -38
  55. package/dist/lib/ops-report.d.ts +18 -10
  56. package/dist/lib/ops-report.js +20 -16
  57. package/dist/lib/params.js +0 -8
  58. package/dist/lib/read-fanout.d.ts +22 -0
  59. package/dist/lib/read-fanout.js +28 -0
  60. package/dist/lib/select-deployment.js +0 -21
  61. package/dist/lib/share-definitions.d.ts +120 -0
  62. package/dist/lib/share-definitions.js +187 -0
  63. package/dist/lib/stub.js +0 -7
  64. package/dist/lib/telemetry-setup.d.ts +70 -0
  65. package/dist/lib/telemetry-setup.js +99 -0
  66. package/dist/lib/telemetry.d.ts +131 -0
  67. package/dist/lib/telemetry.js +94 -0
  68. package/dist/lib/ui.d.ts +33 -1
  69. package/dist/lib/ui.js +32 -22
  70. package/oclif.manifest.json +21 -52
  71. package/package.json +14 -8
@@ -1,27 +1,34 @@
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, documentPrefilter, inFlightFilter, instanceWatchesDocument, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
5
3
  import logSymbols from 'log-symbols';
6
- import { resolveReadContext } from "../lib/context.js";
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
+ import { resolveReadTargets } from "../lib/context.js";
6
+ import { failOnThrow } from "../lib/fail.js";
7
7
  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
- */
8
+ import { runReadAcrossTargets } from "../lib/read-fanout.js";
9
+ import { clipToLimit, formatAge, formatTable } from "../lib/ui.js";
14
10
  export function buildListQuery(flags) {
15
11
  const filters = [`_type == "${WORKFLOW_INSTANCE_TYPE}"`];
16
- if (flags['in-flight'])
17
- filters.push('!defined(completedAt)');
18
- if (flags.definition)
12
+ const params = {};
13
+ if (!flags['include-completed'])
14
+ filters.push(inFlightFilter());
15
+ if (flags.definition) {
19
16
  filters.push('definition == $definition');
20
- // Cheap approximation of --failed: any activity in any stage with status "failed".
17
+ params['definition'] = flags.definition;
18
+ }
21
19
  if (flags.failed)
22
20
  filters.push('count(stages[].activities[status == "failed"]) > 0');
23
- if (flags.tag)
21
+ if (flags.tag) {
24
22
  filters.push(tagScopeFilter());
23
+ params['tag'] = flags.tag;
24
+ }
25
+ if (flags.document) {
26
+ const armParams = {};
27
+ filters.push(`(${documentPrefilter([flags.document], armParams)})`);
28
+ Object.assign(params, armParams);
29
+ return { groq: `*[${filters.join(' && ')}] | order(lastChangedAt desc)`, params };
30
+ }
31
+ params['limit'] = flags.limit + 1;
25
32
  const groq = `*[${filters.join(' && ')}] | order(lastChangedAt desc) [0...$limit]{
26
33
  _id,
27
34
  definition,
@@ -31,50 +38,34 @@ export function buildListQuery(flags) {
31
38
  abortedAt,
32
39
  lastChangedAt
33
40
  }`;
34
- const params = { limit: flags.limit };
35
- if (flags.definition)
36
- params['definition'] = flags.definition;
37
- if (flags.tag)
38
- params['tag'] = flags.tag;
39
41
  return { groq, params };
40
42
  }
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
43
  export function instanceRow(r) {
52
44
  return {
53
45
  _id: r._id,
54
46
  definition: r.definition,
55
47
  tag: r.tag,
56
48
  currentStage: r.currentStage,
57
- status: deriveStatus(r),
49
+ status: terminalState({
50
+ ...(r.completedAt != null ? { completedAt: r.completedAt } : {}),
51
+ ...(r.abortedAt != null ? { abortedAt: r.abortedAt } : {}),
52
+ }),
58
53
  lastChangedAt: r.lastChangedAt,
59
54
  };
60
55
  }
61
- const STATUS_COLOR = {
62
- completed: 'green',
63
- aborted: 'dim',
64
- 'in-flight': 'cyan',
65
- };
66
- export default class List extends Command {
67
- static description = 'List workflow instances in the configured dataset.';
56
+ export default class List extends WorkflowCommand {
57
+ static description = 'List workflow instances in the configured dataset (in-flight by default).';
68
58
  static examples = [
69
59
  '<%= config.bin %> list',
70
- '<%= config.bin %> list --in-flight',
60
+ '<%= config.bin %> list --include-completed',
71
61
  '<%= config.bin %> list --definition productLaunch',
62
+ '<%= config.bin %> list --document dataset:proj:ds:article-1',
72
63
  '<%= config.bin %> list --tag prod',
73
64
  ];
74
65
  static flags = {
75
66
  ...tagFlags,
76
- 'in-flight': Flags.boolean({
77
- description: 'Only instances that have not completed.',
67
+ 'include-completed': Flags.boolean({
68
+ description: 'Include completed/aborted instances (default: in-flight only).',
78
69
  default: false,
79
70
  }),
80
71
  failed: Flags.boolean({
@@ -84,6 +75,10 @@ export default class List extends Command {
84
75
  definition: Flags.string({
85
76
  description: "Only instances of this workflow definition (its `name`; the instance's `definition` field).",
86
77
  }),
78
+ document: Flags.string({
79
+ description: 'Only instances that reference this document (resource-qualified GDR URI, ' +
80
+ 'e.g. "dataset:proj:ds:article-1").',
81
+ }),
87
82
  limit: Flags.integer({
88
83
  description: 'Maximum rows to return.',
89
84
  default: 50,
@@ -91,34 +86,47 @@ export default class List extends Command {
91
86
  };
92
87
  async run() {
93
88
  const { flags } = await this.parse(List);
94
- const { client } = await resolveReadContext(flags);
95
- const { groq, params } = buildListQuery(flags);
96
- const rows = await client.fetch(groq, params);
97
- if (rows.length === 0) {
98
- this.log(`${logSymbols.info} no instances match`);
99
- return;
100
- }
101
- const table = new Table({
102
- columns: [
103
- { name: '_id', title: 'Instance', alignment: 'left' },
104
- { name: 'definition', title: 'Workflow', alignment: 'left' },
105
- { name: 'tag', title: 'Tag', alignment: 'left' },
106
- { name: 'currentStage', title: 'Stage', alignment: 'left' },
107
- { name: 'status', title: 'Status', alignment: 'left' },
108
- { name: 'updated', title: 'Updated', alignment: 'left' },
109
- ],
89
+ const targets = await resolveReadTargets(flags);
90
+ const { groq, params } = failOnThrow('Invalid --document:', () => buildListQuery(flags));
91
+ const document = flags.document;
92
+ const failures = await runReadAcrossTargets({
93
+ targets,
94
+ log: (line) => this.log(line),
95
+ run: async ({ client }) => {
96
+ const fetched = await fetchRows({ client, groq, params, document });
97
+ if (fetched.length === 0) {
98
+ this.log(`${logSymbols.info} no instances match`);
99
+ return;
100
+ }
101
+ const { rows, note } = clipToLimit(fetched, flags.limit);
102
+ const lines = formatTable(['instance', 'workflow', 'tag', 'stage', 'status', 'updated'], rows.map((r) => {
103
+ const row = instanceRow(r);
104
+ return [
105
+ row._id,
106
+ row.definition,
107
+ row.tag,
108
+ row.currentStage,
109
+ row.status,
110
+ formatAge(row.lastChangedAt),
111
+ ];
112
+ }));
113
+ for (const line of lines) {
114
+ this.log(line);
115
+ }
116
+ if (note) {
117
+ this.log(note);
118
+ }
119
+ },
110
120
  });
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
- });
121
+ if (failures.length > 0) {
122
+ this.exit(1);
121
123
  }
122
- table.printTable();
123
124
  }
124
125
  }
126
+ async function fetchRows({ client, groq, params, document, }) {
127
+ if (document === undefined) {
128
+ return client.fetch(groq, params, { tag: 'list' });
129
+ }
130
+ const candidates = await client.fetch(groq, params, { tag: 'list' });
131
+ return candidates.filter((instance) => instanceWatchesDocument(instance, document));
132
+ }
@@ -1,7 +1,6 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { StubCommand } from "../lib/stub.js";
3
3
  export default class ResetActivity extends StubCommand {
4
- // Stubbed, so kept off the help + published surface until it's wired.
5
4
  static hidden = true;
6
5
  static description = 'Reset a failed activity on an in-flight instance — back to pending (re-run) or to skipped (bypass).';
7
6
  static args = {
@@ -1,8 +1,8 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type SetStageArgs, type WorkflowInstance } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
3
  import { type EngineScope } from '../lib/operation-args.ts';
4
4
  import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
5
- export default class SetStage extends Command {
5
+ export default class SetStage extends WorkflowCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static args: {
@@ -1,13 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
2
+ import { Args, Flags } from '@oclif/core';
3
3
  import { workflow } from '@sanity/workflow-engine';
4
- import ora from 'ora';
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
5
  import { resolveInstanceContext } from "../lib/context.js";
6
- import { errorMessage, fail } from "../lib/fail.js";
7
6
  import { tagFlags } from "../lib/flags.js";
8
7
  import { buildOperationArgs } from "../lib/operation-args.js";
9
- import { emitWriteReport, opsAppliedLines, } from "../lib/ops-report.js";
10
- export default class SetStage extends Command {
8
+ import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../lib/ops-report.js";
9
+ export default class SetStage extends WorkflowCommand {
11
10
  static description = "Force an instance into a stage, regardless of its declared transitions and filters — the engine's setStage admin override. The target stage's enter lifecycle still runs (auto-activities start, stage guards reconcile), and the post-move cascade can immediately auto-transition the instance onward.";
12
11
  static examples = [
13
12
  '<%= config.bin %> set-stage wf-instance.abc123 --to ready',
@@ -32,9 +31,11 @@ export default class SetStage extends Command {
32
31
  async run() {
33
32
  const { args, flags } = await this.parse(SetStage);
34
33
  const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
35
- const spinner = ora(`Setting ${args.instanceId} → ${flags.to}…`).start();
36
- try {
37
- const result = await workflow.setStage({
34
+ await runWriteVerb({
35
+ startLabel: `Setting ${args.instanceId} → ${flags.to}…`,
36
+ failLabel: 'Stage move rejected',
37
+ failHeadline: 'set-stage error:',
38
+ run: () => workflow.setStage({
38
39
  client,
39
40
  ...buildSetStageArgs({
40
41
  scope,
@@ -42,44 +43,27 @@ export default class SetStage extends Command {
42
43
  targetStage: flags.to,
43
44
  reason: flags.reason,
44
45
  }),
45
- });
46
- emitWriteReport({
47
- spinner,
48
- report: setStageReport(result, flags.to),
49
- log: (line) => this.log(line),
50
- });
51
- }
52
- catch (error) {
53
- spinner.fail('Stage move rejected');
54
- fail('set-stage error:', errorMessage(error));
55
- }
46
+ }),
47
+ report: (result) => setStageReport(result, flags.to),
48
+ log: (line) => this.log(line),
49
+ });
56
50
  }
57
51
  }
58
- /** The shared operation args ({@link buildOperationArgs}) plus the
59
- * setStage-specific target. */
60
52
  export function buildSetStageArgs({ scope, instanceId, targetStage, reason, }) {
61
53
  return { ...buildOperationArgs({ scope, instanceId, reason }), targetStage };
62
54
  }
63
- /**
64
- * Turn a `setStage` result into the spinner message + ops block. Pure so
65
- * the changed / terminal / already-at-target / cascaded / ops permutations
66
- * can be asserted without driving a real move.
67
- */
68
55
  export function setStageReport(result, to) {
69
56
  const stage = styleText('bold', result.instance.currentStage);
70
57
  if (!result.changed) {
71
- // The engine no-ops in two distinct cases: a terminal instance never
72
- // moves again, and a same-stage move has nothing to do.
73
58
  const { completedAt } = result.instance;
74
59
  const message = completedAt !== undefined
75
60
  ? `setStage changed nothing — instance is terminal (at ${stage} since ${completedAt})`
76
61
  : `setStage changed nothing — instance is already at ${stage}`;
77
62
  return { changed: false, message, opsLines: [] };
78
63
  }
79
- const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
80
64
  return {
81
65
  changed: true,
82
- message: `Now at ${stage} (was → ${to}${tail})`,
66
+ message: `Now at ${stage} (was → ${to}${cascadeTail(result.cascaded)})`,
83
67
  opsLines: opsAppliedLines(result.ranOps),
84
68
  };
85
69
  }
@@ -1,6 +1,6 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type WorkflowInstance } from '@sanity/workflow-engine';
3
- export default class Show extends Command {
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ export default class Show extends WorkflowCommand {
4
4
  static description: string;
5
5
  static examples: string[];
6
6
  static args: {
@@ -1,11 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
3
- import { abortReason } from '@sanity/workflow-engine';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { abortReason, displayTitle, terminalState, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { resolveReadContext } from "../lib/context.js";
5
+ import { WorkflowCommand } from "../lib/base-command.js";
6
+ import { findInstance, resolveReadTargets } from "../lib/context.js";
6
7
  import { tagFlags } from "../lib/flags.js";
7
8
  import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
8
- export default class Show extends Command {
9
+ export default class Show extends WorkflowCommand {
9
10
  static description = 'Show the state, activities, and effects of a workflow instance.';
10
11
  static examples = [
11
12
  '<%= config.bin %> show wf-instance.abc123',
@@ -28,11 +29,12 @@ export default class Show extends Command {
28
29
  };
29
30
  async run() {
30
31
  const { args, flags } = await this.parse(Show);
31
- const { client } = await resolveReadContext(flags);
32
- const instance = await client.getDocument(args.instanceId);
33
- if (!instance) {
32
+ const targets = await resolveReadTargets(flags);
33
+ const hit = await findInstance({ targets, instanceId: args.instanceId, requestTag: 'show' });
34
+ if (!hit) {
34
35
  this.error(`${logSymbols.error} no instance with id "${args.instanceId}"`, { exit: 1 });
35
36
  }
37
+ const { instance } = hit;
36
38
  this.log(instanceHeader(instance));
37
39
  this.log('');
38
40
  for (const line of describeInstance(instance, {
@@ -42,19 +44,14 @@ export default class Show extends Command {
42
44
  }
43
45
  }
44
46
  }
45
- const HEADER_PAD = 9; // "Completed" — the longest label in the block
46
- /** The summary block at the top of `show` (and reused by `diagnose` /
47
- * `fire-action`): a bold workflow + id title, then aligned detail rows. */
47
+ const HEADER_PAD = 9;
48
48
  export function instanceHeader(instance) {
49
49
  const title = `${styleText('bold', `${instance.definition} v${instance.pinnedVersion}`)} ${styleText('cyan', instance._id)}`;
50
50
  const rows = [
51
51
  formatKeyValue({ key: 'Stage', value: instance.currentStage, padTo: HEADER_PAD }),
52
52
  formatKeyValue({ key: 'Started', value: formatTimestamp(instance.startedAt), padTo: HEADER_PAD }),
53
53
  ];
54
- // Aborted instances also carry a completedAt stamped at the abort, so the
55
- // abort line stands in for completion — showing a green "Completed" too would
56
- // misread as success.
57
- if (instance.abortedAt) {
54
+ if (terminalState(instance) === 'aborted' && instance.abortedAt !== undefined) {
58
55
  const reason = abortReason(instance);
59
56
  const tail = reason ? ` — ${reason}` : '';
60
57
  const value = styleText('yellow', `${formatTimestamp(instance.abortedAt)}${tail}`);
@@ -69,11 +66,6 @@ export function instanceHeader(instance) {
69
66
  rows.push(formatKeyValue({ key: 'Tag', value: instance.tag, padTo: HEADER_PAD }));
70
67
  return [title, ...rows].join('\n');
71
68
  }
72
- /**
73
- * The per-instance body lines: every stage with its activities, any pending
74
- * effects, and (optionally) the history log. Pure so it can be asserted
75
- * without driving the oclif command.
76
- */
77
69
  export function describeInstance(instance, options) {
78
70
  const lines = [sectionHeader('Stages')];
79
71
  for (const stage of instance.stages) {
@@ -81,10 +73,6 @@ export function describeInstance(instance, options) {
81
73
  ? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
82
74
  : styleText('cyan', ' (current)');
83
75
  lines.push(` • ${stage.name}${marker}`);
84
- // A persisted stage may carry no activities array — an activity-less stage,
85
- // or an older-shape document — so treat an absent list as none rather than
86
- // crash. This renders whatever is in the lake, not only freshly-stamped
87
- // instances; it does not reconstruct activity state from history.
88
76
  for (const activity of stage.activities ?? []) {
89
77
  lines.push(` ${activityIcon[activity.status]} ${activity.name} ${styleText('dim', `[${activity.status}]`)}`);
90
78
  }
@@ -98,7 +86,7 @@ export function describeInstance(instance, options) {
98
86
  if (options.includeHistory) {
99
87
  lines.push('', sectionHeader('History'));
100
88
  for (const entry of instance.history) {
101
- lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${entry._type}`);
89
+ lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${displayTitle(entry._type)}`);
102
90
  }
103
91
  }
104
92
  return lines;
@@ -1,7 +1,7 @@
1
- import { Command } from '@oclif/core';
2
- import { type Actor, type FieldEntry, type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance, type WorkflowLifecycle } from '@sanity/workflow-engine';
1
+ import { type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
3
  import { type EngineScope } from '../lib/operation-args.ts';
4
- export default class Start extends Command {
4
+ export default class Start extends WorkflowCommand {
5
5
  static description: string;
6
6
  static examples: string[];
7
7
  static args: {
@@ -9,46 +9,22 @@ export default class Start extends Command {
9
9
  };
10
10
  static flags: {
11
11
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
- as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
- 'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
14
12
  version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
13
  field: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
16
14
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
15
  };
18
16
  run(): Promise<void>;
19
17
  }
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
18
  /**
42
19
  * The shared engine args ({@link baseEngineArgs}) plus the start-specific
43
20
  * fields. Optionals spread conditionally because the engine's optional args
44
21
  * reject an explicit `undefined` under `exactOptionalPropertyTypes`.
45
22
  */
46
- export declare function buildStartArgs({ scope, name, version, initialFields, actor, }: {
23
+ export declare function buildStartArgs({ scope, name, version, initialFields, }: {
47
24
  scope: EngineScope;
48
25
  name: string;
49
26
  version: number | undefined;
50
27
  initialFields: InitialFieldValue[];
51
- actor: Actor;
52
28
  }): StartInstanceArgs & EngineScope;
53
29
  /** The success spinner message. Pure so the in-flight / immediately-terminal
54
30
  * permutations can be asserted without driving a real start. */
@@ -1,20 +1,20 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
3
- import { isStartableDefinition, workflow, } from '@sanity/workflow-engine';
4
- import ora from 'ora';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { buildInitialFields, startRefusal, workflow, } from '@sanity/workflow-engine';
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
5
  import { resolveContext } from "../lib/context.js";
6
6
  import { fetchDeployedDefinition } from "../lib/definitions.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";
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";
10
11
  import { parseParams } from "../lib/params.js";
11
- export default class Start extends Command {
12
+ export default class Start extends WorkflowCommand {
12
13
  static description = 'Start a workflow instance from a deployed definition. Supply values for the ' +
13
14
  "workflow's input-sourced fields with --field (e.g. the subject document ref).";
14
15
  static examples = [
15
16
  '<%= config.bin %> start productLaunch',
16
17
  '<%= config.bin %> start article-review --field subject=\'{"id":"dataset:proj:ds:article-1","type":"article"}\'',
17
- '<%= config.bin %> start article-review --as user.xyz --as-role editor',
18
18
  '<%= config.bin %> start productLaunch --version 2 --tag prod',
19
19
  ];
20
20
  static args = {
@@ -32,13 +32,11 @@ export default class Start extends Command {
32
32
  multiple: true,
33
33
  default: [],
34
34
  }),
35
- ...actorFlags,
36
35
  ...jsonFlags,
37
36
  };
38
37
  async run() {
39
38
  const { args, flags } = await this.parse(Start);
40
39
  const { deployment, client } = await resolveContext(flags);
41
- const actor = failOnThrow('start usage error:', () => resolveActor(flags.as, flags['as-role']));
42
40
  const values = failOnThrow('Invalid --field:', () => parseParams(flags.field));
43
41
  const initialFields = await resolveStartInputs({
44
42
  client,
@@ -54,33 +52,25 @@ export default class Start extends Command {
54
52
  name: args.name,
55
53
  version: flags.version,
56
54
  initialFields,
57
- actor,
58
55
  }),
59
56
  };
60
57
  if (flags.json) {
61
58
  const result = await workflow
62
59
  .startInstance(startArgs)
63
- .catch((error) => fail('start error:', errorMessage(error)));
60
+ .catch((error) => fail('start error:', failureDetail(error)));
64
61
  this.log(JSON.stringify(startResultJson(result.instance), null, 2));
65
62
  return;
66
63
  }
67
- const spinner = ora(`Starting ${args.name}…`).start();
68
- try {
69
- const result = await workflow.startInstance(startArgs);
70
- spinner.succeed(startReport(result.instance));
71
- }
72
- catch (error) {
73
- spinner.fail('Start rejected');
74
- fail('start error:', errorMessage(error));
75
- }
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
+ });
76
72
  }
77
73
  }
78
- /**
79
- * Everything `start` must resolve before the engine call: fetch the deployed
80
- * definition (failing when nothing is deployed under the name), refuse
81
- * non-startable ones, and type the operator's `--field` values against the
82
- * declared entries.
83
- */
84
74
  async function resolveStartInputs({ client, name, tag, version, values, }) {
85
75
  const deployed = await fetchDeployedDefinition({ client, name, tag, version });
86
76
  if (deployed === undefined) {
@@ -92,62 +82,14 @@ async function resolveStartInputs({ client, name, tag, version, values, }) {
92
82
  }
93
83
  return failOnThrow('start usage error:', () => buildInitialFields({ declared: deployed.fields ?? [], values }));
94
84
  }
95
- /**
96
- * Why a definition can't be started standalone, or `undefined` when it can.
97
- * Advisory, mirroring the Startable column in `definition list` — the engine
98
- * itself does not refuse ({@link isStartableDefinition}), but the CLI has no
99
- * way to supply the parent context a spawn-only child expects.
100
- */
101
- export function startRefusal(definition) {
102
- if (isStartableDefinition(definition)) {
103
- return undefined;
104
- }
105
- return "definition is spawn-only (lifecycle: 'child') — instances of it are spawned by a parent workflow's activity, not started standalone";
106
- }
107
- /**
108
- * Type each `--field name=value` against the workflow's declared field
109
- * entries — the engine takes typed {@link InitialFieldValue}s, and a value's
110
- * type IS its declared entry's kind. Only `input`-sourced entries read
111
- * caller values (the engine silently ignores the rest), so anything else
112
- * fails here, naming the fields that ARE settable; value validation stays
113
- * in the engine.
114
- */
115
- export function buildInitialFields({ declared, values, }) {
116
- const settable = declared.filter((f) => f.initialValue?.type === 'input');
117
- const settableNames = settable.map((f) => f.name).join(', ');
118
- return Object.entries(values).map(([name, value]) => {
119
- const entry = declared.find((f) => f.name === name);
120
- if (entry === undefined) {
121
- throw new Error(settable.length > 0
122
- ? `workflow declares no field "${name}" — input fields: ${settableNames}`
123
- : `workflow declares no field "${name}" — it declares no input fields at all`);
124
- }
125
- if (entry.initialValue?.type !== 'input') {
126
- throw new Error(`field "${name}" is not input-sourced — the engine would silently ignore the value. ` +
127
- (settable.length > 0
128
- ? `Input fields: ${settableNames}`
129
- : 'This workflow has no input fields.'));
130
- }
131
- // Operator-supplied JSON; the engine validates the value against the
132
- // declared kind at start, so this cast is the CLI/engine boundary.
133
- return { type: entry.type, name, value };
134
- });
135
- }
136
- /**
137
- * The shared engine args ({@link baseEngineArgs}) plus the start-specific
138
- * fields. Optionals spread conditionally because the engine's optional args
139
- * reject an explicit `undefined` under `exactOptionalPropertyTypes`.
140
- */
141
- export function buildStartArgs({ scope, name, version, initialFields, actor, }) {
85
+ export function buildStartArgs({ scope, name, version, initialFields, }) {
142
86
  return {
143
- ...baseEngineArgs(scope, actor),
87
+ ...baseEngineArgs(scope),
144
88
  definition: name,
145
89
  ...(version !== undefined ? { version } : {}),
146
90
  ...(initialFields.length > 0 ? { initialFields } : {}),
147
91
  };
148
92
  }
149
- /** The success spinner message. Pure so the in-flight / immediately-terminal
150
- * permutations can be asserted without driving a real start. */
151
93
  export function startReport(instance) {
152
94
  const id = styleText('bold', instance._id);
153
95
  const stage = styleText('bold', instance.currentStage);
@@ -155,9 +97,6 @@ export function startReport(instance) {
155
97
  ? `Started ${id} — completed immediately at ${stage}`
156
98
  : `Started ${id} — now at ${stage}`;
157
99
  }
158
- /** The `--json` payload. Keys follow the CLI's `--json` vocabulary (matching
159
- * `fire-action`): `instanceId` carries the document `_id`, `version` the
160
- * pinned definition version; the rest mirror the instance fields. */
161
100
  export function startResultJson(instance) {
162
101
  return {
163
102
  instanceId: instance._id,
@@ -1,5 +1,5 @@
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: {