#!/usr/bin/env node // 随包资源声明门禁:把 build_support/package-layout.json(唯一人工声明)渲染成 // build_support/package-layout.generated.rs(Rust 编译期常量),并校验声明结构与不变量。 // // 用法: // node scripts/check-package-layout.mjs 校验生成结果是否与声明一致(不一致 exit 1) // node scripts/check-package-layout.mjs --write 重新生成 // // 设计约束:Rust 侧不解析 JSON(避免运行期解析与生命周期妥协),只使用本脚本产出的常量; // Node 侧准备步骤直接读同一份 JSON。因此本门禁是“单一声明”的机械保障。 import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const APP_ROOT = path.resolve(SCRIPT_DIR, '..'); const SRC_TAURI = path.join(APP_ROOT, 'src-tauri'); const DECLARATION_PATH = path.join( SRC_TAURI, 'build_support/package-layout.json', ); const GENERATED_PATH = path.join( SRC_TAURI, 'build_support/package-layout.generated.rs', ); const EXPECTED_SCHEMA = 'agc-package-layout.v1'; class DeclarationError extends Error {} function fail(message) { throw new DeclarationError(message); } function expectObject(value, at) { if (value === null || typeof value !== 'object' || Array.isArray(value)) { fail(`${at} 必须是对象`); } return value; } function expectArray(value, at) { if (!Array.isArray(value)) { fail(`${at} 必须是数组`); } return value; } function expectString(value, at) { if (typeof value !== 'string' || value.length === 0) { fail(`${at} 必须是非空字符串`); } return value; } function expectBoolean(value, at) { if (typeof value !== 'boolean') { fail(`${at} 必须是布尔值`); } return value; } function expectStringArray(value, at) { return expectArray(value, at).map((item, index) => expectString(item, `${at}[${index}]`), ); } function optionalStringArray(source, key, at) { if (source[key] === undefined) { return []; } return expectStringArray(source[key], `${at}.${key}`); } function expectInteger(value, at) { if (!Number.isInteger(value) || value < 0) { fail(`${at} 必须是非负整数`); } return value; } // 随包子目录来源:`source` 直接来自仓库源码(准备步骤复制), // `prepared` 需要先由准备步骤运行声明的构建命令产出,再复制进随包目录。 function expectOrigin(value, at) { if (value !== 'source' && value !== 'prepared') { fail(`${at} 必须是 source 或 prepared(实际 ${String(value)})`); } return value; } // JSON 字符串字面量与 Rust 字符串字面量几乎一致,唯一差异是控制字符的 \uXXXX 与 \u{XX}。 function rustString(value) { return JSON.stringify(value).replace(/\\u([0-9a-fA-F]{4})/g, '\\u{$1}'); } function rustStrings(values) { return `&[${values.map(rustString).join(', ')}]`; } function indent(level) { return ' '.repeat(level); } function renderStruct(name, fields, level = 0) { const body = fields .map(([key, rendered]) => `${indent(level + 1)}${key}: ${rendered},`) .join('\n'); return `${name} {\n${body}\n${indent(level)}}`; } function parsePackageMetadata(value) { const metadata = expectObject(value, 'codex.packageMetadata'); return { layoutVersion: expectInteger( metadata.layoutVersion, 'codex.packageMetadata.layoutVersion', ), resourcesDir: expectString( metadata.resourcesDir, 'codex.packageMetadata.resourcesDir', ), pathDir: expectString(metadata.pathDir, 'codex.packageMetadata.pathDir'), }; } function parseDeclaration(raw) { const root = expectObject(JSON.parse(raw), 'root'); if (root.schema !== EXPECTED_SCHEMA) { fail( `不支持的声明 schema:${String(root.schema)}(期望 ${EXPECTED_SCHEMA})`, ); } const layoutVersion = expectInteger(root.layoutVersion, 'layoutVersion'); const codex = expectObject(root.codex, 'codex'); const targets = expectArray(codex.targets, 'codex.targets').map( (entry, index) => { const at = `codex.targets[${index}]`; const parsed = expectObject(entry, at); const files = expectStringArray(parsed.files, `${at}.files`); if (files.length === 0) { fail(`${at}.files 不能为空`); } const executable = expectString(parsed.executable, `${at}.executable`); if (!files.includes(executable)) { fail(`${at}.executable 必须属于组件白名单:${executable}`); } return { target: expectString(parsed.target, `${at}.target`), platform: expectString(parsed.platform, `${at}.platform`), directory: expectString(parsed.directory, `${at}.directory`), executable, files, }; }, ); const seenTargets = new Set(); for (const entry of targets) { if (seenTargets.has(entry.target)) { fail(`codex.targets 重复声明目标 ${entry.target}`); } seenTargets.add(entry.target); } const noticeSources = expectArray( codex.noticeSources, 'codex.noticeSources', ).map((entry, index) => { const at = `codex.noticeSources[${index}]`; const parsed = expectObject(entry, at); return { targets: expectStringArray(parsed.targets, `${at}.targets`), source: expectString(parsed.source, `${at}.source`), preserve: expectBoolean(parsed.preserve, `${at}.preserve`), }; }); for (const entry of targets) { const covered = noticeSources.filter((notice) => notice.targets.includes(entry.target), ); if (covered.length !== 1) { fail( `目标 ${entry.target} 必须且只能有一条第三方声明来源(实际 ${covered.length} 条)`, ); } } const universalGroups = expectArray( codex.universalGroups, 'codex.universalGroups', ).map((entry, index) => { const at = `codex.universalGroups[${index}]`; const parsed = expectObject(entry, at); const groupTargets = expectStringArray(parsed.targets, `${at}.targets`); for (const target of groupTargets) { if (!seenTargets.has(target)) { fail(`${at}.targets 含未声明目标 ${target}`); } } return { name: expectString(parsed.name, `${at}.name`), directory: expectString(parsed.directory, `${at}.directory`), targets: groupTargets, }; }); const plugins = expectObject(root.plugins, 'plugins'); const subdirectories = expectArray( plugins.subdirectories, 'plugins.subdirectories', ).map((entry, index) => { const at = `plugins.subdirectories[${index}]`; const parsed = expectObject(entry, at); return { path: expectString(parsed.path, `${at}.path`), origin: expectOrigin(parsed.origin, `${at}.origin`), plugin: parsed.plugin === undefined ? '' : expectString(parsed.plugin, `${at}.plugin`), targetContains: optionalStringArray(parsed, 'targetContains', at), targets: optionalStringArray(parsed, 'targets', at), features: optionalStringArray(parsed, 'features', at), }; }); const libraryStaging = expectArray( plugins.libraryStaging, 'plugins.libraryStaging', ).map((entry, index) => { const at = `plugins.libraryStaging[${index}]`; const parsed = expectObject(entry, at); const files = expectStringArray(parsed.files, `${at}.files`); if (files.length === 0) { fail(`${at}.files 不能为空`); } return { plugin: expectString(parsed.plugin, `${at}.plugin`), sourceSubdirectory: expectString( parsed.sourceSubdirectory, `${at}.sourceSubdirectory`, ), prepare: expectString(parsed.prepare, `${at}.prepare`), targets: expectStringArray(parsed.targets, `${at}.targets`), features: expectStringArray(parsed.features, `${at}.features`), layout: expectString(parsed.layout, `${at}.layout`), files, }; }); const claudeAgent = expectObject(root.claudeAgent, 'claudeAgent'); const claudeAgentTargets = expectArray( claudeAgent.targets, 'claudeAgent.targets', ).map((entry, index) => { const at = `claudeAgent.targets[${index}]`; const parsed = expectObject(entry, at); return { target: expectString(parsed.target, `${at}.target`), runtimePackage: expectString( parsed.runtimePackage, `${at}.runtimePackage`, ), runtimeFileName: expectString( parsed.runtimeFileName, `${at}.runtimeFileName`, ), }; }); const seenClaudeAgentTargets = new Set(); for (const entry of claudeAgentTargets) { if (seenClaudeAgentTargets.has(entry.target)) { fail(`claudeAgent.targets 重复声明目标 ${entry.target}`); } seenClaudeAgentTargets.add(entry.target); } const sdkPackageName = expectString( claudeAgent.sdkPackageName, 'claudeAgent.sdkPackageName', ); if (!/^@[^/]+\/[^/]+$/u.test(sdkPackageName)) { fail(`claudeAgent.sdkPackageName 必须是带 scope 的包名:${sdkPackageName}`); } for (const entry of claudeAgentTargets) { // 原生运行时包与 SDK 同处一个 scope 目录下,包名必须是裸名。 if (entry.runtimePackage.includes('/')) { fail( `claudeAgent.targets 的 runtimePackage 必须是裸包名:${entry.runtimePackage}`, ); } } return { layoutVersion, codex: { version: expectString(codex.version, 'codex.version'), cliVersionPrefix: expectString( codex.cliVersionPrefix, 'codex.cliVersionPrefix', ), manifestSchema: expectString( codex.manifestSchema, 'codex.manifestSchema', ), packageMetadata: parsePackageMetadata(codex.packageMetadata), resourceDirectory: expectString( codex.resourceDirectory, 'codex.resourceDirectory', ), manifestFileName: expectString( codex.manifestFileName, 'codex.manifestFileName', ), packageMetadataFileName: expectString( codex.packageMetadataFileName, 'codex.packageMetadataFileName', ), noticeFileName: expectString( codex.noticeFileName, 'codex.noticeFileName', ), sourceRoots: expectStringArray(codex.sourceRoots, 'codex.sourceRoots'), sourceRelativePaths: expectStringArray( codex.sourceRelativePaths, 'codex.sourceRelativePaths', ), noticeSources, universalGroups, targets, }, claudeAgent: { version: expectString(claudeAgent.version, 'claudeAgent.version'), resourceDirectory: assertRelativePath( expectString( claudeAgent.resourceDirectory, 'claudeAgent.resourceDirectory', ), 'claudeAgent.resourceDirectory', ), entryRelativePath: assertRelativePath( expectString( claudeAgent.entryRelativePath, 'claudeAgent.entryRelativePath', ), 'claudeAgent.entryRelativePath', ), entryFileName: expectString( claudeAgent.entryFileName, 'claudeAgent.entryFileName', ), nodeModulesDirectory: expectString( claudeAgent.nodeModulesDirectory, 'claudeAgent.nodeModulesDirectory', ), sdkPackageName, sdkEntryFileName: expectString( claudeAgent.sdkEntryFileName, 'claudeAgent.sdkEntryFileName', ), sdkPackageMetadataFileName: expectString( claudeAgent.sdkPackageMetadataFileName, 'claudeAgent.sdkPackageMetadataFileName', ), sourceRoots: expectStringArray( claudeAgent.sourceRoots, 'claudeAgent.sourceRoots', ), skipDirectoryNames: expectStringArray( claudeAgent.skipDirectoryNames, 'claudeAgent.skipDirectoryNames', ), skipDirectoryNamePrefixes: expectStringArray( claudeAgent.skipDirectoryNamePrefixes, 'claudeAgent.skipDirectoryNamePrefixes', ), skipFileNamePrefixes: expectStringArray( claudeAgent.skipFileNamePrefixes, 'claudeAgent.skipFileNamePrefixes', ), skipFileNameFragments: expectStringArray( claudeAgent.skipFileNameFragments, 'claudeAgent.skipFileNameFragments', ), targets: claudeAgentTargets, }, plugins: { sourceDirectory: expectString( plugins.sourceDirectory, 'plugins.sourceDirectory', ), destinationDirectory: expectString( plugins.destinationDirectory, 'plugins.destinationDirectory', ), manifestFileName: expectString( plugins.manifestFileName, 'plugins.manifestFileName', ), targetContainsAny: expectStringArray( plugins.targetContainsAny, 'plugins.targetContainsAny', ), subdirectories, libraryStaging, skipDirectoryNames: expectStringArray( plugins.skipDirectoryNames, 'plugins.skipDirectoryNames', ), skipDirectoryNamePrefixes: expectStringArray( plugins.skipDirectoryNamePrefixes, 'plugins.skipDirectoryNamePrefixes', ), skipFileNamePrefixes: expectStringArray( plugins.skipFileNamePrefixes, 'plugins.skipFileNamePrefixes', ), skipFileNameFragments: expectStringArray( plugins.skipFileNameFragments, 'plugins.skipFileNameFragments', ), }, }; } function renderCodexTarget(entry) { return renderStruct( 'CodexTarget', [ ['target', rustString(entry.target)], ['platform', rustString(entry.platform)], ['directory', rustString(entry.directory)], ['executable', rustString(entry.executable)], ['files', rustStrings(entry.files)], ], 1, ); } function renderNoticeSource(entry) { return renderStruct( 'NoticeSource', [ ['targets', rustStrings(entry.targets)], ['source', rustString(entry.source)], ['preserve', entry.preserve ? 'true' : 'false'], ], 1, ); } function renderUniversalGroup(entry) { return renderStruct( 'UniversalGroup', [ ['name', rustString(entry.name)], ['directory', rustString(entry.directory)], ['targets', rustStrings(entry.targets)], ], 1, ); } function renderSubdirectory(entry) { return renderStruct( 'Subdirectory', [ ['path', rustString(entry.path)], ['plugin', rustString(entry.plugin ?? '')], ['origin', rustString(entry.origin)], ['target_contains', rustStrings(entry.targetContains)], ['targets', rustStrings(entry.targets)], ['features', rustStrings(entry.features)], ], 1, ); } function renderLibraryStaging(entry) { return renderStruct( 'LibraryStaging', [ ['plugin', rustString(entry.plugin)], ['source_subdirectory', rustString(entry.sourceSubdirectory)], ['targets', rustStrings(entry.targets)], ['features', rustStrings(entry.features)], ['layout', rustString(entry.layout)], ['files', rustStrings(entry.files)], ], 1, ); } function renderClaudeAgentTarget(entry) { return renderStruct( 'ClaudeAgentTarget', [ ['target', rustString(entry.target)], ['runtime_package', rustString(entry.runtimePackage)], ['runtime_file_name', rustString(entry.runtimeFileName)], ], 1, ); } function renderGenerated(declaration) { const codex = declaration.codex; const godotStaging = declaration.plugins.libraryStaging.find( (entry) => entry.layout === 'godot-bundle', ); if (!godotStaging || godotStaging.files.length === 0) { fail('声明缺少 godot-bundle 随包文件清单,拒绝生成空清单'); } const godotFiles = godotStaging.files; const plugins = declaration.plugins; const codexStruct = renderStruct('Codex', [ [ 'package_metadata', renderStruct('PackageMetadata', [ ['layout_version', String(codex.packageMetadata.layoutVersion)], ['resources_dir', rustString(codex.packageMetadata.resourcesDir)], ['path_dir', rustString(codex.packageMetadata.pathDir)], ]), ], ['resource_directory', rustString(codex.resourceDirectory)], ['manifest_file_name', rustString(codex.manifestFileName)], ['package_metadata_file_name', rustString(codex.packageMetadataFileName)], ['notice_file_name', rustString(codex.noticeFileName)], ['source_roots', rustStrings(codex.sourceRoots)], ['source_relative_paths', rustStrings(codex.sourceRelativePaths)], [ 'notice_sources', `&[\n${codex.noticeSources.map(renderNoticeSource).join(',\n')}\n]`, ], [ 'universal_groups', `&[\n${codex.universalGroups.map(renderUniversalGroup).join(',\n')}\n]`, ], ['targets', `&[\n${codex.targets.map(renderCodexTarget).join(',\n')}\n]`], ]); const claudeAgent = declaration.claudeAgent; const claudeAgentStruct = renderStruct('ClaudeAgent', [ ['version', rustString(claudeAgent.version)], ['resource_directory', rustString(claudeAgent.resourceDirectory)], ['entry_relative_path', rustString(claudeAgent.entryRelativePath)], ['entry_file_name', rustString(claudeAgent.entryFileName)], ['node_modules_directory', rustString(claudeAgent.nodeModulesDirectory)], ['sdk_package_name', rustString(claudeAgent.sdkPackageName)], ['sdk_entry_file_name', rustString(claudeAgent.sdkEntryFileName)], [ 'sdk_package_metadata_file_name', rustString(claudeAgent.sdkPackageMetadataFileName), ], [ 'targets', `&[\n${claudeAgent.targets.map(renderClaudeAgentTarget).join(',\n')}\n]`, ], ]); const pluginsStruct = renderStruct('Plugins', [ ['source_directory', rustString(plugins.sourceDirectory)], ['destination_directory', rustString(plugins.destinationDirectory)], ['manifest_file_name', rustString(plugins.manifestFileName)], ['target_contains_any', rustStrings(plugins.targetContainsAny)], [ 'subdirectories', `&[\n${plugins.subdirectories.map(renderSubdirectory).join(',\n')}\n]`, ], [ 'library_staging', `&[\n${plugins.libraryStaging.map(renderLibraryStaging).join(',\n')}\n]`, ], ['skip_directory_names', rustStrings(plugins.skipDirectoryNames)], [ 'skip_directory_name_prefixes', rustStrings(plugins.skipDirectoryNamePrefixes), ], ['skip_file_name_prefixes', rustStrings(plugins.skipFileNamePrefixes)], ['skip_file_name_fragments', rustStrings(plugins.skipFileNameFragments)], ]); return `// @generated by apps/ai-game-creator-shell/scripts/check-package-layout.mjs // 来源:build_support/package-layout.json。不要手工编辑本文件。 // 修改随包资源布局请编辑声明文件,然后运行 // npm run agc:bundled-resources:sync(在仓库根目录) // 门禁会校验两者一致(npm run agc:typecheck 链内含 check-package-layout.mjs)。 pub const DECLARATION_SCHEMA: &str = ${rustString(EXPECTED_SCHEMA)}; pub const LAYOUT_VERSION: u64 = ${declaration.layoutVersion}; pub const CODEX_VERSION: &str = ${rustString(declaration.codex.version)}; pub const CODEX_CLI_VERSION: &str = ${rustString(declaration.codex.cliVersionPrefix + declaration.codex.version)}; pub const CODEX_MANIFEST_SCHEMA: &str = ${rustString(declaration.codex.manifestSchema)}; pub const CLAUDE_AGENT_SDK_VERSION: &str = ${rustString(declaration.claudeAgent.version)}; pub const GODOT_BUNDLE_FILES: &[&str] = ${rustStrings(godotFiles)}; pub const CODEX: Codex = ${codexStruct}; pub const CLAUDE_AGENT: ClaudeAgent = ${claudeAgentStruct}; pub const PLUGINS: Plugins = ${pluginsStruct}; `; } function appLockedCodexVersion() { const appPackage = expectObject( JSON.parse(readFileSync(path.join(APP_ROOT, 'package.json'), 'utf8')), 'apps/ai-game-creator-shell/package.json', ); const declared = appPackage.dependencies?.['@openai/codex'] ?? appPackage.devDependencies?.['@openai/codex']; if (typeof declared !== 'string' || declared.length === 0) { fail( '应用 package.json 未声明 @openai/codex,无法校验声明的 codex.version', ); } return declared.replace(/^[\^~]/, ''); } /// Claude Agent SDK 有两个锁点:客户端依赖与 sidecar 自己的依赖。两处都必须与声明一致, /// 否则随包的 SDK 与运行它的 sidecar 会悄悄分叉。 function lockedClaudeAgentVersions() { const manifests = [ [ 'apps/ai-game-creator-shell/package.json', path.join(APP_ROOT, 'package.json'), ], [ 'apps/ai-game-creator-shell/agent-sidecar/package.json', path.join(APP_ROOT, 'agent-sidecar', 'package.json'), ], ]; return manifests.map(([label, file]) => { const manifest = expectObject( JSON.parse(readFileSync(file, 'utf8')), label, ); const declared = manifest.dependencies?.['@anthropic-ai/claude-agent-sdk']; if (typeof declared !== 'string' || declared.length === 0) { fail( `${label} 未声明 @anthropic-ai/claude-agent-sdk,无法校验声明的 claudeAgent.version`, ); } return declared.replace(/^[\^~]/, ''); }); } const REPO_ROOT = path.resolve(APP_ROOT, '..', '..'); const PLUGINS_ROOT = path.join(REPO_ROOT, 'plugins'); const SHELL_MANIFEST_PATH = path.join(SRC_TAURI, 'Cargo.toml'); /// 声明里的路径必须是仓库内相对路径:绝对路径或含 `..` 会写到工作区之外。 function assertRelativePath(value, at) { if ( path.isAbsolute(value) || value.startsWith('/') || value.split('/').includes('..') ) { fail(`${at} 必须是仓库内相对路径(不允许绝对路径或 ..):${value}`); } return value; } /// 声明引用的插件必须是真实存在的插件目录(拼错插件的后果是静默不交付)。 function assertKnownPlugin(name, at) { const directory = path.join(PLUGINS_ROOT, name); if (!existsSync(path.join(directory, 'plugin.json'))) { fail(`${at} 指向的插件不存在或缺少 plugin.json:${name}`); } return name; } /// shell crate 的 feature 表(拼错 feature 的后果同样是静默不交付)。 function shellCrateFeatures() { const manifest = readFileSync(SHELL_MANIFEST_PATH, 'utf8'); const section = /\[features\]([\s\S]*?)(?:\n\[|$)/u.exec(manifest); if (!section) { fail('无法从 src-tauri/Cargo.toml 读取 [features] 段'); } return new Set( section[1] .split('\n') .map((line) => /^([A-Za-z0-9_-]+)\s*=/u.exec(line.trim())?.[1]) .filter(Boolean), ); } /// 新 section(prepareSteps / nativePayloads / prepared 来源)不参与 Rust 生成物, /// 必须在门禁里单独把关,避免「声明了却没人用」或引用到不存在的准备步骤。 function validateExtendedDeclarations() { const root = expectObject( JSON.parse(readFileSync(DECLARATION_PATH, 'utf8')), 'root', ); const plugins = expectObject(root.plugins, 'plugins'); const steps = expectArray(plugins.prepareSteps ?? [], 'plugins.prepareSteps'); const names = new Set(); for (const [index, raw] of steps.entries()) { const at = `plugins.prepareSteps[${index}]`; const step = expectObject(raw, at); const name = expectString(step.name, `${at}.name`); if (names.has(name)) { fail(`${at}.name 重复:${name}`); } names.add(name); const kind = expectString(step.kind, `${at}.kind`); if (kind !== 'powershell' && kind !== 'cargo') { fail(`${at}.kind 只能是 powershell 或 cargo`); } if (kind === 'powershell') { expectString(step.workingDirectory, `${at}.workingDirectory`); expectString(step.scriptFileName, `${at}.scriptFileName`); } else { expectString(step.packageDirectory, `${at}.packageDirectory`); } expectStringArray(step.requiredOutputs ?? [], `${at}.requiredOutputs`); } const payloads = expectArray( plugins.nativePayloads ?? [], 'plugins.nativePayloads', ); for (const [index, raw] of payloads.entries()) { const at = `plugins.nativePayloads[${index}]`; const payload = expectObject(raw, at); expectString(payload.plugin, `${at}.plugin`); expectString(payload.prepare, `${at}.prepare`); expectString(payload.sourceFileName, `${at}.sourceFileName`); expectString(payload.destinationFileName, `${at}.destinationFileName`); expectString( payload.destinationSubdirectory, `${at}.destinationSubdirectory`, ); if (!names.has(payload.prepare)) { fail(`${at}.prepare 引用了未声明的准备步骤:${payload.prepare}`); } } const preparedByPlugin = new Map(); for (const entry of expectArray( plugins.subdirectories ?? [], 'plugins.subdirectories', )) { const subdirectory = expectObject(entry, 'subdirectory'); if (subdirectory.origin !== 'prepared') { continue; } const owner = expectString( subdirectory.plugin ?? '', 'subdirectory.plugin', ); if (!owner) { fail(`origin=prepared 的子目录必须声明 plugin:${subdirectory.path}`); } preparedByPlugin.set(owner, [ ...(preparedByPlugin.get(owner) ?? []), subdirectory.path, ]); } for (const payload of payloads) { const candidates = preparedByPlugin.get(payload.plugin) ?? []; if (!candidates.includes(payload.destinationSubdirectory)) { fail( `nativePayloads 的 destinationSubdirectory(${payload.destinationSubdirectory})必须是该插件 origin=prepared 的子目录之一(现有:${candidates.join('、') || '无'})`, ); } } const knownFeatures = shellCrateFeatures(); const checkFeatures = (values, at) => { for (const name of expectStringArray(values ?? [], at)) { if (!knownFeatures.has(name)) { fail(`${at} 引用了 Cargo.toml 里不存在的 feature:${name}`); } } }; for (const [index, raw] of expectArray( plugins.subdirectories ?? [], 'plugins.subdirectories', ).entries()) { const at = `plugins.subdirectories[${index}]`; const subdirectory = expectObject(raw, at); assertRelativePath( expectString(subdirectory.path, `${at}.path`), `${at}.path`, ); if (subdirectory.plugin) { assertKnownPlugin(subdirectory.plugin, `${at}.plugin`); } checkFeatures(subdirectory.features, `${at}.features`); } for (const [index, raw] of expectArray( plugins.libraryStaging ?? [], 'plugins.libraryStaging', ).entries()) { const at = `plugins.libraryStaging[${index}]`; const staging = expectObject(raw, at); assertKnownPlugin( expectString(staging.plugin, `${at}.plugin`), `${at}.plugin`, ); assertRelativePath( expectString(staging.sourceSubdirectory, `${at}.sourceSubdirectory`), `${at}.sourceSubdirectory`, ); for (const relative of expectStringArray( staging.files ?? [], `${at}.files`, )) { assertRelativePath(relative, `${at}.files`); } checkFeatures(staging.features, `${at}.features`); } for (const payload of payloads) { assertRelativePath( payload.destinationSubdirectory, `nativePayloads.${payload.plugin}.destinationSubdirectory`, ); checkFeatures( payload.features, `nativePayloads.${payload.plugin}.features`, ); } for (const [index, raw] of steps.entries()) { const at = `plugins.prepareSteps[${index}]`; const step = expectObject(raw, at); for (const key of ['workingDirectory', 'packageDirectory']) { if (step[key]) { assertRelativePath( expectString(step[key], `${at}.${key}`), `${at}.${key}`, ); } } for (const relative of expectStringArray( step.requiredOutputs ?? [], `${at}.requiredOutputs`, )) { assertRelativePath(relative, `${at}.requiredOutputs`); } } const referenced = [ ...expectArray(plugins.subdirectories ?? [], 'plugins.subdirectories') .filter( (entry) => expectObject(entry, 'subdirectory').origin === 'prepared', ) .map((entry) => [entry.path, entry.prepare]), ...expectArray(plugins.libraryStaging ?? [], 'plugins.libraryStaging').map( (entry) => [entry.sourceSubdirectory, entry.prepare], ), ...payloads.map((entry) => [ `${entry.plugin}/${entry.destinationSubdirectory}`, entry.prepare, ]), ]; for (const [label, prepare] of referenced) { if (!prepare) { fail(`prepared 来源的条目缺少 prepare 声明:${label}`); } if (!names.has(prepare)) { fail(`${label} 引用了未声明的准备步骤:${prepare}`); } } } function main() { const declaration = parseDeclaration(readFileSync(DECLARATION_PATH, 'utf8')); validateExtendedDeclarations(); const lockedVersion = appLockedCodexVersion(); if (lockedVersion !== declaration.codex.version) { fail( `声明 codex.version(${declaration.codex.version})与应用锁定的 @openai/codex(${lockedVersion})不一致;请同步更新 build_support/package-layout.json`, ); } for (const lockedClaudeVersion of lockedClaudeAgentVersions()) { if (lockedClaudeVersion !== declaration.claudeAgent.version) { fail( `声明 claudeAgent.version(${declaration.claudeAgent.version})与锁定的 @anthropic-ai/claude-agent-sdk(${lockedClaudeVersion})不一致;请同步更新 build_support/package-layout.json`, ); } } const rendered = renderGenerated(declaration); const write = process.argv.includes('--write'); if (write) { writeFileSync(GENERATED_PATH, rendered); console.log( `随包资源声明已生成:${path.relative(process.cwd(), GENERATED_PATH)}(layoutVersion ${declaration.layoutVersion})`, ); return; } const current = readFileSync(GENERATED_PATH, 'utf8'); if (current !== rendered) { console.error( [ `随包资源声明与 Rust 常量不一致:`, ` 声明:${path.relative(process.cwd(), DECLARATION_PATH)}`, ` 生成:${path.relative(process.cwd(), GENERATED_PATH)}`, '请运行:npm run agc:bundled-resources:sync', ].join('\n'), ); process.exit(1); } console.log( `随包资源声明一致(layoutVersion ${declaration.layoutVersion},codex ${declaration.codex.version},目标 ${declaration.codex.targets.length},插件子目录 ${declaration.plugins.subdirectories.length})`, ); } try { main(); } catch (error) { if (error instanceof DeclarationError) { console.error(`随包资源声明无效:${error.message}`); process.exit(1); } throw error; }