@cyanheads/mcp-ts-core 0.13.8 → 0.13.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.
- package/AGENTS.md +51 -24
- package/CLAUDE.md +51 -24
- package/README.md +10 -10
- package/changelog/0.13.x/0.13.10.md +118 -0
- package/changelog/0.13.x/0.13.9.md +113 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +31 -9
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +6 -3
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +21 -5
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +114 -21
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +40 -0
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/serverManifest.d.ts +6 -0
- package/dist/core/serverManifest.d.ts.map +1 -1
- package/dist/core/serverManifest.js +6 -0
- package/dist/core/serverManifest.js.map +1 -1
- package/dist/core/worker.d.ts +6 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +1 -0
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/error-contract-rules.d.ts +3 -44
- package/dist/linter/rules/error-contract-rules.d.ts.map +1 -1
- package/dist/linter/rules/error-contract-rules.js +8 -144
- package/dist/linter/rules/error-contract-rules.js.map +1 -1
- package/dist/linter/rules/index.d.ts +1 -1
- package/dist/linter/rules/index.d.ts.map +1 -1
- package/dist/linter/rules/index.js +1 -1
- package/dist/linter/rules/index.js.map +1 -1
- package/dist/linter/rules/resource-rules.d.ts.map +1 -1
- package/dist/linter/rules/resource-rules.js +1 -2
- package/dist/linter/rules/resource-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts +2 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +37 -3
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +26 -13
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +32 -17
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/inputRequired.d.ts +133 -12
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +192 -20
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts +10 -2
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +49 -11
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/resources/resource-registration.d.ts +4 -2
- package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
- package/dist/mcp-server/resources/resource-registration.js +6 -4
- package/dist/mcp-server/resources/resource-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +4 -3
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +42 -14
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/server.d.ts +9 -0
- package/dist/mcp-server/server.d.ts.map +1 -1
- package/dist/mcp-server/server.js +14 -13
- package/dist/mcp-server/server.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts +7 -3
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +9 -5
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +163 -40
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +330 -114
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +40 -19
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +387 -130
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +7 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +65 -9
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.js +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.js.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.d.ts +6 -2
- package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.js +7 -3
- package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +82 -18
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +620 -328
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts +11 -7
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.js +19 -16
- package/dist/services/canvas/providers/duckdb/exportWriter.js.map +1 -1
- package/dist/services/mirror/core/defineMirror.d.ts +1 -0
- package/dist/services/mirror/core/defineMirror.d.ts.map +1 -1
- package/dist/services/mirror/core/defineMirror.js +1 -0
- package/dist/services/mirror/core/defineMirror.js.map +1 -1
- package/dist/testing/index.d.ts +17 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +21 -7
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +18 -15
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +5 -4
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +9 -7
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +3 -1
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/performance.d.ts +4 -2
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +8 -6
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/internal/telemetryMessages.d.ts +0 -1
- package/dist/utils/internal/telemetryMessages.d.ts.map +1 -1
- package/dist/utils/internal/telemetryMessages.js +0 -1
- package/dist/utils/internal/telemetryMessages.js.map +1 -1
- package/dist/utils/network/pacer.d.ts +38 -5
- package/dist/utils/network/pacer.d.ts.map +1 -1
- package/dist/utils/network/pacer.js +87 -25
- package/dist/utils/network/pacer.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +10 -5
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +10 -5
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +3 -3
- package/framework-skills/add-export/SKILL.md +5 -16
- package/framework-skills/add-prompt/SKILL.md +7 -3
- package/framework-skills/add-resource/SKILL.md +7 -5
- package/framework-skills/add-service/SKILL.md +3 -12
- package/framework-skills/add-test/SKILL.md +6 -3
- package/framework-skills/add-tool/SKILL.md +40 -42
- package/framework-skills/api-auth/SKILL.md +2 -2
- package/framework-skills/api-canvas/SKILL.md +17 -8
- package/framework-skills/api-config/SKILL.md +5 -4
- package/framework-skills/api-context/SKILL.md +168 -42
- package/framework-skills/api-errors/SKILL.md +48 -51
- package/framework-skills/api-linter/SKILL.md +30 -35
- package/framework-skills/api-mirror/SKILL.md +2 -1
- package/framework-skills/api-telemetry/SKILL.md +14 -10
- package/framework-skills/api-testing/SKILL.md +43 -11
- package/framework-skills/api-utils/SKILL.md +2 -2
- package/framework-skills/api-workers/SKILL.md +3 -1
- package/framework-skills/design-mcp-server/SKILL.md +6 -6
- package/framework-skills/field-test/SKILL.md +5 -5
- package/framework-skills/git-wrapup/SKILL.md +8 -6
- package/framework-skills/orchestrations/SKILL.md +7 -6
- package/framework-skills/orchestrations/workflows/field-test-fix.md +9 -19
- package/framework-skills/orchestrations/workflows/fix-wrapup-release.md +7 -7
- package/framework-skills/orchestrations/workflows/greenfield-build.md +8 -5
- package/framework-skills/orchestrations/workflows/maintenance-release.md +8 -8
- package/framework-skills/polish-docs-meta/SKILL.md +4 -4
- package/framework-skills/release-and-publish/SKILL.md +8 -6
- package/framework-skills/release-pr-review/SKILL.md +38 -24
- package/framework-skills/report-issue-framework/SKILL.md +7 -5
- package/framework-skills/report-issue-local/SKILL.md +8 -6
- package/framework-skills/security-pass/SKILL.md +14 -13
- package/package.json +6 -5
- package/scripts/devcheck.ts +7 -6
- package/scripts/install-otel.ts +84 -0
- package/scripts/lint-mcp.ts +87 -27
- package/scripts/lint-packaging.ts +226 -4
- package/scripts/release-github.ts +117 -5
- package/templates/.env.example +2 -0
- package/templates/AGENTS.md +5 -4
- package/templates/CLAUDE.md +5 -4
- package/templates/Dockerfile +67 -50
- package/templates/_.mcpbignore +2 -0
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +3 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/mcp-ts-core",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.10",
|
|
4
4
|
"mcpName": "io.github.cyanheads/mcp-ts-core",
|
|
5
5
|
"description": "Agent-native TypeScript framework for MCP servers. Includes runtime infrastructure and agent skills for building, testing, and shipping servers.",
|
|
6
6
|
"files": [
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"scripts/clean-mcpb.ts",
|
|
17
17
|
"scripts/clean.ts",
|
|
18
18
|
"scripts/devcheck.ts",
|
|
19
|
+
"scripts/install-otel.ts",
|
|
19
20
|
"scripts/lint-mcp.ts",
|
|
20
21
|
"scripts/lint-packaging.ts",
|
|
21
22
|
"scripts/list-skills.ts",
|
|
@@ -198,10 +199,10 @@
|
|
|
198
199
|
"devDependencies": {
|
|
199
200
|
"@biomejs/biome": "2.5.14",
|
|
200
201
|
"@cloudflare/vitest-pool-workers": "^0.22.0",
|
|
201
|
-
"@cloudflare/workers-types": "5.
|
|
202
|
+
"@cloudflare/workers-types": "5.20260924.1",
|
|
202
203
|
"@duckdb/node-api": "^1.5.5-r.5",
|
|
203
204
|
"@hono/otel": "^1.1.2",
|
|
204
|
-
"@modelcontextprotocol/client": "^2.
|
|
205
|
+
"@modelcontextprotocol/client": "^2.1.0",
|
|
205
206
|
"@opentelemetry/api-logs": "^0.222.0",
|
|
206
207
|
"@opentelemetry/exporter-logs-otlp-http": "^0.222.0",
|
|
207
208
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.222.0",
|
|
@@ -290,9 +291,9 @@
|
|
|
290
291
|
},
|
|
291
292
|
"dependencies": {
|
|
292
293
|
"@hono/node-server": "^2.1.1",
|
|
293
|
-
"@modelcontextprotocol/server": "^2.
|
|
294
|
+
"@modelcontextprotocol/server": "^2.1.0",
|
|
294
295
|
"@opentelemetry/api": "^1.9.1",
|
|
295
|
-
"hono": "^4.13.
|
|
296
|
+
"hono": "^4.13.9",
|
|
296
297
|
"jose": "^6.2.12",
|
|
297
298
|
"pino": "^10.3.1",
|
|
298
299
|
"zod": "^4.6.5"
|
package/scripts/devcheck.ts
CHANGED
|
@@ -711,12 +711,12 @@ const ALL_CHECKS: Check[] = [
|
|
|
711
711
|
canFix: false,
|
|
712
712
|
// Validates env var alignment between manifest.json (MCPB bundle) and
|
|
713
713
|
// server.json (MCP Registry), plus plugin marketplace manifests (#240), the
|
|
714
|
-
// bundle-content guards on .mcpbignore (#343),
|
|
715
|
-
// (#418). Runs when any of those inputs
|
|
716
|
-
// none exist — consumers on an HTTP-only
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
// only covered incidentally.
|
|
714
|
+
// bundle-content guards on .mcpbignore (#343), the README version badge
|
|
715
|
+
// (#418), and the Dockerfile build platform. Runs when any of those inputs
|
|
716
|
+
// is present; skipped cleanly when none exist — consumers on an HTTP-only
|
|
717
|
+
// deploy are unaffected. README.md and Dockerfile are triggers in their own
|
|
718
|
+
// right: each check must gate a project that carries no bundle or plugin
|
|
719
|
+
// metadata at all, which the other inputs only covered incidentally.
|
|
720
720
|
getCommand: () => {
|
|
721
721
|
const inputs = [
|
|
722
722
|
'manifest.json',
|
|
@@ -725,6 +725,7 @@ const ALL_CHECKS: Check[] = [
|
|
|
725
725
|
'.codex-plugin/mcp.json',
|
|
726
726
|
'.mcpbignore',
|
|
727
727
|
'README.md',
|
|
728
|
+
'Dockerfile',
|
|
728
729
|
];
|
|
729
730
|
if (!inputs.some((input) => existsSync(path.join(ROOT_DIR, input)))) return null;
|
|
730
731
|
return ['bun', 'run', 'scripts/lint-packaging.ts'];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Installs the OpenTelemetry packages the framework loads at
|
|
4
|
+
* runtime into a production dependency tree — the Dockerfile's OTel step.
|
|
5
|
+
*
|
|
6
|
+
* The list is every `peerDependencies` entry named `@opentelemetry/*`, plus
|
|
7
|
+
* `@hono/otel`, in `@cyanheads/mcp-ts-core/package.json` resolved from the
|
|
8
|
+
* project root: the installed framework in a server, the package itself (by
|
|
9
|
+
* self-reference) in the framework. Each package moves into the project's
|
|
10
|
+
* `dependencies` at its declared peer range, out of `peerDependencies`,
|
|
11
|
+
* `peerDependenciesMeta`, and `devDependencies`, so `--omit=peer` keeps it
|
|
12
|
+
* while every other optional peer stays out. Then `bun install --omit=dev
|
|
13
|
+
* --omit=peer --ignore-scripts` runs with every argument given to this script
|
|
14
|
+
* appended, under the project's `bunfig.toml`, and its exit code is this
|
|
15
|
+
* script's. Not `--production`: it implies `--frozen-lockfile`, which fails
|
|
16
|
+
* once the manifest gains packages the lockfile lacks.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // In a Dockerfile stage on $BUILDPLATFORM, cross-installing for the target:
|
|
20
|
+
* // bun scripts/install-otel.ts --os=linux --cpu=x64
|
|
21
|
+
* @module scripts/install-otel
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { join, resolve } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
interface Manifest {
|
|
30
|
+
dependencies?: Record<string, string>;
|
|
31
|
+
devDependencies?: Record<string, string>;
|
|
32
|
+
peerDependencies?: Record<string, string>;
|
|
33
|
+
peerDependenciesMeta?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The OTel optional peers a framework manifest declares, keyed by name, at their declared ranges. */
|
|
37
|
+
export function otelPeers(framework: Manifest): Record<string, string> {
|
|
38
|
+
return Object.fromEntries(
|
|
39
|
+
Object.entries(framework.peerDependencies ?? {}).filter(
|
|
40
|
+
([name]) => name.startsWith('@opentelemetry/') || name === '@hono/otel',
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function main(args: string[]): number {
|
|
46
|
+
const manifestPath = join(process.cwd(), 'package.json');
|
|
47
|
+
const frameworkPath = createRequire(manifestPath).resolve('@cyanheads/mcp-ts-core/package.json');
|
|
48
|
+
const peers = otelPeers(JSON.parse(readFileSync(frameworkPath, 'utf8')) as Manifest);
|
|
49
|
+
const names = Object.keys(peers);
|
|
50
|
+
if (names.length === 0) {
|
|
51
|
+
console.error(
|
|
52
|
+
`install-otel: ${frameworkPath} declares no @opentelemetry/* or @hono/otel peerDependencies — nothing to install.`,
|
|
53
|
+
);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Manifest;
|
|
58
|
+
manifest.dependencies = { ...manifest.dependencies, ...peers };
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
delete manifest.peerDependencies?.[name];
|
|
61
|
+
delete manifest.peerDependenciesMeta?.[name];
|
|
62
|
+
delete manifest.devDependencies?.[name];
|
|
63
|
+
}
|
|
64
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
65
|
+
console.log(
|
|
66
|
+
`install-otel: ${names.length} OpenTelemetry packages from ${frameworkPath}: ` +
|
|
67
|
+
names.map((name) => `${name}@${peers[name]}`).join(' '),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const install = spawnSync(
|
|
71
|
+
'bun',
|
|
72
|
+
['install', '--omit=dev', '--omit=peer', '--ignore-scripts', ...args],
|
|
73
|
+
{ stdio: 'inherit' },
|
|
74
|
+
);
|
|
75
|
+
if (install.error) {
|
|
76
|
+
console.error(`install-otel: could not run bun install: ${install.error.message}`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
return install.status ?? 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
83
|
+
process.exit(main(process.argv.slice(2)));
|
|
84
|
+
}
|
package/scripts/lint-mcp.ts
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* 3. Extracts exported definitions by duck-typing (has name/handler/input etc.)
|
|
13
13
|
* 4. Feeds them into `validateDefinitions()`
|
|
14
14
|
*
|
|
15
|
+
* A definition file whose `import()` rejects, and a `server.json` that exists
|
|
16
|
+
* but does not parse, are errors (`definition-import-failed`,
|
|
17
|
+
* `server-json-parse`): what they declare cannot be checked, so the run fails
|
|
18
|
+
* instead of passing without them. The remaining files are still linted.
|
|
19
|
+
*
|
|
15
20
|
* Runtime-agnostic: works with bun, tsx, and Node.js (via ts-node/esm).
|
|
16
21
|
*
|
|
17
22
|
* Rule knobs come from the project's `devcheck.config.json` `lint` block, so one
|
|
@@ -20,7 +25,7 @@
|
|
|
20
25
|
* @module scripts/lint-mcp
|
|
21
26
|
*/
|
|
22
27
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
23
|
-
import { join, resolve } from 'node:path';
|
|
28
|
+
import { join, relative, resolve } from 'node:path';
|
|
24
29
|
import { fileURLToPath } from 'node:url';
|
|
25
30
|
|
|
26
31
|
// ---------------------------------------------------------------------------
|
|
@@ -100,19 +105,64 @@ function discoverFiles(): string[] {
|
|
|
100
105
|
return [...new Set(files)];
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Load failures
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/** Where the rule reference lives — the breadcrumb `validateDefinitions()` appends too. */
|
|
113
|
+
const SKILL_REFERENCE_PATH = 'framework-skills/api-linter/SKILL.md';
|
|
114
|
+
|
|
115
|
+
/** A file the CLI could not load, shaped like the diagnostics it is printed beside. */
|
|
116
|
+
interface LoadFailure {
|
|
117
|
+
message: string;
|
|
118
|
+
rule: 'definition-import-failed' | 'server-json-parse';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The text of a thrown value. An `AggregateError` keeps its causes in `errors`
|
|
123
|
+
* — Bun rejects a definition that fails to transpile with one whose own message
|
|
124
|
+
* only counts them ("2 errors building …") — so each cause is appended.
|
|
125
|
+
*/
|
|
126
|
+
function describeError(err: unknown): string {
|
|
127
|
+
if (!(err instanceof Error)) return String(err);
|
|
128
|
+
const message = err.message || err.name;
|
|
129
|
+
if (!(err instanceof AggregateError) || err.errors.length === 0) return message;
|
|
130
|
+
return `${message} (${err.errors.map(describeError).join('; ')})`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function loadFailure(rule: LoadFailure['rule'], file: string, err: unknown): LoadFailure {
|
|
134
|
+
const anchor = rule === 'server-json-parse' ? 'server-json-rules' : rule;
|
|
135
|
+
return {
|
|
136
|
+
rule,
|
|
137
|
+
message: `${file}: ${describeError(err)}\nSee: ${SKILL_REFERENCE_PATH}#${anchor}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
103
141
|
// ---------------------------------------------------------------------------
|
|
104
142
|
// Main
|
|
105
143
|
// ---------------------------------------------------------------------------
|
|
106
144
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
145
|
+
/** A parsed JSON file, or the error an existing one failed to read or parse with. */
|
|
146
|
+
type JsonRead = { ok: true; value: unknown } | { ok: false; error: unknown };
|
|
147
|
+
|
|
148
|
+
/** Reads and parses a JSON file. `undefined` when the file does not exist. */
|
|
149
|
+
function readJson(path: string): JsonRead | undefined {
|
|
150
|
+
if (!existsSync(path)) return;
|
|
109
151
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
152
|
+
return { ok: true, value: JSON.parse(readFileSync(path, 'utf-8')) };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return { ok: false, error };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Try to read and parse a JSON file. Returns undefined when absent, and warns when unparseable. */
|
|
159
|
+
function tryReadJson(path: string): unknown {
|
|
160
|
+
const read = readJson(path);
|
|
161
|
+
if (read?.ok === false) {
|
|
162
|
+
console.warn(`Warning: Failed to parse ${path}: ${describeError(read.error)}`);
|
|
114
163
|
return;
|
|
115
164
|
}
|
|
165
|
+
return read?.value;
|
|
116
166
|
}
|
|
117
167
|
|
|
118
168
|
/** The `lint` block of `devcheck.config.json`, as far as this script reads it. */
|
|
@@ -149,12 +199,17 @@ export function readLintOptions(configPath = resolve('devcheck.config.json')): L
|
|
|
149
199
|
|
|
150
200
|
async function main(): Promise<void> {
|
|
151
201
|
const files = discoverFiles();
|
|
202
|
+
const failures: LoadFailure[] = [];
|
|
152
203
|
|
|
153
204
|
// Discover server.json and package.json at project root
|
|
154
|
-
const
|
|
205
|
+
const serverJsonRead = readJson(resolve('server.json'));
|
|
206
|
+
if (serverJsonRead?.ok === false) {
|
|
207
|
+
failures.push(loadFailure('server-json-parse', 'server.json', serverJsonRead.error));
|
|
208
|
+
}
|
|
209
|
+
const serverJson = serverJsonRead?.ok ? serverJsonRead.value : undefined;
|
|
155
210
|
const packageJson = tryReadJson(resolve('package.json')) as { version?: string } | undefined;
|
|
156
211
|
|
|
157
|
-
if (files.length === 0 && serverJson == null) {
|
|
212
|
+
if (files.length === 0 && serverJson == null && failures.length === 0) {
|
|
158
213
|
console.log('No MCP definition files or server.json found. Skipping lint.');
|
|
159
214
|
process.exit(0);
|
|
160
215
|
}
|
|
@@ -162,36 +217,42 @@ async function main(): Promise<void> {
|
|
|
162
217
|
const tools: unknown[] = [];
|
|
163
218
|
const resources: unknown[] = [];
|
|
164
219
|
const prompts: unknown[] = [];
|
|
220
|
+
let failedImports = 0;
|
|
165
221
|
|
|
166
222
|
for (const file of files) {
|
|
223
|
+
let mod: Record<string, unknown>;
|
|
167
224
|
try {
|
|
168
|
-
|
|
169
|
-
for (const exported of Object.values(mod)) {
|
|
170
|
-
if (isToolLike(exported)) tools.push(exported);
|
|
171
|
-
else if (isResourceLike(exported)) resources.push(exported);
|
|
172
|
-
else if (isPromptLike(exported)) prompts.push(exported);
|
|
173
|
-
}
|
|
225
|
+
mod = await import(file);
|
|
174
226
|
} catch (err) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
227
|
+
failures.push(loadFailure('definition-import-failed', relative(process.cwd(), file), err));
|
|
228
|
+
failedImports++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
for (const exported of Object.values(mod)) {
|
|
232
|
+
if (isToolLike(exported)) tools.push(exported);
|
|
233
|
+
else if (isResourceLike(exported)) resources.push(exported);
|
|
234
|
+
else if (isPromptLike(exported)) prompts.push(exported);
|
|
178
235
|
}
|
|
179
236
|
}
|
|
180
237
|
|
|
181
238
|
const defTotal = tools.length + resources.length + prompts.length;
|
|
182
|
-
if (defTotal === 0 && serverJson == null) {
|
|
239
|
+
if (defTotal === 0 && serverJson == null && failures.length === 0) {
|
|
183
240
|
console.log(`Scanned ${files.length} files but found no definitions. Skipping lint.`);
|
|
184
241
|
process.exit(0);
|
|
185
242
|
}
|
|
186
243
|
|
|
187
244
|
const parts: string[] = [];
|
|
188
|
-
if (defTotal > 0) {
|
|
245
|
+
if (defTotal > 0 || failedImports > 0) {
|
|
246
|
+
const fileCount =
|
|
247
|
+
failedImports === 0
|
|
248
|
+
? `${files.length}`
|
|
249
|
+
: `${files.length - failedImports} of ${files.length}`;
|
|
189
250
|
parts.push(
|
|
190
|
-
`${tools.length} tool(s), ${resources.length} resource(s), ${prompts.length} prompt(s) from ${
|
|
251
|
+
`${tools.length} tool(s), ${resources.length} resource(s), ${prompts.length} prompt(s) from ${fileCount} file(s)`,
|
|
191
252
|
);
|
|
192
253
|
}
|
|
193
254
|
if (serverJson != null) parts.push('server.json');
|
|
194
|
-
console.log(`Linting ${parts.join(' + ')}...`);
|
|
255
|
+
if (parts.length > 0) console.log(`Linting ${parts.join(' + ')}...`);
|
|
195
256
|
|
|
196
257
|
const report = validateDefinitions({
|
|
197
258
|
tools,
|
|
@@ -202,14 +263,15 @@ async function main(): Promise<void> {
|
|
|
202
263
|
...readLintOptions(),
|
|
203
264
|
});
|
|
204
265
|
|
|
266
|
+
const errors = [...failures, ...report.errors];
|
|
205
267
|
for (const w of report.warnings) {
|
|
206
268
|
console.warn(` ⚠ [${w.rule}] ${w.message}`);
|
|
207
269
|
}
|
|
208
|
-
for (const e of
|
|
270
|
+
for (const e of errors) {
|
|
209
271
|
console.error(` ✗ [${e.rule}] ${e.message}`);
|
|
210
272
|
}
|
|
211
273
|
|
|
212
|
-
if (
|
|
274
|
+
if (errors.length === 0) {
|
|
213
275
|
if (report.warnings.length > 0) {
|
|
214
276
|
console.log(`\nPassed with ${report.warnings.length} warning(s).`);
|
|
215
277
|
} else {
|
|
@@ -217,9 +279,7 @@ async function main(): Promise<void> {
|
|
|
217
279
|
}
|
|
218
280
|
process.exit(0);
|
|
219
281
|
} else {
|
|
220
|
-
console.error(
|
|
221
|
-
`\nFailed: ${report.errors.length} error(s), ${report.warnings.length} warning(s).`,
|
|
222
|
-
);
|
|
282
|
+
console.error(`\nFailed: ${errors.length} error(s), ${report.warnings.length} warning(s).`);
|
|
223
283
|
process.exit(1);
|
|
224
284
|
}
|
|
225
285
|
}
|
|
@@ -65,6 +65,21 @@
|
|
|
65
65
|
* so without the entry a release that bundles before publishing ships the
|
|
66
66
|
* server and its production dependencies inside the npm tarball
|
|
67
67
|
* (issue #469). Skipped when `manifest.json` or `files` is absent.
|
|
68
|
+
* 14. manifest.json version parity: `version` must equal `package.json`'s, the
|
|
69
|
+
* same rule check 10 applies to the plugin manifests — the bundle's install
|
|
70
|
+
* dialog shows it. Skipped when `manifest.json` or the package version is
|
|
71
|
+
* absent.
|
|
72
|
+
* 15. Dockerfile build platform: a stage that does not start
|
|
73
|
+
* `FROM --platform=$BUILDPLATFORM` must not run JavaScript while it
|
|
74
|
+
* builds — no `RUN` invoking `bun` for anything but `install`/`add`
|
|
75
|
+
* (`bun run build`, `bun -e`, a script, `bunx`), and no `bun install`/
|
|
76
|
+
* `bun add` once `bunfig.toml` is in the stage, since its security scanner
|
|
77
|
+
* runs as a Bun program. The non-native leg of a multi-arch `docker buildx`
|
|
78
|
+
* build runs such a stage under QEMU, where bun >= 1.4 aborts and no image
|
|
79
|
+
* publishes for either architecture — and the image publishes last, after
|
|
80
|
+
* npm and the MCP Registry. `HEALTHCHECK`/`CMD`/`ENTRYPOINT` run at
|
|
81
|
+
* container start and never count. One error per stage, naming each
|
|
82
|
+
* offending line (issue #575). Skipped when there is no `Dockerfile`.
|
|
68
83
|
*
|
|
69
84
|
* Every check skips cleanly when its input is absent — consumers who deleted
|
|
70
85
|
* `manifest.json` for an HTTP-only deploy, or who haven't built a bundle,
|
|
@@ -103,6 +118,7 @@ interface Manifest {
|
|
|
103
118
|
name?: string;
|
|
104
119
|
server?: { mcp_config?: { args?: unknown[]; env?: Record<string, string> } };
|
|
105
120
|
user_config?: Record<string, ManifestUserConfigEntry>;
|
|
121
|
+
version?: unknown;
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
const USER_CONFIG_REF = /^\$\{user_config\.([\w-]+)\}$/;
|
|
@@ -172,10 +188,10 @@ function tryReadJson<T>(path: string): T | undefined {
|
|
|
172
188
|
* to evaluate which paths survive the ignore rules. Returns an array of error
|
|
173
189
|
* strings; empty means all checks passed.
|
|
174
190
|
*
|
|
175
|
-
* **Context note:**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
191
|
+
* **Context note:** `ignore` is a devDependency of the framework and of every
|
|
192
|
+
* scaffold (`templates/package.json`), so it resolves wherever
|
|
193
|
+
* `bun run lint:packaging` runs against an `.mcpbignore` — a server project, or
|
|
194
|
+
* mcp-ts-core itself. Where it cannot load, the guard is skipped.
|
|
179
195
|
*/
|
|
180
196
|
interface IgnoreMatcher {
|
|
181
197
|
add(patterns: string[]): IgnoreMatcher;
|
|
@@ -775,6 +791,205 @@ export function checkBundleExcludedFromFiles(files: unknown): string[] {
|
|
|
775
791
|
];
|
|
776
792
|
}
|
|
777
793
|
|
|
794
|
+
/**
|
|
795
|
+
* Check 14: manifest.json `version` must equal `package.json`'s. Skipped when
|
|
796
|
+
* the package version is absent — the same fail-safe as checks 10 and 12.
|
|
797
|
+
*/
|
|
798
|
+
export function checkManifestVersion(manifest: Manifest, packageVersion?: string): string[] {
|
|
799
|
+
if (!packageVersion || manifest.version === packageVersion) return [];
|
|
800
|
+
return [
|
|
801
|
+
manifest.version === undefined
|
|
802
|
+
? `manifest.json has no "version" — must declare the package.json version "${packageVersion}"`
|
|
803
|
+
: `manifest.json "version" is "${String(manifest.version)}" — must equal the package.json version "${packageVersion}"`,
|
|
804
|
+
];
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** One Dockerfile instruction: continuations joined, a `RUN` heredoc body folded in. */
|
|
808
|
+
interface DockerInstruction {
|
|
809
|
+
args: string;
|
|
810
|
+
keyword: string;
|
|
811
|
+
/** 1-based line the instruction starts on. */
|
|
812
|
+
line: number;
|
|
813
|
+
text: string;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const DOCKERFILE_SKIPPED_LINE = /^\s*(?:#|$)/;
|
|
817
|
+
const DOCKERFILE_CONTINUATION = /\\\s*$/;
|
|
818
|
+
const DOCKERFILE_HEREDOC = /<<-?(["']?)([A-Za-z_]\w*)\1/g;
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Splits a Dockerfile into instructions the way BuildKit reads it: a trailing
|
|
822
|
+
* `\` continues onto the next line, comment and blank lines inside a
|
|
823
|
+
* continuation are dropped, and a `RUN` heredoc's body belongs to its `RUN`.
|
|
824
|
+
*/
|
|
825
|
+
function dockerInstructions(dockerfile: string): DockerInstruction[] {
|
|
826
|
+
const lines = dockerfile.split(/\r?\n/);
|
|
827
|
+
const instructions: DockerInstruction[] = [];
|
|
828
|
+
for (let i = 0; i < lines.length; i++) {
|
|
829
|
+
if (DOCKERFILE_SKIPPED_LINE.test(lines[i] ?? '')) continue;
|
|
830
|
+
const line = i + 1;
|
|
831
|
+
let text = (lines[i] ?? '').trim();
|
|
832
|
+
while (DOCKERFILE_CONTINUATION.test(text) && i + 1 < lines.length) {
|
|
833
|
+
text = text.replace(DOCKERFILE_CONTINUATION, ' ');
|
|
834
|
+
do i++;
|
|
835
|
+
while (i < lines.length && DOCKERFILE_SKIPPED_LINE.test(lines[i] ?? ''));
|
|
836
|
+
text += (lines[i] ?? '').trim();
|
|
837
|
+
}
|
|
838
|
+
const keyword = (text.split(/\s/, 1)[0] ?? '').toUpperCase();
|
|
839
|
+
if (keyword === 'RUN') {
|
|
840
|
+
for (const [, , delimiter] of text.matchAll(DOCKERFILE_HEREDOC)) {
|
|
841
|
+
while (i + 1 < lines.length && (lines[++i] ?? '').trim() !== delimiter) {
|
|
842
|
+
text += `\n${lines[i]}`;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
instructions.push({ keyword, args: text.slice(keyword.length).trim(), line, text });
|
|
847
|
+
}
|
|
848
|
+
return instructions;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Words that may precede the command itself in a simple shell command. */
|
|
852
|
+
const SHELL_COMMAND_PREFIX = new Set([
|
|
853
|
+
'!',
|
|
854
|
+
'command',
|
|
855
|
+
'do',
|
|
856
|
+
'elif',
|
|
857
|
+
'else',
|
|
858
|
+
'env',
|
|
859
|
+
'exec',
|
|
860
|
+
'if',
|
|
861
|
+
'then',
|
|
862
|
+
'time',
|
|
863
|
+
'until',
|
|
864
|
+
'while',
|
|
865
|
+
]);
|
|
866
|
+
|
|
867
|
+
/** A `bun`/`bunx` invocation in a `RUN`: the command word and the words after it. */
|
|
868
|
+
interface BunInvocation {
|
|
869
|
+
args: string[];
|
|
870
|
+
command: 'bun' | 'bunx';
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function bunCommand(word: string | undefined): BunInvocation['command'] | undefined {
|
|
874
|
+
const name = word?.split('/').pop();
|
|
875
|
+
return name === 'bun' || name === 'bunx' ? name : undefined;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The `bun`/`bunx` invocations a `RUN` instruction's command makes, read from
|
|
880
|
+
* command position only — `chown bun:bun`, `/root/.bun/`, and a quoted
|
|
881
|
+
* mention are not invocations. Quoted strings are opaque, so a `bun -e '…'`
|
|
882
|
+
* program never splits into commands of its own.
|
|
883
|
+
*/
|
|
884
|
+
function bunInvocations(runArgs: string): BunInvocation[] {
|
|
885
|
+
const command = runArgs.replace(/^(?:--[\w-]+=\S+\s+)*/, '');
|
|
886
|
+
if (command.startsWith('[')) {
|
|
887
|
+
try {
|
|
888
|
+
const argv: unknown = JSON.parse(command);
|
|
889
|
+
if (Array.isArray(argv)) {
|
|
890
|
+
const name = bunCommand(String(argv[0]));
|
|
891
|
+
return name ? [{ command: name, args: argv.slice(1).map(String) }] : [];
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
// Not a JSON exec form: fall through and read it as shell.
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return command
|
|
898
|
+
.replace(/'[^']*'|"(?:\\.|[^"\\])*"/g, "'…'")
|
|
899
|
+
.split(/&&|\|\||[;|&(){}`\n]/)
|
|
900
|
+
.flatMap((segment) => {
|
|
901
|
+
const words = segment.trim().split(/\s+/);
|
|
902
|
+
const start = words.findIndex(
|
|
903
|
+
(word) => !SHELL_COMMAND_PREFIX.has(word) && !/^[A-Za-z_]\w*=/.test(word),
|
|
904
|
+
);
|
|
905
|
+
const name = bunCommand(words[start]);
|
|
906
|
+
return name ? [{ command: name, args: words.slice(start + 1) }] : [];
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const DOCKERFILE_BUILD_PLATFORM = /--platform=\$\{?BUILDPLATFORM\}?(?:\s|$)/;
|
|
911
|
+
const BUN_INSTALL_SUBCOMMANDS = new Set(['install', 'i', 'add', 'a']);
|
|
912
|
+
const BUNFIG_SOURCE = /(?:^|[\s/"'[,])bunfig\.toml(?=[\s"',\]]|$)/;
|
|
913
|
+
|
|
914
|
+
/** Whether a `COPY`/`ADD` brings `bunfig.toml` into the stage, by name or with the whole context. */
|
|
915
|
+
function copiesBunfig(args: string): boolean {
|
|
916
|
+
if (BUNFIG_SOURCE.test(args)) return true;
|
|
917
|
+
const words = args.split(/\s+/);
|
|
918
|
+
if (words.some((word) => word.startsWith('--from='))) return false;
|
|
919
|
+
const sources = words.filter((word) => !word.startsWith('--')).slice(0, -1);
|
|
920
|
+
return sources.some((source) => source === '.' || source === './');
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Check 15: a Dockerfile stage built for the target platform must not run
|
|
925
|
+
* JavaScript while it builds. A multi-arch `docker buildx` build runs every
|
|
926
|
+
* such stage under QEMU on the non-native leg, where bun >= 1.4 aborts, and the
|
|
927
|
+
* image publishes last — after npm and the MCP Registry. A stage counts as
|
|
928
|
+
* running JavaScript when a `RUN` invokes `bun` for anything but
|
|
929
|
+
* `install`/`add` (`bun run build`, `bun -e`, a script, `bunx`), or runs
|
|
930
|
+
* `bun install`/`bun add` once `bunfig.toml` is in the stage, since its
|
|
931
|
+
* `[install.security]` scanner runs as a Bun program. A stage whose `FROM`
|
|
932
|
+
* carries `--platform=$BUILDPLATFORM` is exempt, and so are `HEALTHCHECK`,
|
|
933
|
+
* `CMD`, and `ENTRYPOINT`, which run at container start on the real target.
|
|
934
|
+
* One error per stage, naming each offending line.
|
|
935
|
+
*/
|
|
936
|
+
export function checkDockerfileBuildPlatform(dockerfile: string): string[] {
|
|
937
|
+
const stages: {
|
|
938
|
+
alias: string | undefined;
|
|
939
|
+
bunfig: boolean;
|
|
940
|
+
findings: string[];
|
|
941
|
+
from: DockerInstruction;
|
|
942
|
+
pinned: boolean;
|
|
943
|
+
}[] = [];
|
|
944
|
+
|
|
945
|
+
for (const instruction of dockerInstructions(dockerfile)) {
|
|
946
|
+
if (instruction.keyword === 'FROM') {
|
|
947
|
+
const [image = '', , alias] = instruction.args
|
|
948
|
+
.toLowerCase()
|
|
949
|
+
.split(/\s+/)
|
|
950
|
+
.filter((word) => !word.startsWith('--'));
|
|
951
|
+
stages.push({
|
|
952
|
+
alias,
|
|
953
|
+
// A stage built FROM an earlier one starts with that stage's files.
|
|
954
|
+
bunfig: stages.find((earlier) => earlier.alias === image)?.bunfig ?? false,
|
|
955
|
+
findings: [],
|
|
956
|
+
from: instruction,
|
|
957
|
+
pinned: DOCKERFILE_BUILD_PLATFORM.test(instruction.args),
|
|
958
|
+
});
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const stage = stages.at(-1);
|
|
962
|
+
if (!stage) continue;
|
|
963
|
+
if (instruction.keyword === 'COPY' || instruction.keyword === 'ADD') {
|
|
964
|
+
stage.bunfig ||= copiesBunfig(instruction.args);
|
|
965
|
+
} else if (instruction.keyword === 'RUN' && !stage.pinned) {
|
|
966
|
+
const finding = bunInvocations(instruction.args)
|
|
967
|
+
.map(({ command, args }) => {
|
|
968
|
+
const subcommand = args.find((arg) => !arg.startsWith('-'));
|
|
969
|
+
if (command === 'bun' && BUN_INSTALL_SUBCOMMANDS.has(subcommand ?? '')) {
|
|
970
|
+
return stage.bunfig
|
|
971
|
+
? `\`bun ${subcommand}\` after bunfig.toml is in the stage, which starts its security scanner as a Bun program`
|
|
972
|
+
: undefined;
|
|
973
|
+
}
|
|
974
|
+
return `\`${[command, args[0]].filter(Boolean).join(' ')}\``;
|
|
975
|
+
})
|
|
976
|
+
.find(Boolean);
|
|
977
|
+
if (finding) stage.findings.push(`Dockerfile:${instruction.line} runs ${finding}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return stages
|
|
982
|
+
.filter((stage) => stage.findings.length > 0)
|
|
983
|
+
.map(
|
|
984
|
+
({ from, findings }) =>
|
|
985
|
+
`Dockerfile:${from.line} "${from.text}" builds for the target platform but runs JavaScript while ` +
|
|
986
|
+
`building — ${findings.join('; ')}. A multi-arch buildx build runs that under QEMU on the non-native ` +
|
|
987
|
+
`leg, where bun aborts and no image publishes. Move these steps into a stage that starts ` +
|
|
988
|
+
`"FROM --platform=$BUILDPLATFORM" (cross-install with \`bun install --os=<os> --cpu=<x64|arm64>\`) and ` +
|
|
989
|
+
`copy their output (dist/, node_modules/) into this stage`,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
778
993
|
/** Read `packaging.pluginManifests` from devcheck.config.json; default on. */
|
|
779
994
|
function pluginManifestsEnabled(): boolean {
|
|
780
995
|
const cfg = tryReadJson<{ packaging?: { pluginManifests?: boolean } }>(
|
|
@@ -862,6 +1077,7 @@ async function main(): Promise<void> {
|
|
|
862
1077
|
errors.push(...checkManifestIdentity(manifest, unscopedName));
|
|
863
1078
|
}
|
|
864
1079
|
|
|
1080
|
+
errors.push(...checkManifestVersion(manifest, pkg?.version));
|
|
865
1081
|
errors.push(...checkBundleExcludedFromFiles(pkg?.files));
|
|
866
1082
|
} else {
|
|
867
1083
|
notes.push('No manifest.json — skipping manifest/server.json alignment checks.');
|
|
@@ -911,6 +1127,12 @@ async function main(): Promise<void> {
|
|
|
911
1127
|
}
|
|
912
1128
|
}
|
|
913
1129
|
|
|
1130
|
+
// ── Dockerfile build platform (check 15) ──
|
|
1131
|
+
const dockerfilePath = resolve('Dockerfile');
|
|
1132
|
+
if (existsSync(dockerfilePath)) {
|
|
1133
|
+
errors.push(...checkDockerfileBuildPlatform(readFileSync(dockerfilePath, 'utf-8')));
|
|
1134
|
+
}
|
|
1135
|
+
|
|
914
1136
|
// ── README version badge (check 12) ──
|
|
915
1137
|
const readmePath = resolve('README.md');
|
|
916
1138
|
if (existsSync(readmePath)) {
|