@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.
@@ -200,6 +200,7 @@ export type ProductCliCommand =
200
200
  readonly organizationId: string;
201
201
  readonly name: string;
202
202
  readonly dataMode?: "shared" | "isolated";
203
+ readonly devAccessMode?: "protected" | "public";
203
204
  readonly region?: "us" | "eu";
204
205
  readonly idempotencyKey: string;
205
206
  readonly credentialStore: "native";
@@ -215,6 +216,30 @@ export type ProductCliCommand =
215
216
  readonly projectId: string;
216
217
  readonly credentialStore: "native";
217
218
  }
219
+ | {
220
+ readonly kind: "project-dev-share-link";
221
+ readonly projectId: string;
222
+ readonly credentialStore: "native";
223
+ }
224
+ | {
225
+ readonly kind: "project-dev-share-rotate";
226
+ readonly projectId: string;
227
+ readonly idempotencyKey: string;
228
+ readonly credentialStore: "native";
229
+ }
230
+ | {
231
+ readonly kind: "project-dev-share-revoke";
232
+ readonly projectId: string;
233
+ readonly idempotencyKey: string;
234
+ readonly credentialStore: "native";
235
+ }
236
+ | {
237
+ readonly kind: "project-dev-access-mode-set";
238
+ readonly projectId: string;
239
+ readonly mode: "protected" | "public";
240
+ readonly idempotencyKey: string;
241
+ readonly credentialStore: "native";
242
+ }
218
243
  | {
219
244
  readonly kind: "project-dev-access-create";
220
245
  readonly projectId: string;
@@ -294,12 +319,16 @@ export type ProductCliCommand =
294
319
  readonly kind: "plan";
295
320
  readonly projectId: string;
296
321
  readonly commitSha: string;
322
+ readonly environment: "dev" | "prod";
297
323
  readonly credentialStore: "native";
298
324
  }
299
325
  | {
300
326
  readonly kind: "deploy";
301
327
  readonly projectId: string;
302
- readonly planId: string;
328
+ /** Exactly one of an existing plan or a commit that is planned first. */
329
+ readonly planId: string | null;
330
+ readonly commitSha: string | null;
331
+ readonly environment: "dev" | "prod";
303
332
  readonly idempotencyKey: string;
304
333
  readonly yes: true;
305
334
  readonly wait: boolean;
@@ -403,6 +432,7 @@ export type ProductCliCommand =
403
432
  readonly label:
404
433
  | "mail domain set"
405
434
  | "mail domain status"
435
+ | "mail domain delete"
406
436
  | "mail setup"
407
437
  | "mail status"
408
438
  | "mail webhook set"
@@ -507,6 +537,10 @@ export type RecognizedCommandLabel =
507
537
  | "project list"
508
538
  | "project status"
509
539
  | "project dev-access create"
540
+ | "project dev-share link"
541
+ | "project dev-share rotate"
542
+ | "project dev-share revoke"
543
+ | "project dev-access mode"
510
544
  | "project handle check"
511
545
  | "project handle set"
512
546
  | "database write"
@@ -548,7 +582,8 @@ export type RecognizedCommandLabel =
548
582
  | "mail messages get"
549
583
  | "mail messages retry"
550
584
  | "mail domain set"
551
- | "mail domain status";
585
+ | "mail domain status"
586
+ | "mail domain delete";
552
587
 
553
588
  export const recognizedCommandLabel = (argv: readonly string[]): RecognizedCommandLabel => {
554
589
  const values = [...argv];
@@ -610,6 +645,18 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
610
645
  return "project handle check";
611
646
  if (value === "project" && values[0] === "handle" && values[1] === "set")
612
647
  return "project handle set";
648
+ if (
649
+ value === "project" &&
650
+ values[0] === "dev-share" &&
651
+ ["link", "rotate", "revoke"].includes(values[1] ?? "")
652
+ )
653
+ return values[1] === "link"
654
+ ? "project dev-share link"
655
+ : values[1] === "rotate"
656
+ ? "project dev-share rotate"
657
+ : "project dev-share revoke";
658
+ if (value === "project" && values[0] === "dev-access" && values[1] === "mode")
659
+ return "project dev-access mode";
613
660
  if (value === "project" && values[0] === "dev-access" && values[1] === "create")
614
661
  return "project dev-access create";
615
662
  if (value === "database" && values[0] === "compute" && values[1] === "set")
@@ -669,6 +716,7 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
669
716
  if (value === "mail" && values[0] === "domain") {
670
717
  if (values[1] === "set") return "mail domain set";
671
718
  if (values[1] === "status") return "mail domain status";
719
+ if (values[1] === "delete") return "mail domain delete";
672
720
  }
673
721
  return "unknown";
674
722
  }
@@ -1116,6 +1164,38 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1116
1164
  const options = parseOptions(values, ["--handle"]);
1117
1165
  return { kind: "project-handle-check", handle: options["--handle"], credentialStore };
1118
1166
  }
