@sanity/workflow-cli 0.9.0 → 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 +62 -18
- package/bin/run.js +0 -4
- package/dist/commands/abort.d.ts +4 -6
- package/dist/commands/abort.js +13 -24
- package/dist/commands/definition/delete.d.ts +2 -2
- package/dist/commands/definition/delete.js +14 -27
- package/dist/commands/definition/diff.d.ts +2 -2
- package/dist/commands/definition/diff.js +3 -12
- package/dist/commands/definition/list.d.ts +3 -4
- package/dist/commands/definition/list.js +40 -46
- package/dist/commands/definition/show.d.ts +22 -3
- package/dist/commands/definition/show.js +23 -21
- package/dist/commands/deploy.d.ts +11 -5
- package/dist/commands/deploy.js +54 -57
- package/dist/commands/diagnose.d.ts +22 -4
- package/dist/commands/diagnose.js +39 -45
- package/dist/commands/fire-action.d.ts +2 -4
- package/dist/commands/fire-action.js +27 -61
- package/dist/commands/list.d.ts +7 -8
- package/dist/commands/list.js +43 -56
- package/dist/commands/reset-activity.js +0 -1
- package/dist/commands/set-stage.d.ts +2 -2
- package/dist/commands/set-stage.js +14 -30
- package/dist/commands/show.d.ts +2 -2
- package/dist/commands/show.js +12 -24
- package/dist/commands/start.d.ts +4 -7
- package/dist/commands/start.js +18 -54
- package/dist/commands/tail.d.ts +2 -2
- package/dist/commands/tail.js +20 -25
- 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.js +13 -30
- package/dist/lib/context.d.ts +49 -13
- package/dist/lib/context.js +49 -48
- package/dist/lib/definitions.js +5 -24
- package/dist/lib/diff.d.ts +5 -2
- package/dist/lib/diff.js +61 -27
- package/dist/lib/env.d.ts +4 -0
- package/dist/lib/env.js +4 -3
- package/dist/lib/fail.d.ts +12 -6
- package/dist/lib/fail.js +24 -25
- package/dist/lib/flags.d.ts +0 -7
- package/dist/lib/flags.js +0 -17
- package/dist/lib/load-config.d.ts +11 -7
- package/dist/lib/load-config.js +40 -20
- package/dist/lib/operation-args.d.ts +14 -23
- package/dist/lib/operation-args.js +6 -38
- package/dist/lib/ops-report.d.ts +18 -10
- package/dist/lib/ops-report.js +20 -16
- package/dist/lib/params.js +0 -8
- package/dist/lib/read-fanout.d.ts +22 -0
- package/dist/lib/read-fanout.js +28 -0
- package/dist/lib/select-deployment.js +0 -21
- 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 +8 -47
- package/package.json +14 -8
package/dist/lib/definitions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WORKFLOW_DEFINITION_TYPE, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
2
|
-
import {
|
|
1
|
+
import { WORKFLOW_DEFINITION_TYPE, errorMessage, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
2
|
+
import { fail } from "./fail.js";
|
|
3
3
|
export function buildDefinitionShowQuery(flags) {
|
|
4
4
|
const params = { name: flags.name };
|
|
5
5
|
const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`, 'name == $name'];
|
|
@@ -14,26 +14,16 @@ export function buildDefinitionShowQuery(flags) {
|
|
|
14
14
|
const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
|
|
15
15
|
return { groq, params };
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Fetch the deployed definition a command targets — latest unless pinned via
|
|
19
|
-
* `version`. An absent pinned version always fails (the caller asked for
|
|
20
|
-
* something specific); an absent latest returns `undefined` so the caller
|
|
21
|
-
* decides whether that's fatal.
|
|
22
|
-
*/
|
|
23
17
|
export async function fetchDeployedDefinition({ client, name, tag, version, }) {
|
|
24
18
|
const { groq, params } = buildDefinitionShowQuery({ name, tag, version });
|
|
25
|
-
const deployed = await client.fetch(groq, params
|
|
19
|
+
const deployed = await client.fetch(groq, params, {
|
|
20
|
+
tag: 'definition.load',
|
|
21
|
+
});
|
|
26
22
|
if (deployed === null && version !== undefined) {
|
|
27
23
|
fail(`No deployed definition "${name}" v${version}.`);
|
|
28
24
|
}
|
|
29
25
|
return deployed ?? undefined;
|
|
30
26
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Narrow a batch to one definition name (`only`), failing with the available
|
|
33
|
-
* names when there's no match. With `only` unset the whole batch passes through
|
|
34
|
-
* (a fresh array, so callers can mutate without aliasing). `context` prefixes
|
|
35
|
-
* the failure so callers whose run spans several deployments can attribute it.
|
|
36
|
-
*/
|
|
37
27
|
export function selectDefinitions(allDefinitions, { only, context = '' }) {
|
|
38
28
|
if (!only) {
|
|
39
29
|
return [...allDefinitions];
|
|
@@ -45,11 +35,6 @@ export function selectDefinitions(allDefinitions, { only, context = '' }) {
|
|
|
45
35
|
}
|
|
46
36
|
return filtered;
|
|
47
37
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Per-definition structural/GROQ checks plus one batch-level check the engine
|
|
50
|
-
* doesn't do for us: duplicate `name` within the set. Returns the
|
|
51
|
-
* human-readable error lines; empty means the batch is valid.
|
|
52
|
-
*/
|
|
53
38
|
export function collectValidationErrors(defs) {
|
|
54
39
|
const errors = [];
|
|
55
40
|
const seen = new Map();
|
|
@@ -70,10 +55,6 @@ export function collectValidationErrors(defs) {
|
|
|
70
55
|
}
|
|
71
56
|
return errors;
|
|
72
57
|
}
|
|
73
|
-
/** Validate a set of definitions, exiting with the collected errors on any
|
|
74
|
-
* failure. The single validate-then-fail gate `deploy` and `definition diff`
|
|
75
|
-
* share, so the two can't drift into divergent failure messages. `context`
|
|
76
|
-
* prefixes the failure with the deployment it belongs to in multi-target runs. */
|
|
77
58
|
export function validateOrFail(defs, context = '') {
|
|
78
59
|
const errors = collectValidationErrors(defs);
|
|
79
60
|
if (errors.length > 0) {
|
package/dist/lib/diff.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ import type { DiffEntry } from '@sanity/workflow-engine';
|
|
|
2
2
|
/** One summary line per diff entry, tagged create / unchanged. Definitions are
|
|
3
3
|
* immutable, so a content change is a new version (`create`), never an update. */
|
|
4
4
|
export declare function summaryLines(entries: DiffEntry[]): string[];
|
|
5
|
-
/** Render a JSON diff of `oldObj` → `newObj` as `+`/`-`/context lines
|
|
5
|
+
/** Render a JSON diff of `oldObj` → `newObj` as `+`/`-`/context lines, long
|
|
6
|
+
* unchanged runs collapsed to a dim `… N unchanged lines` marker. */
|
|
6
7
|
export declare function diffLines(oldObj: object, newObj: object): string[];
|
|
7
|
-
/** The full diff report: the summary block plus a per-change diff
|
|
8
|
+
/** The full diff report: the summary block plus a per-change diff of the
|
|
9
|
+
* authored content — the deploy envelope is stripped from both sides (the
|
|
10
|
+
* version and docId already render on the summary line). */
|
|
8
11
|
export declare function diffReport(entries: DiffEntry[]): string[];
|
package/dist/lib/diff.js
CHANGED
|
@@ -2,8 +2,6 @@ import { styleText } from 'node:util';
|
|
|
2
2
|
import { diffJson } from 'diff';
|
|
3
3
|
import logSymbols from 'log-symbols';
|
|
4
4
|
import { sectionHeader } from "./ui.js";
|
|
5
|
-
/** One summary line per diff entry, tagged create / unchanged. Definitions are
|
|
6
|
-
* immutable, so a content change is a new version (`create`), never an update. */
|
|
7
5
|
export function summaryLines(entries) {
|
|
8
6
|
const tags = {
|
|
9
7
|
create: styleText('green', '+ create '),
|
|
@@ -13,34 +11,73 @@ export function summaryLines(entries) {
|
|
|
13
11
|
return ` ${tags[e.status]} ${e.name} v${e.version} ${styleText('dim', `(${e.docId})`)}`;
|
|
14
12
|
});
|
|
15
13
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const CONTEXT_LINES = 3;
|
|
15
|
+
const DEPLOY_STAMPED_KEYS = new Set(['tag', 'version', 'contentHash']);
|
|
16
|
+
function authoredContent(doc) {
|
|
17
|
+
return Object.fromEntries(Object.entries(doc).filter(([key]) => !key.startsWith('_') && !DEPLOY_STAMPED_KEYS.has(key)));
|
|
18
|
+
}
|
|
19
|
+
function chunkKind(chunk) {
|
|
20
|
+
if (chunk.added) {
|
|
21
|
+
return 'added';
|
|
22
|
+
}
|
|
23
|
+
if (chunk.removed) {
|
|
24
|
+
return 'removed';
|
|
25
|
+
}
|
|
26
|
+
return 'context';
|
|
27
|
+
}
|
|
28
|
+
function taggedLines(oldObj, newObj) {
|
|
29
|
+
return diffJson(oldObj, newObj).flatMap((chunk) => {
|
|
20
30
|
const chunkLines = chunk.value.split('\n');
|
|
21
|
-
|
|
22
|
-
// drop it so we don't emit a blank line per chunk boundary.
|
|
23
|
-
if (chunkLines.at(-1) === '')
|
|
31
|
+
if (chunkLines.at(-1) === '') {
|
|
24
32
|
chunkLines.pop();
|
|
25
|
-
for (const line of chunkLines) {
|
|
26
|
-
if (chunk.added)
|
|
27
|
-
out.push(styleText('green', `+ ${line}`));
|
|
28
|
-
else if (chunk.removed)
|
|
29
|
-
out.push(styleText('red', `- ${line}`));
|
|
30
|
-
else
|
|
31
|
-
out.push(styleText('dim', ` ${line}`));
|
|
32
33
|
}
|
|
34
|
+
return chunkLines.map((text) => ({ kind: chunkKind(chunk), text }));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function collapseUnchanged(lines, context) {
|
|
38
|
+
const changed = lines.flatMap((line, i) => (line.kind === 'context' ? [] : [i]));
|
|
39
|
+
if (changed.length === 0) {
|
|
40
|
+
return lines;
|
|
33
41
|
}
|
|
42
|
+
const keep = lines.map((_, i) => changed.some((c) => Math.abs(c - i) <= context));
|
|
43
|
+
const out = [];
|
|
44
|
+
let run = [];
|
|
45
|
+
const flushRun = () => {
|
|
46
|
+
if (run.length === 1) {
|
|
47
|
+
out.push(...run);
|
|
48
|
+
}
|
|
49
|
+
else if (run.length > 1) {
|
|
50
|
+
out.push({ kind: 'elision', count: run.length });
|
|
51
|
+
}
|
|
52
|
+
run = [];
|
|
53
|
+
};
|
|
54
|
+
lines.forEach((line, i) => {
|
|
55
|
+
if (keep[i]) {
|
|
56
|
+
flushRun();
|
|
57
|
+
out.push(line);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
run.push(line);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
flushRun();
|
|
34
64
|
return out;
|
|
35
65
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
66
|
+
function renderLine(line) {
|
|
67
|
+
if (line.kind === 'elision') {
|
|
68
|
+
return styleText('dim', ` … ${line.count} unchanged lines`);
|
|
69
|
+
}
|
|
70
|
+
if (line.kind === 'added') {
|
|
71
|
+
return styleText('green', `+ ${line.text}`);
|
|
72
|
+
}
|
|
73
|
+
if (line.kind === 'removed') {
|
|
74
|
+
return styleText('red', `- ${line.text}`);
|
|
75
|
+
}
|
|
76
|
+
return styleText('dim', ` ${line.text}`);
|
|
77
|
+
}
|
|
78
|
+
export function diffLines(oldObj, newObj) {
|
|
79
|
+
return collapseUnchanged(taggedLines(oldObj, newObj), CONTEXT_LINES).map(renderLine);
|
|
42
80
|
}
|
|
43
|
-
/** The full diff report: the summary block plus a per-change diff. */
|
|
44
81
|
export function diffReport(entries) {
|
|
45
82
|
const lines = ['', sectionHeader('Summary'), ...summaryLines(entries)];
|
|
46
83
|
const changes = entries.filter((e) => e.status !== 'unchanged');
|
|
@@ -49,10 +86,7 @@ export function diffReport(entries) {
|
|
|
49
86
|
return lines;
|
|
50
87
|
}
|
|
51
88
|
for (const e of changes) {
|
|
52
|
-
|
|
53
|
-
// doc as added lines so the operator sees what would land.
|
|
54
|
-
lines.push('', styleText('bold', `── ${e.name} v${e.version} ──`), ...diffLines(e.existing ?? {}, e.expected));
|
|
89
|
+
lines.push('', styleText('bold', `── ${e.name} v${e.version} ──`), ...diffLines(authoredContent(e.existing ?? {}), authoredContent(e.expected)));
|
|
55
90
|
}
|
|
56
|
-
lines.push('', derivedFieldsNote());
|
|
57
91
|
return lines;
|
|
58
92
|
}
|
package/dist/lib/env.d.ts
CHANGED
|
@@ -2,3 +2,7 @@ export declare const ENV: {
|
|
|
2
2
|
readonly SANITY_AUTH_TOKEN: "SANITY_AUTH_TOKEN";
|
|
3
3
|
readonly SANITY_API_HOST: "SANITY_API_HOST";
|
|
4
4
|
};
|
|
5
|
+
/** The explicit env token, or undefined when unset/blank. The single owner
|
|
6
|
+
* of "did the token come from the env?" — token resolution and the auth
|
|
7
|
+
* recovery hint must never disagree on it. */
|
|
8
|
+
export declare function envAuthToken(): string | undefined;
|
package/dist/lib/env.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Typed env var definitions. Client config (projectId/dataset) is derived from
|
|
2
|
-
// the deployment's resource, and the tag comes from `--tag`/the config — so the
|
|
3
|
-
// only env vars left are auth + host overrides.
|
|
4
1
|
export const ENV = {
|
|
5
2
|
SANITY_AUTH_TOKEN: 'SANITY_AUTH_TOKEN',
|
|
6
3
|
SANITY_API_HOST: 'SANITY_API_HOST',
|
|
7
4
|
};
|
|
5
|
+
export function envAuthToken() {
|
|
6
|
+
const token = process.env[ENV.SANITY_AUTH_TOKEN]?.trim();
|
|
7
|
+
return token ? token : undefined;
|
|
8
|
+
}
|
package/dist/lib/fail.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The human-readable message of a caught `unknown`. Strips control characters
|
|
3
|
-
* (keeping newlines and tabs) so server-derived error text can't smuggle
|
|
4
|
-
* terminal escape sequences into CLI output.
|
|
5
|
-
*/
|
|
6
|
-
export declare function errorMessage(error: unknown): string;
|
|
7
1
|
/**
|
|
8
2
|
* Render a user-facing error and exit non-zero. Writes straight to
|
|
9
3
|
* stderr — no oclif framing, no Node stack — so expected failures
|
|
@@ -20,3 +14,15 @@ export declare function fail(headline: string, detail?: string): never;
|
|
|
20
14
|
* helpers pure and unit-testable while giving callers one clean boundary.
|
|
21
15
|
*/
|
|
22
16
|
export declare function failOnThrow<T>(headline: string, fn: () => T): T;
|
|
17
|
+
/** An API rejection of the auth token itself — as opposed to a 403, where
|
|
18
|
+
* the identity is valid but lacks the role for the operation. Walks the
|
|
19
|
+
* `cause` chain because the 401 often arrives wrapped: the engine's
|
|
20
|
+
* `/users/me` actor resolution rethrows it inside its own `Error`. */
|
|
21
|
+
export declare function isAuthRejection(err: unknown): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The detail block for a command failure: the error's message, plus the
|
|
24
|
+
* auth recovery hint when the API rejected the token. Every site that
|
|
25
|
+
* renders a caught command error should build its detail here, so the
|
|
26
|
+
* hint reaches the user no matter which call the token died at.
|
|
27
|
+
*/
|
|
28
|
+
export declare function failureDetail(err: unknown): string;
|
package/dist/lib/fail.js
CHANGED
|
@@ -1,38 +1,17 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
+
import { ClientError } from '@sanity/client';
|
|
3
|
+
import { errorMessage } from '@sanity/workflow-engine';
|
|
2
4
|
import logSymbols from 'log-symbols';
|
|
3
|
-
|
|
4
|
-
* The human-readable message of a caught `unknown`. Strips control characters
|
|
5
|
-
* (keeping newlines and tabs) so server-derived error text can't smuggle
|
|
6
|
-
* terminal escape sequences into CLI output.
|
|
7
|
-
*/
|
|
8
|
-
export function errorMessage(error) {
|
|
9
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
-
// [^\P{Cc}\n\t] = every control character except \n and \t.
|
|
11
|
-
return message.replace(/[^\P{Cc}\n\t]/gu, '');
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Render a user-facing error and exit non-zero. Writes straight to
|
|
15
|
-
* stderr — no oclif framing, no Node stack — so expected failures
|
|
16
|
-
* (validation, missing input, conflicting flags) read clean.
|
|
17
|
-
*
|
|
18
|
-
* For unexpected failures (network, engine throw) prefer letting the
|
|
19
|
-
* error propagate so the stack is visible for debugging.
|
|
20
|
-
*/
|
|
5
|
+
import { envAuthToken } from "./env.js";
|
|
21
6
|
export function fail(headline, detail) {
|
|
22
7
|
process.stderr.write(`${styleText('red', `${logSymbols.error} ${headline}`)}\n`);
|
|
23
8
|
if (detail !== undefined && detail !== '') {
|
|
24
9
|
for (const line of detail.split('\n')) {
|
|
25
|
-
process.stderr.write(` ${line}\n`);
|
|
10
|
+
process.stderr.write(` ${styleText(['dim', 'red'], line)}\n`);
|
|
26
11
|
}
|
|
27
12
|
}
|
|
28
13
|
process.exit(1);
|
|
29
14
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Run `fn`, funnelling a thrown env/validation error through {@link fail}
|
|
32
|
-
* so an expected, user-fixable failure (missing input, bad value) reads
|
|
33
|
-
* clean instead of as an oclif stack trace. Keeps the throwing `read*`
|
|
34
|
-
* helpers pure and unit-testable while giving callers one clean boundary.
|
|
35
|
-
*/
|
|
36
15
|
export function failOnThrow(headline, fn) {
|
|
37
16
|
try {
|
|
38
17
|
return fn();
|
|
@@ -41,3 +20,23 @@ export function failOnThrow(headline, fn) {
|
|
|
41
20
|
fail(headline, errorMessage(err));
|
|
42
21
|
}
|
|
43
22
|
}
|
|
23
|
+
export function isAuthRejection(err) {
|
|
24
|
+
let current = err;
|
|
25
|
+
for (let depth = 0; depth < 5 && current !== undefined; depth++) {
|
|
26
|
+
if (current instanceof ClientError && current.statusCode === 401) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function authRecoveryHint() {
|
|
34
|
+
if (envAuthToken() !== undefined) {
|
|
35
|
+
return 'The SANITY_AUTH_TOKEN token was rejected — check it is valid and has access to this project.';
|
|
36
|
+
}
|
|
37
|
+
return 'Your `sanity login` session may have expired — run `sanity login` and retry.';
|
|
38
|
+
}
|
|
39
|
+
export function failureDetail(err) {
|
|
40
|
+
const message = errorMessage(err);
|
|
41
|
+
return isAuthRejection(err) ? `${message}\n${authRecoveryHint()}` : message;
|
|
42
|
+
}
|
package/dist/lib/flags.d.ts
CHANGED
|
@@ -5,13 +5,6 @@
|
|
|
5
5
|
export declare const tagFlags: {
|
|
6
6
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
};
|
|
8
|
-
/** Actor attribution for write verbs — name a real user (plus roles) instead
|
|
9
|
-
* of the pinned workflow-cli system actor, so assignee- and role-gated
|
|
10
|
-
* filters pass. Commands resolve the pair with `resolveActor`. */
|
|
11
|
-
export declare const actorFlags: {
|
|
12
|
-
as: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
-
'as-role': import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
-
};
|
|
15
8
|
export declare const jsonFlags: {
|
|
16
9
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
10
|
};
|
package/dist/lib/flags.js
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
/** The environment tag (e.g. prod, test). For write commands it selects which
|
|
3
|
-
* deployment in the discovered `sanity.workflow` config to act on (omittable
|
|
4
|
-
* when only one is configured); for read commands it's an optional query
|
|
5
|
-
* filter — and the resource disambiguator when a config spans more than one. */
|
|
6
2
|
export const tagFlags = {
|
|
7
3
|
tag: Flags.string({
|
|
8
4
|
description: 'Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.',
|
|
9
5
|
}),
|
|
10
6
|
};
|
|
11
|
-
/** Actor attribution for write verbs — name a real user (plus roles) instead
|
|
12
|
-
* of the pinned workflow-cli system actor, so assignee- and role-gated
|
|
13
|
-
* filters pass. Commands resolve the pair with `resolveActor`. */
|
|
14
|
-
export const actorFlags = {
|
|
15
|
-
as: Flags.string({
|
|
16
|
-
description: 'Attribute the write to this Sanity user id (default: a workflow-cli system actor).',
|
|
17
|
-
}),
|
|
18
|
-
'as-role': Flags.string({
|
|
19
|
-
description: 'Role to attribute to the --as user (repeatable), so role-gated filters pass.',
|
|
20
|
-
multiple: true,
|
|
21
|
-
default: [],
|
|
22
|
-
}),
|
|
23
|
-
};
|
|
24
7
|
export const jsonFlags = {
|
|
25
8
|
json: Flags.boolean({
|
|
26
9
|
description: 'Emit structured JSON instead of rendered output.',
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type WorkflowConfig } from '@sanity/workflow-engine';
|
|
2
2
|
/**
|
|
3
3
|
* Discover the `sanity.workflow.{ts,js,mjs}` in `cwd`, import its default
|
|
4
|
-
* export, and validate it through {@link defineWorkflowConfig}.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* as a raw object — `export default {deployments: [...]}` without the helper —
|
|
8
|
-
* is still checked, and a malformed one fails with the engine's path-prefixed
|
|
9
|
-
* message at the CLI boundary instead of mid-deploy.
|
|
4
|
+
* export, and validate it through {@link defineWorkflowConfig}. Any problem —
|
|
5
|
+
* no file, unloadable module, invalid config — exits through the clean `fail`
|
|
6
|
+
* path.
|
|
10
7
|
*/
|
|
11
8
|
export declare function loadWorkflowConfig(cwd?: string): Promise<WorkflowConfig>;
|
|
9
|
+
/**
|
|
10
|
+
* {@link loadWorkflowConfig} for telemetry setup, which must never break (or
|
|
11
|
+
* exit) a command: `undefined` when the file is absent or unusable. A broken
|
|
12
|
+
* config is not swallowed for real — the command's own load reports it through
|
|
13
|
+
* the clean `fail` path; this caller just runs with telemetry off.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadWorkflowConfigIfPresent(cwd?: string): Promise<WorkflowConfig | undefined>;
|
package/dist/lib/load-config.js
CHANGED
|
@@ -9,6 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { basename, dirname, join } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { errorMessage } from '@sanity/workflow-engine';
|
|
12
13
|
import { defineWorkflowConfig } from '@sanity/workflow-engine/define';
|
|
13
14
|
import { createJiti } from 'jiti';
|
|
14
15
|
import { fail } from "./fail.js";
|
|
@@ -17,7 +18,6 @@ const CONFIG_FILE_NAMES = [
|
|
|
17
18
|
'sanity.workflow.js',
|
|
18
19
|
'sanity.workflow.mjs',
|
|
19
20
|
];
|
|
20
|
-
/** First `sanity.workflow.{ts,js,mjs}` in `cwd`, or undefined. */
|
|
21
21
|
function findConfigFile(cwd) {
|
|
22
22
|
for (const name of CONFIG_FILE_NAMES) {
|
|
23
23
|
const candidate = join(cwd, name);
|
|
@@ -27,12 +27,15 @@ function findConfigFile(cwd) {
|
|
|
27
27
|
}
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
class ConfigLoadError extends Error {
|
|
31
|
+
headline;
|
|
32
|
+
detail;
|
|
33
|
+
constructor(headline, detail) {
|
|
34
|
+
super(`${headline} ${detail}`);
|
|
35
|
+
this.headline = headline;
|
|
36
|
+
this.detail = detail;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
36
39
|
async function importDefault(filePath) {
|
|
37
40
|
const url = pathToFileURL(filePath).href;
|
|
38
41
|
try {
|
|
@@ -42,28 +45,45 @@ async function importDefault(filePath) {
|
|
|
42
45
|
return (await import(__rewriteRelativeImportExtension(url, true))).default;
|
|
43
46
|
}
|
|
44
47
|
catch (err) {
|
|
45
|
-
|
|
48
|
+
throw new ConfigLoadError(`Failed to load ${basename(filePath)}:`, errorMessage(err));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const parsedConfigs = new Map();
|
|
52
|
+
function parseConfigFile(filePath) {
|
|
53
|
+
const cached = parsedConfigs.get(filePath);
|
|
54
|
+
if (cached !== undefined) {
|
|
55
|
+
return cached;
|
|
46
56
|
}
|
|
57
|
+
const parsed = importDefault(filePath).then((exported) => {
|
|
58
|
+
try {
|
|
59
|
+
return defineWorkflowConfig(exported);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw new ConfigLoadError(`Invalid config in ${basename(filePath)}:`, errorMessage(err));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
parsedConfigs.set(filePath, parsed);
|
|
66
|
+
return parsed;
|
|
47
67
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Discover the `sanity.workflow.{ts,js,mjs}` in `cwd`, import its default
|
|
50
|
-
* export, and validate it through {@link defineWorkflowConfig}.
|
|
51
|
-
*
|
|
52
|
-
* Re-validating here (rather than trusting the export) means a config authored
|
|
53
|
-
* as a raw object — `export default {deployments: [...]}` without the helper —
|
|
54
|
-
* is still checked, and a malformed one fails with the engine's path-prefixed
|
|
55
|
-
* message at the CLI boundary instead of mid-deploy.
|
|
56
|
-
*/
|
|
57
68
|
export async function loadWorkflowConfig(cwd = process.cwd()) {
|
|
58
69
|
const filePath = findConfigFile(cwd);
|
|
59
70
|
if (filePath === undefined) {
|
|
60
71
|
fail(`No ${CONFIG_FILE_NAMES[0]} found in ${cwd}.`, 'Create one that `export default defineWorkflowConfig({deployments: [...]})`.');
|
|
61
72
|
}
|
|
62
|
-
const exported = await importDefault(filePath);
|
|
63
73
|
try {
|
|
64
|
-
return
|
|
74
|
+
return await parseConfigFile(filePath);
|
|
65
75
|
}
|
|
66
76
|
catch (err) {
|
|
67
|
-
|
|
77
|
+
if (err instanceof ConfigLoadError) {
|
|
78
|
+
fail(err.headline, err.detail);
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function loadWorkflowConfigIfPresent(cwd = process.cwd()) {
|
|
84
|
+
const filePath = findConfigFile(cwd);
|
|
85
|
+
if (filePath === undefined) {
|
|
86
|
+
return undefined;
|
|
68
87
|
}
|
|
88
|
+
return parseConfigFile(filePath).catch(() => undefined);
|
|
69
89
|
}
|
|
@@ -1,33 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type EngineScopeArgs, type OperationArgs, type WorkflowTelemetryLogger } from '@sanity/workflow-engine';
|
|
2
2
|
/**
|
|
3
|
-
* The engine-scope a CLI
|
|
3
|
+
* The engine-scope a CLI verb needs: which tag partition and which resource
|
|
4
4
|
* the engine's own data lives in — {@link EngineScopeArgs} minus the client.
|
|
5
5
|
* A {@link import('@sanity/workflow-engine').WorkflowDeployment} satisfies it,
|
|
6
6
|
* so commands pass their resolved deployment straight through.
|
|
7
7
|
*/
|
|
8
8
|
export type EngineScope = Pick<EngineScopeArgs, 'tag' | 'workflowResource'>;
|
|
9
9
|
/**
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The engine args every CLI verb shares: tag scope, workflow resource,
|
|
11
|
+
* the CLI's declared execution context — the advisory "via what" stamped
|
|
12
|
+
* on history; identity itself is always the token behind the client
|
|
13
|
+
* (`/users/me`), never injected — and the invocation's telemetry logger,
|
|
14
|
+
* so the engine's adoption vocabulary flows from CLI-driven operations
|
|
15
|
+
* (the raw-verb equivalent of `createEngine({telemetry})`). Spread by the
|
|
16
|
+
* instance verbs ({@link buildOperationArgs}), the definition verbs
|
|
17
|
+
* (e.g. `definition delete`), and reads like `diagnose`.
|
|
14
18
|
*/
|
|
15
|
-
export declare
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* workflow resource, and the actor the write is attributed to — commands
|
|
19
|
-
* with `--as` resolve one via {@link resolveActor}; the rest pass
|
|
20
|
-
* {@link SYSTEM_ACTOR} explicitly. Spread by both the instance verbs
|
|
21
|
-
* ({@link buildOperationArgs}) and the definition verbs (e.g. `definition delete`).
|
|
22
|
-
*/
|
|
23
|
-
export declare function baseEngineArgs(scope: EngineScope, actor: Actor): EngineScope & Pick<OperationArgs, 'access'>;
|
|
24
|
-
/**
|
|
25
|
-
* Resolve the actor a write is attributed to. With a named user the fire
|
|
26
|
-
* is pinned to it (plus any roles, so role-gated action filters pass);
|
|
27
|
-
* without one the {@link SYSTEM_ACTOR} fallback applies. Roles without a
|
|
28
|
-
* named user throw — they'd silently attach to nothing otherwise.
|
|
29
|
-
*/
|
|
30
|
-
export declare function resolveActor(as: string | undefined, roles: string[]): Actor;
|
|
19
|
+
export declare function baseEngineArgs(scope: EngineScope): EngineScope & Pick<EngineScopeArgs, 'executionContext'> & {
|
|
20
|
+
telemetry: WorkflowTelemetryLogger;
|
|
21
|
+
};
|
|
31
22
|
/**
|
|
32
23
|
* {@link baseEngineArgs} plus the instance-operation fields (everything
|
|
33
24
|
* but the client and the verb-specific fields). The `reason` is spread
|
|
@@ -38,6 +29,6 @@ export declare function buildOperationArgs({ scope, instanceId, reason, }: {
|
|
|
38
29
|
scope: EngineScope;
|
|
39
30
|
instanceId: string;
|
|
40
31
|
reason: string | undefined;
|
|
41
|
-
}): OperationArgs &
|
|
32
|
+
}): OperationArgs & ReturnType<typeof baseEngineArgs> & {
|
|
42
33
|
reason?: string;
|
|
43
34
|
};
|
|
@@ -1,48 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* `/users/me`, which a token-only CLI can't reach; until the CLI hooks
|
|
5
|
-
* into Sanity auth proper, writes act as this system identity.
|
|
6
|
-
*/
|
|
7
|
-
export const SYSTEM_ACTOR = { kind: 'system', id: 'workflow-cli' };
|
|
8
|
-
/**
|
|
9
|
-
* The write-verb arguments every engine operation shares: tag scope,
|
|
10
|
-
* workflow resource, and the actor the write is attributed to — commands
|
|
11
|
-
* with `--as` resolve one via {@link resolveActor}; the rest pass
|
|
12
|
-
* {@link SYSTEM_ACTOR} explicitly. Spread by both the instance verbs
|
|
13
|
-
* ({@link buildOperationArgs}) and the definition verbs (e.g. `definition delete`).
|
|
14
|
-
*/
|
|
15
|
-
export function baseEngineArgs(scope, actor) {
|
|
1
|
+
import { EXECUTION_KINDS, } from '@sanity/workflow-engine';
|
|
2
|
+
import { cliTelemetry } from "./telemetry.js";
|
|
3
|
+
export function baseEngineArgs(scope) {
|
|
16
4
|
return {
|
|
17
5
|
tag: scope.tag,
|
|
18
6
|
workflowResource: scope.workflowResource,
|
|
19
|
-
|
|
7
|
+
executionContext: { kind: EXECUTION_KINDS.cli, id: 'workflow-cli' },
|
|
8
|
+
telemetry: cliTelemetry().logger,
|
|
20
9
|
};
|
|
21
10
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Resolve the actor a write is attributed to. With a named user the fire
|
|
24
|
-
* is pinned to it (plus any roles, so role-gated action filters pass);
|
|
25
|
-
* without one the {@link SYSTEM_ACTOR} fallback applies. Roles without a
|
|
26
|
-
* named user throw — they'd silently attach to nothing otherwise.
|
|
27
|
-
*/
|
|
28
|
-
export function resolveActor(as, roles) {
|
|
29
|
-
if (as === undefined) {
|
|
30
|
-
if (roles.length > 0) {
|
|
31
|
-
throw new Error('--as-role requires --as <userId>');
|
|
32
|
-
}
|
|
33
|
-
return SYSTEM_ACTOR;
|
|
34
|
-
}
|
|
35
|
-
return { kind: 'person', id: as, ...(roles.length > 0 ? { roles } : {}) };
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* {@link baseEngineArgs} plus the instance-operation fields (everything
|
|
39
|
-
* but the client and the verb-specific fields). The `reason` is spread
|
|
40
|
-
* conditionally because the engine's optional field rejects an explicit
|
|
41
|
-
* `undefined` under `exactOptionalPropertyTypes`.
|
|
42
|
-
*/
|
|
43
11
|
export function buildOperationArgs({ scope, instanceId, reason, }) {
|
|
44
12
|
return {
|
|
45
|
-
...baseEngineArgs(scope
|
|
13
|
+
...baseEngineArgs(scope),
|
|
46
14
|
instanceId,
|
|
47
15
|
...(reason !== undefined ? { reason } : {}),
|
|
48
16
|
};
|
package/dist/lib/ops-report.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Ora } from 'ora';
|
|
2
1
|
/** A single engine primitive that ran during a write commit — the
|
|
3
2
|
* reportable subset of the engine's `OpAppliedSummary`. */
|
|
4
3
|
export interface RanOp {
|
|
@@ -24,19 +23,28 @@ export interface WriteOutcome {
|
|
|
24
23
|
ranOps?: RanOp[];
|
|
25
24
|
}
|
|
26
25
|
/** The spinner message + ops block a write verb prints once its engine
|
|
27
|
-
* call returns. {@link opsAppliedLines} builds `opsLines
|
|
26
|
+
* call returns. {@link opsAppliedLines} builds `opsLines`; verbs whose
|
|
27
|
+
* commits never run ops omit it. */
|
|
28
28
|
export interface WriteReport {
|
|
29
29
|
changed: boolean;
|
|
30
30
|
message: string;
|
|
31
|
-
opsLines
|
|
31
|
+
opsLines?: string[];
|
|
32
32
|
}
|
|
33
|
+
/** The ", then cascaded N auto-transition(s)" suffix a write report appends
|
|
34
|
+
* when the commit cascaded the instance onward; empty when it didn't. */
|
|
35
|
+
export declare function cascadeTail(cascaded: number): string;
|
|
33
36
|
/**
|
|
34
|
-
*
|
|
35
|
-
* (`changed: false`), otherwise
|
|
36
|
-
*
|
|
37
|
+
* Run a write verb behind its spinner: start it, run the engine call, and
|
|
38
|
+
* emit the report — warn and bail on a no-op (`changed: false`), otherwise
|
|
39
|
+
* succeed and print the ops block. A throw fails the spinner and exits
|
|
40
|
+
* through {@link fail}. The one spinner + report + catch shape every write
|
|
41
|
+
* command (abort, set-stage, fire-action, start, definition delete) shares.
|
|
37
42
|
*/
|
|
38
|
-
export declare function
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
export declare function runWriteVerb<T>({ startLabel, failLabel, failHeadline, run, report, log, }: {
|
|
44
|
+
startLabel: string;
|
|
45
|
+
failLabel: string;
|
|
46
|
+
failHeadline: string;
|
|
47
|
+
run: () => Promise<T>;
|
|
48
|
+
report: (result: T) => WriteReport;
|
|
41
49
|
log: (line: string) => void;
|
|
42
|
-
}): void
|
|
50
|
+
}): Promise<void>;
|