@amerged/ohmyhost-mcp 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +5 -2
  2. package/dist/{chunk-NLBFEH6H.js → chunk-EEFUMPKG.js} +25 -24
  3. package/dist/index.js +1 -1
  4. package/dist/stdio-main.js +734 -269
  5. package/package.json +1 -1
  6. package/src/apps/mcp/package.json +47 -0
  7. package/src/apps/mcp/{local-client.ts → src/local-client.ts} +69 -12
  8. package/src/apps/mcp/{local-server.ts → src/local-server.ts} +327 -145
  9. package/src/apps/product-cli/package.json +45 -0
  10. package/src/apps/product-cli/{bin.ts → src/bin.ts} +3 -0
  11. package/src/apps/product-cli/{cli.ts → src/cli.ts} +363 -87
  12. package/src/apps/product-cli/{command.ts → src/command.ts} +58 -11
  13. package/src/apps/product-cli/{composition.ts → src/composition.ts} +98 -21
  14. package/src/apps/product-cli/{index.ts → src/index.ts} +1 -0
  15. package/src/apps/product-cli/{product-api.ts → src/product-api.ts} +65 -19
  16. package/src/apps/product-cli/{repository-init.ts → src/repository-init.ts} +79 -29
  17. package/src/apps/product-cli/{runtime-contract.ts → src/runtime-contract.ts} +16 -309
  18. package/src/apps/product-cli/{user-token-file.ts → src/user-token-file.ts} +46 -35
  19. package/src/apps/product-cli/{workspace.ts → src/workspace.ts} +6 -0
  20. package/src/packages/agent-skills/package.json +24 -0
  21. package/src/packages/agent-skills/src/generated-skill-resources.ts +165 -0
  22. package/src/packages/agent-skills/{index.ts → src/index.ts} +1 -1
  23. package/src/packages/contracts/package.json +137 -0
  24. package/src/packages/contracts/src/browser-capabilities.ts +133 -0
  25. package/src/packages/contracts/src/customer-auth-admission.ts +36 -0
  26. package/src/packages/contracts/{database-access.ts → src/database-access.ts} +10 -2
  27. package/src/packages/contracts/{framework-admission.ts → src/framework-admission.ts} +88 -13
  28. package/src/packages/contracts/src/framework-build-script.mjs +22 -0
  29. package/src/packages/contracts/src/framework-build-script.ts +1 -0
  30. package/src/packages/contracts/{index.ts → src/index.ts} +17 -16
  31. package/src/packages/contracts/src/operation-data-change-result.ts +86 -0
  32. package/src/packages/contracts/{operation-deployment-result.ts → src/operation-deployment-result.ts} +2 -2
  33. package/src/packages/contracts/{operation-failure.ts → src/operation-failure.ts} +24 -12
  34. package/src/packages/contracts/src/problem-policy.ts +498 -0
  35. package/src/packages/contracts/src/project-data-changes.ts +90 -0
  36. package/src/packages/contracts/src/secret-names.ts +27 -0
  37. package/src/packages/sdk-ts/package.json +21 -0
  38. package/src/packages/sdk-ts/{generated → src/generated}/index.ts +15 -0
  39. package/src/packages/sdk-ts/{generated → src/generated}/sdk.gen.ts +114 -5
  40. package/src/packages/sdk-ts/{generated → src/generated}/types.gen.ts +209 -10
  41. package/src/packages/workos-auth-contracts/package.json +39 -0
  42. package/src/packages/agent-skills/generated-skill-resources.ts +0 -165
  43. /package/src/apps/mcp/{index.ts → src/index.ts} +0 -0
  44. /package/src/apps/mcp/{stdio-main.ts → src/stdio-main.ts} +0 -0
  45. /package/src/apps/product-cli/{credential-store.ts → src/credential-store.ts} +0 -0
  46. /package/src/apps/product-cli/{device-token-expiry.ts → src/device-token-expiry.ts} +0 -0
  47. /package/src/apps/product-cli/{file-credential-store.ts → src/file-credential-store.ts} +0 -0
  48. /package/src/apps/product-cli/{profile-store.ts → src/profile-store.ts} +0 -0
  49. /package/src/apps/product-cli/{project-link-store.ts → src/project-link-store.ts} +0 -0
  50. /package/src/apps/product-cli/{psql-session.ts → src/psql-session.ts} +0 -0
  51. /package/src/packages/contracts/{application-root.ts → src/application-root.ts} +0 -0
  52. /package/src/packages/contracts/{billing.ts → src/billing.ts} +0 -0
  53. /package/src/packages/contracts/{credit-pricing.ts → src/credit-pricing.ts} +0 -0
  54. /package/src/packages/contracts/{database-compute.ts → src/database-compute.ts} +0 -0
  55. /package/src/packages/contracts/{database-migration-admission.ts → src/database-migration-admission.ts} +0 -0
  56. /package/src/packages/contracts/{feedback.ts → src/feedback.ts} +0 -0
  57. /package/src/packages/contracts/{framework-family.ts → src/framework-family.ts} +0 -0
  58. /package/src/packages/contracts/{functions.ts → src/functions.ts} +0 -0
  59. /package/src/packages/contracts/{platform-origins.ts → src/platform-origins.ts} +0 -0
  60. /package/src/packages/contracts/{project-context.ts → src/project-context.ts} +0 -0
  61. /package/src/packages/contracts/{project-exports.ts → src/project-exports.ts} +0 -0
  62. /package/src/packages/contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  63. /package/src/packages/sdk-ts/{generated → src/generated}/client/client.gen.ts +0 -0
  64. /package/src/packages/sdk-ts/{generated → src/generated}/client/index.ts +0 -0
  65. /package/src/packages/sdk-ts/{generated → src/generated}/client/types.gen.ts +0 -0
  66. /package/src/packages/sdk-ts/{generated → src/generated}/client/utils.gen.ts +0 -0
  67. /package/src/packages/sdk-ts/{generated → src/generated}/client.gen.ts +0 -0
  68. /package/src/packages/sdk-ts/{generated → src/generated}/core/auth.gen.ts +0 -0
  69. /package/src/packages/sdk-ts/{generated → src/generated}/core/bodySerializer.gen.ts +0 -0
  70. /package/src/packages/sdk-ts/{generated → src/generated}/core/params.gen.ts +0 -0
  71. /package/src/packages/sdk-ts/{generated → src/generated}/core/pathSerializer.gen.ts +0 -0
  72. /package/src/packages/sdk-ts/{generated → src/generated}/core/queryKeySerializer.gen.ts +0 -0
  73. /package/src/packages/sdk-ts/{generated → src/generated}/core/serverSentEvents.gen.ts +0 -0
  74. /package/src/packages/sdk-ts/{generated → src/generated}/core/types.gen.ts +0 -0
  75. /package/src/packages/sdk-ts/{generated → src/generated}/core/utils.gen.ts +0 -0
  76. /package/src/packages/sdk-ts/{index.ts → src/index.ts} +0 -0
  77. /package/src/packages/sdk-ts/{managed-mail-response.ts → src/managed-mail-response.ts} +0 -0
  78. /package/src/packages/sdk-ts/{managed-mail.ts → src/managed-mail.ts} +0 -0
  79. /package/src/packages/workos-auth-contracts/{device-flow.ts → src/device-flow.ts} +0 -0
  80. /package/src/packages/workos-auth-contracts/{index.ts → src/index.ts} +0 -0
  81. /package/src/packages/workos-auth-contracts/{management-rpc.ts → src/management-rpc.ts} +0 -0
  82. /package/src/packages/workos-auth-contracts/{organizations.ts → src/organizations.ts} +0 -0
  83. /package/src/packages/workos-auth-contracts/{parsing.ts → src/parsing.ts} +0 -0
  84. /package/src/packages/workos-auth-contracts/{session-status.ts → src/session-status.ts} +0 -0
  85. /package/src/packages/workos-auth-contracts/{tokens.ts → src/tokens.ts} +0 -0
  86. /package/src/packages/workos-auth-contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  87. /package/src/packages/workos-auth-contracts/{webhooks.ts → src/webhooks.ts} +0 -0
