@sanity/workflow-cli 0.8.0 → 0.9.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 (60) hide show
  1. package/README.md +99 -61
  2. package/dist/commands/abort.d.ts +5 -7
  3. package/dist/commands/abort.js +15 -14
  4. package/dist/commands/definition/delete.d.ts +10 -6
  5. package/dist/commands/definition/delete.js +8 -11
  6. package/dist/commands/definition/diff.d.ts +8 -1
  7. package/dist/commands/definition/diff.js +30 -24
  8. package/dist/commands/definition/list.d.ts +4 -4
  9. package/dist/commands/definition/list.js +8 -8
  10. package/dist/commands/definition/show.d.ts +0 -10
  11. package/dist/commands/definition/show.js +11 -20
  12. package/dist/commands/deploy.d.ts +58 -5
  13. package/dist/commands/deploy.js +134 -27
  14. package/dist/commands/diagnose.d.ts +5 -1
  15. package/dist/commands/diagnose.js +18 -28
  16. package/dist/commands/fire-action.d.ts +15 -28
  17. package/dist/commands/fire-action.js +37 -72
  18. package/dist/commands/list.d.ts +2 -2
  19. package/dist/commands/list.js +8 -8
  20. package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
  21. package/dist/commands/{retry-activity.js → reset-activity.js} +2 -2
  22. package/dist/commands/set-stage.d.ts +27 -3
  23. package/dist/commands/set-stage.js +80 -13
  24. package/dist/commands/show.d.ts +1 -0
  25. package/dist/commands/show.js +14 -8
  26. package/dist/commands/start.d.ts +59 -0
  27. package/dist/commands/start.js +169 -0
  28. package/dist/commands/tail.d.ts +3 -0
  29. package/dist/commands/tail.js +9 -5
  30. package/dist/lib/client.d.ts +11 -14
  31. package/dist/lib/client.js +45 -33
  32. package/dist/lib/context.d.ts +62 -0
  33. package/dist/lib/context.js +82 -0
  34. package/dist/lib/definitions.d.ts +38 -29
  35. package/dist/lib/definitions.js +45 -85
  36. package/dist/lib/diff.js +8 -0
  37. package/dist/lib/env.d.ts +0 -6
  38. package/dist/lib/env.js +3 -9
  39. package/dist/lib/fail.d.ts +6 -0
  40. package/dist/lib/fail.js +11 -1
  41. package/dist/lib/flags.d.ts +14 -2
  42. package/dist/lib/flags.js +24 -3
  43. package/dist/lib/load-config.d.ts +11 -0
  44. package/dist/lib/load-config.js +69 -0
  45. package/dist/lib/operation-args.d.ts +25 -7
  46. package/dist/lib/operation-args.js +12 -11
  47. package/dist/lib/ops-report.d.ts +19 -7
  48. package/dist/lib/ops-report.js +6 -7
  49. package/dist/lib/params.d.ts +7 -0
  50. package/dist/lib/params.js +26 -0
  51. package/dist/lib/select-deployment.d.ts +31 -0
  52. package/dist/lib/select-deployment.js +58 -0
  53. package/dist/lib/ui.d.ts +3 -1
  54. package/dist/lib/ui.js +1 -3
  55. package/oclif.manifest.json +145 -86
  56. package/package.json +6 -4
  57. package/dist/commands/move-stage.d.ts +0 -47
  58. package/dist/commands/move-stage.js +0 -77
  59. package/dist/lib/config.d.ts +0 -18
  60. package/dist/lib/config.js +0 -50
