#!/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/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, 'matrix-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(), 'matrix-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', '@open-matrix/browser-host', '@open-matrix/browser-kit', '@open-matrix/sdk', ].map((packageName) => [packageName, tarballDependencies[packageName]]) ); await writeFile(path.join(toolingDir, 'package.json'), JSON.stringify({ name: 'matrix-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/sdk'] = tarballDependencies['@open-matrix/sdk']; 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/sdk'")) { throw new Error('Fresh actor source does not import @open-matrix/sdk'); } 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: 'matrix-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/sdk'")) { throw new Error('Package-manager installed source does not keep @open-matrix/sdk 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, monorepoPathReferences: false, actorImport: '@open-matrix/sdk', 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); });