@sanity/workflow-cli 0.20.0 → 0.21.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,39 @@
1
1
  # @sanity/workflow-cli
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9273d55: List the nested `definition` commands (`list`, `show`, `diff`, `delete`) in the
8
+ standalone binary's flat `--help` output. A custom oclif help class surfaces the
9
+ bare `definition <verb>` aliases at the root instead of burying them under the
10
+ `editorial-workflows` topic, so `sanity-workflows --help` shows every command
11
+ without topic-digging. `help --all` still prints the full canonical tree.
12
+ - 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.
13
+
14
+ 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.
15
+
16
+ ### Patch Changes
17
+
18
+ - 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.
19
+ - 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.
20
+
21
+ Bound instance and definition list reads with lake-side filtering and consumer-specific projections.
22
+
23
+ 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.
24
+
25
+ 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.
26
+
27
+ MCP tool telemetry reports a `cursorUsed` boolean so continued list-page adoption is measurable without sending cursor values, arguments, or results.
28
+
29
+ - Updated dependencies [d9394e5]
30
+ - Updated dependencies [92e28bd]
31
+ - Updated dependencies [f4bc057]
32
+ - Updated dependencies [bcc30fe]
33
+ - Updated dependencies [e7392af]
34
+ - Updated dependencies [fa9c796]
35
+ - @sanity/workflow-engine@0.21.0
36
+
3
37
  ## 0.20.0
4
38
 
5
39
  ### 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` |
@@ -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;
@@ -58,4 +60,15 @@ export default class List extends WorkflowCommand {
58
60
  };
59
61
  run(): Promise<void>;
60
62
  }
63
+ /** Fetch one target's projected rows, defensively rechecking document refs. */
64
+ export declare function fetchRows({ client, groq, params, document, limit, }: {
65
+ client: ListQueryClient;
66
+ groq: string;
67
+ params: Record<string, unknown>;
68
+ document: GdrUri | undefined;
69
+ limit: number;
70
+ }): Promise<{
71
+ rows: InstanceRow[];
72
+ hasMore: boolean;
73
+ }>;
61
74
  export {};
@@ -7,6 +7,23 @@ import { failOnThrow } from "../../lib/fail.js";
7
7
  import { tagFlags } from "../../lib/flags.js";
