@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 +45 -0
- package/README.md +1 -1
- package/dist/commands/workflows/diagnose.js +17 -8
- package/dist/commands/workflows/list.d.ts +10 -2
- package/dist/commands/workflows/list.js +102 -35
- package/dist/commands/workflows/show.js +7 -6
- package/dist/commands/workflows/tail.js +2 -2
- package/dist/lib/ui.d.ts +0 -6
- package/dist/lib/ui.js +0 -17
- package/oclif.manifest.json +28 -1
- package/package.json +8 -7
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
|
-
-
|
|
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 {
|
|
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 ${
|
|
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 ${
|
|
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.
|
|
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 —
|
|
86
|
-
if (w.
|
|
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 ${
|
|
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 ${
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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,
|
|
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:
|
|
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 = `${
|
|
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 =
|
|
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 ${
|
|
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', `[${
|
|
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', `[${
|
|
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
|
-
}
|
package/oclif.manifest.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
66
|
-
"@sanity/workflow-engine-test": "0.
|
|
67
|
-
"@sanity/workflow-examples": "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/
|
|
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",
|