@sanity/workflow-cli 0.31.0 → 0.33.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.
@@ -1,13 +1,15 @@
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
+ import { stuckDiagnosisLines, stuckHeadline } from "../../lib/cause-detail.js";
6
8
  import { resolveInstanceContext } from "../../lib/context.js";
7
9
  import { fail, failureDetail } from "../../lib/fail.js";
8
10
  import { instanceFlags, jsonFlags } from "../../lib/flags.js";
9
11
  import { baseEngineArgs } from "../../lib/operation-args.js";
10
- import { formatTimestamp, sectionHeader, activityIcon } from "../../lib/ui.js";
12
+ import { sectionHeader, activityIcon } from "../../lib/ui.js";
11
13
  import { instanceHeader } from "./show.js";
12
14
  function formatAssignee(a) {
13
15
  return a.type === 'user' ? `user:${a.id}` : `role:${a.role}`;
@@ -39,57 +41,32 @@ function currentStageLines(input, explanations) {
39
41
  }
40
42
  return lines;
41
43
  }
42
- function failedEffectDetail(effect) {
43
- const ran = effect.durationMs !== undefined ? ` (after ${effect.durationMs}ms)` : '';
44
- return {
45
- headline: `a failed effect from action '${effect.origin.name}' is blocking its activity`,
46
- why: [
47
- styleText('red', `${logSymbols.error} failed effect: ${effect.name}`),
48
- ` queued by action '${effect.origin.name}', failed ${formatTimestamp(effect.ranAt)}${ran}`,
49
- ...(effect.error !== undefined ? [` error: ${effect.error.message}`] : []),
50
- '',
51
- `The activity that fired '${effect.origin.name}' is waiting on this effect. It failed`,
52
- `against an external system, so the activity never resolves and the stage can't advance.`,
53
- ],
54
- };
55
- }
56
- function hungEffectDetail(effect) {
57
- return {
58
- headline: `effect '${effect.name}' was claimed but never completed`,
59
- why: [
60
- styleText('yellow', `${logSymbols.warning} hung effect: ${effect.name}`),
61
- ` claimed ${formatTimestamp(effect.claim?.claimedAt ?? '?')} but never reported back — the`,
62
- ` drainer likely died mid-dispatch, so it won't drain on its own.`,
63
- ],
64
- };
65
- }
66
- function failedActivityDetail(activity) {
67
- return {
68
- headline: `activity '${activity}' failed`,
69
- why: [
70
- styleText('red', `${logSymbols.error} activity '${activity}' is in a terminal failed state.`),
71
- `Any exit transition gated on '${activity}' being done can never fire.`,
72
- ],
73
- };
74
- }
75
44
  function waitingHeadline(w) {
76
45
  const who = w.assignees.length > 0 ? ` on ${w.assignees.map(formatAssignee).join(', ')}` : '';
77
- if (w.actions.length === 0) {
46
+ if (w.waitingFor === 'automation') {
78
47
  return `waiting${who} on automation — a trigger fires on its own when its condition holds`;
79
48
  }
49
+ if (w.waitingFor === 'manual-action')
50
+ return `waiting${who} for an eligible caller to act`;
80
51
  return `waiting${who} for action: ${w.actions.join(' or ')}`;
81
52
  }
82
53
  function waitingLines(w) {
83
54
  const who = w.assignees.length > 0
84
55
  ? `assigned to ${w.assignees.map(formatAssignee).join(', ')}`
85
- : 'unassigned — anyone with permission can act';
86
- if (w.actions.length === 0) {
56
+ : 'unassigned — no holder is selected';
57
+ if (w.waitingFor === 'automation') {
87
58
  return [
88
59
  `${logSymbols.info} activity '${w.activity}' is active (${who}),`,
89
60
  `waiting on automation: its cascade-fired action(s) fire on their own when their`,
90
61
  `trigger conditions hold — nothing for a caller to fire.`,
91
62
  ];
92
63
  }
64
+ if (w.waitingFor === 'manual-action') {
65
+ return [
66
+ `${logSymbols.info} activity '${w.activity}' is active (${who}),`,
67
+ `waiting for a manual action, but none is available to this caller right now.`,
68
+ ];
69
+ }
93
70
  return [
94
71
  `${logSymbols.info} activity '${w.activity}' is active (${who}),`,
95
72
  `waiting for action: ${w.actions.join(' or ')}. This is the normal in-flight`,
@@ -111,44 +88,6 @@ function blockedLines(b) {
111
88
  `activity completing, required content landing). It will not advance on its own.`,
112
89
  ];
113
90
  }
114
- function noTransitionDetail() {
115
- return {
116
- headline: `no exit transition's trigger is satisfied`,
117
- why: [
118
- `${logSymbols.info} every activity is resolved, but no exit transition's \`when\` is true.`,
119
- `Likely a routing state value a trigger reads never got written.`,
120
- ],
121
- };
122
- }
123
- function transitionUnevaluableDetail(transitions) {
124
- return {
125
- headline: `an exit transition's trigger could not be evaluated`,
126
- why: [
127
- `${logSymbols.info} every activity is resolved, but ${transitions.join(', ')} reads an operand`,
128
- `that is missing or unreadable (GROQ null), so routing is held rather than`,
129
- `falling through. Make the data the trigger reads readable — publish the`,
130
- `subject (or fill the field) — and the instance advances on its own; no`,
131
- `set-stage needed.`,
132
- ],
133
- };
134
- }
135
- function remediationLines(remediations) {
136
- return remediations.map((r) => ` • ${r.verb} — ${r.rationale}`);
137
- }
138
- function causeDetail(cause) {
139
- switch (cause.kind) {
140
- case 'failed-effect':
141
- return failedEffectDetail(cause.effect);
142
- case 'hung-effect':
143
- return hungEffectDetail(cause.effect);
144
- case 'failed-activity':
145
- return failedActivityDetail(cause.activity);
146
- case 'no-transition-fires':
147
- return noTransitionDetail();
148
- case 'transition-unevaluable':
149
- return transitionUnevaluableDetail(cause.transitions);
150
- }
151
- }
152
91
  function liveChildrenTail(diagnosis) {
153
92
  if (diagnosis.liveChildren === undefined)
154
93
  return '';
@@ -163,13 +102,13 @@ function statusLine(diagnosis) {
163
102
  case 'blocked':
164
103
  return `${logSymbols.info} ${styleText('cyan', 'blocked')} — ${blockedHeadline(diagnosis)}`;
165
104
  case 'completed':
166
- return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
105
+ return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatDateTime(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
167
106
  case 'aborted': {
168
107
  const tail = diagnosis.reason !== undefined ? ` — ${diagnosis.reason}` : '';
169
- return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
108
+ return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatDateTime(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
170
109
  }
171
110
  case 'stuck':
172
- return `${logSymbols.warning} ${styleText('yellow', 'STUCK')} — ${causeDetail(diagnosis.cause).headline}`;
111
+ return stuckHeadline(diagnosis.cause);
173
112
  }
174
113
  }
175
114
  export function rawTransitionProjection(evaluation) {
@@ -186,7 +125,7 @@ export function rawTransitionProjection(evaluation) {
186
125
  })),
187
126
  }));
188
127
  }
