@sanity/workflow-cli 0.8.1 → 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 (57) 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 +4 -4
  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 +26 -25
  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 +4 -17
  12. package/dist/commands/deploy.d.ts +58 -5
  13. package/dist/commands/deploy.js +134 -27
  14. package/dist/commands/diagnose.js +16 -26
  15. package/dist/commands/fire-action.d.ts +7 -28
  16. package/dist/commands/fire-action.js +29 -68
  17. package/dist/commands/list.d.ts +2 -2
  18. package/dist/commands/list.js +8 -8
  19. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  20. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -2
  21. package/dist/commands/set-stage.d.ts +27 -3
  22. package/dist/commands/set-stage.js +80 -13
  23. package/dist/commands/show.d.ts +1 -0
  24. package/dist/commands/show.js +9 -3
  25. package/dist/commands/start.d.ts +59 -0
  26. package/dist/commands/start.js +169 -0
  27. package/dist/commands/tail.d.ts +3 -0
  28. package/dist/commands/tail.js +7 -3
  29. package/dist/lib/client.d.ts +11 -14
  30. package/dist/lib/client.js +45 -33
  31. package/dist/lib/context.d.ts +62 -0
  32. package/dist/lib/context.js +82 -0
  33. package/dist/lib/definitions.d.ts +38 -29
  34. package/dist/lib/definitions.js +45 -85
  35. package/dist/lib/diff.js +8 -0
  36. package/dist/lib/env.d.ts +0 -6
  37. package/dist/lib/env.js +3 -9
  38. package/dist/lib/fail.d.ts +6 -0
  39. package/dist/lib/fail.js +11 -1
  40. package/dist/lib/flags.d.ts +14 -2
  41. package/dist/lib/flags.js +24 -3
  42. package/dist/lib/load-config.d.ts +11 -0
  43. package/dist/lib/load-config.js +69 -0
  44. package/dist/lib/operation-args.d.ts +23 -9
  45. package/dist/lib/operation-args.js +12 -11
  46. package/dist/lib/ops-report.d.ts +14 -6
  47. package/dist/lib/ops-report.js +5 -6
  48. package/dist/lib/params.d.ts +7 -0
  49. package/dist/lib/params.js +26 -0
  50. package/dist/lib/select-deployment.d.ts +31 -0
  51. package/dist/lib/select-deployment.js +58 -0
  52. package/oclif.manifest.json +145 -86
  53. package/package.json +6 -4
  54. package/dist/commands/move-stage.d.ts +0 -52
  55. package/dist/commands/move-stage.js +0 -86
  56. package/dist/lib/config.d.ts +0 -18
  57. 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,74 @@ 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
