@sanity/workflow-cli 0.14.0 → 0.20.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @sanity/workflow-cli
2
2
 
3
+ ## 0.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6e2a066: Ambiguous deployment/resource selection now prompts on an interactive terminal instead of only erroring. When a config has several deployments and no selector is given, `start`, `definition diff`, and `definition delete` show the same deployment picker `deploy` already offered. The instance-targeted commands (`abort`, `set-stage`, `fire-action`, `diagnose`) show that picker too when the config spans several resources — the interactive counterpart to their `--deployment` selector, reading from the chosen deployment's resource. Non-interactive runs (CI, piped output) keep the existing errors asking for `--deployment`/`--tag`, and `nuke` still requires an explicit selector — for a destructive command, naming exactly what is swept is the right friction.
8
+ - 7c4dd86: Reduce guard lifecycle request volume and chunk orphan cleanup transactions.
9
+ - e860898: **BREAKING:** Published Editorial Workflows packages now ship as one fixed release stack and require exact-version peers for every shared runtime package. Install the matching stack so the engine, reactive core, adapters, tools, and UI cannot silently load private or version-skewed copies.
10
+
11
+ ### Patch Changes
12
+
13
+ - efb4cd9: Error detail below the ✖ headline is no longer dimmed. Validation output, auth hints, and errors caught by command wrappers now render in legible red so the information users need to act on remains readable.
14
+ - Updated dependencies [bce09fc]
15
+ - Updated dependencies [efb4cd9]
16
+ - Updated dependencies [46b0285]
17
+ - Updated dependencies [ab5e454]
18
+ - Updated dependencies [8a76c67]
19
+ - Updated dependencies [e3122cc]
20
+ - Updated dependencies [7e8f459]
21
+ - Updated dependencies [98e488e]
22
+ - Updated dependencies [7c4dd86]
23
+ - @sanity/workflow-engine@0.20.0
24
+
3
25
  ## 0.14.0
4
26
 
5
27
  ### Minor Changes
package/README.md CHANGED
@@ -87,9 +87,12 @@ Pick a deployment with `--deployment <name>`; with a single deployment configure
87
87
  can omit it. `--tag <tag>` also works on single-deployment commands while the
88
88
  tag names exactly one deployment — on `deploy` it targets every deployment
89
89
  carrying the tag (a tag is an environment group). With several configured, a
90
- bare interactive `deploy` presents a keyboard-driven deployment selector (by
91
- name, tag alongside). In CI or another non-interactive shell, it fails asking
92
- for `--deployment`, `--tag`, or `--all-tags` instead of blocking for input.
90
+ bare interactive run presents a keyboard-driven deployment selector (by name,
91
+ tag alongside) on `deploy`, `start`, and `definition diff`/`delete`, and on
92
+ the instance-targeted commands (`abort`, `set-stage`, `fire-action`,
93
+ `diagnose`) when the config spans several resources. In CI or another
94
+ non-interactive shell, the command fails asking for `--deployment` or `--tag`
95
+ (`deploy` also suggests `--all-tags`) instead of blocking for input.
93
96
  `--all-tags` deploys every deployment in the config in one run: a failure in one doesn't
94
97
  stop the rest — the run continues, prints a summary of what failed, and exits
95
98
  non-zero. The client's project + dataset are derived from the deployment's
