@sanity/workflow-cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +99 -61
  2. package/dist/commands/abort.d.ts +5 -7
  3. package/dist/commands/abort.js +15 -14
  4. package/dist/commands/definition/delete.d.ts +10 -6
  5. package/dist/commands/definition/delete.js +8 -11
  6. package/dist/commands/definition/diff.d.ts +8 -1
  7. package/dist/commands/definition/diff.js +30 -24
  8. package/dist/commands/definition/list.d.ts +4 -4
  9. package/dist/commands/definition/list.js +8 -8
  10. package/dist/commands/definition/show.d.ts +0 -10
  11. package/dist/commands/definition/show.js +11 -20
  12. package/dist/commands/deploy.d.ts +58 -5
  13. package/dist/commands/deploy.js +134 -27
  14. package/dist/commands/diagnose.d.ts +5 -1
  15. package/dist/commands/diagnose.js +18 -28
  16. package/dist/commands/fire-action.d.ts +15 -28
  17. package/dist/commands/fire-action.js +37 -72
  18. package/dist/commands/list.d.ts +2 -2
  19. package/dist/commands/list.js +8 -8
  20. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  21. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -2
  22. package/dist/commands/set-stage.d.ts +27 -3
  23. package/dist/commands/set-stage.js +80 -13
  24. package/dist/commands/show.d.ts +1 -0
  25. package/dist/commands/show.js +14 -8
  26. package/dist/commands/start.d.ts +59 -0
  27. package/dist/commands/start.js +169 -0
  28. package/dist/commands/tail.d.ts +3 -0
  29. package/dist/commands/tail.js +9 -5
  30. package/dist/lib/client.d.ts +11 -14
  31. package/dist/lib/client.js +45 -33
  32. package/dist/lib/context.d.ts +62 -0
  33. package/dist/lib/context.js +82 -0
  34. package/dist/lib/definitions.d.ts +38 -29
  35. package/dist/lib/definitions.js +45 -85
  36. package/dist/lib/diff.js +8 -0
  37. package/dist/lib/env.d.ts +0 -6
  38. package/dist/lib/env.js +3 -9
  39. package/dist/lib/fail.d.ts +6 -0
  40. package/dist/lib/fail.js +11 -1
  41. package/dist/lib/flags.d.ts +14 -2
  42. package/dist/lib/flags.js +24 -3
  43. package/dist/lib/load-config.d.ts +11 -0
  44. package/dist/lib/load-config.js +69 -0
  45. package/dist/lib/operation-args.d.ts +25 -7
  46. package/dist/lib/operation-args.js +12 -11
  47. package/dist/lib/ops-report.d.ts +19 -7
  48. package/dist/lib/ops-report.js +6 -7
  49. package/dist/lib/params.d.ts +7 -0
  50. package/dist/lib/params.js +26 -0
  51. package/dist/lib/select-deployment.d.ts +31 -0
  52. package/dist/lib/select-deployment.js +58 -0
  53. package/dist/lib/ui.d.ts +3 -1
  54. package/dist/lib/ui.js +1 -3
  55. package/oclif.manifest.json +145 -86
  56. package/package.json +6 -4
  57. package/dist/commands/move-stage.d.ts +0 -47
  58. package/dist/commands/move-stage.js +0 -77
  59. package/dist/lib/config.d.ts +0 -18
  60. package/dist/lib/config.js +0 -50
@@ -1,24 +1,11 @@
1
1
  import { styleText } from 'node:util';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
