@amerged/ohmyhost-mcp 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.
@@ -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[],
@@ -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,
@@ -24,7 +24,7 @@ import {
24
24
  type UserApiKeyCreation,
25
25
  type UserApiKeyPage,
26
26
  } from "@ohmyhost/contracts/user-api-keys";
27
- import { parseFeedbackReceipt } from "@ohmyhost/contracts/feedback";
27
+ import { parseFeedbackReceipt, parseFeedbackStatus } from "@ohmyhost/contracts/feedback";
28
28
  import {
29
29
  getProjectContext,
30
30
  setProjectNotes,
@@ -57,9 +57,14 @@ 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,
67
+ getFeedback,
63
68
  createOrganization,
64
69
  type Organization,
65
70
  type GithubConnectionAuthorization,
@@ -131,6 +136,7 @@ export interface ProjectCreateInput {
131
136
  readonly organizationId: string;
132
137
  readonly name: string;
133
138
  readonly dataMode?: "shared" | "isolated";
139
+ readonly devAccessMode?: "protected" | "public";
134
140
  readonly region?: "us" | "eu";
135
141
  readonly idempotencyKey: string;
136
142
  }
@@ -257,10 +263,28 @@ export interface ProductApi {
257
263
  readonly idempotencyKey: string;
258
264
  }): Promise<PaidDomain>;
259
265
  createDevAccessTicket(projectId: string): Promise<DevAccessTicket>;