+ ## Configuration
28
+
29
+ The CLI is configured by a `sanity.workflow.ts` (or `.js`/`.mjs`) discovered in
30
+ the directory you run from. It exports a config built with
31
+ `defineWorkflowConfig`, declaring one **deployment** per environment:
32
+
33
+ ```ts
34
+ import {defineWorkflowConfig} from '@sanity/workflow-engine/define'
35
+
36
+ import {articleReview, urlDraft} from './src/workflows.ts'
37
+
38
+ export default defineWorkflowConfig({
39
+ deployments: [
40
+ {
41
+ name: 'production',
42
+ tag: 'prod', // the environment partition the engine's docs are scoped to
43
+ workflowResource: {type: 'dataset', id: 'acme.workflows'}, // where those docs live
44
+ resourceAliases: [
45
+ // binds each `@<handle>:` a definition references to a physical resource
46
+ // in a DIFFERENT dataset from `workflowResource` (same-resource content
47
+ // needs no alias see below)
48
+ {name: 'content', resource: {type: 'dataset', id: 'acme.content'}},
49
+ {name: 'assets', resource: {type: 'dataset', id: 'acme.assets'}},
50
+ ],
51
+ definitions: [articleReview, urlDraft], // the batch this deployment ships
52
+ },
53
+ ],
54
+ })
55
+ ```
49
56
 
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`.
57
+ Pick a deployment with `--tag <tag>`; with a single deployment configured you
58
+ can omit it, and with several configured a bare `deploy` fails asking for
59
+ `--tag` or `--all-tags`. `--all-tags` deploys every deployment in the config in
60
+ one run: a failure in one doesn't stop the rest the run continues, prints a
61
+ summary of what failed, and exits non-zero. The client's project + dataset are derived
62
+ from the deployment's `workflowResource`, and `deploy` expands each
63
+ definition's `@<handle>:` reference to the bound physical resource. An invalid
64
+ config fails with a clean, path-prefixed error before the command runs.
65
+
66
+ `resourceAliases` is only for content that lives in a _different_ resource from
67
+ `workflowResource`. A definition can reference a document in the **same**
68
+ resource by bare id, no alias needed — bare ids root at `workflowResource` at
69
+ runtime. Rule of thumb: **different resource → bind an alias and reference it as
70
+ `@<handle>:<id>`; same resource → use a bare id and omit `resourceAliases`
71
+ entirely.** So the simplest single-dataset setup is just a `workflowResource`
72
+ and `definitions`, with no `resourceAliases` at all.
73
+
74
+ | Var | Purpose |
75
+ | ------------------- | --------------------------------------------------------------------------- |
76
+ | `SANITY_AUTH_TOKEN` | CI fallback when there's no `sanity login` session (editor role for writes) |
77
+ | `SANITY_API_HOST` | Optional API host override (defaults to the prod API) |
78
+
79
+ Run through the `dev` / `cli` scripts and the CLI itself and your
80
+ `sanity.workflow.ts` load straight from TypeScript source: the scripts set
81
+ `NODE_OPTIONS='--conditions=development'` under `tsx`, and the config file is
82
+ transpiled on the fly by `jiti`. Definitions you author **inline** in the
83
+ config, or import by **relative path**, transpile the same way — no build step.
84
+
85
+ One caveat: a definition imported from a _separate workspace package_ (e.g.
86
+ `@sanity/workflow-examples`) resolves to that package's built `dist/`. `jiti`
87
+ applies its own module resolution and does **not** honour
88
+ `--conditions=development`, so it never picks up a package's `src/` export
89
+ condition — rebuild that package after editing it, or the config loads its
90
+ stale compiled output.
56
91
 
57
92
  > [!NOTE]
58
93
  > pnpm intercepts a handful of its own flag names (`--check`,
@@ -75,25 +110,25 @@ built or standalone CLI instead resolves the package's compiled `dist`.
75
110
 
76
111
  ## Command status
77
112
 
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`) |
113
+ | Command | Status |
114
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
115
+ | `deploy` | wired — calls `workflow.deployDefinitions` over the selected deployment's definitions |
116
+ | `deploy --all-tags` | wired — deploys every deployment in the config, continuing past per-deployment failures |
117
+ | `deploy --check` | wired — runs `validateDefinition` over the local batch + a duplicate-name check |
118
+ | `deploy --dry-run` | wired — fetches existing docs and renders a coloured JSON diff per change |
119
+ | `deploy --only <name>` | wired — filters deploy/check/dry-run to one definition by `name` |
120
+ | `start <name>` | wired — calls `workflow.startInstance` (`--field` for input fields, `--as` for attribution) |
121
+ | `list` | wired — `client.fetch` over `workflow.instance` documents (`--definition <name>` to filter) |
122
+ | `show <instance-id>` | wired — `client.getDocument` |
123
+ | `diagnose <instance-id>` | wired — one `workflow.evaluate` call, classifies why the instance is/isn't progressing |
124
+ | `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
125
+ | `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, lifts guards) |
126
+ | `set-stage <instance-id> --to <stage>` | wired — calls `workflow.setStage` (admin override: skips declared transitions/filters; enter lifecycle + cascade still run) |
127
+ | `fire-action <instance-id>` | wired `workflow.evaluate` lists actions; `workflow.fireAction` fires one |
128
+ | `definition list` | wired — `client.fetch` over `workflow.definition` documents |
129
+ | `definition show <name>` | wired — `client.fetch`, latest version unless `--version` |
130
+ | `definition diff <name>` | wired — diffs the in-code definition against the deployed latest (`--version` to pin) |
131
+ | `definition delete <name>` | wired — calls `workflow.deleteDefinition` (refuses on live instances unless `--cascade`) |
97
132
 
