From d941ada33d9d2e5ada881fd9e0dae8830e07f3c6 Mon Sep 17 00:00:00 2001 From: shuv Date: Tue, 28 Jul 2026 00:00:50 -0700 Subject: [PATCH] fix(cli): remove global install postinstall --- .../script/{postinstall.mjs => launcher.mjs} | 128 ++++++------------ packages/cli/script/publish.ts | 16 +-- packages/cli/test/launcher.test.ts | 48 +++++++ 3 files changed, 90 insertions(+), 102 deletions(-) rename packages/cli/script/{postinstall.mjs => launcher.mjs} (51%) create mode 100644 packages/cli/test/launcher.test.ts diff --git a/packages/cli/script/postinstall.mjs b/packages/cli/script/launcher.mjs similarity index 51% rename from packages/cli/script/postinstall.mjs rename to packages/cli/script/launcher.mjs index 8031b67ba74f..916ee25b9a04 100644 --- a/packages/cli/script/postinstall.mjs +++ b/packages/cli/script/launcher.mjs @@ -9,17 +9,16 @@ import { fileURLToPath } from "node:url" const directory = path.dirname(fileURLToPath(import.meta.url)) const require = createRequire(import.meta.url) -const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8")) +const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "../package.json"), "utf8")) const command = Object.keys(packageJson.bin ?? {})[0] -if (!command) throw new Error("Shuvcode package does not declare a binary") +if (!command) fail("Shuvcode package does not declare a binary") const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] ?? os.platform() const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] ?? os.arch() const sourceBinary = platform === "windows" ? `${command}.exe` : command -const targetBinary = path.resolve(directory, packageJson.bin[command]) const dependencies = packageJson.optionalDependencies ?? {} const base = Object.keys(dependencies).find((name) => name.endsWith(`-${platform}-${arch}`)) -if (!base) throw new Error(`Shuvcode does not provide a binary for ${platform}-${arch}`) +if (!base) fail(`Shuvcode does not provide a binary for ${platform}-${arch}`) function supportsAvx2() { if (arch !== "x64") return false @@ -31,33 +30,25 @@ function supportsAvx2() { } } if (platform === "darwin") { - try { - const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { - encoding: "utf8", - timeout: 1500, - }) - return result.status === 0 && (result.stdout || "").trim() === "1" - } catch { - return false - } + const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { + encoding: "utf8", + timeout: 1500, + }) + return result.status === 0 && (result.stdout || "").trim() === "1" } if (platform === "windows") { const script = '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)' for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) { - try { - const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", script], { - encoding: "utf8", - timeout: 3000, - windowsHide: true, - }) - if (result.status !== 0) continue - const output = (result.stdout || "").trim().toLowerCase() - if (output === "true" || output === "1") return true - if (output === "false" || output === "0") return false - } catch { - continue - } + const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", script], { + encoding: "utf8", + timeout: 3000, + windowsHide: true, + }) + if (result.status !== 0) continue + const output = (result.stdout || "").trim().toLowerCase() + if (output === "true" || output === "1") return true + if (output === "false" || output === "0") return false } } return false @@ -97,75 +88,34 @@ function packageNames() { return names.filter((name) => dependencies[name]) } -function copyBinary(source) { - if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`) - fs.mkdirSync(path.dirname(targetBinary), { recursive: true }) - if (fs.existsSync(targetBinary)) fs.unlinkSync(targetBinary) - try { - fs.linkSync(source, targetBinary) - } catch { - fs.copyFileSync(source, targetBinary) - } - fs.chmodSync(targetBinary, 0o755) -} - function resolveBinary(name) { const packagePath = require.resolve(`${name}/package.json`) - return path.join(path.dirname(packagePath), "bin", sourceBinary) + const binary = path.join(path.dirname(packagePath), "bin", sourceBinary) + if (!fs.existsSync(binary)) throw new Error(`Binary not found at ${binary}`) + return binary } -function installPackage(name) { - const temp = fs.mkdtempSync(path.join(os.tmpdir(), "shuvcode-install-")) - try { - const result = childProcess.spawnSync( - "npm", - [ - "install", - "--ignore-scripts", - "--no-save", - "--loglevel=error", - "--prefix", - temp, - `${name}@${dependencies[name]}`, - ], - { stdio: "inherit", windowsHide: true }, - ) - if (result.status !== 0) return false - copyBinary(path.join(temp, "node_modules", name, "bin", sourceBinary)) - return true - } finally { - fs.rmSync(temp, { recursive: true, force: true }) - } -} - -function verifyBinary() { - return ( - childProcess.spawnSync(targetBinary, ["--version"], { - stdio: "ignore", - windowsHide: true, - }).status === 0 - ) +function fail(message) { + console.error(message) + process.exit(1) } -function main() { - const names = packageNames() - for (const name of names) { - try { - copyBinary(resolveBinary(name)) - if (verifyBinary()) return - } catch { - if (installPackage(name) && verifyBinary()) return - } +const names = packageNames() +const binary = names.reduce((result, name) => { + if (result) return result + try { + return resolveBinary(name) + } catch { + return undefined } +}, undefined) - throw new Error( - `Failed to install Shuvcode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`, - ) -} +if (!binary) fail(`Failed to find Shuvcode binary package. Reinstall ${packageJson.name}.`) -try { - main() -} catch (error) { - console.error(error instanceof Error ? error.message : String(error)) - process.exit(1) -} +const result = childProcess.spawnSync(binary, process.argv.slice(2), { + stdio: "inherit", + windowsHide: true, +}) +if (result.error) fail(result.error.message) +if (result.signal) process.kill(process.pid, result.signal) +process.exit(result.status ?? 1) diff --git a/packages/cli/script/publish.ts b/packages/cli/script/publish.ts index d398f0a770e6..09627adcec03 100755 --- a/packages/cli/script/publish.ts +++ b/packages/cli/script/publish.ts @@ -28,24 +28,14 @@ async function publish(dir: string, name: string, version: string) { async function prepareDistribution(input: ForkDistribution) { console.log(input.name, "binaries", input.binaries) + await $`rm -rf ${input.root}/${input.name}` await $`mkdir -p ${input.root}/${input.name}/bin` - await $`cp ./script/postinstall.mjs ${input.root}/${input.name}/postinstall.mjs` - await Bun.file(`${input.root}/${input.name}/bin/${input.binary}.exe`).write( - [ - `echo "Error: ${input.name}'s postinstall script was not run." >&2`, - 'echo "" >&2', - 'echo "This occurs when installation scripts are disabled." >&2', - 'echo "Run the package postinstall script or reinstall with scripts enabled." >&2', - "exit 1", - "", - ].join("\n"), - ) + await $`cp ./script/launcher.mjs ${input.root}/${input.name}/bin/launcher.mjs` await Bun.file(`${input.root}/${input.name}/package.json`).write( JSON.stringify( { name: input.name, - bin: { [input.binary]: `./bin/${input.binary}.exe` }, - scripts: { postinstall: "node ./postinstall.mjs" }, + bin: { [input.binary]: "./bin/launcher.mjs" }, version: input.version, license: pkg.license, repository: { type: "git", url: "git+https://github.com/Latitudes-Dev/shuvcode.git" }, diff --git a/packages/cli/test/launcher.test.ts b/packages/cli/test/launcher.test.ts new file mode 100644 index 000000000000..28056f4e5011 --- /dev/null +++ b/packages/cli/test/launcher.test.ts @@ -0,0 +1,48 @@ +import { afterEach, expect, test } from "bun:test" +import { chmod, copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +const directories: string[] = [] + +afterEach(async () => { + await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))) +}) + +test.skipIf(process.platform === "win32")("launches the installed platform binary without lifecycle scripts", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "shuvcode-launcher-")) + directories.push(root) + const platform = process.platform === "darwin" || process.platform === "linux" ? process.platform : undefined + if (!platform) throw new Error(`Unsupported test platform: ${process.platform}`) + const arch = process.arch === "x64" || process.arch === "arm64" ? process.arch : undefined + if (!arch) throw new Error(`Unsupported test architecture: ${process.arch}`) + const dependency = `shuvcode-${platform}-${arch}` + const packageRoot = path.join(root, "node_modules", dependency) + + await mkdir(path.join(root, "bin"), { recursive: true }) + await mkdir(path.join(packageRoot, "bin"), { recursive: true }) + await copyFile(path.join(import.meta.dir, "../script/launcher.mjs"), path.join(root, "bin", "launcher.mjs")) + await writeFile( + path.join(root, "package.json"), + JSON.stringify({ + name: "shuvcode", + bin: { shuvcode: "./bin/launcher.mjs" }, + optionalDependencies: { [dependency]: "2.0.0-alpha-2" }, + }), + ) + await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: dependency, version: "2.0.0-alpha-2" })) + await writeFile( + path.join(packageRoot, "bin", "shuvcode"), + '#!/usr/bin/env node\nconsole.log(JSON.stringify(process.argv.slice(2)))\nprocess.exit(23)\n', + ) + await chmod(path.join(packageRoot, "bin", "shuvcode"), 0o755) + + const child = Bun.spawn(["node", path.join(root, "bin", "launcher.mjs"), "hello", "two words"], { + stdout: "pipe", + stderr: "pipe", + }) + + expect(await child.exited).toBe(23) + expect(await new Response(child.stdout).text()).toBe('["hello","two words"]\n') + expect(await new Response(child.stderr).text()).toBe("") +})