- import { isTerminalStage, WORKFLOW_DEFINITION_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
3
+ import { isTerminalStage, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { loadClient } from "../../lib/client.js";
5
+ import { resolveReadContext } from "../../lib/context.js";
6
+ import { buildDefinitionShowQuery } from "../../lib/definitions.js";
6
7
  import { tagFlags } from "../../lib/flags.js";
7
8
  import { formatKeyValue, sectionHeader } from "../../lib/ui.js";
8
- export function buildDefinitionShowQuery(flags) {
9
- const params = { name: flags.name };
10
- const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`, 'name == $name'];
11
- if (flags.version !== undefined) {
12
- params.version = flags.version;
13
- filters.push('version == $version');
14
- }
15
- if (flags.tag) {
16
- params.tag = flags.tag;
17
- filters.push(tagScopeFilter());
18
- }
19
- const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
20
- return { groq, params };
21
- }
22
9
  export default class DefinitionShow extends Command {
23
10
  static description = 'Show a deployed workflow definition.';
24
11
  static args = {
@@ -30,12 +17,12 @@ export default class DefinitionShow extends Command {
30
17
  };
31
18
  async run() {
32
19
  const { args, flags } = await this.parse(DefinitionShow);
20
+ const { client } = await resolveReadContext(flags);
33
21
  const { groq, params } = buildDefinitionShowQuery({
34
22
  name: args.name,
35
23
  tag: flags.tag,
36
24
  version: flags.version,
37
25
  });
38
- const client = loadClient();
39
26
  // A deployed document: the authored {@link WorkflowDefinition} plus the
40
27
  // deploy-stamped envelope (`version`, `contentHash`, `tag`).
41
28
  const def = await client.fetch(groq, params);
@@ -57,11 +44,15 @@ const HEADER_PAD = 11; // "Description" — the longest label in the block
57
44
  * authored definition, so it's passed separately and omitted when unknown. */
58
45
  export function definitionHeader(def, tag) {
59
46
  const rows = [
60
- formatKeyValue('Title', def.title, { padTo: HEADER_PAD }),
61
- formatKeyValue('Description', def.description ?? styleText('dim', '—'), { padTo: HEADER_PAD }),
47
+ formatKeyValue({ key: 'Title', value: def.title, padTo: HEADER_PAD }),
48
+ formatKeyValue({
49
+ key: 'Description',
50
+ value: def.description ?? styleText('dim', '—'),
51
+ padTo: HEADER_PAD,
52
+ }),
62
53
  ];
63
54
  if (tag !== undefined) {
64
- rows.push(formatKeyValue('Tag', tag, { padTo: HEADER_PAD }));
55
+ rows.push(formatKeyValue({ key: 'Tag', value: tag, padTo: HEADER_PAD }));
65
56
  }
66
57
  return [styleText('bold', `${def.name} v${def.version}`), ...rows].join('\n');
67
58
  }
@@ -1,22 +1,75 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { type DeployDefinitionResult, type WorkflowDefinition, type WorkflowDeployment } from '@sanity/workflow-engine';
3
+ /** A deployment paired with the definitions selected + validated for it. */
4
+ interface DeployBatch {
5
+ deployment: WorkflowDeployment;
6
+ defs: WorkflowDefinition[];
7
+ }
8
+ /** A deployment whose deploy/diff threw, kept for the closing summary. */
9
+ interface DeployFailure {
10
+ deployment: Pick<WorkflowDeployment, 'name' | 'tag'>;
11
+ message: string;
12
+ }
2
13
  export default class Deploy extends Command {
3
14
  static description: string;
4
15
  static examples: string[];
5
16
  static flags: {
17
+ 'all-tags': import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
18
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
19
  check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
20
  only: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
21
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
22
  };
11
23
  run(): Promise<void>;
24
+ private processDeployment;
12
25
  private executeDeploy;
13
26
  private runDryRun;
14
27
  }
15
28
  /** Reject the contradictory `--check --dry-run` combination. */
16
29
  export declare function validateModeFlags(check: boolean, dryRun: boolean): void;
30
+ /**
31
+ * Select and validate every deployment's definitions up front. Validation runs
32
+ * before any network write ({@link validateOrFail} exits on a bad definition),
33
+ * so a whole-config deploy can never leave a partial reconcile behind a broken
34
+ * batch. In a multi-deployment run the failure names its deployment — it exits
35
+ * before any banner could attribute it.
36
+ */
37
+ export declare function buildBatches(deployments: WorkflowDeployment[], only: string | undefined): DeployBatch[];
38
+ /**
39
+ * Run `processBatch` over each batch, continuing past a failure so one bad
40
+ * deployment can't strand the rest — deployments are independent, and deploys
41
+ * are idempotent, so a partial run is safe to re-run. Failures are flattened
42
+ * to their message for {@link deploySummary}. A single-batch run rethrows
43
+ * instead: it has no summary, so a throw from before the deploy spinner starts
44
+ * would otherwise surface nowhere — and propagating keeps the stack visible.
45
+ */
46
+ export declare function reconcileBatches({ batches, log, processBatch, }: {
47
+ batches: DeployBatch[];
48
+ log: (line: string) => void;
49
+ processBatch: (batch: DeployBatch) => Promise<void>;
50
+ }): Promise<DeployFailure[]>;
51
+ /**
52
+ * The closing summary for a multi-deployment run that had failures: the
53
+ * succeeded/failed tally and each failed deployment. Empty for a clean run.
54
+ * The wording is mode-neutral because a `--dry-run` failure lands here too,
55
+ * where nothing was deployed. Single-batch runs never reach it — their lone
56
+ * failure rethrows out of {@link reconcileBatches}.
57
+ */
58
+ export declare function deploySummary({ total, failures, }: {
59
+ total: number;
60
+ failures: DeployFailure[];
61
+ }): string[];
62
+ /**
63
+ * The banner printed above a deployment's output so a multi-deployment run is
64
+ * attributable. `undefined` for a single-target run — it keeps its original
65
+ * unprefixed output. A leading blank line separates every deployment after the
66
+ * first.
67
+ */
68
+ export declare function deploymentBanner({ deployment, index, total, }: {
69
+ deployment: Pick<WorkflowDeployment, 'name' | 'tag'>;
70
+ index: number;
71
+ total: number;
72
+ }): string | undefined;
17
73
  /** One line per deployed definition, symbol-tagged by its result status. */
18
- export declare function deployResultLines(results: {
19
- status: string;
20
- definition: string;
21
- version: number;
22
- }[]): string[];
74
+ export declare function deployResultLines(results: DeployDefinitionResult[]): string[];
75
+ export {};
@@ -1,23 +1,31 @@
1
+ import { styleText } from 'node:util';
1
2
  import { Command, Flags } from '@oclif/core';
2
- import { computeDiffEntries, workflow } from '@sanity/workflow-engine';
3
+ import { computeDiffEntries, workflow, } from '@sanity/workflow-engine';
3
4
  import logSymbols from 'log-symbols';
4
5
  import ora from 'ora';
5
- import { buildClient, loadClient } from "../lib/client.js";
6
- import { loadWorkflowConfig } from "../lib/config.js";
7
- import { loadValidatedDefinitions } from "../lib/definitions.js";
6
+ import { clientFor, resolveTokenOrFail } from "../lib/client.js";
7
+ import { selectDefinitions, validateOrFail } from "../lib/definitions.js";
8
8
  import { diffReport } from "../lib/diff.js";
9
- import { fail } from "../lib/fail.js";
9
+ import { errorMessage, fail } from "../lib/fail.js";
10
10
  import { tagFlags } from "../lib/flags.js";
11
+ import { loadWorkflowConfig } from "../lib/load-config.js";
12
+ import { deploymentToTarget, selectDeployments } from "../lib/select-deployment.js";
11
13
  export default class Deploy extends Command {
12
- static description = 'Validate, diff, and deploy workflow definitions to the configured dataset.';
14
+ static description = 'Validate, diff, and deploy workflow definitions to the resource bound by the selected deployment.';
13
15
  static examples = [
14
- '<%= config.bin %> deploy',
16
+ '<%= config.bin %> deploy --tag prod',
17
+ '<%= config.bin %> deploy --all-tags',
15
18
  '<%= config.bin %> deploy --check',
16
19
  '<%= config.bin %> deploy --dry-run',
17
20
  '<%= config.bin %> deploy --only productLaunch',
18
21
  ];
19
22
  static flags = {
20
23
  ...tagFlags,
24
+ 'all-tags': Flags.boolean({
25
+ description: 'Deploy every deployment in the config, not just one tag.',
26
+ default: false,
27
+ exclusive: ['tag'],
28
+ }),
21
29
  'dry-run': Flags.boolean({
22
30
  description: 'Validate + diff against the deployed version; do not write.',
23
31
  default: false,
@@ -27,53 +35,75 @@ export default class Deploy extends Command {
27
35
  default: false,
28
36
  }),
29
37
  only: Flags.string({
30
- description: 'Limit deploy/check/diff to a single workflow definition by name.',
38
+ description: 'Limit deploy/check/diff to a single workflow definition by name. Every targeted deployment must contain it.',
31
39
  }),
32
40
  };
33
41
  async run() {
34
42
  const { flags } = await this.parse(Deploy);
35
43
  validateModeFlags(flags.check, flags['dry-run']);
36
- const config = loadWorkflowConfig(flags.tag);
37
- const selected = await loadValidatedDefinitions(flags.only);
44
+ const config = await loadWorkflowConfig();
45
+ // Validate every deployment up front (below), so a bad definition fails
46
+ // before any write — never a partial reconcile from a validation error.
47
+ const batches = buildBatches(selectDeployments(config, { tag: flags.tag, allTags: flags['all-tags'] }), flags.only);
48
+ const log = (line) => this.log(line);
49
+ // buildBatches already validated everything offline — --check only has to
50
+ // report that pass, so it never resolves a token or builds a client.
38
51
  if (flags.check) {
39
- this.log(`${logSymbols.success} ${selected.length} definition(s) passed validation (check only — dataset not contacted).`);
52
+ await reconcileBatches({
53
+ batches,
54
+ log,
55
+ processBatch: async ({ defs }) => {
56
+ log(`${logSymbols.success} ${defs.length} definition(s) passed validation (check only — dataset not contacted).`);
57
+ },
58
+ });
40
59
  return;
41
60
  }
42
- const client = loadClient();
43
- if (flags['dry-run']) {
44
- await this.runDryRun(client, selected, config);
61
+ // One token up front, so an --all-tags run authenticates once, not per deployment.
62
+ const token = await resolveTokenOrFail();
63
+ const failures = await reconcileBatches({
64
+ batches,
65
+ log,
66
+ processBatch: ({ deployment, defs }) => this.processDeployment({ deployment, defs, dryRun: flags['dry-run'], token }),
67
+ });
68
+ for (const line of deploySummary({ total: batches.length, failures })) {
69
+ log(line);
70
+ }
71
+ if (failures.length > 0) {
72
+ this.exit(1);
73
+ }
74
+ }
75
+ async processDeployment({ deployment, defs, dryRun, token, }) {
76
+ const target = deploymentToTarget(deployment);
77
+ const client = clientFor(deployment.workflowResource, token);
78
+ if (dryRun) {
79
+ await this.runDryRun({ client, defs, target });
45
80
  return;
46
81
  }
47
- await this.executeDeploy(client, selected, config);
82
+ await this.executeDeploy({ client, defs, target });
48
83
  }
49
- async executeDeploy(client, defs, config) {
84
+ async executeDeploy({ client, defs, target, }) {
50
85
  const spinner = ora(`Deploying ${defs.length} definition(s)…`).start();
51
86
  try {
52
- const { results } = await workflow.deployDefinitions({
53
- workflowResource: config.workflowResource,
54
- tag: config.tag,
55
- client,
56
- definitions: defs,
57
- });
87
+ const { results } = await workflow.deployDefinitions({ ...target, client, definitions: defs });
58
88
  spinner.succeed(`Processed ${results.length} definition(s)`);
59
89
  for (const line of deployResultLines(results))
60
90
  this.log(line);
61
91
  }
62
92
  catch (error) {
63
- spinner.fail('Deploy failed');
93
+ spinner.fail(`Deploy failed — ${errorMessage(error)}`);
64
94
  throw error;
65
95
  }
66
96
  }
67
- async runDryRun(client, defs, config) {
97
+ async runDryRun({ client, defs, target, }) {
68
98
  const spinner = ora(`Diffing ${defs.length} definition(s) against dataset…`).start();
69
99
  try {
70
- const entries = await computeDiffEntries(client, defs, config);
100
+ const entries = await computeDiffEntries({ client, defs, target });
71
101
  spinner.succeed(`Diffed ${defs.length} definition(s)`);
72
102
  for (const line of diffReport(entries))
73
103
  this.log(line);
74
104
  }
75
105
  catch (error) {
76
- spinner.fail('Diff failed');
106
+ spinner.fail(`Diff failed — ${errorMessage(error)}`);
77
107
  throw error;
78
108
  }
79
109
  }
@@ -84,6 +114,83 @@ export function validateModeFlags(check, dryRun) {
84
114
  fail('Pass either --check or --dry-run, not both.');
85
115
  }
86
116
  }
117
+ /** The one identity format for a deployment in output — banner, failure
118
+ * summary, and validation attribution all render it the same way. */
119
+ function deploymentLabel({ name, tag }) {
120
+ return `${name} (${tag})`;
121
+ }
122
+ /**
123
+ * Select and validate every deployment's definitions up front. Validation runs
124
+ * before any network write ({@link validateOrFail} exits on a bad definition),
125
+ * so a whole-config deploy can never leave a partial reconcile behind a broken
126
+ * batch. In a multi-deployment run the failure names its deployment — it exits
127
+ * before any banner could attribute it.
128
+ */
129
+ export function buildBatches(deployments, only) {
130
+ return deployments.map((deployment) => {
131
+ const context = deployments.length > 1 ? `${deploymentLabel(deployment)} — ` : '';
132
+ const defs = selectDefinitions(deployment.definitions, { only, context });
133
+ validateOrFail(defs, context);
134
+ return { deployment, defs };
135
+ });
136
+ }
137
+ /**
138
+ * Run `processBatch` over each batch, continuing past a failure so one bad
139
+ * deployment can't strand the rest — deployments are independent, and deploys
140
+ * are idempotent, so a partial run is safe to re-run. Failures are flattened
141
+ * to their message for {@link deploySummary}. A single-batch run rethrows
142
+ * instead: it has no summary, so a throw from before the deploy spinner starts
143
+ * would otherwise surface nowhere — and propagating keeps the stack visible.
144
+ */
145
+ export async function reconcileBatches({ batches, log, processBatch, }) {
146
+ const failures = [];
147
+ for (const [index, batch] of batches.entries()) {
148
+ const banner = deploymentBanner({ deployment: batch.deployment, index, total: batches.length });
149
+ if (banner !== undefined) {
150
+ log(banner);
151
+ }
152
+ try {
153
+ await processBatch(batch);
154
+ }
155
+ catch (error) {
156
+ if (batches.length === 1) {
157
+ throw error;
158
+ }
159
+ failures.push({ deployment: batch.deployment, message: errorMessage(error) });
160
+ }
161
+ }
162
+ return failures;
163
+ }
164
+ /**
165
+ * The closing summary for a multi-deployment run that had failures: the
166
+ * succeeded/failed tally and each failed deployment. Empty for a clean run.
167
+ * The wording is mode-neutral because a `--dry-run` failure lands here too,
168
+ * where nothing was deployed. Single-batch runs never reach it — their lone
169
+ * failure rethrows out of {@link reconcileBatches}.
170
+ */
171
+ export function deploySummary({ total, failures, }) {
172
+ if (failures.length === 0) {
173
+ return [];
174
+ }
175
+ return [
176
+ '',
177
+ styleText('bold', `${total - failures.length} of ${total} deployments succeeded — ${failures.length} failed:`),
178
+ ...failures.map((f) => styleText('red', ` ${logSymbols.error} ${deploymentLabel(f.deployment)} — ${f.message}`)),
179
+ ];
180
+ }
181
+ /**
182
+ * The banner printed above a deployment's output so a multi-deployment run is
183
+ * attributable. `undefined` for a single-target run — it keeps its original
184
+ * unprefixed output. A leading blank line separates every deployment after the
185
+ * first.
186
+ */
187
+ export function deploymentBanner({ deployment, index, total, }) {
188
+ if (total <= 1) {
189
+ return undefined;
190
+ }
191
+ const gap = index > 0 ? '\n' : '';
192
+ return `${gap}${styleText('bold', `▸ ${deploymentLabel(deployment)}`)}`;
193
+ }
87
194
  /** One line per deployed definition, symbol-tagged by its result status. */
88
195
  export function deployResultLines(results) {
89
196
  const symbols = {
@@ -91,6 +198,6 @@ export function deployResultLines(results) {
91
198
  };
92
199
  return results.map((r) => {
93
200
  const symbol = symbols[r.status] ?? logSymbols.warning;
94
- return ` ${symbol} ${r.status.padEnd(9)} ${r.definition} v${r.version}`;
201
+ return ` ${symbol} ${r.status.padEnd(9)} ${r.name} v${r.version}`;
95
202
  });
96
203
  }
@@ -8,7 +8,11 @@ import { type Diagnosis, type DiagnoseInput, type SuggestedRemediation } from '@
8
8
  * {@link SuggestedRemediation}s rather than re-deriving them, so the rendered
9
9
  * fix block and the `--json` output stay one computation.
10
10
  */
11
- export declare function renderDiagnosis(diagnosis: Diagnosis, input: DiagnoseInput, remediations: SuggestedRemediation[]): string[];
11
+ export declare function renderDiagnosis({ diagnosis, input, remediations, }: {
12
+ diagnosis: Diagnosis;
13
+ input: DiagnoseInput;
14
+ remediations: SuggestedRemediation[];
15
+ }): string[];
12
16
  export default class Diagnose extends Command {
13
17
  static description: string;
14
18
  static examples: string[];
@@ -1,11 +1,10 @@
1
1
  import { styleText } from 'node:util';
2
- import { Args, Command, Flags } from '@oclif/core';
2
+ import { Args, Command } from '@oclif/core';
3
3
  import { diagnoseInputFromEvaluation, workflow, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { loadClient } from "../lib/client.js";
6
- import { loadWorkflowConfig } from "../lib/config.js";
7
- import { fail } from "../lib/fail.js";
8
- import { tagFlags } from "../lib/flags.js";
5
+ import { resolveInstanceContext } from "../lib/context.js";
6
+ import { errorMessage, fail } from "../lib/fail.js";
7
+ import { jsonFlags, tagFlags } from "../lib/flags.js";
9
8
  import { formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
10
9
  import { instanceHeader } from "./show.js";
11
10
  function formatAssignee(a) {
@@ -121,23 +120,18 @@ function transitionUnevaluableDetail(transitions) {
121
120
  `that is missing or unreadable (GROQ null), so routing is held rather than`,
122
121
  `falling through. Make the data the filter reads readable — publish the`,
123
122
  `subject (or fill the field) — and the instance advances on its own; no`,
124
- `move-stage needed.`,
123
+ `set-stage needed.`,
125
124
  ],
126
125
  };
127
126
  }
128
- const REMEDIATION_LABEL = {
129
- 'retry-effect': 'retry-effect',
130
- 'drain-effects': 're-run the effect drainer',
131
- 'reset-activity': 'reset-activity',
132
- 'set-stage': 'move-stage',
133
- abort: 'abort',
134
- };
135
127
  /** Render the engine's structured remediations as the "suggested fix" block.
136
- * Only the runnable ones reach here{@link renderDiagnosis} filters out
137
- * verbs the CLI can't yet perform so it never advertises a fix you can't run
138
- * (the full set, planned verbs included, still rides `--json`). */
128
+ * Verbs render under their engine namesthe same vocabulary `--json`
129
+ * emits, so scripts and humans read one language. Only runnable ones reach
130
+ * here: {@link renderDiagnosis} filters on the engine's `available` flag,
131
+ * so verbs that aren't callable engine operations yet never render as a
132
+ * fix (the full set, planned verbs included, still rides `--json`). */
139
133
  function remediationLines(remediations) {
140
- return remediations.map((r) => ` • ${REMEDIATION_LABEL[r.verb]} — ${r.rationale}`);
134
+ return remediations.map((r) => ` • ${r.verb} — ${r.rationale}`);
141
135
  }
142
136
  function causeDetail(cause) {
143
137
  switch (cause.kind) {
@@ -179,7 +173,7 @@ function statusLine(diagnosis) {
179
173
  * {@link SuggestedRemediation}s rather than re-deriving them, so the rendered
180
174
  * fix block and the `--json` output stay one computation.
181
175
  */
182
- export function renderDiagnosis(diagnosis, input, remediations) {
176
+ export function renderDiagnosis({ diagnosis, input, remediations, }) {
183
177
  const lines = [statusLine(diagnosis)];
184
178
  const inFlight = diagnosis.state === 'progressing' ||
185
179
  diagnosis.state === 'waiting' ||
@@ -219,20 +213,16 @@ export default class Diagnose extends Command {
219
213
  };
220
214
  static flags = {
221
215
  ...tagFlags,
222
- json: Flags.boolean({
223
- description: 'Emit the structured diagnosis as JSON instead of rendered output.',
224
- default: false,
225
- }),
216
+ ...jsonFlags,
226
217
  };
227
218
  async run() {
228
219
  const { args, flags } = await this.parse(Diagnose);
229
- const config = loadWorkflowConfig(flags.tag);
230
- const client = loadClient();
220
+ const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
231
221
  const { evaluation, diagnosis, remediations } = await workflow
232
222
  .diagnose({
233
223
  client,
234
- tag: config.tag,
235
- workflowResource: config.workflowResource,
224
+ tag: scope.tag,
225
+ workflowResource: scope.workflowResource,
236
226
  instanceId: args.instanceId,
237
227
  // Token-auth CLI can't hit /users/me; pin the same system actor the
238
228
  // write verbs use. Transition filters and activity status are actor-
@@ -242,7 +232,7 @@ export default class Diagnose extends Command {
242
232
  // specific assignee's.
243
233
  access: { actor: { kind: 'system', id: 'workflow-cli' } },
244
234
  })
245
- .catch((error) => fail('diagnose failed:', error instanceof Error ? error.message : String(error)));
235
+ .catch((error) => fail('diagnose failed:', errorMessage(error)));
246
236
  const input = diagnoseInputFromEvaluation(evaluation);
247
237
  if (flags.json) {
248
238
  this.log(JSON.stringify({
@@ -255,7 +245,7 @@ export default class Diagnose extends Command {
255
245
  }
256
246
  this.log(instanceHeader(evaluation.instance));
257
247
  this.log('');
258
- for (const line of renderDiagnosis(diagnosis, input, remediations)) {
248
+ for (const line of renderDiagnosis({ diagnosis, input, remediations })) {
259
249
  this.log(line);
260
250
  }
261
251
  }
@@ -1,46 +1,34 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import { type AvailableAction } from '@sanity/workflow-engine';
3
- import { type RanOp } from '../lib/ops-report.ts';
4
- /**
5
- * Parse repeated `--param key=value` flags into a params object. Each
6
- * value is JSON-parsed so numbers/booleans/objects type correctly,
7
- * falling back to the raw string. The engine validates the result against
8
- * the action's declared param types.
9
- */
10
- export declare function parseParams(pairs: string[]): Record<string, unknown>;
3
+ import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
11
4
  /**
12
5
  * The list-mode body: every action on the current stage's activities, marked
13
6
  * fireable or not (with the disabling reason), plus a hint showing the
14
7
  * flags to fire one. Pure so the populated/empty permutations assert
15
8
  * without a live evaluation.
16
9
  */
17
- export declare function renderAvailableActions(actions: AvailableAction[], stage: string, instanceId: string): string[];
18
- interface FireOutcome {
19
- fired: boolean;
20
- cascaded: number;
21
- instance: {
22
- currentStage: string;
23
- };
24
- ranOps?: RanOp[];
25
- }
10
+ export declare function renderAvailableActions({ actions, stage, instanceId, }: {
11
+ actions: AvailableAction[];
12
+ stage: string;
13
+ instanceId: string;
14
+ }): string[];
26
15
  /** The `--json` payload for a fired action — the structured counterpart of
27
16
  * {@link fireActionReport}. Pure so its shape is asserted directly. */
28
- export declare function fireResultJson(result: FireOutcome, ids: {
17
+ export declare function fireResultJson(result: WriteOutcome, ids: {
29
18
  instanceId: string;
30
19
  activity: string;
31
20
  action: string;
32
21
  }): Record<string, unknown>;
33
- export interface FireActionReport {
34
- fired: boolean;
35
- message: string;
36
- opsLines: string[];
37
- }
38
22
  /**
39
23
  * Turn a `fireAction` result into the spinner message + ops block. Pure so
40
24
  * the fired / no-op / cascaded / ops permutations assert without a live
41
25
  * fire.
42
26
  */
43
- export declare function fireActionReport(result: FireOutcome, activity: string, action: string): FireActionReport;
27
+ export declare function fireActionReport({ result, activity, action, }: {
28
+ result: WriteOutcome;
29
+ activity: string;
30
+ action: string;
31
+ }): WriteReport;
44
32
  export default class FireAction extends Command {
45
33
  static description: string;
46
34
  static examples: string[];
@@ -48,16 +36,15 @@ export default class FireAction extends Command {
48
36
  instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
49
37
  };
50
38
  static flags: {
39
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
40
+ as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
41
+ 'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
51
42
  activity: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
52
43
  action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
53
44
  param: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
54
- as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
55
- 'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
56
- json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
57
45
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
58
46
  };
59
47
  run(): Promise<void>;
60
48
  private listActions;
61
49
  private fireOne;
62
50
  }
63
- export {};