1167
+ if (subcommand === "dev-share") {
1168
+ const action = values.shift();
1169
+ if (action === "link") {
1170
+ if (yes || wait || follow || stdin) throw new InvalidCommandError();
1171
+ const options = parseOptions(values, ["--project"]);
1172
+ return { kind: "project-dev-share-link", projectId: options["--project"], credentialStore };
1173
+ }
1174
+ if ((action === "rotate" || action === "revoke") && yes && !wait && !follow && !stdin) {
1175
+ const options = parseOptions(values, ["--project", "--idempotency-key"]);
1176
+ return {
1177
+ kind: action === "rotate" ? "project-dev-share-rotate" : "project-dev-share-revoke",
1178
+ projectId: options["--project"],
1179
+ idempotencyKey: options["--idempotency-key"],
1180
+ credentialStore,
1181
+ };
1182
+ }
1183
+ throw new InvalidCommandError();
1184
+ }
1185
+ if (subcommand === "dev-access" && values[0] === "mode") {
1186
+ values.shift();
1187
+ if (!yes || wait || follow || stdin) throw new InvalidCommandError();
1188
+ const options = parseOptions(values, ["--project", "--mode", "--idempotency-key"]);
1189
+ if (options["--mode"] !== "protected" && options["--mode"] !== "public")
1190
+ throw new InvalidCommandError();
1191
+ return {
1192
+ kind: "project-dev-access-mode-set",
1193
+ projectId: options["--project"],
1194
+ mode: options["--mode"],
1195
+ idempotencyKey: options["--idempotency-key"],
1196
+ credentialStore,
1197
+ };
1198
+ }
1119
1199
  if (subcommand === "dev-access" && values.shift() === "create") {
1120
1200
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1121
1201
  const options = parseOptions(values, ["--project"]);
@@ -1177,15 +1257,20 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1177
1257
  "--name",
1178
1258
  "--idempotency-key",
1179
1259
  "--data-mode",
1260
+ "--dev-access-mode",
1180
1261
  "--region",
1181
1262
  ]);
1182
1263
  const dataMode = options["--data-mode"];
1264
+ const devAccessMode = options["--dev-access-mode"];
1183
1265
  const region = options["--region"];
1184
1266
  if (
1185
1267
  !options["--organization"] ||
1186
1268
  !options["--name"] ||
1187
1269
  !options["--idempotency-key"] ||
1188
1270
  (dataMode !== undefined && dataMode !== "shared" && dataMode !== "isolated") ||
1271
+ (devAccessMode !== undefined &&
1272
+ devAccessMode !== "protected" &&
1273
+ devAccessMode !== "public") ||
1189
1274
  (region !== undefined && region !== "us" && region !== "eu")
1190
1275
  )
1191
1276
  throw new InvalidCommandError();
@@ -1194,6 +1279,7 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1194
1279
  organizationId: options["--organization"],
1195
1280
  name: options["--name"],
1196
1281
  ...(dataMode === undefined ? {} : { dataMode }),
1282
+ ...(devAccessMode === undefined ? {} : { devAccessMode }),
1197
1283
  ...(region === undefined ? {} : { region }),
1198
1284
  idempotencyKey: options["--idempotency-key"],
1199
1285
  credentialStore,
@@ -1465,22 +1551,46 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1465
1551
  }
