@sanity/workflow-cli 0.30.0 → 0.32.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,50 @@
1
1
  # @sanity/workflow-cli
2
2
 
3
+ ## 0.32.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a8ed312: **BREAKING:** Assignment is now one ordered user-or-role member-list model: a direct user holder shadows every role in the same activity, role-only values route work to a pool, and singular `assignee` fields allow at most one user while retaining any number of roles. The `claim` field and action sugars have been removed from the authoring DSL; authors using them must replace each pair with an `assignee` field, a literal role seed where the work starts in a pool, ordinary field edits for take/release, a guarded `editable` predicate when second-taker exclusion is required, and `$assigned` on holder-only actions. Definitions that still submit `type: 'claim'` now fail validation.
8
+
9
+ Upgrade every engine, Studio, CLI, MCP, and adapter runtime sharing a workflow resource before deploying a definition containing a singular `assignee`, then acknowledge reader model 9 on deployment. Existing model-8-and-earlier instances remain readable and keep their object/null singular representation; no stored-document backfill is required. Deploy now rejects project-role references absent from the target project's live role catalog and warns when a referenced role has no current human holder, so the deploying identity must be able to read the project role and member directories.
10
+
11
+ Studio assignment matching and holder explanations now follow direct-user shadowing. The CLI adds viewer-scoped assignment list flags and counts, MCP instance listing adds corresponding assignment inputs and counts, and the public waiting `Diagnosis` adds a required `waitingFor` discriminant that distinguishes caller-actionable, manual-but-unavailable, and automation waits without implying that unassigned work is freely actionable.
12
+
13
+ `@sanity/workflow-components` now exports `roleMemberCount`, which reports the distinct people who can fulfill any supplied project role using the workflow definition's aliases. Pass the current member directory, required role names, and normalized `roleAliases`; literal members and alias-only fulfillers are deduplicated by account-global user id. No upgrade action is required unless a custom assignment surface wants alias-aware pool sizes.
14
+
15
+ **Docs impact:** Update the assignment model and authoring references, the reader-model rollout guide, guarded role-pool and take/release examples, the `@sanity/workflow-components` member-selection reference for `roleMemberCount`, Studio task-holder explanations, CLI instance-list flags, MCP list tool reference, deploy role-validation requirements, and migration guidance from removed claim sugar.
16
+
17
+ ### Patch Changes
18
+
19
+ - 8fc1f36: `@sanity/cli-core` is now a peer dependency, which reduces the install size of `@sanity/cli`
20
+
21
+ **No upgrade action required.** The peer dependency isn't marked optional, so package managers will still install it like normal.
22
+
23
+ **Docs impact:** None.
24
+
25
+ - f1c9446: The CLI now formats timestamps with `@sanity/cli-core` instead of installing `date-fns`, reducing the standalone installation size while preserving absolute detail timestamps and relative list timestamps.
26
+
27
+ **No upgrade action required.** Relative timestamps use the runtime's standard English wording.
28
+
29
+ **Docs impact:** None. Timestamp presentation and CLI usage remain unchanged.
30
+
31
+ - Updated dependencies [a8ed312]
32
+ - Updated dependencies [2ba0c09]
33
+ - Updated dependencies [2de38fd]
34
+ - Updated dependencies [a2ce4a7]
35
+ - Updated dependencies [2ddd3d7]
36
+ - Updated dependencies [6035672]
37
+ - Updated dependencies [26dd4e5]
38
+ - Updated dependencies [b04580d]
39
+ - @sanity/workflow-engine@0.32.0
40
+
41
+ ## 0.31.0
42
+
43
+ ### Patch Changes
44
+
45
+ - Updated dependencies [ff72ece]
46
+ - @sanity/workflow-engine@0.31.0
47
+
3
48
  ## 0.30.0
4
49
 
5
50
  ### Minor Changes
package/README.md CHANGED
@@ -255,6 +255,6 @@ Mirrors the Sanity CLI's stack so the eventual plugin port is small:
255
255
  - `@oclif/plugin-help` for help output.
