@sanity/workflow-cli 0.13.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +40 -27
  3. package/dist/commands/editorial-workflows/abort.d.ts +1 -0
  4. package/dist/commands/editorial-workflows/abort.js +2 -2
  5. package/dist/commands/editorial-workflows/definition/delete.d.ts +1 -0
  6. package/dist/commands/editorial-workflows/definition/delete.js +2 -2
  7. package/dist/commands/editorial-workflows/definition/diff.d.ts +1 -0
  8. package/dist/commands/editorial-workflows/definition/diff.js +2 -2
  9. package/dist/commands/editorial-workflows/deploy.d.ts +2 -1
  10. package/dist/commands/editorial-workflows/deploy.js +15 -9
  11. package/dist/commands/editorial-workflows/diagnose.d.ts +1 -0
  12. package/dist/commands/editorial-workflows/diagnose.js +2 -2
  13. package/dist/commands/editorial-workflows/fire-action.d.ts +1 -0
  14. package/dist/commands/editorial-workflows/fire-action.js +2 -2
  15. package/dist/commands/editorial-workflows/nuke.d.ts +2 -1
  16. package/dist/commands/editorial-workflows/nuke.js +13 -9
  17. package/dist/commands/editorial-workflows/set-stage.d.ts +1 -0
  18. package/dist/commands/editorial-workflows/set-stage.js +2 -2
  19. package/dist/commands/editorial-workflows/start.d.ts +1 -0
  20. package/dist/commands/editorial-workflows/start.js +2 -2
  21. package/dist/lib/context.d.ts +36 -13
  22. package/dist/lib/context.js +27 -6
  23. package/dist/lib/fail.js +1 -1
  24. package/dist/lib/flags.d.ts +27 -5
  25. package/dist/lib/flags.js +17 -1
  26. package/dist/lib/nuke.d.ts +15 -14
  27. package/dist/lib/nuke.js +27 -8
  28. package/dist/lib/prompt.d.ts +14 -0
  29. package/dist/lib/prompt.js +4 -0
  30. package/dist/lib/select-deployment.d.ts +41 -16
  31. package/dist/lib/select-deployment.js +55 -38
  32. package/oclif.manifest.json +131 -19
  33. package/package.json +8 -5