@@ -39,7 +39,7 @@ export default class Nuke extends WorkflowCommand {
39
39
  async run() {
40
40
  const { flags } = await this.parse(Nuke);
41
41
  const config = await loadWorkflowConfig();
42
- const deployment = selectDeployment(config, { name: flags.deployment, tag: flags.tag });
42
+ const deployment = await selectDeployment(config, { name: flags.deployment, tag: flags.tag });
43
43
  refuseOverlappingNuke(config.deployments, deployment);
44
44
  const targets = buildTargets(deployment, await resolveTokenOrFail());
45
45
  const plan = await resolveNukePlan({ tag: deployment.tag, targets });
@@ -1,6 +1,7 @@
1
1
  import type { SanityClient } from '@sanity/client';
2
2
  import { type WorkflowConfig, type WorkflowDeployment, type WorkflowInstance, type WorkflowResource } from '@sanity/workflow-engine';
3
3
  import type { EngineScope } from './operation-args.ts';
4
+ import { type ChooseDeploymentName } from './select-deployment.ts';
4
5
  export interface DeploymentContext {
5
6
  deployment: WorkflowDeployment;
6
7
  client: SanityClient;
@@ -50,10 +51,11 @@ export declare function dedupeResources(resources: WorkflowResource[]): Workflow
50
51
  * tag (tags may repeat across deployments), or — untagged — every distinct
51
52
  * one the config's deployments mention ({@link dedupeResources}). */
52
53
  export declare function resolveReadResources(config: WorkflowConfig, tag: string | undefined): WorkflowResource[];
53
- /** The single resource a read should target, or fail asking which when the
54
- * config spans more than one. For the paths that need one definite dataset
55
- * (the instance-targeted verbs); listings fan out via
56
- * {@link resolveReadResources} instead. */
54
+ /** The single resource an instance-keyed command reads from when it isn't
55
+ * disambiguated interactively: the sole resource, the `--tag`-narrowed one, or
56
+ * a `fail` asking for `--deployment`/`--tag` when the config spans several. The
57
+ * interactive picker lives in {@link resolveInstanceResource}; listings fan out
58
+ * via {@link resolveReadResources} instead. */
57
59
  export declare function resolveReadResource(config: WorkflowConfig, tag: string | undefined): WorkflowResource;
58
60
  /**
59
61
  * The resolution an instance-id-targeted command shares, read or write: an
@@ -74,16 +76,22 @@ export declare function resolveInstanceContext(flags: {
74
76
  }, instanceId: string): Promise<InstanceContext>;
75
77
  /**
76
78
  * Which resource an instance-keyed command reads from: the one `--deployment`
77
- * names (resolved by its unique deployment name), else the sole or
78
- * `--tag`-narrowed resource ({@link resolveReadResource}). The instance id is
79
- * globally unique, so this only locates the resource to look in never the
80
- * instance's `tag` partition, which the caller takes from the loaded instance.
81
- * The shared path behind all four instance-keyed commands.
79
+ * names (resolved by its unique deployment name); otherwise, when the config
80
+ * spans several resources, an interactive terminal is prompted to pick a
81
+ * deployment ({@link canPromptOnStderr}) its resource is the read source, the
82
+ * interactive counterpart to `--deployment`. A run that can't prompt falls to
83
+ * the sole or `--tag`-narrowed resource, or the ambiguity error
84
+ * ({@link resolveReadResource}). The instance id is globally unique, so this
85
+ * only locates the resource to look in — never the instance's `tag` partition,
86
+ * which the caller takes from the loaded instance. The shared path behind all
87
+ * four instance-keyed commands.
82
88
  */
83
- export declare function resolveInstanceResource(config: WorkflowConfig, { deployment, tag }: {
89
+ export declare function resolveInstanceResource(config: WorkflowConfig, { deployment, tag, interactive, chooseDeployment, }: {
84
90
  deployment?: string | undefined;
85
91
  tag?: string | undefined;
86
- }): WorkflowResource;
92
+ interactive?: boolean | undefined;
93
+ chooseDeployment?: ChooseDeploymentName | undefined;
94
+ }): Promise<WorkflowResource>;
87
95
  /** Fetch an instance by id, exiting cleanly when the resource has no such
88
96
  * document — the diagnostic an operator sees on a mistyped id. Split out from
89
97
  * {@link resolveInstanceContext} so the not-found path is unit-testable
@@ -2,11 +2,12 @@ import { assertReadableModel, resourceGdr, } from '@sanity/workflow-engine';
2
2
  import { clientFor, resolveTokenOrFail } from "./client.js";
3
3
  import { fail } from "./fail.js";
4
4
  import { loadWorkflowConfig } from "./load-config.js";
5
- import { availableDeployments, deploymentsForTag, selectDeployment } from "./select-deployment.js";
5
+ import { canPromptOnStderr } from "./prompt.js";
6
+ import { availableDeployments, deploymentsForTag, selectDeployment, } from "./select-deployment.js";
6
7
  import { resourceLabel } from "./ui.js";
7
8
  export async function resolveContext(flags) {
8
9
  const config = await loadWorkflowConfig();
9
- const deployment = selectDeployment(config, { name: flags.deployment, tag: flags.tag });
10
+ const deployment = await selectDeployment(config, { name: flags.deployment, tag: flags.tag });
10
11
  return { deployment, client: clientFor(deployment.workflowResource, await resolveTokenOrFail()) };
11
12
  }
12
13
  export async function resolveReadTargets(flags) {
@@ -42,14 +43,25 @@ export function resolveReadResource(config, tag) {
42
43
  }
43
44
  export async function resolveInstanceContext(flags, instanceId) {
44
45
  const config = await loadWorkflowConfig();
45
- const workflowResource = resolveInstanceResource(config, flags);
46
+ const workflowResource = await resolveInstanceResource(config, flags);
46
47
  const client = clientFor(workflowResource, await resolveTokenOrFail());
47
48
  const instance = await loadInstanceOrFail(client, instanceId);
48
49
  return { client, scope: { tag: instance.tag, workflowResource } };
49
50
  }
50
- export function resolveInstanceResource(config, { deployment, tag }) {
51
+ export async function resolveInstanceResource(config, { deployment, tag, interactive, chooseDeployment, }) {
51
52
  if (deployment !== undefined) {
52
- return selectDeployment(config, { name: deployment, tag: undefined }).workflowResource;
53
+ return (await selectDeployment(config, { name: deployment, tag: undefined })).workflowResource;
54
+ }
55
+ if (tag === undefined &&
56
+ (interactive ?? canPromptOnStderr()) &&
57
+ resolveReadResources(config, undefined).length > 1) {
58
+ const chosen = await selectDeployment(config, {
59
+ name: undefined,
60
+ tag: undefined,
61
+ interactive: true,
62
+ chooseName: chooseDeployment,
63
+ });
64
+ return chosen.workflowResource;
53
65
  }
54
66
  return resolveReadResource(config, tag);
55
67
  }
package/dist/lib/fail.js CHANGED
@@ -8,7 +8,7 @@ export function fail(headline, detail) {
8
8
  process.stderr.write(`${styleText('red', `${logSymbols.error} ${headline}`)}\n`);
9
9
  if (detail !== undefined && detail !== '') {
10
10
  for (const line of detail.split('\n')) {
11
- process.stderr.write(` ${styleText(['dim', 'red'], line)}\n`);
11
+ process.stderr.write(` ${styleText('red', line)}\n`);
12
12
  }
13
13
  }
14
14
  return Errors.exit(1);
@@ -59,19 +59,6 @@ export declare function resolveNukePlan(args: {
59
59
  tag: string;
60
60
  targets: NukeTarget[];
61
61
  }): Promise<NukePlan>;
62
- /**
63
- * Does a guard doc belong to `tag`? A guard's `sourceInstanceId` is the id of
64
- * the instance that registered it (`<tag>.wf-instance.<random>`), and its own
65
- * `_id` embeds that same instance id
66
- * (`temp.system.guard.<tag>.wf-instance.<random>.<name>`). Matching on either —
67
- * with the trailing `.` so `prod` never matches `prod-eu` — catches guards
68
- * whose `sourceInstanceId` was never written as well as guards whose instance
69
- * is already gone.
70
- */
71
- export declare function guardMatchesTag(guard: {
72
- _id: string;
73
- sourceInstanceId?: string;
74
- }, tag: string): boolean;
75
62
  /** The `project.dataset` labels of resources the plan actually deletes from —
76
63
  * the exact set the operator must type back to confirm. A resource with
77
64
  * nothing to delete is not "involved" and is omitted. */
package/dist/lib/nuke.js CHANGED
@@ -52,14 +52,12 @@ function engineDocIds(args) {
52
52
  return args.client.fetch(`*[_type == $type && ${tagScopeFilter()}]._id`, { type: args.type, tag: args.tag }, { perspective: 'raw', tag: REQUEST_TAG });
53
53
  }
54
54
  async function tagGuardIds(args) {
55
- const guards = await args.client.fetch(`*[_type == $type]{_id, sourceInstanceId}`, { type: GUARD_DOC_TYPE }, { perspective: 'raw', tag: REQUEST_TAG });
56
- return guards.filter((guard) => guardMatchesTag(guard, args.tag)).map((guard) => guard._id);
57
- }
58
- export function guardMatchesTag(guard, tag) {
59
- const instancePrefix = `${tag}.wf-instance.`;
55
+ const instancePrefix = `${args.tag}.wf-instance.`;
60
56
  const guardIdPrefix = `${GUARD_DOC_TYPE}.${instancePrefix}`;
61
- return ((guard.sourceInstanceId?.startsWith(instancePrefix) ?? false) ||
62
- guard._id.startsWith(guardIdPrefix));
57
+ return args.client.fetch(`*[_type == $type && (
58
+ string::startsWith(sourceInstanceId, $instancePrefix) ||
59
+ string::startsWith(_id, $guardIdPrefix)
60
+ )]._id`, { type: GUARD_DOC_TYPE, instancePrefix, guardIdPrefix }, { perspective: 'raw', tag: REQUEST_TAG });
63
61
  }
64
62
  export function involvedTargets(plan) {
65
63
  return plan.resources
@@ -9,3 +9,17 @@
9
9
  * prompts such as nuke's confirmation.
10
10
  */
11
11
  export declare function canPromptOnStderr(): boolean;
12
+ /**
13
+ * Ask a `select` question on stderr — the channel this package renders prompts
14
+ * on so a redirected stdout can't swallow the question (the display side of the
15
+ * {@link canPromptOnStderr} gate). The single site that routes a `select` to
16
+ * stderr, so no `select` prompt can silently regress to stdout.
17
+ */
18
+ export declare function selectOnStderr(config: {
19
+ message: string;
20
+ choices: readonly {
21
+ name: string;
22
+ value: string;
23
+ description?: string;
24
+ }[];
25
+ }): Promise<string>;
@@ -1,4 +1,8 @@
1
1
  import { isInteractive } from '@sanity/cli-core';
2
+ import { select } from '@sanity/cli-core/ux';
2
3
  export function canPromptOnStderr() {
3
4
  return isInteractive() && process.stderr.isTTY === true;
4
5
  }
6
+ export function selectOnStderr(config) {
7
+ return select(config, { output: process.stderr });
8
+ }
@@ -1,5 +1,5 @@
1
1
  import { type DeployTarget, type WorkflowConfig, type WorkflowDeployment } from '@sanity/workflow-engine';
2
- type ChooseDeploymentName = (deployments: WorkflowDeployment[]) => Promise<string>;
2
+ export type ChooseDeploymentName = (deployments: WorkflowDeployment[]) => Promise<string>;
3
3
  interface DeploymentSelectionOptions {
4
4
  name: string | undefined;
5
5
  tag: string | undefined;
@@ -19,17 +19,20 @@ export declare function deploymentLabel({ name, tag }: Pick<WorkflowDeployment,
19
19
  * so `--deployment` can disambiguate.
20
20
  *
21
21
  * With no selector: fall back to the sole deployment when there's exactly one
22
- * (the common single-environment case), otherwise fail asking for
23
- * `--deployment` or `--tag` a multi-deployment config is ambiguous without
24
- * one. `orAlternative`
25
- * extends that ambiguity message for callers with another way out (deploy's
22
+ * (the common single-environment case). When several are configured, an
23
+ * interactive terminal is prompted to pick one ({@link canPromptOnStderr}); a
24
+ * run that cannot prompt fails asking for `--deployment` or `--tag` — a
25
+ * multi-deployment config is ambiguous without one. `orAlternative` extends
26
+ * that ambiguity message for callers with another way out (deploy's
26
27
  * `--all-tags`).
27
28
  */
28
- export declare function selectDeployment(config: WorkflowConfig, { name, tag, orAlternative, }: {
29
+ export declare function selectDeployment(config: WorkflowConfig, { name, tag, orAlternative, interactive, chooseName, }: {
29
30
  name: string | undefined;
30
31
  tag: string | undefined;
31
32
  orAlternative?: string;
32
- }): WorkflowDeployment;
33
+ interactive?: boolean | undefined;
34
+ chooseName?: ChooseDeploymentName | undefined;
35
+ }): Promise<WorkflowDeployment>;
33
36
  /**
34
37
  * Every deployment carrying `tag`, failing when the tag matches none — the
35
38
  * shared resolution for the callers that accept a whole tag group (deploy's
@@ -1,11 +1,10 @@
1
- import { select } from '@sanity/cli-core/ux';
2
1
  import { resourceAliasesToMap, } from '@sanity/workflow-engine';
3
2
  import { fail } from "./fail.js";
4
- import { canPromptOnStderr } from "./prompt.js";
3
+ import { canPromptOnStderr, selectOnStderr } from "./prompt.js";
5
4
  export function deploymentLabel({ name, tag }) {
6
5
  return `${name} (${tag})`;
7
6
  }
8
- export function selectDeployment(config, { name, tag, orAlternative = '', }) {
7
+ export async function selectDeployment(config, { name, tag, orAlternative = '', interactive, chooseName = chooseDeploymentName, }) {
9
8
  if (name !== undefined) {
10
9
  const deployment = config.deployments.find((candidate) => candidate.name === name);
11
10
  if (deployment === undefined) {
@@ -21,7 +20,14 @@ export function selectDeployment(config, { name, tag, orAlternative = '', }) {
21
20
  }
22
21
  return sole;
23
22
  }
23
+ return selectDefaultDeployment(config, { orAlternative, interactive, chooseName });
24
+ }
25
+ async function selectDefaultDeployment(config, { orAlternative, interactive, chooseName, }) {
24
26
  if (config.deployments.length > 1) {
27
+ if (interactive ?? canPromptOnStderr()) {
28
+ const selectedName = await chooseName(config.deployments);
29
+ return selectDeployment(config, { name: selectedName, tag: undefined });
30
+ }
25
31
  fail(`Multiple deployments configured — pass --deployment or --tag${orAlternative}.`, availableDeployments(config));
26
32
  }
27
33
  const [sole] = config.deployments;
@@ -37,43 +43,28 @@ export function deploymentsForTag(config, tag) {
37
43
  }
38
44
  return matches;
39
45
  }
40
- export async function selectDeployments(config, { name, tag, allTags, interactive, chooseName = chooseDeploymentName }) {
46
+ export async function selectDeployments(config, { name, tag, allTags, interactive, chooseName }) {
41
47
  if (allTags) {
42
48
  return config.deployments;
43
49
  }
44
50
  if (tag !== undefined) {
45
51
  return deploymentsForTag(config, tag);
46
52
  }
47
- if (name === undefined && config.deployments.length > 1) {
48
- if (!(interactive ?? canPromptOnStderr())) {
49
- return [
50
- selectDeployment(config, {
51
- name,
52
- tag,
53
- orAlternative: ', or --all-tags to deploy every deployment',
54
- }),
55
- ];
56
- }
57
- const selectedName = await chooseName(config.deployments);
58
- return [selectDeployment(config, { name: selectedName, tag: undefined })];
59
- }
60
53
  return [
61
- selectDeployment(config, {
54
+ await selectDeployment(config, {
62
55
  name,
63
56
  tag,
64
57
  orAlternative: ', or --all-tags to deploy every deployment',
58
+ interactive,
59
+ chooseName,
65
60
  }),
66
61
  ];
67
62
  }
68
63
  async function chooseDeploymentName(deployments) {
69
- return select({
64
+ return selectOnStderr({
70
65
  message: 'Select a deployment',
71
- choices: deployments.map(({ name, tag }) => ({
72
- name,
73
- value: name,
74
- description: `tag: ${tag}`,
75
- })),
76
- }, { output: process.stderr });
66
+ choices: deployments.map(({ name, tag }) => ({ name, value: name, description: `tag: ${tag}` })),
67
+ });
77
68
  }
78
69
  export function availableDeployments(config, deployments = config.deployments) {
79
70
  return `Available deployments: ${deployments.map(deploymentLabel).join(', ')}`;
@@ -913,5 +913,5 @@
913
913
  ]
914
914
  }
915
915
  },
916
- "version": "0.14.0"
916
+ "version": "0.20.0"
917
917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-cli",
3
- "version": "0.14.0",
3
+ "version": "0.20.0",
4
4
  "description": "Command-line tool for deploying, inspecting, and administering Sanity workflow definitions and instances.",
5
5
  "keywords": [
6
6
  "cli",
@@ -55,16 +55,19 @@
55
55
  "diff": "^9.0.0",
56
56
  "jiti": "^2.7.0",
57
57
  "log-symbols": "^7.0.1",
58
- "ora": "^9.4.0",
59
- "@sanity/workflow-engine": "0.19.0"
58
+ "ora": "^9.4.0"
60
59
  },
61
60
  "devDependencies": {
62
61
  "@types/diff": "^8.0.0",
63
62
  "@types/node": "^24.12.4",
64
63
  "oclif": "^4.23.16",
65
64
  "vitest": "^4.1.8",
66
- "@sanity/workflow-engine-test": "0.13.1",
67
- "@sanity/workflow-examples": "0.8.1"
65
+ "@sanity/workflow-engine": "0.20.0",
66
+ "@sanity/workflow-engine-test": "0.20.0",
67
+ "@sanity/workflow-examples": "0.9.0"
68
+ },
69
+ "peerDependencies": {
70
+ "@sanity/workflow-engine": "0.20.0"
68
71
  },
69
72
  "oclif": {
70
73
  "bin": "sanity-workflows",