256
256
  - `ora`, `boxen`, `log-symbols`, `console-table-printer` for terminal UX;
257
257
  color comes from `node:util`'s `styleText` (no separate color dep).
258
- - `date-fns` for timestamp formatting (absolute for detail views, relative
258
+ - `@sanity/cli-core` for timestamp formatting (absolute for detail views, relative
259
259
  "… ago" for list tables).
260
260
  - `diff` for the colored JSON unified diff in `deploy --dry-run`.
@@ -1,5 +1,6 @@
1
1
  import { styleText } from 'node:util';
2
2
  import { Args } from '@oclif/core';
3
+ import { formatDateTime } from '@sanity/cli-core/dates';
3
4
  import { diagnoseInputFromEvaluation, workflow, unsatisfiedTransitionSummaries, } from '@sanity/workflow-engine';
4
5
  import logSymbols from 'log-symbols';
5
6
  import { WorkflowCommand } from "../../lib/base-command.js";
@@ -7,7 +8,7 @@ import { resolveInstanceContext } from "../../lib/context.js";
7
8
  import { fail, failureDetail } from "../../lib/fail.js";
8
9
  import { instanceFlags, jsonFlags } from "../../lib/flags.js";
9
10
  import { baseEngineArgs } from "../../lib/operation-args.js";
10
- import { formatTimestamp, sectionHeader, activityIcon } from "../../lib/ui.js";
11
+ import { sectionHeader, activityIcon } from "../../lib/ui.js";
11
12
  import { instanceHeader } from "./show.js";
12
13
  function formatAssignee(a) {
13
14
  return a.type === 'user' ? `user:${a.id}` : `role:${a.role}`;
@@ -45,7 +46,7 @@ function failedEffectDetail(effect) {
45
46
  headline: `a failed effect from action '${effect.origin.name}' is blocking its activity`,
46
47
  why: [
47
48
  styleText('red', `${logSymbols.error} failed effect: ${effect.name}`),
48
- ` queued by action '${effect.origin.name}', failed ${formatTimestamp(effect.ranAt)}${ran}`,
49
+ ` queued by action '${effect.origin.name}', failed ${formatDateTime(effect.ranAt)}${ran}`,
49
50
  ...(effect.error !== undefined ? [` error: ${effect.error.message}`] : []),
50
51
  '',
51
52
  `The activity that fired '${effect.origin.name}' is waiting on this effect. It failed`,
@@ -58,7 +59,7 @@ function hungEffectDetail(effect) {
58
59
  headline: `effect '${effect.name}' was claimed but never completed`,
59
60
  why: [
60
61
  styleText('yellow', `${logSymbols.warning} hung effect: ${effect.name}`),
61
- ` claimed ${formatTimestamp(effect.claim?.claimedAt ?? '?')} but never reported back — the`,
62
+ ` claimed ${formatDateTime(effect.claim?.claimedAt ?? '?')} but never reported back — the`,
62
63
  ` drainer likely died mid-dispatch, so it won't drain on its own.`,
63
64
  ],
64
65
  };
@@ -74,22 +75,30 @@ function failedActivityDetail(activity) {
74
75
  }
75
76
  function waitingHeadline(w) {
76
77
  const who = w.assignees.length > 0 ? ` on ${w.assignees.map(formatAssignee).join(', ')}` : '';
77
- if (w.actions.length === 0) {
78
+ if (w.waitingFor === 'automation') {
78
79
  return `waiting${who} on automation — a trigger fires on its own when its condition holds`;
79
80
  }
81
+ if (w.waitingFor === 'manual-action')
82
+ return `waiting${who} for an eligible caller to act`;
80
83
  return `waiting${who} for action: ${w.actions.join(' or ')}`;
81
84
  }
82
85
  function waitingLines(w) {
83
86
  const who = w.assignees.length > 0
84
87
  ? `assigned to ${w.assignees.map(formatAssignee).join(', ')}`
85
- : 'unassigned — anyone with permission can act';
86
- if (w.actions.length === 0) {
88
+ : 'unassigned — no holder is selected';
89
+ if (w.waitingFor === 'automation') {
87
90
  return [
88
91
  `${logSymbols.info} activity '${w.activity}' is active (${who}),`,
89
92
  `waiting on automation: its cascade-fired action(s) fire on their own when their`,
90
93
  `trigger conditions hold — nothing for a caller to fire.`,
91
94
  ];
92
95
  }
96
+ if (w.waitingFor === 'manual-action') {
97
+ return [
98
+ `${logSymbols.info} activity '${w.activity}' is active (${who}),`,
99
+ `waiting for a manual action, but none is available to this caller right now.`,
100
+ ];
101
+ }
93
102
  return [
94
103
  `${logSymbols.info} activity '${w.activity}' is active (${who}),`,
95
104
  `waiting for action: ${w.actions.join(' or ')}. This is the normal in-flight`,
@@ -163,10 +172,10 @@ function statusLine(diagnosis) {
163
172
  case 'blocked':
164
173
  return `${logSymbols.info} ${styleText('cyan', 'blocked')} — ${blockedHeadline(diagnosis)}`;
165
174
  case 'completed':
166
- return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
175
+ return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatDateTime(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
167
176
  case 'aborted': {
168
177
  const tail = diagnosis.reason !== undefined ? ` — ${diagnosis.reason}` : '';
169
- return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
178
+ return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatDateTime(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
170
179
  }
171
180
  case 'stuck':
172
181
  return `${logSymbols.warning} ${styleText('yellow', 'STUCK')} — ${causeDetail(diagnosis.cause).headline}`;
@@ -1,4 +1,4 @@
1
- import { type GdrUri, type TerminalState } from '@sanity/workflow-engine';
1
+ import { type AssignmentIdentity, type AssignmentStateCounts, type GdrUri, type StageEntry, type TerminalState } from '@sanity/workflow-engine';
2
2
  import { WorkflowCommand } from '../../lib/base-command.ts';
3
3
  interface InstanceRow {
4
4
  _id: string;
@@ -10,12 +10,16 @@ interface InstanceRow {
10
10
  lastChangedAt: string;
11
11
  modelVersion?: number | null;
12
12
  minReaderModel?: number | null;
13
+ stages?: StageEntry[];
13
14
  }
14
15
  interface ListFlags {
15
16
  'include-completed': boolean;
16
17
  failed: boolean;
17
18
  definition?: string | undefined;
18
19
  document?: string | undefined;
20
+ 'assignment-user'?: string | undefined;
21
+ 'assignment-role'?: string[] | undefined;
22
+ 'assignment-state'?: string[] | undefined;
19
23
  tag?: string | undefined;
20
24
  limit: number;
21
25
  }
@@ -38,13 +42,14 @@ export declare function buildListQuery(flags: ListFlags): {
38
42
  params: Record<string, unknown>;
39
43
  };
40
44
  /** Map a queried instance to its display row (derives the status column). */
41
- export declare function instanceRow(r: InstanceRow): {
45
+ export declare function instanceRow(r: InstanceRow, identity?: AssignmentIdentity): {
42
46
  _id: string;
43
47
  definition: string;
44
48
  tag: string;
45
49
  currentStage: string;
46
50
  status: TerminalState;
47
51
  lastChangedAt: string;
52
+ assignment?: AssignmentStateCounts;
48
53
  };
49
54
  export default class List extends WorkflowCommand {
50
55
  static description: string;
@@ -55,6 +60,9 @@ export default class List extends WorkflowCommand {
55
60
  failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
56
61
  definition: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
57
62
  document: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
63
+ 'assignment-user': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
64
+ 'assignment-role': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
65
+ 'assignment-state': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
58
66
  limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
59
67
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
60
68
  };
@@ -1,12 +1,13 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { WORKFLOW_INSTANCE_TYPE, assertReadableModel, documentPrefilter, inFlightFilter, instanceWatchesDocument, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
2
+ import { formatTimeAgo } from '@sanity/cli-core/dates';
3
+ import { WORKFLOW_INSTANCE_TYPE, assignmentPrefilter, assertReadableModel, documentPrefilter, inFlightFilter, instanceWatchesDocument, instanceAssignmentStateCounts, tagScopeFilter, terminalState, } from '@sanity/workflow-engine';
3
4
  import logSymbols from 'log-symbols';
4
5
  import { WorkflowCommand } from "../../lib/base-command.js";
5
6
  import { resolveReadTargets } from "../../lib/context.js";
6
7
  import { failOnThrow } from "../../lib/fail.js";
7
8
  import { jsonFlags, tagFlags } from "../../lib/flags.js";
8
9
  import { logJsonListing, runReadAcrossTargets } from "../../lib/read-fanout.js";
9
- import { formatAge, logClippedTable } from "../../lib/ui.js";
10
+ import { logClippedTable } from "../../lib/ui.js";
10
11
  const DOCUMENT_LIST_PROJECTION = `{
11
12
  _type,
12
13
  _id,
@@ -39,6 +40,12 @@ export function buildListQuery(flags) {
39
40
  filters.push(tagScopeFilter());
40
41
  params['tag'] = flags.tag;
41
42
  }
43
+ const assignment = assignmentFilter(flags);
44
+ if (assignment !== undefined) {
45
+ const armParams = {};
46
+ filters.push(assignmentPrefilter(assignment, armParams));
47
+ Object.assign(params, armParams);
48
+ }
42
49
  if (flags.document) {
43
50
  const armParams = {};
44
51
  filters.push(`(${documentPrefilter([flags.document], armParams)})`);
@@ -60,10 +67,28 @@ export function buildListQuery(flags) {
60
67
  lastChangedAt,
61
68
  modelVersion,
62
69
  minReaderModel
70
+ ${assignment === undefined ? '' : ', stages'}
63
71
  }`;
64
72
  return { groq, params };
65
73
  }
66
- export function instanceRow(r) {
74
+ function assignmentFilter(flags) {
75
+ const userId = flags['assignment-user'];
76
+ const roles = flags['assignment-role'];
77
+ const rawStates = flags['assignment-state'];
78
+ if (userId === undefined) {
79
+ if (roles !== undefined || rawStates !== undefined) {
80
+ throw new Error('--assignment-role and --assignment-state require --assignment-user');
81
+ }
82
+ return undefined;
83
+ }
84
+ const states = rawStates;
85
+ return {
86
+ userId,
87
+ ...(roles === undefined ? {} : { roles }),
88
+ ...(states === undefined ? {} : { states }),
89
+ };
90
+ }
91
+ export function instanceRow(r, identity) {
67
92
  return {
68
93
  _id: r._id,
69
94
  definition: r.definition,
@@ -74,8 +99,57 @@ export function instanceRow(r) {
74
99
  ...(r.abortedAt != null ? { abortedAt: r.abortedAt } : {}),
75
100
  }),
76
101
  lastChangedAt: r.lastChangedAt,
102
+ ...(identity === undefined ? {} : { assignment: assignmentCounts(r, identity) }),
77
103
  };
78
104
  }
105
+ function assignmentCounts(row, identity) {
106
+ return instanceAssignmentStateCounts({ currentStage: row.currentStage, stages: row.stages ?? [] }, identity);
107
+ }
108
+ function assignmentIdentityFor(flags) {
109
+ const assignment = assignmentFilter(flags);
110
+ return assignment === undefined
111
+ ? undefined
112
+ : { userId: assignment.userId, roles: assignment.roles ?? [] };
113
+ }
114
+ async function logTargetInstances(args) {
115
+ const fetched = await fetchRows(args);
116
+ if (fetched.rows.length === 0) {
117
+ const message = fetched.hasMore
118
+ ? `no matches in the first ${args.limit} candidates — raise --limit to search further`
119
+ : 'no instances match';
120
+ args.log(`${logSymbols.info} ${message}`);
121
+ return;
122
+ }
123
+ logClippedTable({
124
+ rows: fetched.rows,
125
+ limit: args.limit,
126
+ moreAvailable: fetched.hasMore,
127
+ headers: [
128
+ 'instance',
129
+ 'workflow',
130
+ 'tag',
131
+ 'stage',
132
+ 'status',
133
+ 'updated',
134
+ ...(args.identity === undefined ? [] : ['assignment u/r/h']),
135
+ ],
136
+ toCells: (candidate) => {
137
+ const row = instanceRow(candidate, args.identity);
138
+ return [
139
+ row._id,
140
+ row.definition,
141
+ row.tag,
142
+ row.currentStage,
143
+ row.status,
144
+ formatTimeAgo(new Date(row.lastChangedAt)),
145
+ ...(row.assignment === undefined
146
+ ? []
147
+ : [`${row.assignment.unrouted}/${row.assignment.routed}/${row.assignment.held}`]),
148
+ ];
149
+ },
150
+ log: args.log,
151
+ });
152
+ }
79
153
  export default class List extends WorkflowCommand {
80
154
  static description = 'List workflow instances in the configured dataset (in-flight by default).';
81
155
  static examples = [
@@ -83,6 +157,7 @@ export default class List extends WorkflowCommand {
83
157
  '<%= config.bin %> workflows list --include-completed',
84
158
  '<%= config.bin %> workflows list --definition productLaunch',
85
159
  '<%= config.bin %> workflows list --document dataset:proj:ds:article-1',
160
+ '<%= config.bin %> workflows list --assignment-user gAda --assignment-role legal --assignment-state routed',
86
161
  '<%= config.bin %> workflows list --tag prod',
87
162
  '<%= config.bin %> workflows list --json',
88
163
  ];
@@ -103,6 +178,18 @@ export default class List extends WorkflowCommand {
103
178
  description: 'Only instances that reference this document (resource-qualified GDR URI, ' +
104
179
  'e.g. "dataset:proj:ds:article-1").',
105
180
  }),
181
+ 'assignment-user': Flags.string({
182
+ description: 'Account-global user id used for viewer-scoped assignment filtering and counts.',
183
+ }),
184
+ 'assignment-role': Flags.string({
185
+ description: 'A literal project role held by --assignment-user. Repeat for multiple roles.',
186
+ multiple: true,
187
+ }),
188
+ 'assignment-state': Flags.string({
189
+ description: 'Assignment state to include: unrouted, routed (offered through a supplied role), or held. Repeat for multiple states.',
190
+ multiple: true,
191
+ options: ['unrouted', 'routed', 'held'],
192
+ }),
106
193
  limit: Flags.integer({
107
194
  description: 'Maximum rows to return.',
108
195
  default: 50,
@@ -112,8 +199,9 @@ export default class List extends WorkflowCommand {
112
199
  async run() {
113
200
  const { flags } = await this.parse(List);
114
201
  const targets = await resolveReadTargets(flags);
115
- const { groq, params } = failOnThrow('Invalid --document:', () => buildListQuery(flags));
202
+ const { groq, params } = failOnThrow('Invalid list filter:', () => buildListQuery(flags));
116
203
  const document = flags.document;
204
+ const identity = assignmentIdentityFor(flags);
117
205
  if (flags.json) {
118
206
  await logJsonListing({
119
207
  targets,
@@ -121,43 +209,22 @@ export default class List extends WorkflowCommand {
121
209
  limit: flags.limit,
122
210
  log: (line) => this.log(line),
123
211
  fetch: ({ client }) => fetchRows({ client, groq, params, document, limit: flags.limit }),
124
- toRow: instanceRow,
212
+ toRow: (row) => instanceRow(row, identity),
125
213
  });
126
214
  return;
127
215
  }
128
216
  const failures = await runReadAcrossTargets({
129
217
  targets,
130
218
  log: (line) => this.log(line),
131
- run: async ({ client }) => {
132
- const fetched = await fetchRows({ client, groq, params, document, limit: flags.limit });
133
- if (fetched.rows.length === 0) {
134
- if (fetched.hasMore) {
135
- this.log(`${logSymbols.info} no matches in the first ${flags.limit} candidates — raise --limit to search further`);
136
- }
137
- else {
138
- this.log(`${logSymbols.info} no instances match`);
139
- }
140
- return;
141
- }
142
- logClippedTable({
143
- rows: fetched.rows,
144
- limit: flags.limit,
145
- moreAvailable: fetched.hasMore,
146
- headers: ['instance', 'workflow', 'tag', 'stage', 'status', 'updated'],
147
- toCells: (r) => {
148
- const row = instanceRow(r);
149
- return [
150
- row._id,
151
- row.definition,
152
- row.tag,
153
- row.currentStage,
154
- row.status,
155
- formatAge(row.lastChangedAt),
156
- ];
157
- },
158
- log: (line) => this.log(line),
159
- });
160
- },
219
+ run: ({ client }) => logTargetInstances({
220
+ client,
221
+ groq,
222
+ params,
223
+ document,
224
+ limit: flags.limit,
225
+ identity,
226
+ log: (line) => this.log(line),
227
+ }),
161
228
  });
162
229
  if (failures.length > 0) {
163
230
  this.exit(1);
@@ -1,11 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
2
  import { Args, Flags } from '@oclif/core';
3
+ import { formatDateTime } from '@sanity/cli-core/dates';
3
4
  import { abortReason, displayTitle, terminalState, } from '@sanity/workflow-engine';
4
5
  import logSymbols from 'log-symbols';
5
6
  import { WorkflowCommand } from "../../lib/base-command.js";
6
7
  import { findInstance, resolveReadTargets } from "../../lib/context.js";
7
8
  import { jsonFlags, tagFlags } from "../../lib/flags.js";
8
- import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../../lib/ui.js";
9
+ import { formatKeyValue, sectionHeader, activityIcon } from "../../lib/ui.js";
9
10
  export default class Show extends WorkflowCommand {
10
11
  static description = 'Show the state, activities, and effects of a workflow instance.';
11
12
  static examples = [
@@ -55,16 +56,16 @@ export function instanceHeader(instance) {
55
56
  const title = `${styleText('bold', `${instance.definition} v${instance.pinnedVersion}`)} ${styleText('cyan', instance._id)}`;
56
57
  const rows = [
57
58
  formatKeyValue({ key: 'Stage', value: instance.currentStage, padTo: HEADER_PAD }),
58
- formatKeyValue({ key: 'Started', value: formatTimestamp(instance.startedAt), padTo: HEADER_PAD }),
59
+ formatKeyValue({ key: 'Started', value: formatDateTime(instance.startedAt), padTo: HEADER_PAD }),
59
60
  ];
60
61
  if (terminalState(instance) === 'aborted' && instance.abortedAt !== undefined) {
61
62
  const reason = abortReason(instance);
62
63
  const tail = reason ? ` — ${reason}` : '';
63
- const value = `${formatTimestamp(instance.abortedAt)}${tail}`;
64
+ const value = `${formatDateTime(instance.abortedAt)}${tail}`;
64
65
  rows.push(formatKeyValue({ key: 'Aborted', value, padTo: HEADER_PAD, emphasis: 'yellow' }));
65
66
  }
66
67
  else if (instance.completedAt) {
67
- const value = formatTimestamp(instance.completedAt);
68
+ const value = formatDateTime(instance.completedAt);
68
69
  rows.push(formatKeyValue({ key: 'Completed', value, padTo: HEADER_PAD, emphasis: 'green' }));
69
70
  }
70
71
  else {
@@ -92,7 +93,7 @@ function stageLines(instance) {
92
93
  }
93
94
  function stageMarker(stage) {
94
95
  return stage.exitedAt
95
- ? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
96
+ ? styleText('dim', ` (exited ${formatDateTime(stage.exitedAt)})`)
96
97
  : styleText('cyan', ' (current)');
97
98
  }
98
99
  function pendingEffectLines(instance) {
@@ -109,6 +110,6 @@ function historyLines(instance) {
109
110
  return [
110
111
  '',
111
112
  sectionHeader('History'),
112
- ...instance.history.map((entry) => ` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${displayTitle(entry._type)}`),
113
+ ...instance.history.map((entry) => ` ${styleText('dim', `[${formatDateTime(entry.at)}]`)} ${displayTitle(entry._type)}`),
113
114
  ];
114
115
  }
@@ -1,12 +1,12 @@
1
1
  import { styleText } from 'node:util';
2
2
  import { Args } from '@oclif/core';
3
+ import { formatDateTime } from '@sanity/cli-core/dates';
3
4
  import { assertReadableModel, displayTitle, errorMessage, } from '@sanity/workflow-engine';
4
5
  import logSymbols from 'log-symbols';
5
6
  import { WorkflowCommand } from "../../lib/base-command.js";
6
7
  import { findInstance, resolveReadTargets } from "../../lib/context.js";
7
8
  import { fail, failureDetail } from "../../lib/fail.js";
8
9
  import { tagFlags } from "../../lib/flags.js";
9
- import { formatTimestamp } from "../../lib/ui.js";
10
10
  const TAIL_TAG = 'tail';
11
11
  export default class Tail extends WorkflowCommand {
12
12
  static description = 'Stream new history entries on a workflow instance as they land in the dataset.';
@@ -70,7 +70,7 @@ export default class Tail extends WorkflowCommand {
70
70
  const next = assertReadableModel(raw);
71
71
  const newEntries = next.history.slice(seen);
72
72
  for (const entry of newEntries) {
73
- this.log(`${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${styleText('cyan', displayTitle(entry._type))}`);
73
+ this.log(`${styleText('dim', `[${formatDateTime(entry.at)}]`)} ${styleText('cyan', displayTitle(entry._type))}`);
74
74
  }
75
75
  return Math.max(seen, next.history.length);
76
76
  }
package/dist/lib/ui.d.ts CHANGED
@@ -69,9 +69,3 @@ export declare function resourceLabel(resource: WorkflowResource): string;
69
69
  /** Status glyph per activity state. log-symbols ship pre-colored (green ✔, red ✖),
70
70
  * so those need no extra wrap; the rest carry their own state color. */
71
71
  export declare const activityIcon: Record<ActivityStatus, string>;
72
- /** An engine ISO-8601 timestamp as an absolute `yyyy-MM-dd HH:mm:ss` — the
73
- * Sanity CLI's audit-log format (see `backups/list`). For detail views. */
74
- export declare function formatTimestamp(iso: string): string;
75
- /** An engine ISO-8601 timestamp as a relative `… ago` — the Sanity CLI's
76
- * job-list format (see `datasets/copy`). For scannable overview tables. */
77
- export declare function formatAge(iso: string): string;
package/dist/lib/ui.js CHANGED
@@ -1,7 +1,4 @@
1
1
  import { stripVTControlCharacters, styleText } from 'node:util';
2
- import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
3
- import { lightFormat } from 'date-fns/lightFormat';
4
- import { parseISO } from 'date-fns/parseISO';
5
2
  import logSymbols from 'log-symbols';
6
3
  export function sectionHeader(title) {
7
4
  return styleText('bold', `${title}:`);
@@ -61,17 +58,3 @@ export const activityIcon = {
61
58
  failed: logSymbols.error,
62
59
  skipped: styleText('dim', '⊘'),
63
60
  };
64
- function parseTimestamp(iso) {
65
- if (!iso)
66
- return undefined;
67
- const date = parseISO(iso);
68
- return Number.isNaN(date.getTime()) ? undefined : date;
69
- }
70
- export function formatTimestamp(iso) {
71
- const date = parseTimestamp(iso);
72
- return date ? lightFormat(date, 'yyyy-MM-dd HH:mm:ss') : iso;
73
- }
74
- export function formatAge(iso) {
75
- const date = parseTimestamp(iso);
76
- return date ? `${formatDistanceToNow(date)} ago` : iso;
77
- }
@@ -289,6 +289,7 @@
289
289
  "<%= config.bin %> workflows list --include-completed",
290
290
  "<%= config.bin %> workflows list --definition productLaunch",
291
291
  "<%= config.bin %> workflows list --document dataset:proj:ds:article-1",
292
+ "<%= config.bin %> workflows list --assignment-user gAda --assignment-role legal --assignment-state routed",
292
293
  "<%= config.bin %> workflows list --tag prod",
293
294
  "<%= config.bin %> workflows list --json"
294
295
  ],
@@ -326,6 +327,32 @@
326
327
  "multiple": false,
327
328
  "type": "option"
328
329
  },
330
+ "assignment-user": {
331
+ "description": "Account-global user id used for viewer-scoped assignment filtering and counts.",
332
+ "name": "assignment-user",
333
+ "hasDynamicHelp": false,
334
+ "multiple": false,
335
+ "type": "option"
336
+ },
337
+ "assignment-role": {
338
+ "description": "A literal project role held by --assignment-user. Repeat for multiple roles.",
339
+ "name": "assignment-role",
340
+ "hasDynamicHelp": false,
341
+ "multiple": true,
342
+ "type": "option"
343
+ },
344
+ "assignment-state": {
345
+ "description": "Assignment state to include: unrouted, routed (offered through a supplied role), or held. Repeat for multiple states.",
346
+ "name": "assignment-state",
347
+ "hasDynamicHelp": false,
348
+ "multiple": true,
349
+ "options": [
350
+ "unrouted",
351
+ "routed",
352
+ "held"
353
+ ],
354
+ "type": "option"
355
+ },
329
356
  "limit": {
330
357
  "description": "Maximum rows to return.",
331
358
  "name": "limit",
@@ -948,5 +975,5 @@
948
975
  ]
949
976
  }
950
977
  },
951
- "version": "0.30.0"
978
+ "version": "0.32.0"
952
979
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-cli",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Command-line tool for deploying, inspecting, and administering Sanity workflow definitions and instances.",
5
5
  "keywords": [
6
6
  "cli",
@@ -47,27 +47,27 @@
47
47
  "dependencies": {
48
48
  "@oclif/core": "^4.11.4",
49
49
  "@oclif/plugin-help": "^6.2.50",
50
- "@sanity/cli-core": "^2.1.1",
51
50
  "@sanity/client": "^7.22.1",
52
51
  "@sanity/telemetry": "^1.1.0",
53
52
  "boxen": "^8.0.1",
54
- "date-fns": "^4.4.0",
55
53
  "diff": "^9.0.0",
56
54
  "jiti": "^2.7.0",
57
55
  "log-symbols": "^7.0.1",
58
56
  "ora": "^9.4.0"
59
57
  },
60
58
  "devDependencies": {
59
+ "@sanity/cli-core": "^3.6.0",
61
60
  "@types/diff": "^8.0.0",
62
61
  "@types/node": "^24.12.4",
63
62
  "oclif": "^4.23.16",
64
63
  "vitest": "^4.1.8",
65
- "@sanity/workflow-engine": "0.30.0",
66
- "@sanity/workflow-engine-test": "0.30.0",
67
- "@sanity/workflow-examples": "0.11.0"
64
+ "@sanity/workflow-engine": "0.32.0",
65
+ "@sanity/workflow-engine-test": "0.32.0",
66
+ "@sanity/workflow-examples": "0.12.0"
68
67
  },
69
68
  "peerDependencies": {
70
- "@sanity/workflow-engine": "0.30.0"
69
+ "@sanity/cli-core": "^3.6.0",
70
+ "@sanity/workflow-engine": "0.32.0"
71
71
  },
72
72
  "oclif": {
73
73
  "bin": "sanity-workflows",
@@ -98,6 +98,7 @@
98
98
  },
99
99
  "scripts": {
100
100
  "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.declarations.json",
101
+ "docs": "node ../../scripts/run-typedoc.mjs",
101
102
  "typecheck": "tsc --noEmit -p tsconfig.json",
102
103
  "test": "vitest run",
103
104
  "test:packaging": "vitest run --config vitest.packaging.config.ts",