@sanity/workflow-cli 0.8.1 → 0.10.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 +485 -0
- package/README.md +145 -63
- package/bin/run.js +0 -4
- package/dist/commands/abort.d.ts +8 -12
- package/dist/commands/abort.js +24 -34
- package/dist/commands/definition/delete.d.ts +6 -6
- package/dist/commands/definition/delete.js +17 -33
- package/dist/commands/definition/diff.d.ts +10 -3
- package/dist/commands/definition/diff.js +21 -29
- package/dist/commands/definition/list.d.ts +5 -6
- package/dist/commands/definition/list.js +44 -50
- package/dist/commands/definition/show.d.ts +22 -13
- package/dist/commands/definition/show.js +25 -36
- package/dist/commands/deploy.d.ts +67 -8
- package/dist/commands/deploy.js +136 -32
- package/dist/commands/diagnose.d.ts +22 -4
- package/dist/commands/diagnose.js +45 -61
- package/dist/commands/fire-action.d.ts +7 -30
- package/dist/commands/fire-action.js +42 -115
- package/dist/commands/list.d.ts +9 -10
- package/dist/commands/list.js +49 -62
- package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
- package/dist/commands/{retry-activity.js → reset-activity.js} +2 -3
- package/dist/commands/set-stage.d.ts +27 -3
- package/dist/commands/set-stage.js +63 -12
- package/dist/commands/show.d.ts +3 -2
- package/dist/commands/show.js +15 -21
- package/dist/commands/start.d.ts +56 -0
- package/dist/commands/start.js +133 -0
- package/dist/commands/tail.d.ts +5 -2
- package/dist/commands/tail.js +25 -26
- package/dist/hooks/finally/telemetry.d.ts +8 -0
- package/dist/hooks/finally/telemetry.js +13 -0
- package/dist/hooks/prerun/telemetry.d.ts +5 -0
- package/dist/hooks/prerun/telemetry.js +5 -0
- package/dist/index.js +0 -3
- package/dist/lib/base-command.d.ts +14 -0
- package/dist/lib/base-command.js +10 -0
- package/dist/lib/client.d.ts +11 -14
- package/dist/lib/client.js +29 -34
- package/dist/lib/context.d.ts +98 -0
- package/dist/lib/context.js +83 -0
- package/dist/lib/definitions.d.ts +38 -29
- package/dist/lib/definitions.js +34 -93
- package/dist/lib/diff.d.ts +5 -2
- package/dist/lib/diff.js +62 -20
- package/dist/lib/env.d.ts +4 -6
- package/dist/lib/env.js +4 -9
- package/dist/lib/fail.d.ts +12 -0
- package/dist/lib/fail.js +25 -16
- package/dist/lib/flags.d.ts +7 -2
- package/dist/lib/flags.js +7 -3
- package/dist/lib/load-config.d.ts +15 -0
- package/dist/lib/load-config.js +89 -0
- package/dist/lib/operation-args.d.ts +20 -15
- package/dist/lib/operation-args.js +9 -40
- package/dist/lib/ops-report.d.ts +29 -13
- package/dist/lib/ops-report.js +20 -17
- package/dist/lib/params.d.ts +7 -0
- package/dist/lib/params.js +18 -0
- package/dist/lib/read-fanout.d.ts +22 -0
- package/dist/lib/read-fanout.js +28 -0
- package/dist/lib/select-deployment.d.ts +31 -0
- package/dist/lib/select-deployment.js +37 -0
- package/dist/lib/share-definitions.d.ts +86 -0
- package/dist/lib/share-definitions.js +106 -0
- package/dist/lib/stub.js +0 -7
- package/dist/lib/telemetry-setup.d.ts +66 -0
- package/dist/lib/telemetry-setup.js +92 -0
- package/dist/lib/telemetry.d.ts +100 -0
- package/dist/lib/telemetry.js +89 -0
- package/dist/lib/ui.d.ts +33 -1
- package/dist/lib/ui.js +32 -21
- package/oclif.manifest.json +133 -113
- package/package.json +16 -8
- package/dist/commands/move-stage.d.ts +0 -52
- package/dist/commands/move-stage.js +0 -86
- package/dist/lib/config.d.ts +0 -18
- package/dist/lib/config.js +0 -50
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { type Diagnosis, type DiagnoseInput, type SuggestedRemediation, type WorkflowEvaluation } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
|
+
/** The `--json` transitions payload: raw derived state (atom GROQ, negation,
|
|
4
|
+
* pivotality, solvable requirement) — structure for scripts, never baked
|
|
5
|
+
* English. Pure so the shape is a testable contract. */
|
|
6
|
+
export declare function rawTransitionProjection(evaluation: WorkflowEvaluation): {
|
|
7
|
+
name: string;
|
|
8
|
+
to: string;
|
|
9
|
+
filterSatisfied: boolean;
|
|
10
|
+
unevaluable: boolean;
|
|
11
|
+
blockedBy: {
|
|
12
|
+
requirement?: import("@sanity/groq-condition-describe").AtomRequirement;
|
|
13
|
+
groq: string;
|
|
14
|
+
negated: boolean;
|
|
15
|
+
pivotal: boolean;
|
|
16
|
+
}[];
|
|
17
|
+
}[];
|
|
3
18
|
/**
|
|
4
19
|
* The diagnosis body: the verdict line, the current stage's activities +
|
|
5
20
|
* transitions as evidence (for any in-flight state), and the cause block with
|
|
@@ -8,12 +23,15 @@ import { type Diagnosis, type DiagnoseInput, type SuggestedRemediation } from '@
|
|
|
8
23
|
* {@link SuggestedRemediation}s rather than re-deriving them, so the rendered
|
|
9
24
|
* fix block and the `--json` output stay one computation.
|
|
10
25
|
*/
|
|
11
|
-
export declare function renderDiagnosis({ diagnosis, input, remediations, }: {
|
|
26
|
+
export declare function renderDiagnosis({ diagnosis, input, remediations, explanations, }: {
|
|
12
27
|
diagnosis: Diagnosis;
|
|
13
28
|
input: DiagnoseInput;
|
|
14
29
|
remediations: SuggestedRemediation[];
|
|
30
|
+
/** Insight summaries per unsatisfied transition name — rendered under the
|
|
31
|
+
* transition line so "stuck" reads as "stuck until X". */
|
|
32
|
+
explanations?: ReadonlyMap<string, string> | undefined;
|
|
15
33
|
}): string[];
|
|
16
|
-
export default class Diagnose extends
|
|
34
|
+
export default class Diagnose extends WorkflowCommand {
|
|
17
35
|
static description: string;
|
|
18
36
|
static examples: string[];
|
|
19
37
|
static args: {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args
|
|
3
|
-
import { diagnoseInputFromEvaluation, workflow, } from '@sanity/workflow-engine';
|
|
2
|
+
import { Args } from '@oclif/core';
|
|
3
|
+
import { diagnoseInputFromEvaluation, workflow, unsatisfiedTransitionSummaries, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { fail } from "../lib/fail.js";
|
|
8
|
-
import { tagFlags } from "../lib/flags.js";
|
|
5
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
6
|
+
import { resolveInstanceContext } from "../lib/context.js";
|
|
7
|
+
import { fail, failureDetail } from "../lib/fail.js";
|
|
8
|
+
import { jsonFlags, tagFlags } from "../lib/flags.js";
|
|
9
|
+
import { baseEngineArgs } from "../lib/operation-args.js";
|
|
9
10
|
import { formatTimestamp, sectionHeader, activityIcon } from "../lib/ui.js";
|
|
10
11
|
import { instanceHeader } from "./show.js";
|
|
11
12
|
function formatAssignee(a) {
|
|
@@ -17,11 +18,12 @@ function activityLine(t, assignees) {
|
|
|
17
18
|
: '';
|
|
18
19
|
return ` ${activityIcon[t.status]} ${t.activity.name} ${styleText('dim', `[${t.status}]`)}${who}`;
|
|
19
20
|
}
|
|
20
|
-
function transitionLine(tr) {
|
|
21
|
+
function transitionLine(tr, explanation) {
|
|
21
22
|
const mark = tr.filterSatisfied ? styleText('green', 'ready') : styleText('red', 'false');
|
|
22
|
-
|
|
23
|
+
const line = ` → ${tr.transition.to} ${styleText('dim', tr.transition.filter ?? 'true')} ${mark}`;
|
|
24
|
+
return explanation !== undefined ? `${line}\n ${styleText('dim', `↳ ${explanation}`)}` : line;
|
|
23
25
|
}
|
|
24
|
-
function currentStageLines(input) {
|
|
26
|
+
function currentStageLines(input, explanations) {
|
|
25
27
|
const lines = [
|
|
26
28
|
`${sectionHeader('Stage')} ${input.instance.currentStage}`,
|
|
27
29
|
sectionHeader('Activities'),
|
|
@@ -32,7 +34,7 @@ function currentStageLines(input) {
|
|
|
32
34
|
if (input.transitions.length > 0) {
|
|
33
35
|
lines.push('', sectionHeader('Exit transitions'));
|
|
34
36
|
for (const tr of input.transitions) {
|
|
35
|
-
lines.push(transitionLine(tr));
|
|
37
|
+
lines.push(transitionLine(tr, explanations?.get(tr.transition.name)));
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
return lines;
|
|
@@ -108,11 +110,6 @@ function noTransitionDetail() {
|
|
|
108
110
|
],
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
|
-
/** An unevaluable exit filter is a *recoverable* hold, not a dead end: it
|
|
112
|
-
* clears itself once the operand becomes readable, and no engine verb
|
|
113
|
-
* unsticks it (so the engine emits no remediation). The "what to do" —
|
|
114
|
-
* make the operand readable — lives here in the why, since there is no
|
|
115
|
-
* runnable fix to render. */
|
|
116
113
|
function transitionUnevaluableDetail(transitions) {
|
|
117
114
|
return {
|
|
118
115
|
headline: `an exit transition's filter could not be evaluated`,
|
|
@@ -121,23 +118,12 @@ function transitionUnevaluableDetail(transitions) {
|
|
|
121
118
|
`that is missing or unreadable (GROQ null), so routing is held rather than`,
|
|
122
119
|
`falling through. Make the data the filter reads readable — publish the`,
|
|
123
120
|
`subject (or fill the field) — and the instance advances on its own; no`,
|
|
124
|
-
`
|
|
121
|
+
`set-stage needed.`,
|
|
125
122
|
],
|
|
126
123
|
};
|
|
127
124
|
}
|
|
128
|
-
const REMEDIATION_LABEL = {
|
|
129
|
-
'retry-effect': 'retry-effect',
|
|
130
|
-
'drain-effects': 're-run the effect drainer',
|
|
131
|
-
'reset-activity': 'reset-activity',
|
|
132
|
-
'set-stage': 'move-stage',
|
|
133
|
-
abort: 'abort',
|
|
134
|
-
};
|
|
135
|
-
/** Render the engine's structured remediations as the "suggested fix" block.
|
|
136
|
-
* Only the runnable ones reach here — {@link renderDiagnosis} filters out
|
|
137
|
-
* verbs the CLI can't yet perform so it never advertises a fix you can't run
|
|
138
|
-
* (the full set, planned verbs included, still rides `--json`). */
|
|
139
125
|
function remediationLines(remediations) {
|
|
140
|
-
return remediations.map((r) => ` • ${
|
|
126
|
+
return remediations.map((r) => ` • ${r.verb} — ${r.rationale}`);
|
|
141
127
|
}
|
|
142
128
|
function causeDetail(cause) {
|
|
143
129
|
switch (cause.kind) {
|
|
@@ -153,6 +139,11 @@ function causeDetail(cause) {
|
|
|
153
139
|
return transitionUnevaluableDetail(cause.transitions);
|
|
154
140
|
}
|
|
155
141
|
}
|
|
142
|
+
function liveChildrenTail(diagnosis) {
|
|
143
|
+
if (diagnosis.liveChildren === undefined)
|
|
144
|
+
return '';
|
|
145
|
+
return ` ${styleText('yellow', `${diagnosis.liveChildren} spawned child workflow(s) still running.`)}`;
|
|
146
|
+
}
|
|
156
147
|
function statusLine(diagnosis) {
|
|
157
148
|
switch (diagnosis.state) {
|
|
158
149
|
case 'progressing':
|
|
@@ -162,31 +153,37 @@ function statusLine(diagnosis) {
|
|
|
162
153
|
case 'blocked':
|
|
163
154
|
return `${logSymbols.info} ${styleText('cyan', 'blocked')} — ${blockedHeadline(diagnosis)}`;
|
|
164
155
|
case 'completed':
|
|
165
|
-
return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}
|
|
156
|
+
return `${logSymbols.success} ${styleText('green', 'completed')} at ${formatTimestamp(diagnosis.at)}.${liveChildrenTail(diagnosis)}`;
|
|
166
157
|
case 'aborted': {
|
|
167
158
|
const tail = diagnosis.reason !== undefined ? ` — ${diagnosis.reason}` : '';
|
|
168
|
-
return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}
|
|
159
|
+
return `${logSymbols.info} ${styleText('dim', 'aborted')} at ${formatTimestamp(diagnosis.at)}${tail}.${liveChildrenTail(diagnosis)}`;
|
|
169
160
|
}
|
|
170
161
|
case 'stuck':
|
|
171
162
|
return `${logSymbols.warning} ${styleText('yellow', 'STUCK')} — ${causeDetail(diagnosis.cause).headline}`;
|
|
172
163
|
}
|
|
173
164
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
165
|
+
export function rawTransitionProjection(evaluation) {
|
|
166
|
+
return evaluation.currentStage.transitions.map((t) => ({
|
|
167
|
+
name: t.transition.name,
|
|
168
|
+
to: t.transition.to,
|
|
169
|
+
filterSatisfied: t.filterSatisfied,
|
|
170
|
+
unevaluable: t.unevaluable,
|
|
171
|
+
blockedBy: t.insight.blockedBy.map((atom) => ({
|
|
172
|
+
groq: atom.atom.groq,
|
|
173
|
+
negated: atom.atom.negated,
|
|
174
|
+
pivotal: atom.pivotal,
|
|
175
|
+
...(atom.requirement !== undefined ? { requirement: atom.requirement } : {}),
|
|
176
|
+
})),
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
export function renderDiagnosis({ diagnosis, input, remediations, explanations, }) {
|
|
183
180
|
const lines = [statusLine(diagnosis)];
|
|
184
181
|
const inFlight = diagnosis.state === 'progressing' ||
|
|
185
182
|
diagnosis.state === 'waiting' ||
|
|
186
183
|
diagnosis.state === 'blocked' ||
|
|
187
184
|
diagnosis.state === 'stuck';
|
|
188
185
|
if (inFlight) {
|
|
189
|
-
lines.push('', ...currentStageLines(input));
|
|
186
|
+
lines.push('', ...currentStageLines(input, explanations));
|
|
190
187
|
}
|
|
191
188
|
if (diagnosis.state === 'waiting') {
|
|
192
189
|
lines.push('', ...waitingLines(diagnosis));
|
|
@@ -198,16 +195,13 @@ export function renderDiagnosis({ diagnosis, input, remediations, }) {
|
|
|
198
195
|
const detail = causeDetail(diagnosis.cause);
|
|
199
196
|
const runnable = remediations.filter((r) => r.available);
|
|
200
197
|
lines.push('', sectionHeader("Why it's stuck"), ...detail.why);
|
|
201
|
-
// A recoverable hold (e.g. an unevaluable transition filter) has no
|
|
202
|
-
// runnable remediation — its "what to do" is in the why above, so skip
|
|
203
|
-
// the empty fix header rather than dangle it.
|
|
204
198
|
if (runnable.length > 0) {
|
|
205
199
|
lines.push('', sectionHeader('Suggested fix'), ...remediationLines(runnable));
|
|
206
200
|
}
|
|
207
201
|
}
|
|
208
202
|
return lines;
|
|
209
203
|
}
|
|
210
|
-
export default class Diagnose extends
|
|
204
|
+
export default class Diagnose extends WorkflowCommand {
|
|
211
205
|
static description = "Explain why a workflow instance is or isn't progressing, and what would unstick it.";
|
|
212
206
|
static examples = [
|
|
213
207
|
'<%= config.bin %> diagnose wf-instance.abc123',
|
|
@@ -219,30 +213,18 @@ export default class Diagnose extends Command {
|
|
|
219
213
|
};
|
|
220
214
|
static flags = {
|
|
221
215
|
...tagFlags,
|
|
222
|
-
|
|
223
|
-
description: 'Emit the structured diagnosis as JSON instead of rendered output.',
|
|
224
|
-
default: false,
|
|
225
|
-
}),
|
|
216
|
+
...jsonFlags,
|
|
226
217
|
};
|
|
227
218
|
async run() {
|
|
228
219
|
const { args, flags } = await this.parse(Diagnose);
|
|
229
|
-
const
|
|
230
|
-
const client = loadClient();
|
|
220
|
+
const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
|
|
231
221
|
const { evaluation, diagnosis, remediations } = await workflow
|
|
232
222
|
.diagnose({
|
|
233
223
|
client,
|
|
234
|
-
|
|
235
|
-
workflowResource: config.workflowResource,
|
|
224
|
+
...baseEngineArgs(scope),
|
|
236
225
|
instanceId: args.instanceId,
|
|
237
|
-
// Token-auth CLI can't hit /users/me; pin the same system actor the
|
|
238
|
-
// write verbs use. Transition filters and activity status are actor-
|
|
239
|
-
// independent. A requirement that reads $actor/$assigned/$can is the
|
|
240
|
-
// exception — it's evaluated against this system actor, so a `blocked`
|
|
241
|
-
// verdict on such a requirement reflects the engine's view, not a
|
|
242
|
-
// specific assignee's.
|
|
243
|
-
access: { actor: { kind: 'system', id: 'workflow-cli' } },
|
|
244
226
|
})
|
|
245
|
-
.catch((error) => fail('diagnose failed:',
|
|
227
|
+
.catch((error) => fail('diagnose failed:', failureDetail(error)));
|
|
246
228
|
const input = diagnoseInputFromEvaluation(evaluation);
|
|
247
229
|
if (flags.json) {
|
|
248
230
|
this.log(JSON.stringify({
|
|
@@ -250,12 +232,14 @@ export default class Diagnose extends Command {
|
|
|
250
232
|
stage: evaluation.instance.currentStage,
|
|
251
233
|
diagnosis,
|
|
252
234
|
remediations,
|
|
235
|
+
transitions: rawTransitionProjection(evaluation),
|
|
253
236
|
}, null, 2));
|
|
254
237
|
return;
|
|
255
238
|
}
|
|
256
239
|
this.log(instanceHeader(evaluation.instance));
|
|
257
240
|
this.log('');
|
|
258
|
-
|
|
241
|
+
const explanations = new Map(unsatisfiedTransitionSummaries(evaluation).map((entry) => [entry.transition, entry.summary]));
|
|
242
|
+
for (const line of renderDiagnosis({ diagnosis, input, remediations, explanations })) {
|
|
259
243
|
this.log(line);
|
|
260
244
|
}
|
|
261
245
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
1
|
import { type AvailableAction } from '@sanity/workflow-engine';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
* Parse repeated `--param key=value` flags into a params object. Each
|
|
6
|
-
* value is JSON-parsed so numbers/booleans/objects type correctly,
|
|
7
|
-
* falling back to the raw string. The engine validates the result against
|
|
8
|
-
* the action's declared param types.
|
|
9
|
-
*/
|
|
10
|
-
export declare function parseParams(pairs: string[]): Record<string, unknown>;
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
3
|
+
import { type WriteOutcome, type WriteReport } from '../lib/ops-report.ts';
|
|
11
4
|
/**
|
|
12
5
|
* The list-mode body: every action on the current stage's activities, marked
|
|
13
6
|
* fireable or not (with the disabling reason), plus a hint showing the
|
|
@@ -19,53 +12,37 @@ export declare function renderAvailableActions({ actions, stage, instanceId, }:
|
|
|
19
12
|
stage: string;
|
|
20
13
|
instanceId: string;
|
|
21
14
|
}): string[];
|
|
22
|
-
interface FireOutcome {
|
|
23
|
-
fired: boolean;
|
|
24
|
-
cascaded: number;
|
|
25
|
-
instance: {
|
|
26
|
-
currentStage: string;
|
|
27
|
-
};
|
|
28
|
-
ranOps?: RanOp[];
|
|
29
|
-
}
|
|
30
15
|
/** The `--json` payload for a fired action — the structured counterpart of
|
|
31
16
|
* {@link fireActionReport}. Pure so its shape is asserted directly. */
|
|
32
|
-
export declare function fireResultJson(result:
|
|
17
|
+
export declare function fireResultJson(result: WriteOutcome, ids: {
|
|
33
18
|
instanceId: string;
|
|
34
19
|
activity: string;
|
|
35
20
|
action: string;
|
|
36
21
|
}): Record<string, unknown>;
|
|
37
|
-
export interface FireActionReport {
|
|
38
|
-
fired: boolean;
|
|
39
|
-
message: string;
|
|
40
|
-
opsLines: string[];
|
|
41
|
-
}
|
|
42
22
|
/**
|
|
43
23
|
* Turn a `fireAction` result into the spinner message + ops block. Pure so
|
|
44
24
|
* the fired / no-op / cascaded / ops permutations assert without a live
|
|
45
25
|
* fire.
|
|
46
26
|
*/
|
|
47
27
|
export declare function fireActionReport({ result, activity, action, }: {
|
|
48
|
-
result:
|
|
28
|
+
result: WriteOutcome;
|
|
49
29
|
activity: string;
|
|
50
30
|
action: string;
|
|
51
|
-
}):
|
|
52
|
-
export default class FireAction extends
|
|
31
|
+
}): WriteReport;
|
|
32
|
+
export default class FireAction extends WorkflowCommand {
|
|
53
33
|
static description: string;
|
|
54
34
|
static examples: string[];
|
|
55
35
|
static args: {
|
|
56
36
|
instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
57
37
|
};
|
|
58
38
|
static flags: {
|
|
39
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
59
40
|
activity: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
60
41
|
action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
61
42
|
param: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
62
|
-
as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
63
|
-
'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
64
|
-
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
65
43
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
66
44
|
};
|
|
67
45
|
run(): Promise<void>;
|
|
68
46
|
private listActions;
|
|
69
47
|
private fireOne;
|
|
70
48
|
}
|
|
71
|
-
export {};
|
|
@@ -1,60 +1,20 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { Args,
|
|
3
|
-
import { workflow, } from '@sanity/workflow-engine';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { actionDisabledDetail, deniedGuardLabels, workflow, } from '@sanity/workflow-engine';
|
|
4
4
|
import logSymbols from 'log-symbols';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
5
|
+
import { WorkflowCommand } from "../lib/base-command.js";
|
|
6
|
+
import { resolveInstanceContext } from "../lib/context.js";
|
|
7
|
+
import { fail, failOnThrow, failureDetail } from "../lib/fail.js";
|
|
8
|
+
import { jsonFlags, tagFlags } from "../lib/flags.js";
|
|
9
|
+
import { baseEngineArgs } from "../lib/operation-args.js";
|
|
10
|
+
import { cascadeTail, opsAppliedLines, runWriteVerb, } from "../lib/ops-report.js";
|
|
11
|
+
import { parseParams } from "../lib/params.js";
|
|
12
12
|
import { instanceHeader } from "./show.js";
|
|
13
|
-
function errorMessage(error) {
|
|
14
|
-
return error instanceof Error ? error.message : String(error);
|
|
15
|
-
}
|
|
16
|
-
function coerceParamValue(raw) {
|
|
17
|
-
try {
|
|
18
|
-
return JSON.parse(raw);
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
// Not valid JSON — treat it as a plain string so `--param note=hi`
|
|
22
|
-
// needs no quoting. The engine validates against the action's
|
|
23
|
-
// declared param type either way.
|
|
24
|
-
return raw;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function parseParam(pair) {
|
|
28
|
-
const eq = pair.indexOf('=');
|
|
29
|
-
if (eq < 1) {
|
|
30
|
-
throw new Error(`expected key=value, got "${pair}"`);
|
|
31
|
-
}
|
|
32
|
-
return [pair.slice(0, eq), coerceParamValue(pair.slice(eq + 1))];
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Parse repeated `--param key=value` flags into a params object. Each
|
|
36
|
-
* value is JSON-parsed so numbers/booleans/objects type correctly,
|
|
37
|
-
* falling back to the raw string. The engine validates the result against
|
|
38
|
-
* the action's declared param types.
|
|
39
|
-
*/
|
|
40
|
-
export function parseParams(pairs) {
|
|
41
|
-
return Object.fromEntries(pairs.map(parseParam));
|
|
42
|
-
}
|
|
43
13
|
function reasonText(reason) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return `action filter not satisfied${reason.detail ? ` (${reason.detail})` : ''}`;
|
|
47
|
-
case 'activity-not-active':
|
|
48
|
-
return `activity is ${reason.status}`;
|
|
49
|
-
case 'stage-terminal':
|
|
50
|
-
return `stage '${reason.stage}' is terminal`;
|
|
51
|
-
case 'mutation-guard-denied':
|
|
52
|
-
return `blocked by mutation guard(s) ${reason.guardIds.join(', ')}${reason.detail ? ` (${reason.detail})` : ''}`;
|
|
53
|
-
case 'instance-completed':
|
|
54
|
-
return `instance completed at ${reason.completedAt}`;
|
|
55
|
-
case 'requirements-unmet':
|
|
56
|
-
return `unmet requirement(s): ${reason.unmetRequirements.join(', ')}`;
|
|
14
|
+
if (reason.kind === 'mutation-guard-denied') {
|
|
15
|
+
return `blocked by mutation guard(s) ${deniedGuardLabels(reason.denied).join(', ')}`;
|
|
57
16
|
}
|
|
17
|
+
return actionDisabledDetail(reason);
|
|
58
18
|
}
|
|
59
19
|
function paramLabel(p) {
|
|
60
20
|
return `${p.name}${p.required === true ? '' : '?'}:${p.type}`;
|
|
@@ -73,12 +33,6 @@ function actionLines(a) {
|
|
|
73
33
|
}
|
|
74
34
|
return lines;
|
|
75
35
|
}
|
|
76
|
-
/**
|
|
77
|
-
* The list-mode body: every action on the current stage's activities, marked
|
|
78
|
-
* fireable or not (with the disabling reason), plus a hint showing the
|
|
79
|
-
* flags to fire one. Pure so the populated/empty permutations assert
|
|
80
|
-
* without a live evaluation.
|
|
81
|
-
*/
|
|
82
36
|
export function renderAvailableActions({ actions, stage, instanceId, }) {
|
|
83
37
|
if (actions.length === 0) {
|
|
84
38
|
return [`No actions on the activities of the current stage ('${stage}').`];
|
|
@@ -90,46 +44,37 @@ export function renderAvailableActions({ actions, stage, instanceId, }) {
|
|
|
90
44
|
lines.push('', styleText('dim', 'Fire one with:'), styleText('dim', ` fire-action ${instanceId} --activity <activity> --action <action>`));
|
|
91
45
|
return lines;
|
|
92
46
|
}
|
|
93
|
-
/** The `--json` payload for a fired action — the structured counterpart of
|
|
94
|
-
* {@link fireActionReport}. Pure so its shape is asserted directly. */
|
|
95
47
|
export function fireResultJson(result, ids) {
|
|
96
48
|
return {
|
|
97
49
|
instanceId: ids.instanceId,
|
|
98
50
|
activity: ids.activity,
|
|
99
51
|
action: ids.action,
|
|
100
|
-
|
|
52
|
+
changed: result.changed,
|
|
101
53
|
cascaded: result.cascaded,
|
|
102
54
|
currentStage: result.instance.currentStage,
|
|
103
55
|
...(result.ranOps !== undefined ? { ranOps: result.ranOps } : {}),
|
|
104
56
|
};
|
|
105
57
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Turn a `fireAction` result into the spinner message + ops block. Pure so
|
|
108
|
-
* the fired / no-op / cascaded / ops permutations assert without a live
|
|
109
|
-
* fire.
|
|
110
|
-
*/
|
|
111
58
|
export function fireActionReport({ result, activity, action, }) {
|
|
112
59
|
const stage = styleText('bold', result.instance.currentStage);
|
|
113
|
-
if (!result.
|
|
60
|
+
if (!result.changed) {
|
|
114
61
|
return {
|
|
115
|
-
|
|
62
|
+
changed: false,
|
|
116
63
|
message: `fireAction returned without firing — activity '${activity}' is no longer active`,
|
|
117
64
|
opsLines: [],
|
|
118
65
|
};
|
|
119
66
|
}
|
|
120
|
-
const tail = result.cascaded > 0 ? `, then cascaded ${result.cascaded} auto-transition(s)` : '';
|
|
121
67
|
return {
|
|
122
|
-
|
|
123
|
-
message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${
|
|
68
|
+
changed: true,
|
|
69
|
+
message: `Fired ${styleText('bold', action)} on ${styleText('bold', activity)} — now at ${stage}${cascadeTail(result.cascaded)}`,
|
|
124
70
|
opsLines: opsAppliedLines(result.ranOps),
|
|
125
71
|
};
|
|
126
72
|
}
|
|
127
|
-
export default class FireAction extends
|
|
128
|
-
static description =
|
|
73
|
+
export default class FireAction extends WorkflowCommand {
|
|
74
|
+
static description = 'Fire an action on an instance to unstick a waiting activity — the write acts as the configured token. Omit --action to list what can be fired.';
|
|
129
75
|
static examples = [
|
|
130
76
|
'<%= config.bin %> fire-action wf-instance.abc123',
|
|
131
77
|
'<%= config.bin %> fire-action wf-instance.abc123 --activity approve --action approve',
|
|
132
|
-
'<%= config.bin %> fire-action wf-instance.abc123 --activity approve --action approve --as user.xyz --as-role editor',
|
|
133
78
|
'<%= config.bin %> fire-action wf-instance.abc123 --activity publish --action publish --param note=shipping',
|
|
134
79
|
];
|
|
135
80
|
static args = {
|
|
@@ -148,26 +93,18 @@ export default class FireAction extends Command {
|
|
|
148
93
|
multiple: true,
|
|
149
94
|
default: [],
|
|
150
95
|
}),
|
|
151
|
-
|
|
152
|
-
description: 'Attribute the fire to this Sanity user id (default: a workflow-cli system actor).',
|
|
153
|
-
}),
|
|
154
|
-
'as-role': Flags.string({
|
|
155
|
-
description: 'Role to attribute to the --as user (repeatable), so role-gated filters pass.',
|
|
156
|
-
multiple: true,
|
|
157
|
-
default: [],
|
|
158
|
-
}),
|
|
159
|
-
json: Flags.boolean({
|
|
160
|
-
description: 'Emit structured JSON instead of rendered output.',
|
|
161
|
-
default: false,
|
|
162
|
-
}),
|
|
96
|
+
...jsonFlags,
|
|
163
97
|
};
|
|
164
98
|
async run() {
|
|
165
99
|
const { args, flags } = await this.parse(FireAction);
|
|
166
|
-
const
|
|
167
|
-
const client = loadClient();
|
|
168
|
-
const actor = failOnThrow('fire-action usage error:', () => resolveActor(flags.as, flags['as-role']));
|
|
100
|
+
const { client, scope } = await resolveInstanceContext(flags, args.instanceId);
|
|
169
101
|
if (flags.action === undefined) {
|
|
170
|
-
await this.listActions({
|
|
102
|
+
await this.listActions({
|
|
103
|
+
client,
|
|
104
|
+
scope,
|
|
105
|
+
instanceId: args.instanceId,
|
|
106
|
+
json: flags.json,
|
|
107
|
+
});
|
|
171
108
|
return;
|
|
172
109
|
}
|
|
173
110
|
if (flags.activity === undefined) {
|
|
@@ -175,25 +112,22 @@ export default class FireAction extends Command {
|
|
|
175
112
|
}
|
|
176
113
|
await this.fireOne({
|
|
177
114
|
client,
|
|
178
|
-
|
|
115
|
+
scope,
|
|
179
116
|
instanceId: args.instanceId,
|
|
180
|
-
actor,
|
|
181
117
|
activity: flags.activity,
|
|
182
118
|
action: flags.action,
|
|
183
119
|
params: flags.param,
|
|
184
120
|
json: flags.json,
|
|
185
121
|
});
|
|
186
122
|
}
|
|
187
|
-
async listActions({ client,
|
|
123
|
+
async listActions({ client, scope, instanceId, json, }) {
|
|
188
124
|
const { evaluation, actions } = await workflow
|
|
189
125
|
.availableActions({
|
|
190
126
|
client,
|
|
191
|
-
|
|
192
|
-
workflowResource: config.workflowResource,
|
|
127
|
+
...baseEngineArgs(scope),
|
|
193
128
|
instanceId,
|
|
194
|
-
access: { actor },
|
|
195
129
|
})
|
|
196
|
-
.catch((error) => fail('fire-action failed:',
|
|
130
|
+
.catch((error) => fail('fire-action failed:', failureDetail(error)));
|
|
197
131
|
const stage = evaluation.instance.currentStage;
|
|
198
132
|
if (json) {
|
|
199
133
|
this.log(JSON.stringify({ instanceId, stage, actions }, null, 2));
|
|
@@ -206,37 +140,30 @@ export default class FireAction extends Command {
|
|
|
206
140
|
}
|
|
207
141
|
}
|
|
208
142
|
async fireOne(opts) {
|
|
209
|
-
const { client,
|
|
143
|
+
const { client, scope, instanceId, activity, action, json } = opts;
|
|
210
144
|
const params = failOnThrow('Invalid --param:', () => parseParams(opts.params));
|
|
211
145
|
const fireArgs = {
|
|
212
146
|
client,
|
|
213
|
-
|
|
214
|
-
workflowResource: config.workflowResource,
|
|
147
|
+
...baseEngineArgs(scope),
|
|
215
148
|
instanceId,
|
|
216
149
|
activity,
|
|
217
150
|
action,
|
|
218
|
-
access: { actor },
|
|
219
151
|
params,
|
|
220
152
|
};
|
|
221
153
|
if (json) {
|
|
222
154
|
const result = await workflow
|
|
223
155
|
.fireAction(fireArgs)
|
|
224
|
-
.catch((error) => fail('
|
|
156
|
+
.catch((error) => fail('fire-action error:', failureDetail(error)));
|
|
225
157
|
this.log(JSON.stringify(fireResultJson(result, { instanceId, activity, action }), null, 2));
|
|
226
158
|
return;
|
|
227
159
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
spinner.fail('Action rejected');
|
|
239
|
-
fail('fireAction error:', errorMessage(error));
|
|
240
|
-
}
|
|
160
|
+
await runWriteVerb({
|
|
161
|
+
startLabel: `Firing ${action} on ${activity}…`,
|
|
162
|
+
failLabel: 'Action rejected',
|
|
163
|
+
failHeadline: 'fire-action error:',
|
|
164
|
+
run: () => workflow.fireAction(fireArgs),
|
|
165
|
+
report: (result) => fireActionReport({ result, activity, action }),
|
|
166
|
+
log: (line) => this.log(line),
|
|
167
|
+
});
|
|
241
168
|
}
|
|
242
169
|
}
|
package/dist/commands/list.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type TerminalState } from '@sanity/workflow-engine';
|
|
2
|
+
import { WorkflowCommand } from '../lib/base-command.ts';
|
|
2
3
|
interface InstanceRow {
|
|
3
4
|
_id: string;
|
|
4
5
|
definition: string;
|
|
5
6
|
tag: string;
|
|
6
7
|
currentStage: string;
|
|
7
|
-
completedAt?: string;
|
|
8
|
-
abortedAt?: string;
|
|
8
|
+
completedAt?: string | null;
|
|
9
|
+
abortedAt?: string | null;
|
|
9
10
|
lastChangedAt: string;
|
|
10
11
|
}
|
|
11
|
-
type InstanceStatus = 'aborted' | 'completed' | 'in-flight';
|
|
12
12
|
interface ListFlags {
|
|
13
13
|
'in-flight': boolean;
|
|
14
14
|
failed: boolean;
|
|
15
|
-
|
|
15
|
+
definition?: string | undefined;
|
|
16
16
|
tag?: string | undefined;
|
|
17
17
|
limit: number;
|
|
18
18
|
}
|
|
@@ -25,23 +25,22 @@ export declare function buildListQuery(flags: ListFlags): {
|
|
|
25
25
|
groq: string;
|
|
26
26
|
params: Record<string, unknown>;
|
|
27
27
|
};
|
|
28
|
-
/** Map a queried instance to its display row (derives the status column).
|
|
29
|
-
* Pure — status is the semantic value; the table colors it at render. */
|
|
28
|
+
/** Map a queried instance to its display row (derives the status column). */
|
|
30
29
|
export declare function instanceRow(r: InstanceRow): {
|
|
31
30
|
_id: string;
|
|
32
31
|
definition: string;
|
|
33
32
|
tag: string;
|
|
34
33
|
currentStage: string;
|
|
35
|
-
status:
|
|
34
|
+
status: TerminalState;
|
|
36
35
|
lastChangedAt: string;
|
|
37
36
|
};
|
|
38
|
-
export default class List extends
|
|
37
|
+
export default class List extends WorkflowCommand {
|
|
39
38
|
static description: string;
|
|
40
39
|
static examples: string[];
|
|
41
40
|
static flags: {
|
|
42
41
|
'in-flight': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
43
42
|
failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
44
|
-
|
|
43
|
+
definition: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
44
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
46
45
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
47
46
|
};
|