@sanity/workflow-cli 0.9.0 → 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 (70) hide show
  1. package/CHANGELOG.md +485 -0
  2. package/README.md +62 -18
  3. package/bin/run.js +0 -4
  4. package/dist/commands/abort.d.ts +4 -6
  5. package/dist/commands/abort.js +13 -24
  6. package/dist/commands/definition/delete.d.ts +2 -2
  7. package/dist/commands/definition/delete.js +14 -27
  8. package/dist/commands/definition/diff.d.ts +2 -2
  9. package/dist/commands/definition/diff.js +3 -12
  10. package/dist/commands/definition/list.d.ts +3 -4
  11. package/dist/commands/definition/list.js +40 -46
  12. package/dist/commands/definition/show.d.ts +22 -3
  13. package/dist/commands/definition/show.js +23 -21
  14. package/dist/commands/deploy.d.ts +11 -5
  15. package/dist/commands/deploy.js +54 -57
  16. package/dist/commands/diagnose.d.ts +22 -4
  17. package/dist/commands/diagnose.js +39 -45
  18. package/dist/commands/fire-action.d.ts +2 -4
  19. package/dist/commands/fire-action.js +27 -61
  20. package/dist/commands/list.d.ts +7 -8
  21. package/dist/commands/list.js +43 -56
  22. package/dist/commands/reset-activity.js +0 -1
  23. package/dist/commands/set-stage.d.ts +2 -2
  24. package/dist/commands/set-stage.js +14 -30
  25. package/dist/commands/show.d.ts +2 -2
  26. package/dist/commands/show.js +12 -24
  27. package/dist/commands/start.d.ts +4 -7
  28. package/dist/commands/start.js +18 -54
  29. package/dist/commands/tail.d.ts +2 -2
  30. package/dist/commands/tail.js +20 -25
  31. package/dist/hooks/finally/telemetry.d.ts +8 -0
  32. package/dist/hooks/finally/telemetry.js +13 -0
  33. package/dist/hooks/prerun/telemetry.d.ts +5 -0
  34. package/dist/hooks/prerun/telemetry.js +5 -0
  35. package/dist/index.js +0 -3
  36. package/dist/lib/base-command.d.ts +14 -0
  37. package/dist/lib/base-command.js +10 -0
  38. package/dist/lib/client.js +13 -30
  39. package/dist/lib/context.d.ts +49 -13
  40. package/dist/lib/context.js +49 -48
  41. package/dist/lib/definitions.js +5 -24
  42. package/dist/lib/diff.d.ts +5 -2
  43. package/dist/lib/diff.js +61 -27
  44. package/dist/lib/env.d.ts +4 -0
  45. package/dist/lib/env.js +4 -3
  46. package/dist/lib/fail.d.ts +12 -6
  47. package/dist/lib/fail.js +24 -25
  48. package/dist/lib/flags.d.ts +0 -7
  49. package/dist/lib/flags.js +0 -17
  50. package/dist/lib/load-config.d.ts +11 -7
  51. package/dist/lib/load-config.js +40 -20
  52. package/dist/lib/operation-args.d.ts +14 -23
  53. package/dist/lib/operation-args.js +6 -38
  54. package/dist/lib/ops-report.d.ts +18 -10
  55. package/dist/lib/ops-report.js +20 -16
  56. package/dist/lib/params.js +0 -8
  57. package/dist/lib/read-fanout.d.ts +22 -0
  58. package/dist/lib/read-fanout.js +28 -0
  59. package/dist/lib/select-deployment.js +0 -21
  60. package/dist/lib/share-definitions.d.ts +86 -0
  61. package/dist/lib/share-definitions.js +106 -0
  62. package/dist/lib/stub.js +0 -7
  63. package/dist/lib/telemetry-setup.d.ts +66 -0
  64. package/dist/lib/telemetry-setup.js +92 -0
  65. package/dist/lib/telemetry.d.ts +100 -0
  66. package/dist/lib/telemetry.js +89 -0
  67. package/dist/lib/ui.d.ts +33 -1
  68. package/dist/lib/ui.js +32 -21
  69. package/oclif.manifest.json +8 -47
  70. package/package.json +14 -8
