@amerged/ohmyhost-mcp 0.1.31 → 0.1.33

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,487 @@
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
+ // The config `create-next-app@16.4.0 --yes --no-tailwind` writes: Cache Components with partial
52
+ // prefetching.
53
+ const CREATE_NEXT_APP_CONFIG = `import type { NextConfig } from "next";
54
+
55
+ const nextConfig: NextConfig = {
56
+ /* config options here */
57
+ cacheComponents: true,
58
+ partialPrefetching: true,
59
+ };
60
+
61
+ export default nextConfig;
62
+ `;
63
+ // With Tailwind CSS, which --yes selects, create-next-app on its default Turbopack bundler adds this
64
+ // rule instead of a postcss.config.mjs; the Webpack build ignores turbopack.rules.
65
+ const CREATE_NEXT_APP_TAILWIND_RULE = ` turbopack: {
66
+ rules: {
67
+ "*.css": {
68
+ loaders: ["@tailwindcss/turbopack"],
69
+ as: "*.css",
70
+ },
71
+ },
72
+ },
73
+ `;
74
+ const CREATE_NEXT_APP_POSTCSS_CONFIG = `const config = {
75
+ plugins: {
76
+ "@tailwindcss/postcss": {},
77
+ },
78
+ };
79
+
80
+ export default config;
81
+ `;
82
+ const CREATE_NEXT_APP_LAYOUT = `import "./globals.css";
83
+
84
+ export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
85
+ return (
86
+ <html lang="en">
87
+ <body>{children}</body>
88
+ </html>
89
+ );
90
+ }
91
+ `;
92
+ const CORPUS_TANSTACK_CONFIG = `import { tanstackStart } from "@tanstack/react-start/plugin/vite";
93
+ import viteReact from "@vitejs/plugin-react";
94
+ import { defineConfig } from "vite";
95
+
96
+ export default defineConfig({
97
+ plugins: [tanstackStart({ prerender: { enabled: true } }), viteReact()],
98
+ });
99
+ `;
100
+ const CORPUS_AUTH_CONFIGURATION = `database:
101
+ enabled: true
102
+ migrations: migrations
103
+ auth:
104
+ provider: better-auth
105
+ `;
106
+
107
+ /** One commit of the conformance corpus: repository-relative paths and their text, root ".". */
108
+ export interface FrameworkAdmissionConformanceSource {
109
+ readonly name: string;
110
+ readonly files: Readonly<Record<string, string>>;
111
+ }
112
+
113
+ /**
114
+ * Sources every admission surface must judge alike: SourceFetcher plans the admitted and pending
115
+ * ones and answers the refused ones with exactly `diagnostic` first; `ohmyhost init` reports no
116
+ * blocker for the first two and `diagnostic` as its first blocker. A pending source is admitted at
117
+ * planning and its listed packages are decided by the installed-version check of the build.
118
+ */
119
+ export const FRAMEWORK_ADMISSION_CONFORMANCE_CORPUS: Readonly<{
120
+ admitted: readonly FrameworkAdmissionConformanceSource[];
121
+ pending: readonly (FrameworkAdmissionConformanceSource &
122
+ Readonly<{ packages: readonly FrameworkVersionPackage[] }>)[];
123
+ refused: readonly (FrameworkAdmissionConformanceSource &
124
+ Readonly<{ diagnostic: FrameworkConversionDiagnostic }>)[];
125
+ }> = Object.freeze({
126
+ admitted: Object.freeze([
127
+ corpusCase(
128
+ "Next.js 16.3.8 with npm on the edge runtime (the exact customer source)",
129
+ nextSource(),
130
+ ),
131
+ corpusCase("Next.js 17.0.0, a later stable major", nextSource({ declared: "17.0.0" })),
132
+ corpusCase(
133
+ "Vite ^8.2.0 resolved to 8.3.4 by pnpm-lock.yaml",
134
+ viteSource({ declared: "^8.2.0", resolved: "8.3.4" }),
135
+ ),
136
+ corpusCase("TanStack Start 1.168.60 prerendered for the static runtime", tanStackSource()),
137
+ corpusCase(
138
+ "create-next-app 16.4 --yes --no-tailwind: Cache Components with partial prefetching",
139
+ createNextAppSource({ tailwind: false }),
140
+ ),
141
+ corpusCase(
142
+ "create-next-app 16.4 --yes with a postcss.config.mjs added beside its Turbopack rule",
143
+ createNextAppSource({ tailwind: true, postcss: true }),
144
+ ),
145
+ ]),
146
+ pending: Object.freeze([
147
+ Object.freeze({
148
+ ...corpusCase(
149
+ "Next.js ^16.3.0 with bun.lock, which ohmyho.st does not resolve",
150
+ nextSource({ declared: "^16.3.0", resolved: "16.4.0", manager: "bun" }),
151
+ ),
152
+ packages: Object.freeze(["next"] as const),
153
+ }),
154
+ ]),
155
+ refused: Object.freeze([
156
+ refusedCase(
157
+ "Next.js 16.3.7 below the 16.x security minimum",
158
+ nextSource({ declared: "16.3.7" }),
159
+ versionFinding("package-lock.json", "next", "16.3.7", "16.3.8"),
160
+ ),
161
+ refusedCase(
162
+ "Next.js 15.5.26 declared with yarn below the 15.x security minimum",
163
+ nextSource({ declared: "15.5.26", manager: "yarn" }),
164
+ versionFinding("package.json", "next", "15.5.26", "15.5.27"),
165
+ ),
166
+ refusedCase(
167
+ "Next.js ^16.3.0 resolved to 16.3.7 by package-lock.json",
168
+ nextSource({ declared: "^16.3.0", resolved: "16.3.7" }),
169
+ versionFinding("package-lock.json", "next", "16.3.7", "16.3.8"),
170
+ ),
171
+ refusedCase(
172
+ "TanStack Start 1.168.59 resolved by pnpm-lock.yaml below its security minimum",
173
+ tanStackSource({ start: "1.168.59" }),
174
+ versionFinding("pnpm-lock.yaml", "@tanstack/react-start", "1.168.59", "1.168.60"),
175
+ ),
176
+ refusedCase(
177
+ "ohmyhost.yaml that is not valid YAML",
178
+ nextSource({ configuration: "version: 1\nproject: website\nbuild: [npm ci\n" }),
179
+ { code: "repository_configuration_invalid", path: "ohmyhost.yaml" },
180
+ ),
181
+ refusedCase(
182
+ "Next.js with runtime.mode static",
183
+ nextSource({ configuration: corpusConfiguration("npm", ".open-next/assets", "static") }),
184
+ { code: "runtime_mode_unsupported", path: "ohmyhost.yaml", framework: "nextjs" },
185
+ ),
186
+ refusedCase(
187
+ 'Next.js with output: "export"',
188
+ nextSource({
189
+ config: CORPUS_NEXT_CONFIG.replace("reactStrictMode", 'output: "export", reactStrictMode'),
190
+ }),
191
+ { code: "next_output_unsupported", path: "next.config.ts" },
192
+ ),
193
+ refusedCase(
194
+ "Next.js with a basePath",
195
+ nextSource({
196
+ config: CORPUS_NEXT_CONFIG.replace("reactStrictMode", 'basePath: "/docs", reactStrictMode'),
197
+ }),
198
+ { code: "next_base_path_unsupported", path: "next.config.ts" },
199
+ ),
200
+ refusedCase(
201
+ "create-next-app 16.4 --yes: Tailwind CSS only in a Turbopack rule",
202
+ createNextAppSource({ tailwind: true }),
203
+ { code: "next_tailwind_turbopack_only", path: "next.config.ts" },
204
+ ),
205
+ refusedCase(
206
+ "Next.js build script with a second step",
207
+ nextSource({ build: "next build && next-sitemap" }),
208
+ { code: "build_script_unsupported", path: "package.json", framework: "nextjs" },
209
+ ),
210
+ refusedCase("next declared only in devDependencies", nextSource({ field: "devDependencies" }), {
211
+ code: "framework_dependency_required",
212
+ path: "package.json",
213
+ package: "next",
214
+ }),
215
+ refusedCase(
216
+ "auth.provider better-auth without better-auth in package.json",
217
+ nextSource({
218
+ configuration: corpusConfiguration(
219
+ "npm",
220
+ ".open-next/assets",
221
+ "edge",
222
+ CORPUS_AUTH_CONFIGURATION,
223
+ ),
224
+ }),
225
+ { code: "better_auth_version_required", path: "package.json" },
226
+ ),
227
+ refusedCase(
228
+ "Vite with a base below the root origin",
229
+ viteSource({
230
+ config:
231
+ 'import { defineConfig } from "vite";\n\nexport default defineConfig({ base: "/app/" });\n',
232
+ }),
233
+ { code: "framework_base_path_unsupported", path: "vite.config.ts" },
234
+ ),
235
+ refusedCase(
236
+ "TanStack Start for the static runtime without prerender",
237
+ tanStackSource({
238
+ config: CORPUS_TANSTACK_CONFIG.replace("{ prerender: { enabled: true } }", ""),
239
+ }),
240
+ { code: "tanstack_prerender_required", path: "vite.config.ts" },
241
+ ),
242
+ ]),
243
+ });
244
+
245
+ function corpusCase(
246
+ name: string,
247
+ files: Readonly<Record<string, string>>,
248
+ ): FrameworkAdmissionConformanceSource {
249
+ return Object.freeze({ name, files });
250
+ }
251
+
252
+ function refusedCase(
253
+ name: string,
254
+ files: Readonly<Record<string, string>>,
255
+ finding: Readonly<Record<string, string>>,
256
+ ): FrameworkAdmissionConformanceSource & Readonly<{ diagnostic: FrameworkConversionDiagnostic }> {
257
+ const diagnostic = parseFrameworkConversionDiagnostic(finding);
258
+ if (diagnostic === null) throw new TypeError("Framework admission corpus diagnostic is invalid");
259
+ return Object.freeze({ name, files, diagnostic });
260
+ }
261
+
262
+ function versionFinding(
263
+ path: string,
264
+ packageName: FrameworkVersionPackage,
265
+ version: string,
266
+ minimum: string,
267
+ ): Readonly<Record<string, string>> {
268
+ return { code: "framework_version_unsupported", path, package: packageName, version, minimum };
269
+ }
270
+
271
+ function corpusConfiguration(
272
+ manager: CorpusManager,
273
+ output: string,
274
+ mode: string,
275
+ capabilities = "",
276
+ ): string {
277
+ const [, , install, command] = CORPUS_MANAGERS[manager];
278
+ return `version: 1\nproject: website\nbuild:\n install: ${install}\n command: ${command}\n output: ${output}\nruntime:\n mode: ${mode}\n${capabilities}`;
279
+ }
280
+
281
+ function corpusJson(value: unknown): string {
282
+ return `${JSON.stringify(value, null, 2)}\n`;
283
+ }
284
+
285
+ /** The lockfile of a corpus manager resolving `resolved`; only npm and pnpm lockfiles are read. */
286
+ function corpusLockfile(
287
+ manager: CorpusManager,
288
+ declared: Readonly<Record<string, string>>,
289
+ resolved: Readonly<Record<string, string>>,
290
+ development: readonly string[] = [],
291
+ ): string {
292
+ const entries = Object.entries(resolved);
293
+ if (manager === "npm")
294
+ return corpusJson({
295
+ name: "website",
296
+ version: "1.0.0",
297
+ lockfileVersion: 3,
298
+ requires: true,
299
+ packages: {
300
+ "": { name: "website", version: "1.0.0" },
301
+ ...Object.fromEntries(
302
+ entries.map(([name, version]) => [
303
+ `node_modules/${name}`,
304
+ {
305
+ version,
306
+ resolved: `https://registry.npmjs.org/${name}/-/${name.slice(name.lastIndexOf("/") + 1)}-${version}.tgz`,
307
+ license: "MIT",
308
+ },
309
+ ]),
310
+ ),
311
+ },
312
+ });
313
+ if (manager === "bun")
314
+ return corpusJson({
315
+ lockfileVersion: 1,
316
+ workspaces: { "": { name: "website", dependencies: declared } },
317
+ packages: Object.fromEntries(
318
+ entries.map(([name, version]) => [name, [`${name}@${version}`]]),
319
+ ),
320
+ });
321
+ if (manager === "yarn")
322
+ return `__metadata:\n version: 8\n cacheKey: 10c0\n${entries
323
+ .map(
324
+ ([name, version]) =>
325
+ `\n"${name}@npm:${declared[name] ?? version}":\n version: ${version}\n resolution: "${name}@npm:${version}"\n languageName: node\n linkType: hard\n`,
326
+ )
327
+ .join("")}`;
328
+ const group = (field: "dependencies" | "devDependencies") =>
329
+ entries
330
+ .filter(([name]) => development.includes(name) === (field === "devDependencies"))
331
+ .map(
332
+ ([name, version]) =>
333
+ ` ${name.startsWith("@") ? `'${name}'` : name}:\n specifier: ${declared[name] ?? version}\n version: ${version}\n`,
334
+ )
335
+ .join("");
336
+ const groups = (["dependencies", "devDependencies"] as const)
337
+ .map((field) => [field, group(field)] as const)
338
+ .filter(([, text]) => text !== "")
339
+ .map(([field, text]) => ` ${field}:\n${text}`)
340
+ .join("");
341
+ return `lockfileVersion: '9.0'\n\nsettings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\n\nimporters:\n\n .:\n${groups}`;
342
+ }
343
+
344
+ function nextSource(
345
+ options: Readonly<{
346
+ declared?: string;
347
+ resolved?: string;
348
+ manager?: "bun" | "npm" | "yarn";
349
+ field?: "dependencies" | "devDependencies";
350
+ build?: string;
351
+ config?: string;
352
+ configuration?: string;
353
+ }> = {},
354
+ ): Readonly<Record<string, string>> {
355
+ const manager = options.manager ?? "npm";
356
+ const [packageManager, lockfile] = CORPUS_MANAGERS[manager];
357
+ const next = options.declared ?? "16.3.8";
358
+ const react = { react: "19.1.2", "react-dom": "19.1.2" };
359
+ const runtime = options.field === "devDependencies" ? react : { next, ...react };
360
+ return Object.freeze({
361
+ "ohmyhost.yaml":
362
+ options.configuration ?? corpusConfiguration(manager, ".open-next/assets", "edge"),
363
+ "package.json": corpusJson({
364
+ name: "website",
365
+ version: "1.0.0",
366
+ private: true,
367
+ packageManager,
368
+ scripts: { dev: "next dev", build: options.build ?? "next build" },
369
+ dependencies: runtime,
370
+ ...(options.field === "devDependencies" ? { devDependencies: { next } } : {}),
371
+ }),
372
+ [lockfile]: corpusLockfile(
373
+ manager,
374
+ { next, ...react },
375
+ { next: options.resolved ?? next, ...react },
376
+ ),
377
+ "next.config.ts": options.config ?? CORPUS_NEXT_CONFIG,
378
+ "src/app/page.tsx": CORPUS_NEXT_PAGE,
379
+ });
380
+ }
381
+
382
+ /**
383
+ * The next.config.ts and Tailwind CSS files and packages `create-next-app@16.4.0 --yes` writes, in
384
+ * an App Router source with the packageManager, lockfile and ohmyhost.yaml ohmyho.st requires;
385
+ * `tailwind: false` is its --no-tailwind output. On Turbopack, its default bundler, it adds only the
386
+ * Turbopack rule for Tailwind CSS; `postcss` adds the postcss.config.mjs and @tailwindcss/postcss it
387
+ * writes for any other bundler.
388
+ */
389
+ function createNextAppSource(
390
+ options: Readonly<{ tailwind: boolean; postcss?: boolean }>,
391
+ ): Readonly<Record<string, string>> {
392
+ const [packageManager, lockfile] = CORPUS_MANAGERS.npm;
393
+ const runtime = { next: "16.4.0", react: "19.3.0", "react-dom": "19.3.0" };
394
+ const postcss = options.postcss === true;
395
+ return Object.freeze({
396
+ "ohmyhost.yaml": corpusConfiguration("npm", ".open-next/assets", "edge"),
397
+ "package.json": corpusJson({
398
+ name: "website",
399
+ version: "0.1.0",
400
+ private: true,
401
+ packageManager,
402
+ scripts: { dev: "next dev", build: "next build", start: "next start" },
403
+ dependencies: runtime,
404
+ ...(options.tailwind
405
+ ? {
406
+ devDependencies: {
407
+ ...(postcss ? { "@tailwindcss/postcss": "^4" } : {}),
408
+ "@tailwindcss/turbopack": "^4",
409
+ tailwindcss: "^4",
410
+ },
411
+ }
412
+ : {}),
413
+ }),
414
+ [lockfile]: corpusLockfile("npm", runtime, runtime),
415
+ "next.config.ts": options.tailwind
416
+ ? CREATE_NEXT_APP_CONFIG.replace("};\n", `${CREATE_NEXT_APP_TAILWIND_RULE}};\n`)
417
+ : CREATE_NEXT_APP_CONFIG,
418
+ ...(postcss ? { "postcss.config.mjs": CREATE_NEXT_APP_POSTCSS_CONFIG } : {}),
419
+ "app/globals.css": options.tailwind ? '@import "tailwindcss";\n' : "body {\n margin: 0;\n}\n",
420
+ "app/layout.tsx": CREATE_NEXT_APP_LAYOUT,
421
+ "app/page.tsx": CORPUS_NEXT_PAGE,
422
+ });
423
+ }
424
+
425
+ function viteSource(
426
+ options: Readonly<{ declared?: string; resolved?: string; config?: string }> = {},
427
+ ): Readonly<Record<string, string>> {
428
+ const vite = options.declared ?? "8.2.2";
429
+ return Object.freeze({
430
+ "ohmyhost.yaml": corpusConfiguration("pnpm", "dist", "static"),
431
+ "package.json": corpusJson({
432
+ name: "website",
433
+ private: true,
434
+ packageManager: CORPUS_MANAGERS.pnpm[0],
435
+ scripts: { build: "vite build" },
436
+ devDependencies: { vite },
437
+ }),
438
+ "pnpm-lock.yaml": corpusLockfile("pnpm", { vite }, { vite: options.resolved ?? vite }, [
439
+ "vite",
440
+ ]),
441
+ "index.html":
442
+ '<!doctype html>\n<div id="root"></div>\n<script type="module" src="/src/main.ts"></script>\n',
443
+ "src/main.ts": 'document.querySelector("#root")?.append("Hello from ohmyho.st");\n',
444
+ "vite.config.ts":
445
+ options.config ??
446
+ 'import { defineConfig } from "vite";\n\nexport default defineConfig({});\n',
447
+ });
448
+ }
449
+
450
+ function tanStackSource(
451
+ options: Readonly<{ start?: string; config?: string }> = {},
452
+ ): Readonly<Record<string, string>> {
453
+ const declared = {
454
+ "@tanstack/react-router": "1.170.41",
455
+ "@tanstack/react-start": options.start ?? "1.168.60",
456
+ react: "19.2.8",
457
+ "react-dom": "19.2.8",
458
+ };
459
+ const development = { "@vitejs/plugin-react": "6.1.0", vite: "8.2.2" };
460
+ return Object.freeze({
461
+ "ohmyhost.yaml": corpusConfiguration("pnpm", "dist/client", "static"),
462
+ "package.json": corpusJson({
463
+ name: "website",
464
+ private: true,
465
+ packageManager: CORPUS_MANAGERS.pnpm[0],
466
+ scripts: { build: "vite build" },
467
+ dependencies: declared,
468
+ devDependencies: development,
469
+ }),
470
+ "pnpm-lock.yaml": corpusLockfile(
471
+ "pnpm",
472
+ { ...declared, ...development },
473
+ {
474
+ "@tanstack/react-router": `${declared["@tanstack/react-router"]}(react-dom@19.2.8(react@19.2.8))(react@19.2.8)`,
475
+ "@tanstack/react-start": `${declared["@tanstack/react-start"]}(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vite@8.2.2)`,
476
+ react: "19.2.8",
477
+ "react-dom": "19.2.8(react@19.2.8)",
478
+ "@vitejs/plugin-react": "6.1.0(vite@8.2.2)",
479
+ vite: "8.2.2",
480
+ },
481
+ ["@vitejs/plugin-react", "vite"],
482
+ ),
483
+ "vite.config.ts": options.config ?? CORPUS_TANSTACK_CONFIG,
484
+ "src/routes/index.tsx":
485
+ 'import { createFileRoute } from "@tanstack/react-router";\n\nexport const Route = createFileRoute("/")({ component: () => <main>Hello</main> });\n',
486
+ });
487
+ }