#!/usr/bin/env node import { spawn } from 'node:child_process'; import { constants } from 'node:fs'; import { access as fsAccess, readdir as fsReaddir, stat as fsStat } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; function ktxRootDir() { return resolve(dirname(fileURLToPath(import.meta.url)), '..'); } function cliBinPath(rootDir) { return resolve(rootDir, 'packages', 'cli', 'dist', 'bin.js'); } async function fileExists(path, access) { try { await access(path, constants.R_OK); return true; } catch { return false; } } async function packageBuildInputPaths(rootDir, readdir) { const paths = [resolve(rootDir, 'package.json'), resolve(rootDir, 'tsconfig.base.json')]; let packageEntries = []; try { packageEntries = await readdir(resolve(rootDir, 'packages'), { withFileTypes: true }); } catch { return paths; } for (const entry of packageEntries) { if (!entry.isDirectory()) { continue; } const packageDir = resolve(rootDir, 'packages', entry.name); paths.push(resolve(packageDir, 'package.json'), resolve(packageDir, 'tsconfig.json'), resolve(packageDir, 'src')); } return paths; } async function newestMtimeMs(path, fs) { let stats; try { stats = await fs.stat(path); } catch { return 0; } if (!stats.isDirectory()) { return stats.mtimeMs; } let newest = stats.mtimeMs; let entries = []; try { entries = await fs.readdir(path, { withFileTypes: true }); } catch { return newest; } for (const entry of entries) { newest = Math.max(newest, await newestMtimeMs(resolve(path, entry.name), fs)); } return newest; } async function isBuildStale(rootDir, binPath, fs) { let binStats; try { binStats = await fs.stat(binPath); } catch { return true; } const inputPaths = await packageBuildInputPaths(rootDir, fs.readdir); for (const inputPath of inputPaths) { if ((await newestMtimeMs(inputPath, fs)) > binStats.mtimeMs) { return true; } } return false; } function isShellCompletionRequest(argv) { return argv[0] === '__complete' || (argv[0] === 'dev' && argv[1] === '__complete'); } async function runBuffered(execFile, stdout, stderr, command, args, options) { try { const result = await execFile(command, args, { cwd: options.cwd, env: options.env, maxBuffer: 1024 * 1024 * 16 }); if (result.stdout) { stdout.write(result.stdout); } if (result.stderr) { stderr.write(result.stderr); } return 0; } catch (error) { if (typeof error?.stdout === 'string' && error.stdout.length > 0) { stdout.write(error.stdout); } if (typeof error?.stderr === 'string' && error.stderr.length > 0) { stderr.write(error.stderr); } return typeof error?.code === 'number' ? error.code : 1; } } function runInherited(command, args, options) { return new Promise((resolveExitCode) => { const child = spawn(command, args, { cwd: options.cwd, stdio: 'inherit', env: options.env ?? process.env, }); child.on('error', (error) => { process.stderr.write(`${error.message}\n`); resolveExitCode(1); }); child.on('exit', (code, signal) => { if (code !== null) { resolveExitCode(code); return; } process.stderr.write(`Command terminated by signal ${signal ?? 'unknown'}\n`); resolveExitCode(1); }); }); } export async function runWorkspaceKtx(argv, options = {}) { const cliArgv = argv[0] === '--' ? argv.slice(1) : argv; const rootDir = options.rootDir ?? ktxRootDir(); const stdout = options.stdout ?? process.stdout; const stderr = options.stderr ?? process.stderr; const access = options.access ?? fsAccess; const fs = { stat: options.stat ?? fsStat, readdir: options.readdir ?? fsReaddir, }; const binPath = cliBinPath(rootDir); const runCommand = options.runCommand ?? (options.execFile ? (command, args, commandOptions) => runBuffered(options.execFile, stdout, stderr, command, args, commandOptions) : (command, args, commandOptions) => runInherited(command, args, commandOptions)); const commandEnv = options.env; const binExists = await fileExists(binPath, access); const skipStaleBuildCheck = binExists && isShellCompletionRequest(cliArgv); const needsBuild = !binExists || (!skipStaleBuildCheck && (await isBuildStale(rootDir, binPath, fs))); if (needsBuild) { stderr.write( binExists ? 'KTX CLI build output is stale. Rebuilding it now with `pnpm run build`...\n' : 'KTX CLI build output is missing. Building it now with `pnpm run build`...\n', ); const buildExitCode = await runCommand('pnpm', ['run', 'build'], { cwd: rootDir, env: commandEnv }); if (buildExitCode !== 0) { stderr.write( '\nKTX CLI build failed. Run `pnpm run setup:dev` from the KTX directory, then retry this command.\n', ); return buildExitCode; } } return await runCommand(process.execPath, [binPath, ...cliArgv], { cwd: rootDir, env: commandEnv }); } if (import.meta.url === pathToFileURL(process.argv[1]).href) { process.exitCode = await runWorkspaceKtx(process.argv.slice(2)); }