From c08471243823858aa9c23993a1a9a4f6ed44f364 Mon Sep 17 00:00:00 2001 From: Emmanuel Isenah Date: Tue, 12 May 2026 09:27:29 +0100 Subject: [PATCH 1/8] update --- .github/workflows/release.yml | 2 +- package.json | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55d0939..c7ab365 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,7 +88,7 @@ jobs: uses: ./.github/actions/setup - name: Build binary - run: pnpm build:sea + run: pnpm build - name: Archive binary run: | diff --git a/package.json b/package.json index 62733b1..0328fed 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,12 @@ "url": "https://github.com/Armadillidiid/cmdly/issues" }, "author": "Emmanuel Isenah (https://emmanuelisenah.com)", + "bin": { + "cmdly": "./dist/index.cjs" + }, + "files": [ + "dist" + ], "repository": { "type": "git", "url": "https://github.com/Armadillidiid/cmdly" @@ -33,15 +39,14 @@ }, "scripts": { "dev": "node --import=tsx src/bin.ts --watch", - "build": "pnpm build:sea", - "build:sea": "node scripts/build-sea.mjs", - "verify:sea": "node scripts/verify-sea.mjs", + "build": "node scripts/build-sea.mjs", + "build:verify": "node scripts/verify-sea.mjs", "prepare": "effect-language-service patch", - "clean": "rm -rf dist .sea", + "clean": "rm -rf dist", "typecheck": "tsc -b tsconfig.json", "test": "vitest run", "changeset-version": "changeset version", - "changeset-publish": "pnpm build && pnpm vitest && changeset tag && git push --tags", + "changeset-publish": "pnpm build && pnpm vitest && changeset publish && changeset tag && git push --tags", "format": "biome format --write", "lint": "biome lint --write", "check": "biome check --write --unsafe" From af7cf20576f3a92e703c1cc4aa0a01287fb80bc0 Mon Sep 17 00:00:00 2001 From: Emmanuel Isenah Date: Tue, 12 May 2026 11:33:14 +0100 Subject: [PATCH 2/8] update --- .github/workflows/release.yml | 7 +- package.json | 7 +- scripts/launcher.mjs | 68 +++++++++++ scripts/postinstall.mjs | 215 ++++++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 scripts/launcher.mjs create mode 100644 scripts/postinstall.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7ab365..0290483 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,9 +93,10 @@ jobs: - name: Archive binary run: | mkdir -p release - cp "dist/${{ matrix.target }}/cmdly" "release/cmdly-${{ matrix.target }}" - tar -C release -czf "release/cmdly-${{ matrix.target }}.tar.gz" "cmdly-${{ matrix.target }}" - ${{ matrix.checksum_cmd }} "release/cmdly-${{ matrix.target }}.tar.gz" > "release/cmdly-${{ matrix.target }}.tar.gz.sha256" + ASSET_NAME="cmdly-v${{ needs.version.outputs.release_version }}-${{ matrix.target }}" + cp "dist/${{ matrix.target }}/cmdly" "release/${ASSET_NAME}" + tar -C release -czf "release/${ASSET_NAME}.tar.gz" "${ASSET_NAME}" + ${{ matrix.checksum_cmd }} "release/${ASSET_NAME}.tar.gz" > "release/${ASSET_NAME}.tar.gz.sha256" - name: Upload artifacts uses: actions/upload-artifact@v7 diff --git a/package.json b/package.json index 0328fed..43b8f91 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,12 @@ }, "author": "Emmanuel Isenah (https://emmanuelisenah.com)", "bin": { - "cmdly": "./dist/index.cjs" + "cmdly": "./scripts/launcher.mjs" }, "files": [ - "dist" + "dist", + "scripts/launcher.mjs", + "scripts/postinstall.mjs" ], "repository": { "type": "git", @@ -41,6 +43,7 @@ "dev": "node --import=tsx src/bin.ts --watch", "build": "node scripts/build-sea.mjs", "build:verify": "node scripts/verify-sea.mjs", + "postinstall": "node scripts/postinstall.mjs", "prepare": "effect-language-service patch", "clean": "rm -rf dist", "typecheck": "tsc -b tsconfig.json", diff --git a/scripts/launcher.mjs b/scripts/launcher.mjs new file mode 100644 index 0000000..8f52f5f --- /dev/null +++ b/scripts/launcher.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +import { existsSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const packageRoot = join(__dirname, ".."); +const nativeBinary = join(packageRoot, "dist", "native", "cmdly"); +const supportedTargets = ["linux-x64", "linux-arm64", "darwin-arm64"]; + +const detectTarget = () => { + if (process.platform === "linux" && process.arch === "x64") { + return "linux-x64"; + } + if (process.platform === "linux" && process.arch === "arm64") { + return "linux-arm64"; + } + if (process.platform === "darwin" && process.arch === "arm64") { + return "darwin-arm64"; + } + return null; +}; + +const fail = (message) => { + console.error(message); + process.exit(1); +}; + +const target = detectTarget(); + +if (!target) { + fail( + [ + `cmdly: unsupported platform ${process.platform}-${process.arch}.`, + `Supported targets: ${supportedTargets.join(", ")}.`, + "Download manually: https://github.com/Armadillidiid/cmdly/releases/latest", + ].join("\n"), + ); +} + +if (!existsSync(nativeBinary)) { + fail( + [ + `cmdly: native binary missing for ${target}.`, + "Likely causes: install scripts disabled (--ignore-scripts), offline install, or postinstall download failed.", + "Try: npm rebuild cmdly", + "Or reinstall without --ignore-scripts.", + "Releases: https://github.com/Armadillidiid/cmdly/releases/latest", + ].join("\n"), + ); +} + +const result = spawnSync(nativeBinary, process.argv.slice(2), { + stdio: "inherit", + env: process.env, +}); + +if (result.error) { + fail(`cmdly: failed to start native binary: ${result.error.message}`); +} + +if (typeof result.status === "number") { + process.exit(result.status); +} + +process.exit(1); diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs new file mode 100644 index 0000000..7e3da69 --- /dev/null +++ b/scripts/postinstall.mjs @@ -0,0 +1,215 @@ +#!/usr/bin/env node + +import { + chmodSync, + copyFileSync, + createReadStream, + createWriteStream, + existsSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { createHash } from "node:crypto"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { tmpdir } from "node:os"; +import { spawnSync } from "node:child_process"; +import { pipeline } from "node:stream/promises"; +import { Readable } from "node:stream"; +import pkg from "../package.json" with { type: "json" }; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const packageRoot = join(__dirname, ".."); +const distDir = join(packageRoot, "dist"); +const nativeDir = join(distDir, "native"); +const nativeBinaryPath = join(nativeDir, "cmdly"); +const metadataPath = join(nativeDir, "metadata.json"); +const statePath = join(nativeDir, "install-state.json"); +const supportedTargets = ["linux-x64", "linux-arm64", "darwin-arm64"]; +const releaseBaseUrl = + process.env.CMDLY_DOWNLOAD_BASE_URL ?? + `https://github.com/Armadillidiid/cmdly/releases/download/v${pkg.version}`; + +const detectTarget = () => { + if (process.platform === "linux" && process.arch === "x64") { + return "linux-x64"; + } + if (process.platform === "linux" && process.arch === "arm64") { + return "linux-arm64"; + } + if (process.platform === "darwin" && process.arch === "arm64") { + return "darwin-arm64"; + } + return null; +}; + +const loadJson = (path) => { + try { + return JSON.parse(readFileSync(path, "utf8")); + } catch { + return null; + } +}; + +const saveState = (state) => { + mkdirSync(nativeDir, { recursive: true }); + writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); +}; + +const clearState = () => { + if (existsSync(statePath)) { + unlinkSync(statePath); + } +}; + +const downloadToFile = async (url, filePath) => { + const response = await fetch(url, { + headers: { + "user-agent": `cmdly-installer/${pkg.version}`, + accept: "application/octet-stream,text/plain", + }, + }); + + if (!response.ok || !response.body) { + throw new Error(`download failed (${response.status}) for ${url}`); + } + + await pipeline(Readable.fromWeb(response.body), createWriteStream(filePath)); +}; + +const checksumForFile = (filePath) => + new Promise((resolve, reject) => { + const hash = createHash("sha256"); + const stream = createReadStream(filePath); + stream.on("error", reject); + stream.on("data", (chunk) => hash.update(chunk)); + stream.on("end", () => resolve(hash.digest("hex"))); + }); + +const parseSha256File = (content) => { + const line = content.trim().split(/\r?\n/)[0] ?? ""; + const [sum] = line.trim().split(/\s+/); + if (!sum) { + throw new Error("invalid checksum file"); + } + return sum.toLowerCase(); +}; + +const installBinary = (archivePath, binaryName) => { + const tempRoot = join(tmpdir(), `cmdly-install-${Date.now()}`); + mkdirSync(tempRoot, { recursive: true }); + + const extractResult = spawnSync("tar", ["-xzf", archivePath, "-C", tempRoot], { + stdio: "pipe", + }); + + if (extractResult.status !== 0) { + rmSync(tempRoot, { recursive: true, force: true }); + throw new Error( + `failed to extract archive: ${extractResult.stderr?.toString("utf8") ?? "unknown error"}`, + ); + } + + const extractedBinary = join(tempRoot, binaryName); + if (!existsSync(extractedBinary)) { + rmSync(tempRoot, { recursive: true, force: true }); + throw new Error(`archive did not contain expected binary: ${binaryName}`); + } + + mkdirSync(nativeDir, { recursive: true }); + const tempTarget = join(nativeDir, ".cmdly.tmp"); + copyFileSync(extractedBinary, tempTarget); + chmodSync(tempTarget, 0o755); + renameSync(tempTarget, nativeBinaryPath); + + rmSync(tempRoot, { recursive: true, force: true }); +}; + +const main = async () => { + const target = detectTarget(); + + if (!target) { + saveState({ + status: "unsupported_target", + platform: process.platform, + arch: process.arch, + supportedTargets, + installedAt: new Date().toISOString(), + }); + console.warn( + `[postinstall] skipping native install for unsupported target ${process.platform}-${process.arch}`, + ); + return; + } + + const cached = loadJson(metadataPath); + if ( + cached?.version === pkg.version && + cached?.target === target && + existsSync(nativeBinaryPath) + ) { + clearState(); + return; + } + + const assetName = `cmdly-v${pkg.version}-${target}.tar.gz`; + const checksumName = `${assetName}.sha256`; + const archiveUrl = `${releaseBaseUrl}/${assetName}`; + const checksumUrl = `${releaseBaseUrl}/${checksumName}`; + + const tempRoot = join(tmpdir(), `cmdly-download-${Date.now()}`); + mkdirSync(tempRoot, { recursive: true }); + const archivePath = join(tempRoot, assetName); + const checksumPath = join(tempRoot, checksumName); + + try { + await downloadToFile(archiveUrl, archivePath); + await downloadToFile(checksumUrl, checksumPath); + + const expected = parseSha256File(readFileSync(checksumPath, "utf8")); + const actual = await checksumForFile(archivePath); + if (expected !== actual) { + throw new Error(`checksum mismatch for ${assetName}`); + } + + const archiveBinaryName = `cmdly-v${pkg.version}-${target}`; + installBinary(archivePath, archiveBinaryName); + + writeFileSync( + metadataPath, + `${JSON.stringify( + { + version: pkg.version, + target, + assetName, + sha256: actual, + installedAt: new Date().toISOString(), + }, + null, + 2, + )}\n`, + "utf8", + ); + + clearState(); + console.log(`[postinstall] installed native binary for ${target}`); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } +}; + +main().catch((error) => { + saveState({ + status: "install_failed", + message: error instanceof Error ? error.message : String(error), + installedAt: new Date().toISOString(), + }); + console.warn( + `[postinstall] native install failed: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(0); +}); From 1f2e8c2df2c2ba2bb0573cc06932b1cee0a72a3d Mon Sep 17 00:00:00 2001 From: Emmanuel Isenah Date: Tue, 12 May 2026 12:11:50 +0100 Subject: [PATCH 3/8] update --- .github/workflows/release.yml | 4 +- package.json | 7 ++-- scripts/launcher.mjs | 68 ------------------------------- scripts/postinstall.mjs | 75 ++++------------------------------- scripts/stage-default-bin.mjs | 18 +++++++++ 5 files changed, 32 insertions(+), 140 deletions(-) delete mode 100644 scripts/launcher.mjs create mode 100644 scripts/stage-default-bin.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0290483..99f51cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,8 +103,8 @@ jobs: with: name: cmdly-${{ matrix.target }} path: | - release/cmdly-${{ matrix.target }}.tar.gz - release/cmdly-${{ matrix.target }}.tar.gz.sha256 + release/cmdly-v${{ needs.version.outputs.release_version }}-${{ matrix.target }}.tar.gz + release/cmdly-v${{ needs.version.outputs.release_version }}-${{ matrix.target }}.tar.gz.sha256 release: name: Release assets diff --git a/package.json b/package.json index 43b8f91..4cece64 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,10 @@ }, "author": "Emmanuel Isenah (https://emmanuelisenah.com)", "bin": { - "cmdly": "./scripts/launcher.mjs" + "cmdly": "./bin/cmdly" }, "files": [ - "dist", - "scripts/launcher.mjs", + "bin/cmdly", "scripts/postinstall.mjs" ], "repository": { @@ -42,8 +41,10 @@ "scripts": { "dev": "node --import=tsx src/bin.ts --watch", "build": "node scripts/build-sea.mjs", + "stage:bin": "node scripts/stage-default-bin.mjs", "build:verify": "node scripts/verify-sea.mjs", "postinstall": "node scripts/postinstall.mjs", + "prepack": "pnpm build && pnpm stage:bin", "prepare": "effect-language-service patch", "clean": "rm -rf dist", "typecheck": "tsc -b tsconfig.json", diff --git a/scripts/launcher.mjs b/scripts/launcher.mjs deleted file mode 100644 index 8f52f5f..0000000 --- a/scripts/launcher.mjs +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node - -import { existsSync } from "node:fs"; -import { spawnSync } from "node:child_process"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const packageRoot = join(__dirname, ".."); -const nativeBinary = join(packageRoot, "dist", "native", "cmdly"); -const supportedTargets = ["linux-x64", "linux-arm64", "darwin-arm64"]; - -const detectTarget = () => { - if (process.platform === "linux" && process.arch === "x64") { - return "linux-x64"; - } - if (process.platform === "linux" && process.arch === "arm64") { - return "linux-arm64"; - } - if (process.platform === "darwin" && process.arch === "arm64") { - return "darwin-arm64"; - } - return null; -}; - -const fail = (message) => { - console.error(message); - process.exit(1); -}; - -const target = detectTarget(); - -if (!target) { - fail( - [ - `cmdly: unsupported platform ${process.platform}-${process.arch}.`, - `Supported targets: ${supportedTargets.join(", ")}.`, - "Download manually: https://github.com/Armadillidiid/cmdly/releases/latest", - ].join("\n"), - ); -} - -if (!existsSync(nativeBinary)) { - fail( - [ - `cmdly: native binary missing for ${target}.`, - "Likely causes: install scripts disabled (--ignore-scripts), offline install, or postinstall download failed.", - "Try: npm rebuild cmdly", - "Or reinstall without --ignore-scripts.", - "Releases: https://github.com/Armadillidiid/cmdly/releases/latest", - ].join("\n"), - ); -} - -const result = spawnSync(nativeBinary, process.argv.slice(2), { - stdio: "inherit", - env: process.env, -}); - -if (result.error) { - fail(`cmdly: failed to start native binary: ${result.error.message}`); -} - -if (typeof result.status === "number") { - process.exit(result.status); -} - -process.exit(1); diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs index 7e3da69..71349d8 100644 --- a/scripts/postinstall.mjs +++ b/scripts/postinstall.mjs @@ -10,8 +10,6 @@ import { readFileSync, renameSync, rmSync, - unlinkSync, - writeFileSync, } from "node:fs"; import { createHash } from "node:crypto"; import { dirname, join } from "node:path"; @@ -24,11 +22,8 @@ import pkg from "../package.json" with { type: "json" }; const __dirname = dirname(fileURLToPath(import.meta.url)); const packageRoot = join(__dirname, ".."); -const distDir = join(packageRoot, "dist"); -const nativeDir = join(distDir, "native"); -const nativeBinaryPath = join(nativeDir, "cmdly"); -const metadataPath = join(nativeDir, "metadata.json"); -const statePath = join(nativeDir, "install-state.json"); +const binDir = join(packageRoot, "bin"); +const binPath = join(binDir, "cmdly"); const supportedTargets = ["linux-x64", "linux-arm64", "darwin-arm64"]; const releaseBaseUrl = process.env.CMDLY_DOWNLOAD_BASE_URL ?? @@ -47,25 +42,6 @@ const detectTarget = () => { return null; }; -const loadJson = (path) => { - try { - return JSON.parse(readFileSync(path, "utf8")); - } catch { - return null; - } -}; - -const saveState = (state) => { - mkdirSync(nativeDir, { recursive: true }); - writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); -}; - -const clearState = () => { - if (existsSync(statePath)) { - unlinkSync(statePath); - } -}; - const downloadToFile = async (url, filePath) => { const response = await fetch(url, { headers: { @@ -120,11 +96,11 @@ const installBinary = (archivePath, binaryName) => { throw new Error(`archive did not contain expected binary: ${binaryName}`); } - mkdirSync(nativeDir, { recursive: true }); - const tempTarget = join(nativeDir, ".cmdly.tmp"); + mkdirSync(binDir, { recursive: true }); + const tempTarget = join(binDir, ".cmdly.tmp"); copyFileSync(extractedBinary, tempTarget); chmodSync(tempTarget, 0o755); - renameSync(tempTarget, nativeBinaryPath); + renameSync(tempTarget, binPath); rmSync(tempRoot, { recursive: true, force: true }); }; @@ -133,26 +109,13 @@ const main = async () => { const target = detectTarget(); if (!target) { - saveState({ - status: "unsupported_target", - platform: process.platform, - arch: process.arch, - supportedTargets, - installedAt: new Date().toISOString(), - }); console.warn( - `[postinstall] skipping native install for unsupported target ${process.platform}-${process.arch}`, + `[postinstall] skipping unsupported target ${process.platform}-${process.arch}; supported: ${supportedTargets.join(", ")}`, ); return; } - const cached = loadJson(metadataPath); - if ( - cached?.version === pkg.version && - cached?.target === target && - existsSync(nativeBinaryPath) - ) { - clearState(); + if (target === "linux-x64" && existsSync(binPath)) { return; } @@ -179,35 +142,13 @@ const main = async () => { const archiveBinaryName = `cmdly-v${pkg.version}-${target}`; installBinary(archivePath, archiveBinaryName); - writeFileSync( - metadataPath, - `${JSON.stringify( - { - version: pkg.version, - target, - assetName, - sha256: actual, - installedAt: new Date().toISOString(), - }, - null, - 2, - )}\n`, - "utf8", - ); - - clearState(); - console.log(`[postinstall] installed native binary for ${target}`); + console.log(`[postinstall] installed ${target} binary`); } finally { rmSync(tempRoot, { recursive: true, force: true }); } }; main().catch((error) => { - saveState({ - status: "install_failed", - message: error instanceof Error ? error.message : String(error), - installedAt: new Date().toISOString(), - }); console.warn( `[postinstall] native install failed: ${error instanceof Error ? error.message : String(error)}`, ); diff --git a/scripts/stage-default-bin.mjs b/scripts/stage-default-bin.mjs new file mode 100644 index 0000000..0be1764 --- /dev/null +++ b/scripts/stage-default-bin.mjs @@ -0,0 +1,18 @@ +import { chmodSync, copyFileSync, mkdirSync, rmSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = resolve(__dirname, ".."); + +const source = join(rootDir, "dist", "linux-x64", "cmdly"); +const targetDir = join(rootDir, "bin"); +const target = join(targetDir, "cmdly"); + +mkdirSync(targetDir, { recursive: true }); +rmSync(target, { force: true }); +copyFileSync(source, target); +chmodSync(target, 0o755); + +console.log(`[stage:bin] staged ${source} -> ${target}`); From d863af225cf313b05925ffca517b0d7fa2e44eca Mon Sep 17 00:00:00 2001 From: Emmanuel Isenah Date: Tue, 12 May 2026 12:50:27 +0100 Subject: [PATCH 4/8] update --- package.json | 4 +- scripts/postinstall.mjs | 190 +++++++++--------------------- scripts/validate-dist-targets.mjs | 21 ++++ 3 files changed, 79 insertions(+), 136 deletions(-) create mode 100644 scripts/validate-dist-targets.mjs diff --git a/package.json b/package.json index 4cece64..d610d06 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "files": [ "bin/cmdly", + "dist", "scripts/postinstall.mjs" ], "repository": { @@ -42,9 +43,10 @@ "dev": "node --import=tsx src/bin.ts --watch", "build": "node scripts/build-sea.mjs", "stage:bin": "node scripts/stage-default-bin.mjs", + "validate:dist": "node scripts/validate-dist-targets.mjs", "build:verify": "node scripts/verify-sea.mjs", "postinstall": "node scripts/postinstall.mjs", - "prepack": "pnpm build && pnpm stage:bin", + "prepack": "pnpm build && pnpm stage:bin && pnpm validate:dist", "prepare": "effect-language-service patch", "clean": "rm -rf dist", "typecheck": "tsc -b tsconfig.json", diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs index 71349d8..a5cb16d 100644 --- a/scripts/postinstall.mjs +++ b/scripts/postinstall.mjs @@ -1,156 +1,76 @@ #!/usr/bin/env node import { - chmodSync, - copyFileSync, - createReadStream, - createWriteStream, - existsSync, - mkdirSync, - readFileSync, - renameSync, - rmSync, + chmodSync, + copyFileSync, + existsSync, + mkdirSync, + renameSync, } from "node:fs"; -import { createHash } from "node:crypto"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { tmpdir } from "node:os"; -import { spawnSync } from "node:child_process"; -import { pipeline } from "node:stream/promises"; -import { Readable } from "node:stream"; -import pkg from "../package.json" with { type: "json" }; const __dirname = dirname(fileURLToPath(import.meta.url)); const packageRoot = join(__dirname, ".."); const binDir = join(packageRoot, "bin"); const binPath = join(binDir, "cmdly"); const supportedTargets = ["linux-x64", "linux-arm64", "darwin-arm64"]; -const releaseBaseUrl = - process.env.CMDLY_DOWNLOAD_BASE_URL ?? - `https://github.com/Armadillidiid/cmdly/releases/download/v${pkg.version}`; const detectTarget = () => { - if (process.platform === "linux" && process.arch === "x64") { - return "linux-x64"; - } - if (process.platform === "linux" && process.arch === "arm64") { - return "linux-arm64"; - } - if (process.platform === "darwin" && process.arch === "arm64") { - return "darwin-arm64"; - } - return null; + if (process.platform === "linux" && process.arch === "x64") { + return "linux-x64"; + } + if (process.platform === "linux" && process.arch === "arm64") { + return "linux-arm64"; + } + if (process.platform === "darwin" && process.arch === "arm64") { + return "darwin-arm64"; + } + return null; }; -const downloadToFile = async (url, filePath) => { - const response = await fetch(url, { - headers: { - "user-agent": `cmdly-installer/${pkg.version}`, - accept: "application/octet-stream,text/plain", - }, - }); +const installBinary = (sourcePath) => { + if (!existsSync(sourcePath)) { + throw new Error(`missing target binary in package: ${sourcePath}`); + } - if (!response.ok || !response.body) { - throw new Error(`download failed (${response.status}) for ${url}`); - } - - await pipeline(Readable.fromWeb(response.body), createWriteStream(filePath)); + mkdirSync(binDir, { recursive: true }); + const tempTarget = join(binDir, ".cmdly.tmp"); + copyFileSync(sourcePath, tempTarget); + chmodSync(tempTarget, 0o755); + renameSync(tempTarget, binPath); }; -const checksumForFile = (filePath) => - new Promise((resolve, reject) => { - const hash = createHash("sha256"); - const stream = createReadStream(filePath); - stream.on("error", reject); - stream.on("data", (chunk) => hash.update(chunk)); - stream.on("end", () => resolve(hash.digest("hex"))); - }); - -const parseSha256File = (content) => { - const line = content.trim().split(/\r?\n/)[0] ?? ""; - const [sum] = line.trim().split(/\s+/); - if (!sum) { - throw new Error("invalid checksum file"); - } - return sum.toLowerCase(); -}; - -const installBinary = (archivePath, binaryName) => { - const tempRoot = join(tmpdir(), `cmdly-install-${Date.now()}`); - mkdirSync(tempRoot, { recursive: true }); - - const extractResult = spawnSync("tar", ["-xzf", archivePath, "-C", tempRoot], { - stdio: "pipe", - }); - - if (extractResult.status !== 0) { - rmSync(tempRoot, { recursive: true, force: true }); - throw new Error( - `failed to extract archive: ${extractResult.stderr?.toString("utf8") ?? "unknown error"}`, - ); - } - - const extractedBinary = join(tempRoot, binaryName); - if (!existsSync(extractedBinary)) { - rmSync(tempRoot, { recursive: true, force: true }); - throw new Error(`archive did not contain expected binary: ${binaryName}`); - } - - mkdirSync(binDir, { recursive: true }); - const tempTarget = join(binDir, ".cmdly.tmp"); - copyFileSync(extractedBinary, tempTarget); - chmodSync(tempTarget, 0o755); - renameSync(tempTarget, binPath); - - rmSync(tempRoot, { recursive: true, force: true }); -}; - -const main = async () => { - const target = detectTarget(); - - if (!target) { - console.warn( - `[postinstall] skipping unsupported target ${process.platform}-${process.arch}; supported: ${supportedTargets.join(", ")}`, - ); - return; - } - - if (target === "linux-x64" && existsSync(binPath)) { - return; - } - - const assetName = `cmdly-v${pkg.version}-${target}.tar.gz`; - const checksumName = `${assetName}.sha256`; - const archiveUrl = `${releaseBaseUrl}/${assetName}`; - const checksumUrl = `${releaseBaseUrl}/${checksumName}`; - - const tempRoot = join(tmpdir(), `cmdly-download-${Date.now()}`); - mkdirSync(tempRoot, { recursive: true }); - const archivePath = join(tempRoot, assetName); - const checksumPath = join(tempRoot, checksumName); - - try { - await downloadToFile(archiveUrl, archivePath); - await downloadToFile(checksumUrl, checksumPath); - - const expected = parseSha256File(readFileSync(checksumPath, "utf8")); - const actual = await checksumForFile(archivePath); - if (expected !== actual) { - throw new Error(`checksum mismatch for ${assetName}`); - } - - const archiveBinaryName = `cmdly-v${pkg.version}-${target}`; - installBinary(archivePath, archiveBinaryName); - - console.log(`[postinstall] installed ${target} binary`); - } finally { - rmSync(tempRoot, { recursive: true, force: true }); - } +const main = () => { + const target = detectTarget(); + + if (!target) { + console.warn( + `[postinstall] skipping unsupported target ${process.platform}-${process.arch}; supported: ${supportedTargets.join(", ")}`, + ); + return; + } + + if (target === "linux-x64" && existsSync(binPath)) { + // The binary is already included in the package and bin field points to it + // No postinstall setup needed + console.log( + `[postinstall] skipping install for ${target} since binary already exists`, + ); + return; + } + + const sourcePath = join(packageRoot, "dist", target, "cmdly"); + installBinary(sourcePath); + + console.log(`[postinstall] installed ${target} binary`); }; -main().catch((error) => { - console.warn( - `[postinstall] native install failed: ${error instanceof Error ? error.message : String(error)}`, - ); - process.exit(0); -}); +try { + main(); +} catch (error) { + console.warn( + `[postinstall] native install failed: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(0); +} diff --git a/scripts/validate-dist-targets.mjs b/scripts/validate-dist-targets.mjs new file mode 100644 index 0000000..3de8d3d --- /dev/null +++ b/scripts/validate-dist-targets.mjs @@ -0,0 +1,21 @@ +import { existsSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = resolve(__dirname, ".."); + +const requiredTargets = ["linux-x64", "linux-arm64", "darwin-arm64"]; + +const missing = requiredTargets.filter( + (target) => !existsSync(join(rootDir, "dist", target, "cmdly")), +); + +if (missing.length > 0) { + throw new Error( + `Missing dist binaries for: ${missing.join(", ")}. Expected dist//cmdly for all targets.`, + ); +} + +console.log(`[validate:dist] found binaries for ${requiredTargets.join(", ")}`); From 340ee27635f96dc62126b0c7e8ea274ed8c6e240 Mon Sep 17 00:00:00 2001 From: Emmanuel Isenah Date: Tue, 12 May 2026 12:59:51 +0100 Subject: [PATCH 5/8] update --- package.json | 3 +-- scripts/validate-dist-targets.mjs | 21 --------------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 scripts/validate-dist-targets.mjs diff --git a/package.json b/package.json index d610d06..c04f1e5 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,9 @@ "dev": "node --import=tsx src/bin.ts --watch", "build": "node scripts/build-sea.mjs", "stage:bin": "node scripts/stage-default-bin.mjs", - "validate:dist": "node scripts/validate-dist-targets.mjs", "build:verify": "node scripts/verify-sea.mjs", "postinstall": "node scripts/postinstall.mjs", - "prepack": "pnpm build && pnpm stage:bin && pnpm validate:dist", + "prepack": "pnpm build && pnpm stage:bin", "prepare": "effect-language-service patch", "clean": "rm -rf dist", "typecheck": "tsc -b tsconfig.json", diff --git a/scripts/validate-dist-targets.mjs b/scripts/validate-dist-targets.mjs deleted file mode 100644 index 3de8d3d..0000000 --- a/scripts/validate-dist-targets.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import { existsSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const rootDir = resolve(__dirname, ".."); - -const requiredTargets = ["linux-x64", "linux-arm64", "darwin-arm64"]; - -const missing = requiredTargets.filter( - (target) => !existsSync(join(rootDir, "dist", target, "cmdly")), -); - -if (missing.length > 0) { - throw new Error( - `Missing dist binaries for: ${missing.join(", ")}. Expected dist//cmdly for all targets.`, - ); -} - -console.log(`[validate:dist] found binaries for ${requiredTargets.join(", ")}`); From 28244ffcf919ad0649c06fc3dac6d602b64ecd56 Mon Sep 17 00:00:00 2001 From: Emmanuel Isenah <86387828+Armadillidiid@users.noreply.github.com> Date: Tue, 12 May 2026 19:39:00 +0100 Subject: [PATCH 6/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/postinstall.mjs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs index a5cb16d..2aa0263 100644 --- a/scripts/postinstall.mjs +++ b/scripts/postinstall.mjs @@ -69,8 +69,14 @@ const main = () => { try { main(); } catch (error) { - console.warn( - `[postinstall] native install failed: ${error instanceof Error ? error.message : String(error)}`, - ); + const target = detectTarget(); + const message = error instanceof Error ? error.message : String(error); + + if (target) { + console.error(`[postinstall] native install failed for ${target}: ${message}`); + process.exit(1); + } + + console.warn(`[postinstall] native install failed on unsupported target: ${message}`); process.exit(0); } From 2da4d09e3f87f1b90c1104a638873696fd1cac1e Mon Sep 17 00:00:00 2001 From: Emmanuel Isenah Date: Sun, 17 May 2026 15:41:50 +0100 Subject: [PATCH 7/8] refactor: add publish script --- package.json | 3 +-- scripts/publish.mjs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 scripts/publish.mjs diff --git a/package.json b/package.json index c04f1e5..55e8ae7 100644 --- a/package.json +++ b/package.json @@ -44,14 +44,13 @@ "build": "node scripts/build-sea.mjs", "stage:bin": "node scripts/stage-default-bin.mjs", "build:verify": "node scripts/verify-sea.mjs", - "postinstall": "node scripts/postinstall.mjs", "prepack": "pnpm build && pnpm stage:bin", "prepare": "effect-language-service patch", "clean": "rm -rf dist", "typecheck": "tsc -b tsconfig.json", "test": "vitest run", "changeset-version": "changeset version", - "changeset-publish": "pnpm build && pnpm vitest && changeset publish && changeset tag && git push --tags", + "changeset-publish": "node scripts/publish.mjs", "format": "biome format --write", "lint": "biome lint --write", "check": "biome check --write --unsafe" diff --git a/scripts/publish.mjs b/scripts/publish.mjs new file mode 100644 index 0000000..314e805 --- /dev/null +++ b/scripts/publish.mjs @@ -0,0 +1,36 @@ +import { writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import pkg from "../package.json" with { type: "json" }; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = resolve(__dirname, ".."); +const pkgPath = join(rootDir, "package.json"); + +const run = (cmd, args, opts) => { + const result = spawnSync(cmd, args, { + cwd: rootDir, + stdio: "inherit", + ...opts, + }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +}; + +const original = JSON.parse(JSON.stringify(pkg)); + +original.scripts.postinstall = "node scripts/postinstall.mjs"; +writeFileSync(pkgPath, `${JSON.stringify(original, null, "\t")}\n`, "utf8"); + +run("pnpm", ["build"]); +run("pnpm", ["stage:bin"]); +run("pnpm", ["vitest"]); +run("changeset", ["publish"]); +run("changeset", ["tag"]); +run("git", ["push", "--tags"]); + +delete original.scripts.postinstall; +writeFileSync(pkgPath, `${JSON.stringify(original, null, "\t")}\n`, "utf8"); From 3dd19e94dd3f0892c586c0056abb3498f5d547f2 Mon Sep 17 00:00:00 2001 From: Emmanuel Isenah Date: Tue, 19 May 2026 05:11:03 +0100 Subject: [PATCH 8/8] feat: switch temp switch to npm for publishing --- .changeset/icy-melons-tan.md | 5 +++++ scripts/publish.mjs | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 .changeset/icy-melons-tan.md diff --git a/.changeset/icy-melons-tan.md b/.changeset/icy-melons-tan.md new file mode 100644 index 0000000..fb6228d --- /dev/null +++ b/.changeset/icy-melons-tan.md @@ -0,0 +1,5 @@ +--- +"cmdly": patch +--- + +Add postinstall to build for other platforms and arch other than linux-x64 diff --git a/scripts/publish.mjs b/scripts/publish.mjs index 314e805..3f4bea2 100644 --- a/scripts/publish.mjs +++ b/scripts/publish.mjs @@ -20,9 +20,31 @@ const run = (cmd, args, opts) => { } }; +const getNpmVersion = () => { + const result = spawnSync("npm", ["--version"], { + cwd: rootDir, + encoding: "utf8", + stdio: "pipe", + }); + return result.stdout.trim(); +}; + +// @changesets/cli currently only supports npm so we need to temporarily change our package manager +const npmVersion = getNpmVersion(); const original = JSON.parse(JSON.stringify(pkg)); +const savedPackageManager = pkg.packageManager; +const savedDevEnginesPackageManager = JSON.parse( + JSON.stringify(pkg.devEngines?.packageManager), +); original.scripts.postinstall = "node scripts/postinstall.mjs"; +original.packageManager = `npm@${npmVersion}`; +if (original.devEngines) { + original.devEngines.packageManager = { + name: "npm", + version: npmVersion, + }; +} writeFileSync(pkgPath, `${JSON.stringify(original, null, "\t")}\n`, "utf8"); run("pnpm", ["build"]); @@ -33,4 +55,8 @@ run("changeset", ["tag"]); run("git", ["push", "--tags"]); delete original.scripts.postinstall; +original.packageManager = savedPackageManager; +if (original.devEngines) { + original.devEngines.packageManager = savedDevEnginesPackageManager; +} writeFileSync(pkgPath, `${JSON.stringify(original, null, "\t")}\n`, "utf8");