@@ -0,0 +1,169 @@
1
+ import { styleText } from 'node:util';
2
+ import { Args, Command, Flags } from '@oclif/core';
3
+ import { isStartableDefinition, workflow, } from '@sanity/workflow-engine';
4
+ import ora from 'ora';
5
+ import { resolveContext } from "../lib/context.js";
6
+ import { fetchDeployedDefinition } from "../lib/definitions.js";
7
+ import { errorMessage, fail, failOnThrow } from "../lib/fail.js";
8
+ import { actorFlags, jsonFlags, tagFlags } from "../lib/flags.js";
9
+ import { baseEngineArgs, resolveActor } from "../lib/operation-args.js";
10
+ import { parseParams } from "../lib/params.js";
11
+ export default class Start extends Command {
12
+ static description = 'Start a workflow instance from a deployed definition. Supply values for the ' +
13
+ "workflow's input-sourced fields with --field (e.g. the subject document ref).";
14
+ static examples = [
15
+ '<%= config.bin %> start productLaunch',
16
+ '<%= config.bin %> start article-review --field subject=\'{"id":"dataset:proj:ds:article-1","type":"article"}\'',
17
+ '<%= config.bin %> start article-review --as user.xyz --as-role editor',
18
+ '<%= config.bin %> start productLaunch --version 2 --tag prod',
19
+ ];
20
+ static args = {
21
+ name: Args.string({ required: true, description: 'Workflow definition name.' }),
22
+ };
23
+ static flags = {
24
+ ...tagFlags,
25
+ version: Flags.integer({
26
+ description: 'Definition version to start from (default: highest deployed).',
27
+ }),
28
+ field: Flags.string({
29
+ description: 'Initial value for a declared input-sourced field, as name=value (repeatable). Values ' +
30
+ 'are JSON-parsed, falling back to a string; ref kinds take a JSON object with a GDR ' +
31
+ '`id` and doc `type`.',
32
+ multiple: true,
33
+ default: [],
34
+ }),
35
+ ...actorFlags,
36
+ ...jsonFlags,
37
+ };
38
+ async run() {
39
+ const { args, flags } = await this.parse(Start);
40
+ const { deployment, client } = await resolveContext(flags);
41
+ const actor = failOnThrow('start usage error:', () => resolveActor(flags.as, flags['as-role']));
42
+ const values = failOnThrow('Invalid --field:', () => parseParams(flags.field));
43
+ const initialFields = await resolveStartInputs({
44
+ client,
45
+ name: args.name,
46
+ tag: deployment.tag,
47
+ version: flags.version,
48
+ values,
49
+ });
50
+ const startArgs = {
51
+ client,
52
+ ...buildStartArgs({
53
+ scope: deployment,
54
+ name: args.name,
55
+ version: flags.version,
56
+ initialFields,
57
+ actor,
58
+ }),
59
+ };
60
+ if (flags.json) {
61
+ const result = await workflow
62
+ .startInstance(startArgs)
63
+ .catch((error) => fail('start error:', errorMessage(error)));
64
+ this.log(JSON.stringify(startResultJson(result.instance), null, 2));
65
+ return;
66
+ }
67
+ const spinner = ora(`Starting ${args.name}…`).start();
68
+ try {
69
+ const result = await workflow.startInstance(startArgs);
70
+ spinner.succeed(startReport(result.instance));
71
+ }
72
+ catch (error) {
73
+ spinner.fail('Start rejected');
74
+ fail('start error:', errorMessage(error));
75
+ }
76
+ }
77
+ }
78
+ /**
79
+ * Everything `start` must resolve before the engine call: fetch the deployed
80
+ * definition (failing when nothing is deployed under the name), refuse
81
+ * non-startable ones, and type the operator's `--field` values against the
82
+ * declared entries.
83
+ */
84
+ async function resolveStartInputs({ client, name, tag, version, values, }) {
85
+ const deployed = await fetchDeployedDefinition({ client, name, tag, version });
86
+ if (deployed === undefined) {
87
+ fail(`No deployed definition "${name}".`);
88
+ }
89
+ const refusal = startRefusal(deployed);
90
+ if (refusal !== undefined) {
91
+ fail('start error:', refusal);
92
+ }
93
+ return failOnThrow('start usage error:', () => buildInitialFields({ declared: deployed.fields ?? [], values }));
94
+ }
95
+ /**
96
+ * Why a definition can't be started standalone, or `undefined` when it can.
97
+ * Advisory, mirroring the Startable column in `definition list` — the engine
98
+ * itself does not refuse ({@link isStartableDefinition}), but the CLI has no
99
+ * way to supply the parent context a spawn-only child expects.
100
+ */
101
+ export function startRefusal(definition) {
102
+ if (isStartableDefinition(definition)) {
103
+ return undefined;
104
+ }
105
+ return "definition is spawn-only (lifecycle: 'child') — instances of it are spawned by a parent workflow's activity, not started standalone";
106
+ }
107
+ /**
108
+ * Type each `--field name=value` against the workflow's declared field
109
+ * entries — the engine takes typed {@link InitialFieldValue}s, and a value's
110
+ * type IS its declared entry's kind. Only `input`-sourced entries read
111
+ * caller values (the engine silently ignores the rest), so anything else
112
+ * fails here, naming the fields that ARE settable; value validation stays
113
+ * in the engine.
114
+ */
115
+ export function buildInitialFields({ declared, values, }) {
116
+ const settable = declared.filter((f) => f.initialValue?.type === 'input');
117
+ const settableNames = settable.map((f) => f.name).join(', ');
118
+ return Object.entries(values).map(([name, value]) => {
119
+ const entry = declared.find((f) => f.name === name);
120
+ if (entry === undefined) {
121
+ throw new Error(settable.length > 0
122
+ ? `workflow declares no field "${name}" — input fields: ${settableNames}`
123
+ : `workflow declares no field "${name}" — it declares no input fields at all`);
124
+ }
125
+ if (entry.initialValue?.type !== 'input') {
126
+ throw new Error(`field "${name}" is not input-sourced — the engine would silently ignore the value. ` +
127
+ (settable.length > 0
128
+ ? `Input fields: ${settableNames}`
129
+ : 'This workflow has no input fields.'));
130
+ }
131
+ // Operator-supplied JSON; the engine validates the value against the
132
+ // declared kind at start, so this cast is the CLI/engine boundary.
133
+ return { type: entry.type, name, value };
134
+ });
135
+ }
136
+ /**
137
+ * The shared engine args ({@link baseEngineArgs}) plus the start-specific
138
+ * fields. Optionals spread conditionally because the engine's optional args
139
+ * reject an explicit `undefined` under `exactOptionalPropertyTypes`.
140
+ */
141
+ export function buildStartArgs({ scope, name, version, initialFields, actor, }) {
142
+ return {
143
+ ...baseEngineArgs(scope, actor),
144
+ definition: name,
145
+ ...(version !== undefined ? { version } : {}),
146
+ ...(initialFields.length > 0 ? { initialFields } : {}),
147
+ };
148
+ }
149
+ /** The success spinner message. Pure so the in-flight / immediately-terminal
150
+ * permutations can be asserted without driving a real start. */
151
+ export function startReport(instance) {
152
+ const id = styleText('bold', instance._id);
153
+ const stage = styleText('bold', instance.currentStage);
154
+ return instance.completedAt !== undefined
155
+ ? `Started ${id} — completed immediately at ${stage}`
156
+ : `Started ${id} — now at ${stage}`;
157
+ }
158
+ /** The `--json` payload. Keys follow the CLI's `--json` vocabulary (matching
159
+ * `fire-action`): `instanceId` carries the document `_id`, `version` the
160
+ * pinned definition version; the rest mirror the instance fields. */
161
+ export function startResultJson(instance) {
162
+ return {
163
+ instanceId: instance._id,
164
+ definition: instance.definition,
165
+ version: instance.pinnedVersion,
166
+ currentStage: instance.currentStage,
167
+ ...(instance.completedAt !== undefined ? { completedAt: instance.completedAt } : {}),
168
+ };
169
+ }
@@ -5,6 +5,9 @@ export default class Tail extends Command {
5
5
  static args: {
6
6
  instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
+ static flags: {
9
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
8
11
  run(): Promise<void>;
9
12
  /**
10
13
  * Fetch the current instance state, print history entries past the
@@ -1,8 +1,9 @@
1
1
  import { styleText } from 'node:util';
2
2
  import { Args, Command } from '@oclif/core';
3
3
  import logSymbols from 'log-symbols';
4
- import { buildClient, loadClient } from "../lib/client.js";
4
+ import { resolveReadContext } from "../lib/context.js";
5
5
  import { fail } from "../lib/fail.js";
6
+ import { tagFlags } from "../lib/flags.js";
6
7
  import { formatTimestamp } from "../lib/ui.js";
7
8
  export default class Tail extends Command {
8
9
  static description = 'Stream new history entries on a workflow instance as they land in the dataset.';
@@ -13,9 +14,12 @@ export default class Tail extends Command {
13
14
  description: 'Workflow instance id to tail.',
14
15
  }),
15
16
  };
17
+ static flags = {
18
+ ...tagFlags,
19
+ };
16
20
  async run() {
17
- const { args } = await this.parse(Tail);
18
- const client = loadClient();
21
+ const { args, flags } = await this.parse(Tail);
22
+ const { client } = await resolveReadContext(flags);
19
23
  const initial = await client.getDocument(args.instanceId);
20
24
  if (!initial) {
21
25
  fail(`No instance with id "${args.instanceId}"`);
@@ -33,7 +37,7 @@ export default class Tail extends Command {
33
37
  .subscribe({
34
38
  next: async () => {
35
39
  try {
36
- seen = await this.printNewEntries(client, args.instanceId, seen);
40
+ seen = await this.printNewEntries({ client, instanceId: args.instanceId, seen });
37
41
  }
38
42
  catch (err) {
39
43
  const message = err instanceof Error ? err.message : String(err);
@@ -61,7 +65,7 @@ export default class Tail extends Command {
61
65
  * given high-water mark, return the new mark. Pulled out so the
62
66
  * async work in the subscription's `next` handler stays tidy.
63
67
  */
64
- async printNewEntries(client, instanceId, seen) {
68
+ async printNewEntries({ client, instanceId, seen, }) {
65
69
  const next = await client.getDocument(instanceId);
66
70
  if (!next)
67
71
  return seen;
@@ -1,17 +1,14 @@
1
1
  import { type SanityClient } from '@sanity/client';
2
- export interface ClientEnv {
3
- projectId: string;
4
- dataset: string;
5
- token: string;
6
- apiHost: string;
7
- apiVersion: string;
8
- }
9
- export declare function buildClient(env?: ClientEnv): SanityClient;
2
+ import { type WorkflowResource } from '@sanity/workflow-engine';
10
3
  /**
11
- * {@link buildClient} but funnel a missing/empty `SANITY_*` env var
12
- * through the standard {@link failOnThrow} path, so commands fail with a
13
- * clean message rather than a raw stack trace. This is the boundary
14
- * commands call; {@link buildClient} stays throw-based for tests and for
15
- * callers that supply their own {@link ClientEnv}.
4
+ * Resolve the Sanity auth token. An explicit `SANITY_AUTH_TOKEN` wins (CI /
5
+ * scripted use); otherwise fall back to the `sanity login` session token.
6
+ * Throws when neither exists rather than building an unauthenticated client
7
+ * that only 401s once it hits the API.
16
8
  */
17
- export declare function loadClient(): SanityClient;
9
+ export declare function resolveToken(): Promise<string>;
10
+ /** {@link resolveToken}, funnelling the no-token case through the clean
11
+ * {@link fail} path rather than letting it surface as an oclif stack. */
12
+ export declare function resolveTokenOrFail(): Promise<string>;
13
+ /** Build a client for a workflow resource, authenticated with `token`. */
14
+ export declare function clientFor(resource: WorkflowResource, token: string): SanityClient;
@@ -1,40 +1,52 @@
1
+ import { getCliToken, isStaging } from '@sanity/cli-core';
1
2
  import { createClient } from '@sanity/client';
3
+ import { datasetResourceParts, ENGINE_API_VERSION, } from '@sanity/workflow-engine';
2
4
  import { ENV } from "./env.js";
3
- import { failOnThrow } from "./fail.js";
4
- function readClientEnv() {
5
- const projectId = process.env[ENV.SANITY_PROJECT_ID];
6
- const token = process.env[ENV.SANITY_AUTH_TOKEN];
7
- if (!projectId) {
8
- throw new Error('SANITY_PROJECT_ID is required (set in root .env)');
5
+ import { fail } from "./fail.js";
6
+ const API_VERSION = ENGINE_API_VERSION;
7
+ const STAGING_API_HOST = 'https://api.sanity.work';
8
+ /**
9
+ * The API host to hit: an explicit `SANITY_API_HOST` wins; otherwise mirror
10
+ * the Sanity CLI staging routes to the staging API, prod falls through to
11
+ * `@sanity/client`'s default. Pairs with {@link resolveToken}, which reads the
12
+ * matching session token.
13
+ */
14
+ function resolveApiHost() {
15
+ return process.env[ENV.SANITY_API_HOST] ?? (isStaging() ? STAGING_API_HOST : undefined);
16
+ }
17
+ /**
18
+ * Resolve the Sanity auth token. An explicit `SANITY_AUTH_TOKEN` wins (CI /
19
+ * scripted use); otherwise fall back to the `sanity login` session token.
20
+ * Throws when neither exists rather than building an unauthenticated client
21
+ * that only 401s once it hits the API.
22
+ */
23
+ export async function resolveToken() {
24
+ const fromEnv = process.env[ENV.SANITY_AUTH_TOKEN]?.trim();
25
+ if (fromEnv) {
26
+ return fromEnv;
9
27
  }
10
- if (!token) {
11
- throw new Error('SANITY_AUTH_TOKEN is required with editor role (set in root .env)');
28
+ const fromSession = await getCliToken();
29
+ if (fromSession) {
30
+ return fromSession;
12
31
  }
13
- return {
14
- projectId,
15
- dataset: process.env[ENV.SANITY_DATASET] ?? 'production',
16
- token,
17
- apiHost: process.env[ENV.SANITY_API_HOST] ?? 'https://api.sanity.io',
18
- apiVersion: '2026-04-29',
19
- };
32
+ throw new Error('No Sanity token found — run `sanity login`, or set SANITY_AUTH_TOKEN.');
20
33
  }
21
- export function buildClient(env = readClientEnv()) {
22
- return createClient({
23
- projectId: env.projectId,
24
- dataset: env.dataset,
25
- apiVersion: env.apiVersion,
26
- token: env.token,
27
- apiHost: env.apiHost,
28
- useCdn: false,
29
- });
34
+ /** {@link resolveToken}, funnelling the no-token case through the clean
35
+ * {@link fail} path rather than letting it surface as an oclif stack. */
36
+ export async function resolveTokenOrFail() {
37
+ return resolveToken().catch((err) => fail('Authentication required:', err instanceof Error ? err.message : String(err)));
30
38
  }
31
- /**
32
- * {@link buildClient} but funnel a missing/empty `SANITY_*` env var
33
- * through the standard {@link failOnThrow} path, so commands fail with a
34
- * clean message rather than a raw stack trace. This is the boundary
35
- * commands call; {@link buildClient} stays throw-based for tests and for
36
- * callers that supply their own {@link ClientEnv}.
37
- */
38
- export function loadClient() {
39
- return failOnThrow('Invalid Sanity client config:', () => buildClient());
39
+ /** Build a client for a workflow resource, authenticated with `token`. */
40
+ export function clientFor(resource, token) {
41
+ const apiHost = resolveApiHost();
42
+ const baseParams = { token, apiVersion: API_VERSION, useCdn: false, ...(apiHost ? { apiHost } : {}) };
43
+ // A dataset goes through the classic {projectId, dataset} config the proven
44
+ // path, and it keeps `tail`'s client.listen() on well-trodden ground. Other
45
+ // resource types have no projectId/dataset split, so they pass through as a
46
+ // resource directly.
47
+ if (resource.type === 'dataset') {
48
+ const { projectId, dataset } = datasetResourceParts(resource.id);
49
+ return createClient({ ...baseParams, projectId, dataset });
50
+ }
51
+ return createClient({ ...baseParams, resource });
40
52
  }
@@ -0,0 +1,62 @@
1
+ import type { SanityClient } from '@sanity/client';
2
+ import type { WorkflowConfig, WorkflowDeployment, WorkflowInstance, WorkflowResource } from '@sanity/workflow-engine';
3
+ import type { EngineScope } from './operation-args.ts';
4
+ export interface DeploymentContext {
5
+ deployment: WorkflowDeployment;
6
+ client: SanityClient;
7
+ }
8
+ export interface InstanceContext {
9
+ client: SanityClient;
10
+ /** The engine scope derived from the instance itself — `{tag, workflowResource}`
11
+ * ready to pass straight to an engine verb, so callers never re-assemble it. */
12
+ scope: EngineScope;
13
+ }
14
+ /**
15
+ * The resolution a WRITE shares: discover the config, pick the deployment for
16
+ * `--tag` (or the sole one), and build an authenticated client for that
17
+ * deployment's resource. A write acts on one specific deployment, so a
18
+ * single, definite target is exactly what it needs.
19
+ */
20
+ export declare function resolveContext(flags: {
21
+ tag?: string | undefined;
22
+ }): Promise<DeploymentContext>;
23
+ /**
24
+ * The resolution a READ shares: an authenticated client for the resource to
25
+ * inspect — and nothing more. A read lists/inspects whatever is in the Content
26
+ * Lake, so it must NOT inherit a deployment's tag or definition set as a
27
+ * filter. The config only locates the resource here; the command applies the
28
+ * user's explicit `--tag` (if any) as a query filter itself.
29
+ *
30
+ * Resource selection: when every deployment targets the same resource (the
31
+ * common case) it's used with no `--tag` needed; only a config that spans
32
+ * *different* resources needs `--tag` to say which dataset to read.
33
+ */
34
+ export declare function resolveReadContext(flags: {
35
+ tag?: string | undefined;
36
+ }): Promise<{
37
+ client: SanityClient;
38
+ }>;
39
+ /** The single resource a read should target, or fail asking which when the
40
+ * config spans more than one. */
41
+ export declare function resolveReadResource(config: WorkflowConfig, tag: string | undefined): WorkflowResource;
42
+ /**
43
+ * The resolution an instance-id-targeted command shares, read or write: an
44
+ * authenticated client for the resource, plus the instance itself — so the
45
+ * command derives its partition (`tag`) from the instance, never from the
46
+ * config's declared deployments. An instance id is globally unique and carries
47
+ * its own `tag`, so a command that names one acts on that instance regardless
48
+ * of which tags the config happens to deploy; the config only locates the
49
+ * resource (via {@link resolveReadResource}, no tag filter).
50
+ *
51
+ * Contrast {@link resolveContext}: the deploy/diff/delete path IS scoped to a
52
+ * declared deployment because it acts on the authored definition set (or a
53
+ * definition name, which — unlike an instance id — isn't unique across tags).
54
+ */
55
+ export declare function resolveInstanceContext(flags: {
56
+ tag?: string | undefined;
57
+ }, instanceId: string): Promise<InstanceContext>;
58
+ /** Fetch an instance by id, exiting cleanly when the resource has no such
59
+ * document — the diagnostic an operator sees on a mistyped id. Split out from
60
+ * {@link resolveInstanceContext} so the not-found path is unit-testable
61
+ * without a live config/token/client. */
62
+ export declare function loadInstanceOrFail(client: Pick<SanityClient, 'getDocument'>, instanceId: string): Promise<WorkflowInstance>;
@@ -0,0 +1,82 @@
1
+ import { clientFor, resolveTokenOrFail } from "./client.js";
2
+ import { fail } from "./fail.js";
3
+ import { loadWorkflowConfig } from "./load-config.js";
4
+ import { selectDeployment } from "./select-deployment.js";
5
+ /**
6
+ * The resolution a WRITE shares: discover the config, pick the deployment for
7
+ * `--tag` (or the sole one), and build an authenticated client for that
8
+ * deployment's resource. A write acts on one specific deployment, so a
9
+ * single, definite target is exactly what it needs.
10
+ */
11
+ export async function resolveContext(flags) {
12
+ const config = await loadWorkflowConfig();
13
+ const deployment = selectDeployment(config, { tag: flags.tag });
14
+ return { deployment, client: clientFor(deployment.workflowResource, await resolveTokenOrFail()) };
15
+ }
16
+ /**
17
+ * The resolution a READ shares: an authenticated client for the resource to
18
+ * inspect — and nothing more. A read lists/inspects whatever is in the Content
19
+ * Lake, so it must NOT inherit a deployment's tag or definition set as a
20
+ * filter. The config only locates the resource here; the command applies the
21
+ * user's explicit `--tag` (if any) as a query filter itself.
22
+ *
23
+ * Resource selection: when every deployment targets the same resource (the
24
+ * common case) it's used with no `--tag` needed; only a config that spans
25
+ * *different* resources needs `--tag` to say which dataset to read.
26
+ */
27
+ export async function resolveReadContext(flags) {
28
+ const config = await loadWorkflowConfig();
29
+ return { client: clientFor(resolveReadResource(config, flags.tag), await resolveTokenOrFail()) };
30
+ }
31
+ /** The single resource a read should target, or fail asking which when the
32
+ * config spans more than one. */
33
+ export function resolveReadResource(config, tag) {
34
+ const byKey = new Map(config.deployments.map((d) => [
35
+ `${d.workflowResource.type}:${d.workflowResource.id}`,
36
+ d.workflowResource,
37
+ ]));
38
+ const resources = [...byKey.values()];
39
+ const [sole] = resources;
40
+ if (sole === undefined) {
41
+ fail('No deployments configured.');
42
+ }
43
+ if (resources.length === 1) {
44
+ return sole;
45
+ }
46
+ if (tag === undefined) {
47
+ const tags = config.deployments.map((d) => d.tag).join(', ');
48
+ fail('Config spans multiple resources — pass --tag to choose one.', `Available tags: ${tags}`);
49
+ }
50
+ return selectDeployment(config, { tag }).workflowResource;
51
+ }
52
+ /**
53
+ * The resolution an instance-id-targeted command shares, read or write: an
54
+ * authenticated client for the resource, plus the instance itself — so the
55
+ * command derives its partition (`tag`) from the instance, never from the
56
+ * config's declared deployments. An instance id is globally unique and carries
57
+ * its own `tag`, so a command that names one acts on that instance regardless
58
+ * of which tags the config happens to deploy; the config only locates the
59
+ * resource (via {@link resolveReadResource}, no tag filter).
60
+ *
61
+ * Contrast {@link resolveContext}: the deploy/diff/delete path IS scoped to a
62
+ * declared deployment because it acts on the authored definition set (or a
63
+ * definition name, which — unlike an instance id — isn't unique across tags).
64
+ */
65
+ export async function resolveInstanceContext(flags, instanceId) {
66
+ const config = await loadWorkflowConfig();
67
+ const workflowResource = resolveReadResource(config, flags.tag);
68
+ const client = clientFor(workflowResource, await resolveTokenOrFail());
69
+ const instance = await loadInstanceOrFail(client, instanceId);
70
+ return { client, scope: { tag: instance.tag, workflowResource } };
71
+ }
72
+ /** Fetch an instance by id, exiting cleanly when the resource has no such
73
+ * document — the diagnostic an operator sees on a mistyped id. Split out from
74
+ * {@link resolveInstanceContext} so the not-found path is unit-testable
75
+ * without a live config/token/client. */
76
+ export async function loadInstanceOrFail(client, instanceId) {
77
+ const instance = await client.getDocument(instanceId);
78
+ if (!instance) {
79
+ fail(`Workflow instance ${instanceId} not found`);
80
+ }
81
+ return instance;
82
+ }
@@ -1,36 +1,45 @@
1
- import { type WorkflowDefinition } from '@sanity/workflow-engine';
1
+ import type { SanityClient } from '@sanity/client';
2
+ import { type DeployedDefinition, type WorkflowDefinition } from '@sanity/workflow-engine';
3
+ interface DefinitionShowQueryArgs {
4
+ name: string;
5
+ tag?: string | undefined;
6
+ version?: number | undefined;
7
+ }
8
+ export declare function buildDefinitionShowQuery(flags: DefinitionShowQueryArgs): {
9
+ groq: string;
10
+ params: Record<string, unknown>;
11
+ };
2
12
  /**
3
- * Turn a configured definitions source into a specifier `import()` can
4
- * load. Path-like values (relative or absolute) resolve against `cwd` and
5
- * become file URLs so they're imported as files, not bare package names;
6
- * anything else passes through as a module specifier (e.g. a workspace
7
- * package such as `@sanity/workflow-examples`).
13
+ * Fetch the deployed definition a command targets latest unless pinned via
14
+ * `version`. An absent pinned version always fails (the caller asked for
15
+ * something specific); an absent latest returns `undefined` so the caller
16
+ * decides whether that's fatal.
8
17
  */
9
- export declare function resolveDefinitionsSpecifier(source: string, cwd: string): string;
18
+ export declare function fetchDeployedDefinition({ client, name, tag, version, }: {
19
+ client: SanityClient;
20
+ name: string;
21
+ tag: string;
22
+ version: number | undefined;
23
+ }): Promise<DeployedDefinition | undefined>;
10
24
  /**
11
- * Pull the `allDefinitions` array out of an imported definitions module,
12
- * failing hard with a source-specific message when the module doesn't
13
- * expose it. Per-definition structure is validated downstream by
14
- * {@link collectValidationErrors}, so this only asserts the array shape.
25
+ * Narrow a batch to one definition name (`only`), failing with the available
26
+ * names when there's no match. With `only` unset the whole batch passes through
27
+ * (a fresh array, so callers can mutate without aliasing). `context` prefixes
28
+ * the failure so callers whose run spans several deployments can attribute it.
15
29
  */
16
- export declare function extractDefinitions(mod: unknown, source: string): WorkflowDefinition[];
30
+ export declare function selectDefinitions(allDefinitions: WorkflowDefinition[], { only, context }: {
31
+ only: string | undefined;
32
+ context?: string;
33
+ }): WorkflowDefinition[];
17
34
  /**
18
- * Narrow the local batch to one definition `name`, failing with the
19
- * available names when there's no match. With `only` unset the whole batch passes
20
- * through (a fresh array, so callers can mutate without aliasing).
21
- */
22
- export declare function selectDefinitions(allDefinitions: WorkflowDefinition[], only: string | undefined): WorkflowDefinition[];
23
- /**
24
- * The full local-definitions pipeline shared by `deploy` and
25
- * `definition diff`: resolve the `WORKFLOW_DEFS` source, import it,
26
- * narrow to `only` (when given), and fail on any validation error.
27
- */
28
- export declare function loadValidatedDefinitions(only: string | undefined): Promise<WorkflowDefinition[]>;
29
- /** {@link loadValidatedDefinitions} narrowed to exactly one definition name. */
30
- export declare function loadValidatedDefinition(name: string): Promise<WorkflowDefinition>;
31
- /**
32
- * Per-definition structural/GROQ checks plus one batch-level check the
33
- * engine doesn't do for us: duplicate `name` within the local set.
34
- * Returns the human-readable error lines; empty means the batch is valid.
35
+ * Per-definition structural/GROQ checks plus one batch-level check the engine
36
+ * doesn't do for us: duplicate `name` within the set. Returns the
37
+ * human-readable error lines; empty means the batch is valid.
35
38
  */
36
39
  export declare function collectValidationErrors(defs: WorkflowDefinition[]): string[];
40
+ /** Validate a set of definitions, exiting with the collected errors on any
41
+ * failure. The single validate-then-fail gate `deploy` and `definition diff`
42
+ * share, so the two can't drift into divergent failure messages. `context`
43
+ * prefixes the failure with the deployment it belongs to in multi-target runs. */
44
+ export declare function validateOrFail(defs: WorkflowDefinition[], context?: string): void;
45
+ export {};