98
133
  ## Known gaps
99
134
 
@@ -101,15 +136,18 @@ built or standalone CLI instead resolves the package's compiled `dist`.
101
136
  Sanity CLI as a plugin host. The package publishes as a standalone
102
137
  `sanity-workflows` binary today; plugging into `sanity` as an oclif
103
138
  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.
139
+ - **No true bypass.** `set-stage` is the engine's `setStage` admin
140
+ override: it skips the definition's declared transitions and filters, but
141
+ the target stage's enter lifecycle and the post-move cascade still run. A
142
+ command that also bypasses guards + enter effects (`force-stage`) needs
143
+ engine work that hasn't landed.
107
144
  - **Actor identity.** Write paths default the actor to
108
145
  `{ 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.
146
+ user via `/users/me` (which a token-only CLI can't reach). `start` and
147
+ `fire-action` take `--as <userId>` / `--as-role <role>` to attribute the
148
+ write to a real user, so assignee- and role-gated filters pass. Once we
149
+ host inside the Sanity CLI's auth flow, the real user can flow through
150
+ everywhere.
113
151
 
114
152
  ## Tech stack
115
153
 
@@ -1,4 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { type WorkflowInstance } from '@sanity/workflow-engine';
2
3
  export default class Abort extends Command {
3
4
  static description: string;
4
5
  static examples: string[];
@@ -12,19 +13,16 @@ export default class Abort extends Command {
12
13
  run(): Promise<void>;
13
14
  }
14
15
  interface AbortOutcome {
15
- fired: boolean;
16
- instance: {
17
- currentStage: string;
18
- completedAt?: string | undefined;
19
- };
16
+ changed: boolean;
17
+ instance: Pick<WorkflowInstance, 'currentStage' | 'completedAt'>;
20
18
  }
21
19
  /**
22
20
  * Turn an `abortInstance` result into the spinner message. Pure so the
23
- * fired / already-terminal permutations can be asserted without driving
21
+ * aborted / already-terminal permutations can be asserted without driving
24
22
  * a real abort.
25
23
  */
26
24
  export declare function abortReport(result: AbortOutcome): {
27
- fired: boolean;
25
+ changed: boolean;
28
26
  message: string;
29
27
  };
30
28
  export {};
@@ -2,9 +2,8 @@ import { styleText } from 'node:util';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
3
  import { workflow } from '@sanity/workflow-engine';
4
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";
5
+ import { resolveInstanceContext } from "../lib/context.js";
6
+ import { errorMessage, fail } from "../lib/fail.js";
8
7
  import { tagFlags } from "../lib/flags.js";
9
8
  import { buildOperationArgs } from "../lib/operation-args.js";
10
9
  export default class Abort extends Command {
@@ -22,16 +21,19 @@ 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();
24
+ const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
27
25
  const spinner = ora(`Aborting ${args.instanceId}…`).start();
28
26
  try {
29
27
  const result = await workflow.abortInstance({
30
28
  client,
31
- ...buildOperationArgs({ config, instanceId: args.instanceId, reason: flags.reason }),
29
+ ...buildOperationArgs({
30
+ scope,
31
+ instanceId: args.instanceId,
32
+ reason: flags.reason,
33
+ }),
32
34
  });
33
35
  const report = abortReport(result);
34
- if (!report.fired) {
36
+ if (!report.changed) {
35
37
  spinner.warn(report.message);
36
38
  return;
37
39
  }
@@ -39,24 +41,23 @@ export default class Abort extends Command {
39
41
  }
40
42
  catch (error) {
41
43
  spinner.fail('Abort rejected');
42
- const message = error instanceof Error ? error.message : String(error);
43
- fail('abortInstance error:', message);
44
+ fail('abort error:', errorMessage(error));
44
45
  }
45
46
  }
46
47
  }
47
48
  /**
48
49
  * Turn an `abortInstance` result into the spinner message. Pure so the
49
- * fired / already-terminal permutations can be asserted without driving
50
+ * aborted / already-terminal permutations can be asserted without driving
50
51
  * a real abort.
51
52
  */
52
53
  export function abortReport(result) {
53
54
  const stage = styleText('bold', result.instance.currentStage);
54
- if (!result.fired) {
55
+ if (!result.changed) {
55
56
  const at = result.instance.completedAt ?? 'unknown';
56
57
  return {
57
- fired: false,
58
- message: `abortInstance returned without firing — instance is already terminal (at ${stage} since ${at})`,
58
+ changed: false,
59
+ message: `abortInstance changed nothing — instance is already terminal (at ${stage} since ${at})`,
59
60
  };
60
61
  }
61
- return { fired: true, message: `Aborted — instance hard-stopped at ${stage}` };
62
+ return { changed: true, message: `Aborted — instance hard-stopped at ${stage}` };
62
63
  }
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import { type DeleteDefinitionArgs, type DeleteDefinitionResult } from '@sanity/workflow-engine';
3
- import { type WorkflowConfig } from '../../lib/config.ts';
3
+ import { type EngineScope } from '../../lib/operation-args.ts';
4
4
  export default class DefinitionDelete extends Command {
5
5
  static description: string;
6
6
  static examples: string[];
@@ -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
  /**
@@ -2,11 +2,10 @@ import { styleText } from 'node:util';
2
2
  import { Args, Command, Flags } from '@oclif/core';
3
3
  import { workflow, } from '@sanity/workflow-engine';
4
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";
5
+ import { resolveContext } from "../../lib/context.js";
6
+ import { errorMessage, fail } from "../../lib/fail.js";
8
7
  import { tagFlags } from "../../lib/flags.js";
9
- import { baseEngineArgs } from "../../lib/operation-args.js";
8
+ import { SYSTEM_ACTOR, baseEngineArgs } from "../../lib/operation-args.js";
10
9
  export default class DefinitionDelete extends Command {
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 — ' +
@@ -34,9 +33,8 @@ 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 });
36
+ const { deployment, client } = await resolveContext(flags);
37
+ const deleteArgs = buildDeleteArgs({ scope: deployment, name: args.name, flags });
40
38
  const spinner = ora(`Deleting definition ${deleteTargetLabel(deleteArgs)}…`).start();
41
39
  try {
42
40
  const result = await workflow.deleteDefinition({ client, ...deleteArgs });
@@ -44,8 +42,7 @@ export default class DefinitionDelete extends Command {
44
42
  }
45
43
  catch (error) {
46
44
  spinner.fail('Delete rejected');
47
- const message = error instanceof Error ? error.message : String(error);
48
- fail('deleteDefinition error:', message);
45
+ fail('definition delete error:', errorMessage(error));
49
46
  }
50
47
  }
51
48
  }
@@ -55,9 +52,9 @@ export default class DefinitionDelete extends Command {
55
52
  * engine's optionals reject an explicit `undefined` under
56
53
  * `exactOptionalPropertyTypes`.
57
54
  */
58
- export function buildDeleteArgs({ config, name, flags, }) {
55
+ export function buildDeleteArgs({ scope, name, flags, }) {
59
56
  return {
60
- ...baseEngineArgs(config),
57
+ ...baseEngineArgs(scope, SYSTEM_ACTOR),
61
58
  definition: name,
62
59
  cascade: flags.cascade,
63
60
  ...(flags.version !== undefined ? { version: flags.version } : {}),
@@ -1,9 +1,10 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { type WorkflowDefinition } from '@sanity/workflow-engine';
2
3
  export default class DefinitionDiff extends Command {
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,13 +1,11 @@
1
1
  import { Args, Command, 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 { resolveContext } from "../../lib/context.js";
5
+ import { fetchDeployedDefinition, selectDefinitions, validateOrFail } from "../../lib/definitions.js";
7
6
  import { diffReport } from "../../lib/diff.js";
8
- import { fail } from "../../lib/fail.js";
9
7
  import { tagFlags } from "../../lib/flags.js";
10
- import { buildDefinitionShowQuery } from "./show.js";
8
+ import { deploymentToTarget } from "../../lib/select-deployment.js";
11
9
  export default class DefinitionDiff extends Command {
12
10
  static description = 'Diff an in-code definition against the deployed version (latest by default).';
13
11
  static examples = [
@@ -15,7 +13,7 @@ export default class DefinitionDiff extends Command {
15
13
  '<%= config.bin %> definition diff productLaunch --version 2',
16
14
  ];
17
15
  static args = {
18
- definition: Args.string({ required: true, description: 'Workflow definition name.' }),
16
+ name: Args.string({ required: true, description: 'Workflow definition name.' }),
19
17
  };
20
18
  static flags = {
21
19
  ...tagFlags,
@@ -23,40 +21,43 @@ export default class DefinitionDiff extends Command {
23
21
  };
24
22
  async run() {
25
23
  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();
24
+ const { deployment, client } = await resolveContext(flags);
25
+ const def = selectDefinitionToDiff(deployment.definitions, args.name);
26
+ const target = deploymentToTarget(deployment);
27
+ const spinner = ora(`Diffing ${args.name} against deployed…`).start();
30
28
  let deployed;
31
29
  try {
30
+ // An absent latest is a legitimate "create" diff, so `undefined`
31
+ // flows straight into the diff entry.
32
32
  deployed = await fetchDeployedDefinition({
33
33
  client,
34
- name: args.definition,
35
- config,
34
+ name: args.name,
35
+ tag: target.tag,
36
36
  version: flags.version,
37
37
  });
38
- spinner.succeed(`Diffed ${args.definition}`);
38
+ spinner.succeed(`Diffed ${args.name}`);
39
39
  }
40
40
  catch (error) {
41
41
  spinner.fail('Diff failed');
42
42
  throw error;
43
43
  }
44
- for (const line of diffReport([diffEntry({ def, latestRaw: deployed, target: config })]))
44
+ for (const line of diffReport([diffEntry({ def, latestRaw: deployed, target })]))
45
45
  this.log(line);
46
46
  }
47
47
  }
48
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.
49
+ * Pick the single definition to diff by name and validate it, failing cleanly
50
+ * when it's missing from the deployment or doesn't pass validation. Extracted
51
+ * from `run` so the command's orchestration stays thin and this stays testable.
54
52
  */
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}.`);
53
+ export function selectDefinitionToDiff(definitions, name) {
54
+ const [def] = selectDefinitions(definitions, { only: name });
55
+ // selectDefinitions already fails on no-match (naming the available defs), so
56
+ // a named lookup always yields at least one — this narrows the type, it is
57
+ // not a reachable failure.
58
+ if (def === undefined) {
59
+ throw new Error(`selectDefinitions returned no match for "${name}"`);
60
60
  }
61
- return deployed ?? undefined;
61
+ validateOrFail([def]);
62
+ return def;
62
63
  }
