536 lines
19 KiB
JavaScript
536 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawnSync, spawn } from 'node:child_process';
|
|
import { createServer } from 'node:http';
|
|
import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const sourceWorkspaceDir = path.resolve(scriptDir, '..');
|
|
let workspaceDir = sourceWorkspaceDir;
|
|
|
|
const packages = [
|
|
'@open-matrix/contracts',
|
|
'@open-matrix/federation',
|
|
'@open-matrix/omega-core',
|
|
'@open-matrix/core',
|
|
'@open-matrix/browser-host',
|
|
'@open-matrix/browser-kit',
|
|
'@open-matrix/hivecast-sdk',
|
|
'@open-matrix/cli',
|
|
];
|
|
|
|
function run(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: options.cwd ?? workspaceDir,
|
|
stdio: options.stdio ?? 'inherit',
|
|
env: { ...process.env, ...options.env },
|
|
encoding: 'utf8',
|
|
});
|
|
if (result.status !== 0) {
|
|
const details = options.stdio === 'pipe'
|
|
? `\nstdout:\n${result.stdout ?? ''}\nstderr:\n${result.stderr ?? ''}`
|
|
: '';
|
|
throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}${details}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function parseJsonFromOutput(output, label) {
|
|
const trimmed = output.trim();
|
|
if (!trimmed) throw new Error(`${label} produced no output`);
|
|
const start = trimmed.lastIndexOf('\n{');
|
|
const jsonText = start >= 0 ? trimmed.slice(start + 1) : trimmed;
|
|
try {
|
|
return JSON.parse(jsonText);
|
|
} catch (error) {
|
|
throw new Error(`${label} did not end with JSON: ${error.message}\n${output}`);
|
|
}
|
|
}
|
|
|
|
function runRuntimeHost(runtimeHostBin, args, options = {}) {
|
|
const command = runtimeHostBin.endsWith('.js') || runtimeHostBin.endsWith('.mjs')
|
|
? process.execPath
|
|
: runtimeHostBin;
|
|
const prefix = command === process.execPath ? [runtimeHostBin] : [];
|
|
return run(command, [...prefix, '--workspace-source', ...args], options);
|
|
}
|
|
|
|
async function findTarball(tarballDir, packageName) {
|
|
const packageDir = path.join(workspaceDir, 'packages', packageName.split('/').at(-1));
|
|
const packageJson = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
|
|
const expectedPrefix = packageJson.name.replace('@', '').replace('/', '-');
|
|
const entries = await readdir(tarballDir);
|
|
const tarball = entries.find((entry) => entry.startsWith(expectedPrefix) && entry.endsWith('.tgz'));
|
|
if (!tarball) throw new Error(`Packed tarball missing for ${packageName}`);
|
|
return path.join(tarballDir, tarball);
|
|
}
|
|
|
|
async function getFreePort() {
|
|
return new Promise((resolvePort, reject) => {
|
|
const server = createServer();
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
server.close(() => reject(new Error('Unable to allocate a local port')));
|
|
return;
|
|
}
|
|
const { port } = address;
|
|
server.close(() => resolvePort(port));
|
|
});
|
|
});
|
|
}
|
|
|
|
function envString(...names) {
|
|
for (const name of names) {
|
|
const value = process.env[name];
|
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function resolveLiveKeyConfig() {
|
|
const key = envString('MATRIX_API_KEY', 'HIVECAST_TEST_KEY', 'HIVECAST_API_KEY');
|
|
const cloud = envString('MATRIX_CLOUD', 'HIVECAST_TEST_CLOUD', 'HIVECAST_CLOUD');
|
|
const space = envString('MATRIX_SPACE', 'HIVECAST_TEST_SPACE', 'HIVECAST_SPACE');
|
|
const provided = [key, cloud, space].filter(Boolean).length;
|
|
if (provided > 0 && provided < 3) {
|
|
throw new Error('Live SDK CLI proof requires MATRIX_API_KEY/MATRIX_CLOUD/MATRIX_SPACE together');
|
|
}
|
|
if (!key || !cloud || !space) return null;
|
|
return { key, cloud: cloud.replace(/\/+$/g, ''), space };
|
|
}
|
|
|
|
async function validateLiveKeyConfig(config) {
|
|
const response = await fetch(`${config.cloud}/api/install/key-info`, {
|
|
method: 'POST',
|
|
headers: {
|
|
authorization: `Bearer ${config.key}`,
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ space: config.space }),
|
|
});
|
|
const text = await response.text();
|
|
if (!response.ok) {
|
|
throw new Error(`Live SDK CLI key validation failed (${response.status}): ${text}`);
|
|
}
|
|
const body = text.trim() ? JSON.parse(text) : {};
|
|
if (body?.ok !== true) {
|
|
throw new Error(`Live SDK CLI key validation did not return ok:true: ${text}`);
|
|
}
|
|
return {
|
|
ok: true,
|
|
cloud: config.cloud,
|
|
space: config.space,
|
|
keyId: body.key?.id ?? body.id ?? null,
|
|
fingerprint: body.key?.fingerprint ?? body.fingerprint ?? null,
|
|
};
|
|
}
|
|
|
|
async function findOnlyTarball(dir, label) {
|
|
const entries = (await readdir(dir)).filter((entry) => entry.endsWith('.tgz'));
|
|
if (entries.length !== 1) {
|
|
throw new Error(`${label} expected exactly one packed tarball, found ${entries.length}: ${entries.join(', ')}`);
|
|
}
|
|
return path.join(dir, entries[0]);
|
|
}
|
|
|
|
function shouldCopySdkPath(relativePath) {
|
|
if (!relativePath) return true;
|
|
const parts = relativePath.split(path.sep);
|
|
if (parts.includes('node_modules')) return false;
|
|
if (parts.includes('dist')) return false;
|
|
if (parts.includes('.turbo')) return false;
|
|
const base = parts.at(-1) ?? '';
|
|
if (base === '.tsbuildinfo') return false;
|
|
if (base.endsWith('.tgz')) return false;
|
|
return true;
|
|
}
|
|
|
|
async function createProofWorkspace(proofRoot) {
|
|
if (process.env.MATRIX_SDK_CLI_UAT_IN_PLACE === '1') {
|
|
return {
|
|
workspaceDir: sourceWorkspaceDir,
|
|
copied: false,
|
|
};
|
|
}
|
|
const copiedWorkspace = path.join(proofRoot, 'hivecast-sdk-workspace');
|
|
await cp(sourceWorkspaceDir, copiedWorkspace, {
|
|
recursive: true,
|
|
dereference: false,
|
|
filter: (src) => shouldCopySdkPath(path.relative(sourceWorkspaceDir, src)),
|
|
});
|
|
return {
|
|
workspaceDir: copiedWorkspace,
|
|
copied: true,
|
|
};
|
|
}
|
|
|
|
function waitForRunReady(child) {
|
|
return new Promise((resolveReady, reject) => {
|
|
let stdout = '';
|
|
let stderr = '';
|
|
let settled = false;
|
|
const timer = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
reject(new Error(`matrix run did not become ready\nstdout:\n${stdout}\nstderr:\n${stderr}`));
|
|
}, 10_000);
|
|
|
|
child.stdout.setEncoding('utf8');
|
|
child.stderr.setEncoding('utf8');
|
|
child.stdout.on('data', (chunk) => {
|
|
stdout += chunk;
|
|
if (!stdout.includes('"ok": true') || !stdout.includes('"command": "run"')) return;
|
|
if (!settled) {
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
resolveReady({ ok: true, command: 'run' });
|
|
}
|
|
});
|
|
child.stderr.on('data', (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.once('exit', (code) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
reject(new Error(`matrix run exited before ready (${code})\nstdout:\n${stdout}\nstderr:\n${stderr}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function stopChild(child) {
|
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
child.kill('SIGTERM');
|
|
await new Promise((resolveStop) => {
|
|
const timer = setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
resolveStop();
|
|
}, 3000);
|
|
child.once('exit', () => {
|
|
clearTimeout(timer);
|
|
resolveStop();
|
|
});
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const proofRoot = await mkdtemp(path.join(tmpdir(), 'hivecast-sdk-cli-uat.'));
|
|
const proofWorkspace = await createProofWorkspace(proofRoot);
|
|
workspaceDir = proofWorkspace.workspaceDir;
|
|
const tarballDir = path.join(proofRoot, 'tarballs');
|
|
const packageTarballDir = path.join(proofRoot, 'package-tarballs');
|
|
const toolingDir = path.join(proofRoot, 'tooling');
|
|
const authorDir = path.join(proofRoot, 'author-package');
|
|
const packageConsumerDir = path.join(proofRoot, 'package-manager-consumer');
|
|
const port = await getFreePort();
|
|
const packagePort = await getFreePort();
|
|
const liveKeyConfig = resolveLiveKeyConfig();
|
|
let liveKeyValidation = null;
|
|
let keep = process.env.MATRIX_SDK_KEEP_CLI_UAT === '1';
|
|
let runner;
|
|
let packageRunner;
|
|
const runtimeHostBin = envString('MATRIX_SDK_RUNTIME_HOST_BIN');
|
|
const managedHostProofEnabled = Boolean(runtimeHostBin);
|
|
let runtimeHostStarted = false;
|
|
let managedHostProof = null;
|
|
|
|
try {
|
|
await mkdir(tarballDir, { recursive: true });
|
|
await mkdir(packageTarballDir, { recursive: true });
|
|
await mkdir(toolingDir, { recursive: true });
|
|
await mkdir(authorDir, { recursive: true });
|
|
await mkdir(packageConsumerDir, { recursive: true });
|
|
|
|
if (proofWorkspace.copied) {
|
|
run('pnpm', ['install', '--ignore-scripts']);
|
|
}
|
|
run('pnpm', ['run', 'clean']);
|
|
run('pnpm', ['run', 'build']);
|
|
for (const packageName of packages) {
|
|
run('pnpm', ['--filter', packageName, 'pack', '--pack-destination', tarballDir]);
|
|
}
|
|
|
|
const tarballs = Object.fromEntries(
|
|
await Promise.all(packages.map(async (packageName) => [packageName, await findTarball(tarballDir, packageName)]))
|
|
);
|
|
const tarballDependencies = Object.fromEntries(
|
|
Object.entries(tarballs).map(([packageName, tarballPath]) => [packageName, `file:${tarballPath}`])
|
|
);
|
|
const authorOverrides = Object.fromEntries(
|
|
[
|
|
'@open-matrix/contracts',
|
|
'@open-matrix/federation',
|
|
'@open-matrix/omega-core',
|
|
'@open-matrix/core',
|
|
].map((packageName) => [packageName, tarballDependencies[packageName]])
|
|
);
|
|
|
|
await writeFile(path.join(toolingDir, 'package.json'), JSON.stringify({
|
|
name: 'hivecast-sdk-cli-uat-tooling',
|
|
version: '0.0.0',
|
|
private: true,
|
|
type: 'module',
|
|
dependencies: {
|
|
'@open-matrix/cli': tarballDependencies['@open-matrix/cli'],
|
|
},
|
|
pnpm: {
|
|
overrides: tarballDependencies,
|
|
},
|
|
}, null, 2));
|
|
|
|
run('pnpm', ['install', '--ignore-scripts'], { cwd: toolingDir });
|
|
run('pnpm', ['exec', 'matrix', 'init', authorDir, '--name', 'fresh-sdk-author'], { cwd: toolingDir });
|
|
|
|
const authorPackagePath = path.join(authorDir, 'package.json');
|
|
const authorPackage = JSON.parse(await readFile(authorPackagePath, 'utf8'));
|
|
authorPackage.dependencies['@open-matrix/core'] = tarballDependencies['@open-matrix/core'];
|
|
authorPackage.pnpm = { overrides: authorOverrides };
|
|
await writeFile(authorPackagePath, `${JSON.stringify(authorPackage, null, 2)}\n`);
|
|
|
|
run('pnpm', ['exec', 'matrix', 'add', 'actor', 'greeter', '--dir', authorDir, '--mount', 'demo.greeter'], { cwd: toolingDir });
|
|
if (liveKeyConfig) {
|
|
liveKeyValidation = await validateLiveKeyConfig(liveKeyConfig);
|
|
}
|
|
const configureKey = liveKeyConfig?.key ?? 'matrix_sdk_test_key_redacted';
|
|
const configureCloud = liveKeyConfig?.cloud ?? 'https://example.invalid';
|
|
const configureSpace = liveKeyConfig?.space ?? 'fresh-sdk-space';
|
|
|
|
const configureArgs = [
|
|
'exec', 'matrix', 'configure',
|
|
'--dir', authorDir,
|
|
'--key', configureKey,
|
|
'--cloud', configureCloud,
|
|
'--space', configureSpace,
|
|
];
|
|
if (runtimeHostBin) {
|
|
configureArgs.push(
|
|
'--runtime-host-bin', runtimeHostBin,
|
|
'--runtime-host-routing', 'workspace-source',
|
|
);
|
|
}
|
|
run('pnpm', configureArgs, { cwd: toolingDir });
|
|
run('pnpm', ['install', '--ignore-scripts'], { cwd: authorDir });
|
|
run('pnpm', ['run', 'build'], { cwd: authorDir });
|
|
|
|
runner = spawn('pnpm', [
|
|
'exec', 'matrix', 'run',
|
|
'--dir', authorDir,
|
|
'--port', String(port),
|
|
], {
|
|
cwd: toolingDir,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: process.env,
|
|
});
|
|
await waitForRunReady(runner);
|
|
|
|
const invoke = run('pnpm', [
|
|
'exec', 'matrix', 'invoke',
|
|
'demo.greeter',
|
|
'echo',
|
|
'{"message":"hello-from-sdk-cli"}',
|
|
'--dir', authorDir,
|
|
'--port', String(port),
|
|
], { cwd: toolingDir, stdio: 'pipe' });
|
|
const invoked = JSON.parse(invoke.stdout.trim());
|
|
if (invoked.ok !== true || invoked.result?.message !== 'hello-from-sdk-cli') {
|
|
throw new Error(`Unexpected matrix invoke output: ${invoke.stdout}`);
|
|
}
|
|
|
|
const [freshPackageJson, tsconfig, actorSource] = await Promise.all([
|
|
readFile(path.join(authorDir, 'package.json'), 'utf8'),
|
|
readFile(path.join(authorDir, 'tsconfig.json'), 'utf8'),
|
|
readFile(path.join(authorDir, 'src', 'GreeterActor.ts'), 'utf8'),
|
|
]);
|
|
const combinedAuthorFiles = `${freshPackageJson}\n${tsconfig}\n${actorSource}`;
|
|
const forbidden = [
|
|
'@matrix/mx-cli',
|
|
'@open-matrix/host-service',
|
|
'@open-matrix/host-control',
|
|
'@open-matrix/system-auth',
|
|
'@open-matrix/system-gateway-http',
|
|
'@open-matrix/system-',
|
|
'@open-matrix/cli',
|
|
'hivecast',
|
|
'projects/matrix-3',
|
|
'matrix-work-harness',
|
|
];
|
|
const foundForbidden = forbidden.find((needle) => combinedAuthorFiles.includes(needle));
|
|
if (foundForbidden) throw new Error(`Fresh author package contains forbidden dependency/path: ${foundForbidden}`);
|
|
if (!actorSource.includes("from '@open-matrix/core'")) {
|
|
throw new Error('Fresh actor source does not import @open-matrix/core');
|
|
}
|
|
if (!existsSync(path.join(authorDir, '.matrix', 'run.json'))) {
|
|
throw new Error('matrix run did not write package-local .matrix/run.json');
|
|
}
|
|
await stopChild(runner);
|
|
runner = undefined;
|
|
|
|
if (managedHostProofEnabled) {
|
|
if (!runtimeHostBin || !existsSync(runtimeHostBin)) {
|
|
throw new Error(`Managed host matrix command not found for optional managed proof: ${runtimeHostBin}`);
|
|
}
|
|
const up = run('pnpm', [
|
|
'exec', 'matrix', 'up',
|
|
'--dir', authorDir,
|
|
'--http-port', 'auto',
|
|
'--host-nats-port', 'auto',
|
|
'--host-nats-ws-port', 'auto',
|
|
'--runtime-id', 'SDK-UAT-GREETER',
|
|
'--json',
|
|
'--timeout', '45000',
|
|
], { cwd: toolingDir, stdio: 'pipe' });
|
|
runtimeHostStarted = true;
|
|
const upResult = parseJsonFromOutput(up.stdout, 'matrix up');
|
|
if (upResult.ok !== true || upResult.mode !== 'natural-launch-up') {
|
|
throw new Error(`Unexpected matrix up output: ${up.stdout}`);
|
|
}
|
|
const upService = upResult.services?.find?.((service) => service.runtimeId === 'SDK-UAT-GREETER');
|
|
if (!upService || upService.ok !== true) {
|
|
throw new Error(`matrix up did not start SDK-UAT-GREETER: ${up.stdout}`);
|
|
}
|
|
const managedInvokeOutput = runRuntimeHost(runtimeHostBin, [
|
|
'invoke',
|
|
'demo.greeter',
|
|
'getStatus',
|
|
'{}',
|
|
'--json',
|
|
'--timeout', '10000',
|
|
], { cwd: authorDir, stdio: 'pipe' });
|
|
const managedInvoke = parseJsonFromOutput(managedInvokeOutput.stdout, 'managed matrix invoke');
|
|
if (managedInvoke.ok !== true || managedInvoke.result?.mount !== 'demo.greeter') {
|
|
throw new Error(`Unexpected managed matrix invoke output: ${managedInvokeOutput.stdout}`);
|
|
}
|
|
managedHostProof = {
|
|
runtimeHostBin,
|
|
up: {
|
|
ok: upResult.ok,
|
|
mode: upResult.mode,
|
|
runtimeId: upService.runtimeId,
|
|
packageDir: upService.packageDir,
|
|
},
|
|
invoke: {
|
|
ok: managedInvoke.ok,
|
|
target: managedInvoke.target,
|
|
result: managedInvoke.result,
|
|
},
|
|
};
|
|
}
|
|
|
|
run('pnpm', ['pack', '--pack-destination', packageTarballDir], { cwd: authorDir, stdio: 'pipe' });
|
|
const authorPackageTarball = await findOnlyTarball(packageTarballDir, 'generated author package');
|
|
await writeFile(path.join(packageConsumerDir, 'package.json'), JSON.stringify({
|
|
name: 'hivecast-sdk-package-manager-consumer',
|
|
version: '0.0.0',
|
|
private: true,
|
|
type: 'module',
|
|
dependencies: {
|
|
'@open-matrix/cli': tarballDependencies['@open-matrix/cli'],
|
|
[authorPackage.name]: `file:${authorPackageTarball}`,
|
|
},
|
|
pnpm: {
|
|
overrides: tarballDependencies,
|
|
},
|
|
}, null, 2));
|
|
run('pnpm', ['install', '--ignore-scripts'], { cwd: packageConsumerDir });
|
|
const installedPackageDir = path.join(packageConsumerDir, 'node_modules', authorPackage.name);
|
|
const installedPackageJson = JSON.parse(await readFile(path.join(installedPackageDir, 'package.json'), 'utf8'));
|
|
if (installedPackageJson.private === true) {
|
|
throw new Error('Generated author package is still private after package-manager install');
|
|
}
|
|
const installedActorSource = await readFile(path.join(installedPackageDir, 'src', 'GreeterActor.ts'), 'utf8');
|
|
if (!installedActorSource.includes("from '@open-matrix/core'")) {
|
|
throw new Error('Package-manager installed source does not keep @open-matrix/core import');
|
|
}
|
|
packageRunner = spawn('pnpm', [
|
|
'exec', 'matrix', 'run',
|
|
installedPackageDir,
|
|
'--dir', packageConsumerDir,
|
|
'--port', String(packagePort),
|
|
], {
|
|
cwd: packageConsumerDir,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: process.env,
|
|
});
|
|
await waitForRunReady(packageRunner);
|
|
const packageInvoke = run('pnpm', [
|
|
'exec', 'matrix', 'invoke',
|
|
'demo.greeter',
|
|
'echo',
|
|
'{"message":"hello-from-package-manager"}',
|
|
'--dir', packageConsumerDir,
|
|
'--port', String(packagePort),
|
|
], { cwd: packageConsumerDir, stdio: 'pipe' });
|
|
const packageInvoked = JSON.parse(packageInvoke.stdout.trim());
|
|
if (packageInvoked.ok !== true || packageInvoked.result?.message !== 'hello-from-package-manager') {
|
|
throw new Error(`Unexpected package-manager matrix invoke output: ${packageInvoke.stdout}`);
|
|
}
|
|
await stopChild(packageRunner);
|
|
packageRunner = undefined;
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
proof: 'sdk-cli-uat',
|
|
sourceWorkspaceDir,
|
|
sdkWorkspaceDir: workspaceDir,
|
|
destructiveBuildIsolated: proofWorkspace.copied,
|
|
toolingDir,
|
|
authorDir,
|
|
cliInstalledSeparately: true,
|
|
authorPackageDependsOnCli: false,
|
|
legacyMonorepoPathReferences: false,
|
|
actorImport: '@open-matrix/core',
|
|
configuredKeyStored: existsSync(path.join(authorDir, '.matrix', 'credentials', 'matrix-api-key.json')),
|
|
validKeyConfigSupplied: Boolean(liveKeyConfig),
|
|
liveKeyValidation,
|
|
runState: '.matrix/run.json',
|
|
invoke: {
|
|
ok: invoked.ok,
|
|
result: invoked.result,
|
|
},
|
|
managedHostProof: managedHostProof ?? {
|
|
skipped: true,
|
|
reason: 'MATRIX_SDK_RUNTIME_HOST_BIN not supplied',
|
|
},
|
|
packageManagerInstall: {
|
|
tarball: path.basename(authorPackageTarball),
|
|
consumerDir: packageConsumerDir,
|
|
installedPackage: authorPackage.name,
|
|
sourceImportUnchanged: true,
|
|
packagePrivate: installedPackageJson.private === true,
|
|
invoke: {
|
|
ok: packageInvoked.ok,
|
|
result: packageInvoked.result,
|
|
},
|
|
},
|
|
}, null, 2));
|
|
} catch (error) {
|
|
keep = true;
|
|
throw error;
|
|
} finally {
|
|
if (runner) await stopChild(runner);
|
|
if (packageRunner) await stopChild(packageRunner);
|
|
if (runtimeHostStarted && runtimeHostBin) {
|
|
try {
|
|
runRuntimeHost(runtimeHostBin, ['stop', '--timeout', '15000'], { cwd: authorDir, stdio: 'pipe' });
|
|
} catch (error) {
|
|
console.error(`[prove-sdk-cli-uat] failed to stop managed host: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
if (!keep) {
|
|
await rm(proofRoot, { recursive: true, force: true });
|
|
} else {
|
|
console.error(`[prove-sdk-cli-uat] kept proof directory: ${proofRoot}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.stack : error);
|
|
process.exit(1);
|
|
});
|