@amerged/ohmyhost-mcp 0.1.23 → 0.1.24

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.
Files changed (87) hide show
  1. package/README.md +5 -2
  2. package/dist/{chunk-P44YTAKK.js → chunk-EEFUMPKG.js} +25 -24
  3. package/dist/index.js +1 -1
  4. package/dist/stdio-main.js +731 -266
  5. package/package.json +1 -1
  6. package/src/apps/mcp/package.json +47 -0
  7. package/src/apps/mcp/{local-client.ts → src/local-client.ts} +69 -12
  8. package/src/apps/mcp/{local-server.ts → src/local-server.ts} +324 -142
  9. package/src/apps/product-cli/package.json +45 -0
  10. package/src/apps/product-cli/{bin.ts → src/bin.ts} +3 -0
  11. package/src/apps/product-cli/{cli.ts → src/cli.ts} +363 -87
  12. package/src/apps/product-cli/{command.ts → src/command.ts} +58 -11
  13. package/src/apps/product-cli/{composition.ts → src/composition.ts} +98 -21
  14. package/src/apps/product-cli/{index.ts → src/index.ts} +1 -0
  15. package/src/apps/product-cli/{product-api.ts → src/product-api.ts} +65 -19
  16. package/src/apps/product-cli/{repository-init.ts → src/repository-init.ts} +79 -29
  17. package/src/apps/product-cli/{runtime-contract.ts → src/runtime-contract.ts} +16 -309
  18. package/src/apps/product-cli/{user-token-file.ts → src/user-token-file.ts} +46 -35
  19. package/src/apps/product-cli/{workspace.ts → src/workspace.ts} +6 -0
  20. package/src/packages/agent-skills/package.json +24 -0
  21. package/src/packages/agent-skills/src/generated-skill-resources.ts +165 -0
  22. package/src/packages/agent-skills/{index.ts → src/index.ts} +1 -1
  23. package/src/packages/contracts/package.json +137 -0
  24. package/src/packages/contracts/src/browser-capabilities.ts +133 -0
  25. package/src/packages/contracts/src/customer-auth-admission.ts +36 -0
  26. package/src/packages/contracts/{database-access.ts → src/database-access.ts} +10 -2
  27. package/src/packages/contracts/{framework-admission.ts → src/framework-admission.ts} +88 -13
  28. package/src/packages/contracts/src/framework-build-script.mjs +22 -0
  29. package/src/packages/contracts/src/framework-build-script.ts +1 -0
  30. package/src/packages/contracts/{index.ts → src/index.ts} +17 -16
  31. package/src/packages/contracts/src/operation-data-change-result.ts +86 -0
  32. package/src/packages/contracts/{operation-deployment-result.ts → src/operation-deployment-result.ts} +2 -2
  33. package/src/packages/contracts/{operation-failure.ts → src/operation-failure.ts} +24 -12
  34. package/src/packages/contracts/src/problem-policy.ts +498 -0
  35. package/src/packages/contracts/src/project-data-changes.ts +90 -0
  36. package/src/packages/contracts/src/secret-names.ts +27 -0
  37. package/src/packages/sdk-ts/package.json +21 -0
  38. package/src/packages/sdk-ts/{generated → src/generated}/index.ts +15 -0
  39. package/src/packages/sdk-ts/{generated → src/generated}/sdk.gen.ts +112 -3
  40. package/src/packages/sdk-ts/{generated → src/generated}/types.gen.ts +201 -10
  41. package/src/packages/workos-auth-contracts/package.json +39 -0
  42. package/src/packages/agent-skills/generated-skill-resources.ts +0 -165
  43. /package/src/apps/mcp/{index.ts → src/index.ts} +0 -0
  44. /package/src/apps/mcp/{stdio-main.ts → src/stdio-main.ts} +0 -0
  45. /package/src/apps/product-cli/{credential-store.ts → src/credential-store.ts} +0 -0
  46. /package/src/apps/product-cli/{device-token-expiry.ts → src/device-token-expiry.ts} +0 -0
  47. /package/src/apps/product-cli/{file-credential-store.ts → src/file-credential-store.ts} +0 -0
  48. /package/src/apps/product-cli/{profile-store.ts → src/profile-store.ts} +0 -0
  49. /package/src/apps/product-cli/{project-link-store.ts → src/project-link-store.ts} +0 -0
  50. /package/src/apps/product-cli/{psql-session.ts → src/psql-session.ts} +0 -0
  51. /package/src/packages/contracts/{application-root.ts → src/application-root.ts} +0 -0
  52. /package/src/packages/contracts/{billing.ts → src/billing.ts} +0 -0
  53. /package/src/packages/contracts/{credit-pricing.ts → src/credit-pricing.ts} +0 -0
  54. /package/src/packages/contracts/{database-compute.ts → src/database-compute.ts} +0 -0
  55. /package/src/packages/contracts/{database-migration-admission.ts → src/database-migration-admission.ts} +0 -0
  56. /package/src/packages/contracts/{feedback.ts → src/feedback.ts} +0 -0
  57. /package/src/packages/contracts/{framework-family.ts → src/framework-family.ts} +0 -0
  58. /package/src/packages/contracts/{functions.ts → src/functions.ts} +0 -0
  59. /package/src/packages/contracts/{platform-origins.ts → src/platform-origins.ts} +0 -0
  60. /package/src/packages/contracts/{project-context.ts → src/project-context.ts} +0 -0
  61. /package/src/packages/contracts/{project-exports.ts → src/project-exports.ts} +0 -0
  62. /package/src/packages/contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  63. /package/src/packages/sdk-ts/{generated → src/generated}/client/client.gen.ts +0 -0
  64. /package/src/packages/sdk-ts/{generated → src/generated}/client/index.ts +0 -0
  65. /package/src/packages/sdk-ts/{generated → src/generated}/client/types.gen.ts +0 -0
  66. /package/src/packages/sdk-ts/{generated → src/generated}/client/utils.gen.ts +0 -0
  67. /package/src/packages/sdk-ts/{generated → src/generated}/client.gen.ts +0 -0
  68. /package/src/packages/sdk-ts/{generated → src/generated}/core/auth.gen.ts +0 -0
  69. /package/src/packages/sdk-ts/{generated → src/generated}/core/bodySerializer.gen.ts +0 -0
  70. /package/src/packages/sdk-ts/{generated → src/generated}/core/params.gen.ts +0 -0
  71. /package/src/packages/sdk-ts/{generated → src/generated}/core/pathSerializer.gen.ts +0 -0
  72. /package/src/packages/sdk-ts/{generated → src/generated}/core/queryKeySerializer.gen.ts +0 -0
  73. /package/src/packages/sdk-ts/{generated → src/generated}/core/serverSentEvents.gen.ts +0 -0
  74. /package/src/packages/sdk-ts/{generated → src/generated}/core/types.gen.ts +0 -0
  75. /package/src/packages/sdk-ts/{generated → src/generated}/core/utils.gen.ts +0 -0
  76. /package/src/packages/sdk-ts/{index.ts → src/index.ts} +0 -0
  77. /package/src/packages/sdk-ts/{managed-mail-response.ts → src/managed-mail-response.ts} +0 -0
  78. /package/src/packages/sdk-ts/{managed-mail.ts → src/managed-mail.ts} +0 -0
  79. /package/src/packages/workos-auth-contracts/{device-flow.ts → src/device-flow.ts} +0 -0
  80. /package/src/packages/workos-auth-contracts/{index.ts → src/index.ts} +0 -0
  81. /package/src/packages/workos-auth-contracts/{management-rpc.ts → src/management-rpc.ts} +0 -0
  82. /package/src/packages/workos-auth-contracts/{organizations.ts → src/organizations.ts} +0 -0
  83. /package/src/packages/workos-auth-contracts/{parsing.ts → src/parsing.ts} +0 -0
  84. /package/src/packages/workos-auth-contracts/{session-status.ts → src/session-status.ts} +0 -0
  85. /package/src/packages/workos-auth-contracts/{tokens.ts → src/tokens.ts} +0 -0
  86. /package/src/packages/workos-auth-contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  87. /package/src/packages/workos-auth-contracts/{webhooks.ts → src/webhooks.ts} +0 -0
