@sanity/workflow-cli 0.12.0 → 0.14.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 (55) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +77 -48
  3. package/dist/commands/{abort.d.ts → editorial-workflows/abort.d.ts} +4 -2
  4. package/dist/commands/{abort.js → editorial-workflows/abort.js} +8 -7
  5. package/dist/commands/{definition → editorial-workflows/definition}/delete.d.ts +4 -2
  6. package/dist/commands/{definition → editorial-workflows/definition}/delete.js +7 -6
  7. package/dist/commands/{definition → editorial-workflows/definition}/diff.d.ts +3 -1
  8. package/dist/commands/{definition → editorial-workflows/definition}/diff.js +8 -7
  9. package/dist/commands/{definition → editorial-workflows/definition}/list.d.ts +2 -1
  10. package/dist/commands/{definition → editorial-workflows/definition}/list.js +21 -21
  11. package/dist/commands/{definition → editorial-workflows/definition}/show.d.ts +3 -2
  12. package/dist/commands/{definition → editorial-workflows/definition}/show.js +7 -6
  13. package/dist/commands/{deploy.d.ts → editorial-workflows/deploy.d.ts} +4 -2
  14. package/dist/commands/{deploy.js → editorial-workflows/deploy.js} +26 -19
  15. package/dist/commands/{diagnose.d.ts → editorial-workflows/diagnose.d.ts} +3 -1
  16. package/dist/commands/{diagnose.js → editorial-workflows/diagnose.js} +8 -7
  17. package/dist/commands/{fire-action.d.ts → editorial-workflows/fire-action.d.ts} +4 -2
  18. package/dist/commands/{fire-action.js → editorial-workflows/fire-action.js} +9 -8
  19. package/dist/commands/{list.d.ts → editorial-workflows/list.d.ts} +2 -1
  20. package/dist/commands/{list.js → editorial-workflows/list.js} +24 -24
  21. package/dist/commands/editorial-workflows/nuke.d.ts +13 -0
  22. package/dist/commands/{nuke.js → editorial-workflows/nuke.js} +22 -17
  23. package/dist/commands/{reset-activity.d.ts → editorial-workflows/reset-activity.d.ts} +2 -1
  24. package/dist/commands/{reset-activity.js → editorial-workflows/reset-activity.js} +2 -1
  25. package/dist/commands/{set-stage.d.ts → editorial-workflows/set-stage.d.ts} +5 -3
  26. package/dist/commands/{set-stage.js → editorial-workflows/set-stage.js} +7 -6
  27. package/dist/commands/{show.d.ts → editorial-workflows/show.d.ts} +2 -1
  28. package/dist/commands/{show.js → editorial-workflows/show.js} +40 -26
  29. package/dist/commands/{start.d.ts → editorial-workflows/start.d.ts} +4 -2
  30. package/dist/commands/{start.js → editorial-workflows/start.js} +10 -9
  31. package/dist/commands/{tail.d.ts → editorial-workflows/tail.d.ts} +2 -1
  32. package/dist/commands/{tail.js → editorial-workflows/tail.js} +6 -5
  33. package/dist/hooks/finally/telemetry.js +2 -2
  34. package/dist/hooks/prerun/telemetry.d.ts +4 -3
  35. package/dist/lib/base-command.d.ts +4 -6
  36. package/dist/lib/base-command.js +6 -0
  37. package/dist/lib/context.d.ts +24 -9
  38. package/dist/lib/context.js +15 -6
  39. package/dist/lib/flags.d.ts +27 -4
  40. package/dist/lib/flags.js +17 -1
  41. package/dist/lib/nuke.d.ts +15 -1
  42. package/dist/lib/nuke.js +22 -1
  43. package/dist/lib/prompt.d.ts +3 -2
  44. package/dist/lib/select-deployment.d.ts +43 -16
  45. package/dist/lib/select-deployment.js +66 -15
  46. package/dist/lib/share-definitions.d.ts +23 -28
  47. package/dist/lib/share-definitions.js +26 -38
  48. package/dist/lib/telemetry-setup.d.ts +2 -2
  49. package/dist/lib/telemetry.d.ts +18 -10
  50. package/dist/lib/telemetry.js +5 -2
  51. package/dist/lib/ui.d.ts +12 -0
  52. package/dist/lib/ui.js +9 -0
  53. package/oclif.manifest.json +223 -66
  54. package/package.json +11 -9
  55. package/dist/commands/nuke.d.ts +0 -11
@@ -2,13 +2,14 @@ import { styleText } from 'node:util';
2
2
  import { Args } from '@oclif/core';
3
3
  import { assertReadableModel, displayTitle, errorMessage, } from '@sanity/workflow-engine';
