#!/usr/bin/env node import { execFile } from 'node:child_process'; import { constants } from 'node:fs'; import { access as fsAccess, chmod as fsChmod, writeFile as fsWriteFile } from 'node:fs/promises'; import { delimiter, join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; import { ensureCliBinExecutable, ktxRootDir } from './prepare-cli-bin.mjs'; const execFileAsync = promisify(execFile); function hasFlag(flag) { return process.argv.includes(flag); } function optionValue(flag, fallback) { const index = process.argv.indexOf(flag); if (index === -1) { return fallback; } const value = process.argv[index + 1]; if (!value || value.startsWith('-')) { throw new Error(`${flag} requires a value`); } return value; } function commandEnv(extraPath) { if (!extraPath) { return process.env; } return { ...process.env, PATH: `${extraPath}${delimiter}${process.env.PATH ?? ''}`, }; } async function execText(command, args, options = {}) { const result = await execFileAsync(command, args, { cwd: options.cwd, env: options.env, maxBuffer: 1024 * 1024, }); return `${result.stdout}${result.stderr}`.trim(); } async function optionalText(command, args, options = {}) { try { return await execText(command, args, options); } catch { return ''; } } async function findPnpmGlobalBin() { const output = await optionalText('pnpm', ['bin', '--global']); return output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? ''; } function shellDoubleQuote(value) { return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('$', '\\$').replaceAll('`', '\\`')}"`; } function assertBinaryName(binaryName) { if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(binaryName)) { throw new Error(`Invalid binary name: ${binaryName}`); } } async function writePinnedPosixLauncher(globalBin, binPath, binaryName, writeFile, chmod) { const launcherPath = join(globalBin, binaryName); const script = [ '#!/bin/sh', '# Generated by `pnpm run link:dev` in the KTX workspace.', '# Keep this launcher pinned to the Node binary that built native dependencies.', `exec ${shellDoubleQuote(process.execPath)} ${shellDoubleQuote(binPath)} "$@"`, '', ].join('\n'); await writeFile(launcherPath, script, 'utf-8'); await chmod(launcherPath, 0o755); return launcherPath; } async function writePinnedWindowsLauncher(globalBin, binPath, binaryName, writeFile) { const launcherPath = join(globalBin, `${binaryName}.cmd`); const script = [ '@echo off', 'REM Generated by `pnpm run link:dev` in the KTX workspace.', `"${process.execPath}" "${binPath}" %*`, '', ].join('\r\n'); await writeFile(launcherPath, script, 'utf-8'); return launcherPath; } async function writePinnedLauncher(globalBin, binPath, binaryName, deps) { if (!globalBin) { throw new Error('Could not find pnpm global bin directory. Run `pnpm setup`, restart your shell, then retry.'); } if (process.platform === 'win32') { return writePinnedWindowsLauncher(globalBin, binPath, binaryName, deps.writeFile); } return writePinnedPosixLauncher(globalBin, binPath, binaryName, deps.writeFile, deps.chmod); } async function verifyBinaryOnPath(binaryName, globalBin, execTextFn) { try { const output = await execTextFn(binaryName, ['--version']); return { ok: true, output }; } catch (error) { if (!globalBin) { return { ok: false, output: '', error }; } try { const output = await execTextFn(binaryName, ['--version'], { env: commandEnv(globalBin) }); return { ok: false, output, globalBin, error }; } catch { return { ok: false, output: '', error }; } } } async function assertBuiltCli(rootDir, access, binPathOverride) { const binPath = binPathOverride ?? (await ensureCliBinExecutable(rootDir)); await access(binPath, constants.X_OK); return binPath; } export async function linkDevCli(options = {}) { const rootDir = options.rootDir ?? ktxRootDir(); const binaryName = options.binaryName ?? 'ktx-dev'; const access = options.access ?? fsAccess; const chmod = options.chmod ?? fsChmod; const writeFile = options.writeFile ?? fsWriteFile; const execTextFn = options.execText ?? execText; assertBinaryName(binaryName); const binPath = await assertBuiltCli(rootDir, access, options.binPath); const globalBin = options.globalBin ?? (await findPnpmGlobalBin()); if (options.checkOnly) { return { binaryName, binPath, linked: false, verification: await verifyBinaryOnPath(binaryName, globalBin, execTextFn), }; } const launcherPath = await writePinnedLauncher(globalBin, binPath, binaryName, { writeFile, chmod }); const verification = await verifyBinaryOnPath(binaryName, globalBin, execTextFn); if (!verification.ok) { const pathHint = verification.globalBin ? `\nAdd pnpm's global bin directory to PATH, then retry:\n\n export PATH="${verification.globalBin}:$PATH"\n\n` : '\nRun `pnpm setup`, restart your shell, then rerun `pnpm run link:dev`.\n\n'; throw new Error(`${binaryName} was linked at ${launcherPath}, but it is not available on PATH.${pathHint}`); } return { binaryName, binPath, launcherPath, linked: true, verification, }; } if (import.meta.url === pathToFileURL(process.argv[1]).href) { try { const result = await linkDevCli({ checkOnly: hasFlag('--check-only'), binaryName: optionValue('--name', 'ktx-dev'), }); process.stdout.write(`KTX CLI bin: ${result.binPath}\n`); if (result.linked) { process.stdout.write(`Linked binary: ${result.binaryName}\n`); process.stdout.write(`Verified: ${result.verification.output}\n`); process.stdout.write(`Pinned Node: ${process.execPath} ${process.version} ABI ${process.versions.modules}\n`); process.stdout.write(`You can now run \`${result.binaryName} --help\` from any directory.\n`); } else if (result.verification.ok) { process.stdout.write(`Already available: ${result.verification.output}\n`); } else { process.stdout.write(`${result.binaryName} is not linked on PATH yet.\n`); } } catch (error) { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; } }