@@ -7,9 +7,14 @@ import {
7
7
  type ApplicationRoot,
8
8
  } from "@ohmyhost/contracts/application-root";
9
9
  import { parseOhmyhostConfigYaml, type OhmyhostConfig } from "@ohmyhost/contracts";
10
+ import {
11
+ isAdmittedFrameworkBuildScript,
12
+ PINNED_BUN_VERSION,
13
+ } from "@ohmyhost/contracts/framework-build-script";
10
14
  import {
11
15
  classifyFrameworkVersion,
12
16
  FrameworkAdmissionError,
17
+ managedCustomerAuthAdmissionIssue,
13
18
  resolveFrameworkConfig,
14
19
  resolvePackageManager,
15
20
  type AdmittedFramework,
@@ -24,6 +29,7 @@ import {
24
29
  import {
25
30
  admitExpandOnlyMigrations,
26
31
  DatabaseMigrationAdmissionError,
32
+ type DatabaseMigrationAdmissionReason,
27
33
  } from "@ohmyhost/contracts/database-migration-admission";
28
34
 
29
35
  import cliPackage from "../package.json" with { type: "json" };
@@ -96,6 +102,8 @@ export interface RepositoryInitBlocker {
96
102
  | "database_driver_unsupported"
97
103
  | "build_script_missing"
98
104
  | "better_auth_version_unsupported"
105
+ | "managed_auth_mail_required"
106
+ | "managed_auth_database_required"
99
107
  | "framework_ambiguous"
100
108
  | "framework_unsupported"
101
109
  | "framework_version_unsupported"
@@ -184,6 +192,11 @@ export interface RepositoryInitResult {
184
192
  bindings: readonly string[];
185
193
  packages: RepositoryInitRuntimePackages;
186
194
  }> | null;
195
+ /** What server code of any framework installs and reads once a capability is enabled. */
196
+ readonly runtime: Readonly<{
197
+ bindings: readonly string[];
198
+ packages: RepositoryInitRuntimePackages;
199
+ }> | null;
187
200
  }
188
201
 
189
202
  export class RepositoryInitError extends Error {
@@ -447,6 +460,8 @@ export async function initializeRepository(
447
460
  ...runtimeContract(capabilities),
448
461
  })
449
462
  : null,
463
+ // Next.js and TanStack Start have neither a companion nor a Worker module, and read it here.
464
+ runtime: Object.values(capabilities).some(Boolean) ? runtimeContract(capabilities) : null,
450
465
  });
