@sanity/workflow-cli 0.20.0 → 0.22.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @sanity/workflow-cli
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1c6d4c0: Name the target deployment and resource on `deploy` progress and result lines. The spinner and completion line now read `Deploying N definition(s) to <name> (<tag>) → <resource>…` / `Processed N definition(s) · <name> (<tag>) → <resource>` (and the dry-run equivalents), so a single-deployment run — the common case, which prints no per-batch banner — always shows where the deploy landed.
8
+ - fb42001: The instance-keyed commands — `diagnose`, `fire-action`, `set-stage`, `abort`, `reset-activity` — now locate an instance by fanning out across every configured resource, the same way `show` and the other read commands already do. Passing an instance id is enough: on a config that spans several resources they no longer prompt for (or require) a `--deployment` / `--tag` selector — those stay as optional narrowers. As an optimization, an id carrying the engine's default `<tag>.` mint prefix probes that tag's resource first and skips the fan-out on a hit. `reset-activity` also gains the `--deployment` narrower its siblings already had.
9
+ - 84ff807: The read commands — `list`, `show`, `definition list`, `definition show` — now take `--json`, emitting structured output on stdout for parity with the verb commands. `show` and `definition show` print the fetched document as stored; `list` and `definition list` print their table's row view (each row annotated with the `resource` it came from) under a `truncated` flag. A JSON read spanning several resources is all-or-nothing: an unreadable resource fails the whole command rather than emitting a partial listing a script would mistake for the complete result.
10
+
11
+ ### Patch Changes
12
+
13
+ - a8c460c: Colour the whole terminal-status row in the instance header — aborted rows render bold yellow, completed rows bold green — so a finished instance stands out from the neutral detail rows instead of only its timestamp carrying colour.
14
+ - Updated dependencies [88ba4ba]
15
+ - @sanity/workflow-engine@0.22.0
16
+
17
+ ## 0.21.0
18
+
19
+ ### Minor Changes
20
+
21
+ - 9273d55: List the nested `definition` commands (`list`, `show`, `diff`, `delete`) in the
22
+ standalone binary's flat `--help` output. A custom oclif help class surfaces the
23
+ bare `definition <verb>` aliases at the root instead of burying them under the
24
+ `editorial-workflows` topic, so `sanity-workflows --help` shows every command
25
+ without topic-digging. `help --all` still prints the full canonical tree.
26
+ - e7392af: Add the `resetActivity` admin verb — reset a failed (or otherwise terminal) activity in an instance's current stage back to `active` (re-run) or `skipped` (bypass), then cascade so a `$allActivitiesDone`-gated exit transition can fire. `workflow.diagnose` now marks its `reset-activity` remediation `available`, and the CLI ships the real `reset-activity <instanceId> <activity> [--skip]` command in place of the stub. Emits the `Editorial Workflows Activity Reset` adoption event on every attempt (`changed: false` on a no-op), matching the `set-stage` and `abort` overrides.
27
+
28
+ Writes strictly within the existing model-3 grammar: it reuses the `activityStatusChanged` history variant and the existing activity statuses, so it adds no persisted shape — there is no data-model version bump and no reader-floor change.
29
+
30
+ ### Patch Changes
31
+
32
+ - b248789: CLI error detail lines now render in the terminal's default colour instead of red. The red headline with its `✖` still signals the failure; the detail beneath it (validation output, auth hints, wrapped errors) is the information the user has to read, and long runs of red text were hard to read.
33
+ - f4bc057: **BREAKING:** `list_workflow_instances` now returns one page with `has_more` and an optional `next_cursor`; callers that need every match must continue with the cursor. The optional `limit` accepts 1–100 rows and defaults to 25.
34
+
35
+ Bound instance and definition list reads with lake-side filtering and consumer-specific projections.
36
+
37
+ Document filtering now excludes exited-stage references and includes live unresolved child-workflow references. Definition discovery selects and model-gates only the latest deployed version of each workflow name; historical versions remain stored and available to history-oriented APIs.
38
+
39
+ The MCP eval suite now verifies that agents follow list cursors to find a target beyond the first default page, including in persisted Braintrust runs.
40
+
41
+ MCP tool telemetry reports a `cursorUsed` boolean so continued list-page adoption is measurable without sending cursor values, arguments, or results.
42
+
43
+ - Updated dependencies [d9394e5]
44
+ - Updated dependencies [92e28bd]
45
+ - Updated dependencies [f4bc057]
46
+ - Updated dependencies [bcc30fe]
47
+ - Updated dependencies [e7392af]
48
+ - Updated dependencies [fa9c796]
49
+ - @sanity/workflow-engine@0.21.0
50
+
3
51
  ## 0.20.0
