@amerged/ohmyhost-cli 0.1.23 → 0.1.25

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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.js +3 -2
  3. package/dist/{chunk-74DROG4F.js → chunk-YOEEWHSQ.js} +985 -304
  4. package/dist/index.js +13 -1
  5. package/package.json +1 -1
  6. package/src/apps/product-cli/package.json +45 -0
  7. package/src/apps/product-cli/{bin.ts → src/bin.ts} +3 -0
  8. package/src/apps/product-cli/{cli.ts → src/cli.ts} +363 -87
  9. package/src/apps/product-cli/{command.ts → src/command.ts} +58 -11
  10. package/src/apps/product-cli/{composition.ts → src/composition.ts} +98 -21
  11. package/src/apps/product-cli/{index.ts → src/index.ts} +1 -0
  12. package/src/apps/product-cli/{product-api.ts → src/product-api.ts} +65 -19
  13. package/src/apps/product-cli/{repository-init.ts → src/repository-init.ts} +90 -36
  14. package/src/apps/product-cli/{runtime-contract.ts → src/runtime-contract.ts} +16 -309
  15. package/src/apps/product-cli/{user-token-file.ts → src/user-token-file.ts} +46 -35
  16. package/src/apps/product-cli/{workspace.ts → src/workspace.ts} +6 -0
  17. package/src/packages/contracts/package.json +137 -0
  18. package/src/packages/contracts/src/browser-capabilities.ts +133 -0
  19. package/src/packages/contracts/src/customer-auth-admission.ts +28 -0
  20. package/src/packages/contracts/{database-access.ts → src/database-access.ts} +10 -2
  21. package/src/packages/contracts/{framework-admission.ts → src/framework-admission.ts} +76 -13
  22. package/src/packages/contracts/src/framework-build-script.mjs +22 -0
  23. package/src/packages/contracts/src/framework-build-script.ts +1 -0
  24. package/src/packages/contracts/{index.ts → src/index.ts} +17 -16
  25. package/src/packages/contracts/src/operation-data-change-result.ts +86 -0
  26. package/src/packages/contracts/{operation-deployment-result.ts → src/operation-deployment-result.ts} +2 -2
  27. package/src/packages/contracts/{operation-failure.ts → src/operation-failure.ts} +24 -12
  28. package/src/packages/contracts/src/problem-policy.ts +498 -0
  29. package/src/packages/contracts/src/project-data-changes.ts +90 -0
  30. package/src/packages/contracts/src/secret-names.ts +27 -0
  31. package/src/packages/sdk-ts/package.json +21 -0
  32. package/src/packages/sdk-ts/{generated → src/generated}/index.ts +15 -0
  33. package/src/packages/sdk-ts/{generated → src/generated}/sdk.gen.ts +113 -4
  34. package/src/packages/sdk-ts/{generated → src/generated}/types.gen.ts +201 -10
  35. package/src/packages/workos-auth-contracts/package.json +39 -0
  36. /package/src/apps/product-cli/{credential-store.ts → src/credential-store.ts} +0 -0
  37. /package/src/apps/product-cli/{device-token-expiry.ts → src/device-token-expiry.ts} +0 -0
  38. /package/src/apps/product-cli/{file-credential-store.ts → src/file-credential-store.ts} +0 -0
  39. /package/src/apps/product-cli/{profile-store.ts → src/profile-store.ts} +0 -0
  40. /package/src/apps/product-cli/{project-link-store.ts → src/project-link-store.ts} +0 -0
  41. /package/src/apps/product-cli/{psql-session.ts → src/psql-session.ts} +0 -0
  42. /package/src/packages/contracts/{application-root.ts → src/application-root.ts} +0 -0
  43. /package/src/packages/contracts/{billing.ts → src/billing.ts} +0 -0
  44. /package/src/packages/contracts/{credit-pricing.ts → src/credit-pricing.ts} +0 -0
  45. /package/src/packages/contracts/{database-compute.ts → src/database-compute.ts} +0 -0
  46. /package/src/packages/contracts/{database-migration-admission.ts → src/database-migration-admission.ts} +0 -0
  47. /package/src/packages/contracts/{feedback.ts → src/feedback.ts} +0 -0
  48. /package/src/packages/contracts/{framework-family.ts → src/framework-family.ts} +0 -0
  49. /package/src/packages/contracts/{functions.ts → src/functions.ts} +0 -0
  50. /package/src/packages/contracts/{platform-origins.ts → src/platform-origins.ts} +0 -0
  51. /package/src/packages/contracts/{project-context.ts → src/project-context.ts} +0 -0
  52. /package/src/packages/contracts/{project-exports.ts → src/project-exports.ts} +0 -0
  53. /package/src/packages/contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  54. /package/src/packages/sdk-ts/{generated → src/generated}/client/client.gen.ts +0 -0
  55. /package/src/packages/sdk-ts/{generated → src/generated}/client/index.ts +0 -0
  56. /package/src/packages/sdk-ts/{generated → src/generated}/client/types.gen.ts +0 -0
  57. /package/src/packages/sdk-ts/{generated → src/generated}/client/utils.gen.ts +0 -0
  58. /package/src/packages/sdk-ts/{generated → src/generated}/client.gen.ts +0 -0
  59. /package/src/packages/sdk-ts/{generated → src/generated}/core/auth.gen.ts +0 -0
  60. /package/src/packages/sdk-ts/{generated → src/generated}/core/bodySerializer.gen.ts +0 -0
  61. /package/src/packages/sdk-ts/{generated → src/generated}/core/params.gen.ts +0 -0
  62. /package/src/packages/sdk-ts/{generated → src/generated}/core/pathSerializer.gen.ts +0 -0
  63. /package/src/packages/sdk-ts/{generated → src/generated}/core/queryKeySerializer.gen.ts +0 -0
  64. /package/src/packages/sdk-ts/{generated → src/generated}/core/serverSentEvents.gen.ts +0 -0
  65. /package/src/packages/sdk-ts/{generated → src/generated}/core/types.gen.ts +0 -0
  66. /package/src/packages/sdk-ts/{generated → src/generated}/core/utils.gen.ts +0 -0
  67. /package/src/packages/sdk-ts/{index.ts → src/index.ts} +0 -0
  68. /package/src/packages/sdk-ts/{managed-mail-response.ts → src/managed-mail-response.ts} +0 -0
  69. /package/src/packages/sdk-ts/{managed-mail.ts → src/managed-mail.ts} +0 -0
  70. /package/src/packages/workos-auth-contracts/{device-flow.ts → src/device-flow.ts} +0 -0
  71. /package/src/packages/workos-auth-contracts/{index.ts → src/index.ts} +0 -0
  72. /package/src/packages/workos-auth-contracts/{management-rpc.ts → src/management-rpc.ts} +0 -0
  73. /package/src/packages/workos-auth-contracts/{organizations.ts → src/organizations.ts} +0 -0
  74. /package/src/packages/workos-auth-contracts/{parsing.ts → src/parsing.ts} +0 -0
  75. /package/src/packages/workos-auth-contracts/{session-status.ts → src/session-status.ts} +0 -0
  76. /package/src/packages/workos-auth-contracts/{tokens.ts → src/tokens.ts} +0 -0
  77. /package/src/packages/workos-auth-contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  78. /package/src/packages/workos-auth-contracts/{webhooks.ts → src/webhooks.ts} +0 -0
@@ -5019,8 +5019,8 @@ var require_multipleOf = __commonJS({
5019
5019
  const { gen, data, schemaCode, it } = cxt;
5020
5020
  const prec = it.opts.multipleOfPrecision;
5021
5021
  const res = gen.let("res");
5022
- const invalid4 = prec ? (0, codegen_1._)`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : (0, codegen_1._)`${res} !== parseInt(${res})`;
5023
- cxt.fail$data((0, codegen_1._)`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid4}))`);
5022
+ const invalid5 = prec ? (0, codegen_1._)`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : (0, codegen_1._)`${res} !== parseInt(${res})`;
5023
+ cxt.fail$data((0, codegen_1._)`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid5}))`);
5024
5024
  }
5025
5025
  };
5026
5026
  exports.default = def;
@@ -14769,17 +14769,25 @@ var UserTokenFileError = class extends Error {
14769
14769
  code;
14770
14770
  keyId;
14771
14771
  constructor(code, keyId) {
14772
- super({
14773
- token_file_not_ignored: "Add the selected env file to .gitignore or choose a location outside the repository. No token was created or written.",
14774
- token_file_unsafe: "Choose a private .env file owned by you. Symlinks, shared files and files larger than 64 KiB are not accepted.",
14775
- token_file_has_token: "The selected file already contains OHMYHOST_TOKEN. Use that token or choose a new file; existing credentials were preserved.",
14776
- token_file_changed: "The environment file changed during creation. It was not overwritten. List tokens to inspect the issued key before creating another.",
14777
- token_value_unavailable: "This token was already issued and its full value cannot be retrieved. Use the previously saved file, or explicitly revoke the listed key before creating a replacement."
14778
- }[code] + (keyId ? ` Token ID: ${keyId}.` : ""));
14772
+ super(tokenFileAction(code, keyId));
14779
14773
  this.code = code;
14780
14774
  this.keyId = keyId;
14781
14775
  }
14782
14776
  };
14777
+ var tokenFileAction = (code, keyId) => {
14778
+ switch (code) {
14779
+ case "token_file_not_ignored":
14780
+ return "Add the selected env file to .gitignore or choose a location outside the repository. No token was created or written.";
14781
+ case "token_file_unsafe":
14782
+ return "Choose an env file whose name starts or ends with .env, for example .env.local: a regular file you own, not a symlink or hard link, at most 64 KiB.";
14783
+ case "token_file_has_token":
14784
+ return "The selected file already contains OHMYHOST_TOKEN. Use that token or choose a new file; existing credentials were preserved.";
14785
+ case "token_file_changed":
14786
+ return keyId === void 0 ? "The env file changed while it was being read; no key was issued. Run the same command again." : `The env file changed while the token was being saved, so its value may not be in the file; key ${keyId} was issued. Revoke it with 'ohmyhost token revoke --organization ULID --key ${keyId} --yes --json' (MCP token_revoke), then create a new one with a new idempotency key.`;
14787
+ case "token_value_unavailable":
14788
+ return `A key was already issued for this idempotency key, and its value is shown only once. Use the file it was saved to, or revoke key ${String(keyId)} (ohmyhost token revoke, MCP token_revoke) and create a replacement with a new idempotency key.`;
14789
+ }
14790
+ };
14783
14791
  async function prepareUserTokenFile(output) {
14784
14792
  const path = resolve(output);
14785
14793
  if (!basename(path).startsWith(".env") && !basename(path).endsWith(".env"))
@@ -14812,18 +14820,22 @@ OHMYHOST_TOKEN=${preview}
14812
14820
  const result = parseUserApiKeyCreation(value);
14813
14821
  if (result.value === null)
14814
14822
  throw new UserTokenFileError("token_value_unavailable", result.key.id);
14815
- const current = await lstat(path);
14816
- if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || current.ino !== identity.ino || current.dev !== identity.dev || await tokenFileText(handle) !== initial)
14817
- throw new UserTokenFileError("token_file_changed");
14818
- await handle.appendFile(`
14823
+ try {
14824
+ const current = await lstat(path);
14825
+ if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || current.ino !== identity.ino || current.dev !== identity.dev || await tokenFileText(handle) !== initial)
14826
+ throw new UserTokenFileError("token_file_changed");
14827
+ await handle.appendFile(`
14819
14828
  OHMYHOST_TOKEN=${result.value}
14820
14829
  `, "utf8");
14821
- await handle.sync();
14822
- if (parseEnv(await tokenFileText(handle))["OHMYHOST_TOKEN"] !== result.value)
14823
- throw new UserTokenFileError("token_file_changed");
14824
- const saved = await lstat(path);
14825
- if (!saved.isFile() || saved.isSymbolicLink() || saved.ino !== identity.ino || saved.dev !== identity.dev)
14830
+ await handle.sync();
14831
+ if (parseEnv(await tokenFileText(handle))["OHMYHOST_TOKEN"] !== result.value)
14832
+ throw new UserTokenFileError("token_file_changed");
14833
+ const saved = await lstat(path);
14834
+ if (!saved.isFile() || saved.isSymbolicLink() || saved.ino !== identity.ino || saved.dev !== identity.dev)
14835
+ throw new UserTokenFileError("token_file_changed");
14836
+ } catch {
14826
14837
  throw new UserTokenFileError("token_file_changed", result.key.id);
14838
+ }
14827
14839
  return { request_id: result.request_id, key: result.key, token_file: path };
14828
14840
  },
14829
14841
  close: () => handle.close()
@@ -15271,6 +15283,63 @@ var describeRequest = (request) => {
15271
15283
  };
15272
15284
  var describeProfile = (profile) => `profile ${profile.name} (user ${profile.userId}, ${profile.organizationId === null ? "no organization" : `organization ${profile.organizationName === null ? "" : `${profile.organizationName} `}${profile.organizationId}`})`;
15273
15285
 
15286
+ // packages/contracts/dist/secret-names.js
15287
+ var USER_SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/u;
15288
+ var FIXED_PLATFORM_NAMES = /* @__PURE__ */ new Set([
15289
+ "BETTER_AUTH_SECRET",
15290
+ "BETTER_AUTH_URL",
15291
+ "DATABASE_URL",
15292
+ "HYPERDRIVE",
15293
+ "ASSETS",
15294
+ "FILES",
15295
+ "IMAGES",
15296
+ "STORAGE"
15297
+ ]);
15298
+ var INSTALLED_PLATFORM_KEYS = /* @__PURE__ */ new Set([
15299
+ "OHMYHOST_STORAGE_KEY",
15300
+ "OHMYHOST_MAIL_KEY",
15301
+ "BETTER_AUTH_SECRET"
15302
+ ]);
15303
+ function isUserSecretName(value, action = "set") {
15304
+ if (typeof value !== "string" || !USER_SECRET_NAME_PATTERN.test(value))
15305
+ return false;
15306
+ return action === "delete" ? !INSTALLED_PLATFORM_KEYS.has(value) : !value.startsWith("OHMYHOST_") && !FIXED_PLATFORM_NAMES.has(value);
15307
+ }
15308
+
15309
+ // packages/contracts/dist/project-data-changes.js
15310
+ var PROJECT_DATA_CHANGE_KINDS = [
15311
+ "isolate_prod_keeps_data",
15312
+ "isolate_dev_keeps_data",
15313
+ "share_prod_keeps_data",
15314
+ "reset_dev"
15315
+ ];
15316
+ function parseProjectDataChangePlan(value) {
15317
+ if (value === null || typeof value !== "object" || Array.isArray(value))
15318
+ invalid();
15319
+ const input = value;
15320
+ if (Object.keys(input).sort().join(",") !== "action,change,confirmation_token,created_at,data_mode_after,data_mode_before,destructive_effects,effects,expires_at,project_id,redeploy_environments,resource_etag,risks" || input["action"] !== "data_change" || typeof input["project_id"] !== "string" || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(input["project_id"]) || !isProjectDataChangeKind(input["change"]) || !["shared", "isolated"].includes(String(input["data_mode_before"])) || input["data_mode_after"] !== (input["change"] === "share_prod_keeps_data" ? "shared" : "isolated") || input["data_mode_before"] !== (input["change"].startsWith("isolate_") ? "shared" : "isolated") || !Array.isArray(input["redeploy_environments"]) || input["redeploy_environments"].length !== 1 || input["redeploy_environments"][0] !== (input["change"] === "isolate_dev_keeps_data" ? "prod" : "dev") || !planStrings(input["effects"]) || !planStrings(input["destructive_effects"]) || !planStrings(input["risks"]) || typeof input["resource_etag"] !== "string" || !/^"sha256-[a-f0-9]{64}"$/u.test(input["resource_etag"]) || typeof input["confirmation_token"] !== "string" || !/^[A-Za-z0-9_-]{43}$/u.test(input["confirmation_token"]) || !instant(input["created_at"]) || !instant(input["expires_at"]) || Date.parse(input["expires_at"]) - Date.parse(input["created_at"]) !== 6e5)
15321
+ invalid();
15322
+ return Object.freeze({
15323
+ ...input,
15324
+ effects: Object.freeze([...input["effects"]]),
15325
+ destructive_effects: Object.freeze([...input["destructive_effects"]]),
15326
+ risks: Object.freeze([...input["risks"]]),
15327
+ redeploy_environments: Object.freeze([...input["redeploy_environments"]])
15328
+ });
15329
+ }
15330
+ function isProjectDataChangeKind(value) {
15331
+ return typeof value === "string" && PROJECT_DATA_CHANGE_KINDS.some((kind) => kind === value);
15332
+ }
15333
+ function planStrings(value) {
15334
+ return Array.isArray(value) && value.length <= 32 && value.every((text) => typeof text === "string" && text.length > 0 && text.length <= 2e3 && !/[\p{Cc}\p{Cf}]/u.test(text));
15335
+ }
15336
+ function instant(value) {
15337
+ return typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
15338
+ }
15339
+ function invalid() {
15340
+ throw new TypeError("Project data change plan is invalid");
15341
+ }
15342
+
15274
15343
  // packages/workos-auth-contracts/dist/parsing.js
15275
15344
  var WorkosContractError = class extends Error {
15276
15345
  code;
@@ -15390,6 +15459,10 @@ var recognizedCommandLabel = (argv) => {
15390
15459
  return "feedback status";
15391
15460
  if (value === "project" && values[0] === "create")
15392
15461
  return "project create";
15462
+ if (value === "project" && values[0] === "data" && values[1] === "plan")
15463
+ return "project data plan";
15464
+ if (value === "project" && values[0] === "data" && values[1] === "change")
15465
+ return "project data change";
15393
15466
  if (value === "organization" && values[0] === "create")
15394
15467
  return "organization create";
15395
15468
  if (value === "organization" && values[0] === "list")
@@ -15906,6 +15979,27 @@ var parseProductCliCommand = (argv) => {
15906
15979
  }
15907
15980
  if (first === "project") {
15908
15981
  const subcommand = values.shift();
15982
+ if (subcommand === "data") {
15983
+ const action = values.shift();
15984
+ if (follow || stdin || action !== "plan" && action !== "change")
15985
+ throw new InvalidCommandError();
15986
+ if (action === "plan" && (yes || wait) || action === "change" && !yes)
15987
+ throw new InvalidCommandError();
15988
+ const options2 = parseOptions(values, action === "plan" ? ["--project", "--change"] : ["--project", "--change", "--if-match", "--confirmation-token", "--idempotency-key"]);
15989
+ const change = options2["--change"];
15990
+ if (!isProjectDataChangeKind(change))
15991
+ throw new InvalidCommandError();
15992
+ const common = { projectId: options2["--project"], change, credentialStore };
15993
+ return action === "plan" ? { kind: "project-data-plan", ...common } : {
15994
+ kind: "project-data-change",
15995
+ ...common,
15996
+ ifMatch: options2["--if-match"],
15997
+ confirmationToken: options2["--confirmation-token"],
15998
+ idempotencyKey: options2["--idempotency-key"],
15999
+ yes: true,
16000
+ wait
16001
+ };
16002
+ }
15909
16003
  if (subcommand === "handle" && values[0] === "set") {
15910
16004
  values.shift();
15911
16005
  if (yes || wait || follow || stdin)
@@ -16565,7 +16659,7 @@ var parseProductCliCommand = (argv) => {
16565
16659
  throw new InvalidCommandError();
16566
16660
  values.shift();
16567
16661
  const name = values.shift();
16568
- if (!isUserSecretName(name))
16662
+ if (!isUserSecretName(name, "delete"))
16569
16663
  throw new InvalidCommandError();
16570
16664
  const options = parseOptions(values, ["--project", "--environment", "--idempotency-key"]);
16571
16665
  return {
@@ -16763,6 +16857,8 @@ var parseOptions = (values, allowed) => {
16763
16857
  if (option === void 0 || !allowed.includes(option) || !isValue(value)) {
16764
16858
  throw new InvalidCommandError();
16765
16859
  }
16860
+ if (option === "--idempotency-key" && value.length > 128)
16861
+ throw new InvalidCommandError();
16766
16862
  if (option in result)
16767
16863
  throw new InvalidCommandError();
16768
16864
  result[option] = value;
@@ -16786,6 +16882,8 @@ var parseOptionalOptions = (values, allowed) => {
16786
16882
  if (option === void 0 || !allowed.includes(option) || !isValue(value)) {
16787
16883
  throw new InvalidCommandError();
16788
16884
  }
16885
+ if (option === "--idempotency-key" && value.length > 128)
16886
+ throw new InvalidCommandError();
16789
16887
  if (option in result)
16790
16888
  throw new InvalidCommandError();
16791
16889
  result[option] = value;
@@ -16818,7 +16916,6 @@ var validGithubBranchName = (value) => !(value.length < 1 || value.length > 1024
16818
16916
  return code <= 32 || code === 127 || "\\~^:?*[]".includes(character);
16819
16917
  }) || value.split("/").some((component) => component.startsWith(".") || component.endsWith(".lock")));
16820
16918
  var validPaidHostname = (value) => /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(value) && !value.endsWith(".poc.aixyte.com") && value !== "omh.st" && !value.endsWith(".omh.st") && value !== "ohmyho.st" && !value.endsWith(".ohmyho.st");
16821
- var isUserSecretName = (value) => isValue(value) && /^[A-Z][A-Z0-9_]{0,127}$/u.test(value) && value !== "DATABASE_URL" && value !== "HYPERDRIVE" && value !== "BETTER_AUTH_SECRET" && value !== "BETTER_AUTH_URL" && value !== "OHMYHOST_MAIL_KEY" && value !== "OHMYHOST_MAIL_GATEWAY_URL";
16822
16919
 
16823
16920
  // packages/contracts/dist/feedback.js
16824
16921
  var FEEDBACK_HISTORY_PAGE_SIZE = 25;
@@ -16892,6 +16989,41 @@ function hasControlCharacter(value, multiline) {
16892
16989
  });
16893
16990
  }
16894
16991
 
16992
+ // packages/contracts/dist/framework-build-script.mjs
16993
+ var PINNED_BUN_VERSION = "1.2.22";
16994
+ var TYPESCRIPT_BUILD_STAGE = /^tsc(?:[ \t]+(?:--noEmit|-b|--build))?$/u;
16995
+ function isAdmittedFrameworkBuildScript(framework, script) {
16996
+ if (typeof script !== "string")
16997
+ return false;
16998
+ if (framework === "nextjs")
16999
+ return script === "next build" || script === "next build --webpack";
17000
+ if (framework !== "vite" && framework !== "tanstack-start")
17001
+ return false;
17002
+ const stages = script.trim().split(/[ \t]*&&[ \t]*/u);
17003
+ if (stages.length === 1)
17004
+ return stages[0] === "vite build";
17005
+ if (stages.length !== 2)
17006
+ return false;
17007
+ return stages[0] === "vite build" && TYPESCRIPT_BUILD_STAGE.test(stages[1] ?? "") || TYPESCRIPT_BUILD_STAGE.test(stages[0] ?? "") && stages[1] === "vite build";
17008
+ }
17009
+
17010
+ // packages/contracts/dist/customer-auth-admission.js
17011
+ function managedCustomerAuthDatabaseMessage(path) {
17012
+ return `"${path}" selects the managed Better Auth bridge, which requires the managed PostgreSQL database. Set database.enabled: true with an admitted migrations path, or set auth.provider: none and keep the application-owned authentication configuration. Commit and push the change, then plan the new commit.`;
17013
+ }
17014
+ function managedCustomerAuthAdmissionIssue(capabilities) {
17015
+ if (capabilities?.auth?.provider !== "better-auth") {
17016
+ return null;
17017
+ }
17018
+ if (capabilities.database?.enabled !== true) {
17019
+ return Object.freeze({
17020
+ code: "managed_auth_database_required",
17021
+ message: managedCustomerAuthDatabaseMessage("ohmyhost.yaml")
17022
+ });
17023
+ }
17024
+ return null;
17025
+ }
17026
+
16895
17027
  // packages/contracts/dist/framework-admission.js
16896
17028
  var FrameworkAdmissionError = class extends Error {
16897
17029
  reason;
@@ -16946,6 +17078,9 @@ function resolvePackageManager(input) {
16946
17078
  if (!contract.lockfiles.includes(lockfile)) {
16947
17079
  throw new FrameworkAdmissionError("package_manager_lockfile_mismatch");
16948
17080
  }
17081
+ if (manager === "bun" && version2 !== PINNED_BUN_VERSION) {
17082
+ throw new FrameworkAdmissionError("package_manager_version_unsupported");
17083
+ }
16949
17084
  return Object.freeze({
16950
17085
  manager,
16951
17086
  version: version2,
@@ -17006,26 +17141,47 @@ function compareVersions(left, right) {
17006
17141
  function versionAdmission(classification, reason) {
17007
17142
  return Object.freeze({ classification, reason });
17008
17143
  }
17144
+ var PUSH_AND_PLAN = "Commit and push the change, then plan the new commit.";
17009
17145
  var FRAMEWORK_CONVERSION_DETAILS = Object.freeze({
17146
+ managed_auth_database_required: Object.freeze({
17147
+ render: managedCustomerAuthDatabaseMessage,
17148
+ pattern: /^"([^"]+)" selects the managed Better Auth bridge, which requires the managed PostgreSQL database\. Set database\.enabled: true with an admitted migrations path, or set auth\.provider: none and keep the application-owned authentication configuration\. Commit and push the change, then plan the new commit\.$/u
17149
+ }),
17010
17150
  database_binding_private: Object.freeze({
17011
- render: (path) => `"${path}" reads 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". Commit the change, then plan again.`,
17012
- pattern: /^"([^"]+)" reads 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"\. Commit the change, then plan again\.$/u
17151
+ render: (path) => `"${path}" reads a database connection string (DATABASE_URL, HYPERDRIVE, connectionString or a postgres:// URL); none reaches a customer Worker. Call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database". ${PUSH_AND_PLAN}`,
17152
+ pattern: /^"([^"]+)" reads a database connection string \(DATABASE_URL, HYPERDRIVE, connectionString or a postgres:\/\/ URL\); none reaches a customer Worker\. Call the database with createPrivateDatabaseClient from "@ohmyhost\/customer-runtime\/database"\. Commit and push the change, then plan the new commit\.$/u
17013
17153
  }),
