@amerged/ohmyhost-mcp 0.1.15 → 0.1.17

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.
@@ -53,6 +53,20 @@ export interface LocalMcpProductClient {
53
53
  }): Promise<CloudflareDnsAuthorization>;
54
54
  getCloudflareDnsStatus(projectId: string): Promise<CloudflareDnsAuthorizationStatus>;
55
55
  createDevAccessTicket(projectId: string): Promise<DevAccessTicket>;
56
+ ensureDevShareLink(projectId: string): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
57
+ setDevAccessMode(input: {
58
+ projectId: string;
59
+ mode: "protected" | "public";
60
+ idempotencyKey: string;
61
+ }): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
62
+ rotateDevShareLink(input: {
63
+ projectId: string;
64
+ idempotencyKey: string;
65
+ }): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
66
+ revokeDevShareLink(input: {
67
+ projectId: string;
68
+ idempotencyKey: string;
69
+ }): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
56
70
  changeDatabaseCompute(input: {
57
71
  projectId: string;
58
72
  environment: "dev" | "prod";
@@ -264,6 +278,7 @@ interface ProjectCreateInput {
264
278
  readonly organizationId: string;
265
279
  readonly name: string;
266
280
  readonly dataMode?: "shared" | "isolated";
281
+ readonly devAccessMode?: "protected" | "public";
267
282
  readonly region?: "us" | "eu";
268
283
  readonly idempotencyKey: string;
269
284
  }
@@ -278,6 +293,7 @@ interface SourceLinkInput {
278
293
  interface DeploymentPlanInput {
279
294
  readonly projectId: string;
280
295
  readonly commitSha: string;
296
+ readonly environment: "dev" | "prod";
281
297
  }
282
298
 
283
299
  interface DeploymentCreateInput {
@@ -461,7 +477,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
461
477
  server,
462
478
  dependencies,
463
479
  "domain_paid_plan",
464
- "Plan a customer-owned production hostname and return the manual CNAME/validation instructions. This does not activate or change DNS. Free projects use their platform domain without customer DNS authorization.",
480
+ "Plan a customer-owned production hostname and return the manual CNAME/validation instructions. This does not activate or change DNS. Free projects use their platform domain without customer DNS authorization. production_deployment_required means Prod has no active deployment: deploy to Prod or promote the current Dev deployment, then plan again.",
465
481
  z.object({ project_id: identifier, hostname: domainName }),
466
482
  (client, input) =>
467
483
  client.planPaidDomain({ projectId: input.project_id, hostname: input.hostname }),
@@ -470,7 +486,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
470
486
  server,
471
487
  dependencies,
472
488
  "domain_paid_apply",
473
- "Activate the explicitly requested customer hostname. Requires Paid; the backend may reuse only its scoped customer OAuth grant. After domain_cloudflare_status is authorized, replay the original hostname/key to reconcile its exact DNS records. Otherwise return the manual DNS records to the human. Preserve the original hostname/key after uncertainty; no arbitrary DNS or provider credentials are accepted.",
489
+ "Activate the explicitly requested customer hostname. Requires Paid; the backend may reuse only its scoped customer OAuth grant. After domain_cloudflare_status is authorized, replay the original hostname/key to reconcile its exact DNS records. Otherwise return the manual DNS records to the human. Preserve the original hostname/key after uncertainty; no arbitrary DNS or provider credentials are accepted. production_deployment_required changed nothing: deploy to Prod or promote Dev first, then repeat the same hostname/key.",
474
490
  z.object({
475
491
  project_id: identifier,
476
492
  hostname: domainName,
@@ -1001,11 +1017,12 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1001
1017
  server,
1002
1018
  dependencies,
1003
1019
  "project_create",
1004
- "Create an ohmyho.st project. Recommend isolated Dev/Prod data, but respect the customer's choice and added database consumption. shared is the default and uses one database/file namespace; isolated separates database/Auth records and files. Choose before provisioning; changing the established mode requires a data migration. region defaults to us; eu places the project's database, files and builds in the EU at the same prices. The region cannot be changed later, and ohmyhost.yaml storage.jurisdiction must equal it.",
1020
+ "Create an ohmyho.st project. Ask whether Dev should be protected (default) or public before creation; public means anyone with the Dev URL can open it, including when Dev and Prod share data. Recommend isolated Dev/Prod data, but respect the customer's choice and added database consumption. shared is the default and uses one database/file namespace; isolated separates database/Auth records and files. Choose before provisioning; changing the established mode requires a data migration. region defaults to us; eu places the project's database, files and builds in the EU at the same prices. The region cannot be changed later, and ohmyhost.yaml storage.jurisdiction must equal it.",
1005
1021
  z.object({
1006
1022
  organization_id: identifier,
1007
1023
  name: z.string().min(1).max(128),
1008
1024
  data_mode: z.enum(["shared", "isolated"]).optional(),
1025
+ dev_access_mode: z.enum(["protected", "public"]).optional(),
1009
1026
  region: z.enum(["us", "eu"]).optional(),
1010
1027
  idempotency_key: idempotencyKey,
1011
1028
  }),
@@ -1014,6 +1031,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1014
1031
  organizationId: input.organization_id,
1015
1032
  name: input.name,
1016
1033
  ...(input.data_mode === undefined ? {} : { dataMode: input.data_mode }),
1034
+ ...(input.dev_access_mode === undefined ? {} : { devAccessMode: input.dev_access_mode }),
1017
1035
  ...(input.region === undefined ? {} : { region: input.region }),
1018
1036
  idempotencyKey: input.idempotency_key,
1019
1037
  }),
@@ -1031,15 +1049,68 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1031
1049
  server,
1032
1050
  dependencies,
1033
1051
  "project_status",
1034
- "Get source, both Dev/Prod environment IDs, deployment URLs, latest operation and cleanup status. Dev URLs are protected: use project_dev_access_create before browser verification. Select environments by name when configuring secrets; never infer a Prod ID from the default Dev environment.",
1052
+ "Get source, both Dev/Prod environment IDs, deployment URLs, Dev access mode, latest operation and cleanup status. Protected Dev needs project_dev_share_link_get for a persistent share link. Select environments by name when configuring secrets; never infer a Prod ID from the default Dev environment.",
1035
1053
  z.object({ project_id: identifier }),
1036
1054
  (client, input) => client.getProjectStatus(input.project_id),
1037
1055
  );
1056
+ registerProductTool(
1057
+ server,
1058
+ dependencies,
1059
+ "project_dev_share_link_get",
1060
+ "Owner only: get or create the persistent protected Dev link. The same link works for multiple visitors and has no automatic expiry. Keep it out of notes and logs.",
1061
+ z.object({ project_id: identifier }),
1062
+ (client, input) => client.ensureDevShareLink(input.project_id),
1063
+ mutationAnnotations(),
1064
+ );
1065
+ registerProductTool(
1066
+ server,
1067
+ dependencies,
1068
+ "project_dev_access_mode_set",
1069
+ "Owner only: choose public Dev (no platform token) or protected Dev (share link required). A switch takes effect on the next request; switching back creates a new link.",
1070
+ z.object({
1071
+ project_id: identifier,
1072
+ mode: z.enum(["protected", "public"]),
1073
+ idempotency_key: idempotencyKey,
1074
+ }),
1075
+ (client, input) =>
1076
+ client.setDevAccessMode({
1077
+ projectId: input.project_id,
1078
+ mode: input.mode,
1079
+ idempotencyKey: input.idempotency_key,
1080
+ }),
1081
+ destructiveAnnotations(),
1082
+ );
1083
+ registerProductTool(
1084
+ server,
1085
+ dependencies,
1086
+ "project_dev_share_link_rotate",
1087
+ "Owner only: replace the persistent Dev link and immediately revoke old links and sessions. Repeat the same key after uncertainty.",
1088
+ z.object({ project_id: identifier, idempotency_key: idempotencyKey }),
1089
+ (client, input) =>
1090
+ client.rotateDevShareLink({
1091
+ projectId: input.project_id,
1092
+ idempotencyKey: input.idempotency_key,
1093
+ }),
1094
+ destructiveAnnotations(),
1095
+ );
1096
+ registerProductTool(
1097
+ server,
1098
+ dependencies,
1099
+ "project_dev_share_link_revoke",
1100
+ "Owner only: revoke the persistent Dev link and active sessions immediately; Dev stays protected until a new link is obtained.",
1101
+ z.object({ project_id: identifier, idempotency_key: idempotencyKey }),
1102
+ (client, input) =>
1103
+ client.revokeDevShareLink({
1104
+ projectId: input.project_id,
1105
+ idempotencyKey: input.idempotency_key,
1106
+ }),
1107
+ destructiveAnnotations(),
1108
+ );
1038
1109
  registerProductTool(
1039
1110
  server,
1040
1111
  dependencies,
1041
1112
  "project_dev_access_create",
1042
- "Create an owner-only ten-minute single-use access link for the protected Dev app. Anonymous Dev HTTP 404 is expected; it does not prove deployment failure. Open redeem_url once in the intended browser or isolated cookie jar, then use the clean origin with its session cookie. Keep the link and cookie private, out of reports, logs and project notes. A new ticket invalidates earlier Dev access and unused owner tickets; do not blindly retry an uncertain request. This grants no customer application login or Prod access.",
1113
+ "Create an owner-only one-hour single-use access link for the protected Dev app. Anonymous Dev HTTP 404 is expected; it does not prove deployment failure. Open redeem_url once in the intended browser or isolated cookie jar, then use the clean origin with its session cookie. Keep the link and cookie private, out of reports, logs and project notes. A new ticket invalidates earlier one-time ticket sessions and unused owner tickets but not the persistent share link; do not blindly retry an uncertain request. This grants no customer application login or Prod access.",
1043
1114
  z.object({ project_id: identifier }),
1044
1115
  (client, input) => client.createDevAccessTicket(input.project_id),
1045
1116
  { ...mutationAnnotations(), idempotentHint: false },
@@ -1097,10 +1168,18 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1097
1168
  server,
1098
1169
  dependencies,
1099
1170
  "deployment_plan",
1100
- "Plan an immutable deployment. Preserve the customer's existing authentication integration, such as Better Auth or WorkOS, and use the same generic Next.js/Vite/TanStack runtime contract. ohmyho.st login is separate. Read the app Skill's auth guidance and report actual plan blockers, required secret names/callback configuration and unverified runtime compatibility. Never assume a working platform login proves application login or silently replace its auth provider.",
1101
- z.object({ project_id: identifier, commit_sha: commitSha }),
1171
+ "Plan an immutable deployment. Preserve the customer's existing authentication integration, such as Better Auth or WorkOS, and use the same generic Next.js/Vite/TanStack runtime contract. ohmyho.st login is separate. Read the app Skill's auth guidance and report actual plan blockers, required secret names/callback configuration and unverified runtime compatibility. Hosting, including Better Auth, needs no mail domain unless the app declares mail.enabled. Never assume a working platform login proves application login or silently replace its auth provider. The plan builds into Dev unless environment is prod; prod builds straight into Prod without a Dev deployment (a project with shared data and a database must deploy to Dev and promote instead). The plan stays valid for 24 hours.",
1172
+ z.object({
1173
+ project_id: identifier,
1174
+ commit_sha: commitSha,
1175
+ environment: z.enum(["dev", "prod"]).optional(),
1176
+ }),
1102
1177
  (client, input) =>
1103
- client.planDeployment({ projectId: input.project_id, commitSha: input.commit_sha }),
1178
+ client.planDeployment({
1179
+ projectId: input.project_id,
1180
+ commitSha: input.commit_sha,
1181
+ environment: input.environment ?? "dev",
1182
+ }),
1104
1183
  );
1105
1184
  registerProductTool(
1106
1185
  server,
@@ -1219,7 +1298,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1219
1298
  server,
1220
1299
  dependencies,
1221
1300
  "mail_setup",
1222
- "Configure the customer's one production mail domain using the project Prod environment ID. The Dev and Prod application URLs do not create separate mail domains. Ask whether they want sending and receiving. Configure sending first; receiving requires mail_webhook_set, application signature verification and mail_webhook_verify. Preserve existing mailbox MX records. No Resend account or key is needed.",
1301
+ "Configure the customer's one production mail domain using the project Prod environment ID, only when the customer wants mail or the app declares mail.enabled; hosting needs no mail domain and none is registered automatically. The Dev and Prod application URLs do not create separate mail domains. Ask whether they want sending and receiving. Configure sending first; receiving requires mail_webhook_set, application signature verification and mail_webhook_verify. Preserve existing mailbox MX records. No Resend account or key is needed.",
1223
1302
  z.object({
1224
1303
  project_id: identifier,
1225
1304
  environment_id: identifier,
@@ -1397,6 +1476,28 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1397
1476
  });
1398
1477
  },
1399
1478
  );
1479
+ registerProductTool(
1480
+ server,
1481
+ dependencies,
1482
+ "mail_domain_delete",
1483
+ "Retire the project's mail domain while the project stays active; use its Prod environment ID. Sending and receiving stop at once, then its provider domain and key are removed. DNS is not changed: remove the returned dns_records from the domain's DNS. Only when the customer no longer wants mail. Requires explicit confirmation; repeat the same key after uncertainty.",
1484
+ z.object({
1485
+ project_id: identifier,
1486
+ environment_id: identifier,
1487
+ idempotency_key: idempotencyKey,
1488
+ confirm: z.literal(true),
1489
+ }),
1490
+ async (client, input) => {
1491
+ if (!client.managedMail) throw new Error("Mail unavailable");
1492
+ return client.managedMail({
1493
+ action: "domain_delete",
1494
+ projectId: input.project_id,
1495
+ environmentId: input.environment_id,
1496
+ idempotencyKey: input.idempotency_key,
1497
+ });
1498
+ },
1499
+ destructiveAnnotations(),
1500
+ );
1400
1501
  registerProductTool(
1401
1502
  server,
1402
1503
  dependencies,
@@ -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
 
@@ -303,13 +305,20 @@ const HELP_USAGE = Object.freeze({
303
305
  "feedback submit":
304
306
  "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
307
  "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)",
308
+ "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
309
  "project list": "ohmyhost project list [--cursor ULID] [--limit LIMIT] --json",
308
310
  "project context": "ohmyhost project context --project ULID --json",
309
311
  "project notes set":
310
312
  "ohmyhost project notes set --project ULID --version NUMBER --markdown TEXT --idempotency-key KEY --json (no credentials or signed URLs)",
311
313
  "project status": "ohmyhost project status --project ULID --json",
312
314
  "project dev-access create": "ohmyhost project dev-access create --project ULID --json",
315
+ "project dev-share link": "ohmyhost project dev-share link --project ULID --json",
316
+ "project dev-share rotate":
317
+ "ohmyhost project dev-share rotate --project ULID --idempotency-key KEY --yes --json",
318
+ "project dev-share revoke":
319
+ "ohmyhost project dev-share revoke --project ULID --idempotency-key KEY --yes --json",
320
+ "project dev-access mode":
321
+ "ohmyhost project dev-access mode --project ULID --mode protected|public --idempotency-key KEY --yes --json",
313
322
  "project handle check":
314
323
  "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
324
  "project handle set":
@@ -347,9 +356,9 @@ const HELP_USAGE = Object.freeze({
347
356
  "domain paid status": "ohmyhost domain paid status --project ULID --json",
348
357
  "domain paid delete":
349
358
  "ohmyhost domain paid delete --project ULID --hostname HOST --idempotency-key KEY --yes --json",
350
- plan: "ohmyhost plan --project ULID --commit SHA --json",
359
+ plan: "ohmyhost plan --project ULID --commit SHA [--environment dev|prod] --json",
351
360
  deploy:
352
- "ohmyhost deploy --project ULID --plan-id ULID --idempotency-key KEY --yes [--wait] --json",
361
+ "ohmyhost deploy --project ULID (--plan-id ULID | --commit SHA [--environment dev|prod]) --idempotency-key KEY --yes [--wait] --json",
353
362
  logs: "ohmyhost logs OPERATION_ULID --follow --json",
354
363
  "deployment logs": "ohmyhost deployment logs --project ULID --deployment ULID --follow --json",
355
364
  "rollback plan": "ohmyhost rollback plan --project ULID --deployment DEPLOYMENT_ULID --json",
@@ -387,6 +396,8 @@ const HELP_USAGE = Object.freeze({
387
396
  "mail domain set":
388
397
  "ohmyhost mail domain set --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
389
398
  "mail domain status": "ohmyhost mail domain status --project ULID --environment ULID --json",
399
+ "mail domain delete":
400
+ "ohmyhost mail domain delete --project ULID --environment ULID --idempotency-key KEY --yes --json",
390
401
  } satisfies Readonly<Record<CustomerCommandLabel, string>>);
391
402
 
392
403
  const HELP_COMMANDS = Object.freeze(
@@ -740,6 +751,37 @@ export class ProductCli {
740
751
  if (command.kind === "project-status") return await this.getProjectStatus(command, signal);
741
752
  if (command.kind === "project-dev-access-create")
742
753
  return await this.createDevAccessTicket(command, signal);
754
+ if (
755
+ command.kind === "project-dev-share-link" ||
756
+ command.kind === "project-dev-share-rotate" ||
757
+ command.kind === "project-dev-share-revoke" ||
758
+ command.kind === "project-dev-access-mode-set"
759
+ ) {
760
+ const api = await this.authenticatedApi(command.credentialStore, signal);
761
+ const state = parseDevAccessState(
762
+ command.kind === "project-dev-share-link"
763
+ ? await api.ensureDevShareLink(command.projectId)
764
+ : command.kind === "project-dev-share-rotate"
765
+ ? await api.rotateDevShareLink(command.projectId, command.idempotencyKey)
766
+ : command.kind === "project-dev-share-revoke"
767
+ ? await api.revokeDevShareLink(command.projectId, command.idempotencyKey)
768
+ : await api.setDevAccessMode(
769
+ command.projectId,
770
+ command.mode,
771
+ command.idempotencyKey,
772
+ ),
773
+ );
774
+ return success(
775
+ command.kind === "project-dev-share-link"
776
+ ? "project dev-share link"
777
+ : command.kind === "project-dev-share-rotate"
778
+ ? "project dev-share rotate"
779
+ : command.kind === "project-dev-share-revoke"
780
+ ? "project dev-share revoke"
781
+ : "project dev-access mode",
782
+ { status: "succeeded", state },
783
+ );
784
+ }
743
785
  if (command.kind === "database-write") {
744
786
  const file = await open(command.statementFile, "r"),
745
787
  buffer = Buffer.alloc(65_537);
@@ -1169,6 +1211,7 @@ export class ProductCli {
1169
1211
  organizationId: command.organizationId,
1170
1212
  name: command.name,
1171
1213
  ...(command.dataMode === undefined ? {} : { dataMode: command.dataMode }),
1214
+ ...(command.devAccessMode === undefined ? {} : { devAccessMode: command.devAccessMode }),
1172
1215
  ...(command.region === undefined ? {} : { region: command.region }),
1173
1216
  idempotencyKey: command.idempotencyKey,
1174
1217
  }),
@@ -1451,7 +1494,10 @@ export class ProductCli {
1451
1494
  ): Promise<CliResult> {
1452
1495
  const api = await this.authenticatedApi(command.credentialStore, signal);
1453
1496
  const autoDeploy = await api.getAutoDeploy(command.projectId);
1454
- return success("source auto-deploy status", { status: "ready", auto_deploy: autoDeploy });
1497
+ return success("source auto-deploy status", {
1498
+ status: autoDeploy.generation === 0 ? "not_configured" : "ready",
1499
+ auto_deploy: autoDeploy,
1500
+ });
1455
1501
  }
1456
1502
 
1457
1503
  private async authorizeCloudflareDns(
@@ -1507,7 +1553,16 @@ export class ProductCli {
1507
1553
  ): Promise<CliResult> {
1508
1554
  const api = await this.authenticatedApi(command.credentialStore, signal);
1509
1555
  const plan = parseDeploymentPlan(await api.planDeployment(command));
1510
- return success("plan", { status: "planned", plan });
1556
+ // Say which environment this plan changes and how the change reaches the live site.
1557
+ const notes =
1558
+ plan["environment"] === "prod"
1559
+ ? [
1560
+ `This plan builds straight into Prod (${String(plan["route"])}); Prod traffic switches once it is activated.`,
1561
+ ]
1562
+ : [
1563
+ `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'.`,
1564
+ ];
1565
+ return success("plan", { status: "planned", plan, notes });
1511
1566
  }
1512
1567
 
1513
1568
  private async deploy(
@@ -1515,20 +1570,37 @@ export class ProductCli {
1515
1570
  signal: AbortSignal,
1516
1571
  ): Promise<CliResult> {
1517
1572
  const api = await this.authenticatedApi(command.credentialStore, signal);
1518
- let operation = parseOperation(await api.createDeployment(command));
1573
+ // With --commit the deploy plans first, so an approved deploy needs no separate plan step.
1574
+ const plan =
1575
+ command.commitSha === null
1576
+ ? null
1577
+ : parseDeploymentPlan(
1578
+ await api.planDeployment({
1579
+ projectId: command.projectId,
1580
+ commitSha: command.commitSha,
1581
+ environment: command.environment,
1582
+ }),
1583
+ );
1584
+ let operation = parseOperation(
1585
+ await api.createDeployment({
1586
+ projectId: command.projectId,
1587
+ planId: plan === null ? String(command.planId) : String(plan["id"]),
1588
+ idempotencyKey: command.idempotencyKey,
1589
+ }),
1590
+ );
1519
1591
  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.
1592
+ // Dev access follows the project-selected mode; the clean URL alone is insufficient when protected.
1522
1593
  const notes =
1523
1594
  operation.state === "succeeded"
1524
1595
  ? [
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.`,
1596
+ `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
1597
  ]
1527
1598
  : [];
1528
1599
  return success("deploy", {
1529
1600
  status: command.wait ? "completed" : "accepted",
1530
1601
  operation,
1531
- ...(notes.length > 0 ? { notes } : {}),
1602
+ ...(plan === null ? {} : { plan }),
1603
+ ...(notes.length > 0 && plan?.["environment"] !== "prod" ? { notes } : {}),
1532
1604
  });
1533
1605
  }
1534
1606
 
@@ -2044,6 +2116,7 @@ const classifyFailure = (command: CommandLabel, error: unknown, signal: AbortSig
2044
2116
  undefined,
2045
2117
  problem.retryAfterSeconds,
2046
2118
  problem.requestId,
2119
+ problem.docsUrl,
2047
2120
  );
2048
2121
  } catch {
2049
2122
  return failure(
@@ -2094,6 +2167,7 @@ const failure = (
2094
2167
  operation?: ReturnType<typeof parseOperation>,
2095
2168
  retryAfterSeconds?: number,
2096
2169
  requestId?: string,
2170
+ docsUrl?: string,
2097
2171
  ): CliResult => {
2098
2172
  const document: CliErrorDocument = {
2099
2173
  version: 1,
@@ -2106,6 +2180,7 @@ const failure = (
2106
2180
  suggested_action: suggestedAction,
2107
2181
  ...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
2108
2182
  ...(requestId === undefined ? {} : { request_id: requestId }),
2183
+ ...(docsUrl === undefined ? {} : { docs_url: docsUrl }),
2109
2184
  ...(operation?.error === undefined ? {} : { message: operation.error.message }),
2110
2185
  },
2111
2186
  };
@@ -2144,6 +2219,10 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2144
2219
  if (command.kind === "project-notes-set") return "project notes set";
2145
2220
  if (command.kind === "project-status") return "project status";
2146
2221
  if (command.kind === "project-dev-access-create") return "project dev-access create";
2222
+ if (command.kind === "project-dev-share-link") return "project dev-share link";
2223
+ if (command.kind === "project-dev-share-rotate") return "project dev-share rotate";
2224
+ if (command.kind === "project-dev-share-revoke") return "project dev-share revoke";
2225
+ if (command.kind === "project-dev-access-mode-set") return "project dev-access mode";
2147
2226
  if (command.kind === "project-handle-check") return "project handle check";
2148
2227
  if (command.kind === "project-handle-set") return "project handle set";
2149
2228
  if (command.kind === "database-write") return "database write";