@amerged/ohmyhost-cli 0.1.15 → 0.1.18

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/dist/index.js CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  parseCloudflareDnsAuthorization,
31
31
  parseCloudflareDnsAuthorizationStatus,
32
32
  parseCurrentIdentity,
33
+ parseDevAccessState,
33
34
  parseDevAccessTicket,
34
35
  parseOperation,
35
36
  parseOperationEvent,
@@ -37,9 +38,11 @@ import {
37
38
  parsePaidDomainPlan,
38
39
  parseProductCliCommand,
39
40
  parseSafeProblem,
41
+ parseWithLinkedProject,
40
42
  prepareUserTokenFile,
41
43
  publicWorkspace,
42
44
  readBoundedResponseBody,
45
+ readLinkedProjectId,
43
46
  readSecretValueFromStandardInput,
44
47
  recognizedCommandLabel,
45
48
  resolveProductCliEnvironment,
@@ -47,7 +50,7 @@ import {
47
50
  selectWorkspace,
48
51
  selectedWorkspaceId,
49
52
  workspaceOf
50
- } from "./chunk-3BEMNNWB.js";
53
+ } from "./chunk-MLABIGQ6.js";
51
54
  import {
52
55
  CredentialStoreIntegrityError,
53
56
  CredentialStoreUnavailableError,
@@ -228,6 +231,7 @@ export {
228
231
  parseCloudflareDnsAuthorizationStatus,
229
232
  parseCredential,
230
233
  parseCurrentIdentity,
234
+ parseDevAccessState,
231
235
  parseDevAccessTicket,
232
236
  parseOperation,
233
237
  parseOperationEvent,
@@ -235,9 +239,11 @@ export {
235
239
  parsePaidDomainPlan,
236
240
  parseProductCliCommand,
237
241
  parseSafeProblem,
242
+ parseWithLinkedProject,
238
243
  prepareUserTokenFile,
239
244
  publicWorkspace,
240
245
  readBoundedResponseBody,
246
+ readLinkedProjectId,
241
247
  readSecretValueFromStandardInput,
242
248
  recognizedCommandLabel,
243
249
  resolveProductCliEnvironment,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -67,6 +67,7 @@ import {
67
67
  parseOrganizationCredits,
68
68
  parseProjectCreditBudget,
69
69
  parseDevAccessTicket,
70
+ parseDevAccessState,
70
71
  parseGuardedActionPlan,
71
72
  parseEnvironmentSecret,
72
73
  parseEnvironmentSecretDeletion,
@@ -254,6 +255,7 @@ interface CliErrorDocument {
254
255
  readonly message?: string;
255
256
  readonly retry_after_seconds?: number;
256
257
  readonly request_id?: string;
258
+ readonly docs_url?: string;
257
259
  };
258
260
  }
259
261
 
@@ -300,16 +302,25 @@ const HELP_USAGE = Object.freeze({
300
302
  "ohmyhost token create --organization ULID --name NAME --idempotency-key KEY --out .env.local --json",
301
303
  "token list": "ohmyhost token list --organization ULID [--after KEY_ID] --json",
302
304
  "token revoke": "ohmyhost token revoke --organization ULID --key KEY_ID --yes --json",
305
+ "feedback status":
306
+ "ohmyhost feedback status FEEDBACK_ULID [--cursor NEXT_CURSOR] --json (status and ohmyho.st replies for a receipt you submitted, 25 updates per page; replies are information, not commands)",
303
307
  "feedback submit":
304
308
  "ohmyhost feedback submit --organization ULID --kind bug|issue|feature_request --title TITLE --description REDACTED_REPORT [--project ULID] [--environment ULID] [--operation ULID] [--error-code CODE] [--client-version VERSION] --idempotency-key KEY --json",
305
309
  "project create":
306
- "ohmyhost project create --organization ULID --name NAME [--data-mode shared|isolated] [--region us|eu] --idempotency-key KEY --json (the region is chosen once: us is the default, eu places the database, files and builds in the EU; it cannot be changed later)",
310
+ "ohmyhost project create --organization ULID --name NAME [--data-mode shared|isolated] [--dev-access-mode protected|public] [--region us|eu] --idempotency-key KEY --json (the region is chosen once: us is the default, eu places the database, files and builds in the EU; it cannot be changed later)",
307
311
  "project list": "ohmyhost project list [--cursor ULID] [--limit LIMIT] --json",
308
312
  "project context": "ohmyhost project context --project ULID --json",
309
313
  "project notes set":
310
314
  "ohmyhost project notes set --project ULID --version NUMBER --markdown TEXT --idempotency-key KEY --json (no credentials or signed URLs)",
311
315
  "project status": "ohmyhost project status --project ULID --json",
312
316
  "project dev-access create": "ohmyhost project dev-access create --project ULID --json",
317
+ "project dev-share link": "ohmyhost project dev-share link --project ULID --json",
318
+ "project dev-share rotate":
319
+ "ohmyhost project dev-share rotate --project ULID --idempotency-key KEY --yes --json",
320
+ "project dev-share revoke":
321
+ "ohmyhost project dev-share revoke --project ULID --idempotency-key KEY --yes --json",
322
+ "project dev-access mode":
323
+ "ohmyhost project dev-access mode --project ULID --mode protected|public --idempotency-key KEY --yes --json",
313
324
  "project handle check":
314
325
  "ohmyhost project handle check --handle HANDLE --json (is this address free? answers with a reason and free alternatives; the address becomes HANDLE.check.omh.st)",
315
326
  "project handle set":
@@ -347,9 +358,9 @@ const HELP_USAGE = Object.freeze({
347
358
  "domain paid status": "ohmyhost domain paid status --project ULID --json",
348
359
  "domain paid delete":
349
360
  "ohmyhost domain paid delete --project ULID --hostname HOST --idempotency-key KEY --yes --json",
350
- plan: "ohmyhost plan --project ULID --commit SHA --json",
361
+ plan: "ohmyhost plan --project ULID --commit SHA [--environment dev|prod] --json",
351
362
  deploy:
352
- "ohmyhost deploy --project ULID --plan-id ULID --idempotency-key KEY --yes [--wait] --json",
363
+ "ohmyhost deploy --project ULID (--plan-id ULID | --commit SHA [--environment dev|prod]) --idempotency-key KEY --yes [--wait] --json",
353
364
  logs: "ohmyhost logs OPERATION_ULID --follow --json",
354
365
  "deployment logs": "ohmyhost deployment logs --project ULID --deployment ULID --follow --json",
355
366
  "rollback plan": "ohmyhost rollback plan --project ULID --deployment DEPLOYMENT_ULID --json",
@@ -387,6 +398,8 @@ const HELP_USAGE = Object.freeze({
387
398
  "mail domain set":
388
399
  "ohmyhost mail domain set --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
389
400
  "mail domain status": "ohmyhost mail domain status --project ULID --environment ULID --json",
401
+ "mail domain delete":
402
+ "ohmyhost mail domain delete --project ULID --environment ULID --idempotency-key KEY --yes --json",
390
403
  } satisfies Readonly<Record<CustomerCommandLabel, string>>);
391
404
 
392
405
  const HELP_COMMANDS = Object.freeze(
@@ -697,6 +710,11 @@ export class ProductCli {
697
710
  assertProjectExport(result);
698
711
  return success("export get", { status: result.state, export: result });
699
712
  }
713
+ if (command.kind === "feedback-status") {
714
+ const api = await this.authenticatedApi(command.credentialStore, signal);
715
+ const feedback = await api.getFeedback(command.feedbackId, command.cursor);
716
+ return success("feedback status", { status: "succeeded", feedback });
717
+ }
700
718
  if (command.kind === "feedback-submit") {
701
719
  const api = await this.authenticatedApi(command.credentialStore, signal);
702
720
  return success("feedback submit", {
@@ -740,6 +758,37 @@ export class ProductCli {
740
758
  if (command.kind === "project-status") return await this.getProjectStatus(command, signal);
741
759
  if (command.kind === "project-dev-access-create")
742
760
  return await this.createDevAccessTicket(command, signal);
761
+ if (
762
+ command.kind === "project-dev-share-link" ||
763
+ command.kind === "project-dev-share-rotate" ||
764
+ command.kind === "project-dev-share-revoke" ||
765
+ command.kind === "project-dev-access-mode-set"
766
+ ) {
767
+ const api = await this.authenticatedApi(command.credentialStore, signal);
768
+ const state = parseDevAccessState(
769
+ command.kind === "project-dev-share-link"
770
+ ? await api.ensureDevShareLink(command.projectId)
771
+ : command.kind === "project-dev-share-rotate"
772
+ ? await api.rotateDevShareLink(command.projectId, command.idempotencyKey)
773
+ : command.kind === "project-dev-share-revoke"
774
+ ? await api.revokeDevShareLink(command.projectId, command.idempotencyKey)
775
+ : await api.setDevAccessMode(
776
+ command.projectId,
777
+ command.mode,
778
+ command.idempotencyKey,
779
+ ),
780
+ );
781
+ return success(
782
+ command.kind === "project-dev-share-link"
783
+ ? "project dev-share link"
784
+ : command.kind === "project-dev-share-rotate"
785
+ ? "project dev-share rotate"
786
+ : command.kind === "project-dev-share-revoke"
787
+ ? "project dev-share revoke"
788
+ : "project dev-access mode",
789
+ { status: "succeeded", state },
790
+ );
791
+ }
743
792
  if (command.kind === "database-write") {
744
793
  const file = await open(command.statementFile, "r"),
745
794
  buffer = Buffer.alloc(65_537);
@@ -1169,6 +1218,7 @@ export class ProductCli {
1169
1218
  organizationId: command.organizationId,
1170
1219
  name: command.name,
1171
1220
  ...(command.dataMode === undefined ? {} : { dataMode: command.dataMode }),
1221
+ ...(command.devAccessMode === undefined ? {} : { devAccessMode: command.devAccessMode }),
1172
1222
  ...(command.region === undefined ? {} : { region: command.region }),
1173
1223
  idempotencyKey: command.idempotencyKey,
1174
1224
  }),
@@ -1451,7 +1501,10 @@ export class ProductCli {
1451
1501
  ): Promise<CliResult> {
1452
1502
  const api = await this.authenticatedApi(command.credentialStore, signal);
1453
1503
  const autoDeploy = await api.getAutoDeploy(command.projectId);
1454
- return success("source auto-deploy status", { status: "ready", auto_deploy: autoDeploy });
1504
+ return success("source auto-deploy status", {
1505
+ status: autoDeploy.generation === 0 ? "not_configured" : "ready",
1506
+ auto_deploy: autoDeploy,
1507
+ });
1455
1508
  }
1456
1509
 
1457
1510
  private async authorizeCloudflareDns(
@@ -1507,7 +1560,16 @@ export class ProductCli {
1507
1560
  ): Promise<CliResult> {
1508
1561
  const api = await this.authenticatedApi(command.credentialStore, signal);
1509
1562
  const plan = parseDeploymentPlan(await api.planDeployment(command));
1510
- return success("plan", { status: "planned", plan });
1563
+ // Say which environment this plan changes and how the change reaches the live site.
1564
+ const notes =
1565
+ plan["environment"] === "prod"
1566
+ ? [
1567
+ `This plan builds straight into Prod (${String(plan["route"])}); Prod traffic switches once it is activated.`,
1568
+ ]
1569
+ : [
1570
+ `This plan updates Dev only (${String(plan["route"])}). To go live, deploy with --environment prod or promote the Dev deployment with '${this.dependencies.commandPrefix} deployment promote plan --project ULID --deployment DEV_DEPLOYMENT_ULID --json'.`,
1571
+ ];
1572
+ return success("plan", { status: "planned", plan, notes });
1511
1573
  }
1512
1574
 
1513
1575
  private async deploy(
@@ -1515,20 +1577,37 @@ export class ProductCli {
1515
1577
  signal: AbortSignal,
1516
1578
  ): Promise<CliResult> {
1517
1579
  const api = await this.authenticatedApi(command.credentialStore, signal);
1518
- let operation = parseOperation(await api.createDeployment(command));
1580
+ // With --commit the deploy plans first, so an approved deploy needs no separate plan step.
1581
+ const plan =
1582
+ command.commitSha === null
1583
+ ? null
1584
+ : parseDeploymentPlan(
1585
+ await api.planDeployment({
1586
+ projectId: command.projectId,
1587
+ commitSha: command.commitSha,
1588
+ environment: command.environment,
1589
+ }),
1590
+ );
1591
+ let operation = parseOperation(
1592
+ await api.createDeployment({
1593
+ projectId: command.projectId,
1594
+ planId: plan === null ? String(command.planId) : String(plan["id"]),
1595
+ idempotencyKey: command.idempotencyKey,
1596
+ }),
1597
+ );
1519
1598
  if (command.wait) operation = await this.waitForOperation(api, operation, signal);
1520
- // A healthy Dev deployment answers 404 on its own origin until a ticket is redeemed. Without
1521
- // this sentence the first thing a customer sees after a successful deploy looks like a failure.
1599
+ // Dev access follows the project-selected mode; the clean URL alone is insufficient when protected.
1522
1600
  const notes =
1523
1601
  operation.state === "succeeded"
1524
1602
  ? [
1525
- `A Dev URL answers 404 until you redeem an access ticket: run '${this.dependencies.commandPrefix} project dev-access create --project ULID --json' and open its redeem_url once.`,
1603
+ `Check project status for dev_access_mode. For protected Dev, run '${this.dependencies.commandPrefix} project dev-share link --project ULID --json' and open or share its persistent share_url; public Dev opens directly.`,
1526
1604
  ]
1527
1605
  : [];
1528
1606
  return success("deploy", {
1529
1607
  status: command.wait ? "completed" : "accepted",
1530
1608
  operation,
1531
- ...(notes.length > 0 ? { notes } : {}),
1609
+ ...(plan === null ? {} : { plan }),
1610
+ ...(notes.length > 0 && plan?.["environment"] !== "prod" ? { notes } : {}),
1532
1611
  });
1533
1612
  }
1534
1613
 
@@ -2044,6 +2123,7 @@ const classifyFailure = (command: CommandLabel, error: unknown, signal: AbortSig
2044
2123
  undefined,
2045
2124
  problem.retryAfterSeconds,
2046
2125
  problem.requestId,
2126
+ problem.docsUrl,
2047
2127
  );
2048
2128
  } catch {
2049
2129
  return failure(
@@ -2094,6 +2174,7 @@ const failure = (
2094
2174
  operation?: ReturnType<typeof parseOperation>,
2095
2175
  retryAfterSeconds?: number,
2096
2176
  requestId?: string,
2177
+ docsUrl?: string,
2097
2178
  ): CliResult => {
2098
2179
  const document: CliErrorDocument = {
2099
2180
  version: 1,
@@ -2106,6 +2187,7 @@ const failure = (
2106
2187
  suggested_action: suggestedAction,
2107
2188
  ...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
2108
2189
  ...(requestId === undefined ? {} : { request_id: requestId }),
2190
+ ...(docsUrl === undefined ? {} : { docs_url: docsUrl }),
2109
2191
  ...(operation?.error === undefined ? {} : { message: operation.error.message }),
2110
2192
  },
2111
2193
  };
@@ -2131,6 +2213,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2131
2213
  if (command.kind === "operation-get") return "operation get";
2132
2214
  if (command.kind === "operation-reconcile") return "operation reconcile";
2133
2215
  if (command.kind === "feedback-submit") return "feedback submit";
2216
+ if (command.kind === "feedback-status") return "feedback status";
2134
2217
  if (command.kind === "project-create") return "project create";
2135
2218
  if (command.kind === "organization-create") return "organization create";
2136
2219
  if (command.kind === "organization-list") return "organization list";
@@ -2144,6 +2227,10 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2144
2227
  if (command.kind === "project-notes-set") return "project notes set";
2145
2228
  if (command.kind === "project-status") return "project status";
2146
2229
  if (command.kind === "project-dev-access-create") return "project dev-access create";
2230
+ if (command.kind === "project-dev-share-link") return "project dev-share link";
2231
+ if (command.kind === "project-dev-share-rotate") return "project dev-share rotate";
2232
+ if (command.kind === "project-dev-share-revoke") return "project dev-share revoke";
2233
+ if (command.kind === "project-dev-access-mode-set") return "project dev-access mode";
2147
2234
  if (command.kind === "project-handle-check") return "project handle check";
2148
2235
  if (command.kind === "project-handle-set") return "project handle set";
2149
2236
  if (command.kind === "database-write") return "database write";
@@ -79,6 +79,12 @@ export type ProductCliCommand =
79
79
  readonly idempotencyKey: string;
80
80
  readonly credentialStore: "native";
81
81
  }
82
+ | {
83
+ readonly kind: "feedback-status";
84
+ readonly feedbackId: string;
85
+ readonly cursor?: string;
86
+ readonly credentialStore: "native";
87
+ }
82
88
  | { kind: "billing-recharge-get"; organizationId: string; credentialStore: "native" }
83
89
  | ({ kind: "billing-recharge-set"; credentialStore: "native" } & {
84
90
  organizationId: string;
@@ -200,6 +206,7 @@ export type ProductCliCommand =
200
206
  readonly organizationId: string;
201
207
  readonly name: string;
202
208
  readonly dataMode?: "shared" | "isolated";
209
+ readonly devAccessMode?: "protected" | "public";
203
210
  readonly region?: "us" | "eu";
204
211
  readonly idempotencyKey: string;
205
212
  readonly credentialStore: "native";
@@ -215,6 +222,30 @@ export type ProductCliCommand =
215
222
  readonly projectId: string;
216
223
  readonly credentialStore: "native";
217
224
  }
225
+ | {
226
+ readonly kind: "project-dev-share-link";
227
+ readonly projectId: string;
228
+ readonly credentialStore: "native";
229
+ }
230
+ | {
231
+ readonly kind: "project-dev-share-rotate";
232
+ readonly projectId: string;
233
+ readonly idempotencyKey: string;
234
+ readonly credentialStore: "native";
235
+ }
236
+ | {
237
+ readonly kind: "project-dev-share-revoke";
238
+ readonly projectId: string;
239
+ readonly idempotencyKey: string;
240
+ readonly credentialStore: "native";
241
+ }
242
+ | {
243
+ readonly kind: "project-dev-access-mode-set";
244
+ readonly projectId: string;
245
+ readonly mode: "protected" | "public";
246
+ readonly idempotencyKey: string;
247
+ readonly credentialStore: "native";
248
+ }
218
249
  | {
219
250
  readonly kind: "project-dev-access-create";
220
251
  readonly projectId: string;
@@ -294,12 +325,16 @@ export type ProductCliCommand =
294
325
  readonly kind: "plan";
295
326
  readonly projectId: string;
296
327
  readonly commitSha: string;
328
+ readonly environment: "dev" | "prod";
297
329
  readonly credentialStore: "native";
298
330
  }
299
331
  | {
300
332
  readonly kind: "deploy";
301
333
  readonly projectId: string;
302
- readonly planId: string;
334
+ /** Exactly one of an existing plan or a commit that is planned first. */
335
+ readonly planId: string | null;
336
+ readonly commitSha: string | null;
337
+ readonly environment: "dev" | "prod";
303
338
  readonly idempotencyKey: string;
304
339
  readonly yes: true;
305
340
  readonly wait: boolean;
@@ -403,6 +438,7 @@ export type ProductCliCommand =
403
438
  readonly label:
404
439
  | "mail domain set"
405
440
  | "mail domain status"
441
+ | "mail domain delete"
406
442
  | "mail setup"
407
443
  | "mail status"
408
444
  | "mail webhook set"
@@ -498,6 +534,7 @@ export type RecognizedCommandLabel =
498
534
  | "token list"
499
535
  | "token revoke"
500
536
  | "feedback submit"
537
+ | "feedback status"
501
538
  | "project create"
502
539
  | "organization create"
503
540
  | "organization list"
@@ -507,6 +544,10 @@ export type RecognizedCommandLabel =
507
544
  | "project list"
508
545
  | "project status"
509
546
  | "project dev-access create"
547
+ | "project dev-share link"
548
+ | "project dev-share rotate"
549
+ | "project dev-share revoke"
550
+ | "project dev-access mode"
510
551
  | "project handle check"
511
552
  | "project handle set"
512
553
  | "database write"
@@ -548,7 +589,8 @@ export type RecognizedCommandLabel =
548
589
  | "mail messages get"
549
590
  | "mail messages retry"
550
591
  | "mail domain set"
551
- | "mail domain status";
592
+ | "mail domain status"
593
+ | "mail domain delete";
552
594
 
553
595
  export const recognizedCommandLabel = (argv: readonly string[]): RecognizedCommandLabel => {
554
596
  const values = [...argv];
@@ -597,6 +639,7 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
597
639
  if (value === "export" && (values[0] === "create" || values[0] === "get"))
598
640
  return `export ${values[0]}`;
599
641
  if (value === "feedback" && values[0] === "submit") return "feedback submit";
642
+ if (value === "feedback" && values[0] === "status") return "feedback status";
600
643
  if (value === "project" && values[0] === "create") return "project create";
601
644
  if (value === "organization" && values[0] === "create") return "organization create";
602
645
  if (value === "organization" && values[0] === "list") return "organization list";
@@ -610,6 +653,18 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
610
653
  return "project handle check";
611
654
  if (value === "project" && values[0] === "handle" && values[1] === "set")
612
655
  return "project handle set";
656
+ if (
657
+ value === "project" &&
658
+ values[0] === "dev-share" &&
659
+ ["link", "rotate", "revoke"].includes(values[1] ?? "")
660
+ )
661
+ return values[1] === "link"
662
+ ? "project dev-share link"
663
+ : values[1] === "rotate"
664
+ ? "project dev-share rotate"
665
+ : "project dev-share revoke";
666
+ if (value === "project" && values[0] === "dev-access" && values[1] === "mode")
667
+ return "project dev-access mode";
613
668
  if (value === "project" && values[0] === "dev-access" && values[1] === "create")
614
669
  return "project dev-access create";
615
670
  if (value === "database" && values[0] === "compute" && values[1] === "set")
@@ -669,6 +724,7 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
669
724
  if (value === "mail" && values[0] === "domain") {
670
725
  if (values[1] === "set") return "mail domain set";
671
726
  if (values[1] === "status") return "mail domain status";
727
+ if (values[1] === "delete") return "mail domain delete";
672
728
  }
673
729
  return "unknown";
674
730
  }
@@ -798,6 +854,18 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
798
854
  return { kind: "token-revoke", organizationId, keyId: options["--key"], credentialStore };
799
855
  throw new InvalidCommandError();
800
856
  }
857
+ if (first === "feedback" && values[0] === "status") {
858
+ values.shift();
859
+ const feedbackId = values.shift();
860
+ if (yes || wait || follow || stdin || !isValue(feedbackId)) throw new InvalidCommandError();
861
+ const cursor = parseOptionalOptions(values, ["--cursor"])["--cursor"];
862
+ return {
863
+ kind: "feedback-status",
864
+ feedbackId,
865
+ ...(cursor === undefined ? {} : { cursor }),
866
+ credentialStore,
867
+ };
868
+ }
801
869
  if (first === "feedback" && values.shift() === "submit") {
802
870
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
803
871
  const options = parseOptionalOptions(values, [
@@ -1116,6 +1184,38 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1116
1184
  const options = parseOptions(values, ["--handle"]);
1117
1185
  return { kind: "project-handle-check", handle: options["--handle"], credentialStore };
1118
1186
  }
1187
+ if (subcommand === "dev-share") {
1188
+ const action = values.shift();
1189
+ if (action === "link") {
1190
+ if (yes || wait || follow || stdin) throw new InvalidCommandError();
1191
+ const options = parseOptions(values, ["--project"]);
1192
+ return { kind: "project-dev-share-link", projectId: options["--project"], credentialStore };
1193
+ }
1194
+ if ((action === "rotate" || action === "revoke") && yes && !wait && !follow && !stdin) {
1195
+ const options = parseOptions(values, ["--project", "--idempotency-key"]);
1196
+ return {
1197
+ kind: action === "rotate" ? "project-dev-share-rotate" : "project-dev-share-revoke",
1198
+ projectId: options["--project"],
1199
+ idempotencyKey: options["--idempotency-key"],
1200
+ credentialStore,
1201
+ };
1202
+ }
1203
+ throw new InvalidCommandError();
1204
+ }
1205
+ if (subcommand === "dev-access" && values[0] === "mode") {
1206
+ values.shift();
1207
+ if (!yes || wait || follow || stdin) throw new InvalidCommandError();
1208
+ const options = parseOptions(values, ["--project", "--mode", "--idempotency-key"]);
1209
+ if (options["--mode"] !== "protected" && options["--mode"] !== "public")
1210
+ throw new InvalidCommandError();
1211
+ return {
1212
+ kind: "project-dev-access-mode-set",
1213
+ projectId: options["--project"],
1214
+ mode: options["--mode"],
1215
+ idempotencyKey: options["--idempotency-key"],
1216
+ credentialStore,
1217
+ };
1218
+ }
1119
1219
  if (subcommand === "dev-access" && values.shift() === "create") {
1120
1220
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1121
1221
  const options = parseOptions(values, ["--project"]);
@@ -1177,15 +1277,20 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1177
1277
  "--name",
1178
1278
  "--idempotency-key",
1179
1279
  "--data-mode",
1280
+ "--dev-access-mode",
1180
1281
  "--region",
1181
1282
  ]);
1182
1283
  const dataMode = options["--data-mode"];
1284
+ const devAccessMode = options["--dev-access-mode"];
1183
1285
  const region = options["--region"];
1184
1286
  if (
1185
1287
  !options["--organization"] ||
1186
1288
  !options["--name"] ||
1187
1289
  !options["--idempotency-key"] ||
1188
1290
  (dataMode !== undefined && dataMode !== "shared" && dataMode !== "isolated") ||
1291
+ (devAccessMode !== undefined &&
1292
+ devAccessMode !== "protected" &&
1293
+ devAccessMode !== "public") ||
1189
1294
  (region !== undefined && region !== "us" && region !== "eu")
1190
1295
  )
1191
1296
  throw new InvalidCommandError();
@@ -1194,6 +1299,7 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1194
1299
  organizationId: options["--organization"],
1195
1300
  name: options["--name"],
1196
1301
  ...(dataMode === undefined ? {} : { dataMode }),
1302
+ ...(devAccessMode === undefined ? {} : { devAccessMode }),
1197
1303
  ...(region === undefined ? {} : { region }),
1198
1304
  idempotencyKey: options["--idempotency-key"],
1199
1305
  credentialStore,
@@ -1465,22 +1571,46 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1465
1571
  }
1466
1572
  if (first === "plan") {
1467
1573
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1468
- const options = parseOptions(values, ["--project", "--commit"]);
1574
+ const options = parseOptionalOptions(values, ["--project", "--commit", "--environment"]);
1575
+ const projectId = options["--project"];
1576
+ const commitSha = options["--commit"];
1577
+ if (projectId === undefined || commitSha === undefined) throw new InvalidCommandError();
1469
1578
  return {
1470
1579
  kind: "plan",
1471
- projectId: options["--project"],
1472
- commitSha: options["--commit"],
1580
+ projectId,
1581
+ commitSha,
1582
+ environment: deploymentEnvironment(options["--environment"]),
1473
1583
  credentialStore,
1474
1584
  };
1475
1585
  }
1476
1586
  if (first === "deploy") {
1477
1587
  if (!yes || follow || stdin) throw new InvalidCommandError();
1478
- const options = parseOptions(values, ["--project", "--plan-id", "--idempotency-key"]);
1588
+ const options = parseOptionalOptions(values, [
1589
+ "--project",
1590
+ "--plan-id",
1591
+ "--commit",
1592
+ "--environment",
1593
+ "--idempotency-key",
1594
+ ]);
1595
+ const projectId = options["--project"];
1596
+ const idempotencyKey = options["--idempotency-key"];
1597
+ const planId = options["--plan-id"] ?? null;
1598
+ const commitSha = options["--commit"] ?? null;
1599
+ // A plan already names its environment, so --environment only accompanies --commit.
1600
+ if (
1601
+ projectId === undefined ||
1602
+ idempotencyKey === undefined ||
1603
+ (planId === null) === (commitSha === null) ||
1604
+ (planId !== null && options["--environment"] !== undefined)
1605
+ )
1606
+ throw new InvalidCommandError();
1479
1607
  return {
1480
1608
  kind: "deploy",
1481
- projectId: options["--project"],
1482
- planId: options["--plan-id"],
1483
- idempotencyKey: options["--idempotency-key"],
1609
+ projectId,
1610
+ planId,
1611
+ commitSha,
1612
+ environment: deploymentEnvironment(options["--environment"]),
1613
+ idempotencyKey,
1484
1614
  yes,
1485
1615
  wait,
1486
1616
  credentialStore,
@@ -1752,10 +1882,12 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1752
1882
  throw new InvalidCommandError();
1753
1883
  }
1754
1884
  if (first === "mail") {
1755
- if (yes || wait || follow || stdin) throw new InvalidCommandError();
1756
1885
  const area = values.shift();
1757
1886
  const sub =
1758
1887
  area === "webhook" || area === "messages" || area === "domain" ? values.shift() : undefined;
1888
+ // Only retiring the mail domain is destructive, and it requires --yes.
1889
+ const retiring = area === "domain" && sub === "delete";
1890
+ if (yes !== retiring || wait || follow || stdin) throw new InvalidCommandError();
1759
1891
  const options = parseOptionalOptions(values, [
1760
1892
  "--project",
1761
1893
  "--environment",
@@ -1792,6 +1924,13 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1792
1924
  } else if (area === "status" || (area === "domain" && sub === "status")) {
1793
1925
  request = { ...scope, action: "status" };
1794
1926
  label = area === "domain" ? "mail domain status" : "mail status";
1927
+ } else if (retiring) {
1928
+ request = {
1929
+ ...scope,
1930
+ action: "domain_delete",
1931
+ idempotencyKey: required("--idempotency-key"),
1932
+ };
1933
+ label = "mail domain delete";
1795
1934
  } else if (area === "webhook" && sub === "set") {
1796
1935
  request = {
1797
1936
  ...scope,
@@ -1857,6 +1996,12 @@ const parseOptions = <T extends string>(
1857
1996
  return result;
1858
1997
  };
1859
1998
 
1999
+ const deploymentEnvironment = (value: string | undefined): "dev" | "prod" => {
2000
+ if (value === undefined || value === "dev") return "dev";
2001
+ if (value === "prod") return "prod";
2002
+ throw new InvalidCommandError();
2003
+ };
2004
+
1860
2005
  const parseOptionalOptions = <T extends string>(
1861
2006
  values: readonly string[],
1862
2007
  allowed: readonly T[],