@sanity/workflow-cli 0.11.0 → 0.13.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 (59) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +75 -46
  3. package/dist/commands/{abort.d.ts → editorial-workflows/abort.d.ts} +3 -2
  4. package/dist/commands/{abort.js → editorial-workflows/abort.js} +7 -6
  5. package/dist/commands/{definition → editorial-workflows/definition}/delete.d.ts +3 -2
  6. package/dist/commands/{definition → editorial-workflows/definition}/delete.js +6 -5
  7. package/dist/commands/{definition → editorial-workflows/definition}/diff.d.ts +2 -1
  8. package/dist/commands/{definition → editorial-workflows/definition}/diff.js +7 -6
  9. package/dist/commands/{definition → editorial-workflows/definition}/list.d.ts +5 -1
  10. package/dist/commands/{definition → editorial-workflows/definition}/list.js +27 -24
  11. package/dist/commands/{definition → editorial-workflows/definition}/show.d.ts +3 -2
  12. package/dist/commands/{definition → editorial-workflows/definition}/show.js +9 -8
  13. package/dist/commands/{deploy.d.ts → editorial-workflows/deploy.d.ts} +2 -1
  14. package/dist/commands/{deploy.js → editorial-workflows/deploy.js} +14 -13
  15. package/dist/commands/{diagnose.d.ts → editorial-workflows/diagnose.d.ts} +2 -1
  16. package/dist/commands/{diagnose.js → editorial-workflows/diagnose.js} +7 -6
  17. package/dist/commands/{fire-action.d.ts → editorial-workflows/fire-action.d.ts} +3 -2
  18. package/dist/commands/{fire-action.js → editorial-workflows/fire-action.js} +8 -7
  19. package/dist/commands/{list.d.ts → editorial-workflows/list.d.ts} +4 -1
  20. package/dist/commands/{list.js → editorial-workflows/list.js} +33 -28
  21. package/dist/commands/editorial-workflows/nuke.d.ts +12 -0
  22. package/dist/commands/editorial-workflows/nuke.js +77 -0
  23. package/dist/commands/{reset-activity.d.ts → editorial-workflows/reset-activity.d.ts} +2 -1
  24. package/dist/commands/{reset-activity.js → editorial-workflows/reset-activity.js} +2 -1
  25. package/dist/commands/{set-stage.d.ts → editorial-workflows/set-stage.d.ts} +4 -3
  26. package/dist/commands/{set-stage.js → editorial-workflows/set-stage.js} +6 -5
  27. package/dist/commands/{show.d.ts → editorial-workflows/show.d.ts} +2 -1
  28. package/dist/commands/{show.js → editorial-workflows/show.js} +40 -26
  29. package/dist/commands/{start.d.ts → editorial-workflows/start.d.ts} +20 -4
  30. package/dist/commands/{start.js → editorial-workflows/start.js} +58 -16
  31. package/dist/commands/{tail.d.ts → editorial-workflows/tail.d.ts} +2 -1
  32. package/dist/commands/{tail.js → editorial-workflows/tail.js} +10 -8
  33. package/dist/hooks/finally/telemetry.js +2 -2
  34. package/dist/hooks/prerun/telemetry.d.ts +4 -3
  35. package/dist/lib/base-command.d.ts +4 -6
  36. package/dist/lib/base-command.js +6 -0
  37. package/dist/lib/client.d.ts +8 -0
  38. package/dist/lib/client.js +1 -1
  39. package/dist/lib/context.d.ts +6 -2
  40. package/dist/lib/context.js +9 -7
  41. package/dist/lib/definitions.js +2 -2
  42. package/dist/lib/flags.d.ts +4 -3
  43. package/dist/lib/nuke.d.ts +89 -0
  44. package/dist/lib/nuke.js +111 -0
  45. package/dist/lib/operation-args.d.ts +3 -1
  46. package/dist/lib/ops-report.d.ts +2 -1
  47. package/dist/lib/prompt.d.ts +11 -0
  48. package/dist/lib/prompt.js +4 -0
  49. package/dist/lib/select-deployment.d.ts +12 -7
  50. package/dist/lib/select-deployment.js +26 -1
  51. package/dist/lib/share-definitions.d.ts +23 -28
  52. package/dist/lib/share-definitions.js +30 -40
  53. package/dist/lib/telemetry-setup.d.ts +2 -2
  54. package/dist/lib/telemetry.d.ts +18 -10
  55. package/dist/lib/telemetry.js +5 -2
  56. package/dist/lib/ui.d.ts +12 -0
  57. package/dist/lib/ui.js +9 -0
  58. package/oclif.manifest.json +138 -46
  59. package/package.json +11 -9
