511 lines
20 KiB
JavaScript
511 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import pg from "pg";
|
|
|
|
export const REPOSITORY_ROOT = path.resolve(import.meta.dirname, "..");
|
|
export const BASELINE_MANIFEST_PATH = path.join(REPOSITORY_ROOT, "prisma/baseline-legacy-manifest.json");
|
|
export const LEGACY_MIGRATION_ARCHIVE_PATH = path.join(REPOSITORY_ROOT, "prisma/baseline-legacy-migrations.tar.gz");
|
|
export const PRISMA_CLI_PATH = path.join(REPOSITORY_ROOT, "node_modules/prisma/build/index.js");
|
|
|
|
const IMPERSONATION_AUDIT_MIGRATION = "20260508140000_impersonation_audit";
|
|
const BOOKING_OVERLAP_MIGRATION = "20260717090000_ak_p16b_booking_overlap_constraint";
|
|
const TENANT_DELETION_MIGRATION = "20260721090000_ak_p21_tenant_deletion_capabilities";
|
|
const SUPPORT_RETENTION_MIGRATION = "20260721093000_ak_p21_support_retention_purge";
|
|
|
|
export const MIGRATION_TABLE_EXISTS_QUERY = `
|
|
SELECT to_regclass('public._prisma_migrations') IS NOT NULL AS "tableExists";
|
|
`;
|
|
|
|
export const MIGRATION_HISTORY_QUERY = `
|
|
SELECT
|
|
migration_name AS "name",
|
|
checksum,
|
|
finished_at AS "finishedAt",
|
|
rolled_back_at AS "rolledBackAt"
|
|
FROM public._prisma_migrations
|
|
ORDER BY started_at, id;
|
|
`;
|
|
|
|
export const LEGACY_VARIANT_CATALOG_CHECKS = [
|
|
{
|
|
id: "impersonation audit delete semantics",
|
|
migrations: [IMPERSONATION_AUDIT_MIGRATION],
|
|
query: `
|
|
SELECT
|
|
(SELECT is_nullable = 'YES'
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public' AND table_name = 'ImpersonationAudit' AND column_name = 'actorUserId')
|
|
AND
|
|
(SELECT is_nullable = 'YES'
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public' AND table_name = 'ImpersonationAudit' AND column_name = 'targetUserId')
|
|
AND
|
|
(SELECT confdeltype = 'n'
|
|
FROM pg_constraint
|
|
WHERE conrelid = 'public."ImpersonationAudit"'::regclass AND conname = 'ImpersonationAudit_actorUserId_fkey')
|
|
AND
|
|
(SELECT confdeltype = 'n'
|
|
FROM pg_constraint
|
|
WHERE conrelid = 'public."ImpersonationAudit"'::regclass AND conname = 'ImpersonationAudit_targetUserId_fkey')
|
|
AS "matches";
|
|
`,
|
|
},
|
|
{
|
|
id: "tenant-scoped booking overlap constraint",
|
|
migrations: [BOOKING_OVERLAP_MIGRATION],
|
|
query: `
|
|
SELECT COALESCE(
|
|
(SELECT pg_get_constraintdef(oid, false) LIKE '%"tenantId" WITH =%'
|
|
FROM pg_constraint
|
|
WHERE conrelid = 'public."Appointment"'::regclass AND conname = 'appointment_no_overlap'),
|
|
false
|
|
) AS "matches";
|
|
`,
|
|
},
|
|
{
|
|
id: "tenant deletion function ownership and ACLs",
|
|
migrations: [TENANT_DELETION_MIGRATION],
|
|
query: `
|
|
WITH expected(function_oid) AS (
|
|
VALUES
|
|
('public.hivegrid_purge_tenant_guarded_rows(text)'::regprocedure::oid),
|
|
('public.hivegrid_minimize_tenant_support_pii(text)'::regprocedure::oid)
|
|
)
|
|
SELECT COUNT(*) = 2
|
|
AND BOOL_AND(pg_get_userbyid(proc.proowner) = 'hivegrid_rls_definer')
|
|
AND BOOL_AND(has_function_privilege('hivegrid_worker', proc.oid, 'EXECUTE'))
|
|
AND BOOL_AND(NOT has_function_privilege('hivegrid_app', proc.oid, 'EXECUTE'))
|
|
AND BOOL_AND(NOT has_function_privilege('hivegrid_authentik_jit', proc.oid, 'EXECUTE'))
|
|
AND BOOL_AND(NOT EXISTS (
|
|
SELECT 1
|
|
FROM aclexplode(COALESCE(proc.proacl, acldefault('f', proc.proowner))) AS acl
|
|
WHERE acl.grantee = 0 AND acl.privilege_type = 'EXECUTE'
|
|
)) AS "matches"
|
|
FROM expected
|
|
JOIN pg_proc AS proc ON proc.oid = expected.function_oid;
|
|
`,
|
|
},
|
|
{
|
|
id: "support retention function ownership and ACLs",
|
|
migrations: [SUPPORT_RETENTION_MIGRATION],
|
|
query: `
|
|
SELECT pg_get_userbyid(proc.proowner) = 'hivegrid_rls_definer'
|
|
AND has_function_privilege('hivegrid_worker', proc.oid, 'EXECUTE')
|
|
AND NOT has_function_privilege('hivegrid_app', proc.oid, 'EXECUTE')
|
|
AND NOT has_function_privilege('hivegrid_authentik_jit', proc.oid, 'EXECUTE')
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM aclexplode(COALESCE(proc.proacl, acldefault('f', proc.proowner))) AS acl
|
|
WHERE acl.grantee = 0 AND acl.privilege_type = 'EXECUTE'
|
|
) AS "matches"
|
|
FROM pg_proc AS proc
|
|
WHERE proc.oid = 'public.hivegrid_purge_tenant_retained_support(text)'::regprocedure;
|
|
`,
|
|
},
|
|
];
|
|
|
|
export class BaselineAdoptionError extends Error {
|
|
constructor(message, options) {
|
|
super(message, options);
|
|
this.name = "BaselineAdoptionError";
|
|
}
|
|
}
|
|
|
|
export function readBaselineManifest(manifestPath = BASELINE_MANIFEST_PATH) {
|
|
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
}
|
|
|
|
function assertManifest(manifest) {
|
|
if (
|
|
manifest?.version !== 1 ||
|
|
typeof manifest.baselineMigration !== "string" ||
|
|
!/^[a-f0-9]{64}$/.test(manifest.baselineChecksum ?? "") ||
|
|
!Array.isArray(manifest.migrations) ||
|
|
manifest.legacyMigrationCount !== manifest.migrations.length
|
|
) {
|
|
throw new BaselineAdoptionError("The Prisma baseline legacy manifest is malformed");
|
|
}
|
|
|
|
const names = new Set();
|
|
for (const migration of manifest.migrations) {
|
|
if (typeof migration?.name !== "string" || !/^[a-f0-9]{64}$/.test(migration.checksum ?? "") || names.has(migration.name)) {
|
|
throw new BaselineAdoptionError("The Prisma baseline legacy manifest contains an invalid or duplicate migration");
|
|
}
|
|
names.add(migration.name);
|
|
}
|
|
|
|
if (!Array.isArray(manifest.legacyChecksumVariants)) {
|
|
throw new BaselineAdoptionError("The Prisma baseline legacy manifest is missing its reviewed checksum variants");
|
|
}
|
|
|
|
const currentChecksums = new Map(manifest.migrations.map((migration) => [migration.name, migration.checksum]));
|
|
const variantKeys = new Set();
|
|
for (const variant of manifest.legacyChecksumVariants) {
|
|
const key = `${variant?.name}:${variant?.checksum}`;
|
|
if (
|
|
!names.has(variant?.name) ||
|
|
!/^[a-f0-9]{64}$/.test(variant?.checksum ?? "") ||
|
|
!/^[a-f0-9]{40}$/.test(variant?.sourceCommit ?? "") ||
|
|
currentChecksums.get(variant.name) === variant.checksum ||
|
|
variantKeys.has(key)
|
|
) {
|
|
throw new BaselineAdoptionError("The Prisma baseline legacy manifest contains an invalid checksum variant");
|
|
}
|
|
variantKeys.add(key);
|
|
}
|
|
}
|
|
|
|
function isSuccessful(row) {
|
|
return row.finishedAt != null && row.rolledBackAt == null;
|
|
}
|
|
|
|
function isUnresolvedFailure(row) {
|
|
return row.finishedAt == null && row.rolledBackAt == null;
|
|
}
|
|
|
|
function matchesReviewedChecksum(name, checksum, expectedChecksum, reviewedVariants) {
|
|
return checksum === expectedChecksum || reviewedVariants.get(name)?.has(checksum);
|
|
}
|
|
|
|
export function classifyMigrationHistory({ tableExists, rows }, manifest) {
|
|
assertManifest(manifest);
|
|
|
|
if (!tableExists) return { mode: "fresh", detail: "migration table absent" };
|
|
if (!Array.isArray(rows)) throw new BaselineAdoptionError("Prisma migration history rows are unavailable");
|
|
if (rows.some(isUnresolvedFailure)) {
|
|
throw new BaselineAdoptionError("Prisma migration history contains an unresolved failed migration");
|
|
}
|
|
|
|
const successful = rows.filter(isSuccessful);
|
|
const baselineRows = successful.filter((row) => row.name === manifest.baselineMigration);
|
|
if (baselineRows.length > 0) {
|
|
if (baselineRows.length !== 1 || baselineRows[0].checksum !== manifest.baselineChecksum) {
|
|
throw new BaselineAdoptionError("The applied Prisma baseline does not match the committed baseline checksum");
|
|
}
|
|
return { mode: "baseline", detail: "baseline already applied" };
|
|
}
|
|
|
|
if (rows.length === 0) return { mode: "fresh", detail: "migration history empty" };
|
|
|
|
const expected = new Map(manifest.migrations.map((migration) => [migration.name, migration.checksum]));
|
|
const reviewedVariants = new Map();
|
|
for (const variant of manifest.legacyChecksumVariants) {
|
|
const checksums = reviewedVariants.get(variant.name) ?? new Set();
|
|
checksums.add(variant.checksum);
|
|
reviewedVariants.set(variant.name, checksums);
|
|
}
|
|
const rowNames = new Set(rows.map((row) => row.name));
|
|
const unexpectedNames = [...rowNames].filter((name) => !expected.has(name));
|
|
if (unexpectedNames.length > 0) {
|
|
throw new BaselineAdoptionError("Prisma migration history contains entries outside the reviewed legacy chain");
|
|
}
|
|
|
|
const successfulByName = new Map();
|
|
for (const row of successful) {
|
|
const entries = successfulByName.get(row.name) ?? [];
|
|
entries.push(row);
|
|
successfulByName.set(row.name, entries);
|
|
}
|
|
|
|
const variantNames = [];
|
|
let successfulPrefixLength = 0;
|
|
let gapFound = false;
|
|
for (const [name, checksum] of expected) {
|
|
const entries = successfulByName.get(name) ?? [];
|
|
if (entries.length === 0) {
|
|
gapFound = true;
|
|
continue;
|
|
}
|
|
if (entries.length !== 1 || gapFound) {
|
|
throw new BaselineAdoptionError("Prisma migration history is not an exact legacy prefix");
|
|
}
|
|
if (!matchesReviewedChecksum(name, entries[0].checksum, checksum, reviewedVariants)) {
|
|
throw new BaselineAdoptionError(`Prisma migration history checksum differs from the reviewed legacy chain (${name})`);
|
|
}
|
|
if (entries[0].checksum !== checksum) {
|
|
variantNames.push(name);
|
|
}
|
|
successfulPrefixLength += 1;
|
|
}
|
|
|
|
if (successful.length !== successfulPrefixLength) {
|
|
throw new BaselineAdoptionError("Prisma migration history contains duplicate successful legacy entries");
|
|
}
|
|
|
|
if (successfulPrefixLength < manifest.migrations.length) {
|
|
return {
|
|
mode: "legacy-prefix",
|
|
detail: "checksum-verified legacy prefix",
|
|
variantNames,
|
|
appliedMigrationCount: successfulPrefixLength,
|
|
missingMigrations: manifest.migrations.slice(successfulPrefixLength).map((migration) => migration.name),
|
|
};
|
|
}
|
|
|
|
if (successful.length !== manifest.migrations.length) {
|
|
throw new BaselineAdoptionError("Prisma migration history has an unexpected successful legacy migration count");
|
|
}
|
|
|
|
return {
|
|
mode: "legacy",
|
|
detail: variantNames.length === 0 ? "complete reviewed legacy chain" : "complete reviewed legacy chain with historical checksums",
|
|
variantNames,
|
|
};
|
|
}
|
|
|
|
function sha256(value) {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
export function prepareLegacyMigrationArchive({ manifest, archivePath = LEGACY_MIGRATION_ARCHIVE_PATH, spawnImpl = spawnSync, tempRoot = os.tmpdir() } = {}) {
|
|
assertManifest(manifest);
|
|
if (!/^[a-f0-9]{64}$/.test(manifest.legacyArchiveChecksum ?? "") || sha256(readFileSync(archivePath)) !== manifest.legacyArchiveChecksum) {
|
|
throw new BaselineAdoptionError("The Prisma legacy migration archive does not match the reviewed manifest");
|
|
}
|
|
|
|
const temporaryDirectory = mkdtempSync(path.join(tempRoot, "hivegrid-legacy-catch-up-"));
|
|
const cleanup = () => rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
try {
|
|
const extraction = spawnImpl("tar", ["-xzf", archivePath, "-C", temporaryDirectory], {
|
|
cwd: REPOSITORY_ROOT,
|
|
stdio: "inherit",
|
|
});
|
|
if (extraction.error || extraction.status !== 0) {
|
|
throw new BaselineAdoptionError("Could not extract the reviewed Prisma legacy migration archive", {
|
|
cause: extraction.error,
|
|
});
|
|
}
|
|
|
|
const migrationsPath = path.join(temporaryDirectory, "prisma/migrations");
|
|
const migrationNames = readdirSync(migrationsPath, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
const expectedNames = manifest.migrations.map((migration) => migration.name);
|
|
if (JSON.stringify(migrationNames) !== JSON.stringify(expectedNames)) {
|
|
throw new BaselineAdoptionError("The Prisma legacy migration archive contains an unexpected migration set");
|
|
}
|
|
for (const migration of manifest.migrations) {
|
|
const sqlPath = path.join(migrationsPath, migration.name, "migration.sql");
|
|
if (sha256(readFileSync(sqlPath)) !== migration.checksum) {
|
|
throw new BaselineAdoptionError(`The Prisma legacy migration archive checksum differs (${migration.name})`);
|
|
}
|
|
}
|
|
|
|
return { migrationsPath, cleanup };
|
|
} catch (error) {
|
|
cleanup();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function deployMissingLegacyMigrations({
|
|
manifest,
|
|
missingMigrations,
|
|
env = process.env,
|
|
prepareArchive = prepareLegacyMigrationArchive,
|
|
spawnImpl = spawnSync,
|
|
tempRoot = os.tmpdir(),
|
|
} = {}) {
|
|
const connectionString = env.DIRECT_URL?.trim();
|
|
if (!connectionString) throw new BaselineAdoptionError("DIRECT_URL is required for Prisma legacy catch-up");
|
|
|
|
const expectedMissing = manifest.migrations.slice(manifest.migrations.length - missingMigrations.length).map((migration) => migration.name);
|
|
if (missingMigrations.length === 0 || JSON.stringify(missingMigrations) !== JSON.stringify(expectedMissing)) {
|
|
throw new BaselineAdoptionError("Prisma legacy catch-up requires the exact missing manifest suffix");
|
|
}
|
|
|
|
const prepared = prepareArchive({ manifest });
|
|
let configDirectory;
|
|
try {
|
|
configDirectory = mkdtempSync(path.join(tempRoot, "hivegrid-prisma-config-"));
|
|
const configPath = path.join(configDirectory, "prisma.config.ts");
|
|
const prismaConfigUrl = pathToFileURL(path.join(REPOSITORY_ROOT, "node_modules/prisma/config.js")).href;
|
|
writeFileSync(
|
|
configPath,
|
|
[
|
|
`import { defineConfig, env } from ${JSON.stringify(prismaConfigUrl)};`,
|
|
"",
|
|
"export default defineConfig({",
|
|
` schema: ${JSON.stringify(path.join(REPOSITORY_ROOT, "prisma/schema"))},`,
|
|
` migrations: { path: ${JSON.stringify(prepared.migrationsPath)} },`,
|
|
' datasource: { url: env("DIRECT_URL") },',
|
|
"});",
|
|
"",
|
|
].join("\n"),
|
|
{ encoding: "utf8", mode: 0o600 }
|
|
);
|
|
const result = spawnImpl(process.execPath, [PRISMA_CLI_PATH, "migrate", "deploy", "--config", configPath], {
|
|
cwd: REPOSITORY_ROOT,
|
|
env,
|
|
stdio: "inherit",
|
|
});
|
|
if (result.error || result.status !== 0) {
|
|
throw new BaselineAdoptionError("Prisma could not apply the missing reviewed legacy migration suffix", {
|
|
cause: result.error,
|
|
});
|
|
}
|
|
} finally {
|
|
if (configDirectory) rmSync(configDirectory, { recursive: true, force: true });
|
|
prepared.cleanup();
|
|
}
|
|
}
|
|
|
|
export function verifyPrismaSchemaParity({ env = process.env, spawnImpl = spawnSync } = {}) {
|
|
const result = spawnImpl(
|
|
process.execPath,
|
|
[PRISMA_CLI_PATH, "migrate", "diff", "--from-config-datasource", "--to-schema", "prisma/schema", "--exit-code", "--config", "prisma.config.ts"],
|
|
{
|
|
cwd: REPOSITORY_ROOT,
|
|
env,
|
|
stdio: "inherit",
|
|
}
|
|
);
|
|
if (result.error || result.status === 1 || result.status == null) {
|
|
throw new BaselineAdoptionError("Could not verify the live database schema after applying migrations", {
|
|
cause: result.error,
|
|
});
|
|
}
|
|
if (result.status === 2) {
|
|
throw new BaselineAdoptionError("Live database schema differs from the reviewed current Prisma schema after applying migrations");
|
|
}
|
|
if (result.status !== 0) {
|
|
throw new BaselineAdoptionError(`Live database schema verification returned unsupported exit status ${result.status}`);
|
|
}
|
|
}
|
|
|
|
export async function verifyLegacyVariantCatalog({
|
|
variantNames,
|
|
env = process.env,
|
|
clientFactory = (connectionString) => new pg.Client({ connectionString, application_name: "hivegrid-prisma-baseline-variant-verification" }),
|
|
} = {}) {
|
|
const requiredChecks = LEGACY_VARIANT_CATALOG_CHECKS.filter((check) => check.migrations.some((migration) => variantNames.includes(migration)));
|
|
if (requiredChecks.length === 0) return;
|
|
|
|
const connectionString = env.DIRECT_URL?.trim();
|
|
if (!connectionString) throw new BaselineAdoptionError("DIRECT_URL is required for Prisma baseline variant verification");
|
|
|
|
const client = clientFactory(connectionString);
|
|
try {
|
|
await client.connect();
|
|
for (const check of requiredChecks) {
|
|
const result = await client.query(check.query);
|
|
if (result.rows[0]?.matches !== true) {
|
|
throw new BaselineAdoptionError(`Live database does not satisfy the reviewed ${check.id} contract`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof BaselineAdoptionError) throw error;
|
|
throw new BaselineAdoptionError("Could not verify the live database catalog before adopting historical checksums", {
|
|
cause: error,
|
|
});
|
|
} finally {
|
|
await client.end().catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
export async function inspectMigrationHistory({
|
|
env = process.env,
|
|
clientFactory = (connectionString) => new pg.Client({ connectionString, application_name: "hivegrid-prisma-baseline-adoption" }),
|
|
} = {}) {
|
|
const connectionString = env.DIRECT_URL?.trim();
|
|
if (!connectionString) throw new BaselineAdoptionError("DIRECT_URL is required for Prisma baseline adoption");
|
|
|
|
const client = clientFactory(connectionString);
|
|
try {
|
|
await client.connect();
|
|
const tableState = await client.query(MIGRATION_TABLE_EXISTS_QUERY);
|
|
const tableExists = tableState.rows[0]?.tableExists === true;
|
|
if (!tableExists) return { tableExists: false, rows: [] };
|
|
const history = await client.query(MIGRATION_HISTORY_QUERY);
|
|
return { tableExists: true, rows: history.rows };
|
|
} catch (error) {
|
|
if (error instanceof BaselineAdoptionError) throw error;
|
|
throw new BaselineAdoptionError("Could not inspect Prisma migration history; inspect restricted database logs", { cause: error });
|
|
} finally {
|
|
await client.end().catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
export function resolveBaselineMigration({ manifest, env = process.env, spawnImpl = spawnSync } = {}) {
|
|
const result = spawnImpl(process.execPath, [PRISMA_CLI_PATH, "migrate", "resolve", "--applied", manifest.baselineMigration, "--config", "prisma.config.ts"], {
|
|
cwd: REPOSITORY_ROOT,
|
|
env,
|
|
stdio: "inherit",
|
|
});
|
|
if (result.error || result.status !== 0) {
|
|
throw new BaselineAdoptionError("Prisma could not record the reviewed baseline migration", { cause: result.error });
|
|
}
|
|
}
|
|
|
|
export async function adoptPrismaBaseline({
|
|
manifest = readBaselineManifest(),
|
|
inspect = inspectMigrationHistory,
|
|
deployMissing = deployMissingLegacyMigrations,
|
|
verifyCatalog = verifyLegacyVariantCatalog,
|
|
resolve = resolveBaselineMigration,
|
|
stdout = process.stdout,
|
|
} = {}) {
|
|
let before = classifyMigrationHistory(await inspect(), manifest);
|
|
if (before.mode === "fresh") {
|
|
stdout.write("[baseline] fresh database; migrate deploy will apply the baseline\n");
|
|
return before;
|
|
}
|
|
if (before.mode === "baseline") {
|
|
stdout.write("[baseline] committed baseline already recorded\n");
|
|
return before;
|
|
}
|
|
if (before.mode === "legacy-prefix") {
|
|
stdout.write(
|
|
`[baseline] applying ${before.missingMigrations.length} checksum-verified legacy migration(s) after ${before.appliedMigrationCount} applied entries\n`
|
|
);
|
|
await deployMissing({ manifest, missingMigrations: before.missingMigrations });
|
|
before = classifyMigrationHistory(await inspect(), manifest);
|
|
if (before.mode !== "legacy") {
|
|
throw new BaselineAdoptionError("Prisma legacy catch-up did not produce the complete reviewed legacy chain");
|
|
}
|
|
stdout.write("[baseline] legacy migration suffix applied and verified\n");
|
|
}
|
|
|
|
if (before.variantNames.length > 0) {
|
|
stdout.write(`[baseline] verifying ${before.variantNames.length} reviewed historical checksum variant catalog contract(s)\n`);
|
|
await verifyCatalog({ variantNames: before.variantNames });
|
|
stdout.write("[baseline] security-sensitive historical catalog contracts verified\n");
|
|
}
|
|
|
|
stdout.write(`[baseline] adopting ${manifest.baselineMigration} over the checksum-verified legacy chain\n`);
|
|
await resolve({ manifest });
|
|
const after = classifyMigrationHistory(await inspect(), manifest);
|
|
if (after.mode !== "baseline") {
|
|
throw new BaselineAdoptionError("Prisma baseline adoption did not produce a verified baseline history entry");
|
|
}
|
|
stdout.write("[baseline] adoption verified\n");
|
|
return { mode: "adopted", detail: before.detail };
|
|
}
|
|
|
|
const isCli = process.argv[1] && path.join(import.meta.dirname, "adopt-prisma-baseline.mjs") === path.resolve(process.argv[1]);
|
|
|
|
if (isCli) {
|
|
try {
|
|
const args = process.argv.slice(2);
|
|
if (args.length === 0 || (args.length === 1 && args[0] === "--adopt")) {
|
|
await adoptPrismaBaseline();
|
|
} else if (args.length === 1 && args[0] === "--verify-current-schema") {
|
|
verifyPrismaSchemaParity();
|
|
process.stdout.write("[baseline] current live schema verified after migrations\n");
|
|
} else {
|
|
throw new BaselineAdoptionError("Unsupported Prisma baseline adoption argument");
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof BaselineAdoptionError ? error.message : "Prisma baseline adoption failed without a safe diagnostic";
|
|
process.stderr.write(`FAIL ${message}\n`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|