4
52
 
5
53
  ### Minor Changes
package/README.md CHANGED
@@ -60,7 +60,7 @@ import {articleReview, urlDraft} from './src/workflows.ts'
60
60
  export default defineWorkflowConfig({
61
61
  deployments: [
62
62
  {
63
- expectedMinReaderModel: 2,
63
+ expectedMinReaderModel: 4,
64
64
  name: 'production', // the deployment's unique identity (lowercase letters, digits, dashes)
65
65
  tag: 'prod', // the environment partition the engine's docs are scoped to
66
66
  workflowResource: {type: 'dataset', id: 'acme.workflows'}, // where those docs live
@@ -89,8 +89,8 @@ tag names exactly one deployment — on `deploy` it targets every deployment
89
89
  carrying the tag (a tag is an environment group). With several configured, a
90
90
  bare interactive run presents a keyboard-driven deployment selector (by name,
91
91
  tag alongside) — on `deploy`, `start`, and `definition diff`/`delete`, and on
92
- the instance-targeted commands (`abort`, `set-stage`, `fire-action`,
93
- `diagnose`) when the config spans several resources. In CI or another
92
+ the instance-targeted commands (`abort`, `set-stage`, `reset-activity`,
93
+ `fire-action`, `diagnose`) when the config spans several resources. In CI or another
94
94
  non-interactive shell, the command fails asking for `--deployment` or `--tag`
95
95
  (`deploy` also suggests `--all-tags`) instead of blocking for input.
96
96
  `--all-tags` deploys every deployment in the config in one run: a failure in one doesn't
@@ -170,6 +170,7 @@ stale compiled output.
170
170
  | `tail <instance-id>` | wired — `client.listen()` over the instance, prints new history entries |
171
171
  | `abort <instance-id>` | wired — calls `workflow.abortInstance` (hard stop: cancels pending effects, removes guards) |
172
172
  | `set-stage <instance-id> --to <stage>` | wired — calls `workflow.setStage` (admin override: skips declared transitions/filters; enter lifecycle + cascade still run) |
173
+ | `reset-activity <instance-id> <activity>` | wired — calls `workflow.resetActivity` (recovery override: reset a failed activity to `active`, or `--skip` to bypass it; then cascade) |
173
174
  | `fire-action <instance-id>` | wired — `workflow.availableActions` lists actions; `workflow.fireAction` fires one |
174
175
  | `definition list` | wired — `client.fetch` over `sanity.workflow.definition` documents |
175
176
  | `definition show <name>` | wired — `client.fetch`, latest version unless `--version` |
@@ -26,10 +26,26 @@ export default class DefinitionList extends WorkflowCommand {
26
26
  static description: string;
27
27
  static examples: string[];
28
28
  static flags: {
29
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
30
  limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
30
31
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
31
32
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
32
33
  };
33
34
  run(): Promise<void>;
34
35
  }
36
+ /** Map a queried definition to its display row. Drops the stamp pair (gate
37
+ * plumbing, not view data) and `_id` — `tag.name.vN`, which just repeats
38
+ * name + tag + version, and no command takes an `_id` anyway (they take
39
+ * name + --tag/--version); `name vN` is this CLI's identifier format (the
40
+ * `definition show` title). */
41
+ export declare function definitionRow(r: DefinitionListRow): {
42
+ name: string;
43
+ version: number;
44
+ title: string;
45
+ tag: string;
46
+ stageCount: number;
47
+ inFlightCount: number;
48
+ totalInstances: number;
49
+ _createdAt: string;
50
+ };
35
51
  export {};
@@ -3,8 +3,8 @@ import { WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, assertReadableModel,
3
3
  import logSymbols from 'log-symbols';
4
4
  import { WorkflowCommand } from "../../../lib/base-command.js";
5
5
  import { resolveReadTargets } from "../../../lib/context.js";
6
- import { tagFlags } from "../../../lib/flags.js";
7
- import { runReadAcrossTargets } from "../../../lib/read-fanout.js";
6
+ import { jsonFlags, tagFlags } from "../../../lib/flags.js";
7
+ import { logJsonListing, runReadAcrossTargets } from "../../../lib/read-fanout.js";
8
8
  import { logClippedTable } from "../../../lib/ui.js";
9
9
  export function buildDefinitionListQuery(flags) {
10
10
  const params = { limit: flags.limit + 1 };
@@ -45,6 +45,7 @@ export default class DefinitionList extends WorkflowCommand {
45
45
  static examples = [
46
46
  '<%= config.bin %> definition list',
47
47
  '<%= config.bin %> definition list --tag prod',
48
+ '<%= config.bin %> definition list --json',
48
49
  ];
49
50
  static flags = {
50
51
  ...tagFlags,
@@ -55,35 +56,48 @@ export default class DefinitionList extends WorkflowCommand {
55
56
  name: Flags.string({
56
57
  description: 'Filter to a single workflow definition name (e.g. product-launch).',
57
58
  }),
59
+ ...jsonFlags,
58
60
  };
59
61
  async run() {
60
62
  const { flags } = await this.parse(DefinitionList);
61
63
  const targets = await resolveReadTargets(flags);
62
64
  const { groq, params } = buildDefinitionListQuery(flags);
65
+ if (flags.json) {
66
+ await logJsonListing({
67
+ targets,
68
+ key: 'definitions',
69
+ limit: flags.limit,
70
+ log: (line) => this.log(line),
71
+ fetch: ({ client }) => fetchDefinitionRows({ client, groq, params, limit: flags.limit }),
72
+ toRow: definitionRow,
73
+ });
74
+ return;
75
+ }
63
76
  const failures = await runReadAcrossTargets({
64
77
  targets,
65
78
  log: (line) => this.log(line),
66
79
  run: async ({ client }) => {
67
- const fetched = (await client.fetch(groq, params, {
68
- tag: 'definition.list',
69
- })).map(assertReadableModel);
70
- if (!fetched.length) {
80
+ const fetched = await fetchDefinitionRows({ client, groq, params, limit: flags.limit });
81
+ if (!fetched.rows.length) {
71
82
  this.log(`${logSymbols.info} no definitions found`);
72
83
  return;
73
84
  }
74
85
  logClippedTable({
75
- rows: fetched,
86
+ rows: fetched.rows,
76
87
  limit: flags.limit,
77
88
  headers: ['workflow', 'title', 'tag', 'stages', 'in flight', 'instances', 'created'],
78
- toCells: (r) => [
79
- `${r.name} v${r.version}`,
80
- r.title,
81
- r.tag,
82
- String(r.stageCount),
83
- String(r.inFlightCount),
84
- String(r.totalInstances),
85
- r._createdAt,
86
- ],
89
+ toCells: (r) => {
90
+ const row = definitionRow(r);
91
+ return [
92
+ `${row.name} v${row.version}`,
93
+ row.title,
94
+ row.tag,
95
+ String(row.stageCount),
96
+ String(row.inFlightCount),
97
+ String(row.totalInstances),
98
+ row._createdAt,
99
+ ];
100
+ },
87
101
  log: (line) => this.log(line),
88
102
  });
89
103
  },
@@ -93,3 +107,19 @@ export default class DefinitionList extends WorkflowCommand {
93
107
  }
94
108
  }
95
109
  }
110
+ export function definitionRow(r) {
111
+ return {
112
+ name: r.name,
113
+ version: r.version,
114
+ title: r.title,
115
+ tag: r.tag,
116
+ stageCount: r.stageCount,
117
+ inFlightCount: r.inFlightCount,
118
+ totalInstances: r.totalInstances,
119
+ _createdAt: r._createdAt,
120
+ };
121
+ }
122
+ async function fetchDefinitionRows({ client, groq, params, limit, }) {
123
+ const rows = await client.fetch(groq, params, { tag: 'definition.list' });
124
+ return { rows: rows.map(assertReadableModel), hasMore: rows.length > limit };
125
+ }
@@ -8,6 +8,7 @@ export default class DefinitionShow extends WorkflowCommand {
8
8
  name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
9
9
  };
10
10
  static flags: {
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
12
  version: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  };
@@ -6,7 +6,7 @@ import { WorkflowCommand } from "../../../lib/base-command.js";
6
6
  import { resolveReadTargets, soleHitOrFail } from "../../../lib/context.js";
7
7
  import { buildDefinitionShowQuery, buildDefinitionTagsQuery } from "../../../lib/definitions.js";
8
8
  import { fail } from "../../../lib/fail.js";
9
- import { tagFlags } from "../../../lib/flags.js";
9
+ import { jsonFlags, tagFlags } from "../../../lib/flags.js";
10
10
  import { formatKeyValue, resourceLabel, sectionHeader } from "../../../lib/ui.js";
11
11
  export default class DefinitionShow extends WorkflowCommand {
12
12
  static aliases = ['definition:show'];
@@ -17,6 +17,7 @@ export default class DefinitionShow extends WorkflowCommand {
17
17
  static flags = {
18
18
  ...tagFlags,
19
19
  version: Flags.integer({ description: 'Specific version (default: latest).' }),
20
+ ...jsonFlags,
20
21
  };
21
22
  async run() {
22
23
  const { args, flags } = await this.parse(DefinitionShow);
@@ -32,6 +33,10 @@ export default class DefinitionShow extends WorkflowCommand {
32
33
  exit: 1,
33
34
  });
34
35
  }
36
+ if (flags.json) {
37
+ this.log(JSON.stringify(def, null, 2));
38
+ return;
39
+ }
35
40
  this.log(definitionHeader(def, def.tag));
36
41
  this.log('');
37
42
  for (const line of describeDefinition(def)) {
@@ -77,6 +77,24 @@ export declare function deploymentBanner({ deployment, index, total, }: {
77
77
  index: number;
78
78
  total: number;
79
79
  }): string | undefined;
80
+ /** The deploy target as one label — the deployment's friendly name/tag
81
+ * ({@link deploymentLabel}) and the resource it writes to
82
+ * ({@link resourceLabel}) — so every progress and result line names where a
83
+ * deploy landed, including the common single-deployment run that prints no
84
+ * banner. */
85
+ export declare function targetLabel(deployment: WorkflowDeployment): string;
86
+ /** The deploy spinner's progress line — names the full target it writes to. */
87
+ export declare function deployStartLine(count: number, where: string): string;
88
+ /** The dry-run spinner's progress line — a diff reads the resource's deployed
89
+ * state, so it names the resource it compares against, not the deployment. */
90
+ export declare function dryRunStartLine(count: number, resource: string): string;
91
+ /** A completed deploy/dry-run result line: the past-tense verb, the count, and
92
+ * the full target, so a finished run always shows where it landed. */
93
+ export declare function resultLine({ verb, count, where, }: {
94
+ verb: 'Processed' | 'Diffed';
95
+ count: number;
96
+ where: string;
97
+ }): string;
80
98
  /** One line per deployed definition, symbol-tagged by its result status,
81
99
  * followed by any advisory effect-output lint warnings indented beneath it. */
82
100
  export declare function deployResultLines(results: DeployDefinitionResult[]): string[];
@@ -13,7 +13,7 @@ import { loadWorkflowConfig } from "../../lib/load-config.js";
13
13
  import { deploymentLabel, deploymentToTarget, selectDeployments, } from "../../lib/select-deployment.js";
14
14
  import { shareDefinitionsAfterDeploy } from "../../lib/share-definitions.js";
15
15
  import { cliTelemetry } from "../../lib/telemetry.js";
16
- import { groupBanner } from "../../lib/ui.js";
16
+ import { groupBanner, resourceLabel } from "../../lib/ui.js";
17
17
  export default class Deploy extends WorkflowCommand {
18
18
  static aliases = ['deploy'];
19
19
  static description = 'Validate, diff, and deploy workflow definitions to the resource bound by the selected deployment.';
@@ -110,14 +110,16 @@ export default class Deploy extends WorkflowCommand {
110
110
  }
111
111
  async processDeployment({ deployment, defs, dryRun, client, }) {
112
112
  const target = deploymentToTarget(deployment);
113
+ const where = targetLabel(deployment);
114
+ const resource = resourceLabel(deployment.workflowResource);
113
115
  if (dryRun) {
114
- await this.runDryRun({ client, defs, target });
116
+ await this.runDryRun({ client, defs, target, where, resource });
115
117
  return undefined;
116
118
  }
117
- return this.executeDeploy({ client, defs, target });
119
+ return this.executeDeploy({ client, defs, target, where });
118
120
  }
119
- async executeDeploy({ client, defs, target, }) {
120
- const spinner = ora(`Deploying ${defs.length} definition(s)…`).start();
121
+ async executeDeploy({ client, defs, target, where, }) {
122
+ const spinner = ora(deployStartLine(defs.length, where)).start();
121
123
  try {
122
124
  const deployed = await workflow.deployDefinitions({
123
125
  ...target,
@@ -125,7 +127,7 @@ export default class Deploy extends WorkflowCommand {
125
127
  definitions: defs,
126
128
  telemetry: cliTelemetry().logger,
127
129
  });
128
- spinner.succeed(`Processed ${deployed.results.length} definition(s)`);
130
+ spinner.succeed(resultLine({ verb: 'Processed', count: deployed.results.length, where }));
129
131
  for (const line of deployResultLines(deployed.results))
130
132
  this.log(line);
131
133
  return deployed;
@@ -135,11 +137,11 @@ export default class Deploy extends WorkflowCommand {
135
137
  throw error;
136
138
  }
137
139
  }
138
- async runDryRun({ client, defs, target, }) {
139
- const spinner = ora(`Diffing ${defs.length} definition(s) against dataset…`).start();
140
+ async runDryRun({ client, defs, target, where, resource, }) {
141
+ const spinner = ora(dryRunStartLine(defs.length, resource)).start();
140
142
  try {
141
143
  const entries = await computeDiffEntries({ client, defs, target });
142
- spinner.succeed(`Diffed ${defs.length} definition(s)`);
144
+ spinner.succeed(resultLine({ verb: 'Diffed', count: defs.length, where }));
143
145
  for (const line of diffReport(entries))
144
146
  this.log(line);
145
147
  }
@@ -194,6 +196,18 @@ export function deploySummary({ total, failures, }) {
194
196
  export function deploymentBanner({ deployment, index, total, }) {
195
197
  return groupBanner({ label: deploymentLabel(deployment), index, total });
196
198
  }
199
+ export function targetLabel(deployment) {
200
+ return `${deploymentLabel(deployment)} → ${resourceLabel(deployment.workflowResource)}`;
201
+ }
202
+ export function deployStartLine(count, where) {
203
+ return `Deploying ${count} definition(s) to ${where}…`;
204
+ }
205
+ export function dryRunStartLine(count, resource) {
206
+ return `Diffing ${count} definition(s) against ${resource}…`;
207
+ }
208
+ export function resultLine({ verb, count, where, }) {
209
+ return `${verb} ${count} definition(s) · ${where}`;
210
+ }
197
211
  export function deployResultLines(results) {
198
212
  const symbols = {
199
213
  created: logSymbols.success,
@@ -1,4 +1,4 @@
1
- import { type TerminalState } from '@sanity/workflow-engine';
1
+ import { type GdrUri, type TerminalState } from '@sanity/workflow-engine';
2
2
  import { WorkflowCommand } from '../../lib/base-command.ts';
3
3
  interface InstanceRow {
4
4
  _id: string;
@@ -19,17 +19,19 @@ interface ListFlags {
19
19
  tag?: string | undefined;
20
20
  limit: number;
21
21
  }
22
+ interface ListQueryClient {
23
+ fetch<T>(groq: string, params: Record<string, unknown>, options: {
24
+ tag: string;
25
+ }): Promise<T>;
26
+ }
22
27
  /**
23
28
  * Build the GROQ query + params for `list` from its flags. Pure so the
24
29
  * filter combinations can be asserted without a live dataset. `--tag` is an
25
30
  * optional filter: omit it and the listing spans every partition.
26
31
  *
27
- * With `--document` the lake filter is the engine's prefilter arm — a
28
- * deliberate superset narrowed to the exact watch-set in JS
29
- * ({@link instanceWatchesDocument}) so the rows come back as full
30
- * documents (the narrowing reads fields/stages/ancestors) and UNCAPPED: a
31
- * capped superset could silently hide matches. The display limit applies
32
- * after narrowing.
32
+ * With `--document` the lake filter is the engine's exact watch-set arm. The
33
+ * projection keeps its ref-bearing fields for a defensive JS recheck while
34
+ * omitting the instance's unbounded ledgers and definition snapshot.
33
35
  */
34
36
  export declare function buildListQuery(flags: ListFlags): {
35
37
  groq: string;
@@ -49,6 +51,7 @@ export default class List extends WorkflowCommand {
49
51
  static description: string;
50
52
  static examples: string[];
51
53
  static flags: {
54
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
52
55
  'include-completed': import("@oclif/core/interfaces").BooleanFlag<boolean>;
53
56
  failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
54
57
  definition: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -58,4 +61,15 @@ export default class List extends WorkflowCommand {
58
61
  };
59
62
  run(): Promise<void>;
60
63
  }
64
+ /** Fetch one target's projected rows, defensively rechecking document refs. */
65
+ export declare function fetchRows({ client, groq, params, document, limit, }: {
66
+ client: ListQueryClient;
67
+ groq: string;
68
+ params: Record<string, unknown>;
69
+ document: GdrUri | undefined;
70
+ limit: number;
71
+ }): Promise<{
72
+ rows: InstanceRow[];
73
+ hasMore: boolean;
74
+ }>;
61
75
  export {};
@@ -4,9 +4,26 @@ import logSymbols from 'log-symbols';
4
4
  import { WorkflowCommand } from "../../lib/base-command.js";
5
5
  import { resolveReadTargets } from "../../lib/context.js";
6
6
  import { failOnThrow } from "../../lib/fail.js";
7
- import { tagFlags } from "../../lib/flags.js";
8
- import { runReadAcrossTargets } from "../../lib/read-fanout.js";
7
+ import { jsonFlags, tagFlags } from "../../lib/flags.js";
8
+ import { logJsonListing, runReadAcrossTargets } from "../../lib/read-fanout.js";
9
9
  import { formatAge, logClippedTable } from "../../lib/ui.js";
10
+ const DOCUMENT_LIST_PROJECTION = `{
11
+ _type,
12
+ _id,
13
+ workflowResource,
14
+ modelVersion,
15
+ minReaderModel,
16
+ definition,
17
+ tag,
18
+ currentStage,
19
+ fields,
20
+ stages,
21
+ ancestors,
22
+ subworkflows,
23
+ completedAt,
24
+ abortedAt,
25
+ lastChangedAt
26
+ }`;
10
27
  export function buildListQuery(flags) {
11
28
  const filters = [`_type == "${WORKFLOW_INSTANCE_TYPE}"`];
12
29
  const params = {};
@@ -26,7 +43,11 @@ export function buildListQuery(flags) {
26
43
  const armParams = {};
27
44
  filters.push(`(${documentPrefilter([flags.document], armParams)})`);
28
45
  Object.assign(params, armParams);
29
- return { groq: `*[${filters.join(' && ')}] | order(lastChangedAt desc)`, params };
46
+ params['limit'] = flags.limit + 1;
47
+ return {
48
+ groq: `*[${filters.join(' && ')}] | order(lastChangedAt desc)[0...$limit]${DOCUMENT_LIST_PROJECTION}`,
49
+ params,
50
+ };
30
51
  }
31
52
  params['limit'] = flags.limit + 1;
32
53
  const groq = `*[${filters.join(' && ')}] | order(lastChangedAt desc) [0...$limit]{
@@ -64,6 +85,7 @@ export default class List extends WorkflowCommand {
64
85
  '<%= config.bin %> list --definition productLaunch',
65
86
  '<%= config.bin %> list --document dataset:proj:ds:article-1',
66
87
  '<%= config.bin %> list --tag prod',
88
+ '<%= config.bin %> list --json',
67
89
  ];
68
90
  static flags = {
69
91
  ...tagFlags,
@@ -86,24 +108,42 @@ export default class List extends WorkflowCommand {
86
108
  description: 'Maximum rows to return.',
87
109
  default: 50,
88
110
  }),
111
+ ...jsonFlags,
89
112
  };
90
113
  async run() {
91
114
  const { flags } = await this.parse(List);
92
115
  const targets = await resolveReadTargets(flags);
93
116
  const { groq, params } = failOnThrow('Invalid --document:', () => buildListQuery(flags));
94
117
  const document = flags.document;
118
+ if (flags.json) {
119
+ await logJsonListing({
120
+ targets,
121
+ key: 'instances',
122
+ limit: flags.limit,
123
+ log: (line) => this.log(line),
124
+ fetch: ({ client }) => fetchRows({ client, groq, params, document, limit: flags.limit }),
125
+ toRow: instanceRow,
126
+ });
127
+ return;
128
+ }
95
129
  const failures = await runReadAcrossTargets({
96
130
  targets,
97
131
  log: (line) => this.log(line),
98
132
  run: async ({ client }) => {
99
- const fetched = await fetchRows({ client, groq, params, document });
100
- if (fetched.length === 0) {
101
- this.log(`${logSymbols.info} no instances match`);
133
+ const fetched = await fetchRows({ client, groq, params, document, limit: flags.limit });
134
+ if (fetched.rows.length === 0) {
135
+ if (fetched.hasMore) {
136
+ this.log(`${logSymbols.info} no matches in the first ${flags.limit} candidates — raise --limit to search further`);
137
+ }
138
+ else {
139
+ this.log(`${logSymbols.info} no instances match`);
140
+ }
102
141
  return;
103
142
  }
104
143
  logClippedTable({
105
- rows: fetched,
144
+ rows: fetched.rows,
106
145
  limit: flags.limit,
146
+ moreAvailable: fetched.hasMore,
107
147
  headers: ['instance', 'workflow', 'tag', 'stage', 'status', 'updated'],
108
148
  toCells: (r) => {
109
149
  const row = instanceRow(r);
@@ -125,13 +165,16 @@ export default class List extends WorkflowCommand {
125
165
  }
126
166
  }
127
167
  }
128
- async function fetchRows({ client, groq, params, document, }) {
168
+ export async function fetchRows({ client, groq, params, document, limit, }) {
129
169
  if (document === undefined) {
130
170
  const rows = await client.fetch(groq, params, { tag: 'list' });
131
- return rows.map(assertReadableModel);
171
+ return { rows: rows.map(assertReadableModel), hasMore: rows.length > limit };
132
172
  }
133
173
  const candidates = await client.fetch(groq, params, { tag: 'list' });
134
- return candidates
135
- .map(assertReadableModel)
136
- .filter((instance) => instanceWatchesDocument(instance, document));
174
+ return {
175
+ rows: candidates
176
+ .map(assertReadableModel)
177
+ .filter((instance) => instanceWatchesDocument(instance, document)),
178
+ hasMore: candidates.length > limit,
179
+ };
137
180
  }
@@ -1,10 +1,32 @@
1
- import { StubCommand } from '../../lib/stub.ts';
2
- export default class ResetActivity extends StubCommand {
3
- static hiddenAliases: string[];
4
- static hidden: boolean;
1
+ import { type ResetActivityTarget, type WorkflowInstance } from '@sanity/workflow-engine';
2
+ import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ import { type WriteOutcome, type WriteReport } from '../../lib/ops-report.ts';
4
+ export default class ResetActivity extends WorkflowCommand {
5
+ static aliases: string[];
5
6
  static description: string;
7
+ static examples: string[];
6
8
  static args: {
7
9
  instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
10
  activity: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
9
11
  };
12
+ static flags: {
13
+ skip: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ };
17
+ run(): Promise<void>;
18
+ }
19
+ /** {@link WriteOutcome} with the terminal stamp the no-op copy reads. */
20
+ interface ResetActivityOutcome extends WriteOutcome {
21
+ instance: Pick<WorkflowInstance, 'currentStage' | 'completedAt'>;
10
22
  }
23
+ /**
24
+ * Turn a `resetActivity` result into the spinner message. Pure so the
25
+ * skipped / re-run / already-at-target / terminal / cascaded permutations can
26
+ * be asserted without driving a real reset.
27
+ */
28
+ export declare function resetActivityReport(result: ResetActivityOutcome, { activity, to }: {
29
+ activity: string;
30
+ to: ResetActivityTarget;
31
+ }): WriteReport;
32
+ export {};
@@ -1,11 +1,61 @@
1
- import { Args } from '@oclif/core';
2
- import { StubCommand } from "../../lib/stub.js";
3
- export default class ResetActivity extends StubCommand {
4
- static hiddenAliases = ['reset-activity'];
5
- static hidden = true;
6
- static description = 'Reset a failed activity on an in-flight instance — back to pending (re-run) or to skipped (bypass).';
1
+ import { styleText } from 'node:util';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { workflow } from '@sanity/workflow-engine';
4
+ import { WorkflowCommand } from "../../lib/base-command.js";
5
+ import { resolveInstanceContext } from "../../lib/context.js";
6
+ import { instanceFlags } from "../../lib/flags.js";
7
+ import { buildOperationArgs } from "../../lib/operation-args.js";
8
+ import { cascadeTail, runWriteVerb, } from "../../lib/ops-report.js";
9
+ export default class ResetActivity extends WorkflowCommand {
10
+ static aliases = ['reset-activity'];
11
+ static description = 'Reset a failed activity on an in-flight instance — back to active to re-run it, or --skip to bypass it (mark it skipped) so a gated exit transition can fire.';
12
+ static examples = [
13
+ '<%= config.bin %> reset-activity wf-instance.abc123 legal-review',
14
+ '<%= config.bin %> reset-activity wf-instance.abc123 legal-review --skip',
15
+ ];
7
16
  static args = {
8
17
  instanceId: Args.string({ required: true, description: 'Workflow instance id.' }),
9
18
  activity: Args.string({ required: true, description: 'Activity name within the current stage.' }),
10
19
  };
20
+ static flags = {
21
+ ...instanceFlags,
22
+ skip: Flags.boolean({
23
+ default: false,
24
+ description: 'Bypass the activity (mark it skipped) instead of re-running it (back to active).',
25
+ }),
26
+ };
27
+ async run() {
28
+ const { args, flags } = await this.parse(ResetActivity);
29
+ const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
30
+ const to = flags.skip ? 'skipped' : 'active';
31
+ await runWriteVerb({
32
+ startLabel: `Resetting ${args.activity} on ${args.instanceId}…`,
33
+ failLabel: 'Reset rejected',
34
+ failHeadline: 'reset-activity error:',
35
+ run: () => workflow.resetActivity({
36
+ client,
37
+ ...buildOperationArgs({ scope, instanceId: args.instanceId, reason: undefined }),
38
+ activity: args.activity,
39
+ to,
40
+ }),
41
+ report: (result) => resetActivityReport(result, { activity: args.activity, to }),
42
+ log: (line) => this.log(line),
43
+ });
44
+ }
45
+ }
46
+ export function resetActivityReport(result, { activity, to }) {
47
+ const name = styleText('bold', activity);
48
+ if (!result.changed) {
49
+ const { completedAt } = result.instance;
50
+ const message = completedAt !== undefined
51
+ ? `reset-activity changed nothing — instance is terminal (since ${completedAt})`
52
+ : `reset-activity changed nothing — ${name} is already ${to}`;
53
+ return { changed: false, message };
54
+ }
55
+ const stage = styleText('bold', result.instance.currentStage);
56
+ const verb = to === 'skipped' ? 'Skipped' : 'Reset';
57
+ return {
58
+ changed: true,
59
+ message: `${verb} ${name} — now at ${stage}${cascadeTail(result.cascaded)}`,
60
+ };
11
61
  }
@@ -8,6 +8,7 @@ export default class Show extends WorkflowCommand {
8
8
  instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
9
9
  };
10
10
  static flags: {
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
12
  include: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
12
13
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  };