@amerged/ohmyhost-mcp 0.1.31 → 0.1.32

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.
@@ -70,6 +70,16 @@
70
70
  "types": "./dist/framework-build-script.d.ts",
71
71
  "import": "./dist/framework-build-script.js"
72
72
  },
73
+ "./framework-source-rules": {
74
+ "source": "./src/framework-source-rules.ts",
75
+ "types": "./dist/framework-source-rules.d.ts",
76
+ "import": "./dist/framework-source-rules.js"
77
+ },
78
+ "./framework-admission-corpus": {
79
+ "source": "./src/framework-admission-corpus.ts",
80
+ "types": "./dist/framework-admission-corpus.d.ts",
81
+ "import": "./dist/framework-admission-corpus.js"
82
+ },
73
83
  "./browser-capabilities": {
74
84
  "source": "./src/browser-capabilities.ts",
75
85
  "types": "./dist/browser-capabilities.d.ts",
@@ -0,0 +1,390 @@
1
+ /** The test-only conformance corpus of the framework admission; production code never imports it. */
2
+ import {
3
+ parseFrameworkConversionDiagnostic,
4
+ type FrameworkConversionDiagnostic,
5
+ type FrameworkVersionPackage,
6
+ type LockfileName,
7
+ } from "./framework-admission.js";
8
+ import { PINNED_BUN_VERSION } from "./framework-build-script.mjs";
9
+
10
+ type CorpusManager = "bun" | "npm" | "pnpm" | "yarn";
11
+
12
+ /** Package manager pin, lockfile and the build commands ohmyhost.yaml must carry for it. */
13
+ const CORPUS_MANAGERS = Object.freeze({
14
+ bun: Object.freeze([
15
+ `bun@${PINNED_BUN_VERSION}`,
16
+ "bun.lock",
17
+ "bun install --frozen-lockfile --ignore-scripts",
18
+ "bun run build",
19
+ ] as const),
20
+ npm: Object.freeze([
21
+ "npm@11.5.1",
22
+ "package-lock.json",
23
+ "npm ci --ignore-scripts",
24
+ "npm run build",
25
+ ] as const),
26
+ pnpm: Object.freeze([
27
+ "pnpm@10.28.2",
28
+ "pnpm-lock.yaml",
29
+ "pnpm install --frozen-lockfile --ignore-scripts",
30
+ "pnpm run build",
31
+ ] as const),
32
+ yarn: Object.freeze([
33
+ "yarn@4.9.4",
34
+ "yarn.lock",
35
+ "yarn install --immutable --mode=skip-build",
36
+ "yarn run build",
37
+ ] as const),
38
+ } satisfies Record<CorpusManager, readonly [string, LockfileName, string, string]>);
39
+ const CORPUS_NEXT_CONFIG = `import type { NextConfig } from "next";
40
+
41
+ const nextConfig: NextConfig = {
42
+ reactStrictMode: true,
43
+ };
44
+
45
+ export default nextConfig;
46
+ `;
47
+ const CORPUS_NEXT_PAGE = `export default function Home() {
48
+ return <main>Hello from ohmyho.st</main>;
49
+ }
50
+ `;
51
+ const CORPUS_TANSTACK_CONFIG = `import { tanstackStart } from "@tanstack/react-start/plugin/vite";
52
+ import viteReact from "@vitejs/plugin-react";
53
+ import { defineConfig } from "vite";
54
+
55
+ export default defineConfig({
56
+ plugins: [tanstackStart({ prerender: { enabled: true } }), viteReact()],
57
+ });
58
+ `;
59
+ const CORPUS_AUTH_CONFIGURATION = `database:
60
+ enabled: true
61
+ migrations: migrations
62
+ auth:
63
+ provider: better-auth
64
+ `;
65
+
66
+ /** One commit of the conformance corpus: repository-relative paths and their text, root ".". */
67
+ export interface FrameworkAdmissionConformanceSource {
68
+ readonly name: string;
69
+ readonly files: Readonly<Record<string, string>>;
70
+ }
71
+
72
+ /**
73
+ * Sources every admission surface must judge alike: SourceFetcher plans the admitted and pending
74
+ * ones and answers the refused ones with exactly `diagnostic` first; `ohmyhost init` reports no
75
+ * blocker for the first two and `diagnostic` as its first blocker. A pending source is admitted at
76
+ * planning and its listed packages are decided by the installed-version check of the build.
77
+ */
78
+ export const FRAMEWORK_ADMISSION_CONFORMANCE_CORPUS: Readonly<{
79
+ admitted: readonly FrameworkAdmissionConformanceSource[];
80
+ pending: readonly (FrameworkAdmissionConformanceSource &
81
+ Readonly<{ packages: readonly FrameworkVersionPackage[] }>)[];
82
+ refused: readonly (FrameworkAdmissionConformanceSource &
83
+ Readonly<{ diagnostic: FrameworkConversionDiagnostic }>)[];
84
+ }> = Object.freeze({
85
+ admitted: Object.freeze([
86
+ corpusCase(
87
+ "Next.js 16.3.8 with npm on the edge runtime (the exact customer source)",
88
+ nextSource(),
89
+ ),
90
+ corpusCase("Next.js 17.0.0, a later stable major", nextSource({ declared: "17.0.0" })),
91
+ corpusCase(
92
+ "Vite ^8.2.0 resolved to 8.3.4 by pnpm-lock.yaml",
93
+ viteSource({ declared: "^8.2.0", resolved: "8.3.4" }),
94
+ ),
95
+ corpusCase("TanStack Start 1.168.60 prerendered for the static runtime", tanStackSource()),
96
+ ]),
97
+ pending: Object.freeze([
98
+ Object.freeze({
99
+ ...corpusCase(
100
+ "Next.js ^16.3.0 with bun.lock, which ohmyho.st does not resolve",
101
+ nextSource({ declared: "^16.3.0", resolved: "16.4.0", manager: "bun" }),
102
+ ),
103
+ packages: Object.freeze(["next"] as const),
104
+ }),
105
+ ]),
106
+ refused: Object.freeze([
107
+ refusedCase(
108
+ "Next.js 16.3.7 below the 16.x security minimum",
109
+ nextSource({ declared: "16.3.7" }),
110
+ versionFinding("package-lock.json", "next", "16.3.7", "16.3.8"),
111
+ ),
112
+ refusedCase(
113
+ "Next.js 15.5.26 declared with yarn below the 15.x security minimum",
114
+ nextSource({ declared: "15.5.26", manager: "yarn" }),
115
+ versionFinding("package.json", "next", "15.5.26", "15.5.27"),
116
+ ),
117
+ refusedCase(
118
+ "Next.js ^16.3.0 resolved to 16.3.7 by package-lock.json",
119
+ nextSource({ declared: "^16.3.0", resolved: "16.3.7" }),
120
+ versionFinding("package-lock.json", "next", "16.3.7", "16.3.8"),
121
+ ),
122
+ refusedCase(
123
+ "TanStack Start 1.168.59 resolved by pnpm-lock.yaml below its security minimum",
124
+ tanStackSource({ start: "1.168.59" }),
125
+ versionFinding("pnpm-lock.yaml", "@tanstack/react-start", "1.168.59", "1.168.60"),
126
+ ),
127
+ refusedCase(
128
+ "ohmyhost.yaml that is not valid YAML",
129
+ nextSource({ configuration: "version: 1\nproject: website\nbuild: [npm ci\n" }),
130
+ { code: "repository_configuration_invalid", path: "ohmyhost.yaml" },
131
+ ),
132
+ refusedCase(
133
+ "Next.js with runtime.mode static",
134
+ nextSource({ configuration: corpusConfiguration("npm", ".open-next/assets", "static") }),
135
+ { code: "runtime_mode_unsupported", path: "ohmyhost.yaml", framework: "nextjs" },
136
+ ),
137
+ refusedCase(
138
+ 'Next.js with output: "export"',
139
+ nextSource({
140
+ config: CORPUS_NEXT_CONFIG.replace("reactStrictMode", 'output: "export", reactStrictMode'),
141
+ }),
142
+ { code: "next_output_unsupported", path: "next.config.ts" },
143
+ ),
144
+ refusedCase(
145
+ "Next.js with a basePath",
146
+ nextSource({
147
+ config: CORPUS_NEXT_CONFIG.replace("reactStrictMode", 'basePath: "/docs", reactStrictMode'),
148
+ }),
149
+ { code: "next_base_path_unsupported", path: "next.config.ts" },
150
+ ),
151
+ refusedCase(
152
+ "Next.js build script with a second step",
153
+ nextSource({ build: "next build && next-sitemap" }),
154
+ { code: "build_script_unsupported", path: "package.json", framework: "nextjs" },
155
+ ),
156
+ refusedCase("next declared only in devDependencies", nextSource({ field: "devDependencies" }), {
157
+ code: "framework_dependency_required",
158
+ path: "package.json",
159
+ package: "next",
160
+ }),
161
+ refusedCase(
162
+ "auth.provider better-auth without better-auth in package.json",
163
+ nextSource({
164
+ configuration: corpusConfiguration(
165
+ "npm",
166
+ ".open-next/assets",
167
+ "edge",
168
+ CORPUS_AUTH_CONFIGURATION,
169
+ ),
170
+ }),
171
+ { code: "better_auth_version_required", path: "package.json" },
172
+ ),
173
+ refusedCase(
174
+ "Vite with a base below the root origin",
175
+ viteSource({
176
+ config:
177
+ 'import { defineConfig } from "vite";\n\nexport default defineConfig({ base: "/app/" });\n',
178
+ }),
179
+ { code: "framework_base_path_unsupported", path: "vite.config.ts" },
180
+ ),
181
+ refusedCase(
182
+ "TanStack Start for the static runtime without prerender",
183
+ tanStackSource({
184
+ config: CORPUS_TANSTACK_CONFIG.replace("{ prerender: { enabled: true } }", ""),
185
+ }),
186
+ { code: "tanstack_prerender_required", path: "vite.config.ts" },
187
+ ),
188
+ ]),
189
+ });
190
+
191
+ function corpusCase(
192
+ name: string,
193
+ files: Readonly<Record<string, string>>,
194
+ ): FrameworkAdmissionConformanceSource {
195
+ return Object.freeze({ name, files });
196
+ }
197
+
198
+ function refusedCase(
199
+ name: string,
200
+ files: Readonly<Record<string, string>>,
201
+ finding: Readonly<Record<string, string>>,
202
+ ): FrameworkAdmissionConformanceSource & Readonly<{ diagnostic: FrameworkConversionDiagnostic }> {
203
+ const diagnostic = parseFrameworkConversionDiagnostic(finding);
204
+ if (diagnostic === null) throw new TypeError("Framework admission corpus diagnostic is invalid");
205
+ return Object.freeze({ name, files, diagnostic });
206
+ }
207
+
208
+ function versionFinding(
209
+ path: string,
210
+ packageName: FrameworkVersionPackage,
211
+ version: string,
212
+ minimum: string,
213
+ ): Readonly<Record<string, string>> {
214
+ return { code: "framework_version_unsupported", path, package: packageName, version, minimum };
215
+ }
216
+
217
+ function corpusConfiguration(
218
+ manager: CorpusManager,
219
+ output: string,
220
+ mode: string,
221
+ capabilities = "",
222
+ ): string {
223
+ const [, , install, command] = CORPUS_MANAGERS[manager];
224
+ return `version: 1\nproject: website\nbuild:\n install: ${install}\n command: ${command}\n output: ${output}\nruntime:\n mode: ${mode}\n${capabilities}`;
225
+ }
226
+
227
+ function corpusJson(value: unknown): string {
228
+ return `${JSON.stringify(value, null, 2)}\n`;
229
+ }
230
+
231
+ /** The lockfile of a corpus manager resolving `resolved`; only npm and pnpm lockfiles are read. */
232
+ function corpusLockfile(
233
+ manager: CorpusManager,
234
+ declared: Readonly<Record<string, string>>,
235
+ resolved: Readonly<Record<string, string>>,
236
+ development: readonly string[] = [],
237
+ ): string {
238
+ const entries = Object.entries(resolved);
239
+ if (manager === "npm")
240
+ return corpusJson({
241
+ name: "website",
242
+ version: "1.0.0",
243
+ lockfileVersion: 3,
244
+ requires: true,
245
+ packages: {
246
+ "": { name: "website", version: "1.0.0" },
247
+ ...Object.fromEntries(
248
+ entries.map(([name, version]) => [
249
+ `node_modules/${name}`,
250
+ {
251
+ version,
252
+ resolved: `https://registry.npmjs.org/${name}/-/${name.slice(name.lastIndexOf("/") + 1)}-${version}.tgz`,
253
+ license: "MIT",
254
+ },
255
+ ]),
256
+ ),
257
+ },
258
+ });
259
+ if (manager === "bun")
260
+ return corpusJson({
261
+ lockfileVersion: 1,
262
+ workspaces: { "": { name: "website", dependencies: declared } },
263
+ packages: Object.fromEntries(
264
+ entries.map(([name, version]) => [name, [`${name}@${version}`]]),
265
+ ),
266
+ });
267
+ if (manager === "yarn")
268
+ return `__metadata:\n version: 8\n cacheKey: 10c0\n${entries
269
+ .map(
270
+ ([name, version]) =>
271
+ `\n"${name}@npm:${declared[name] ?? version}":\n version: ${version}\n resolution: "${name}@npm:${version}"\n languageName: node\n linkType: hard\n`,
272
+ )
273
+ .join("")}`;
274
+ const group = (field: "dependencies" | "devDependencies") =>
275
+ entries
276
+ .filter(([name]) => development.includes(name) === (field === "devDependencies"))
277
+ .map(
278
+ ([name, version]) =>
279
+ ` ${name.startsWith("@") ? `'${name}'` : name}:\n specifier: ${declared[name] ?? version}\n version: ${version}\n`,
280
+ )
281
+ .join("");
282
+ const groups = (["dependencies", "devDependencies"] as const)
283
+ .map((field) => [field, group(field)] as const)
284
+ .filter(([, text]) => text !== "")
285
+ .map(([field, text]) => ` ${field}:\n${text}`)
286
+ .join("");
287
+ return `lockfileVersion: '9.0'\n\nsettings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\n\nimporters:\n\n .:\n${groups}`;
288
+ }
289
+
290
+ function nextSource(
291
+ options: Readonly<{
292
+ declared?: string;
293
+ resolved?: string;
294
+ manager?: "bun" | "npm" | "yarn";
295
+ field?: "dependencies" | "devDependencies";
296
+ build?: string;
297
+ config?: string;
298
+ configuration?: string;
299
+ }> = {},
300
+ ): Readonly<Record<string, string>> {
301
+ const manager = options.manager ?? "npm";
302
+ const [packageManager, lockfile] = CORPUS_MANAGERS[manager];
303
+ const next = options.declared ?? "16.3.8";
304
+ const react = { react: "19.1.2", "react-dom": "19.1.2" };
305
+ const runtime = options.field === "devDependencies" ? react : { next, ...react };
306
+ return Object.freeze({
307
+ "ohmyhost.yaml":
308
+ options.configuration ?? corpusConfiguration(manager, ".open-next/assets", "edge"),
309
+ "package.json": corpusJson({
310
+ name: "website",
311
+ version: "1.0.0",
312
+ private: true,
313
+ packageManager,
314
+ scripts: { dev: "next dev", build: options.build ?? "next build" },
315
+ dependencies: runtime,
316
+ ...(options.field === "devDependencies" ? { devDependencies: { next } } : {}),
317
+ }),
318
+ [lockfile]: corpusLockfile(
319
+ manager,
320
+ { next, ...react },
321
+ { next: options.resolved ?? next, ...react },
322
+ ),
323
+ "next.config.ts": options.config ?? CORPUS_NEXT_CONFIG,
324
+ "src/app/page.tsx": CORPUS_NEXT_PAGE,
325
+ });
326
+ }
327
+
328
+ function viteSource(
329
+ options: Readonly<{ declared?: string; resolved?: string; config?: string }> = {},
330
+ ): Readonly<Record<string, string>> {
331
+ const vite = options.declared ?? "8.2.2";
332
+ return Object.freeze({
333
+ "ohmyhost.yaml": corpusConfiguration("pnpm", "dist", "static"),
334
+ "package.json": corpusJson({
335
+ name: "website",
336
+ private: true,
337
+ packageManager: CORPUS_MANAGERS.pnpm[0],
338
+ scripts: { build: "vite build" },
339
+ devDependencies: { vite },
340
+ }),
341
+ "pnpm-lock.yaml": corpusLockfile("pnpm", { vite }, { vite: options.resolved ?? vite }, [
342
+ "vite",
343
+ ]),
344
+ "index.html":
345
+ '<!doctype html>\n<div id="root"></div>\n<script type="module" src="/src/main.ts"></script>\n',
346
+ "src/main.ts": 'document.querySelector("#root")?.append("Hello from ohmyho.st");\n',
347
+ "vite.config.ts":
348
+ options.config ??
349
+ 'import { defineConfig } from "vite";\n\nexport default defineConfig({});\n',
350
+ });
351
+ }
352
+
353
+ function tanStackSource(
354
+ options: Readonly<{ start?: string; config?: string }> = {},
355
+ ): Readonly<Record<string, string>> {
356
+ const declared = {
357
+ "@tanstack/react-router": "1.170.41",
358
+ "@tanstack/react-start": options.start ?? "1.168.60",
359
+ react: "19.2.8",
360
+ "react-dom": "19.2.8",
361
+ };
362
+ const development = { "@vitejs/plugin-react": "6.1.0", vite: "8.2.2" };
363
+ return Object.freeze({
364
+ "ohmyhost.yaml": corpusConfiguration("pnpm", "dist/client", "static"),
365
+ "package.json": corpusJson({
366
+ name: "website",
367
+ private: true,
368
+ packageManager: CORPUS_MANAGERS.pnpm[0],
369
+ scripts: { build: "vite build" },
370
+ dependencies: declared,
371
+ devDependencies: development,
372
+ }),
373
+ "pnpm-lock.yaml": corpusLockfile(
374
+ "pnpm",
375
+ { ...declared, ...development },
376
+ {
377
+ "@tanstack/react-router": `${declared["@tanstack/react-router"]}(react-dom@19.2.8(react@19.2.8))(react@19.2.8)`,
378
+ "@tanstack/react-start": `${declared["@tanstack/react-start"]}(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vite@8.2.2)`,
379
+ react: "19.2.8",
380
+ "react-dom": "19.2.8(react@19.2.8)",
381
+ "@vitejs/plugin-react": "6.1.0(vite@8.2.2)",
382
+ vite: "8.2.2",
383
+ },
384
+ ["@vitejs/plugin-react", "vite"],
385
+ ),
386
+ "vite.config.ts": options.config ?? CORPUS_TANSTACK_CONFIG,
387
+ "src/routes/index.tsx":
388
+ 'import { createFileRoute } from "@tanstack/react-router";\n\nexport const Route = createFileRoute("/")({ component: () => <main>Hello</main> });\n',
389
+ });
390
+ }