@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +485 -0
  2. package/README.md +145 -63
  3. package/bin/run.js +0 -4
  4. package/dist/commands/abort.d.ts +8 -12
  5. package/dist/commands/abort.js +24 -34
  6. package/dist/commands/definition/delete.d.ts +6 -6
  7. package/dist/commands/definition/delete.js +17 -33
  8. package/dist/commands/definition/diff.d.ts +10 -3
  9. package/dist/commands/definition/diff.js +21 -29
  10. package/dist/commands/definition/list.d.ts +5 -6
  11. package/dist/commands/definition/list.js +44 -50
  12. package/dist/commands/definition/show.d.ts +22 -13
  13. package/dist/commands/definition/show.js +25 -36
  14. package/dist/commands/deploy.d.ts +67 -8
  15. package/dist/commands/deploy.js +136 -32
  16. package/dist/commands/diagnose.d.ts +22 -4
  17. package/dist/commands/diagnose.js +45 -61
  18. package/dist/commands/fire-action.d.ts +7 -30
  19. package/dist/commands/fire-action.js +42 -115
  20. package/dist/commands/list.d.ts +9 -10
  21. package/dist/commands/list.js +49 -62
  22. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  23. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -3
  24. package/dist/commands/set-stage.d.ts +27 -3
  25. package/dist/commands/set-stage.js +63 -12
  26. package/dist/commands/show.d.ts +3 -2
  27. package/dist/commands/show.js +15 -21
  28. package/dist/commands/start.d.ts +56 -0
  29. package/dist/commands/start.js +133 -0
  30. package/dist/commands/tail.d.ts +5 -2
  31. package/dist/commands/tail.js +25 -26
  32. package/dist/hooks/finally/telemetry.d.ts +8 -0
  33. package/dist/hooks/finally/telemetry.js +13 -0
  34. package/dist/hooks/prerun/telemetry.d.ts +5 -0
  35. package/dist/hooks/prerun/telemetry.js +5 -0
  36. package/dist/index.js +0 -3
  37. package/dist/lib/base-command.d.ts +14 -0
  38. package/dist/lib/base-command.js +10 -0
  39. package/dist/lib/client.d.ts +11 -14
  40. package/dist/lib/client.js +29 -34
  41. package/dist/lib/context.d.ts +98 -0
  42. package/dist/lib/context.js +83 -0
  43. package/dist/lib/definitions.d.ts +38 -29
  44. package/dist/lib/definitions.js +34 -93
  45. package/dist/lib/diff.d.ts +5 -2
  46. package/dist/lib/diff.js +62 -20
  47. package/dist/lib/env.d.ts +4 -6
  48. package/dist/lib/env.js +4 -9
  49. package/dist/lib/fail.d.ts +12 -0
  50. package/dist/lib/fail.js +25 -16
  51. package/dist/lib/flags.d.ts +7 -2
  52. package/dist/lib/flags.js +7 -3
  53. package/dist/lib/load-config.d.ts +15 -0
  54. package/dist/lib/load-config.js +89 -0
  55. package/dist/lib/operation-args.d.ts +20 -15
  56. package/dist/lib/operation-args.js +9 -40
  57. package/dist/lib/ops-report.d.ts +29 -13
  58. package/dist/lib/ops-report.js +20 -17
  59. package/dist/lib/params.d.ts +7 -0
  60. package/dist/lib/params.js +18 -0
  61. package/dist/lib/read-fanout.d.ts +22 -0
  62. package/dist/lib/read-fanout.js +28 -0
  63. package/dist/lib/select-deployment.d.ts +31 -0
  64. package/dist/lib/select-deployment.js +37 -0
  65. package/dist/lib/share-definitions.d.ts +86 -0
  66. package/dist/lib/share-definitions.js +106 -0
  67. package/dist/lib/stub.js +0 -7
  68. package/dist/lib/telemetry-setup.d.ts +66 -0
  69. package/dist/lib/telemetry-setup.js +92 -0
  70. package/dist/lib/telemetry.d.ts +100 -0
  71. package/dist/lib/telemetry.js +89 -0
  72. package/dist/lib/ui.d.ts +33 -1
  73. package/dist/lib/ui.js +32 -21
  74. package/oclif.manifest.json +133 -113
  75. package/package.json +16 -8
  76. package/dist/commands/move-stage.d.ts +0 -52
  77. package/dist/commands/move-stage.js +0 -86
  78. package/dist/lib/config.d.ts +0 -18
  79. package/dist/lib/config.js +0 -50