@@ -1,8 +1,30 @@
1
+ import { isUserSecretName } from "@ohmyhost/contracts/secret-names";
2
+ import {
3
+ isProjectDataChangeKind,
4
+ type ProjectDataChangeKind,
5
+ } from "@ohmyhost/contracts/project-data-changes";
1
6
  import type { ManagedMailCommand } from "@ohmyhost/sdk-ts";
2
7
  import { USER_API_KEY_ID } from "@ohmyhost/contracts/user-api-keys";
3
8
  import { WORKOS_USER_API_KEY_PATTERN } from "@ohmyhost/workos-auth-contracts/user-api-keys";
4
9
  import { isProfileName, isProfileUserId } from "./profile-store.js";
5
10
  export type ProductCliCommand =
11
+ | {
12
+ readonly kind: "project-data-plan";
13
+ readonly projectId: string;
14
+ readonly change: ProjectDataChangeKind;
15
+ readonly credentialStore: "native";
16
+ }
17
+ | {
18
+ readonly kind: "project-data-change";
19
+ readonly projectId: string;
20
+ readonly change: ProjectDataChangeKind;
21
+ readonly ifMatch: string;
22
+ readonly confirmationToken: string;
23
+ readonly idempotencyKey: string;
24
+ readonly yes: true;
25
+ readonly wait: boolean;
26
+ readonly credentialStore: "native";
27
+ }
6
28
  | {
7
29
  readonly kind: "github-connect";
8
30
  readonly organizationId: string;
@@ -550,6 +572,8 @@ export class InvalidCommandError extends Error {
550
572
  }
551
573
 
552
574
  export type RecognizedCommandLabel =
575
+ | "project data plan"
576
+ | "project data change"
553
577
  | "database compute set"
554
578
  | "database compute get"
555
579
  | "project context"
@@ -696,6 +720,10 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
696
720
  if (value === "feedback" && values[0] === "submit") return "feedback submit";
697
721
  if (value === "feedback" && values[0] === "status") return "feedback status";
698
722
  if (value === "project" && values[0] === "create") return "project create";
723
+ if (value === "project" && values[0] === "data" && values[1] === "plan")
724
+ return "project data plan";
725
+ if (value === "project" && values[0] === "data" && values[1] === "change")
726
+ return "project data change";
699
727
  if (value === "organization" && values[0] === "create") return "organization create";
700
728
  if (value === "organization" && values[0] === "list") return "organization list";
701
729
  if (value === "organization" && values[0] === "use") return "organization use";
@@ -1242,6 +1270,33 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1242
1270
  }