189
- export function renderDiagnosis({ diagnosis, input, remediations, explanations, }) {
128
+ export function renderDiagnosis({ diagnosis, input, remediations, explanations, allMissingDocuments, }) {
190
129
  const lines = [statusLine(diagnosis)];
191
130
  const inFlight = diagnosis.state === 'progressing' ||
192
131
  diagnosis.state === 'waiting' ||
@@ -201,14 +140,7 @@ export function renderDiagnosis({ diagnosis, input, remediations, explanations,
201
140
  if (diagnosis.state === 'blocked') {
202
141
  lines.push('', ...blockedLines(diagnosis));
203
142
  }
204
- if (diagnosis.state === 'stuck') {
205
- const detail = causeDetail(diagnosis.cause);
206
- const runnable = remediations.filter((r) => r.available);
207
- lines.push('', sectionHeader("Why it's stuck"), ...detail.why);
208
- if (runnable.length > 0) {
209
- lines.push('', sectionHeader('Suggested fix'), ...remediationLines(runnable));
210
- }
211
- }
143
+ lines.push(...stuckDiagnosisLines({ diagnosis, allMissingDocuments, remediations }));
212
144
  return lines;
213
145
  }
214
146
  export default class Diagnose extends WorkflowCommand {
@@ -249,7 +181,13 @@ export default class Diagnose extends WorkflowCommand {
249
181
  this.log(instanceHeader(evaluation.instance));
250
182
  this.log('');
251
183
  const explanations = new Map(unsatisfiedTransitionSummaries(evaluation).map((entry) => [entry.transition, entry.summary]));
252
- for (const line of renderDiagnosis({ diagnosis, input, remediations, explanations })) {
184
+ for (const line of renderDiagnosis({
185
+ diagnosis,
186
+ input,
187
+ remediations,
188
+ explanations,
189
+ allMissingDocuments: evaluation.missingDocuments,
190
+ })) {
253
191
  this.log(line);
254
192
  }
255
193
  }
@@ -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,5 +1,6 @@
1
1
  import { type WorkflowInstance } from '@sanity/workflow-engine';
2
2
  import { WorkflowCommand } from '../../lib/base-command.ts';
3
+ import { type InstanceDiagnostic } from '../../lib/cause-detail.ts';
3
4
  export default class Show extends WorkflowCommand {
4
5
  static description: string;
5
6
  static examples: string[];
@@ -12,15 +13,12 @@ export default class Show extends WorkflowCommand {
12
13
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  };
14
15
  run(): Promise<void>;
16
+ private diagnostic;
15
17
  }
16
18
  /** The summary block at the top of `show` (and reused by `diagnose` /
17
19
  * `fire-action`): a bold workflow + id title, then aligned detail rows. */
18
20
  export declare function instanceHeader(instance: WorkflowInstance): string;
19
- /**
20
- * The per-instance body lines: every stage with its activities, any pending
21
- * effects, and (optionally) the history log. Pure so it can be asserted
22
- * without driving the oclif command.
23
- */
24
21
  export declare function describeInstance(instance: WorkflowInstance, options: {
25
22
  includeHistory: boolean;
23
+ diagnostic?: InstanceDiagnostic | undefined;
26
24
  }): string[];
@@ -1,11 +1,14 @@
1
1
  import { styleText } from 'node:util';
2
2
  import { Args, Flags } from '@oclif/core';
3
- import { abortReason, displayTitle, terminalState, } from '@sanity/workflow-engine';
3
+ import { formatDateTime } from '@sanity/cli-core/dates';
4
+ import { abortReason, workflow, errorMessage, displayTitle, terminalState, } from '@sanity/workflow-engine';
4
5
  import logSymbols from 'log-symbols';
5
6
  import { WorkflowCommand } from "../../lib/base-command.js";
7
+ import { stuckDiagnosisLines, stuckHeadline, } from "../../lib/cause-detail.js";
6
8
  import { findInstance, resolveReadTargets } from "../../lib/context.js";
7
9
  import { jsonFlags, tagFlags } from "../../lib/flags.js";
8
- import { formatKeyValue, formatTimestamp, sectionHeader, activityIcon } from "../../lib/ui.js";
10
+ import { baseEngineArgs } from "../../lib/operation-args.js";
11
+ import { formatKeyValue, sectionHeader, activityIcon } from "../../lib/ui.js";
9
12
  export default class Show extends WorkflowCommand {
10
13
  static description = 'Show the state, activities, and effects of a workflow instance.';
11
14
  static examples = [
@@ -41,30 +44,49 @@ export default class Show extends WorkflowCommand {
41
44
  this.log(JSON.stringify(instance, null, 2));
42
45
  return;
43
46
  }
47
+ const diagnostic = await this.diagnostic(hit);
44
48
  this.log(instanceHeader(instance));
45
49
  this.log('');
46
50
  for (const line of describeInstance(instance, {
47
51
  includeHistory: flags.include.includes('history'),
52
+ diagnostic,
48
53
  })) {
49
54
  this.log(line);
50
55
  }
51
56
  }
57
+ async diagnostic(hit) {
58
+ const { instance } = hit;
59
+ if (terminalState(instance) !== 'in-flight')
60
+ return undefined;
61
+ try {
62
+ const { diagnosis, evaluation, remediations } = await workflow.diagnose({
63
+ client: hit.client,
64
+ ...baseEngineArgs({ tag: instance.tag, workflowResource: instance.workflowResource }),
65
+ instanceId: instance._id,
66
+ });
67
+ return { diagnosis, remediations, allMissingDocuments: evaluation.missingDocuments };
68
+ }
69
+ catch (error) {
70
+ this.warn(`Could not evaluate the instance: ${errorMessage(error)}. Showing stored state; reference availability is unknown.`);
71
+ return undefined;
72
+ }
73
+ }
52
74
  }
53
75
  const HEADER_PAD = 9;
54
76
  export function instanceHeader(instance) {
55
77
  const title = `${styleText('bold', `${instance.definition} v${instance.pinnedVersion}`)} ${styleText('cyan', instance._id)}`;
56
78
  const rows = [
57
79
  formatKeyValue({ key: 'Stage', value: instance.currentStage, padTo: HEADER_PAD }),
58
- formatKeyValue({ key: 'Started', value: formatTimestamp(instance.startedAt), padTo: HEADER_PAD }),
80
+ formatKeyValue({ key: 'Started', value: formatDateTime(instance.startedAt), padTo: HEADER_PAD }),
59
81
  ];
60
82
  if (terminalState(instance) === 'aborted' && instance.abortedAt !== undefined) {
61
83
  const reason = abortReason(instance);
62
84
  const tail = reason ? ` — ${reason}` : '';
63
- const value = `${formatTimestamp(instance.abortedAt)}${tail}`;
85
+ const value = `${formatDateTime(instance.abortedAt)}${tail}`;
64
86
  rows.push(formatKeyValue({ key: 'Aborted', value, padTo: HEADER_PAD, emphasis: 'yellow' }));
65
87
  }
66
88
  else if (instance.completedAt) {
67
- const value = formatTimestamp(instance.completedAt);
89
+ const value = formatDateTime(instance.completedAt);
68
90
  rows.push(formatKeyValue({ key: 'Completed', value, padTo: HEADER_PAD, emphasis: 'green' }));
69
91
  }
70
92
  else {
@@ -76,11 +98,17 @@ export function instanceHeader(instance) {
76
98
  }
77
99
  export function describeInstance(instance, options) {
78
100
  return [
101
+ ...diagnosticLines(options.diagnostic),
79
102
  ...stageLines(instance),
80
103
  ...pendingEffectLines(instance),
81
104
  ...(options.includeHistory ? historyLines(instance) : []),
82
105
  ];
83
106
  }
107
+ function diagnosticLines(diagnostic) {
108
+ if (diagnostic?.diagnosis.state !== 'stuck')
109
+ return [];
110
+ return [stuckHeadline(diagnostic.diagnosis.cause), ...stuckDiagnosisLines(diagnostic), ''];
111
+ }
84
112
  function stageLines(instance) {
85
113
  return [
86
114
  sectionHeader('Stages'),
@@ -92,7 +120,7 @@ function stageLines(instance) {
92
120
  }
93
121
  function stageMarker(stage) {
94
122
  return stage.exitedAt
95
- ? styleText('dim', ` (exited ${formatTimestamp(stage.exitedAt)})`)
123
+ ? styleText('dim', ` (exited ${formatDateTime(stage.exitedAt)})`)
96
124
  : styleText('cyan', ' (current)');
97
125
  }
98
126
  function pendingEffectLines(instance) {
@@ -109,6 +137,6 @@ function historyLines(instance) {
109
137
  return [
110
138
  '',
111
139
  sectionHeader('History'),
112
- ...instance.history.map((entry) => ` ${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${displayTitle(entry._type)}`),
140
+ ...instance.history.map((entry) => ` ${styleText('dim', `[${formatDateTime(entry.at)}]`)} ${displayTitle(entry._type)}`),
113
141
  ];
114
142
  }
@@ -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
  }
@@ -6,5 +6,11 @@ import type { Hook } from '@oclif/core';
6
6
  * resolved from raw argv because the store is built before oclif parses
7
7
  * flags. Skips non-workflows commands when this package is mounted as a
8
8
  * host plugin so `sanity dataset list` does not inherit the shell. */
9
+ export declare function installPrerunTelemetry(args: {
10
+ bin: string;
11
+ version: string;
12
+ commandId: string | undefined;
13
+ argv: string[];
14
+ }): Promise<void>;
9
15
  declare const hook: Hook<'prerun'>;
10
16
  export default hook;
@@ -1,16 +1,23 @@
1
1
  import { shouldRunWorkflowCliTelemetry } from "../../command-ids.js";
2
2
  import { shouldForceShareTelemetry } from "../../lib/share-definitions.js";
3
3
  import { setupCliTelemetry } from "../../lib/telemetry-setup.js";
4
- const hook = async function ({ config, Command, argv }) {
5
- if (!shouldRunWorkflowCliTelemetry({
6
- bin: config.bin,
7
- commandId: Command?.id,
8
- })) {
4
+ export async function installPrerunTelemetry(args) {
5
+ const { bin, version, commandId, argv } = args;
6
+ if (!shouldRunWorkflowCliTelemetry({ bin, commandId })) {
9
7
  return;
10
8
  }
11
9
  await setupCliTelemetry({
12
- cliVersion: config.version,
13
- forceSend: shouldForceShareTelemetry({ commandId: Command?.id, argv }),
10
+ cliVersion: version,
11
+ forceSend: shouldForceShareTelemetry({ commandId, argv }),
12
+ ...(commandId !== undefined ? { commandId } : {}),
13
+ });
14
+ }
15
+ const hook = async function ({ config, Command, argv }) {
16
+ await installPrerunTelemetry({
17
+ bin: config.bin,
18
+ version: config.version,
19
+ commandId: Command?.id,
20
+ argv,
14
21
  });
15
22
  };
16
23
  export default hook;