@amerged/ohmyhost-cli 0.1.11 → 0.1.14

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
@@ -47,7 +47,7 @@ import {
47
47
  selectWorkspace,
48
48
  selectedWorkspaceId,
49
49
  workspaceOf
50
- } from "./chunk-WT2RRQUB.js";
50
+ } from "./chunk-U32PJQHH.js";
51
51
  import {
52
52
  CredentialStoreIntegrityError,
53
53
  CredentialStoreUnavailableError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -75,7 +75,6 @@ import {
75
75
  type PublicEnvironmentSecret,
76
76
  parseOperation,
77
77
  parseOperationEvent,
78
- parseProjectMailDomain,
79
78
  parseProjectPage,
80
79
  parseProjectStatus,
81
80
  parseProjectDatabaseQuery,
@@ -254,6 +253,7 @@ interface CliErrorDocument {
254
253
  readonly suggested_action: string;
255
254
  readonly message?: string;
256
255
  readonly retry_after_seconds?: number;
256
+ readonly request_id?: string;
257
257
  };
258
258
  }
259
259
 
@@ -312,6 +312,8 @@ const HELP_USAGE = Object.freeze({
312
312
  "project dev-access create": "ohmyhost project dev-access create --project ULID --json",
313
313
  "project handle check":
314
314
  "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
+ "project handle set":
316
+ "ohmyhost project handle set --project ULID --handle HANDLE --if-match ETAG --idempotency-key KEY --json (moves the project to a free address; the old one stops working and anyone may claim it)",
315
317
  "database compute set":
316
318
  "ohmyhost database compute set --project ULID --environment dev|prod --profile standard|performance --idempotency-key KEY --yes [--wait] --json\n" +
317
319
  " standard: Free 0.25 CU/1 GB/60s idle, Paid 0.5 CU/2 GB/60s idle. performance: Paid 1 CU/4 GB/300s idle, 2.5x database compute credits per equal active minute; longer idle time also consumes credits.",
@@ -367,9 +369,24 @@ const HELP_USAGE = Object.freeze({
367
369
  "printf '%s' \"$SECRET_VALUE\" | ohmyhost secret set NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY --stdin [--wait] --json",
368
370
  "secret delete":
369
371
  "ohmyhost secret delete NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY [--wait] --json",
372
+ "mail setup":
373
+ "ohmyhost mail setup --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
374
+ "mail status": "ohmyhost mail status --project ULID --environment ULID --json",
375
+ "mail webhook set":
376
+ "ohmyhost mail webhook set --project ULID --environment ULID --url HTTPS_URL --idempotency-key KEY --json",
377
+ "mail webhook verify":
378
+ "ohmyhost mail webhook verify --project ULID --environment ULID --idempotency-key KEY --json",
379
+ "mail webhook disable":
380
+ "ohmyhost mail webhook disable --project ULID --environment ULID --idempotency-key KEY --json",
381
+ "mail messages list":
382
+ "ohmyhost mail messages list --project ULID --environment ULID [--after ULID] --json",
383
+ "mail messages get":
384
+ "ohmyhost mail messages get --project ULID --environment ULID --message ULID --json",
385
+ "mail messages retry":
386
+ "ohmyhost mail messages retry --project ULID --environment ULID --message ULID --idempotency-key KEY --json",
370
387
  "mail domain set":
371
- "ohmyhost mail domain set --project ULID --domain DOMAIN --idempotency-key KEY --json",
372
- "mail domain status": "ohmyhost mail domain status --project ULID --json",
388
+ "ohmyhost mail domain set --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
389
+ "mail domain status": "ohmyhost mail domain status --project ULID --environment ULID --json",
373
390
  } satisfies Readonly<Record<CustomerCommandLabel, string>>);
374
391
 
375
392
  const HELP_COMMANDS = Object.freeze(
@@ -691,6 +708,8 @@ export class ProductCli {
691
708
  if (command.kind === "project-list") return await this.listProjects(command, signal);
692
709
  if (command.kind === "project-handle-check")
693
710
  return await this.checkProjectHandle(command, signal);
711
+ if (command.kind === "project-handle-set")
712
+ return await this.changeProjectHandle(command, signal);
694
713
  if (command.kind === "database-compute-set") {
695
714
  const api = await this.authenticatedApi(command.credentialStore, signal);
696
715
  let operation = parseOperation(await api.changeDatabaseCompute(command));
@@ -802,9 +821,11 @@ export class ProductCli {
802
821
  if (command.kind === "secret-list") return await this.listEnvironmentSecrets(command, signal);
803
822
  if (command.kind === "function-runs") return await this.listFunctionRuns(command, signal);
804
823
  if (command.kind === "secret-set") return await this.putEnvironmentSecret(command, signal);
805
- if (command.kind === "mail-domain-set")
806
- return await this.configureMailDomain(command, signal);
807
- if (command.kind === "mail-domain-status") return await this.getMailDomain(command, signal);
824
+ if (command.kind === "managed-mail") {
825
+ const api = await this.authenticatedApi(command.credentialStore, signal);
826
+ if (!api.managedMail) throw new Error("Mail unavailable");
827
+ return success(command.label, { mail: await api.managedMail(command.request) });
828
+ }
808
829
  return await this.deleteEnvironmentSecret(command, signal);
809
830
  } catch (error) {
810
831
  if (error instanceof UserTokenFileError)
@@ -1169,6 +1190,22 @@ export class ProductCli {
1169
1190
  return success("project list", { status: "succeeded", projects });
1170
1191
  }
1171
1192
 
1193
+ private async changeProjectHandle(
1194
+ command: Extract<ProductCliCommand, { kind: "project-handle-set" }>,
1195
+ signal: AbortSignal,
1196
+ ): Promise<CliResult> {
1197
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1198
+ const operation = parseOperation(
1199
+ await api.changeProjectHandle({
1200
+ projectId: command.projectId,
1201
+ handle: command.handle,
1202
+ ifMatch: command.ifMatch,
1203
+ idempotencyKey: command.idempotencyKey,
1204
+ }),
1205
+ );
1206
+ return success("project handle set", { status: "accepted", operation });
1207
+ }
1208
+
1172
1209
  private async checkProjectHandle(
1173
1210
  command: Extract<ProductCliCommand, { kind: "project-handle-check" }>,
1174
1211
  signal: AbortSignal,
@@ -1742,30 +1779,6 @@ export class ProductCli {
1742
1779
  );
1743
1780
  }
1744
1781
 
1745
- private async configureMailDomain(
1746
- command: Extract<ProductCliCommand, { kind: "mail-domain-set" }>,
1747
- signal: AbortSignal,
1748
- ): Promise<CliResult> {
1749
- const api = await this.authenticatedApi(command.credentialStore, signal);
1750
- const mailDomain = parseProjectMailDomain(
1751
- await api.configureMailDomain({
1752
- projectId: command.projectId,
1753
- domain: command.domain,
1754
- idempotencyKey: command.idempotencyKey,
1755
- }),
1756
- );
1757
- return success("mail domain set", { status: mailDomain.status, mail_domain: mailDomain });
1758
- }
1759
-
1760
- private async getMailDomain(
1761
- command: Extract<ProductCliCommand, { kind: "mail-domain-status" }>,
1762
- signal: AbortSignal,
1763
- ): Promise<CliResult> {
1764
- const api = await this.authenticatedApi(command.credentialStore, signal);
1765
- const mailDomain = parseProjectMailDomain(await api.getMailDomain(command.projectId));
1766
- return success("mail domain status", { status: mailDomain.status, mail_domain: mailDomain });
1767
- }
1768
-
1769
1782
  private async waitForOperation(
1770
1783
  api: ProductApi,
1771
1784
  initial: ReturnType<typeof parseOperation>,
@@ -2030,6 +2043,7 @@ const classifyFailure = (command: CommandLabel, error: unknown, signal: AbortSig
2030
2043
  problem.suggestedAction,
2031
2044
  undefined,
2032
2045
  problem.retryAfterSeconds,
2046
+ problem.requestId,
2033
2047
  );
2034
2048
  } catch {
2035
2049
  return failure(
@@ -2079,6 +2093,7 @@ const failure = (
2079
2093
  suggestedAction: string,
2080
2094
  operation?: ReturnType<typeof parseOperation>,
2081
2095
  retryAfterSeconds?: number,
2096
+ requestId?: string,
2082
2097
  ): CliResult => {
2083
2098
  const document: CliErrorDocument = {
2084
2099
  version: 1,
@@ -2090,6 +2105,7 @@ const failure = (
2090
2105
  retryable,
2091
2106
  suggested_action: suggestedAction,
2092
2107
  ...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
2108
+ ...(requestId === undefined ? {} : { request_id: requestId }),
2093
2109
  ...(operation?.error === undefined ? {} : { message: operation.error.message }),
2094
2110
  },
2095
2111
  };
@@ -2129,6 +2145,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2129
2145
  if (command.kind === "project-status") return "project status";
2130
2146
  if (command.kind === "project-dev-access-create") return "project dev-access create";
2131
2147
  if (command.kind === "project-handle-check") return "project handle check";
2148
+ if (command.kind === "project-handle-set") return "project handle set";
2132
2149
  if (command.kind === "database-write") return "database write";
2133
2150
  if (command.kind === "database-query") return "database query";
2134
2151
  if (command.kind === "database-access-create") return "database access create";
@@ -2153,8 +2170,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2153
2170
  if (command.kind === "function-runs") return "function runs";
2154
2171
  if (command.kind === "secret-set") return "secret set";
2155
2172
  if (command.kind === "secret-delete") return "secret delete";
2156
- if (command.kind === "mail-domain-set") return "mail domain set";
2157
- if (command.kind === "mail-domain-status") return "mail domain status";
2173
+ if (command.kind === "managed-mail") return command.label;
2158
2174
  return command.kind;
2159
2175
  };
2160
2176
 
@@ -1,3 +1,4 @@
1
+ import type { ManagedMailCommand } from "@ohmyhost/sdk-ts";
1
2
  import { USER_API_KEY_ID } from "@ohmyhost/contracts/user-api-keys";
2
3
  export type ProductCliCommand =
3
4
  | {
@@ -132,6 +133,15 @@ export type ProductCliCommand =
132
133
  readonly idempotencyKey: string;
133
134
  readonly credentialStore: "native";
134
135
  }
136
+ | {
137
+ /** Moves a project to a chosen address once its gateways serve it. */
138
+ readonly kind: "project-handle-set";
139
+ readonly projectId: string;
140
+ readonly handle: string;
141
+ readonly ifMatch: string;
142
+ readonly idempotencyKey: string;
143
+ readonly credentialStore: "native";
144
+ }
135
145
  | {
136
146
  /** Asks whether a project address is free; the candidate is sent exactly as the agent typed it. */
137
147
  readonly kind: "project-handle-check";
@@ -388,15 +398,19 @@ export type ProductCliCommand =
388
398
  readonly credentialStore: "native";
389
399
  }
390
400
  | {
391
- readonly kind: "mail-domain-set";
392
- readonly projectId: string;
393
- readonly domain: string;
394
- readonly idempotencyKey: string;
395
- readonly credentialStore: "native";
396
- }
397
- | {
398
- readonly kind: "mail-domain-status";
399
- readonly projectId: string;
401
+ readonly kind: "managed-mail";
402
+ readonly request: ManagedMailCommand;
403
+ readonly label:
404
+ | "mail domain set"
405
+ | "mail domain status"
406
+ | "mail setup"
407
+ | "mail status"
408
+ | "mail webhook set"
409
+ | "mail webhook verify"
410
+ | "mail webhook disable"
411
+ | "mail messages list"
412
+ | "mail messages get"
413
+ | "mail messages retry";
400
414
  readonly credentialStore: "native";
401
415
  }
402
416
  | {
@@ -494,6 +508,7 @@ export type RecognizedCommandLabel =
494
508
  | "project status"
495
509
  | "project dev-access create"
496
510
  | "project handle check"
511
+ | "project handle set"
497
512
  | "database write"
498
513
  | "database query"
499
514
  | "database access create"
@@ -524,6 +539,14 @@ export type RecognizedCommandLabel =
524
539
  | "function runs"
525
540
  | "secret set"
526
541
  | "secret delete"
542
+ | "mail setup"
543
+ | "mail status"
544
+ | "mail webhook set"
545
+ | "mail webhook verify"
546
+ | "mail webhook disable"
547
+ | "mail messages list"
548
+ | "mail messages get"
549
+ | "mail messages retry"
527
550
  | "mail domain set"
528
551
  | "mail domain status";
529
552
 
@@ -585,6 +608,8 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
585
608
  if (value === "project" && values[0] === "status") return "project status";
586
609
  if (value === "project" && values[0] === "handle" && values[1] === "check")
587
610
  return "project handle check";
611
+ if (value === "project" && values[0] === "handle" && values[1] === "set")
612
+ return "project handle set";
588
613
  if (value === "project" && values[0] === "dev-access" && values[1] === "create")
589
614
  return "project dev-access create";
590
615
  if (value === "database" && values[0] === "compute" && values[1] === "set")
@@ -627,6 +652,20 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
627
652
  if (values[0] === "delete") return "secret delete";
628
653
  }
629
654
  if (value === "function" && values[0] === "runs") return "function runs";
655
+ if (value === "mail") {
656
+ if (values[0] === "setup") return "mail setup";
657
+ if (values[0] === "status") return "mail status";
658
+ if (values[0] === "webhook") {
659
+ if (values[1] === "set") return "mail webhook set";
660
+ if (values[1] === "verify") return "mail webhook verify";
661
+ if (values[1] === "disable") return "mail webhook disable";
662
+ }
663
+ if (values[0] === "messages") {
664
+ if (values[1] === "list") return "mail messages list";
665
+ if (values[1] === "get") return "mail messages get";
666
+ if (values[1] === "retry") return "mail messages retry";
667
+ }
668
+ }
630
669
  if (value === "mail" && values[0] === "domain") {
631
670
  if (values[1] === "set") return "mail domain set";
632
671
  if (values[1] === "status") return "mail domain status";
@@ -1054,6 +1093,24 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1054
1093
  }
1055
1094
  if (first === "project") {
1056
1095
  const subcommand = values.shift();
1096
+ if (subcommand === "handle" && values[0] === "set") {
1097
+ values.shift();
1098
+ if (yes || wait || follow || stdin) throw new InvalidCommandError();
1099
+ const options = parseOptions(values, [
1100
+ "--project",
1101
+ "--handle",
1102
+ "--if-match",
1103
+ "--idempotency-key",
1104
+ ]);
1105
+ return {
1106
+ kind: "project-handle-set",
1107
+ projectId: options["--project"],
1108
+ handle: options["--handle"],
1109
+ ifMatch: options["--if-match"],
1110
+ idempotencyKey: options["--idempotency-key"],
1111
+ credentialStore,
1112
+ };
1113
+ }
1057
1114
  if (subcommand === "handle" && values.shift() === "check") {
1058
1115
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1059
1116
  const options = parseOptions(values, ["--handle"]);
@@ -1694,34 +1751,82 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1694
1751
  }
1695
1752
  throw new InvalidCommandError();
1696
1753
  }
1697
- if (first === "mail" && values[0] === "domain" && values[1] === "set") {
1754
+ if (first === "mail") {
1698
1755
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1699
- values.splice(0, 2);
1700
- const options = parseOptions(values, ["--project", "--domain", "--idempotency-key"]);
1701
- if (
1702
- !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(
1703
- options["--domain"],
1704
- )
1705
- ) {
1706
- throw new InvalidCommandError();
1707
- }
1708
- return {
1709
- kind: "mail-domain-set",
1710
- projectId: options["--project"],
1711
- domain: options["--domain"],
1712
- idempotencyKey: options["--idempotency-key"],
1713
- credentialStore,
1714
- };
1715
- }
1716
- if (first === "mail" && values[0] === "domain" && values[1] === "status") {
1717
- if (yes || wait || follow || stdin) throw new InvalidCommandError();
1718
- values.splice(0, 2);
1719
- const options = parseOptions(values, ["--project"]);
1720
- return {
1721
- kind: "mail-domain-status",
1722
- projectId: options["--project"],
1723
- credentialStore,
1756
+ const area = values.shift();
1757
+ const sub =
1758
+ area === "webhook" || area === "messages" || area === "domain" ? values.shift() : undefined;
1759
+ const options = parseOptionalOptions(values, [
1760
+ "--project",
1761
+ "--environment",
1762
+ "--domain",
1763
+ "--sending",
1764
+ "--receiving",
1765
+ "--url",
1766
+ "--idempotency-key",
1767
+ "--message",
1768
+ "--after",
1769
+ ]);
1770
+ const required = (key: keyof typeof options) => {
1771
+ const value = options[key];
1772
+ if (!value) throw new InvalidCommandError();
1773
+ return value;
1724
1774
  };
1775
+ const scope = { projectId: required("--project"), environmentId: required("--environment") };
1776
+ let request: ManagedMailCommand,
1777
+ label: Extract<ProductCliCommand, { kind: "managed-mail" }>["label"];
1778
+ if (area === "setup" || (area === "domain" && sub === "set")) {
1779
+ const sending = required("--sending"),
1780
+ receiving = required("--receiving");
1781
+ if (!["true", "false"].includes(sending) || !["true", "false"].includes(receiving))
1782
+ throw new InvalidCommandError();
1783
+ request = {
1784
+ ...scope,
1785
+ action: "configure",
1786
+ domain: required("--domain"),
1787
+ sending: sending === "true",
1788
+ receiving: receiving === "true",
1789
+ idempotencyKey: required("--idempotency-key"),
1790
+ };
1791
+ label = area === "domain" ? "mail domain set" : "mail setup";
1792
+ } else if (area === "status" || (area === "domain" && sub === "status")) {
1793
+ request = { ...scope, action: "status" };
1794
+ label = area === "domain" ? "mail domain status" : "mail status";
1795
+ } else if (area === "webhook" && sub === "set") {
1796
+ request = {
1797
+ ...scope,
1798
+ action: "webhook_set",
1799
+ url: required("--url"),
1800
+ idempotencyKey: required("--idempotency-key"),
1801
+ };
1802
+ label = "mail webhook set";
1803
+ } else if (area === "webhook" && (sub === "verify" || sub === "disable")) {
1804
+ request = {
1805
+ ...scope,
1806
+ action: sub === "verify" ? "webhook_verify" : "webhook_disable",
1807
+ idempotencyKey: required("--idempotency-key"),
1808
+ };
1809
+ label = sub === "verify" ? "mail webhook verify" : "mail webhook disable";
1810
+ } else if (area === "messages" && sub === "list") {
1811
+ request = {
1812
+ ...scope,
1813
+ action: "messages_list",
1814
+ ...(options["--after"] ? { after: options["--after"] } : {}),
1815
+ };
1816
+ label = "mail messages list";
1817
+ } else if (area === "messages" && sub === "get") {
1818
+ request = { ...scope, action: "message_get", messageId: required("--message") };
1819
+ label = "mail messages get";
1820
+ } else if (area === "messages" && sub === "retry") {
1821
+ request = {
1822
+ ...scope,
1823
+ action: "message_retry",
1824
+ messageId: required("--message"),
1825
+ idempotencyKey: required("--idempotency-key"),
1826
+ };
1827
+ label = "mail messages retry";
1828
+ } else throw new InvalidCommandError();
1829
+ return { kind: "managed-mail", request, label, credentialStore };
1725
1830
  }
1726
1831
  throw new InvalidCommandError();
1727
1832
  };
@@ -429,9 +429,11 @@ const runOfflineInit = async (
429
429
  false,
430
430
  error instanceof RepositoryInitConflictError
431
431
  ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying."
432
- : error instanceof RepositoryInitError && error.code === "project_name_invalid"
433
- ? "Pass a lowercase deployment slug with '--project', then rerun init."
434
- : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
432
+ : error instanceof RepositoryInitError && error.code === "repository_root_required"
433
+ ? "Run ohmyhost init from the Git repository root with --root set to the application directory (for example --root website); commit the root ohmyhost.yaml before planning."
434
+ : error instanceof RepositoryInitError && error.code === "project_name_invalid"
435
+ ? "Pass a lowercase deployment slug with '--project', then rerun init."
436
+ : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
435
437
  );
436
438
  io.writeStdout(result.stdout);
437
439
  return result.exitCode;
@@ -1,3 +1,4 @@
1
+ import { executeManagedMail, type ManagedMailCommand } from "@ohmyhost/sdk-ts";
1
2
  import {
2
3
  assertProjectDatabaseAccess,
3
4
  assertProjectDatabaseAccessCredential,
@@ -53,7 +54,6 @@ import {
53
54
  type ProjectCreditBudget,
54
55
  applyPaidProjectDomain,
55
56
  configureProjectDomains,
56
- configureProjectMailDomain,
57
57
  configureProjectSourceAutoDeploy,
58
58
  createCloudflareDnsAuthorization,
59
59
  createProjectDevAccessTicket,
@@ -74,7 +74,6 @@ import {
74
74
  getAccountProfile,
75
75
  getCurrentIdentity,
76
76
  getDeployment,
77
- getProjectMailDomain,
78
77
  getProjectSourceAutoDeploy,
79
78
  getProjectStatus,
80
79
  getPaidProjectDomain,
@@ -83,6 +82,7 @@ import {
83
82
  listEnvironmentSecrets,
84
83
  listFunctionRuns,
85
84
  listProjects,
85
+ changeProjectHandle,
86
86
  getProjectHandleAvailability,
87
87
  planDeployment,
88
88
  planPaidProjectDomain,
@@ -112,7 +112,6 @@ import {
112
112
  type DeleteEnvironmentSecretResult,
113
113
  type OhMyHostClient,
114
114
  type ProviderReconciliationAttempt,
115
- type ProjectMailDomain,
116
115
  type CurrentIdentity,
117
116
  type DevAccessTicket,
118
117
  type ProjectPage,
@@ -211,6 +210,12 @@ export interface ProductApi {
211
210
  getAccountProfile(): Promise<readonly PublicWorkspace[]>;
212
211
  listProjects(input: { readonly cursor?: string; readonly limit: number }): Promise<ProjectPage>;
213
212
  checkProjectHandle(input: { readonly handle: string }): Promise<ProjectHandleAvailability>;
213
+ changeProjectHandle(input: {
214
+ readonly projectId: string;
215
+ readonly handle: string;
216
+ readonly ifMatch: string;
217
+ readonly idempotencyKey: string;
218
+ }): Promise<Operation>;
214
219
  getProjectStatus(projectId: string): Promise<ProjectStatus>;
215
220
  writeProjectDatabase(
216
221
  input: ProjectDatabaseWriteRequest & { projectId: string; idempotencyKey: string },
@@ -368,12 +373,7 @@ export interface ProductApi {
368
373
  readonly name: string;
369
374
  readonly idempotencyKey: string;
370
375
  }): Promise<DeleteEnvironmentSecretResult>;
371
- configureMailDomain(input: {
372
- readonly projectId: string;
373
- readonly domain: string;
374
- readonly idempotencyKey: string;
375
- }): Promise<ProjectMailDomain>;
376
- getMailDomain(projectId: string): Promise<ProjectMailDomain>;
376
+ managedMail?(input: ManagedMailCommand): Promise<unknown>;
377
377
  }
378
378
 
379
379
  export class GeneratedSdkProductApi implements ProductApi {
@@ -585,6 +585,23 @@ export class GeneratedSdkProductApi implements ProductApi {
585
585
  return listProjects(input, { client: this.#client });
586
586
  }
587
587
 
588
+ public async changeProjectHandle(input: {
589
+ readonly projectId: string;
590
+ readonly handle: string;
591
+ readonly ifMatch: string;
592
+ readonly idempotencyKey: string;
593
+ }): Promise<Operation> {
594
+ return changeProjectHandle(
595
+ {
596
+ project_id: input.projectId,
597
+ "Idempotency-Key": input.idempotencyKey,
598
+ "If-Match": input.ifMatch,
599
+ handle: input.handle,
600
+ },
601
+ { client: this.#client },
602
+ );
603
+ }
604
+
588
605
  public async checkProjectHandle(input: {
589
606
  readonly handle: string;
590
607
  }): Promise<ProjectHandleAvailability> {
@@ -1070,22 +1087,7 @@ export class GeneratedSdkProductApi implements ProductApi {
1070
1087
  );
1071
1088
  }
1072
1089
 
1073
- public async configureMailDomain(input: {
1074
- readonly projectId: string;
1075
- readonly domain: string;
1076
- readonly idempotencyKey: string;
1077
- }): Promise<ProjectMailDomain> {
1078
- return configureProjectMailDomain(
1079
- {
1080
- "Idempotency-Key": input.idempotencyKey,
1081
- project_id: input.projectId,
1082
- configureProjectMailDomainRequest: { domain: input.domain },
1083
- },
1084
- { client: this.#client },
1085
- );
1086
- }
1087
-
1088
- public async getMailDomain(projectId: string): Promise<ProjectMailDomain> {
1089
- return getProjectMailDomain({ project_id: projectId }, { client: this.#client });
1090
+ public managedMail(input: ManagedMailCommand) {
1091
+ return executeManagedMail(this.#client, input);
1090
1092
  }
1091
1093
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, open, readFile, readdir, stat } from "node:fs/promises";
3
- import { posix, resolve } from "node:path";
3
+ import { dirname, posix, resolve } from "node:path";
4
4
 
5
5
  import {
6
6
  normalizeApplicationRoot,
@@ -50,7 +50,7 @@ const MAXIMUM_SOURCE_BYTES = 2 * 1024 * 1024;
50
50
  const SOURCE_MODULE_PATTERN = /\.(?:[cm]?[jt]s|[jt]sx)$/u;
51
51
  const STORAGE_CLIENT_CALL_PATTERN = /\bcreatePrivateStorageClient\s*\(/u;
52
52
  // Both database mistakes of 2026-09-18 are visible in the source before anything is built: one read
53
- // a Hyperdrive connection string, the other opened a socket driver. Neither can work in a Worker, so
53
+ // a retired database binding, the other opened a socket driver. Neither can work in a customer Worker, so
54
54
  // naming them here costs a customer minutes instead of a deployment that fails its health check.
55
55
  const DATABASE_PRIVATE_BINDING_PATTERN =
56
56
  /\bHYPERDRIVE\b|\bconnectionString\b|\bDATABASE_URL\b|postgres(?:ql)?:\/\//u;
@@ -549,7 +549,7 @@ async function databaseContractBlockers(
549
549
  if (DATABASE_PRIVATE_BINDING_PATTERN.test(source)) {
550
550
  blockers.push({
551
551
  code: "database_binding_private",
552
- message: `${path} reaches for a managed database connection string. Hyperdrive, DATABASE_URL and postgres:// URLs are platform-private and never reach a customer Worker. Call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database".`,
552
+ message: `${path} reaches for a managed database connection string. HYPERDRIVE is retired; DATABASE_URL and postgres:// URLs never reach a customer Worker. Call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database".`,
553
553
  });
554
554
  }
555
555
  if (DATABASE_SOCKET_DRIVER_PATTERN.test(source)) {
@@ -876,6 +876,30 @@ async function assertRepositoryRoot(root: string): Promise<void> {
876
876
  if (!information.isDirectory()) {
877
877
  throw new RepositoryInitError("repository_not_directory", "Repository path is not a directory");
878
878
  }
879
+ // A Git worktree uses a .git file; never read or follow that file's target.
880
+ for (let ancestor = root; ; ancestor = dirname(ancestor)) {
881
+ let marker;
882
+ try {
883
+ marker = await lstat(resolve(ancestor, ".git"));
884
+ } catch (error) {
885
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
886
+ throw new RepositoryInitError(
887
+ "repository_unreadable",
888
+ "Could not determine the Git repository root",
889
+ );
890
+ }
891
+ }
892
+ if (marker !== undefined) {
893
+ if (ancestor !== root) {
894
+ throw new RepositoryInitError(
895
+ "repository_root_required",
896
+ "Run ohmyhost init from the Git repository root and select the application directory with --root; ohmyhost.yaml must be at the repository root.",
897
+ );
898
+ }
899
+ return;
900
+ }
901
+ if (dirname(ancestor) === ancestor) return;
902
+ }
879
903
  }
880
904
 
881
905
  async function collectRepositoryFiles(root: string): Promise<string[]> {