1243
1271
  if (first === "project") {
1244
1272
  const subcommand = values.shift();
1273
+ if (subcommand === "data") {
1274
+ const action = values.shift();
1275
+ if (follow || stdin || (action !== "plan" && action !== "change"))
1276
+ throw new InvalidCommandError();
1277
+ if ((action === "plan" && (yes || wait)) || (action === "change" && !yes))
1278
+ throw new InvalidCommandError();
1279
+ const options = parseOptions(
1280
+ values,
1281
+ action === "plan"
1282
+ ? ["--project", "--change"]
1283
+ : ["--project", "--change", "--if-match", "--confirmation-token", "--idempotency-key"],
1284
+ );
1285
+ const change = options["--change"];
1286
+ if (!isProjectDataChangeKind(change)) throw new InvalidCommandError();
1287
+ const common = { projectId: options["--project"], change, credentialStore };
1288
+ return action === "plan"
1289
+ ? { kind: "project-data-plan", ...common }
1290
+ : {
1291
+ kind: "project-data-change",
1292
+ ...common,
1293
+ ifMatch: options["--if-match"],
1294
+ confirmationToken: options["--confirmation-token"],
1295
+ idempotencyKey: options["--idempotency-key"],
1296
+ yes: true,
1297
+ wait,
1298
+ };
1299
+ }
1245
1300
  if (subcommand === "handle" && values[0] === "set") {
1246
1301
  values.shift();
1247
1302
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
@@ -1928,7 +1983,7 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1928
1983
  if (yes || follow || stdin) throw new InvalidCommandError();
1929
1984
  values.shift();
1930
1985
  const name = values.shift();
1931
- if (!isUserSecretName(name)) throw new InvalidCommandError();
1986
+ if (!isUserSecretName(name, "delete")) throw new InvalidCommandError();
1932
1987
  const options = parseOptions(values, ["--project", "--environment", "--idempotency-key"]);
1933
1988
  return {
1934
1989
  kind: "secret-delete",
@@ -2120,6 +2175,7 @@ const parseOptions = <T extends string>(
2120
2175
  if (option === undefined || !allowed.includes(option as T) || !isValue(value)) {
2121
2176
  throw new InvalidCommandError();
2122
2177
  }
2178
+ if (option === "--idempotency-key" && value.length > 128) throw new InvalidCommandError();
2123
2179
  if (option in result) throw new InvalidCommandError();
2124
2180
  result[option as T] = value;
2125
2181
  }
@@ -2144,6 +2200,7 @@ const parseOptionalOptions = <T extends string>(
2144
2200
  if (option === undefined || !allowed.includes(option as T) || !isValue(value)) {
2145
2201
  throw new InvalidCommandError();
2146
2202
  }
2203
+ if (option === "--idempotency-key" && value.length > 128) throw new InvalidCommandError();
2147
2204
  if (option in result) throw new InvalidCommandError();
2148
2205
  result[option as T] = value;
2149
2206
  }
@@ -2220,13 +2277,3 @@ const validPaidHostname = (value: string): boolean =>
2220
2277
  !value.endsWith(".omh.st") &&
2221
2278
  value !== "ohmyho.st" &&
2222
2279
  !value.endsWith(".ohmyho.st");
2223
-
2224
- const isUserSecretName = (value: string | undefined): value is string =>
2225
- isValue(value) &&
2226
- /^[A-Z][A-Z0-9_]{0,127}$/u.test(value) &&
2227
- value !== "DATABASE_URL" &&
2228
- value !== "HYPERDRIVE" &&
2229
- value !== "BETTER_AUTH_SECRET" &&
2230
- value !== "BETTER_AUTH_URL" &&
2231
- value !== "OHMYHOST_MAIL_KEY" &&
2232
- value !== "OHMYHOST_MAIL_GATEWAY_URL";
@@ -21,6 +21,11 @@ import {
21
21
  ProductCli,
22
22
  classifyCliFailure,
23
23
  cliMetadataResult,
24
+ CREDENTIAL_STORE_UNAVAILABLE_ACTION,
25
+ endedLoginAction,
26
+ INTERNAL_ERROR_ACTION,
27
+ invalidCommandAction,
28
+ refreshFailedAction,
24
29
  NetworkTransportError,
25
30
  LifecycleTransportError,
26
31
  TokenRefreshRejectedError,
@@ -550,7 +555,7 @@ export const runProductionCli = async (
550
555
  args = extracted.argv;
551
556
  profileName = extracted.profileName;
552
557
  } catch {
553
- return written(invalidCommandFailure(recognizedCommandLabel(argv)));
558
+ return written(invalidCommandFailure(argv, environment));
554
559
  }
555
560
  const workingDirectory = overrides.workingDirectory ?? process.cwd();
556
561
  let command: ProductCliCommand;
@@ -607,14 +612,20 @@ export const runProductionCli = async (
607
612
  recognizedCommandLabel(args),
608
613
  3,
609
614
  "credential_store_unavailable",
610
- true,
611
- "Unlock or repair the selected credential store and retry.",
615
+ false,
616
+ CREDENTIAL_STORE_UNAVAILABLE_ACTION,
612
617
  ),
613
618
  );
614
619
  const result =
615
620
  error instanceof InvalidCommandError
616
- ? invalidCommandFailure(recognizedCommandLabel(args))
617
- : errorDocument("unknown", 1, "internal_error", false, "Retry the command.");
621
+ ? invalidCommandFailure(args, environment)
622
+ : errorDocument(
623
+ recognizedCommandLabel(args),
624
+ 1,
625
+ "internal_error",
626
+ false,
627
+ INTERNAL_ERROR_ACTION,
628
+ );
618
629
  if (!(error instanceof InvalidCommandError)) {
619
630
  diagnostics.record({
620
631
  correlationId: randomUUID(),
@@ -626,7 +637,16 @@ export const runProductionCli = async (
626
637
  return written(result);
627
638
  }
628
639
  if (command.kind === "init") {
629
- if (profileName !== undefined) return written(invalidCommandFailure("init"));
640
+ if (profileName !== undefined)
641
+ return written(
642
+ errorDocument(
643
+ "init",
644
+ 2,
645
+ "invalid_command",
646
+ false,
647
+ "ohmyhost init runs offline and uses no login: remove --profile-name and run it again.",
648
+ ),
649
+ );
630
650
  return runOfflineInit(command, io, diagnostics);
631
651
  }
632
652
  if (command.kind === "help" || command.kind === "version") {
@@ -708,6 +728,9 @@ export const runProductionCli = async (
708
728
  overrides.profiles ?? new NativeKeyringProfileRegistry(undefined, platform.credentialService);
709
729
  const linkDirectory = join(workingDirectory, platform.linkDirectory);
710
730
  const projectId = commandProjectId(command);
731
+ // A command that named its project read no link yet, but the link still says whose project it is.
732
+ if (projectId !== undefined && linked === null)
733
+ links = await readProjectLinks(linkDirectory, platform.apiOrigin);
711
734
  // The organization this command is about: named on the command, or recorded by this checkout's
712
735
  // link of its project. It narrows the saved logins and is checked against OHMYHOST_TOKEN.
713
736
  const organizationId =
@@ -803,9 +826,21 @@ export const runProductionCli = async (
803
826
  }
804
827
  }
805
828
  } catch (error) {
806
- return written(
807
- compositionFailure(recognizedCommandLabel(args), error, signal, platform.commandPrefix),
829
+ const result = compositionFailure(
830
+ recognizedCommandLabel(args),
831
+ error,
832
+ signal,
833
+ platform.commandPrefix,
834
+ profile,
808
835
  );
836
+ if (result.exitCode === 1)
837
+ diagnostics.record({
838
+ correlationId: randomUUID(),
839
+ category: "internal",
840
+ subsystem: "authentication",
841
+ errorKind: "unexpected",
842
+ });
843
+ return written(result);
809
844
  }
810
845
  const selected = profile;
811
846
  const productApiOrigin = platform.apiOrigin;
@@ -939,6 +974,7 @@ const compositionFailure = (
939
974
  error: unknown,
940
975
  signal: AbortSignal,
941
976
  commandPrefix: string,
977
+ profile: LocalProfile | undefined,
942
978
  ): CliResult => {
943
979
  if (error instanceof ProfileSelectionError)
944
980
  return errorDocument(command, error.exitCode, error.code, false, error.suggestedAction);
@@ -957,14 +993,14 @@ const compositionFailure = (
957
993
  5,
958
994
  "authentication_required",
959
995
  false,
960
- `Run '${commandPrefix} login --json'.`,
996
+ endedLoginAction(commandPrefix, profile),
961
997
  )
962
998
  : errorDocument(
963
999
  command,
964
1000
  5,
965
1001
  "token_refresh_failed",
966
1002
  true,
967
- `Run '${commandPrefix} login --json' if retrying does not succeed.`,
1003
+ refreshFailedAction(commandPrefix, profile),
968
1004
  );
969
1005
  return classifyCliFailure(command, error, signal);
970
1006
  };
@@ -1006,6 +1042,7 @@ const runOfflineInit = async (
1006
1042
  // The functions runtime reports its own bindings and packages; dropping it left a
1007
1043
  // storage-enabled functions app with no contract to read at all.
1008
1044
  worker: result.worker,
1045
+ runtime: result.runtime,
1009
1046
  })}\n`,
1010
1047
  );
1011
1048
  return blocked ? 9 : 0;
@@ -1031,19 +1068,42 @@ const runOfflineInit = async (
1031
1068
  exitCode,
1032
1069
  code,
1033
1070
  false,
1034
- error instanceof RepositoryInitConflictError
1035
- ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying."
1036
- : error instanceof RepositoryInitError && error.code === "repository_root_required"
1037
- ? "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."
1038
- : error instanceof RepositoryInitError && error.code === "project_name_invalid"
1039
- ? "Pass a lowercase deployment slug with '--project', then rerun init."
1040
- : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
1071
+ known ? initFailureAction(error) : INTERNAL_ERROR_ACTION,
1041
1072
  );
1042
1073
  io.writeStdout(result.stdout);
1043
1074
  return result.exitCode;
1044
1075
  }
1045
1076
  };
1046
1077
 
1078
+ /** How to fix one init failure: what went wrong, then the exact next step. */
1079
+ const initFailureAction = (error: RepositoryInitError): string => {
1080
+ if (error instanceof RepositoryInitBlockedError)
1081
+ return "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.";
1082
+ if (error instanceof RepositoryInitConflictError)
1083
+ return "ohmyhost.yaml already exists and init never overwrites it; that file stays authoritative. Run 'ohmyhost init --dry-run --json' to check it and list blockers, then edit the file by hand. Do not delete it to rerun init.";
1084
+ const message = /[.!?]$/u.test(error.message) ? error.message : `${error.message}.`;
1085
+ switch (error.code) {
1086
+ case "repository_root_required":
1087
+ return "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.";
1088
+ case "project_name_invalid":
1089
+ return "Pass a lowercase deployment slug with '--project', then rerun init.";
1090
+ case "application_root_ambiguous":
1091
+ return `${message} Run 'ohmyhost init --root DIR --dry-run --json' from the Git repository root with the directory to deploy; ask the customer if it is unclear.`;
1092
+ case "application_root_not_found":
1093
+ case "application_root_unsupported":
1094
+ return `${message} init needs a package.json that depends on next (without vite or @tanstack/react-start), on vite, or on @tanstack/react-start, or a functions app with src/ohmyhost/worker.ts. Pass --root DIR with the app's directory, or fix its package.json, then run init again.`;
1095
+ case "application_root_invalid":
1096
+ case "application_root_missing":
1097
+ return `${message} Pass --root as a path relative to the repository root, for example apps/web.`;
1098
+ case "repository_configuration_invalid":
1099
+ return `${error.message.replace(/\.$/u, "")}. Fix that, then run 'ohmyhost init --dry-run --json' again; do not delete ohmyhost.yaml.`;
1100
+ case "repository_configuration_root_mismatch":
1101
+ return error.message;
1102
+ default:
1103
+ return `${message} Fix that and run 'ohmyhost init --json' again from the Git repository root.`;
1104
+ }
1105
+ };
1106
+
1047
1107
  class FetchWorkosPublicTransport implements WorkosPublicTransport {
1048
1108
  public constructor(
1049
1109
  private readonly parentSignal: AbortSignal,
@@ -1423,8 +1483,8 @@ const runOfflineLogout = async (
1423
1483
  "logout",
1424
1484
  3,
1425
1485
  "credential_store_unavailable",
1426
- true,
1427
- "Unlock or repair the selected credential store and retry.",
1486
+ false,
1487
+ CREDENTIAL_STORE_UNAVAILABLE_ACTION,
1428
1488
  );
1429
1489
  }
1430
1490
  io.writeStdout(result.stdout);
@@ -1461,8 +1521,25 @@ export const readSecretValueFromStandardInput = async (
1461
1521
  }
1462
1522
  };
1463
1523
 
1464
- const invalidCommandFailure = (command: OutputCommand): CliResult =>
1465
- errorDocument(command, 2, "invalid_command", false, "Check 'ohmyhost --help'.");
1524
+ /** The refused command's usage, with the command prefix of the selected platform environment. */
1525
+ const invalidCommandFailure = (
1526
+ argv: readonly string[],
1527
+ environment: ProductCliEnvironment,
1528
+ ): CliResult => {
1529
+ let commandPrefix = "ohmyhost";
1530
+ try {
1531
+ commandPrefix = resolveProductCliEnvironment(environment).commandPrefix;
1532
+ } catch {
1533
+ // An invalid OHMYHOST_ENVIRONMENT is reported once the command itself is valid.
1534
+ }
1535
+ return errorDocument(
1536
+ recognizedCommandLabel(argv),
1537
+ 2,
1538
+ "invalid_command",
1539
+ false,
1540
+ invalidCommandAction(argv, commandPrefix),
1541
+ );
1542
+ };
1466
1543
 
1467
1544
  const resultDocument = (
1468
1545
  command: OutputCommand,
@@ -23,6 +23,7 @@ export {
23
23
  parseOperation,
24
24
  parseOperationEvent,
25
25
  parseSafeProblem,
26
+ parseProjectDataChangePlan,
26
27
  } from "./runtime-contract.js";
27
28
 
28
29
  export * from "./user-token-file.js";
@@ -1,4 +1,8 @@
1
1
  import { executeManagedMail, type ManagedMailCommand } from "@ohmyhost/sdk-ts";
2
+ import type {
3
+ ProjectDataChangeKind,
4
+ ProjectDataChangePlan,
5
+ } from "@ohmyhost/contracts/project-data-changes";
2
6
  import {
3
7
  assertProjectDatabaseAccess,
4
8
  assertProjectDatabaseAccessCredential,
@@ -66,6 +70,8 @@ import {
66
70
  setProjectPoweredByFlag,
67
71
  createDeployment,
68
72
  createProject,
73
+ planProjectDataChange,
74
+ createProjectDataChange,
69
75
  submitFeedback,
70
76
  getFeedback,
71
77
  createOrganization,
@@ -107,7 +113,6 @@ import {
107
113
  revokeProjectDatabaseAccess,
108
114
  reconcileOperation,
109
115
  streamOperationEvents,
110
- streamDeploymentLogs,
111
116
  type DeploymentPlan,
112
117
  type CloudflareDnsAuthorization,
113
118
  type CloudflareDnsAuthorizationStatus,
@@ -128,12 +133,16 @@ import {
128
133
  type PaidDomain,
129
134
  type PaidDomainPlan,
130
135
  type ProjectDatabaseQueryResult,
131
- type DeploymentDiagnostic,
132
136
  type DeploymentDiagnosticPage,
133
137
  type Deployment,
134
138
  type SourceAutoDeploy,
135
139
  } from "@ohmyhost/sdk-ts";
136
- import { parseAccountProfile, type PublicAccount } from "./runtime-contract.js";
140
+ import {
141
+ parseAccountProfile,
142
+ parseProjectDataChangePlan,
143
+ parseOperation,
144
+ type PublicAccount,
145
+ } from "./runtime-contract.js";
137
146
 
138
147
  export interface ProjectCreateInput {
139
148
  readonly organizationId: string;
@@ -145,6 +154,17 @@ export interface ProjectCreateInput {
145
154
  }
146
155
 
147
156
  export interface ProductApi {
157
+ planProjectDataChange(input: {
158
+ projectId: string;
159
+ change: ProjectDataChangeKind;
160
+ }): Promise<ProjectDataChangePlan>;
161
+ createProjectDataChange(input: {
162
+ projectId: string;
163
+ change: ProjectDataChangeKind;
164
+ ifMatch: string;
165
+ confirmationToken: string;
166
+ idempotencyKey: string;
167
+ }): Promise<Operation>;
148
168
  changeDatabaseCompute(input: {
149
169
  projectId: string;
150
170
  environment: "dev" | "prod";
@@ -345,9 +365,14 @@ export interface ProductApi {
345
365
  readonly projectId: string;
346
366
  readonly deploymentId: string;
347
367
  }): Promise<Deployment>;
368
+ /**
369
+ * One connection to the operation's events, resumed after `lastEventId` when given. It ends,
370
+ * without retrying, when the server closes it or refuses it; the caller decides what follows.
371
+ */
348
372
  streamOperationEvents(
349
373
  operationId: string,
350
374
  signal: AbortSignal,
375
+ lastEventId?: string,
351
376
  ): Promise<AsyncIterable<OperationEvent>>;
352
377
  getDeploymentLogs(input: {
353
378
  readonly projectId: string;
@@ -355,10 +380,6 @@ export interface ProductApi {
355
380
  readonly cursor?: string;
356
381
  readonly limit: number;
357
382
  }): Promise<DeploymentDiagnosticPage>;
358
- streamDeploymentLogs(
359
- input: { readonly projectId: string; readonly deploymentId: string },
360
- signal: AbortSignal,
361
- ): Promise<AsyncIterable<DeploymentDiagnostic>>;
362
383
  planRollback(input: {
363
384
  readonly projectId: string;
364
385
  readonly deploymentId: string;
@@ -846,6 +867,32 @@ export class GeneratedSdkProductApi implements ProductApi {
846
867
  ) as import("@ohmyhost/sdk-ts").FeedbackStatus;
847
868
  }
848
869
 
870
+ public async planProjectDataChange(
871
+ input: Parameters<ProductApi["planProjectDataChange"]>[0],
872
+ ): Promise<ProjectDataChangePlan> {
873
+ return parseProjectDataChangePlan(
874
+ await planProjectDataChange(
875
+ { project_id: input.projectId, projectDataChangeRequest: { change: input.change } },
876
+ { client: this.#client },
877
+ ),
878
+ );
879
+ }
880
+ public async createProjectDataChange(
881
+ input: Parameters<ProductApi["createProjectDataChange"]>[0],
882
+ ): Promise<Operation> {
883
+ return parseOperation(
884
+ await createProjectDataChange(
885
+ {
886
+ project_id: input.projectId,
887
+ projectDataChangeRequest: { change: input.change },
888
+ "If-Match": input.ifMatch,
889
+ "X-Confirmation-Token": input.confirmationToken,
890
+ "Idempotency-Key": input.idempotencyKey,
891
+ },
892
+ { client: this.#client },
893
+ ),
894
+ );
895
+ }
849
896
  public async createProject(input: ProjectCreateInput): Promise<Operation> {
850
897
  return createProject(
851
898
  {
@@ -1014,11 +1061,21 @@ export class GeneratedSdkProductApi implements ProductApi {
1014
1061
  public async streamOperationEvents(
1015
1062
  operationId: string,
1016
1063
  signal: AbortSignal,
1064
+ lastEventId?: string,
1017
1065
  ): Promise<AsyncIterable<OperationEvent>> {
1018
1066
  return (
1019
1067
  await streamOperationEvents(
1020
1068
  { operation_id: operationId },
1021
- { client: this.#client, signal, headers: { Accept: "text/event-stream" } },
1069
+ {
1070
+ client: this.#client,
1071
+ signal,
1072
+ // The generated client would otherwise retry a refused stream, such as a 404, forever.
1073
+ sseMaxRetryAttempts: 1,
1074
+ headers: {
1075
+ Accept: "text/event-stream",
1076
+ ...(lastEventId === undefined ? {} : { "Last-Event-ID": lastEventId }),
1077
+ },
1078
+ },
1022
1079
  )
1023
1080
  ).stream;
1024
1081
  }
@@ -1038,17 +1095,6 @@ export class GeneratedSdkProductApi implements ProductApi {
1038
1095
  { client: this.#client },
1039
1096
  );
1040
1097
  }
1041
- public async streamDeploymentLogs(
1042
- input: { readonly projectId: string; readonly deploymentId: string },
1043
- signal: AbortSignal,
1044
- ): Promise<AsyncIterable<DeploymentDiagnostic>> {
1045
- return (
1046
- await streamDeploymentLogs(
1047
- { project_id: input.projectId, deployment_id: input.deploymentId },
1048
- { client: this.#client, signal, headers: { Accept: "text/event-stream" } },
1049
- )
1050
- ).stream;
1051
- }
1052
1098
 
1053
1099
  public async planRollback(input: {
1054
1100
  readonly projectId: string;