@amerged/ohmyhost-mcp 0.1.10

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 (72) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +11 -0
  4. package/THIRD_PARTY_NOTICES.md +474 -0
  5. package/dist/chunk-7MV2EVXV.js +30779 -0
  6. package/dist/index.js +9 -0
  7. package/dist/stdio-main.js +5607 -0
  8. package/package.json +32 -0
  9. package/src/apps/mcp/index.ts +30 -0
  10. package/src/apps/mcp/local-client.ts +891 -0
  11. package/src/apps/mcp/local-server.ts +1512 -0
  12. package/src/apps/mcp/stdio-main.ts +26 -0
  13. package/src/apps/product-cli/bin.ts +24 -0
  14. package/src/apps/product-cli/cli.ts +2271 -0
  15. package/src/apps/product-cli/command.ts +1811 -0
  16. package/src/apps/product-cli/composition.ts +823 -0
  17. package/src/apps/product-cli/credential-store.ts +149 -0
  18. package/src/apps/product-cli/device-token-expiry.ts +89 -0
  19. package/src/apps/product-cli/file-credential-store.ts +166 -0
  20. package/src/apps/product-cli/index.ts +23 -0
  21. package/src/apps/product-cli/product-api.ts +1082 -0
  22. package/src/apps/product-cli/project-link-store.ts +30 -0
  23. package/src/apps/product-cli/psql-session.ts +29 -0
  24. package/src/apps/product-cli/repository-init.ts +1622 -0
  25. package/src/apps/product-cli/runtime-contract.ts +1747 -0
  26. package/src/apps/product-cli/user-token-file.ts +214 -0
  27. package/src/apps/product-cli/workspace.ts +92 -0
  28. package/src/packages/agent-skills/generated-skill-resources.ts +147 -0
  29. package/src/packages/agent-skills/index.ts +37 -0
  30. package/src/packages/contracts/application-root.ts +80 -0
  31. package/src/packages/contracts/billing.ts +172 -0
  32. package/src/packages/contracts/credit-pricing.ts +214 -0
  33. package/src/packages/contracts/database-access.ts +257 -0
  34. package/src/packages/contracts/database-compute.ts +72 -0
  35. package/src/packages/contracts/database-migration-admission.ts +235 -0
  36. package/src/packages/contracts/feedback.ts +96 -0
  37. package/src/packages/contracts/framework-admission.ts +280 -0
  38. package/src/packages/contracts/framework-family.ts +18 -0
  39. package/src/packages/contracts/functions.ts +76 -0
  40. package/src/packages/contracts/index.ts +245 -0
  41. package/src/packages/contracts/operation-deployment-result.ts +165 -0
  42. package/src/packages/contracts/operation-failure.ts +150 -0
  43. package/src/packages/contracts/platform-origins.ts +5 -0
  44. package/src/packages/contracts/project-context.ts +59 -0
  45. package/src/packages/contracts/project-exports.ts +102 -0
  46. package/src/packages/contracts/user-api-keys.ts +98 -0
  47. package/src/packages/sdk-ts/generated/client/client.gen.ts +277 -0
  48. package/src/packages/sdk-ts/generated/client/index.ts +27 -0
  49. package/src/packages/sdk-ts/generated/client/types.gen.ts +217 -0
  50. package/src/packages/sdk-ts/generated/client/utils.gen.ts +316 -0
  51. package/src/packages/sdk-ts/generated/client.gen.ts +24 -0
  52. package/src/packages/sdk-ts/generated/core/auth.gen.ts +48 -0
  53. package/src/packages/sdk-ts/generated/core/bodySerializer.gen.ts +82 -0
  54. package/src/packages/sdk-ts/generated/core/params.gen.ts +178 -0
  55. package/src/packages/sdk-ts/generated/core/pathSerializer.gen.ts +171 -0
  56. package/src/packages/sdk-ts/generated/core/queryKeySerializer.gen.ts +117 -0
  57. package/src/packages/sdk-ts/generated/core/serverSentEvents.gen.ts +242 -0
  58. package/src/packages/sdk-ts/generated/core/types.gen.ts +114 -0
  59. package/src/packages/sdk-ts/generated/core/utils.gen.ts +140 -0
  60. package/src/packages/sdk-ts/generated/index.ts +578 -0
  61. package/src/packages/sdk-ts/generated/sdk.gen.ts +3440 -0
  62. package/src/packages/sdk-ts/generated/types.gen.ts +5790 -0
  63. package/src/packages/sdk-ts/index.ts +7 -0
  64. package/src/packages/workos-auth-contracts/device-flow.ts +271 -0
  65. package/src/packages/workos-auth-contracts/index.ts +8 -0
  66. package/src/packages/workos-auth-contracts/management-rpc.ts +379 -0
  67. package/src/packages/workos-auth-contracts/organizations.ts +113 -0
  68. package/src/packages/workos-auth-contracts/parsing.ts +75 -0
  69. package/src/packages/workos-auth-contracts/session-status.ts +53 -0
  70. package/src/packages/workos-auth-contracts/tokens.ts +216 -0
  71. package/src/packages/workos-auth-contracts/user-api-keys.ts +85 -0
  72. package/src/packages/workos-auth-contracts/webhooks.ts +127 -0