@@ -1,8 +1,8 @@
1
1
  import { Command } from '@oclif/core';
2
- import { type WorkflowRole } from '@sanity/workflow-engine';
2
+ import { type WorkflowLifecycle } from '@sanity/workflow-engine';
3
3
  interface DefinitionListFlags {
4
4
  tag?: string | undefined;
5
- 'workflow-name'?: string | undefined;
5
+ name?: string | undefined;
6
6
  limit: number;
7
7
  }
8
8
  export interface DefinitionListRow {
@@ -10,7 +10,7 @@ export interface DefinitionListRow {
10
10
  version: number;
11
11
  title: string;
12
12
  tag: string;
13
- role?: WorkflowRole;
13
+ lifecycle?: WorkflowLifecycle;
14
14
  stageCount: number;
15
15
  inFlightCount: number;
16
16
  totalInstances: number;
@@ -24,7 +24,7 @@ export default class DefinitionList extends Command {
24
24
  static examples: string[];
25
25
  static flags: {
26
26
  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>;
27
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
28
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
29
  };
30
30
  run(): Promise<void>;
@@ -2,7 +2,7 @@ import { Command, Flags } from '@oclif/core';
2
2
  import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, isStartableDefinition, tagScopeFilter, } from '@sanity/workflow-engine';
3
3
  import { Table } from 'console-table-printer';
4
4
  import logSymbols from 'log-symbols';
5
- import { loadClient } from "../../lib/client.js";
5
+ import { resolveReadContext } from "../../lib/context.js";
6
6
  import { tagFlags } from "../../lib/flags.js";
7
7
  export function buildDefinitionListQuery(flags) {
8
8
  const params = { limit: flags.limit };
@@ -21,8 +21,8 @@ export function buildDefinitionListQuery(flags) {
21
21
  params.tag = flags.tag;
22
22
  filters.push(tagScopeFilter());
23
23
  }
24
- if (flags['workflow-name']) {
25
- params.name = flags['workflow-name'];
24
+ if (flags.name) {
25
+ params.name = flags.name;
26
26
  filters.push('name == $name');
27
27
  }
28
28
  const instanceMatch = instanceFilters.join(' && ');
@@ -31,7 +31,7 @@ export function buildDefinitionListQuery(flags) {
31
31
  version,
32
32
  title,
33
33
  tag,
34
- role,
34
+ lifecycle,
35
35
  "stageCount": count(stages),
36
36
  "inFlightCount": count(*[${instanceMatch} && !defined(completedAt)]),
37
37
  "totalInstances": count(*[${instanceMatch}])
@@ -50,14 +50,14 @@ export default class DefinitionList extends Command {
50
50
  description: 'The maximum number of definitions to return.',
51
51
  default: 100,
52
52
  }),
53
- 'workflow-name': Flags.string({
54
- description: 'Filter to a single workflow definition name (e.g. product-launch)',
53
+ name: Flags.string({
54
+ description: 'Filter to a single workflow definition name (e.g. product-launch).',
55
55
  }),
56
56
  };
57
57
  async run() {
58
58
  const { flags } = await this.parse(DefinitionList);
59
+ const { client } = await resolveReadContext(flags);
59
60
  const { groq, params } = buildDefinitionListQuery(flags);
60
- const client = loadClient();
61
61
  const rows = await client.fetch(groq, params);
62
62
  if (!rows.length) {
63
63
  this.log(`${logSymbols.info} no definitions found`);
@@ -81,7 +81,7 @@ export default class DefinitionList extends Command {
81
81
  // `definition show` title). Only declared keys are passed — console-table-
82
82
  // printer renders any extra key as a stray column.
83
83
  //
84
- // `child`-role defs are spawn-only; the Startable column tells an admin
84
+ // `lifecycle: 'child'` defs are spawn-only; the Startable column tells an admin
85
85
  // which definitions a human can actually start standalone.
86
86
  table.addRow({
87
87
  workflow: `${r.name} v${r.version}`,
@@ -1,14 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import { type WorkflowDefinition } from '@sanity/workflow-engine';
3
- interface DefinitionShowFlags {
4
- name: string;
5
- tag?: string | undefined;
6
- version?: number | undefined;
7
- }
8
- export declare function buildDefinitionShowQuery(flags: DefinitionShowFlags): {
9
- groq: string;
10
- params: Record<string, unknown>;
11
- };
12
3
  export default class DefinitionShow extends Command {
13
4
  static description: string;
14
5
  static args: {
@@ -32,4 +23,3 @@ export declare function definitionHeader(def: WorkflowDefinition & {
32
23
  * activity's actions. Pure so it can be asserted without driving the oclif command.
33
24
  */
34
25
  export declare function describeDefinition(def: WorkflowDefinition): string[];
35
- export {};