git init
This commit is contained in:
502
tools/validate-generation.mjs
Normal file
502
tools/validate-generation.mjs
Normal file
@@ -0,0 +1,502 @@
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { getApiDslFiles, buildApiSummary } from './api-summary.mjs';
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const artifactsOnly = args.has('--artifacts-only');
|
||||
const runRuntime = args.has('--run-runtime');
|
||||
|
||||
const failures = [];
|
||||
const warnings = [];
|
||||
|
||||
function assertCondition(condition, message) {
|
||||
if (!condition) {
|
||||
failures.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
function warn(message) {
|
||||
warnings.push(message);
|
||||
}
|
||||
|
||||
function readIfExists(relativePath) {
|
||||
const filePath = path.join(rootDir, relativePath);
|
||||
if (!existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
function requireFile(relativePath) {
|
||||
assertCondition(existsSync(path.join(rootDir, relativePath)), `Missing file: ${relativePath}`);
|
||||
}
|
||||
|
||||
function requireFiles(relativePaths) {
|
||||
relativePaths.forEach(requireFile);
|
||||
}
|
||||
|
||||
function requireContent(relativePath, pattern, message) {
|
||||
const contents = readIfExists(relativePath);
|
||||
assertCondition(Boolean(contents), `Missing file: ${relativePath}`);
|
||||
if (!contents) {
|
||||
return;
|
||||
}
|
||||
|
||||
assertCondition(pattern.test(contents), `${message} (${relativePath})`);
|
||||
}
|
||||
|
||||
function parseJson(relativePath) {
|
||||
const raw = readIfExists(relativePath);
|
||||
if (!raw) {
|
||||
failures.push(`Missing file: ${relativePath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
failures.push(`Invalid JSON in ${relativePath}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getRealmArtifactPath() {
|
||||
const rootFiles = readdirSync(rootDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name);
|
||||
|
||||
const realmArtifacts = rootFiles.filter((entry) => /-realm\.json$/i.test(entry));
|
||||
assertCondition(realmArtifacts.length === 1, 'Expected exactly one root-level *-realm.json artifact');
|
||||
|
||||
return realmArtifacts[0] ?? null;
|
||||
}
|
||||
|
||||
function getWorkspaceInfo() {
|
||||
return {
|
||||
server: {
|
||||
dir: path.join(rootDir, 'server'),
|
||||
packagePath: 'server/package.json',
|
||||
scaffoldFiles: [
|
||||
'server/package.json',
|
||||
'server/tsconfig.json',
|
||||
'server/tsconfig.build.json',
|
||||
'server/nest-cli.json',
|
||||
'server/src/main.ts',
|
||||
'server/src/app.module.ts',
|
||||
],
|
||||
},
|
||||
client: {
|
||||
dir: path.join(rootDir, 'client'),
|
||||
packagePath: 'client/package.json',
|
||||
scaffoldFiles: [
|
||||
'client/package.json',
|
||||
'client/index.html',
|
||||
'client/tsconfig.json',
|
||||
'client/tsconfig.node.json',
|
||||
'client/vite.config.ts',
|
||||
'client/src/main.tsx',
|
||||
'client/src/vite-env.d.ts',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function validateBuildChecks() {
|
||||
requireFiles([
|
||||
'README.md',
|
||||
'package.json',
|
||||
'domain/dsl-spec.md',
|
||||
'api-summary.json',
|
||||
'server/prisma/schema.prisma',
|
||||
'docker-compose.yml',
|
||||
'server/Dockerfile',
|
||||
'client/Dockerfile',
|
||||
'client/nginx/default.conf',
|
||||
'server/.env.example',
|
||||
'client/.env.example',
|
||||
'prompts/general-prompt.md',
|
||||
'prompts/auth-rules.md',
|
||||
'prompts/backend-rules.md',
|
||||
'prompts/frontend-rules.md',
|
||||
'prompts/runtime-rules.md',
|
||||
'prompts/validation-rules.md',
|
||||
]);
|
||||
|
||||
// rule: AGENTS.md §Tier-1 — api.dsl must exist
|
||||
const apiDslFiles = getApiDslFiles(rootDir);
|
||||
assertCondition(apiDslFiles.length > 0, 'Expected at least one domain/*.api.dsl file');
|
||||
|
||||
// rule: AGENTS.md §Tier-2 — api-summary.json must match parsed api.dsl
|
||||
const actualApiSummaryRaw = readIfExists('api-summary.json');
|
||||
if (actualApiSummaryRaw) {
|
||||
try {
|
||||
const expectedApiSummary = JSON.stringify(buildApiSummary(rootDir), null, 2);
|
||||
assertCondition(
|
||||
actualApiSummaryRaw.trim() === expectedApiSummary,
|
||||
'api-summary.json is out of date. Run `npm run generate:api-summary`.',
|
||||
);
|
||||
} catch (error) {
|
||||
failures.push(`api-summary.json freshness check failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { server, client } = getWorkspaceInfo();
|
||||
requireFiles(server.scaffoldFiles);
|
||||
requireFiles(client.scaffoldFiles);
|
||||
|
||||
const serverPackage = parseJson(server.packagePath);
|
||||
if (serverPackage) {
|
||||
assertCondition(serverPackage.scripts?.build === 'nest build', 'server/package.json must keep `build = nest build`');
|
||||
assertCondition(serverPackage.scripts?.start === 'nest start', 'server/package.json must keep `start = nest start`');
|
||||
assertCondition(serverPackage.scripts?.['start:dev'] === 'nest start --watch', 'server/package.json must keep `start:dev = nest start --watch`');
|
||||
assertCondition(Boolean(serverPackage.scripts?.['start:prod']), 'server/package.json must define a start:prod script');
|
||||
assertCondition(Boolean(serverPackage.dependencies?.['@nestjs/core']), 'server/package.json must keep Nest runtime dependencies');
|
||||
}
|
||||
|
||||
const clientPackage = parseJson(client.packagePath);
|
||||
if (clientPackage) {
|
||||
assertCondition(clientPackage.scripts?.dev === 'vite', 'client/package.json must keep `dev = vite`');
|
||||
assertCondition(clientPackage.scripts?.build === 'vite build', 'client/package.json must keep `build = vite build`');
|
||||
assertCondition(clientPackage.scripts?.preview === 'vite preview', 'client/package.json must keep `preview = vite preview`');
|
||||
assertCondition(Boolean(clientPackage.devDependencies?.vite), 'client/package.json must keep Vite as a dev dependency');
|
||||
assertCondition(Boolean(clientPackage.devDependencies?.['@vitejs/plugin-react']), 'client/package.json must keep @vitejs/plugin-react as a dev dependency');
|
||||
}
|
||||
}
|
||||
|
||||
function validateAuthChecks() {
|
||||
requireFiles([
|
||||
'client/src/auth/keycloak.ts',
|
||||
'client/src/auth/authProvider.ts',
|
||||
'client/src/dataProvider.ts',
|
||||
'client/src/config/env.ts',
|
||||
'client/src/main.tsx',
|
||||
'client/src/App.tsx',
|
||||
'server/src/auth/auth.module.ts',
|
||||
'server/src/auth/auth.service.ts',
|
||||
'server/src/auth/guards/jwt-auth.guard.ts',
|
||||
'server/src/auth/guards/roles.guard.ts',
|
||||
'server/src/auth/decorators/public.decorator.ts',
|
||||
'server/src/auth/decorators/roles.decorator.ts',
|
||||
]);
|
||||
|
||||
requireContent(
|
||||
'client/src/auth/keycloak.ts',
|
||||
/onLoad:\s*'login-required'/,
|
||||
'Frontend auth must initialize Keycloak with login-required',
|
||||
);
|
||||
requireContent(
|
||||
'client/src/auth/keycloak.ts',
|
||||
/pkceMethod:\s*'S256'/,
|
||||
'Frontend auth must use PKCE S256',
|
||||
);
|
||||
requireContent(
|
||||
'client/src/auth/keycloak.ts',
|
||||
/updateToken\(/,
|
||||
'Frontend auth must refresh access tokens through the Keycloak adapter',
|
||||
);
|
||||
|
||||
const keycloakSource = readIfExists('client/src/auth/keycloak.ts') ?? '';
|
||||
assertCondition(
|
||||
!/loadUserProfile\(/.test(keycloakSource),
|
||||
'Frontend auth must not call keycloak.loadUserProfile()',
|
||||
);
|
||||
|
||||
requireContent(
|
||||
'client/src/dataProvider.ts',
|
||||
/Authorization', `Bearer \$\{token\}`/,
|
||||
'dataProvider must attach bearer tokens in the shared request seam',
|
||||
);
|
||||
|
||||
const authProvider = readIfExists('client/src/auth/authProvider.ts') ?? '';
|
||||
assertCondition(
|
||||
/status === 401/.test(authProvider) && /status === 403/.test(authProvider),
|
||||
'authProvider must distinguish 401 and 403 semantics',
|
||||
);
|
||||
|
||||
const authService = readIfExists('server/src/auth/auth.service.ts') ?? '';
|
||||
assertCondition(
|
||||
/jwtVerify/.test(authService) && /KEYCLOAK_ISSUER_URL/.test(authService) && /KEYCLOAK_AUDIENCE/.test(authService),
|
||||
'Backend auth must verify JWTs with issuer and audience',
|
||||
);
|
||||
assertCondition(/realm_access/.test(authService), 'Backend auth must extract roles from realm_access.roles');
|
||||
assertCondition(/KEYCLOAK_JWKS_URL/.test(authService), 'Backend auth must support explicit KEYCLOAK_JWKS_URL');
|
||||
assertCondition(/\.well-known\/openid-configuration/.test(authService), 'Backend auth must try OIDC discovery before fallback certs');
|
||||
assertCondition(/protocol\/openid-connect\/certs/.test(authService), 'Backend auth must keep Keycloak certs fallback resolution');
|
||||
}
|
||||
|
||||
function validateRealmChecks() {
|
||||
const realmArtifactName = getRealmArtifactPath();
|
||||
if (!realmArtifactName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const artifact = parseJson(realmArtifactName);
|
||||
if (!artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
const realmRoles = artifact.roles?.realm?.map((role) => role.name) ?? [];
|
||||
const frontendClient = artifact.clients?.find((client) => client.clientId?.endsWith('-frontend'));
|
||||
const backendClient = artifact.clients?.find((client) => client.clientId?.endsWith('-backend'));
|
||||
const audienceScope = artifact.clientScopes?.find((scope) => scope.name === 'api-audience');
|
||||
|
||||
['admin', 'editor', 'viewer'].forEach((role) => {
|
||||
assertCondition(realmRoles.includes(role), `Realm artifact must define realm role ${role}`);
|
||||
});
|
||||
|
||||
assertCondition(Boolean(frontendClient), 'Realm artifact must define the frontend SPA client');
|
||||
assertCondition(Boolean(backendClient), 'Realm artifact must define the backend resource client');
|
||||
assertCondition(Boolean(audienceScope), 'Realm artifact must define the api-audience client scope');
|
||||
|
||||
if (frontendClient) {
|
||||
assertCondition(frontendClient.publicClient === true, 'Frontend realm client must be public');
|
||||
assertCondition(
|
||||
frontendClient.standardFlowEnabled === true &&
|
||||
frontendClient.implicitFlowEnabled === false &&
|
||||
frontendClient.directAccessGrantsEnabled === false,
|
||||
'Frontend realm client must use standard flow only',
|
||||
);
|
||||
assertCondition(
|
||||
frontendClient.attributes?.['pkce.code.challenge.method'] === 'S256',
|
||||
'Frontend realm client must enforce PKCE S256',
|
||||
);
|
||||
|
||||
const mapperNames = new Set((frontendClient.protocolMappers ?? []).map((mapper) => mapper.name));
|
||||
['sub', 'preferred_username', 'email', 'name', 'realm roles'].forEach((mapperName) => {
|
||||
assertCondition(mapperNames.has(mapperName), `Frontend realm client must include protocol mapper ${mapperName}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (backendClient && audienceScope) {
|
||||
const audienceMapper = (audienceScope.protocolMappers ?? []).find(
|
||||
(mapper) => mapper.protocolMapper === 'oidc-audience-mapper',
|
||||
);
|
||||
assertCondition(Boolean(audienceMapper), 'api-audience scope must include an audience mapper');
|
||||
assertCondition(
|
||||
audienceMapper?.config?.['included.client.audience'] === backendClient.clientId,
|
||||
'api-audience scope must deliver the backend audience/client id',
|
||||
);
|
||||
assertCondition(backendClient.bearerOnly === true, 'Backend realm client must be bearer-only');
|
||||
}
|
||||
}
|
||||
|
||||
function validateRuntimeContractChecks() {
|
||||
requireFiles([
|
||||
'docker-compose.yml',
|
||||
'server/Dockerfile',
|
||||
'client/Dockerfile',
|
||||
'client/nginx/default.conf',
|
||||
]);
|
||||
const compose = readIfExists('docker-compose.yml') ?? '';
|
||||
assertCondition(/image:\s*postgres:16/.test(compose), 'docker-compose must provision postgres:16');
|
||||
assertCondition(/^\s{2}server\s*:/m.test(compose), 'docker-compose must define a server service');
|
||||
assertCondition(/^\s{2}client\s*:/m.test(compose), 'docker-compose must define a client service');
|
||||
assertCondition(
|
||||
/^\s{4}ports:\n\s{6}-\s*"\$\{CLIENT_PORT:-8080\}:80"/m.test(compose),
|
||||
'docker-compose client service must publish the SPA on ${CLIENT_PORT:-8080}:80 by default',
|
||||
);
|
||||
const hasKeycloakService =
|
||||
/^\s{2}keycloak\s*:/m.test(compose) || /image:\s*.*keycloak/i.test(compose);
|
||||
assertCondition(!hasKeycloakService, 'docker-compose must remain PostgreSQL-only (no Keycloak container)');
|
||||
|
||||
const codexConfig = readIfExists('.codex/config.toml') ?? '';
|
||||
if (codexConfig) {
|
||||
const agentConfigPaths = [...codexConfig.matchAll(/config_file\s*=\s*"([^"]+)"/g)].map((match) => match[1]);
|
||||
for (const configPath of agentConfigPaths) {
|
||||
assertCondition(!path.isAbsolute(configPath), `.codex/config.toml must not hardcode absolute config_file paths: ${configPath}`);
|
||||
const relativeToCodex = path.join('.codex', configPath);
|
||||
assertCondition(
|
||||
existsSync(path.join(rootDir, relativeToCodex)),
|
||||
`.codex/config.toml references a missing agent config: ${relativeToCodex}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const serverDockerfile = readIfExists('server/Dockerfile') ?? '';
|
||||
assertCondition(/FROM node:/i.test(serverDockerfile), 'server/Dockerfile must build from a Node base image');
|
||||
assertCondition(/npm run build/.test(serverDockerfile), 'server/Dockerfile must build the Nest workspace');
|
||||
assertCondition(/docker-entrypoint\.sh/.test(serverDockerfile), 'server/Dockerfile must use the repository entrypoint');
|
||||
|
||||
const clientDockerfile = readIfExists('client/Dockerfile') ?? '';
|
||||
assertCondition(/FROM nginx:/i.test(clientDockerfile), 'client/Dockerfile must use nginx for runtime serving');
|
||||
assertCondition(/COPY nginx\/default\.conf/.test(clientDockerfile), 'client/Dockerfile must install the nginx proxy config');
|
||||
assertCondition(/COPY --from=build \/app\/dist/.test(clientDockerfile), 'client/Dockerfile must copy the built SPA bundle');
|
||||
|
||||
const nginxConfig = readIfExists('client/nginx/default.conf') ?? '';
|
||||
assertCondition(/location \/api\//.test(nginxConfig), 'client/nginx/default.conf must proxy /api requests');
|
||||
assertCondition(/try_files \$uri \$uri\/ \/index\.html;/.test(nginxConfig), 'client/nginx/default.conf must preserve SPA routing');
|
||||
assertCondition(/proxy_pass/.test(nginxConfig), 'client/nginx/default.conf must proxy backend traffic');
|
||||
|
||||
const serverEnvExample = readIfExists('server/.env.example') ?? '';
|
||||
assertCondition(
|
||||
/KEYCLOAK_ISSUER_URL="https:\/\/sso\.greact\.ru\/realms\/toir"/.test(serverEnvExample),
|
||||
'server/.env.example must keep the working Keycloak issuer example',
|
||||
);
|
||||
assertCondition(
|
||||
/KEYCLOAK_AUDIENCE="?toir-backend"?/.test(serverEnvExample),
|
||||
'server/.env.example must keep the working backend audience example',
|
||||
);
|
||||
assertCondition(
|
||||
/CORS_ALLOWED_ORIGINS="http:\/\/localhost:5173,https:\/\/toir-frontend\.greact\.ru"/.test(serverEnvExample),
|
||||
'server/.env.example must keep the working CORS example with the production frontend domain',
|
||||
);
|
||||
assertCondition(
|
||||
!/KEYCLOAK_ISSUER_URL=http:\/\/localhost:8080\/realms\/toir/.test(serverEnvExample),
|
||||
'server/.env.example must not regress to localhost Keycloak as the baseline issuer example',
|
||||
);
|
||||
|
||||
const clientEnvExample = readIfExists('client/.env.example') ?? '';
|
||||
assertCondition(
|
||||
/VITE_KEYCLOAK_URL=https:\/\/sso\.greact\.ru/.test(clientEnvExample),
|
||||
'client/.env.example must keep the working domain-based Keycloak URL example',
|
||||
);
|
||||
assertCondition(
|
||||
/VITE_KEYCLOAK_REALM=toir/.test(clientEnvExample) && /VITE_KEYCLOAK_CLIENT_ID=toir-frontend/.test(clientEnvExample),
|
||||
'client/.env.example must keep the working realm and frontend client examples',
|
||||
);
|
||||
assertCondition(
|
||||
!/VITE_KEYCLOAK_URL=http:\/\/localhost:8080/.test(clientEnvExample),
|
||||
'client/.env.example must not regress to localhost Keycloak as the baseline example',
|
||||
);
|
||||
|
||||
const healthController = readIfExists('server/src/health/health.controller.ts') ?? '';
|
||||
assertCondition(Boolean(healthController), 'Missing file: server/src/health/health.controller.ts');
|
||||
if (healthController) {
|
||||
assertCondition(
|
||||
/@Public\(\)/.test(healthController) && /@Controller\('health'\)/.test(healthController),
|
||||
'/health must stay public',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(command, commandArgs, workdir, failureLabel) {
|
||||
const runtimeEnv = { ...process.env };
|
||||
const envExamplePath = path.join(workdir, '.env.example');
|
||||
if (existsSync(envExamplePath)) {
|
||||
const envExample = readFileSync(envExamplePath, 'utf8');
|
||||
for (const line of envExample.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const separator = trimmed.indexOf('=');
|
||||
if (separator <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = trimmed.slice(0, separator).trim();
|
||||
const value = trimmed.slice(separator + 1).trim().replace(/^"|"$/g, '');
|
||||
if (!(key in runtimeEnv)) {
|
||||
runtimeEnv[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const commandLine = [command, ...commandArgs].join(' ');
|
||||
const result = spawnSync(commandLine, {
|
||||
cwd: workdir,
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
env: runtimeEnv,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
failures.push(`${failureLabel}: ${commandLine}\n${result.error.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
const stderr = result.stderr?.trim();
|
||||
const stdout = result.stdout?.trim();
|
||||
failures.push(
|
||||
`${failureLabel}: ${commandLine}${stderr ? `\n${stderr}` : stdout ? `\n${stdout}` : ''}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function maybeValidateWorkspaceBuild(relativeDir) {
|
||||
const workspaceDir = path.join(rootDir, relativeDir);
|
||||
if (!existsSync(path.join(workspaceDir, 'package.json'))) {
|
||||
failures.push(`Missing file: ${relativeDir}/package.json`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existsSync(path.join(workspaceDir, 'node_modules'))) {
|
||||
warn(`Skipped build verification for ${relativeDir}: install dependencies in ${relativeDir}/ to validate workspace buildability.`);
|
||||
return;
|
||||
}
|
||||
|
||||
runCommand('npm', ['run', 'build'], workspaceDir, `Build verification failed in ${relativeDir}`);
|
||||
}
|
||||
|
||||
function validateBuildExecutionChecks() {
|
||||
maybeValidateWorkspaceBuild('server');
|
||||
maybeValidateWorkspaceBuild('client');
|
||||
}
|
||||
|
||||
function validateRuntimeExecutionChecks() {
|
||||
const serverDir = path.join(rootDir, 'server');
|
||||
if (!existsSync(path.join(serverDir, 'node_modules'))) {
|
||||
failures.push(
|
||||
'Runtime validation requires installed backend dependencies. Run `npm install` in server/ before `npm run validate:generation:runtime`.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
runCommand('npx', ['prisma', 'generate'], serverDir, 'Prisma generate failed');
|
||||
const migrationsDir = path.join(serverDir, 'prisma', 'migrations');
|
||||
const hasMigrations =
|
||||
existsSync(migrationsDir) &&
|
||||
readdirSync(migrationsDir, { withFileTypes: true }).some((entry) => entry.isDirectory());
|
||||
|
||||
if (hasMigrations) {
|
||||
runCommand('npx', ['prisma', 'migrate', 'deploy'], serverDir, 'Prisma migrate deploy failed');
|
||||
} else {
|
||||
warn('No committed Prisma migrations found; runtime validation is using `prisma db push` as a temporary bootstrap fallback.');
|
||||
runCommand('npx', ['prisma', 'db', 'push'], serverDir, 'Prisma db push failed');
|
||||
}
|
||||
runCommand('npx', ['prisma', 'db', 'seed'], serverDir, 'Prisma seed failed');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Structural validator scope only:
|
||||
// keep stable scaffold/runtime/auth/realm/build invariants here.
|
||||
// Entity-level DSL fidelity and CRUD/UI semantics are owned by `npm run eval:generation`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
validateBuildChecks();
|
||||
validateAuthChecks();
|
||||
validateRealmChecks();
|
||||
validateRuntimeContractChecks();
|
||||
|
||||
if (!artifactsOnly) {
|
||||
validateBuildExecutionChecks();
|
||||
}
|
||||
|
||||
if (!artifactsOnly && runRuntime) {
|
||||
validateRuntimeExecutionChecks();
|
||||
} else if (!artifactsOnly) {
|
||||
warn('Runtime command execution skipped. Use --run-runtime after installing dependencies and starting the local database.');
|
||||
}
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.warn(`WARN: ${warning}`);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('Generation validation failed:');
|
||||
for (const failure of failures) {
|
||||
console.error(`- ${failure}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Generation validation passed.');
|
||||
Reference in New Issue
Block a user