17014
17154
  database_driver_unsupported: Object.freeze({
17015
- render: (path) => `"${path}" imports a PostgreSQL socket driver. Outbound connect() is disabled for customer Workers, so it can never open a connection; call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database". Commit the change, then plan again.`,
17016
- pattern: /^"([^"]+)" imports a PostgreSQL socket driver\. Outbound connect\(\) is disabled for customer Workers, so it can never open a connection; call the database with createPrivateDatabaseClient from "@ohmyhost\/customer-runtime\/database"\. Commit the change, then plan again\.$/u
17155
+ render: (path) => `"${path}" imports a database socket driver (pg, postgres, pg-native or mysql2); customer Workers cannot open sockets. Call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database". ${PUSH_AND_PLAN}`,
17156
+ pattern: /^"([^"]+)" imports a database socket driver \(pg, postgres, pg-native or mysql2\); customer Workers cannot open sockets\. Call the database with createPrivateDatabaseClient from "@ohmyhost\/customer-runtime\/database"\. Commit and push the change, then plan the new commit\.$/u
17017
17157
  }),
17018
17158
  source_syntax_invalid: Object.freeze({
17019
- render: (path) => `Invalid JavaScript or TypeScript in "${path}". Fix the file, commit the change, then plan again.`,
17020
- pattern: /^Invalid JavaScript or TypeScript in "([^"]+)"\. Fix the file, commit the change, then plan again\.$/u
17159
+ render: (path) => `Invalid JavaScript or TypeScript in "${path}". Fix the file, commit and push the change, then plan the new commit.`,
17160
+ pattern: /^Invalid JavaScript or TypeScript in "([^"]+)"\. Fix the file, commit and push the change, then plan the new commit\.$/u
17021
17161
  }),
17022
17162
  worker_module_default_export_required: Object.freeze({
17023
- render: (path) => `Worker module "${path}" has no default export. Export default { fetch, scheduled } with the handlers ohmyho.st invokes; named exports are never called. Commit the change, then plan again.`,
17024
- pattern: /^Worker module "([^"]+)" has no default export\. Export default \{ fetch, scheduled \} with the handlers ohmyho\.st invokes; named exports are never called\. Commit the change, then plan again\.$/u
17163
+ render: (path) => `Worker module "${path}" has no default export. Export default { fetch, scheduled } with the handlers ohmyho.st invokes; named exports are never called. ${PUSH_AND_PLAN}`,
17164
+ pattern: /^Worker module "([^"]+)" has no default export\. Export default \{ fetch, scheduled \} with the handlers ohmyho\.st invokes; named exports are never called\. Commit and push the change, then plan the new commit\.$/u
17025
17165
  }),
17026
17166
  scheduled_handler_required: Object.freeze({
17027
- render: (path) => `functions.crons is declared but "${path}" exports no scheduled handler. Add scheduled(controller, env, ctx) to its default export, commit the change, then plan again.`,
17028
- pattern: /^functions\.crons is declared but "([^"]+)" exports no scheduled handler\. Add scheduled\(controller, env, ctx\) to its default export, commit the change, then plan again\.$/u
17167
+ render: (path) => `functions.crons is declared but "${path}" exports no scheduled handler. Add scheduled(controller, env, ctx) to its default export, commit and push the change, then plan the new commit.`,
17168
+ pattern: /^functions\.crons is declared but "([^"]+)" exports no scheduled handler\. Add scheduled\(controller, env, ctx\) to its default export, commit and push the change, then plan the new commit\.$/u
17169
+ }),
17170
+ framework_base_path_unsupported: Object.freeze({
17171
+ render: (path) => `"${path}" sets a Vite base. ohmyho.st serves the app at the root origin: remove base or set base: "/". ${PUSH_AND_PLAN}`,
17172
+ pattern: /^"([^"]+)" sets a Vite base\. ohmyho\.st serves the app at the root origin: remove base or set base: "\/"\. Commit and push the change, then plan the new commit\.$/u
17173
+ }),
17174
+ tanstack_prerender_required: Object.freeze({
17175
+ render: (path) => `"${path}" does not call tanstackStart({ prerender: { enabled: true } }), which the static TanStack Start runtime needs; enable prerender, or set runtime.mode: edge with the Cloudflare Vite plugin. ${PUSH_AND_PLAN}`,
17176
+ pattern: /^"([^"]+)" does not call tanstackStart\(\{ prerender: \{ enabled: true \} \}\), which the static TanStack Start runtime needs; enable prerender, or set runtime\.mode: edge with the Cloudflare Vite plugin\. Commit and push the change, then plan the new commit\.$/u
17177
+ }),
17178
+ tanstack_vite_plugins_required: Object.freeze({
17179
+ render: (path) => `"${path}" does not follow the TanStack Start plugin contract: import tanstackStart from "@tanstack/react-start/plugin/vite"; on runtime edge also import cloudflare from "@cloudflare/vite-plugin" and call cloudflare({ viteEnvironment: { name: "ssr" } }) before tanstackStart(); on runtime static do not import the Cloudflare plugin. ${PUSH_AND_PLAN}`,
17180
+ pattern: /^"([^"]+)" does not follow the TanStack Start plugin contract: import tanstackStart from "@tanstack\/react-start\/plugin\/vite"; on runtime edge also import cloudflare from "@cloudflare\/vite-plugin" and call cloudflare\(\{ viteEnvironment: \{ name: "ssr" \} \}\) before tanstackStart\(\); on runtime static do not import the Cloudflare plugin\. Commit and push the change, then plan the new commit\.$/u
17181
+ }),
17182
+ next_cache_components_unsupported: Object.freeze({
17183
+ render: (path) => `"${path}" sets cacheComponents: true, which ohmyho.st does not support; remove it or set it to false. ${PUSH_AND_PLAN}`,
17184
+ pattern: /^"([^"]+)" sets cacheComponents: true, which ohmyho\.st does not support; remove it or set it to false\. Commit and push the change, then plan the new commit\.$/u
17029
17185
  })
17030
17186
  });
17031
17187
  function isFrameworkConversionDiagnosticCode(value) {
@@ -17037,7 +17193,7 @@ function parseFrameworkConversionDiagnostic(value) {
17037
17193
  const record5 = value;
17038
17194
  const path = record5["path"];
17039
17195
  const code = record5["code"];
17040
- if (Object.keys(record5).length !== 2 || !isFrameworkConversionDiagnosticCode(code) || typeof path !== "string" || path.length > 240 || !/^[A-Za-z0-9_@()[\]./-]+\.(?:[cm]?[jt]sx?)$/u.test(path) || path.split("/").some((segment) => segment === "" || segment === "." || segment === ".."))
17196
+ if (Object.keys(record5).length !== 2 || !isFrameworkConversionDiagnosticCode(code) || typeof path !== "string" || path.length > 240 || (code === "managed_auth_database_required" ? path !== "ohmyhost.yaml" : !/^[A-Za-z0-9_@()[\]./-]+\.(?:[cm]?[jt]sx?)$/u.test(path)) || path.split("/").some((segment) => segment === "" || segment === "." || segment === ".."))
17041
17197
  return null;
17042
17198
  return Object.freeze({ code, path });
17043
17199
  }
@@ -17059,36 +17215,36 @@ function parseFrameworkConversionDetail(value) {
17059
17215
  // packages/contracts/dist/operation-deployment-result.js
17060
17216
  function exactRecord(value, keys) {
17061
17217
  if (typeof value !== "object" || value === null || Array.isArray(value))
17062
- invalid();
17218
+ invalid2();
17063
17219
  const actual = Object.keys(value).sort();
17064
17220
  const expected = [...keys].sort();
17065
17221
  if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
17066
- invalid();
17222
+ invalid2();
17067
17223
  }
17068
17224
  return value;
17069
17225
  }
17070
17226
  function ulid2(value) {
17071
17227
  if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value))
17072
- invalid();
17228
+ invalid2();
17073
17229
  return value;
17074
17230
  }