266
+ ensureDevShareLink(projectId: string): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
267
+ rotateDevShareLink(
268
+ projectId: string,
269
+ idempotencyKey: string,
270
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
271
+ revokeDevShareLink(
272
+ projectId: string,
273
+ idempotencyKey: string,
274
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
275
+ setDevAccessMode(
276
+ projectId: string,
277
+ mode: "protected" | "public",
278
+ idempotencyKey: string,
279
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
260
280
  submitFeedback(input: {
261
281
  report: import("@ohmyhost/sdk-ts").FeedbackSubmission;
262
282
  idempotencyKey: string;
263
283
  }): Promise<import("@ohmyhost/sdk-ts").FeedbackReceipt>;
284
+ getFeedback(
285
+ feedbackId: string,
286
+ cursor?: string,
287
+ ): Promise<import("@ohmyhost/sdk-ts").FeedbackStatus>;
264
288
  createProject(input: ProjectCreateInput): Promise<Operation>;
265
289
  getOperation(operationId: string): Promise<Operation>;
266
290
  reconcileOperation(input: {
@@ -298,6 +322,7 @@ export interface ProductApi {
298
322
  planDeployment(input: {
299
323
  readonly projectId: string;
300
324
  readonly commitSha: string;
325
+ readonly environment: "dev" | "prod";
301
326
  }): Promise<DeploymentPlan>;
302
327
  createDeployment(input: {
303
328
  readonly projectId: string;
@@ -736,6 +761,35 @@ export class GeneratedSdkProductApi implements ProductApi {
736
761
  );
737
762
  }
738
763
 
764
+ public async ensureDevShareLink(projectId: string) {
765
+ return ensureProjectDevShareLink({ project_id: projectId }, { client: this.#client });
766
+ }
767
+ public async rotateDevShareLink(projectId: string, idempotencyKey: string) {
768
+ return rotateProjectDevShareLink(
769
+ { project_id: projectId, "Idempotency-Key": idempotencyKey },
770
+ { client: this.#client },
771
+ );
772
+ }
773
+ public async revokeDevShareLink(projectId: string, idempotencyKey: string) {
774
+ return revokeProjectDevShareLink(
775
+ { project_id: projectId, "Idempotency-Key": idempotencyKey },
776
+ { client: this.#client },
777
+ );
778
+ }
779
+ public async setDevAccessMode(
780
+ projectId: string,
781
+ mode: "protected" | "public",
782
+ idempotencyKey: string,
783
+ ) {
784
+ return setProjectDevAccessMode(
785
+ {
786
+ project_id: projectId,
787
+ "Idempotency-Key": idempotencyKey,
788
+ setDevAccessModeRequest: { mode },
789
+ },
790
+ { client: this.#client },
791
+ );
792
+ }
739
793
  public async createDevAccessTicket(projectId: string): Promise<DevAccessTicket> {
740
794
  return createProjectDevAccessTicket({ project_id: projectId }, { client: this.#client });
741
795
  }
@@ -752,6 +806,18 @@ export class GeneratedSdkProductApi implements ProductApi {
752
806
  );
753
807
  }
754
808
 
809
+ public async getFeedback(
810
+ feedbackId: string,
811
+ cursor?: string,
812
+ ): Promise<import("@ohmyhost/sdk-ts").FeedbackStatus> {
813
+ return parseFeedbackStatus(
814
+ await getFeedback(
815
+ { feedback_id: feedbackId, ...(cursor === undefined ? {} : { cursor }) },
816
+ { client: this.#client },
817
+ ),
818
+ ) as import("@ohmyhost/sdk-ts").FeedbackStatus;
819
+ }
820
+
755
821
  public async createProject(input: ProjectCreateInput): Promise<Operation> {
756
822
  return createProject(
757
823
  {
@@ -760,6 +826,7 @@ export class GeneratedSdkProductApi implements ProductApi {
760
826
  organization_id: input.organizationId,
761
827
  name: input.name,
762
828
  ...(input.dataMode === undefined ? {} : { data_mode: input.dataMode }),
829
+ ...(input.devAccessMode === undefined ? {} : { dev_access_mode: input.devAccessMode }),
763
830
  ...(input.region === undefined ? {} : { region: input.region }),
764
831
  },
765
832
  },
@@ -880,9 +947,13 @@ export class GeneratedSdkProductApi implements ProductApi {
880
947
  public async planDeployment(input: {
881
948
  readonly projectId: string;
882
949
  readonly commitSha: string;
950
+ readonly environment: "dev" | "prod";
883
951
  }): Promise<DeploymentPlan> {
884
952
  return planDeployment(
885
- { project_id: input.projectId, planDeploymentRequest: { commit_sha: input.commitSha } },
953
+ {
954
+ project_id: input.projectId,
955
+ planDeploymentRequest: { commit_sha: input.commitSha, environment: input.environment },
956
+ },
886
957
  { client: this.#client },
887
958
  );
888
959
  }
@@ -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
  }
@@ -73,9 +73,9 @@ const STORAGE_RUNTIME_BINDINGS = Object.freeze([
73
73
  "OHMYHOST_STORAGE_GATEWAY_URL",
74
74
  "OHMYHOST_STORAGE_KEY",
75
75
  ]);
76
- // `@ohmyhost/customer-runtime` is private and resolves from no registry; report the release tarball
77
- // of this client so a bare package name never reaches a customer.
78
- const CUSTOMER_RUNTIME_SOURCE = `https://ohmyho.st/releases/${cliPackage.version}/ohmyhost-customer-runtime-${cliPackage.version}.tgz`;
76
+ // The runtime release is published unchanged as `@amerged/ohmyhost-runtime`; this pinned alias keeps
77
+ // its import name and, unlike the website archive each release removes, stays installable.
78
+ const CUSTOMER_RUNTIME_SOURCE = `npm:@amerged/ohmyhost-runtime@${cliPackage.version}`;
79
79
  const SCHEDULED_HANDLER_PATTERN = /\bscheduled\s*(?:\(|:|,|\})/u;
80
80
  // The platform imports the module's default export; handlers that are only named exports are
81
81
  // never invoked, so init blocks them before a build or a runtime rejection would.
@@ -139,7 +139,7 @@ export interface RepositoryInitInventory {
139
139
 
140
140
  export interface RepositoryInitRuntimePackages {
141
141
  readonly betterAuth?: "1.7.1";
142
- /** Exact install source: the package is private and cannot be resolved from a registry. */
142
+ /** Exact install source: an npm alias pinned to the published runtime of this client version. */
143
143
  readonly customerRuntime?: string;
144
144
  }
145
145
 
@@ -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 &&