@sanity/workflow-cli 0.9.0 → 0.11.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 (71) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +71 -19
  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 +41 -47
  12. package/dist/commands/definition/show.d.ts +35 -3
  13. package/dist/commands/definition/show.js +44 -24
  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 +60 -56
  18. package/dist/commands/fire-action.d.ts +2 -4
  19. package/dist/commands/fire-action.js +39 -62
  20. package/dist/commands/list.d.ts +18 -10
  21. package/dist/commands/list.js +77 -69
  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 -28
  28. package/dist/commands/start.js +19 -80
  29. package/dist/commands/tail.d.ts +2 -2
  30. package/dist/commands/tail.js +34 -36
  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 +8 -0
  34. package/dist/hooks/prerun/telemetry.js +9 -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.d.ts +10 -0
  42. package/dist/lib/definitions.js +11 -24
  43. package/dist/lib/diff.d.ts +5 -2
  44. package/dist/lib/diff.js +61 -27
  45. package/dist/lib/env.d.ts +4 -0
  46. package/dist/lib/env.js +4 -3
  47. package/dist/lib/fail.d.ts +19 -6
  48. package/dist/lib/fail.js +26 -26
  49. package/dist/lib/flags.d.ts +0 -7
  50. package/dist/lib/flags.js +0 -17
  51. package/dist/lib/load-config.d.ts +11 -7
  52. package/dist/lib/load-config.js +40 -20
  53. package/dist/lib/operation-args.d.ts +14 -23
  54. package/dist/lib/operation-args.js +6 -38
  55. package/dist/lib/ops-report.d.ts +18 -10
  56. package/dist/lib/ops-report.js +20 -16
  57. package/dist/lib/params.js +0 -8
  58. package/dist/lib/read-fanout.d.ts +22 -0
  59. package/dist/lib/read-fanout.js +28 -0
  60. package/dist/lib/select-deployment.js +0 -21
  61. package/dist/lib/share-definitions.d.ts +120 -0
  62. package/dist/lib/share-definitions.js +187 -0
  63. package/dist/lib/stub.js +0 -7
  64. package/dist/lib/telemetry-setup.d.ts +70 -0
  65. package/dist/lib/telemetry-setup.js +99 -0
  66. package/dist/lib/telemetry.d.ts +131 -0
  67. package/dist/lib/telemetry.js +94 -0
  68. package/dist/lib/ui.d.ts +33 -1
  69. package/dist/lib/ui.js +32 -22
  70. package/oclif.manifest.json +21 -52
  71. package/package.json +14 -8
package/README.md CHANGED
@@ -16,7 +16,7 @@ pnpm --filter @sanity/workflow-cli dev <command> [...args]
16
16
  # Examples:
17
17
  pnpm --filter @sanity/workflow-cli dev --help
18
18
  pnpm --filter @sanity/workflow-cli dev deploy --dry-run
19
- pnpm --filter @sanity/workflow-cli dev list --in-flight
19
+ pnpm --filter @sanity/workflow-cli dev list --include-completed
20
20
  pnpm --filter @sanity/workflow-cli dev show wf-instance.abc123