1466
1552
  if (first === "plan") {
1467
1553
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1468
- const options = parseOptions(values, ["--project", "--commit"]);
1554
+ const options = parseOptionalOptions(values, ["--project", "--commit", "--environment"]);
1555
+ const projectId = options["--project"];
1556
+ const commitSha = options["--commit"];
1557
+ if (projectId === undefined || commitSha === undefined) throw new InvalidCommandError();
1469
1558
  return {
1470
1559
  kind: "plan",
1471
- projectId: options["--project"],
1472
- commitSha: options["--commit"],
1560
+ projectId,
1561
+ commitSha,
1562
+ environment: deploymentEnvironment(options["--environment"]),
1473
1563
  credentialStore,
1474
1564
  };
1475
1565
  }
1476
1566
  if (first === "deploy") {
1477
1567
  if (!yes || follow || stdin) throw new InvalidCommandError();
1478
- const options = parseOptions(values, ["--project", "--plan-id", "--idempotency-key"]);
1568
+ const options = parseOptionalOptions(values, [
1569
+ "--project",
1570
+ "--plan-id",
1571
+ "--commit",
1572
+ "--environment",
1573
+ "--idempotency-key",
1574
+ ]);
1575
+ const projectId = options["--project"];
1576
+ const idempotencyKey = options["--idempotency-key"];
1577
+ const planId = options["--plan-id"] ?? null;
1578
+ const commitSha = options["--commit"] ?? null;
1579
+ // A plan already names its environment, so --environment only accompanies --commit.
1580
+ if (
1581
+ projectId === undefined ||
1582
+ idempotencyKey === undefined ||
1583
+ (planId === null) === (commitSha === null) ||
1584
+ (planId !== null && options["--environment"] !== undefined)
1585
+ )
1586
+ throw new InvalidCommandError();
1479
1587
  return {
1480
1588
  kind: "deploy",
1481
- projectId: options["--project"],
1482
- planId: options["--plan-id"],
1483
- idempotencyKey: options["--idempotency-key"],
1589
+ projectId,
1590
+ planId,
1591
+ commitSha,
1592
+ environment: deploymentEnvironment(options["--environment"]),
1593
+ idempotencyKey,
1484
1594
  yes,
1485
1595
  wait,
1486
1596
  credentialStore,
@@ -1752,10 +1862,12 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1752
1862
  throw new InvalidCommandError();
1753
1863
  }
1754
1864
  if (first === "mail") {
1755
- if (yes || wait || follow || stdin) throw new InvalidCommandError();
1756
1865
  const area = values.shift();
1757
1866
  const sub =
1758
1867
  area === "webhook" || area === "messages" || area === "domain" ? values.shift() : undefined;
1868
+ // Only retiring the mail domain is destructive, and it requires --yes.
1869
+ const retiring = area === "domain" && sub === "delete";
1870
+ if (yes !== retiring || wait || follow || stdin) throw new InvalidCommandError();
1759
1871
  const options = parseOptionalOptions(values, [
1760
1872
  "--project",
1761
1873
  "--environment",
@@ -1792,6 +1904,13 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1792
1904
  } else if (area === "status" || (area === "domain" && sub === "status")) {
1793
1905
  request = { ...scope, action: "status" };
1794
1906
  label = area === "domain" ? "mail domain status" : "mail status";
1907
+ } else if (retiring) {
1908
+ request = {
1909
+ ...scope,
1910
+ action: "domain_delete",
1911
+ idempotencyKey: required("--idempotency-key"),
1912
+ };
1913
+ label = "mail domain delete";
1795
1914
  } else if (area === "webhook" && sub === "set") {
1796
1915
  request = {
1797
1916
  ...scope,
@@ -1857,6 +1976,12 @@ const parseOptions = <T extends string>(
1857
1976
  return result;
1858
1977
  };
1859
1978
 
1979
+ const deploymentEnvironment = (value: string | undefined): "dev" | "prod" => {
1980
+ if (value === undefined || value === "dev") return "dev";
1981
+ if (value === "prod") return "prod";
1982
+ throw new InvalidCommandError();
1983
+ };
1984
+
1860
1985
  const parseOptionalOptions = <T extends string>(
1861
1986
  values: readonly string[],
1862
1987
  allowed: readonly T[],
@@ -44,7 +44,7 @@ import {
44
44
  } from "./credential-store.js";
45
45
  import { GeneratedSdkProductApi, type ProductApi } from "./product-api.js";
46
46
  import { workspaceOf, type WorkspaceSession } from "./workspace.js";
47
- import { OwnerOnlyProjectLinkStore } from "./project-link-store.js";
47
+ import { OwnerOnlyProjectLinkStore, readLinkedProjectId } from "./project-link-store.js";
48
48
  import {
49
49
  initializeRepository,
50
50
  RepositoryInitBlockedError,
@@ -67,6 +67,8 @@ export interface ProductCliIo {
67
67
 
68
68
  export interface ProductCliCompositionOverrides {
69
69
  readonly credentialStore?: CredentialStore;
70
+ /** Directory whose project link supplies a missing --project; defaults to the process directory. */
71
+ readonly workingDirectory?: string;
70
72
  readonly readSecretValue?: (signal: AbortSignal) => Promise<string>;
71
73
  }
72
74
 
@@ -221,7 +223,16 @@ export const runProductionCli = async (
221
223
  const diagnostics = new StderrDiagnosticSink(io);
222
224
  let command: ProductCliCommand;
223
225
  try {
224
- command = parseProductCliCommand(argv);
226
+ const parsed = await parseWithLinkedProject(argv, async () => {
227
+ const profile = resolveProductCliEnvironment(environment);
228
+ return readLinkedProjectId(
229
+ join(overrides.workingDirectory ?? process.cwd(), profile.linkDirectory),
230
+ profile.apiOrigin,
231
+ );
232
+ });
233
+ command = parsed.command;
234
+ if (parsed.linkedProjectId !== null)
235
+ io.writeStderr(`ohmyhost: using linked project ${parsed.linkedProjectId}\n`);
225
236
  } catch (error) {
226
237
  const result =
227
238
  error instanceof InvalidCommandError
@@ -344,7 +355,10 @@ export const runProductionCli = async (
344
355
  signal,
345
356
  },
346
357
  ),
347
- projectLinkStore: new OwnerOnlyProjectLinkStore(join(process.cwd(), profile.linkDirectory)),
358
+ projectLinkStore: new OwnerOnlyProjectLinkStore(
359
+ join(process.cwd(), profile.linkDirectory),
360
+ process.cwd(),
361
+ ),
348
362
  productApiOrigin: profile.apiOrigin,
349
363
  tokenRefresher: new PublicWorkosTokenRefresher(environment),
350
364
  tokenRevoker: lifecycle,
@@ -588,6 +602,36 @@ export class GeneratedSdkSessionRevoker implements TokenRevoker {
588
602
  }
589
603
  }
590
604
 
605
+ /**
606
+ * A command run inside a linked directory may omit --project; the link supplies it. The
607
+ * link is read only when the command needs a project it did not name.
608
+ */
609
+ export async function parseWithLinkedProject(
610
+ argv: readonly string[],
611
+ linkedProjectId: () => Promise<string | null>,
612
+ ): Promise<{ readonly command: ProductCliCommand; readonly linkedProjectId: string | null }> {
613
+ try {
614
+ return { command: parseProductCliCommand(argv), linkedProjectId: null };
615
+ } catch (error) {
616
+ if (!(error instanceof InvalidCommandError) || argv.includes("--project")) throw error;
617
+ let projectId: string | null;
618
+ try {
619
+ projectId = await linkedProjectId();
620
+ } catch {
621
+ projectId = null;
622
+ }
623
+ if (projectId === null) throw error;
624
+ try {
625
+ return {
626
+ command: parseProductCliCommand([...argv, "--project", projectId]),
627
+ linkedProjectId: projectId,
628
+ };
629
+ } catch {
630
+ throw error;
631
+ }
632
+ }
633
+ }
634
+
591
635
  class StderrDiagnosticSink implements DiagnosticSink {
592
636
  public constructor(private readonly io: ProductCliIo) {}
593
637
 
@@ -11,6 +11,7 @@ export {
11
11
  parseAccountProfile,
12
12
  parseCurrentIdentity,
13
13
  parseDevAccessTicket,
14
+ parseDevAccessState,
14
15
  parseCloudflareDnsAuthorization,
15
16
  parseCloudflareDnsAuthorizationStatus,
16
17
  parsePaidDomain,
@@ -57,6 +57,10 @@ import {
57
57
  configureProjectSourceAutoDeploy,
58
58
  createCloudflareDnsAuthorization,
59
59
  createProjectDevAccessTicket,
60
+ ensureProjectDevShareLink,
61
+ rotateProjectDevShareLink,
62
+ revokeProjectDevShareLink,
63
+ setProjectDevAccessMode,
60
64
  createDeployment,
61
65
  createProject,
62
66
  submitFeedback,
@@ -131,6 +135,7 @@ export interface ProjectCreateInput {
131
135
  readonly organizationId: string;
132
136
  readonly name: string;
133
137
  readonly dataMode?: "shared" | "isolated";
138
+ readonly devAccessMode?: "protected" | "public";
134
139
  readonly region?: "us" | "eu";
135
140
  readonly idempotencyKey: string;
136
141
  }
@@ -257,6 +262,20 @@ export interface ProductApi {
257
262
  readonly idempotencyKey: string;
258
263
  }): Promise<PaidDomain>;
259
264
  createDevAccessTicket(projectId: string): Promise<DevAccessTicket>;
265
+ ensureDevShareLink(projectId: string): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
266
+ rotateDevShareLink(
267
+ projectId: string,
268
+ idempotencyKey: string,
269
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
270
+ revokeDevShareLink(
271
+ projectId: string,
272
+ idempotencyKey: string,
273
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
274
+ setDevAccessMode(
275
+ projectId: string,
276
+ mode: "protected" | "public",
277
+ idempotencyKey: string,
278
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
260
279
  submitFeedback(input: {
261
280
  report: import("@ohmyhost/sdk-ts").FeedbackSubmission;
262
281
  idempotencyKey: string;
@@ -298,6 +317,7 @@ export interface ProductApi {
298
317
  planDeployment(input: {
299
318
  readonly projectId: string;
300
319
  readonly commitSha: string;
320
+ readonly environment: "dev" | "prod";
301
321
  }): Promise<DeploymentPlan>;
302
322
  createDeployment(input: {
303
323
  readonly projectId: string;
@@ -736,6 +756,35 @@ export class GeneratedSdkProductApi implements ProductApi {
736
756
  );
737
757
  }
738
758
 
759
+ public async ensureDevShareLink(projectId: string) {
760
+ return ensureProjectDevShareLink({ project_id: projectId }, { client: this.#client });
761
+ }
762
+ public async rotateDevShareLink(projectId: string, idempotencyKey: string) {
763
+ return rotateProjectDevShareLink(
764
+ { project_id: projectId, "Idempotency-Key": idempotencyKey },
765
+ { client: this.#client },
766
+ );
767
+ }
768
+ public async revokeDevShareLink(projectId: string, idempotencyKey: string) {
769
+ return revokeProjectDevShareLink(
770
+ { project_id: projectId, "Idempotency-Key": idempotencyKey },
771
+ { client: this.#client },
772
+ );
773
+ }
774
+ public async setDevAccessMode(
775
+ projectId: string,
776
+ mode: "protected" | "public",
777
+ idempotencyKey: string,
778
+ ) {
779
+ return setProjectDevAccessMode(
780
+ {
781
+ project_id: projectId,
782
+ "Idempotency-Key": idempotencyKey,
783
+ setDevAccessModeRequest: { mode },
784
+ },
785
+ { client: this.#client },
786
+ );
787
+ }
739
788
  public async createDevAccessTicket(projectId: string): Promise<DevAccessTicket> {
740
789
  return createProjectDevAccessTicket({ project_id: projectId }, { client: this.#client });
741
790
  }
@@ -760,6 +809,7 @@ export class GeneratedSdkProductApi implements ProductApi {
760
809
  organization_id: input.organizationId,
761
810
  name: input.name,
762
811
  ...(input.dataMode === undefined ? {} : { data_mode: input.dataMode }),
812
+ ...(input.devAccessMode === undefined ? {} : { dev_access_mode: input.devAccessMode }),
763
813
  ...(input.region === undefined ? {} : { region: input.region }),
764
814
  },
765
815
  },
@@ -880,9 +930,13 @@ export class GeneratedSdkProductApi implements ProductApi {
880
930
  public async planDeployment(input: {
881
931
  readonly projectId: string;
882
932
  readonly commitSha: string;
933
+ readonly environment: "dev" | "prod";
883
934
  }): Promise<DeploymentPlan> {
884
935
  return planDeployment(
885
- { project_id: input.projectId, planDeploymentRequest: { commit_sha: input.commitSha } },
936
+ {
937
+ project_id: input.projectId,
938
+ planDeploymentRequest: { commit_sha: input.commitSha, environment: input.environment },
939
+ },
886
940
  { client: this.#client },
887
941
  );
888
942
  }
@@ -1,4 +1,4 @@
1
- import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
1
+ import { appendFile, chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  export interface ProjectLinkMetadata {
@@ -14,8 +14,32 @@ export interface ProjectLinkStore {
14
14
  save(metadata: ProjectLinkMetadata): Promise<void>;
15
15
  }
16
16
 
17
+ /** The project this directory was linked to for this API, or null when there is no usable link. */
18
+ export async function readLinkedProjectId(
19
+ directory: string,
20
+ apiOrigin: string,
21
+ ): Promise<string | null> {
22
+ try {
23
+ const link = JSON.parse(await readFile(join(directory, "link.json"), "utf8")) as unknown;
24
+ if (typeof link !== "object" || link === null) return null;
25
+ const { version, api_origin, project_id } = link as Record<string, unknown>;
26
+ return version === 1 &&
27
+ api_origin === apiOrigin &&
28
+ typeof project_id === "string" &&
29
+ /^[0-9A-HJKMNP-TV-Z]{26}$/u.test(project_id)
30
+ ? project_id
31
+ : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
17
37
  export class OwnerOnlyProjectLinkStore implements ProjectLinkStore {
18
- public constructor(private readonly directory: string) {}
38
+ /** repositoryRoot: when it is a Git working tree, the link directory is kept out of Git. */
39
+ public constructor(
40
+ private readonly directory: string,
41
+ private readonly repositoryRoot?: string,
42
+ ) {}
19
43
 
20
44
  public async save(metadata: ProjectLinkMetadata): Promise<void> {
21
45
  await mkdir(this.directory, { mode: 0o700, recursive: true });
@@ -26,5 +50,24 @@ export class OwnerOnlyProjectLinkStore implements ProjectLinkStore {
26
50
  await chmod(temporary, 0o600);
27
51
  await rename(temporary, target);
28
52
  await chmod(target, 0o600);
53
+ if (this.repositoryRoot !== undefined) await ignoreLinkDirectory(this.repositoryRoot);
54
+ }
55
+ }
56
+
57
+ async function ignoreLinkDirectory(root: string): Promise<void> {
58
+ try {
59
+ await stat(join(root, ".git"));
60
+ } catch {
61
+ return;
62
+ }
63
+ const file = join(root, ".gitignore");
64
+ let current = "";
65
+ try {
66
+ current = await readFile(file, "utf8");
67
+ } catch {
68
+ current = "";
29
69
  }
70
+ const lines = current.split(/\r?\n/u).map((line) => line.trim());
71
+ if (lines.some((line) => /^\/?\.ohmyhost\/?$/u.test(line))) return;
72
+ await appendFile(file, `${current === "" || current.endsWith("\n") ? "" : "\n"}/.ohmyhost/\n`);
30
73
  }
@@ -339,7 +339,8 @@ export async function initializeRepository(
339
339
  }),
340
340
  });
341
341
  const targetBetterAuth = existingConfiguration?.auth?.provider === "better-auth";
342
- const targetMail = targetBetterAuth || existingConfiguration?.mail?.enabled === true;
342
+ // Mail stays an explicit choice: Better Auth or a detected mail package never enables it.
343
+ const targetMail = existingConfiguration?.mail?.enabled === true;
343
344
  const targetStorage = r2 || supabaseStorage;
344
345
  const configuredCompanionRequired =
345
346
  existingConfiguration !== null &&