@sanity/workflow-cli 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +485 -0
  2. package/README.md +145 -63
  3. package/bin/run.js +0 -4
  4. package/dist/commands/abort.d.ts +8 -12
  5. package/dist/commands/abort.js +24 -34
  6. package/dist/commands/definition/delete.d.ts +6 -6
  7. package/dist/commands/definition/delete.js +17 -33
  8. package/dist/commands/definition/diff.d.ts +10 -3
  9. package/dist/commands/definition/diff.js +21 -29
  10. package/dist/commands/definition/list.d.ts +5 -6
  11. package/dist/commands/definition/list.js +44 -50
  12. package/dist/commands/definition/show.d.ts +22 -13
  13. package/dist/commands/definition/show.js +25 -36
  14. package/dist/commands/deploy.d.ts +67 -8
  15. package/dist/commands/deploy.js +136 -32
  16. package/dist/commands/diagnose.d.ts +22 -4
  17. package/dist/commands/diagnose.js +45 -61
  18. package/dist/commands/fire-action.d.ts +7 -30
  19. package/dist/commands/fire-action.js +42 -115
  20. package/dist/commands/list.d.ts +9 -10
  21. package/dist/commands/list.js +49 -62
  22. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  23. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -3
  24. package/dist/commands/set-stage.d.ts +27 -3
  25. package/dist/commands/set-stage.js +63 -12
  26. package/dist/commands/show.d.ts +3 -2
  27. package/dist/commands/show.js +15 -21
  28. package/dist/commands/start.d.ts +56 -0
  29. package/dist/commands/start.js +133 -0
  30. package/dist/commands/tail.d.ts +5 -2
  31. package/dist/commands/tail.js +25 -26
  32. package/dist/hooks/finally/telemetry.d.ts +8 -0
  33. package/dist/hooks/finally/telemetry.js +13 -0
  34. package/dist/hooks/prerun/telemetry.d.ts +5 -0
  35. package/dist/hooks/prerun/telemetry.js +5 -0
  36. package/dist/index.js +0 -3
  37. package/dist/lib/base-command.d.ts +14 -0
  38. package/dist/lib/base-command.js +10 -0
  39. package/dist/lib/client.d.ts +11 -14
  40. package/dist/lib/client.js +29 -34
  41. package/dist/lib/context.d.ts +98 -0
  42. package/dist/lib/context.js +83 -0
  43. package/dist/lib/definitions.d.ts +38 -29
  44. package/dist/lib/definitions.js +34 -93
  45. package/dist/lib/diff.d.ts +5 -2
  46. package/dist/lib/diff.js +62 -20
  47. package/dist/lib/env.d.ts +4 -6
  48. package/dist/lib/env.js +4 -9
  49. package/dist/lib/fail.d.ts +12 -0
  50. package/dist/lib/fail.js +25 -16
  51. package/dist/lib/flags.d.ts +7 -2
  52. package/dist/lib/flags.js +7 -3
  53. package/dist/lib/load-config.d.ts +15 -0
  54. package/dist/lib/load-config.js +89 -0
  55. package/dist/lib/operation-args.d.ts +20 -15
  56. package/dist/lib/operation-args.js +9 -40
  57. package/dist/lib/ops-report.d.ts +29 -13
  58. package/dist/lib/ops-report.js +20 -17
  59. package/dist/lib/params.d.ts +7 -0
  60. package/dist/lib/params.js +18 -0
  61. package/dist/lib/read-fanout.d.ts +22 -0
  62. package/dist/lib/read-fanout.js +28 -0
  63. package/dist/lib/select-deployment.d.ts +31 -0
  64. package/dist/lib/select-deployment.js +37 -0
  65. package/dist/lib/share-definitions.d.ts +86 -0
  66. package/dist/lib/share-definitions.js +106 -0
  67. package/dist/lib/stub.js +0 -7
  68. package/dist/lib/telemetry-setup.d.ts +66 -0
  69. package/dist/lib/telemetry-setup.js +92 -0
  70. package/dist/lib/telemetry.d.ts +100 -0
  71. package/dist/lib/telemetry.js +89 -0
  72. package/dist/lib/ui.d.ts +33 -1
  73. package/dist/lib/ui.js +32 -21
  74. package/oclif.manifest.json +133 -113
  75. package/package.json +16 -8
  76. package/dist/commands/move-stage.d.ts +0 -52
  77. package/dist/commands/move-stage.js +0 -86
  78. package/dist/lib/config.d.ts +0 -18
  79. package/dist/lib/config.js +0 -50