17075
- function invalid() {
17231
+ function invalid2() {
17076
17232
  throw new TypeError("Operation deployment result is invalid");
17077
17233
  }
17078
17234
  var deploymentProgressActions = {
17079
17235
  queued: "The deployment is queued. Poll this same operation; do not submit another deployment.",
17080
17236
  building: "The build has not yet completed. Poll this same operation and read deployment logs if it fails.",
17081
17237
  publishing: "The build has completed. Runtime preparation or publication is still running; poll this same operation.",
17082
- waiting_for_mail: "Read 'ohmyhost mail domain status --project PROJECT --json' now and follow its verification issue and DNS records. Keep this operation; do not rebuild or repeat correct DNS changes.",
17083
- mail_status_unavailable: "Mail readiness could not be observed. Read 'ohmyhost mail domain status --project PROJECT --json' and report a persistent error; keep this operation."
17238
+ waiting_for_mail: "Read 'ohmyhost mail status --project PROJECT --environment PROD_ENVIRONMENT_ULID --json' (MCP mail_status) now, with the Prod environment ID from 'ohmyhost project status', and follow its verification issue and DNS records. Keep this operation; do not rebuild or repeat correct DNS changes.",
17239
+ mail_status_unavailable: "Mail readiness could not be observed. Read 'ohmyhost mail status --project PROJECT --environment PROD_ENVIRONMENT_ULID --json' (MCP mail_status) with the Prod environment ID from 'ohmyhost project status', and report a persistent error; keep this operation."
17084
17240
  };
17085
17241
  function createOperationDeploymentProgress(input) {
17086
17242
  const project = ulid2(input.project_id);
17087
17243
  ulid2(input.deployment_id);
17088
17244
  if (!Object.hasOwn(deploymentProgressActions, input.phase) || !Number.isFinite(Date.parse(input.observed_at)) || new Date(input.observed_at).toISOString() !== input.observed_at)
17089
- invalid();
17245
+ invalid2();
17090
17246
  if (input.build_completed_at !== void 0 && (!Number.isFinite(Date.parse(input.build_completed_at)) || new Date(input.build_completed_at).toISOString() !== input.build_completed_at))
17091
- invalid();
17247
+ invalid2();
17092
17248
  return Object.freeze({
17093
17249
  ...input.build_completed_at === void 0 ? {} : { build_completed_at: input.build_completed_at },
17094
17250
  phase: input.phase,
@@ -17119,7 +17275,7 @@ function parseOperationDeploymentProgress(value) {
17119
17275
  observed_at: row["observed_at"]
17120
17276
  });
17121
17277
  if (row["next_poll_after_seconds"] !== 60 || row["suggested_action"] !== parsed.suggested_action)
17122
- invalid();
17278
+ invalid2();
17123
17279
  return parsed;
17124
17280
  }
17125
17281
 
@@ -17223,12 +17379,12 @@ function assertOrganizationAccount(value) {
17223
17379
  // packages/contracts/dist/operation-failure.js
17224
17380
  var failures = {
17225
17381
  operation_abandoned: [
17226
- "The operation stopped renewing its execution lease and was ended.",
17227
- "Inspect project status and, when this operation names a deployment_id, its diagnostics with 'ohmyhost deployment logs --project PROJECT_ULID --deployment DEPLOYMENT_ULID --follow --json' or MCP deployment_logs before creating a fresh plan. Keep this operation ID; the expired execution cannot resume."
17382
+ "ohmyho.st stopped this operation before it finished and ended it.",
17383
+ "Repeating this operation cannot resume it. Read project status for what is live; if this operation names a deployment_id, read 'ohmyhost deployment logs --project PROJECT_ULID --deployment DEPLOYMENT_ULID --follow --json' or MCP deployment_logs. Then submit the request once more with a new idempotency key: a deployment, promotion, rollback or deletion from a fresh plan, a rename with the ETag from project status. If it ends the same way, report both operation IDs through feedback."
17228
17384
  ],
17229
17385
  database_write_rejected: [
17230
17386
  "The database rejected this write; it was not committed.",
17231
- "Inspect the schema and parameter types with database query, then correct the statement and use a new key. Do not reset the database."
17387
+ "The statement failed on PostgreSQL, ran or waited for a lock longer than 5 seconds, affected more than 1,000 rows, or was not one INSERT, UPDATE or DELETE. Check table, column and parameter types with database query, split a large change into batches of at most 1,000 rows, then submit with a new key. Nothing was committed; do not reset the database."
17232
17388
  ],
17233
17389
  database_write_unavailable: [
17234
17390
  "The database write could not start.",
@@ -17280,7 +17436,7 @@ var failures = {
17280
17436
  ],
17281
17437
  operation_failed: [
17282
17438
  "The operation failed.",
17283
- "Inspect the operation events and deployment diagnostics. Include the operation ID when reporting the issue."
17439
+ "Read project status and, if this operation names a deployment_id, its diagnostics with 'ohmyhost deployment logs --project PROJECT_ULID --deployment DEPLOYMENT_ULID --follow --json'. Without one (a source link or project creation), check project status or project list, then repeat that command once with a new idempotency key. If it fails again, report the operation ID through feedback."
17284
17440
  ],
17285
17441
  build_not_started: [
17286
17442
  "The deployment ended without starting a build.",
@@ -17288,7 +17444,7 @@ var failures = {
17288
17444
  ],
17289
17445
  build_failed: [
17290
17446
  "The build did not produce a verified deployable artifact.",
17291
- "Read the BUILD_FAILED diagnostic of this operation's deployment_id with 'ohmyhost deployment logs --project PROJECT_ULID --deployment DEPLOYMENT_ULID --follow --json' or MCP deployment_logs; its excerpt is the tail of the install and build output. Correct that failure and verify the commands for the admitted commit before planning a new deployment."
17447
+ "Read the BUILD_FAILED item of this operation's deployment_id with 'ohmyhost deployment logs --project PROJECT_ULID --deployment DEPLOYMENT_ULID --follow --json' or MCP deployment_logs. Its excerpt is the tail of your install and build output: fix that error, push and plan the new commit. Without an excerpt the build printed nothing, passed the 8-minute limit or failed inside ohmyho.st: plan the same commit once more, and report the operation ID through feedback if it fails again."
17292
17448
  ],
17293
17449
  insufficient_organization_credits: [
17294
17450
  "The organization does not have sufficient available credits for this operation.",
@@ -17296,27 +17452,39 @@ var failures = {
17296
17452
  ],
17297
17453
  paid_plan_required: [
17298
17454
  "Transactional mail requires an active Paid plan for this organization.",
17299
- "Activate or renew Paid for the organization, then create a new deployment plan. Replaying this terminal operation cannot enable mail. Do not change application code or reset its database."
17455
+ "Ask the organization Owner to activate Paid through billing checkout; top-ups and signup credits do not activate it. Then plan the same deployment or promotion again. If the app should send no mail, instead set mail.enabled to false in ohmyhost.yaml, commit and push, and plan the new commit. Replaying this operation cannot enable mail. Do not reset the database."
17300
17456
  ],
17301
17457
  database_migration_failed: [
17302
- "The database migrations could not be applied.",
17303
- "Inspect the migration diagnostics and verify the canonical SQL migrations on PostgreSQL. Preserve production data; do not reset the database to retry a deployment."
17458
+ "The database migrations were not applied; none of this deployment's new migration files took effect.",
17459
+ "Read the DATABASE_MIGRATION_FAILED item in 'ohmyhost deployment logs --project PROJECT_ULID --deployment DEPLOYMENT_ULID --follow --json' and fix what it names; run 'ohmyhost init --dry-run --json' for migration_sql_not_admitted. Keep applied files unchanged, give new files later timestamps, and add to existing tables only nullable columns without default and non-unique indexes. Push and plan the new commit; never reset the database."
17304
17460
  ],
17305
17461
  runtime_candidate_failed: [
17306
17462
  "The deployed candidate failed its runtime checks.",
17307
- "Read the HEALTH_CHECK_FAILED diagnostic of this operation's deployment_id with 'ohmyhost deployment logs --project PROJECT_ULID --deployment DEPLOYMENT_ULID --follow --json' or MCP deployment_logs. Its route must answer 2xx to a cookieless GET without a redirect; status_code and, for owners, excerpt show what it answered. Fix that, often a missing secret or a redirect, then plan a new deployment."
17463
+ "Read the HEALTH_CHECK_FAILED item with 'ohmyhost deployment logs --project PROJECT_ULID --deployment DEPLOYMENT_ULID --follow --json' or MCP deployment_logs. ohmyhost.yaml runtime.healthcheck (default /) must answer 2xx to a cookieless GET without redirect; status_code and, for owners, excerpt show the answer. After a promotion or rollback, fix that environment, often a missing secret, and plan it again; else fix the app, push and plan the new commit. No such item: plan once more, then report."
17308
17464
  ],
17309
17465
  runtime_candidate_rejected: [
17310
- "The runtime rejected the built Worker script.",
17311
- "Check that src/ohmyhost/worker.ts exports its handlers as the default export and imports no framework-only modules, then plan the new commit. Replaying this terminal operation cannot stage the same script."
17466
+ "The runtime refused to load the built Worker.",
17467
+ "The built Worker could not be loaded: top-level code throws or calls fetch, timers or random values outside a handler, an import cannot be resolved, a Worker module such as src/ohmyhost/companion.ts has no default export, or the bundle is too large. Fix that, push and plan the new commit; replaying this operation cannot pass. If none of this applies, report the operation ID through feedback."
17312
17468
  ],
17313
17469
  storage_jurisdiction_conflict: [
17314
17470
  "storage.jurisdiction must equal the project's hosting region.",
17315
- "Set storage.jurisdiction in ohmyhost.yaml to the project's hosting region shown by project status and plan again. A project cannot move its files between regions; do not delete or recreate the project to change it."
17471
+ "Set storage.jurisdiction in ohmyhost.yaml to the project's region (us or eu) shown by 'ohmyhost project status', commit and push, then plan the new commit (for a promotion, deploy it to Dev and promote again); the unchanged commit fails again. A project cannot move its files between regions; do not delete or recreate the project to change it."
17316
17472
  ],
17317
17473
  provider_state_absent: [
17318
17474
  "Reconciliation confirmed that the required deployment resources are absent.",
17319
17475
  "Create a fresh deployment plan. The original operation is terminal; do not repeatedly reconcile it."
17476
+ ],
17477
+ auto_deploy_plan_rejected: [
17478
+ "The GitHub push could not be planned for deployment.",
17479
+ "Read project status and the operation's error or deployment diagnostics. Correct the source or configuration named there, push the new commit and plan it before deploying. Replaying this failed operation cannot start a build."
17480
+ ],
17481
+ promotion_source_stale: [
17482
+ "The Dev deployment changed before promotion could start.",
17483
+ "Read the current Dev deployment with 'ohmyhost project status --project ULID --json', then run 'ohmyhost deployment promote plan' for that succeeded deployment and confirm the new plan. Replaying this terminal promotion cannot select another source."
17484
+ ],
17485
+ promotion_target_stale: [
17486
+ "The Prod deployment changed before promotion could start.",
17487
+ "Read current Prod status, run 'ohmyhost deployment promote plan' again and confirm it with the new resource_etag and confirmation_token. Replaying this terminal operation cannot replace its stale target."
17320
17488
  ]
17321
17489
  };
17322
17490
  function publicOperationReconciliation(observation) {
@@ -17336,280 +17504,436 @@ function publicOperationFailure(internalCode) {
17336
17504
  return Object.freeze({ code, message, retryable: false, suggested_action });
17337
17505
  }
17338
17506
 
17339
- // apps/product-cli/dist/runtime-contract.js
17507
+ // packages/contracts/dist/problem-policy.js
17340
17508
  var PROBLEM_POLICY = {
17341
17509
  cloudflare_zone_not_bound: {
17342
17510
  status: 409,
17343
17511
  retryable: false,
17344
- action: "Configure the project domain first with 'ohmyhost domain paid plan' then 'ohmyhost domain paid apply', or configure its sender domain with 'ohmyhost mail setup'. Then request Cloudflare authorization for that domain's zone."
17512
+ action: "Configure the project domain first with 'ohmyhost domain paid plan' then 'ohmyhost domain paid apply', or configure its sender domain with 'ohmyhost mail setup'. Then request Cloudflare authorization for that domain's zone.",
17513
+ title: "Cloudflare zone is not bound to this project"
17345
17514
  },
17346
17515
  github_connection_required: {
17347
17516
  status: 409,
17348
17517
  retryable: false,
17349
- action: "This workspace has no GitHub connection yet. Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' for it, complete its browser link, then repeat the original source link. If the project belongs to another account, use that account's login instead; error.acting_as names the login this ran as."
17518
+ action: "This workspace has no GitHub connection yet. Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' for it, complete its browser link, then repeat the original source link. If the project belongs to another account, use that account's login instead; error.acting_as in the CLI and MCP names the login this ran as.",
17519
+ title: "GitHub connection required"
17350
17520
  },
17351
17521
  github_connection_revoked: {
17352
17522
  status: 409,
17353
17523
  retryable: false,
17354
- action: "Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' to restore this workspace's GitHub connection, then repeat the original source link."
17524
+ action: "The workspace's GitHub App installation was removed or can no longer be used. Run 'ohmyhost github connect --organization ULID --idempotency-key NEW_KEY --json' with a new key (an earlier key only replays the old connection), send the customer its authorization_url, repeat that command with the same new key until status is connected, then repeat the original source link.",
17525
+ title: "GitHub connection revoked",
17526
+ restAction: "The workspace's GitHub App installation was removed or can no longer be used. Run 'ohmyhost github connect --organization ULID --idempotency-key NEW_KEY --json' with a new key (an earlier key only replays the old connection), send the customer its authorization_url, repeat that command with the same new key until status is connected, then repeat the original source link."
17355
17527
  },
17356
17528
  repository_not_installed: {
17357
17529
  status: 403,
17358
17530
  retryable: false,
17359
- action: "Read 'ohmyhost github status --organization ULID --json', open connection.settings_url and add this repository to the installation. Then repeat the original source link and key."
17531
+ action: "Read 'ohmyhost github status --organization ULID --json', open connection.settings_url and add this repository to the installation. Then repeat the original source link and key.",
17532
+ title: "Repository access required"
17360
17533
  },
17361
17534
  database_write_pending: {
17362
17535
  status: 409,
17363
17536
  retryable: false,
17364
- action: "Poll the original database-write operation before transfer or deletion."
17537
+ action: "A database write of this project is still running, so the project cannot be deleted yet. Read latest_operation from 'ohmyhost project status --project ULID --json' until it is no longer running; writes are short. Then repeat the deletion, planning it again if its confirmation expired.",
17538
+ title: "Database write in progress",
17539
+ restAction: "A database write of this project is still running, so the project cannot be deleted yet. Read latest_operation from 'ohmyhost project status --project ULID --json' until it is no longer running; writes are short. Then repeat the deletion, planning it again if its confirmation expired."
17365
17540
  },
17366
17541
  cloudflare_authorization_closed: {
17367
17542
  status: 409,
17368
17543
  retryable: false,
17369
- action: "Read Cloudflare DNS status. Reuse a valid grant for the requested zone; otherwise request fresh authorization with a new idempotency key. Do not replay the callback."
17544
+ action: "Read Cloudflare DNS status. Reuse a valid grant for the requested zone; otherwise request fresh authorization with a new idempotency key. Do not replay the callback.",
17545
+ title: "Cloudflare authorization closed"
17370
17546
  },
17371
17547
  billing_recharge_conflict: {
17372
17548
  status: 409,
17373
17549
  retryable: false,
17374
- action: "Read billing recharge get and the current revision. Preserve the original key after uncertainty; do not start a checkout or automatically re-enable charges."
17550
+ action: "The auto-recharge settings changed since you read them, or this key was used with other settings. Run 'ohmyhost billing recharge get --organization ULID --json'. If it already shows the requested change, stop. Otherwise, only if the Owner still wants it, send the change again with the returned revision and a new idempotency key. Never re-enable charges without the Owner's approval.",
17551
+ title: "Auto-recharge settings conflict",
17552
+ restAction: "The auto-recharge settings changed since you read them, or this key was used with other settings. Run 'ohmyhost billing recharge get --organization ULID --json'. If it already shows the requested change, stop. Otherwise, only if the Owner still wants it, send the change again with the returned revision and a new idempotency key. Never re-enable charges without the Owner's approval."
17375
17553
  },
17376
17554
  billing_purchase_conflict: {
17377
17555
  status: 409,
17378
17556
  retryable: false,
17379
- action: "Read the original checkout and use billing portal to manage the existing subscription. Do not create another purchase after uncertainty."
17557
+ action: "This workspace already has Paid or an open Paid checkout, this key belongs to a checkout with a different offer or pack count, or, for 'billing portal', the workspace has never started a checkout. Read the original checkout with 'ohmyhost billing status --organization ULID --checkout CHECKOUT_ULID --json'; manage an existing Paid subscription with 'ohmyhost billing portal'. Never start a second purchase after an uncertain one.",
17558
+ title: "Billing purchase conflict",
17559
+ restAction: "This workspace already has Paid or an open Paid checkout, this key belongs to a checkout with a different offer or pack count, or, for 'billing portal', the workspace has never started a checkout. Read the original checkout with 'ohmyhost billing status --organization ULID --checkout CHECKOUT_ULID --json'; manage an existing Paid subscription with 'ohmyhost billing portal'. Never start a second purchase after an uncertain one."
17380
17560
  },
17381
17561
  compute_performance_paid_required: {
17382
17562
  status: 403,
17383
17563
  retryable: false,
17384
- action: "Check Paid service and usable credits or grace, or choose standard compute."
17564
+ action: "Performance compute needs active Paid and usable credits or grace; repeating unchanged does not help. Read 'ohmyhost credits account --organization ULID --json'. On Free, the Owner may start Paid with 'ohmyhost billing checkout --offer paid'; without usable credits, they may add credits. Until then, keep standard compute.",
17565
+ title: "Performance requires Paid access",
17566
+ restAction: "Performance compute needs active Paid and usable credits or grace; repeating unchanged does not help. Read 'ohmyhost credits account --organization ULID --json'. On Free, the Owner may start Paid with 'ohmyhost billing checkout --offer paid'; without usable credits, they may add credits. Until then, keep standard compute."
17385
17567
  },
17386
17568
  compute_performance_unavailable: {
17387
17569
  status: 503,
17388
17570
  retryable: false,
17389
- action: "Choose standard compute or report inactive performance pricing through feedback. Do not repeatedly resubmit."
17571
+ action: "Choose standard compute or report inactive performance pricing through feedback. Do not repeatedly resubmit.",
17572
+ title: "Performance pricing unavailable"
17390
17573
  },
17391
17574
  compute_change_pending: {
17392
17575
  status: 409,
17393
17576
  retryable: false,
17394
- action: "Poll the existing operation after 60 seconds. Do not submit another size change, deletion or transfer while it runs."
17577
+ action: "A database size change of this project is still running. Every 60 seconds read latest_operation from 'ohmyhost project status --project ULID --json', or 'ohmyhost operation get OPERATION_ID --json' for the change you started, until it ends, then repeat this request. Do not submit another size change or a deletion meanwhile.",
17578
+ title: "Database size change running",
17579
+ restAction: "A database size change of this project is still running. Every 60 seconds read latest_operation from 'ohmyhost project status --project ULID --json', or 'ohmyhost operation get OPERATION_ID --json' for the change you started, until it ends, then repeat this request. Do not submit another size change or a deletion meanwhile."
17395
17580
  },
17396
17581
  compute_change_conflict: {
17397
17582
  status: 409,
17398
17583
  retryable: false,
17399
- action: "Read the original operation and current database compute. Preserve data and report unresolved conflicts through feedback with the operation ID."
17584
+ action: "Read the original operation and current database compute. Preserve data and report unresolved conflicts through feedback with the operation ID.",
17585
+ title: "Database size change conflict"
17400
17586
  },
17401
17587
  database_access_limit: {
17402
17588
  status: 409,
17403
17589
  retryable: false,
17404
- action: "List the project's database credentials and revoke one you no longer need, then request access again."
17590
+ action: "List the project's database credentials and revoke one you no longer need, then request access again.",
17591
+ title: "Database access limit reached"
17405
17592
  },
17406
17593
  project_notes_conflict: {
17407
17594
  status: 409,
17408
17595
  retryable: false,
17409
- action: "Read project context, merge the current notes and retry with its version and a new idempotency key."
17596
+ action: "Read project context, merge the current notes and retry with its version and a new idempotency key.",
17597
+ title: "Project notes changed"
17598
+ },
17599
+ invalid_request: {
17600
+ status: 400,
17601
+ retryable: false,
17602
+ action: "The server rejected this request; repeating it unchanged fails again. Check every argument against the command usage or the tool schema. For 'mail webhook verify' and mail receiving, the app's webhook route must answer the signed test with 2xx using the installed signing secret; receiving needs a verified webhook. For 'database query', send one read-only SELECT or WITH statement that runs on the current schema.",
17603
+ title: "Invalid request",
17604
+ restAction: "Read detail for the rejected field or condition, correct it and send the corrected request; repeating it unchanged fails again."
17410
17605
  },
17411
- invalid_request: { status: 400, retryable: false, action: "Check the request and retry." },
17412
17606
  unauthenticated: {
17413
17607
  status: 401,
17414
17608
  retryable: false,
17415
- action: "Check OHMYHOST_TOKEN and the selected environment. Replace invalid tokens, or unset OHMYHOST_TOKEN and run 'ohmyhost login'."
17609
+ action: "The login or token was not accepted; repeating unchanged does not help. If OHMYHOST_TOKEN is set, it was revoked, mistyped or belongs to a different platform than OHMYHOST_ENVIRONMENT selects (a platform setting, not the project's dev or prod): ask the customer for a current token from the portal's API Tokens page, or unset it and run 'ohmyhost login --json'. Otherwise run 'ohmyhost login --json', then repeat the request.",
17610
+ title: "Authentication required",
17611
+ restAction: "The login or token was not accepted; repeating unchanged does not help. If OHMYHOST_TOKEN is set, it was revoked, mistyped or belongs to a different platform than OHMYHOST_ENVIRONMENT selects (a platform setting, not the project's dev or prod): ask the customer for a current token from the portal's API Tokens page, or unset it and run 'ohmyhost login --json'. Otherwise run 'ohmyhost login --json', then repeat the request."
17612
+ },
17613
+ forbidden: {
17614
+ status: 403,
17615
+ retryable: false,
17616
+ action: "This login may not do this; repeating unchanged does not help. error.acting_as names the login used: use a workspace this login belongs to ('ohmyhost whoami --json'), or the login of the account that owns it. For 'organization create', pass --source exactly as the Signup source on the portal Profile page, or omit it when that page shows Direct.",
17617
+ title: "Forbidden",
17618
+ restAction: "This identity may not do this; repeating unchanged does not help. Use a workspace this identity belongs to. A new workspace must preserve the signup source already recorded for this user; a scoped API token may create another workspace through the ordinary authorized flow."
17416
17619
  },
17417
- forbidden: { status: 403, retryable: false, action: "Check organization access." },
17418
17620
  organization_required: {
17419
17621
  status: 409,
17420
17622
  retryable: false,
17421
- action: "Run 'ohmyhost organization list --json' and select one with 'ohmyhost organization use --organization ULID --json', or create the first workspace."
17623
+ action: "Run 'ohmyhost organization list --json' and select one with 'ohmyhost organization use --organization ULID --json', or create the first workspace.",
17624
+ title: "Workspace selection required"
17422
17625
  },
17423
17626
  resource_not_found: {
17424
17627
  status: 404,
17425
17628
  retryable: false,
17426
- action: "Check the resource identifier and the account: a project is visible only to its own organization, and error.acting_as names the login this ran as. Use the login of the account that owns it."
17629
+ action: "Check the resource identifier and the account: a project is visible only to its own organization, and error.acting_as in the CLI and MCP names the login this ran as. Use the login of the account that owns it.",
17630
+ title: "Resource not found"
17427
17631
  },
17428
17632
  idempotency_key_reused: {
17429
17633
  status: 409,
17430
17634
  retryable: false,
17431
- action: "Use a new idempotency key for a different request."
17635
+ action: "This key was already used for a request with different arguments or from another login. To repeat that original request, send exactly its original arguments with this key from the same login; for a new request, use a new key. Never pair a new key with a request whose first attempt had an uncertain outcome: read its result first.",
17636
+ title: "Idempotency key reused",
17637
+ restAction: "This key was already used for a request with different arguments or from another login. To repeat that original request, send exactly its original arguments with this key from the same login; for a new request, use a new key. Never pair a new key with a request whose first attempt had an uncertain outcome: read its result first."
17432
17638
  },
17433
17639
  project_handle_unavailable: {
17434
17640
  status: 503,
17435
17641
  retryable: true,
17436
- action: "Retry project creation with the same idempotency key."
17642
+ action: "Retry project creation with the same idempotency key.",
17643
+ title: "Project handle unavailable"
17437
17644
  },
17438
17645
  project_handle_taken: {
17439
17646
  status: 409,
17440
17647
  retryable: false,
17441
- action: "Run 'ohmyhost project handle check' for a free address, then rename to one of the alternatives it returns."
17648
+ action: "Another project uses this address. Run 'ohmyhost project handle check --handle NAME --json' and offer the customer its alternatives; rename only to the one they choose.",
17649
+ title: "Project address is taken",
17650
+ restAction: "Another project uses this address. Run 'ohmyhost project handle check --handle NAME --json' and offer the customer its alternatives; rename only to the one they choose."
17442
17651
  },
17443
17652
  project_handle_invalid: {
17444
17653
  status: 409,
17445
17654
  retryable: false,
17446
- action: "Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words."
17655
+ action: "Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words.",
17656
+ title: "Project address is invalid"
17447
17657
  },
17448
17658
  project_handle_unchanged: {
17449
17659
  status: 409,
17450
17660
  retryable: false,
17451
- action: "The project already answers on this address; no rename is needed."
17661
+ action: "The project already answers on this address; no rename is needed.",
17662
+ title: "Project address is unchanged"
17452
17663
  },
17453
17664
  project_rename_blocked: {
17454
17665
  status: 409,
17455
17666
  retryable: true,
17456
- action: "Wait for the running operation to finish, read the project again, then rename with the new ETag."
17667
+ action: "Wait for the running operation to finish, read the project again, then rename with the new ETag.",
17668
+ title: "Project address change blocked"
17457
17669
  },
17458
17670
  project_identity_unavailable: {
17459
17671
  status: 503,
17460
17672
  retryable: true,
17461
- action: "Retry project creation with the same idempotency key."
17673
+ action: "Retry project creation with the same idempotency key.",
17674
+ title: "Project identity unavailable"
17462
17675
  },
17463
17676
  deployment_plan_expired: {
17464
17677
  status: 409,
17465
17678
  retryable: false,
17466
- action: "Run 'ohmyhost plan' again, then deploy the new plan."
17679
+ action: "Run 'ohmyhost plan' again, then deploy the new plan.",
17680
+ title: "Deployment plan expired"
17467
17681
  },
17468
17682
  deployment_plan_incompatible: {
17469
17683
  status: 409,
17470
17684
  retryable: false,
17471
- action: "Run 'ohmyhost plan' again with the current source revision."
17685
+ action: "Run 'ohmyhost plan' again with the current source revision.",
17686
+ title: "Deployment plan incompatible"
17472
17687
  },
17473
17688
  confirmation_expired: {
17474
17689
  status: 409,
17475
17690
  retryable: false,
17476
- action: "Plan the destructive action again before confirming it."
17691
+ action: "Plan the destructive action again before confirming it.",
17692
+ title: "Confirmation expired"
17477
17693
  },
17478
17694
  confirmation_invalid: {
17479
17695
  status: 409,
17480
17696
  retryable: false,
17481
- action: "Use the confirmation token from a fresh matching plan."
17697
+ action: "The confirmation token does not match this action or the project's current state. A deletion is also refused while a SQL export of the project is being captured, for up to about ten minutes ('ohmyhost export get'). Then create a new plan and confirm with its confirmation_token and resource_etag before the token expires.",
17698
+ title: "Confirmation invalid",
17699
+ restAction: "The confirmation token does not match this action or the project's current state. A deletion is also refused while a SQL export of the project is being captured, for up to about ten minutes ('ohmyhost export get'). Then create a new plan and confirm with its confirmation_token and resource_etag before the token expires."
17482
17700
  },
17483
17701
  etag_mismatch: {
17484
17702
  status: 409,
17485
17703
  retryable: false,
17486
- action: "Refresh the resource, plan the action again, and retry."
17704
+ action: "The project changed after you read its ETag. For a rename, read etag from 'ohmyhost project status --project ULID --json' and repeat 'ohmyhost project handle set' with it. For a rollback, promotion or deletion, create a new plan and confirm with its resource_etag and confirmation_token.",
17705
+ title: "Resource changed",
17706
+ restAction: "The project changed after you read its ETag. For a rename, read etag from 'ohmyhost project status --project ULID --json' and repeat 'ohmyhost project handle set' with it. For a rollback, promotion or deletion, create a new plan and confirm with its resource_etag and confirmation_token."
17487
17707
  },
17488
17708
  promotion_source_stale: {
17489
17709
  status: 409,
17490
17710
  retryable: false,
17491
- action: "Read the current dev deployment, then plan promotion again."
17711
+ action: "Read the current dev deployment, then plan promotion again.",
17712
+ title: "Promotion source changed"
17492
17713
  },
17493
17714
  promotion_target_stale: {
17494
17715
  status: 409,
17495
17716
  retryable: false,
17496
- action: "Read the current prod status, then plan promotion again."
17717
+ action: "Read the current prod status, then plan promotion again.",
17718
+ title: "Promotion target changed"
17497
17719
  },
17498
17720
  promotion_invalid_target: {
17499
17721
  status: 409,
17500
17722
  retryable: false,
17501
- action: "Select the current succeeded dev deployment and target prod."
17723
+ action: "Select the current succeeded dev deployment and target prod.",
17724
+ title: "Promotion target invalid"
17502
17725
  },
17503
17726
  mail_capacity_unavailable: {
17504
17727
  status: 503,
17505
17728
  retryable: true,
17506
- action: "Report this request ID through feedback. The stored mail setup continues automatically once ohmyho.st has capacity; follow mail status and keep the original idempotency key."
17729
+ action: "Report this request ID through feedback. The stored mail setup continues automatically once ohmyho.st has capacity; follow mail status and keep the original idempotency key.",
17730
+ title: "Mail capacity unavailable"
17507
17731
  },
17508
17732
  mail_domain_conflict: {
17509
17733
  status: 409,
17510
17734
  retryable: false,
17511
- action: "Read and use the sender domain already configured for this project."
17735
+ action: "Use the sender domain already configured for this project, or retire it with 'ohmyhost mail domain delete' (repeat while the removal is pending) and then set up the new domain; a domain registered to another project cannot be set up.",
17736
+ title: "Mail domain conflict"
17512
17737
  },
17513
17738
  mail_domain_required: {
17514
17739
  status: 409,
17515
17740
  retryable: false,
17516
- action: "This app declares mail.enabled. Configure the project's own sender domain with 'ohmyhost mail setup' using its Prod environment ID, or set mail.enabled to false if the app sends no mail, then plan again."
17741
+ action: "This app declares mail.enabled. Configure the project's own sender domain with 'ohmyhost mail setup' using its Prod environment ID, or set mail.enabled to false if the app sends no mail, then plan again.",
17742
+ title: "Mail domain required"
17517
17743
  },
17518
17744
  storage_jurisdiction_conflict: {
17519
17745
  status: 409,
17520
17746
  retryable: false,
17521
- action: "Set storage.jurisdiction in ohmyhost.yaml to the project's hosting region shown by project status, then plan again."
17747
+ action: "Set storage.jurisdiction in ohmyhost.yaml to the project's region (us or eu) shown by 'ohmyhost project status', commit and push, then plan the new commit (for a promotion, deploy it to Dev and promote again); the unchanged commit fails again. A project cannot move its files between regions; do not delete or recreate the project to change it.",
17748
+ title: "Storage jurisdiction conflict",
17749
+ restAction: "Set storage.jurisdiction in ohmyhost.yaml to the project's region (us or eu) shown by 'ohmyhost project status', commit and push, then plan the new commit (for a promotion, deploy it to Dev and promote again); the unchanged commit fails again. A project cannot move its files between regions; do not delete or recreate the project to change it."
17522
17750
  },
17523
17751
  shared_data_requires_promotion: {
17524
17752
  status: 409,
17525
17753
  retryable: false,
17526
- action: "Plan and deploy this commit to Dev, then run 'ohmyhost deployment promote plan' to reach Prod."
17754
+ action: "Plan and deploy this commit to Dev, then run 'ohmyhost deployment promote plan' to reach Prod.",
17755
+ title: "Shared data requires promotion"
17527
17756
  },
17528
17757
  production_deployment_required: {
17529
17758
  status: 409,
17530
17759
  retryable: false,
17531
- action: "Deploy to Prod first: run 'ohmyhost plan --environment prod' and deploy that plan, or promote the current Dev deployment with 'ohmyhost deployment promote plan'. Then plan the domain again."
17760
+ action: "Deploy to Prod first: run 'ohmyhost plan --environment prod' and deploy that plan, or promote the current Dev deployment with 'ohmyhost deployment promote plan'. Then plan the domain again.",
17761
+ title: "Production deployment required"
17532
17762
  },
17533
17763
  framework_conversion_required: {
17534
17764
  status: 409,
17535
17765
  retryable: false,
17536
- action: "Run 'ohmyhost init', apply the matching ohmyhost framework skill, then plan the new commit."
17766
+ action: "Read detail for the file, reason and required change. Run ohmyhost init --dry-run --json at that commit, apply its blockers and the matching ohmyho.st framework Skill, then commit and push. Plan the new commit; repeating the unchanged source cannot fix an admission error.",
17767
+ title: "Framework conversion required",
17768
+ restAction: "Read detail for the file, reason and required change. Run ohmyhost init --dry-run --json at that commit, apply its blockers and the matching ohmyho.st framework Skill, then commit and push. Plan the new commit; repeating the unchanged source cannot fix an admission error."
17537
17769
  },
17538
17770
  repository_configuration_missing: {
17539
17771
  status: 409,
17540
17772
  retryable: false,
17541
- action: "Place ohmyhost.yaml at the Git repository root and set applicationRoot to the application directory (for example website). Commit and push the change, then plan the new commit."
17773
+ action: "The planned commit has no ohmyhost.yaml at the Git repository root. If the repository has none, run 'ohmyhost init --json' at the root (--root DIR when the app lives in a subdirectory, --region eu for an EU project). Otherwise move the existing file to the root and set applicationRoot to the app directory, for example website. Commit and push, then plan the new commit.",
17774
+ title: "Repository configuration missing",
17775
+ restAction: "The planned commit has no ohmyhost.yaml at the Git repository root. If the repository has none, run 'ohmyhost init --json' at the root (--root DIR when the app lives in a subdirectory, --region eu for an EU project). Otherwise move the existing file to the root and set applicationRoot to the app directory, for example website. Commit and push, then plan the new commit."
17542
17776
  },
17543
17777
  migration_filename_noncanonical: {
17544
17778
  status: 409,
17545
17779
  retryable: false,
17546
- action: "Rename every migration to YYYYMMDDHHMMSS_name.sql, commit the change, then plan again."
17780
+ action: "Every file in the migrations directory (database.migrations in ohmyhost.yaml) must be named YYYYMMDDHHMMSS_name.sql: 14 digits, an underscore, then lowercase letters, digits, _ or -. Rename each file that does not match and move any other file, such as README.md or .gitkeep, out of that directory. Commit and push, then plan the new commit. Never rename a migration a deployment already applied.",
17781
+ title: "Migration filename noncanonical",
17782
+ restAction: "Every file in the migrations directory (database.migrations in ohmyhost.yaml) must be named YYYYMMDDHHMMSS_name.sql: 14 digits, an underscore, then lowercase letters, digits, _ or -. Rename each file that does not match and move any other file, such as README.md or .gitkeep, out of that directory. Commit and push, then plan the new commit. Never rename a migration a deployment already applied."
17547
17783
  },
17548
17784
  environment_secret_mutation_blocked: {
17549
17785
  status: 409,
17550
17786
  retryable: true,
17551
- action: "Wait for the active deployment or rollback to finish, then retry unchanged."
17787
+ action: "Wait for the active deployment or rollback to finish, then retry unchanged.",
17788
+ title: "Environment secret mutation blocked"
17552
17789
  },
17553
17790
  workers_runtime_incompatible: {
17554
17791
  status: 409,
17555
17792
  retryable: false,
17556
- action: "Remove or replace the reported module, commit the change, then create a new plan."
17793
+ action: "A Next.js file under app/, pages/, middleware or proxy imports a native addon (.node) or a Node.js module that does not run on Workers: child_process, cluster, dgram, domain, http2, inspector, readline, repl, sqlite, trace_events, tty, v8, vm, wasi, worker_threads or _stream_wrap, with or without node:. Remove or replace that import, commit and push, then plan the new commit.",
17794
+ title: "Workers runtime incompatible",
17795
+ restAction: "A Next.js file under app/, pages/, middleware or proxy imports a native addon (.node) or a Node.js module that does not run on Workers: child_process, cluster, dgram, domain, http2, inspector, readline, repl, sqlite, trace_events, tty, v8, vm, wasi, worker_threads or _stream_wrap, with or without node:. Remove or replace that import, commit and push, then plan the new commit."
17796
+ },
17797
+ payload_too_large: {
17798
+ status: 413,
17799
+ retryable: false,
17800
+ action: "Reduce the request size.",
17801
+ title: "Payload too large"
17557
17802
  },
17558
- payload_too_large: { status: 413, retryable: false, action: "Reduce the request size." },
17559
17803
  project_export_not_ready: {
17560
17804
  status: 409,
17561
17805
  retryable: false,
17562
- action: "Finish database provisioning, then request the export again. No backup was accepted."
17806
+ action: "No database of this project is ready, so no export was accepted. A database is created when a commit whose ohmyhost.yaml sets database.enabled: true is deployed; request the export again after that deployment succeeds ('ohmyhost operation get OPERATION_ID --json'). A project without a database has no SQL export.",
17807
+ title: "Project database not ready for export",
17808
+ restAction: "No database of this project is ready, so no export was accepted. A database is created when a commit whose ohmyhost.yaml sets database.enabled: true is deployed; request the export again after that deployment succeeds ('ohmyhost operation get OPERATION_ID --json'). A project without a database has no SQL export."
17563
17809
  },
17564
17810
  powered_by_flag_required: {
17565
17811
  status: 409,
17566
17812
  retryable: false,
17567
- action: "Keep the flag on, remove the custom domain with 'ohmyhost domain paid delete', or buy Paid, then switch the flag off."
17813
+ action: "Keep the flag on, remove the custom domain with 'ohmyhost domain paid delete', or buy Paid, then switch the flag off.",
17814
+ title: "Powered-by flag required"
17815
+ },
17816
+ rate_limited: {
17817
+ status: 429,
17818
+ retryable: true,
17819
+ action: "Too many requests. Wait retry_after_seconds, or 60 seconds when none is shown, then repeat the same request with the same idempotency key. SQL exports allow one request per project per rolling 24 hours: poll the export already requested with 'ohmyhost export get EXPORT_ID --project ULID --json' instead of creating another.",
17820
+ title: "Rate limited",
17821
+ restAction: "Wait the Retry-After interval (also retry_after_seconds), then repeat the same request with the same idempotency key."
17568
17822
  },
17569
- rate_limited: { status: 429, retryable: true, action: "Retry after the server delay." },
17570
17823
  insufficient_organization_credits: {
17571
17824
  status: 402,
17572
17825
  retryable: false,
17573
- action: "Add organization credits or wait for reservations to settle, then retry the same request."
17826
+ action: "Add organization credits or wait for reservations to settle, then retry the same request.",
17827
+ title: "Insufficient organization credits"
17574
17828
  },
17575
17829
  project_budget_exceeded: {
17576
17830
  status: 402,
17577
17831
  retryable: false,
17578
- action: "Raise the project's stop budget or select continue, then retry the same request."
17832
+ action: "This project's Stop budget for the current UTC month is reached; nothing was started. Show the customer 'ohmyhost budget get --project ULID --json'. Only on their request raise the limit or switch the mode to continue with 'ohmyhost budget set', then retry the same request; otherwise wait for the next UTC month.",
17833
+ title: "Project budget exceeded",
17834
+ restAction: "This project's Stop budget for the current UTC month is reached; nothing was started. Show the customer 'ohmyhost budget get --project ULID --json'. Only on their request raise the limit or switch the mode to continue with 'ohmyhost budget set', then retry the same request; otherwise wait for the next UTC month."
17579
17835
  },
17580
17836
  paid_plan_required: {
17581
17837
  status: 402,
17582
17838
  retryable: false,
17583
- action: "Ask the organization Owner to activate Paid through billing checkout, then retry. Top-ups and signup credits do not activate Paid. For a website domain alone, a Free workspace may instead show the powered-by flag on the project."
17839
+ action: "Ask the organization Owner to activate Paid through billing checkout, then retry. Top-ups and signup credits do not activate Paid. For a website domain alone, a Free workspace may instead show the powered-by flag on the project.",
17840
+ title: "Paid plan required"
17584
17841
  },
17585
17842
  interactive_login_required: {
17586
17843
  status: 403,
17587
17844
  retryable: false,
17588
- action: "Unset OHMYHOST_TOKEN and run ohmyhost login before managing tokens. Existing token files remain unchanged."
17845
+ action: "Unset OHMYHOST_TOKEN and run ohmyhost login before managing tokens. Existing token files remain unchanged.",
17846
+ title: "Interactive login required"
17589
17847
  },
17590
17848
  api_key_creation_uncertain: {
17591
17849
  status: 503,
17592
17850
  retryable: true,
17593
- action: "Repeat the original token name and Idempotency-Key to observe its result; do not blindly create another key."
17851
+ action: "Repeat the original token name and Idempotency-Key to observe its result; do not blindly create another key.",
17852
+ title: "Token creation unconfirmed"
17594
17853
  },
17595
17854
  api_key_permissions_unavailable: {
17596
17855
  status: 503,
17597
17856
  retryable: false,
17598
- action: "Report the platform user-key permission configuration through feedback. Repeated login or creation will not fix it."
17857
+ action: "Report the platform user-key permission configuration through feedback. Repeated login or creation will not fix it.",
17858
+ title: "Token permissions unavailable"
17599
17859
  },
17600
17860
  reconciliation_exhausted: {
17601
17861
  status: 409,
17602
17862
  retryable: false,
17603
- action: "Stop retries and submit feedback with this operation ID. A platform recovery review is required."
17863
+ action: "Stop retries and submit feedback with this operation ID. A platform recovery review is required.",
17864
+ title: "Recovery attempts exhausted"
17865
+ },
17866
+ service_unavailable: {
17867
+ status: 503,
17868
+ retryable: true,
17869
+ action: "The service could not complete or confirm this request; it may already have taken effect. Wait, then repeat exactly the same request with the same idempotency key, waiting longer each time; never switch to a new key. If 'ohmyhost plan' keeps failing, check that the commit is pushed to the linked repository and that 'ohmyhost init --dry-run --json' at that commit reports no blockers. If it still fails, report the request_id through feedback.",
17870
+ title: "Service unavailable",
17871
+ restAction: "The service could not complete or confirm this request; it may already have taken effect. Wait, then repeat exactly the same request with the same idempotency key, waiting longer each time; never switch to a new key. If 'ohmyhost plan' keeps failing, check that the commit is pushed to the linked repository and that 'ohmyhost init --dry-run --json' at that commit reports no blockers. If it still fails, report the request_id through feedback."
17604
17872
  },
17605
- service_unavailable: { status: 503, retryable: true, action: "Retry later." }
17873
+ source_commit_not_found: {
17874
+ status: 409,
17875
+ retryable: false,
17876
+ action: "The requested commit is not in the linked repository. Push that exact commit to the repository named by project status, or select a commit already there, then run 'ohmyhost plan' with its full SHA. Repeating the unchanged unavailable commit cannot work.",
17877
+ restAction: "The requested commit is not in the linked repository. Push that exact commit to the repository named by project status, or select a commit already there, then run 'ohmyhost plan' with its full SHA. Repeating the unchanged unavailable commit cannot work.",
17878
+ title: "Source commit not found"
17879
+ },
17880
+ domain_hostname_taken: {
17881
+ status: 409,
17882
+ retryable: false,
17883
+ action: "A live project connection already holds this hostname. Remove it from the project you manage with 'ohmyhost domain paid delete --project ULID --hostname HOST --yes --json', then apply it to the new project. Repeating while that connection remains cannot work; report the request_id if you cannot manage its owning project.",
17884
+ restAction: "A live project connection already holds this hostname. Remove it from the project you manage with 'ohmyhost domain paid delete --project ULID --hostname HOST --yes --json', then apply it to the new project. Repeating while that connection remains cannot work; report the request_id if you cannot manage its owning project.",
17885
+ title: "Domain hostname is already connected"
17886
+ },
17887
+ data_change_blocked: {
17888
+ status: 409,
17889
+ retryable: true,
17890
+ action: "An operation of this project is still running. Wait until 'ohmyhost operation get OPERATION_ULID --json' shows it finished, plan the data change again and confirm it with the new ETag and token.",
17891
+ restAction: "An operation of this project is still running. Wait until 'ohmyhost operation get OPERATION_ULID --json' shows it finished, plan the data change again and confirm it with the new ETag and token.",
17892
+ title: "Data change blocked"
17893
+ },
17894
+ data_change_in_progress: {
17895
+ status: 409,
17896
+ retryable: true,
17897
+ action: "A data change of this project is running. Wait until its operation has finished ('ohmyhost project status --project ULID --json'), then repeat the request unchanged.",
17898
+ restAction: "A data change of this project is running. Wait until its operation has finished ('ohmyhost project status --project ULID --json'), then repeat the request unchanged.",
17899
+ title: "Data change in progress"
17900
+ },
17901
+ data_change_not_applicable: {
17902
+ status: 409,
17903
+ retryable: false,
17904
+ action: "Read 'ohmyhost project status --project ULID --json'. Isolation applies to shared projects; returning to shared needs a deployed Prod database, and reset_dev applies to isolated projects. If detail names an old database or storage binding, deploy the affected environment again before planning the change. Repeating unchanged does not help.",
17905
+ restAction: "Read 'ohmyhost project status --project ULID --json'. Isolation applies to shared projects; returning to shared needs a deployed Prod database, and reset_dev applies to isolated projects. If detail names an old database or storage binding, deploy the affected environment again before planning the change. Repeating unchanged does not help.",
17906
+ title: "Data change is not applicable"
17907
+ },
17908
+ rollback_target_data_changed: {
17909
+ status: 409,
17910
+ retryable: false,
17911
+ action: "This deployment used a data area its environment no longer has. Deploy that commit again with a fresh plan instead of rolling back to its old bindings; repeating this rollback cannot work.",
17912
+ restAction: "This deployment used a data area its environment no longer has. Deploy that commit again with a fresh plan instead of rolling back to its old bindings; repeating this rollback cannot work.",
17913
+ title: "Rollback data area changed"
17914
+ },
17915
+ project_domain_delete_required: {
17916
+ status: 409,
17917
+ retryable: false,
17918
+ title: "Remove the project domain first",
17919
+ action: "This project still has a live or pending custom-domain connection. Read 'ohmyhost domain paid status --project ULID --json', remove that connection with 'ohmyhost domain paid delete --project ULID --hostname HOST --yes --json', then create a fresh project deletion plan. Repeating while it remains connected cannot work."
17920
+ }
17606
17921
  };
17922
+
17923
+ // apps/product-cli/dist/runtime-contract.js
17607
17924
  var ResponseContractError = class extends Error {
17608
17925
  constructor() {
17609
17926
  super("The server response does not match the public contract.");
17610
17927
  this.name = "ResponseContractError";
17611
17928
  }
17612
17929
  };
17930
+ function parseProjectDataChangePlan2(value) {
17931
+ try {
17932
+ return parseProjectDataChangePlan(value);
17933
+ } catch {
17934
+ throw new ResponseContractError();
17935
+ }
17936
+ }
17613
17937
  var parsePaidDomainPlan = (value) => {
17614
17938
  const input = exactRecord2(value, [
17615
17939
  "cname_target",
@@ -18259,6 +18583,7 @@ var parseSafeProblem = (value) => {
18259
18583
  const docsUrl = String(input["type"]);
18260
18584
  return {
18261
18585
  requestId: input["request_id"],
18586
+ ...input["detail"] === void 0 ? {} : { message: String(input["detail"]) },
18262
18587
  ...docsUrl.startsWith("https://docs.ohmyho.st/errors/") ? { docsUrl } : {},
18263
18588
  code,
18264
18589
  retryable: policy.retryable,
@@ -18439,8 +18764,10 @@ var selectWorkspace = async (session, organization, signal, identity) => {
18439
18764
  throw new InteractiveSessionRequiredError();
18440
18765
  const refreshed = await session.refresher.refresh(stored.refreshToken, signal, organization.workos_id);
18441
18766
  assertVerifiedLogin(await identity(refreshed.accessToken), session.profile.userId, organization.id);
18767
+ const existing = (await session.profiles.list()).find((saved) => saved.userId === session.profile.userId && saved.organizationId === organization.id);
18442
18768
  const bound = {
18443
18769
  ...session.profile,
18770
+ name: existing?.name ?? session.profile.name,
18444
18771
  organizationId: organization.id,
18445
18772
  organizationName: organization.name
18446
18773
  };
@@ -18537,7 +18864,7 @@ function assertProjectDatabaseAccessPage(value) {
18537
18864
  }
18538
18865
  function assertAccessFields(value) {
18539
18866
  const revoked = value["revoked_at"], reason = value["revocation_reason"];
18540
- if (typeof value["access_id"] !== "string" || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(value["access_id"]) || !["dev", "prod"].includes(String(value["environment"])) || !["read", "write"].includes(String(value["mode"])) || typeof value["role_name"] !== "string" || !ACCESS_ROLE_PATTERN.test(value["role_name"]) || typeof value["host"] !== "string" || !/^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$/u.test(value["host"]) || value["database"] !== "neondb" || value["label"] !== null && accessLabel(value["label"]) !== value["label"] || !["active", "expired", "revoked"].includes(String(value["state"])) || !timestamp(value["issued_at"]) || !timestamp(value["expires_at"]) || Date.parse(String(value["expires_at"])) <= Date.parse(String(value["issued_at"])) || revoked !== null && !timestamp(revoked) || revoked === null !== (reason === null) || reason !== null && !["principal", "expired", "budget", "creation_failed"].includes(String(reason)) || value["state"] === "revoked" !== (revoked !== null))
18867
+ if (typeof value["access_id"] !== "string" || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(value["access_id"]) || !["dev", "prod"].includes(String(value["environment"])) || !["read", "write"].includes(String(value["mode"])) || typeof value["role_name"] !== "string" || !ACCESS_ROLE_PATTERN.test(value["role_name"]) || typeof value["host"] !== "string" || !/^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$/u.test(value["host"]) || value["database"] !== "neondb" || value["label"] !== null && accessLabel(value["label"]) !== value["label"] || !["active", "expired", "revoked"].includes(String(value["state"])) || !timestamp(value["issued_at"]) || !timestamp(value["expires_at"]) || Date.parse(String(value["expires_at"])) <= Date.parse(String(value["issued_at"])) || revoked !== null && !timestamp(revoked) || revoked === null !== (reason === null) || reason !== null && !["principal", "expired", "budget", "creation_failed", "data_change"].includes(String(reason)) || value["state"] === "revoked" !== (revoked !== null))
18541
18868
  throw new TypeError("Invalid database access");
18542
18869
  }
18543
18870
  function accessLabel(value) {
@@ -18609,7 +18936,7 @@ function exact2(v, keys) {
18609
18936
  // apps/product-cli/package.json
18610
18937
  var package_default = {
18611
18938
  name: "@ohmyhost/product-cli",
18612
- version: "0.1.23",
18939
+ version: "0.1.25",
18613
18940
  private: true,
18614
18941
  ohmyhost: {
18615
18942
  deployment: "production",
@@ -18672,43 +18999,43 @@ function assertProjectContext(v) {
18672
18999
  // packages/contracts/dist/project-exports.js
18673
19000
  function assertProjectExport(value) {
18674
19001
  if (!record4(value) || Object.keys(value).sort().join(",") !== "archive,error,id,next_poll_after_seconds,next_request_at,project_id,requested_at,state" || typeof value["id"] !== "string" || !id.test(value["id"]) || typeof value["project_id"] !== "string" || !id.test(value["project_id"]) || !["queued", "running", "succeeded", "failed", "cancelled"].includes(String(value["state"])) || !timestamp3(value["requested_at"]) || !timestamp3(value["next_request_at"]))
18675
- invalid2();
19002
+ invalid3();
18676
19003
  const active = value["state"] === "queued" || value["state"] === "running";
18677
19004
  if (value["next_poll_after_seconds"] !== (active ? 5 : null))
18678
- invalid2();
19005
+ invalid3();
18679
19006
  if (value["state"] === "failed") {
18680
19007
  const error = value["error"];
18681
19008
  if (!record4(error) || Object.keys(error).sort().join(",") !== "code,message,retryable,suggested_action" || typeof error["code"] !== "string")
18682
- invalid2();
19009
+ invalid3();
18683
19010
  const expected = publicOperationFailure(error["code"]);
18684
19011
  for (const key of Object.keys(expected))
18685
19012
  if (error[key] !== expected[key])
18686
- invalid2();
19013
+ invalid3();
18687
19014
  } else if (value["error"] !== null)
18688
- invalid2();
19015
+ invalid3();
18689
19016
  const archive = value["archive"];
18690
19017
  if (archive === null) {
18691
19018
  if (value["state"] === "succeeded")
18692
- invalid2();
19019
+ invalid3();
18693
19020
  return;
18694
19021
  }
18695
19022
  if (value["state"] !== "succeeded" || !record4(archive) || Object.keys(archive).sort().join(",") !== "bytes,captured_at,download_expires_at,download_url,expires_at,sha256,sql_files" || !Number.isSafeInteger(archive["bytes"]) || Number(archive["bytes"]) < 1 || Number(archive["bytes"]) > 536870912 || typeof archive["sha256"] !== "string" || !/^sha256:[a-f0-9]{64}$/u.test(archive["sha256"]) || !['["dev.sql"]', '["prod.sql"]', '["dev.sql","prod.sql"]', '["shared.sql"]'].includes(JSON.stringify(archive["sql_files"])) || !timestamp3(archive["captured_at"]) || !timestamp3(archive["expires_at"]))
18696
- invalid2();
19023
+ invalid3();
18697
19024
  if (archive["download_url"] === null) {
18698
19025
  if (archive["download_expires_at"] !== null)
18699
- invalid2();
19026
+ invalid3();
18700
19027
  return;
18701
19028
  }
18702
19029
  if (typeof archive["download_url"] !== "string" || !timestamp3(archive["download_expires_at"]) || Date.parse(archive["download_expires_at"]) > Date.parse(archive["expires_at"]))
18703
- invalid2();
19030
+ invalid3();
18704
19031
  let url;
18705
19032
  try {
18706
19033
  url = new URL(archive["download_url"]);
18707
19034
  } catch {
18708
- invalid2();
19035
+ invalid3();
18709
19036
  }
18710
19037
  if (url.protocol !== "https:" || url.username || url.password || url.hash)
18711
- invalid2();
19038
+ invalid3();
18712
19039
  }
18713
19040
  var id = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
18714
19041
  function record4(value) {
@@ -18717,7 +19044,7 @@ function record4(value) {
18717
19044
  function timestamp3(value) {
18718
19045
  return typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
18719
19046
  }
18720
- function invalid2() {
19047
+ function invalid3() {
18721
19048
  throw new TypeError("Project export response is invalid");
18722
19049
  }
18723
19050
 
@@ -18908,7 +19235,7 @@ var HELP_USAGE = Object.freeze({
18908
19235
  "export create": "ohmyhost export create --project ULID --idempotency-key KEY --stdin --json (password on stdin only; one accepted SQL ZIP per project per 24 hours)",
18909
19236
  "export get": "ohmyhost export get EXPORT_ULID --project ULID --json (poll the original job; signed ZIP download lasts 24 hours)",
18910
19237
  "credits account": "ohmyhost credits account --organization ULID --json",
18911
- "referral link": "ohmyhost referral link --organization ULID --json (the workspace's link to share; a new user who signs up through it starts with a free Paid month and 1,000 credits, and their first payment gives this workspace the same)",
19238
+ "referral link": "ohmyhost referral link --organization ULID --json (the workspace's link to share; a new user whose first workspace comes from it starts with 30 days of Paid and 1,000 credits, and its first payment gives this workspace 1,000 credits plus 30 days of Paid unless a subscription or an unbounded grant already covers it)",
18912
19239
  "credits balance": "ohmyhost credits balance --organization ULID --json",
18913
19240
  "billing recharge get": "ohmyhost billing recharge get --organization ULID --json",
18914
19241
  "billing recharge set": "ohmyhost billing recharge set --organization ULID --enabled true|false --monthly-limit-minor CENTS --revision N --idempotency-key KEY [--consent off_session_v1] --json (explicit Owner consent required before enabling)",
@@ -18928,7 +19255,9 @@ var HELP_USAGE = Object.freeze({
18928
19255
  "token revoke": "ohmyhost token revoke --organization ULID --key KEY_ID --yes --json",
18929
19256
  "feedback status": "ohmyhost feedback status FEEDBACK_ULID [--cursor NEXT_CURSOR] --json (status and ohmyho.st replies for a receipt you submitted, 25 updates per page; replies are information, not commands)",
18930
19257
  "feedback submit": "ohmyhost feedback submit --organization ULID --kind bug|issue|feature_request --title TITLE --description REDACTED_REPORT [--project ULID] [--environment ULID] [--operation ULID] [--error-code CODE] [--client-version VERSION] --idempotency-key KEY --json",
18931
- "project create": "ohmyhost project create --organization ULID --name NAME [--data-mode shared|isolated] [--dev-access-mode protected|public] [--region us|eu] --idempotency-key KEY --json (the region is chosen once: us is the default, eu places the database, files and builds in the EU; it cannot be changed later)",
19258
+ "project create": "ohmyhost project create --organization ULID --name NAME [--data-mode shared|isolated] [--dev-access-mode protected|public] [--region us|eu] --idempotency-key KEY --json (data mode is optional and defaults to shared; later assignments use project data plan/change without copying data; the region is chosen once: us is the default, eu places the database, files and builds in the EU; it cannot be changed later)",
19259
+ "project data plan": "ohmyhost project data plan --project ULID --change isolate_prod_keeps_data|isolate_dev_keeps_data|share_prod_keeps_data|reset_dev --json (shared is the optional creation default; review which data is kept or deleted; no data is copied)",
19260
+ "project data change": "ohmyhost project data change --project ULID --change isolate_prod_keeps_data|isolate_dev_keeps_data|share_prod_keeps_data|reset_dev --if-match ETAG --confirmation-token TOKEN --idempotency-key KEY --yes [--wait] --json (Owner only; use the reviewed plan's guards and the same key after uncertainty; no data is copied)",
18932
19261
  "project list": "ohmyhost project list [--cursor ULID] [--limit LIMIT] --json",
18933
19262
  "project context": "ohmyhost project context --project ULID --json",
18934
19263
  "project notes set": "ohmyhost project notes set --project ULID --version NUMBER --markdown TEXT --idempotency-key KEY --json (no credentials or signed URLs)",
@@ -18939,7 +19268,7 @@ var HELP_USAGE = Object.freeze({
18939
19268
  "project dev-share revoke": "ohmyhost project dev-share revoke --project ULID --idempotency-key KEY --yes --json",
18940
19269
  "project dev-access mode": "ohmyhost project dev-access mode --project ULID --mode protected|public --idempotency-key KEY --yes --json",
18941
19270
  "project flag status": "ohmyhost project flag status --project ULID --json",
18942
- "project flag set": "ohmyhost project flag set --project ULID --enabled true|false --idempotency-key KEY --json (shows the small Powered by ohmyho.st flag on the production site; while it shows, a Free workspace may connect its own domain without the domain fee and each Paid period adds 250 credits)",
19271
+ "project flag set": "ohmyhost project flag set --project ULID --enabled true|false --idempotency-key KEY --json (shows the small Powered by ohmyho.st flag on the production site; while it shows, this project's custom domain uses no domain credits and also works on Free, and each Paid period bought through Stripe adds 250 credits to the workspace, however many projects show the flag)",
18943
19272
  "project handle check": "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)",
18944
19273
  "project handle set": "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)",
18945
19274
  "database compute set": "ohmyhost database compute set --project ULID --environment dev|prod --profile standard|performance --idempotency-key KEY --yes [--wait] --json\n 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.",
@@ -18994,6 +19323,30 @@ var cliMetadataResult = (kind, topic) => kind === "help" ? success("help", {
18994
19323
  environment: "OHMYHOST_ENVIRONMENT=development|production (default: production); OHMYHOST_PROFILE=NAME binds this process to one saved login",
18995
19324
  commands: HELP_COMMANDS.filter((entry) => !topic || entry.command === topic)
18996
19325
  }) : success("version", { status: "succeeded", cli_version: CLI_VERSION });
19326
+ var invalidCommandAction = (argv, commandPrefix) => {
19327
+ const label = recognizedCommandLabel(argv);
19328
+ if (label === "secret set" || label === "secret delete") {
19329
+ const action = label === "secret set" ? "set" : "delete";
19330
+ const at = argv.findIndex((value, index) => value === "secret" && argv[index + 1] === action);
19331
+ const name = at < 0 ? void 0 : argv[at + 2];
19332
+ if (name !== void 0 && !isUserSecretName(name, action))
19333
+ return action === "set" ? "Choose a customer secret name of at most 128 uppercase letters, digits and underscores, starting with a letter. OHMYHOST_* and DATABASE_URL, HYPERDRIVE, BETTER_AUTH_SECRET, BETTER_AUTH_URL, ASSETS, FILES, IMAGES and STORAGE are reserved for the platform. Rename the application's own variable and its source references; platform bindings are installed automatically. Nothing was sent; changing only the idempotency key will not help." : "Choose an existing customer secret name of at most 128 uppercase letters, digits and underscores, starting with a letter. The installed platform keys OHMYHOST_STORAGE_KEY, OHMYHOST_MAIL_KEY and BETTER_AUTH_SECRET cannot be deleted. Older reserved-name collisions can be removed with secret delete; deployed platform keys stay managed by ohmyho.st. Nothing was sent; changing only the idempotency key will not help.";
19334
+ }
19335
+ const usage = Object.hasOwn(HELP_USAGE, label) ? HELP_USAGE[label].split("\n")[0] : void 0;
19336
+ if (usage === void 0)
19337
+ return "Unknown command; nothing was sent. 'ohmyhost --help --json' lists every command with its flags. If a documented command is missing, this CLI is older than https://ohmyho.st/client-release.json: install the published version.";
19338
+ const linkable = /(?:^|\s)--project ULID/u.test(usage) && !argv.includes("--project");
19339
+ return `The arguments of '${label}' are invalid or incomplete; nothing was sent. Usage: ${usage}.${linkable ? ` --project may be omitted only inside a directory linked with '${commandPrefix} link' for this OHMYHOST_ENVIRONMENT; otherwise pass --project ULID.` : ""}`;
19340
+ };
19341
+ var INTERNAL_ERROR_ACTION = "The ohmyhost client stopped on an unexpected error; the stderr line starting with [ohmyhost: identifies it. Compare 'ohmyhost --version' with https://ohmyho.st/client-release.json and install the published client if it is older, then run the same command again with the same --idempotency-key. If the current client fails the same way, report it with feedback submit (kind bug, error code internal_error) and quote that stderr line.";
19342
+ var CREDENTIAL_STORE_UNAVAILABLE_ACTION = "This computer's credential store (for example macOS Keychain or Windows Credential Manager) or the saved-login list in ~/.ohmyhost/profiles could not be read or written; nothing was changed. If the agent runs in a sandbox, run the command with access to them before asking the customer to sign in again. If the store is locked, ask the customer to unlock it, then run the command again. On a server, container or CI runner without a credential store, do not run login: load OHMYHOST_TOKEN from a private env file with a token from the portal's API Tokens page.";
19343
+ var RESPONSE_CONTRACT_INVALID_ACTION = "The ohmyho.st API answered in a form this client version does not accept, usually because the client is older than the service. Compare 'ohmyhost --version' with https://ohmyho.st/client-release.json; if it is older, install the published CLI and MCP and reload MCP. Then run the same command again with the same --idempotency-key: the change may already have been applied. If the current version answers this too, report it with feedback submit (kind bug, error code response_contract_invalid).";
19344
+ var REQUEST_TIMEOUT_ACTION = "The ohmyho.st API did not answer within 30 seconds. Run the same command again with the same --idempotency-key; the change may already be under way, and a new key could apply it twice.";
19345
+ var LOGOUT_UNCONFIRMED_ACTION = "The saved login was removed from this computer, but ohmyho.st did not confirm that its session ended. Running logout again cannot retry this, because the login is gone; do not sign in again just to revoke it. Tell the customer the session end was not confirmed.";
19346
+ var STATEMENT_FILE_INVALID_ACTION = "Put one INSERT, UPDATE or DELETE statement that starts with that keyword (no leading comment or WITH), UTF-8, at most 64 KiB, in the file named by --statement-file, then rerun with the same --idempotency-key.";
19347
+ var signInCommand = (commandPrefix, profile) => profile === void 0 ? `${commandPrefix} login --json` : `${commandPrefix} login${profile.organizationId === null ? "" : ` --organization ${profile.organizationId}`} --user ${profile.userId} --profile-name ${profile.name} --json`;
19348
+ var endedLoginAction = (commandPrefix, profile) => `The sign-in of ${profile === void 0 ? "this computer's saved login" : `saved login ${profile.name} (user ${profile.userId}${profile.organizationId === null ? "" : `, organization ${profile.organizationId}`})`} ended and it was removed. Run '${signInCommand(commandPrefix, profile)}' to sign it in again.`;
19349
+ var refreshFailedAction = (commandPrefix, profile) => `Renewing the saved login${profile === void 0 ? "" : ` ${profile.name}`} at the sign-in service failed. Run the same command again in a minute, with the same --idempotency-key if it has one. If it fails again, run '${signInCommand(commandPrefix, profile)}' and send the customer the link and code.`;
18997
19350
  var ProductCliFailure = class extends Error {
18998
19351
  exitCode;
18999
19352
  code;
@@ -19024,14 +19377,14 @@ var ProductCli = class {
19024
19377
  command = parseProductCliCommand(argv);
19025
19378
  } catch (error) {
19026
19379
  if (error instanceof InvalidCommandError) {
19027
- return failure(recognizedCommandLabel(argv), 2, "invalid_command", false, "Check 'ohmyhost --help'.");
19380
+ return failure(recognizedCommandLabel(argv), 2, "invalid_command", false, invalidCommandAction(argv, this.dependencies.commandPrefix));
19028
19381
  }
19029
19382
  this.recordDiagnostic({
19030
19383
  category: "internal",
19031
19384
  subsystem: "parser",
19032
19385
  errorKind: "unexpected"
19033
19386
  });
19034
- return failure("unknown", 1, "internal_error", false, "Retry the command.");
19387
+ return failure(recognizedCommandLabel(argv), 1, "internal_error", false, INTERNAL_ERROR_ACTION);
19035
19388
  }
19036
19389
  return this.runCommand(command, signal);
19037
19390
  }
@@ -19251,6 +19604,10 @@ var ProductCli = class {
19251
19604
  }
19252
19605
  if (command.kind === "project-create")
19253
19606
  return await this.createProject(command, signal);
19607
+ if (command.kind === "project-data-plan")
19608
+ return await this.planProjectData(command, signal);
19609
+ if (command.kind === "project-data-change")
19610
+ return await this.changeProjectData(command, signal);
19254
19611
  if (command.kind === "project-list")
19255
19612
  return await this.listProjects(command, signal);
19256
19613
  if (command.kind === "project-handle-check")
@@ -19299,25 +19656,15 @@ var ProductCli = class {
19299
19656
  return success(command.kind === "project-dev-share-link" ? "project dev-share link" : command.kind === "project-dev-share-rotate" ? "project dev-share rotate" : command.kind === "project-dev-share-revoke" ? "project dev-share revoke" : "project dev-access mode", { status: "succeeded", state });
19300
19657
  }
19301
19658
  if (command.kind === "database-write") {
19302
- const file = await open3(command.statementFile, "r"), buffer = Buffer.alloc(65537);
19303
- let statement;
19659
+ const statement = await readStatementFile(command.statementFile);
19304
19660
  try {
19305
- const stat3 = await file.stat();
19306
- if (!stat3.isFile() || stat3.size > 65536)
19307
- throw new InvalidCommandError();
19308
- let length = 0;
19309
- while (length < buffer.length) {
19310
- const read = await file.read(buffer, length, buffer.length - length, null);
19311
- if (read.bytesRead === 0)
19312
- break;
19313
- length += read.bytesRead;
19314
- }
19315
- if (length > 65536)
19316
- throw new InvalidCommandError();
19317
- statement = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, length));
19318
- } finally {
19319
- buffer.fill(0);
19320
- await file.close();
19661
+ parseProjectDatabaseWrite({
19662
+ environment: command.environment,
19663
+ statement,
19664
+ parameters: []
19665
+ });
19666
+ } catch {
19667
+ throw new ProductCliFailure(2, "statement_file_invalid", false, STATEMENT_FILE_INVALID_ACTION);
19321
19668
  }
19322
19669
  const body = parseProjectDatabaseWrite({
19323
19670
  environment: command.environment,
@@ -19544,8 +19891,13 @@ var ProductCli = class {
19544
19891
  expiresAt
19545
19892
  };
19546
19893
  const account = await this.dependencies.accountProfile(async () => signedIn.accessToken);
19894
+ const loginFlags = {
19895
+ organizationId,
19896
+ userId: expectedUserId,
19897
+ profileName: this.dependencies.profileName
19898
+ };
19547
19899
  if (!isProfileUserId(account.userId))
19548
- throw this.unverifiedLogin();
19900
+ throw this.unverifiedLogin(loginFlags);
19549
19901
  if (expectedUserId !== void 0 && account.userId !== expectedUserId)
19550
19902
  throw new ProductCliFailure(4, "login_account_mismatch", false, `The browser signed in as user ${account.userId}, not ${expectedUserId}. Nothing was saved and every saved login is unchanged. Sign out of ohmyho.st in that browser or use the browser profile of ${expectedUserId}, then run '${this.dependencies.commandPrefix} login${organizationId === void 0 ? "" : ` --organization ${organizationId}`} --user ${expectedUserId} --json' again.`);
19551
19903
  const selection = chooseWorkspace(account.organizations, organizationId);
@@ -19556,12 +19908,12 @@ var ProductCli = class {
19556
19908
  const scopedId = selection.outcome === "selected" ? selection.organization.id : parseCurrentIdentity(identity).organization_ids[0] ?? null;
19557
19909
  const organization = scopedId === null ? null : account.organizations.find((candidate) => candidate.id === scopedId);
19558
19910
  if (organization === void 0)
19559
- throw this.unverifiedLogin();
19911
+ throw this.unverifiedLogin(loginFlags);
19560
19912
  try {
19561
19913
  assertVerifiedLogin(identity, account.userId, scopedId);
19562
19914
  } catch (error) {
19563
19915
  if (error instanceof AccountVerificationError)
19564
- throw this.unverifiedLogin();
19916
+ throw this.unverifiedLogin(loginFlags);
19565
19917
  throw error;
19566
19918
  }
19567
19919
  const login = {
@@ -19605,8 +19957,8 @@ var ProductCli = class {
19605
19957
  }
19606
19958
  }
19607
19959
  /** The provider answered with another account than the one this login requested. */
19608
- unverifiedLogin() {
19609
- return new ProductCliFailure(4, "login_verification_failed", true, `The signed-in account could not be verified; nothing was saved. Run '${this.dependencies.commandPrefix} login --json' again.`);
19960
+ unverifiedLogin(flags) {
19961
+ return new ProductCliFailure(4, "login_verification_failed", true, `The signed-in account could not be verified; nothing was saved. Run '${this.dependencies.commandPrefix} login${flags.organizationId === void 0 ? "" : ` --organization ${flags.organizationId}`}${flags.userId === void 0 ? "" : ` --user ${flags.userId}`}${flags.profileName === void 0 ? "" : ` --profile-name ${flags.profileName}`} --json' again with the same flags and send the customer the new link and code.`);
19610
19962
  }
19611
19963
  /**
19612
19964
  * Bind the login of this run, which has no organization yet, to one of its user's organizations.
@@ -19627,7 +19979,11 @@ var ProductCli = class {
19627
19979
  }, organization, bounded2, (accessToken) => this.dependencies.productApi(async () => accessToken).getCurrentIdentity()), signal, this.dependencies.lifecycleTimeoutMs);
19628
19980
  } catch (error) {
19629
19981
  if (error instanceof AccountVerificationError)
19630
- throw this.unverifiedLogin();
19982
+ throw this.unverifiedLogin({
19983
+ organizationId: organization.id,
19984
+ userId: profile.userId,
19985
+ profileName: profile.name
19986
+ });
19631
19987
  if (!(error instanceof ProfileOrganizationFixedError))
19632
19988
  throw error;
19633
19989
  throw new ProductCliFailure(2, "profile_organization_fixed", false, `Profile ${profile.name} stays signed in to organization ${String(profile.organizationId)}. Run '${this.dependencies.commandPrefix} login --organization ${organization.id} --user ${profile.userId} --json' to add a separate login for ${organization.id} as the same user; a saved login never switches organizations.`);
@@ -19663,10 +20019,10 @@ var ProductCli = class {
19663
20019
  throw new ProductCliFailure(9, "blocked_prerequisite", false, "Remote token revocation is not available in this release.");
19664
20020
  }
19665
20021
  if (isTimeout(revocationError)) {
19666
- throw new ProductCliFailure(8, "request_timeout", true, "Remote revocation timed out; local credentials were removed.");
20022
+ throw new ProductCliFailure(8, "request_timeout", false, LOGOUT_UNCONFIRMED_ACTION);
19667
20023
  }
19668
20024
  if (revocationError instanceof LifecycleTransportError) {
19669
- throw new ProductCliFailure(6, "token_revocation_failed", true, "Remote revocation failed; the local credential was removed.");
20025
+ throw new ProductCliFailure(6, "token_revocation_failed", false, LOGOUT_UNCONFIRMED_ACTION);
19670
20026
  }
19671
20027
  throw revocationError;
19672
20028
  }
@@ -19707,6 +20063,29 @@ var ProductCli = class {
19707
20063
  }));
19708
20064
  return success("operation reconcile", { status: "accepted", reconciliation });
19709
20065
  }
20066
+ async planProjectData(command, signal) {
20067
+ const api = await this.authenticatedApi(command.credentialStore, signal);
20068
+ return success("project data plan", {
20069
+ status: "planned",
20070
+ plan: parseProjectDataChangePlan2(await api.planProjectDataChange({ projectId: command.projectId, change: command.change }))
20071
+ });
20072
+ }
20073
+ async changeProjectData(command, signal) {
20074
+ const api = await this.authenticatedApi(command.credentialStore, signal);
20075
+ let operation = parseOperation(await api.createProjectDataChange({
20076
+ projectId: command.projectId,
20077
+ change: command.change,
20078
+ ifMatch: command.ifMatch,
20079
+ confirmationToken: command.confirmationToken,
20080
+ idempotencyKey: command.idempotencyKey
20081
+ }));
20082
+ if (command.wait)
20083
+ operation = await this.waitForOperation(api, operation, signal);
20084
+ return success("project data change", {
20085
+ status: command.wait ? "completed" : "accepted",
20086
+ operation
20087
+ });
20088
+ }
19710
20089
  async createProject(command, signal) {
19711
20090
  const api = await this.authenticatedApi(command.credentialStore, signal);
19712
20091
  const operation = parseOperation(await api.createProject({
@@ -19960,11 +20339,20 @@ var ProductCli = class {
19960
20339
  commitSha: command.commitSha,
19961
20340
  environment: command.environment
19962
20341
  }));
19963
- let operation = parseOperation(await api.createDeployment({
19964
- projectId: command.projectId,
19965
- planId: plan === null ? String(command.planId) : String(plan["id"]),
19966
- idempotencyKey: command.idempotencyKey
19967
- }));
20342
+ let accepted;
20343
+ try {
20344
+ accepted = await api.createDeployment({
20345
+ projectId: command.projectId,
20346
+ planId: plan === null ? String(command.planId) : String(plan["id"]),
20347
+ idempotencyKey: command.idempotencyKey
20348
+ });
20349
+ } catch (error) {
20350
+ const reused = plan === null ? void 0 : reusedKeyProblem(error);
20351
+ if (reused === void 0)
20352
+ throw error;
20353
+ return failure("deploy", 7, reused.code, false, `This key already started a deployment from an earlier run of this command; a new key would build the same commit again. Run 'ohmyhost project status --project ${command.projectId} --json' and follow latest_operation with 'ohmyhost operation get OPERATION_ULID --json'.`, void 0, void 0, reused.requestId, reused.docsUrl, void 0, reused.message);
20354
+ }
20355
+ let operation = parseOperation(accepted);
19968
20356
  if (command.wait)
19969
20357
  operation = await this.waitForOperation(api, operation, signal);
19970
20358
  const notes = operation.state === "succeeded" ? [
@@ -19979,27 +20367,40 @@ var ProductCli = class {
19979
20367
  }
19980
20368
  async logs(command, signal) {
19981
20369
  const api = await this.authenticatedApi(command.credentialStore, signal);
20370
+ let operation = parseOperation(await api.getOperation(command.operationId));
19982
20371
  let eventCount = 0;
19983
- for await (const event of await api.streamOperationEvents(command.operationId, signal)) {
19984
- const parsed = parseOperationEvent(event);
19985
- this.dependencies.operationEventSink?.write(`${JSON.stringify(parsed)}
20372
+ let lastEventId;
20373
+ for (let connection = 0; connection < OPERATION_STREAM_CONNECTIONS; connection += 1) {
20374
+ if (connection > 0)
20375
+ await this.dependencies.waitUntil(new Date(this.dependencies.now().getTime() + 1e3), signal);
20376
+ const events = lastEventId === void 0 ? await api.streamOperationEvents(command.operationId, signal) : await api.streamOperationEvents(command.operationId, signal, lastEventId);
20377
+ for await (const event of events) {
20378
+ const parsed = parseOperationEvent(event);
20379
+ this.dependencies.operationEventSink?.write(`${JSON.stringify(parsed)}
19986
20380
  `);
19987
- eventCount += 1;
19988
- if (parsed["type"] === "OperationSucceeded" || parsed["type"] === "OperationFailed" || parsed["type"] === "OperationCancelled") {
19989
- break;
20381
+ eventCount += 1;
20382
+ lastEventId = String(parsed["event_id"]);
20383
+ if (parsed["type"] === "OperationSucceeded" || parsed["type"] === "OperationFailed" || parsed["type"] === "OperationCancelled")
20384
+ return success("logs", { status: "completed", event_count: eventCount });
19990
20385
  }
20386
+ throwIfAborted(signal);
20387
+ operation = parseOperation(await api.getOperation(command.operationId));
20388
+ if (operation.state !== "queued" && operation.state !== "running")
20389
+ return success("logs", { status: "completed", event_count: eventCount, operation });
19991
20390
  }
19992
- return success("logs", { status: "completed", event_count: eventCount });
20391
+ throw new ProductCliFailure(8, "operation_stream_ended", true, `The event stream ended before operation ${command.operationId} finished, and it is still running; nothing was cancelled. Poll 'ohmyhost operation get ${command.operationId} --json' about every 60 seconds until it is succeeded, failed or cancelled.`, operation);
19993
20392
  }
19994
20393
  async deploymentLogs(command, signal) {
19995
20394
  const api = await this.authenticatedApi(command.credentialStore, signal);
19996
- let count = 0;
19997
- for await (const event of await api.streamDeploymentLogs({ projectId: command.projectId, deploymentId: command.deploymentId }, signal)) {
19998
- this.dependencies.operationEventSink?.write(`${JSON.stringify(event)}
20395
+ const page = await api.getDeploymentLogs({
20396
+ projectId: command.projectId,
20397
+ deploymentId: command.deploymentId,
20398
+ limit: 100
20399
+ });
20400
+ for (const item of page.items)
20401
+ this.dependencies.operationEventSink?.write(`${JSON.stringify(item)}
19999
20402
  `);
20000
- count += 1;
20001
- }
20002
- return success("deployment logs", { status: "completed", event_count: count });
20403
+ return success("deployment logs", { status: "completed", event_count: page.items.length });
20003
20404
  }
20004
20405
  async planRollback(command, signal) {
20005
20406
  const api = await this.authenticatedApi(command.credentialStore, signal);
@@ -20077,7 +20478,7 @@ var ProductCli = class {
20077
20478
  }));
20078
20479
  const completed = command.wait ? await this.waitForSecretDelivery(api, command.projectId, command.environmentId, secret, signal) : secret;
20079
20480
  return success("secret set", {
20080
- status: command.wait ? "ready" : "stored",
20481
+ status: command.wait && completed.delivery_state === "ready" ? "ready" : "stored",
20081
20482
  secret: completed
20082
20483
  });
20083
20484
  } finally {
@@ -20094,7 +20495,7 @@ var ProductCli = class {
20094
20495
  }));
20095
20496
  const delivery = command.wait ? await this.waitForSecretDelivery(api, command.projectId, command.environmentId, { desired_generation: deleted.desired_generation }, signal) : deleted;
20096
20497
  return success("secret delete", {
20097
- status: command.wait ? "ready" : deleted.status,
20498
+ status: command.wait && delivery.delivery_state === "ready" ? "ready" : deleted.status,
20098
20499
  secret: { name: deleted.name, ...secretDeliveryProjection(delivery) }
20099
20500
  });
20100
20501
  }
@@ -20103,9 +20504,9 @@ var ProductCli = class {
20103
20504
  for (let attempt = 0; attempt < 120; attempt += 1) {
20104
20505
  const page = parseEnvironmentSecretPage(await api.listEnvironmentSecrets({ projectId, environmentId }));
20105
20506
  if (page.last_error !== null || page.delivery_state === "failed") {
20106
- throw new ProductCliFailure(10, "secret_delivery_failed", true, "Inspect secret metadata and deployment logs, then retry reconciliation.");
20507
+ throw new ProductCliFailure(10, "secret_delivery_failed", true, `The secret is saved, but delivering it to the running app failed (last_error in 'ohmyhost secret list --project ${projectId} --environment ${environmentId} --json'). Do not set the value again: ohmyho.st retries delivery automatically, and the next deployment of this environment also delivers it. Read secret list again in a few minutes; if delivery_state stays failed, report last_error with feedback submit (kind bug, error code secret_delivery_failed).`);
20107
20508
  }
20108
- if (page.delivery_state === "ready" && page.desired_generation >= desiredGeneration && page.applied_generation >= desiredGeneration) {
20509
+ if (page.delivery_state === "not_deployed" || page.delivery_state === "ready" && page.desired_generation >= desiredGeneration && page.applied_generation >= desiredGeneration) {
20109
20510
  if (!("name" in stored))
20110
20511
  return page;
20111
20512
  return page.items.find((secret) => secret.name === stored.name) ?? {
@@ -20117,7 +20518,7 @@ var ProductCli = class {
20117
20518
  }
20118
20519
  await this.dependencies.waitUntil(new Date(this.dependencies.now().getTime() + 1e3), signal);
20119
20520
  }
20120
- throw new ProductCliFailure(8, "secret_delivery_wait_timeout", true, "Run 'ohmyhost secret list --json' to inspect delivery metadata.");
20521
+ throw new ProductCliFailure(8, "secret_delivery_wait_timeout", true, `The secret is saved, but it had not reached the running app after about 2 minutes. Read 'ohmyhost secret list --project ${projectId} --environment ${environmentId} --json': pending means delivery is still under way, and not_deployed means it arrives with the first deployment. Do not set the value again.`);
20121
20522
  }
20122
20523
  async waitForOperation(api, initial, signal) {
20123
20524
  let operation = initial;
@@ -20128,7 +20529,7 @@ var ProductCli = class {
20128
20529
  if (operation.reconciliation?.state === "required")
20129
20530
  throw new ProductCliFailure(10, "operation_reconciliation_required", false, `${operation.reconciliation.suggested_action} Inspect 'ohmyhost operation get ${operation.id} --json' and use 'ohmyhost operation reconcile' after confirmation.`, operation);
20130
20531
  if (operation.state === "queued" || operation.state === "running")
20131
- throw new ProductCliFailure(8, "operation_wait_timeout", true, `Run 'ohmyhost operation get ${operation.id} --json' to inspect the existing operation before resubmitting a mutation.`, operation);
20532
+ throw new ProductCliFailure(8, "operation_wait_timeout", true, `The operation is still running; this command stopped waiting after about 2 minutes and cancelled nothing. Poll 'ohmyhost operation get ${operation.id} --json' about every 60 seconds until it is succeeded, failed or cancelled. Do not submit the same change again; a new key starts a second operation.`, operation);
20132
20533
  if (operation.state === "failed" && operation.error !== void 0)
20133
20534
  throw new ProductCliFailure(10, operation.error.code, operation.error.retryable, operation.error.suggested_action, operation);
20134
20535
  if (operation.state === "cancelled")
@@ -20153,8 +20554,9 @@ var ProductCli = class {
20153
20554
  } catch (error) {
20154
20555
  if (!(error instanceof TokenRefreshRejectedError))
20155
20556
  throw error;
20557
+ const ended = this.#profile;
20156
20558
  await this.forgetLogin(store);
20157
- throw new ProductCliFailure(5, "authentication_required", false, this.loginAction());
20559
+ throw new ProductCliFailure(5, "authentication_required", false, endedLoginAction(this.dependencies.commandPrefix, ended));
20158
20560
  }
20159
20561
  if (refresh.refreshed)
20160
20562
  await store.replace(refresh.credential);
@@ -20187,10 +20589,10 @@ var ProductCli = class {
20187
20589
  throw new ProductCliFailure(9, "blocked_prerequisite", false, "Token refresh is not available in this release; run 'ohmyhost login'.");
20188
20590
  }
20189
20591
  if (isTimeout(error)) {
20190
- throw new ProductCliFailure(8, "request_timeout", true, "Retry the command.");
20592
+ throw new ProductCliFailure(8, "request_timeout", true, REQUEST_TIMEOUT_ACTION);
20191
20593
  }
20192
20594
  if (error instanceof LifecycleTransportError) {
20193
- throw new ProductCliFailure(5, "token_refresh_failed", true, "Run 'ohmyhost login' if retrying does not succeed.");
20595
+ throw new ProductCliFailure(5, "token_refresh_failed", true, refreshFailedAction(this.dependencies.commandPrefix, this.#profile));
20194
20596
  }
20195
20597
  throw error;
20196
20598
  }
@@ -20231,31 +20633,31 @@ var ACCOUNT_SENSITIVE_PROBLEMS = /* @__PURE__ */ new Set([
20231
20633
  var classifyCliFailure = (command, error, signal) => classifyFailure(command, error, signal);
20232
20634
  var classifyFailure = (command, error, signal, actingAs) => {
20233
20635
  if (signal.aborted || isCancellation(error)) {
20234
- return failure(command, 130, "command_cancelled", true, "Run the command again.");
20636
+ return failure(command, 130, "command_cancelled", true, "The command was stopped by an interrupt or termination signal, for example the shell's command timeout, before it finished. Run it again with the same --idempotency-key; a change may already be under way. Give login and commands with --wait or --follow several minutes, or run them in the background.");
20235
20637
  }
20236
20638
  if (isTimeout(error)) {
20237
- return failure(command, 8, "request_timeout", true, "Retry the command.");
20639
+ return failure(command, 8, "request_timeout", true, REQUEST_TIMEOUT_ACTION);
20238
20640
  }
20239
20641
  if (error instanceof ProductCliFailure) {
20240
20642
  return failure(command, error.exitCode, error.code, error.retryable, error.suggestedAction, error.operation);
20241
20643
  }
20242
20644
  if (error instanceof SecretInputError) {
20243
- return failure(command, 2, "secret_input_invalid", false, "Pipe a non-empty UTF-8 value of at most 5120 bytes through standard input.");
20645
+ return failure(command, 2, "secret_input_invalid", false, command === "export create" ? "Pipe the ZIP password through standard input: non-blank UTF-8 of at most 1024 bytes. A final line break becomes part of the password." : "Pipe a non-empty UTF-8 value of at most 5120 bytes through standard input.");
20244
20646
  }
20245
20647
  if (error instanceof WorkosContractError) {
20246
20648
  if (error.code === "access_denied") {
20247
- return failure(command, 4, "device_authorization_denied", false, "Run login again.");
20649
+ return failure(command, 4, "device_authorization_denied", false, "The customer declined the sign-in in the browser; nothing was saved. Ask the customer whether to sign in, then run the same login command again and send them the new link and code.");
20248
20650
  }
20249
20651
  if (error.code === "expired_token") {
20250
- return failure(command, 4, "device_authorization_expired", false, "Run login again.");
20652
+ return failure(command, 4, "device_authorization_expired", false, "Nobody finished the sign-in within 30 minutes; nothing was saved. Run the same login command again and send the customer the new link and code at once.");
20251
20653
  }
20252
- return failure(command, 4, "device_authorization_failed", true, "Run login again.");
20654
+ return failure(command, 4, "device_authorization_failed", true, "The sign-in service answered unexpectedly; nothing was saved. Run the same login command again; if it fails twice more, ask the customer to try again later.");
20253
20655
  }
20254
20656
  if (error instanceof CredentialStoreUnavailableError) {
20255
- return failure(command, 3, "credential_store_unavailable", true, "Unlock or repair the selected credential store and retry.");
20657
+ return failure(command, 3, "credential_store_unavailable", false, CREDENTIAL_STORE_UNAVAILABLE_ACTION);
20256
20658
  }
20257
20659
  if (error instanceof ResponseContractError) {
20258
- return failure(command, 7, "response_contract_invalid", false, "Retry only after the client or server contract is repaired.");
20660
+ return failure(command, 7, "response_contract_invalid", false, RESPONSE_CONTRACT_INVALID_ACTION);
20259
20661
  }
20260
20662
  if (error instanceof PublicResponseTooLargeError) {
20261
20663
  return failure(command, 7, "response_too_large", false, "Retry only after the remote response is repaired.");
@@ -20263,18 +20665,61 @@ var classifyFailure = (command, error, signal, actingAs) => {
20263
20665
  if (looksLikeProblem(error)) {
20264
20666
  try {
20265
20667
  const problem = parseSafeProblem(error);
20266
- return failure(command, 7, problem.code, problem.retryable, problem.suggestedAction, void 0, problem.retryAfterSeconds, problem.requestId, problem.docsUrl, ACCOUNT_SENSITIVE_PROBLEMS.has(problem.code) ? actingAs : void 0);
20668
+ return failure(command, 7, problem.code, problem.retryable, problem.suggestedAction, void 0, problem.retryAfterSeconds, problem.requestId, problem.docsUrl, ACCOUNT_SENSITIVE_PROBLEMS.has(problem.code) ? actingAs : void 0, problem.message);
20267
20669
  } catch {
20268
- return failure(command, 7, "response_contract_invalid", false, "Retry only after the client or server contract is repaired.");
20670
+ return failure(command, 7, "response_contract_invalid", false, RESPONSE_CONTRACT_INVALID_ACTION);
20269
20671
  }
20270
20672
  }
20271
20673
  if (looksLikeRemoteObject(error)) {
20272
- return failure(command, 7, "response_contract_invalid", false, "Retry only after the client or server contract is repaired.");
20674
+ return failure(command, 7, "response_contract_invalid", false, RESPONSE_CONTRACT_INVALID_ACTION);
20273
20675
  }
20274
20676
  if (error instanceof NetworkTransportError) {
20275
- return failure(command, 8, "network_error", true, "Check connectivity and retry.");
20677
+ return failure(command, 8, "network_error", true, "The request did not reach the ohmyho.st API, or its answer was cut off. If the agent runs in a sandbox without network access, run the command with network access. Otherwise run the same command again with the same --idempotency-key; the change may already have been applied, and a new key could apply it twice.");
20678
+ }
20679
+ return failure(command, 1, "internal_error", false, INTERNAL_ERROR_ACTION);
20680
+ };
20681
+ var reusedKeyProblem = (error) => {
20682
+ if (!looksLikeProblem(error))
20683
+ return void 0;
20684
+ try {
20685
+ const problem = parseSafeProblem(error);
20686
+ return problem.code === "idempotency_key_reused" ? problem : void 0;
20687
+ } catch {
20688
+ return void 0;
20689
+ }
20690
+ };
20691
+ var OPERATION_STREAM_CONNECTIONS = 4;
20692
+ var readStatementFile = async (path) => {
20693
+ const invalid5 = () => new ProductCliFailure(2, "statement_file_invalid", false, STATEMENT_FILE_INVALID_ACTION);
20694
+ let file;
20695
+ try {
20696
+ file = await open3(path, "r");
20697
+ } catch {
20698
+ throw invalid5();
20699
+ }
20700
+ const buffer = Buffer.alloc(65537);
20701
+ try {
20702
+ const stat3 = await file.stat();
20703
+ if (!stat3.isFile() || stat3.size > 65536)
20704
+ throw invalid5();
20705
+ let length = 0;
20706
+ while (length < buffer.length) {
20707
+ const read = await file.read(buffer, length, buffer.length - length, null);
20708
+ if (read.bytesRead === 0)
20709
+ break;
20710
+ length += read.bytesRead;
20711
+ }
20712
+ if (length > 65536)
20713
+ throw invalid5();
20714
+ return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, length));
20715
+ } catch (error) {
20716
+ if (error instanceof ProductCliFailure)
20717
+ throw error;
20718
+ throw invalid5();
20719
+ } finally {
20720
+ buffer.fill(0);
20721
+ await file.close();
20276
20722
  }
20277
- return failure(command, 1, "internal_error", false, "Retry the command.");
20278
20723
  };
20279
20724
  var psqlUnavailable = () => failure("database psql", 9, "psql_unavailable", false, "Install the PostgreSQL client (psql) and make it available on PATH, or run 'ohmyhost database access create' and connect with your own SQL client.");
20280
20725
  var success = (command, body) => ({
@@ -20283,7 +20728,7 @@ var success = (command, body) => ({
20283
20728
  `,
20284
20729
  stderr: ""
20285
20730
  });
20286
- var failure = (command, exitCode, code, retryable, suggestedAction, operation, retryAfterSeconds, requestId, docsUrl, actingAs) => {
20731
+ var failure = (command, exitCode, code, retryable, suggestedAction, operation, retryAfterSeconds, requestId, docsUrl, actingAs, message) => {
20287
20732
  const document = {
20288
20733
  version: 1,
20289
20734
  command,
@@ -20297,6 +20742,7 @@ var failure = (command, exitCode, code, retryable, suggestedAction, operation, r
20297
20742
  ...requestId === void 0 ? {} : { request_id: requestId },
20298
20743
  ...docsUrl === void 0 ? {} : { docs_url: docsUrl },
20299
20744
  ...operation?.error === void 0 ? {} : { message: operation.error.message },
20745
+ ...message === void 0 ? {} : { message },
20300
20746
  ...actingAs === void 0 ? {} : { acting_as: actingAs }
20301
20747
  }
20302
20748
  };
@@ -20346,6 +20792,10 @@ var parsedCommandName = (command) => {
20346
20792
  return "feedback status";
20347
20793
  if (command.kind === "project-create")
20348
20794
  return "project create";
20795
+ if (command.kind === "project-data-plan")
20796
+ return "project data plan";
20797
+ if (command.kind === "project-data-change")
20798
+ return "project data change";
20349
20799
  if (command.kind === "organization-create")
20350
20800
  return "organization create";
20351
20801
  if (command.kind === "organization-list")
@@ -22459,24 +22909,6 @@ var getDeploymentLogs = (parameters, options) => {
22459
22909
  ...params
22460
22910
  });
22461
22911
  };
22462
- var streamDeploymentLogs = (parameters, options) => {
22463
- const params = buildClientParams([parameters], [
22464
- {
22465
- args: [
22466
- { in: "headers", key: "X-Request-Id" },
22467
- { in: "path", key: "project_id" },
22468
- { in: "path", key: "deployment_id" }
22469
- ]
22470
- }
22471
- ]);
22472
- return (options?.client ?? client).sse.get({
22473
- responseStyle: "data",
22474
- security: [{ scheme: "bearer", type: "http" }],
22475
- url: "/v1/projects/{project_id}/deployments/{deployment_id}/logs/events",
22476
- ...options,
22477
- ...params
22478
- });
22479
- };
22480
22912
  var getDatabaseCompute = (parameters, options) => {
22481
22913
  const params = buildClientParams([parameters], [
22482
22914
  {
@@ -23023,6 +23455,55 @@ var retryManagedMailMessage = (parameters, options) => {
23023
23455
  ...params
23024
23456
  });
23025
23457
  };
23458
+ var planProjectDataChange = (parameters, options) => {
23459
+ const params = buildClientParams([parameters], [
23460
+ {
23461
+ args: [
23462
+ { in: "headers", key: "X-Request-Id" },
23463
+ { in: "path", key: "project_id" },
23464
+ { key: "projectDataChangeRequest", map: "body" }
23465
+ ]
23466
+ }
23467
+ ]);
23468
+ return (options?.client ?? client).post({
23469
+ responseStyle: "data",
23470
+ security: [{ scheme: "bearer", type: "http" }],
23471
+ url: "/v1/projects/{project_id}/data-changes:plan",
23472
+ ...options,
23473
+ ...params,
23474
+ headers: {
23475
+ "Content-Type": "application/json",
23476
+ ...options?.headers,
23477
+ ...params.headers
23478
+ }
23479
+ });
23480
+ };
23481
+ var createProjectDataChange = (parameters, options) => {
23482
+ const params = buildClientParams([parameters], [
23483
+ {
23484
+ args: [
23485
+ { in: "headers", key: "X-Request-Id" },
23486
+ { in: "headers", key: "Idempotency-Key" },
23487
+ { in: "headers", key: "If-Match" },
23488
+ { in: "headers", key: "X-Confirmation-Token" },
23489
+ { in: "path", key: "project_id" },
23490
+ { key: "projectDataChangeRequest", map: "body" }
23491
+ ]
23492
+ }
23493
+ ]);
23494
+ return (options?.client ?? client).post({
23495
+ responseStyle: "data",
23496
+ security: [{ scheme: "bearer", type: "http" }],
23497
+ url: "/v1/projects/{project_id}/data-changes",
23498
+ ...options,
23499
+ ...params,
23500
+ headers: {
23501
+ "Content-Type": "application/json",
23502
+ ...options?.headers,
23503
+ ...params.headers
23504
+ }
23505
+ });
23506
+ };
23026
23507
 
23027
23508
  // packages/sdk-ts/dist/managed-mail-response.js
23028
23509
  var ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
@@ -23045,30 +23526,30 @@ function assertManagedMailResponse(value, input) {
23045
23526
  id2(r["domain_id"]);
23046
23527
  domain(r["domain"]);
23047
23528
  if (r["environment_id"] !== input.environmentId)
23048
- invalid3();
23529
+ invalid4();
23049
23530
  for (const key of ["sending", "receiving"])
23050
23531
  if (!["disabled", "pending", "ready", "failed"].includes(String(r[key])))
23051
- invalid3();
23532
+ invalid4();
23052
23533
  if (!["ready", "verification_pending"].includes(String(r["status"])))
23053
- invalid3();
23534
+ invalid4();
23054
23535
  time(r["configured_at"]);
23055
23536
  if (r["observed_at"] !== null)
23056
23537
  time(r["observed_at"]);
23057
23538
  records(r["dns_records"]);
23058
23539
  if (r["next_check_after_seconds"] !== null && r["next_check_after_seconds"] !== 60)
23059
- invalid3();
23540
+ invalid4();
23060
23541
  if (r["webhook"] !== null) {
23061
23542
  const hook = object2(r["webhook"]);
23062
23543
  exact3(hook, ["url", "verified"]);
23063
23544
  https(hook["url"]);
23064
23545
  if (typeof hook["verified"] !== "boolean")
23065
- invalid3();
23546
+ invalid4();
23066
23547
  }
23067
23548
  if (r["dns"] !== void 0) {
23068
23549
  const dns = object2(r["dns"]);
23069
23550
  exact3(dns, ["status", "records"]);
23070
23551
  if (!["configured", "action_required", "conflict"].includes(String(dns["status"])))
23071
- invalid3();
23552
+ invalid4();
23072
23553
  records(dns["records"]);
23073
23554
  }
23074
23555
  return;
@@ -23076,19 +23557,19 @@ function assertManagedMailResponse(value, input) {
23076
23557
  if (input.action === "webhook_set") {
23077
23558
  exact3(r, ["url", "signing_secret", "status"]);
23078
23559
  if (r["url"] !== input.url || r["status"] !== "verification_required" || typeof r["signing_secret"] !== "string" || !/^whsec_[A-Za-z0-9+/]{43}=$/u.test(r["signing_secret"]))
23079
- invalid3();
23560
+ invalid4();
23080
23561
  return;
23081
23562
  }
23082
23563
  if (input.action === "webhook_disable") {
23083
23564
  exact3(r, ["status"]);
23084
23565
  if (r["status"] !== "disabled")
23085
- invalid3();
23566
+ invalid4();
23086
23567
  return;
23087
23568
  }
23088
23569
  if (input.action === "domain_delete") {
23089
23570
  exact3(r, ["status", "domain", "dns_records"]);
23090
23571
  if (r["status"] !== "deleted")
23091
- invalid3();
23572
+ invalid4();
23092
23573
  domain(r["domain"]);
23093
23574
  records(r["dns_records"]);
23094
23575
  return;
@@ -23096,25 +23577,25 @@ function assertManagedMailResponse(value, input) {
23096
23577
  if (input.action === "messages_list") {
23097
23578
  exact3(r, ["items"]);
23098
23579
  if (!Array.isArray(r["items"]) || r["items"].length > 50)
23099
- invalid3();
23580
+ invalid4();
23100
23581
  for (const value2 of r["items"]) {
23101
23582
  const item = object2(value2);
23102
23583
  exact3(item, ["message_id", "received_at", "state", "attempts"]);
23103
23584
  id2(item["message_id"]);
23104
23585
  time(item["received_at"]);
23105
23586
  if (!["pending", "delivering", "delivered", "delivery_failed", "expired"].includes(String(item["state"])) || !Number.isInteger(item["attempts"]) || Number(item["attempts"]) < 0 || Number(item["attempts"]) > 4)
23106
- invalid3();
23587
+ invalid4();
23107
23588
  }
23108
23589
  return;
23109
23590
  }
23110
23591
  if (input.action === "message_retry") {
23111
23592
  exact3(r, ["attempted"], ["delivered"]);
23112
23593
  if (typeof r["attempted"] !== "boolean" || r["delivered"] !== void 0 && typeof r["delivered"] !== "boolean")
23113
- invalid3();
23594
+ invalid4();
23114
23595
  return;
23115
23596
  }
23116
23597
  if (input.action !== "message_get")
23117
- invalid3();
23598
+ invalid4();
23118
23599
  if (r["status"] === "expired") {
23119
23600
  exact3(r, ["status"]);
23120
23601
  return;
@@ -23133,65 +23614,65 @@ function assertManagedMailResponse(value, input) {
23133
23614
  "attachments"
23134
23615
  ]);
23135
23616
  if (r["id"] !== input.messageId)
23136
- invalid3();
23617
+ invalid4();
23137
23618
  domain(r["domain"]);
23138
23619
  time(r["received_at"]);
23139
23620
  time(r["expires_at"]);
23140
23621
  if (Date.parse(String(r["expires_at"])) - Date.parse(String(r["received_at"])) !== 72 * 36e5)
23141
- invalid3();
23622
+ invalid4();
23142
23623
  for (const key of ["from", "subject", "message_id"])
23143
23624
  if (typeof r[key] !== "string")
23144
- invalid3();
23625
+ invalid4();
23145
23626
  for (const key of ["text", "html"])
23146
23627
  if (r[key] !== null && typeof r[key] !== "string")
23147
- invalid3();
23628
+ invalid4();
23148
23629
  if (!Array.isArray(r["to"]) || r["to"].some((v) => typeof v !== "string" || v.slice(v.lastIndexOf("@") + 1).toLowerCase() !== r["domain"]))
23149
- invalid3();
23630
+ invalid4();
23150
23631
  if (!Array.isArray(r["attachments"]))
23151
- invalid3();
23632
+ invalid4();
23152
23633
  for (const value2 of r["attachments"]) {
23153
23634
  const a = object2(value2);
23154
23635
  exact3(a, ["id", "filename", "content_type", "download_path"]);
23155
23636
  if (typeof a["id"] !== "string" || !/^[A-Za-z0-9_-]{1,128}$/u.test(a["id"]) || a["filename"] !== null && typeof a["filename"] !== "string" || typeof a["content_type"] !== "string" || a["download_path"] !== `/v1/projects/${input.projectId}/environments/${input.environmentId}/mail/messages/${input.messageId}/attachments/${a["id"]}`)
23156
- invalid3();
23637
+ invalid4();
23157
23638
  }
23158
23639
  }
23159
23640
  function object2(v) {
23160
23641
  if (typeof v !== "object" || v === null || Array.isArray(v))
23161
- invalid3();
23642
+ invalid4();
23162
23643
  return v;
23163
23644
  }
23164
23645
  function exact3(v, required, optional = []) {
23165
23646
  if (required.some((k) => !(k in v)) || Object.keys(v).some((k) => !required.includes(k) && !optional.includes(k)))
23166
- invalid3();
23647
+ invalid4();
23167
23648
  }
23168
23649
  function id2(v) {
23169
23650
  if (typeof v !== "string" || !ULID.test(v))
23170
- invalid3();
23651
+ invalid4();
23171
23652
  }
23172
23653
  function domain(v) {
23173
23654
  if (typeof v !== "string" || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(v))
23174
- invalid3();
23655
+ invalid4();
23175
23656
  }
23176
23657
  function time(v) {
23177
23658
  if (typeof v !== "string" || !Number.isFinite(Date.parse(v)) || new Date(v).toISOString() !== v)
23178
- invalid3();
23659
+ invalid4();
23179
23660
  }
23180
23661
  function https(v) {
23181
23662
  if (typeof v !== "string" || !v.startsWith("https://"))
23182
- invalid3();
23663
+ invalid4();
23183
23664
  }
23184
23665
  function records(v) {
23185
23666
  if (!Array.isArray(v))
23186
- invalid3();
23667
+ invalid4();
23187
23668
  for (const value of v) {
23188
23669
  const r = object2(value);
23189
23670
  exact3(r, ["type", "name", "value", "purpose", "status"], ["priority"]);
23190
23671
  if (!["TXT", "CNAME", "MX"].includes(String(r["type"])) || ["name", "value", "purpose", "status"].some((k) => typeof r[k] !== "string") || r["type"] === "MX" && (!Number.isInteger(r["priority"]) || Number(r["priority"]) < 0 || Number(r["priority"]) > 65535))
23191
- invalid3();
23672
+ invalid4();
23192
23673
  }
23193
23674
  }
23194
- function invalid3() {
23675
+ function invalid4() {
23195
23676
  throw new TypeError("Mail response contract invalid");
23196
23677
  }
23197
23678
 
@@ -23489,6 +23970,18 @@ var GeneratedSdkProductApi = class {
23489
23970
  async getFeedback(feedbackId, cursor) {
23490
23971
  return parseFeedbackStatus(await getFeedback({ feedback_id: feedbackId, ...cursor === void 0 ? {} : { cursor } }, { client: this.#client }));
23491
23972
  }
23973
+ async planProjectDataChange(input) {
23974
+ return parseProjectDataChangePlan2(await planProjectDataChange({ project_id: input.projectId, projectDataChangeRequest: { change: input.change } }, { client: this.#client }));
23975
+ }
23976
+ async createProjectDataChange(input) {
23977
+ return parseOperation(await createProjectDataChange({
23978
+ project_id: input.projectId,
23979
+ projectDataChangeRequest: { change: input.change },
23980
+ "If-Match": input.ifMatch,
23981
+ "X-Confirmation-Token": input.confirmationToken,
23982
+ "Idempotency-Key": input.idempotencyKey
23983
+ }, { client: this.#client }));
23984
+ }
23492
23985
  async createProject(input) {
23493
23986
  return createProject({
23494
23987
  "Idempotency-Key": input.idempotencyKey,
@@ -23569,8 +24062,17 @@ var GeneratedSdkProductApi = class {
23569
24062
  async getDeployment(input) {
23570
24063
  return getDeployment({ project_id: input.projectId, deployment_id: input.deploymentId }, { client: this.#client });
23571
24064
  }
23572
- async streamOperationEvents(operationId, signal) {
23573
- return (await streamOperationEvents({ operation_id: operationId }, { client: this.#client, signal, headers: { Accept: "text/event-stream" } })).stream;
24065
+ async streamOperationEvents(operationId, signal, lastEventId) {
24066
+ return (await streamOperationEvents({ operation_id: operationId }, {
24067
+ client: this.#client,
24068
+ signal,
24069
+ // The generated client would otherwise retry a refused stream, such as a 404, forever.
24070
+ sseMaxRetryAttempts: 1,
24071
+ headers: {
24072
+ Accept: "text/event-stream",
24073
+ ...lastEventId === void 0 ? {} : { "Last-Event-ID": lastEventId }
24074
+ }
24075
+ })).stream;
23574
24076
  }
23575
24077
  async getDeploymentLogs(input) {
23576
24078
  return getDeploymentLogs({
@@ -23580,9 +24082,6 @@ var GeneratedSdkProductApi = class {
23580
24082
  ...input.cursor === void 0 ? {} : { cursor: input.cursor }
23581
24083
  }, { client: this.#client });
23582
24084
  }
23583
- async streamDeploymentLogs(input, signal) {
23584
- return (await streamDeploymentLogs({ project_id: input.projectId, deployment_id: input.deploymentId }, { client: this.#client, signal, headers: { Accept: "text/event-stream" } })).stream;
23585
- }
23586
24085
  async planRollback(input) {
23587
24086
  return planDeploymentRollback({ project_id: input.projectId, target_deployment_id: input.deploymentId }, { client: this.#client });
23588
24087
  }
@@ -23951,6 +24450,9 @@ var ohmyhost_schema_default = {
23951
24450
  maxLength: 256,
23952
24451
  pattern: "^/"
23953
24452
  },
24453
+ browser: {
24454
+ $ref: "#/$defs/browser"
24455
+ },
23954
24456
  egress: {
23955
24457
  type: "object",
23956
24458
  additionalProperties: false,
@@ -23958,7 +24460,7 @@ var ohmyhost_schema_default = {
23958
24460
  properties: {
23959
24461
  allow: {
23960
24462
  type: "array",
23961
- maxItems: 16,
24463
+ maxItems: 13,
23962
24464
  uniqueItems: true,
23963
24465
  items: {
23964
24466
  type: "string",
@@ -23971,6 +24473,56 @@ var ohmyhost_schema_default = {
23971
24473
  }
23972
24474
  }
23973
24475
  },
24476
+ browserOrigins: {
24477
+ type: "array",
24478
+ maxItems: 16,
24479
+ uniqueItems: true,
24480
+ items: {
24481
+ type: "string",
24482
+ minLength: 1,
24483
+ maxLength: 256,
24484
+ pattern: "^https://"
24485
+ }
24486
+ },
24487
+ browser: {
24488
+ type: "object",
24489
+ additionalProperties: false,
24490
+ properties: {
24491
+ connect: {
24492
+ type: "array",
24493
+ maxItems: 16,
24494
+ uniqueItems: true,
24495
+ items: {
24496
+ type: "string",
24497
+ minLength: 1,
24498
+ maxLength: 256,
24499
+ pattern: "^(?:https|wss)://"
24500
+ }
24501
+ },
24502
+ scripts: { $ref: "#/$defs/browserOrigins" },
24503
+ styles: { $ref: "#/$defs/browserOrigins" },
24504
+ images: { $ref: "#/$defs/browserOrigins" },
24505
+ fonts: { $ref: "#/$defs/browserOrigins" },
24506
+ frames: { $ref: "#/$defs/browserOrigins" },
24507
+ frameAncestors: { $ref: "#/$defs/browserOrigins" },
24508
+ workers: {
24509
+ type: "object",
24510
+ additionalProperties: false,
24511
+ properties: {
24512
+ self: { type: "boolean" },
24513
+ blob: { type: "boolean" }
24514
+ }
24515
+ },
24516
+ cors: {
24517
+ type: "object",
24518
+ additionalProperties: false,
24519
+ properties: {
24520
+ origins: { $ref: "#/$defs/browserOrigins" },
24521
+ credentials: { type: "boolean" }
24522
+ }
24523
+ }
24524
+ }
24525
+ },
23974
24526
  database: {
23975
24527
  type: "object",
23976
24528
  additionalProperties: false,
@@ -24228,6 +24780,74 @@ function validCronField(value, minimum, maximum) {
24228
24780
  return parsed >= minimum && parsed <= maximum;
24229
24781
  }
24230
24782
 
24783
+ // packages/contracts/dist/browser-capabilities.js
24784
+ var RESOURCE_GROUPS = [
24785
+ "connect",
24786
+ "scripts",
24787
+ "styles",
24788
+ "images",
24789
+ "fonts",
24790
+ "frames",
24791
+ "frameAncestors"
24792
+ ];
24793
+ var POLICY_KEYS = [...RESOURCE_GROUPS, "workers", "cors"];
24794
+ var MAX_ORIGINS = 16;
24795
+ function normalizeBrowserPolicy(value) {
24796
+ const input = value === void 0 ? {} : policyRecord(value, POLICY_KEYS);
24797
+ const workers = input["workers"] === void 0 ? {} : policyRecord(input["workers"], ["self", "blob"]);
24798
+ const cors = input["cors"] === void 0 ? {} : policyRecord(input["cors"], ["origins", "credentials"]);
24799
+ const resources = Object.fromEntries(RESOURCE_GROUPS.map((group) => [group, normalizeOrigins(input[group], group === "connect")]));
24800
+ return Object.freeze({
24801
+ ...resources,
24802
+ workers: Object.freeze({
24803
+ self: normalizeBoolean(workers["self"]),
24804
+ blob: normalizeBoolean(workers["blob"])
24805
+ }),
24806
+ cors: Object.freeze({
24807
+ origins: normalizeOrigins(cors["origins"], false),
24808
+ credentials: normalizeBoolean(cors["credentials"])
24809
+ })
24810
+ });
24811
+ }
24812
+ function policyRecord(value, keys, complete = false) {
24813
+ if (typeof value !== "object" || value === null || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)) || Object.keys(value).some((key) => !keys.includes(key)) || complete && Object.keys(value).length !== keys.length)
24814
+ throw invalidPolicy();
24815
+ return value;
24816
+ }
24817
+ function normalizeBoolean(value) {
24818
+ if (value === void 0)
24819
+ return false;
24820
+ if (typeof value !== "boolean")
24821
+ throw invalidPolicy();
24822
+ return value;
24823
+ }
24824
+ function normalizeOrigins(value, allowWebSocket) {
24825
+ if (value === void 0)
24826
+ return Object.freeze([]);
24827
+ if (!Array.isArray(value) || value.length > MAX_ORIGINS)
24828
+ throw invalidPolicy();
24829
+ const origins = [];
24830
+ for (const origin of value) {
24831
+ if (!browserOrigin(origin, allowWebSocket) || origins.includes(origin))
24832
+ throw invalidPolicy();
24833
+ origins.push(origin);
24834
+ }
24835
+ return Object.freeze(origins.sort());
24836
+ }
24837
+ function browserOrigin(value, allowWebSocket) {
24838
+ if (typeof value !== "string" || value.length > 256)
24839
+ return false;
24840
+ try {
24841
+ const url = new URL(value);
24842
+ return (url.protocol === "https:" || allowWebSocket && url.protocol === "wss:") && url.origin === value && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "" && url.hostname.includes(".") && !url.hostname.endsWith(".local") && !url.hostname.endsWith(".internal") && !/^\d+(?:\.\d+){3}$/u.test(url.hostname) && url.hostname.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label));
24843
+ } catch {
24844
+ return false;
24845
+ }
24846
+ }
24847
+ function invalidPolicy() {
24848
+ return new TypeError("Invalid browser capability policy");
24849
+ }
24850
+
24231
24851
  // packages/contracts/dist/index.js
24232
24852
  var validateOhmyhostConfig = new import__.Ajv2020({
24233
24853
  allErrors: true,
@@ -24252,7 +24872,8 @@ function parseOhmyhostConfig(value) {
24252
24872
  healthcheck: normalizeHealthcheck(input.runtime.healthcheck),
24253
24873
  egress: Object.freeze({
24254
24874
  allow: normalizeEgressOrigins(input.runtime.egress?.allow)
24255
- })
24875
+ }),
24876
+ ...input.runtime.browser === void 0 ? {} : { browser: normalizeBrowserPolicy(input.runtime.browser) }
24256
24877
  }),
24257
24878
  ...input.database === void 0 ? {} : {
24258
24879
  database: Object.freeze({
@@ -24306,7 +24927,7 @@ function normalizeEgressOrigins(value) {
24306
24927
  } catch {
24307
24928
  throw new TypeError("ohmyhost.yaml runtime egress origin is invalid");
24308
24929
  }
24309
- if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.origin !== origin || parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "" || forbiddenEgressHostname(parsed.hostname)) {
24930
+ if (parsed.protocol !== "https:" || parsed.port !== "" || parsed.username !== "" || parsed.password !== "" || parsed.origin !== origin || parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "" || forbiddenEgressHostname(parsed.hostname)) {
24310
24931
  throw new TypeError("ohmyhost.yaml runtime egress origin is invalid");
24311
24932
  }
24312
24933
  }
@@ -24315,16 +24936,7 @@ function normalizeEgressOrigins(value) {
24315
24936
  }
24316
24937
  function forbiddenEgressHostname(hostname) {
24317
24938
  const normalized = hostname.toLowerCase();
24318
- if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal") || normalized === "0.0.0.0" || normalized === "::1") {
24319
- return true;
24320
- }
24321
- const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(normalized);
24322
- if (ipv4 === null)
24323
- return normalized.startsWith("[") || normalized.endsWith("]");
24324
- const octets = ipv4.slice(1).map(Number);
24325
- if (octets.some((octet) => octet > 255))
24326
- return true;
24327
- return octets[0] === 10 || octets[0] === 127 || octets[0] === 169 && octets[1] === 254 || octets[0] === 172 && (octets[1] ?? 0) >= 16 && (octets[1] ?? 0) <= 31 || octets[0] === 192 && octets[1] === 168;
24939
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal") || /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(normalized) || normalized.startsWith("[") || normalized.endsWith("]");
24328
24940
  }
24329
24941
  function parseOhmyhostConfigYaml(source) {
24330
24942
  if (typeof source !== "string") {
@@ -24625,6 +25237,7 @@ async function initializeRepository(input) {
24625
25237
  const configuredDatabase = existingConfiguration?.database?.enabled === true;
24626
25238
  const databaseProvider = supabaseMigrationCount > 0 ? "supabase" : postgresMigrationCount > 0 || configuredDatabase ? "postgresql" : "none";
24627
25239
  const migrationCount = databaseProvider === "supabase" ? supabaseMigrationCount : postgresMigrationCount;
25240
+ const selectedDatabaseProvider = existingConfiguration === null ? databaseProvider : configuredDatabase ? "postgresql" : "none";
24628
25241
  const betterAuth = dependencies.has("better-auth") || existingConfiguration?.auth?.provider === "better-auth";
24629
25242
  const workos = [
24630
25243
  "@workos-inc/authkit-react",
@@ -24677,19 +25290,19 @@ async function initializeRepository(input) {
24677
25290
  });
24678
25291
  const configurationYaml = existingConfigurationYaml ?? generatedConfigurationYaml;
24679
25292
  const capabilities = Object.freeze({
24680
- database: databaseProvider !== "none",
25293
+ database: selectedDatabaseProvider !== "none",
24681
25294
  auth: targetBetterAuth,
24682
25295
  mail: targetMail,
24683
25296
  storage: targetStorage
24684
25297
  });
24685
25298
  const companion = companionRequired ? viteCompanionScaffold(applicationFiles, capabilities) : null;
24686
25299
  const companionSourcePresent = companion !== null && applicationFiles.includes(companion.sourceEntryPoint);
24687
- const migrationSqlAdmissionBlockers = await migrationSqlBlockers(applicationDirectory, applicationFiles, existingConfiguration?.database?.migrations, databaseProvider);
25300
+ const migrationSqlAdmissionBlockers = await migrationSqlBlockers(applicationDirectory, applicationFiles, existingConfiguration?.database?.migrations, selectedDatabaseProvider);
24688
25301
  const blockers = [
24689
25302
  ...repositoryAdmissionBlockers,
24690
25303
  ...await workerModuleBlockers(framework, applicationDirectory, applicationFiles),
24691
25304
  ...await cronBlockers(framework, existingConfiguration, applicationDirectory, applicationFiles),
24692
- ...migrationFilenameBlockers(applicationFiles, existingConfiguration?.database?.migrations, databaseProvider),
25305
+ ...selectedDatabaseProvider === "none" ? [] : migrationFilenameBlockers(applicationFiles, existingConfiguration?.database?.migrations, selectedDatabaseProvider),
24693
25306
  ...migrationSqlAdmissionBlockers,
24694
25307
  ...await storageClientBlockers(existingConfiguration, applicationDirectory, applicationFiles),
24695
25308
  ...await databaseContractBlockers(existingConfiguration, applicationDirectory, applicationFiles)
@@ -24718,7 +25331,9 @@ async function initializeRepository(input) {
24718
25331
  sourceEntryPoint: WORKER_MODULE_PATH,
24719
25332
  present: applicationFiles.includes(WORKER_MODULE_PATH),
24720
25333
  ...runtimeContract(capabilities)
24721
- }) : null
25334
+ }) : null,
25335
+ // Next.js and TanStack Start have neither a companion nor a Worker module, and read it here.
25336
+ runtime: Object.values(capabilities).some(Boolean) ? runtimeContract(capabilities) : null
24722
25337
  });
24723
25338
  }
24724
25339
  function detectRepositoryFramework(dependencies, files, existingConfiguration) {
@@ -24738,7 +25353,7 @@ async function cronBlockers(framework, config, applicationDirectory, files) {
24738
25353
  return [
24739
25354
  {
24740
25355
  code: "scheduled_functions_runtime_unsupported",
24741
- message: "Declared crons run on the edge runtime of a Vite API companion, Next.js or TanStack Start application, or on the functions runtime."
25356
+ message: "functions.crons needs runtime.mode edge (Next.js, TanStack Start, or Vite with src/ohmyhost/companion.ts) or functions (src/ohmyhost/worker.ts) in ohmyhost.yaml. Change runtime.mode, or remove functions.crons."
24742
25357
  }
24743
25358
  ];
24744
25359
  }
@@ -24815,14 +25430,20 @@ function migrationFilenameBlockers(files, configuredPath, provider) {
24815
25430
  return [];
24816
25431
  const root = configuredPath ?? "postgres/migrations";
24817
25432
  const prefix = `${root}/`;
24818
- const invalid4 = files.filter((path) => path.startsWith(prefix)).map((path) => path.slice(prefix.length)).some((path) => !/^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u.test(path));
24819
- return invalid4 ? [
25433
+ const invalid5 = files.filter((path) => path.startsWith(prefix)).map((path) => path.slice(prefix.length)).find((path) => !/^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u.test(path));
25434
+ return invalid5 === void 0 ? [] : [
24820
25435
  {
24821
25436
  code: "migration_filename_noncanonical",
24822
- message: "Rename every PostgreSQL migration to YYYYMMDDHHMMSS_name.sql before initialization."
25437
+ message: `Migration file '${invalid5}' in ${root} is not named YYYYMMDDHHMMSS_name.sql (14 digits, an underscore, then lowercase letters, digits, _ or -). Every file in that directory must match, README and .gitkeep included: rename or move it, keeping the run order.`
24823
25438
  }
24824
- ] : [];
25439
+ ];
24825
25440
  }
25441
+ var MIGRATION_REJECTION = {
25442
+ destructive: "It contains DROP, TRUNCATE, INSERT, UPDATE, DELETE, BEGIN/COMMIT/ROLLBACK, CREATE OR REPLACE, or ALTER TABLE other than ADD; move row changes to 'ohmyhost database write'.",
25443
+ provider_specific: "It mentions supabase, CREATE POLICY, ROW LEVEL SECURITY, the auth. or storage. schema, or the anon, authenticated or service_role role; comments count.",
25444
+ unsupported: "It has a statement other than CREATE TABLE, TYPE, DOMAIN, SEQUENCE, VIEW, MATERIALIZED VIEW, TRIGGER, INDEX or FUNCTION, ALTER TABLE ... ADD, COMMENT ON, CREATE SCHEMA IF NOT EXISTS auth|extensions|private or CREATE EXTENSION IF NOT EXISTS pgcrypto|btree_gist|unaccent; or a function not in LANGUAGE sql or plpgsql, SECURITY DEFINER without SET search_path, or the word ohmyhost, comments included.",
25445
+ invalid: "It is empty, not UTF-8 with LF line endings, has a BOM or NUL byte or an unterminated quote or comment, or exceeds 256 KiB (at most 128 files and 2 MiB in total)."
25446
+ };
24826
25447
  async function migrationSqlBlockers(applicationDirectory, files, configuredPath, provider) {
24827
25448
  if (provider !== "postgresql")
24828
25449
  return [];
@@ -24845,7 +25466,7 @@ async function migrationSqlBlockers(applicationDirectory, files, configuredPath,
24845
25466
  throw error;
24846
25467
  blockers.push({
24847
25468
  code: "migration_sql_not_admitted",
24848
- message: `PostgreSQL migration '${migration.path}' is not admitted (${error.reason}).`
25469
+ message: `PostgreSQL migration '${migration.path}' is not admitted (${error.reason}): ${MIGRATION_REJECTION[error.reason]} Migrations are expand-only. Fix the file before its first deployment; never edit a migration a deployment already applied.`
24849
25470
  });
24850
25471
  }
24851
25472
  }
@@ -24860,7 +25481,7 @@ async function migrationSqlBlockers(applicationDirectory, files, configuredPath,
24860
25481
  return [
24861
25482
  {
24862
25483
  code: "migration_sql_not_admitted",
24863
- message: `PostgreSQL migration set is not admitted (${error.reason}).`
25484
+ message: `The migration set is not admitted (${error.reason}): ${MIGRATION_REJECTION[error.reason]}`
24864
25485
  }
24865
25486
  ];
24866
25487
  }
@@ -24871,17 +25492,17 @@ async function readExistingConfiguration(repositoryRoot, repositoryFiles, applic
24871
25492
  const configurationPath = resolve2(repositoryRoot, "ohmyhost.yaml");
24872
25493
  const information = await stat2(configurationPath);
24873
25494
  if (!information.isFile() || information.size > MAXIMUM_CONFIGURATION_BYTES) {
24874
- throw new RepositoryInitError("repository_configuration_invalid", "Existing ohmyhost.yaml must be one bounded regular file.");
25495
+ throw new RepositoryInitError("repository_configuration_invalid", "ohmyhost.yaml must be one regular file of at most 64 KiB");
24875
25496
  }
24876
25497
  const configuration = await readFile3(configurationPath, "utf8");
24877
25498
  let parsed;
24878
25499
  try {
24879
25500
  parsed = parseOhmyhostConfigYaml(configuration);
24880
- } catch {
24881
- throw new RepositoryInitError("repository_configuration_invalid", "Existing ohmyhost.yaml is invalid.");
25501
+ } catch (error) {
25502
+ throw new RepositoryInitError("repository_configuration_invalid", error instanceof TypeError ? error.message : "ohmyhost.yaml is invalid");
24882
25503
  }
24883
25504
  if (parsed.applicationRoot !== applicationRoot) {
24884
- throw new RepositoryInitError("repository_configuration_root_mismatch", "Existing ohmyhost.yaml must select the detected application root.");
25505
+ throw new RepositoryInitError("repository_configuration_root_mismatch", `ohmyhost.yaml sets applicationRoot to '${parsed.applicationRoot}', but init selected '${applicationRoot}'. Run 'ohmyhost init --root ${parsed.applicationRoot} --dry-run --json', or correct applicationRoot.`);
24885
25506
  }
24886
25507
  return configuration;
24887
25508
  }
@@ -25003,12 +25624,13 @@ function hasDirectSafeBuildCommand(packageJson, framework) {
25003
25624
  const scripts = packageJson["scripts"];
25004
25625
  if (!isRecord4(scripts) || typeof scripts["build"] !== "string")
25005
25626
  return false;
25006
- const command = scripts["build"];
25007
- if (command.includes("..") || /(?:^|\s)cd(?:\s|$)/u.test(command))
25008
- return false;
25627
+ return (framework === "nextjs" || framework === "vite" || framework === "tanstack-start") && isAdmittedFrameworkBuildScript(framework, scripts["build"]);
25628
+ }
25629
+ function buildScriptMessage(framework, command) {
25630
+ const quoted = command.slice(0, 200);
25009
25631
  if (framework === "nextjs")
25010
- return command === "next build" || command === "next build --webpack";
25011
- return command === "vite build" || command === "vite build && tsc --noEmit";
25632
+ return `package.json scripts.build must be exactly "next build" or "next build --webpack", not "${quoted}". Move other steps into their own scripts, then commit and push.`;
25633
+ return `package.json scripts.build must be "vite build", optionally joined with && to exactly one "tsc", "tsc -b", "tsc --build" or "tsc --noEmit" stage before or after it, not "${quoted}". Move other steps into their own scripts, then commit and push.`;
25012
25634
  }
25013
25635
  async function assertRepositoryRoot(root) {
25014
25636
  let information;
@@ -25157,11 +25779,16 @@ function frameworkConfigurations(files) {
25157
25779
  ambiguous: Object.freeze(ambiguous)
25158
25780
  });
25159
25781
  }
25782
+ var FRAMEWORK_PACKAGE = {
25783
+ nextjs: "next",
25784
+ "tanstack-start": "@tanstack/react-start",
25785
+ vite: "vite"
25786
+ };
25160
25787
  function frameworkCompatibility(packageJson, framework) {
25161
25788
  const targets = framework === "tanstack-start" ? [
25162
- { framework: "tanstack-start", packageName: "@tanstack/react-start" },
25163
- { framework: "vite", packageName: "vite" }
25164
- ] : framework === "nextjs" ? [{ framework: "nextjs", packageName: "next" }] : framework === "vite" ? [{ framework: "vite", packageName: "vite" }] : [];
25789
+ { framework: "tanstack-start", packageName: FRAMEWORK_PACKAGE["tanstack-start"] },
25790
+ { framework: "vite", packageName: FRAMEWORK_PACKAGE.vite }
25791
+ ] : framework === "nextjs" ? [{ framework: "nextjs", packageName: FRAMEWORK_PACKAGE.nextjs }] : framework === "vite" ? [{ framework: "vite", packageName: FRAMEWORK_PACKAGE.vite }] : [];
25165
25792
  const frameworks = targets.map(({ framework: targetFramework, packageName }) => {
25166
25793
  const version2 = dependencyVersion(packageJson, packageName) ?? "";
25167
25794
  return Object.freeze({
@@ -25199,7 +25826,7 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
25199
25826
  if (tanStackRuntime === "ambiguous") {
25200
25827
  blockers.push({
25201
25828
  code: "framework_ambiguous",
25202
- message: "TanStack Start must explicitly enable static prerendering or the complete Cloudflare edge contract."
25829
+ message: 'TanStack Start needs one vite.config.js, .mjs or .ts that selects a mode. Static: tanstackStart({ prerender: { enabled: true } }) from "@tanstack/react-start/plugin/vite" and no "@cloudflare/vite-plugin". Server: cloudflare({ viteEnvironment: { name: "ssr" } }) from "@cloudflare/vite-plugin" listed before tanstackStart() in plugins, with @cloudflare/vite-plugin, @tanstack/react-router, @tanstack/react-start, react, react-dom, @vitejs/plugin-react and vite in package.json.'
25203
25830
  });
25204
25831
  }
25205
25832
  const relevantAmbiguousConfigs = frameworkConfigs.ambiguous.filter((config) => framework === "nextjs" ? config === "next" || config === "open-next" : config === "vite");
@@ -25217,19 +25844,19 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
25217
25844
  } else if (packageManagerAdmission instanceof FrameworkAdmissionError) {
25218
25845
  blockers.push({
25219
25846
  code: "package_manager_unpinned",
25220
- message: "Declare one exact npm, pnpm, Yarn, or Bun packageManager version and commit its matching lockfile."
25847
+ message: packageManagerAdmission.reason === "package_manager_version_unsupported" ? `This build image runs Bun ${PINNED_BUN_VERSION}; pin packageManager to bun@${PINNED_BUN_VERSION} and commit its matching lockfile. Repeating the unchanged version cannot work.` : "Declare one exact npm, pnpm, Yarn, or Bun packageManager version and commit its matching lockfile."
25221
25848
  });
25222
25849
  } else if (existingConfiguration !== null && (existingConfiguration.build.install !== packageManagerAdmission.installCommand || "command" in existingConfiguration.build && existingConfiguration.build.command !== packageManagerAdmission.buildCommand)) {
25223
25850
  blockers.push({
25224
25851
  code: "build_command_unsupported",
25225
- message: "Use the admitted package manager's secure install and direct build commands."
25852
+ message: `ohmyhost.yaml build.install must be exactly "${packageManagerAdmission.installCommand}"${"command" in existingConfiguration.build ? ` and build.command "${packageManagerAdmission.buildCommand}"` : ""} for the packageManager in package.json. Set them, then commit and push.`
25226
25853
  });
25227
25854
  }
25228
25855
  if (compatibility.classification === "unsupported" && compatibility.frameworks.length > 0) {
25229
- const unsupported = compatibility.frameworks.filter((entry) => entry.classification === "unsupported").map((entry) => `${entry.framework}@${entry.version || "missing"}`).join(", ");
25856
+ const unsupported = compatibility.frameworks.filter((entry) => entry.classification === "unsupported").map((entry) => `${FRAMEWORK_PACKAGE[entry.framework]}@${entry.version || "missing"}`).join(", ");
25230
25857
  blockers.push({
25231
25858
  code: "framework_version_unsupported",
25232
- message: `Framework version is outside the admitted POC window: ${unsupported}.`
25859
+ message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.x or 16.0.0\u201316.3.4, vite 5.4.0\u20138.2.2, @tanstack/react-start 1.168.26\u20131.168.49, as an exact version or a ^ or ~ range whose base version is inside. Pin a version in that range in package.json, update the lockfile, commit and push.`
25233
25860
  });
25234
25861
  }
25235
25862
  const scripts = packageJson["scripts"];
@@ -25240,10 +25867,13 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
25240
25867
  } else if (!hasDirectSafeBuildCommand(packageJson, framework)) {
25241
25868
  blockers.push({
25242
25869
  code: "build_command_unsupported",
25243
- message: "Use a direct framework build command without traversal or directory changes."
25870
+ message: buildScriptMessage(framework, scripts["build"])
25244
25871
  });
25245
25872
  }
25246
25873
  const betterAuthVersion = dependencyVersion(packageJson, "better-auth");
25874
+ const managedAuthIssue = managedCustomerAuthAdmissionIssue(existingConfiguration);
25875
+ if (managedAuthIssue !== null)
25876
+ blockers.push(managedAuthIssue);
25247
25877
  if (existingConfiguration?.auth?.provider === "better-auth" && betterAuthVersion !== null && betterAuthVersion !== SUPPORTED_BETTER_AUTH_VERSION) {
25248
25878
  blockers.push({
25249
25879
  code: "better_auth_version_unsupported",
@@ -25264,7 +25894,7 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
25264
25894
  if (!nextConfigured) {
25265
25895
  blockers.push({
25266
25896
  code: "next_adapter_unconfigured",
25267
- message: "Declare direct next build, exact Next/React versions, and one supported Next config; ohmyhost supplies the OpenNext build overlay."
25897
+ message: 'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.x or 16.0.0\u201316.3.4, and exactly one next.config.js, .mjs or .ts, with at most one open-next.config file. ohmyho.st adds the OpenNext build itself; do not make it the build script.'
25268
25898
  });
25269
25899
  }
25270
25900
  }
@@ -26021,7 +26651,7 @@ var runProductionCli = async (argv, environment, io, signal, overrides = {}) =>
26021
26651
  args = extracted.argv;
26022
26652
  profileName = extracted.profileName;
26023
26653
  } catch {
26024
- return written(invalidCommandFailure(recognizedCommandLabel(argv)));
26654
+ return written(invalidCommandFailure(argv, environment));
26025
26655
  }
26026
26656
  const workingDirectory = overrides.workingDirectory ?? process.cwd();
26027
26657
  let command;
@@ -26053,8 +26683,8 @@ var runProductionCli = async (argv, environment, io, signal, overrides = {}) =>
26053
26683
  if (error instanceof LinkedProjectAmbiguousError)
26054
26684
  return written(errorDocument(recognizedCommandLabel(args), 2, "linked_project_selection_required", false, `This directory is linked to projects of several organizations (${error.organizationIds.join(", ")}). Pass --project ULID, or choose the login with --profile-name NAME; '${resolveProductCliEnvironment(environment).commandPrefix} profile list --json' shows the saved logins.`));
26055
26685
  if (error instanceof CredentialStoreUnavailableError)
26056
- return written(errorDocument(recognizedCommandLabel(args), 3, "credential_store_unavailable", true, "Unlock or repair the selected credential store and retry."));
26057
- const result = error instanceof InvalidCommandError ? invalidCommandFailure(recognizedCommandLabel(args)) : errorDocument("unknown", 1, "internal_error", false, "Retry the command.");
26686
+ return written(errorDocument(recognizedCommandLabel(args), 3, "credential_store_unavailable", false, CREDENTIAL_STORE_UNAVAILABLE_ACTION));
26687
+ const result = error instanceof InvalidCommandError ? invalidCommandFailure(args, environment) : errorDocument(recognizedCommandLabel(args), 1, "internal_error", false, INTERNAL_ERROR_ACTION);
26058
26688
  if (!(error instanceof InvalidCommandError)) {
26059
26689
  diagnostics.record({
26060
26690
  correlationId: randomUUID2(),
@@ -26067,7 +26697,7 @@ var runProductionCli = async (argv, environment, io, signal, overrides = {}) =>
26067
26697
  }
26068
26698
  if (command.kind === "init") {
26069
26699
  if (profileName !== void 0)
26070
- return written(invalidCommandFailure("init"));
26700
+ return written(errorDocument("init", 2, "invalid_command", false, "ohmyhost init runs offline and uses no login: remove --profile-name and run it again."));
26071
26701
  return runOfflineInit(command, io, diagnostics);
26072
26702
  }
26073
26703
  if (command.kind === "help" || command.kind === "version") {
@@ -26101,6 +26731,8 @@ var runProductionCli = async (argv, environment, io, signal, overrides = {}) =>
26101
26731
  const registry = overrides.profiles ?? new NativeKeyringProfileRegistry(void 0, platform.credentialService);
26102
26732
  const linkDirectory = join3(workingDirectory, platform.linkDirectory);
26103
26733
  const projectId = commandProjectId(command);
26734
+ if (projectId !== void 0 && linked === null)
26735
+ links = await readProjectLinks(linkDirectory, platform.apiOrigin);
26104
26736
  const organizationId = handedAccount2?.organizationId ?? commandOrganizationId(command) ?? linked?.organizationId ?? (projectId === void 0 ? void 0 : linkedOrganization(links, projectId));
26105
26737
  const request = profileRequest({
26106
26738
  ...profileName === void 0 ? {} : { name: profileName },
@@ -26179,7 +26811,15 @@ var runProductionCli = async (argv, environment, io, signal, overrides = {}) =>
26179
26811
  }
26180
26812
  }
26181
26813
  } catch (error) {
26182
- return written(compositionFailure(recognizedCommandLabel(args), error, signal, platform.commandPrefix));
26814
+ const result = compositionFailure(recognizedCommandLabel(args), error, signal, platform.commandPrefix, profile);
26815
+ if (result.exitCode === 1)
26816
+ diagnostics.record({
26817
+ correlationId: randomUUID2(),
26818
+ category: "internal",
26819
+ subsystem: "authentication",
26820
+ errorKind: "unexpected"
26821
+ });
26822
+ return written(result);
26183
26823
  }
26184
26824
  const selected = profile;
26185
26825
  const productApiOrigin = platform.apiOrigin;
@@ -26263,13 +26903,13 @@ var adoptLegacyLink = async (input) => {
26263
26903
  });
26264
26904
  await removeLegacyProjectLink(input.directory);
26265
26905
  };
26266
- var compositionFailure = (command, error, signal, commandPrefix) => {
26906
+ var compositionFailure = (command, error, signal, commandPrefix, profile) => {
26267
26907
  if (error instanceof ProfileSelectionError)
26268
26908
  return errorDocument(command, error.exitCode, error.code, false, error.suggestedAction);
26269
26909
  if (error instanceof ProfileLimitError)
26270
26910
  return errorDocument(command, 2, "profile_limit_reached", false, profileLimitAction(commandPrefix));
26271
26911
  if (error instanceof ProductApiAuthenticationError)
26272
- return error.code === "authentication_required" ? errorDocument(command, 5, "authentication_required", false, `Run '${commandPrefix} login --json'.`) : errorDocument(command, 5, "token_refresh_failed", true, `Run '${commandPrefix} login --json' if retrying does not succeed.`);
26912
+ return error.code === "authentication_required" ? errorDocument(command, 5, "authentication_required", false, endedLoginAction(commandPrefix, profile)) : errorDocument(command, 5, "token_refresh_failed", true, refreshFailedAction(commandPrefix, profile));
26273
26913
  return classifyCliFailure(command, error, signal);
26274
26914
  };
26275
26915
  var runOfflineInit = async (command, io, diagnostics) => {
@@ -26301,7 +26941,8 @@ var runOfflineInit = async (command, io, diagnostics) => {
26301
26941
  companion: result.companion,
26302
26942
  // The functions runtime reports its own bindings and packages; dropping it left a
26303
26943
  // storage-enabled functions app with no contract to read at all.
26304
- worker: result.worker
26944
+ worker: result.worker,
26945
+ runtime: result.runtime
26305
26946
  })}
26306
26947
  `);
26307
26948
  return blocked ? 9 : 0;
@@ -26315,11 +26956,38 @@ var runOfflineInit = async (command, io, diagnostics) => {
26315
26956
  });
26316
26957
  const code = error instanceof RepositoryInitBlockedError ? "repository_init_blocked" : error instanceof RepositoryInitConflictError ? "repository_configuration_exists" : known ? error.code : "internal_error";
26317
26958
  const exitCode = error instanceof RepositoryInitBlockedError ? 9 : known ? 2 : 1;
26318
- const result = errorDocument("init", exitCode, code, false, error instanceof RepositoryInitConflictError ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying." : error instanceof RepositoryInitError && error.code === "repository_root_required" ? "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." : error instanceof RepositoryInitError && error.code === "project_name_invalid" ? "Pass a lowercase deployment slug with '--project', then rerun init." : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.");
26959
+ const result = errorDocument("init", exitCode, code, false, known ? initFailureAction(error) : INTERNAL_ERROR_ACTION);
26319
26960
  io.writeStdout(result.stdout);
26320
26961
  return result.exitCode;
26321
26962
  }
26322
26963
  };
26964
+ var initFailureAction = (error) => {
26965
+ if (error instanceof RepositoryInitBlockedError)
26966
+ return "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.";
26967
+ if (error instanceof RepositoryInitConflictError)
26968
+ 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.";
26969
+ const message = /[.!?]$/u.test(error.message) ? error.message : `${error.message}.`;
26970
+ switch (error.code) {
26971
+ case "repository_root_required":
26972
+ 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.";
26973
+ case "project_name_invalid":
26974
+ return "Pass a lowercase deployment slug with '--project', then rerun init.";
26975
+ case "application_root_ambiguous":
26976
+ 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.`;
26977
+ case "application_root_not_found":
26978
+ case "application_root_unsupported":
26979
+ 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.`;
26980
+ case "application_root_invalid":
26981
+ case "application_root_missing":
26982
+ return `${message} Pass --root as a path relative to the repository root, for example apps/web.`;
26983
+ case "repository_configuration_invalid":
26984
+ return `${error.message.replace(/\.$/u, "")}. Fix that, then run 'ohmyhost init --dry-run --json' again; do not delete ohmyhost.yaml.`;
26985
+ case "repository_configuration_root_mismatch":
26986
+ return error.message;
26987
+ default:
26988
+ return `${message} Fix that and run 'ohmyhost init --json' again from the Git repository root.`;
26989
+ }
26990
+ };
26323
26991
  var FetchWorkosPublicTransport = class {
26324
26992
  parentSignal;
26325
26993
  timeoutMs;
@@ -26565,8 +27233,8 @@ var decodeUtf8 = (decoder, chunk, stream) => {
26565
27233
  throw new ResponseContractError();
26566
27234
  }
26567
27235
  };
26568
- var waitUntil = async (instant, signal) => {
26569
- const delay = Math.max(0, instant.getTime() - Date.now());
27236
+ var waitUntil = async (instant2, signal) => {
27237
+ const delay = Math.max(0, instant2.getTime() - Date.now());
26570
27238
  await new Promise((resolve3, reject) => {
26571
27239
  const cleanup = () => signal.removeEventListener("abort", cancel);
26572
27240
  const complete = () => {
@@ -26610,7 +27278,7 @@ var runOfflineLogout = async (registry, request, commandPrefix, io, diagnostics)
26610
27278
  subsystem: "credential_store",
26611
27279
  errorKind: "unavailable"
26612
27280
  });
26613
- result = errorDocument("logout", 3, "credential_store_unavailable", true, "Unlock or repair the selected credential store and retry.");
27281
+ result = errorDocument("logout", 3, "credential_store_unavailable", false, CREDENTIAL_STORE_UNAVAILABLE_ACTION);
26614
27282
  }
26615
27283
  io.writeStdout(result.stdout);
26616
27284
  return result.exitCode;
@@ -26645,7 +27313,14 @@ var readSecretValueFromStandardInput = async (input, signal) => {
26645
27313
  value.fill(0);
26646
27314
  }
26647
27315
  };
26648
- var invalidCommandFailure = (command) => errorDocument(command, 2, "invalid_command", false, "Check 'ohmyhost --help'.");
27316
+ var invalidCommandFailure = (argv, environment) => {
27317
+ let commandPrefix = "ohmyhost";
27318
+ try {
27319
+ commandPrefix = resolveProductCliEnvironment(environment).commandPrefix;
27320
+ } catch {
27321
+ }
27322
+ return errorDocument(recognizedCommandLabel(argv), 2, "invalid_command", false, invalidCommandAction(argv, commandPrefix));
27323
+ };
26649
27324
  var resultDocument = (command, exitCode, status, body = {}) => ({
26650
27325
  exitCode,
26651
27326
  stdout: `${JSON.stringify({ version: 1, command, status, ...body })}
@@ -26691,6 +27366,7 @@ export {
26691
27366
  InvalidCommandError,
26692
27367
  recognizedCommandLabel,
26693
27368
  parseProductCliCommand,
27369
+ parseProjectDataChangePlan2 as parseProjectDataChangePlan,
26694
27370
  parsePaidDomainPlan,
26695
27371
  parsePaidDomain,
26696
27372
  parseCloudflareDnsAuthorization,
@@ -26721,6 +27397,11 @@ export {
26721
27397
  PublicResponseTooLargeError,
26722
27398
  PsqlUnavailableError,
26723
27399
  cliMetadataResult,
27400
+ invalidCommandAction,
27401
+ INTERNAL_ERROR_ACTION,
27402
+ CREDENTIAL_STORE_UNAVAILABLE_ACTION,
27403
+ endedLoginAction,
27404
+ refreshFailedAction,
26724
27405
  ProductCli,
26725
27406
  ACCOUNT_SENSITIVE_PROBLEMS,
26726
27407
  classifyCliFailure,