4
4
  import logSymbols from 'log-symbols';
5
- import { WorkflowCommand } from "../lib/base-command.js";
6
- import { findInstance, resolveReadTargets } from "../lib/context.js";
7
- import { fail, failureDetail } from "../lib/fail.js";
8
- import { tagFlags } from "../lib/flags.js";
9
- import { formatTimestamp } from "../lib/ui.js";
5
+ import { WorkflowCommand } from "../../lib/base-command.js";
6
+ import { findInstance, resolveReadTargets } from "../../lib/context.js";
7
+ import { fail, failureDetail } from "../../lib/fail.js";
8
+ import { tagFlags } from "../../lib/flags.js";
9
+ import { formatTimestamp } from "../../lib/ui.js";
10
10
  const TAIL_TAG = 'tail';
11
11
  export default class Tail extends WorkflowCommand {
12
+ static aliases = ['tail'];
12
13
  static description = 'Stream new history entries on a workflow instance as they land in the dataset.';
13
14
  static examples = ['<%= config.bin %> tail wf-instance.abc123'];
14
15
  static args = {
@@ -1,8 +1,8 @@
1
- import { finishCliTelemetry, usedFlagNames } from "../../lib/telemetry.js";
1
+ import { finishCliTelemetry, traceCommandId, usedFlagNames } from "../../lib/telemetry.js";
2
2
  const hook = async function (options) {
3
3
  try {
4
4
  await finishCliTelemetry({
5
- command: options.id,
5
+ command: traceCommandId(options),
6
6
  flags: usedFlagNames(options.argv, options.Command?.flags),
7
7
  success: options.error === undefined,
8
8
  });
@@ -1,8 +1,9 @@
1
1
  import type { Hook } from '@oclif/core';
2
2
  /** Builds and installs the invocation's telemetry shell before the command
3
3
  * runs; the `finally` hook completes the trace and flushes. Never throws.
4
- * An explicit `--share-defs` forces the store to send despite CI /
5
- * `DO_NOT_TRACK` resolved from raw argv here, since the store is built
6
- * before oclif parses flags. */
4
+ * A deploy that may share definitions—flagless by default or explicitly
5
+ * enabledforces the store to send despite CI / `DO_NOT_TRACK`. This is
6
+ * resolved from raw argv because the store is built before oclif parses
7
+ * flags. */
7
8
  declare const hook: Hook<'prerun'>;
8
9
  export default hook;
@@ -1,11 +1,9 @@
1
1
  import { Command } from '@oclif/core';
2
2
  /**
3
- * Base for the API-backed workflow commands: the backstop that renders an
4
- * auth rejection (HTTP 401) escaping a command's `run` through the clean
5
- * {@link fail} path with {@link failureDetail}'s recovery hint — instead
6
- * of an oclif error dump. Errors caught inside commands render the same
7
- * detail at their own `fail` sites; every other error propagates to oclif
8
- * unchanged.
3
+ * Base for workflow commands: prompt interrupts exit with the conventional
4
+ * SIGINT code and no stack, while an auth rejection escaping `run` renders
5
+ * through the clean {@link fail} path with {@link failureDetail}'s recovery
6
+ * hint. Every other error propagates to oclif unchanged.
9
7
  */
10
8
  export declare abstract class WorkflowCommand extends Command {
11
9
  protected catch(err: Error & {
@@ -1,7 +1,13 @@
1
+ import { styleText } from 'node:util';
1
2
  import { Command } from '@oclif/core';
3
+ import { exitCodes } from '@sanity/cli-core';
2
4
  import { fail, failureDetail, isAuthRejection } from "./fail.js";
3
5
  export class WorkflowCommand extends Command {
4
6
  async catch(err) {
7
+ if (err.name === 'ExitPromptError' || err.message === 'SIGINT') {
8
+ this.logToStderr(`${styleText('yellow', '›')} Aborted by user`);
9
+ return this.exit(exitCodes.SIGINT);
10
+ }
5
11
  if (isAuthRejection(err)) {
6
12
  fail('Authentication failed:', failureDetail(err));
7
13
  }
@@ -13,11 +13,12 @@ export interface InstanceContext {
13
13
  }
14
14
  /**
15
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.
16
+ * `--deployment` or `--tag` (or the sole one), and build an authenticated
17
+ * client for that deployment's resource. A write acts on one specific
18
+ * deployment, so a single, definite target is exactly what it needs.
19
19
  */
20
20
  export declare function resolveContext(flags: {
21
+ deployment?: string | undefined;
21
22
  tag?: string | undefined;
22
23
  }): Promise<DeploymentContext>;
23
24
  /** One resource a read inspects, with its authenticated client. */
@@ -34,8 +35,9 @@ export interface ReadTarget {
34
35
  *
35
36
  * Untagged, this is every distinct resource the config mentions — a read is
36
37
  * harmless to fan out, so "show me what's deployed" spans the whole config.
37
- * `--tag` narrows to that deployment's resource. The token resolves once for
38
- * the run, however many targets it spans.
38
+ * `--tag` narrows to the resources deployed under that tag (tags may repeat
39
+ * across deployments). The token resolves once for the run, however many
40
+ * targets it spans.
39
41
  */
40
42
  export declare function resolveReadTargets(flags: {
41
43
  tag?: string | undefined;
@@ -44,9 +46,9 @@ export declare function resolveReadTargets(flags: {
44
46
  * The first occurrence wins its position, so a caller-ordered list (e.g. the
45
47
  * engine resource first) keeps its lead entry. */
46
48
  export declare function dedupeResources(resources: WorkflowResource[]): WorkflowResource[];
47
- /** The distinct resources a read targets: the tagged deployment's sole
48
- * resource, or — untagged — every distinct one the config's deployments
49
- * mention ({@link dedupeResources}). */
49
+ /** The distinct resources a read targets: every resource deployed under the
50
+ * tag (tags may repeat across deployments), or — untagged — every distinct
51
+ * one the config's deployments mention ({@link dedupeResources}). */
50
52
  export declare function resolveReadResources(config: WorkflowConfig, tag: string | undefined): WorkflowResource[];
51
53
  /** The single resource a read should target, or fail asking which when the
52
54
  * config spans more than one. For the paths that need one definite dataset
@@ -60,15 +62,28 @@ export declare function resolveReadResource(config: WorkflowConfig, tag: string
60
62
  * config's declared deployments. An instance id is globally unique and carries
61
63
  * its own `tag`, so a command that names one acts on that instance regardless
62
64
  * of which tags the config happens to deploy; the config only locates the
63
- * resource (via {@link resolveReadResource}, no tag filter).
65
+ * resource (via {@link resolveInstanceResource}, no tag filter).
64
66
  *
65
67
  * Contrast {@link resolveContext}: the deploy/diff/delete path IS scoped to a
66
68
  * declared deployment because it acts on the authored definition set (or a
67
69
  * definition name, which — unlike an instance id — isn't unique across tags).
68
70
  */
69
71
  export declare function resolveInstanceContext(flags: {
72
+ deployment?: string | undefined;
70
73
  tag?: string | undefined;
71
74
  }, instanceId: string): Promise<InstanceContext>;
75
+ /**
76
+ * 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.
82
+ */
83
+ export declare function resolveInstanceResource(config: WorkflowConfig, { deployment, tag }: {
84
+ deployment?: string | undefined;
85
+ tag?: string | undefined;
86
+ }): WorkflowResource;
72
87
  /** Fetch an instance by id, exiting cleanly when the resource has no such
73
88
  * document — the diagnostic an operator sees on a mistyped id. Split out from
74
89
  * {@link resolveInstanceContext} so the not-found path is unit-testable
@@ -2,11 +2,11 @@ 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 { selectDeployment } from "./select-deployment.js";
5
+ import { availableDeployments, deploymentsForTag, selectDeployment } from "./select-deployment.js";
6
6
  import { resourceLabel } from "./ui.js";
7
7
  export async function resolveContext(flags) {
8
8
  const config = await loadWorkflowConfig();
9
- const deployment = selectDeployment(config, { tag: flags.tag });
9
+ const deployment = selectDeployment(config, { name: flags.deployment, tag: flags.tag });
10
10
  return { deployment, client: clientFor(deployment.workflowResource, await resolveTokenOrFail()) };
11
11
  }
12
12
  export async function resolveReadTargets(flags) {
@@ -20,7 +20,7 @@ export function dedupeResources(resources) {
20
20
  }
21
21
  export function resolveReadResources(config, tag) {
22
22
  if (tag !== undefined) {
23
- return [selectDeployment(config, { tag }).workflowResource];
23
+ return dedupeResources(deploymentsForTag(config, tag).map((deployment) => deployment.workflowResource));
24
24
  }
25
25
  const resources = dedupeResources(config.deployments.map((d) => d.workflowResource));
26
26
  if (resources.length === 0) {
@@ -34,16 +34,25 @@ export function resolveReadResource(config, tag) {
34
34
  if (resources.length === 1 && sole !== undefined) {
35
35
  return sole;
36
36
  }
37
- const tags = config.deployments.map((d) => d.tag).join(', ');
38
- fail('Config spans multiple resources — pass --tag to choose one.', `Available tags: ${tags}`);
37
+ if (tag === undefined) {
38
+ fail('Config spans multiple resources — pass --deployment or --tag to choose one.', availableDeployments(config));
39
+ }
40
+ const carriers = config.deployments.filter((d) => d.tag === tag);
41
+ fail(`Tag "${tag}" spans multiple resources — pass --deployment to choose one.`, availableDeployments(config, carriers));
39
42
  }
40
43
  export async function resolveInstanceContext(flags, instanceId) {
41
44
  const config = await loadWorkflowConfig();
42
- const workflowResource = resolveReadResource(config, flags.tag);
45
+ const workflowResource = resolveInstanceResource(config, flags);
43
46
  const client = clientFor(workflowResource, await resolveTokenOrFail());
44
47
  const instance = await loadInstanceOrFail(client, instanceId);
45
48
  return { client, scope: { tag: instance.tag, workflowResource } };
46
49
  }
50
+ export function resolveInstanceResource(config, { deployment, tag }) {
51
+ if (deployment !== undefined) {
52
+ return selectDeployment(config, { name: deployment, tag: undefined }).workflowResource;
53
+ }
54
+ return resolveReadResource(config, tag);
55
+ }
47
56
  export async function loadInstanceOrFail(client, instanceId) {
48
57
  const instance = await client.getDocument(instanceId, {
49
58
  tag: 'instance.load',
@@ -1,10 +1,33 @@
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. */
1
+ /** The environment tag (e.g. prod, test). Tags group deployments by
2
+ * environment and may repeat across a config. For the read commands that
3
+ * consume this directly it's an optional query filter — and the resource
4
+ * disambiguator when a config spans more than one. Write commands layer
5
+ * selection semantics on top via {@link deploymentFlags}; instance-keyed
6
+ * commands add `--deployment` alongside it via {@link instanceFlags}. */
5
7
  export declare const tagFlags: {
6
8
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
9
  };
10
+ /** The selectors an instance-keyed command (`diagnose`, `abort`, `set-stage`,
11
+ * `fire-action`) takes. An instance id is globally unique and carries its own
12
+ * `tag`, so these only pick WHICH resource to read it from — never the
13
+ * instance's partition, which always comes from the loaded instance.
14
+ * `--deployment` names one deployment and reads from the resource it targets;
15
+ * `--tag` stays the optional narrower {@link tagFlags} describes. Mutually
16
+ * exclusive; a sole-resource config needs neither. */
17
+ export declare const instanceFlags: {
18
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ };
21
+ /** The selectors a single-deployment (write) command takes: `--deployment` —
22
+ * the deployment's unique name identity — or `--tag`, which resolves while the
23
+ * tag names exactly one deployment and errors asking for `--deployment` when
24
+ * it spans several. Mutually exclusive; a sole-deployment config needs neither.
25
+ * `deploy` overrides the `tag` description with its group semantics (every
26
+ * deployment carrying the tag). */
27
+ export declare const deploymentFlags: {
28
+ deployment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
+ };
8
31
  export declare const jsonFlags: {
9
32
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
33
  };
package/dist/lib/flags.js CHANGED
@@ -1,7 +1,23 @@
1
1
  import { Flags } from '@oclif/core';
2
+ const tagAsFilterDescription = 'Workflow environment tag (e.g. prod, test) — an optional query filter, and the resource disambiguator when the config spans several.';
2
3
  export const tagFlags = {
4
+ tag: Flags.string({ description: tagAsFilterDescription }),
5
+ };
6
+ export const instanceFlags = {
7
+ deployment: Flags.string({
8
+ description: 'Deployment name — read the instance from the resource that deployment targets; the tag partition still comes from the loaded instance.',
9
+ exclusive: ['tag'],
10
+ }),
11
+ tag: Flags.string({ description: tagAsFilterDescription, exclusive: ['deployment'] }),
12
+ };
13
+ export const deploymentFlags = {
14
+ deployment: Flags.string({
15
+ description: 'Deployment name — the unique identity of one deployment in the config.',
16
+ exclusive: ['tag'],
17
+ }),
3
18
  tag: Flags.string({
4
- description: 'Workflow environment tag (e.g. prod, test) — the deployment to target for writes; an optional filter for reads.',
19
+ description: 'Workflow environment tag (e.g. prod, test) — selects the deployment to act on while the tag names exactly one; pass --deployment when it spans several.',
20
+ exclusive: ['deployment'],
5
21
  }),
6
22
  };
7
23
  export const jsonFlags = {
@@ -1,4 +1,4 @@
1
- import { type WorkflowClient, type WorkflowResource } from '@sanity/workflow-engine';
1
+ import { type WorkflowClient, type WorkflowDeployment, type WorkflowResource } from '@sanity/workflow-engine';
2
2
  /** A resource a nuke may delete from, with the client that reads/writes it and
3
3
  * whether it is the engine's own resource — the only one that holds instances
4
4
  * and definitions (guards can co-locate with subjects in any resource). */
@@ -32,6 +32,20 @@ export interface PlanCounts {
32
32
  datasets: number;
33
33
  total: number;
34
34
  }
35
+ /** Every resource a deployment's nuke sweeps: its own workflow resource (which
36
+ * holds instances + definitions + guards) plus each alias-bound resource
37
+ * (guards co-locate with the subjects they lock). */
38
+ export declare function sweptResources(deployment: WorkflowDeployment): WorkflowResource[];
39
+ /**
40
+ * Fail before any nuke work when another same-tag deployment sweeps a resource
41
+ * the selected one does. Guard document ids embed only the tag
42
+ * (`temp.system.guard.<tag>.wf-instance.*`), so within a shared resource the
43
+ * sweep cannot tell such deployments' guards apart — nuking one would delete
44
+ * the others' live locks, invisibly to the plan. Config validation already
45
+ * forbids sharing tag + workflow resource, so an overlap can only arrive
46
+ * through alias bindings.
47
+ */
48
+ export declare function refuseOverlappingNuke(deployments: WorkflowDeployment[], selected: WorkflowDeployment): void;
35
49
  /**
36
50
  * Resolve what a tag-scoped nuke would delete, per resource — the dry-run plan
37
51
  * printed before any deletion. Instances and definitions are read only from the
package/dist/lib/nuke.js CHANGED
@@ -1,8 +1,29 @@
1
1
  import { styleText } from 'node:util';
2
- import { GUARD_DOC_TYPE, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, tagScopeFilter, } from '@sanity/workflow-engine';
2
+ import { GUARD_DOC_TYPE, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, resourceGdr, tagScopeFilter, } from '@sanity/workflow-engine';
3
+ import { fail } from "./fail.js";
4
+ import { deploymentLabel } from "./select-deployment.js";
3
5
  import { formatTable, resourceLabel, sectionHeader } from "./ui.js";
4
6
  const CHUNK = 200;
5
7
  const REQUEST_TAG = 'nuke';
8
+ export function sweptResources(deployment) {
9
+ return [
10
+ deployment.workflowResource,
11
+ ...(deployment.resourceAliases ?? []).map((binding) => binding.resource),
12
+ ];
13
+ }
14
+ export function refuseOverlappingNuke(deployments, selected) {
15
+ const swept = new Set(sweptResources(selected).map(resourceGdr));
16
+ const clashing = deployments.filter((candidate) => candidate.name !== selected.name &&
17
+ candidate.tag === selected.tag &&
18
+ sweptResources(candidate).some((resource) => swept.has(resourceGdr(resource))));
19
+ if (clashing.length === 0) {
20
+ return;
21
+ }
22
+ fail(`Refusing to nuke "${deploymentLabel(selected)}" — its guards are indistinguishable ` +
23
+ `from ${clashing.map(deploymentLabel).join(', ')}'s in the shared resource(s).`, `Guard ids embed only the tag, and these deployments share tag "${selected.tag}" plus ` +
24
+ "a swept resource — the sweep would delete the other deployments' live guards. " +
25
+ 'Give them distinct tags first.');
26
+ }
6
27
  export async function resolveNukePlan(args) {
7
28
  const { tag, targets } = args;
8
29
  const resources = await Promise.all(targets.map((target) => resolveResourcePlan({ tag, target })));
@@ -4,7 +4,8 @@
4
4
  * `TERM` not dumb, no `CI` — so its `NonInteractiveError` can never escape a
5
5
  * caller that checked here first), adding stderr-TTY because this package's
6
6
  * prompts render on stderr: a redirected stderr must refuse rather than block
7
- * on stdin with the question swallowed. The one predicate every prompt site
8
- * (the deploy consent prompt, nuke's confirmation) shares.
7
+ * on stdin with the question swallowed. It classifies whether definition
8
+ * sharing can show its full first-run disclosure and guards interactive
9
+ * prompts such as nuke's confirmation.
9
10
  */
10
11
  export declare function canPromptOnStderr(): boolean;
@@ -1,31 +1,58 @@
1
1
  import { type DeployTarget, type WorkflowConfig, type WorkflowDeployment } from '@sanity/workflow-engine';
2
+ type ChooseDeploymentName = (deployments: WorkflowDeployment[]) => Promise<string>;
3
+ interface DeploymentSelectionOptions {
4
+ name: string | undefined;
5
+ tag: string | undefined;
6
+ allTags: boolean;
7
+ interactive?: boolean;
8
+ chooseName?: ChooseDeploymentName;
9
+ }
10
+ /** The one identity format for a deployment in output — banners, failure
11
+ * summaries, and "available deployments" listings all render it the same
12
+ * way. */
13
+ export declare function deploymentLabel({ name, tag }: Pick<WorkflowDeployment, 'name' | 'tag'>): string;
2
14
  /**
3
- * Pick the deployment for the requested `--tag`. Tags are unique across a
4
- * config (enforced by defineWorkflowConfig), so a tag names at most one
5
- * deployment.
15
+ * Pick the deployment for the requested `--deployment` or `--tag`. Names are
16
+ * unique across a config (enforced by defineWorkflowConfig), so a name resolves
17
+ * exactly one deployment. Tags are repeatable — a tag resolves only while it
18
+ * names exactly one deployment; when it spans several, the failure lists them
19
+ * so `--deployment` can disambiguate.
6
20
  *
7
- * With no tag: fall back to the sole deployment when there's exactly one
8
- * (the common single-environment case), otherwise fail asking for `--tag` —
9
- * a multi-deployment config is ambiguous without it. `orAlternative` extends
10
- * that ambiguity message for callers with another way out (deploy's `--all-tags`).
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
26
+ * `--all-tags`).
11
27
  */
12
- export declare function selectDeployment(config: WorkflowConfig, { tag, orAlternative }: {
28
+ export declare function selectDeployment(config: WorkflowConfig, { name, tag, orAlternative, }: {
29
+ name: string | undefined;
13
30
  tag: string | undefined;
14
31
  orAlternative?: string;
15
32
  }): WorkflowDeployment;
16
33
  /**
17
- * The deployments a `deploy` run acts on: every one with `--all-tags`,
18
- * otherwise a single target via {@link selectDeployment} and its failure
19
- * modes. Deploying everything is an explicit opt-in never a default so an
20
- * ambiguous bare `deploy` can't fan a write out across environments.
34
+ * Every deployment carrying `tag`, failing when the tag matches none — the
35
+ * shared resolution for the callers that accept a whole tag group (deploy's
36
+ * tag-as-environment run, the read paths' resource narrowing).
21
37
  */
22
- export declare function selectDeployments(config: WorkflowConfig, { tag, allTags }: {
23
- tag: string | undefined;
24
- allTags: boolean;
25
- }): WorkflowDeployment[];
38
+ export declare function deploymentsForTag(config: WorkflowConfig, tag: string): WorkflowDeployment[];
39
+ /**
40
+ * The deployments a `deploy` run acts on: every one with `--all-tags`, one
41
+ * definite deployment via `--deployment`, every deployment carrying the tag with
42
+ * `--tag` (a tag is an environment group), otherwise the sole deployment. An
43
+ * ambiguous bare interactive run asks which deployment; a run that cannot
44
+ * prompt keeps the explicit flag guidance. Deploying everything is an
45
+ * explicit opt-in — never a default.
46
+ */
47
+ export declare function selectDeployments(config: WorkflowConfig, { name, tag, allTags, interactive, chooseName }: DeploymentSelectionOptions): Promise<WorkflowDeployment[]>;
48
+ /** The "Available deployments: name (tag), …" hint every ambiguity failure
49
+ * prints — one vocabulary across the write and instance-keyed paths, so a
50
+ * reader always sees both the `--deployment` names and their `--tag`s. */
51
+ export declare function availableDeployments(config: WorkflowConfig, deployments?: WorkflowDeployment[]): string;
26
52
  /**
27
53
  * Project a deployment into the engine's {@link DeployTarget} — the shape the
28
54
  * deploy/diff verbs (`deployDefinitions`, `computeDiffEntries`, `diffEntry`)
29
55
  * consume — expanding its handle bindings into the alias map in the same step.
30
56
  */
31
57
  export declare function deploymentToTarget(deployment: WorkflowDeployment): DeployTarget;
58
+ export {};
@@ -1,35 +1,86 @@
1
+ import { select } from '@sanity/cli-core/ux';
1
2
  import { resourceAliasesToMap, } from '@sanity/workflow-engine';
2
3
  import { fail } from "./fail.js";
3
- export function selectDeployment(config, { tag, orAlternative = '' }) {
4
- if (tag === undefined) {
5
- if (config.deployments.length > 1) {
6
- fail(`Multiple deployments configured — pass --tag${orAlternative}.`, availableTags(config));
4
+ import { canPromptOnStderr } from "./prompt.js";
5
+ export function deploymentLabel({ name, tag }) {
6
+ return `${name} (${tag})`;
7
+ }
8
+ export function selectDeployment(config, { name, tag, orAlternative = '', }) {
9
+ if (name !== undefined) {
10
+ const deployment = config.deployments.find((candidate) => candidate.name === name);
11
+ if (deployment === undefined) {
12
+ fail(`No deployment named "${name}".`, availableDeployments(config));
7
13
  }
8
- const [sole] = config.deployments;
9
- if (sole === undefined) {
10
- fail('No deployments configured.');
14
+ return deployment;
15
+ }
16
+ if (tag !== undefined) {
17
+ const matches = deploymentsForTag(config, tag);
18
+ const [sole, ...rest] = matches;
19
+ if (sole === undefined || rest.length > 0) {
20
+ fail(`Multiple deployments tagged "${tag}" — pass --deployment to choose one.`, availableDeployments(config, matches));
11
21
  }
12
22
  return sole;
13
23
  }
14
- const deployment = config.deployments.find((candidate) => candidate.tag === tag);
15
- if (deployment === undefined) {
16
- fail(`No deployment for tag "${tag}".`, availableTags(config));
24
+ if (config.deployments.length > 1) {
25
+ fail(`Multiple deployments configured — pass --deployment or --tag${orAlternative}.`, availableDeployments(config));
26
+ }
27
+ const [sole] = config.deployments;
28
+ if (sole === undefined) {
29
+ fail('No deployments configured.');
30
+ }
31
+ return sole;
32
+ }
33
+ export function deploymentsForTag(config, tag) {
34
+ const matches = config.deployments.filter((candidate) => candidate.tag === tag);
35
+ if (matches.length === 0) {
36
+ fail(`No deployment for tag "${tag}".`, availableDeployments(config));
17
37
  }
18
- return deployment;
38
+ return matches;
19
39
  }
20
- export function selectDeployments(config, { tag, allTags }) {
40
+ export async function selectDeployments(config, { name, tag, allTags, interactive, chooseName = chooseDeploymentName }) {
21
41
  if (allTags) {
22
42
  return config.deployments;
23
43
  }
44
+ if (tag !== undefined) {
45
+ return deploymentsForTag(config, tag);
46
+ }
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
+ }
24
60
  return [
25
- selectDeployment(config, { tag, orAlternative: ', or --all-tags to deploy every deployment' }),
61
+ selectDeployment(config, {
62
+ name,
63
+ tag,
64
+ orAlternative: ', or --all-tags to deploy every deployment',
65
+ }),
26
66
  ];
27
67
  }
28
- function availableTags(config) {
29
- return `Available tags: ${config.deployments.map((candidate) => candidate.tag).join(', ')}`;
68
+ async function chooseDeploymentName(deployments) {
69
+ return select({
70
+ message: 'Select a deployment',
71
+ choices: deployments.map(({ name, tag }) => ({
72
+ name,
73
+ value: name,
74
+ description: `tag: ${tag}`,
75
+ })),
76
+ }, { output: process.stderr });
77
+ }
78
+ export function availableDeployments(config, deployments = config.deployments) {
79
+ return `Available deployments: ${deployments.map(deploymentLabel).join(', ')}`;
30
80
  }
31
81
  export function deploymentToTarget(deployment) {
32
82
  return {
83
+ expectedMinReaderModel: deployment.expectedMinReaderModel,
33
84
  tag: deployment.tag,
34
85
  workflowResource: deployment.workflowResource,
35
86
  resourceAliases: resourceAliasesToMap(deployment.resourceAliases),
@@ -10,12 +10,11 @@
10
10
  * Consent model: opt-OUT. A deploy that creates new definition versions
11
11
  * donates by default. The choice is skipped by an explicit flag:
12
12
  * `--share-defs` (donate, no prompt) or `--no-share-defs` (don't). Without a
13
- * flag, an interactive terminal takes a ONE-TIME acknowledgement (persisted
14
- * in the shared Sanity user config, so it is asked once ever) before the
15
- * first donation notice must precede the send, since we cannot disclose
16
- * taking content after taking it. Unattended runs (CI / `DO_NOT_TRACK` / a
17
- * non-TTY pipe) can't prompt, so they donate nothing — the standard Sanity-CLI
18
- * "silent in CI / `DO_NOT_TRACK`" rule; opt in per run with `--share-defs`.
13
+ * flag, the CLI shares. An interactive terminal gets a ONE-TIME notice
14
+ * (persisted in the shared Sanity user config) before the first donation;
15
+ * every later flagless deploy, and every unattended deploy, prints a concise
16
+ * reminder. CI, `DO_NOT_TRACK`, and non-TTY execution do not change the
17
+ * content-sharing default. Only `--no-share-defs` opts out.
19
18
  */
20
19
  import { type DeployDefinitionResult, type DeployedDefinition, type WorkflowResource } from '@sanity/workflow-engine';
21
20
  import type { UserConfigStore } from './telemetry-setup.ts';
@@ -50,22 +49,20 @@ export interface ShareCandidate {
50
49
  * routed project-agnostically through the API gateway). */
51
50
  export declare const SHARE_ENDPOINT_URI = "/workflow/definition-feedback";
52
51
  /**
53
- * The first-run consent prompt — a product contract, pinned by test. It must
52
+ * The first-run disclosure — a product contract, pinned by test. It must
54
53
  * name the recipient (Sanity), state that the document ships VERBATIM with its
55
54
  * deployment coordinates and is kept, name what is never shared, and disclose
56
- * that the answer is remembered machine-wide (not just for this deploy) with
57
- * the per-run override. Shown once (persisted) before the first donation on an
58
- * interactive terminal; the answer decides and is remembered.
55
+ * that the notice is remembered machine-wide and name the per-run opt-out.
56
+ * This is a notice, not a choice: sharing is the default unless the invocation
57
+ * supplies `--no-share-defs`.
59
58
  */
60
- export declare const SHARE_CONSENT_PROMPT: string;
59
+ export declare const SHARE_FIRST_RUN_NOTICE: string;
61
60
  /**
62
- * The one-line reminders printed on every flagless interactive deploy after the
63
- * first product contracts, pinned by test. They keep an ongoing donation (or
64
- * a remembered decline) visible every run, never silent, and name the per-run
65
- * override. `_ON` shows when the remembered answer shares, `_OFF` when it does not.
61
+ * The status block printed on every flagless deploy that does not show the
62
+ * first-run disclosure. It keeps the default donation obvious in terminals and
63
+ * unattended logs and names both explicit controls.
66
64
  */
67
- export declare const SHARE_REMINDER_ON: string;
68
- export declare const SHARE_REMINDER_OFF: string;
65
+ export declare const SHARE_REMINDER: string;
69
66
  /**
70
67
  * The content-free telemetry marker for one shared definition — projected
71
68
  * from the engine's own deploy-event derivation ({@link definitionDeployedData})
@@ -80,9 +77,9 @@ export declare function definitionShareMarker(args: {
80
77
  }): WorkflowDefinitionSharedData;
81
78
  /**
82
79
  * Whether the invocation's telemetry should send despite the environment
83
- * denial (CI / `DO_NOT_TRACK`). Only an explicit `--share-defs` that actually
84
- * donates forces it a command-line arg beats env vars, and a `--share-defs`
85
- * donation must never be recorded-less. Account-level consent is never
80
+ * denial (CI / `DO_NOT_TRACK`). Every flagless or explicitly enabled deploy
81
+ * that can create versions forces it, because definition donation defaults on
82
+ * in those environments too. Account-level consent is never
86
83
  * overridden (the intake still reads `/intake/telemetry-status`).
87
84
  *
88
85
  * Computed from raw argv because the prerun hook builds the store before oclif
@@ -98,12 +95,12 @@ export declare function shouldForceShareTelemetry(args: {
98
95
  argv: string[];
99
96
  }): boolean;
100
97
  /**
101
- * The whole post-deploy sharing flow. Resolves the opt-out decision (explicit
102
- * flag, one-time acknowledgement, or standing policy), donates when it lands
103
- * on share, and always records the content-free per-invocation decision so
104
- * adoption dashboards can measure opt-out rate. No newly created versions
105
- * (unchanged re-deploys, dry runs, nothing succeeded) means no decision, no
106
- * prompt, and no POST.
98
+ * The whole post-deploy sharing flow. An explicit flag decides directly;
99
+ * otherwise sharing defaults on after emitting either the one-time disclosure
100
+ * or recurring reminder. It always records the content-free per-invocation
101
+ * decision so adoption dashboards can measure opt-out rate. No newly created
102
+ * versions (unchanged re-deploys, dry runs, nothing succeeded) means no
103
+ * decision, notice, or POST.
107
104
  *
108
105
  * The optional args exist for the flow's dependencies, mirroring
109
106
  * `setupCliTelemetry` — omitted, the real process surfaces apply.
@@ -113,8 +110,6 @@ export declare function shareDefinitionsAfterDeploy(args: {
113
110
  candidates: ShareCandidate[];
114
111
  warn: (message: string) => void;
115
112
  interactive?: boolean;
116
- envDenied?: boolean;
117
113
  writeStderr?: (message: string) => void;
118
114
  userConfig?: UserConfigStore;
119
- confirmShare?: () => Promise<boolean>;
120
115
  }): Promise<void>;