@sanity/workflow-cli 0.5.1

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/bin/run.js +8 -0
  4. package/dist/commands/abort.d.ts +30 -0
  5. package/dist/commands/abort.js +62 -0
  6. package/dist/commands/definition/delete.d.ts +36 -0
  7. package/dist/commands/definition/delete.js +86 -0
  8. package/dist/commands/definition/diff.d.ts +13 -0
  9. package/dist/commands/definition/diff.js +57 -0
  10. package/dist/commands/definition/list.d.ts +31 -0
  11. package/dist/commands/definition/list.js +83 -0
  12. package/dist/commands/definition/show.d.ts +33 -0
  13. package/dist/commands/definition/show.js +118 -0
  14. package/dist/commands/deploy.d.ts +22 -0
  15. package/dist/commands/deploy.js +97 -0
  16. package/dist/commands/diagnose.d.ts +23 -0
  17. package/dist/commands/diagnose.js +269 -0
  18. package/dist/commands/fire-action.d.ts +63 -0
  19. package/dist/commands/fire-action.js +241 -0
  20. package/dist/commands/list.d.ts +46 -0
  21. package/dist/commands/list.js +106 -0
  22. package/dist/commands/move-stage.d.ts +47 -0
  23. package/dist/commands/move-stage.js +77 -0
  24. package/dist/commands/retry-task.d.ts +9 -0
  25. package/dist/commands/retry-task.js +11 -0
  26. package/dist/commands/set-stage.d.ts +12 -0
  27. package/dist/commands/set-stage.js +18 -0
  28. package/dist/commands/show.d.ts +23 -0
  29. package/dist/commands/show.js +85 -0
  30. package/dist/commands/tail.d.ts +15 -0
  31. package/dist/commands/tail.js +73 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +4 -0
  34. package/dist/lib/client.d.ts +17 -0
  35. package/dist/lib/client.js +40 -0
  36. package/dist/lib/config.d.ts +18 -0
  37. package/dist/lib/config.js +50 -0
  38. package/dist/lib/definitions.d.ts +36 -0
  39. package/dist/lib/definitions.js +122 -0
  40. package/dist/lib/diff.d.ts +7 -0
  41. package/dist/lib/diff.js +49 -0
  42. package/dist/lib/env.d.ts +10 -0
  43. package/dist/lib/env.js +13 -0
  44. package/dist/lib/fail.d.ts +16 -0
  45. package/dist/lib/fail.js +33 -0
  46. package/dist/lib/flags.d.ts +5 -0
  47. package/dist/lib/flags.js +8 -0
  48. package/dist/lib/operation-args.d.ts +25 -0
  49. package/dist/lib/operation-args.js +48 -0
  50. package/dist/lib/ops-report.d.ts +30 -0
  51. package/dist/lib/ops-report.js +28 -0
  52. package/dist/lib/stub.d.ts +10 -0
  53. package/dist/lib/stub.js +24 -0
  54. package/oclif.manifest.json +684 -0
  55. package/package.json +94 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sanity, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @sanity/workflow-cli