451
466
  }
452
467
 
@@ -480,7 +495,7 @@ async function cronBlockers(
480
495
  {
481
496
  code: "scheduled_functions_runtime_unsupported",
482
497
  message:
483
- "Declared crons run on the edge runtime of a Vite API companion, Next.js or TanStack Start application, or on the functions runtime.",
498
+ "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.",
484
499
  },
485
500
  ];
486
501
  }
@@ -585,20 +600,31 @@ function migrationFilenameBlockers(
585
600
  const invalid = files
586
601
  .filter((path) => path.startsWith(prefix))
587
602
  .map((path) => path.slice(prefix.length))
588
- .some(
603
+ .find(
589
604
  (path) => !/^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u.test(path),
590
605
  );
591
- return invalid
592
- ? [
606
+ return invalid === undefined
607
+ ? []
608
+ : [
593
609
  {
594
610
  code: "migration_filename_noncanonical",
595
- message:
596
- "Rename every PostgreSQL migration to YYYYMMDDHHMMSS_name.sql before initialization.",
611
+ message: `Migration file '${invalid}' 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.`,
597
612
  },
598
- ]
599
- : [];
613
+ ];
600
614
  }
601
615
 
616
+ /** What each rejection reason of the expand-only migration gate means, and how to satisfy it. */
617
+ const MIGRATION_REJECTION: Readonly<Record<DatabaseMigrationAdmissionReason, string>> = {
618
+ destructive:
619
+ "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'.",
620
+ provider_specific:
621
+ "It mentions supabase, CREATE POLICY, ROW LEVEL SECURITY, the auth. or storage. schema, or the anon, authenticated or service_role role; comments count.",
622
+ unsupported:
623
+ "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.",
624
+ invalid:
625
+ "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).",
626
+ };
627
+
602
628
  async function migrationSqlBlockers(
603
629
  applicationDirectory: string,
604
630
  files: readonly string[],
@@ -634,7 +660,7 @@ async function migrationSqlBlockers(
634
660
  if (!(error instanceof DatabaseMigrationAdmissionError)) throw error;
635
661
  blockers.push({
636
662
  code: "migration_sql_not_admitted",
637
- message: `PostgreSQL migration '${migration.path}' is not admitted (${error.reason}).`,
663
+ 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.`,
638
664
  });
639
665
  }
640
666
  }
@@ -647,7 +673,7 @@ async function migrationSqlBlockers(
647
673
  return [
648
674
  {
649
675
  code: "migration_sql_not_admitted",
650
- message: `PostgreSQL migration set is not admitted (${error.reason}).`,
676
+ message: `The migration set is not admitted (${error.reason}): ${MIGRATION_REJECTION[error.reason]}`,
651
677
  },
652
678
  ];
653
679
  }
@@ -664,23 +690,24 @@ async function readExistingConfiguration(
664
690
  if (!information.isFile() || information.size > MAXIMUM_CONFIGURATION_BYTES) {
665
691
  throw new RepositoryInitError(
666
692
  "repository_configuration_invalid",
667
- "Existing ohmyhost.yaml must be one bounded regular file.",
693
+ "ohmyhost.yaml must be one regular file of at most 64 KiB",
668
694
  );
669
695
  }
670
696
  const configuration = await readFile(configurationPath, "utf8");
671
697
  let parsed;
672
698
  try {
673
699
  parsed = parseOhmyhostConfigYaml(configuration);
674
- } catch {
700
+ } catch (error) {
701
+ // The parser names the offending key; dropping it left the agent nothing to fix.
675
702
  throw new RepositoryInitError(
676
703
  "repository_configuration_invalid",
677
- "Existing ohmyhost.yaml is invalid.",
704
+ error instanceof TypeError ? error.message : "ohmyhost.yaml is invalid",
678
705
  );
679
706
  }
680
707
  if (parsed.applicationRoot !== applicationRoot) {
681
708
  throw new RepositoryInitError(
682
709
  "repository_configuration_root_mismatch",
683
- "Existing ohmyhost.yaml must select the detected application root.",
710
+ `ohmyhost.yaml sets applicationRoot to '${parsed.applicationRoot}', but init selected '${applicationRoot}'. Run 'ohmyhost init --root ${parsed.applicationRoot} --dry-run --json', or correct applicationRoot.`,
684
711
  );
685
712
  }
686
713
  return configuration;
@@ -858,10 +885,18 @@ function hasDirectSafeBuildCommand(
858
885
  ): boolean {
859
886
  const scripts = packageJson["scripts"];
860
887
  if (!isRecord(scripts) || typeof scripts["build"] !== "string") return false;
861
- const command = scripts["build"];
862
- if (command.includes("..") || /(?:^|\s)cd(?:\s|$)/u.test(command)) return false;
863
- if (framework === "nextjs") return command === "next build" || command === "next build --webpack";
864
- return command === "vite build" || command === "vite build && tsc --noEmit";
888
+ return (
889
+ (framework === "nextjs" || framework === "vite" || framework === "tanstack-start") &&
890
+ isAdmittedFrameworkBuildScript(framework, scripts["build"])
891
+ );
892
+ }
893
+
894
+ /** The package.json build-script blocker, quoting at most 200 characters of the rejected script. */
895
+ function buildScriptMessage(framework: RepositoryFramework, command: string): string {
896
+ const quoted = command.slice(0, 200);
897
+ if (framework === "nextjs")
898
+ 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.`;
899
+ 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.`;
865
900
  }
866
901
 
867
902
  async function assertRepositoryRoot(root: string): Promise<void> {
@@ -1049,6 +1084,13 @@ function frameworkConfigurations(files: readonly string[]): FrameworkConfigurati
1049
1084
  });
1050
1085
  }
1051
1086
 
1087
+ /** The package.json dependency that carries each admitted framework's version. */
1088
+ const FRAMEWORK_PACKAGE: Readonly<Record<AdmittedFramework, string>> = {
1089
+ nextjs: "next",
1090
+ "tanstack-start": "@tanstack/react-start",
1091
+ vite: "vite",
1092
+ };
1093
+
1052
1094
  function frameworkCompatibility(
1053
1095
  packageJson: Record<string, unknown>,
1054
1096
  framework: RepositoryFramework,
@@ -1056,13 +1098,13 @@ function frameworkCompatibility(
1056
1098
  const targets: readonly Readonly<{ framework: AdmittedFramework; packageName: string }>[] =
1057
1099
  framework === "tanstack-start"
1058
1100
  ? [
1059
- { framework: "tanstack-start", packageName: "@tanstack/react-start" },
1060
- { framework: "vite", packageName: "vite" },
1101
+ { framework: "tanstack-start", packageName: FRAMEWORK_PACKAGE["tanstack-start"] },
1102
+ { framework: "vite", packageName: FRAMEWORK_PACKAGE.vite },
1061
1103
  ]
1062
1104
  : framework === "nextjs"
1063
- ? [{ framework: "nextjs", packageName: "next" }]
1105
+ ? [{ framework: "nextjs", packageName: FRAMEWORK_PACKAGE.nextjs }]
1064
1106
  : framework === "vite"
1065
- ? [{ framework: "vite", packageName: "vite" }]
1107
+ ? [{ framework: "vite", packageName: FRAMEWORK_PACKAGE.vite }]
1066
1108
  : [];
1067
1109
  const frameworks = targets.map(({ framework: targetFramework, packageName }) => {
1068
1110
  const version = dependencyVersion(packageJson, packageName) ?? "";
@@ -1132,7 +1174,7 @@ function repositoryBlockers(
1132
1174
  blockers.push({
1133
1175
  code: "framework_ambiguous",
1134
1176
  message:
1135
- "TanStack Start must explicitly enable static prerendering or the complete Cloudflare edge contract.",
1177
+ '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.',
1136
1178
  });
1137
1179
  }
1138
1180
  const relevantAmbiguousConfigs = frameworkConfigs.ambiguous.filter((config) =>
@@ -1156,7 +1198,9 @@ function repositoryBlockers(
1156
1198
  blockers.push({
1157
1199
  code: "package_manager_unpinned",
1158
1200
  message:
1159
- "Declare one exact npm, pnpm, Yarn, or Bun packageManager version and commit its matching lockfile.",
1201
+ packageManagerAdmission.reason === "package_manager_version_unsupported"
1202
+ ? `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.`
1203
+ : "Declare one exact npm, pnpm, Yarn, or Bun packageManager version and commit its matching lockfile.",
1160
1204
  });
1161
1205
  } else if (
1162
1206
  existingConfiguration !== null &&
@@ -1166,17 +1210,21 @@ function repositoryBlockers(
1166
1210
  ) {
1167
1211
  blockers.push({
1168
1212
  code: "build_command_unsupported",
1169
- message: "Use the admitted package manager's secure install and direct build commands.",
1213
+ message: `ohmyhost.yaml build.install must be exactly "${packageManagerAdmission.installCommand}"${
1214
+ "command" in existingConfiguration.build
1215
+ ? ` and build.command "${packageManagerAdmission.buildCommand}"`
1216
+ : ""
1217
+ } for the packageManager in package.json. Set them, then commit and push.`,
1170
1218
  });
1171
1219
  }
1172
1220
  if (compatibility.classification === "unsupported" && compatibility.frameworks.length > 0) {
1173
1221
  const unsupported = compatibility.frameworks
1174
1222
  .filter((entry) => entry.classification === "unsupported")
1175
- .map((entry) => `${entry.framework}@${entry.version || "missing"}`)
1223
+ .map((entry) => `${FRAMEWORK_PACKAGE[entry.framework]}@${entry.version || "missing"}`)
1176
1224
  .join(", ");
1177
1225
  blockers.push({
1178
1226
  code: "framework_version_unsupported",
1179
- message: `Framework version is outside the admitted POC window: ${unsupported}.`,
1227
+ message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.x or 16.0.0–16.3.4, vite 5.4.0–8.2.2, @tanstack/react-start 1.168.26–1.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.`,
1180
1228
  });
1181
1229
  }
1182
1230
  const scripts = packageJson["scripts"];
@@ -1192,10 +1240,12 @@ function repositoryBlockers(
1192
1240
  } else if (!hasDirectSafeBuildCommand(packageJson, framework)) {
1193
1241
  blockers.push({
1194
1242
  code: "build_command_unsupported",
1195
- message: "Use a direct framework build command without traversal or directory changes.",
1243
+ message: buildScriptMessage(framework, scripts["build"]),
1196
1244
  });
1197
1245
  }
1198
1246
  const betterAuthVersion = dependencyVersion(packageJson, "better-auth");
1247
+ const managedMailIssue = managedCustomerAuthAdmissionIssue(existingConfiguration);
1248
+ if (managedMailIssue !== null) blockers.push(managedMailIssue);
1199
1249
  if (
1200
1250
  existingConfiguration?.auth?.provider === "better-auth" &&
1201
1251
  betterAuthVersion !== null &&
@@ -1229,7 +1279,7 @@ function repositoryBlockers(
1229
1279
  blockers.push({
1230
1280
  code: "next_adapter_unconfigured",
1231
1281
  message:
1232
- "Declare direct next build, exact Next/React versions, and one supported Next config; ohmyhost supplies the OpenNext build overlay.",
1282
+ '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–16.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.',
1233
1283
  });
1234
1284
  }
1235
1285
  }
@@ -1,4 +1,8 @@
1
1
  import { parseFeedbackSubmissionDetail } from "@ohmyhost/contracts/feedback";
2
+ import {
3
+ parseProjectDataChangePlan as readProjectDataChangePlan,
4
+ type ProjectDataChangePlan,
5
+ } from "@ohmyhost/contracts/project-data-changes";
2
6
  import { parseFrameworkConversionDetail } from "@ohmyhost/contracts/framework-admission";
3
7
  import {
4
8
  parseOperationDeploymentProgress,
@@ -8,7 +12,6 @@ import type {
8
12
  GithubConnectionAuthorization,
9
13
  GithubOrganizationConnection,
10
14
  GithubOrganizationConnectionStatus,
11
- ProblemDetails,
12
15
  OrganizationCredits,
13
16
  ProjectCreditBudget,
14
17
  } from "@ohmyhost/sdk-ts";
@@ -22,314 +25,8 @@ import {
22
25
  } from "@ohmyhost/contracts/operation-failure";
23
26
  export type { PublicOperationFailure } from "@ohmyhost/contracts/operation-failure";
24
27
 
25
- /** Exported for the documentation build, which publishes one page per code. */
26
- export const PROBLEM_POLICY = {
27
- cloudflare_zone_not_bound: {
28
- status: 409,
29
- retryable: false,
30
- action:
31
- "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.",
32
- },
33
- github_connection_required: {
34
- status: 409,
35
- retryable: false,
36
- action:
37
- "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.",
38
- },
39
- github_connection_revoked: {
40
- status: 409,
41
- retryable: false,
42
- action:
43
- "Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' to restore this workspace's GitHub connection, then repeat the original source link.",
44
- },
45
- repository_not_installed: {
46
- status: 403,
47
- retryable: false,
48
- action:
49
- "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.",
50
- },
51
- database_write_pending: {
52
- status: 409,
53
- retryable: false,
54
- action: "Poll the original database-write operation before transfer or deletion.",
55
- },
56
- cloudflare_authorization_closed: {
57
- status: 409,
58
- retryable: false,
59
- action:
60
- "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.",
61
- },
62
- billing_recharge_conflict: {
63
- status: 409,
64
- retryable: false,
65
- action:
66
- "Read billing recharge get and the current revision. Preserve the original key after uncertainty; do not start a checkout or automatically re-enable charges.",
67
- },
68
- billing_purchase_conflict: {
69
- status: 409,
70
- retryable: false,
71
- action:
72
- "Read the original checkout and use billing portal to manage the existing subscription. Do not create another purchase after uncertainty.",
73
- },
74
- compute_performance_paid_required: {
75
- status: 403,
76
- retryable: false,
77
- action: "Check Paid service and usable credits or grace, or choose standard compute.",
78
- },
79
- compute_performance_unavailable: {
80
- status: 503,
81
- retryable: false,
82
- action:
83
- "Choose standard compute or report inactive performance pricing through feedback. Do not repeatedly resubmit.",
84
- },
85
- compute_change_pending: {
86
- status: 409,
87
- retryable: false,
88
- action:
89
- "Poll the existing operation after 60 seconds. Do not submit another size change, deletion or transfer while it runs.",
90
- },
91
- compute_change_conflict: {
92
- status: 409,
93
- retryable: false,
94
- action:
95
- "Read the original operation and current database compute. Preserve data and report unresolved conflicts through feedback with the operation ID.",
96
- },
97
- database_access_limit: {
98
- status: 409,
99
- retryable: false,
100
- action:
101
- "List the project's database credentials and revoke one you no longer need, then request access again.",
102
- },
103
- project_notes_conflict: {
104
- status: 409,
105
- retryable: false,
106
- action:
107
- "Read project context, merge the current notes and retry with its version and a new idempotency key.",
108
- },
109
- invalid_request: { status: 400, retryable: false, action: "Check the request and retry." },
110
- unauthenticated: {
111
- status: 401,
112
- retryable: false,
113
- action:
114
- "Check OHMYHOST_TOKEN and the selected environment. Replace invalid tokens, or unset OHMYHOST_TOKEN and run 'ohmyhost login'.",
115
- },
116
- forbidden: { status: 403, retryable: false, action: "Check organization access." },
117
- organization_required: {
118
- status: 409,
119
- retryable: false,
120
- action:
121
- "Run 'ohmyhost organization list --json' and select one with 'ohmyhost organization use --organization ULID --json', or create the first workspace.",
122
- },
123
- resource_not_found: {
124
- status: 404,
125
- retryable: false,
126
- action:
127
- "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.",
128
- },
129
- idempotency_key_reused: {
130
- status: 409,
131
- retryable: false,
132
- action: "Use a new idempotency key for a different request.",
133
- },
134
- project_handle_unavailable: {
135
- status: 503,
136
- retryable: true,
137
- action: "Retry project creation with the same idempotency key.",
138
- },
139
- project_handle_taken: {
140
- status: 409,
141
- retryable: false,
142
- action:
143
- "Run 'ohmyhost project handle check' for a free address, then rename to one of the alternatives it returns.",
144
- },
145
- project_handle_invalid: {
146
- status: 409,
147
- retryable: false,
148
- action:
149
- "Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words.",
150
- },
151
- project_handle_unchanged: {
152
- status: 409,
153
- retryable: false,
154
- action: "The project already answers on this address; no rename is needed.",
155
- },
156
- project_rename_blocked: {
157
- status: 409,
158
- retryable: true,
159
- action:
160
- "Wait for the running operation to finish, read the project again, then rename with the new ETag.",
161
- },
162
- project_identity_unavailable: {
163
- status: 503,
164
- retryable: true,
165
- action: "Retry project creation with the same idempotency key.",
166
- },
167
- deployment_plan_expired: {
168
- status: 409,
169
- retryable: false,
170
- action: "Run 'ohmyhost plan' again, then deploy the new plan.",
171
- },
172
- deployment_plan_incompatible: {
173
- status: 409,
174
- retryable: false,
175
- action: "Run 'ohmyhost plan' again with the current source revision.",
176
- },
177
- confirmation_expired: {
178
- status: 409,
179
- retryable: false,
180
- action: "Plan the destructive action again before confirming it.",
181
- },
182
- confirmation_invalid: {
183
- status: 409,
184
- retryable: false,
185
- action: "Use the confirmation token from a fresh matching plan.",
186
- },
187
- etag_mismatch: {
188
- status: 409,
189
- retryable: false,
190
- action: "Refresh the resource, plan the action again, and retry.",
191
- },
192
- promotion_source_stale: {
193
- status: 409,
194
- retryable: false,
195
- action: "Read the current dev deployment, then plan promotion again.",
196
- },
197
- promotion_target_stale: {
198
- status: 409,
199
- retryable: false,
200
- action: "Read the current prod status, then plan promotion again.",
201
- },
202
- promotion_invalid_target: {
203
- status: 409,
204
- retryable: false,
205
- action: "Select the current succeeded dev deployment and target prod.",
206
- },
207
- mail_capacity_unavailable: {
208
- status: 503,
209
- retryable: true,
210
- action:
211
- "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.",
212
- },
213
- mail_domain_conflict: {
214
- status: 409,
215
- retryable: false,
216
- action: "Read and use the sender domain already configured for this project.",
217
- },
218
- mail_domain_required: {
219
- status: 409,
220
- retryable: false,
221
- action:
222
- "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.",
223
- },
224
- storage_jurisdiction_conflict: {
225
- status: 409,
226
- retryable: false,
227
- action:
228
- "Set storage.jurisdiction in ohmyhost.yaml to the project's hosting region shown by project status, then plan again.",
229
- },
230
- shared_data_requires_promotion: {
231
- status: 409,
232
- retryable: false,
233
- action:
234
- "Plan and deploy this commit to Dev, then run 'ohmyhost deployment promote plan' to reach Prod.",
235
- },
236
- production_deployment_required: {
237
- status: 409,
238
- retryable: false,
239
- action:
240
- "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.",
241
- },
242
- framework_conversion_required: {
243
- status: 409,
244
- retryable: false,
245
- action:
246
- "Run 'ohmyhost init', apply the matching ohmyhost framework skill, then plan the new commit.",
247
- },
248
- repository_configuration_missing: {
249
- status: 409,
250
- retryable: false,
251
- action:
252
- "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.",
253
- },
254
- migration_filename_noncanonical: {
255
- status: 409,
256
- retryable: false,
257
- action:
258
- "Rename every migration to YYYYMMDDHHMMSS_name.sql, commit the change, then plan again.",
259
- },
260
- environment_secret_mutation_blocked: {
261
- status: 409,
262
- retryable: true,
263
- action: "Wait for the active deployment or rollback to finish, then retry unchanged.",
264
- },
265
- workers_runtime_incompatible: {
266
- status: 409,
267
- retryable: false,
268
- action: "Remove or replace the reported module, commit the change, then create a new plan.",
269
- },
270
- payload_too_large: { status: 413, retryable: false, action: "Reduce the request size." },
271
- project_export_not_ready: {
272
- status: 409,
273
- retryable: false,
274
- action: "Finish database provisioning, then request the export again. No backup was accepted.",
275
- },
276
- powered_by_flag_required: {
277
- status: 409,
278
- retryable: false,
279
- action:
280
- "Keep the flag on, remove the custom domain with 'ohmyhost domain paid delete', or buy Paid, then switch the flag off.",
281
- },
282
- rate_limited: { status: 429, retryable: true, action: "Retry after the server delay." },
283
- insufficient_organization_credits: {
284
- status: 402,
285
- retryable: false,
286
- action:
287
- "Add organization credits or wait for reservations to settle, then retry the same request.",
288
- },
289
- project_budget_exceeded: {
290
- status: 402,
291
- retryable: false,
292
- action: "Raise the project's stop budget or select continue, then retry the same request.",
293
- },
294
- paid_plan_required: {
295
- status: 402,
296
- retryable: false,
297
- action:
298
- "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.",
299
- },
300
- interactive_login_required: {
301
- status: 403,
302
- retryable: false,
303
- action:
304
- "Unset OHMYHOST_TOKEN and run ohmyhost login before managing tokens. Existing token files remain unchanged.",
305
- },
306
- api_key_creation_uncertain: {
307
- status: 503,
308
- retryable: true,
309
- action:
310
- "Repeat the original token name and Idempotency-Key to observe its result; do not blindly create another key.",
311
- },
312
- api_key_permissions_unavailable: {
313
- status: 503,
314
- retryable: false,
315
- action:
316
- "Report the platform user-key permission configuration through feedback. Repeated login or creation will not fix it.",
317
- },
318
- reconciliation_exhausted: {
319
- status: 409,
320
- retryable: false,
321
- action:
322
- "Stop retries and submit feedback with this operation ID. A platform recovery review is required.",
323
- },
324
- service_unavailable: { status: 503, retryable: true, action: "Retry later." },
325
- } as const satisfies Readonly<
326
- Record<
327
- ProblemDetails["code"],
328
- { readonly status: number; readonly retryable: boolean; readonly action: string }
329
- >
330
- >;
331
-
332
- type ProblemCode = keyof typeof PROBLEM_POLICY;
28
+ import { PROBLEM_POLICY, type ProblemCode } from "@ohmyhost/contracts/problem-policy";
29
+ export { PROBLEM_POLICY } from "@ohmyhost/contracts/problem-policy";
333
30
 
334
31
  export class ResponseContractError extends Error {
335
32
  public constructor() {
@@ -338,8 +35,17 @@ export class ResponseContractError extends Error {
338
35
  }
339
36
  }
340
37
 
38
+ export function parseProjectDataChangePlan(value: unknown): ProjectDataChangePlan {
39
+ try {
40
+ return readProjectDataChangePlan(value);
41
+ } catch {
42
+ throw new ResponseContractError();
43
+ }
44
+ }
45
+
341
46
  export interface SafeProblem {
342
47
  readonly requestId: string;
48
+ readonly message?: string;
343
49
  /** The error's documentation page, when the server names one on docs.ohmyho.st. */
344
50
  readonly docsUrl?: string;
345
51
  readonly retryAfterSeconds?: number;
@@ -1604,6 +1310,7 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
1604
1310
  const docsUrl = String(input["type"]);
1605
1311
  return {
1606
1312
  requestId: input["request_id"],
1313
+ ...(input["detail"] === undefined ? {} : { message: String(input["detail"]) }),
1607
1314
  ...(docsUrl.startsWith("https://docs.ohmyho.st/errors/") ? { docsUrl } : {}),
1608
1315
  code: code as ProblemCode,
1609
1316
  retryable: policy.retryable,