Files
Genarrative/scripts/check-npm-workspaces.mjs
T
kdletters d75547b36d 自动化 Skill 指纹同步并发布 AGC 0.1.10
新增 skill-pack-manifest 脚本统一计算并校验内置 AGC Skill 内容摘要,--write 模式自动递增清单版本并同步 SHA-256。

新增 check-skill-pack 只读门禁与 Node 回归测试,无参数默认只读,只有单个 --write 才进入写入模式。

AGC typecheck 与 release build 接入 Skill 清单指纹校验,内容漂移时直接列出 Skill 与实际摘要阻断构建。

修正 agc-client-projection 清单指纹并升级 Skill pack 版本到 2026-08-26.3。

升级 AGC 标准版到 0.1.10,同步 package、Cargo、Tauri 与 npm 工作区锁文件。

首页空输入占位符锚定到编辑区,避免整页滚动后占位文本脱离输入框。

更新 AGC 实施计划中 Skill 指纹同步与只读门禁说明。
2026-08-26 23:23:24 +08:00

298 lines
8.6 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
export const REQUIRED_PACKAGE_MANAGER = 'npm@10.9.7';
export const REQUIRED_WORKSPACES = Object.freeze([
'apps/admin-web',
'apps/ai-game-creator-shell',
'apps/desktop-shell',
'apps/mobile-shell',
'apps/preview-deployer-web',
'packages/image-canvas-core',
'packages/image-canvas-react',
'packages/shared',
'tools/spine-json-export-validator',
]);
const WORKSPACE_NAMES = Object.freeze({
'apps/admin-web': '@genarrative/admin-web',
'apps/ai-game-creator-shell': '@genarrative/ai-game-creator-shell',
'apps/desktop-shell': '@genarrative/desktop-shell',
'apps/mobile-shell': '@genarrative/mobile-shell',
'apps/preview-deployer-web': '@genarrative/preview-deployer-web',
'packages/image-canvas-core': '@genarrative/image-canvas-core',
'packages/image-canvas-react': '@genarrative/image-canvas-react',
'packages/shared': '@genarrative/shared',
'tools/spine-json-export-validator':
'@genarrative/spine-json-export-validator',
});
const REQUIRED_LOCAL_DEPENDENCIES = Object.freeze({
'package.json': [
'@genarrative/image-canvas-core',
'@genarrative/image-canvas-react',
'@genarrative/shared',
],
'apps/admin-web/package.json': ['@genarrative/shared'],
'apps/ai-game-creator-shell/package.json': [
'@genarrative/image-canvas-core',
'@genarrative/image-canvas-react',
'@genarrative/shared',
],
'apps/mobile-shell/package.json': ['@genarrative/shared'],
'packages/image-canvas-react/package.json': [
'@genarrative/image-canvas-core',
],
});
const DEPENDENCY_FIELDS = Object.freeze([
'dependencies',
'devDependencies',
'optionalDependencies',
'peerDependencies',
]);
const IGNORED_NESTED_DIRECTORIES = new Set([
'.git',
'.vite',
'build',
'dist',
'node_modules',
'target',
]);
const HOIST_SENSITIVE_CONFIGS = Object.freeze([
'vite.config.ts',
'vitest.config.ts',
'apps/ai-game-creator-shell/vite.config.ts',
]);
const FORBIDDEN_WORKSPACE_NODE_MODULES_PATH =
'apps/ai-game-creator-shell/node_modules/';
function readJson(rootDir, relativePath, errors) {
const absolutePath = path.join(rootDir, relativePath);
try {
return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
} catch (error) {
errors.push(`${relativePath}: cannot read valid JSON (${error.message})`);
return null;
}
}
function normalizeWorkspaceLink(value) {
return typeof value === 'string'
? value.replace(/^\.\//u, '').replaceAll('\\', '/')
: value;
}
function findNestedLockfiles(rootDir, workspacePath) {
const matches = [];
const pending = [path.join(rootDir, workspacePath)];
while (pending.length > 0) {
const directory = pending.pop();
let entries;
try {
entries = fs.readdirSync(directory, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.isSymbolicLink()) {
continue;
}
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (!IGNORED_NESTED_DIRECTORIES.has(entry.name)) {
pending.push(entryPath);
}
continue;
}
if (
entry.name === 'package-lock.json' ||
entry.name === 'npm-shrinkwrap.json'
) {
matches.push(path.relative(rootDir, entryPath).replaceAll('\\', '/'));
}
}
}
return matches.sort();
}
export function collectNpmWorkspaceErrors(rootDir) {
const errors = [];
const rootPackage = readJson(rootDir, 'package.json', errors);
const lockfile = readJson(rootDir, 'package-lock.json', errors);
if (!rootPackage || !lockfile) {
return errors;
}
for (const configPath of HOIST_SENSITIVE_CONFIGS) {
const absolutePath = path.join(rootDir, configPath);
if (
fs.existsSync(absolutePath) &&
fs
.readFileSync(absolutePath, 'utf8')
.includes(FORBIDDEN_WORKSPACE_NODE_MODULES_PATH)
) {
errors.push(
`${configPath}: resolve dependencies from the workspace manifest instead of hard-coding ${FORBIDDEN_WORKSPACE_NODE_MODULES_PATH}`,
);
}
}
if (rootPackage.packageManager !== REQUIRED_PACKAGE_MANAGER) {
errors.push(
`package.json: packageManager must be ${REQUIRED_PACKAGE_MANAGER}, received ${String(rootPackage.packageManager)}`,
);
}
if (
JSON.stringify(rootPackage.workspaces) !==
JSON.stringify(REQUIRED_WORKSPACES)
) {
errors.push(
`package.json: workspaces must be the explicit ordered list ${JSON.stringify(REQUIRED_WORKSPACES)}`,
);
}
const manifests = new Map([['package.json', rootPackage]]);
for (const workspacePath of REQUIRED_WORKSPACES) {
const manifestPath = `${workspacePath}/package.json`;
const manifest = readJson(rootDir, manifestPath, errors);
if (!manifest) {
continue;
}
manifests.set(manifestPath, manifest);
if (manifest.name !== WORKSPACE_NAMES[workspacePath]) {
errors.push(
`${manifestPath}: name must be ${WORKSPACE_NAMES[workspacePath]}, received ${String(manifest.name)}`,
);
}
const expectedWorkspaceVersion =
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.10' : '0.1.0';
if (manifest.version !== expectedWorkspaceVersion) {
errors.push(
`${manifestPath}: workspace version must be ${expectedWorkspaceVersion}`,
);
}
for (const nestedLockfile of findNestedLockfiles(rootDir, workspacePath)) {
errors.push(
`${nestedLockfile}: nested npm lockfiles are forbidden inside workspaces`,
);
}
}
const localPackageNames = new Set(Object.values(WORKSPACE_NAMES));
for (const [manifestPath, manifest] of manifests) {
for (const dependencyField of DEPENDENCY_FIELDS) {
for (const [dependencyName, dependencySpec] of Object.entries(
manifest[dependencyField] ?? {},
)) {
if (
typeof dependencySpec === 'string' &&
dependencySpec.startsWith('workspace:')
) {
errors.push(
`${manifestPath}: ${dependencyField}.${dependencyName} must not use the unsupported workspace: protocol`,
);
}
if (
localPackageNames.has(dependencyName) &&
dependencySpec !== '0.1.0'
) {
errors.push(
`${manifestPath}: ${dependencyField}.${dependencyName} must use exact version 0.1.0`,
);
}
}
}
}
for (const [manifestPath, requiredDependencies] of Object.entries(
REQUIRED_LOCAL_DEPENDENCIES,
)) {
const manifest = manifests.get(manifestPath);
if (!manifest) {
continue;
}
for (const dependencyName of requiredDependencies) {
if (manifest.dependencies?.[dependencyName] !== '0.1.0') {
errors.push(
`${manifestPath}: dependencies.${dependencyName} must be declared as exact version 0.1.0`,
);
}
}
}
const lockPackages = lockfile.packages;
if (!lockPackages || typeof lockPackages !== 'object') {
errors.push('package-lock.json: packages map is required');
return errors;
}
if (
JSON.stringify(lockPackages['']?.workspaces) !==
JSON.stringify(REQUIRED_WORKSPACES)
) {
errors.push(
'package-lock.json: root package entry must contain the explicit workspace list',
);
}
for (const workspacePath of REQUIRED_WORKSPACES) {
const expectedName = WORKSPACE_NAMES[workspacePath];
const workspaceEntry = lockPackages[workspacePath];
if (!workspaceEntry || workspaceEntry.name !== expectedName) {
errors.push(
`package-lock.json: packages[${JSON.stringify(workspacePath)}] must describe ${expectedName}`,
);
}
const linkKey = `node_modules/${expectedName}`;
const linkEntry = lockPackages[linkKey];
if (
!linkEntry ||
linkEntry.link !== true ||
normalizeWorkspaceLink(linkEntry.resolved) !== workspacePath
) {
errors.push(
`package-lock.json: packages[${JSON.stringify(linkKey)}] must link to ${workspacePath}`,
);
}
}
return errors;
}
export function checkNpmWorkspaces(rootDir) {
const errors = collectNpmWorkspaceErrors(rootDir);
if (errors.length > 0) {
throw new Error(
`npm workspace validation failed:\n- ${errors.join('\n- ')}`,
);
}
}
const isMainModule =
process.argv[1] &&
pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
if (isMainModule) {
const rootDir = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
);
try {
checkNpmWorkspaces(rootDir);
console.log('[check:npm-workspaces] OK');
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}