@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
package/dist/lib/definitions.js
CHANGED
|
@@ -1,104 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import { isAbsolute, resolve } from 'node:path';
|
|
10
|
-
import { pathToFileURL } from 'node:url';
|
|
11
|
-
import { validateDefinition } from '@sanity/workflow-engine';
|
|
12
|
-
import { loadDefinitionsSource } from "./config.js";
|
|
1
|
+
import { WORKFLOW_DEFINITION_TYPE, errorMessage, tagScopeFilter, validateDefinition, } from '@sanity/workflow-engine';
|
|
13
2
|
import { fail } from "./fail.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
3
|
+
export function buildDefinitionShowQuery(flags) {
|
|
4
|
+
const params = { name: flags.name };
|
|
5
|
+
const filters = [`_type == "${WORKFLOW_DEFINITION_TYPE}"`, 'name == $name'];
|
|
6
|
+
if (flags.version !== undefined) {
|
|
7
|
+
params.version = flags.version;
|
|
8
|
+
filters.push('version == $version');
|
|
9
|
+
}
|
|
10
|
+
if (flags.tag) {
|
|
11
|
+
params.tag = flags.tag;
|
|
12
|
+
filters.push(tagScopeFilter());
|
|
13
|
+
}
|
|
14
|
+
const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
|
|
15
|
+
return { groq, params };
|
|
26
16
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
fail(`Definitions module "${source}" has no \`allDefinitions\` array export.`, 'Export `allDefinitions: WorkflowDefinition[]` from the module named by WORKFLOW_DEFS.');
|
|
37
|
-
}
|
|
38
|
-
return exported;
|
|
17
|
+
export async function fetchDeployedDefinition({ client, name, tag, version, }) {
|
|
18
|
+
const { groq, params } = buildDefinitionShowQuery({ name, tag, version });
|
|
19
|
+
const deployed = await client.fetch(groq, params, {
|
|
20
|
+
tag: 'definition.load',
|
|
21
|
+
});
|
|
22
|
+
if (deployed === null && version !== undefined) {
|
|
23
|
+
fail(`No deployed definition "${name}" v${version}.`);
|
|
24
|
+
}
|
|
25
|
+
return deployed ?? undefined;
|
|
39
26
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* import is dynamic (not a static top-level import) so a `defineWorkflow`
|
|
43
|
-
* throw inside the module — a missing `version`, a bad schema field —
|
|
44
|
-
* surfaces here as a friendly CLI error rather than an oclif command-load
|
|
45
|
-
* crash with a raw stack trace.
|
|
46
|
-
*/
|
|
47
|
-
async function importDefinitions(source) {
|
|
48
|
-
const specifier = resolveDefinitionsSpecifier(source, process.cwd());
|
|
49
|
-
let mod;
|
|
50
|
-
try {
|
|
51
|
-
mod = await import(__rewriteRelativeImportExtension(specifier, true));
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
-
fail(`Failed to load definitions from "${source}":`, message);
|
|
56
|
-
}
|
|
57
|
-
return extractDefinitions(mod, source);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Narrow the local batch to one definition `name`, failing with the
|
|
61
|
-
* available names when there's no match. With `only` unset the whole batch passes
|
|
62
|
-
* through (a fresh array, so callers can mutate without aliasing).
|
|
63
|
-
*/
|
|
64
|
-
export function selectDefinitions(allDefinitions, only) {
|
|
65
|
-
if (!only)
|
|
27
|
+
export function selectDefinitions(allDefinitions, { only, context = '' }) {
|
|
28
|
+
if (!only) {
|
|
66
29
|
return [...allDefinitions];
|
|
30
|
+
}
|
|
67
31
|
const filtered = allDefinitions.filter((d) => d.name === only);
|
|
68
32
|
if (filtered.length === 0) {
|
|
69
33
|
const available = allDefinitions.map((d) => d.name).join(', ');
|
|
70
|
-
fail(
|
|
34
|
+
fail(`${context}No definition named "${only}".`, `Available: ${available}`);
|
|
71
35
|
}
|
|
72
36
|
return filtered;
|
|
73
37
|
}
|
|
74
|
-
/**
|
|
75
|
-
* The full local-definitions pipeline shared by `deploy` and
|
|
76
|
-
* `definition diff`: resolve the `WORKFLOW_DEFS` source, import it,
|
|
77
|
-
* narrow to `only` (when given), and fail on any validation error.
|
|
78
|
-
*/
|
|
79
|
-
export async function loadValidatedDefinitions(only) {
|
|
80
|
-
const allDefinitions = await importDefinitions(loadDefinitionsSource());
|
|
81
|
-
const selected = selectDefinitions(allDefinitions, only);
|
|
82
|
-
const errors = collectValidationErrors(selected);
|
|
83
|
-
if (errors.length > 0) {
|
|
84
|
-
fail('Validation failed:', errors.join('\n'));
|
|
85
|
-
}
|
|
86
|
-
return selected;
|
|
87
|
-
}
|
|
88
|
-
/** {@link loadValidatedDefinitions} narrowed to exactly one definition name. */
|
|
89
|
-
export async function loadValidatedDefinition(name) {
|
|
90
|
-
const [def] = await loadValidatedDefinitions(name);
|
|
91
|
-
// selectDefinitions already failed on no match; this narrows the
|
|
92
|
-
// indexed access for the type checker.
|
|
93
|
-
if (!def)
|
|
94
|
-
fail(`No definition named "${name}".`);
|
|
95
|
-
return def;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Per-definition structural/GROQ checks plus one batch-level check the
|
|
99
|
-
* engine doesn't do for us: duplicate `name` within the local set.
|
|
100
|
-
* Returns the human-readable error lines; empty means the batch is valid.
|
|
101
|
-
*/
|
|
102
38
|
export function collectValidationErrors(defs) {
|
|
103
39
|
const errors = [];
|
|
104
40
|
const seen = new Map();
|
|
@@ -114,9 +50,14 @@ export function collectValidationErrors(defs) {
|
|
|
114
50
|
validateDefinition(d);
|
|
115
51
|
}
|
|
116
52
|
catch (err) {
|
|
117
|
-
|
|
118
|
-
errors.push(` · ${d.name}: ${message}`);
|
|
53
|
+
errors.push(` · ${d.name}: ${errorMessage(err)}`);
|
|
119
54
|
}
|
|
120
55
|
}
|
|
121
56
|
return errors;
|
|
122
57
|
}
|
|
58
|
+
export function validateOrFail(defs, context = '') {
|
|
59
|
+
const errors = collectValidationErrors(defs);
|
|
60
|
+
if (errors.length > 0) {
|
|
61
|
+
fail(`${context}Validation failed:`, errors.join('\n'));
|
|
62
|
+
}
|
|
63
|
+
}
|
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,27 +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
|
-
|
|
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);
|
|
80
|
+
}
|
|
37
81
|
export function diffReport(entries) {
|
|
38
82
|
const lines = ['', sectionHeader('Summary'), ...summaryLines(entries)];
|
|
39
83
|
const changes = entries.filter((e) => e.status !== 'unchanged');
|
|
@@ -42,9 +86,7 @@ export function diffReport(entries) {
|
|
|
42
86
|
return lines;
|
|
43
87
|
}
|
|
44
88
|
for (const e of changes) {
|
|
45
|
-
|
|
46
|
-
// doc as added lines so the operator sees what would land.
|
|
47
|
-
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)));
|
|
48
90
|
}
|
|
49
91
|
return lines;
|
|
50
92
|
}
|
package/dist/lib/env.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
export declare const ENV: {
|
|
2
|
-
readonly SANITY_PROJECT_ID: "SANITY_PROJECT_ID";
|
|
3
2
|
readonly SANITY_AUTH_TOKEN: "SANITY_AUTH_TOKEN";
|
|
4
|
-
readonly SANITY_DATASET: "SANITY_DATASET";
|
|
5
3
|
readonly SANITY_API_HOST: "SANITY_API_HOST";
|
|
6
|
-
readonly WORKFLOW_RESOURCE_TYPE: "WORKFLOW_RESOURCE_TYPE";
|
|
7
|
-
readonly WORKFLOW_RESOURCE_ID: "WORKFLOW_RESOURCE_ID";
|
|
8
|
-
readonly WORKFLOW_TAG: "WORKFLOW_TAG";
|
|
9
|
-
readonly WORKFLOW_DEFS: "WORKFLOW_DEFS";
|
|
10
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,13 +1,8 @@
|
|
|
1
|
-
// Typed env var definitions
|
|
2
1
|
export const ENV = {
|
|
3
|
-
// Sanity client
|
|
4
|
-
SANITY_PROJECT_ID: 'SANITY_PROJECT_ID',
|
|
5
2
|
SANITY_AUTH_TOKEN: 'SANITY_AUTH_TOKEN',
|
|
6
|
-
SANITY_DATASET: 'SANITY_DATASET',
|
|
7
3
|
SANITY_API_HOST: 'SANITY_API_HOST',
|
|
8
|
-
// Workflow config
|
|
9
|
-
WORKFLOW_RESOURCE_TYPE: 'WORKFLOW_RESOURCE_TYPE',
|
|
10
|
-
WORKFLOW_RESOURCE_ID: 'WORKFLOW_RESOURCE_ID',
|
|
11
|
-
WORKFLOW_TAG: 'WORKFLOW_TAG',
|
|
12
|
-
WORKFLOW_DEFS: 'WORKFLOW_DEFS',
|
|
13
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
|
@@ -14,3 +14,15 @@ export declare function fail(headline: string, detail?: string): never;
|
|
|
14
14
|
* helpers pure and unit-testable while giving callers one clean boundary.
|
|
15
15
|
*/
|
|
16
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,33 +1,42 @@
|
|
|
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
|
-
* Render a user-facing error and exit non-zero. Writes straight to
|
|
5
|
-
* stderr — no oclif framing, no Node stack — so expected failures
|
|
6
|
-
* (validation, missing input, conflicting flags) read clean.
|
|
7
|
-
*
|
|
8
|
-
* For unexpected failures (network, engine throw) prefer letting the
|
|
9
|
-
* error propagate so the stack is visible for debugging.
|
|
10
|
-
*/
|
|
5
|
+
import { envAuthToken } from "./env.js";
|
|
11
6
|
export function fail(headline, detail) {
|
|
12
7
|
process.stderr.write(`${styleText('red', `${logSymbols.error} ${headline}`)}\n`);
|
|
13
8
|
if (detail !== undefined && detail !== '') {
|
|
14
9
|
for (const line of detail.split('\n')) {
|
|
15
|
-
process.stderr.write(` ${line}\n`);
|
|
10
|
+
process.stderr.write(` ${styleText(['dim', 'red'], line)}\n`);
|
|
16
11
|
}
|
|
17
12
|
}
|
|
18
13
|
process.exit(1);
|
|
19
14
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Run `fn`, funnelling a thrown env/validation error through {@link fail}
|
|
22
|
-
* so an expected, user-fixable failure (missing input, bad value) reads
|
|
23
|
-
* clean instead of as an oclif stack trace. Keeps the throwing `read*`
|
|
24
|
-
* helpers pure and unit-testable while giving callers one clean boundary.
|
|
25
|
-
*/
|
|
26
15
|
export function failOnThrow(headline, fn) {
|
|
27
16
|
try {
|
|
28
17
|
return fn();
|
|
29
18
|
}
|
|
30
19
|
catch (err) {
|
|
31
|
-
fail(headline,
|
|
20
|
+
fail(headline, errorMessage(err));
|
|
32
21
|
}
|
|
33
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
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
1
|
+
/** The environment tag (e.g. prod, test). For write commands it selects which
|
|
2
|
+
* deployment in the discovered `sanity.workflow` config to act on (omittable
|
|
3
|
+
* when only one is configured); for read commands it's an optional query
|
|
4
|
+
* filter — and the resource disambiguator when a config spans more than one. */
|
|
3
5
|
export declare const tagFlags: {
|
|
4
6
|
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
7
|
};
|
|
8
|
+
export declare const jsonFlags: {
|
|
9
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
package/dist/lib/flags.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
/** Selects the engine environment partition (e.g. prod, test).
|
|
3
|
-
* Resolved flag → WORKFLOW_TAG (required) in {@link loadWorkflowConfig}. */
|
|
4
2
|
export const tagFlags = {
|
|
5
3
|
tag: Flags.string({
|
|
6
|
-
description: 'Workflow environment tag (e.g. prod, test)
|
|
4
|
+
description: 'Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.',
|
|
5
|
+
}),
|
|
6
|
+
};
|
|
7
|
+
export const jsonFlags = {
|
|
8
|
+
json: Flags.boolean({
|
|
9
|
+
description: 'Emit structured JSON instead of rendered output.',
|
|
10
|
+
default: false,
|
|
7
11
|
}),
|
|
8
12
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type WorkflowConfig } from '@sanity/workflow-engine';
|
|
2
|
+
/**
|
|
3
|
+
* Discover the `sanity.workflow.{ts,js,mjs}` in `cwd`, import its default
|
|
4
|
+
* export, and validate it through {@link defineWorkflowConfig}. Any problem —
|
|
5
|
+
* no file, unloadable module, invalid config — exits through the clean `fail`
|
|
6
|
+
* path.
|
|
7
|
+
*/
|
|
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>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { basename, dirname, join } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { errorMessage } from '@sanity/workflow-engine';
|
|
13
|
+
import { defineWorkflowConfig } from '@sanity/workflow-engine/define';
|
|
14
|
+
import { createJiti } from 'jiti';
|
|
15
|
+
import { fail } from "./fail.js";
|
|
16
|
+
const CONFIG_FILE_NAMES = [
|
|
17
|
+
'sanity.workflow.ts',
|
|
18
|
+
'sanity.workflow.js',
|
|
19
|
+
'sanity.workflow.mjs',
|
|
20
|
+
];
|
|
21
|
+
function findConfigFile(cwd) {
|
|
22
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
23
|
+
const candidate = join(cwd, name);
|
|
24
|
+
if (existsSync(candidate)) {
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
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
|
+
}
|
|
39
|
+
async function importDefault(filePath) {
|
|
40
|
+
const url = pathToFileURL(filePath).href;
|
|
41
|
+
try {
|
|
42
|
+
if (filePath.endsWith('.ts')) {
|
|
43
|
+
return await createJiti(dirname(filePath)).import(url, { default: true });
|
|
44
|
+
}
|
|
45
|
+
return (await import(__rewriteRelativeImportExtension(url, true))).default;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
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;
|
|
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;
|
|
67
|
+
}
|
|
68
|
+
export async function loadWorkflowConfig(cwd = process.cwd()) {
|
|
69
|
+
const filePath = findConfigFile(cwd);
|
|
70
|
+
if (filePath === undefined) {
|
|
71
|
+
fail(`No ${CONFIG_FILE_NAMES[0]} found in ${cwd}.`, 'Create one that `export default defineWorkflowConfig({deployments: [...]})`.');
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return await parseConfigFile(filePath);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
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;
|
|
87
|
+
}
|
|
88
|
+
return parseConfigFile(filePath).catch(() => undefined);
|
|
89
|
+
}
|
|
@@ -1,29 +1,34 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { WorkflowConfig } from './config.ts';
|
|
1
|
+
import { type EngineScopeArgs, type OperationArgs, type WorkflowTelemetryLogger } from '@sanity/workflow-engine';
|
|
3
2
|
/**
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* The engine-scope a CLI verb needs: which tag partition and which resource
|
|
4
|
+
* the engine's own data lives in — {@link EngineScopeArgs} minus the client.
|
|
5
|
+
* A {@link import('@sanity/workflow-engine').WorkflowDeployment} satisfies it,
|
|
6
|
+
* so commands pass their resolved deployment straight through.
|
|
8
7
|
*/
|
|
9
|
-
export
|
|
8
|
+
export type EngineScope = Pick<EngineScopeArgs, 'tag' | 'workflowResource'>;
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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`.
|
|
15
18
|
*/
|
|
16
|
-
export declare function
|
|
19
|
+
export declare function baseEngineArgs(scope: EngineScope): EngineScope & Pick<EngineScopeArgs, 'executionContext'> & {
|
|
20
|
+
telemetry: WorkflowTelemetryLogger;
|
|
21
|
+
};
|
|
17
22
|
/**
|
|
18
23
|
* {@link baseEngineArgs} plus the instance-operation fields (everything
|
|
19
24
|
* but the client and the verb-specific fields). The `reason` is spread
|
|
20
25
|
* conditionally because the engine's optional field rejects an explicit
|
|
21
26
|
* `undefined` under `exactOptionalPropertyTypes`.
|
|
22
27
|
*/
|
|
23
|
-
export declare function buildOperationArgs({
|
|
24
|
-
|
|
28
|
+
export declare function buildOperationArgs({ scope, instanceId, reason, }: {
|
|
29
|
+
scope: EngineScope;
|
|
25
30
|
instanceId: string;
|
|
26
31
|
reason: string | undefined;
|
|
27
|
-
}):
|
|
32
|
+
}): OperationArgs & ReturnType<typeof baseEngineArgs> & {
|
|
28
33
|
reason?: string;
|
|
29
34
|
};
|
|
@@ -1,47 +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
|
-
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 CLI's pinned actor ({@link SYSTEM_ACTOR}).
|
|
11
|
-
* Spread by both the instance verbs ({@link buildOperationArgs}) and the
|
|
12
|
-
* definition verbs (e.g. `definition delete`).
|
|
13
|
-
*/
|
|
14
|
-
export function baseEngineArgs(config) {
|
|
1
|
+
import { EXECUTION_KINDS, } from '@sanity/workflow-engine';
|
|
2
|
+
import { cliTelemetry } from "./telemetry.js";
|
|
3
|
+
export function baseEngineArgs(scope) {
|
|
15
4
|
return {
|
|
16
|
-
tag:
|
|
17
|
-
workflowResource:
|
|
18
|
-
|
|
5
|
+
tag: scope.tag,
|
|
6
|
+
workflowResource: scope.workflowResource,
|
|
7
|
+
executionContext: { kind: EXECUTION_KINDS.cli, id: 'workflow-cli' },
|
|
8
|
+
telemetry: cliTelemetry().logger,
|
|
19
9
|
};
|
|
20
10
|
}
|
|
21
|
-
|
|
22
|
-
* Resolve the actor a write is attributed to. With a named user the fire
|
|
23
|
-
* is pinned to it (plus any roles, so role-gated action filters pass);
|
|
24
|
-
* without one the {@link SYSTEM_ACTOR} fallback applies. Roles without a
|
|
25
|
-
* named user throw — they'd silently attach to nothing otherwise.
|
|
26
|
-
*/
|
|
27
|
-
export function resolveActor(as, roles) {
|
|
28
|
-
if (as === undefined) {
|
|
29
|
-
if (roles.length > 0) {
|
|
30
|
-
throw new Error('--as-role requires --as <userId>');
|
|
31
|
-
}
|
|
32
|
-
return SYSTEM_ACTOR;
|
|
33
|
-
}
|
|
34
|
-
return { kind: 'user', id: as, ...(roles.length > 0 ? { roles } : {}) };
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* {@link baseEngineArgs} plus the instance-operation fields (everything
|
|
38
|
-
* but the client and the verb-specific fields). The `reason` is spread
|
|
39
|
-
* conditionally because the engine's optional field rejects an explicit
|
|
40
|
-
* `undefined` under `exactOptionalPropertyTypes`.
|
|
41
|
-
*/
|
|
42
|
-
export function buildOperationArgs({ config, instanceId, reason, }) {
|
|
11
|
+
export function buildOperationArgs({ scope, instanceId, reason, }) {
|
|
43
12
|
return {
|
|
44
|
-
...baseEngineArgs(
|
|
13
|
+
...baseEngineArgs(scope),
|
|
45
14
|
instanceId,
|
|
46
15
|
...(reason !== undefined ? { reason } : {}),
|
|
47
16
|
};
|