2
+
3
+ Command-line tool for deploying, inspecting, and administering Sanity workflow
4
+ definitions and instances.
5
+
6
+ > [!WARNING]
7
+ > Early access, restricted. `retry-task` 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.
10
+
11
+ ## Run
12
+
13
+ ```bash
14
+ pnpm --filter @sanity/workflow-cli dev <command> [...args]
15
+
16
+ # Examples:
17
+ pnpm --filter @sanity/workflow-cli dev --help
18
+ pnpm --filter @sanity/workflow-cli dev deploy --dry-run
19
+ pnpm --filter @sanity/workflow-cli dev list --in-flight
20
+ pnpm --filter @sanity/workflow-cli dev show wf-instance.abc123
21
+ ```
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.
49
+
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`.
56
+
57
+ > [!NOTE]
58
+ > pnpm intercepts a handful of its own flag names (`--check`,
59
+ > `--filter`, `--recursive`, …) when they appear after a script name.
60
+ > To invoke `deploy --check`, run from inside the package or via
61
+ > `pnpm exec`:
62
+ >
63
+ > ```bash
64
+ > # from inside packages/workflow-cli
65
+ > pnpm dev deploy --check
66
+ >
67
+ > # or, from anywhere
68
+ > pnpm --filter @sanity/workflow-cli exec \
69
+ > env NODE_OPTIONS='--conditions=development' \
70
+ > tsx --env-file-if-exists=../../.env bin/dev.js deploy --check
71
+ > ```
72
+ >
73
+ > `--dry-run` and `--only` don't collide with pnpm flags and work
74
+ > through the normal `pnpm --filter … dev …` invocation.
75
+
76
+ ## Command status
77
+
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-task <instance-id> <task-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`) |
97
+
98
+ ## Known gaps
99
+
100
+ - **No `sanity workflow` plugin wiring yet.** The proposal targets the
101
+ Sanity CLI as a plugin host. The package publishes as a standalone
102
+ `sanity-workflows` binary today; plugging into `sanity` as an oclif
103
+ 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.
113
+
114
+ ## Tech stack
115
+
116
+ Mirrors the Sanity CLI's stack so the eventual plugin port is small:
117
+
118
+ - [`@oclif/core`](https://oclif.io) for command definitions, flags,
119
+ help.
120
+ - `@oclif/plugin-help` for help output.
121
+ - `ora`, `boxen`, `log-symbols`, `console-table-printer`, `picocolors`
122
+ for terminal UX.
123
+ - `diff` for the colored JSON unified diff in `deploy --dry-run`.
package/bin/run.js ADDED
@@ -0,0 +1,8 @@
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
+ import {execute} from '@oclif/core'
7
+
8
+ await execute({dir: import.meta.url})
@@ -0,0 +1,30 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Abort extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
14
+ interface AbortOutcome {
15
+ fired: boolean;
16
+ instance: {
17
+ currentStage: string;
18
+ completedAt?: string | undefined;
19
+ };
20
+ }
21
+ /**
22
+ * Turn an `abortInstance` result into the spinner message. Pure so the
23
+ * fired / already-terminal permutations can be asserted without driving
24
+ * a real abort.
25
+ */
26
+ export declare function abortReport(result: AbortOutcome): {
27
+ fired: boolean;
28
+ message: string;
29
+ };
30
+ export {};
@@ -0,0 +1,62 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { workflow } from '@sanity/workflow-engine';
3
+ import ora from 'ora';
4
+ import pc from 'picocolors';
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";
9
+ import { buildOperationArgs } from "../lib/operation-args.js";
10
+ export default class Abort extends Command {
11
+ 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
+ static examples = [
13
+ '<%= config.bin %> abort wf-instance.abc123',
14
+ "<%= config.bin %> abort wf-instance.abc123 --reason 'superseded by relaunch'",
15
+ ];
16
+ static args = {
17
+ instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
18
+ };
19
+ static flags = {
20
+ ...tagFlags,
21
+ reason: Flags.string({ description: 'Reason for aborting (recorded in history).' }),
22
+ };
23
+ async run() {
24
+ 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({
30
+ client,
31
+ ...buildOperationArgs(config, args.instanceId, 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
+ }
45
+ }
46
+ }
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
+ export function abortReport(result) {
53
+ const stage = pc.bold(result.instance.currentStage);
54
+ if (!result.fired) {
55
+ const at = result.instance.completedAt ?? 'unknown';
56
+ return {
57
+ fired: false,
58
+ message: `abortInstance returned without firing — instance is already terminal (at ${stage} since ${at})`,
59
+ };
60
+ }
61
+ return { fired: true, message: `Aborted — instance hard-stopped at ${stage}` };
62
+ }
@@ -0,0 +1,36 @@
1
+ import { Command } from '@oclif/core';
2
+ import { type DeleteDefinitionArgs, type DeleteDefinitionResult } from '@sanity/workflow-engine';
3
+ import { type WorkflowConfig } from '../../lib/config.ts';
4
+ export default class DefinitionDelete extends Command {
5
+ static description: string;
6
+ static examples: string[];
7
+ static args: {
8
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
9
+ };
10
+ static flags: {
11
+ version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ cascade: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
18
+ /**
19
+ * The shared engine args ({@link baseEngineArgs}) plus the delete-specific
20
+ * targeting. `version` and `reason` spread conditionally because the
21
+ * engine's optionals reject an explicit `undefined` under
22
+ * `exactOptionalPropertyTypes`.
23
+ */
24
+ export declare function buildDeleteArgs(config: WorkflowConfig, name: string, flags: {
25
+ version?: number | undefined;
26
+ cascade: boolean;
27
+ reason?: string | undefined;
28
+ }): Omit<DeleteDefinitionArgs, 'client'>;
29
+ /** What the spinner names: the workflow, or one pinned version of it. */
30
+ export declare function deleteTargetLabel(args: Pick<DeleteDefinitionArgs, 'definition' | 'version'>): string;
31
+ /**
32
+ * Turn a `deleteDefinition` result into the spinner message. Pure so the
33
+ * version / cascade / guard permutations can be asserted without driving
34
+ * a real delete.
35
+ */
36
+ export declare function deleteReport(result: DeleteDefinitionResult): string;
@@ -0,0 +1,86 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { workflow, } from '@sanity/workflow-engine';
3
+ import ora from 'ora';
4
+ import pc from 'picocolors';
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";
9
+ import { baseEngineArgs } from "../../lib/operation-args.js";
10
+ export default class DefinitionDelete extends Command {
11
+ static description = 'Delete a deployed workflow definition (every version, or one via --version). ' +
12
+ 'Refuses while non-terminal instances exist unless --cascade aborts them first — ' +
13
+ 'instances are aborted in place, never deleted.';
14
+ static examples = [
15
+ '<%= config.bin %> definition delete my-workflow',
16
+ '<%= config.bin %> definition delete my-workflow --version 2',
17
+ "<%= config.bin %> definition delete my-workflow --cascade --reason 'workflow retired'",
18
+ ];
19
+ static args = {
20
+ name: Args.string({ required: true, description: 'Workflow definition name.' }),
21
+ };
22
+ static flags = {
23
+ ...tagFlags,
24
+ version: Flags.integer({
25
+ description: 'Delete only this deployed version (default: every version).',
26
+ }),
27
+ cascade: Flags.boolean({
28
+ description: 'Abort every non-terminal instance pinned to the targeted versions, then delete.',
29
+ default: false,
30
+ }),
31
+ reason: Flags.string({
32
+ description: 'Free-text reason — recorded on each cascade-abort history entry.',
33
+ }),
34
+ };
35
+ async run() {
36
+ const { args, flags } = await this.parse(DefinitionDelete);
37
+ const config = loadWorkflowConfig(flags.tag);
38
+ const client = loadClient();
39
+ const deleteArgs = buildDeleteArgs(config, 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
+ }
50
+ }
51
+ }
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) {
59
+ return {
60
+ ...baseEngineArgs(config),
61
+ definition: name,
62
+ cascade: flags.cascade,
63
+ ...(flags.version !== undefined ? { version: flags.version } : {}),
64
+ ...(flags.reason !== undefined ? { reason: flags.reason } : {}),
65
+ };
66
+ }
67
+ /** What the spinner names: the workflow, or one pinned version of it. */
68
+ export function deleteTargetLabel(args) {
69
+ return args.version === undefined ? args.definition : `${args.definition} v${args.version}`;
70
+ }
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
+ export function deleteReport(result) {
77
+ const versions = result.deletedVersions.map((v) => `v${v}`).join(', ');
78
+ const parts = [`Deleted ${pc.bold(result.name)} ${versions}`];
79
+ if (result.abortedInstanceIds.length > 0) {
80
+ parts.push(`aborted ${result.abortedInstanceIds.length} instance(s)`);
81
+ }
82
+ if (result.deletedGuardCount > 0) {
83
+ parts.push(`removed ${result.deletedGuardCount} orphaned guard doc(s)`);
84
+ }
85
+ return parts.join(' — ');
86
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class DefinitionDiff extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ definition: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,57 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { diffEntry } from '@sanity/workflow-engine';
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";
7
+ import { diffReport } from "../../lib/diff.js";
8
+ import { fail } from "../../lib/fail.js";
9
+ import { tagFlags } from "../../lib/flags.js";
10
+ import { buildDefinitionShowQuery } from "./show.js";
11
+ export default class DefinitionDiff extends Command {
12
+ static description = 'Diff an in-code definition against the deployed version (latest by default).';
13
+ static examples = [
14
+ '<%= config.bin %> definition diff productLaunch',
15
+ '<%= config.bin %> definition diff productLaunch --version 2',
16
+ ];
17
+ static args = {
18
+ definition: Args.string({ required: true, description: 'Workflow definition name.' }),
19
+ };
20
+ static flags = {
21
+ ...tagFlags,
22
+ version: Flags.integer({ description: 'Deployed version to diff against (default: latest).' }),
23
+ };
24
+ async run() {
25
+ 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();
30
+ let deployed;
31
+ try {
32
+ deployed = await fetchDeployedDefinition(client, args.definition, config, flags.version);
33
+ spinner.succeed(`Diffed ${args.definition} v${def.version}`);
34
+ }
35
+ catch (error) {
36
+ spinner.fail('Diff failed');
37
+ throw error;
38
+ }
39
+ for (const line of diffReport([diffEntry(def, deployed, config)]))
40
+ this.log(line);
41
+ }
42
+ }
43
+ /**
44
+ * Fetch the deployed document to compare against. Unlike `deploy
45
+ * --dry-run` (which looks up the exact docId, so a version bump always
46
+ * reads as "create"), diff targets the deployed version that would be
47
+ * superseded — latest unless pinned via `version`. An absent latest is a
48
+ * legitimate "create" diff, but an absent *pinned* version is an error.
49
+ */
50
+ async function fetchDeployedDefinition(client, name, config, version) {
51
+ const { groq, params } = buildDefinitionShowQuery({ name, tag: config.tag, version });
52
+ const deployed = await client.fetch(groq, params);
53
+ if (deployed === null && version !== undefined) {
54
+ fail(`No deployed definition "${name}" v${version}.`);
55
+ }
56
+ return deployed ?? undefined;
57
+ }
@@ -0,0 +1,31 @@
1
+ import { Command } from '@oclif/core';
2
+ interface DefinitionListFlags {
3
+ tag?: string | undefined;
4
+ 'workflow-id'?: string | undefined;
5
+ limit: number;
6
+ }
7
+ export interface DefinitionListRow {
8
+ _id: string;
9
+ name: string;
10
+ version: number;
11
+ title: string;
12
+ tag: string;
13
+ stageCount: number;
14
+ inFlightCount: number;
15
+ totalInstances: number;
16
+ }
17
+ export declare function buildDefinitionListQuery(flags: DefinitionListFlags): {
18
+ groq: string;
19
+ params: Record<string, unknown>;
20
+ };
21
+ export default class DefinitionList extends Command {
22
+ static description: string;
23
+ static examples: string[];
24
+ static flags: {
25
+ limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
26
+ 'workflow-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
27
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ };
29
+ run(): Promise<void>;
30
+ }
31
+ export {};
@@ -0,0 +1,83 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
3
+ import { Table } from 'console-table-printer';
4
+ import logSymbols from 'log-symbols';
5
+ import { loadClient } from "../../lib/client.js";
6
+ import { tagFlags } from "../../lib/flags.js";
7
+ export function buildDefinitionListQuery(flags) {
8
+ const params = { limit: flags.limit };
9
+ const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`];
10
+ const instanceFilters = [
11
+ `_type == "${WORKFLOW_INSTANCE_TYPE}"`,
12
+ 'definition == ^.name',
13
+ 'pinnedVersion == ^.version',
14
+ // Correlate the count to each definition's own partition. Listing
15
+ // can span partitions (when `--tag` is omitted), and two partitions
16
+ // can hold a same-named, same-version definition — without this, one
17
+ // row's count would bleed in the other partition's instances.
18
+ 'tag == ^.tag',
19
+ ];
20
+ if (flags.tag) {
21
+ params.tag = flags.tag;
22
+ filters.push(tagScopeFilter());
23
+ }
24
+ if (flags['workflow-id']) {
25
+ params.name = flags['workflow-id'];
26
+ filters.push('name == $name');
27
+ }
28
+ const instanceMatch = instanceFilters.join(' && ');
29
+ const groq = `*[${filters.join(' && ')}] | order(name asc, version desc) [0...$limit]{
30
+ _id,
31
+ name,
32
+ version,
33
+ title,
34
+ tag,
35
+ "stageCount": count(stages),
36
+ "inFlightCount": count(*[${instanceMatch} && !defined(completedAt)]),
37
+ "totalInstances": count(*[${instanceMatch}])
38
+ }`;
39
+ return { groq, params };
40
+ }
41
+ export default class DefinitionList extends Command {
42
+ static description = 'List deployed workflow definitions.';
43
+ static examples = [
44
+ '<%= config.bin %> definition list',
45
+ '<%= config.bin %> definition list --tag prod',
46
+ ];
47
+ static flags = {
48
+ ...tagFlags,
49
+ limit: Flags.integer({
50
+ description: 'The maximum number of definitions to return.',
51
+ default: 100,
52
+ }),
53
+ 'workflow-id': Flags.string({
54
+ description: 'Filter to a single workflow definition name (e.g. product-launch)',
55
+ }),
56
+ };
57
+ async run() {
58
+ const { flags } = await this.parse(DefinitionList);
59
+ const { groq, params } = buildDefinitionListQuery(flags);
60
+ const client = loadClient();
61
+ const rows = await client.fetch(groq, params);
62
+ if (!rows.length) {
63
+ this.log(`${logSymbols.info} no definitions found`);
64
+ return;
65
+ }
66
+ const table = new Table({
67
+ columns: [
68
+ { name: '_id', title: 'Definition ID', alignment: 'left' },
69
+ { name: 'title', title: 'Title', alignment: 'left' },
70
+ { name: 'name', title: 'Workflow', alignment: 'left' },
71
+ { name: 'tag', title: 'Tag', alignment: 'left' },
72
+ { name: 'version', title: 'Version', alignment: 'right' },
73
+ { name: 'stageCount', title: 'Total stages', alignment: 'right' },
74
+ { name: 'inFlightCount', title: 'Instances in flight', alignment: 'right' },
75
+ { name: 'totalInstances', title: 'Total instances', alignment: 'right' },
76
+ ],
77
+ });
78
+ for (const r of rows) {
79
+ table.addRow(r);
80
+ }
81
+ table.printTable();
82
+ }
83
+ }
@@ -0,0 +1,33 @@
1
+ import { Command } from '@oclif/core';
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
+ export default class DefinitionShow extends Command {
13
+ static description: string;
14
+ static args: {
15
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
16
+ };
17
+ static flags: {
18
+ version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ };
21
+ run(): Promise<void>;
22
+ }
23
+ /**
24
+ * The boxed summary block at the top of `definition show` output. `columns`
25
+ * is the terminal width; values wrap to fit it so `boxen` never re-wraps them.
26
+ */
27
+ export declare function definitionHeader(def: WorkflowDefinition, columns?: number): string;
28
+ /**
29
+ * The body lines for `definition show`: every stage with its tasks and each
30
+ * task's actions. Pure so it can be asserted without driving the oclif command.
31
+ */
32
+ export declare function describeDefinition(def: WorkflowDefinition): string[];
33
+ export {};