@@ -2,12 +2,12 @@ import { styleText } from 'node:util';
2
2
  import { Args } from '@oclif/core';
3
3
  import { diagnoseInputFromEvaluation, workflow, unsatisfiedTransitionSummaries, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { WorkflowCommand } from "../lib/base-command.js";
6
- import { resolveInstanceContext } from "../lib/context.js";
7
- import { fail, failureDetail } from "../lib/fail.js";
8
- import { jsonFlags, tagFlags } from "../lib/flags.js";
9
- import { baseEngineArgs } from "../lib/operation-args.js";
10
- import { formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
5
+ import { WorkflowCommand } from "../../lib/base-command.js";
6
+ import { resolveInstanceContext } from "../../lib/context.js";
7
+ import { fail, failureDetail } from "../../lib/fail.js";
8
+ import { jsonFlags, tagFlags } from "../../lib/flags.js";
9
+ import { baseEngineArgs } from "../../lib/operation-args.js";
10
+ import { formatTimestamp, sectionHeader, activityIcon } from "../../lib/ui.js";
11
11
  import { instanceHeader } from "./show.js";
12
12
  function formatAssignee(a) {
13
13
  return a.type === 'user' ? `user:${a.id}` : `role:${a.role}`;
@@ -212,6 +212,7 @@ export function renderDiagnosis({ diagnosis, input, remediations, explanations,
212
212
  return lines;
213
213
  }
214
214
  export default class Diagnose extends WorkflowCommand {
215
+ static aliases = ['diagnose'];
215
216
  static description = "Explain why a workflow instance is or isn't progressing, and what would unstick it.";
216
217
  static examples = [
217
218
  '<%= config.bin %> diagnose wf-instance.abc123',
@@ -1,6 +1,6 @@
1
1
  import { type AvailableAction } from '@sanity/workflow-engine';
2
- import { WorkflowCommand } from '../lib/base-command.ts';
3
- import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ import { type WriteOutcome, type WriteReport } from '../../lib/ops-report.ts';
4
4
  /**
5
5
  * The list-mode body: every action on the current stage's activities, marked
6
6
  * fireable or not (with the disabling reason), plus a hint showing the
@@ -30,6 +30,7 @@ export declare function fireActionReport({ result, activity, action, }: {
30
30
  action: string;
31
31
  }): WriteReport;
32
32
  export default class FireAction extends WorkflowCommand {
33
+ static aliases: string[];
33
34
  static description: string;
34
35
  static examples: string[];
35
36
  static args: {
@@ -2,13 +2,13 @@ import { styleText } from 'node:util';
2
2
  import { Args, Flags } from '@oclif/core';
3
3
  import { actionRendering, actionDisabledDetail, deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { WorkflowCommand } from "../lib/base-command.js";
6
- import { resolveInstanceContext } from "../lib/context.js";
7
- import { fail, failOnThrow, failureDetail } from "../lib/fail.js";
8
- import { jsonFlags, tagFlags } from "../lib/flags.js";
9
- import { baseEngineArgs } from "../lib/operation-args.js";
10
- import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../lib/ops-report.js";
11
- import { parseParams } from "../lib/params.js";
5
+ import { WorkflowCommand } from "../../lib/base-command.js";
6
+ import { resolveInstanceContext } from "../../lib/context.js";
7
+ import { fail, failOnThrow, failureDetail } from "../../lib/fail.js";
8
+ import { jsonFlags, tagFlags } from "../../lib/flags.js";
9
+ import { baseEngineArgs } from "../../lib/operation-args.js";
10
+ import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../../lib/ops-report.js";
11
+ import { parseParams } from "../../lib/params.js";
12
12
  import { instanceHeader } from "./show.js";
13
13
  function reasonText(reason) {
14
14
  if (reason.kind === 'mutation-guard-denied') {
@@ -82,6 +82,7 @@ export function fireActionReport({ result, activity, action, }) {
82
82
  };
83
83
  }
84
84
  export default class FireAction extends WorkflowCommand {
85
+ static aliases = ['fire-action'];
85
86
  static description = 'Fire an action on an instance to unstick a waiting activity — the write acts as the configured token. Omit --action to list what can be fired.';
86
87
  static examples = [
87
88
  '<%= config.bin %> fire-action wf-instance.abc123',
@@ -1,5 +1,5 @@
1
1
  import { type TerminalState } from '@sanity/workflow-engine';
2
- import { WorkflowCommand } from '../lib/base-command.ts';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
3
  interface InstanceRow {
4
4
  _id: string;
5
5
  definition: string;
@@ -8,6 +8,8 @@ interface InstanceRow {
8
8
  completedAt?: string | null;
9
9
  abortedAt?: string | null;
10
10
  lastChangedAt: string;
11
+ modelVersion?: number | null;
12
+ minReaderModel?: number | null;
11
13
  }
12
14
  interface ListFlags {
13
15
  'include-completed': boolean;
@@ -43,6 +45,7 @@ export declare function instanceRow(r: InstanceRow): {
43
45
  lastChangedAt: string;
44
46
  };
45
47
  export default class List extends WorkflowCommand {
48
+ static aliases: string[];
46
49
  static description: string;
47
50
  static examples: string[];
48
51
  static flags: {
@@ -1,12 +1,12 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { WORKFLOW_INSTANCE_TYPE, documentPrefilter, inFlightFilter, instanceWatchesDocument, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
2
+ import { WORKFLOW_INSTANCE_TYPE, assertReadableModel, documentPrefilter, inFlightFilter, instanceWatchesDocument, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
3
3
  import logSymbols from 'log-symbols';
4
- import { WorkflowCommand } from "../lib/base-command.js";
5
- import { resolveReadTargets } from "../lib/context.js";
6
- import { failOnThrow } from "../lib/fail.js";
7
- import { tagFlags } from "../lib/flags.js";
8
- import { runReadAcrossTargets } from "../lib/read-fanout.js";
9
- import { clipToLimit, formatAge, formatTable } from "../lib/ui.js";
4
+ import { WorkflowCommand } from "../../lib/base-command.js";
5
+ import { resolveReadTargets } from "../../lib/context.js";
6
+ import { failOnThrow } from "../../lib/fail.js";
7
+ import { tagFlags } from "../../lib/flags.js";
8
+ import { runReadAcrossTargets } from "../../lib/read-fanout.js";
9
+ import { formatAge, logClippedTable } from "../../lib/ui.js";
10
10
  export function buildListQuery(flags) {
11
11
  const filters = [`_type == "${WORKFLOW_INSTANCE_TYPE}"`];
12
12
  const params = {};
@@ -36,7 +36,9 @@ export function buildListQuery(flags) {
36
36
  currentStage,
37
37
  completedAt,
38
38
  abortedAt,
39
- lastChangedAt
39
+ lastChangedAt,
40
+ modelVersion,
41
+ minReaderModel
40
42
  }`;
41
43
  return { groq, params };
42
44
  }
@@ -54,6 +56,7 @@ export function instanceRow(r) {
54
56
  };
55
57
  }
56
58
  export default class List extends WorkflowCommand {
59
+ static aliases = ['list'];
57
60
  static description = 'List workflow instances in the configured dataset (in-flight by default).';
58
61
  static examples = [
59
62
  '<%= config.bin %> list',
@@ -98,24 +101,23 @@ export default class List extends WorkflowCommand {
98
101
  this.log(`${logSymbols.info} no instances match`);
99
102
  return;
100
103
  }
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
- }
104
+ logClippedTable({
105
+ rows: fetched,
106
+ limit: flags.limit,
107
+ headers: ['instance', 'workflow', 'tag', 'stage', 'status', 'updated'],
108
+ toCells: (r) => {
109
+ const row = instanceRow(r);
110
+ return [
111
+ row._id,
112
+ row.definition,
113
+ row.tag,
114
+ row.currentStage,
115
+ row.status,
116
+ formatAge(row.lastChangedAt),
117
+ ];
118
+ },
119
+ log: (line) => this.log(line),
120
+ });
119
121
  },
120
122
  });
121
123
  if (failures.length > 0) {
@@ -125,8 +127,11 @@ export default class List extends WorkflowCommand {
125
127
  }
126
128
  async function fetchRows({ client, groq, params, document, }) {
127
129
  if (document === undefined) {
128
- return client.fetch(groq, params, { tag: 'list' });
130
+ const rows = await client.fetch(groq, params, { tag: 'list' });
131
+ return rows.map(assertReadableModel);
129
132
  }
130
133
  const candidates = await client.fetch(groq, params, { tag: 'list' });
131
- return candidates.filter((instance) => instanceWatchesDocument(instance, document));
134
+ return candidates
135
+ .map(assertReadableModel)
136
+ .filter((instance) => instanceWatchesDocument(instance, document));
132
137
  }
@@ -0,0 +1,12 @@
1
+ import { WorkflowCommand } from '../../lib/base-command.ts';
2
+ export default class Nuke extends WorkflowCommand {
3
+ static aliases: string[];
4
+ static summary: string;
5
+ static description: string;
6
+ static examples: string[];
7
+ static flags: {
8
+ tag: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,77 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { input } from '@sanity/cli-core/ux';
3
+ import { resourceGdr } from '@sanity/workflow-engine';
4
+ import logSymbols from 'log-symbols';
5
+ import { WorkflowCommand } from "../../lib/base-command.js";
6
+ import { clientFor, resolveApiHost, resolveTokenOrFail } from "../../lib/client.js";
7
+ import { dedupeResources } from "../../lib/context.js";
8
+ import { fail } from "../../lib/fail.js";
9
+ import { loadWorkflowConfig } from "../../lib/load-config.js";
10
+ import { confirmationMatches, executeNuke, formatNukeSummary, involvedTargets, planCounts, renderNukePlan, resolveNukePlan, } from "../../lib/nuke.js";
11
+ import { runWriteVerb } from "../../lib/ops-report.js";
12
+ import { canPromptOnStderr } from "../../lib/prompt.js";
13
+ import { selectDeployment } from "../../lib/select-deployment.js";
14
+ export default class Nuke extends WorkflowCommand {
15
+ static aliases = ['nuke'];
16
+ static summary = 'Delete every engine-owned document for a deployment tag — the dev-period big red button.';
17
+ static description = 'The reset for a dataset holding engine documents the versioned upgrade framework cannot yet ' +
18
+ "migrate: deletes the tag's instances, definitions, and guards (across every alias-bound " +
19
+ 'resource). Content documents are never touched. Prints a dry-run plan, then requires you to ' +
20
+ 'type back every involved dataset (--force skips the prompt; the plan still prints).';
21
+ static examples = [
22
+ '<%= config.bin %> nuke --tag plugin-dev',
23
+ '<%= config.bin %> nuke --tag plugin-dev --force',
24
+ ];
25
+ static flags = {
26
+ tag: Flags.string({
27
+ description: 'The deployment tag to reset. Required — a destructive reset never guesses the environment.',
28
+ required: true,
29
+ }),
30
+ force: Flags.boolean({
31
+ description: 'Skip the confirmation prompt (for scripts/CI). The plan still prints.',
32
+ default: false,
33
+ }),
34
+ };
35
+ async run() {
36
+ const { flags } = await this.parse(Nuke);
37
+ const config = await loadWorkflowConfig();
38
+ const deployment = selectDeployment(config, { tag: flags.tag });
39
+ const targets = buildTargets(deployment, await resolveTokenOrFail());
40
+ const plan = await resolveNukePlan({ tag: deployment.tag, targets });
41
+ const apiHost = resolveApiHost() ?? 'https://api.sanity.io (production default)';
42
+ renderNukePlan(plan, apiHost).forEach((line) => this.log(line));
43
+ const counts = planCounts(plan);
44
+ if (counts.total === 0) {
45
+ this.log(`${logSymbols.info} Nothing to nuke for tag "${deployment.tag}".`);
46
+ return;
47
+ }
48
+ if (!flags.force)
49
+ await confirmOrFail(involvedTargets(plan));
50
+ await runWriteVerb({
51
+ startLabel: 'Nuking…',
52
+ failLabel: 'Nuke failed',
53
+ failHeadline: 'workflow nuke error:',
54
+ run: () => executeNuke(plan),
55
+ report: () => ({ changed: true, message: formatNukeSummary(deployment.tag, counts) }),
56
+ log: (line) => this.log(line),
57
+ });
58
+ }
59
+ }
60
+ function buildTargets(deployment, token) {
61
+ const engineResource = deployment.workflowResource;
62
+ const aliasResources = (deployment.resourceAliases ?? []).map((binding) => binding.resource);
63
+ return dedupeResources([engineResource, ...aliasResources]).map((resource) => ({
64
+ resource,
65
+ client: clientFor(resource, token),
66
+ holdsEngineDocs: resourceGdr(resource) === resourceGdr(engineResource),
67
+ }));
68
+ }
69
+ async function confirmOrFail(targets) {
70
+ if (!canPromptOnStderr()) {
71
+ fail('Refusing to nuke without confirmation in a non-interactive shell.', 'Re-run with --force to skip the prompt.');
72
+ }
73
+ const answer = await input({ message: `Type every target to confirm deletion (space-separated):\n ${targets.join(' ')}\n` }, { output: process.stderr });
74
+ if (!confirmationMatches(answer, targets)) {
75
+ fail('Confirmation did not match — nothing was deleted.', `Expected: ${targets.join(' ')}`);
76
+ }
77
+ }
@@ -1,5 +1,6 @@
1
- import { StubCommand } from '../lib/stub.ts';
1
+ import { StubCommand } from '../../lib/stub.ts';
2
2
  export default class ResetActivity extends StubCommand {
3
+ static hiddenAliases: string[];
3
4
  static hidden: boolean;
4
5
  static description: string;
5
6
  static args: {
@@ -1,6 +1,7 @@
1
1
  import { Args } from '@oclif/core';
2
- import { StubCommand } from "../lib/stub.js";
2
+ import { StubCommand } from "../../lib/stub.js";
3
3
  export default class ResetActivity extends StubCommand {
4
+ static hiddenAliases = ['reset-activity'];
4
5
  static hidden = true;
5
6
  static description = 'Reset a failed activity on an in-flight instance — back to pending (re-run) or to skipped (bypass).';
6
7
  static args = {
@@ -1,8 +1,9 @@
1
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';
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
5
  export default class SetStage extends WorkflowCommand {
6
+ static aliases: string[];
6
7
  static description: string;
7
8
  static examples: string[];
8
9
  static args: {
@@ -1,12 +1,13 @@
1
1
  import { styleText } from 'node:util';
2
2
  import { Args, Flags } from '@oclif/core';
3
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";
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
9
  export default class SetStage extends WorkflowCommand {
10
+ static aliases = ['set-stage'];
10
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.";
11
12
  static examples = [
12
13
  '<%= config.bin %> set-stage wf-instance.abc123 --to ready',
@@ -1,6 +1,7 @@
1
1
  import { type WorkflowInstance } from '@sanity/workflow-engine';
2
- import { WorkflowCommand } from '../lib/base-command.ts';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
3
  export default class Show extends WorkflowCommand {
4
+ static aliases: string[];
4
5
  static description: string;
5
6
  static examples: string[];
6
7
  static args: {
@@ -2,11 +2,12 @@ import { styleText } from 'node:util';
2
2
  import { Args, Flags } from '@oclif/core';
3
3
  import { abortReason, displayTitle, terminalState, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { WorkflowCommand } from "../lib/base-command.js";
6
- import { findInstance, resolveReadTargets } from "../lib/context.js";
7
- import { tagFlags } from "../lib/flags.js";
8
- import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
5
+ import { WorkflowCommand } from "../../lib/base-command.js";
6
+ import { findInstance, resolveReadTargets } from "../../lib/context.js";
7
+ import { tagFlags } from "../../lib/flags.js";
8
+ import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../../lib/ui.js";
9
9
  export default class Show extends WorkflowCommand {
10
+ static aliases = ['show'];
10
11
  static description = 'Show the state, activities, and effects of a workflow instance.';
11
12
  static examples = [
12
13
  '<%= config.bin %> show wf-instance.abc123',
@@ -67,27 +68,40 @@ export function instanceHeader(instance) {
67
68
  return [title, ...rows].join('\n');
68
69
  }
69
70
  export function describeInstance(instance, options) {
70
- const lines = [sectionHeader('Stages')];
71
- for (const stage of instance.stages) {
72
- const marker = stage.exitedAt
73
- ? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
74
- : styleText('cyan', ' (current)');
75
- lines.push(` • ${stage.name}${marker}`);
76
- for (const activity of stage.activities ?? []) {
77
- lines.push(` ${activityIcon[activity.status]} ${activity.name} ${styleText('dim', `[${activity.status}]`)}`);
78
- }
79
- }
80
- if (instance.pendingEffects.length > 0) {
81
- lines.push('', sectionHeader('Pending effects'));
82
- for (const eff of instance.pendingEffects) {
83
- lines.push(` • ${eff.name} ${styleText('dim', `(key=${eff._key})`)}`);
84
- }
85
- }
86
- if (options.includeHistory) {
87
- lines.push('', sectionHeader('History'));
88
- for (const entry of instance.history) {
89
- lines.push(` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${displayTitle(entry._type)}`);
90
- }
71
+ return [
72
+ ...stageLines(instance),
73
+ ...pendingEffectLines(instance),
74
+ ...(options.includeHistory ? historyLines(instance) : []),
75
+ ];
76
+ }
77
+ function stageLines(instance) {
78
+ return [
79
+ sectionHeader('Stages'),
80
+ ...instance.stages.flatMap((stage) => [
81
+ ` • ${stage.name}${stageMarker(stage)}`,
82
+ ...(stage.activities ?? []).map((activity) => ` ${activityIcon[activity.status]} ${activity.name} ${styleText('dim', `[${activity.status}]`)}`),
83
+ ]),
84
+ ];
85
+ }
86
+ function stageMarker(stage) {
87
+ return stage.exitedAt
88
+ ? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
89
+ : styleText('cyan', ' (current)');
90
+ }
91
+ function pendingEffectLines(instance) {
92
+ if (instance.pendingEffects.length === 0) {
93
+ return [];
91
94
  }
92
- return lines;
95
+ return [
96
+ '',
97
+ sectionHeader('Pending effects'),
98
+ ...instance.pendingEffects.map((eff) => ` • ${eff.name} ${styleText('dim', `(key=${eff._key})`)}`),
99
+ ];
100
+ }
101
+ function historyLines(instance) {
102
+ return [
103
+ '',
104
+ sectionHeader('History'),
105
+ ...instance.history.map((entry) => ` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${displayTitle(entry._type)}`),
106
+ ];
93
107
  }
@@ -1,7 +1,8 @@
1
- import { type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance } from '@sanity/workflow-engine';
2
- import { WorkflowCommand } from '../lib/base-command.ts';
3
- import { type EngineScope } from '../lib/operation-args.ts';
1
+ import { StartNotSettledError, type InitialFieldValue, type StartInstanceArgs, type WorkflowInstance } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ import { type EngineScope } from '../../lib/operation-args.ts';
4
4
  export default class Start extends WorkflowCommand {
5
+ static aliases: string[];
5
6
  static description: string;
6
7
  static examples: string[];
7
8
  static args: {
@@ -11,20 +12,35 @@ export default class Start extends WorkflowCommand {
11
12
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
13
  version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  field: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
15
+ 'instance-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
16
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
17
  };
16
18
  run(): Promise<void>;
17
19
  }
20
+ /** A created-but-unprimed failure is resumable — the engine message names
21
+ * the id and the generic remediation; operators need the CLI's own retry
22
+ * spelling appended. Every other error passes through untouched. */
23
+ export declare function withRetryHint(error: unknown): unknown;
24
+ /** The spinner message for a start whose first cascade failed after the
25
+ * instance was created and primed: the run exists — say so, name the cause,
26
+ * and point at the retry rail instead of reporting a rejection. */
27
+ export declare function startNotSettledReport(error: StartNotSettledError): string;
28
+ /** The `--json` payload for a not-settled start — same `instanceId` key as
29
+ * the success payload so scripts can pick up the retry id either way. */
30
+ export declare function startNotSettledJson(error: StartNotSettledError): Record<string, unknown>;
18
31
  /**
19
32
  * The shared engine args ({@link baseEngineArgs}) plus the start-specific
20
33
  * fields. Optionals spread conditionally because the engine's optional args
21
34
  * reject an explicit `undefined` under `exactOptionalPropertyTypes`.
22
35
  */
23
- export declare function buildStartArgs({ scope, name, version, initialFields, }: {
36
+ export declare function buildStartArgs({ scope, name, version, initialFields, instanceId, }: {
24
37
  scope: EngineScope;
25
38
  name: string;
26
39
  version: number | undefined;
27
40
  initialFields: InitialFieldValue[];
41
+ /** The start's idempotency key (`--instance-id`) — a retry carrying it
42
+ * resumes the earlier attempt instead of creating a duplicate. */
43
+ instanceId?: string | undefined;
28
44
  }): StartInstanceArgs & EngineScope;
29
45
  /** The success spinner message. Pure so the in-flight / immediately-terminal
30
46
  * permutations can be asserted without driving a real start. */
@@ -1,21 +1,23 @@
1
1
  import { styleText } from 'node:util';
2
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
- 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";
3
+ import { buildInitialFields, errorMessage, StartNotPrimedError, StartNotSettledError, startRefusal, 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
12
  export default class Start extends WorkflowCommand {
13
+ static aliases = ['start'];
13
14
  static description = 'Start a workflow instance from a deployed definition. Supply values for the ' +
14
15
  "workflow's input-sourced fields with --field (e.g. the subject document ref).";
15
16
  static examples = [
16
17
  '<%= config.bin %> start productLaunch',
17
18
  '<%= config.bin %> start article-review --field subject=\'{"id":"dataset:proj:ds:article-1","type":"article"}\'',
18
19
  '<%= config.bin %> start productLaunch --version 2 --tag prod',
20
+ '<%= config.bin %> start productLaunch --instance-id prod.wf-instance.a1b2c3d4e5f6',
19
21
  ];
20
22
  static args = {
21
23
  name: Args.string({ required: true, description: 'Workflow definition name.' }),
@@ -32,6 +34,12 @@ export default class Start extends WorkflowCommand {
32
34
  multiple: true,
33
35
  default: [],
34
36
  }),
37
+ 'instance-id': Flags.string({
38
+ description: "Start under this instance id — for retries. The id is the start's idempotency key: " +
39
+ 'pass the id of a start that failed partway and the engine resumes it instead of ' +
40
+ 'creating a duplicate (an already-settled start replays as a no-op). Omit to mint ' +
41
+ 'a fresh id.',
42
+ }),
35
43
  ...jsonFlags,
36
44
  };
37
45
  async run() {
@@ -52,25 +60,58 @@ export default class Start extends WorkflowCommand {
52
60
  name: args.name,
53
61
  version: flags.version,
54
62
  initialFields,
63
+ instanceId: flags['instance-id'],
55
64
  }),
56
65
  };
57
66
  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));
67
+ const result = await workflow.startInstance(startArgs).catch((error) => {
68
+ if (error instanceof StartNotSettledError) {
69
+ this.log(JSON.stringify(startNotSettledJson(error), null, 2));
70
+ return undefined;
71
+ }
72
+ fail('start error:', failureDetail(withRetryHint(error)));
73
+ });
74
+ if (result !== undefined) {
75
+ this.log(JSON.stringify(startResultJson(result.instance), null, 2));
76
+ }
62
77
  return;
63
78
  }
64
79
  await runWriteVerb({
65
80
  startLabel: `Starting ${args.name}…`,
66
81
  failLabel: 'Start rejected',
67
82
  failHeadline: 'start error:',
68
- run: () => workflow.startInstance(startArgs),
69
- report: (result) => ({ changed: true, message: startReport(result.instance) }),
83
+ run: () => workflow.startInstance(startArgs).catch((error) => {
84
+ if (error instanceof StartNotSettledError)
85
+ return error;
86
+ throw withRetryHint(error);
87
+ }),
88
+ report: (result) => result instanceof StartNotSettledError
89
+ ? { changed: false, message: startNotSettledReport(result) }
90
+ : { changed: true, message: startReport(result.instance) },
70
91
  log: (line) => this.log(line),
71
92
  });
72
93
  }
73
94
  }
95
+ export function withRetryHint(error) {
96
+ return error instanceof StartNotPrimedError
97
+ ? new Error(`${errorMessage(error)} (--instance-id ${error.instanceId})`)
98
+ : error;
99
+ }
100
+ export function startNotSettledReport(error) {
101
+ const id = styleText('bold', error.instanceId);
102
+ return (`Started ${id} — but its first auto-advance failed: ${errorMessage(error.cause)}. ` +
103
+ `It settles on the next tick, or retry with --instance-id ${error.instanceId}.`);
104
+ }
105
+ export function startNotSettledJson(error) {
106
+ return {
107
+ instanceId: error.instanceId,
108
+ notSettled: true,
109
+ detail: errorMessage(error.cause),
110
+ ...(error.instance !== undefined
111
+ ? { currentStage: error.instance.currentStage, definition: error.instance.definition }
112
+ : {}),
113
+ };
114
+ }
74
115
  async function resolveStartInputs({ client, name, tag, version, values, }) {
75
116
  const deployed = await fetchDeployedDefinition({ client, name, tag, version });
76
117
  if (deployed === undefined) {
@@ -82,12 +123,13 @@ async function resolveStartInputs({ client, name, tag, version, values, }) {
82
123
  }
83
124
  return failOnThrow('start usage error:', () => buildInitialFields({ declared: deployed.fields ?? [], values }));
84
125
  }
85
- export function buildStartArgs({ scope, name, version, initialFields, }) {
126
+ export function buildStartArgs({ scope, name, version, initialFields, instanceId, }) {
86
127
  return {
87
128
  ...baseEngineArgs(scope),
88
129
  definition: name,
89
130
  ...(version !== undefined ? { version } : {}),
90
131
  ...(initialFields.length > 0 ? { initialFields } : {}),
132
+ ...(instanceId !== undefined ? { instanceId } : {}),
91
133
  };
92
134
  }
93
135
  export function startReport(instance) {
@@ -1,5 +1,6 @@
1
- import { WorkflowCommand } from '../lib/base-command.ts';
1
+ import { WorkflowCommand } from '../../lib/base-command.ts';
2
2
  export default class Tail extends WorkflowCommand {
3
+ static aliases: string[];
3
4
  static description: string;
4
5
  static examples: string[];
5
6
  static args: {