@@ -1,104 +1,40 @@
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 { 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
- * Turn a configured definitions source into a specifier `import()` can
16
- * load. Path-like values (relative or absolute) resolve against `cwd` and
17
- * become file URLs so they're imported as files, not bare package names;
18
- * anything else passes through as a module specifier (e.g. a workspace
19
- * package such as `@sanity/workflow-examples`).
20
- */
21
- export function resolveDefinitionsSpecifier(source, cwd) {
22
- if (source.startsWith('.') || isAbsolute(source)) {
23
- return pathToFileURL(resolve(cwd, source)).href;
24
- }
25
- return source;
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
- * Pull the `allDefinitions` array out of an imported definitions module,
29
- * failing hard with a source-specific message when the module doesn't
30
- * expose it. Per-definition structure is validated downstream by
31
- * {@link collectValidationErrors}, so this only asserts the array shape.
32
- */
33
- export function extractDefinitions(mod, source) {
34
- const exported = mod.allDefinitions;
35
- if (!Array.isArray(exported)) {
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
- * Resolve the configured definitions module and dynamic-import it. The
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(`No definition named "${only}".`, `Available: ${available}`);
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
- const message = err instanceof Error ? err.message : String(err);
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
+ }
@@ -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
- /** Render a JSON diff of `oldObj` → `newObj` as `+`/`-`/context lines. */
17
- export function diffLines(oldObj, newObj) {
18
- const out = [];
19
- for (const chunk of diffJson(oldObj, newObj)) {
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
- // diffJson appends a trailing empty string when the chunk ends in "\n";
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
- /** The full diff report: the summary block plus a per-change diff. */
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
- // For net-new docs, existing is undefined render the whole expected
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
+ }
@@ -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, err instanceof Error ? err.message : String(err));
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
+ }
@@ -1,5 +1,10 @@
1
- /** Selects the engine environment partition (e.g. prod, test).
2
- * Resolved flag WORKFLOW_TAG (required) in {@link loadWorkflowConfig}. */
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). Overrides WORKFLOW_TAG.',
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 { Actor, OperationArgs } from '@sanity/workflow-engine';
2
- import type { WorkflowConfig } from './config.ts';
1
+ import { type EngineScopeArgs, type OperationArgs, type WorkflowTelemetryLogger } from '@sanity/workflow-engine';
3
2
  /**
4
- * The write-verb arguments every engine operation shares: tag scope,
5
- * workflow resource, and the CLI's pinned actor ({@link SYSTEM_ACTOR}).
6
- * Spread by both the instance verbs ({@link buildOperationArgs}) and the
7
- * definition verbs (e.g. `definition delete`).
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 declare function baseEngineArgs(config: WorkflowConfig): Pick<OperationArgs, 'tag' | 'workflowResource' | 'access'>;
8
+ export type EngineScope = Pick<EngineScopeArgs, 'tag' | 'workflowResource'>;
10
9
  /**
11
- * Resolve the actor a write is attributed to. With a named user the fire
12
- * is pinned to it (plus any roles, so role-gated action filters pass);
13
- * without one the {@link SYSTEM_ACTOR} fallback applies. Roles without a
14
- * named user throwthey'd silently attach to nothing otherwise.
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 injectedand 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 resolveActor(as: string | undefined, roles: string[]): Actor;
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({ config, instanceId, reason, }: {
24
- config: WorkflowConfig;
28
+ export declare function buildOperationArgs({ scope, instanceId, reason, }: {
29
+ scope: EngineScope;
25
30
  instanceId: string;
26
31
  reason: string | undefined;
27
- }): Omit<OperationArgs, 'client'> & {
32
+ }): OperationArgs & ReturnType<typeof baseEngineArgs> & {
28
33
  reason?: string;
29
34
  };
@@ -1,47 +1,16 @@
1
- /**
2
- * The synthetic actor a CLI write is attributed to when the operator
3
- * doesn't name a real user. The engine resolves a real actor from
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: config.tag,
17
- workflowResource: config.workflowResource,
18
- access: { actor: SYSTEM_ACTOR },
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(config),
13
+ ...baseEngineArgs(scope),
45
14
  instanceId,
46
15
  ...(reason !== undefined ? { reason } : {}),
47
16
  };