package/README.md CHANGED
@@ -24,6 +24,14 @@ Authenticate once with `sanity login` (the CLI reads that session token); for
24
24
  CI, set `SANITY_AUTH_TOKEN` instead. Then create a `sanity.workflow.ts` in the
25
25
  directory you run from — see [Configuration](#configuration).
26
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
+
27
35
  ## Configuration
28
36
 
29
37
  The CLI is configured by a `sanity.workflow.ts` (or `.js`/`.mjs`) discovered in
@@ -71,10 +79,10 @@ runtime. Rule of thumb: **different resource → bind an alias and reference it
71
79
  entirely.** So the simplest single-dataset setup is just a `workflowResource`
72
80
  and `definitions`, with no `resourceAliases` at all.
73
81
 
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) |
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) |
78
86
 
79
87
  Run through the `dev` / `cli` scripts and the CLI itself and your
80
88
  `sanity.workflow.ts` load straight from TypeScript source: the scripts set
@@ -117,19 +125,52 @@ stale compiled output.
117
125
  | `deploy --check` | wired — runs `validateDefinition` over the local batch + a duplicate-name check |
118
126
  | `deploy --dry-run` | wired — fetches existing docs and renders a coloured JSON diff per change |
119
127
  | `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) |
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) |
122
130
  | `show <instance-id>` | wired — `client.getDocument` |
123
- | `diagnose <instance-id>` | wired — one `workflow.evaluate` call, classifies why the instance is/isn't progressing |
131
+ | `diagnose <instance-id>` | wired — calls `workflow.diagnose`, classifies why the instance is/isn't progressing |
124
132
  | `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
125
133
  | `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, lifts guards) |
126
134
  | `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 |
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 |
129
137
  | `definition show <name>` | wired — `client.fetch`, latest version unless `--version` |
130
138
  | `definition diff <name>` | wired — diffs the in-code definition against the deployed latest (`--version` to pin) |
131
139
  | `definition delete <name>` | wired — calls `workflow.deleteDefinition` (refuses on live instances unless `--cascade`) |
132
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.
173
+
133
174
  ## Known gaps
134
175
 
135
176
  - **No `sanity workflow` plugin wiring yet.** The proposal targets the
@@ -139,15 +180,18 @@ stale compiled output.
139
180
  - **No true bypass.** `set-stage` is the engine's `setStage` admin
140
181
  override: it skips the definition's declared transitions and filters, but
141
182
  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.
144
- - **Actor identity.** Write paths default the actor to
145
- `{ kind: "system", id: "workflow-cli" }` rather than resolving the real
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.
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.
151
195
 
152
196
  ## Tech stack