@@ -0,0 +1,72 @@
1
+ export interface DatabaseCompute {
2
+ readonly project_id: string;
3
+ readonly environment: "dev" | "prod";
4
+ readonly data_mode: "shared" | "isolated";
5
+ readonly observed_at: string;
6
+ readonly database: null | {
7
+ readonly min_cu: number;
8
+ readonly max_cu: number;
9
+ readonly min_memory_gb: number;
10
+ readonly max_memory_gb: number;
11
+ readonly suspend_timeout_seconds: number;
12
+ readonly state: "init" | "active" | "idle";
13
+ readonly pending_state: "init" | "active" | "idle" | null;
14
+ readonly disabled: boolean;
15
+ readonly region: string;
16
+ };
17
+ }
18
+ export function assertDatabaseCompute(value: unknown): asserts value is DatabaseCompute {
19
+ if (
20
+ !record(value) ||
21
+ !exact(value, ["data_mode", "database", "environment", "observed_at", "project_id"]) ||
22
+ typeof value["project_id"] !== "string" ||
23
+ !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(value["project_id"]) ||
24
+ !["dev", "prod"].includes(String(value["environment"])) ||
25
+ !["shared", "isolated"].includes(String(value["data_mode"])) ||
26
+ typeof value["observed_at"] !== "string" ||
27
+ !Number.isFinite(Date.parse(value["observed_at"])) ||
28
+ new Date(value["observed_at"]).toISOString() !== value["observed_at"]
29
+ )
30
+ throw new TypeError("Invalid database compute report");
31
+ const db = value["database"];
32
+ if (db === null) return;
33
+ if (
34
+ !record(db) ||
35
+ !exact(db, [
36
+ "disabled",
37
+ "max_cu",
38
+ "max_memory_gb",
39
+ "min_cu",
40
+ "min_memory_gb",
41
+ "pending_state",
42
+ "region",
43
+ "state",
44
+ "suspend_timeout_seconds",
45
+ ]) ||
46
+ typeof db["min_cu"] !== "number" ||
47
+ typeof db["max_cu"] !== "number" ||
48
+ !Number.isFinite(db["min_cu"]) ||
49
+ !Number.isFinite(db["max_cu"]) ||
50
+ db["min_cu"] < 0.25 ||
51
+ db["max_cu"] < db["min_cu"] ||
52
+ db["max_cu"] > Number.MAX_SAFE_INTEGER / 4096 ||
53
+ db["min_memory_gb"] !== db["min_cu"] * 4 ||
54
+ db["max_memory_gb"] !== db["max_cu"] * 4 ||
55
+ !Number.isSafeInteger(db["suspend_timeout_seconds"]) ||
56
+ Number(db["suspend_timeout_seconds"]) < -1 ||
57
+ Number(db["suspend_timeout_seconds"]) > 604800 ||
58
+ !["init", "active", "idle"].includes(String(db["state"])) ||
59
+ (db["pending_state"] !== null &&
60
+ !["init", "active", "idle"].includes(String(db["pending_state"]))) ||
61
+ typeof db["disabled"] !== "boolean" ||
62
+ typeof db["region"] !== "string" ||
63
+ !/^aws-[a-z0-9]+(?:-[a-z0-9]+)*-[0-9]+$/u.test(db["region"])
64
+ )
65
+ throw new TypeError("Invalid database compute configuration");
66
+ }
67
+ function record(v: unknown): v is Record<string, unknown> {
68
+ return typeof v === "object" && v !== null && !Array.isArray(v);
69
+ }
70
+ function exact(v: Record<string, unknown>, keys: readonly string[]) {
71
+ return Object.keys(v).sort().join(",") === keys.join(",");
72
+ }
@@ -0,0 +1,235 @@
1
+ const MAX_MIGRATION_COUNT = 128;
2
+ const MAX_MIGRATION_BYTES = 256 * 1024;
3
+ const MAX_MIGRATION_TOTAL_BYTES = 2 * 1024 * 1024;
4
+ const MIGRATION_PATH = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u;
5
+
6
+ export type DatabaseMigrationAdmissionReason =
7
+ | "destructive"
8
+ | "invalid"
9
+ | "provider_specific"
10
+ | "unsupported";
11
+
12
+ export class DatabaseMigrationAdmissionError extends Error {
13
+ public readonly code = "database_migration_not_admitted";
14
+
15
+ public constructor(public readonly reason: DatabaseMigrationAdmissionReason) {
16
+ super("Database migration is not admitted");
17
+ this.name = "DatabaseMigrationAdmissionError";
18
+ }
19
+ }
20
+
21
+ export interface DatabaseMigrationInput {
22
+ readonly path: string;
23
+ readonly body: Uint8Array;
24
+ }
25
+
26
+ export interface DatabaseMigrationAdmission {
27
+ readonly files: readonly Readonly<{ path: string; statementCount: number }>[];
28
+ readonly statementCount: number;
29
+ }
30
+
31
+ export function admitExpandOnlyMigrations(
32
+ migrations: readonly DatabaseMigrationInput[],
33
+ ): DatabaseMigrationAdmission {
34
+ if (
35
+ !Array.isArray(migrations) ||
36
+ migrations.length === 0 ||
37
+ migrations.length > MAX_MIGRATION_COUNT
38
+ ) {
39
+ throw new DatabaseMigrationAdmissionError("invalid");
40
+ }
41
+ let previousPath = "";
42
+ let totalBytes = 0;
43
+ let statementCount = 0;
44
+ const files = migrations.map((migration) => {
45
+ if (
46
+ typeof migration !== "object" ||
47
+ migration === null ||
48
+ !MIGRATION_PATH.test(migration.path) ||
49
+ migration.path <= previousPath ||
50
+ !(migration.body instanceof Uint8Array) ||
51
+ migration.body.byteLength === 0 ||
52
+ migration.body.byteLength > MAX_MIGRATION_BYTES
53
+ ) {
54
+ throw new DatabaseMigrationAdmissionError("invalid");
55
+ }
56
+ previousPath = migration.path;
57
+ totalBytes += migration.body.byteLength;
58
+ if (totalBytes > MAX_MIGRATION_TOTAL_BYTES || migration.body.includes(0)) {
59
+ throw new DatabaseMigrationAdmissionError("invalid");
60
+ }
61
+ let sql: string;
62
+ try {
63
+ sql = new TextDecoder("utf-8", { fatal: true }).decode(migration.body);
64
+ } catch {
65
+ throw new DatabaseMigrationAdmissionError("invalid");
66
+ }
67
+ if (sql.length === 0 || sql.includes("\r") || sql.charCodeAt(0) === 0xfeff) {
68
+ throw new DatabaseMigrationAdmissionError("invalid");
69
+ }
70
+ if (/\bohmyhost\b/iu.test(sql)) throw new DatabaseMigrationAdmissionError("unsupported");
71
+ rejectProviderSpecificSql(sql);
72
+ const statements = splitSqlStatements(sql);
73
+ if (statements.length === 0) throw new DatabaseMigrationAdmissionError("invalid");
74
+ for (const statement of statements) admitStatement(statement);
75
+ statementCount += statements.length;
76
+ return Object.freeze({ path: migration.path, statementCount: statements.length });
77
+ });
78
+ return Object.freeze({ files: Object.freeze(files), statementCount });
79
+ }
80
+
81
+ function rejectProviderSpecificSql(sql: string): void {
82
+ const normalized = sql.toLowerCase();
83
+ const withoutPortableAuth = normalized.replace(
84
+ /\bauth\s*\.\s*"(?:account|ratelimit|session|user|verification)"/gu,
85
+ "",
86
+ );
87
+ if (
88
+ /\bauth\s*\./u.test(withoutPortableAuth) ||
89
+ /\bstorage\s*\./u.test(normalized) ||
90
+ /\b(?:to|from)\s+(?:anon|authenticated|service_role)\b/u.test(normalized) ||
91
+ /\bcreate\s+policy\b/u.test(normalized) ||
92
+ /\brow\s+level\s+security\b/u.test(normalized) ||
93
+ /\bsupabase\b/u.test(normalized)
94
+ ) {
95
+ throw new DatabaseMigrationAdmissionError("provider_specific");
96
+ }
97
+ }
98
+
99
+ function splitSqlStatements(sql: string): readonly string[] {
100
+ const statements: string[] = [];
101
+ let start = 0;
102
+ let index = 0;
103
+ while (index < sql.length) {
104
+ const character = sql[index];
105
+ const next = sql[index + 1];
106
+ if (character === "-" && next === "-") {
107
+ index = skipLineComment(sql, index + 2);
108
+ continue;
109
+ }
110
+ if (character === "/" && next === "*") {
111
+ index = skipBlockComment(sql, index + 2);
112
+ continue;
113
+ }
114
+ if (character === "'") {
115
+ index = skipQuoted(sql, index + 1, "'");
116
+ continue;
117
+ }
118
+ if (character === '"') {
119
+ index = skipQuoted(sql, index + 1, '"');
120
+ continue;
121
+ }
122
+ if (character === "$") {
123
+ const delimiter = dollarDelimiter(sql, index);
124
+ if (delimiter !== null) {
125
+ const end = sql.indexOf(delimiter, index + delimiter.length);
126
+ if (end < 0) throw new DatabaseMigrationAdmissionError("invalid");
127
+ index = end + delimiter.length;
128
+ continue;
129
+ }
130
+ }
131
+ if (character === ";") {
132
+ const statement = sql.slice(start, index).trim();
133
+ if (statement.length > 0) statements.push(statement);
134
+ start = index + 1;
135
+ }
136
+ index += 1;
137
+ }
138
+ const tail = sql.slice(start).trim();
139
+ if (tail.length > 0) statements.push(tail);
140
+ return Object.freeze(statements);
141
+ }
142
+
143
+ function skipLineComment(sql: string, index: number): number {
144
+ const newline = sql.indexOf("\n", index);
145
+ return newline < 0 ? sql.length : newline + 1;
146
+ }
147
+
148
+ function skipBlockComment(sql: string, index: number): number {
149
+ let depth = 1;
150
+ while (index < sql.length) {
151
+ if (sql[index] === "/" && sql[index + 1] === "*") {
152
+ depth += 1;
153
+ index += 2;
154
+ continue;
155
+ }
156
+ if (sql[index] === "*" && sql[index + 1] === "/") {
157
+ depth -= 1;
158
+ index += 2;
159
+ if (depth === 0) return index;
160
+ continue;
161
+ }
162
+ index += 1;
163
+ }
164
+ throw new DatabaseMigrationAdmissionError("invalid");
165
+ }
166
+
167
+ function skipQuoted(sql: string, index: number, quote: "'" | '"'): number {
168
+ while (index < sql.length) {
169
+ if (sql[index] !== quote) {
170
+ index += 1;
171
+ continue;
172
+ }
173
+ if (sql[index + 1] === quote) {
174
+ index += 2;
175
+ continue;
176
+ }
177
+ return index + 1;
178
+ }
179
+ throw new DatabaseMigrationAdmissionError("invalid");
180
+ }
181
+
182
+ function dollarDelimiter(sql: string, index: number): string | null {
183
+ const match = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(sql.slice(index));
184
+ return match?.[0] ?? null;
185
+ }
186
+
187
+ function admitStatement(statement: string): void {
188
+ const normalized = normalizeStatement(statement);
189
+ if (normalized === "set check_function_bodies = false") return;
190
+ if (/^create\s+function\b/u.test(normalized)) {
191
+ if (
192
+ !/\blanguage\s+(?:sql|plpgsql)\b/u.test(normalized) ||
193
+ (/\bsecurity\s+definer\b/u.test(normalized) &&
194
+ !/\bset\s+search_path\s*(?:=\s*|to\s+)/u.test(normalized))
195
+ ) {
196
+ throw new DatabaseMigrationAdmissionError("unsupported");
197
+ }
198
+ return;
199
+ }
200
+ if (
201
+ /^create\s+schema\s+if\s+not\s+exists\s+(?:auth|extensions|private)$/u.test(normalized) ||
202
+ /^create\s+extension\s+if\s+not\s+exists\s+(?:pgcrypto|btree_gist|unaccent)(?:\s+with\s+schema\s+extensions)?$/u.test(
203
+ normalized,
204
+ ) ||
205
+ /^create\s+(?:table|type|domain|sequence|view|materialized\s+view|(?:constraint\s+)?trigger)\b/u.test(
206
+ normalized,
207
+ ) ||
208
+ /^create\s+(?:unique\s+)?index\b/u.test(normalized) ||
209
+ /^alter\s+table\s+(?:only\s+)?[a-z0-9_."]+\s+add\s+(?:(?:column|constraint)\s+)?(?:if\s+not\s+exists\s+)?[a-z0-9_"]+/u.test(
210
+ normalized,
211
+ ) ||
212
+ /^comment\s+on\b/u.test(normalized)
213
+ ) {
214
+ if (/^create\s+or\s+replace\b/u.test(normalized)) {
215
+ throw new DatabaseMigrationAdmissionError("destructive");
216
+ }
217
+ return;
218
+ }
219
+ if (/^(?:drop|truncate|delete|update|insert|begin|commit|rollback)\b/u.test(normalized)) {
220
+ throw new DatabaseMigrationAdmissionError("destructive");
221
+ }
222
+ if (/^alter\s+table\b/u.test(normalized) || /^create\s+or\s+replace\b/u.test(normalized)) {
223
+ throw new DatabaseMigrationAdmissionError("destructive");
224
+ }
225
+ throw new DatabaseMigrationAdmissionError("unsupported");
226
+ }
227
+
228
+ function normalizeStatement(statement: string): string {
229
+ return statement
230
+ .replace(/--[^\n]*(?:\n|$)/gu, " ")
231
+ .replace(/\/\*[\s\S]*?\*\//gu, " ")
232
+ .trim()
233
+ .replace(/\s+/gu, " ")
234
+ .toLowerCase();
235
+ }
@@ -0,0 +1,96 @@
1
+ export interface FeedbackSubmission {
2
+ readonly organization_id: string;
3
+ readonly project_id?: string;
4
+ readonly environment_id?: string;
5
+ readonly operation_id?: string;
6
+ readonly kind: "bug" | "issue" | "feature_request";
7
+ readonly title: string;
8
+ /** Redacted expected/actual behavior and minimal reproduction; never raw logs or secrets. */
9
+ readonly description: string;
10
+ readonly error_code?: string;
11
+ readonly client_version?: string;
12
+ }
13
+
14
+ export interface FeedbackReceipt {
15
+ readonly id: string;
16
+ readonly organization_id: string;
17
+ readonly submitted_at: string;
18
+ }
19
+
20
+ const identifier = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
21
+ const fields = new Set([
22
+ "organization_id",
23
+ "project_id",
24
+ "environment_id",
25
+ "operation_id",
26
+ "kind",
27
+ "title",
28
+ "description",
29
+ "error_code",
30
+ "client_version",
31
+ ]);
32
+
33
+ export function parseFeedbackSubmission(value: unknown): FeedbackSubmission {
34
+ if (typeof value !== "object" || value === null || Array.isArray(value))
35
+ throw new TypeError("Invalid feedback");
36
+ const v = value as Record<string, unknown>;
37
+ if (
38
+ Object.keys(v).some((key) => !fields.has(key)) ||
39
+ typeof v["organization_id"] !== "string" ||
40
+ !identifier.test(v["organization_id"]) ||
41
+ typeof v["kind"] !== "string" ||
42
+ !["bug", "issue", "feature_request"].includes(v["kind"]) ||
43
+ !validText(v["title"], 160, false) ||
44
+ !validText(v["description"], 8000, true)
45
+ )
46
+ throw new TypeError("Invalid feedback");
47
+ for (const key of ["project_id", "environment_id", "operation_id"]) {
48
+ if (key in v && (typeof v[key] !== "string" || !identifier.test(v[key])))
49
+ throw new TypeError("Invalid feedback scope");
50
+ }
51
+ if (("environment_id" in v || "operation_id" in v) && !("project_id" in v))
52
+ throw new TypeError("Project scope is required");
53
+ for (const key of ["error_code", "client_version"]) {
54
+ if (
55
+ key in v &&
56
+ (typeof v[key] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$/u.test(v[key]))
57
+ )
58
+ throw new TypeError("Invalid feedback metadata");
59
+ }
60
+ return v as unknown as FeedbackSubmission;
61
+ }
62
+
63
+ export function parseFeedbackReceipt(value: unknown): FeedbackReceipt {
64
+ if (typeof value !== "object" || value === null || Array.isArray(value))
65
+ throw new TypeError("Feedback was not acknowledged");
66
+ const v = value as Record<string, unknown>;
67
+ if (
68
+ Object.keys(v).sort().join(",") !== "id,organization_id,submitted_at" ||
69
+ typeof v["id"] !== "string" ||
70
+ !identifier.test(v["id"]) ||
71
+ typeof v["organization_id"] !== "string" ||
72
+ !identifier.test(v["organization_id"]) ||
73
+ typeof v["submitted_at"] !== "string" ||
74
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(v["submitted_at"]) ||
75
+ !Number.isFinite(Date.parse(v["submitted_at"])) ||
76
+ new Date(v["submitted_at"]).toISOString() !== v["submitted_at"]
77
+ )
78
+ throw new TypeError("Feedback was not acknowledged");
79
+ return v as unknown as FeedbackReceipt;
80
+ }
81
+
82
+ function validText(value: unknown, max: number, multiline: boolean): boolean {
83
+ return (
84
+ typeof value === "string" &&
85
+ value === value.trim() &&
86
+ Array.from(value).length >= 1 &&
87
+ Array.from(value).length <= max &&
88
+ !Array.from(value).some((character) => {
89
+ const code = character.charCodeAt(0);
90
+ return (
91
+ (code < 32 && !(multiline && (code === 9 || code === 10 || code === 13))) ||
92
+ (code >= 127 && code <= 159)
93
+ );
94
+ })
95
+ );
96
+ }
@@ -0,0 +1,280 @@
1
+ export type PackageManagerName = "bun" | "npm" | "pnpm" | "yarn";
2
+
3
+ export type FrameworkAdmissionReason =
4
+ | "ambiguous_config"
5
+ | "ambiguous_lockfile"
6
+ | "invalid_package_manager"
7
+ | "missing_lockfile"
8
+ | "missing_package_manager"
9
+ | "package_manager_lockfile_mismatch";
10
+
11
+ export class FrameworkAdmissionError extends Error {
12
+ public readonly code = "framework_admission_failed";
13
+
14
+ public constructor(public readonly reason: FrameworkAdmissionReason) {
15
+ super("Framework source is not admitted");
16
+ this.name = "FrameworkAdmissionError";
17
+ }
18
+ }
19
+
20
+ export interface PackageManagerInput {
21
+ readonly packageManager: string | undefined;
22
+ readonly files: readonly string[];
23
+ }
24
+
25
+ export interface PackageManagerDescriptor {
26
+ readonly manager: PackageManagerName;
27
+ readonly version: string;
28
+ readonly lockfile:
29
+ | "bun.lock"
30
+ | "bun.lockb"
31
+ | "package-lock.json"
32
+ | "pnpm-lock.yaml"
33
+ | "yarn.lock";
34
+ readonly installCommand: string;
35
+ readonly buildCommand: string;
36
+ }
37
+
38
+ interface PackageManagerContract {
39
+ readonly lockfiles: readonly PackageManagerDescriptor["lockfile"][];
40
+ readonly installCommand: string;
41
+ readonly buildCommand: string;
42
+ }
43
+
44
+ const PACKAGE_MANAGER_CONTRACTS: Readonly<Record<PackageManagerName, PackageManagerContract>> =
45
+ Object.freeze({
46
+ bun: Object.freeze({
47
+ lockfiles: Object.freeze(["bun.lock", "bun.lockb"] as const),
48
+ installCommand: "bun install --frozen-lockfile --ignore-scripts",
49
+ buildCommand: "bun run build",
50
+ }),
51
+ npm: Object.freeze({
52
+ lockfiles: Object.freeze(["package-lock.json"] as const),
53
+ installCommand: "npm ci --ignore-scripts",
54
+ buildCommand: "npm run build",
55
+ }),
56
+ pnpm: Object.freeze({
57
+ lockfiles: Object.freeze(["pnpm-lock.yaml"] as const),
58
+ installCommand: "pnpm install --frozen-lockfile --ignore-scripts",
59
+ buildCommand: "pnpm run build",
60
+ }),
61
+ yarn: Object.freeze({
62
+ lockfiles: Object.freeze(["yarn.lock"] as const),
63
+ installCommand: "yarn install --immutable --mode=skip-build",
64
+ buildCommand: "yarn run build",
65
+ }),
66
+ });
67
+
68
+ const ALL_LOCKFILES = Object.freeze(
69
+ Object.values(PACKAGE_MANAGER_CONTRACTS).flatMap(({ lockfiles }) => lockfiles),
70
+ );
71
+ const EXACT_VERSION = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/u;
72
+ const PACKAGE_MANAGER_DECLARATION =
73
+ /^(bun|npm|pnpm|yarn)@((?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))$/u;
74
+
75
+ export function resolvePackageManager(input: PackageManagerInput): PackageManagerDescriptor {
76
+ const presentLockfiles = ALL_LOCKFILES.filter((lockfile) => input.files.includes(lockfile));
77
+ if (presentLockfiles.length === 0) throw new FrameworkAdmissionError("missing_lockfile");
78
+ if (presentLockfiles.length !== 1) throw new FrameworkAdmissionError("ambiguous_lockfile");
79
+ if (input.packageManager === undefined) {
80
+ throw new FrameworkAdmissionError("missing_package_manager");
81
+ }
82
+ const declaration = PACKAGE_MANAGER_DECLARATION.exec(input.packageManager);
83
+ if (declaration === null) throw new FrameworkAdmissionError("invalid_package_manager");
84
+ const manager = declaration[1] as PackageManagerName;
85
+ const version = declaration[2] as string;
86
+ const contract = PACKAGE_MANAGER_CONTRACTS[manager];
87
+ const lockfile = presentLockfiles[0] as PackageManagerDescriptor["lockfile"];
88
+ if (!contract.lockfiles.includes(lockfile)) {
89
+ throw new FrameworkAdmissionError("package_manager_lockfile_mismatch");
90
+ }
91
+ return Object.freeze({
92
+ manager,
93
+ version,
94
+ lockfile,
95
+ installCommand: contract.installCommand,
96
+ buildCommand: contract.buildCommand,
97
+ });
98
+ }
99
+
100
+ export function resolveFrameworkConfig(basename: string, files: readonly string[]): string | null {
101
+ const supported = [`${basename}.js`, `${basename}.mjs`, `${basename}.ts`] as const;
102
+ const matches = supported.filter((candidate) => files.includes(candidate));
103
+ if (matches.length > 1) throw new FrameworkAdmissionError("ambiguous_config");
104
+ return matches[0] ?? null;
105
+ }
106
+
107
+ export type AdmittedFramework = "nextjs" | "tanstack-start" | "vite";
108
+ export type FrameworkVersionClassification = "experimental" | "unsupported" | "verified";
109
+ export type FrameworkVersionReason =
110
+ | "inside_admitted_window"
111
+ | "invalid_version"
112
+ | "outside_admitted_window"
113
+ | "range_inside_admitted_window"
114
+ | "verified_fixture";
115
+
116
+ export interface FrameworkVersionAdmission {
117
+ readonly classification: FrameworkVersionClassification;
118
+ readonly reason: FrameworkVersionReason;
119
+ }
120
+
121
+ const VERIFIED_VERSIONS: Readonly<Record<AdmittedFramework, ReadonlySet<string>>> = Object.freeze({
122
+ vite: new Set(["5.4.21", "8.0.16", "8.2.2"]),
123
+ "tanstack-start": new Set(["1.168.26", "1.168.49"]),
124
+ nextjs: new Set(["15.5.23", "16.3.2"]),
125
+ });
126
+
127
+ export function classifyFrameworkVersion(
128
+ framework: AdmittedFramework,
129
+ version: string,
130
+ ): FrameworkVersionAdmission {
131
+ const parsed = parseVersionSpec(version);
132
+ if (parsed === null) return versionAdmission("unsupported", "invalid_version");
133
+ if (!insideAdmittedWindow(framework, parsed.version)) {
134
+ return versionAdmission("unsupported", "outside_admitted_window");
135
+ }
136
+ if (!parsed.exact) {
137
+ return versionAdmission("experimental", "range_inside_admitted_window");
138
+ }
139
+ return VERIFIED_VERSIONS[framework].has(version)
140
+ ? versionAdmission("verified", "verified_fixture")
141
+ : versionAdmission("experimental", "inside_admitted_window");
142
+ }
143
+
144
+ type VersionTuple = readonly [major: number, minor: number, patch: number];
145
+
146
+ function parseVersionSpec(
147
+ version: string,
148
+ ): Readonly<{ exact: boolean; version: VersionTuple }> | null {
149
+ const exact = EXACT_VERSION.exec(version);
150
+ const match =
151
+ exact ?? (/^[~^](.*)$/u.exec(version)?.[1] ? EXACT_VERSION.exec(version.slice(1)) : null);
152
+ if (match === null) return null;
153
+ const tuple = [Number(match[1]), Number(match[2]), Number(match[3])] as const;
154
+ return tuple.every(Number.isSafeInteger)
155
+ ? Object.freeze({ exact: exact !== null, version: tuple })
156
+ : null;
157
+ }
158
+
159
+ function insideAdmittedWindow(framework: AdmittedFramework, version: VersionTuple): boolean {
160
+ switch (framework) {
161
+ case "vite":
162
+ return compareVersions(version, [5, 4, 0]) >= 0 && compareVersions(version, [8, 2, 2]) <= 0;
163
+ case "tanstack-start":
164
+ return (
165
+ compareVersions(version, [1, 168, 26]) >= 0 && compareVersions(version, [1, 168, 49]) <= 0
166
+ );
167
+ case "nextjs":
168
+ return (
169
+ (version[0] === 15 && version[1] === 5) ||
170
+ (compareVersions(version, [16, 0, 0]) >= 0 && compareVersions(version, [16, 3, 4]) <= 0)
171
+ );
172
+ }
173
+ }
174
+
175
+ function compareVersions(left: VersionTuple, right: VersionTuple): number {
176
+ const majorDifference = left[0] - right[0];
177
+ if (majorDifference !== 0) return majorDifference;
178
+ const minorDifference = left[1] - right[1];
179
+ return minorDifference !== 0 ? minorDifference : left[2] - right[2];
180
+ }
181
+
182
+ function versionAdmission(
183
+ classification: FrameworkVersionClassification,
184
+ reason: FrameworkVersionReason,
185
+ ): FrameworkVersionAdmission {
186
+ return Object.freeze({ classification, reason });
187
+ }
188
+
189
+ export type FrameworkConversionDiagnosticCode =
190
+ | "database_binding_private"
191
+ | "database_driver_unsupported"
192
+ | "source_syntax_invalid"
193
+ | "worker_module_default_export_required"
194
+ | "scheduled_handler_required";
195
+
196
+ export interface FrameworkConversionDiagnostic {
197
+ readonly code: FrameworkConversionDiagnosticCode;
198
+ readonly path: string;
199
+ }
200
+
201
+ // Detail texts are the customer-facing sentence for one diagnostic; parseFrameworkConversionDetail
202
+ // recognizes exactly these sentences so a client can map a detail back to its typed diagnostic.
203
+ const FRAMEWORK_CONVERSION_DETAILS: Readonly<
204
+ Record<
205
+ FrameworkConversionDiagnosticCode,
206
+ Readonly<{ render(path: string): string; pattern: RegExp }>
207
+ >
208
+ > = Object.freeze({
209
+ database_binding_private: Object.freeze({
210
+ render: (path: string) =>
211
+ `"${path}" reads a managed database connection string. Hyperdrive, 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.`,
212
+ pattern:
213
+ /^"([^"]+)" reads a managed database connection string\. Hyperdrive, 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,
214
+ }),
215
+ database_driver_unsupported: Object.freeze({
216
+ render: (path: string) =>
217
+ `"${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.`,
218
+ pattern:
219
+ /^"([^"]+)" 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,
220
+ }),
221
+ source_syntax_invalid: Object.freeze({
222
+ render: (path: string) =>
223
+ `Invalid JavaScript or TypeScript in "${path}". Fix the file, commit the change, then plan again.`,
224
+ pattern:
225
+ /^Invalid JavaScript or TypeScript in "([^"]+)"\. Fix the file, commit the change, then plan again\.$/u,
226
+ }),
227
+ worker_module_default_export_required: Object.freeze({
228
+ render: (path: string) =>
229
+ `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.`,
230
+ pattern:
231
+ /^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,
232
+ }),
233
+ scheduled_handler_required: Object.freeze({
234
+ render: (path: string) =>
235
+ `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.`,
236
+ pattern:
237
+ /^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,
238
+ }),
239
+ });
240
+
241
+ function isFrameworkConversionDiagnosticCode(
242
+ value: unknown,
243
+ ): value is FrameworkConversionDiagnosticCode {
244
+ return typeof value === "string" && Object.hasOwn(FRAMEWORK_CONVERSION_DETAILS, value);
245
+ }
246
+
247
+ export function parseFrameworkConversionDiagnostic(
248
+ value: unknown,
249
+ ): FrameworkConversionDiagnostic | null {
250
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
251
+ const record = value as Record<string, unknown>;
252
+ const path = record["path"];
253
+ const code = record["code"];
254
+ if (
255
+ Object.keys(record).length !== 2 ||
256
+ !isFrameworkConversionDiagnosticCode(code) ||
257
+ typeof path !== "string" ||
258
+ path.length > 240 ||
259
+ !/^[A-Za-z0-9_@()[\]./-]+\.(?:[cm]?[jt]sx?)$/u.test(path) ||
260
+ path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
261
+ )
262
+ return null;
263
+ return Object.freeze({ code, path });
264
+ }
265
+
266
+ export function frameworkConversionDetail(value: unknown): string | null {
267
+ const diagnostic = parseFrameworkConversionDiagnostic(value);
268
+ return diagnostic === null
269
+ ? null
270
+ : FRAMEWORK_CONVERSION_DETAILS[diagnostic.code].render(diagnostic.path);
271
+ }
272
+
273
+ export function parseFrameworkConversionDetail(value: unknown): string | null {
274
+ if (typeof value !== "string") return null;
275
+ for (const [code, detail] of Object.entries(FRAMEWORK_CONVERSION_DETAILS)) {
276
+ const match = detail.pattern.exec(value);
277
+ if (match !== null) return frameworkConversionDetail({ code, path: match[1] });
278
+ }
279
+ return null;
280
+ }