#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import vm from 'node:vm'; const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const nativeShellPlanPath = 'docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md'; const hostBridgeProtocolDocPath = 'docs/【前端架构】宿主壳能力统一协议-2026-06-17.md'; const developmentWorkflowDocPath = 'docs/project-memory/shared-memory/development-workflow.md'; const decisionLogDocPath = 'docs/project-memory/shared-memory/decision-log.md'; const rootPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); const mobileShellConfigCheckSource = fs.readFileSync( 'apps/mobile-shell/scripts/check-config.mjs', 'utf8', ); const desktopShellConfigCheckSource = fs.readFileSync( 'apps/desktop-shell/scripts/check-config.mjs', 'utf8', ); const aiGameCreatorShellAppSource = fs.readFileSync( 'apps/ai-game-creator-shell/src/App.tsx', 'utf8', ); const aiGameCreatorShellAppModelSource = fs.readFileSync( 'apps/ai-game-creator-shell/src/features/app-shell/model.ts', 'utf8', ); const aiGameCreatorShellProjectWorkspaceChatPaneSource = fs.readFileSync( 'apps/ai-game-creator-shell/src/features/project-workspace/ProjectWorkspaceChatPane.tsx', 'utf8', ); const aiGameCreatorShellDeveloperProjectPanelsSource = fs.readFileSync( 'apps/ai-game-creator-shell/src/features/project-workspace/DeveloperProjectPanels.tsx', 'utf8', ); const aiGameCreatorLocalGamePreviewFrameSource = fs.readFileSync( 'apps/ai-game-creator-shell/src/features/project-workspace/LocalGamePreviewFrame.tsx', 'utf8', ); const aiGameCreatorProjectDevelopmentSource = fs.readFileSync( 'apps/ai-game-creator-shell/src/view/project-development/index.tsx', 'utf8', ); const aiGameCreatorPreviewRustSource = fs.readFileSync( 'apps/ai-game-creator-shell/src-tauri/src/preview.rs', 'utf8', ); const aiGameCreatorShellTauriConfig = JSON.parse( fs.readFileSync( 'apps/ai-game-creator-shell/src-tauri/tauri.conf.json', 'utf8', ), ); const aiGameCreatorShellTauriSource = readSourceTree( 'apps/ai-game-creator-shell/src-tauri/src', '.rs', ); const aiGameCreatorClientHttpSource = fs.readFileSync( 'apps/ai-game-creator-shell/src/services/clientHttp.ts', 'utf8', ); const aiGameCreatorMainCapability = JSON.parse( fs.readFileSync( 'apps/ai-game-creator-shell/src-tauri/capabilities/main.json', 'utf8', ), ); const aiGameCreatorCargoManifestSource = fs.readFileSync( 'apps/ai-game-creator-shell/src-tauri/Cargo.toml', 'utf8', ); const aiGameCreatorViteConfigSource = fs.readFileSync( 'apps/ai-game-creator-shell/vite.config.ts', 'utf8', ); // 按门禁组运行:默认跑全部分组(本地语义不变),CI 用 `--groups=` 把互不依赖的 // 分组拆成独立 job。每个步骤和静态断言必须属于且只属于一个分组,分组名同时是 // 根 `check:native-shells:` 脚本和 workflow job 的拆分口径。 // - contract:纯源码 / 契约 / 清单断言,不需要任何构建产物。 // - shells:微信壳、Expo 移动壳、Tauri 桌面壳与 H5 HostBridge 的现役运行时门禁。 // - agc-web:AI 游戏创作壳的前端门禁(typecheck 与壳内测试,不触碰 Cargo)。 // - agc-rust-crates:AGC 壳依赖的共享 / 平台 crate 测试(server-rs workspace 加两个 // 无锁独立 crate),只需 server-rs 侧的依赖预热。 // - agc-rust-shard-1..4:AGC 壳自身的 Rust bin target 单测,按名单切成 4 片,一片一个 // 分组(CI 里就是一个 job),片内仍保持 `--test-threads=1`;每个分片分组都会做一次 // 「片并集等于全集且互斥」的自校验。 // - agc-rust-smoke:会用 `src-tauri/Cargo.toml` spawn `cargo run` 的 agent-run smoke。 // 与分片分开,免得把已经压到 4 分钟级的片 job 拖长。 // - release:发布构建 smoke 和依赖发布产物的落盘断言。 const nativeShellGateGroups = [ 'contract', 'shells', 'agc-web', 'agc-rust-crates', 'agc-rust-shard-1', 'agc-rust-shard-2', 'agc-rust-shard-3', 'agc-rust-shard-4', 'agc-rust-smoke', 'release', ]; const requestedNativeShellGroups = readRequestedNativeShellGroups( process.argv.slice(2), ); function readRequestedNativeShellGroups(argv) { const groupsFlag = argv.find((argument) => argument.startsWith('--groups=')); if (groupsFlag === undefined) { return nativeShellGateGroups; } const requested = groupsFlag .slice('--groups='.length) .split(',') .map((group) => group.trim()) .filter(Boolean); if (requested.length === 0) { throw new Error( `--groups requires at least one of: ${nativeShellGateGroups.join(', ')}`, ); } const unknownGroups = requested.filter( (group) => !nativeShellGateGroups.includes(group), ); if (unknownGroups.length > 0) { throw new Error( `unknown native shell gate group(s): ${unknownGroups.join(', ')}; expected ${nativeShellGateGroups.join(', ')}`, ); } return nativeShellGateGroups.filter((group) => requested.includes(group)); } function runsNativeShellGateGroup(group) { if (!nativeShellGateGroups.includes(group)) { throw new Error(`unknown native shell gate group: ${group}`); } return requestedNativeShellGroups.includes(group); } function runNativeShellGate(group, gate) { if (!runsNativeShellGateGroup(group)) { return; } gate(); } const productionShellScanRoots = [ 'apps/mobile-shell', 'apps/desktop-shell', 'miniprogram', 'packages/shared/src/contracts/hostBridge.ts', 'src/services/host-bridge', ]; const h5HostBridgeFacadeModule = 'src/services/host-bridge/hostBridge'; const h5NativeAppHostBridgeTransportModule = 'src/services/host-bridge/nativeAppHostBridge'; const h5HostBridgeScannedFacadeImports = new Set([ 'canUseHostShareGrid', 'captureHostImageFile', 'exportHostAudioFile', 'exportHostImageFile', 'exportHostTextFile', 'getHostAppearanceColorScheme', 'getHostNetworkStatus', 'getNativeAppHostRuntime', 'importHostAudioFile', 'importHostDocumentFile', 'importHostImageFile', 'importHostTextFile', 'navigateHostNativePage', 'openHostExternalUrl', 'openHostShare', 'openHostShareGrid', 'readHostClipboardText', 'refreshNativeAppHostRuntime', 'reloadHostWebView', 'requestHostHapticsImpact', 'requestHostLogin', 'requestHostPayment', 'scanHostQrCode', 'setHostAppBadgeCount', 'setHostAppTitle', 'setHostShareTarget', 'showHostLocalNotification', 'subscribeHostAppLifecycle', 'subscribeHostImageDrop', 'subscribeHostNavigationCanGoBack', 'subscribeHostNetworkStatusChange', 'subscribeHostRuntimeChange', 'writeHostClipboardText', ]); function readSourceTree(entryPath, extension) { const stats = fs.statSync(entryPath); if (stats.isDirectory()) { return fs .readdirSync(entryPath, { withFileTypes: true }) .sort((left, right) => left.name.localeCompare(right.name)) .map((entry) => readSourceTree(path.join(entryPath, entry.name), extension), ) .join('\n'); } if (path.extname(entryPath) !== extension) { return ''; } return fs.readFileSync(entryPath, 'utf8'); } function assertRootNativeShellCheckScripts() { if ( rootPackageJson.scripts?.['check:native-shells'] !== 'node scripts/check-native-shells.mjs' ) { throw new Error( 'root check:native-shells script must run scripts/check-native-shells.mjs', ); } if ( rootPackageJson.scripts?.check !== 'npm run lint && npm run test && npm run build && npm run check:native-shells' ) { throw new Error( 'root check script must include check:native-shells after build', ); } } // 每个门禁在自己的执行体里打印 `[check:native-shells]