21
21
  ```
22
22
 
@@ -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,60 @@ 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 (except a `deploy --share-defs`,
149
+ which forces that deploy's telemetry — see [Definition sharing](#definition-sharing)),
150
+ and a session that isn't logged in sends nothing. A one-time notice on stderr
151
+ discloses collection on first use.
152
+
153
+ One extension point: supply your own logger as `telemetry` in
154
+ `sanity.workflow.ts` and the built-in pipeline is not constructed at all —
155
+ every event flows to your implementation unconditionally (CI included), and
156
+ consent, suppression, and destination become its business.
157
+
158
+ ## Definition sharing
159
+
160
+ Separate from telemetry, a `deploy` that creates new definition versions
161
+ donates those documents to Sanity — verbatim (structure, names, titles, GROQ
162
+ filters, effect configuration, seeded values), plus their deployment
163
+ coordinates (project and dataset, or resource id) — never content documents,
164
+ instances, or your auth token. The documents go to Sanity's first-party
165
+ definition-feedback endpoint, not the telemetry pipeline; telemetry carries
166
+ only a content-free marker (content hash plus structural counts) per shared
167
+ definition and a per-invocation decision event.
168
+
169
+ Sharing is **opt-out**. Pass `--share-defs` to donate without being asked, or
170
+ `--no-share-defs` to decline. With neither flag, an interactive terminal is
171
+ asked **once** — the answer is remembered in your Sanity user config, so the
172
+ prompt never repeats — before anything is sent; notice always precedes the
173
+ send. Every later flagless deploy then prints a one-line reminder of the
174
+ current state (sharing, or not) and the flag to change it, so an ongoing
175
+ donation is never silent. Unattended runs (CI, `DO_NOT_TRACK`, a non-TTY pipe)
176
+ can't prompt: by default they donate nothing (opt in per run with
177
+ `--share-defs`). An explicit `--share-defs` also sends that deploy's telemetry
178
+ regardless of CI / `DO_NOT_TRACK` — a command-line arg beats env vars;
179
+ account-level `sanity telemetry disable` still applies. A failed share only
180
+ warns, never failing a completed deploy.
181
+
133
182
  ## Known gaps
134
183
 
135
184
  - **No `sanity workflow` plugin wiring yet.** The proposal targets the
@@ -139,15 +188,18 @@ stale compiled output.
139
188
  - **No true bypass.** `set-stage` is the engine's `setStage` admin
140
189
  override: it skips the definition's declared transitions and filters, but
141
190
  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.
191
+ true force-set that also bypasses guards + enter effects does not exist
192
+ yet — it needs engine work that hasn't landed.
193
+
194
+ ## Identity
195
+
196
+ Identity is the token behind the configured client: the engine resolves the
197
+ acting user (and their roles) from `/users/me`, so every write is attributed
198
+ to whoever the token authenticates there is no identity injection and no
199
+ `--as` impersonation. Alongside that "who", the CLI declares an advisory
200
+ "via what": every verb carries
201
+ `executionContext: { kind: "cli", id: "workflow-cli" }`, stamped on history
202
+ entries as provenance.
151
203
 
152
204
  ## Tech stack
153
205
 
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, inFlightFilter, 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
- "inFlightCount": count(*[${instanceMatch} && !defined(completedAt)]),
34
+ "inFlightCount": count(*[${instanceMatch} && ${inFlightFilter()}]),
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,37 @@ export default class DefinitionShow extends Command {
11
12
  };
12
13
  run(): Promise<void>;
13
14
  }
15
+ /**
16
+ * Resolve the one definition `show` names, refusing every ambiguity: an
17
+ * untagged name spanning several tag partitions within a dataset
18
+ * ({@link assertSingleTagPartition}) and a name held by several datasets
19
+ * ({@link soleHitOrFail}) both error, never pick. `undefined` means nothing
20
+ * is deployed under the name.
21
+ */
22
+ export declare function resolveShownDefinition({ targets, name, tag, version, }: {
23
+ targets: ReadTarget[];
24
+ name: string;
25
+ tag: string | undefined;
26
+ version: number | undefined;
27
+ }): Promise<DeployedDefinition | undefined>;
28
+ /** How a lookup names the version it asked for: `vN` for a pinned
29
+ * `--version`, `latest` when unpinned. */
30
+ export declare function versionLabel(version: number | undefined): string;
31
+ /**
32
+ * One deployed document per target that has the name: the authored
33
+ * {@link WorkflowDefinition} plus the deploy-stamped envelope (`version`,
34
+ * `contentHash`, `tag`). A probe failure propagates — with an unreadable
35
+ * resource, "which dataset holds this name" is unanswerable, unlike an
36
+ * instance-id lookup where any hit IS the instance.
37
+ */
38
+ export declare function fetchDefinitionHits({ targets, groq, params, }: {
39
+ targets: ReadTarget[];
40
+ groq: string;
41
+ params: Record<string, unknown>;
42
+ }): Promise<{
43
+ resource: WorkflowResource;
44
+ def: DeployedDefinition;
45
+ }[]>;
14
46
  /** The summary block at the top of `definition show`: a bold name + version
15
47
  * title, then the human title, description, and deploy `tag` as aligned
16
48
  * detail rows. `tag` is deployment metadata (the partition), not part of the