package/README.md CHANGED
@@ -4,9 +4,9 @@ Command-line tool for deploying, inspecting, and administering Sanity workflow
4
4
  definitions and instances.
5
5
 
6
6
  > [!WARNING]
7
- > Early access, restricted. `retry-activity` and `set-stage` are still stubs
8
- > they print their parsed input and exit non-zero. The "wired" commands below
9
- > talk to a real Sanity dataset using `SANITY_*` env vars.
7
+ > Early access, restricted. The commands below talk to a real Sanity dataset,
8
+ > configured by a `sanity.workflow.ts` file and authenticated with
9
+ > `sanity login`.
10
10
 
11
11
  ## Run
12
12
 
@@ -20,39 +20,82 @@ pnpm --filter @sanity/workflow-cli dev list --in-flight
20
20
  pnpm --filter @sanity/workflow-cli dev show wf-instance.abc123
21
21
  ```
22
22
 
23
- Copy the template and fill it in first: `cp ../../.env.example ../../.env`
24
- (from the repo root, `cp .env.example .env`). Env vars are read from
25
- the root `.env` (`SANITY_PROJECT_ID`, `SANITY_DATASET`,
26
- `SANITY_AUTH_TOKEN`). The dev script loads them via `tsx --env-file`.
27
-
28
- ### Workflow target (resource + tags)
29
-
30
- The CLI targets one engine "resource" (a dataset, canvas, media
31
- library, or dashboard) tagged with one or more engine tags. Defaults
32
- match the bench fixtures the rest of this repo uses.
33
-
34
- | Env var | Default | Allowed values |
35
- | ------------------------ | ------------ | ------------------------------------------------------- |
36
- | `WORKFLOW_RESOURCE_TYPE` | `dataset` | `dataset` / `canvas` / `media-library` / `dashboard` |
37
- | `WORKFLOW_RESOURCE_ID` | `test.test` | any string |
38
- | `WORKFLOW_TAG` | _(required)_ | single environment tag, e.g. `prod` (`--tag` overrides) |
39
- | `WORKFLOW_DEFS` | _(required)_ | module path or package exporting `allDefinitions` |
40
-
41
- Invalid values fail with a clean error before the command runs.
42
-
43
- `WORKFLOW_DEFS` has no default — `deploy` is agnostic about which
44
- definitions it ships, so it must be told where to find them. Point it at a
45
- module (a path like `./src/workflows.ts`, or a package name) whose
46
- `allDefinitions` export is a `WorkflowDefinition[]`. This repo's own demo
47
- set lives in `@sanity/workflow-examples`, so `WORKFLOW_DEFS=@sanity/workflow-examples`
48
- deploys it.
23
+ Authenticate once with `sanity login` (the CLI reads that session token); for
24
+ CI, set `SANITY_AUTH_TOKEN` instead. Then create a `sanity.workflow.ts` in the
25
+ directory you run from — see [Configuration](#configuration).
26
+
27
+ The token must span **every resource the workflow references**, not just the
28
+ workflow resource. Runtime commands build one client from the deployment
29
+ descriptor; when a workflow's subject or `doc.ref` fields point into other
30
+ datasets (or a canvas / media library), the engine reaches them through
31
+ sibling clients derived from that same token. A `sanity login` session or an
32
+ org-capable token covers this out of the box; a token scoped to a single
33
+ project will fail on cross-resource reads.
34
+
35
+ ## Configuration
36
+
37
+ The CLI is configured by a `sanity.workflow.ts` (or `.js`/`.mjs`) discovered in
38
+ the directory you run from. It exports a config built with
39
+ `defineWorkflowConfig`, declaring one **deployment** per environment:
40
+
41
+ ```ts
42
+ import {defineWorkflowConfig} from '@sanity/workflow-engine/define'
43
+
44
+ import {articleReview, urlDraft} from './src/workflows.ts'
45
+
46
+ export default defineWorkflowConfig({
47
+ deployments: [
48
+ {
49
+ name: 'production',
50
+ tag: 'prod', // the environment partition the engine's docs are scoped to
51
+ workflowResource: {type: 'dataset', id: 'acme.workflows'}, // where those docs live
52
+ resourceAliases: [
53
+ // binds each `@<handle>:` a definition references to a physical resource
54
+ // in a DIFFERENT dataset from `workflowResource` (same-resource content
55
+ // needs no alias — see below)
56
+ {name: 'content', resource: {type: 'dataset', id: 'acme.content'}},
57
+ {name: 'assets', resource: {type: 'dataset', id: 'acme.assets'}},
58
+ ],
59
+ definitions: [articleReview, urlDraft], // the batch this deployment ships
60
+ },
61
+ ],
62
+ })
63
+ ```
49
64
 
50
- Run through the `dev` / `cli` scripts and this loads straight from
51
- TypeScript source no build step between editing a definition and
52
- deploying it. Those scripts set `NODE_OPTIONS='--conditions=development'`
53
- and run under `tsx`, so a package specifier resolves to its `src/` via the
54
- `development` export condition and the `.ts` is transpiled on the fly. A
55
- built or standalone CLI instead resolves the package's compiled `dist`.
65
+ Pick a deployment with `--tag <tag>`; with a single deployment configured you
66
+ can omit it, and with several configured a bare `deploy` fails asking for
67
+ `--tag` or `--all-tags`. `--all-tags` deploys every deployment in the config in
68
+ one run: a failure in one doesn't stop the rest the run continues, prints a
69
+ summary of what failed, and exits non-zero. The client's project + dataset are derived
70
+ from the deployment's `workflowResource`, and `deploy` expands each
71
+ definition's `@<handle>:` reference to the bound physical resource. An invalid
72
+ config fails with a clean, path-prefixed error before the command runs.
73
+
74
+ `resourceAliases` is only for content that lives in a _different_ resource from
75
+ `workflowResource`. A definition can reference a document in the **same**
76
+ resource by bare id, no alias needed — bare ids root at `workflowResource` at
77
+ runtime. Rule of thumb: **different resource → bind an alias and reference it as
78
+ `@<handle>:<id>`; same resource → use a bare id and omit `resourceAliases`
79
+ entirely.** So the simplest single-dataset setup is just a `workflowResource`
80
+ and `definitions`, with no `resourceAliases` at all.
81
+
82
+ | Var | Purpose |
83
+ | ------------------- | ---------------------------------------------------------------------------------------------------- |
84
+ | `SANITY_AUTH_TOKEN` | Explicit token for CI / scripted use — wins over the `sanity login` session (editor role for writes) |
85
+ | `SANITY_API_HOST` | Optional API host override (defaults to the prod API) |
86
+
87
+ Run through the `dev` / `cli` scripts and the CLI itself and your
88
+ `sanity.workflow.ts` load straight from TypeScript source: the scripts set
89
+ `NODE_OPTIONS='--conditions=development'` under `tsx`, and the config file is
90
+ transpiled on the fly by `jiti`. Definitions you author **inline** in the
91
+ config, or import by **relative path**, transpile the same way — no build step.
92
+
93
+ One caveat: a definition imported from a _separate workspace package_ (e.g.
94
+ `@sanity/workflow-examples`) resolves to that package's built `dist/`. `jiti`
95
+ applies its own module resolution and does **not** honour
96
+ `--conditions=development`, so it never picks up a package's `src/` export
97
+ condition — rebuild that package after editing it, or the config loads its
98
+ stale compiled output.
56
99
 
57
100
  > [!NOTE]
58
101
  > pnpm intercepts a handful of its own flag names (`--check`,
@@ -75,25 +118,58 @@ built or standalone CLI instead resolves the package's compiled `dist`.
75
118
 
76
119
  ## Command status
77
120
 
78
- | Command | Status |
79
- | -------------------------------------------- | ----------------------------------------------------------------------------------------- |
80
- | `deploy` | wired — calls `workflow.deployDefinitions` over the `WORKFLOW_DEFS` module |
81
- | `deploy --check` | wired — runs `validateDefinition` over the local batch + a duplicate-id check |
82
- | `deploy --dry-run` | wired — fetches existing docs and renders a coloured JSON diff per change |
83
- | `deploy --only <id>` | wired — filters deploy/check/dry-run to one `definition` |
84
- | `list` | wired — `client.fetch` over `workflow.instance` documents |
85
- | `show <instance-id>` | wired — `client.getDocument` |
86
- | `diagnose <instance-id>` | wired — one `workflow.evaluate` call, classifies why the instance is/isn't progressing |
87
- | `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
88
- | `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, lifts guards) |
89
- | `retry-activity <instance-id> <activity-id>` | stub |
90
- | `move-stage <instance-id> --to <stage-id>` | wired — calls `workflow.setStage` (runs guards + onEnter effects) |
91
- | `fire-action <instance-id>` | wired — `workflow.evaluate` lists actions; `workflow.fireAction` fires one |
92
- | `set-stage <instance-id> --to <stage-id>` | stub (engine has no guard-bypass path yet) |
93
- | `definition list` | wired — `client.fetch` over `workflow.definition` documents |
94
- | `definition show <id>` | wired — `client.fetch`, latest version unless `--version` |
95
- | `definition diff <id>` | wired — diffs the in-code definition against the deployed latest (`--version` to pin) |
96
- | `definition delete <name>` | wired — calls `workflow.deleteDefinition` (refuses on live instances unless `--cascade`) |
121
+ | Command | Status |
122
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
123
+ | `deploy` | wired — calls `workflow.deployDefinitions` over the selected deployment's definitions |
124
+ | `deploy --all-tags` | wired — deploys every deployment in the config, continuing past per-deployment failures |
125
+ | `deploy --check` | wired — runs `validateDefinition` over the local batch + a duplicate-name check |
126
+ | `deploy --dry-run` | wired — fetches existing docs and renders a coloured JSON diff per change |
127
+ | `deploy --only <name>` | wired — filters deploy/check/dry-run to one definition by `name` |
128
+ | `start <name>` | wired — calls `workflow.startInstance` (`--field` for input fields) |
129
+ | `list` | wired — `client.fetch` over `sanity.workflow.instance` documents (`--definition <name>` to filter) |
130
+ | `show <instance-id>` | wired — `client.getDocument` |
131
+ | `diagnose <instance-id>` | wired — calls `workflow.diagnose`, classifies why the instance is/isn't progressing |
132
+ | `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
133
+ | `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, lifts guards) |
134
+ | `set-stage <instance-id> --to <stage>` | wired — calls `workflow.setStage` (admin override: skips declared transitions/filters; enter lifecycle + cascade still run) |
135
+ | `fire-action <instance-id>` | wired `workflow.availableActions` lists actions; `workflow.fireAction` fires one |
136
+ | `definition list` | wired — `client.fetch` over `sanity.workflow.definition` documents |
137
+ | `definition show <name>` | wired — `client.fetch`, latest version unless `--version` |
138
+ | `definition diff <name>` | wired — diffs the in-code definition against the deployed latest (`--version` to pin) |
139
+ | `definition delete <name>` | wired — calls `workflow.deleteDefinition` (refuses on live instances unless `--cascade`) |
140
+
141
+ ## Telemetry
142
+
143
+ The CLI collects usage telemetry through Sanity's standard pipeline: a
144
+ per-command trace (`Editorial Workflows CLI Command Executed` — the command id, the
145
+ names of declared flags used, never their values, and a success flag) plus
146
+ the engine's adoption events from the operations it drives. Consent is the
147
+ account-wide status managed by `npx sanity telemetry enable|disable|status`;
148
+ CI and trueish `DO_NOT_TRACK` suppress everything, and a session that isn't
149
+ logged in sends nothing. A one-time notice on stderr discloses collection on
150
+ first use.
151
+
152
+ One extension point: supply your own logger as `telemetry` in
153
+ `sanity.workflow.ts` and the built-in pipeline is not constructed at all —
154
+ every event flows to your implementation unconditionally (CI included), and
155
+ consent, suppression, and destination become its business.
156
+
157
+ ## Definition sharing
158
+
159
+ Separate from telemetry, `deploy --share-definitions-with-sanity` donates the
160
+ definition documents the deploy newly created to Sanity — verbatim (structure,
161
+ names, titles, GROQ filters, effect configuration, seeded values), plus their
162
+ deployment coordinates (project and dataset, or resource id) — never content
163
+ documents, instances, or your auth token. The documents go to Sanity's
164
+ first-party definition-feedback endpoint, not the telemetry pipeline;
165
+ telemetry carries only a content-free marker (content hash plus structural
166
+ counts) per shared definition.
167
+
168
+ Sharing is opt-in by the flag alone, per invocation: the CLI never prompts
169
+ and nothing persists. An interactive deploy that created new definition
170
+ versions without the flag logs a one-line stderr hint pointing at it — CI /
171
+ non-TTY runs stay silent. Telemetry consent and `DO_NOT_TRACK` play no part:
172
+ sharing is its own explicit consent, given per deploy.
97
173
 
98
174
  ## Known gaps
99
175
 
@@ -101,15 +177,21 @@ built or standalone CLI instead resolves the package's compiled `dist`.
101
177
  Sanity CLI as a plugin host. The package publishes as a standalone
102
178
  `sanity-workflows` binary today; plugging into `sanity` as an oclif
103
179
  plugin is a separate piece of work.
104
- - **`set-stage` manual override.** The engine's `workflow.setStage`
105
- always runs guards + effects; the proposal's "bypass" semantics
106
- need engine work before this command can do anything useful.
107
- - **Actor identity.** Write paths default the actor to
108
- `{ kind: "system", id: "workflow-cli" }` rather than resolving the real
109
- user via `/users/me` (which a token-only CLI can't reach). `fire-action`
110
- takes `--as <userId>` / `--as-role <role>` to attribute a fire to a real
111
- user, so assignee- and role-gated action filters pass. Once we host inside
112
- the Sanity CLI's auth flow, the real user can flow through everywhere.
180
+ - **No true bypass.** `set-stage` is the engine's `setStage` admin
181
+ override: it skips the definition's declared transitions and filters, but
182
+ the target stage's enter lifecycle and the post-move cascade still run. A
183
+ true force-set that also bypasses guards + enter effects does not exist
184
+ yet it needs engine work that hasn't landed.
185
+
186
+ ## Identity
187
+
188
+ Identity is the token behind the configured client: the engine resolves the
189
+ acting user (and their roles) from `/users/me`, so every write is attributed
190
+ to whoever the token authenticates — there is no identity injection and no
191
+ `--as` impersonation. Alongside that "who", the CLI declares an advisory
192
+ "via what": every verb carries
193
+ `executionContext: { kind: "cli", id: "workflow-cli" }`, stamped on history
194
+ entries as provenance.
113
195
 
114
196
  ## Tech stack
115
197
 
package/bin/run.js CHANGED
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Production entry for the published package: loads the compiled commands
3
- // from ./dist/commands (per the `oclif.commands` field). The dev counterpart,
4
- // bin/dev.js, passes `development: true` to load TS sources instead.
5
-
6
2
  import {execute} from '@oclif/core'
7
3
 
8
4
  await execute({dir: import.meta.url})
@@ -1,5 +1,7 @@
1
- import { Command } from '@oclif/core';
2
- export default class Abort extends Command {
1
+ import { type WorkflowInstance } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ import { type WriteReport } from '../lib/ops-report.ts';
4
+ export default class Abort extends WorkflowCommand {
3
5
  static description: string;
4
6
  static examples: string[];
5
7
  static args: {
@@ -12,19 +14,13 @@ export default class Abort extends Command {
12
14
  run(): Promise<void>;
13
15
  }
14
16
  interface AbortOutcome {
15
- fired: boolean;
16
- instance: {
17
- currentStage: string;
18
- completedAt?: string | undefined;
19
- };
17
+ changed: boolean;
18
+ instance: Pick<WorkflowInstance, 'currentStage' | 'completedAt'>;
20
19
  }
21
20
  /**
22
21
  * Turn an `abortInstance` result into the spinner message. Pure so the
23
- * fired / already-terminal permutations can be asserted without driving
22
+ * aborted / already-terminal permutations can be asserted without driving
24
23
  * a real abort.
25
24
  */
26
- export declare function abortReport(result: AbortOutcome): {
27
- fired: boolean;
28
- message: string;
29
- };
25
+ export declare function abortReport(result: AbortOutcome): WriteReport;
30
26
  export {};
@@ -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';
5
- import { loadClient } from "../lib/client.js";
6
- import { loadWorkflowConfig } from "../lib/config.js";
7
- import { fail } from "../lib/fail.js";
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
+ import { resolveInstanceContext } from "../lib/context.js";
8
6
  import { tagFlags } from "../lib/flags.js";
9
7
  import { buildOperationArgs } from "../lib/operation-args.js";
10
- export default class Abort extends Command {
8
+ import { runWriteVerb } from "../lib/ops-report.js";
9
+ export default class Abort extends WorkflowCommand {
11
10
  static description = 'Abort an in-flight workflow instance — a hard stop: pending effects are cancelled, stage guards lifted, and the instance is marked terminal where it stands.';
12
11
  static examples = [
13
12
  '<%= config.bin %> abort wf-instance.abc123',
@@ -22,41 +21,32 @@ export default class Abort extends Command {
22
21
  };
23
22
  async run() {
24
23
  const { args, flags } = await this.parse(Abort);
25
- const config = loadWorkflowConfig(flags.tag);
26
- const client = loadClient();
27
- const spinner = ora(`Aborting ${args.instanceId}…`).start();
28
- try {
29
- const result = await workflow.abortInstance({
24
+ const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
25
+ await runWriteVerb({
26
+ startLabel: `Aborting ${args.instanceId}…`,
27
+ failLabel: 'Abort rejected',
28
+ failHeadline: 'abort error:',
29
+ run: () => workflow.abortInstance({
30
30
  client,
31
- ...buildOperationArgs({ config, instanceId: args.instanceId, reason: flags.reason }),
32
- });
33
- const report = abortReport(result);
34
- if (!report.fired) {
35
- spinner.warn(report.message);
36
- return;
37
- }
38
- spinner.succeed(report.message);
39
- }
40
- catch (error) {
41
- spinner.fail('Abort rejected');
42
- const message = error instanceof Error ? error.message : String(error);
43
- fail('abortInstance error:', message);
44
- }
31
+ ...buildOperationArgs({
32
+ scope,
33
+ instanceId: args.instanceId,
34
+ reason: flags.reason,
35
+ }),
36
+ }),
37
+ report: abortReport,
38
+ log: (line) => this.log(line),
39
+ });
45
40
  }
46
41
  }
47
- /**
48
- * Turn an `abortInstance` result into the spinner message. Pure so the
49
- * fired / already-terminal permutations can be asserted without driving
50
- * a real abort.
51
- */
52
42
  export function abortReport(result) {
53
43
  const stage = styleText('bold', result.instance.currentStage);
54
- if (!result.fired) {
44
+ if (!result.changed) {
55
45
  const at = result.instance.completedAt ?? 'unknown';
56
46
  return {
57
- fired: false,
58
- message: `abortInstance returned without firing — instance is already terminal (at ${stage} since ${at})`,
47
+ changed: false,
48
+ message: `abortInstance changed nothing — instance is already terminal (at ${stage} since ${at})`,
59
49
  };
60
50
  }
61
- return { fired: true, message: `Aborted — instance hard-stopped at ${stage}` };
51
+ return { changed: true, message: `Aborted — instance hard-stopped at ${stage}` };
62
52
  }
@@ -1,7 +1,7 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type DeleteDefinitionArgs, type DeleteDefinitionResult } from '@sanity/workflow-engine';
3
- import { type WorkflowConfig } from '../../lib/config.ts';
4
- export default class DefinitionDelete extends Command {
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ import { type EngineScope } from '../../lib/operation-args.ts';
4
+ export default class DefinitionDelete extends WorkflowCommand {
5
5
  static description: string;
6
6
  static examples: string[];
7
7
  static args: {
@@ -21,15 +21,15 @@ export default class DefinitionDelete extends Command {
21
21
  * engine's optionals reject an explicit `undefined` under
22
22
  * `exactOptionalPropertyTypes`.
23
23
  */
24
- export declare function buildDeleteArgs({ config, name, flags, }: {
25
- config: WorkflowConfig;
24
+ export declare function buildDeleteArgs({ scope, name, flags, }: {
25
+ scope: EngineScope;
26
26
  name: string;
27
27
  flags: {
28
28
  version?: number | undefined;
29
29
  cascade: boolean;
30
30
  reason?: string | undefined;
31
31
  };
32
- }): Omit<DeleteDefinitionArgs, 'client'>;
32
+ }): DeleteDefinitionArgs & EngineScope;
33
33
  /** What the spinner names: the workflow, or one pinned version of it. */
34
34
  export declare function deleteTargetLabel(args: Pick<DeleteDefinitionArgs, 'definition' | 'version'>): string;
35
35
  /**
@@ -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';
5
- import { loadClient } from "../../lib/client.js";
6
- import { loadWorkflowConfig } from "../../lib/config.js";
7
- import { fail } from "../../lib/fail.js";
4
+ import { WorkflowCommand } from "../../lib/base-command.js";
5
+ import { resolveContext } from "../../lib/context.js";
8
6
  import { tagFlags } from "../../lib/flags.js";
9
7
  import { baseEngineArgs } from "../../lib/operation-args.js";
10
- export default class DefinitionDelete extends Command {
8
+ import { runWriteVerb } from "../../lib/ops-report.js";
9
+ export default class DefinitionDelete extends WorkflowCommand {
11
10
  static description = 'Delete a deployed workflow definition (every version, or one via --version). ' +
12
11
  'Refuses while non-terminal instances exist unless --cascade aborts them first — ' +
13
12
  'instances are aborted in place, never deleted.';
@@ -34,45 +33,30 @@ export default class DefinitionDelete extends Command {
34
33
  };
35
34
  async run() {
36
35
  const { args, flags } = await this.parse(DefinitionDelete);
37
- const config = loadWorkflowConfig(flags.tag);
38
- const client = loadClient();
39
- const deleteArgs = buildDeleteArgs({ config, name: args.name, flags });
40
- const spinner = ora(`Deleting definition ${deleteTargetLabel(deleteArgs)}…`).start();
41
- try {
42
- const result = await workflow.deleteDefinition({ client, ...deleteArgs });
43
- spinner.succeed(deleteReport(result));
44
- }
45
- catch (error) {
46
- spinner.fail('Delete rejected');
47
- const message = error instanceof Error ? error.message : String(error);
48
- fail('deleteDefinition error:', message);
49
- }
36
+ const { deployment, client } = await resolveContext(flags);
37
+ const deleteArgs = buildDeleteArgs({ scope: deployment, name: args.name, flags });
38
+ await runWriteVerb({
39
+ startLabel: `Deleting definition ${deleteTargetLabel(deleteArgs)}…`,
40
+ failLabel: 'Delete rejected',
41
+ failHeadline: 'definition delete error:',
42
+ run: () => workflow.deleteDefinition({ client, ...deleteArgs }),
43
+ report: (result) => ({ changed: true, message: deleteReport(result) }),
44
+ log: (line) => this.log(line),
45
+ });
50
46
  }
51
47
  }
52
- /**
53
- * The shared engine args ({@link baseEngineArgs}) plus the delete-specific
54
- * targeting. `version` and `reason` spread conditionally because the
55
- * engine's optionals reject an explicit `undefined` under
56
- * `exactOptionalPropertyTypes`.
57
- */
58
- export function buildDeleteArgs({ config, name, flags, }) {
48
+ export function buildDeleteArgs({ scope, name, flags, }) {
59
49
  return {
60
- ...baseEngineArgs(config),
50
+ ...baseEngineArgs(scope),
61
51
  definition: name,
62
52
  cascade: flags.cascade,
63
53
  ...(flags.version !== undefined ? { version: flags.version } : {}),
64
54
  ...(flags.reason !== undefined ? { reason: flags.reason } : {}),
65
55
  };
66
56
  }
67
- /** What the spinner names: the workflow, or one pinned version of it. */
68
57
  export function deleteTargetLabel(args) {
69
58
  return args.version === undefined ? args.definition : `${args.definition} v${args.version}`;
70
59
  }
71
- /**
72
- * Turn a `deleteDefinition` result into the spinner message. Pure so the
73
- * version / cascade / guard permutations can be asserted without driving
74
- * a real delete.
75
- */
76
60
  export function deleteReport(result) {
77
61
  const versions = result.deletedVersions.map((v) => `v${v}`).join(', ');
78
62
  const parts = [`Deleted ${styleText('bold', result.name)} ${versions}`];
@@ -1,9 +1,10 @@
1
- import { Command } from '@oclif/core';
2
- export default class DefinitionDiff extends Command {
1
+ import { type WorkflowDefinition } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ export default class DefinitionDiff extends WorkflowCommand {
3
4
  static description: string;
4
5
  static examples: string[];
5
6
  static args: {
6
- definition: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
8
  };
8
9
  static flags: {
9
10
  version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -11,3 +12,9 @@ export default class DefinitionDiff extends Command {
11
12
  };
12
13
  run(): Promise<void>;
13
14
  }
15
+ /**
16
+ * Pick the single definition to diff by name and validate it, failing cleanly
17
+ * when it's missing from the deployment or doesn't pass validation. Extracted
18
+ * from `run` so the command's orchestration stays thin and this stays testable.
19
+ */
20
+ export declare function selectDefinitionToDiff(definitions: WorkflowDefinition[], name: string): WorkflowDefinition;
@@ -1,21 +1,20 @@
1
- import { Args, Command, Flags } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import { diffEntry } from '@sanity/workflow-engine';
3
3
  import ora from 'ora';
4
- import { loadClient } from "../../lib/client.js";
5
- import { loadWorkflowConfig } from "../../lib/config.js";
6
- import { loadValidatedDefinition } from "../../lib/definitions.js";
4
+ import { WorkflowCommand } from "../../lib/base-command.js";
5
+ import { resolveContext } from "../../lib/context.js";
6
+ import { fetchDeployedDefinition, selectDefinitions, validateOrFail } from "../../lib/definitions.js";
7
7
  import { diffReport } from "../../lib/diff.js";
8
- import { fail } from "../../lib/fail.js";
9
8
  import { tagFlags } from "../../lib/flags.js";
10
- import { buildDefinitionShowQuery } from "./show.js";
11
- export default class DefinitionDiff extends Command {
9
+ import { deploymentToTarget } from "../../lib/select-deployment.js";
10
+ export default class DefinitionDiff extends WorkflowCommand {
12
11
  static description = 'Diff an in-code definition against the deployed version (latest by default).';
13
12
  static examples = [
14
13
  '<%= config.bin %> definition diff productLaunch',
15
14
  '<%= config.bin %> definition diff productLaunch --version 2',
16
15
  ];
17
16
  static args = {
18
- definition: Args.string({ required: true, description: 'Workflow definition name.' }),
17
+ name: Args.string({ required: true, description: 'Workflow definition name.' }),
19
18
  };
20
19
  static flags = {
21
20
  ...tagFlags,
@@ -23,40 +22,33 @@ export default class DefinitionDiff extends Command {
23
22
  };
24
23
  async run() {
25
24
  const { args, flags } = await this.parse(DefinitionDiff);
26
- const config = loadWorkflowConfig(flags.tag);
27
- const def = await loadValidatedDefinition(args.definition);
28
- const client = loadClient();
29
- const spinner = ora(`Diffing ${args.definition} against deployed…`).start();
25
+ const { deployment, client } = await resolveContext(flags);
26
+ const def = selectDefinitionToDiff(deployment.definitions, args.name);
27
+ const target = deploymentToTarget(deployment);
28
+ const spinner = ora(`Diffing ${args.name} against deployed…`).start();
30
29
  let deployed;
31
30
  try {
32
31
  deployed = await fetchDeployedDefinition({
33
32
  client,
34
- name: args.definition,
35
- config,
33
+ name: args.name,
34
+ tag: target.tag,
36
35
  version: flags.version,
37
36
  });
38
- spinner.succeed(`Diffed ${args.definition}`);
37
+ spinner.succeed(`Diffed ${args.name}`);
39
38
  }
40
39
  catch (error) {
41
40
  spinner.fail('Diff failed');
42
41
  throw error;
43
42
  }
44
- for (const line of diffReport([diffEntry({ def, latestRaw: deployed, target: config })]))
43
+ for (const line of diffReport([diffEntry({ def, latestRaw: deployed, target })]))
45
44
  this.log(line);
46
45
  }
47
46
  }
48
- /**
49
- * Fetch the deployed document to compare against. Unlike `deploy
50
- * --dry-run` (which looks up the exact docId, so a version bump always
51
- * reads as "create"), diff targets the deployed version that would be
52
- * superseded — latest unless pinned via `version`. An absent latest is a
53
- * legitimate "create" diff, but an absent *pinned* version is an error.
54
- */
55
- async function fetchDeployedDefinition({ client, name, config, version, }) {
56
- const { groq, params } = buildDefinitionShowQuery({ name, tag: config.tag, version });
57
- const deployed = await client.fetch(groq, params);
58
- if (deployed === null && version !== undefined) {
59
- fail(`No deployed definition "${name}" v${version}.`);
47
+ export function selectDefinitionToDiff(definitions, name) {
48
+ const [def] = selectDefinitions(definitions, { only: name });
49
+ if (def === undefined) {
50
+ throw new Error(`selectDefinitions returned no match for "${name}"`);
60
51
  }
61
- return deployed ?? undefined;
52
+ validateOrFail([def]);
53
+ return def;
62
54
  }
@@ -1,8 +1,7 @@
1
- import { Command } from '@oclif/core';
2
- import { type WorkflowRole } from '@sanity/workflow-engine';
1
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
2
  interface DefinitionListFlags {
4
3
  tag?: string | undefined;
5
- 'workflow-name'?: string | undefined;
4
+ name?: string | undefined;
6
5
  limit: number;
7
6
  }
8
7
  export interface DefinitionListRow {
@@ -10,21 +9,21 @@ export interface DefinitionListRow {
10
9
  version: number;
11
10
  title: string;
12
11
  tag: string;
13
- role?: WorkflowRole;
14
12
  stageCount: number;
15
13
  inFlightCount: number;
16
14
  totalInstances: number;
15
+ _createdAt: string;
17
16
  }
18
17
  export declare function buildDefinitionListQuery(flags: DefinitionListFlags): {
19
18
  groq: string;
20
19
  params: Record<string, unknown>;
21
20
  };
22
- export default class DefinitionList extends Command {
21
+ export default class DefinitionList extends WorkflowCommand {
23
22
  static description: string;
24
23
  static examples: string[];
25
24
  static flags: {
26
25
  limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
27
- 'workflow-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
27
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
28
  };
30
29
  run(): Promise<void>;