153
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,6 +1,7 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type WorkflowInstance } from '@sanity/workflow-engine';
3
- export default class Abort extends Command {
2
+ import { WorkflowCommand } from '../lib/base-command.ts';
3
+ import { type WriteReport } from '../lib/ops-report.ts';
4
+ export default class Abort extends WorkflowCommand {
4
5
  static description: string;
5
6
  static examples: string[];
6
7
  static args: {
@@ -21,8 +22,5 @@ interface AbortOutcome {
21
22
  * aborted / already-terminal permutations can be asserted without driving
22
23
  * a real abort.
23
24
  */
24
- export declare function abortReport(result: AbortOutcome): {
25
- changed: boolean;
26
- message: string;
27
- };
25
+ export declare function abortReport(result: AbortOutcome): WriteReport;
28
26
  export {};
@@ -1,12 +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';
4
+ import { WorkflowCommand } from "../lib/base-command.js";
5
5
  import { resolveInstanceContext } from "../lib/context.js";
6
- import { errorMessage, fail } from "../lib/fail.js";
7
6
  import { tagFlags } from "../lib/flags.js";
8
7
  import { buildOperationArgs } from "../lib/operation-args.js";
9
- export default class Abort extends Command {
8
+ import { runWriteVerb } from "../lib/ops-report.js";
9
+ export default class Abort extends WorkflowCommand {
10
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.';
11
11
  static examples = [
12
12
  '<%= config.bin %> abort wf-instance.abc123',
@@ -22,34 +22,23 @@ export default class Abort extends Command {
22
22
  async run() {
23
23
  const { args, flags } = await this.parse(Abort);
24
24
  const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
25
- const spinner = ora(`Aborting ${args.instanceId}…`).start();
26
- try {
27
- const result = await workflow.abortInstance({
25
+ await runWriteVerb({
26
+ startLabel: `Aborting ${args.instanceId}…`,
27
+ failLabel: 'Abort rejected',
28
+ failHeadline: 'abort error:',
29
+ run: () => workflow.abortInstance({
28
30
  client,
29
31
  ...buildOperationArgs({
30
32
  scope,
31
33
  instanceId: args.instanceId,
32
34
  reason: flags.reason,
33
35
  }),
34
- });
35
- const report = abortReport(result);
36
- if (!report.changed) {
37
- spinner.warn(report.message);
38
- return;
39
- }
40
- spinner.succeed(report.message);
41
- }
42
- catch (error) {
43
- spinner.fail('Abort rejected');
44
- fail('abort error:', errorMessage(error));
45
- }
36
+ }),
37
+ report: abortReport,
38
+ log: (line) => this.log(line),
39
+ });
46
40
  }
47
41
  }
48
- /**
49
- * Turn an `abortInstance` result into the spinner message. Pure so the
50
- * aborted / already-terminal permutations can be asserted without driving
51
- * a real abort.
52
- */
53
42
  export function abortReport(result) {
54
43
  const stage = styleText('bold', result.instance.currentStage);
55
44
  if (!result.changed) {
@@ -1,7 +1,7 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type DeleteDefinitionArgs, type DeleteDefinitionResult } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
3
  import { type EngineScope } from '../../lib/operation-args.ts';
4
- export default class DefinitionDelete extends Command {
4
+ export default class DefinitionDelete extends WorkflowCommand {
5
5
  static description: string;
6
6
  static examples: string[];
7
7
  static args: {
@@ -1,12 +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';
4
+ import { WorkflowCommand } from "../../lib/base-command.js";
5
5
  import { resolveContext } from "../../lib/context.js";
6
- import { errorMessage, fail } from "../../lib/fail.js";
7
6
  import { tagFlags } from "../../lib/flags.js";
8
- import { SYSTEM_ACTOR, baseEngineArgs } from "../../lib/operation-args.js";
9
- export default class DefinitionDelete extends Command {
7
+ import { baseEngineArgs } from "../../lib/operation-args.js";
8
+ import { runWriteVerb } from "../../lib/ops-report.js";
9
+ export default class DefinitionDelete extends WorkflowCommand {
10
10
  static description = 'Delete a deployed workflow definition (every version, or one via --version). ' +
11
11
  'Refuses while non-terminal instances exist unless --cascade aborts them first — ' +
12
12
  'instances are aborted in place, never deleted.';
@@ -35,41 +35,28 @@ export default class DefinitionDelete extends Command {
35
35
  const { args, flags } = await this.parse(DefinitionDelete);
36
36
  const { deployment, client } = await resolveContext(flags);
37
37
  const deleteArgs = buildDeleteArgs({ scope: deployment, name: args.name, flags });
38
- const spinner = ora(`Deleting definition ${deleteTargetLabel(deleteArgs)}…`).start();
39
- try {
40
- const result = await workflow.deleteDefinition({ client, ...deleteArgs });
41
- spinner.succeed(deleteReport(result));
42
- }
43
- catch (error) {
44
- spinner.fail('Delete rejected');
45
- fail('definition delete error:', errorMessage(error));
46
- }
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
+ });
47
46
  }
48
47
  }
49
- /**
50
- * The shared engine args ({@link baseEngineArgs}) plus the delete-specific
51
- * targeting. `version` and `reason` spread conditionally because the
52
- * engine's optionals reject an explicit `undefined` under
53
- * `exactOptionalPropertyTypes`.
54
- */
55
48
  export function buildDeleteArgs({ scope, name, flags, }) {
56
49
  return {
57
- ...baseEngineArgs(scope, SYSTEM_ACTOR),
50
+ ...baseEngineArgs(scope),
58
51
  definition: name,
59
52
  cascade: flags.cascade,
60
53
  ...(flags.version !== undefined ? { version: flags.version } : {}),
61
54
  ...(flags.reason !== undefined ? { reason: flags.reason } : {}),
62
55
  };
63
56
  }
64
- /** What the spinner names: the workflow, or one pinned version of it. */
65
57
  export function deleteTargetLabel(args) {
66
58
  return args.version === undefined ? args.definition : `${args.definition} v${args.version}`;
67
59
  }
68
- /**
69
- * Turn a `deleteDefinition` result into the spinner message. Pure so the
70
- * version / cascade / guard permutations can be asserted without driving
71
- * a real delete.
72
- */
73
60
  export function deleteReport(result) {
74
61
  const versions = result.deletedVersions.map((v) => `v${v}`).join(', ');
75
62
  const parts = [`Deleted ${styleText('bold', result.name)} ${versions}`];
@@ -1,6 +1,6 @@
1
- import { Command } from '@oclif/core';
2
1
  import { type WorkflowDefinition } from '@sanity/workflow-engine';
3
- export default class DefinitionDiff extends Command {
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ export default class DefinitionDiff extends WorkflowCommand {
4
4
  static description: string;
5
5
  static examples: string[];
6
6
  static args: {
@@ -1,12 +1,13 @@
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 { WorkflowCommand } from "../../lib/base-command.js";
4
5
  import { resolveContext } from "../../lib/context.js";
5
6
  import { fetchDeployedDefinition, selectDefinitions, validateOrFail } from "../../lib/definitions.js";
6
7
  import { diffReport } from "../../lib/diff.js";
7
8
  import { tagFlags } from "../../lib/flags.js";
8
9
  import { deploymentToTarget } from "../../lib/select-deployment.js";
9
- export default class DefinitionDiff extends Command {
10
+ export default class DefinitionDiff extends WorkflowCommand {
10
11
  static description = 'Diff an in-code definition against the deployed version (latest by default).';
11
12
  static examples = [
12
13
  '<%= config.bin %> definition diff productLaunch',
@@ -27,8 +28,6 @@ export default class DefinitionDiff extends Command {
27
28
  const spinner = ora(`Diffing ${args.name} against deployed…`).start();
28
29
  let deployed;
29
30
  try {
30
- // An absent latest is a legitimate "create" diff, so `undefined`
31
- // flows straight into the diff entry.
32
31
  deployed = await fetchDeployedDefinition({
33
32
  client,
34
33
  name: args.name,
@@ -45,16 +44,8 @@ export default class DefinitionDiff extends Command {
45
44
  this.log(line);
46
45
  }
47
46
  }
48
- /**
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.
52
- */
53
47
  export function selectDefinitionToDiff(definitions, name) {
54
48
  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
49
  if (def === undefined) {
59
50
  throw new Error(`selectDefinitions returned no match for "${name}"`);
60
51
  }
@@ -1,5 +1,4 @@
1
- import { Command } from '@oclif/core';
2
- import { type WorkflowLifecycle } from '@sanity/workflow-engine';
1
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
2
  interface DefinitionListFlags {
4
3
  tag?: string | undefined;
5
4
  name?: string | undefined;
@@ -10,16 +9,16 @@ export interface DefinitionListRow {
10
9
  version: number;
11
10
  title: string;
12
11
  tag: string;
13
- lifecycle?: WorkflowLifecycle;
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: {
@@ -1,20 +1,18 @@
1
- import { Command, Flags } from '@oclif/core';
2
- import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, isStartableDefinition, tagScopeFilter, } from '@sanity/workflow-engine';
3
- import { Table } from 'console-table-printer';
1
+ import { Flags } from '@oclif/core';
2
+ import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
4
3
  import logSymbols from 'log-symbols';
5
- import { resolveReadContext } from "../../lib/context.js";
4
+ import { WorkflowCommand } from "../../lib/base-command.js";
5
+ import { resolveReadTargets } from "../../lib/context.js";
6
6
  import { tagFlags } from "../../lib/flags.js";
7
+ import { runReadAcrossTargets } from "../../lib/read-fanout.js";
8
+ import { clipToLimit, formatTable } from "../../lib/ui.js";
7
9
  export function buildDefinitionListQuery(flags) {
8
- const params = { limit: flags.limit };
10
+ const params = { limit: flags.limit + 1 };
9
11
  const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`];
10
12
  const instanceFilters = [
11
13
  `_type == "${WORKFLOW_INSTANCE_TYPE}"`,
12
14
  'definition == ^.name',
13
15
  '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
16
  'tag == ^.tag',
19
17
  ];
20
18
  if (flags.tag) {
@@ -31,14 +29,14 @@ export function buildDefinitionListQuery(flags) {
31
29
  version,
32
30
  title,
33
31
  tag,
34
- lifecycle,
32
+ _createdAt,
35
33
  "stageCount": count(stages),
36
34
  "inFlightCount": count(*[${instanceMatch} && !defined(completedAt)]),
37
35
  "totalInstances": count(*[${instanceMatch}])
38
36
  }`;
39
37
  return { groq, params };
40
38
  }
41
- export default class DefinitionList extends Command {
39
+ export default class DefinitionList extends WorkflowCommand {
42
40
  static description = 'List deployed workflow definitions.';
43
41
  static examples = [
44
42
  '<%= config.bin %> definition list',
@@ -56,43 +54,39 @@ export default class DefinitionList extends Command {
56
54
  };
57
55
  async run() {
58
56
  const { flags } = await this.parse(DefinitionList);
59
- const { client } = await resolveReadContext(flags);
57
+ const targets = await resolveReadTargets(flags);
60
58
  const { groq, params } = buildDefinitionListQuery(flags);
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: 'workflow', title: 'Workflow', alignment: 'left' },
69
- { name: 'title', title: 'Title', alignment: 'left' },
70
- { name: 'tag', title: 'Tag', alignment: 'left' },
71
- { name: 'startable', title: 'Startable', alignment: 'center' },
72
- { name: 'stageCount', title: 'Stages', alignment: 'right' },
73
- { name: 'inFlightCount', title: 'In flight', alignment: 'right' },
74
- { name: 'totalInstances', title: 'Instances', alignment: 'right' },
75
- ],
59
+ const failures = await runReadAcrossTargets({
60
+ targets,
61
+ log: (line) => this.log(line),
62
+ run: async ({ client }) => {
63
+ const fetched = await client.fetch(groq, params, {
64
+ tag: 'definition.list',
65
+ });
66
+ if (!fetched.length) {
67
+ this.log(`${logSymbols.info} no definitions found`);
68
+ return;
69
+ }
70
+ const { rows, note } = clipToLimit(fetched, flags.limit);
71
+ const lines = formatTable(['workflow', 'title', 'tag', 'stages', 'in flight', 'instances', 'created'], rows.map((r) => [
72
+ `${r.name} v${r.version}`,
73
+ r.title,
74
+ r.tag,
75
+ String(r.stageCount),
76
+ String(r.inFlightCount),
77
+ String(r.totalInstances),
78
+ r._createdAt,
79
+ ]));
80
+ for (const line of lines) {
81
+ this.log(line);
82
+ }
83
+ if (note) {
84
+ this.log(note);
85
+ }
86
+ },
76
87
  });
77
- for (const r of rows) {
78
- // The doc `_id` is `tag.name.vN`, so a Definition ID column would just
79
- // repeat Workflow + Tag — and no command takes an `_id` anyway (they take
80
- // name + --tag/--version). `name vN` is this CLI's identifier format (the
81
- // `definition show` title). Only declared keys are passed — console-table-
82
- // printer renders any extra key as a stray column.
83
- //
84
- // `lifecycle: 'child'` defs are spawn-only; the Startable column tells an admin
85
- // which definitions a human can actually start standalone.
86
- table.addRow({
87
- workflow: `${r.name} v${r.version}`,
88
- title: r.title,
89
- tag: r.tag,
90
- startable: isStartableDefinition(r) ? 'yes' : 'spawn-only',
91
- stageCount: r.stageCount,
92
- inFlightCount: r.inFlightCount,
93
- totalInstances: r.totalInstances,
94
- });
88
+ if (failures.length > 0) {
89
+ this.exit(1);
95
90
  }
96
- table.printTable();
97
91
  }
98
92
  }
@@ -1,6 +1,7 @@
1
- import { Command } from '@oclif/core';
2
- import { type WorkflowDefinition } from '@sanity/workflow-engine';
3
- export default class DefinitionShow extends Command {
1
+ import { type DeployedDefinition, type WorkflowDefinition, type WorkflowResource } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ import { type ReadTarget } from '../../lib/context.ts';
4
+ export default class DefinitionShow extends WorkflowCommand {
4
5
  static description: string;
5
6
  static args: {
6
7
  name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
@@ -11,6 +12,24 @@ export default class DefinitionShow extends Command {
11
12
  };
12
13
  run(): Promise<void>;
13
14
  }
15
+ /** How a lookup names the version it asked for: `vN` for a pinned
16
+ * `--version`, `latest` when unpinned. */
17
+ export declare function versionLabel(version: number | undefined): string;
18
+ /**
19
+ * One deployed document per target that has the name: the authored
20
+ * {@link WorkflowDefinition} plus the deploy-stamped envelope (`version`,
21
+ * `contentHash`, `tag`). A probe failure propagates — with an unreadable
22
+ * resource, "which dataset holds this name" is unanswerable, unlike an
23
+ * instance-id lookup where any hit IS the instance.
24
+ */
25
+ export declare function fetchDefinitionHits({ targets, groq, params, }: {
26
+ targets: ReadTarget[];
27
+ groq: string;
28
+ params: Record<string, unknown>;
29
+ }): Promise<{
30
+ resource: WorkflowResource;
31
+ def: DeployedDefinition;
32
+ }[]>;
14
33
  /** The summary block at the top of `definition show`: a bold name + version
15
34
  * title, then the human title, description, and deploy `tag` as aligned
16
35
  * detail rows. `tag` is deployment metadata (the partition), not part of the
@@ -1,12 +1,13 @@
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 { isTerminalStage, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { resolveReadContext } from "../../lib/context.js";
5
+ import { WorkflowCommand } from "../../lib/base-command.js";
6
+ import { resolveReadTargets, soleHitOrFail } from "../../lib/context.js";
6
7
  import { buildDefinitionShowQuery } from "../../lib/definitions.js";
7
8
  import { tagFlags } from "../../lib/flags.js";
8
9
  import { formatKeyValue, sectionHeader } from "../../lib/ui.js";
9
- export default class DefinitionShow extends Command {
10
+ export default class DefinitionShow extends WorkflowCommand {
10
11
  static description = 'Show a deployed workflow definition.';
11
12
  static args = {
12
13
  name: Args.string({ required: true, description: 'Workflow definition name.' }),
@@ -17,18 +18,18 @@ export default class DefinitionShow extends Command {
17
18
  };
18
19
  async run() {
19
20
  const { args, flags } = await this.parse(DefinitionShow);
20
- const { client } = await resolveReadContext(flags);
21
+ const targets = await resolveReadTargets(flags);
21
22
  const { groq, params } = buildDefinitionShowQuery({
22
23
  name: args.name,
23
24
  tag: flags.tag,
24
25
  version: flags.version,
25
26
  });
26
- // A deployed document: the authored {@link WorkflowDefinition} plus the
27
- // deploy-stamped envelope (`version`, `contentHash`, `tag`).
28
- const def = await client.fetch(groq, params);
27
+ const hits = await fetchDefinitionHits({ targets, groq, params });
28
+ const def = soleHitOrFail(hits, `Definition "${args.name}"`)?.def;
29
29
  if (!def) {
30
- const at = flags.version ? `v${flags.version}` : 'latest';
31
- this.error(`${logSymbols.error} no definition "${args.name}" (${at})`, { exit: 1 });
30
+ this.error(`${logSymbols.error} no definition "${args.name}" (${versionLabel(flags.version)})`, {
31
+ exit: 1,
32
+ });
32
33
  }
33
34
  this.log(definitionHeader(def, def.tag));
34
35
  this.log('');
@@ -37,11 +38,19 @@ export default class DefinitionShow extends Command {
37
38
  }
38
39
  }
39
40
  }
40
- const HEADER_PAD = 11; // "Description" — the longest label in the block
41
- /** The summary block at the top of `definition show`: a bold name + version
42
- * title, then the human title, description, and deploy `tag` as aligned
43
- * detail rows. `tag` is deployment metadata (the partition), not part of the
44
- * authored definition, so it's passed separately and omitted when unknown. */
41
+ export function versionLabel(version) {
42
+ return version === undefined ? 'latest' : `v${version}`;
43
+ }
44
+ export async function fetchDefinitionHits({ targets, groq, params, }) {
45
+ const probes = await Promise.all(targets.map(async (target) => {
46
+ const def = await target.client.fetch(groq, params, {
47
+ tag: 'definition.show',
48
+ });
49
+ return def === null ? undefined : { resource: target.resource, def };
50
+ }));
51
+ return probes.filter((hit) => hit !== undefined);
52
+ }
53
+ const HEADER_PAD = 11;
45
54
  export function definitionHeader(def, tag) {
46
55
  const rows = [
47
56
  formatKeyValue({ key: 'Title', value: def.title, padTo: HEADER_PAD }),
@@ -56,18 +65,15 @@ export function definitionHeader(def, tag) {
56
65
  }
57
66
  return [styleText('bold', `${def.name} v${def.version}`), ...rows].join('\n');
58
67
  }
59
- /** One `· action` line, annotated with the terminal status its `status.set` op sets, if any. */
60
68
  function actionLine(action) {
61
69
  const statusOp = (action.ops ?? []).find((op) => op.type === 'status.set');
62
70
  const status = statusOp ? styleText('dim', ` → ${statusOp.status}`) : '';
63
71
  return ` · ${action.name}${status}`;
64
72
  }
65
- /** A `- activity` line followed by one line per action it exposes. */
66
73
  function activityBlock(activity) {
67
74
  const title = activity.title ? styleText('dim', ` — ${activity.title}`) : '';
68
75
  return [` - ${activity.name}${title}`, ...(activity.actions ?? []).map(actionLine)];
69
76
  }
70
- /** A `• stage` line (with initial/terminal markers) followed by its activity blocks. */
71
77
  function stageBlock(stage, initialStage) {
72
78
  const title = stage.title ? styleText('dim', ` — ${stage.title}`) : '';
73
79
  const terminal = isTerminalStage(stage) ? styleText('dim', ' (terminal)') : '';
@@ -77,10 +83,6 @@ function stageBlock(stage, initialStage) {
77
83
  ...(stage.activities ?? []).flatMap(activityBlock),
78
84
  ];
79
85
  }
80
- /**
81
- * The body lines for `definition show`: every stage with its activities and each
82
- * activity's actions. Pure so it can be asserted without driving the oclif command.
83
- */
84
86
  export function describeDefinition(def) {
85
87
  return [
86
88
  sectionHeader('Stages'),