@@ -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 { 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, { 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) {
@@ -20,7 +21,7 @@ export function dedupeResources(resources) {
20
21
  }
21
22
  export function resolveReadResources(config, tag) {
22
23
  if (tag !== undefined) {
23
- return [selectDeployment(config, { tag }).workflowResource];
24
+ return dedupeResources(deploymentsForTag(config, tag).map((deployment) => deployment.workflowResource));
24
25
  }
25
26
  const resources = dedupeResources(config.deployments.map((d) => d.workflowResource));
26
27
  if (resources.length === 0) {
@@ -34,16 +35,36 @@ export function resolveReadResource(config, tag) {
34
35
  if (resources.length === 1 && sole !== undefined) {
35
36
  return sole;
36
37
  }
37
- const tags = config.deployments.map((d) => d.tag).join(', ');
38
- fail('Config spans multiple resources — pass --tag to choose one.', `Available tags: ${tags}`);
38
+ if (tag === undefined) {
39
+ fail('Config spans multiple resources — pass --deployment or --tag to choose one.', availableDeployments(config));
40
+ }
41
+ const carriers = config.deployments.filter((d) => d.tag === tag);
42
+ fail(`Tag "${tag}" spans multiple resources — pass --deployment to choose one.`, availableDeployments(config, carriers));
39
43
  }
40
44
  export async function resolveInstanceContext(flags, instanceId) {
41
45
  const config = await loadWorkflowConfig();
42
- const workflowResource = resolveReadResource(config, flags.tag);
46
+ const workflowResource = await resolveInstanceResource(config, flags);
43
47
  const client = clientFor(workflowResource, await resolveTokenOrFail());
44
48
  const instance = await loadInstanceOrFail(client, instanceId);
45
49
  return { client, scope: { tag: instance.tag, workflowResource } };
46
50
  }
51
+ export async function resolveInstanceResource(config, { deployment, tag, interactive, chooseDeployment, }) {
52
+ if (deployment !== undefined) {
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;
65
+ }
66
+ return resolveReadResource(config, tag);
67
+ }
47
68
  export async function loadInstanceOrFail(client, instanceId) {
48
69
  const instance = await client.getDocument(instanceId, {
49
70
  tag: 'instance.load',
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);
@@ -1,11 +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. A sole
3
- * deployment needs no tag, and an interactive `deploy` asks when several are
4
- * configured. For read commands it's an optional query filter and the
5
- * 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}. */
6
7
  export declare const tagFlags: {
7
8
  tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
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
+ };
9
31
  export declare const jsonFlags: {
10
32
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
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
@@ -45,19 +59,6 @@ export declare function resolveNukePlan(args: {
45
59
  tag: string;
46
60
  targets: NukeTarget[];
47
61
  }): Promise<NukePlan>;
48
- /**
49
- * Does a guard doc belong to `tag`? A guard's `sourceInstanceId` is the id of
50
- * the instance that registered it (`<tag>.wf-instance.<random>`), and its own
51
- * `_id` embeds that same instance id
52
- * (`temp.system.guard.<tag>.wf-instance.<random>.<name>`). Matching on either —
53
- * with the trailing `.` so `prod` never matches `prod-eu` — catches guards
54
- * whose `sourceInstanceId` was never written as well as guards whose instance
55
- * is already gone.
56
- */
57
- export declare function guardMatchesTag(guard: {
58
- _id: string;
59
- sourceInstanceId?: string;
60
- }, tag: string): boolean;
61
62
  /** The `project.dataset` labels of resources the plan actually deletes from —
62
63
  * the exact set the operator must type back to confirm. A resource with
63
64
  * nothing to delete is not "involved" and is omitted. */
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 })));
@@ -31,14 +52,12 @@ function engineDocIds(args) {
31
52
  return args.client.fetch(`*[_type == $type && ${tagScopeFilter()}]._id`, { type: args.type, tag: args.tag }, { perspective: 'raw', tag: REQUEST_TAG });
32
53
  }
33
54
  async function tagGuardIds(args) {
34
- const guards = await args.client.fetch(`*[_type == $type]{_id, sourceInstanceId}`, { type: GUARD_DOC_TYPE }, { perspective: 'raw', tag: REQUEST_TAG });
35
- return guards.filter((guard) => guardMatchesTag(guard, args.tag)).map((guard) => guard._id);
36
- }
37
- export function guardMatchesTag(guard, tag) {
38
- const instancePrefix = `${tag}.wf-instance.`;
55
+ const instancePrefix = `${args.tag}.wf-instance.`;
39
56
  const guardIdPrefix = `${GUARD_DOC_TYPE}.${instancePrefix}`;
40
- return ((guard.sourceInstanceId?.startsWith(instancePrefix) ?? false) ||
41
- 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 });
42
61
  }
43
62
  export function involvedTargets(plan) {
44
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,32 +1,57 @@
1
1
  import { type DeployTarget, type WorkflowConfig, type WorkflowDeployment } from '@sanity/workflow-engine';
2
- type ChooseDeploymentTag = (deployments: WorkflowDeployment[]) => Promise<string>;
2
+ export type ChooseDeploymentName = (deployments: WorkflowDeployment[]) => Promise<string>;
3
3
  interface DeploymentSelectionOptions {
4
+ name: string | undefined;
4
5
  tag: string | undefined;
5
6
  allTags: boolean;
6
7
  interactive?: boolean;
7
- chooseTag?: ChooseDeploymentTag;
8
+ chooseName?: ChooseDeploymentName;
8
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;
9
14
  /**
10
- * Pick the deployment for the requested `--tag`. Tags are unique across a
11
- * config (enforced by defineWorkflowConfig), so a tag names at most one
12
- * 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.
13
20
  *
14
- * With no tag: fall back to the sole deployment when there's exactly one
15
- * (the common single-environment case), otherwise fail asking for `--tag` —
16
- * a multi-deployment config is ambiguous without it. `orAlternative` extends
17
- * 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). 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
27
+ * `--all-tags`).
18
28
  */
19
- export declare function selectDeployment(config: WorkflowConfig, { tag, orAlternative }: {
29
+ export declare function selectDeployment(config: WorkflowConfig, { name, tag, orAlternative, interactive, chooseName, }: {
30
+ name: string | undefined;
20
31
  tag: string | undefined;
21
32
  orAlternative?: string;
22
- }): WorkflowDeployment;
33
+ interactive?: boolean | undefined;
34
+ chooseName?: ChooseDeploymentName | undefined;
35
+ }): Promise<WorkflowDeployment>;
23
36
  /**
24
- * The deployments a `deploy` run acts on: every one with `--all-tags`,
25
- * otherwise a single target via {@link selectDeployment}. An ambiguous bare
26
- * interactive run asks for a tag; a run that cannot prompt keeps the explicit
27
- * flag guidance. Deploying everything is an explicit opt-in — never a default.
37
+ * Every deployment carrying `tag`, failing when the tag matches none — the
38
+ * shared resolution for the callers that accept a whole tag group (deploy's
39
+ * tag-as-environment run, the read paths' resource narrowing).
28
40
  */
29
- export declare function selectDeployments(config: WorkflowConfig, { tag, allTags, interactive, chooseTag }: DeploymentSelectionOptions): Promise<WorkflowDeployment[]>;
41
+ export declare function deploymentsForTag(config: WorkflowConfig, tag: string): WorkflowDeployment[];
42
+ /**
43
+ * The deployments a `deploy` run acts on: every one with `--all-tags`, one
44
+ * definite deployment via `--deployment`, every deployment carrying the tag with
45
+ * `--tag` (a tag is an environment group), otherwise the sole deployment. An
46
+ * ambiguous bare interactive run asks which deployment; a run that cannot
47
+ * prompt keeps the explicit flag guidance. Deploying everything is an
48
+ * explicit opt-in — never a default.
49
+ */
50
+ export declare function selectDeployments(config: WorkflowConfig, { name, tag, allTags, interactive, chooseName }: DeploymentSelectionOptions): Promise<WorkflowDeployment[]>;
51
+ /** The "Available deployments: name (tag), …" hint every ambiguity failure
52
+ * prints — one vocabulary across the write and instance-keyed paths, so a
53
+ * reader always sees both the `--deployment` names and their `--tag`s. */
54
+ export declare function availableDeployments(config: WorkflowConfig, deployments?: WorkflowDeployment[]): string;
30
55
  /**
31
56
  * Project a deployment into the engine's {@link DeployTarget} — the shape the
32
57
  * deploy/diff verbs (`deployDefinitions`, `computeDiffEntries`, `diffEntry`)
@@ -1,56 +1,73 @@
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";
5
- export function selectDeployment(config, { tag, orAlternative = '' }) {
6
- if (tag === undefined) {
7
- if (config.deployments.length > 1) {
8
- fail(`Multiple deployments configured pass --tag${orAlternative}.`, availableTags(config));
3
+ import { canPromptOnStderr, selectOnStderr } from "./prompt.js";
4
+ export function deploymentLabel({ name, tag }) {
5
+ return `${name} (${tag})`;
6
+ }
7
+ export async function selectDeployment(config, { name, tag, orAlternative = '', interactive, chooseName = chooseDeploymentName, }) {
8
+ if (name !== undefined) {
9
+ const deployment = config.deployments.find((candidate) => candidate.name === name);
10
+ if (deployment === undefined) {
11
+ fail(`No deployment named "${name}".`, availableDeployments(config));
9
12
  }
10
- const [sole] = config.deployments;
11
- if (sole === undefined) {
12
- fail('No deployments configured.');
13
+ return deployment;
14
+ }
15
+ if (tag !== undefined) {
16
+ const matches = deploymentsForTag(config, tag);
17
+ const [sole, ...rest] = matches;
18
+ if (sole === undefined || rest.length > 0) {
19
+ fail(`Multiple deployments tagged "${tag}" — pass --deployment to choose one.`, availableDeployments(config, matches));
13
20
  }
14
21
  return sole;
15
22
  }
16
- const deployment = config.deployments.find((candidate) => candidate.tag === tag);
17
- if (deployment === undefined) {
18
- fail(`No deployment for tag "${tag}".`, availableTags(config));
23
+ return selectDefaultDeployment(config, { orAlternative, interactive, chooseName });
24
+ }
25
+ async function selectDefaultDeployment(config, { orAlternative, interactive, chooseName, }) {
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
+ }
31
+ fail(`Multiple deployments configured — pass --deployment or --tag${orAlternative}.`, availableDeployments(config));
32
+ }
33
+ const [sole] = config.deployments;
34
+ if (sole === undefined) {
35
+ fail('No deployments configured.');
19
36
  }
20
- return deployment;
37
+ return sole;
21
38
  }
22
- export async function selectDeployments(config, { tag, allTags, interactive, chooseTag = chooseDeploymentTag }) {
39
+ export function deploymentsForTag(config, tag) {
40
+ const matches = config.deployments.filter((candidate) => candidate.tag === tag);
41
+ if (matches.length === 0) {
42
+ fail(`No deployment for tag "${tag}".`, availableDeployments(config));
43
+ }
44
+ return matches;
45
+ }
46
+ export async function selectDeployments(config, { name, tag, allTags, interactive, chooseName }) {
23
47
  if (allTags) {
24
48
  return config.deployments;
25
49
  }
26
- if (tag === undefined && config.deployments.length > 1) {
27
- if (!(interactive ?? canPromptOnStderr())) {
28
- return [
29
- selectDeployment(config, {
30
- tag,
31
- orAlternative: ', or --all-tags to deploy every deployment',
32
- }),
33
- ];
34
- }
35
- const selectedTag = await chooseTag(config.deployments);
36
- return [selectDeployment(config, { tag: selectedTag })];
50
+ if (tag !== undefined) {
51
+ return deploymentsForTag(config, tag);
37
52
  }
38
53
  return [
39
- selectDeployment(config, { tag, orAlternative: ', or --all-tags to deploy every deployment' }),
54
+ await selectDeployment(config, {
55
+ name,
56
+ tag,
57
+ orAlternative: ', or --all-tags to deploy every deployment',
58
+ interactive,
59
+ chooseName,
60
+ }),
40
61
  ];
41
62
  }
42
- async function chooseDeploymentTag(deployments) {
43
- return select({
44
- message: 'Select a deployment tag',
45
- choices: deployments.map(({ name, tag }) => ({
46
- name: tag,
47
- value: tag,
48
- description: name,
49
- })),
50
- }, { output: process.stderr });
51
- }
52
- function availableTags(config) {
53
- return `Available tags: ${config.deployments.map((candidate) => candidate.tag).join(', ')}`;
63
+ async function chooseDeploymentName(deployments) {
64
+ return selectOnStderr({
65
+ message: 'Select a deployment',
66
+ choices: deployments.map(({ name, tag }) => ({ name, value: name, description: `tag: ${tag}` })),
67
+ });
68
+ }
69
+ export function availableDeployments(config, deployments = config.deployments) {
70
+ return `Available deployments: ${deployments.map(deploymentLabel).join(', ')}`;
54
71
  }
55
72
  export function deploymentToTarget(deployment) {
56
73
  return {