8
8
  import { 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]{
@@ -96,14 +117,20 @@ export default class List extends WorkflowCommand {
96
117
  targets,
97
118
  log: (line) => this.log(line),
98
119
  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`);
120
+ const fetched = await fetchRows({ client, groq, params, document, limit: flags.limit });
121
+ if (fetched.rows.length === 0) {
122
+ if (fetched.hasMore) {
123
+ this.log(`${logSymbols.info} no matches in the first ${flags.limit} candidates — raise --limit to search further`);
124
+ }
125
+ else {
126
+ this.log(`${logSymbols.info} no instances match`);
127
+ }
102
128
  return;
103
129
  }
104
130
  logClippedTable({
105
- rows: fetched,
131
+ rows: fetched.rows,
106
132
  limit: flags.limit,
133
+ moreAvailable: fetched.hasMore,
107
134
  headers: ['instance', 'workflow', 'tag', 'stage', 'status', 'updated'],
108
135
  toCells: (r) => {
109
136
  const row = instanceRow(r);
@@ -125,13 +152,16 @@ export default class List extends WorkflowCommand {
125
152
  }
126
153
  }
127
154
  }
128
- async function fetchRows({ client, groq, params, document, }) {
155
+ export async function fetchRows({ client, groq, params, document, limit, }) {
129
156
  if (document === undefined) {
130
157
  const rows = await client.fetch(groq, params, { tag: 'list' });
131
- return rows.map(assertReadableModel);
158
+ return { rows: rows.map(assertReadableModel), hasMore: rows.length > limit };
132
159
  }
133
160
  const candidates = await client.fetch(groq, params, { tag: 'list' });
134
- return candidates
135
- .map(assertReadableModel)
136
- .filter((instance) => instanceWatchesDocument(instance, document));
161
+ return {
162
+ rows: candidates
163
+ .map(assertReadableModel)
164
+ .filter((instance) => instanceWatchesDocument(instance, document)),
165
+ hasMore: candidates.length > limit,
166
+ };
137
167
  }
@@ -1,10 +1,31 @@
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
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
18
+ /** {@link WriteOutcome} with the terminal stamp the no-op copy reads. */
19
+ interface ResetActivityOutcome extends WriteOutcome {
20
+ instance: Pick<WorkflowInstance, 'currentStage' | 'completedAt'>;
10
21
  }
22
+ /**
23
+ * Turn a `resetActivity` result into the spinner message. Pure so the
24
+ * skipped / re-run / already-at-target / terminal / cascaded permutations can
25
+ * be asserted without driving a real reset.
26
+ */
27
+ export declare function resetActivityReport(result: ResetActivityOutcome, { activity, to }: {
28
+ activity: string;
29
+ to: ResetActivityTarget;
30
+ }): WriteReport;
31
+ 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 { tagFlags } 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
+ ...tagFlags,
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
  }
package/dist/help.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { Help } from '@oclif/core';
2
+ export declare const isBareSurface: (idOrName: string) => boolean;
3
+ export declare const keepCommandAtRoot: (id: string) => boolean;
4
+ /** Standalone-binary help that lists the nested `definition` commands flat in
5
+ * `--help` rather than under a topic. A mounted host governs its own root help. */
6
+ export default class WorkflowHelp extends Help {
7
+ protected showRootHelp(): Promise<void>;
8
+ }
package/dist/help.js ADDED
@@ -0,0 +1,23 @@
1
+ import { Help } from '@oclif/core';
2
+ export const isBareSurface = (idOrName) => !idOrName.startsWith('editorial-workflows');
3
+ export const keepCommandAtRoot = (id) => id !== '' && isBareSurface(id);
4
+ export default class WorkflowHelp extends Help {
5
+ async showRootHelp() {
6
+ if (this.opts.all) {
7
+ await super.showRootHelp();
8
+ return;
9
+ }
10
+ this.log(this.formatRoot());
11
+ this.log('');
12
+ const topics = this.sortedTopics.filter((topic) => isBareSurface(topic.name));
13
+ if (topics.length > 0) {
14
+ this.log(this.formatTopics(topics));
15
+ this.log('');
16
+ }
17
+ const commands = this.sortedCommands.filter((command) => keepCommandAtRoot(command.id));
18
+ if (commands.length > 0) {
19
+ this.log(this.formatCommands(commands));
20
+ this.log('');
21
+ }
22
+ }
23
+ }
package/dist/lib/fail.js CHANGED
@@ -8,7 +8,7 @@ export function fail(headline, detail) {
8
8
  process.stderr.write(`${styleText('red', `${logSymbols.error} ${headline}`)}\n`);
9
9
  if (detail !== undefined && detail !== '') {
10
10
  for (const line of detail.split('\n')) {
11
- process.stderr.write(` ${styleText('red', line)}\n`);
11
+ process.stderr.write(` ${line}\n`);
12
12
  }
13
13
  }
14
14
  return Errors.exit(1);
package/dist/lib/ui.d.ts CHANGED
@@ -37,9 +37,10 @@ export declare function clipToLimit<T>(rows: T[], limit: number): {
37
37
  * rows, render them through {@link formatTable}, print every line, and print
38
38
  * the clip note (when present) beneath the table.
39
39
  */
40
- export declare function logClippedTable<T>({ rows, limit, headers, toCells, log, }: {
40
+ export declare function logClippedTable<T>({ rows, limit, moreAvailable, headers, toCells, log, }: {
41
41
  rows: T[];
42
42
  limit: number;
43
+ moreAvailable?: boolean;
43
44
  headers: string[];
44
45
  toCells: (row: T) => string[];
45
46
  log: (line: string) => void;
package/dist/lib/ui.js CHANGED
@@ -31,13 +31,14 @@ export function clipToLimit(rows, limit) {
31
31
  note: styleText('dim', `showing the first ${limit} — raise --limit to see more`),
32
32
  };
33
33
  }
34
- export function logClippedTable({ rows, limit, headers, toCells, log, }) {
34
+ export function logClippedTable({ rows, limit, moreAvailable, headers, toCells, log, }) {
35
35
  const { rows: clipped, note } = clipToLimit(rows, limit);
36
36
  for (const line of formatTable(headers, clipped.map(toCells))) {
37
37
  log(line);
38
38
  }
39
- if (note) {
40
- log(note);
39
+ if (note || moreAvailable) {
40
+ log(note ??
41
+ styleText('dim', `searched the first ${limit} candidates — raise --limit to see more`));
41
42
  }
42
43
  }
43
44
  export function groupBanner({ label, index, total, }) {
@@ -408,7 +408,9 @@
408
408
  ]
409
409
  },
410
410
  "editorial-workflows:reset-activity": {
411
- "aliases": [],
411
+ "aliases": [
412
+ "reset-activity"
413
+ ],
412
414
  "args": {
413
415
  "instanceId": {
414
416
  "description": "Workflow instance id.",
@@ -421,13 +423,28 @@
421
423
  "required": true
422
424
  }
423
425
  },
424
- "description": "Reset a failed activity on an in-flight instance — back to pending (re-run) or to skipped (bypass).",
425
- "flags": {},
426
- "hasDynamicHelp": false,
427
- "hidden": true,
428
- "hiddenAliases": [
429
- "reset-activity"
426
+ "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.",
427
+ "examples": [
428
+ "<%= config.bin %> reset-activity wf-instance.abc123 legal-review",
429
+ "<%= config.bin %> reset-activity wf-instance.abc123 legal-review --skip"
430
430
  ],
431
+ "flags": {
432
+ "tag": {
433
+ "description": "Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.",
434
+ "name": "tag",
435
+ "hasDynamicHelp": false,
436
+ "multiple": false,
437
+ "type": "option"
438
+ },
439
+ "skip": {
440
+ "description": "Bypass the activity (mark it skipped) instead of re-running it (back to active).",
441
+ "name": "skip",
442
+ "allowNo": false,
443
+ "type": "boolean"
444
+ }
445
+ },
446
+ "hasDynamicHelp": false,
447
+ "hiddenAliases": [],
431
448
  "id": "editorial-workflows:reset-activity",
432
449
  "pluginAlias": "@sanity/workflow-cli",
433
450
  "pluginName": "@sanity/workflow-cli",
@@ -913,5 +930,5 @@
913
930
  ]
914
931
  }
915
932
  },
916
- "version": "0.20.0"
933
+ "version": "0.21.0"
917
934
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-cli",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Command-line tool for deploying, inspecting, and administering Sanity workflow definitions and instances.",
5
5
  "keywords": [
6
6
  "cli",
@@ -62,17 +62,18 @@
62
62
  "@types/node": "^24.12.4",
63
63
  "oclif": "^4.23.16",
64
64
  "vitest": "^4.1.8",
65
- "@sanity/workflow-engine": "0.20.0",
66
- "@sanity/workflow-engine-test": "0.20.0",
67
- "@sanity/workflow-examples": "0.9.0"
65
+ "@sanity/workflow-engine": "0.21.0",
66
+ "@sanity/workflow-engine-test": "0.21.0",
67
+ "@sanity/workflow-examples": "0.10.0"
68
68
  },
69
69
  "peerDependencies": {
70
- "@sanity/workflow-engine": "0.20.0"
70
+ "@sanity/workflow-engine": "0.21.0"
71
71
  },
72
72
  "oclif": {
73
73
  "bin": "sanity-workflows",
74
74
  "commands": "./dist/commands",
75
75
  "dirname": "sanity-workflows",
76
+ "helpClass": "./dist/help",
76
77
  "hooks": {
77
78
  "finally": "./dist/hooks/finally/telemetry",
78
79
  "prerun": "./dist/hooks/prerun/telemetry"