From cc09b9485385f763310be19a746b894d4a1a622f Mon Sep 17 00:00:00 2001 From: Moon Date: Fri, 14 Aug 2026 14:53:19 +0200 Subject: [PATCH 1/4] fix: allow running quartz via bunx --- package-lock.json | 1 + package.json | 3 ++- quartz/bootstrap-postinstall.mjs | 27 +++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 quartz/bootstrap-postinstall.mjs diff --git a/package-lock.json b/package-lock.json index cc63450c62269..4ce9617faf636 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@jackyzha0/quartz", "version": "5.0.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@clack/prompts": "^0.11.0", diff --git a/package.json b/package.json index 8705f4e2680e2..21d17b876063a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "test": "tsx --test", "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1", "install-plugins": "npx tsx ./quartz/plugins/loader/install-plugins.ts", - "prebuild": "npm run install-plugins" + "prebuild": "npm run install-plugins", + "postinstall": "node quartz/bootstrap-postinstall.mjs" }, "engines": { "npm": ">=10.9.2", diff --git a/quartz/bootstrap-postinstall.mjs b/quartz/bootstrap-postinstall.mjs new file mode 100644 index 0000000000000..3569e0d508959 --- /dev/null +++ b/quartz/bootstrap-postinstall.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +import { chmodSync, existsSync, mkdirSync, symlinkSync, unlinkSync } from "fs" +import path from "path" + +const root = path.resolve(import.meta.dirname, "..") +const target = path.join(root, "quartz", "bootstrap-cli.mjs") + +try { + chmodSync(target, 0o755) +} catch { + // best-effort, not fatal if it were to fail +} + +const binDir = path.join(root, "node_modules", ".bin") +mkdirSync(binDir, { recursive: true }) + +// only tested on mac, someone on windows will have to add a .cmd shim for this +const linkPath = path.join(binDir, "quartz") +const relativeTarget = path.relative(binDir, target) +try { + if (existsSync(linkPath)) { + unlinkSync(linkPath) + } + symlinkSync(relativeTarget, linkPath) +} catch (err) { + console.warn(`[quartz] could not link node_modules/.bin/quartz: ${err.message}`) +} From 8ec0b6526ac7f26836b7df49484aa47499927ee9 Mon Sep 17 00:00:00 2001 From: Moon Date: Fri, 14 Aug 2026 23:46:03 +0200 Subject: [PATCH 2/4] improve: make postinstall cli setup robust --- quartz/bootstrap-postinstall.mjs | 46 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/quartz/bootstrap-postinstall.mjs b/quartz/bootstrap-postinstall.mjs index 3569e0d508959..968b061c4e683 100644 --- a/quartz/bootstrap-postinstall.mjs +++ b/quartz/bootstrap-postinstall.mjs @@ -1,27 +1,41 @@ #!/usr/bin/env node -import { chmodSync, existsSync, mkdirSync, symlinkSync, unlinkSync } from "fs" +import { chmodSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync } from "fs" import path from "path" -const root = path.resolve(import.meta.dirname, "..") -const target = path.join(root, "quartz", "bootstrap-cli.mjs") +// npm links the `bin` entry into node_modules/.bin on install, bun does not -try { - chmodSync(target, 0o755) -} catch { - // best-effort, not fatal if it were to fail +// a symlink is useless on windows, this needs a .cmd/.ps1 shim instead +if (process.platform === "win32") { + process.exit(0) } -const binDir = path.join(root, "node_modules", ".bin") -mkdirSync(binDir, { recursive: true }) - -// only tested on mac, someone on windows will have to add a .cmd shim for this -const linkPath = path.join(binDir, "quartz") -const relativeTarget = path.relative(binDir, target) try { - if (existsSync(linkPath)) { - unlinkSync(linkPath) + const root = path.resolve(import.meta.dirname, "..") + const target = path.join(root, "quartz", "bootstrap-cli.mjs") + + try { + // checked in as 755, but bunx won't run the cli if a setup drops the mode + chmodSync(target, 0o755) + } catch (err) { + console.warn(`[quartz] could not make ${target} executable: ${err.message}`) + } + + const binDir = path.join(root, "node_modules", ".bin") + const linkPath = path.join(binDir, "quartz") + const relativeTarget = path.relative(binDir, target) + + mkdirSync(binDir, { recursive: true }) + try { + symlinkSync(relativeTarget, linkPath) + } catch (err) { + if (err.code !== "EEXIST") { + throw err + } + if (readlinkSync(linkPath) !== relativeTarget) { + unlinkSync(linkPath) + symlinkSync(relativeTarget, linkPath) + } } - symlinkSync(relativeTarget, linkPath) } catch (err) { console.warn(`[quartz] could not link node_modules/.bin/quartz: ${err.message}`) } From 3356c8e9908607052f09ccbb22183c318b5d3ae5 Mon Sep 17 00:00:00 2001 From: Moon Date: Sat, 15 Aug 2026 21:12:24 +0200 Subject: [PATCH 3/4] improve: add shims for bunx windows support --- .github/workflows/ci.yaml | 8 +++++ quartz/bootstrap-postinstall.mjs | 55 +++++++++++++++++++------------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4aeda17fdac59..9cbd004522110 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,6 +28,9 @@ jobs: with: node-version: 24 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + - name: Cache dependencies uses: actions/cache@v5 with: @@ -46,6 +49,11 @@ jobs: - run: npm install + # --no-install because a bare `bunx quartz` falls back to an unrelated + # package on npm instead of failing + - name: Ensure bunx resolves the local CLI + run: bunx --no-install quartz --help + - name: Install Quartz plugins run: npx quartz plugin install diff --git a/quartz/bootstrap-postinstall.mjs b/quartz/bootstrap-postinstall.mjs index 968b061c4e683..87de3b6853425 100644 --- a/quartz/bootstrap-postinstall.mjs +++ b/quartz/bootstrap-postinstall.mjs @@ -1,39 +1,50 @@ #!/usr/bin/env node -import { chmodSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync } from "fs" +import { chmodSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "fs" import path from "path" // npm links the `bin` entry into node_modules/.bin on install, bun does not - -// a symlink is useless on windows, this needs a .cmd/.ps1 shim instead -if (process.platform === "win32") { - process.exit(0) -} - try { const root = path.resolve(import.meta.dirname, "..") const target = path.join(root, "quartz", "bootstrap-cli.mjs") - - try { - // checked in as 755, but bunx won't run the cli if a setup drops the mode - chmodSync(target, 0o755) - } catch (err) { - console.warn(`[quartz] could not make ${target} executable: ${err.message}`) - } - const binDir = path.join(root, "node_modules", ".bin") const linkPath = path.join(binDir, "quartz") const relativeTarget = path.relative(binDir, target) mkdirSync(binDir, { recursive: true }) - try { - symlinkSync(relativeTarget, linkPath) - } catch (err) { - if (err.code !== "EEXIST") { - throw err + + if (process.platform === "win32") { + // windows ignores the shebang, so it needs the same shims npm writes + const posixTarget = relativeTarget.split(path.sep).join("/") + writeFileSync( + linkPath, + `#!/bin/sh\nexec node --no-deprecation "$(dirname "$0")/${posixTarget}" "$@"\n`, + ) + writeFileSync( + `${linkPath}.cmd`, + `@ECHO off\r\nnode --no-deprecation "%~dp0${relativeTarget}" %*\r\n`, + ) + writeFileSync( + `${linkPath}.ps1`, + `#!/usr/bin/env pwsh\nnode --no-deprecation "$PSScriptRoot/${posixTarget}" $args\nexit $LASTEXITCODE\n`, + ) + } else { + try { + // checked in as 755, but bunx won't run the cli if a setup drops the mode + chmodSync(target, 0o755) + } catch (err) { + console.warn(`[quartz] could not make ${target} executable: ${err.message}`) } - if (readlinkSync(linkPath) !== relativeTarget) { - unlinkSync(linkPath) + + try { symlinkSync(relativeTarget, linkPath) + } catch (err) { + if (err.code !== "EEXIST") { + throw err + } + if (readlinkSync(linkPath) !== relativeTarget) { + unlinkSync(linkPath) + symlinkSync(relativeTarget, linkPath) + } } } } catch (err) { From 4afc5ecabc5357d2cc67909d398ac3d4859efd19 Mon Sep 17 00:00:00 2001 From: Moon Date: Sun, 16 Aug 2026 18:05:20 +0200 Subject: [PATCH 4/4] fix: stop postinstall from writing through the .bin symlink --- quartz/bootstrap-postinstall.mjs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/quartz/bootstrap-postinstall.mjs b/quartz/bootstrap-postinstall.mjs index 87de3b6853425..d0a4f4b85e7d6 100644 --- a/quartz/bootstrap-postinstall.mjs +++ b/quartz/bootstrap-postinstall.mjs @@ -1,5 +1,13 @@ #!/usr/bin/env node -import { chmodSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "fs" +import { + chmodSync, + existsSync, + mkdirSync, + readlinkSync, + symlinkSync, + unlinkSync, + writeFileSync, +} from "fs" import path from "path" // npm links the `bin` entry into node_modules/.bin on install, bun does not @@ -10,11 +18,22 @@ try { const linkPath = path.join(binDir, "quartz") const relativeTarget = path.relative(binDir, target) + // as a dependency, npm links the bin itself, running here would only litter a node_modules into the package + if (!existsSync(path.join(root, "node_modules"))) { + process.exit(0) + } + mkdirSync(binDir, { recursive: true }) if (process.platform === "win32") { // windows ignores the shebang, so it needs the same shims npm writes const posixTarget = relativeTarget.split(path.sep).join("/") + + // a symlink left by a wsl run would be followed and the shim written straight into the cli source file + try { + unlinkSync(linkPath) + } catch {} + writeFileSync( linkPath, `#!/bin/sh\nexec node --no-deprecation "$(dirname "$0")/${posixTarget}" "$@"\n`, @@ -41,7 +60,13 @@ try { if (err.code !== "EEXIST") { throw err } - if (readlinkSync(linkPath) !== relativeTarget) { + // not necessarily a symlink, a windows checkout leaves a plain file here + let current = null + try { + current = readlinkSync(linkPath) + } catch {} + + if (current !== relativeTarget) { unlinkSync(linkPath) symlinkSync(relativeTarget, linkPath) }