From d0a00ae798b6555ed62f5f91e17544b312924aa7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 04:13:41 +0000 Subject: [PATCH 1/7] Add standalone native binaries for Windows/macOS/Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/packages/binaries/ (@pptxdiff/binaries): builds a standalone pptxdiff executable per OS via Node's Single Executable Applications feature, so a user can download one artifact and run it without installing Node.js. Deliberately standalone binaries, not signed OS installers — asked directly, matching a prior explicit decision to avoid Electron/Tauri-style installer/signing overhead. - bin/cli.js: startServer() gains a backward-compatible optional `root` param so the packaged binary can serve assets from next to itself instead of the npm package's own directory. - sea-entry.cjs + build.mjs: bundle via esbuild, generate the SEA blob, inject via postject, copy static app assets alongside the binary, zip as the downloadable artifact. - .github/workflows/binaries.yml: 3-OS CI matrix (SEA has no cross-compile mode, so each OS's binary is built on that OS). - make pkg.binaries.build / npm run build:binary for local builds. Verified end-to-end on Linux: built, ran the actual packaged binary, confirmed it serves index.html/support.js/vendor/* correctly. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BxwMTp6RQJ6j6K5K8Jjdpm --- .github/workflows/binaries.yml | 42 ++ .gitignore | 13 + .gitignores/user.gitignore | 13 + Makefile | 15 +- bin/cli.js | 10 +- docs/.scrolls/GAP_ANALYSIS.md | 7 + docs/.scrolls/GAP_CONTEXT.md | 12 + docs/.scrolls/HANDOFF.md | 12 + docs/.scrolls/PLAN.md | 32 + docs/.scrolls/SPEC.md | 8 + package.json | 3 +- src/packages/binaries/README.md | 63 ++ src/packages/binaries/build.mjs | 176 +++++ src/packages/binaries/package-lock.json | 625 ++++++++++++++++++ src/packages/binaries/package.json | 37 ++ .../binaries/pptxdiff-linux/README.md | 23 + src/packages/binaries/pptxdiff-mac/README.md | 26 + src/packages/binaries/pptxdiff-win/README.md | 24 + src/packages/binaries/sea-entry.cjs | 39 ++ 19 files changed, 1175 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/binaries.yml create mode 100644 src/packages/binaries/README.md create mode 100644 src/packages/binaries/build.mjs create mode 100644 src/packages/binaries/package-lock.json create mode 100644 src/packages/binaries/package.json create mode 100644 src/packages/binaries/pptxdiff-linux/README.md create mode 100644 src/packages/binaries/pptxdiff-mac/README.md create mode 100644 src/packages/binaries/pptxdiff-win/README.md create mode 100644 src/packages/binaries/sea-entry.cjs diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml new file mode 100644 index 0000000..7936cf7 --- /dev/null +++ b/.github/workflows/binaries.yml @@ -0,0 +1,42 @@ +name: binaries + +on: + push: + branches: [master] + paths: + - "bin/cli.js" + - "src/pptxdiff/**" + - "src/packages/binaries/**" + - ".github/workflows/binaries.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + target: linux + - runner: macos-latest + target: mac + - runner: windows-latest + target: win + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v5 + with: + node-version: "22" + - run: npm install + working-directory: src/packages/binaries + - run: npm run build + working-directory: src/packages/binaries + - uses: actions/upload-artifact@v4 + with: + name: pptxdiff-${{ matrix.target }} + path: src/packages/binaries/pptxdiff-${{ matrix.target }}/*.zip + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 4d55fa9..6cf4ef3 100644 --- a/.gitignore +++ b/.gitignore @@ -385,3 +385,16 @@ src/pptxdiff/docs-site/site/ ## Covers pptxdiff-cli, @pptxdiff/server, and any future src/packages/*/lib/. !src/packages/*/lib/ !src/packages/*/lib/** + +## Native-binary build output (src/packages/binaries/build.mjs) — the +## executable, the copied "assets" folder, and the zipped artifact are all +## generated per-OS, not source; keep each OS folder's own README.md tracked +## (it documents the folder before a build has ever run there) but ignore +## everything else build.mjs writes into it. +src/packages/binaries/pptxdiff-win/* +src/packages/binaries/pptxdiff-mac/* +src/packages/binaries/pptxdiff-linux/* +!src/packages/binaries/pptxdiff-win/README.md +!src/packages/binaries/pptxdiff-mac/README.md +!src/packages/binaries/pptxdiff-linux/README.md +src/packages/binaries/.build/ diff --git a/.gitignores/user.gitignore b/.gitignores/user.gitignore index d50e3a3..38a243f 100644 --- a/.gitignores/user.gitignore +++ b/.gitignores/user.gitignore @@ -29,3 +29,16 @@ src/pptxdiff/docs-site/site/ ## Covers pptxdiff-cli, @pptxdiff/server, and any future src/packages/*/lib/. !src/packages/*/lib/ !src/packages/*/lib/** + +## Native-binary build output (src/packages/binaries/build.mjs) — the +## executable, the copied "assets" folder, and the zipped artifact are all +## generated per-OS, not source; keep each OS folder's own README.md tracked +## (it documents the folder before a build has ever run there) but ignore +## everything else build.mjs writes into it. +src/packages/binaries/pptxdiff-win/* +src/packages/binaries/pptxdiff-mac/* +src/packages/binaries/pptxdiff-linux/* +!src/packages/binaries/pptxdiff-win/README.md +!src/packages/binaries/pptxdiff-mac/README.md +!src/packages/binaries/pptxdiff-linux/README.md +src/packages/binaries/.build/ diff --git a/Makefile b/Makefile index f20a91b..dcccba3 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,9 @@ VSCE_EXT_VERSION := $(shell node -p "require('$(VSCE_PKGJSON_PATH)').version") VSCE_EXT_NAME := $(shell node -p "require('$(VSCE_PKGJSON_PATH)').name") VSIX_EXT_PATH_LOCAL := dist/$(VSCE_EXT_NAME)-$(VSCE_EXT_VERSION).vsix +BINARIES_PKG_DIR_RELPATH := ./src/packages/binaries +BINARIES_PKG_DIR := $(abspath $(ROOT_DIR)/$(BINARIES_PKG_DIR_RELPATH)) + MKDOCS_YML_PATH := $(abspath $(ROOT_DIR)/$(MKDOCS_YML_RELPATH)) ####################### DETERMINE VSCODE EDITOR TYPE ########################### @@ -97,6 +100,7 @@ help: # pkg.build : Build the extension (creates a.vsix file). # pkg.publish : Publish the extension. # pkg.release : Build and Publish the extension. + # pkg.binaries.build: Build a standalone native pptxdiff executable for the current OS. # # vsce.open : Opens the VS Code Extension Management page for a Publisher. # vsce.token : Opens the Azure DevOps Page to Manage the Personal Access Token for VSCE. @@ -209,7 +213,16 @@ pkg.vsce.install.local: @echo -e "\n✨ Installing VS Code extension locally (from 'dist/' folder)... ⏳\n" @echo -e "\n✨ VS Code Type: $(VSCODE_CMD)" @$(VSCODE_CMD) --install-extension $(VSCE_PKG_DIR)/$(VSIX_EXT_PATH_LOCAL) --force - + +.PHONY: pkg.binaries.build +pkg.binaries.build: + @## npm run build:binary — builds a standalone native pptxdiff executable + @## for the CURRENT host OS only (Node SEA has no cross-compile mode); see + @## src/packages/binaries/README.md and .github/workflows/binaries.yml for + @## how all three (win/mac/linux) get built via a CI matrix. + @echo -e "\n✨ Building native pptxdiff binary for the current OS... ⏳\n" + @cd $(BINARIES_PKG_DIR) && npm install && npm run build + ############################## ..: COMMANDS vsce.*:.. ################################ diff --git a/bin/cli.js b/bin/cli.js index 9c12d81..b94adab 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -97,15 +97,19 @@ function isPathContained(root, candidate) { // existing trap about `pptxdiff-vscode/extension.js` carrying its own // independent (and once out-of-sync) copy of this same server; new // consumers should import this function rather than repeat that mistake. +// `root` defaults to this package's own bundled `src/pptxdiff` (the normal +// npm-install case); `src/packages/binaries`'s Node SEA entry point passes +// an explicit `root` instead, since a packaged single-executable binary has +// no `__dirname`-relative sibling files to find the assets from. // Resolves to { server, port, url } once listening; the caller decides // what to do with the URL (print it, open a browser, hand it to Playwright). -function startServer() { +function startServer(root = ROOT) { return new Promise((resolve, reject) => { const server = http.createServer((req, res) => { const reqPath = decodeURIComponent(req.url.split("?")[0]); - const filePath = path.join(ROOT, path.normalize(reqPath === "/" ? "/index.html" : reqPath)); + const filePath = path.join(root, path.normalize(reqPath === "/" ? "/index.html" : reqPath)); - if (!isPathContained(ROOT, filePath)) { + if (!isPathContained(root, filePath)) { res.writeHead(403, SECURITY_HEADERS); res.end("Forbidden"); return; diff --git a/docs/.scrolls/GAP_ANALYSIS.md b/docs/.scrolls/GAP_ANALYSIS.md index 7f88cef..a1be8e3 100644 --- a/docs/.scrolls/GAP_ANALYSIS.md +++ b/docs/.scrolls/GAP_ANALYSIS.md @@ -163,6 +163,13 @@ Concrete, testable gaps between what SPEC.md describes and a fully "real" implem - [ ] **No MCP server** (`pptxdiff-cli mcp`, CLI_API_DESIGN.md §9) — the more idiomatic AI-agent integration point than shelling out to the CLI or hand-rolling HTTP calls. Not started; `--json` output + the API's request/response shapes are the current AI-agent integration surface. - [ ] **Not published to npm.** Both packages depend on their monorepo siblings via `file:` protocol (documented in each README) rather than a real published semver range — intentional for local development, but a real blocker before either package can actually be `npm install`ed by anyone outside this repo. +## Standalone native binaries (this session) +- [ ] **Windows `.exe` is unsigned; macOS binary is ad-hoc signed only (no Developer ID).** No code-signing certificate exists for this project — Windows SmartScreen and macOS Gatekeeper will both warn on a freshly-downloaded copy. Documented per-OS in `src/packages/binaries/pptxdiff-{win,mac}/README.md`; a real fix needs a paid cert (Apple Developer ID + Windows Authenticode), not a code change. +- [ ] **No cross-compilation** — Node SEA builds by injecting into a copy of the currently-running `node` binary, so each OS's binary can only be built ON that OS. `.github/workflows/binaries.yml`'s 3-way CI matrix is the actual mechanism that produces all three; there is no single-machine "build everything" path. +- [ ] **Not yet attached to GitHub Releases.** The CI workflow uploads each OS's zip as a workflow artifact (downloadable from the Actions run page) but nothing wires a release-tag push to attach them to an actual GitHub Release yet — a real, small follow-up (e.g. `softprops/action-gh-release` on `release: types: [published]`), not attempted this session. +- [ ] **~120MB per binary.** Node SEA embeds the entire Node runtime into the executable; there's no way to shrink this within the SEA approach itself (it isn't a JS-bundle-size problem). +- [ ] **macOS/Windows builds are unverified in this sandbox** — only the Linux build was actually run and its binary actually executed end-to-end (built, launched, served `index.html`/`support.js`/`vendor/*` via real HTTP requests). The macOS/Windows code paths (codesign steps, `.exe` naming) are structurally parallel but exercised for the first time whenever CI first runs them, not locally. + ## Content checksum (this session) - [ ] **The content checksum shows "unavailable" under the plain `file://` open path.** `crypto.subtle` (native Web Crypto, no new dependency) requires a secure context — guaranteed under the CLI's `http://localhost` default, not guaranteed under this project's other documented launch path (`git clone` + open `index.html` directly). Handled honestly (checked once at boot via `cryptoSubtleAvailable`, shown as "unavailable (requires a secure context)" rather than a wrong/fabricated hash or a permanently-stuck "computing…"), but not worked around — would need either a pure-JS SHA-256 fallback (a real new dependency, or a hand-rolled implementation neither asked for nor free of its own correctness risk) or accepting the gap under that one launch path. - [ ] **The checksum's excluded-parts list (`docProps/core.xml`, `docProps/app.xml`, `docProps/thumbnail.*`) is a fixed, hardcoded set, not user-configurable.** If a future real-world `.pptx` turns out to carry other non-content, save-time-varying metadata this project hasn't seen yet, it would need a code change to exclude, not a setting. Not speculatively generalized ahead of an actual observed case. diff --git a/docs/.scrolls/GAP_CONTEXT.md b/docs/.scrolls/GAP_CONTEXT.md index 1d2ec44..855ddbd 100644 --- a/docs/.scrolls/GAP_CONTEXT.md +++ b/docs/.scrolls/GAP_CONTEXT.md @@ -163,6 +163,18 @@ Three options existed: (1) npm workspaces at the repo root, (2) a real published ## Why @pptxdiff/server's file upload wire format is base64-in-JSON, not multipart/form-data `multipart/form-data` is the more standard shape for file uploads, but parsing it correctly (boundary handling, streaming, encoding edge cases) either needs a new dependency or a genuinely nontrivial hand-rolled parser — neither justified for a Phase 1 whose goal was proving the endpoint surface and automation wiring, not building production-grade upload handling. Base64-in-JSON needs zero new code beyond `Buffer.from(content, 'base64')` and is fully consistent with the same `node:http`-only, no-framework style `bin/cli.js` already established. Documented as a real, named gap (GAP_ANALYSIS.md) rather than left undiscoverable — a future session should build multipart support when large-file efficiency actually matters, not before. +## Why the native binaries are standalone executables, not real OS installers +The user's own task description asked to place "downloadable installers" under `src/packages/binaries/pptxdiff-{win,mac,linux}` and explicitly asked whether the folder should be called `binaries` or `installers` — a genuine fork with materially different cost, not a naming bikeshed. Two options existed: (1) real OS-native installers (`.msi`/signed `.exe` wizard on Windows, `.pkg`/`.dmg` on macOS, `.deb`/`.rpm`/AppImage on Linux) that register the CLI on PATH and appear in the system's app list, needing a paid code-signing certificate (Apple Developer ID, Windows Authenticode) to avoid OS security warnings; (2) standalone single-file-ish executables (Node SEA) that a user downloads and runs directly, no install step, unsigned/ad-hoc-signed. Asked directly, the user picked (2). This is also the option consistent with a PRIOR explicit decision already on record (see "Why the npm CLI opens a browser tab instead of a real native window" below): that session was asked Electron vs. Tauri vs. CLI+browser specifically to avoid installer/signing overhead, and picked CLI+browser for exactly that reason. Building real signed installers now would have silently reversed that reasoning without a corresponding explicit ask to do so — per WISDOM.md's rule ("a new user ask directly contradicting a prior decision is the current truth"), the fact that THIS ask was answered "standalone binaries," not "real installers," means the prior decision's reasoning still holds and wasn't overridden. Folder name `binaries` (already what the user's own task description used) is the accurate name for what's actually produced. + +## Why the folder structure keeps build output out of git except a README per OS folder +`src/packages/binaries/pptxdiff-{win,mac,linux}/` are build-artifact directories (the actual `.exe`/binary/`assets/`/`.zip`, all generated by `build.mjs`), not source — same category as `dist/` (npm pack output) or `pptxdiff-vscode/dist/` (`.vsix` output), both already gitignored in this repo. Following that existing precedent rather than inventing a new one: gitignore everything build.mjs writes, but keep one tracked `README.md` per OS folder so the folder structure the user asked for (and a description of what will appear there) exists in a fresh clone even before anyone has run a build. + +## Why the binaries reuse `bin/cli.js`'s `startServer()` instead of a separate server implementation +This project already has a documented trap for exactly this mistake: `pptxdiff-vscode/extension.js` once grew its OWN independent copy of `bin/cli.js`'s static file server, which silently fell out of sync when the real one was security-hardened (see WISDOM.md's trap entry and GAP_ANALYSIS.md's "Security hardening" section). `startServer()` was already designed to be reusable (exported specifically so `pptxdiff-cli`'s automation layer could reuse it) — it only needed one small addition, an optional `root` parameter (default unchanged, so `bin/cli.js`'s own direct-run path and every existing caller keep working exactly as before) so the SEA-packaged binary could point it at an `assets/` folder next to the executable instead of the npm package's own `src/pptxdiff` directory. Reusing it here means the packaged binary's path-containment/security-header/loopback-binding behavior can never drift from the already-hardened, already-tested original — a third copy was never on the table. + +## Why the binary resolves its assets relative to `process.execPath`, not embedded via SEA's asset store +Node SEA does support embedding arbitrary binary assets directly into the executable (retrieved at runtime via `require('node:sea').getAsset()`), which would give a genuinely single-file artifact with no separate `assets/` folder to keep alongside it. That was considered and deliberately not used for this first pass: `bin/cli.js`'s static server already does ordinary `fs.readFile()` against a directory — reusing it as-is (only changing what `root` points to) meant zero changes to the actual file-serving logic, versus rewriting it to read from `sea.getAsset()` instead (a different API, and a real behavior change to already-hardened, already-tested code) purely to shave one folder off the download. The shipped shape — binary + `assets/` folder, zipped together as the actual download — still satisfies "no Node.js install required, download and run"; true single-file embedding is a reasonable follow-up if the extra folder turns out to matter in practice, not a requirement of what was asked. + ## Why @pptxdiff/server ships with no authentication rather than a minimal API key CLI_API_DESIGN.md §8 calls for API-key-required-on-non-loopback-bind as part of the design, but implementing even a minimal key check touches real security-sensitive surface (where the key comes from, how it's compared, timing-attack considerations) that deserves its own deliberate pass rather than being bolted on inside a Phase-1 session already covering three other new pieces (automation shim, CLI, server routing). The loopback-by-default bind (matching `bin/cli.js`'s existing precedent) is the one security property that WAS carried over faithfully; the auth gap is real, named explicitly in the package's own README (not just a scroll only this project's own sessions read), and is the literal next thing to build before anyone binds this server to a non-loopback host in practice. diff --git a/docs/.scrolls/HANDOFF.md b/docs/.scrolls/HANDOFF.md index 4b48cfd..e6f2235 100644 --- a/docs/.scrolls/HANDOFF.md +++ b/docs/.scrolls/HANDOFF.md @@ -2,6 +2,18 @@ **Read `.scrolls/SPEC.md` first for the full feature list.** This file is the "what's the state of things right now" note — update it at the end of every session, keep it short and current (prune stale entries). +## Update (2026-08-05 — standalone native binaries for Windows/macOS/Linux, `src/packages/binaries/`) +- Task asked for a "mechanism to create downloadable installers" under `src/packages/binaries/pptxdiff-{win,mac,linux}`, and explicitly asked whether the folder should be named `binaries` or `installers` — flagged this as a real fork (not a naming bikeshed) since it changes scope by an order of magnitude, and asked the user directly via `AskUserQuestion` before building anything: standalone binaries (Node SEA, no install wizard, no signing) vs. true OS installers (`.msi`/`.pkg`/`.deb` with code signing). User picked **standalone binaries** — consistent with a prior session's explicit Electron/Tauri-vs-CLI+browser decision already on record in GAP_CONTEXT.md (picked CLI+browser specifically to avoid installer/signing overhead). +- **Implementation**: `src/packages/binaries/` (new package, `@pptxdiff/binaries`, private). + - `bin/cli.js`'s `startServer()` gained a backward-compatible optional `root = ROOT` parameter (one-line change, every existing caller — `pptxdiff-cli`'s automation layer, `bin/cli.js`'s own direct-run path — unaffected) so a packaged binary can point it at an `assets/` folder next to the executable instead of the npm package's own `__dirname`-relative `src/pptxdiff`. + - `sea-entry.cjs`: the SEA entry point — resolves `root` from `path.dirname(process.execPath)`, requires `../../../bin/cli.js` (resolved by esbuild at BUILD time, not a runtime path lookup), calls the same `startServer()`/`buildBrowserOpenCommand()` bin/cli.js already uses. Zero server-logic duplication — deliberately avoiding the exact mistake `pptxdiff-vscode/extension.js` made once before (WISDOM.md's existing trap entry about that drift). + - `build.mjs`: bundles `sea-entry.cjs` via esbuild → generates the Node SEA blob (`node --experimental-sea-config`) → copies `process.execPath` and injects the blob via `postject` → (macOS only) `codesign --remove-signature` before injection, `codesign --sign -` (ad-hoc) after → copies `index.html`/`support.js`/`sample-pptx.js`/`vendor/` into `assets/` next to the binary → zips binary+assets into `pptxdiff--.zip`. + - `.github/workflows/binaries.yml`: 3-OS CI matrix (`ubuntu-latest`/`macos-latest`/`windows-latest`) — Node SEA has no cross-compile mode, each OS's binary can only be built ON that OS, so this is the actual mechanism that produces all three, not a single build script. Uploads each as a workflow artifact. + - `make pkg.binaries.build` / `npm run build:binary` build for the current host only (local dev convenience). + - Output folders `src/packages/binaries/pptxdiff-{win,mac,linux}/` are gitignored (build artifacts, `.gitignores/user.gitignore` + regenerated `.gitignore`, same treatment as `dist/`) except one tracked `README.md` per OS folder describing what a build produces there. +- **Verified for real on Linux (this sandbox)**: ran `npm install && node build.mjs` in `src/packages/binaries/`, got a real 120MB ELF executable + a 42MB zip. Then actually RAN the packaged binary directly (not `bin/cli.js`) — it printed `pptxdiff running at http://localhost:`, and real `curl` requests confirmed `index.html`/`support.js`/`vendor/react.production.min.js` all served correctly (200, correct content-type) from its own `assets/` folder resolved via `process.execPath`. macOS/Windows code paths (codesign steps, `.exe` naming) are structurally parallel but genuinely unverified until CI runs them — no non-Linux host available in this sandbox. +- **Known, documented gaps** (see GAP_ANALYSIS.md/PLAN.md): unsigned Windows `.exe` / ad-hoc-signed-only macOS binary (no code-signing cert — real ongoing cost, not a code fix); not yet attached to GitHub Releases (workflow artifacts only); ~120MB per binary (SEA embeds the whole Node runtime, inherent to the approach); true single-file binaries via SEA's embedded-asset store (`sea.getAsset()`) not attempted — shipped as binary+`assets/`-folder zipped together instead, to avoid changing `bin/cli.js`'s already-hardened file-serving logic for this first pass. +- Scrolls updated to match: SPEC.md §32 (new), PLAN.md (new "Done this session" entry + 3 new tickets), GAP_ANALYSIS.md (new "Standalone native binaries" section), GAP_CONTEXT.md (three new entries: why standalone-not-installers, why build output stays out of git except a README, why `startServer()` was reused instead of a third server copy, why assets aren't SEA-embedded). ## Update (2026-08-09 — fixed the sync-homebrew-tap.yml `brew-audit` job) - Direct ask: "the brew github actions pipeline ... did not succeed. Fix it." The workflow's first real `workflow_dispatch` run (2026-08-08) failed at the `brew-audit` (macOS) job's `brew audit diff --git a/docs/.scrolls/PLAN.md b/docs/.scrolls/PLAN.md index 0b98ae8..00bd3a3 100644 --- a/docs/.scrolls/PLAN.md +++ b/docs/.scrolls/PLAN.md @@ -391,3 +391,35 @@ shim, not sequentially — that plan is what shipped below. ## Done this session (Red/Green regression test for the Chocolatey package) - [x] **P2 — `test_chocolatey_package.mjs`**: pure-Node static-analysis regression test for `src/packages/pptxdiff-chocolatey/` (no `choco`/`pwsh` needed). 21 assertions covering version-sync across `pptxdiff.nuspec`/root `package.json`/the install script's fallback pin, the nuspec's `nodejs` dependency version, both `.ps1` scripts' npm commands, the cmdlet-argument-mode `+`-concatenation bug staying absent, `tools/LICENSE.txt` staying byte-identical to root `LICENSE`, and required companion files existing. Genuinely demonstrated RED (18/21, 3 real failures) before GREEN (21/21) by temporarily reintroducing a version mismatch and the PowerShell bug, then restoring both. - [x] Ticket 2 above ("Automate version sync") is now PARTIALLY addressed: still a manual bump, but drift is caught automatically by the new test instead of shipping silently — see GAP_ANALYSIS.md's updated entry. +## Done this session (standalone native binaries for Windows/macOS/Linux) +- [x] **P2 — `src/packages/binaries/`: standalone native `pptxdiff` executables via Node SEA.** + Explicit ask, with an explicit up-front choice (asked directly): standalone binaries vs. real + signed OS installers — user picked standalone binaries, consistent with the prior explicit + Electron/Tauri-vs-CLI+browser decision (see GAP_CONTEXT.md). `build.mjs` bundles a small SEA entry + point (reusing `bin/cli.js`'s `startServer()`/`buildBrowserOpenCommand()` — `startServer()` gained + a backward-compatible optional `root` param for this) via esbuild, generates the Node SEA blob, + injects it into a copy of the current `node` binary via `postject`, and copies the static app + files into an `assets/` folder next to it. Output lands in + `src/packages/binaries/pptxdiff-{win,mac,linux}/` (gitignored — build artifacts, one tracked + `README.md` each) as a `pptxdiff--.zip`. `.github/workflows/binaries.yml` runs the + build on a 3-OS CI matrix (SEA has no cross-compile mode — each OS's binary can only be built ON + that OS) and uploads each as a workflow artifact. `make pkg.binaries.build` / `npm run + build:binary` build for the current host only. Verified for real on Linux (this sandbox): built, + ran the actual packaged binary (not just `bin/cli.js`), confirmed it serves `index.html`/ + `support.js`/`vendor/*` correctly via real HTTP requests. macOS/Windows are structurally identical + but unverified until CI runs them (no non-Linux host in this sandbox) — see GAP_ANALYSIS.md. + +## New tickets opened this session +1. **P2 — Attach the built binaries to GitHub Releases**, not just CI workflow artifacts. Needs a + `release: types: [published]`-triggered job (or similar) that re-runs the 3-OS build matrix and + uploads the zips to the release — not built this session, current CI only produces downloadable + workflow artifacts on push/dispatch. +2. **P3 — Code signing for the Windows `.exe` and a real Apple Developer ID for macOS.** Needs a + purchased/managed certificate (real ongoing cost, not a code change) — until then, both binaries + trigger their OS's "unidentified/unsigned" security warning on first run. Documented per-OS in + each `pptxdiff-/README.md`. +3. **P4 — True single-file binaries via Node SEA's embedded-asset store** (`node:sea`'s + `getAsset()`), instead of shipping a `binary + assets/ folder`, zipped together. Would need + `bin/cli.js`'s static server to read from `sea.getAsset()` when running under SEA instead of + `fs.readFile()` — a real behavior change to already-hardened code, deliberately not made for this + first pass (see GAP_CONTEXT.md). diff --git a/docs/.scrolls/SPEC.md b/docs/.scrolls/SPEC.md index 0540754..8db1fdf 100644 --- a/docs/.scrolls/SPEC.md +++ b/docs/.scrolls/SPEC.md @@ -300,3 +300,11 @@ Word-level diff (LCS-based) highlights changed words within text/table-cell/char - **Explicit browser behavior**: `--browser=chrome` opens Google Chrome (`open -a "Google Chrome"` on macOS, `start chrome` on Windows, `google-chrome` on Linux). `--browser=msedge` opens Microsoft Edge (`open -a "Microsoft Edge"` on macOS, `start msedge` on Windows, `microsoft-edge` on Linux). - **Failure behavior**: unsupported browser values or unknown options fail before the local server starts, with exit code `2` and a clear error. If the selected browser command itself is missing or cannot launch in a headless/no-GUI environment, the CLI still prints the local URL and ignores the browser-open failure, preserving the prior "URL is enough to proceed manually" behavior. - **Testing**: `src/pptxdiff/test_execfile_browser_open_cli.mjs` now covers `parseArgs()` for both `--browser=value` and `--browser value`, rejects unsupported values, and verifies all platform/browser command builders still pass the URL as a single `execFile()` argv element rather than shell-interpolating it. +## 36. Standalone native binaries (`@pptxdiff/binaries`, added this session) +- **What it does**: `src/packages/binaries/` builds a standalone, native `pptxdiff` executable per OS — download one artifact, run it, `pptxdiff` opens in the browser. No Node.js install, no `npm install -g`, no `npx`. Output lands in `src/packages/binaries/pptxdiff-win/`, `pptxdiff-mac/`, `pptxdiff-linux/` (the folder names the user requested), each holding the built binary, an `assets/` copy of the served static app files, and a `pptxdiff--.zip` bundling both — the zip is the actual downloadable artifact. Build outputs are gitignored (generated, not source); each OS folder keeps a tracked `README.md` describing what a build produces there. +- **Deliberately standalone binaries, not real OS installers**: asked directly (binaries vs. true `.msi`/`.pkg`/`.deb` installers with an install wizard, PATH registration, code signing) and the user picked standalone binaries — consistent with this project's prior explicit decision (see GAP_CONTEXT.md "Why the npm CLI opens a browser tab instead of a real native window") to avoid Electron/Tauri-style installer and code-signing overhead. See `src/packages/binaries/README.md` for the full reasoning. +- **Mechanism**: Node's [Single Executable Applications (SEA)](https://nodejs.org/api/single-executable-applications.html) feature — `build.mjs` bundles `sea-entry.cjs` (which reuses `bin/cli.js`'s existing `startServer()`/`buildBrowserOpenCommand()`, zero server-logic duplication) via `esbuild` into one flat CommonJS file, generates the SEA blob (`node --experimental-sea-config`), copies `process.execPath` and injects the blob via `postject`, then copies the same static files the npm package ships (`index.html`/`support.js`/`sample-pptx.js`/`vendor/`) into an `assets/` folder next to the binary. `bin/cli.js`'s `startServer(root = ROOT)` gained an optional `root` param (backward-compatible default, every existing caller unaffected) specifically so the SEA entry point can pass `path.dirname(process.execPath)`-relative assets instead of the npm package's own `__dirname`-relative ones, which don't exist inside a packaged single-file binary. +- **No cross-compilation**: SEA builds by injecting into a copy of the *currently running* `node` binary — there is no supported way to build a Windows `.exe` from a Linux machine. `.github/workflows/binaries.yml` runs the same build on a `windows-latest`/`macos-latest`/`ubuntu-latest` CI matrix to actually produce all three, uploaded as workflow artifacts (attaching them to GitHub Releases on a tag push is a documented follow-up, not built this session — see PLAN.md). +- **Known, documented gaps** (see GAP_ANALYSIS.md): unsigned Windows `.exe` (SmartScreen warning) and ad-hoc-signed-only macOS binary (Gatekeeper warning) — no code-signing certificate exists for this project; ~120MB per binary since SEA embeds the entire Node runtime; not yet wired to GitHub Releases. +- **Verified locally** (Linux, this sandbox): built for real (`npm install && node build.mjs`), producing a real ELF executable + `pptxdiff-linux-0.7.0.zip`; ran the built binary directly (not just `bin/cli.js`), confirmed it prints `pptxdiff running at http://localhost:` and correctly serves `index.html`/`support.js`/`vendor/*` from its own `assets/` folder via real `curl` requests. Windows/macOS builds are structurally identical (same `build.mjs`, platform-branched only for the codesign step) but unverified locally — no Windows/macOS host in this sandbox; CI will exercise them on first push. + diff --git a/package.json b/package.json index 96c2fab..7e3457b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ }, "scripts": { "build:npm": "mkdir -p dist && npm pack --pack-destination dist", - "package:vscode": "cd src/packages/pptxdiff-vscode && mkdir -p dist && vsce package --out dist/pptxdiff-vscode-$(node -p \"require('./package.json').version\").vsix" + "package:vscode": "cd src/packages/pptxdiff-vscode && mkdir -p dist && vsce package --out dist/pptxdiff-vscode-$(node -p \"require('./package.json').version\").vsix", + "build:binary": "cd src/packages/binaries && npm install && npm run build" }, "files": [ "bin", diff --git a/src/packages/binaries/README.md b/src/packages/binaries/README.md new file mode 100644 index 0000000..b005f55 --- /dev/null +++ b/src/packages/binaries/README.md @@ -0,0 +1,63 @@ +# @pptxdiff/binaries + +Builds standalone, native `pptxdiff` executables for Windows, macOS, and +Linux — download one file (well, one file plus its `assets/` folder, +zipped together), run it, and pptxdiff opens in your browser. No Node.js +install, no `npm install -g`, no `npx`. + +This is deliberately **not** a real OS installer (no `.msi`/`.pkg`/`.deb` +wizard, no PATH registration, no entry in Add/Remove Programs) — see +`docs/.scrolls/GAP_CONTEXT.md` for why: this project already made an +explicit, reasoned call to avoid Electron/Tauri-style installer and +code-signing overhead when it chose the CLI+browser architecture over a +native-window app, and building real signed installers would mean +reversing that without a corresponding ask. A standalone executable gets +"download and run, no Node.js required" — the actual pain point — without +that cost. + +## How it works + +[Node's Single Executable Applications (SEA)](https://nodejs.org/api/single-executable-applications.html) +feature injects a JS blob into a **copy of the currently-running `node` +binary**. `build.mjs`: + +1. Bundles `sea-entry.cjs` (which reuses `bin/cli.js`'s existing + `startServer()`/`buildBrowserOpenCommand()` — no server logic is + duplicated) into one flat CommonJS file via `esbuild`. +2. Generates the SEA blob (`node --experimental-sea-config`). +3. Copies `process.execPath` and injects the blob via `postject`. +4. Copies the same static app files the npm package ships + (`index.html`/`support.js`/`sample-pptx.js`/`vendor/`) into an + `assets/` folder next to the built binary — `sea-entry.cjs` resolves + `root` from `path.dirname(process.execPath)` at runtime, since a + packaged executable has no `__dirname`-relative sibling files of its + own the way an npm-installed package does. +5. Zips the binary + `assets/` into `pptxdiff--.zip`, the + actual downloadable artifact. + +## Building locally + +```sh +cd src/packages/binaries +npm install +npm run build # or: make pkg.binaries.build, from the repo root +``` + +Output lands in `./pptxdiff-/` — whichever one matches the +OS you ran this on. **SEA has no cross-compilation mode**: this only ever +builds for the platform it's currently running on. To get all three, run +it on all three platforms — `.github/workflows/binaries.yml` does exactly +that via a `windows-latest`/`macos-latest`/`ubuntu-latest` CI matrix and +uploads each as a workflow artifact. + +## Known gaps (see `docs/.scrolls/GAP_ANALYSIS.md`) + +- **Unsigned/ad-hoc-signed.** No code-signing certificate — Windows + SmartScreen and macOS Gatekeeper will warn on a freshly-downloaded copy. + Documented per-OS in each `pptxdiff-/README.md`. +- **Not attached to GitHub Releases yet.** The CI workflow currently only + uploads build artifacts on push/dispatch; wiring a release-tag trigger + to attach the zips to a GitHub Release is a follow-up, not done here. +- **~120MB per binary.** SEA embeds the entire Node runtime — there's no + way around this with the SEA approach itself (it's not a JS-only + bundle-size problem). diff --git a/src/packages/binaries/build.mjs b/src/packages/binaries/build.mjs new file mode 100644 index 0000000..82005d4 --- /dev/null +++ b/src/packages/binaries/build.mjs @@ -0,0 +1,176 @@ +#!/usr/bin/env node +"use strict"; + +// Builds a standalone native pptxdiff executable for the CURRENT host OS +// using Node's Single Executable Applications (SEA) feature, and drops it +// (plus the static app files it serves, plus a zip of both) into +// src/packages/binaries/pptxdiff-/. +// +// Node SEA has no supported cross-platform mode: a SEA binary is built by +// injecting a JS blob into a COPY OF THE CURRENTLY RUNNING node executable +// (process.execPath). Building all three platforms' binaries therefore +// means running this script once per OS — see .github/workflows/binaries.yml +// for a CI matrix (ubuntu-latest/macos-latest/windows-latest) that does +// exactly that. There is no attempt here to fake cross-compilation. +// +// No code-signing certificate is available (or in scope — see +// docs/.scrolls/GAP_CONTEXT.md), so the macOS binary is only ad-hoc signed +// (runs locally, still triggers Gatekeeper's "unidentified developer" +// warning on a freshly-downloaded copy) and the Windows .exe is unsigned +// (triggers a SmartScreen warning). Documented, not silently hidden. + +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import JSZip from "jszip"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); +const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")).version; + +const PLATFORM_MAP = { + win32: { osKey: "win", binName: "pptxdiff-win.exe", isWin: true, isMac: false }, + darwin: { osKey: "mac", binName: "pptxdiff-mac", isWin: false, isMac: true }, + linux: { osKey: "linux", binName: "pptxdiff-linux", isWin: false, isMac: false }, +}; + +const target = PLATFORM_MAP[process.platform]; +if (!target) { + console.error(`No SEA build mapping for process.platform=${process.platform} (supported: win32, darwin, linux).`); + process.exitCode = 1; + process.exit(); +} + +const OUT_DIR = path.join(__dirname, `pptxdiff-${target.osKey}`); +const BUILD_TMP = path.join(__dirname, ".build"); +const ASSETS_OUT = path.join(OUT_DIR, "assets"); +const BIN_OUT = path.join(OUT_DIR, target.binName); + +// Same subset root package.json's "files" ships to npm — the exact set of +// static files bin/cli.js's server actually reads from ROOT. +const ASSET_ENTRIES = [ + ["src/pptxdiff/index.html", "index.html"], + ["src/pptxdiff/support.js", "support.js"], + ["src/pptxdiff/sample-pptx.js", "sample-pptx.js"], + ["src/pptxdiff/vendor", "vendor"], +]; + +function log(msg) { + console.log(`[build-binary:${target.osKey}] ${msg}`); +} + +function run(cmd, args, opts = {}) { + log(`$ ${cmd} ${args.join(" ")}`); + execFileSync(cmd, args, { stdio: "inherit", ...opts }); +} + +function cleanDir(dir) { + fs.rmSync(dir, { recursive: true, force: true }); + fs.mkdirSync(dir, { recursive: true }); +} + +async function zipDir(dir, outZipPath) { + const zip = new JSZip(); + const walk = (abs, rel) => { + for (const entry of fs.readdirSync(abs, { withFileTypes: true })) { + const absChild = path.join(abs, entry.name); + const relChild = rel ? `${rel}/${entry.name}` : entry.name; + if (entry.isDirectory()) walk(absChild, relChild); + else zip.file(relChild, fs.readFileSync(absChild)); + } + }; + walk(dir, ""); + const buf = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" }); + fs.writeFileSync(outZipPath, buf); +} + +async function main() { + log(`Building for ${process.platform} -> ${OUT_DIR}`); + cleanDir(OUT_DIR); + cleanDir(BUILD_TMP); + + // 1. Bundle sea-entry.cjs (which itself inlines bin/cli.js's exports) into + // a single flat CommonJS file — SEA's `main` must be one self-contained + // file; it does not resolve a script's own `require("./other-file")` + // calls at runtime. + const esbuild = await import("esbuild"); + const bundlePath = path.join(BUILD_TMP, "bundle.cjs"); + await esbuild.build({ + entryPoints: [path.join(__dirname, "sea-entry.cjs")], + outfile: bundlePath, + bundle: true, + platform: "node", + format: "cjs", + target: "node20", + }); + + // 2. Generate the SEA config + blob. + const seaConfigPath = path.join(BUILD_TMP, "sea-config.json"); + const blobPath = path.join(BUILD_TMP, "sea-prep.blob"); + fs.writeFileSync( + seaConfigPath, + JSON.stringify( + { + main: bundlePath, + output: blobPath, + disableExperimentalSEAWarning: true, + }, + null, + 2 + ) + ); + run(process.execPath, ["--experimental-sea-config", seaConfigPath]); + + // 3. Copy the currently-running node executable as the base, then inject + // the blob into it. + fs.copyFileSync(process.execPath, BIN_OUT); + fs.chmodSync(BIN_OUT, 0o755); + + if (target.isMac) { + // Required by Node's SEA guide: an existing signature on the copied + // node binary must be removed before injecting, or postject's write + // corrupts it. + run("codesign", ["--remove-signature", BIN_OUT]); + } + + run("npx", [ + "--no-install", + "postject", + BIN_OUT, + "NODE_SEA_BLOB", + blobPath, + "--sentinel-fuse", + "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2", + ...(target.isMac ? ["--macho-segment-name", "NODE_SEA"] : []), + ]); + + if (target.isMac) { + // Ad-hoc signature (no cert) so the binary can run locally at all; + // Gatekeeper still warns on a freshly-downloaded copy — see the file + // header comment and GAP_ANALYSIS.md. + run("codesign", ["--sign", "-", BIN_OUT]); + } + if (!target.isWin) fs.chmodSync(BIN_OUT, 0o755); + + // 4. Copy the static app assets the server reads from `root`. + fs.mkdirSync(ASSETS_OUT, { recursive: true }); + for (const [srcRel, destRel] of ASSET_ENTRIES) { + const src = path.join(REPO_ROOT, srcRel); + const dest = path.join(ASSETS_OUT, destRel); + fs.cpSync(src, dest, { recursive: true }); + } + + // 5. Zip the binary + assets together as the actual downloadable artifact. + const zipPath = path.join(OUT_DIR, `pptxdiff-${target.osKey}-${PKG_VERSION}.zip`); + await zipDir(OUT_DIR, zipPath); + + fs.rmSync(BUILD_TMP, { recursive: true, force: true }); + log(`Done: ${BIN_OUT}`); + log(`Done: ${zipPath}`); +} + +main().catch((e) => { + console.error(e); + process.exitCode = 1; +}); diff --git a/src/packages/binaries/package-lock.json b/src/packages/binaries/package-lock.json new file mode 100644 index 0000000..9b7f340 --- /dev/null +++ b/src/packages/binaries/package-lock.json @@ -0,0 +1,625 @@ +{ + "name": "@pptxdiff/binaries", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@pptxdiff/binaries", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "esbuild": "^0.24.0", + "jszip": "^3.10.1", + "postject": "^1.0.0-alpha.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/src/packages/binaries/package.json b/src/packages/binaries/package.json new file mode 100644 index 0000000..bcac013 --- /dev/null +++ b/src/packages/binaries/package.json @@ -0,0 +1,37 @@ +{ + "name": "@pptxdiff/binaries", + "version": "0.1.0", + "description": "Build script producing standalone native pptxdiff executables (Node Single Executable Applications) for Windows, macOS, and Linux — no separate Node.js install required to run them.", + "license": "Apache-2.0", + "private": true, + "author": { + "name": "Sugato Ray", + "email": "sugatoray.dev@gmail.com", + "url": "https://github.com/sugatoray" + }, + "scripts": { + "build": "node build.mjs" + }, + "devDependencies": { + "esbuild": "^0.24.0", + "jszip": "^3.10.1", + "postject": "^1.0.0-alpha.6" + }, + "engines": { + "node": ">=20" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sugatoray/pptxdiff.git", + "directory": "src/packages/binaries" + }, + "homepage": "https://github.com/sugatoray/pptxdiff#readme", + "keywords": [ + "pptx", + "powerpoint", + "diff", + "sea", + "single-executable-application", + "native-binary" + ] +} diff --git a/src/packages/binaries/pptxdiff-linux/README.md b/src/packages/binaries/pptxdiff-linux/README.md new file mode 100644 index 0000000..451c34d --- /dev/null +++ b/src/packages/binaries/pptxdiff-linux/README.md @@ -0,0 +1,23 @@ +# pptxdiff for Linux + +This folder holds the built Linux artifact — not committed here, generated +by `../build.mjs` (run on a Linux host or Linux CI runner; see +`../README.md`). + +After a build, this folder contains: + +- `pptxdiff-linux` — the standalone executable (bundles the Node runtime; + no separate Node.js install needed to run it). +- `assets/` — the static app files it serves (must stay next to the + binary). +- `pptxdiff-linux-.zip` — the two above, zipped, as the actual + downloadable artifact. + +Run it with `chmod +x pptxdiff-linux && ./pptxdiff-linux` (the build +already sets the executable bit; re-set it if you unzipped the artifact +somewhere that dropped it). + +To build: `cd src/packages/binaries && npm install && npm run build` (from +a Linux machine — Node's Single Executable Applications feature builds +from the currently-running platform's own Node binary, it doesn't +cross-compile). diff --git a/src/packages/binaries/pptxdiff-mac/README.md b/src/packages/binaries/pptxdiff-mac/README.md new file mode 100644 index 0000000..42b4601 --- /dev/null +++ b/src/packages/binaries/pptxdiff-mac/README.md @@ -0,0 +1,26 @@ +# pptxdiff for macOS + +This folder holds the built macOS artifact — not committed here, generated +by `../build.mjs` (run on a macOS host or macOS CI runner; see +`../README.md`). + +After a build, this folder contains: + +- `pptxdiff-mac` — the standalone executable (bundles the Node runtime; no + separate Node.js install needed to run it), ad-hoc signed. +- `assets/` — the static app files it serves (must stay next to the binary). +- `pptxdiff-mac-.zip` — the two above, zipped, as the actual + downloadable artifact. + +**Ad-hoc signed, not notarized.** There is no Apple Developer ID +certificate for this project, so Gatekeeper will likely block a +freshly-downloaded copy on first launch ("cannot be opened because the +developer cannot be verified") — right-click the binary → Open, or run +`xattr -d com.apple.quarantine pptxdiff-mac` first. See +`docs/.scrolls/GAP_ANALYSIS.md` for why this is a documented, accepted +tradeoff rather than an oversight. + +To build: `cd src/packages/binaries && npm install && npm run build` (from +a macOS machine — Node's Single Executable Applications feature builds +from the currently-running platform's own Node binary, it doesn't +cross-compile). diff --git a/src/packages/binaries/pptxdiff-win/README.md b/src/packages/binaries/pptxdiff-win/README.md new file mode 100644 index 0000000..22f5281 --- /dev/null +++ b/src/packages/binaries/pptxdiff-win/README.md @@ -0,0 +1,24 @@ +# pptxdiff for Windows + +This folder holds the built Windows artifact — not committed here, generated +by `../build.mjs` (run on a Windows host or Windows CI runner; see +`../README.md`). + +After a build, this folder contains: + +- `pptxdiff-win.exe` — the standalone executable (bundles the Node runtime; + no separate Node.js install needed to run it). +- `assets/` — the static app files it serves (must stay next to the `.exe`). +- `pptxdiff-win-.zip` — the two above, zipped, as the actual + downloadable artifact. + +**Unsigned.** There is no code-signing certificate for this project, so +Windows SmartScreen will likely warn on first run ("Windows protected your +PC") — click "More info" → "Run anyway". See +`docs/.scrolls/GAP_ANALYSIS.md` for why this is a documented, accepted +tradeoff rather than an oversight. + +To build: `cd src/packages/binaries && npm install && npm run build` (from +a Windows machine — Node's Single Executable Applications feature builds +from the currently-running platform's own Node binary, it doesn't +cross-compile). diff --git a/src/packages/binaries/sea-entry.cjs b/src/packages/binaries/sea-entry.cjs new file mode 100644 index 0000000..b24ef7b --- /dev/null +++ b/src/packages/binaries/sea-entry.cjs @@ -0,0 +1,39 @@ +"use strict"; + +// Entry point bundled (via esbuild) into a single CommonJS file and then +// embedded into a copy of the Node executable via Node's Single Executable +// Applications feature (`--experimental-sea-config` + `postject`) — see +// build.mjs. Requiring "../../../bin/cli.js" is resolved by esbuild at +// BUILD time (it inlines the file's contents into the bundle); nothing in +// the packaged binary does a runtime `require()` of a path outside itself. +const path = require("node:path"); +const { execFile } = require("node:child_process"); +const { startServer, buildBrowserOpenCommand } = require("../../../bin/cli.js"); + +// A packaged single-executable binary has no meaningful sibling files of +// its own (bin/cli.js's module-level `ROOT`, computed from its *build-time* +// `__dirname`, is unused here on purpose — see startServer()'s `root` +// param). build.mjs copies this project's static app files into an +// "assets" folder placed next to the built executable; resolving from +// `process.execPath` (where THIS binary actually lives on disk right now) +// is the only location that's true regardless of where a user unzips it. +const ROOT = path.join(path.dirname(process.execPath), "assets"); + +const LITE_MODE = ["1", "y", "yes", "true"].includes( + String(process.env.PPTXDIFF_LITE_MODE || "").trim().toLowerCase() +); + +startServer(ROOT) + .then(({ url: baseUrl }) => { + const url = `${baseUrl}${LITE_MODE ? "/?lite=1" : ""}`; + console.log(`pptxdiff running at ${url}`); + if (LITE_MODE) { + console.log("PPTXDIFF_LITE_MODE is set — loading React/ReactDOM/Babel/JSZip/pptx-renderer/fonts from their original CDNs instead of the vendored local copies."); + } + const { command, args } = buildBrowserOpenCommand(process.platform, url); + execFile(command, args, () => {}); // ignore failure (e.g. headless/no GUI) — URL is printed above regardless + }) + .catch((e) => { + console.error(e && e.message ? e.message : e); + process.exitCode = 1; + }); From 15ead526584fcf20fcbe7d4f6c1518f0e8971c4b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 04:44:19 +0000 Subject: [PATCH 2/7] Add Red/Green TDD tests and per-OS CHANGELOG.md for binaries build.mjs gains an entrypoint guard (matching capture_screenshots.mjs's existing pattern) exporting PLATFORM_MAP/ASSET_ENTRIES/resolveTarget/ buildBinary for testability. - test_build_config.mjs (fast, pure, `npm test`): 17 assertions, including a static regression guard on bin/cli.js's startServer(root = ROOT) signature. Demonstrated genuine RED->GREEN by temporarily reverting that signature and confirming the test catches it. - test_build_e2e.mjs (slow, real, `npm run test:e2e`): builds and runs the actual packaged binary, verifying real HTTP responses and path-traversal rejection. 11/11 against a real build in this sandbox. - Fixed a real bug found while writing the e2e test: the "clean the output dir" step was a blind rm -rf that would have deleted each OS folder's tracked README.md/CHANGELOG.md on every build. Replaced with a targeted cleanup that only removes generated entries; verified by running the real build twice and confirming the docs survive both times. - Added CHANGELOG.md to each pptxdiff-{win,mac,linux} folder and filled in the root CHANGELOG.md's previously-empty [Unreleased] section. - .github/workflows/binaries.yml now runs both test suites before building each OS's release artifact. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BxwMTp6RQJ6j6K5K8Jjdpm --- .github/workflows/binaries.yml | 4 + .gitignore | 9 +- .gitignores/user.gitignore | 9 +- CHANGELOG.md | 5 + docs/.scrolls/HANDOFF.md | 10 ++ docs/.scrolls/PLAN.md | 21 +++ docs/.scrolls/SPEC.md | 3 + docs/.scrolls/WISDOM.md | 4 + src/packages/binaries/README.md | 24 +++ src/packages/binaries/build.mjs | 122 ++++++++----- src/packages/binaries/package.json | 4 +- .../binaries/pptxdiff-linux/CHANGELOG.md | 32 ++++ .../binaries/pptxdiff-mac/CHANGELOG.md | 32 ++++ .../binaries/pptxdiff-win/CHANGELOG.md | 31 ++++ src/packages/binaries/test_build_config.mjs | 106 ++++++++++++ src/packages/binaries/test_build_e2e.mjs | 163 ++++++++++++++++++ 16 files changed, 527 insertions(+), 52 deletions(-) create mode 100644 src/packages/binaries/pptxdiff-linux/CHANGELOG.md create mode 100644 src/packages/binaries/pptxdiff-mac/CHANGELOG.md create mode 100644 src/packages/binaries/pptxdiff-win/CHANGELOG.md create mode 100644 src/packages/binaries/test_build_config.mjs create mode 100644 src/packages/binaries/test_build_e2e.mjs diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 7936cf7..3a688ba 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -33,6 +33,10 @@ jobs: node-version: "22" - run: npm install working-directory: src/packages/binaries + - run: npm test + working-directory: src/packages/binaries + - run: npm run test:e2e + working-directory: src/packages/binaries - run: npm run build working-directory: src/packages/binaries - uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 6cf4ef3..4a316d7 100644 --- a/.gitignore +++ b/.gitignore @@ -388,13 +388,16 @@ src/pptxdiff/docs-site/site/ ## Native-binary build output (src/packages/binaries/build.mjs) — the ## executable, the copied "assets" folder, and the zipped artifact are all -## generated per-OS, not source; keep each OS folder's own README.md tracked -## (it documents the folder before a build has ever run there) but ignore -## everything else build.mjs writes into it. +## generated per-OS, not source; keep each OS folder's own README.md and +## CHANGELOG.md tracked (they document the folder even before a build has +## ever run there) but ignore everything else build.mjs writes into it. src/packages/binaries/pptxdiff-win/* src/packages/binaries/pptxdiff-mac/* src/packages/binaries/pptxdiff-linux/* !src/packages/binaries/pptxdiff-win/README.md !src/packages/binaries/pptxdiff-mac/README.md !src/packages/binaries/pptxdiff-linux/README.md +!src/packages/binaries/pptxdiff-win/CHANGELOG.md +!src/packages/binaries/pptxdiff-mac/CHANGELOG.md +!src/packages/binaries/pptxdiff-linux/CHANGELOG.md src/packages/binaries/.build/ diff --git a/.gitignores/user.gitignore b/.gitignores/user.gitignore index 38a243f..56cec48 100644 --- a/.gitignores/user.gitignore +++ b/.gitignores/user.gitignore @@ -32,13 +32,16 @@ src/pptxdiff/docs-site/site/ ## Native-binary build output (src/packages/binaries/build.mjs) — the ## executable, the copied "assets" folder, and the zipped artifact are all -## generated per-OS, not source; keep each OS folder's own README.md tracked -## (it documents the folder before a build has ever run there) but ignore -## everything else build.mjs writes into it. +## generated per-OS, not source; keep each OS folder's own README.md and +## CHANGELOG.md tracked (they document the folder even before a build has +## ever run there) but ignore everything else build.mjs writes into it. src/packages/binaries/pptxdiff-win/* src/packages/binaries/pptxdiff-mac/* src/packages/binaries/pptxdiff-linux/* !src/packages/binaries/pptxdiff-win/README.md !src/packages/binaries/pptxdiff-mac/README.md !src/packages/binaries/pptxdiff-linux/README.md +!src/packages/binaries/pptxdiff-win/CHANGELOG.md +!src/packages/binaries/pptxdiff-mac/CHANGELOG.md +!src/packages/binaries/pptxdiff-linux/CHANGELOG.md src/packages/binaries/.build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 84c4303..221868b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 state to the tap even without a version-pin change, while a scheduled run still only does real work on an actual version bump. `test_formula.mjs` now also asserts `LICENSE` stays byte-identical to the repo root's copy, catching drift instead of silently shipping a stale license to the tap. +- New private `@pptxdiff/binaries` package (`src/packages/binaries/`) building standalone native `pptxdiff` executables for Windows, macOS, and Linux via Node's Single Executable Applications feature — download one artifact and run it, no separate Node.js install required. +- Per-OS build output folders `src/packages/binaries/pptxdiff-{win,mac,linux}/`, each with its own `README.md` and `CHANGELOG.md`. +- `.github/workflows/binaries.yml`: a 3-OS CI matrix building all three binaries (Node SEA has no cross-compile mode, so each OS's binary is built on that OS). +- `make pkg.binaries.build` / `npm run build:binary` for local single-OS builds. +- `bin/cli.js`'s `startServer()` gained a backward-compatible optional `root` parameter so the packaged binaries can serve static assets from next to themselves. ## [0.7.0] - 2026-08-02 diff --git a/docs/.scrolls/HANDOFF.md b/docs/.scrolls/HANDOFF.md index e6f2235..7b72357 100644 --- a/docs/.scrolls/HANDOFF.md +++ b/docs/.scrolls/HANDOFF.md @@ -2,6 +2,16 @@ **Read `.scrolls/SPEC.md` first for the full feature list.** This file is the "what's the state of things right now" note — update it at the end of every session, keep it short and current (prune stale entries). +## Update (2026-08-05 — binaries follow-up: Red/Green TDD + per-OS CHANGELOG.md) +- Direct follow-up ask on the same-day binaries work below: "use Red/Green TDD and update documentation and add a CHANGELOG.md for each os specific folder." +- **`build.mjs` refactored for testability**: added the same entrypoint guard `capture_screenshots.mjs` already established (`process.argv[1] === fileURLToPath(import.meta.url)`) so `PLATFORM_MAP`/`ASSET_ENTRIES`/`resolveTarget()`/`buildBinary(target)` are now named exports importable without a real build running as a side effect. +- **`test_build_config.mjs`** (fast/pure, wired as `npm test`): 17 assertions — `PLATFORM_MAP` per-OS shape, an `ASSET_ENTRIES`-vs-root-`package.json`-"files" drift guard (mirrors the project's existing fixture-drift-check concern), and a static-source regression check that `bin/cli.js`'s `startServer()` still has the `root = ROOT` optional param this entire feature depends on. **Demonstrated genuine RED→GREEN**: temporarily reverted that exact signature back to the old no-param form, ran the test, confirmed exactly the one dependent assertion failed (16/17 — every other check stayed green), restored it via the pre-edit backup, confirmed `bin/cli.js` was byte-identical to HEAD (`git diff` empty) and 17/17 passed again. +- **`test_build_e2e.mjs`** (slow/real, wired as `npm run test:e2e`, current-platform only — same split rationale as `pptxdiff-cli`'s `test:difftool`): calls the real `buildBinary()`, then spawns the ACTUAL resulting executable and drives it over real HTTP (`GET /`, `/support.js`, `/vendor/react.production.min.js` — all 200 with correct bodies/content-types — plus a path-traversal request confirming `isPathContained` still holds for this feature's different `root` value). Ran for real in this sandbox: built a genuine ~120MB Linux binary, launched it, got 11/11 GREEN. +- **Real bug found and fixed while writing the e2e test, before it ever touched committed files**: both `build.mjs`'s "clean the output directory before building" step AND the e2e test's own post-run cleanup were a blind `rm -rf ` — fine when that directory held only generated output, but it's the SAME directory as each OS's tracked `README.md` (and now `CHANGELOG.md`). Fixed both with a `cleanGeneratedOutDir()`/equivalent that removes only the specific known-generated entries (binary by exact name, `assets/` folder, `*.zip` files) rather than the whole directory. Verified the fix for real: ran the actual build twice in a row and confirmed `README.md`/`CHANGELOG.md` survived both times. New WISDOM.md trap entry recorded so a future generator touching a mixed generated/tracked-content directory doesn't repeat this. +- **`src/packages/binaries/pptxdiff-{win,mac,linux}/CHANGELOG.md`** added (Keep a Changelog format, un-ignored in `.gitignores/user.gitignore` + regenerated `.gitignore` the same way `README.md` already was) — tracks the bundled `pptxdiff` app version per OS folder, since the binary itself has no independent feature set to version separately. +- **Root `CHANGELOG.md`**: filled in the previously-empty `[Unreleased]` placeholder with this whole binaries feature (both this update and the earlier same-day one). +- Scrolls updated to match: SPEC.md §32 (testing subsection added), PLAN.md (new "Done this session" bullet + a 4th ticket), WISDOM.md (new "clean the output dir" trap). + ## Update (2026-08-05 — standalone native binaries for Windows/macOS/Linux, `src/packages/binaries/`) - Task asked for a "mechanism to create downloadable installers" under `src/packages/binaries/pptxdiff-{win,mac,linux}`, and explicitly asked whether the folder should be named `binaries` or `installers` — flagged this as a real fork (not a naming bikeshed) since it changes scope by an order of magnitude, and asked the user directly via `AskUserQuestion` before building anything: standalone binaries (Node SEA, no install wizard, no signing) vs. true OS installers (`.msi`/`.pkg`/`.deb` with code signing). User picked **standalone binaries** — consistent with a prior session's explicit Electron/Tauri-vs-CLI+browser decision already on record in GAP_CONTEXT.md (picked CLI+browser specifically to avoid installer/signing overhead). - **Implementation**: `src/packages/binaries/` (new package, `@pptxdiff/binaries`, private). diff --git a/docs/.scrolls/PLAN.md b/docs/.scrolls/PLAN.md index 00bd3a3..d95897a 100644 --- a/docs/.scrolls/PLAN.md +++ b/docs/.scrolls/PLAN.md @@ -408,6 +408,23 @@ shim, not sequentially — that plan is what shipped below. ran the actual packaged binary (not just `bin/cli.js`), confirmed it serves `index.html`/ `support.js`/`vendor/*` correctly via real HTTP requests. macOS/Windows are structurally identical but unverified until CI runs them (no non-Linux host in this sandbox) — see GAP_ANALYSIS.md. +- [x] **P2 — Red/Green TDD for the binaries build (explicit follow-up ask), plus CHANGELOG.md per OS + folder.** `build.mjs` refactored with an entrypoint guard (same pattern as `capture_screenshots.mjs`) + exporting `PLATFORM_MAP`/`ASSET_ENTRIES`/`resolveTarget`/`buildBinary` for testability. + `test_build_config.mjs` (fast/pure, `npm test`): 17 assertions including a static-source regression + guard on `bin/cli.js`'s `startServer(root = ROOT)` signature — demonstrated genuine RED→GREEN by + temporarily reverting that exact signature, confirming the one dependent assertion failed (16/17), + restoring it, confirming 17/17. `test_build_e2e.mjs` (slow/real, `npm run test:e2e`, current-platform + only): actually builds and RUNS the real packaged binary, 11/11 assertions against real HTTP + responses (including a path-traversal check against this feature's different `root` value). Caught + and fixed a real bug while writing the e2e test: `build.mjs`'s (and the test's own) "clean the + output dir" step was a blind `rm -rf` that would have deleted the tracked `README.md`/`CHANGELOG.md` + living in the same per-OS folder on every build — fixed with a `cleanGeneratedOutDir()` that removes + only the specific generated entries; verified by running the real build twice in a row and + confirming both docs files survive. `src/packages/binaries/pptxdiff-{win,mac,linux}/CHANGELOG.md` + added (Keep a Changelog format, tracks the bundled `pptxdiff` app version). Root `CHANGELOG.md` + `[Unreleased]` section filled in for this whole feature (previously an empty placeholder). See + SPEC.md §32, WISDOM.md's new "clean the output dir" trap entry. ## New tickets opened this session 1. **P2 — Attach the built binaries to GitHub Releases**, not just CI workflow artifacts. Needs a @@ -423,3 +440,7 @@ shim, not sequentially — that plan is what shipped below. `bin/cli.js`'s static server to read from `sea.getAsset()` when running under SEA instead of `fs.readFile()` — a real behavior change to already-hardened code, deliberately not made for this first pass (see GAP_CONTEXT.md). +4. **P4 — `test_build_e2e.mjs` only exercises the CURRENT host's platform branch.** The macOS/Windows + `buildBinary()` branches (codesign steps, `.exe` naming) are covered by `test_build_config.mjs`'s + static checks but not by a real build+run — that only happens via CI's 3-OS matrix. Not a gap in + this session's TDD work so much as an inherent constraint of Node SEA itself (see GAP_ANALYSIS.md). diff --git a/docs/.scrolls/SPEC.md b/docs/.scrolls/SPEC.md index 8db1fdf..e951a19 100644 --- a/docs/.scrolls/SPEC.md +++ b/docs/.scrolls/SPEC.md @@ -307,4 +307,7 @@ Word-level diff (LCS-based) highlights changed words within text/table-cell/char - **No cross-compilation**: SEA builds by injecting into a copy of the *currently running* `node` binary — there is no supported way to build a Windows `.exe` from a Linux machine. `.github/workflows/binaries.yml` runs the same build on a `windows-latest`/`macos-latest`/`ubuntu-latest` CI matrix to actually produce all three, uploaded as workflow artifacts (attaching them to GitHub Releases on a tag push is a documented follow-up, not built this session — see PLAN.md). - **Known, documented gaps** (see GAP_ANALYSIS.md): unsigned Windows `.exe` (SmartScreen warning) and ad-hoc-signed-only macOS binary (Gatekeeper warning) — no code-signing certificate exists for this project; ~120MB per binary since SEA embeds the entire Node runtime; not yet wired to GitHub Releases. - **Verified locally** (Linux, this sandbox): built for real (`npm install && node build.mjs`), producing a real ELF executable + `pptxdiff-linux-0.7.0.zip`; ran the built binary directly (not just `bin/cli.js`), confirmed it prints `pptxdiff running at http://localhost:` and correctly serves `index.html`/`support.js`/`vendor/*` from its own `assets/` folder via real `curl` requests. Windows/macOS builds are structurally identical (same `build.mjs`, platform-branched only for the codesign step) but unverified locally — no Windows/macOS host in this sandbox; CI will exercise them on first push. +- **Red/Green TDD, two test files** (`build.mjs` refactored with an entrypoint guard — same pattern as `capture_screenshots.mjs`, see WISDOM.md — so `PLATFORM_MAP`/`ASSET_ENTRIES`/`resolveTarget`/`buildBinary` are importable without a real build running as a side effect): + - `test_build_config.mjs` (fast, pure, no subprocess/network, part of `npm test`): 17 assertions covering `PLATFORM_MAP`'s per-OS shape, an `ASSET_ENTRIES`-vs-root-`package.json`-"files" drift guard, and — the sharpest one — a static-source regression check that `bin/cli.js`'s `startServer()` still accepts the optional `root` param this whole feature depends on. Demonstrated genuine RED→GREEN: temporarily reverted `startServer(root = ROOT)` back to the old no-param signature, confirmed exactly that one assertion failed (16/17), restored it, confirmed 17/17. + - `test_build_e2e.mjs` (slow, real, current-platform-only, separate `npm run test:e2e` script — same split as `pptxdiff-cli`'s `test:difftool`): actually calls `buildBinary()`, then spawns the REAL resulting executable and drives it over real HTTP — `GET /`, `/support.js`, `/vendor/react.production.min.js` all 200 with correct content, plus a path-traversal request confirming `isPathContained` still applies correctly to this feature's different `root` value (not just assumed because it's "the same function"). 11/11 assertions, genuinely GREEN against a real ~120MB binary built and run in this sandbox. Cleans up only what it generated (binary/`assets/`/zip) afterward, never the tracked `README.md`/`CHANGELOG.md` in the same folder — a real bug in the first draft (a blind `rm -rf` of the whole output folder, which would have deleted the tracked docs on every build) was caught and fixed before it ever ran against committed files. diff --git a/docs/.scrolls/WISDOM.md b/docs/.scrolls/WISDOM.md index b638c81..7d8438d 100644 --- a/docs/.scrolls/WISDOM.md +++ b/docs/.scrolls/WISDOM.md @@ -152,6 +152,10 @@ - The `capture_screenshots.mjs` ESM entrypoint guard (`if (process.argv[1] === fileURLToPath(import.meta.url))`) has a direct CommonJS equivalent: `if (require.main === module) { ... }`. Used this to make `bin/cli.js`'s pure `buildBrowserOpenCommand(platform, url)` helper importable/unit-testable without the side effect of actually starting the real static server — when a CJS file is loaded via a dynamic ESM `import()` from a test script (rather than run directly as `node bin/cli.js`), `require.main` is `undefined` inside it (there's no CJS main module when the process entrypoint is ESM), so the guard correctly skips the server-start/browser-open block. Same underlying principle as the ESM case: a self-executing script becomes testable by guarding its own execution, not by splitting into a separate always-imported library file. - When exec-vs-execFile matters for shell-injection hardening, the strongest test isn't just "does the source contain the word execFile" (a static grep) — it's asserting the pure command-builder passes a shell-metacharacter-laden string through as ONE untouched argv array element (proving no shell interpolation is possible), on every platform branch, including the Windows one (`cmd.exe /c start "" ` — `start` is a cmd.exe built-in, not a standalone executable, so it must be routed through `cmd.exe` rather than `execFile("start", ...)` directly). +## Wisdom — a build script's "clean the output dir" step can eat tracked files sitting in that same dir (addendum, `@pptxdiff/binaries` session) +- **`rm -rf ` followed by `mkdir` is only safe if EVERYTHING in that directory is generated.** `src/packages/binaries/build.mjs`'s first draft blindly wiped `pptxdiff-/` before every build — fine when the directory held only build output, but it was also the same directory holding that OS's tracked `README.md`/`CHANGELOG.md`. The bug was caught before it ever ran against the committed files (found while writing this session's e2e test, which needed to assert those docs survive a real build) — the fix was a `cleanGeneratedOutDir()` that removes only the specific known-generated entries (the binary by exact name, the `assets/` folder, any `*.zip`) rather than nuking the whole directory. **Any time a build/generator script's output directory is ALSO where hand-written, tracked files live (a README describing the folder, a CHANGELOG, a `.gitkeep`), a full-directory wipe is the wrong tool — enumerate and remove only what the script itself produces.** Same root mistake class as `test_build_e2e.mjs`'s own cleanup step, which had to be fixed the identical way for the identical reason. +- **Test the "does cleanup preserve what it should" property for real, not just "does the build succeed."** The e2e test ran the real build TWICE in a row specifically to prove `README.md`/`CHANGELOG.md` survive repeated builds (not just present after one run, which wouldn't catch a wipe-then-never-restore bug) — a cheap, high-value check once the fix was in place, worth doing any time a generator's repeated-run idempotency touches a directory with mixed generated/tracked content. + ## Wisdom — verify a "known weakness" is actually reachable through the specific call site before testing for it (addendum, security-hardening session) - `path.normalize()` on an already-absolute string (leading `/`) clamps `..` at the root — it can't produce a result that still starts with `..`. `path.join(root, laterArg)` does NOT treat a leading `/` in `laterArg` as an absolute-path reset (unlike `path.resolve()`) — it just concatenates segments. Combined, `path.join(ROOT, path.normalize(reqPath))` in `bin/cli.js`'s request handler already prevented `../`-style HTTP traversal from escaping `ROOT` *by construction*, before the containment check (`startsWith(ROOT)` or its `path.relative()`-based replacement) even runs — confirmed with a plain `node -e` trace comparing several traversal payloads against both forms. - This mattered concretely when writing `test_path_containment_cli.mjs` for the `isPathContained()` hardening (P0 ticket 3): an HTTP-level test asserting the old code returned `403` for `../../../etc/passwd` was simply wrong — it returned `404` (the resolved-but-still-contained path just doesn't exist), both before and after the fix. The actual, meaningful `startsWith()`-vs-`path.relative()` divergence (a sibling directory sharing `ROOT`'s prefix, e.g. `/app/pptxdiff-evil`) isn't reachable through this call site's `path.join(ROOT, ...)` construction at all — a `path.join` can't literally produce a `ROOT + "-evil"` string, since it always inserts a separator. That case is only testable by calling the pure containment function directly with a crafted sibling path. diff --git a/src/packages/binaries/README.md b/src/packages/binaries/README.md index b005f55..3c14344 100644 --- a/src/packages/binaries/README.md +++ b/src/packages/binaries/README.md @@ -50,6 +50,30 @@ it on all three platforms — `.github/workflows/binaries.yml` does exactly that via a `windows-latest`/`macos-latest`/`ubuntu-latest` CI matrix and uploads each as a workflow artifact. +Each OS folder keeps a tracked `README.md` (usage/known-warnings) and +`CHANGELOG.md` (Keep a Changelog, tracks the bundled `pptxdiff` app +version) — `build.mjs` only ever removes the specific files/folders it +itself generates (the binary, `assets/`, `*.zip`), never those two, even +across repeated builds. + +## Testing (Red/Green TDD) + +```sh +npm test # fast, pure — PLATFORM_MAP/ASSET_ENTRIES/resolveTarget shape, + # an ASSET_ENTRIES-vs-root-package.json drift guard, and a + # regression guard on bin/cli.js's startServer(root = ROOT) + # signature this whole feature depends on +npm run test:e2e # slow, real — builds an actual binary for the CURRENT host + # OS and drives it over real HTTP (index.html/support.js/ + # vendor/* + a path-traversal check), same split as + # pptxdiff-cli's `npm test` vs `npm run test:difftool` +``` + +`test:e2e` only exercises the current host's platform branch — the other +two OS branches are structurally identical (same `build.mjs`, only the +codesign step differs) but only actually built-and-run by CI's 3-OS +matrix. + ## Known gaps (see `docs/.scrolls/GAP_ANALYSIS.md`) - **Unsigned/ad-hoc-signed.** No code-signing certificate — Windows diff --git a/src/packages/binaries/build.mjs b/src/packages/binaries/build.mjs index 82005d4..9a00a75 100644 --- a/src/packages/binaries/build.mjs +++ b/src/packages/binaries/build.mjs @@ -18,6 +18,14 @@ // (runs locally, still triggers Gatekeeper's "unidentified developer" // warning on a freshly-downloaded copy) and the Windows .exe is unsigned // (triggers a SmartScreen warning). Documented, not silently hidden. +// +// PLATFORM_MAP/ASSET_ENTRIES/resolveTarget are exported (pure, no side +// effects) so test_build_config.mjs can assert on them without triggering +// a real build; buildBinary() is exported so test_build_e2e.mjs can run a +// real build for the current platform and drive the actual output binary. +// The entrypoint guard below (same pattern as capture_screenshots.mjs — +// see WISDOM.md) means importing this module never runs a build as a side +// effect — only `node build.mjs` (or an explicit buildBinary() call) does. import { execFileSync } from "node:child_process"; import fs from "node:fs"; @@ -27,41 +35,33 @@ import JSZip from "jszip"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); -const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")).version; -const PLATFORM_MAP = { +export const PLATFORM_MAP = { win32: { osKey: "win", binName: "pptxdiff-win.exe", isWin: true, isMac: false }, darwin: { osKey: "mac", binName: "pptxdiff-mac", isWin: false, isMac: true }, linux: { osKey: "linux", binName: "pptxdiff-linux", isWin: false, isMac: false }, }; -const target = PLATFORM_MAP[process.platform]; -if (!target) { - console.error(`No SEA build mapping for process.platform=${process.platform} (supported: win32, darwin, linux).`); - process.exitCode = 1; - process.exit(); -} - -const OUT_DIR = path.join(__dirname, `pptxdiff-${target.osKey}`); -const BUILD_TMP = path.join(__dirname, ".build"); -const ASSETS_OUT = path.join(OUT_DIR, "assets"); -const BIN_OUT = path.join(OUT_DIR, target.binName); - // Same subset root package.json's "files" ships to npm — the exact set of // static files bin/cli.js's server actually reads from ROOT. -const ASSET_ENTRIES = [ +export const ASSET_ENTRIES = [ ["src/pptxdiff/index.html", "index.html"], ["src/pptxdiff/support.js", "support.js"], ["src/pptxdiff/sample-pptx.js", "sample-pptx.js"], ["src/pptxdiff/vendor", "vendor"], ]; -function log(msg) { - console.log(`[build-binary:${target.osKey}] ${msg}`); +// Pure: `platform` -> PLATFORM_MAP entry, or null if unsupported. +export function resolveTarget(platform) { + return PLATFORM_MAP[platform] || null; +} + +function log(osKey, msg) { + console.log(`[build-binary:${osKey}] ${msg}`); } -function run(cmd, args, opts = {}) { - log(`$ ${cmd} ${args.join(" ")}`); +function run(osKey, cmd, args, opts = {}) { + log(osKey, `$ ${cmd} ${args.join(" ")}`); execFileSync(cmd, args, { stdio: "inherit", ...opts }); } @@ -70,6 +70,20 @@ function cleanDir(dir) { fs.mkdirSync(dir, { recursive: true }); } +// Removes only what a previous build.mjs run generated inside an OS folder +// (the binary, the copied assets/ folder, any zip artifacts) — NOT a blind +// `rm -rf` of the whole folder, which would also delete the tracked +// README.md/CHANGELOG.md that live there. Safe to call whether or not a +// prior build has ever run (nothing to remove on a fresh clone). +function cleanGeneratedOutDir(outDir, target) { + fs.mkdirSync(outDir, { recursive: true }); + fs.rmSync(path.join(outDir, target.binName), { force: true }); + fs.rmSync(path.join(outDir, "assets"), { recursive: true, force: true }); + for (const entry of fs.readdirSync(outDir)) { + if (entry.endsWith(".zip")) fs.rmSync(path.join(outDir, entry), { force: true }); + } +} + async function zipDir(dir, outZipPath) { const zip = new JSZip(); const walk = (abs, rel) => { @@ -85,17 +99,26 @@ async function zipDir(dir, outZipPath) { fs.writeFileSync(outZipPath, buf); } -async function main() { - log(`Building for ${process.platform} -> ${OUT_DIR}`); - cleanDir(OUT_DIR); - cleanDir(BUILD_TMP); +// Builds the given PLATFORM_MAP `target` (must match the CURRENT +// process.platform — SEA injects into a copy of the running node binary, +// it cannot target a different OS). Returns {outDir, binPath, zipPath}. +export async function buildBinary(target) { + const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")).version; + const outDir = path.join(__dirname, `pptxdiff-${target.osKey}`); + const buildTmp = path.join(__dirname, ".build"); + const assetsOut = path.join(outDir, "assets"); + const binOut = path.join(outDir, target.binName); + + log(target.osKey, `Building for ${process.platform} -> ${outDir}`); + cleanGeneratedOutDir(outDir, target); + cleanDir(buildTmp); // 1. Bundle sea-entry.cjs (which itself inlines bin/cli.js's exports) into // a single flat CommonJS file — SEA's `main` must be one self-contained // file; it does not resolve a script's own `require("./other-file")` // calls at runtime. const esbuild = await import("esbuild"); - const bundlePath = path.join(BUILD_TMP, "bundle.cjs"); + const bundlePath = path.join(buildTmp, "bundle.cjs"); await esbuild.build({ entryPoints: [path.join(__dirname, "sea-entry.cjs")], outfile: bundlePath, @@ -106,8 +129,8 @@ async function main() { }); // 2. Generate the SEA config + blob. - const seaConfigPath = path.join(BUILD_TMP, "sea-config.json"); - const blobPath = path.join(BUILD_TMP, "sea-prep.blob"); + const seaConfigPath = path.join(buildTmp, "sea-config.json"); + const blobPath = path.join(buildTmp, "sea-prep.blob"); fs.writeFileSync( seaConfigPath, JSON.stringify( @@ -120,24 +143,24 @@ async function main() { 2 ) ); - run(process.execPath, ["--experimental-sea-config", seaConfigPath]); + run(target.osKey, process.execPath, ["--experimental-sea-config", seaConfigPath]); // 3. Copy the currently-running node executable as the base, then inject // the blob into it. - fs.copyFileSync(process.execPath, BIN_OUT); - fs.chmodSync(BIN_OUT, 0o755); + fs.copyFileSync(process.execPath, binOut); + fs.chmodSync(binOut, 0o755); if (target.isMac) { // Required by Node's SEA guide: an existing signature on the copied // node binary must be removed before injecting, or postject's write // corrupts it. - run("codesign", ["--remove-signature", BIN_OUT]); + run(target.osKey, "codesign", ["--remove-signature", binOut]); } - run("npx", [ + run(target.osKey, "npx", [ "--no-install", "postject", - BIN_OUT, + binOut, "NODE_SEA_BLOB", blobPath, "--sentinel-fuse", @@ -149,28 +172,37 @@ async function main() { // Ad-hoc signature (no cert) so the binary can run locally at all; // Gatekeeper still warns on a freshly-downloaded copy — see the file // header comment and GAP_ANALYSIS.md. - run("codesign", ["--sign", "-", BIN_OUT]); + run(target.osKey, "codesign", ["--sign", "-", binOut]); } - if (!target.isWin) fs.chmodSync(BIN_OUT, 0o755); + if (!target.isWin) fs.chmodSync(binOut, 0o755); // 4. Copy the static app assets the server reads from `root`. - fs.mkdirSync(ASSETS_OUT, { recursive: true }); + fs.mkdirSync(assetsOut, { recursive: true }); for (const [srcRel, destRel] of ASSET_ENTRIES) { const src = path.join(REPO_ROOT, srcRel); - const dest = path.join(ASSETS_OUT, destRel); + const dest = path.join(assetsOut, destRel); fs.cpSync(src, dest, { recursive: true }); } // 5. Zip the binary + assets together as the actual downloadable artifact. - const zipPath = path.join(OUT_DIR, `pptxdiff-${target.osKey}-${PKG_VERSION}.zip`); - await zipDir(OUT_DIR, zipPath); + const zipPath = path.join(outDir, `pptxdiff-${target.osKey}-${PKG_VERSION}.zip`); + await zipDir(outDir, zipPath); - fs.rmSync(BUILD_TMP, { recursive: true, force: true }); - log(`Done: ${BIN_OUT}`); - log(`Done: ${zipPath}`); + fs.rmSync(buildTmp, { recursive: true, force: true }); + log(target.osKey, `Done: ${binOut}`); + log(target.osKey, `Done: ${zipPath}`); + return { outDir, binPath: binOut, zipPath }; } -main().catch((e) => { - console.error(e); - process.exitCode = 1; -}); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const target = resolveTarget(process.platform); + if (!target) { + console.error(`No SEA build mapping for process.platform=${process.platform} (supported: win32, darwin, linux).`); + process.exitCode = 1; + } else { + buildBinary(target).catch((e) => { + console.error(e); + process.exitCode = 1; + }); + } +} diff --git a/src/packages/binaries/package.json b/src/packages/binaries/package.json index bcac013..89411a7 100644 --- a/src/packages/binaries/package.json +++ b/src/packages/binaries/package.json @@ -10,7 +10,9 @@ "url": "https://github.com/sugatoray" }, "scripts": { - "build": "node build.mjs" + "build": "node build.mjs", + "test": "node test_build_config.mjs", + "test:e2e": "node test_build_e2e.mjs" }, "devDependencies": { "esbuild": "^0.24.0", diff --git a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md new file mode 100644 index 0000000..5272af2 --- /dev/null +++ b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog — pptxdiff for Linux (standalone binary) + +All notable changes to the Linux standalone `pptxdiff-linux` build are +documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version +tracked is the `pptxdiff` app version bundled into the binary (see the +root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) +since the binary has no independent feature set of its own. + +## [Unreleased] + +... + +## [0.7.0] - 2026-08-05 + +### Added + +- First standalone Linux executable, built via Node's Single Executable + Applications feature (see `../README.md` and + `docs/.scrolls/SPEC.md` §32) — download `pptxdiff-linux-0.7.0.zip`, + unzip, `chmod +x pptxdiff-linux && ./pptxdiff-linux`. No separate + Node.js install required. +- Verified end-to-end in this project's own dev sandbox: built for real, + the actual packaged binary was run and confirmed to correctly serve + `index.html`/`support.js`/`vendor/*` over real HTTP requests (see + `../test_build_e2e.mjs`). + +### Known limitations + +- Not yet attached to GitHub Releases — built by + `.github/workflows/binaries.yml`'s CI matrix and available as a workflow + artifact. diff --git a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md new file mode 100644 index 0000000..1c2ad9f --- /dev/null +++ b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog — pptxdiff for macOS (standalone binary) + +All notable changes to the macOS standalone `pptxdiff-mac` build are +documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version +tracked is the `pptxdiff` app version bundled into the binary (see the +root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) +since the binary has no independent feature set of its own. + +## [Unreleased] + +... + +## [0.7.0] - 2026-08-05 + +### Added + +- First standalone macOS executable, built via Node's Single Executable + Applications feature (see `../README.md` and + `docs/.scrolls/SPEC.md` §32) — download `pptxdiff-mac-0.7.0.zip`, unzip, + run `./pptxdiff-mac`. No separate Node.js install required. + +### Known limitations + +- **Ad-hoc signed, not notarized.** No Apple Developer ID — Gatekeeper + will likely block a freshly-downloaded copy ("cannot be opened because + the developer cannot be verified"); right-click → Open, or + `xattr -d com.apple.quarantine pptxdiff-mac` first. See `../README.md` + and `docs/.scrolls/GAP_ANALYSIS.md`. +- Not yet attached to GitHub Releases — built by + `.github/workflows/binaries.yml`'s CI matrix and available as a workflow + artifact. diff --git a/src/packages/binaries/pptxdiff-win/CHANGELOG.md b/src/packages/binaries/pptxdiff-win/CHANGELOG.md new file mode 100644 index 0000000..969b580 --- /dev/null +++ b/src/packages/binaries/pptxdiff-win/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog — pptxdiff for Windows (standalone binary) + +All notable changes to the Windows standalone `pptxdiff-win.exe` build are +documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version +tracked is the `pptxdiff` app version bundled into the binary (see the +root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) +since the binary has no independent feature set of its own. + +## [Unreleased] + +... + +## [0.7.0] - 2026-08-05 + +### Added + +- First standalone Windows executable, built via Node's Single Executable + Applications feature (see `../README.md` and + `docs/.scrolls/SPEC.md` §32) — download `pptxdiff-win-0.7.0.zip`, unzip, + run `pptxdiff-win.exe`. No separate Node.js install required. + +### Known limitations + +- **Unsigned.** No code-signing certificate — Windows SmartScreen will + likely warn on first run ("Windows protected your PC"); click "More + info" → "Run anyway". See `../README.md` and + `docs/.scrolls/GAP_ANALYSIS.md`. +- Not yet attached to GitHub Releases — built by + `.github/workflows/binaries.yml`'s CI matrix and available as a workflow + artifact. diff --git a/src/packages/binaries/test_build_config.mjs b/src/packages/binaries/test_build_config.mjs new file mode 100644 index 0000000..4b6c5bb --- /dev/null +++ b/src/packages/binaries/test_build_config.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +"use strict"; + +// Fast, pure regression checks for build.mjs's build CONFIGURATION — no +// real SEA build, no subprocess, no network. Complements test_build_e2e.mjs +// (which actually builds and runs a real binary but is slow/heavy) the +// same way this project's other packages split a fast pure-unit suite from +// a slower real-process/real-browser one (e.g. pptxdiff-cli's `npm test` +// vs `npm run test:difftool`). +// +// Run: node test_build_config.mjs + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { PLATFORM_MAP, ASSET_ENTRIES, resolveTarget } from "./build.mjs"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); + +let pass = 0; +let fail = 0; +function assert(name, cond) { + if (cond) { + pass++; + } else { + fail++; + console.error(`FAIL: ${name}`); + } +} + +// --- PLATFORM_MAP / resolveTarget --- +assert("PLATFORM_MAP has exactly win32/darwin/linux keys", ( + JSON.stringify(Object.keys(PLATFORM_MAP).sort()) === JSON.stringify(["darwin", "linux", "win32"]) +)); +assert("win32 maps to osKey=win, binName ends .exe, isWin=true", ( + PLATFORM_MAP.win32.osKey === "win" && PLATFORM_MAP.win32.binName === "pptxdiff-win.exe" && PLATFORM_MAP.win32.isWin === true +)); +assert("darwin maps to osKey=mac, isMac=true, isWin=false", ( + PLATFORM_MAP.darwin.osKey === "mac" && PLATFORM_MAP.darwin.isMac === true && PLATFORM_MAP.darwin.isWin === false +)); +assert("linux maps to osKey=linux, isMac=false, isWin=false, no .exe suffix", ( + PLATFORM_MAP.linux.osKey === "linux" && PLATFORM_MAP.linux.isMac === false && PLATFORM_MAP.linux.isWin === false && !PLATFORM_MAP.linux.binName.includes(".") +)); +assert("resolveTarget('win32') === PLATFORM_MAP.win32", resolveTarget("win32") === PLATFORM_MAP.win32); +assert("resolveTarget returns null for an unsupported platform", resolveTarget("aix") === null); +assert("resolveTarget returns null for a made-up platform string", resolveTarget("not-a-real-platform") === null); + +// --- ASSET_ENTRIES drift guard: must match root package.json's "files" --- +// (mirrors the project's existing fixture-drift-check concern — see +// GAP_ANALYSIS.md's "Fixture drift-check" ticket — applied here to the +// asset set a packaged binary ships, so it can never silently diverge from +// what the npm package itself ships.) +const rootPkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")); +const npmStaticFiles = rootPkg.files.filter((f) => f.startsWith("src/pptxdiff/")); +const assetSrcPaths = ASSET_ENTRIES.map(([src]) => src).sort(); +assert( + `ASSET_ENTRIES source paths match root package.json "files" static app subset (got ${JSON.stringify(assetSrcPaths)} vs ${JSON.stringify([...npmStaticFiles].sort())})`, + JSON.stringify(assetSrcPaths) === JSON.stringify([...npmStaticFiles].sort()) +); +assert("every ASSET_ENTRIES source path exists on disk", ( + ASSET_ENTRIES.every(([src]) => fs.existsSync(path.join(REPO_ROOT, src))) +)); +assert("every ASSET_ENTRIES dest path is a plain relative name (no traversal)", ( + ASSET_ENTRIES.every(([, dest]) => !dest.includes("..") && !path.isAbsolute(dest)) +)); + +// --- bin/cli.js contract sea-entry.cjs depends on --- +// A regression guard, not a design assertion: if a future edit to +// bin/cli.js drops startServer()'s optional `root` param (or its default), +// the packaged binary silently breaks (it would try to serve from the npm +// package's own ROOT instead of the assets folder next to the executable) +// with no error at build time — only a confusing 404 at runtime. Catch it +// here instead, the same static-source-check pattern WISDOM.md's +// "stale renderVals binding" entry established for a similar class of +// silent-breakage risk. +const cliSrc = fs.readFileSync(path.join(REPO_ROOT, "bin", "cli.js"), "utf8"); +assert("bin/cli.js's startServer() still accepts an optional root param defaulting to ROOT", ( + /function startServer\(root\s*=\s*ROOT\)/.test(cliSrc) +)); +assert("bin/cli.js's startServer() still exports (module.exports includes startServer)", ( + /module\.exports\s*=\s*\{[^}]*startServer[^}]*\}/.test(cliSrc) +)); + +// --- sea-entry.cjs's own asset-resolution contract --- +const entrySrc = fs.readFileSync(path.join(__dirname, "sea-entry.cjs"), "utf8"); +assert("sea-entry.cjs resolves ROOT relative to process.execPath, not __dirname", ( + entrySrc.includes("path.dirname(process.execPath)") && /const ROOT = path\.join\(path\.dirname\(process\.execPath\)/.test(entrySrc) +)); +assert("sea-entry.cjs passes ROOT into startServer() explicitly", ( + /startServer\(ROOT\)/.test(entrySrc) +)); + +// --- package.json devDependencies actually present --- +const binPkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")); +for (const dep of ["esbuild", "jszip", "postject"]) { + assert(`package.json devDependencies includes ${dep}`, Boolean(binPkg.devDependencies && binPkg.devDependencies[dep])); +} + +console.log(`build-config check: ${pass}/${pass + fail} passed`); +if (fail > 0) { + console.error(`${fail} check(s) FAILED (RED).`); + process.exitCode = 1; +} else { + console.log("All build-config checks passed (GREEN)."); +} diff --git a/src/packages/binaries/test_build_e2e.mjs b/src/packages/binaries/test_build_e2e.mjs new file mode 100644 index 0000000..3cceef4 --- /dev/null +++ b/src/packages/binaries/test_build_e2e.mjs @@ -0,0 +1,163 @@ +#!/usr/bin/env node +"use strict"; + +// Real, slow, current-platform-only end-to-end check: actually runs +// buildBinary() (the same code `node build.mjs` runs), then spawns the +// REAL packaged executable it produced and drives it over real HTTP — +// same spirit as pptxdiff-cli's *_e2e.mjs files (real browser, real +// spawned process) rather than mocking any of this. Deliberately kept out +// of the default `npm test` (this alone takes well over a minute and +// produces a ~100MB+ binary) — run explicitly via `npm run test:e2e`, +// mirroring pptxdiff-cli's `test:difftool` split for the same reason +// (a slow/heavy check that needs real platform resources). +// +// Only exercises the CURRENT host's platform branch (Node SEA has no +// cross-platform build mode — see build.mjs's header comment) — the other +// two OS branches are structurally identical but only really exercised by +// CI's 3-OS matrix (.github/workflows/binaries.yml). +// +// Run: node test_build_e2e.mjs + +import { execFile } from "node:child_process"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { buildBinary, resolveTarget } from "./build.mjs"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +let pass = 0; +let fail = 0; +function assert(name, cond) { + if (cond) { + pass++; + console.log(`ok - ${name}`); + } else { + fail++; + console.error(`FAIL: ${name}`); + } +} + +function fetchText(url) { + return new Promise((resolve, reject) => { + http + .get(url, (res) => { + let body = ""; + res.on("data", (c) => (body += c)); + res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, body })); + }) + .on("error", reject); + }); +} + +function waitForLine(child, matcher, timeoutMs) { + return new Promise((resolve, reject) => { + let buf = ""; + const onData = (chunk) => { + buf += chunk.toString(); + const m = buf.match(matcher); + if (m) { + cleanup(); + resolve(m); + } + }; + const timer = setTimeout(() => { + cleanup(); + reject(new Error(`Timed out waiting for ${matcher} in output:\n${buf}`)); + }, timeoutMs); + function cleanup() { + clearTimeout(timer); + child.stdout.off("data", onData); + } + child.stdout.on("data", onData); + }); +} + +async function main() { + const target = resolveTarget(process.platform); + if (!target) { + console.error(`No SEA build mapping for process.platform=${process.platform} — nothing to e2e-test here.`); + process.exitCode = 1; + return; + } + + console.log(`Building a real ${target.osKey} binary (this takes a while)...`); + const { outDir, binPath, zipPath } = await buildBinary(target); + + assert("build produced the binary file", fs.existsSync(binPath)); + assert("build produced the assets folder", fs.existsSync(path.join(outDir, "assets", "index.html"))); + assert("build produced the zip artifact", fs.existsSync(zipPath)); + if (!target.isWin) { + const mode = fs.statSync(binPath).mode; + assert("binary is executable (owner +x bit set)", Boolean(mode & 0o100)); + } + + // Actually run the packaged binary and talk to it over real HTTP — + // proves the assets/-folder-next-to-the-executable resolution (sea-entry.cjs's + // `path.dirname(process.execPath)` logic) genuinely works, not just that + // the files exist on disk in the right place. + const child = execFile(binPath, { cwd: outDir, env: {} }); + let urlMatch; + try { + urlMatch = await waitForLine(child, /pptxdiff running at (http:\/\/localhost:\d+)/, 15000); + } catch (e) { + console.error("Binary never printed its startup line:", e.message); + child.kill(); + fail++; + reportAndExit(); + return; + } + const baseUrl = urlMatch[1]; + assert("binary printed a startup URL", Boolean(baseUrl)); + + try { + const index = await fetchText(`${baseUrl}/`); + assert("GET / returns 200", index.status === 200); + assert("GET / body looks like the real app shell", index.body.includes("") && index.body.includes('src="./support.js"')); + + const supportJs = await fetchText(`${baseUrl}/support.js`); + assert("GET /support.js returns 200", supportJs.status === 200); + assert("GET /support.js has JS content-type", (supportJs.headers["content-type"] || "").includes("javascript")); + + const vendorFile = await fetchText(`${baseUrl}/vendor/react.production.min.js`); + assert("GET /vendor/react.production.min.js returns 200 (assets/ folder is actually being served)", vendorFile.status === 200); + + // Path-containment regression check against THIS root (assets/), not + // just bin/cli.js's default ROOT — a different `root` value is exactly + // what this whole feature changed, so re-prove isPathContained still + // applies to it rather than assuming it does because it's "the same + // function." + const traversal = await fetchText(`${baseUrl}/../../../etc/passwd`); + assert("path traversal against the packaged binary's assets root is rejected (403 or 404, never 200)", traversal.status !== 200); + } finally { + child.kill(); + } + + reportAndExit(); + + function reportAndExit() { + console.log(`build-e2e check: ${pass}/${pass + fail} passed`); + if (fail > 0) { + console.error(`${fail} check(s) FAILED (RED).`); + process.exitCode = 1; + } else { + console.log("All build-e2e checks passed (GREEN)."); + } + // Clean up the built artifact afterward — this test's job is to prove + // the build+run path works, not to leave a ~100MB+ binary lying around. + // Only removes what THIS build generated (binary/assets/zip), not the + // whole outDir — that folder also holds the tracked README.md/ + // CHANGELOG.md, which a blind `rm -rf` would delete too. + fs.rmSync(binPath, { force: true }); + fs.rmSync(path.join(outDir, "assets"), { recursive: true, force: true }); + for (const entry of fs.readdirSync(outDir)) { + if (entry.endsWith(".zip")) fs.rmSync(path.join(outDir, entry), { force: true }); + } + } +} + +main().catch((e) => { + console.error(e); + process.exitCode = 1; +}); From 5ba3aa1b8b6c6e1532ee8f10961823cbe0a4375a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 05:07:09 +0000 Subject: [PATCH 3/7] Switch standalone binaries from Node SEA to @yao-pkg/pkg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Direct follow-up to "why aren't you using yao-pkg/pkg?" — investigated hands-on and confirmed two real advantages over Node's SEA feature: - Genuine cross-compilation: built real Windows (PE32+) and macOS (Mach-O) binaries from this Linux sandbox, something SEA cannot do at all (it can only build for its own currently-running platform). - Built-in asset embedding needing zero bin/cli.js changes: pkg's snapshot filesystem preserves the real project's relative directory layout, so pointing pkg directly at the unmodified bin/cli.js with src/pptxdiff/** as assets makes the existing ROOT computation just resolve correctly. Reverted the SEA-era startServer(root) parameter entirely — bin/cli.js is now byte-identical to before this feature. Found and fixed a real, silently-failing gotcha mid-switch: pkg's "assets" glob paths resolve relative to wherever the config file itself lives, not cwd or the entry file's directory. Confirmed via a controlled A/B test; fixed by writing the temp pkg config directly at the repo root (removed in a finally block). New WISDOM.md trap entry with the full reproduction. macOS is deliberately NOT cross-compiled even though pkg technically can: codesign only exists on macOS, and an unsigned binary may not launch at all on Apple Silicon. .github/workflows/binaries.yml now runs 2 jobs instead of 3: linux+win build together on ubuntu-latest (genuine cross-compile), mac stays on its own macos-latest runner. Both test files rewritten and re-verified with genuine RED->GREEN on the config-colocation guard. Output is now a true single file per OS (no more assets/ folder, no more zip wrapper). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BxwMTp6RQJ6j6K5K8Jjdpm --- .github/workflows/binaries.yml | 53 +- .gitignore | 15 +- .gitignores/user.gitignore | 15 +- CHANGELOG.md | 8 +- bin/cli.js | 10 +- docs/.scrolls/GAP_ANALYSIS.md | 10 +- docs/.scrolls/GAP_CONTEXT.md | 20 +- docs/.scrolls/HANDOFF.md | 11 + docs/.scrolls/PLAN.md | 61 +- docs/.scrolls/SPEC.md | 19 +- docs/.scrolls/WISDOM.md | 6 + src/packages/binaries/README.md | 137 +- src/packages/binaries/build.mjs | 279 +-- src/packages/binaries/package-lock.json | 1830 +++++++++++++++-- src/packages/binaries/package.json | 9 +- .../binaries/pptxdiff-linux/CHANGELOG.md | 25 +- .../binaries/pptxdiff-linux/README.md | 26 +- .../binaries/pptxdiff-mac/CHANGELOG.md | 18 +- src/packages/binaries/pptxdiff-mac/README.md | 23 +- .../binaries/pptxdiff-win/CHANGELOG.md | 15 +- src/packages/binaries/pptxdiff-win/README.md | 22 +- src/packages/binaries/sea-entry.cjs | 39 - src/packages/binaries/test_build_config.mjs | 113 +- src/packages/binaries/test_build_e2e.mjs | 77 +- 24 files changed, 2169 insertions(+), 672 deletions(-) delete mode 100644 src/packages/binaries/sea-entry.cjs diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 3a688ba..bf409f7 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -14,18 +14,10 @@ permissions: contents: read jobs: - build: - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-latest - target: linux - - runner: macos-latest - target: mac - - runner: windows-latest - target: win - runs-on: ${{ matrix.runner }} + # linux + win are genuinely cross-compiled by @yao-pkg/pkg from one host — + # no codesigning concern for either, so they build together in one job. + build-linux-win: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v5 @@ -37,10 +29,41 @@ jobs: working-directory: src/packages/binaries - run: npm run test:e2e working-directory: src/packages/binaries - - run: npm run build + - run: npm run build -- linux win working-directory: src/packages/binaries - uses: actions/upload-artifact@v4 with: - name: pptxdiff-${{ matrix.target }} - path: src/packages/binaries/pptxdiff-${{ matrix.target }}/*.zip + name: pptxdiff-linux + path: src/packages/binaries/pptxdiff-linux/pptxdiff-linux + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: pptxdiff-win + path: src/packages/binaries/pptxdiff-win/pptxdiff-win.exe + if-no-files-found: error + + # mac builds on a REAL macOS runner specifically so it can be ad-hoc + # codesigned (codesign only exists on macOS) — an unsigned mac binary is + # a real functional problem on Apple Silicon, not just a warning, so this + # is NOT cross-compiled from the linux job above. See build.mjs's header + # comment and docs/.scrolls/GAP_CONTEXT.md. + build-mac: + runs-on: macos-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v5 + with: + node-version: "22" + - run: npm install + working-directory: src/packages/binaries + - run: npm test + working-directory: src/packages/binaries + - run: npm run test:e2e + working-directory: src/packages/binaries + - run: npm run build -- mac + working-directory: src/packages/binaries + - uses: actions/upload-artifact@v4 + with: + name: pptxdiff-mac + path: src/packages/binaries/pptxdiff-mac/pptxdiff-mac if-no-files-found: error diff --git a/.gitignore b/.gitignore index 4a316d7..a5d792c 100644 --- a/.gitignore +++ b/.gitignore @@ -386,11 +386,14 @@ src/pptxdiff/docs-site/site/ !src/packages/*/lib/ !src/packages/*/lib/** -## Native-binary build output (src/packages/binaries/build.mjs) — the -## executable, the copied "assets" folder, and the zipped artifact are all -## generated per-OS, not source; keep each OS folder's own README.md and -## CHANGELOG.md tracked (they document the folder even before a build has -## ever run there) but ignore everything else build.mjs writes into it. +## Native-binary build output (src/packages/binaries/build.mjs, via +## @yao-pkg/pkg) — the single packaged executable per OS is generated, not +## source; keep each OS folder's own README.md and CHANGELOG.md tracked +## (they document the folder even before a build has ever run there) but +## ignore the built binary itself. build.mjs also writes a temp pkg config +## directly at the repo root (see WISDOM.md's pkg-config-colocation trap +## entry for why) and always removes it in a `finally` — ignored here too +## as a defensive backstop in case a build is interrupted mid-run. src/packages/binaries/pptxdiff-win/* src/packages/binaries/pptxdiff-mac/* src/packages/binaries/pptxdiff-linux/* @@ -400,4 +403,4 @@ src/packages/binaries/pptxdiff-linux/* !src/packages/binaries/pptxdiff-win/CHANGELOG.md !src/packages/binaries/pptxdiff-mac/CHANGELOG.md !src/packages/binaries/pptxdiff-linux/CHANGELOG.md -src/packages/binaries/.build/ +/.pkg-binaries-config.*.json diff --git a/.gitignores/user.gitignore b/.gitignores/user.gitignore index 56cec48..832da05 100644 --- a/.gitignores/user.gitignore +++ b/.gitignores/user.gitignore @@ -30,11 +30,14 @@ src/pptxdiff/docs-site/site/ !src/packages/*/lib/ !src/packages/*/lib/** -## Native-binary build output (src/packages/binaries/build.mjs) — the -## executable, the copied "assets" folder, and the zipped artifact are all -## generated per-OS, not source; keep each OS folder's own README.md and -## CHANGELOG.md tracked (they document the folder even before a build has -## ever run there) but ignore everything else build.mjs writes into it. +## Native-binary build output (src/packages/binaries/build.mjs, via +## @yao-pkg/pkg) — the single packaged executable per OS is generated, not +## source; keep each OS folder's own README.md and CHANGELOG.md tracked +## (they document the folder even before a build has ever run there) but +## ignore the built binary itself. build.mjs also writes a temp pkg config +## directly at the repo root (see WISDOM.md's pkg-config-colocation trap +## entry for why) and always removes it in a `finally` — ignored here too +## as a defensive backstop in case a build is interrupted mid-run. src/packages/binaries/pptxdiff-win/* src/packages/binaries/pptxdiff-mac/* src/packages/binaries/pptxdiff-linux/* @@ -44,4 +47,4 @@ src/packages/binaries/pptxdiff-linux/* !src/packages/binaries/pptxdiff-win/CHANGELOG.md !src/packages/binaries/pptxdiff-mac/CHANGELOG.md !src/packages/binaries/pptxdiff-linux/CHANGELOG.md -src/packages/binaries/.build/ +/.pkg-binaries-config.*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 221868b..ecbd6cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,11 +40,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 state to the tap even without a version-pin change, while a scheduled run still only does real work on an actual version bump. `test_formula.mjs` now also asserts `LICENSE` stays byte-identical to the repo root's copy, catching drift instead of silently shipping a stale license to the tap. -- New private `@pptxdiff/binaries` package (`src/packages/binaries/`) building standalone native `pptxdiff` executables for Windows, macOS, and Linux via Node's Single Executable Applications feature — download one artifact and run it, no separate Node.js install required. +- New private `@pptxdiff/binaries` package (`src/packages/binaries/`) building standalone native `pptxdiff` executables for Windows, macOS, and Linux via `@yao-pkg/pkg` — download one file and run it, no separate Node.js install required. - Per-OS build output folders `src/packages/binaries/pptxdiff-{win,mac,linux}/`, each with its own `README.md` and `CHANGELOG.md`. -- `.github/workflows/binaries.yml`: a 3-OS CI matrix building all three binaries (Node SEA has no cross-compile mode, so each OS's binary is built on that OS). -- `make pkg.binaries.build` / `npm run build:binary` for local single-OS builds. -- `bin/cli.js`'s `startServer()` gained a backward-compatible optional `root` parameter so the packaged binaries can serve static assets from next to themselves. +- `.github/workflows/binaries.yml`: `pkg` genuinely cross-compiles, so Windows and Linux build together in one `ubuntu-latest` job; macOS builds in its own `macos-latest` job so it can be properly ad-hoc codesigned. +- `make pkg.binaries.build` / `npm run build:binary` for local builds (all three OSes by default, or a specific subset). +- Red/Green TDD test suite for the build tooling itself: `npm test` (fast, pure — config/asset-drift/regression checks) and `npm run test:e2e` (slow, real — builds and runs the actual packaged binary over real HTTP) in `src/packages/binaries/`. ## [0.7.0] - 2026-08-02 diff --git a/bin/cli.js b/bin/cli.js index b94adab..9c12d81 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -97,19 +97,15 @@ function isPathContained(root, candidate) { // existing trap about `pptxdiff-vscode/extension.js` carrying its own // independent (and once out-of-sync) copy of this same server; new // consumers should import this function rather than repeat that mistake. -// `root` defaults to this package's own bundled `src/pptxdiff` (the normal -// npm-install case); `src/packages/binaries`'s Node SEA entry point passes -// an explicit `root` instead, since a packaged single-executable binary has -// no `__dirname`-relative sibling files to find the assets from. // Resolves to { server, port, url } once listening; the caller decides // what to do with the URL (print it, open a browser, hand it to Playwright). -function startServer(root = ROOT) { +function startServer() { return new Promise((resolve, reject) => { const server = http.createServer((req, res) => { const reqPath = decodeURIComponent(req.url.split("?")[0]); - const filePath = path.join(root, path.normalize(reqPath === "/" ? "/index.html" : reqPath)); + const filePath = path.join(ROOT, path.normalize(reqPath === "/" ? "/index.html" : reqPath)); - if (!isPathContained(root, filePath)) { + if (!isPathContained(ROOT, filePath)) { res.writeHead(403, SECURITY_HEADERS); res.end("Forbidden"); return; diff --git a/docs/.scrolls/GAP_ANALYSIS.md b/docs/.scrolls/GAP_ANALYSIS.md index a1be8e3..be25d94 100644 --- a/docs/.scrolls/GAP_ANALYSIS.md +++ b/docs/.scrolls/GAP_ANALYSIS.md @@ -163,12 +163,12 @@ Concrete, testable gaps between what SPEC.md describes and a fully "real" implem - [ ] **No MCP server** (`pptxdiff-cli mcp`, CLI_API_DESIGN.md §9) — the more idiomatic AI-agent integration point than shelling out to the CLI or hand-rolling HTTP calls. Not started; `--json` output + the API's request/response shapes are the current AI-agent integration surface. - [ ] **Not published to npm.** Both packages depend on their monorepo siblings via `file:` protocol (documented in each README) rather than a real published semver range — intentional for local development, but a real blocker before either package can actually be `npm install`ed by anyone outside this repo. -## Standalone native binaries (this session) +## Standalone native binaries (this session, switched from Node SEA to `@yao-pkg/pkg` mid-session) - [ ] **Windows `.exe` is unsigned; macOS binary is ad-hoc signed only (no Developer ID).** No code-signing certificate exists for this project — Windows SmartScreen and macOS Gatekeeper will both warn on a freshly-downloaded copy. Documented per-OS in `src/packages/binaries/pptxdiff-{win,mac}/README.md`; a real fix needs a paid cert (Apple Developer ID + Windows Authenticode), not a code change. -- [ ] **No cross-compilation** — Node SEA builds by injecting into a copy of the currently-running `node` binary, so each OS's binary can only be built ON that OS. `.github/workflows/binaries.yml`'s 3-way CI matrix is the actual mechanism that produces all three; there is no single-machine "build everything" path. -- [ ] **Not yet attached to GitHub Releases.** The CI workflow uploads each OS's zip as a workflow artifact (downloadable from the Actions run page) but nothing wires a release-tag push to attach them to an actual GitHub Release yet — a real, small follow-up (e.g. `softprops/action-gh-release` on `release: types: [published]`), not attempted this session. -- [ ] **~120MB per binary.** Node SEA embeds the entire Node runtime into the executable; there's no way to shrink this within the SEA approach itself (it isn't a JS-bundle-size problem). -- [ ] **macOS/Windows builds are unverified in this sandbox** — only the Linux build was actually run and its binary actually executed end-to-end (built, launched, served `index.html`/`support.js`/`vendor/*` via real HTTP requests). The macOS/Windows code paths (codesign steps, `.exe` naming) are structurally parallel but exercised for the first time whenever CI first runs them, not locally. +- [ ] **macOS is not cross-compiled** — `pkg` genuinely can cross-compile a macOS binary from Linux, but the result can't be codesigned there (`codesign` is macOS-only), and a completely unsigned binary may not even launch on Apple Silicon. `.github/workflows/binaries.yml` therefore still runs macOS on a real `macos-latest` runner (`build-mac`), separate from the `build-linux-win` job that genuinely does cross-compile both those targets from one Linux host. This is a real, load-bearing constraint, not leftover caution from the SEA-based version. +- [ ] **Not yet attached to GitHub Releases.** The CI workflow uploads each OS's binary as a workflow artifact (downloadable from the Actions run page) but nothing wires a release-tag push to attach them to an actual GitHub Release yet — a real, small follow-up (e.g. `softprops/action-gh-release` on `release: types: [published]`), not attempted this session. +- [ ] **x64 only, no native arm64 build for any OS.** Matches the scope of the original (SEA-based) version; an Apple Silicon Mac runs the x64 binary via Rosetta 2. `pkg` does support arm64 targets (`node22-linux-arm64`, `node22-macos-arm64`, etc.) if this is ever asked for — not attempted, to keep this session's scope to what was requested. +- [ ] **macOS/Windows builds are unverified end-to-end in this sandbox** — only the Linux build was actually run and its binary actually executed (built, launched, served `index.html`/`support.js`/`vendor/*` via real HTTP requests) — under both the original SEA mechanism and, again, after switching to `@yao-pkg/pkg`. The Windows binary WAS structurally produced here (real `.exe`, verified during exploration before writing the final `build.mjs`) but not run (no Windows host in this sandbox); the macOS codesign branch is exercised for the first time whenever CI's `build-mac` job runs, not locally. ## Content checksum (this session) - [ ] **The content checksum shows "unavailable" under the plain `file://` open path.** `crypto.subtle` (native Web Crypto, no new dependency) requires a secure context — guaranteed under the CLI's `http://localhost` default, not guaranteed under this project's other documented launch path (`git clone` + open `index.html` directly). Handled honestly (checked once at boot via `cryptoSubtleAvailable`, shown as "unavailable (requires a secure context)" rather than a wrong/fabricated hash or a permanently-stuck "computing…"), but not worked around — would need either a pure-JS SHA-256 fallback (a real new dependency, or a hand-rolled implementation neither asked for nor free of its own correctness risk) or accepting the gap under that one launch path. diff --git a/docs/.scrolls/GAP_CONTEXT.md b/docs/.scrolls/GAP_CONTEXT.md index 855ddbd..8f701ba 100644 --- a/docs/.scrolls/GAP_CONTEXT.md +++ b/docs/.scrolls/GAP_CONTEXT.md @@ -169,11 +169,23 @@ The user's own task description asked to place "downloadable installers" under ` ## Why the folder structure keeps build output out of git except a README per OS folder `src/packages/binaries/pptxdiff-{win,mac,linux}/` are build-artifact directories (the actual `.exe`/binary/`assets/`/`.zip`, all generated by `build.mjs`), not source — same category as `dist/` (npm pack output) or `pptxdiff-vscode/dist/` (`.vsix` output), both already gitignored in this repo. Following that existing precedent rather than inventing a new one: gitignore everything build.mjs writes, but keep one tracked `README.md` per OS folder so the folder structure the user asked for (and a description of what will appear there) exists in a fresh clone even before anyone has run a build. -## Why the binaries reuse `bin/cli.js`'s `startServer()` instead of a separate server implementation -This project already has a documented trap for exactly this mistake: `pptxdiff-vscode/extension.js` once grew its OWN independent copy of `bin/cli.js`'s static file server, which silently fell out of sync when the real one was security-hardened (see WISDOM.md's trap entry and GAP_ANALYSIS.md's "Security hardening" section). `startServer()` was already designed to be reusable (exported specifically so `pptxdiff-cli`'s automation layer could reuse it) — it only needed one small addition, an optional `root` parameter (default unchanged, so `bin/cli.js`'s own direct-run path and every existing caller keep working exactly as before) so the SEA-packaged binary could point it at an `assets/` folder next to the executable instead of the npm package's own `src/pptxdiff` directory. Reusing it here means the packaged binary's path-containment/security-header/loopback-binding behavior can never drift from the already-hardened, already-tested original — a third copy was never on the table. +## Why the binaries reuse `bin/cli.js`'s `startServer()` instead of a separate server implementation — SUPERSEDED, see below (still true, mechanism changed) +This project already has a documented trap for exactly this mistake: `pptxdiff-vscode/extension.js` once grew its OWN independent copy of `bin/cli.js`'s static file server, which silently fell out of sync when the real one was security-hardened (see WISDOM.md's trap entry and GAP_ANALYSIS.md's "Security hardening" section). `startServer()` was already designed to be reusable (exported specifically so `pptxdiff-cli`'s automation layer could reuse it) — under the original Node-SEA-based version, it needed one small addition, an optional `root` parameter, so the packaged binary could point it at an `assets/` folder next to the executable instead of the npm package's own `src/pptxdiff` directory. Reusing `startServer()` at all (rather than writing a third copy) is still exactly right — see the entry below for why the `root` parameter itself turned out to be avoidable entirely once the mechanism changed. -## Why the binary resolves its assets relative to `process.execPath`, not embedded via SEA's asset store -Node SEA does support embedding arbitrary binary assets directly into the executable (retrieved at runtime via `require('node:sea').getAsset()`), which would give a genuinely single-file artifact with no separate `assets/` folder to keep alongside it. That was considered and deliberately not used for this first pass: `bin/cli.js`'s static server already does ordinary `fs.readFile()` against a directory — reusing it as-is (only changing what `root` points to) meant zero changes to the actual file-serving logic, versus rewriting it to read from `sea.getAsset()` instead (a different API, and a real behavior change to already-hardened, already-tested code) purely to shave one folder off the download. The shipped shape — binary + `assets/` folder, zipped together as the actual download — still satisfies "no Node.js install required, download and run"; true single-file embedding is a reasonable follow-up if the extra folder turns out to matter in practice, not a requirement of what was asked. +## Why the binary resolves its assets relative to `process.execPath`, not embedded via SEA's asset store — SUPERSEDED, see below +Node SEA does support embedding arbitrary binary assets directly into the executable (retrieved at runtime via `require('node:sea').getAsset()`), which would give a genuinely single-file artifact with no separate `assets/` folder to keep alongside it. That was considered and deliberately not used for the first (SEA-based) pass: `bin/cli.js`'s static server already does ordinary `fs.readFile()` against a directory — reusing it as-is (only changing what `root` points to) meant zero changes to the actual file-serving logic, versus rewriting it to read from `sea.getAsset()` instead. The shipped shape at the time — binary + `assets/` folder, zipped together — was judged good enough; true single-file embedding was flagged as a reasonable follow-up. The entry below explains what actually superseded this. + +## Why the switch from Node SEA to `@yao-pkg/pkg` (and why `bin/cli.js` needed zero changes afterward) +Explicit follow-up question: "Why aren't you using the npm library yao-pkg/pkg?" Honest answer at the time: SEA was reached for by default (a Node core feature, no added third-party build-tool dependency, consistent with this project's general dependency-conservatism — see the vendoring entries elsewhere in this file) without seriously evaluating `pkg`/`@yao-pkg/pkg` first. Once actually compared, two of `pkg`'s properties were concretely better for this feature's own stated goals, not just "different": +1. **Real cross-compilation.** Verified directly in this project's own dev sandbox (Linux): `pkg -t node22-win-x64 ...` produced a genuine `PE32+ executable ... for MS Windows`, and `-t node22-macos-x64` produced a genuine `Mach-O 64-bit x86_64 executable` — both from one Linux host, `pkg`'s own fetched base-binary cache as the proof (`/root/.pkg-cache/v3.6/fetched-v22.23.2-{win,macos}-x64`), not assumed from documentation. SEA cannot do this at all (it injects into a copy of the CURRENTLY RUNNING node binary, full stop) — the original 3-OS CI matrix existed purely to work around that; `pkg` collapses it to two jobs (see below for why not one). +2. **Built-in asset embedding that needed ZERO `bin/cli.js` changes**, discovered empirically, not assumed: `pkg`'s snapshot filesystem preserves the real project's relative directory structure at runtime (confirmed by walking `/snapshot` inside a real built-and-run test binary). Pointing `pkg` directly at the UNMODIFIED `bin/cli.js` as the entry, with `src/pptxdiff/**` listed as `pkg` "assets," made `bin/cli.js`'s own pre-existing `ROOT = path.join(__dirname, "..", "src", "pptxdiff")` resolve correctly with no packaging-specific code at all — reverting the `root` parameter SEA had required, back to the exact `startServer()` this project shipped before this whole feature existed (confirmed byte-identical via `git show`). + +The honest tradeoff accepted: one more devDependency (`@yao-pkg/pkg`, dev-time only — never shipped in the binaries or the npm package), and it's a community fork of a project (`vercel/pkg`) its original maintainer archived rather than an org-backed package. Judged worth it because the two wins above directly serve THIS feature's own goals (fewer CI jobs, a genuinely single-file artifact) in a way SEA structurally cannot, not because "more modern tool" is inherently better — SEA remains a perfectly reasonable choice for a project that weighs the zero-added-dependency property more heavily than these two wins. + +**A hard-won gotcha found DURING this switch, not from documentation**: `pkg`'s `"assets"` glob paths in a config file resolve relative to wherever THAT CONFIG FILE ITSELF lives, not cwd, not the entry file's directory — confirmed by a controlled A/B test (same relative glob, config file moved to a different directory, assets silently stopped embedding with zero error/warning either time). This is why `build.mjs`'s temp pkg config is written directly at `REPO_ROOT` (next to the real `package.json`) rather than kept as a normal file inside `src/packages/binaries/` itself — full write-up in WISDOM.md's new trap entry, since a build tool silently producing an asset-less binary with no error message is exactly the kind of failure mode worth flagging loudly for future sessions. + +## Why macOS is still built on its own CI runner instead of also being cross-compiled from the Linux job +`pkg` genuinely CAN produce a macOS binary from Linux (verified — see above), so "just cross-compile all three from one job" was the first instinct. Rejected after considering what codesigning actually requires: `codesign` only exists on macOS, so a Linux-built mac binary can never be even ad-hoc signed, and on Apple Silicon specifically, AMFI (Apple Mobile File Integrity) requires AT LEAST an ad-hoc signature for an arm64 executable to launch at all — this isn't "a Gatekeeper warning users can click through" the way it is on Intel Macs, it can mean the binary refuses to run, full stop. Shipping a binary that might not even launch on the now-dominant Apple Silicon Macs would be a real regression from the original SEA-based version (which DID build mac on an actual `macos-latest` runner and could ad-hoc sign it). `.github/workflows/binaries.yml` keeps macOS on its own `macos-latest` job specifically so `codesign --sign -` runs for real; `build.mjs`'s `buildOne()` warns loudly (not silently) if the mac target is ever built off of a non-darwin host, rather than producing something that looks fine at build time and fails mysteriously at launch time. ## Why @pptxdiff/server ships with no authentication rather than a minimal API key CLI_API_DESIGN.md §8 calls for API-key-required-on-non-loopback-bind as part of the design, but implementing even a minimal key check touches real security-sensitive surface (where the key comes from, how it's compared, timing-attack considerations) that deserves its own deliberate pass rather than being bolted on inside a Phase-1 session already covering three other new pieces (automation shim, CLI, server routing). The loopback-by-default bind (matching `bin/cli.js`'s existing precedent) is the one security property that WAS carried over faithfully; the auth gap is real, named explicitly in the package's own README (not just a scroll only this project's own sessions read), and is the literal next thing to build before anyone binds this server to a non-loopback host in practice. diff --git a/docs/.scrolls/HANDOFF.md b/docs/.scrolls/HANDOFF.md index 7b72357..06b12e2 100644 --- a/docs/.scrolls/HANDOFF.md +++ b/docs/.scrolls/HANDOFF.md @@ -2,6 +2,17 @@ **Read `.scrolls/SPEC.md` first for the full feature list.** This file is the "what's the state of things right now" note — update it at the end of every session, keep it short and current (prune stale entries). +## Update (2026-08-05 — binaries: switched from Node SEA to `@yao-pkg/pkg`) +- Direct follow-up question on the same-day binaries work below: "Why aren't you using the npm library yao-pkg/pkg?" Honest answer given first: SEA was reached for by default (Node core feature, no added third-party build-tool dependency) without actually evaluating `pkg` first — then investigated hands-on rather than defending the choice abstractly. +- **Verified two real advantages in this sandbox, not just cited from memory**: (1) genuine cross-compilation — built a real Windows `.exe` (`file` confirmed `PE32+ executable ... for MS Windows`) and a real macOS binary (`Mach-O 64-bit x86_64 executable`) FROM THIS LINUX SANDBOX, something Node SEA cannot do at all; (2) built-in asset embedding needing zero `bin/cli.js` changes — `pkg`'s snapshot filesystem preserves the real project's relative directory layout, so pointing `pkg` directly at the UNMODIFIED `bin/cli.js` with `src/pptxdiff/**` as `pkg` assets made the existing `ROOT = path.join(__dirname, "..", "src", "pptxdiff")` just resolve correctly. Asked the user to confirm via `AskUserQuestion` before doing the rework (real effort, real new dependency) rather than just switching unilaterally; user picked switch. +- **`bin/cli.js` fully reverted** — the SEA-era `startServer(root = ROOT)` optional parameter is gone; `startServer()` is back to taking no arguments, confirmed byte-identical to the pre-binaries-feature version via `git show ee9bec7~1:bin/cli.js | diff -`. `sea-entry.cjs` deleted entirely (no longer needed — `pkg` points straight at the real `bin/cli.js`). +- **Real, silently-failing gotcha found DURING the switch** (not from docs): `pkg`'s `"assets"` glob paths in a config file resolve relative to WHEREVER THE CONFIG FILE ITSELF LIVES — not cwd, not the entry file's directory. Confirmed via a controlled A/B test (same relative glob string, config file moved between directories) — assets silently stopped embedding with zero error or warning either time, only showing up as 404s when the built binary was actually run. Fixed by having `buildOne()` write its temp pkg config directly at `REPO_ROOT` (next to the real `package.json`, where `src/pptxdiff/**` actually resolves), removed in a `finally` block. Full reproduction recorded in WISDOM.md's new trap entry — this is exactly the kind of failure a future session could easily reintroduce by "simplifying" the config placement. +- **`build.mjs` rewritten** around `pkg`'s programmatic `exec()` API: `buildOne(osKey, target)` (single target) and `buildAll(osKeys)` (defaults to all three, used by local `npm run build`). Output is now a genuine single file per OS — no more `assets/` folder, no more zip wrapper (the whole point of switching). +- **macOS deliberately NOT cross-compiled**, even though `pkg` technically can produce a mac binary from Linux: `codesign` only exists on macOS, and on Apple Silicon a completely unsigned binary may not even launch (AMFI requires at least an ad-hoc signature — not just a Gatekeeper warning the way Intel Macs work). `buildOne()` only codesigns when `process.platform === "darwin"`, warns loudly otherwise. `.github/workflows/binaries.yml` restructured to 2 jobs: `build-linux-win` on `ubuntu-latest` (genuine cross-compile, both targets in one job), `build-mac` kept on its own `macos-latest` runner. +- **Both test files rewritten and re-verified with genuine RED→GREEN**: `test_build_config.mjs` (18 assertions, fast/pure) — demonstrated RED on the sharpest new guard by moving the temp-config write location from `REPO_ROOT` to `__dirname` and confirming the test caught it (17/18), restored, confirmed 18/18. `test_build_e2e.mjs` (10 assertions, slow/real) re-run against the new mechanism: built and ran an actual `pkg`-produced binary, confirmed real HTTP responses, added an explicit assertion that no separate `assets/` folder exists. +- Per-OS `README.md`/`CHANGELOG.md` (still unreleased, so amended in place) and root `CHANGELOG.md` updated to match — no remaining mention of Node SEA or the `root` parameter as the current mechanism (both fully retired), though GAP_CONTEXT.md keeps the old reasoning entries with explicit "SUPERSEDED, see below" markers rather than deleting them, per this project's own convention. +- Scrolls updated to match: SPEC.md §32 (rewritten for `pkg`), PLAN.md (new "switched to @yao-pkg/pkg" Done-this-session block, ticket 3 marked done-by-supersession), GAP_ANALYSIS.md (macOS-not-cross-compiled reframed as a real load-bearing constraint, not leftover SEA caution), GAP_CONTEXT.md (two entries marked superseded, two new entries: the switch reasoning and why mac stays on its own runner), WISDOM.md (new trap entry with the full A/B reproduction). + ## Update (2026-08-05 — binaries follow-up: Red/Green TDD + per-OS CHANGELOG.md) - Direct follow-up ask on the same-day binaries work below: "use Red/Green TDD and update documentation and add a CHANGELOG.md for each os specific folder." - **`build.mjs` refactored for testability**: added the same entrypoint guard `capture_screenshots.mjs` already established (`process.argv[1] === fileURLToPath(import.meta.url)`) so `PLATFORM_MAP`/`ASSET_ENTRIES`/`resolveTarget()`/`buildBinary(target)` are now named exports importable without a real build running as a side effect. diff --git a/docs/.scrolls/PLAN.md b/docs/.scrolls/PLAN.md index d95897a..6b5e235 100644 --- a/docs/.scrolls/PLAN.md +++ b/docs/.scrolls/PLAN.md @@ -391,7 +391,7 @@ shim, not sequentially — that plan is what shipped below. ## Done this session (Red/Green regression test for the Chocolatey package) - [x] **P2 — `test_chocolatey_package.mjs`**: pure-Node static-analysis regression test for `src/packages/pptxdiff-chocolatey/` (no `choco`/`pwsh` needed). 21 assertions covering version-sync across `pptxdiff.nuspec`/root `package.json`/the install script's fallback pin, the nuspec's `nodejs` dependency version, both `.ps1` scripts' npm commands, the cmdlet-argument-mode `+`-concatenation bug staying absent, `tools/LICENSE.txt` staying byte-identical to root `LICENSE`, and required companion files existing. Genuinely demonstrated RED (18/21, 3 real failures) before GREEN (21/21) by temporarily reintroducing a version mismatch and the PowerShell bug, then restoring both. - [x] Ticket 2 above ("Automate version sync") is now PARTIALLY addressed: still a manual bump, but drift is caught automatically by the new test instead of shipping silently — see GAP_ANALYSIS.md's updated entry. -## Done this session (standalone native binaries for Windows/macOS/Linux) +## Done this session (standalone native binaries for Windows/macOS/Linux) — mechanism superseded later this same session, see "switched to @yao-pkg/pkg" below - [x] **P2 — `src/packages/binaries/`: standalone native `pptxdiff` executables via Node SEA.** Explicit ask, with an explicit up-front choice (asked directly): standalone binaries vs. real signed OS installers — user picked standalone binaries, consistent with the prior explicit @@ -428,19 +428,54 @@ shim, not sequentially — that plan is what shipped below. ## New tickets opened this session 1. **P2 — Attach the built binaries to GitHub Releases**, not just CI workflow artifacts. Needs a - `release: types: [published]`-triggered job (or similar) that re-runs the 3-OS build matrix and - uploads the zips to the release — not built this session, current CI only produces downloadable - workflow artifacts on push/dispatch. + `release: types: [published]`-triggered job (or similar) that re-runs the build and uploads the + binaries to the release — not built this session, current CI only produces downloadable workflow + artifacts on push/dispatch. 2. **P3 — Code signing for the Windows `.exe` and a real Apple Developer ID for macOS.** Needs a purchased/managed certificate (real ongoing cost, not a code change) — until then, both binaries trigger their OS's "unidentified/unsigned" security warning on first run. Documented per-OS in each `pptxdiff-/README.md`. -3. **P4 — True single-file binaries via Node SEA's embedded-asset store** (`node:sea`'s - `getAsset()`), instead of shipping a `binary + assets/ folder`, zipped together. Would need - `bin/cli.js`'s static server to read from `sea.getAsset()` when running under SEA instead of - `fs.readFile()` — a real behavior change to already-hardened code, deliberately not made for this - first pass (see GAP_CONTEXT.md). -4. **P4 — `test_build_e2e.mjs` only exercises the CURRENT host's platform branch.** The macOS/Windows - `buildBinary()` branches (codesign steps, `.exe` naming) are covered by `test_build_config.mjs`'s - static checks but not by a real build+run — that only happens via CI's 3-OS matrix. Not a gap in - this session's TDD work so much as an inherent constraint of Node SEA itself (see GAP_ANALYSIS.md). +3. ~~**P4 — True single-file binaries via Node SEA's embedded-asset store.**~~ **[DONE, by switching + mechanism entirely]** — see the `@yao-pkg/pkg` switch below. `pkg`'s built-in asset embedding gave + a genuine single file with zero `bin/cli.js` changes, superseding this ticket rather than closing + it as originally scoped. +4. ~~**P4 — `test_build_e2e.mjs` only exercises the CURRENT host's platform branch.**~~ Still true + under `pkg` (same reason: only the current host's binary can actually be RUN and verified over + HTTP locally) — carried forward, not re-opened as new. + +## Done this session (switched `@pptxdiff/binaries` from Node SEA to `@yao-pkg/pkg`) +- [x] **P2 — Switched the native-binary build mechanism after an explicit follow-up question** ("why + aren't you using yao-pkg/pkg?"). Investigated hands-on rather than reasoning abstractly: built real + test binaries in this sandbox confirming `pkg` genuinely cross-compiles (a real Linux-built `.exe` + confirmed via `file` as `PE32+ executable ... for MS Windows`, a real Linux-built mac binary + confirmed as `Mach-O 64-bit x86_64 executable`) and that its snapshot filesystem lets `bin/cli.js` + serve its static assets with ZERO code changes (reverted the SEA-era `root` parameter entirely — + `bin/cli.js` is now byte-identical to before this whole feature, confirmed via `git show` diff). + `build.mjs` rewritten around `pkg`'s `exec()` API (`buildOne(osKey, target)`/`buildAll(osKeys)`), + `sea-entry.cjs` deleted (no longer needed — `pkg` points directly at the real `bin/cli.js`). + Output is now a true single file per OS (no more `assets/` folder, no more zip wrapper). +- [x] **Found and fixed a real, silently-failing gotcha mid-switch**: `pkg`'s `"assets"` glob paths + resolve relative to wherever the CONFIG FILE ITSELF lives, not cwd or the entry file's directory — + confirmed via a controlled A/B test (moving the config file between directories with the identical + glob, watching assets silently stop embedding with zero error). Fixed by having `buildOne()` write + its temp pkg config directly at `REPO_ROOT` (removed in a `finally`); new WISDOM.md trap entry with + the full reproduction, since this fails completely silently at build time. +- [x] **`.github/workflows/binaries.yml` restructured to 2 jobs** (from the original 3-OS matrix): + `build-linux-win` on `ubuntu-latest` builds both those targets in one job (genuine cross-compile); + `build-mac` stays on its own `macos-latest` runner — NOT collapsed into the Linux job, because + `codesign` only exists on macOS and a completely unsigned binary may not even launch on Apple + Silicon (AMFI requires at least an ad-hoc signature) — see GAP_CONTEXT.md's new entry for the full + reasoning on why this one target intentionally isn't cross-compiled despite `pkg` technically being + able to. +- [x] **Both test files (`test_build_config.mjs`/`test_build_e2e.mjs`) rewritten for the new shape** + and re-verified with genuine RED→GREEN on the sharpest new guard (the config-colocation regression + check) — moved the temp-config write location, confirmed the test caught it (17/18), restored, + confirmed 18/18. `test_build_e2e.mjs` re-run for real against the new mechanism: 10/10, a real + binary built via `pkg` and run, serving the real app over real HTTP, with an explicit assertion + that no separate `assets/` folder exists anymore. +- [x] Per-OS `README.md`/`CHANGELOG.md` (both still-unreleased, so amended in place rather than + given a second changelog entry) updated to describe the single-file artifact and, for macOS + specifically, the "must be built on a real Mac" constraint. +- [x] Root `CHANGELOG.md`'s `[Unreleased]` section updated to match (mentions `@yao-pkg/pkg`, the + 2-job CI split, and the Red/Green test suite — no longer mentions Node SEA or `startServer`'s + `root` param, since that was fully reverted). diff --git a/docs/.scrolls/SPEC.md b/docs/.scrolls/SPEC.md index e951a19..f61e5c1 100644 --- a/docs/.scrolls/SPEC.md +++ b/docs/.scrolls/SPEC.md @@ -301,13 +301,16 @@ Word-level diff (LCS-based) highlights changed words within text/table-cell/char - **Failure behavior**: unsupported browser values or unknown options fail before the local server starts, with exit code `2` and a clear error. If the selected browser command itself is missing or cannot launch in a headless/no-GUI environment, the CLI still prints the local URL and ignores the browser-open failure, preserving the prior "URL is enough to proceed manually" behavior. - **Testing**: `src/pptxdiff/test_execfile_browser_open_cli.mjs` now covers `parseArgs()` for both `--browser=value` and `--browser value`, rejects unsupported values, and verifies all platform/browser command builders still pass the URL as a single `execFile()` argv element rather than shell-interpolating it. ## 36. Standalone native binaries (`@pptxdiff/binaries`, added this session) -- **What it does**: `src/packages/binaries/` builds a standalone, native `pptxdiff` executable per OS — download one artifact, run it, `pptxdiff` opens in the browser. No Node.js install, no `npm install -g`, no `npx`. Output lands in `src/packages/binaries/pptxdiff-win/`, `pptxdiff-mac/`, `pptxdiff-linux/` (the folder names the user requested), each holding the built binary, an `assets/` copy of the served static app files, and a `pptxdiff--.zip` bundling both — the zip is the actual downloadable artifact. Build outputs are gitignored (generated, not source); each OS folder keeps a tracked `README.md` describing what a build produces there. +- **What it does**: `src/packages/binaries/` builds a standalone, native `pptxdiff` executable per OS — download ONE file, run it, `pptxdiff` opens in the browser. No Node.js install, no `npm install -g`, no `npx`, no separate assets folder to keep alongside it. Output lands in `src/packages/binaries/pptxdiff-win/pptxdiff-win.exe`, `pptxdiff-mac/pptxdiff-mac`, `pptxdiff-linux/pptxdiff-linux` (the folder names the user requested). Build output is gitignored (generated, not source); each OS folder keeps a tracked `README.md` and `CHANGELOG.md` describing what a build produces there. - **Deliberately standalone binaries, not real OS installers**: asked directly (binaries vs. true `.msi`/`.pkg`/`.deb` installers with an install wizard, PATH registration, code signing) and the user picked standalone binaries — consistent with this project's prior explicit decision (see GAP_CONTEXT.md "Why the npm CLI opens a browser tab instead of a real native window") to avoid Electron/Tauri-style installer and code-signing overhead. See `src/packages/binaries/README.md` for the full reasoning. -- **Mechanism**: Node's [Single Executable Applications (SEA)](https://nodejs.org/api/single-executable-applications.html) feature — `build.mjs` bundles `sea-entry.cjs` (which reuses `bin/cli.js`'s existing `startServer()`/`buildBrowserOpenCommand()`, zero server-logic duplication) via `esbuild` into one flat CommonJS file, generates the SEA blob (`node --experimental-sea-config`), copies `process.execPath` and injects the blob via `postject`, then copies the same static files the npm package ships (`index.html`/`support.js`/`sample-pptx.js`/`vendor/`) into an `assets/` folder next to the binary. `bin/cli.js`'s `startServer(root = ROOT)` gained an optional `root` param (backward-compatible default, every existing caller unaffected) specifically so the SEA entry point can pass `path.dirname(process.execPath)`-relative assets instead of the npm package's own `__dirname`-relative ones, which don't exist inside a packaged single-file binary. -- **No cross-compilation**: SEA builds by injecting into a copy of the *currently running* `node` binary — there is no supported way to build a Windows `.exe` from a Linux machine. `.github/workflows/binaries.yml` runs the same build on a `windows-latest`/`macos-latest`/`ubuntu-latest` CI matrix to actually produce all three, uploaded as workflow artifacts (attaching them to GitHub Releases on a tag push is a documented follow-up, not built this session — see PLAN.md). -- **Known, documented gaps** (see GAP_ANALYSIS.md): unsigned Windows `.exe` (SmartScreen warning) and ad-hoc-signed-only macOS binary (Gatekeeper warning) — no code-signing certificate exists for this project; ~120MB per binary since SEA embeds the entire Node runtime; not yet wired to GitHub Releases. -- **Verified locally** (Linux, this sandbox): built for real (`npm install && node build.mjs`), producing a real ELF executable + `pptxdiff-linux-0.7.0.zip`; ran the built binary directly (not just `bin/cli.js`), confirmed it prints `pptxdiff running at http://localhost:` and correctly serves `index.html`/`support.js`/`vendor/*` from its own `assets/` folder via real `curl` requests. Windows/macOS builds are structurally identical (same `build.mjs`, platform-branched only for the codesign step) but unverified locally — no Windows/macOS host in this sandbox; CI will exercise them on first push. -- **Red/Green TDD, two test files** (`build.mjs` refactored with an entrypoint guard — same pattern as `capture_screenshots.mjs`, see WISDOM.md — so `PLATFORM_MAP`/`ASSET_ENTRIES`/`resolveTarget`/`buildBinary` are importable without a real build running as a side effect): - - `test_build_config.mjs` (fast, pure, no subprocess/network, part of `npm test`): 17 assertions covering `PLATFORM_MAP`'s per-OS shape, an `ASSET_ENTRIES`-vs-root-`package.json`-"files" drift guard, and — the sharpest one — a static-source regression check that `bin/cli.js`'s `startServer()` still accepts the optional `root` param this whole feature depends on. Demonstrated genuine RED→GREEN: temporarily reverted `startServer(root = ROOT)` back to the old no-param signature, confirmed exactly that one assertion failed (16/17), restored it, confirmed 17/17. - - `test_build_e2e.mjs` (slow, real, current-platform-only, separate `npm run test:e2e` script — same split as `pptxdiff-cli`'s `test:difftool`): actually calls `buildBinary()`, then spawns the REAL resulting executable and drives it over real HTTP — `GET /`, `/support.js`, `/vendor/react.production.min.js` all 200 with correct content, plus a path-traversal request confirming `isPathContained` still applies correctly to this feature's different `root` value (not just assumed because it's "the same function"). 11/11 assertions, genuinely GREEN against a real ~120MB binary built and run in this sandbox. Cleans up only what it generated (binary/`assets/`/zip) afterward, never the tracked `README.md`/`CHANGELOG.md` in the same folder — a real bug in the first draft (a blind `rm -rf` of the whole output folder, which would have deleted the tracked docs on every build) was caught and fixed before it ever ran against committed files. +- **Mechanism: `@yao-pkg/pkg`, not Node's own SEA feature.** This package originally shipped using Node's built-in Single Executable Applications (SEA) feature. Switched after an explicit user question ("why aren't you using yao-pkg/pkg?") surfaced two real advantages SEA doesn't have — see GAP_CONTEXT.md for the full reasoning and the honest tradeoff (one more third-party build-tool devDependency, dev-time only): + 1. **Real cross-compilation.** `pkg` downloads a prebuilt base `node` binary per TARGET platform and injects the bundled app into it, so one Linux host builds the Windows AND Linux binaries. SEA can only build for whatever OS it's currently running on. + 2. **Built-in asset embedding, with zero `bin/cli.js` changes.** `pkg`'s snapshot filesystem mirrors the real project's relative directory layout at runtime — `bin/cli.js`'s existing, completely UNMODIFIED `ROOT = path.join(__dirname, "..", "src", "pptxdiff")` computation resolves correctly with `build.mjs` simply listing `src/pptxdiff/index.html`/`support.js`/`sample-pptx.js`/`vendor/**` as pkg `"assets"`. (The SEA version had needed an added `root` parameter on `startServer()` plus a separate `assets/` folder shipped next to the binary — both fully reverted; `bin/cli.js` is now byte-identical to how it looked before this whole feature.) +- **A hard-won gotcha discovered mid-build** (see WISDOM.md's new trap entry): `pkg`'s `"assets"` glob paths in a config file resolve relative to wherever THAT CONFIG FILE ITSELF lives — not cwd, not the entry file's directory. Get it wrong and it fails completely silently (zero assets embedded, no warning, only a 404 when the packaged binary actually runs). `build.mjs`'s `buildOne()` writes a temp pkg config directly at the repo root (next to the real `package.json`, where `src/pptxdiff/**` actually resolves), removed in a `finally` block. +- **macOS is the one target NOT cross-compiled.** `pkg` can produce a macOS binary from Linux, but can't codesign it (`codesign` only exists on macOS) — and on Apple Silicon, a completely unsigned binary may not even launch (AMFI requires at least an ad-hoc signature, unlike Intel Macs where it's "only" a Gatekeeper warning). `buildOne()` only runs its codesign step when `process.platform === "darwin"`, warning loudly otherwise rather than silently shipping something that might not run. `.github/workflows/binaries.yml` reflects this: Windows+Linux build together in one `ubuntu-latest` job (`build-linux-win`); macOS builds separately on `macos-latest` (`build-mac`) so it's genuinely signed. +- **Known, documented gaps** (see GAP_ANALYSIS.md): unsigned Windows `.exe` (SmartScreen warning) and ad-hoc-signed-only macOS binary (Gatekeeper warning) — no code-signing certificate exists for this project; not yet wired to GitHub Releases; x64 only, no native arm64 build for any OS. +- **Verified locally** (Linux, this sandbox), for both mechanisms in turn: built for real, ran the actual packaged binary directly (not just `bin/cli.js`), confirmed it correctly serves `index.html`/`support.js`/`vendor/*` via real `curl` requests — once under Node SEA, and again after the switch to `@yao-pkg/pkg`, this time with a true single file and zero `bin/cli.js` changes. Windows/macOS builds are structurally identical (same `buildOne()`, only the mac codesign branch differs) but unverified end-to-end locally — no Windows/macOS host in this sandbox; CI exercises them for real. +- **Red/Green TDD, two test files** (`build.mjs` has an entrypoint guard — same pattern as `capture_screenshots.mjs`, see WISDOM.md — so `TARGET_MAP`/`ASSET_GLOBS`/`resolveTarget`/`buildOne`/`buildAll` are importable without a real build running as a side effect): + - `test_build_config.mjs` (fast, pure, no subprocess/network, `npm test`): 18 assertions covering `TARGET_MAP`'s per-OS shape, an `ASSET_GLOBS`-vs-root-`package.json`-"files" drift guard, that `bin/cli.js` is passed to pkg completely unmodified (no packaging-specific parameter), the macOS-signing safety checks, and — the sharpest one — a static-source regression check that `buildOne()` still writes its temp pkg config at `REPO_ROOT` (the exact gotcha above). Demonstrated genuine RED→GREEN twice in this session: once for the SEA-era `startServer(root)` contract (now retired along with SEA itself), and again for the pkg-config-colocation guard — moved the write location to `__dirname`, confirmed exactly that one assertion failed (17/18), restored it, confirmed 18/18. + - `test_build_e2e.mjs` (slow, real, current-platform-only, `npm run test:e2e` — same split as `pptxdiff-cli`'s `test:difftool`): actually calls `buildOne()`, then spawns the REAL resulting single-file executable and drives it over real HTTP — `GET /`, `/support.js`, `/vendor/react.production.min.js` all 200 with correct content, an explicit assertion that no separate `assets/` folder exists, plus a path-traversal request. 10/10 assertions, genuinely GREEN against a real binary built and run in this sandbox by BOTH mechanisms in turn. Cleans up only the binary file afterward, never the tracked `README.md`/`CHANGELOG.md` in the same folder. diff --git a/docs/.scrolls/WISDOM.md b/docs/.scrolls/WISDOM.md index 7d8438d..51262f9 100644 --- a/docs/.scrolls/WISDOM.md +++ b/docs/.scrolls/WISDOM.md @@ -222,3 +222,9 @@ ## Wisdom — PowerShell command-mode vs. expression-mode argument parsing (addendum, Chocolatey package session) - **`SomeCmdlet "a" + "b"` is NOT string concatenation in PowerShell.** Cmdlet/function calls are parsed in "command mode," where `+` between two quoted-string arguments is just another positional argument token, not the binary `+` operator — `Write-Warning "line one " + "line two"` actually passes `Write-Warning` three positional arguments (`"line one "`, `"+"`, `"line two"`), and since `Write-Warning` only accepts one positional `-Message` argument, this throws `A positional parameter cannot be found that accepts argument '+'` the first time that code path actually runs. `throw` doesn't have this problem — it's a language keyword taking a real expression, so `throw "a" + "b"` concatenates correctly. **Rule**: any multi-line message string passed to a cmdlet (`Write-Warning`, `Write-Host`, `Write-Error`, etc.) must be built as a separate variable assignment first (`$msg = "a" + "b"`, which IS expression mode) and then passed as `SomeCmdlet $msg` — never inline `+`-concatenated directly in the argument position. Found while writing `src/packages/pptxdiff-chocolatey/tools/chocolateyuninstall.ps1`, with no `pwsh` available in this sandbox to catch it by running — caught by reasoning through PowerShell's command-vs-expression parsing modes instead. Worth a deliberate second look any time a future session writes/edits `.ps1` files in this repo (currently just the Chocolatey package's `tools/*.ps1`), since there's no automated PowerShell test/lint in this project's CI to catch it otherwise. **Now permanently guarded**: `src/packages/pptxdiff-chocolatey/test_chocolatey_package.mjs`'s `hasCmdletPlusConcatBug()` regex-scans both `.ps1` scripts for exactly this pattern (a `Write-Warning`/`Write-Host`/`Write-Error`/`Write-Output`/`Write-Verbose` call followed by a quoted string then `+`) and fails the build if it's ever reintroduced — demonstrated genuinely RED (reintroduced the bug, confirmed the check caught it) before being restored GREEN, so this doesn't rely on a future session remembering to re-read this note. +## Trap — `@yao-pkg/pkg`'s `"assets"` glob paths resolve relative to the CONFIG FILE'S OWN directory, not cwd or the entry file (found switching `@pptxdiff/binaries` from Node SEA to `pkg`) +- A `pkg` config's `"assets"` array is documented as accepting glob patterns, but WHERE those patterns are resolved FROM is not obvious from the CLI's own `--help` output, and getting it wrong produces **zero error and zero warning** — the build succeeds, produces a binary of a plausible size, and only fails at RUNTIME with 404s on every asset request. This is a nastier failure mode than a build-time error: nothing in the build log tells you anything is wrong. +- **Found by a controlled A/B test, not by reading docs closely enough**: built the exact same app (entry file + relative `"assets"` glob) three different ways — (1) config file in the SAME directory as the entry/project root: assets embedded correctly, confirmed by walking `/snapshot` inside the running packaged binary; (2) config passed via `-c /absolute/path/elsewhere/config.json` with the SAME relative glob string: silently embedded NOTHING (glob resolved against the config file's own directory, which had no matching files); (3) absolute asset paths in a config file living elsewhere: STILL embedded nothing — ruling out "just make the glob absolute" as a fix, and confirming the resolution base is specifically the config file's directory, not affected by whether the glob itself is relative or absolute. +- **The concrete, verified-working fix**: write the pkg config file directly into the SAME directory as the project's real `package.json` (this project's case: `REPO_ROOT`, since `bin/cli.js`'s entry and `src/pptxdiff/**`'s assets are both relative to there) — even though that config is temporary/generated and doesn't belong there as a normal tracked file. `build.mjs`'s `buildOne()` writes it fresh before each `pkg` invocation and removes it in a `finally` block (success or failure), with a matching gitignore entry (`/.pkg-binaries-config.*.json`) as a defensive backstop if a build is ever interrupted mid-run. +- **General lesson for any build tool with a similarly "helpful" auto-detected project root**: when a tool infers "the project" from the entry file's location (rather than requiring an explicit project-root flag), assume ANY other path-bearing config value it reads is ALSO resolved relative to that same inferred root, not to wherever you happen to have put the config file or run the command from — verify this empirically (write a debug entry file that dumps what the tool actually sees, e.g. `fs.readdirSync` on whatever virtual/snapshot root it exposes) rather than trusting a relative path "should just work" from wherever felt natural to place the config. +- **Why this made choosing between mechanisms (Node SEA vs. `pkg`) worth re-litigating mid-implementation rather than shipping the first choice**: this gotcha was found DURING an explicit follow-up question ("why not use yao-pkg/pkg?") that prompted actually trying it, rather than reasoning about it in the abstract — the same spirit as this project's standing rule to verify claims empirically (real builds, real `curl` requests, real `git diff` checks) rather than trust documentation or a plausible-sounding design on paper. A tool switch motivated by a real question, tested hands-on before committing to it, surfaced both a genuine capability win (cross-compilation, confirmed with real `file` output on genuine PE32+/Mach-O binaries) AND a real gotcha (this one) that pure reading wouldn't have surfaced as concretely. diff --git a/src/packages/binaries/README.md b/src/packages/binaries/README.md index 3c14344..24a33c3 100644 --- a/src/packages/binaries/README.md +++ b/src/packages/binaries/README.md @@ -1,9 +1,8 @@ # @pptxdiff/binaries Builds standalone, native `pptxdiff` executables for Windows, macOS, and -Linux — download one file (well, one file plus its `assets/` folder, -zipped together), run it, and pptxdiff opens in your browser. No Node.js -install, no `npm install -g`, no `npx`. +Linux — download one file, run it, and pptxdiff opens in your browser. No +Node.js install, no `npm install -g`, no `npx`. This is deliberately **not** a real OS installer (no `.msi`/`.pkg`/`.deb` wizard, no PATH registration, no entry in Add/Remove Programs) — see @@ -15,73 +14,103 @@ reversing that without a corresponding ask. A standalone executable gets "download and run, no Node.js required" — the actual pain point — without that cost. +## Why `@yao-pkg/pkg`, not Node's own SEA feature + +This package originally used Node's built-in Single Executable +Applications (SEA) feature. Switched to `@yao-pkg/pkg` (the +actively-maintained community fork of the Vercel-archived `pkg`) after an +explicit question about it, for two concrete reasons SEA can't match: + +1. **Real cross-compilation.** `pkg` downloads a prebuilt "base" node + binary per target platform and injects the bundled app into it — one + Linux host can build the Windows AND Linux binaries. SEA injects into a + copy of the *currently running* node binary, so it can only ever build + for the OS it's actually running on (the old 3-OS CI matrix existed + solely to work around that). +2. **Built-in asset embedding.** `pkg`'s snapshot filesystem preserves the + real project's relative directory structure at runtime, so + `bin/cli.js`'s existing, UNMODIFIED `ROOT = path.join(__dirname, "..", + "src", "pptxdiff")` resolution just works — no `assets/` folder + shipped alongside the binary, no `root` parameter added to `bin/cli.js` + for packaging's sake. A true single file. + +The tradeoff: one more third-party build-tool devDependency (dev-time +only — never shipped in the binaries or the npm package), vs. a fork of a +project Vercel walked away from. Judged worth it for the two wins above; +see `docs/.scrolls/GAP_CONTEXT.md` for the full reasoning. + +**macOS is the one target NOT cross-compiled here.** `pkg` CAN produce a +macOS binary from Linux, but it can't codesign it (`codesign` only exists +on macOS) — and on Apple Silicon, a completely unsigned binary may not +even *launch* (AMFI requires at least an ad-hoc signature, not just a +Gatekeeper warning the way Intel Macs work). So the mac target only runs +its codesign step when actually built on a macOS host — see +`.github/workflows/binaries.yml`'s separate `build-mac` job. + ## How it works -[Node's Single Executable Applications (SEA)](https://nodejs.org/api/single-executable-applications.html) -feature injects a JS blob into a **copy of the currently-running `node` -binary**. `build.mjs`: - -1. Bundles `sea-entry.cjs` (which reuses `bin/cli.js`'s existing - `startServer()`/`buildBrowserOpenCommand()` — no server logic is - duplicated) into one flat CommonJS file via `esbuild`. -2. Generates the SEA blob (`node --experimental-sea-config`). -3. Copies `process.execPath` and injects the blob via `postject`. -4. Copies the same static app files the npm package ships - (`index.html`/`support.js`/`sample-pptx.js`/`vendor/`) into an - `assets/` folder next to the built binary — `sea-entry.cjs` resolves - `root` from `path.dirname(process.execPath)` at runtime, since a - packaged executable has no `__dirname`-relative sibling files of its - own the way an npm-installed package does. -5. Zips the binary + `assets/` into `pptxdiff--.zip`, the - actual downloadable artifact. +`build.mjs`'s `buildOne(osKey, target)`: + +1. Writes a temporary pkg config (`{"assets": [...]}` — the same static + files the npm package ships: `index.html`/`support.js`/ + `sample-pptx.js`/`vendor/**`) **directly at the repo root**, next to the + real `package.json`. This placement matters — see "A hard-won gotcha" + below. +2. Calls `@yao-pkg/pkg`'s `exec()` with `bin/cli.js` as the entry, that + config, and the target platform string (e.g. `node22-linux-x64`). +3. On the `mac` target, ad-hoc codesigns the result if running on an + actual macOS host (`codesign --sign -`); otherwise warns loudly that + the binary is unsigned rather than silently shipping it. +4. Removes the temporary config in a `finally` block, success or failure. + +### A hard-won gotcha (see `docs/.scrolls/WISDOM.md`) + +`pkg`'s `"assets"` glob paths in a config file resolve relative to +**wherever that config file itself lives** — not the process's cwd, not +the entry file's directory. Get this wrong and the failure is silent: no +error, no warning, the binary just embeds zero assets and 404s on every +request at runtime. This is why the config is written to `REPO_ROOT` +(where `src/pptxdiff/**` actually resolves) rather than kept as a normal +tracked file inside this package's own directory. ## Building locally ```sh cd src/packages/binaries npm install -npm run build # or: make pkg.binaries.build, from the repo root +npm run build # builds all three (win/mac/linux) by default +npm run build -- linux win # or build a specific subset ``` -Output lands in `./pptxdiff-/` — whichever one matches the -OS you ran this on. **SEA has no cross-compilation mode**: this only ever -builds for the platform it's currently running on. To get all three, run -it on all three platforms — `.github/workflows/binaries.yml` does exactly -that via a `windows-latest`/`macos-latest`/`ubuntu-latest` CI matrix and -uploads each as a workflow artifact. - -Each OS folder keeps a tracked `README.md` (usage/known-warnings) and -`CHANGELOG.md` (Keep a Changelog, tracks the bundled `pptxdiff` app -version) — `build.mjs` only ever removes the specific files/folders it -itself generates (the binary, `assets/`, `*.zip`), never those two, even -across repeated builds. +Output lands in `./pptxdiff-/` — one file per OS, +nothing else needed alongside it. Each OS folder keeps a tracked +`README.md` (usage/known-warnings) and `CHANGELOG.md` (Keep a Changelog, +tracks the bundled `pptxdiff` app version) — `build.mjs` only ever +touches the binary file itself, never those two. ## Testing (Red/Green TDD) ```sh -npm test # fast, pure — PLATFORM_MAP/ASSET_ENTRIES/resolveTarget shape, - # an ASSET_ENTRIES-vs-root-package.json drift guard, and a - # regression guard on bin/cli.js's startServer(root = ROOT) - # signature this whole feature depends on -npm run test:e2e # slow, real — builds an actual binary for the CURRENT host - # OS and drives it over real HTTP (index.html/support.js/ - # vendor/* + a path-traversal check), same split as - # pptxdiff-cli's `npm test` vs `npm run test:difftool` +npm test # fast, pure — TARGET_MAP/ASSET_GLOBS shape, a drift + # guard against root package.json's "files", and a + # regression guard on the config-colocation gotcha above +npm run test:e2e # slow, real — builds an actual binary for the CURRENT + # host OS and drives it over real HTTP (index.html/ + # support.js/vendor/* + a path-traversal check) ``` -`test:e2e` only exercises the current host's platform branch — the other -two OS branches are structurally identical (same `build.mjs`, only the -codesign step differs) but only actually built-and-run by CI's 3-OS -matrix. +`test:e2e` only exercises the current host's own target — win/mac are +structurally identical (same `buildOne()`, only the mac codesign branch +differs) but only actually built-and-run by CI. ## Known gaps (see `docs/.scrolls/GAP_ANALYSIS.md`) -- **Unsigned/ad-hoc-signed.** No code-signing certificate — Windows - SmartScreen and macOS Gatekeeper will warn on a freshly-downloaded copy. - Documented per-OS in each `pptxdiff-/README.md`. -- **Not attached to GitHub Releases yet.** The CI workflow currently only - uploads build artifacts on push/dispatch; wiring a release-tag trigger - to attach the zips to a GitHub Release is a follow-up, not done here. -- **~120MB per binary.** SEA embeds the entire Node runtime — there's no - way around this with the SEA approach itself (it's not a JS-only - bundle-size problem). +- **Unsigned Windows `.exe` / ad-hoc-signed-only macOS binary.** No + code-signing certificate — Windows SmartScreen and macOS Gatekeeper will + warn on a freshly-downloaded copy. Documented per-OS in each + `pptxdiff-/README.md`. +- **Not attached to GitHub Releases yet.** CI currently only uploads build + artifacts on push/dispatch; wiring a release-tag trigger to attach them + to a GitHub Release is a follow-up, not done here. +- **x64 only, no native arm64 build** for any OS (matches the original + scope) — an Apple Silicon Mac runs the x64 binary via Rosetta 2. diff --git a/src/packages/binaries/build.mjs b/src/packages/binaries/build.mjs index 9a00a75..5b3579b 100644 --- a/src/packages/binaries/build.mjs +++ b/src/packages/binaries/build.mjs @@ -1,208 +1,143 @@ #!/usr/bin/env node "use strict"; -// Builds a standalone native pptxdiff executable for the CURRENT host OS -// using Node's Single Executable Applications (SEA) feature, and drops it -// (plus the static app files it serves, plus a zip of both) into -// src/packages/binaries/pptxdiff-/. +// Builds standalone native pptxdiff executables using @yao-pkg/pkg (the +// actively-maintained fork of the Vercel-archived `pkg` — see +// docs/.scrolls/GAP_CONTEXT.md for why this was chosen over Node's own +// Single Executable Applications feature, which this package originally +// used). // -// Node SEA has no supported cross-platform mode: a SEA binary is built by -// injecting a JS blob into a COPY OF THE CURRENTLY RUNNING node executable -// (process.execPath). Building all three platforms' binaries therefore -// means running this script once per OS — see .github/workflows/binaries.yml -// for a CI matrix (ubuntu-latest/macos-latest/windows-latest) that does -// exactly that. There is no attempt here to fake cross-compilation. +// Unlike Node SEA, pkg genuinely cross-compiles: it downloads a prebuilt +// "base" node binary for each TARGET platform and injects the bundled app +// into it, so a single Linux (or any) host can build the Windows and Linux +// binaries. macOS is the one exception in THIS build — see below. // -// No code-signing certificate is available (or in scope — see -// docs/.scrolls/GAP_CONTEXT.md), so the macOS binary is only ad-hoc signed -// (runs locally, still triggers Gatekeeper's "unidentified developer" -// warning on a freshly-downloaded copy) and the Windows .exe is unsigned -// (triggers a SmartScreen warning). Documented, not silently hidden. +// Points bin/cli.js's UNMODIFIED entry point directly at pkg, and embeds +// the same static app files (index.html/support.js/sample-pptx.js/ +// vendor/**) as pkg "assets" — pkg's snapshot filesystem preserves the +// real relative directory structure (entry at snapshot `/bin/cli.js`, +// assets at snapshot `/src/pptxdiff/...`), so `bin/cli.js`'s own +// `ROOT = path.join(__dirname, "..", "src", "pptxdiff")` resolves +// correctly with ZERO code changes — no assets-folder-next-to-the-binary +// workaround needed the way Node SEA required. // -// PLATFORM_MAP/ASSET_ENTRIES/resolveTarget are exported (pure, no side -// effects) so test_build_config.mjs can assert on them without triggering -// a real build; buildBinary() is exported so test_build_e2e.mjs can run a -// real build for the current platform and drive the actual output binary. -// The entrypoint guard below (same pattern as capture_screenshots.mjs — -// see WISDOM.md) means importing this module never runs a build as a side -// effect — only `node build.mjs` (or an explicit buildBinary() call) does. +// IMPORTANT, hard-won gotcha (see WISDOM.md): pkg's "assets" glob paths in +// a config file resolve relative to WHATEVER DIRECTORY THAT CONFIG FILE +// ITSELF LIVES IN — not the process's cwd, not the entry file's directory. +// Silently: no error, no warning, it just embeds nothing if the globs +// don't match from the config's own location. The config therefore has to +// be written to REPO_ROOT (next to the real package.json) for +// "src/pptxdiff/**" to resolve — written fresh before each build and +// removed in a `finally`, since it isn't a real project file. +// +// **macOS is NOT cross-compiled from this build.** pkg can produce a +// macOS binary from Linux/Windows, but it cannot codesign it (`codesign` +// only exists on macOS) — and an entirely unsigned binary is a real +// functional problem on Apple Silicon (arm64 requires at least an ad-hoc +// signature to launch at all under AMFI, not just a Gatekeeper warning +// like on Intel). So `buildOne('mac', ...)` only runs its codesign step +// when `process.platform === 'darwin'`; on any other host it still +// produces a binary (for local experimentation) but loudly warns it's +// unsigned rather than silently shipping something that may not launch. +// .github/workflows/binaries.yml reflects this: linux+win build together +// on ubuntu-latest, mac builds separately on macos-latest. import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import JSZip from "jszip"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); -export const PLATFORM_MAP = { - win32: { osKey: "win", binName: "pptxdiff-win.exe", isWin: true, isMac: false }, - darwin: { osKey: "mac", binName: "pptxdiff-mac", isWin: false, isMac: true }, - linux: { osKey: "linux", binName: "pptxdiff-linux", isWin: false, isMac: false }, +export const TARGET_MAP = { + linux: { pkgTarget: "node22-linux-x64", binName: "pptxdiff-linux", needsMacSign: false }, + win: { pkgTarget: "node22-win-x64", binName: "pptxdiff-win.exe", needsMacSign: false }, + mac: { pkgTarget: "node22-macos-x64", binName: "pptxdiff-mac", needsMacSign: true }, }; -// Same subset root package.json's "files" ships to npm — the exact set of -// static files bin/cli.js's server actually reads from ROOT. -export const ASSET_ENTRIES = [ - ["src/pptxdiff/index.html", "index.html"], - ["src/pptxdiff/support.js", "support.js"], - ["src/pptxdiff/sample-pptx.js", "sample-pptx.js"], - ["src/pptxdiff/vendor", "vendor"], +// Same subset root package.json's "files" ships to npm — the exact static +// files bin/cli.js's server reads from ROOT. Relative to REPO_ROOT, which +// is where the temp pkg config gets written (see the file header comment +// on WHY that placement matters). +export const ASSET_GLOBS = [ + "src/pptxdiff/index.html", + "src/pptxdiff/support.js", + "src/pptxdiff/sample-pptx.js", + "src/pptxdiff/vendor/**/*", ]; -// Pure: `platform` -> PLATFORM_MAP entry, or null if unsupported. -export function resolveTarget(platform) { - return PLATFORM_MAP[platform] || null; +// Pure: osKey -> TARGET_MAP entry, or null if unknown. +export function resolveTarget(osKey) { + return TARGET_MAP[osKey] || null; } function log(osKey, msg) { console.log(`[build-binary:${osKey}] ${msg}`); } -function run(osKey, cmd, args, opts = {}) { - log(osKey, `$ ${cmd} ${args.join(" ")}`); - execFileSync(cmd, args, { stdio: "inherit", ...opts }); -} - -function cleanDir(dir) { - fs.rmSync(dir, { recursive: true, force: true }); - fs.mkdirSync(dir, { recursive: true }); -} - -// Removes only what a previous build.mjs run generated inside an OS folder -// (the binary, the copied assets/ folder, any zip artifacts) — NOT a blind -// `rm -rf` of the whole folder, which would also delete the tracked -// README.md/CHANGELOG.md that live there. Safe to call whether or not a -// prior build has ever run (nothing to remove on a fresh clone). -function cleanGeneratedOutDir(outDir, target) { +// Builds ONE OS's binary. `target` must be a TARGET_MAP entry. Returns the +// absolute path to the built executable. Writes bin.mjs's temp pkg config +// at REPO_ROOT and always removes it afterward, success or failure. +export async function buildOne(osKey, target) { + const outDir = path.join(__dirname, `pptxdiff-${osKey}`); fs.mkdirSync(outDir, { recursive: true }); - fs.rmSync(path.join(outDir, target.binName), { force: true }); - fs.rmSync(path.join(outDir, "assets"), { recursive: true, force: true }); - for (const entry of fs.readdirSync(outDir)) { - if (entry.endsWith(".zip")) fs.rmSync(path.join(outDir, entry), { force: true }); - } -} - -async function zipDir(dir, outZipPath) { - const zip = new JSZip(); - const walk = (abs, rel) => { - for (const entry of fs.readdirSync(abs, { withFileTypes: true })) { - const absChild = path.join(abs, entry.name); - const relChild = rel ? `${rel}/${entry.name}` : entry.name; - if (entry.isDirectory()) walk(absChild, relChild); - else zip.file(relChild, fs.readFileSync(absChild)); - } - }; - walk(dir, ""); - const buf = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" }); - fs.writeFileSync(outZipPath, buf); -} - -// Builds the given PLATFORM_MAP `target` (must match the CURRENT -// process.platform — SEA injects into a copy of the running node binary, -// it cannot target a different OS). Returns {outDir, binPath, zipPath}. -export async function buildBinary(target) { - const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")).version; - const outDir = path.join(__dirname, `pptxdiff-${target.osKey}`); - const buildTmp = path.join(__dirname, ".build"); - const assetsOut = path.join(outDir, "assets"); const binOut = path.join(outDir, target.binName); + fs.rmSync(binOut, { force: true }); + + const tmpConfigPath = path.join(REPO_ROOT, `.pkg-binaries-config.${osKey}.json`); + fs.writeFileSync(tmpConfigPath, JSON.stringify({ assets: ASSET_GLOBS }, null, 2)); + + try { + log(osKey, `Building ${target.pkgTarget} -> ${binOut}`); + const { exec } = await import("@yao-pkg/pkg"); + await exec([ + path.join(REPO_ROOT, "bin", "cli.js"), + "-c", + tmpConfigPath, + "-t", + target.pkgTarget, + "-o", + binOut, + ]); + + if (target.needsMacSign) { + if (process.platform === "darwin") { + log(osKey, "codesign --sign - (ad-hoc; no paid cert available — see GAP_ANALYSIS.md)"); + execFileSync("codesign", ["--sign", "-", binOut], { stdio: "inherit" }); + } else { + console.warn( + `[build-binary:${osKey}] WARNING: built on ${process.platform}, not darwin — this binary is COMPLETELY UNSIGNED (not even ad-hoc). ` + + "It may not launch at all on Apple Silicon (AMFI requires at least an ad-hoc signature). Build on a real macOS host/runner for a distributable artifact." + ); + } + } - log(target.osKey, `Building for ${process.platform} -> ${outDir}`); - cleanGeneratedOutDir(outDir, target); - cleanDir(buildTmp); - - // 1. Bundle sea-entry.cjs (which itself inlines bin/cli.js's exports) into - // a single flat CommonJS file — SEA's `main` must be one self-contained - // file; it does not resolve a script's own `require("./other-file")` - // calls at runtime. - const esbuild = await import("esbuild"); - const bundlePath = path.join(buildTmp, "bundle.cjs"); - await esbuild.build({ - entryPoints: [path.join(__dirname, "sea-entry.cjs")], - outfile: bundlePath, - bundle: true, - platform: "node", - format: "cjs", - target: "node20", - }); - - // 2. Generate the SEA config + blob. - const seaConfigPath = path.join(buildTmp, "sea-config.json"); - const blobPath = path.join(buildTmp, "sea-prep.blob"); - fs.writeFileSync( - seaConfigPath, - JSON.stringify( - { - main: bundlePath, - output: blobPath, - disableExperimentalSEAWarning: true, - }, - null, - 2 - ) - ); - run(target.osKey, process.execPath, ["--experimental-sea-config", seaConfigPath]); - - // 3. Copy the currently-running node executable as the base, then inject - // the blob into it. - fs.copyFileSync(process.execPath, binOut); - fs.chmodSync(binOut, 0o755); - - if (target.isMac) { - // Required by Node's SEA guide: an existing signature on the copied - // node binary must be removed before injecting, or postject's write - // corrupts it. - run(target.osKey, "codesign", ["--remove-signature", binOut]); - } - - run(target.osKey, "npx", [ - "--no-install", - "postject", - binOut, - "NODE_SEA_BLOB", - blobPath, - "--sentinel-fuse", - "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2", - ...(target.isMac ? ["--macho-segment-name", "NODE_SEA"] : []), - ]); - - if (target.isMac) { - // Ad-hoc signature (no cert) so the binary can run locally at all; - // Gatekeeper still warns on a freshly-downloaded copy — see the file - // header comment and GAP_ANALYSIS.md. - run(target.osKey, "codesign", ["--sign", "-", binOut]); + if (!target.binName.endsWith(".exe")) fs.chmodSync(binOut, 0o755); + log(osKey, `Done: ${binOut}`); + return binOut; + } finally { + fs.rmSync(tmpConfigPath, { force: true }); } - if (!target.isWin) fs.chmodSync(binOut, 0o755); +} - // 4. Copy the static app assets the server reads from `root`. - fs.mkdirSync(assetsOut, { recursive: true }); - for (const [srcRel, destRel] of ASSET_ENTRIES) { - const src = path.join(REPO_ROOT, srcRel); - const dest = path.join(assetsOut, destRel); - fs.cpSync(src, dest, { recursive: true }); +// Builds every osKey in `osKeys` (default: all three — a reasonable local- +// dev default since pkg CAN cross-compile all three from one machine; the +// mac-signing caveat above still applies). Returns { [osKey]: binPath }. +export async function buildAll(osKeys = Object.keys(TARGET_MAP)) { + const results = {}; + for (const osKey of osKeys) { + const target = resolveTarget(osKey); + if (!target) throw new Error(`Unknown osKey "${osKey}" (expected one of ${Object.keys(TARGET_MAP).join(", ")})`); + results[osKey] = await buildOne(osKey, target); } - - // 5. Zip the binary + assets together as the actual downloadable artifact. - const zipPath = path.join(outDir, `pptxdiff-${target.osKey}-${PKG_VERSION}.zip`); - await zipDir(outDir, zipPath); - - fs.rmSync(buildTmp, { recursive: true, force: true }); - log(target.osKey, `Done: ${binOut}`); - log(target.osKey, `Done: ${zipPath}`); - return { outDir, binPath: binOut, zipPath }; + return results; } if (process.argv[1] === fileURLToPath(import.meta.url)) { - const target = resolveTarget(process.platform); - if (!target) { - console.error(`No SEA build mapping for process.platform=${process.platform} (supported: win32, darwin, linux).`); + const requested = process.argv.slice(2); + buildAll(requested.length ? requested : undefined).catch((e) => { + console.error(e); process.exitCode = 1; - } else { - buildBinary(target).catch((e) => { - console.error(e); - process.exitCode = 1; - }); - } + }); } diff --git a/src/packages/binaries/package-lock.json b/src/packages/binaries/package-lock.json index 9b7f340..6e73b4f 100644 --- a/src/packages/binaries/package-lock.json +++ b/src/packages/binaries/package-lock.json @@ -9,18 +9,142 @@ "version": "0.1.0", "license": "Apache-2.0", "devDependencies": { - "esbuild": "^0.24.0", - "jszip": "^3.10.1", - "postject": "^1.0.0-alpha.6" + "@yao-pkg/pkg": "^6.22.0" }, "engines": { "node": ">=20" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -35,9 +159,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -52,9 +176,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -69,9 +193,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -86,9 +210,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -103,9 +227,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -120,9 +244,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -137,9 +261,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -154,9 +278,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -171,9 +295,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -188,9 +312,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -205,9 +329,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -222,9 +346,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -239,9 +363,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -256,9 +380,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -273,9 +397,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -290,9 +414,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -307,9 +431,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -324,9 +448,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -341,9 +465,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -358,9 +482,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -374,10 +498,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -392,9 +533,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -409,9 +550,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -426,9 +567,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -442,139 +583,435 @@ "node": ">=18" } }, - "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || >=14" + "node": ">=6.0.0" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@roberts_lando/vfs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@roberts_lando/vfs/-/vfs-0.3.3.tgz", + "integrity": "sha512-YjkxVSLw5WMZQoARaryRAjcxA+GbBzWMJdwYZX5oLUt9cC/gew9as4Dn7tcLzPp7BPoR221VpTZ+78TRPawnjg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 22" + } + }, + "node_modules/@yao-pkg/pkg": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-6.22.0.tgz", + "integrity": "sha512-u+ZgwLsvEFB+Q1rA+IGymgxnm+anl1qGJWsTH6hDHyy2cCiOvMteDNK7NgqFYxN/zdithtORIlCsYSyhAXAgQA==", "dev": true, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "@roberts_lando/vfs": "^0.3.3", + "@yao-pkg/pkg-fetch": "3.6.5", + "esbuild": "^0.28.1", + "into-stream": "^9.1.0", + "multistream": "^4.1.0", + "picocolors": "^1.1.0", + "picomatch": "^4.0.2", + "postject": "^1.0.0-alpha.6", + "prebuild-install": "^7.1.1", + "resolve": "^1.22.10", + "resolve.exports": "^2.0.3", + "stream-meter": "^1.0.4", + "tar": "^7.5.7", + "tinyglobby": "^0.2.11", + "unzipper": "^0.12.3" + }, "bin": { - "esbuild": "bin/esbuild" + "pkg": "lib-es5/bin.js" }, "engines": { - "node": ">=18" + "node": ">=22.0.0" + } + }, + "node_modules/@yao-pkg/pkg-fetch": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.6.5.tgz", + "integrity": "sha512-Sd1Hff7imsF2rcZ2GLQuIzVm2fc3Za+nE4KSWPTwIYegN/r90UFg3bq0MLSugbWQaVht72zzxV0MiKE0wvT1zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^3.1.1", + "undici": "^7.28.0", + "yargs": "^16.2.0" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.7.tgz", + "integrity": "sha512-o8CRCiJtib+ycO3mE4A5UChtGX4dDP2XxsWVu9P+Zc3H8tcmKwNVEDoDTXmwN+uuMhfKeT7/i7Y26xS8W7ohoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true, "license": "MIT" }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", + "license": "ISC", "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { - "immediate": "~3.0.5" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "(MIT AND Zlib)" + "license": "MIT" }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^9.4.0" + "ms": "^2.1.3" }, - "bin": { - "postject": "dist/cli.js" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } }, - "node_modules/readable-stream": { + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", @@ -590,21 +1027,14 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/safe-buffer": { + "node_modules/duplexer2/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "license": "MIT" }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/string_decoder": { + "node_modules/duplexer2/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", @@ -614,12 +1044,1050 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/into-stream": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-9.1.0.tgz", + "integrity": "sha512-DRsRnQrbzdFjaQ1oe4C6/EIUymIOEix1qROEJTF9dbMq+M4Zrm6VaLp6SD/B9IsiEjPZuBSnWWFN+udajugdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/stream-meter/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-meter/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-meter/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } } } } diff --git a/src/packages/binaries/package.json b/src/packages/binaries/package.json index 89411a7..1bb8e45 100644 --- a/src/packages/binaries/package.json +++ b/src/packages/binaries/package.json @@ -1,7 +1,7 @@ { "name": "@pptxdiff/binaries", "version": "0.1.0", - "description": "Build script producing standalone native pptxdiff executables (Node Single Executable Applications) for Windows, macOS, and Linux — no separate Node.js install required to run them.", + "description": "Build script producing standalone native pptxdiff executables (via @yao-pkg/pkg) for Windows, macOS, and Linux — no separate Node.js install required to run them.", "license": "Apache-2.0", "private": true, "author": { @@ -15,9 +15,7 @@ "test:e2e": "node test_build_e2e.mjs" }, "devDependencies": { - "esbuild": "^0.24.0", - "jszip": "^3.10.1", - "postject": "^1.0.0-alpha.6" + "@yao-pkg/pkg": "^6.22.0" }, "engines": { "node": ">=20" @@ -32,8 +30,7 @@ "pptx", "powerpoint", "diff", - "sea", - "single-executable-application", + "pkg", "native-binary" ] } diff --git a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md index 5272af2..0af4e35 100644 --- a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md @@ -15,18 +15,23 @@ since the binary has no independent feature set of its own. ### Added -- First standalone Linux executable, built via Node's Single Executable - Applications feature (see `../README.md` and - `docs/.scrolls/SPEC.md` §32) — download `pptxdiff-linux-0.7.0.zip`, - unzip, `chmod +x pptxdiff-linux && ./pptxdiff-linux`. No separate - Node.js install required. -- Verified end-to-end in this project's own dev sandbox: built for real, - the actual packaged binary was run and confirmed to correctly serve - `index.html`/`support.js`/`vendor/*` over real HTTP requests (see +- First standalone Linux executable, built via `@yao-pkg/pkg` (see + `../README.md` and `docs/.scrolls/SPEC.md` §32) — download + `pptxdiff-linux`, `chmod +x pptxdiff-linux && ./pptxdiff-linux`. A true + single file (Node runtime and the static app files it serves are both + embedded inside it) — no separate Node.js install, no companion folder + needed. +- Genuinely cross-compiled: this binary can be built from any host OS, not + just Linux itself. +- Verified end-to-end in this project's own dev sandbox, twice: once + against the original Node-SEA-based mechanism, and again after switching + to `@yao-pkg/pkg` — the actual packaged binary was built and run for + real, confirmed to correctly serve `index.html`/`support.js`/`vendor/*` + over real HTTP requests with zero code changes to `bin/cli.js` (see `../test_build_e2e.mjs`). ### Known limitations - Not yet attached to GitHub Releases — built by - `.github/workflows/binaries.yml`'s CI matrix and available as a workflow - artifact. + `.github/workflows/binaries.yml`'s `build-linux-win` job and available + as a workflow artifact. diff --git a/src/packages/binaries/pptxdiff-linux/README.md b/src/packages/binaries/pptxdiff-linux/README.md index 451c34d..5388dc9 100644 --- a/src/packages/binaries/pptxdiff-linux/README.md +++ b/src/packages/binaries/pptxdiff-linux/README.md @@ -1,23 +1,23 @@ # pptxdiff for Linux This folder holds the built Linux artifact — not committed here, generated -by `../build.mjs` (run on a Linux host or Linux CI runner; see -`../README.md`). +by `../build.mjs` (see `../README.md`; can be built from any host OS, +`@yao-pkg/pkg` cross-compiles it too — no Linux machine strictly needed, +though this one's easiest to verify on Linux itself). After a build, this folder contains: -- `pptxdiff-linux` — the standalone executable (bundles the Node runtime; - no separate Node.js install needed to run it). -- `assets/` — the static app files it serves (must stay next to the - binary). -- `pptxdiff-linux-.zip` — the two above, zipped, as the actual - downloadable artifact. +- `pptxdiff-linux` — the standalone executable. A true single file: the + Node runtime AND the static app files it serves are both embedded + inside it. No separate folder needed alongside it. Run it with `chmod +x pptxdiff-linux && ./pptxdiff-linux` (the build -already sets the executable bit; re-set it if you unzipped the artifact +already sets the executable bit; re-set it if you moved/copied it somewhere that dropped it). -To build: `cd src/packages/binaries && npm install && npm run build` (from -a Linux machine — Node's Single Executable Applications feature builds -from the currently-running platform's own Node binary, it doesn't -cross-compile). +Verified end-to-end in this project's own dev sandbox: built for real, the +actual packaged binary was run and confirmed to correctly serve +`index.html`/`support.js`/`vendor/*` over real HTTP requests, with zero +code changes to `bin/cli.js` itself (see `../test_build_e2e.mjs`). + +To build: `cd src/packages/binaries && npm install && npm run build -- linux`. diff --git a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md index 1c2ad9f..b2e93b9 100644 --- a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md @@ -15,18 +15,24 @@ since the binary has no independent feature set of its own. ### Added -- First standalone macOS executable, built via Node's Single Executable - Applications feature (see `../README.md` and - `docs/.scrolls/SPEC.md` §32) — download `pptxdiff-mac-0.7.0.zip`, unzip, - run `./pptxdiff-mac`. No separate Node.js install required. +- First standalone macOS executable, built via `@yao-pkg/pkg` (see + `../README.md` and `docs/.scrolls/SPEC.md` §32) — download + `pptxdiff-mac`, run it. A true single file (Node runtime and the static + app files it serves are both embedded inside it) — no separate Node.js + install, no companion folder needed. ### Known limitations +- **Must be built on an actual macOS host, not cross-compiled.** Unlike + the Windows/Linux targets, this one needs `codesign` (macOS-only) for a + usable result — an unsigned build may not even launch on Apple Silicon. + See `../README.md`. - **Ad-hoc signed, not notarized.** No Apple Developer ID — Gatekeeper will likely block a freshly-downloaded copy ("cannot be opened because the developer cannot be verified"); right-click → Open, or `xattr -d com.apple.quarantine pptxdiff-mac` first. See `../README.md` and `docs/.scrolls/GAP_ANALYSIS.md`. +- x64 only — no native arm64 build; runs via Rosetta 2 on Apple Silicon. - Not yet attached to GitHub Releases — built by - `.github/workflows/binaries.yml`'s CI matrix and available as a workflow - artifact. + `.github/workflows/binaries.yml`'s dedicated `build-mac` job and + available as a workflow artifact. diff --git a/src/packages/binaries/pptxdiff-mac/README.md b/src/packages/binaries/pptxdiff-mac/README.md index 42b4601..5f49f1c 100644 --- a/src/packages/binaries/pptxdiff-mac/README.md +++ b/src/packages/binaries/pptxdiff-mac/README.md @@ -1,16 +1,17 @@ # pptxdiff for macOS This folder holds the built macOS artifact — not committed here, generated -by `../build.mjs` (run on a macOS host or macOS CI runner; see -`../README.md`). +by `../build.mjs`. **Must be built on an actual macOS host** (or +`macos-latest` CI runner) — see `../README.md`'s "Why `@yao-pkg/pkg`" +section for why this one target isn't cross-compiled: it needs `codesign` +(macOS-only) to be ad-hoc signed, without which the binary may not even +launch on Apple Silicon. After a build, this folder contains: -- `pptxdiff-mac` — the standalone executable (bundles the Node runtime; no - separate Node.js install needed to run it), ad-hoc signed. -- `assets/` — the static app files it serves (must stay next to the binary). -- `pptxdiff-mac-.zip` — the two above, zipped, as the actual - downloadable artifact. +- `pptxdiff-mac` — the standalone executable, ad-hoc signed. A true + single file: the Node runtime AND the static app files it serves are + both embedded inside it. **Ad-hoc signed, not notarized.** There is no Apple Developer ID certificate for this project, so Gatekeeper will likely block a @@ -20,7 +21,7 @@ developer cannot be verified") — right-click the binary → Open, or run `docs/.scrolls/GAP_ANALYSIS.md` for why this is a documented, accepted tradeoff rather than an oversight. -To build: `cd src/packages/binaries && npm install && npm run build` (from -a macOS machine — Node's Single Executable Applications feature builds -from the currently-running platform's own Node binary, it doesn't -cross-compile). +To build (on macOS only, for a properly-signed result): +`cd src/packages/binaries && npm install && npm run build -- mac`. Building +this target from Linux/Windows produces an unsigned binary that likely +won't launch on Apple Silicon — `build.mjs` warns loudly if you try. diff --git a/src/packages/binaries/pptxdiff-win/CHANGELOG.md b/src/packages/binaries/pptxdiff-win/CHANGELOG.md index 969b580..fe7f13f 100644 --- a/src/packages/binaries/pptxdiff-win/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-win/CHANGELOG.md @@ -15,10 +15,13 @@ since the binary has no independent feature set of its own. ### Added -- First standalone Windows executable, built via Node's Single Executable - Applications feature (see `../README.md` and - `docs/.scrolls/SPEC.md` §32) — download `pptxdiff-win-0.7.0.zip`, unzip, - run `pptxdiff-win.exe`. No separate Node.js install required. +- First standalone Windows executable, built via `@yao-pkg/pkg` (see + `../README.md` and `docs/.scrolls/SPEC.md` §32) — download + `pptxdiff-win.exe`, run it. A true single file (Node runtime and the + static app files it serves are both embedded inside it) — no separate + Node.js install, no companion folder needed. +- Genuinely cross-compiled: this binary can be built from any host OS + (Linux, macOS, or Windows), not just Windows itself. ### Known limitations @@ -27,5 +30,5 @@ since the binary has no independent feature set of its own. info" → "Run anyway". See `../README.md` and `docs/.scrolls/GAP_ANALYSIS.md`. - Not yet attached to GitHub Releases — built by - `.github/workflows/binaries.yml`'s CI matrix and available as a workflow - artifact. + `.github/workflows/binaries.yml`'s `build-linux-win` job and available + as a workflow artifact. diff --git a/src/packages/binaries/pptxdiff-win/README.md b/src/packages/binaries/pptxdiff-win/README.md index 22f5281..73bbae4 100644 --- a/src/packages/binaries/pptxdiff-win/README.md +++ b/src/packages/binaries/pptxdiff-win/README.md @@ -1,16 +1,15 @@ # pptxdiff for Windows -This folder holds the built Windows artifact — not committed here, generated -by `../build.mjs` (run on a Windows host or Windows CI runner; see -`../README.md`). +This folder holds the built Windows artifact — not committed here, +generated by `../build.mjs` (see `../README.md`; can be built from ANY +host OS, `@yao-pkg/pkg` cross-compiles it — no Windows machine needed). After a build, this folder contains: -- `pptxdiff-win.exe` — the standalone executable (bundles the Node runtime; - no separate Node.js install needed to run it). -- `assets/` — the static app files it serves (must stay next to the `.exe`). -- `pptxdiff-win-.zip` — the two above, zipped, as the actual - downloadable artifact. +- `pptxdiff-win.exe` — the standalone executable. A true single file: the + Node runtime AND the static app files it serves are both embedded + inside it. No separate folder needed alongside it, no Node.js install + needed to run it. **Unsigned.** There is no code-signing certificate for this project, so Windows SmartScreen will likely warn on first run ("Windows protected your @@ -18,7 +17,6 @@ PC") — click "More info" → "Run anyway". See `docs/.scrolls/GAP_ANALYSIS.md` for why this is a documented, accepted tradeoff rather than an oversight. -To build: `cd src/packages/binaries && npm install && npm run build` (from -a Windows machine — Node's Single Executable Applications feature builds -from the currently-running platform's own Node binary, it doesn't -cross-compile). +To build: `cd src/packages/binaries && npm install && npm run build -- win` +(works from Linux, macOS, or Windows — this target is genuinely +cross-compiled). diff --git a/src/packages/binaries/sea-entry.cjs b/src/packages/binaries/sea-entry.cjs deleted file mode 100644 index b24ef7b..0000000 --- a/src/packages/binaries/sea-entry.cjs +++ /dev/null @@ -1,39 +0,0 @@ -"use strict"; - -// Entry point bundled (via esbuild) into a single CommonJS file and then -// embedded into a copy of the Node executable via Node's Single Executable -// Applications feature (`--experimental-sea-config` + `postject`) — see -// build.mjs. Requiring "../../../bin/cli.js" is resolved by esbuild at -// BUILD time (it inlines the file's contents into the bundle); nothing in -// the packaged binary does a runtime `require()` of a path outside itself. -const path = require("node:path"); -const { execFile } = require("node:child_process"); -const { startServer, buildBrowserOpenCommand } = require("../../../bin/cli.js"); - -// A packaged single-executable binary has no meaningful sibling files of -// its own (bin/cli.js's module-level `ROOT`, computed from its *build-time* -// `__dirname`, is unused here on purpose — see startServer()'s `root` -// param). build.mjs copies this project's static app files into an -// "assets" folder placed next to the built executable; resolving from -// `process.execPath` (where THIS binary actually lives on disk right now) -// is the only location that's true regardless of where a user unzips it. -const ROOT = path.join(path.dirname(process.execPath), "assets"); - -const LITE_MODE = ["1", "y", "yes", "true"].includes( - String(process.env.PPTXDIFF_LITE_MODE || "").trim().toLowerCase() -); - -startServer(ROOT) - .then(({ url: baseUrl }) => { - const url = `${baseUrl}${LITE_MODE ? "/?lite=1" : ""}`; - console.log(`pptxdiff running at ${url}`); - if (LITE_MODE) { - console.log("PPTXDIFF_LITE_MODE is set — loading React/ReactDOM/Babel/JSZip/pptx-renderer/fonts from their original CDNs instead of the vendored local copies."); - } - const { command, args } = buildBrowserOpenCommand(process.platform, url); - execFile(command, args, () => {}); // ignore failure (e.g. headless/no GUI) — URL is printed above regardless - }) - .catch((e) => { - console.error(e && e.message ? e.message : e); - process.exitCode = 1; - }); diff --git a/src/packages/binaries/test_build_config.mjs b/src/packages/binaries/test_build_config.mjs index 4b6c5bb..16ec9b7 100644 --- a/src/packages/binaries/test_build_config.mjs +++ b/src/packages/binaries/test_build_config.mjs @@ -2,18 +2,16 @@ "use strict"; // Fast, pure regression checks for build.mjs's build CONFIGURATION — no -// real SEA build, no subprocess, no network. Complements test_build_e2e.mjs -// (which actually builds and runs a real binary but is slow/heavy) the -// same way this project's other packages split a fast pure-unit suite from -// a slower real-process/real-browser one (e.g. pptxdiff-cli's `npm test` -// vs `npm run test:difftool`). +// real pkg invocation, no network, no subprocess beyond reading files. +// Complements test_build_e2e.mjs (real build + real run, slow). Same +// fast/slow split as pptxdiff-cli's `npm test` vs `npm run test:difftool`. // // Run: node test_build_config.mjs import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { PLATFORM_MAP, ASSET_ENTRIES, resolveTarget } from "./build.mjs"; +import { TARGET_MAP, ASSET_GLOBS, resolveTarget } from "./build.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); @@ -29,73 +27,86 @@ function assert(name, cond) { } } -// --- PLATFORM_MAP / resolveTarget --- -assert("PLATFORM_MAP has exactly win32/darwin/linux keys", ( - JSON.stringify(Object.keys(PLATFORM_MAP).sort()) === JSON.stringify(["darwin", "linux", "win32"]) +// --- TARGET_MAP / resolveTarget --- +assert("TARGET_MAP has exactly linux/win/mac keys", ( + JSON.stringify(Object.keys(TARGET_MAP).sort()) === JSON.stringify(["linux", "mac", "win"]) )); -assert("win32 maps to osKey=win, binName ends .exe, isWin=true", ( - PLATFORM_MAP.win32.osKey === "win" && PLATFORM_MAP.win32.binName === "pptxdiff-win.exe" && PLATFORM_MAP.win32.isWin === true +assert("linux target: node22-linux-x64, no .exe suffix, doesn't need mac signing", ( + TARGET_MAP.linux.pkgTarget === "node22-linux-x64" && !TARGET_MAP.linux.binName.includes(".") && TARGET_MAP.linux.needsMacSign === false )); -assert("darwin maps to osKey=mac, isMac=true, isWin=false", ( - PLATFORM_MAP.darwin.osKey === "mac" && PLATFORM_MAP.darwin.isMac === true && PLATFORM_MAP.darwin.isWin === false +assert("win target: node22-win-x64, binName ends .exe, doesn't need mac signing", ( + TARGET_MAP.win.pkgTarget === "node22-win-x64" && TARGET_MAP.win.binName.endsWith(".exe") && TARGET_MAP.win.needsMacSign === false )); -assert("linux maps to osKey=linux, isMac=false, isWin=false, no .exe suffix", ( - PLATFORM_MAP.linux.osKey === "linux" && PLATFORM_MAP.linux.isMac === false && PLATFORM_MAP.linux.isWin === false && !PLATFORM_MAP.linux.binName.includes(".") +assert("mac target: node22-macos-x64, needsMacSign true (the whole reason it's built separately in CI)", ( + TARGET_MAP.mac.pkgTarget === "node22-macos-x64" && TARGET_MAP.mac.needsMacSign === true )); -assert("resolveTarget('win32') === PLATFORM_MAP.win32", resolveTarget("win32") === PLATFORM_MAP.win32); -assert("resolveTarget returns null for an unsupported platform", resolveTarget("aix") === null); -assert("resolveTarget returns null for a made-up platform string", resolveTarget("not-a-real-platform") === null); +assert("resolveTarget('linux') === TARGET_MAP.linux", resolveTarget("linux") === TARGET_MAP.linux); +assert("resolveTarget returns null for an unknown osKey", resolveTarget("solaris") === null); +assert("resolveTarget returns null for an empty string", resolveTarget("") === null); -// --- ASSET_ENTRIES drift guard: must match root package.json's "files" --- -// (mirrors the project's existing fixture-drift-check concern — see -// GAP_ANALYSIS.md's "Fixture drift-check" ticket — applied here to the -// asset set a packaged binary ships, so it can never silently diverge from +// --- ASSET_GLOBS drift guard: must match root package.json's "files" --- +// (same fixture-drift-check concern this project already tracks elsewhere +// — see GAP_ANALYSIS.md's "Fixture drift-check" ticket — applied here so +// the asset set a packaged binary embeds can never silently diverge from // what the npm package itself ships.) const rootPkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")); const npmStaticFiles = rootPkg.files.filter((f) => f.startsWith("src/pptxdiff/")); -const assetSrcPaths = ASSET_ENTRIES.map(([src]) => src).sort(); +// ASSET_GLOBS uses a "/**/*" suffix for the vendor directory (a pkg glob +// requirement — bare "vendor" alone does not recurse); normalize before +// comparing against package.json's plain-directory "vendor" entry. +const assetGlobsNormalized = ASSET_GLOBS.map((g) => g.replace(/\/\*\*\/\*$/, "")).sort(); assert( - `ASSET_ENTRIES source paths match root package.json "files" static app subset (got ${JSON.stringify(assetSrcPaths)} vs ${JSON.stringify([...npmStaticFiles].sort())})`, - JSON.stringify(assetSrcPaths) === JSON.stringify([...npmStaticFiles].sort()) + `ASSET_GLOBS (normalized) match root package.json "files" static app subset (got ${JSON.stringify(assetGlobsNormalized)} vs ${JSON.stringify([...npmStaticFiles].sort())})`, + JSON.stringify(assetGlobsNormalized) === JSON.stringify([...npmStaticFiles].sort()) ); -assert("every ASSET_ENTRIES source path exists on disk", ( - ASSET_ENTRIES.every(([src]) => fs.existsSync(path.join(REPO_ROOT, src))) +assert("every ASSET_GLOBS entry's literal (non-glob) prefix exists on disk", ( + ASSET_GLOBS.every((g) => fs.existsSync(path.join(REPO_ROOT, g.replace(/\/\*\*\/\*$/, "")))) )); -assert("every ASSET_ENTRIES dest path is a plain relative name (no traversal)", ( - ASSET_ENTRIES.every(([, dest]) => !dest.includes("..") && !path.isAbsolute(dest)) + +// --- pkg's config-colocation requirement (see WISDOM.md's trap entry): --- +// buildOne() MUST write its temp pkg config directly at REPO_ROOT (next to +// the real package.json) — pkg resolves "assets" glob paths relative to +// wherever the CONFIG FILE ITSELF lives, not cwd, not the entry file's +// directory. A regression here fails SILENTLY at build time (pkg embeds +// zero assets, no error) and only shows up as 404s when the binary is +// actually run — exactly the kind of bug this static check exists to +// catch before it ever reaches test_build_e2e.mjs. +const buildSrc = fs.readFileSync(path.join(__dirname, "build.mjs"), "utf8"); +assert("buildOne() writes its temp pkg config at REPO_ROOT, not __dirname or cwd", ( + /tmpConfigPath\s*=\s*path\.join\(REPO_ROOT,/.test(buildSrc) +)); +assert("buildOne() removes the temp pkg config in a finally block", ( + /finally\s*\{[^}]*rmSync\(tmpConfigPath/.test(buildSrc) +)); +assert("ASSET_GLOBS are relative (repo-root-relative) paths, not absolute", ( + ASSET_GLOBS.every((g) => !path.isAbsolute(g)) )); -// --- bin/cli.js contract sea-entry.cjs depends on --- -// A regression guard, not a design assertion: if a future edit to -// bin/cli.js drops startServer()'s optional `root` param (or its default), -// the packaged binary silently breaks (it would try to serve from the npm -// package's own ROOT instead of the assets folder next to the executable) -// with no error at build time — only a confusing 404 at runtime. Catch it -// here instead, the same static-source-check pattern WISDOM.md's -// "stale renderVals binding" entry established for a similar class of -// silent-breakage risk. +// --- bin/cli.js is passed to pkg UNMODIFIED — no assets-folder workaround --- +// This is the whole point of switching to pkg (see GAP_CONTEXT.md): the +// packaged binary should need zero special-casing in bin/cli.js itself. const cliSrc = fs.readFileSync(path.join(REPO_ROOT, "bin", "cli.js"), "utf8"); -assert("bin/cli.js's startServer() still accepts an optional root param defaulting to ROOT", ( - /function startServer\(root\s*=\s*ROOT\)/.test(cliSrc) +assert("bin/cli.js's startServer() takes no parameters (no packaging-specific root override)", ( + /function startServer\(\) \{/.test(cliSrc) )); -assert("bin/cli.js's startServer() still exports (module.exports includes startServer)", ( +assert("bin/cli.js still exports startServer for pptxdiff-cli's reuse", ( /module\.exports\s*=\s*\{[^}]*startServer[^}]*\}/.test(cliSrc) )); +assert("build.mjs points pkg directly at the real bin/cli.js (no wrapper entry file)", ( + /path\.join\(REPO_ROOT, "bin", "cli\.js"\)/.test(buildSrc) +)); -// --- sea-entry.cjs's own asset-resolution contract --- -const entrySrc = fs.readFileSync(path.join(__dirname, "sea-entry.cjs"), "utf8"); -assert("sea-entry.cjs resolves ROOT relative to process.execPath, not __dirname", ( - entrySrc.includes("path.dirname(process.execPath)") && /const ROOT = path\.join\(path\.dirname\(process\.execPath\)/.test(entrySrc) +// --- macOS signing safety: never silently ship an unsigned mac binary --- +assert("buildOne() warns explicitly when building the mac target off of a non-darwin host", ( + /WARNING.*UNSIGNED/.test(buildSrc) )); -assert("sea-entry.cjs passes ROOT into startServer() explicitly", ( - /startServer\(ROOT\)/.test(entrySrc) +assert("buildOne() only runs codesign when process.platform === \"darwin\"", ( + /process\.platform === "darwin"/.test(buildSrc) )); -// --- package.json devDependencies actually present --- +// --- package.json devDependency present --- const binPkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")); -for (const dep of ["esbuild", "jszip", "postject"]) { - assert(`package.json devDependencies includes ${dep}`, Boolean(binPkg.devDependencies && binPkg.devDependencies[dep])); -} +assert("package.json devDependencies includes @yao-pkg/pkg", Boolean(binPkg.devDependencies && binPkg.devDependencies["@yao-pkg/pkg"])); console.log(`build-config check: ${pass}/${pass + fail} passed`); if (fail > 0) { diff --git a/src/packages/binaries/test_build_e2e.mjs b/src/packages/binaries/test_build_e2e.mjs index 3cceef4..78b1dca 100644 --- a/src/packages/binaries/test_build_e2e.mjs +++ b/src/packages/binaries/test_build_e2e.mjs @@ -1,32 +1,33 @@ #!/usr/bin/env node "use strict"; -// Real, slow, current-platform-only end-to-end check: actually runs -// buildBinary() (the same code `node build.mjs` runs), then spawns the -// REAL packaged executable it produced and drives it over real HTTP — -// same spirit as pptxdiff-cli's *_e2e.mjs files (real browser, real -// spawned process) rather than mocking any of this. Deliberately kept out -// of the default `npm test` (this alone takes well over a minute and -// produces a ~100MB+ binary) — run explicitly via `npm run test:e2e`, -// mirroring pptxdiff-cli's `test:difftool` split for the same reason -// (a slow/heavy check that needs real platform resources). +// Real, slow, current-platform-only end-to-end check: actually calls +// buildOne() for the CURRENT host's OS (the same code `node build.mjs` +// runs), then spawns the REAL resulting single-file executable and drives +// it over real HTTP. Deliberately kept out of the default `npm test` (a +// real pkg build downloads/uses a base binary and takes a while) — run via +// `npm run test:e2e`, same split as pptxdiff-cli's `test:difftool`. // -// Only exercises the CURRENT host's platform branch (Node SEA has no -// cross-platform build mode — see build.mjs's header comment) — the other -// two OS branches are structurally identical but only really exercised by -// CI's 3-OS matrix (.github/workflows/binaries.yml). +// Only exercises the CURRENT host's own platform target — win/mac builds +// are structurally identical (same buildOne(), only the mac codesign step +// differs) but only actually built-and-run by CI's linux+win / +// macos-specific jobs (see .github/workflows/binaries.yml and +// build.mjs's header comment for why mac isn't cross-built here). // // Run: node test_build_e2e.mjs import { execFile } from "node:child_process"; import fs from "node:fs"; import http from "node:http"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { buildBinary, resolveTarget } from "./build.mjs"; +import { buildOne, resolveTarget } from "./build.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const HOST_TO_OSKEY = { win32: "win", darwin: "mac", linux: "linux" }; + let pass = 0; let fail = 0; function assert(name, cond) { @@ -75,32 +76,32 @@ function waitForLine(child, matcher, timeoutMs) { } async function main() { - const target = resolveTarget(process.platform); + const osKey = HOST_TO_OSKEY[process.platform]; + const target = osKey && resolveTarget(osKey); if (!target) { - console.error(`No SEA build mapping for process.platform=${process.platform} — nothing to e2e-test here.`); + console.error(`No target mapping for process.platform=${process.platform} — nothing to e2e-test here.`); process.exitCode = 1; return; } - console.log(`Building a real ${target.osKey} binary (this takes a while)...`); - const { outDir, binPath, zipPath } = await buildBinary(target); + console.log(`Building a real ${osKey} binary via @yao-pkg/pkg (this takes a while, downloads a base binary on first run)...`); + const binPath = await buildOne(osKey, target); assert("build produced the binary file", fs.existsSync(binPath)); - assert("build produced the assets folder", fs.existsSync(path.join(outDir, "assets", "index.html"))); - assert("build produced the zip artifact", fs.existsSync(zipPath)); - if (!target.isWin) { + assert("no separate assets/ folder needed (pkg embeds them in the one file)", !fs.existsSync(path.join(path.dirname(binPath), "assets"))); + if (!target.binName.endsWith(".exe")) { const mode = fs.statSync(binPath).mode; assert("binary is executable (owner +x bit set)", Boolean(mode & 0o100)); } - // Actually run the packaged binary and talk to it over real HTTP — - // proves the assets/-folder-next-to-the-executable resolution (sea-entry.cjs's - // `path.dirname(process.execPath)` logic) genuinely works, not just that - // the files exist on disk in the right place. - const child = execFile(binPath, { cwd: outDir, env: {} }); + // Actually run the packaged binary and talk to it over real HTTP — proves + // pkg's snapshot filesystem genuinely satisfies bin/cli.js's UNMODIFIED + // `ROOT = path.join(__dirname, "..", "src", "pptxdiff")` resolution, not + // just that files exist somewhere inside the binary. + const child = execFile(binPath, { cwd: os.tmpdir(), env: {} }); let urlMatch; try { - urlMatch = await waitForLine(child, /pptxdiff running at (http:\/\/localhost:\d+)/, 15000); + urlMatch = await waitForLine(child, /pptxdiff running at (http:\/\/localhost:\d+)/, 20000); } catch (e) { console.error("Binary never printed its startup line:", e.message); child.kill(); @@ -121,15 +122,10 @@ async function main() { assert("GET /support.js has JS content-type", (supportJs.headers["content-type"] || "").includes("javascript")); const vendorFile = await fetchText(`${baseUrl}/vendor/react.production.min.js`); - assert("GET /vendor/react.production.min.js returns 200 (assets/ folder is actually being served)", vendorFile.status === 200); + assert("GET /vendor/react.production.min.js returns 200 (embedded vendor/ assets are actually being served)", vendorFile.status === 200); - // Path-containment regression check against THIS root (assets/), not - // just bin/cli.js's default ROOT — a different `root` value is exactly - // what this whole feature changed, so re-prove isPathContained still - // applies to it rather than assuming it does because it's "the same - // function." const traversal = await fetchText(`${baseUrl}/../../../etc/passwd`); - assert("path traversal against the packaged binary's assets root is rejected (403 or 404, never 200)", traversal.status !== 200); + assert("path traversal against the packaged binary is rejected (never 200)", traversal.status !== 200); } finally { child.kill(); } @@ -144,16 +140,11 @@ async function main() { } else { console.log("All build-e2e checks passed (GREEN)."); } - // Clean up the built artifact afterward — this test's job is to prove - // the build+run path works, not to leave a ~100MB+ binary lying around. - // Only removes what THIS build generated (binary/assets/zip), not the - // whole outDir — that folder also holds the tracked README.md/ - // CHANGELOG.md, which a blind `rm -rf` would delete too. + // Clean up the built binary afterward — this test's job is to prove + // the build+run path works, not to leave a ~70MB+ binary lying around. + // Only removes the binary itself, never the tracked README.md/ + // CHANGELOG.md that live in the same per-OS folder. fs.rmSync(binPath, { force: true }); - fs.rmSync(path.join(outDir, "assets"), { recursive: true, force: true }); - for (const entry of fs.readdirSync(outDir)) { - if (entry.endsWith(".zip")) fs.rmSync(path.join(outDir, entry), { force: true }); - } } } From 47192c3b1612984f519f26d7ae17c9b4f2ff64b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 05:29:55 +0000 Subject: [PATCH 4/7] Add native Apple Silicon (arm64) build for macOS Direct follow-up to "does the mac binary work for Apple Silicon MacBooks?" - the honest answer was yes, but only via Rosetta 2 translation, since only the Intel (x64) target existed. TARGET_MAP gains an outDirKey field, separate from the map's own key, so `mac` and the new `mac-arm64` (node22-macos-arm64) share one output folder (pptxdiff-mac/) while keeping distinct binary names. buildOne() now computes outDir from target.outDirKey rather than the osKey argument. .github/workflows/binaries.yml's build-mac job now builds both mac targets; test_build_e2e.mjs picks mac vs mac-arm64 based on the host's actual os.arch(), so GitHub's Apple Silicon macos-latest runners genuinely exercise the native build. Verified for real in this sandbox: built the arm64 target directly, confirmed via `file` a genuine Mach-O 64-bit arm64 executable, confirmed it lands in the shared folder without disturbing the tracked README.md/CHANGELOG.md there. pkg's own error output independently confirmed the Apple Silicon signing requirement this project's reasoning already relied on. Windows and Linux stay x64-only - not asked about, and arm64 desktop usage is a smaller fraction of their likely audience than Apple Silicon is of the Mac audience. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BxwMTp6RQJ6j6K5K8Jjdpm --- .github/workflows/binaries.yml | 20 ++++-- CHANGELOG.md | 7 +- docs/.scrolls/GAP_ANALYSIS.md | 4 +- docs/.scrolls/GAP_CONTEXT.md | 7 +- docs/.scrolls/HANDOFF.md | 10 +++ docs/.scrolls/PLAN.md | 34 ++++++++++ docs/.scrolls/SPEC.md | 12 ++-- src/packages/binaries/README.md | 68 +++++++++++++------ src/packages/binaries/build.mjs | 44 +++++++----- .../binaries/pptxdiff-mac/CHANGELOG.md | 37 ++++++---- src/packages/binaries/pptxdiff-mac/README.md | 33 +++++---- src/packages/binaries/test_build_config.mjs | 23 +++++-- src/packages/binaries/test_build_e2e.mjs | 24 ++++--- 13 files changed, 231 insertions(+), 92 deletions(-) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index bf409f7..b2ac091 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -42,11 +42,14 @@ jobs: path: src/packages/binaries/pptxdiff-win/pptxdiff-win.exe if-no-files-found: error - # mac builds on a REAL macOS runner specifically so it can be ad-hoc - # codesigned (codesign only exists on macOS) — an unsigned mac binary is - # a real functional problem on Apple Silicon, not just a warning, so this - # is NOT cross-compiled from the linux job above. See build.mjs's header - # comment and docs/.scrolls/GAP_CONTEXT.md. + # mac (both Intel x64 and Apple Silicon arm64) builds on a REAL macOS + # runner specifically so both can be ad-hoc codesigned (codesign only + # exists on macOS) — an unsigned mac binary is a real functional problem + # on Apple Silicon, not just a warning, so neither is cross-compiled from + # the linux job above. See build.mjs's header comment and + # docs/.scrolls/GAP_CONTEXT.md. GitHub's macos-latest runners are + # themselves Apple Silicon as of 2024, so the arm64 build here is a + # genuinely native build+sign, not a translated one. build-mac: runs-on: macos-latest steps: @@ -60,10 +63,15 @@ jobs: working-directory: src/packages/binaries - run: npm run test:e2e working-directory: src/packages/binaries - - run: npm run build -- mac + - run: npm run build -- mac mac-arm64 working-directory: src/packages/binaries - uses: actions/upload-artifact@v4 with: name: pptxdiff-mac path: src/packages/binaries/pptxdiff-mac/pptxdiff-mac if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: pptxdiff-mac-arm64 + path: src/packages/binaries/pptxdiff-mac/pptxdiff-mac-arm64 + if-no-files-found: error diff --git a/CHANGELOG.md b/CHANGELOG.md index ecbd6cf..8ba5b3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,10 +40,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 state to the tap even without a version-pin change, while a scheduled run still only does real work on an actual version bump. `test_formula.mjs` now also asserts `LICENSE` stays byte-identical to the repo root's copy, catching drift instead of silently shipping a stale license to the tap. -- New private `@pptxdiff/binaries` package (`src/packages/binaries/`) building standalone native `pptxdiff` executables for Windows, macOS, and Linux via `@yao-pkg/pkg` — download one file and run it, no separate Node.js install required. +- New private `@pptxdiff/binaries` package (`src/packages/binaries/`) building standalone native `pptxdiff` executables for Windows, macOS (Intel and Apple Silicon), and Linux via `@yao-pkg/pkg` — download one file and run it, no separate Node.js install required. +- Native Apple Silicon (`pptxdiff-mac-arm64`) build alongside the existing Intel one, both sharing `src/packages/binaries/pptxdiff-mac/` — avoids Apple Silicon Macs having to run the Intel binary via Rosetta 2 translation. - Per-OS build output folders `src/packages/binaries/pptxdiff-{win,mac,linux}/`, each with its own `README.md` and `CHANGELOG.md`. -- `.github/workflows/binaries.yml`: `pkg` genuinely cross-compiles, so Windows and Linux build together in one `ubuntu-latest` job; macOS builds in its own `macos-latest` job so it can be properly ad-hoc codesigned. -- `make pkg.binaries.build` / `npm run build:binary` for local builds (all three OSes by default, or a specific subset). +- `.github/workflows/binaries.yml`: `pkg` genuinely cross-compiles, so Windows and Linux build together in one `ubuntu-latest` job; both macOS targets build in their own `macos-latest` job so they can be properly ad-hoc codesigned. +- `make pkg.binaries.build` / `npm run build:binary` for local builds (all four targets by default, or a specific subset). - Red/Green TDD test suite for the build tooling itself: `npm test` (fast, pure — config/asset-drift/regression checks) and `npm run test:e2e` (slow, real — builds and runs the actual packaged binary over real HTTP) in `src/packages/binaries/`. ## [0.7.0] - 2026-08-02 diff --git a/docs/.scrolls/GAP_ANALYSIS.md b/docs/.scrolls/GAP_ANALYSIS.md index be25d94..6677c6d 100644 --- a/docs/.scrolls/GAP_ANALYSIS.md +++ b/docs/.scrolls/GAP_ANALYSIS.md @@ -167,7 +167,9 @@ Concrete, testable gaps between what SPEC.md describes and a fully "real" implem - [ ] **Windows `.exe` is unsigned; macOS binary is ad-hoc signed only (no Developer ID).** No code-signing certificate exists for this project — Windows SmartScreen and macOS Gatekeeper will both warn on a freshly-downloaded copy. Documented per-OS in `src/packages/binaries/pptxdiff-{win,mac}/README.md`; a real fix needs a paid cert (Apple Developer ID + Windows Authenticode), not a code change. - [ ] **macOS is not cross-compiled** — `pkg` genuinely can cross-compile a macOS binary from Linux, but the result can't be codesigned there (`codesign` is macOS-only), and a completely unsigned binary may not even launch on Apple Silicon. `.github/workflows/binaries.yml` therefore still runs macOS on a real `macos-latest` runner (`build-mac`), separate from the `build-linux-win` job that genuinely does cross-compile both those targets from one Linux host. This is a real, load-bearing constraint, not leftover caution from the SEA-based version. - [ ] **Not yet attached to GitHub Releases.** The CI workflow uploads each OS's binary as a workflow artifact (downloadable from the Actions run page) but nothing wires a release-tag push to attach them to an actual GitHub Release yet — a real, small follow-up (e.g. `softprops/action-gh-release` on `release: types: [published]`), not attempted this session. -- [ ] **x64 only, no native arm64 build for any OS.** Matches the scope of the original (SEA-based) version; an Apple Silicon Mac runs the x64 binary via Rosetta 2. `pkg` does support arm64 targets (`node22-linux-arm64`, `node22-macos-arm64`, etc.) if this is ever asked for — not attempted, to keep this session's scope to what was requested. +- [x] **macOS native arm64 build** — CLOSED: `pptxdiff-mac-arm64` (`node22-macos-arm64`) added after an explicit follow-up question, sharing `pptxdiff-mac/` with the existing Intel `pptxdiff-mac`. Structurally verified in this sandbox (real `Mach-O 64-bit arm64 executable` via `file`); actual signed/run verification only happens on CI's `macos-latest` runner (itself Apple Silicon as of 2024). +- [ ] **Windows and Linux are still x64 only, no native arm64 build for either.** `pkg` does support `node22-win-arm64`/`node22-linux-arm64` if this is ever asked for — not attempted; the macOS arm64 gap was closed specifically because it was asked about directly (and because Apple Silicon has a real functional consequence — Rosetta translation overhead/availability — that Windows-on-ARM/Linux-on-ARM don't have in the same way for this app's target audience). +- [ ] **`ldid`-based Linux-side ad-hoc signing for macOS binaries was not pursued.** `pkg` itself suggests installing the `ldid` utility so a Linux host could ad-hoc-sign a Mach-O binary without any real Mac at all, which would let `build-mac`'s two targets fold into the `build-linux-win` job (three targets cross-compiled + signed from one Linux runner instead of two CI jobs). Not attempted — a real `macos-latest` CI runner was judged more reliable than depending on a third-party signing tool this project has no experience with; worth revisiting if CI cost/time ever becomes a real concern. - [ ] **macOS/Windows builds are unverified end-to-end in this sandbox** — only the Linux build was actually run and its binary actually executed (built, launched, served `index.html`/`support.js`/`vendor/*` via real HTTP requests) — under both the original SEA mechanism and, again, after switching to `@yao-pkg/pkg`. The Windows binary WAS structurally produced here (real `.exe`, verified during exploration before writing the final `build.mjs`) but not run (no Windows host in this sandbox); the macOS codesign branch is exercised for the first time whenever CI's `build-mac` job runs, not locally. ## Content checksum (this session) diff --git a/docs/.scrolls/GAP_CONTEXT.md b/docs/.scrolls/GAP_CONTEXT.md index 8f701ba..c7e1e85 100644 --- a/docs/.scrolls/GAP_CONTEXT.md +++ b/docs/.scrolls/GAP_CONTEXT.md @@ -185,7 +185,12 @@ The honest tradeoff accepted: one more devDependency (`@yao-pkg/pkg`, dev-time o **A hard-won gotcha found DURING this switch, not from documentation**: `pkg`'s `"assets"` glob paths in a config file resolve relative to wherever THAT CONFIG FILE ITSELF lives, not cwd, not the entry file's directory — confirmed by a controlled A/B test (same relative glob, config file moved to a different directory, assets silently stopped embedding with zero error/warning either time). This is why `build.mjs`'s temp pkg config is written directly at `REPO_ROOT` (next to the real `package.json`) rather than kept as a normal file inside `src/packages/binaries/` itself — full write-up in WISDOM.md's new trap entry, since a build tool silently producing an asset-less binary with no error message is exactly the kind of failure mode worth flagging loudly for future sessions. ## Why macOS is still built on its own CI runner instead of also being cross-compiled from the Linux job -`pkg` genuinely CAN produce a macOS binary from Linux (verified — see above), so "just cross-compile all three from one job" was the first instinct. Rejected after considering what codesigning actually requires: `codesign` only exists on macOS, so a Linux-built mac binary can never be even ad-hoc signed, and on Apple Silicon specifically, AMFI (Apple Mobile File Integrity) requires AT LEAST an ad-hoc signature for an arm64 executable to launch at all — this isn't "a Gatekeeper warning users can click through" the way it is on Intel Macs, it can mean the binary refuses to run, full stop. Shipping a binary that might not even launch on the now-dominant Apple Silicon Macs would be a real regression from the original SEA-based version (which DID build mac on an actual `macos-latest` runner and could ad-hoc sign it). `.github/workflows/binaries.yml` keeps macOS on its own `macos-latest` job specifically so `codesign --sign -` runs for real; `build.mjs`'s `buildOne()` warns loudly (not silently) if the mac target is ever built off of a non-darwin host, rather than producing something that looks fine at build time and fails mysteriously at launch time. +`pkg` genuinely CAN produce a macOS binary from Linux (verified — see above), so "just cross-compile all three from one job" was the first instinct. Rejected after considering what codesigning actually requires: `codesign` only exists on macOS, so a Linux-built mac binary can never be even ad-hoc signed, and on Apple Silicon specifically, AMFI (Apple Mobile File Integrity) requires AT LEAST an ad-hoc signature for an arm64 executable to launch at all — this isn't "a Gatekeeper warning users can click through" the way it is on Intel Macs, it can mean the binary refuses to run, full stop. Shipping a binary that might not even launch on the now-dominant Apple Silicon Macs would be a real regression from the original SEA-based version (which DID build mac on an actual `macos-latest` runner and could ad-hoc sign it). `.github/workflows/binaries.yml` keeps macOS on its own `macos-latest` job specifically so `codesign --sign -` runs for real; `build.mjs`'s `buildOne()` warns loudly (not silently) if the mac target is ever built off of a non-darwin host, rather than producing something that looks fine at build time and fails mysteriously at launch time. `pkg`'s own error output independently confirmed this reasoning is correct, not just this project's own assumption: attempting `pkg -t node22-macos-arm64` from Linux prints "Due to the mandatory code signing requirement... it will be immediately killed by kernel on launch. An ad-hoc signature is sufficient" verbatim — Apple's own enforcement, described by a tool with no stake in this project's specific decisions. + +## Why macOS got a native arm64 build but Windows/Linux didn't (yet) +Explicit follow-up question: "Does the mac binary work for Apple Silicon MacBooks?" Honest answer at the time: yes, but only via Rosetta 2 translation, since only the `node22-macos-x64` target had been built — a real, asked-about gap, not a hypothetical one. `pkg` supports `node22-win-arm64`/`node22-linux-arm64` equally well, so in principle all three OSes could get a native arm64 build — but only macOS's was added, for two concrete reasons specific to that platform, not "arm64 in general": (1) it was the one actually asked about; (2) macOS is the platform where running the "wrong" chip's binary has a REAL functional cost for this app's audience — Apple Silicon is the dominant Mac chip as of 2026, Rosetta 2 isn't guaranteed pre-installed on a fresh machine (first x64-binary launch prompts an install), and there's real translation overhead on every launch. Windows-on-ARM and Linux-on-ARM are comparatively rare desktop targets for THIS app's users (a local PowerPoint-diffing tool), so adding those wasn't scoped in without being asked — same "respect the literal scope of the ask" principle WISDOM.md already documents for confirmation-gate scoping, applied here to a build-target decision instead. +- **Mechanically**: `TARGET_MAP` entries gained an `outDirKey` field, separate from the map's own key, specifically so `mac` and `mac-arm64` (two different pkg targets, two different `binName`s) can share ONE output folder (`pptxdiff-mac/`) — from a user's perspective picking a download, "which folder do I look in" should be answered by OS, and "which specific file do I download" by chip, not two different folders for the same OS. `buildOne()` was changed to compute `outDir` from `target.outDirKey` rather than the `osKey` argument it's called with, with a dedicated regression test (`test_build_config.mjs`) protecting exactly that computation, demonstrated RED→GREEN for real. +- **Why not use `ldid` to also fold macOS into the cross-compiled Linux job**: `pkg`'s own error output (see the entry above) explicitly suggests installing `ldid` — a tool that can produce ad-hoc-equivalent Mach-O signatures from a non-macOS host — as an alternative to building on a real Mac. Considered but not pursued: this project has zero prior experience with `ldid`'s reliability/compatibility characteristics, and a real `macos-latest` GitHub Actions runner (using Apple's own `codesign`, the same tool a real Mac uses) is unambiguously the more trustworthy signing path for a first implementation. Worth revisiting explicitly if CI job count/time ever becomes a real constraint — flagged as a named, deliberate non-choice in GAP_ANALYSIS.md rather than silently never considered. ## Why @pptxdiff/server ships with no authentication rather than a minimal API key CLI_API_DESIGN.md §8 calls for API-key-required-on-non-loopback-bind as part of the design, but implementing even a minimal key check touches real security-sensitive surface (where the key comes from, how it's compared, timing-attack considerations) that deserves its own deliberate pass rather than being bolted on inside a Phase-1 session already covering three other new pieces (automation shim, CLI, server routing). The loopback-by-default bind (matching `bin/cli.js`'s existing precedent) is the one security property that WAS carried over faithfully; the auth gap is real, named explicitly in the package's own README (not just a scroll only this project's own sessions read), and is the literal next thing to build before anyone binds this server to a non-loopback host in practice. diff --git a/docs/.scrolls/HANDOFF.md b/docs/.scrolls/HANDOFF.md index 06b12e2..c2955a3 100644 --- a/docs/.scrolls/HANDOFF.md +++ b/docs/.scrolls/HANDOFF.md @@ -2,6 +2,16 @@ **Read `.scrolls/SPEC.md` first for the full feature list.** This file is the "what's the state of things right now" note — update it at the end of every session, keep it short and current (prune stale entries). +## Update (2026-08-05 — binaries: native Apple Silicon build, `pptxdiff-mac-arm64`) +- Direct follow-up question on the same-day `@yao-pkg/pkg` switch below: "Does the mac binary work for Apple Silicon MacBooks?" Honest answer given first: yes, but only via Rosetta 2 translation — only the `node22-macos-x64` target existed. Asked whether to add a native arm64 build (real effort: another CI target, another README/CHANGELOG update) via `AskUserQuestion` before doing the work; user said yes. +- **`TARGET_MAP` gained an `outDirKey` field**, separate from the map's own key, so `mac` (Intel) and the new `mac-arm64` (Apple Silicon, `node22-macos-arm64`) can share ONE output folder (`pptxdiff-mac/`) while keeping distinct `binName`s — from a user's download perspective, "which folder" should be answered by OS, "which file" by chip, not two folders for the same OS. `buildOne()` now computes `outDir` from `target.outDirKey`, not the `osKey` it's called with. +- **`.github/workflows/binaries.yml`'s `build-mac` job now builds both mac targets** (`npm run build -- mac mac-arm64`), uploading each as its own artifact. `test_build_e2e.mjs` now picks `mac` vs `mac-arm64` based on the host's actual `os.arch()` (not just `process.platform`), so GitHub's `macos-latest` runners — themselves Apple Silicon as of 2024 — genuinely exercise the native arm64 build in CI, not the Intel one. +- **Verified for real in this sandbox (Linux)**: ran `node build.mjs mac-arm64` directly, confirmed via `file` a genuine `Mach-O 64-bit arm64 executable` (unsigned, as expected off a non-macOS host), confirmed it landed correctly in the SHARED `pptxdiff-mac/` folder without disturbing the tracked `README.md`/`CHANGELOG.md` already there, then cleaned it up (can't be run/signed/verified over HTTP from Linux — that's CI's job). `pkg` itself independently printed the exact Apple-Silicon-signing warning ("Due to the mandatory code signing requirement... it will be immediately killed by kernel on launch") this project's own reasoning already relied on — real, external confirmation, not just this project's own assumption. +- **Genuine RED→GREEN demonstrated on the new `outDirKey` guard**: temporarily reverted `buildOne()`'s `outDir` computation back to using the `osKey` argument, confirmed the dedicated regression test caught it (22/23), restored it, confirmed 23/23 — `test_build_config.mjs` now 23 assertions total (was 18). +- **Windows/Linux deliberately stay x64-only** — not asked about, and arm64 desktop usage is a much smaller fraction of this app's likely audience for those two OSes than Apple Silicon is of the Mac audience. `pkg` supports `node22-win-arm64`/`node22-linux-arm64` equally well if this is ever revisited. +- **`ldid`-based Linux-side signing considered, not pursued**: `pkg`'s own error output suggests it as an alternative to needing a real Mac at all (would let `build-mac` fold into the cross-compiled `build-linux-win` job) — flagged as a real, deliberate non-choice in GAP_ANALYSIS.md rather than silently never considered; a real `macos-latest` runner using Apple's own `codesign` was judged more trustworthy for a first pass. +- Scrolls updated to match: SPEC.md §32, PLAN.md (new "Done this session" block + 2 new tickets), GAP_ANALYSIS.md (arm64 gap closed for mac, new ldid/win-linux-arm64 tickets), GAP_CONTEXT.md (two new entries — why mac got native arm64 but win/linux didn't, why `ldid` wasn't pursued), per-OS mac `README.md`/`CHANGELOG.md`, top-level `src/packages/binaries/README.md`, root `CHANGELOG.md`. + ## Update (2026-08-05 — binaries: switched from Node SEA to `@yao-pkg/pkg`) - Direct follow-up question on the same-day binaries work below: "Why aren't you using the npm library yao-pkg/pkg?" Honest answer given first: SEA was reached for by default (Node core feature, no added third-party build-tool dependency) without actually evaluating `pkg` first — then investigated hands-on rather than defending the choice abstractly. - **Verified two real advantages in this sandbox, not just cited from memory**: (1) genuine cross-compilation — built a real Windows `.exe` (`file` confirmed `PE32+ executable ... for MS Windows`) and a real macOS binary (`Mach-O 64-bit x86_64 executable`) FROM THIS LINUX SANDBOX, something Node SEA cannot do at all; (2) built-in asset embedding needing zero `bin/cli.js` changes — `pkg`'s snapshot filesystem preserves the real project's relative directory layout, so pointing `pkg` directly at the UNMODIFIED `bin/cli.js` with `src/pptxdiff/**` as `pkg` assets made the existing `ROOT = path.join(__dirname, "..", "src", "pptxdiff")` just resolve correctly. Asked the user to confirm via `AskUserQuestion` before doing the rework (real effort, real new dependency) rather than just switching unilaterally; user picked switch. diff --git a/docs/.scrolls/PLAN.md b/docs/.scrolls/PLAN.md index 6b5e235..e773f23 100644 --- a/docs/.scrolls/PLAN.md +++ b/docs/.scrolls/PLAN.md @@ -479,3 +479,37 @@ shim, not sequentially — that plan is what shipped below. - [x] Root `CHANGELOG.md`'s `[Unreleased]` section updated to match (mentions `@yao-pkg/pkg`, the 2-job CI split, and the Red/Green test suite — no longer mentions Node SEA or `startServer`'s `root` param, since that was fully reverted). + +## Done this session (native Apple Silicon build: `pptxdiff-mac-arm64`) +- [x] **P3 — Added `pptxdiff-mac-arm64`, a native Apple Silicon binary**, after an explicit follow-up + question ("does the mac binary work for Apple Silicon MacBooks?"). Before this, Apple Silicon Macs + could only run the Intel `pptxdiff-mac` binary via Rosetta 2 translation. `TARGET_MAP` gained an + `outDirKey` field (separate from the map key) so `mac`/`mac-arm64` share `pptxdiff-mac/` as their + output folder while keeping distinct `binName`s — `buildOne()` computes `outDir` from + `target.outDirKey`, not the `osKey` it's called with. `.github/workflows/binaries.yml`'s `build-mac` + job now builds both mac targets (`npm run build -- mac mac-arm64`) and uploads both as separate + artifacts; `test_build_e2e.mjs` picks `mac` vs `mac-arm64` based on the host's actual `os.arch()`, + so GitHub's Apple-Silicon `macos-latest` runners genuinely exercise the native build. Verified for + real in this sandbox (Linux): built the `node22-macos-arm64` target directly, confirmed via `file` + it's a genuine `Mach-O 64-bit arm64 executable`, confirmed it landed in the shared `pptxdiff-mac/` + folder without disturbing the tracked `README.md`/`CHANGELOG.md`, and confirmed `pkg`'s own error + output independently prints the same Apple-Silicon-signing warning this project's reasoning already + relied on. Windows/Linux stay x64-only — not asked about, and arm64 desktops are a much smaller + fraction of that audience than Apple Silicon is of the Mac audience (see GAP_CONTEXT.md). +- [x] **Genuine RED→GREEN demonstrated on the new `outDirKey` guard**: temporarily reverted + `buildOne()`'s `outDir` computation to use the `osKey` argument instead of `target.outDirKey`, + confirmed the dedicated regression test caught it (22/23), restored it, confirmed 23/23. +- [x] Per-OS mac `README.md`/`CHANGELOG.md`, the top-level `src/packages/binaries/README.md`, root + `CHANGELOG.md`, `SPEC.md` §32, `GAP_ANALYSIS.md`, and `GAP_CONTEXT.md` all updated to describe both + mac targets. + +## New tickets opened this session +1. **P4 — Native Windows/Linux arm64 builds**, if ever asked for — `pkg` supports + `node22-win-arm64`/`node22-linux-arm64` equally well; not attempted since neither was asked about + and arm64 desktop/laptop usage is comparatively rare for those two OSes among this app's likely + users (see GAP_CONTEXT.md). +2. **P4 — Investigate `ldid` for Linux-side ad-hoc signing of macOS binaries**, which `pkg`'s own + error output suggests as an alternative to a real macOS CI runner — would let `build-mac` fold + into the cross-compiled `build-linux-win` job (one CI job instead of two). Not pursued; a real + `macos-latest` runner using Apple's own `codesign` was judged more trustworthy for a first pass — + revisit if CI job count/time ever becomes a real constraint. diff --git a/docs/.scrolls/SPEC.md b/docs/.scrolls/SPEC.md index f61e5c1..956f689 100644 --- a/docs/.scrolls/SPEC.md +++ b/docs/.scrolls/SPEC.md @@ -301,16 +301,18 @@ Word-level diff (LCS-based) highlights changed words within text/table-cell/char - **Failure behavior**: unsupported browser values or unknown options fail before the local server starts, with exit code `2` and a clear error. If the selected browser command itself is missing or cannot launch in a headless/no-GUI environment, the CLI still prints the local URL and ignores the browser-open failure, preserving the prior "URL is enough to proceed manually" behavior. - **Testing**: `src/pptxdiff/test_execfile_browser_open_cli.mjs` now covers `parseArgs()` for both `--browser=value` and `--browser value`, rejects unsupported values, and verifies all platform/browser command builders still pass the URL as a single `execFile()` argv element rather than shell-interpolating it. ## 36. Standalone native binaries (`@pptxdiff/binaries`, added this session) -- **What it does**: `src/packages/binaries/` builds a standalone, native `pptxdiff` executable per OS — download ONE file, run it, `pptxdiff` opens in the browser. No Node.js install, no `npm install -g`, no `npx`, no separate assets folder to keep alongside it. Output lands in `src/packages/binaries/pptxdiff-win/pptxdiff-win.exe`, `pptxdiff-mac/pptxdiff-mac`, `pptxdiff-linux/pptxdiff-linux` (the folder names the user requested). Build output is gitignored (generated, not source); each OS folder keeps a tracked `README.md` and `CHANGELOG.md` describing what a build produces there. +- **What it does**: `src/packages/binaries/` builds a standalone, native `pptxdiff` executable per target — download ONE file, run it, `pptxdiff` opens in the browser. No Node.js install, no `npm install -g`, no `npx`, no separate assets folder to keep alongside it. Output lands in `src/packages/binaries/pptxdiff-win/pptxdiff-win.exe`, `pptxdiff-mac/{pptxdiff-mac,pptxdiff-mac-arm64}`, `pptxdiff-linux/pptxdiff-linux` (the folder names the user requested — the two mac binaries share one folder, distinguished by chip). Build output is gitignored (generated, not source); each OS folder keeps a tracked `README.md` and `CHANGELOG.md` describing what a build produces there. +- **Native Apple Silicon build** (`pptxdiff-mac-arm64`), added after an explicit follow-up question ("does the mac binary work for Apple Silicon MacBooks?"). Before this, Apple Silicon Macs could only run the Intel (`pptxdiff-mac`) binary via Rosetta 2 translation — real launch overhead, and Rosetta isn't guaranteed pre-installed on a fresh Mac. `TARGET_MAP` entries gained an `outDirKey` field (separate from the map's own key) so `mac` and `mac-arm64` can share `pptxdiff-mac/` as their output folder while each keeping distinct `binName`s — `buildOne()` computes `outDir` from `target.outDirKey`, not the `osKey` argument. Verified for real in this sandbox (Linux): built the `node22-macos-arm64` target and confirmed via `file` it's a genuine `Mach-O 64-bit arm64 executable` (unsigned, as expected off a non-macOS host); `pkg` itself independently prints the same Apple-Silicon-signing warning this project's own reasoning already relied on. - **Deliberately standalone binaries, not real OS installers**: asked directly (binaries vs. true `.msi`/`.pkg`/`.deb` installers with an install wizard, PATH registration, code signing) and the user picked standalone binaries — consistent with this project's prior explicit decision (see GAP_CONTEXT.md "Why the npm CLI opens a browser tab instead of a real native window") to avoid Electron/Tauri-style installer and code-signing overhead. See `src/packages/binaries/README.md` for the full reasoning. - **Mechanism: `@yao-pkg/pkg`, not Node's own SEA feature.** This package originally shipped using Node's built-in Single Executable Applications (SEA) feature. Switched after an explicit user question ("why aren't you using yao-pkg/pkg?") surfaced two real advantages SEA doesn't have — see GAP_CONTEXT.md for the full reasoning and the honest tradeoff (one more third-party build-tool devDependency, dev-time only): 1. **Real cross-compilation.** `pkg` downloads a prebuilt base `node` binary per TARGET platform and injects the bundled app into it, so one Linux host builds the Windows AND Linux binaries. SEA can only build for whatever OS it's currently running on. 2. **Built-in asset embedding, with zero `bin/cli.js` changes.** `pkg`'s snapshot filesystem mirrors the real project's relative directory layout at runtime — `bin/cli.js`'s existing, completely UNMODIFIED `ROOT = path.join(__dirname, "..", "src", "pptxdiff")` computation resolves correctly with `build.mjs` simply listing `src/pptxdiff/index.html`/`support.js`/`sample-pptx.js`/`vendor/**` as pkg `"assets"`. (The SEA version had needed an added `root` parameter on `startServer()` plus a separate `assets/` folder shipped next to the binary — both fully reverted; `bin/cli.js` is now byte-identical to how it looked before this whole feature.) - **A hard-won gotcha discovered mid-build** (see WISDOM.md's new trap entry): `pkg`'s `"assets"` glob paths in a config file resolve relative to wherever THAT CONFIG FILE ITSELF lives — not cwd, not the entry file's directory. Get it wrong and it fails completely silently (zero assets embedded, no warning, only a 404 when the packaged binary actually runs). `build.mjs`'s `buildOne()` writes a temp pkg config directly at the repo root (next to the real `package.json`, where `src/pptxdiff/**` actually resolves), removed in a `finally` block. -- **macOS is the one target NOT cross-compiled.** `pkg` can produce a macOS binary from Linux, but can't codesign it (`codesign` only exists on macOS) — and on Apple Silicon, a completely unsigned binary may not even launch (AMFI requires at least an ad-hoc signature, unlike Intel Macs where it's "only" a Gatekeeper warning). `buildOne()` only runs its codesign step when `process.platform === "darwin"`, warning loudly otherwise rather than silently shipping something that might not run. `.github/workflows/binaries.yml` reflects this: Windows+Linux build together in one `ubuntu-latest` job (`build-linux-win`); macOS builds separately on `macos-latest` (`build-mac`) so it's genuinely signed. -- **Known, documented gaps** (see GAP_ANALYSIS.md): unsigned Windows `.exe` (SmartScreen warning) and ad-hoc-signed-only macOS binary (Gatekeeper warning) — no code-signing certificate exists for this project; not yet wired to GitHub Releases; x64 only, no native arm64 build for any OS. +- **Neither macOS target is cross-compiled.** `pkg` can produce macOS binaries (either chip) from Linux, but can't codesign them (`codesign` only exists on macOS) — and on Apple Silicon, a completely unsigned binary may not even launch (AMFI requires at least an ad-hoc signature, unlike Intel Macs where it's "only" a Gatekeeper warning). `buildOne()` only runs its codesign step when `process.platform === "darwin"`, warning loudly otherwise rather than silently shipping something that might not run. `.github/workflows/binaries.yml` reflects this: Windows+Linux build together in one `ubuntu-latest` job (`build-linux-win`); both `mac`/`mac-arm64` build separately on `macos-latest` (`build-mac`, itself an Apple Silicon runner as of 2024) so they're genuinely signed. +- **Known, documented gaps** (see GAP_ANALYSIS.md): unsigned Windows `.exe` (SmartScreen warning) and ad-hoc-signed-only macOS binaries (Gatekeeper warning) — no code-signing certificate exists for this project; not yet wired to GitHub Releases; Windows/Linux are x64 only, no native arm64 build for either. - **Verified locally** (Linux, this sandbox), for both mechanisms in turn: built for real, ran the actual packaged binary directly (not just `bin/cli.js`), confirmed it correctly serves `index.html`/`support.js`/`vendor/*` via real `curl` requests — once under Node SEA, and again after the switch to `@yao-pkg/pkg`, this time with a true single file and zero `bin/cli.js` changes. Windows/macOS builds are structurally identical (same `buildOne()`, only the mac codesign branch differs) but unverified end-to-end locally — no Windows/macOS host in this sandbox; CI exercises them for real. - **Red/Green TDD, two test files** (`build.mjs` has an entrypoint guard — same pattern as `capture_screenshots.mjs`, see WISDOM.md — so `TARGET_MAP`/`ASSET_GLOBS`/`resolveTarget`/`buildOne`/`buildAll` are importable without a real build running as a side effect): - - `test_build_config.mjs` (fast, pure, no subprocess/network, `npm test`): 18 assertions covering `TARGET_MAP`'s per-OS shape, an `ASSET_GLOBS`-vs-root-`package.json`-"files" drift guard, that `bin/cli.js` is passed to pkg completely unmodified (no packaging-specific parameter), the macOS-signing safety checks, and — the sharpest one — a static-source regression check that `buildOne()` still writes its temp pkg config at `REPO_ROOT` (the exact gotcha above). Demonstrated genuine RED→GREEN twice in this session: once for the SEA-era `startServer(root)` contract (now retired along with SEA itself), and again for the pkg-config-colocation guard — moved the write location to `__dirname`, confirmed exactly that one assertion failed (17/18), restored it, confirmed 18/18. - - `test_build_e2e.mjs` (slow, real, current-platform-only, `npm run test:e2e` — same split as `pptxdiff-cli`'s `test:difftool`): actually calls `buildOne()`, then spawns the REAL resulting single-file executable and drives it over real HTTP — `GET /`, `/support.js`, `/vendor/react.production.min.js` all 200 with correct content, an explicit assertion that no separate `assets/` folder exists, plus a path-traversal request. 10/10 assertions, genuinely GREEN against a real binary built and run in this sandbox by BOTH mechanisms in turn. Cleans up only the binary file afterward, never the tracked `README.md`/`CHANGELOG.md` in the same folder. + - `test_build_config.mjs` (fast, pure, no subprocess/network, `npm test`): 23 assertions covering `TARGET_MAP`'s per-target shape (now four entries — `linux`/`win`/`mac`/`mac-arm64`), that `mac`/`mac-arm64` share one `outDirKey`, an `ASSET_GLOBS`-vs-root-`package.json`-"files" drift guard, that `bin/cli.js` is passed to pkg completely unmodified (no packaging-specific parameter), the macOS-signing safety checks, and two sharp static-source regression checks: that `buildOne()` writes its temp pkg config at `REPO_ROOT` (the config-colocation gotcha above), and that it computes `outDir` from `target.outDirKey` rather than the `osKey` argument (the exact mechanism that lets `mac`/`mac-arm64` share one folder). Demonstrated genuine RED→GREEN three times across this session's two rounds: the now-retired SEA-era `startServer(root)` contract, the pkg-config-colocation guard, and the `outDirKey` guard (temporarily reverted `outDir`'s computation to use `osKey`, confirmed the one dependent assertion failed (22/23), restored it, confirmed 23/23). + - `test_build_e2e.mjs` (slow, real, current-platform-only, `npm run test:e2e` — same split as `pptxdiff-cli`'s `test:difftool`): picks the host's own target via `process.platform`+`os.arch()` (so a `darwin`/`arm64` host tests `mac-arm64`, not `mac`), calls `buildOne()`, then spawns the REAL resulting single-file executable and drives it over real HTTP — `GET /`, `/support.js`, `/vendor/react.production.min.js` all 200 with correct content, an explicit assertion that no separate `assets/` folder exists, plus a path-traversal request. 10/10 assertions, genuinely GREEN against a real binary built and run in this sandbox by BOTH mechanisms in turn. Cleans up only the binary file afterward, never the tracked `README.md`/`CHANGELOG.md` in the same folder. +- **Structurally verified the new `mac-arm64` target directly in this sandbox** (Linux): ran a real `node build.mjs mac-arm64` build, confirmed via `file` a genuine `Mach-O 64-bit arm64 executable`, confirmed it landed in the SHARED `pptxdiff-mac/` folder alongside the tracked `README.md`/`CHANGELOG.md` without disturbing them, and confirmed `pkg` itself independently prints the exact Apple-Silicon-signing warning this project's own reasoning already relied on — cleaned up afterward (can't be run/verified over HTTP from Linux; that's CI's job). diff --git a/src/packages/binaries/README.md b/src/packages/binaries/README.md index 24a33c3..f87da7f 100644 --- a/src/packages/binaries/README.md +++ b/src/packages/binaries/README.md @@ -39,13 +39,34 @@ only — never shipped in the binaries or the npm package), vs. a fork of a project Vercel walked away from. Judged worth it for the two wins above; see `docs/.scrolls/GAP_CONTEXT.md` for the full reasoning. -**macOS is the one target NOT cross-compiled here.** `pkg` CAN produce a -macOS binary from Linux, but it can't codesign it (`codesign` only exists -on macOS) — and on Apple Silicon, a completely unsigned binary may not -even *launch* (AMFI requires at least an ad-hoc signature, not just a -Gatekeeper warning the way Intel Macs work). So the mac target only runs -its codesign step when actually built on a macOS host — see -`.github/workflows/binaries.yml`'s separate `build-mac` job. +**macOS is the one OS NOT cross-compiled here** (for either chip). `pkg` +CAN produce macOS binaries from Linux, but it can't codesign them +(`codesign` only exists on macOS) — and on Apple Silicon, a completely +unsigned binary may not even *launch* (AMFI requires at least an ad-hoc +signature, not just a Gatekeeper warning the way Intel Macs work; `pkg` +itself prints this exact warning if you try). So both mac targets only run +their codesign step when actually built on a macOS host — see +`.github/workflows/binaries.yml`'s separate `build-mac` job. (`pkg` does +mention one Linux-side workaround — installing the `ldid` utility so it +can ad-hoc-sign Mach-O binaries without a real Mac at all — not pursued +here; a real `macos-latest` CI runner was judged simpler and more +reliable than depending on a third tool for signing.) + +## Apple Silicon (arm64) + +Two mac targets exist, both landing in `pptxdiff-mac/`: + +| osKey | pkg target | binary | chip | +|---|---|---|---| +| `mac` | `node22-macos-x64` | `pptxdiff-mac` | Intel | +| `mac-arm64` | `node22-macos-arm64` | `pptxdiff-mac-arm64` | Apple Silicon (native) | + +Without the `mac-arm64` target, an Apple Silicon Mac would only be able to +run the Intel binary via Rosetta 2 translation (extra launch overhead, +and Rosetta isn't guaranteed pre-installed on a fresh Mac). `TARGET_MAP` +entries share an `outDirKey` (`"mac"` for both) separate from their own +map key, specifically so multiple chip variants of the same OS can land +in one folder — see `build.mjs`'s `buildOne()`. ## How it works @@ -78,15 +99,18 @@ tracked file inside this package's own directory. ```sh cd src/packages/binaries npm install -npm run build # builds all three (win/mac/linux) by default -npm run build -- linux win # or build a specific subset +npm run build # builds all four targets by default +npm run build -- linux win # or build a specific subset +npm run build -- mac mac-arm64 # (on macOS, for a properly-signed result) ``` -Output lands in `./pptxdiff-/` — one file per OS, -nothing else needed alongside it. Each OS folder keeps a tracked -`README.md` (usage/known-warnings) and `CHANGELOG.md` (Keep a Changelog, -tracks the bundled `pptxdiff` app version) — `build.mjs` only ever -touches the binary file itself, never those two. +Output lands in `./pptxdiff-/` — one file per +target, nothing else needed alongside it (the two mac targets share the +`pptxdiff-mac/` folder). Each OS folder keeps a tracked `README.md` +(usage/known-warnings) and `CHANGELOG.md` (Keep a Changelog, tracks the +bundled `pptxdiff` app version) — `build.mjs` only ever touches the +specific binary file it's building, never those two or the other target's +binary. ## Testing (Red/Green TDD) @@ -99,18 +123,22 @@ npm run test:e2e # slow, real — builds an actual binary for the CURRENT # support.js/vendor/* + a path-traversal check) ``` -`test:e2e` only exercises the current host's own target — win/mac are -structurally identical (same `buildOne()`, only the mac codesign branch -differs) but only actually built-and-run by CI. +`test:e2e` only exercises the current host's own target (on macOS, it +picks `mac` vs `mac-arm64` based on the host's actual `os.arch()`, so an +Apple Silicon CI runner genuinely tests the native `mac-arm64` build) — +every target is structurally identical (same `buildOne()`, only the mac +codesign branch differs) but only actually built-and-run by CI. ## Known gaps (see `docs/.scrolls/GAP_ANALYSIS.md`) -- **Unsigned Windows `.exe` / ad-hoc-signed-only macOS binary.** No +- **Unsigned Windows `.exe` / ad-hoc-signed-only macOS binaries.** No code-signing certificate — Windows SmartScreen and macOS Gatekeeper will warn on a freshly-downloaded copy. Documented per-OS in each `pptxdiff-/README.md`. - **Not attached to GitHub Releases yet.** CI currently only uploads build artifacts on push/dispatch; wiring a release-tag trigger to attach them to a GitHub Release is a follow-up, not done here. -- **x64 only, no native arm64 build** for any OS (matches the original - scope) — an Apple Silicon Mac runs the x64 binary via Rosetta 2. +- **x64 only for Windows and Linux** — no native arm64 build for either + (matches the original scope; macOS is the one OS with a native arm64 + build, added after an explicit follow-up ask since Apple Silicon is now + the dominant Mac). diff --git a/src/packages/binaries/build.mjs b/src/packages/binaries/build.mjs index 5b3579b..8d741fc 100644 --- a/src/packages/binaries/build.mjs +++ b/src/packages/binaries/build.mjs @@ -30,17 +30,21 @@ // "src/pptxdiff/**" to resolve — written fresh before each build and // removed in a `finally`, since it isn't a real project file. // -// **macOS is NOT cross-compiled from this build.** pkg can produce a -// macOS binary from Linux/Windows, but it cannot codesign it (`codesign` -// only exists on macOS) — and an entirely unsigned binary is a real -// functional problem on Apple Silicon (arm64 requires at least an ad-hoc -// signature to launch at all under AMFI, not just a Gatekeeper warning -// like on Intel). So `buildOne('mac', ...)` only runs its codesign step -// when `process.platform === 'darwin'`; on any other host it still -// produces a binary (for local experimentation) but loudly warns it's -// unsigned rather than silently shipping something that may not launch. +// **macOS (both `mac`/Intel and `mac-arm64`/Apple Silicon) is NOT +// cross-compiled from this build.** pkg can produce either macOS binary +// from Linux/Windows regardless of the BUILD host's own architecture, but +// it cannot codesign either one (`codesign` only exists on macOS) — and an +// entirely unsigned binary is a real functional problem specifically on +// Apple Silicon (arm64 requires at least an ad-hoc signature to launch at +// all under AMFI, not just a Gatekeeper warning like on Intel). So +// `buildOne()` only runs its codesign step when `process.platform === +// 'darwin'`, for either mac target; on any other host it still produces a +// binary (for local experimentation) but loudly warns it's unsigned +// rather than silently shipping something that may not launch. // .github/workflows/binaries.yml reflects this: linux+win build together -// on ubuntu-latest, mac builds separately on macos-latest. +// on ubuntu-latest, both mac targets build separately on macos-latest +// (GitHub's macos-latest runners are themselves Apple Silicon as of 2024, +// so `mac-arm64` there is a genuinely native build+sign, not translated). import { execFileSync } from "node:child_process"; import fs from "node:fs"; @@ -50,10 +54,15 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); +// `outDirKey` is separate from the map's own key so multiple targets can +// share one OS folder — `mac` (Intel/x64) and `mac-arm64` (Apple Silicon, +// native) both land in `pptxdiff-mac/`, since they're the same OS as far +// as a user picking a download is concerned, just a different chip. export const TARGET_MAP = { - linux: { pkgTarget: "node22-linux-x64", binName: "pptxdiff-linux", needsMacSign: false }, - win: { pkgTarget: "node22-win-x64", binName: "pptxdiff-win.exe", needsMacSign: false }, - mac: { pkgTarget: "node22-macos-x64", binName: "pptxdiff-mac", needsMacSign: true }, + linux: { pkgTarget: "node22-linux-x64", binName: "pptxdiff-linux", outDirKey: "linux", needsMacSign: false }, + win: { pkgTarget: "node22-win-x64", binName: "pptxdiff-win.exe", outDirKey: "win", needsMacSign: false }, + mac: { pkgTarget: "node22-macos-x64", binName: "pptxdiff-mac", outDirKey: "mac", needsMacSign: true }, + "mac-arm64": { pkgTarget: "node22-macos-arm64", binName: "pptxdiff-mac-arm64", outDirKey: "mac", needsMacSign: true }, }; // Same subset root package.json's "files" ships to npm — the exact static @@ -80,7 +89,7 @@ function log(osKey, msg) { // absolute path to the built executable. Writes bin.mjs's temp pkg config // at REPO_ROOT and always removes it afterward, success or failure. export async function buildOne(osKey, target) { - const outDir = path.join(__dirname, `pptxdiff-${osKey}`); + const outDir = path.join(__dirname, `pptxdiff-${target.outDirKey}`); fs.mkdirSync(outDir, { recursive: true }); const binOut = path.join(outDir, target.binName); fs.rmSync(binOut, { force: true }); @@ -121,9 +130,10 @@ export async function buildOne(osKey, target) { } } -// Builds every osKey in `osKeys` (default: all three — a reasonable local- -// dev default since pkg CAN cross-compile all three from one machine; the -// mac-signing caveat above still applies). Returns { [osKey]: binPath }. +// Builds every osKey in `osKeys` (default: every TARGET_MAP entry — a +// reasonable local-dev default since pkg CAN cross-compile all of them +// from one machine; the mac-signing caveat above still applies to both +// `mac` and `mac-arm64`). Returns { [osKey]: binPath }. export async function buildAll(osKeys = Object.keys(TARGET_MAP)) { const results = {}; for (const osKey of osKeys) { diff --git a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md index b2e93b9..9433780 100644 --- a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md @@ -1,11 +1,11 @@ -# Changelog — pptxdiff for macOS (standalone binary) +# Changelog — pptxdiff for macOS (standalone binaries) -All notable changes to the macOS standalone `pptxdiff-mac` build are -documented here. The format is based on +All notable changes to the macOS standalone `pptxdiff-mac`/ +`pptxdiff-mac-arm64` builds are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version tracked is the `pptxdiff` app version bundled into the binary (see the root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) -since the binary has no independent feature set of its own. +since the binaries have no independent feature set of their own. ## [Unreleased] @@ -15,24 +15,35 @@ since the binary has no independent feature set of its own. ### Added -- First standalone macOS executable, built via `@yao-pkg/pkg` (see +- First standalone macOS executables, built via `@yao-pkg/pkg` (see `../README.md` and `docs/.scrolls/SPEC.md` §32) — download - `pptxdiff-mac`, run it. A true single file (Node runtime and the static - app files it serves are both embedded inside it) — no separate Node.js + `pptxdiff-mac` (Intel) or `pptxdiff-mac-arm64` (Apple Silicon, native), + run it. Each is a true single file (Node runtime and the static app + files it serves are both embedded inside it) — no separate Node.js install, no companion folder needed. +- **Native Apple Silicon (`pptxdiff-mac-arm64`) build**, added after an + explicit follow-up question ("does the mac binary work for Apple + Silicon?"). Without it, an Apple Silicon Mac could only run the Intel + binary via Rosetta 2 translation — real launch overhead, and Rosetta + isn't guaranteed pre-installed on a fresh Mac. Verified for real in this + project's own dev sandbox (Linux): built the `node22-macos-arm64` target + and confirmed via `file` it's a genuine `Mach-O 64-bit arm64 executable` + (unsigned, since built off a non-macOS host — real signing only happens + on the `macos-latest` CI runner or a real Mac). ### Known limitations - **Must be built on an actual macOS host, not cross-compiled.** Unlike - the Windows/Linux targets, this one needs `codesign` (macOS-only) for a - usable result — an unsigned build may not even launch on Apple Silicon. - See `../README.md`. + the Windows/Linux targets, both of these need `codesign` (macOS-only) + for a usable result — an unsigned build may not even launch on Apple + Silicon (confirmed independently by `pkg` itself, which prints this + exact warning when building `mac-arm64` off of a non-macOS host). See + `../README.md`. - **Ad-hoc signed, not notarized.** No Apple Developer ID — Gatekeeper will likely block a freshly-downloaded copy ("cannot be opened because the developer cannot be verified"); right-click → Open, or - `xattr -d com.apple.quarantine pptxdiff-mac` first. See `../README.md` - and `docs/.scrolls/GAP_ANALYSIS.md`. -- x64 only — no native arm64 build; runs via Rosetta 2 on Apple Silicon. + `xattr -d com.apple.quarantine ` first. See `../README.md` and + `docs/.scrolls/GAP_ANALYSIS.md`. - Not yet attached to GitHub Releases — built by `.github/workflows/binaries.yml`'s dedicated `build-mac` job and available as a workflow artifact. diff --git a/src/packages/binaries/pptxdiff-mac/README.md b/src/packages/binaries/pptxdiff-mac/README.md index 5f49f1c..fb81242 100644 --- a/src/packages/binaries/pptxdiff-mac/README.md +++ b/src/packages/binaries/pptxdiff-mac/README.md @@ -1,27 +1,36 @@ # pptxdiff for macOS -This folder holds the built macOS artifact — not committed here, generated -by `../build.mjs`. **Must be built on an actual macOS host** (or -`macos-latest` CI runner) — see `../README.md`'s "Why `@yao-pkg/pkg`" -section for why this one target isn't cross-compiled: it needs `codesign` +This folder holds the built macOS artifacts — not committed here, +generated by `../build.mjs`. **Must be built on an actual macOS host** +(or `macos-latest` CI runner) — see `../README.md`'s "Why `@yao-pkg/pkg`" +section for why these targets aren't cross-compiled: they need `codesign` (macOS-only) to be ad-hoc signed, without which the binary may not even launch on Apple Silicon. -After a build, this folder contains: +After a build, this folder contains one or both of: -- `pptxdiff-mac` — the standalone executable, ad-hoc signed. A true - single file: the Node runtime AND the static app files it serves are - both embedded inside it. +- `pptxdiff-mac` — Intel (x64), ad-hoc signed. +- `pptxdiff-mac-arm64` — Apple Silicon (arm64), native, ad-hoc signed. + +**If you're not sure which one you need**: Apple menu → About This Mac → +"Chip" — `Apple M1`/`M2`/`M3`/`M4` (or similar) means download +`pptxdiff-mac-arm64`; `Intel` means download `pptxdiff-mac`. Both are true +single files: the Node runtime AND the static app files served are both +embedded inside them. **Ad-hoc signed, not notarized.** There is no Apple Developer ID certificate for this project, so Gatekeeper will likely block a freshly-downloaded copy on first launch ("cannot be opened because the developer cannot be verified") — right-click the binary → Open, or run -`xattr -d com.apple.quarantine pptxdiff-mac` first. See +`xattr -d com.apple.quarantine ` first. See `docs/.scrolls/GAP_ANALYSIS.md` for why this is a documented, accepted tradeoff rather than an oversight. To build (on macOS only, for a properly-signed result): -`cd src/packages/binaries && npm install && npm run build -- mac`. Building -this target from Linux/Windows produces an unsigned binary that likely -won't launch on Apple Silicon — `build.mjs` warns loudly if you try. +`cd src/packages/binaries && npm install && npm run build -- mac mac-arm64`. +Building either target from Linux/Windows produces a completely unsigned +binary — for `pptxdiff-mac-arm64` specifically, that means it likely won't +launch at all (Apple's AMFI enforcement requires at least an ad-hoc +signature for arm64 executables); the Intel `pptxdiff-mac` is less strict +about this but still untrustworthy to ship unsigned. `build.mjs` warns +loudly if you try either, and `pkg` itself prints the same warning. diff --git a/src/packages/binaries/test_build_config.mjs b/src/packages/binaries/test_build_config.mjs index 16ec9b7..da3198d 100644 --- a/src/packages/binaries/test_build_config.mjs +++ b/src/packages/binaries/test_build_config.mjs @@ -28,21 +28,29 @@ function assert(name, cond) { } // --- TARGET_MAP / resolveTarget --- -assert("TARGET_MAP has exactly linux/win/mac keys", ( - JSON.stringify(Object.keys(TARGET_MAP).sort()) === JSON.stringify(["linux", "mac", "win"]) +assert("TARGET_MAP has exactly linux/win/mac/mac-arm64 keys", ( + JSON.stringify(Object.keys(TARGET_MAP).sort()) === JSON.stringify(["linux", "mac", "mac-arm64", "win"]) )); -assert("linux target: node22-linux-x64, no .exe suffix, doesn't need mac signing", ( - TARGET_MAP.linux.pkgTarget === "node22-linux-x64" && !TARGET_MAP.linux.binName.includes(".") && TARGET_MAP.linux.needsMacSign === false +assert("linux target: node22-linux-x64, no .exe suffix, own outDirKey, doesn't need mac signing", ( + TARGET_MAP.linux.pkgTarget === "node22-linux-x64" && !TARGET_MAP.linux.binName.includes(".") && TARGET_MAP.linux.outDirKey === "linux" && TARGET_MAP.linux.needsMacSign === false )); -assert("win target: node22-win-x64, binName ends .exe, doesn't need mac signing", ( - TARGET_MAP.win.pkgTarget === "node22-win-x64" && TARGET_MAP.win.binName.endsWith(".exe") && TARGET_MAP.win.needsMacSign === false +assert("win target: node22-win-x64, binName ends .exe, own outDirKey, doesn't need mac signing", ( + TARGET_MAP.win.pkgTarget === "node22-win-x64" && TARGET_MAP.win.binName.endsWith(".exe") && TARGET_MAP.win.outDirKey === "win" && TARGET_MAP.win.needsMacSign === false )); assert("mac target: node22-macos-x64, needsMacSign true (the whole reason it's built separately in CI)", ( TARGET_MAP.mac.pkgTarget === "node22-macos-x64" && TARGET_MAP.mac.needsMacSign === true )); +assert("mac-arm64 target: node22-macos-arm64, needsMacSign true, binName distinct from the x64 one", ( + TARGET_MAP["mac-arm64"].pkgTarget === "node22-macos-arm64" && TARGET_MAP["mac-arm64"].needsMacSign === true && TARGET_MAP["mac-arm64"].binName !== TARGET_MAP.mac.binName +)); +assert("mac and mac-arm64 share the SAME outDirKey (both download from pptxdiff-mac/)", ( + TARGET_MAP.mac.outDirKey === "mac" && TARGET_MAP["mac-arm64"].outDirKey === "mac" +)); assert("resolveTarget('linux') === TARGET_MAP.linux", resolveTarget("linux") === TARGET_MAP.linux); +assert("resolveTarget('mac-arm64') === TARGET_MAP['mac-arm64']", resolveTarget("mac-arm64") === TARGET_MAP["mac-arm64"]); assert("resolveTarget returns null for an unknown osKey", resolveTarget("solaris") === null); assert("resolveTarget returns null for an empty string", resolveTarget("") === null); +assert("every TARGET_MAP entry declares an outDirKey", Object.values(TARGET_MAP).every((t) => typeof t.outDirKey === "string" && t.outDirKey.length > 0)); // --- ASSET_GLOBS drift guard: must match root package.json's "files" --- // (same fixture-drift-check concern this project already tracks elsewhere @@ -81,6 +89,9 @@ assert("buildOne() removes the temp pkg config in a finally block", ( assert("ASSET_GLOBS are relative (repo-root-relative) paths, not absolute", ( ASSET_GLOBS.every((g) => !path.isAbsolute(g)) )); +assert("buildOne() computes outDir from target.outDirKey, not the osKey argument (so mac/mac-arm64 share pptxdiff-mac/)", ( + /outDir\s*=\s*path\.join\(__dirname, `pptxdiff-\$\{target\.outDirKey\}`\)/.test(buildSrc) +)); // --- bin/cli.js is passed to pkg UNMODIFIED — no assets-folder workaround --- // This is the whole point of switching to pkg (see GAP_CONTEXT.md): the diff --git a/src/packages/binaries/test_build_e2e.mjs b/src/packages/binaries/test_build_e2e.mjs index 78b1dca..47be304 100644 --- a/src/packages/binaries/test_build_e2e.mjs +++ b/src/packages/binaries/test_build_e2e.mjs @@ -8,11 +8,14 @@ // real pkg build downloads/uses a base binary and takes a while) — run via // `npm run test:e2e`, same split as pptxdiff-cli's `test:difftool`. // -// Only exercises the CURRENT host's own platform target — win/mac builds -// are structurally identical (same buildOne(), only the mac codesign step -// differs) but only actually built-and-run by CI's linux+win / -// macos-specific jobs (see .github/workflows/binaries.yml and -// build.mjs's header comment for why mac isn't cross-built here). +// Only exercises the CURRENT host's own platform+arch target — win/linux/ +// the other mac arch build are structurally identical (same buildOne(), +// only the mac codesign branch differs) but only actually built-and-run by +// CI's linux+win / macos-specific jobs (see .github/workflows/binaries.yml +// and build.mjs's header comment for why neither mac target is +// cross-built here). On macOS this picks `mac-arm64` vs `mac` based on the +// HOST's actual arch, so an Apple Silicon runner (GitHub's macos-latest, +// as of 2024) genuinely exercises the native arm64 build, not the x64 one. // // Run: node test_build_e2e.mjs @@ -26,7 +29,12 @@ import { buildOne, resolveTarget } from "./build.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const HOST_TO_OSKEY = { win32: "win", darwin: "mac", linux: "linux" }; +function hostOsKey() { + if (process.platform === "win32") return "win"; + if (process.platform === "linux") return "linux"; + if (process.platform === "darwin") return os.arch() === "arm64" ? "mac-arm64" : "mac"; + return null; +} let pass = 0; let fail = 0; @@ -76,10 +84,10 @@ function waitForLine(child, matcher, timeoutMs) { } async function main() { - const osKey = HOST_TO_OSKEY[process.platform]; + const osKey = hostOsKey(); const target = osKey && resolveTarget(osKey); if (!target) { - console.error(`No target mapping for process.platform=${process.platform} — nothing to e2e-test here.`); + console.error(`No target mapping for process.platform=${process.platform}/${os.arch()} — nothing to e2e-test here.`); process.exitCode = 1; return; } From c01cce76935aa82db796de9935d580189694e72d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 05:40:21 +0000 Subject: [PATCH 5/7] Add native arm64 builds for Windows and Linux Direct follow-up to "can we support arm64 for windows and linux as well?" - immediately after the mac-only arm64 addition. Unlike macOS, neither Windows nor Linux needs a signing step, so both new targets (node22-win-arm64, node22-linux-arm64) fold into the existing cross-compiled build-linux-win CI job rather than needing a new one. Found and fixed a real build failure while verifying pkg actually supports these targets: cross-arch builds need to execute a matching-arch helper binary to generate V8 bytecode, which fails with a genuine exec-format error on a host with no QEMU/binfmt emulation for that arch (confirmed absent in this sandbox, not assumed). pkg's own warning named the fix: --fallback-to-source, now applied unconditionally in buildOne() (a no-op for same-arch builds). Verified via the real production build path: real ELF aarch64 and PE32+ Aarch64 executables, binary size consistent with real assets being embedded, landing correctly in each OS's existing shared output folder without disturbing the tracked docs there. Neither binary was run (no arm64 execution emulation in this sandbox) - CI is what actually executes them for the first time. Genuine RED->GREEN demonstrated on the new --fallback-to-source regression guard, which protects against a class of failure that would silently break every arm64 target while leaving x64 green. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BxwMTp6RQJ6j6K5K8Jjdpm --- .github/workflows/binaries.yml | 22 ++++- CHANGELOG.md | 8 +- docs/.scrolls/GAP_ANALYSIS.md | 6 +- docs/.scrolls/GAP_CONTEXT.md | 12 ++- docs/.scrolls/HANDOFF.md | 8 ++ docs/.scrolls/PLAN.md | 37 ++++++++- docs/.scrolls/SPEC.md | 12 +-- src/packages/binaries/README.md | 82 ++++++++++++------- src/packages/binaries/build.mjs | 30 +++++-- .../binaries/pptxdiff-linux/CHANGELOG.md | 20 ++++- .../binaries/pptxdiff-linux/README.md | 38 +++++---- .../binaries/pptxdiff-win/CHANGELOG.md | 19 +++-- src/packages/binaries/pptxdiff-win/README.md | 29 ++++--- src/packages/binaries/test_build_config.mjs | 28 ++++++- src/packages/binaries/test_build_e2e.mjs | 24 +++--- 15 files changed, 271 insertions(+), 104 deletions(-) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index b2ac091..3f34a35 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -14,8 +14,14 @@ permissions: contents: read jobs: - # linux + win are genuinely cross-compiled by @yao-pkg/pkg from one host — - # no codesigning concern for either, so they build together in one job. + # linux + win (x64 AND arm64) are genuinely cross-compiled by + # @yao-pkg/pkg from one host — confirmed directly (real ELF aarch64 / + # PE32+ Aarch64 executables built on an x64 dev sandbox) — no + # codesigning concern for either OS, so all four build together in one + # job. Cross-ARCH builds need --fallback-to-source (see build.mjs) since + # V8 bytecode generation for a foreign arch fails without QEMU emulation + # — harmless, this is already-open-source code with nothing to protect + # by shipping bytecode instead of plain source. build-linux-win: runs-on: ubuntu-latest steps: @@ -29,18 +35,28 @@ jobs: working-directory: src/packages/binaries - run: npm run test:e2e working-directory: src/packages/binaries - - run: npm run build -- linux win + - run: npm run build -- linux linux-arm64 win win-arm64 working-directory: src/packages/binaries - uses: actions/upload-artifact@v4 with: name: pptxdiff-linux path: src/packages/binaries/pptxdiff-linux/pptxdiff-linux if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: pptxdiff-linux-arm64 + path: src/packages/binaries/pptxdiff-linux/pptxdiff-linux-arm64 + if-no-files-found: error - uses: actions/upload-artifact@v4 with: name: pptxdiff-win path: src/packages/binaries/pptxdiff-win/pptxdiff-win.exe if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: pptxdiff-win-arm64 + path: src/packages/binaries/pptxdiff-win/pptxdiff-win-arm64.exe + if-no-files-found: error # mac (both Intel x64 and Apple Silicon arm64) builds on a REAL macOS # runner specifically so both can be ad-hoc codesigned (codesign only diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba5b3c..a800f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,11 +40,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 state to the tap even without a version-pin change, while a scheduled run still only does real work on an actual version bump. `test_formula.mjs` now also asserts `LICENSE` stays byte-identical to the repo root's copy, catching drift instead of silently shipping a stale license to the tap. -- New private `@pptxdiff/binaries` package (`src/packages/binaries/`) building standalone native `pptxdiff` executables for Windows, macOS (Intel and Apple Silicon), and Linux via `@yao-pkg/pkg` — download one file and run it, no separate Node.js install required. -- Native Apple Silicon (`pptxdiff-mac-arm64`) build alongside the existing Intel one, both sharing `src/packages/binaries/pptxdiff-mac/` — avoids Apple Silicon Macs having to run the Intel binary via Rosetta 2 translation. +- New private `@pptxdiff/binaries` package (`src/packages/binaries/`) building standalone native `pptxdiff` executables for Windows, macOS, and Linux (x64 AND arm64 for all three) via `@yao-pkg/pkg` — download one file and run it, no separate Node.js install required. +- Native arm64 builds for all three OSes (`pptxdiff-mac-arm64`, `pptxdiff-win-arm64.exe`, `pptxdiff-linux-arm64`), each sharing its OS's output folder with the existing x64 build — avoids Apple Silicon Macs having to run the Intel binary via Rosetta 2 translation, and gives Windows-on-ARM/arm64-Linux users a native option too. - Per-OS build output folders `src/packages/binaries/pptxdiff-{win,mac,linux}/`, each with its own `README.md` and `CHANGELOG.md`. -- `.github/workflows/binaries.yml`: `pkg` genuinely cross-compiles, so Windows and Linux build together in one `ubuntu-latest` job; both macOS targets build in their own `macos-latest` job so they can be properly ad-hoc codesigned. -- `make pkg.binaries.build` / `npm run build:binary` for local builds (all four targets by default, or a specific subset). +- `.github/workflows/binaries.yml`: `pkg` genuinely cross-compiles, so Windows and Linux (both chip variants) build together in one `ubuntu-latest` job; both macOS targets build in their own `macos-latest` job so they can be properly ad-hoc codesigned. +- `make pkg.binaries.build` / `npm run build:binary` for local builds (all six targets by default, or a specific subset). - Red/Green TDD test suite for the build tooling itself: `npm test` (fast, pure — config/asset-drift/regression checks) and `npm run test:e2e` (slow, real — builds and runs the actual packaged binary over real HTTP) in `src/packages/binaries/`. ## [0.7.0] - 2026-08-02 diff --git a/docs/.scrolls/GAP_ANALYSIS.md b/docs/.scrolls/GAP_ANALYSIS.md index 6677c6d..7f370b2 100644 --- a/docs/.scrolls/GAP_ANALYSIS.md +++ b/docs/.scrolls/GAP_ANALYSIS.md @@ -168,9 +168,9 @@ Concrete, testable gaps between what SPEC.md describes and a fully "real" implem - [ ] **macOS is not cross-compiled** — `pkg` genuinely can cross-compile a macOS binary from Linux, but the result can't be codesigned there (`codesign` is macOS-only), and a completely unsigned binary may not even launch on Apple Silicon. `.github/workflows/binaries.yml` therefore still runs macOS on a real `macos-latest` runner (`build-mac`), separate from the `build-linux-win` job that genuinely does cross-compile both those targets from one Linux host. This is a real, load-bearing constraint, not leftover caution from the SEA-based version. - [ ] **Not yet attached to GitHub Releases.** The CI workflow uploads each OS's binary as a workflow artifact (downloadable from the Actions run page) but nothing wires a release-tag push to attach them to an actual GitHub Release yet — a real, small follow-up (e.g. `softprops/action-gh-release` on `release: types: [published]`), not attempted this session. - [x] **macOS native arm64 build** — CLOSED: `pptxdiff-mac-arm64` (`node22-macos-arm64`) added after an explicit follow-up question, sharing `pptxdiff-mac/` with the existing Intel `pptxdiff-mac`. Structurally verified in this sandbox (real `Mach-O 64-bit arm64 executable` via `file`); actual signed/run verification only happens on CI's `macos-latest` runner (itself Apple Silicon as of 2024). -- [ ] **Windows and Linux are still x64 only, no native arm64 build for either.** `pkg` does support `node22-win-arm64`/`node22-linux-arm64` if this is ever asked for — not attempted; the macOS arm64 gap was closed specifically because it was asked about directly (and because Apple Silicon has a real functional consequence — Rosetta translation overhead/availability — that Windows-on-ARM/Linux-on-ARM don't have in the same way for this app's target audience). -- [ ] **`ldid`-based Linux-side ad-hoc signing for macOS binaries was not pursued.** `pkg` itself suggests installing the `ldid` utility so a Linux host could ad-hoc-sign a Mach-O binary without any real Mac at all, which would let `build-mac`'s two targets fold into the `build-linux-win` job (three targets cross-compiled + signed from one Linux runner instead of two CI jobs). Not attempted — a real `macos-latest` CI runner was judged more reliable than depending on a third-party signing tool this project has no experience with; worth revisiting if CI cost/time ever becomes a real concern. -- [ ] **macOS/Windows builds are unverified end-to-end in this sandbox** — only the Linux build was actually run and its binary actually executed (built, launched, served `index.html`/`support.js`/`vendor/*` via real HTTP requests) — under both the original SEA mechanism and, again, after switching to `@yao-pkg/pkg`. The Windows binary WAS structurally produced here (real `.exe`, verified during exploration before writing the final `build.mjs`) but not run (no Windows host in this sandbox); the macOS codesign branch is exercised for the first time whenever CI's `build-mac` job runs, not locally. +- [x] **Windows and Linux native arm64 builds** — CLOSED: `pptxdiff-win-arm64.exe` (`node22-win-arm64`) and `pptxdiff-linux-arm64` (`node22-linux-arm64`) added after an explicit follow-up question ("can we support arm64 for windows and linux as well?"), sharing their OS's existing folder. Both genuinely cross-compiled from this x64 Linux sandbox (unlike macOS) — required discovering and adding `pkg`'s `--fallback-to-source` flag, since cross-ARCH V8 bytecode generation fails outright without QEMU/binfmt emulation (confirmed via a real exec-format error before the fix). Structurally verified via the real production `buildOne()` path (real `ELF ... ARM aarch64` / `PE32+ ... Aarch64` executables via `file`, landing in the correct SHARED output folder without disturbing tracked docs); not run (no arm64 emulation available in this sandbox for either). +- [ ] **`ldid`-based Linux-side ad-hoc signing for macOS binaries was not pursued.** `pkg` itself suggests installing the `ldid` utility so a Linux host could ad-hoc-sign a Mach-O binary without any real Mac at all, which would let `build-mac`'s two targets fold into the `build-linux-win` job (all six targets cross-compiled + signed from one Linux runner instead of two CI jobs). Not attempted — a real `macos-latest` CI runner was judged more reliable than depending on a third-party signing tool this project has no experience with; worth revisiting if CI cost/time ever becomes a real concern. +- [ ] **macOS/Windows builds are unverified end-to-end in this sandbox** — only the Linux x64 build was actually RUN (built, launched, served `index.html`/`support.js`/`vendor/*` via real HTTP requests) — under both the original SEA mechanism and, again, after switching to `@yao-pkg/pkg`. The other five targets (win x64/arm64, mac x64/arm64, linux-arm64) were all structurally produced here (real, correctly-typed executables, verified via `file`) but none of them RUN — no Windows/macOS host in this sandbox, and no arm64 execution emulation available for the arm64 targets. CI's `build-linux-win`/`build-mac` jobs are what actually execute and verify each of the other five for the first time. ## Content checksum (this session) - [ ] **The content checksum shows "unavailable" under the plain `file://` open path.** `crypto.subtle` (native Web Crypto, no new dependency) requires a secure context — guaranteed under the CLI's `http://localhost` default, not guaranteed under this project's other documented launch path (`git clone` + open `index.html` directly). Handled honestly (checked once at boot via `cryptoSubtleAvailable`, shown as "unavailable (requires a secure context)" rather than a wrong/fabricated hash or a permanently-stuck "computing…"), but not worked around — would need either a pure-JS SHA-256 fallback (a real new dependency, or a hand-rolled implementation neither asked for nor free of its own correctness risk) or accepting the gap under that one launch path. diff --git a/docs/.scrolls/GAP_CONTEXT.md b/docs/.scrolls/GAP_CONTEXT.md index c7e1e85..6c8219f 100644 --- a/docs/.scrolls/GAP_CONTEXT.md +++ b/docs/.scrolls/GAP_CONTEXT.md @@ -187,10 +187,16 @@ The honest tradeoff accepted: one more devDependency (`@yao-pkg/pkg`, dev-time o ## Why macOS is still built on its own CI runner instead of also being cross-compiled from the Linux job `pkg` genuinely CAN produce a macOS binary from Linux (verified — see above), so "just cross-compile all three from one job" was the first instinct. Rejected after considering what codesigning actually requires: `codesign` only exists on macOS, so a Linux-built mac binary can never be even ad-hoc signed, and on Apple Silicon specifically, AMFI (Apple Mobile File Integrity) requires AT LEAST an ad-hoc signature for an arm64 executable to launch at all — this isn't "a Gatekeeper warning users can click through" the way it is on Intel Macs, it can mean the binary refuses to run, full stop. Shipping a binary that might not even launch on the now-dominant Apple Silicon Macs would be a real regression from the original SEA-based version (which DID build mac on an actual `macos-latest` runner and could ad-hoc sign it). `.github/workflows/binaries.yml` keeps macOS on its own `macos-latest` job specifically so `codesign --sign -` runs for real; `build.mjs`'s `buildOne()` warns loudly (not silently) if the mac target is ever built off of a non-darwin host, rather than producing something that looks fine at build time and fails mysteriously at launch time. `pkg`'s own error output independently confirmed this reasoning is correct, not just this project's own assumption: attempting `pkg -t node22-macos-arm64` from Linux prints "Due to the mandatory code signing requirement... it will be immediately killed by kernel on launch. An ad-hoc signature is sufficient" verbatim — Apple's own enforcement, described by a tool with no stake in this project's specific decisions. -## Why macOS got a native arm64 build but Windows/Linux didn't (yet) -Explicit follow-up question: "Does the mac binary work for Apple Silicon MacBooks?" Honest answer at the time: yes, but only via Rosetta 2 translation, since only the `node22-macos-x64` target had been built — a real, asked-about gap, not a hypothetical one. `pkg` supports `node22-win-arm64`/`node22-linux-arm64` equally well, so in principle all three OSes could get a native arm64 build — but only macOS's was added, for two concrete reasons specific to that platform, not "arm64 in general": (1) it was the one actually asked about; (2) macOS is the platform where running the "wrong" chip's binary has a REAL functional cost for this app's audience — Apple Silicon is the dominant Mac chip as of 2026, Rosetta 2 isn't guaranteed pre-installed on a fresh machine (first x64-binary launch prompts an install), and there's real translation overhead on every launch. Windows-on-ARM and Linux-on-ARM are comparatively rare desktop targets for THIS app's users (a local PowerPoint-diffing tool), so adding those wasn't scoped in without being asked — same "respect the literal scope of the ask" principle WISDOM.md already documents for confirmation-gate scoping, applied here to a build-target decision instead. +## Why macOS got a native arm64 build but Windows/Linux didn't (yet) — SUPERSEDED, see below +Explicit follow-up question: "Does the mac binary work for Apple Silicon MacBooks?" Honest answer at the time: yes, but only via Rosetta 2 translation, since only the `node22-macos-x64` target had been built — a real, asked-about gap, not a hypothetical one. `pkg` supports `node22-win-arm64`/`node22-linux-arm64` equally well, so in principle all three OSes could get a native arm64 build — but only macOS's was added AT THE TIME, for two concrete reasons specific to that platform, not "arm64 in general": (1) it was the one actually asked about; (2) macOS is the platform where running the "wrong" chip's binary has a REAL functional cost for this app's audience — Apple Silicon is the dominant Mac chip as of 2026, Rosetta 2 isn't guaranteed pre-installed on a fresh machine, and there's real translation overhead on every launch. Windows-on-ARM/Linux-on-ARM were judged comparatively rare enough not to scope in without being asked — same "respect the literal scope of the ask" principle WISDOM.md documents for confirmation-gate scoping. The very next follow-up question directly asked for them anyway ("can we support arm64 for windows and linux as well?") — see the entry below for what shipped and what that surfaced. - **Mechanically**: `TARGET_MAP` entries gained an `outDirKey` field, separate from the map's own key, specifically so `mac` and `mac-arm64` (two different pkg targets, two different `binName`s) can share ONE output folder (`pptxdiff-mac/`) — from a user's perspective picking a download, "which folder do I look in" should be answered by OS, and "which specific file do I download" by chip, not two different folders for the same OS. `buildOne()` was changed to compute `outDir` from `target.outDirKey` rather than the `osKey` argument it's called with, with a dedicated regression test (`test_build_config.mjs`) protecting exactly that computation, demonstrated RED→GREEN for real. -- **Why not use `ldid` to also fold macOS into the cross-compiled Linux job**: `pkg`'s own error output (see the entry above) explicitly suggests installing `ldid` — a tool that can produce ad-hoc-equivalent Mach-O signatures from a non-macOS host — as an alternative to building on a real Mac. Considered but not pursued: this project has zero prior experience with `ldid`'s reliability/compatibility characteristics, and a real `macos-latest` GitHub Actions runner (using Apple's own `codesign`, the same tool a real Mac uses) is unambiguously the more trustworthy signing path for a first implementation. Worth revisiting explicitly if CI job count/time ever becomes a real constraint — flagged as a named, deliberate non-choice in GAP_ANALYSIS.md rather than silently never considered. +- **Why not use `ldid` to also fold macOS into the cross-compiled Linux job**: `pkg`'s own error output (see the entry above) explicitly suggests installing `ldid` — a tool that can produce ad-hoc-equivalent Mach-O signatures from a non-macOS host — as an alternative to building on a real Mac. Considered but not pursued: this project has zero prior experience with `ldid`'s reliability/compatibility characteristics, and a real `macos-latest` GitHub Actions runner (using Apple's own `codesign`, the same tool a real Mac uses) is unambiguously the more trustworthy signing path for a first implementation. Worth revisiting explicitly if CI job count/time ever becomes a real constraint — flagged as a named, deliberate non-choice in GAP_ANALYSIS.md rather than silently never considered. (This reasoning still stands — unaffected by the Windows/Linux arm64 addition below, which has nothing to do with macOS signing.) + +## Why Windows/Linux arm64 was added after all, and what it took to actually work +The premise of the entry above ("Windows-on-ARM/Linux-on-ARM are comparatively rare, not asked about") was accurate for its moment but stopped applying the instant the user asked directly — per WISDOM.md's standing rule that a new explicit ask supersedes a prior scope decision rather than needing to be argued with. Unlike the mac case, there was no codesigning blocker to work around: `pkg` genuinely cross-compiles both `node22-win-arm64` and `node22-linux-arm64` from an x64 host with no signing step needed at all for either OS, so both fold directly into the existing `build-linux-win` CI job rather than needing a new one. +- **A real, previously-unencountered build failure was found and fixed, not just "it worked because pkg supports arm64 targets."** The very first attempt (`pkg -t node22-linux-arm64 ...`) failed with a genuine `ERR_ASSERTION`; with `--debug`, the actual cause was a `Syntax error: ")" unexpected` coming from `/root/.pkg-cache/.../fetched-v22.23.2-linuxstatic-arm64` — `pkg` needs to EXECUTE a matching-arch "fabricator" helper binary to generate V8 bytecode for the entry script, and running a foreign-arch ELF directly on this x64 sandbox (no QEMU/binfmt emulation registered — confirmed via `which qemu-aarch64`/`/proc/sys/fs/binfmt_misc` both coming up empty) makes the shell try to interpret the binary's raw bytes as a script, producing exactly that syntax error. `pkg`'s own warning pointed at the fix: `--fallback-to-source`, which ships the entry as plain JS instead of failing when bytecode generation isn't possible. Verified the fix actually works (not just that the error message went away) by checking the resulting binary's `file` output and comparing its size to the known-good x64 build (72-75MB either way, consistent with real assets actually being embedded, not a stripped-down failure artifact). +- **Why `--fallback-to-source` unconditionally, not just for arm64 targets**: it's a no-op for a same-arch build (bytecode generation succeeds normally there, so the flag is never invoked) — conditionally applying it only for `*-arm64` targets would add a branch to protect against a failure mode that doesn't exist for the other case, for no behavioral difference. Simpler to always pass it and let `pkg` decide per-build whether it's needed. +- **Why this wasn't cross-verified by actually RUNNING the arm64 binaries**: this sandbox is x64 with no arm64 execution emulation available (confirmed, not assumed — see above). The structural verification performed (a real build via the actual production `buildOne()` path with the real asset config, `file` confirming the correct architecture/format, confirming the binary size is consistent with assets actually being embedded, confirming it landed in the correct shared output folder without disturbing tracked docs) is the strongest verification achievable in this environment; CI's `build-linux-win` job is what will actually execute these two targets for the first time, the same honest gap already true of the Windows x64 binary before any of this session's work. ## Why @pptxdiff/server ships with no authentication rather than a minimal API key CLI_API_DESIGN.md §8 calls for API-key-required-on-non-loopback-bind as part of the design, but implementing even a minimal key check touches real security-sensitive surface (where the key comes from, how it's compared, timing-attack considerations) that deserves its own deliberate pass rather than being bolted on inside a Phase-1 session already covering three other new pieces (automation shim, CLI, server routing). The loopback-by-default bind (matching `bin/cli.js`'s existing precedent) is the one security property that WAS carried over faithfully; the auth gap is real, named explicitly in the package's own README (not just a scroll only this project's own sessions read), and is the literal next thing to build before anyone binds this server to a non-loopback host in practice. diff --git a/docs/.scrolls/HANDOFF.md b/docs/.scrolls/HANDOFF.md index c2955a3..1e1a711 100644 --- a/docs/.scrolls/HANDOFF.md +++ b/docs/.scrolls/HANDOFF.md @@ -2,6 +2,14 @@ **Read `.scrolls/SPEC.md` first for the full feature list.** This file is the "what's the state of things right now" note — update it at the end of every session, keep it short and current (prune stale entries). +## Update (2026-08-05 — binaries: native Windows/Linux arm64 builds, `pptxdiff-win-arm64.exe`/`pptxdiff-linux-arm64`) +- Direct, immediate follow-up to the mac-only arm64 addition below: "Can we support arm64 for windows and linux as well?" Unlike macOS, neither Windows nor Linux needs a signing step, so no new CI job was needed — both fold straight into the existing `build-linux-win` job. +- **Verified `pkg` genuinely supports both targets first, hands-on, before implementing** (same rigor as every other step in this feature): the FIRST attempt (`pkg -t node22-linux-arm64 bin/cli.js ...`) failed outright with `ERR_ASSERTION`. `--debug` traced the real cause to a genuine exec-format failure: `pkg` needs to run a matching-arch "fabricator" helper binary to generate V8 bytecode for the entry script, and this x64 sandbox has no QEMU/binfmt arm64 emulation registered (confirmed empty, not assumed: `which qemu-aarch64` and `/proc/sys/fs/binfmt_misc` both came up empty) — the shell tried to interpret the foreign-arch ELF's raw bytes as a script, producing a `Syntax error`. `pkg`'s own warning named the fix: `--fallback-to-source` (ships the entry as plain JS instead of failing when bytecode generation isn't possible for the target arch) — added unconditionally to `buildOne()`'s pkg invocation (harmless no-op for same-arch builds, where bytecode generation just succeeds normally). +- **Confirmed the fix actually works, not just that the error message went away**: rebuilt both targets via the real production `buildOne()` path (real asset config, not a bare entry-only smoke test), confirmed via `file` genuine `ELF 64-bit LSB executable, ARM aarch64` and `PE32+ executable ... Aarch64` outputs, and — since this sandbox can't execute either binary (no arm64 emulation) — cross-checked binary SIZE against the known-good x64 builds (72-75MB either way) as evidence real assets are actually embedded, not a truncated/failed artifact. +- **`TARGET_MAP` gained `linux-arm64`/`win-arm64` entries**, each sharing its OS's existing `outDirKey` (same mechanism the mac-arm64 addition established) — `.github/workflows/binaries.yml`'s `build-linux-win` job now builds and uploads all four Windows/Linux target/arch combinations. `test_build_e2e.mjs`'s host-detection generalized from "only darwin checks arch" to checking `os.arch()` uniformly across all three platforms, so a future arm64 Linux/Windows CI runner would also genuinely test its native target. +- **Genuine RED→GREEN demonstrated on the new `--fallback-to-source` guard**: temporarily removed the flag, confirmed the dedicated regression test caught it (28/29 — this is exactly the kind of regression that would silently break every arm64 target while every x64 test kept passing, hence a dedicated static check rather than trusting "the tests still pass"), restored it, confirmed 29/29. `test_build_config.mjs` now 29 assertions (was 23). +- Scrolls updated to match: SPEC.md §32, PLAN.md (new "Done this session" block, prior "not attempted" ticket closed), GAP_ANALYSIS.md (arm64 gap for win/linux closed, the "unverified end-to-end" gap widened to name all five non-run targets honestly), GAP_CONTEXT.md (prior entry marked superseded, new entry with the full exec-format-failure reproduction), per-OS win/linux `README.md`/`CHANGELOG.md`, top-level `src/packages/binaries/README.md`, root `CHANGELOG.md`. + ## Update (2026-08-05 — binaries: native Apple Silicon build, `pptxdiff-mac-arm64`) - Direct follow-up question on the same-day `@yao-pkg/pkg` switch below: "Does the mac binary work for Apple Silicon MacBooks?" Honest answer given first: yes, but only via Rosetta 2 translation — only the `node22-macos-x64` target existed. Asked whether to add a native arm64 build (real effort: another CI target, another README/CHANGELOG update) via `AskUserQuestion` before doing the work; user said yes. - **`TARGET_MAP` gained an `outDirKey` field**, separate from the map's own key, so `mac` (Intel) and the new `mac-arm64` (Apple Silicon, `node22-macos-arm64`) can share ONE output folder (`pptxdiff-mac/`) while keeping distinct `binName`s — from a user's download perspective, "which folder" should be answered by OS, "which file" by chip, not two folders for the same OS. `buildOne()` now computes `outDir` from `target.outDirKey`, not the `osKey` it's called with. diff --git a/docs/.scrolls/PLAN.md b/docs/.scrolls/PLAN.md index e773f23..ebcc19f 100644 --- a/docs/.scrolls/PLAN.md +++ b/docs/.scrolls/PLAN.md @@ -504,12 +504,41 @@ shim, not sequentially — that plan is what shipped below. mac targets. ## New tickets opened this session -1. **P4 — Native Windows/Linux arm64 builds**, if ever asked for — `pkg` supports - `node22-win-arm64`/`node22-linux-arm64` equally well; not attempted since neither was asked about - and arm64 desktop/laptop usage is comparatively rare for those two OSes among this app's likely - users (see GAP_CONTEXT.md). +1. ~~**P4 — Native Windows/Linux arm64 builds**, if ever asked for.~~ **[DONE, same day]** — see + below; the user asked directly in an immediate follow-up. 2. **P4 — Investigate `ldid` for Linux-side ad-hoc signing of macOS binaries**, which `pkg`'s own error output suggests as an alternative to a real macOS CI runner — would let `build-mac` fold into the cross-compiled `build-linux-win` job (one CI job instead of two). Not pursued; a real `macos-latest` runner using Apple's own `codesign` was judged more trustworthy for a first pass — revisit if CI job count/time ever becomes a real constraint. + +## Done this session (native Windows/Linux arm64 builds: `pptxdiff-win-arm64.exe`, `pptxdiff-linux-arm64`) +- [x] **P4 — Added `pptxdiff-win-arm64.exe` and `pptxdiff-linux-arm64`**, direct follow-up to "can we + support arm64 for windows and linux as well?" (immediately after the mac-only arm64 addition + above). Unlike macOS, neither needs a signing step, so both fold straight into the existing + `build-linux-win` CI job — `TARGET_MAP` gained `linux-arm64`/`win-arm64` entries sharing their + OS's `outDirKey`, `.github/workflows/binaries.yml`'s `build-linux-win` job now builds and uploads + all four Windows/Linux target/arch combos. +- [x] **Found and fixed a genuine new build failure, not just "it worked because pkg supports arm64 + targets."** First attempt (`pkg -t node22-linux-arm64 ...`) failed with `ERR_ASSERTION`; `--debug` + traced it to a real exec-format error — generating V8 bytecode for a foreign arch requires running + a matching-arch "fabricator" helper, which fails outright without QEMU/binfmt emulation (confirmed + absent in this sandbox: `which qemu-aarch64` and `/proc/sys/fs/binfmt_misc` both empty). `pkg`'s + own warning named the fix: `--fallback-to-source`, now applied unconditionally in `buildOne()`'s + pkg invocation (a no-op for same-arch builds, where bytecode generation just succeeds normally). +- [x] **Verified via the real production `buildOne()` path** (not a bare smoke test): built + `node22-linux-arm64` and `node22-win-arm64` with the real asset config, confirmed via `file` genuine + `ELF ... ARM aarch64` / `PE32+ ... Aarch64` executables, confirmed binary SIZE is consistent with + real assets actually being embedded (72-75MB, matching the known-good x64 builds — not a + stripped-down failure artifact), confirmed each landed in its correct SHARED output folder without + disturbing the tracked `README.md`/`CHANGELOG.md` already there. Not run — no arm64 execution + emulation available in this sandbox; CI is what actually executes these for the first time. +- [x] **Genuine RED→GREEN demonstrated on the new `--fallback-to-source` regression guard**: + temporarily removed the flag from `buildOne()`'s pkg invocation, confirmed the dedicated test + caught it (28/29), restored it, confirmed 29/29. `test_build_config.mjs` now 29 assertions (was 23). +- [x] `test_build_e2e.mjs`'s host-target detection generalized from "arm64 only matters on darwin" to + checking `os.arch()` for every platform, so an arm64 Linux/Windows CI runner would also genuinely + exercise its native target rather than always falling back to x64. +- [x] Per-OS win/linux `README.md`/`CHANGELOG.md`, the top-level `src/packages/binaries/README.md`, + root `CHANGELOG.md`, `SPEC.md` §32, `GAP_ANALYSIS.md`, and `GAP_CONTEXT.md` all updated — all six + targets now documented consistently. diff --git a/docs/.scrolls/SPEC.md b/docs/.scrolls/SPEC.md index 956f689..b092989 100644 --- a/docs/.scrolls/SPEC.md +++ b/docs/.scrolls/SPEC.md @@ -301,18 +301,18 @@ Word-level diff (LCS-based) highlights changed words within text/table-cell/char - **Failure behavior**: unsupported browser values or unknown options fail before the local server starts, with exit code `2` and a clear error. If the selected browser command itself is missing or cannot launch in a headless/no-GUI environment, the CLI still prints the local URL and ignores the browser-open failure, preserving the prior "URL is enough to proceed manually" behavior. - **Testing**: `src/pptxdiff/test_execfile_browser_open_cli.mjs` now covers `parseArgs()` for both `--browser=value` and `--browser value`, rejects unsupported values, and verifies all platform/browser command builders still pass the URL as a single `execFile()` argv element rather than shell-interpolating it. ## 36. Standalone native binaries (`@pptxdiff/binaries`, added this session) -- **What it does**: `src/packages/binaries/` builds a standalone, native `pptxdiff` executable per target — download ONE file, run it, `pptxdiff` opens in the browser. No Node.js install, no `npm install -g`, no `npx`, no separate assets folder to keep alongside it. Output lands in `src/packages/binaries/pptxdiff-win/pptxdiff-win.exe`, `pptxdiff-mac/{pptxdiff-mac,pptxdiff-mac-arm64}`, `pptxdiff-linux/pptxdiff-linux` (the folder names the user requested — the two mac binaries share one folder, distinguished by chip). Build output is gitignored (generated, not source); each OS folder keeps a tracked `README.md` and `CHANGELOG.md` describing what a build produces there. -- **Native Apple Silicon build** (`pptxdiff-mac-arm64`), added after an explicit follow-up question ("does the mac binary work for Apple Silicon MacBooks?"). Before this, Apple Silicon Macs could only run the Intel (`pptxdiff-mac`) binary via Rosetta 2 translation — real launch overhead, and Rosetta isn't guaranteed pre-installed on a fresh Mac. `TARGET_MAP` entries gained an `outDirKey` field (separate from the map's own key) so `mac` and `mac-arm64` can share `pptxdiff-mac/` as their output folder while each keeping distinct `binName`s — `buildOne()` computes `outDir` from `target.outDirKey`, not the `osKey` argument. Verified for real in this sandbox (Linux): built the `node22-macos-arm64` target and confirmed via `file` it's a genuine `Mach-O 64-bit arm64 executable` (unsigned, as expected off a non-macOS host); `pkg` itself independently prints the same Apple-Silicon-signing warning this project's own reasoning already relied on. +- **What it does**: `src/packages/binaries/` builds a standalone, native `pptxdiff` executable per target — six total (x64 + arm64 for each of Windows/macOS/Linux) — download ONE file, run it, `pptxdiff` opens in the browser. No Node.js install, no `npm install -g`, no `npx`, no separate assets folder to keep alongside it. Output lands in `src/packages/binaries/pptxdiff-win/{pptxdiff-win.exe,pptxdiff-win-arm64.exe}`, `pptxdiff-mac/{pptxdiff-mac,pptxdiff-mac-arm64}`, `pptxdiff-linux/{pptxdiff-linux,pptxdiff-linux-arm64}` (the folder names the user requested — each OS's two chip variants share one folder, distinguished by binary name). Build output is gitignored (generated, not source); each OS folder keeps a tracked `README.md` and `CHANGELOG.md` describing what a build produces there. +- **Native arm64 builds for all three OSes**, added incrementally across two follow-up questions: first macOS ("does the mac binary work for Apple Silicon MacBooks?" — answer: only via Rosetta 2 translation until this), then Windows/Linux ("can we support arm64 for windows and linux as well?"). `TARGET_MAP` entries gained an `outDirKey` field (separate from the map's own key) so e.g. `mac`/`mac-arm64` share `pptxdiff-mac/` as their output folder while each keeping distinct `binName`s — `buildOne()` computes `outDir` from `target.outDirKey`, not the `osKey` argument. Cross-ARCH builds (`*-arm64` from an x64 host, or vice versa) need `pkg`'s `--fallback-to-source` flag: generating V8 bytecode for a foreign architecture requires executing a matching-arch helper binary, which fails outright without QEMU/binfmt emulation — confirmed directly via a genuine exec-format error before the flag was added, not assumed from documentation. Verified for real in this sandbox (x64 Linux): built `node22-macos-arm64`, `node22-linux-arm64`, and `node22-win-arm64` directly via the real production `buildOne()` path (real asset config, not a bare smoke test), confirmed via `file` they're genuine `Mach-O 64-bit arm64`/`ELF ... ARM aarch64`/`PE32+ ... Aarch64` executables landing correctly in their SHARED output folders without disturbing the tracked `README.md`/`CHANGELOG.md` already there; `pkg` itself independently prints the same Apple-Silicon-signing warning this project's own reasoning already relied on for the mac case. - **Deliberately standalone binaries, not real OS installers**: asked directly (binaries vs. true `.msi`/`.pkg`/`.deb` installers with an install wizard, PATH registration, code signing) and the user picked standalone binaries — consistent with this project's prior explicit decision (see GAP_CONTEXT.md "Why the npm CLI opens a browser tab instead of a real native window") to avoid Electron/Tauri-style installer and code-signing overhead. See `src/packages/binaries/README.md` for the full reasoning. - **Mechanism: `@yao-pkg/pkg`, not Node's own SEA feature.** This package originally shipped using Node's built-in Single Executable Applications (SEA) feature. Switched after an explicit user question ("why aren't you using yao-pkg/pkg?") surfaced two real advantages SEA doesn't have — see GAP_CONTEXT.md for the full reasoning and the honest tradeoff (one more third-party build-tool devDependency, dev-time only): 1. **Real cross-compilation.** `pkg` downloads a prebuilt base `node` binary per TARGET platform and injects the bundled app into it, so one Linux host builds the Windows AND Linux binaries. SEA can only build for whatever OS it's currently running on. 2. **Built-in asset embedding, with zero `bin/cli.js` changes.** `pkg`'s snapshot filesystem mirrors the real project's relative directory layout at runtime — `bin/cli.js`'s existing, completely UNMODIFIED `ROOT = path.join(__dirname, "..", "src", "pptxdiff")` computation resolves correctly with `build.mjs` simply listing `src/pptxdiff/index.html`/`support.js`/`sample-pptx.js`/`vendor/**` as pkg `"assets"`. (The SEA version had needed an added `root` parameter on `startServer()` plus a separate `assets/` folder shipped next to the binary — both fully reverted; `bin/cli.js` is now byte-identical to how it looked before this whole feature.) - **A hard-won gotcha discovered mid-build** (see WISDOM.md's new trap entry): `pkg`'s `"assets"` glob paths in a config file resolve relative to wherever THAT CONFIG FILE ITSELF lives — not cwd, not the entry file's directory. Get it wrong and it fails completely silently (zero assets embedded, no warning, only a 404 when the packaged binary actually runs). `build.mjs`'s `buildOne()` writes a temp pkg config directly at the repo root (next to the real `package.json`, where `src/pptxdiff/**` actually resolves), removed in a `finally` block. -- **Neither macOS target is cross-compiled.** `pkg` can produce macOS binaries (either chip) from Linux, but can't codesign them (`codesign` only exists on macOS) — and on Apple Silicon, a completely unsigned binary may not even launch (AMFI requires at least an ad-hoc signature, unlike Intel Macs where it's "only" a Gatekeeper warning). `buildOne()` only runs its codesign step when `process.platform === "darwin"`, warning loudly otherwise rather than silently shipping something that might not run. `.github/workflows/binaries.yml` reflects this: Windows+Linux build together in one `ubuntu-latest` job (`build-linux-win`); both `mac`/`mac-arm64` build separately on `macos-latest` (`build-mac`, itself an Apple Silicon runner as of 2024) so they're genuinely signed. -- **Known, documented gaps** (see GAP_ANALYSIS.md): unsigned Windows `.exe` (SmartScreen warning) and ad-hoc-signed-only macOS binaries (Gatekeeper warning) — no code-signing certificate exists for this project; not yet wired to GitHub Releases; Windows/Linux are x64 only, no native arm64 build for either. +- **Neither macOS target is cross-compiled** (Windows and Linux both ARE, x64 and arm64 alike). `pkg` can produce macOS binaries (either chip) from Linux, but can't codesign them (`codesign` only exists on macOS) — and on Apple Silicon, a completely unsigned binary may not even launch (AMFI requires at least an ad-hoc signature, unlike Intel Macs where it's "only" a Gatekeeper warning). `buildOne()` only runs its codesign step when `process.platform === "darwin"`, warning loudly otherwise rather than silently shipping something that might not run. `.github/workflows/binaries.yml` reflects this: Windows+Linux (all four target/arch combos) build together in one `ubuntu-latest` job (`build-linux-win`); both `mac`/`mac-arm64` build separately on `macos-latest` (`build-mac`, itself an Apple Silicon runner as of 2024) so they're genuinely signed. +- **Known, documented gaps** (see GAP_ANALYSIS.md): unsigned Windows `.exe`s (SmartScreen warning) and ad-hoc-signed-only macOS binaries (Gatekeeper warning) — no code-signing certificate exists for this project; not yet wired to GitHub Releases; a Linux-side `ldid`-based signing workaround for macOS (which would let `build-mac` fold into the cross-compiled Linux job) was considered but not pursued. - **Verified locally** (Linux, this sandbox), for both mechanisms in turn: built for real, ran the actual packaged binary directly (not just `bin/cli.js`), confirmed it correctly serves `index.html`/`support.js`/`vendor/*` via real `curl` requests — once under Node SEA, and again after the switch to `@yao-pkg/pkg`, this time with a true single file and zero `bin/cli.js` changes. Windows/macOS builds are structurally identical (same `buildOne()`, only the mac codesign branch differs) but unverified end-to-end locally — no Windows/macOS host in this sandbox; CI exercises them for real. - **Red/Green TDD, two test files** (`build.mjs` has an entrypoint guard — same pattern as `capture_screenshots.mjs`, see WISDOM.md — so `TARGET_MAP`/`ASSET_GLOBS`/`resolveTarget`/`buildOne`/`buildAll` are importable without a real build running as a side effect): - - `test_build_config.mjs` (fast, pure, no subprocess/network, `npm test`): 23 assertions covering `TARGET_MAP`'s per-target shape (now four entries — `linux`/`win`/`mac`/`mac-arm64`), that `mac`/`mac-arm64` share one `outDirKey`, an `ASSET_GLOBS`-vs-root-`package.json`-"files" drift guard, that `bin/cli.js` is passed to pkg completely unmodified (no packaging-specific parameter), the macOS-signing safety checks, and two sharp static-source regression checks: that `buildOne()` writes its temp pkg config at `REPO_ROOT` (the config-colocation gotcha above), and that it computes `outDir` from `target.outDirKey` rather than the `osKey` argument (the exact mechanism that lets `mac`/`mac-arm64` share one folder). Demonstrated genuine RED→GREEN three times across this session's two rounds: the now-retired SEA-era `startServer(root)` contract, the pkg-config-colocation guard, and the `outDirKey` guard (temporarily reverted `outDir`'s computation to use `osKey`, confirmed the one dependent assertion failed (22/23), restored it, confirmed 23/23). - - `test_build_e2e.mjs` (slow, real, current-platform-only, `npm run test:e2e` — same split as `pptxdiff-cli`'s `test:difftool`): picks the host's own target via `process.platform`+`os.arch()` (so a `darwin`/`arm64` host tests `mac-arm64`, not `mac`), calls `buildOne()`, then spawns the REAL resulting single-file executable and drives it over real HTTP — `GET /`, `/support.js`, `/vendor/react.production.min.js` all 200 with correct content, an explicit assertion that no separate `assets/` folder exists, plus a path-traversal request. 10/10 assertions, genuinely GREEN against a real binary built and run in this sandbox by BOTH mechanisms in turn. Cleans up only the binary file afterward, never the tracked `README.md`/`CHANGELOG.md` in the same folder. + - `test_build_config.mjs` (fast, pure, no subprocess/network, `npm test`): 29 assertions covering `TARGET_MAP`'s per-target shape (six entries — `linux`/`linux-arm64`/`win`/`win-arm64`/`mac`/`mac-arm64`), that each OS's two chip variants share one `outDirKey` and have distinct `binName`s (no collision), an `ASSET_GLOBS`-vs-root-`package.json`-"files" drift guard, that `bin/cli.js` is passed to pkg completely unmodified (no packaging-specific parameter), the macOS-signing safety checks, and three sharp static-source regression checks: that `buildOne()` writes its temp pkg config at `REPO_ROOT` (the config-colocation gotcha above), that it computes `outDir` from `target.outDirKey` rather than the `osKey` argument (the exact mechanism that lets chip variants share one folder), and that its `pkg` invocation includes `--fallback-to-source` (required for every cross-arch build — losing it would silently break all four arm64 targets while leaving the x64 ones unaffected, easy to miss without a dedicated check). Demonstrated genuine RED→GREEN four times across this session's three rounds: the now-retired SEA-era `startServer(root)` contract, the pkg-config-colocation guard, the `outDirKey` guard, and the `--fallback-to-source` guard (removed the flag, confirmed the one dependent assertion failed (28/29), restored it, confirmed 29/29). + - `test_build_e2e.mjs` (slow, real, current-platform-only, `npm run test:e2e` — same split as `pptxdiff-cli`'s `test:difftool`): picks the host's own target via `process.platform`+`os.arch()` (an arm64 host of any OS tests its `-arm64` target, not the x64 one), calls `buildOne()`, then spawns the REAL resulting single-file executable and drives it over real HTTP — `GET /`, `/support.js`, `/vendor/react.production.min.js` all 200 with correct content, an explicit assertion that no separate `assets/` folder exists, plus a path-traversal request. 10/10 assertions, genuinely GREEN against a real binary built and run in this sandbox by BOTH mechanisms in turn, and unaffected by the arm64 target additions (this sandbox is x64, so it still exercises `linux`). Cleans up only the binary file afterward, never the tracked `README.md`/`CHANGELOG.md` in the same folder. - **Structurally verified the new `mac-arm64` target directly in this sandbox** (Linux): ran a real `node build.mjs mac-arm64` build, confirmed via `file` a genuine `Mach-O 64-bit arm64 executable`, confirmed it landed in the SHARED `pptxdiff-mac/` folder alongside the tracked `README.md`/`CHANGELOG.md` without disturbing them, and confirmed `pkg` itself independently prints the exact Apple-Silicon-signing warning this project's own reasoning already relied on — cleaned up afterward (can't be run/verified over HTTP from Linux; that's CI's job). diff --git a/src/packages/binaries/README.md b/src/packages/binaries/README.md index f87da7f..1f95f42 100644 --- a/src/packages/binaries/README.md +++ b/src/packages/binaries/README.md @@ -22,11 +22,13 @@ actively-maintained community fork of the Vercel-archived `pkg`) after an explicit question about it, for two concrete reasons SEA can't match: 1. **Real cross-compilation.** `pkg` downloads a prebuilt "base" node - binary per target platform and injects the bundled app into it — one - Linux host can build the Windows AND Linux binaries. SEA injects into a - copy of the *currently running* node binary, so it can only ever build - for the OS it's actually running on (the old 3-OS CI matrix existed - solely to work around that). + binary per target platform+arch and injects the bundled app into it — + one Linux host can build the Windows AND Linux binaries, x64 AND arm64 + (confirmed directly: real `ELF ... ARM aarch64` and `PE32+ ... Aarch64` + executables, both built on this project's own x64 Linux dev sandbox). + SEA injects into a copy of the *currently running* node binary, so it + can only ever build for the OS+arch it's actually running on (the old + 3-OS CI matrix existed solely to work around that). 2. **Built-in asset embedding.** `pkg`'s snapshot filesystem preserves the real project's relative directory structure at runtime, so `bin/cli.js`'s existing, UNMODIFIED `ROOT = path.join(__dirname, "..", @@ -52,21 +54,43 @@ can ad-hoc-sign Mach-O binaries without a real Mac at all — not pursued here; a real `macos-latest` CI runner was judged simpler and more reliable than depending on a third tool for signing.) -## Apple Silicon (arm64) +## arm64 targets -Two mac targets exist, both landing in `pptxdiff-mac/`: +Six targets exist total, two per OS, sharing one output folder each: | osKey | pkg target | binary | chip | |---|---|---|---| +| `linux` | `node22-linux-x64` | `pptxdiff-linux` | x64 | +| `linux-arm64` | `node22-linux-arm64` | `pptxdiff-linux-arm64` | arm64 | +| `win` | `node22-win-x64` | `pptxdiff-win.exe` | x64 | +| `win-arm64` | `node22-win-arm64` | `pptxdiff-win-arm64.exe` | arm64 | | `mac` | `node22-macos-x64` | `pptxdiff-mac` | Intel | | `mac-arm64` | `node22-macos-arm64` | `pptxdiff-mac-arm64` | Apple Silicon (native) | -Without the `mac-arm64` target, an Apple Silicon Mac would only be able to +`TARGET_MAP` entries share an `outDirKey` (e.g. `"mac"` for both mac +targets) separate from their own map key, specifically so multiple chip +variants of the same OS land in one folder — see `build.mjs`'s +`buildOne()`. + +**Cross-ARCH builds need `--fallback-to-source`.** Generating V8 bytecode +for a foreign architecture requires running a matching-arch "fabricator" +helper — confirmed directly: attempting `node22-linux-arm64` from this +project's x64 sandbox without the flag failed with a genuine exec-format +error (the shell tried to interpret the arm64 ELF helper's raw bytes as a +script). `--fallback-to-source` ships the entry as plain JS instead of +precompiled bytecode whenever that happens — a real, worthwhile tradeoff +here (this is already-open-source code; bytecode's only benefit is +marginal startup speed and minor reverse-engineering friction, neither +worth a hard build failure). Harmless for same-arch builds, where +bytecode generation just succeeds and the flag is never invoked. + +macOS is the one OS where arm64 isn't "just another cross-compiled +target" — without a native arm64 build, an Apple Silicon Mac could only run the Intel binary via Rosetta 2 translation (extra launch overhead, -and Rosetta isn't guaranteed pre-installed on a fresh Mac). `TARGET_MAP` -entries share an `outDirKey` (`"mac"` for both) separate from their own -map key, specifically so multiple chip variants of the same OS can land -in one folder — see `build.mjs`'s `buildOne()`. +and Rosetta isn't guaranteed pre-installed on a fresh Mac). Windows/Linux +arm64 desktops are a comparatively small fraction of this app's likely +audience, so those two targets exist for completeness/parity rather than +a specific reported need — see `docs/.scrolls/GAP_CONTEXT.md`. ## How it works @@ -99,17 +123,17 @@ tracked file inside this package's own directory. ```sh cd src/packages/binaries npm install -npm run build # builds all four targets by default -npm run build -- linux win # or build a specific subset -npm run build -- mac mac-arm64 # (on macOS, for a properly-signed result) +npm run build # builds all six targets by default +npm run build -- linux linux-arm64 win win-arm64 # or build a specific subset +npm run build -- mac mac-arm64 # (on macOS, for a properly-signed result) ``` Output lands in `./pptxdiff-/` — one file per -target, nothing else needed alongside it (the two mac targets share the -`pptxdiff-mac/` folder). Each OS folder keeps a tracked `README.md` +target, nothing else needed alongside it (each OS's two chip variants +share one folder). Each OS folder keeps a tracked `README.md` (usage/known-warnings) and `CHANGELOG.md` (Keep a Changelog, tracks the bundled `pptxdiff` app version) — `build.mjs` only ever touches the -specific binary file it's building, never those two or the other target's +specific binary file it's building, never those two or any other target's binary. ## Testing (Red/Green TDD) @@ -123,22 +147,24 @@ npm run test:e2e # slow, real — builds an actual binary for the CURRENT # support.js/vendor/* + a path-traversal check) ``` -`test:e2e` only exercises the current host's own target (on macOS, it -picks `mac` vs `mac-arm64` based on the host's actual `os.arch()`, so an -Apple Silicon CI runner genuinely tests the native `mac-arm64` build) — -every target is structurally identical (same `buildOne()`, only the mac -codesign branch differs) but only actually built-and-run by CI. +`test:e2e` only exercises the current host's own target — it picks the +`-arm64` variant of whatever OS it's running on when the HOST's actual +`os.arch()` is arm64, so an arm64 CI runner (or Apple Silicon macOS one) +genuinely tests the native build, not the x64 one. Every target is +structurally identical (same `buildOne()`, only the mac codesign branch +differs) but only actually built-and-run by CI — this sandbox is x64, so +only `linux`/`win`/`mac`'s x64 code paths are actually EXECUTED locally; +the four arm64 targets are structurally verified (real builds via +`buildOne()`, confirmed via `file`) but not run. ## Known gaps (see `docs/.scrolls/GAP_ANALYSIS.md`) -- **Unsigned Windows `.exe` / ad-hoc-signed-only macOS binaries.** No +- **Unsigned Windows `.exe`s / ad-hoc-signed-only macOS binaries.** No code-signing certificate — Windows SmartScreen and macOS Gatekeeper will warn on a freshly-downloaded copy. Documented per-OS in each `pptxdiff-/README.md`. - **Not attached to GitHub Releases yet.** CI currently only uploads build artifacts on push/dispatch; wiring a release-tag trigger to attach them to a GitHub Release is a follow-up, not done here. -- **x64 only for Windows and Linux** — no native arm64 build for either - (matches the original scope; macOS is the one OS with a native arm64 - build, added after an explicit follow-up ask since Apple Silicon is now - the dominant Mac). +- **`ldid` (Linux-side macOS signing) not pursued** — see "arm64 targets" + above; would let `build-mac` fold into the cross-compiled Linux job. diff --git a/src/packages/binaries/build.mjs b/src/packages/binaries/build.mjs index 8d741fc..58a40f4 100644 --- a/src/packages/binaries/build.mjs +++ b/src/packages/binaries/build.mjs @@ -8,9 +8,12 @@ // used). // // Unlike Node SEA, pkg genuinely cross-compiles: it downloads a prebuilt -// "base" node binary for each TARGET platform and injects the bundled app -// into it, so a single Linux (or any) host can build the Windows and Linux -// binaries. macOS is the one exception in THIS build — see below. +// "base" node binary for each TARGET platform+arch and injects the +// bundled app into it, so a single Linux (or any) host can build the +// Windows and Linux binaries (x64 AND arm64 — confirmed directly: a real +// ELF aarch64 executable and a real PE32+ Aarch64 executable, both built +// on this project's own x64 Linux dev sandbox). macOS is the one +// exception in THIS build — see below. // // Points bin/cli.js's UNMODIFIED entry point directly at pkg, and embeds // the same static app files (index.html/support.js/sample-pptx.js/ @@ -55,12 +58,15 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, "..", "..", ".."); // `outDirKey` is separate from the map's own key so multiple targets can -// share one OS folder — `mac` (Intel/x64) and `mac-arm64` (Apple Silicon, -// native) both land in `pptxdiff-mac/`, since they're the same OS as far -// as a user picking a download is concerned, just a different chip. +// share one OS folder — e.g. `mac`/`mac-arm64` (Intel/Apple Silicon) both +// land in `pptxdiff-mac/`, `win`/`win-arm64` both land in `pptxdiff-win/`, +// since they're the same OS as far as a user picking a download is +// concerned, just a different chip. export const TARGET_MAP = { linux: { pkgTarget: "node22-linux-x64", binName: "pptxdiff-linux", outDirKey: "linux", needsMacSign: false }, + "linux-arm64": { pkgTarget: "node22-linux-arm64", binName: "pptxdiff-linux-arm64", outDirKey: "linux", needsMacSign: false }, win: { pkgTarget: "node22-win-x64", binName: "pptxdiff-win.exe", outDirKey: "win", needsMacSign: false }, + "win-arm64": { pkgTarget: "node22-win-arm64", binName: "pptxdiff-win-arm64.exe", outDirKey: "win", needsMacSign: false }, mac: { pkgTarget: "node22-macos-x64", binName: "pptxdiff-mac", outDirKey: "mac", needsMacSign: true }, "mac-arm64": { pkgTarget: "node22-macos-arm64", binName: "pptxdiff-mac-arm64", outDirKey: "mac", needsMacSign: true }, }; @@ -108,6 +114,18 @@ export async function buildOne(osKey, target) { target.pkgTarget, "-o", binOut, + // A cross-ARCH build (e.g. building *-arm64 from this x64 host) needs + // to run a foreign-arch "fabricator" helper to generate V8 bytecode, + // which fails outright without QEMU/binfmt emulation registered — + // confirmed directly (a genuine exec-format failure, not asset- or + // config-related). --fallback-to-source ships the entry as plain JS + // instead of pre-compiled bytecode when that happens; harmless for a + // same-arch build (bytecode generation just succeeds normally and + // this flag is never invoked) and an acceptable tradeoff here — this + // is already-open-source code, so bytecode's only real benefit + // (marginal startup speed / minor reverse-engineering friction) + // isn't worth failing the build over. + "--fallback-to-source", ]); if (target.needsMacSign) { diff --git a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md index 0af4e35..e54acc4 100644 --- a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md @@ -1,11 +1,11 @@ -# Changelog — pptxdiff for Linux (standalone binary) +# Changelog — pptxdiff for Linux (standalone binaries) -All notable changes to the Linux standalone `pptxdiff-linux` build are -documented here. The format is based on +All notable changes to the Linux standalone `pptxdiff-linux`/ +`pptxdiff-linux-arm64` builds are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version tracked is the `pptxdiff` app version bundled into the binary (see the root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) -since the binary has no independent feature set of its own. +since the binaries have no independent feature set of their own. ## [Unreleased] @@ -29,6 +29,18 @@ since the binary has no independent feature set of its own. real, confirmed to correctly serve `index.html`/`support.js`/`vendor/*` over real HTTP requests with zero code changes to `bin/cli.js` (see `../test_build_e2e.mjs`). +- **Native arm64 build (`pptxdiff-linux-arm64`)**, added after an explicit + follow-up ask to extend the macOS arm64 work to Windows/Linux too. Needs + `--fallback-to-source` (see `../README.md`) since generating V8 bytecode + for a foreign architecture isn't possible without QEMU emulation on the + build host — confirmed directly via a genuine exec-format failure + without the flag. Verified for real in this project's own dev sandbox + (x64): built the `node22-linux-arm64` target via the real production + path (`buildOne()`, real asset config) and confirmed via `file` it's a + genuine `ELF ... ARM aarch64` executable, landing correctly in this + shared folder without disturbing the tracked `README.md`/`CHANGELOG.md`. + Not run (no arm64 emulation available in that sandbox) — real execution + verification is CI's job. ### Known limitations diff --git a/src/packages/binaries/pptxdiff-linux/README.md b/src/packages/binaries/pptxdiff-linux/README.md index 5388dc9..e78686e 100644 --- a/src/packages/binaries/pptxdiff-linux/README.md +++ b/src/packages/binaries/pptxdiff-linux/README.md @@ -1,23 +1,31 @@ # pptxdiff for Linux -This folder holds the built Linux artifact — not committed here, generated -by `../build.mjs` (see `../README.md`; can be built from any host OS, -`@yao-pkg/pkg` cross-compiles it too — no Linux machine strictly needed, -though this one's easiest to verify on Linux itself). +This folder holds the built Linux artifacts — not committed here, +generated by `../build.mjs` (see `../README.md`; can be built from any +host OS, `@yao-pkg/pkg` cross-compiles both chip variants). -After a build, this folder contains: +After a build, this folder contains one or both of: -- `pptxdiff-linux` — the standalone executable. A true single file: the - Node runtime AND the static app files it serves are both embedded - inside it. No separate folder needed alongside it. +- `pptxdiff-linux` — x64. +- `pptxdiff-linux-arm64` — arm64 (e.g. Raspberry Pi 4/5 running a 64-bit + OS, AWS Graviton, most modern arm64 SBCs/servers). -Run it with `chmod +x pptxdiff-linux && ./pptxdiff-linux` (the build -already sets the executable bit; re-set it if you moved/copied it -somewhere that dropped it). +If you're not sure which one you need, run `uname -m` — `x86_64` means +`pptxdiff-linux`, `aarch64`/`arm64` means `pptxdiff-linux-arm64`. Both are +true single files: the Node runtime AND the static app files it serves +are both embedded inside them. No separate folder needed alongside +either one. -Verified end-to-end in this project's own dev sandbox: built for real, the -actual packaged binary was run and confirmed to correctly serve +Run with `chmod +x && ./` (the build already sets the +executable bit; re-set it if you moved/copied it somewhere that dropped +it). + +Verified end-to-end in this project's own dev sandbox: built for real, +the actual x64 packaged binary was run and confirmed to correctly serve `index.html`/`support.js`/`vendor/*` over real HTTP requests, with zero -code changes to `bin/cli.js` itself (see `../test_build_e2e.mjs`). +code changes to `bin/cli.js` (see `../test_build_e2e.mjs`). The arm64 +binary was built for real too (confirmed via `file` as a genuine `ELF ... +ARM aarch64` executable) but not run — this sandbox is x64 and has no +QEMU/binfmt arm64 emulation available to execute it. -To build: `cd src/packages/binaries && npm install && npm run build -- linux`. +To build: `cd src/packages/binaries && npm install && npm run build -- linux linux-arm64`. diff --git a/src/packages/binaries/pptxdiff-win/CHANGELOG.md b/src/packages/binaries/pptxdiff-win/CHANGELOG.md index fe7f13f..41c7275 100644 --- a/src/packages/binaries/pptxdiff-win/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-win/CHANGELOG.md @@ -1,11 +1,11 @@ -# Changelog — pptxdiff for Windows (standalone binary) +# Changelog — pptxdiff for Windows (standalone binaries) -All notable changes to the Windows standalone `pptxdiff-win.exe` build are -documented here. The format is based on -[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version +All notable changes to the Windows standalone `pptxdiff-win.exe`/ +`pptxdiff-win-arm64.exe` builds are documented here. The format is based +on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version tracked is the `pptxdiff` app version bundled into the binary (see the root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) -since the binary has no independent feature set of its own. +since the binaries have no independent feature set of their own. ## [Unreleased] @@ -22,6 +22,15 @@ since the binary has no independent feature set of its own. Node.js install, no companion folder needed. - Genuinely cross-compiled: this binary can be built from any host OS (Linux, macOS, or Windows), not just Windows itself. +- **Native arm64 build (`pptxdiff-win-arm64.exe`)**, added after an + explicit follow-up ask to extend the macOS arm64 work to Windows/Linux + too. Needs `--fallback-to-source` (see `../README.md`) since generating + V8 bytecode for a foreign architecture isn't possible without QEMU + emulation on the build host — confirmed directly via a genuine + exec-format failure without the flag. Verified for real in this + project's own dev sandbox (x64 Linux): built the `node22-win-arm64` + target and confirmed via `file` it's a genuine `PE32+ ... Aarch64` + executable. ### Known limitations diff --git a/src/packages/binaries/pptxdiff-win/README.md b/src/packages/binaries/pptxdiff-win/README.md index 73bbae4..68bed32 100644 --- a/src/packages/binaries/pptxdiff-win/README.md +++ b/src/packages/binaries/pptxdiff-win/README.md @@ -1,15 +1,22 @@ # pptxdiff for Windows -This folder holds the built Windows artifact — not committed here, +This folder holds the built Windows artifacts — not committed here, generated by `../build.mjs` (see `../README.md`; can be built from ANY -host OS, `@yao-pkg/pkg` cross-compiles it — no Windows machine needed). +host OS, `@yao-pkg/pkg` cross-compiles both chip variants — no Windows +machine needed). -After a build, this folder contains: +After a build, this folder contains one or both of: -- `pptxdiff-win.exe` — the standalone executable. A true single file: the - Node runtime AND the static app files it serves are both embedded - inside it. No separate folder needed alongside it, no Node.js install - needed to run it. +- `pptxdiff-win.exe` — x64 (Intel/AMD). +- `pptxdiff-win-arm64.exe` — arm64 (Windows on ARM, e.g. Surface Pro X and + newer Snapdragon-based laptops). + +If you're not sure which one you need, `pptxdiff-win.exe` (x64) is almost +certainly correct — it also runs on most arm64 Windows machines via +built-in x64 emulation, just not natively. Both are true single files: +the Node runtime AND the static app files it serves are both embedded +inside them. No separate folder needed alongside either one, no Node.js +install needed to run them. **Unsigned.** There is no code-signing certificate for this project, so Windows SmartScreen will likely warn on first run ("Windows protected your @@ -17,6 +24,8 @@ PC") — click "More info" → "Run anyway". See `docs/.scrolls/GAP_ANALYSIS.md` for why this is a documented, accepted tradeoff rather than an oversight. -To build: `cd src/packages/binaries && npm install && npm run build -- win` -(works from Linux, macOS, or Windows — this target is genuinely -cross-compiled). +To build: `cd src/packages/binaries && npm install && npm run build -- win win-arm64` +(works from Linux, macOS, or Windows — both targets are genuinely +cross-compiled; the arm64 one needs `--fallback-to-source`, already baked +into `build.mjs`, since cross-arch V8 bytecode generation isn't possible +without QEMU emulation on the build host). diff --git a/src/packages/binaries/test_build_config.mjs b/src/packages/binaries/test_build_config.mjs index da3198d..ee9277e 100644 --- a/src/packages/binaries/test_build_config.mjs +++ b/src/packages/binaries/test_build_config.mjs @@ -28,15 +28,22 @@ function assert(name, cond) { } // --- TARGET_MAP / resolveTarget --- -assert("TARGET_MAP has exactly linux/win/mac/mac-arm64 keys", ( - JSON.stringify(Object.keys(TARGET_MAP).sort()) === JSON.stringify(["linux", "mac", "mac-arm64", "win"]) +const EXPECTED_KEYS = ["linux", "linux-arm64", "mac", "mac-arm64", "win", "win-arm64"]; +assert(`TARGET_MAP has exactly the six expected keys (got ${JSON.stringify(Object.keys(TARGET_MAP).sort())})`, ( + JSON.stringify(Object.keys(TARGET_MAP).sort()) === JSON.stringify(EXPECTED_KEYS) )); assert("linux target: node22-linux-x64, no .exe suffix, own outDirKey, doesn't need mac signing", ( TARGET_MAP.linux.pkgTarget === "node22-linux-x64" && !TARGET_MAP.linux.binName.includes(".") && TARGET_MAP.linux.outDirKey === "linux" && TARGET_MAP.linux.needsMacSign === false )); +assert("linux-arm64 target: node22-linux-arm64, shares linux's outDirKey, distinct binName, doesn't need mac signing", ( + TARGET_MAP["linux-arm64"].pkgTarget === "node22-linux-arm64" && TARGET_MAP["linux-arm64"].outDirKey === "linux" && TARGET_MAP["linux-arm64"].binName !== TARGET_MAP.linux.binName && TARGET_MAP["linux-arm64"].needsMacSign === false +)); assert("win target: node22-win-x64, binName ends .exe, own outDirKey, doesn't need mac signing", ( TARGET_MAP.win.pkgTarget === "node22-win-x64" && TARGET_MAP.win.binName.endsWith(".exe") && TARGET_MAP.win.outDirKey === "win" && TARGET_MAP.win.needsMacSign === false )); +assert("win-arm64 target: node22-win-arm64, binName ends .exe, shares win's outDirKey, distinct binName, doesn't need mac signing", ( + TARGET_MAP["win-arm64"].pkgTarget === "node22-win-arm64" && TARGET_MAP["win-arm64"].binName.endsWith(".exe") && TARGET_MAP["win-arm64"].outDirKey === "win" && TARGET_MAP["win-arm64"].binName !== TARGET_MAP.win.binName && TARGET_MAP["win-arm64"].needsMacSign === false +)); assert("mac target: node22-macos-x64, needsMacSign true (the whole reason it's built separately in CI)", ( TARGET_MAP.mac.pkgTarget === "node22-macos-x64" && TARGET_MAP.mac.needsMacSign === true )); @@ -46,11 +53,18 @@ assert("mac-arm64 target: node22-macos-arm64, needsMacSign true, binName distinc assert("mac and mac-arm64 share the SAME outDirKey (both download from pptxdiff-mac/)", ( TARGET_MAP.mac.outDirKey === "mac" && TARGET_MAP["mac-arm64"].outDirKey === "mac" )); +assert("only the two mac targets need signing — every linux/win target (x64 or arm64) does not", ( + ["linux", "linux-arm64", "win", "win-arm64"].every((k) => TARGET_MAP[k].needsMacSign === false) +)); assert("resolveTarget('linux') === TARGET_MAP.linux", resolveTarget("linux") === TARGET_MAP.linux); assert("resolveTarget('mac-arm64') === TARGET_MAP['mac-arm64']", resolveTarget("mac-arm64") === TARGET_MAP["mac-arm64"]); +assert("resolveTarget('win-arm64') === TARGET_MAP['win-arm64']", resolveTarget("win-arm64") === TARGET_MAP["win-arm64"]); assert("resolveTarget returns null for an unknown osKey", resolveTarget("solaris") === null); assert("resolveTarget returns null for an empty string", resolveTarget("") === null); assert("every TARGET_MAP entry declares an outDirKey", Object.values(TARGET_MAP).every((t) => typeof t.outDirKey === "string" && t.outDirKey.length > 0)); +assert("every TARGET_MAP binName is unique (no two targets would collide in the same outDir)", ( + new Set(Object.values(TARGET_MAP).map((t) => t.outDirKey + "/" + t.binName)).size === Object.keys(TARGET_MAP).length +)); // --- ASSET_GLOBS drift guard: must match root package.json's "files" --- // (same fixture-drift-check concern this project already tracks elsewhere @@ -92,6 +106,16 @@ assert("ASSET_GLOBS are relative (repo-root-relative) paths, not absolute", ( assert("buildOne() computes outDir from target.outDirKey, not the osKey argument (so mac/mac-arm64 share pptxdiff-mac/)", ( /outDir\s*=\s*path\.join\(__dirname, `pptxdiff-\$\{target\.outDirKey\}`\)/.test(buildSrc) )); +// A cross-ARCH build (e.g. linux-arm64/win-arm64 from an x64 host) fails +// outright without this flag — confirmed directly, a genuine exec-format +// error trying to run a foreign-arch bytecode-generation helper. Losing +// this flag would silently break EVERY arm64 target's build (pkg errors +// out instead of falling back to plain-source shipping), the same +// "passes on x64, breaks only for arm64" blind spot the config-colocation +// gotcha above already represents for a different reason. +assert("buildOne()'s pkg invocation includes --fallback-to-source (required for cross-arch builds)", ( + /"--fallback-to-source"/.test(buildSrc) +)); // --- bin/cli.js is passed to pkg UNMODIFIED — no assets-folder workaround --- // This is the whole point of switching to pkg (see GAP_CONTEXT.md): the diff --git a/src/packages/binaries/test_build_e2e.mjs b/src/packages/binaries/test_build_e2e.mjs index 47be304..46e5d84 100644 --- a/src/packages/binaries/test_build_e2e.mjs +++ b/src/packages/binaries/test_build_e2e.mjs @@ -8,14 +8,15 @@ // real pkg build downloads/uses a base binary and takes a while) — run via // `npm run test:e2e`, same split as pptxdiff-cli's `test:difftool`. // -// Only exercises the CURRENT host's own platform+arch target — win/linux/ -// the other mac arch build are structurally identical (same buildOne(), -// only the mac codesign branch differs) but only actually built-and-run by -// CI's linux+win / macos-specific jobs (see .github/workflows/binaries.yml -// and build.mjs's header comment for why neither mac target is -// cross-built here). On macOS this picks `mac-arm64` vs `mac` based on the -// HOST's actual arch, so an Apple Silicon runner (GitHub's macos-latest, -// as of 2024) genuinely exercises the native arm64 build, not the x64 one. +// Only exercises the CURRENT host's own platform+arch target — every other +// target is structurally identical (same buildOne(), only the mac +// codesign branch differs) but only actually built-and-run by CI's +// linux+win / macos-specific jobs (see .github/workflows/binaries.yml and +// build.mjs's header comment for why neither mac target is cross-built +// there). Picks the `-arm64` variant of whatever OS it's running on when +// the HOST's actual `os.arch()` is arm64, so an Apple-Silicon macOS +// runner (GitHub's macos-latest, as of 2024) or an arm64 Linux/Windows +// runner genuinely exercises the native build, not the x64 one. // // Run: node test_build_e2e.mjs @@ -30,9 +31,10 @@ import { buildOne, resolveTarget } from "./build.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); function hostOsKey() { - if (process.platform === "win32") return "win"; - if (process.platform === "linux") return "linux"; - if (process.platform === "darwin") return os.arch() === "arm64" ? "mac-arm64" : "mac"; + const isArm64 = os.arch() === "arm64"; + if (process.platform === "win32") return isArm64 ? "win-arm64" : "win"; + if (process.platform === "linux") return isArm64 ? "linux-arm64" : "linux"; + if (process.platform === "darwin") return isArm64 ? "mac-arm64" : "mac"; return null; } From 8719f73187d88f49e2256824d82313ebef0b36a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 06:11:56 +0000 Subject: [PATCH 6/7] Update docs-site for the standalone native binaries feature The public MkDocs docs-site had never been touched across any of the @pptxdiff/binaries commits, even though docs/.scrolls was kept current throughout - confirmed via git log rather than assumed. - getting-started.md: new "Option D - standalone binary" install option, with a matching doc_coverage id (native-binaries, partial). - cli.md: cross-link to Option D. index.md: updated install card. - limitations.md: new row for the binaries' own limitations, and reworded the existing "browser tab, not native window" row so it doesn't read like the binaries are an exception to it. - New changelog subpage changelogs/pptxdiff-binaries.md, transcluding all three per-OS CHANGELOG.md files (this package has no single package-level one) - added to nav and the changelog index. - coverage_registry.yml: two new ids, native-binaries and native-binaries-limitations; sync_doc_coverage.py --write/--check re-run (36 complete, 4 partial, 0 missing). Found and fixed a real mkdocs build --strict failure: the three per-OS CHANGELOG.md files' relative link back to the root CHANGELOG.md was correct on GitHub but broke once transcluded into the new subpage at a different path. Fixed at the source with an absolute GitHub URL, matching every other changelog subpage's existing pattern. Verified for real: mkdocs build --strict clean, and directly grepped the built HTML to confirm the new anchor matches exactly between the defining page and both pages that link to it. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BxwMTp6RQJ6j6K5K8Jjdpm --- docs/.scrolls/DOCS.md | 65 +++++++++++++++++++ docs/.scrolls/HANDOFF.md | 11 ++++ .../binaries/pptxdiff-linux/CHANGELOG.md | 2 +- .../binaries/pptxdiff-mac/CHANGELOG.md | 2 +- .../binaries/pptxdiff-win/CHANGELOG.md | 2 +- src/pptxdiff/docs-site/CHANGELOG.md | 16 +++++ src/pptxdiff/docs-site/docs/changelog.md | 1 + .../docs/changelogs/pptxdiff-binaries.md | 25 +++++++ src/pptxdiff/docs-site/docs/cli.md | 2 + .../docs-site/docs/documentation-coverage.md | 17 ++++- .../docs-site/docs/getting-started.md | 32 ++++++++- src/pptxdiff/docs-site/docs/index.md | 2 +- src/pptxdiff/docs-site/docs/limitations.md | 5 +- src/pptxdiff/docs-site/mkdocs.yml | 1 + .../docs-site/scripts/coverage_registry.yml | 6 ++ 15 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 src/pptxdiff/docs-site/docs/changelogs/pptxdiff-binaries.md diff --git a/docs/.scrolls/DOCS.md b/docs/.scrolls/DOCS.md index b1bd964..a550a35 100644 --- a/docs/.scrolls/DOCS.md +++ b/docs/.scrolls/DOCS.md @@ -286,3 +286,68 @@ PUBLIC-docs-site side of the same rule. index), so no FAQ change was needed; `docs/.scrolls/SPEC.md`/`GAP_ANALYSIS.md`/`GAP_CONTEXT.md` already fully cover the underlying feature from the sessions that built it — this session's scope was specifically the public docs site, per the explicit ask. +## 16. Content updates + new changelog subpage for standalone native binaries (`@pptxdiff/binaries`, added a later session) + +Follow-up to the `@pptxdiff/binaries` feature (SPEC.md §36 — six standalone executables, x64+arm64 +for Windows/macOS/Linux, via `@yao-pkg/pkg`) shipped across several turns of the same session — the +`.scrolls/` working-memory docs were updated as each piece landed, but the PUBLIC docs-site was not +touched at all until an explicit follow-up ("verify docs/.scrolls folder is updated and so is the +docs-site folder"). Confirmed via `git log --oneline master..HEAD -- src/pptxdiff/docs-site/` +returning zero commits before this entry — a real, not hypothetical, gap. + +- **`getting-started.md` gained "Option D — standalone binary (no Node.js at all)"**, between the + existing Option C (just the file) and "What happens on first load" — this page's whole job is + already "here are the ways to run pptxdiff," so a new install option belongs here, not a new + top-level page (see §14's own reasoning for the opposite call on `headless-cli-api.md`, which + documents a genuinely different tool with a different purpose — this is the SAME `bin/cli.js`, + just packaged differently). New `doc_coverage:` id `native-binaries` (`partial` — the page is + accurate, but the underlying feature has known gaps: unsigned/ad-hoc-signed, not on GitHub + Releases yet), anchored to the new section. +- **`cli.md` gained a one-paragraph cross-link** ("No Node.js at all?") pointing at the new Option D + — no new `doc_coverage:` id here, since `getting-started.md`'s new id already covers the feature + and this is supplementary framing on an already-covered page, not a second independent unit of + coverage. +- **`limitations.md` gained a new row** (`native-binaries-limitations`, `complete` — the limitation + itself is fully documented even though the underlying feature has real gaps, same convention §14 + established for `headless-cli-api-limitations`) and the existing "npm CLI opens a browser tab" row + was reworded to note the binaries share that same property (same `bin/cli.js`, not a native-window + wrapper) rather than reading as if the binaries somehow escaped it. +- **`index.md`'s "No install required" card** updated to mention the binaries option — a factual + correction (the card previously implied only two options existed), no coverage-registry impact + (the card doesn't carry its own `doc_coverage:` entry, `index.md`'s existing ids are unaffected). +- **New changelog subpage `changelogs/pptxdiff-binaries.md`**, added to nav under "NPM Package(s)" + (same category `pptxdiff-cli`/`@pptxdiff/server` already live in, despite also being `private: + true` unpublished packages — the nav grouping in this site means "package.json-defined sibling in + this repo," not "published to the npm registry") and to `changelog.md`'s index. **Structurally + different from every other changelog subpage**: `@pptxdiff/binaries` has no single package-level + `CHANGELOG.md` (each OS's binary has its own, since they're independent downloadable artifacts — + see `SPEC.md` §36/`GAP_CONTEXT.md`) — so this page transcludes all THREE + (`src/packages/binaries/pptxdiff-{win,mac,linux}/CHANGELOG.md`) under their own `## Windows` / + `## macOS` / `## Linux` subheadings, rather than one `--8<--` include like every other subpage. +- **A real `mkdocs build --strict` failure found and fixed, not just described**: the three per-OS + `CHANGELOG.md` files each linked back to the root `CHANGELOG.md` via a relative path + (`../../../../CHANGELOG.md`) that's correct when the file is read on GitHub (four levels up from + `src/packages/binaries/pptxdiff-win/`) but WRONG once transcluded verbatim into + `docs-site/docs/changelogs/pptxdiff-binaries.md` — `pymdownx.snippets` is a textual include, it + does not rewrite relative links to account for where the content ends up, so the link resolved + against the WRONG base and `mkdocs build --strict`'s link checker correctly flagged it (twice — the + same relative link appears in the transcluded content of two other, unrelated warnings the build + also printed for the same reason). Fixed at the SOURCE (`src/packages/binaries/pptxdiff-{win,mac, + linux}/CHANGELOG.md` themselves, since the docs-site page transcludes them verbatim) by swapping + the relative link for the same absolute GitHub URL pattern every other subpage's own "Source:" + line already uses — the fix had to happen in the package-level files, not the docs-site page, since + the page has no content of its own to fix. +- **`scripts/coverage_registry.yml` gained two new ids** (`native-binaries` under `features:`, + `native-binaries-limitations` under `limitations:`), each added in the same change as the + page/row that declares them via `doc_coverage:` front matter, per this file's own §8/§14 rule — + `sync_doc_coverage.py --write` then `--check` re-run to confirm 36 complete + 4 partial + 0 missing + (of 40; was 34 complete + 1 partial before this session's headless-CLI-API work, then presumably + higher still after intervening sessions not otherwise noted here). +- **Verified for real**: `mkdocs build --strict` clean after the link fix (one remaining, expected, + harmless warning — `git-revision-date-localized-plugin` complaining the brand-new, not-yet-committed + `pptxdiff-binaries.md` has no git history yet; resolves itself once committed, not a structural + issue). Directly grepped the built HTML (`getting-started/index.html`, `cli/index.html`, + `limitations/index.html`) to confirm the new anchor (`option-e-standalone-binary-no-nodejs-at-all`) + matches EXACTLY between the page that defines it and the two pages that link to it, rather than + trusting that `--strict` alone would have caught a mismatched anchor (MkDocs's built-in link + checker validates that a linked FILE exists, not that a `#fragment` inside it does). diff --git a/docs/.scrolls/HANDOFF.md b/docs/.scrolls/HANDOFF.md index 1e1a711..50f4bb6 100644 --- a/docs/.scrolls/HANDOFF.md +++ b/docs/.scrolls/HANDOFF.md @@ -2,6 +2,17 @@ **Read `.scrolls/SPEC.md` first for the full feature list.** This file is the "what's the state of things right now" note — update it at the end of every session, keep it short and current (prune stale entries). +## Update (2026-08-05 — docs-site update for the binaries feature, a real gap closed) +- Explicit ask: "verify docs/.scrolls folder is updated and so is the docs-site folder, apart from the README/CHANGELOG file(s)." Checked rather than assumed: `docs/.scrolls/` was confirmed fully updated across all four `@pptxdiff/binaries` commits so far, but `git log --oneline master..HEAD -- src/pptxdiff/docs-site/` came back with **zero commits** — the public MkDocs site had never been touched by any of this session's binaries work. A real gap, not a formality. +- Read `docs/.scrolls/DOCS.md` first, per its own standing rule (anything beyond a routine single-page content edit gets checked against it) — this was more than routine (new page, new nav entry, new coverage-registry ids). +- **`getting-started.md`** gained "Option D — standalone binary (no Node.js at all)," landing between the existing Option C and "What happens on first load" (this page's whole job is already "here are the ways to run pptxdiff"). New `doc_coverage:` id `native-binaries` (`partial`). +- **`cli.md`** gained a one-line cross-link to Option D; **`index.md`**'s "No install required" card updated to mention it; **`limitations.md`** gained a new `native-binaries-limitations` row (`complete`) and had its existing "npm CLI opens a browser tab" row reworded to note the binaries share that property rather than reading as an exception. +- **New changelog subpage `changelogs/pptxdiff-binaries.md`**, added to nav — structurally different from every sibling subpage since `@pptxdiff/binaries` has no single package-level `CHANGELOG.md` (each OS has its own); this page transcludes all three under `## Windows`/`## macOS`/`## Linux` headings instead of one `--8<--` include. +- **Real `mkdocs build --strict` failure found and fixed, not just described**: the three per-OS `CHANGELOG.md` files' relative link back to the root `CHANGELOG.md` (`../../../../CHANGELOG.md`, correct on GitHub) broke once transcluded into the new subpage — `pymdownx.snippets` doesn't rewrite relative links for their new location. Fixed at the source (the package-level files themselves, since the docs-site page has no content of its own) by swapping to the same absolute GitHub URL pattern every other subpage's "Source:" line already uses. +- **`scripts/coverage_registry.yml`** gained `native-binaries`/`native-binaries-limitations`; `sync_doc_coverage.py --write` then `--check` re-run, confirmed 36 complete + 4 partial + 0 missing (of 40). +- **Verified for real**: `mkdocs build --strict` clean (one remaining, expected, harmless warning about the brand-new file having no git history yet — resolves on commit). Directly grepped the built HTML to confirm the new anchor (`option-e-standalone-binary-no-nodejs-at-all`) matches exactly between the defining page and both linking pages, rather than trusting `--strict` alone (it validates linked files exist, not that `#fragment`s inside them do). +- Full technical writeup: `docs/.scrolls/DOCS.md` §15 (new). `docs-site/CHANGELOG.md` gained a matching dated entry per the project's own house rule. + ## Update (2026-08-05 — binaries: native Windows/Linux arm64 builds, `pptxdiff-win-arm64.exe`/`pptxdiff-linux-arm64`) - Direct, immediate follow-up to the mac-only arm64 addition below: "Can we support arm64 for windows and linux as well?" Unlike macOS, neither Windows nor Linux needs a signing step, so no new CI job was needed — both fold straight into the existing `build-linux-win` job. - **Verified `pkg` genuinely supports both targets first, hands-on, before implementing** (same rigor as every other step in this feature): the FIRST attempt (`pkg -t node22-linux-arm64 bin/cli.js ...`) failed outright with `ERR_ASSERTION`. `--debug` traced the real cause to a genuine exec-format failure: `pkg` needs to run a matching-arch "fabricator" helper binary to generate V8 bytecode for the entry script, and this x64 sandbox has no QEMU/binfmt arm64 emulation registered (confirmed empty, not assumed: `which qemu-aarch64` and `/proc/sys/fs/binfmt_misc` both came up empty) — the shell tried to interpret the foreign-arch ELF's raw bytes as a script, producing a `Syntax error`. `pkg`'s own warning named the fix: `--fallback-to-source` (ships the entry as plain JS instead of failing when bytecode generation isn't possible for the target arch) — added unconditionally to `buildOne()`'s pkg invocation (harmless no-op for same-arch builds, where bytecode generation just succeeds normally). diff --git a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md index e54acc4..a1831cb 100644 --- a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to the Linux standalone `pptxdiff-linux`/ `pptxdiff-linux-arm64` builds are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version tracked is the `pptxdiff` app version bundled into the binary (see the -root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) +root [`CHANGELOG.md`](https://github.com/sugatoray/pptxdiff/blob/master/CHANGELOG.md) for the app's own history) since the binaries have no independent feature set of their own. ## [Unreleased] diff --git a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md index 9433780..31c0d45 100644 --- a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to the macOS standalone `pptxdiff-mac`/ `pptxdiff-mac-arm64` builds are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version tracked is the `pptxdiff` app version bundled into the binary (see the -root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) +root [`CHANGELOG.md`](https://github.com/sugatoray/pptxdiff/blob/master/CHANGELOG.md) for the app's own history) since the binaries have no independent feature set of their own. ## [Unreleased] diff --git a/src/packages/binaries/pptxdiff-win/CHANGELOG.md b/src/packages/binaries/pptxdiff-win/CHANGELOG.md index 41c7275..eabe629 100644 --- a/src/packages/binaries/pptxdiff-win/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-win/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to the Windows standalone `pptxdiff-win.exe`/ `pptxdiff-win-arm64.exe` builds are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version tracked is the `pptxdiff` app version bundled into the binary (see the -root [`CHANGELOG.md`](../../../../CHANGELOG.md) for the app's own history) +root [`CHANGELOG.md`](https://github.com/sugatoray/pptxdiff/blob/master/CHANGELOG.md) for the app's own history) since the binaries have no independent feature set of their own. ## [Unreleased] diff --git a/src/pptxdiff/docs-site/CHANGELOG.md b/src/pptxdiff/docs-site/CHANGELOG.md index d41b709..594f39b 100644 --- a/src/pptxdiff/docs-site/CHANGELOG.md +++ b/src/pptxdiff/docs-site/CHANGELOG.md @@ -35,6 +35,22 @@ No changes yet. ### Changed - `index.md`'s Install tabs gained a "Homebrew" option; `getting-started.md` gained "Option D — Homebrew" (intro sentence reworded from "three equivalent ways" to "several," so it won't need editing again next time); `architecture.md`'s packaging-layers diagram now shows five surfaces instead of four; `limitations.md` gained two new rows (tap doesn't exist yet; not verified against real `brew` locally, with the real root cause explained). +## 2026-08-05 — Content updates + new changelog subpage for standalone native binaries + +### Added + +- `getting-started.md`: new "Option E — standalone binary (no Node.js at all)" section (rebased after master's Homebrew work claimed "Option D" first); `cli.md` gained a one-paragraph cross-link to it. +- `limitations.md`: new row for the standalone binaries' own limitations (unsigned/ad-hoc-signed, not on GitHub Releases yet). +- New changelog subpage `changelogs/pptxdiff-binaries.md`, added to nav — structurally different from every other subpage since it transcludes THREE per-OS `CHANGELOG.md` files (no single package-level one exists for this package) under `## Windows` / `## macOS` / `## Linux` subheadings. +- New `documentation-coverage.md` registry entries: `native-binaries` (feature, `partial`) and `native-binaries-limitations` (limitation, `complete`). + +### Fixed + +- The three per-OS `CHANGELOG.md` files' relative link back to the root `CHANGELOG.md` broke once transcluded into the new changelog subpage (`pymdownx.snippets` doesn't rewrite relative links for their new location) — caught by `mkdocs build --strict`, fixed by swapping to an absolute GitHub URL at the source. + +### Changed + +- `index.md`'s "No install required" card now mentions the binaries option. See `docs/.scrolls/DOCS.md` §15 for the full reasoning. diff --git a/src/pptxdiff/docs-site/docs/changelog.md b/src/pptxdiff/docs-site/docs/changelog.md index 8d08224..12bdaa3 100644 --- a/src/pptxdiff/docs-site/docs/changelog.md +++ b/src/pptxdiff/docs-site/docs/changelog.md @@ -11,6 +11,7 @@ title: Changelog - [Changelog: pptxdiff](changelogs/pptxdiff.md) - [Changelog: @pptxdiff/cli](changelogs/pptxdiff-cli.md) - [Changelog: @pptxdiff/server](changelogs/pptxdiff-server.md) +- [Changelog: @pptxdiff/binaries](changelogs/pptxdiff-binaries.md) ## VS Code Extension(s) diff --git a/src/pptxdiff/docs-site/docs/changelogs/pptxdiff-binaries.md b/src/pptxdiff/docs-site/docs/changelogs/pptxdiff-binaries.md new file mode 100644 index 0000000..55a1695 --- /dev/null +++ b/src/pptxdiff/docs-site/docs/changelogs/pptxdiff-binaries.md @@ -0,0 +1,25 @@ +--- +title: "Changelog: @pptxdiff/binaries" +--- + +# Changelog: @pptxdiff/binaries + +Unlike this site's other package changelogs, `@pptxdiff/binaries` has no single package-level `CHANGELOG.md` — each OS's standalone binary has its own, since they're independent downloadable artifacts. + +## Windows + +Source: [`src/packages/binaries/pptxdiff-win/CHANGELOG.md`](https://github.com/sugatoray/pptxdiff/blob/master/src/packages/binaries/pptxdiff-win/CHANGELOG.md) + +--8<-- "src/packages/binaries/pptxdiff-win/CHANGELOG.md" + +## macOS + +Source: [`src/packages/binaries/pptxdiff-mac/CHANGELOG.md`](https://github.com/sugatoray/pptxdiff/blob/master/src/packages/binaries/pptxdiff-mac/CHANGELOG.md) + +--8<-- "src/packages/binaries/pptxdiff-mac/CHANGELOG.md" + +## Linux + +Source: [`src/packages/binaries/pptxdiff-linux/CHANGELOG.md`](https://github.com/sugatoray/pptxdiff/blob/master/src/packages/binaries/pptxdiff-linux/CHANGELOG.md) + +--8<-- "src/packages/binaries/pptxdiff-linux/CHANGELOG.md" diff --git a/src/pptxdiff/docs-site/docs/cli.md b/src/pptxdiff/docs-site/docs/cli.md index 30424e1..6e355d6 100644 --- a/src/pptxdiff/docs-site/docs/cli.md +++ b/src/pptxdiff/docs-site/docs/cli.md @@ -24,6 +24,8 @@ npm install -g pptxdiff && pptxdiff # global install There are no command-line flags. One optional environment variable is supported — see [Lite mode](#lite-mode-cdn-sourcing) below. +**No Node.js at all?** A standalone native executable per OS+chip runs this exact, unmodified `bin/cli.js` with the Node runtime embedded inside it — see [Getting Started → Option E](getting-started.md#option-e-standalone-binary-no-nodejs-at-all). + ## What it does `bin/cli.js` is a stdlib-only Node script (`node:http` / `node:fs` / `node:child_process`, no npm dependencies): diff --git a/src/pptxdiff/docs-site/docs/documentation-coverage.md b/src/pptxdiff/docs-site/docs/documentation-coverage.md index c516eae..884c3d9 100644 --- a/src/pptxdiff/docs-site/docs/documentation-coverage.md +++ b/src/pptxdiff/docs-site/docs/documentation-coverage.md @@ -2,7 +2,7 @@ title: Documentation Coverage render_macros: true coverage_summary: - generated_at: '2026-08-05T08:54:58Z' + generated_at: '2026-08-05T06:08:51Z' totals: overall: items: 40 @@ -123,6 +123,13 @@ coverage_summary: locations: - page: features/exports.md anchor: live-push + - id: native-binaries + kind: feature + title: Standalone native binaries (@pptxdiff/binaries, x64+arm64 for win/mac/linux) + quality: partial + locations: + - page: getting-started.md + anchor: option-d-standalone-binary-no-nodejs-at-all - id: npm-cli-packaging kind: feature title: npm CLI (bin/cli.js) @@ -293,6 +300,14 @@ coverage_summary: locations: - page: limitations.md anchor: null + - id: native-binaries-limitations + kind: limitation + title: 'Standalone binaries: unsigned/ad-hoc-signed, macOS not cross-compiled, + not on GitHub Releases' + quality: complete + locations: + - page: limitations.md + anchor: null - id: offline-capability-limitations kind: limitation title: Offline-capability limitations (pdfjs-dist unvendored, latin-only font diff --git a/src/pptxdiff/docs-site/docs/getting-started.md b/src/pptxdiff/docs-site/docs/getting-started.md index b98c346..62ad9fe 100644 --- a/src/pptxdiff/docs-site/docs/getting-started.md +++ b/src/pptxdiff/docs-site/docs/getting-started.md @@ -11,6 +11,9 @@ doc_coverage: - id: homebrew-formula quality: partial anchor: option-d-homebrew + - id: native-binaries + quality: partial + anchor: option-e-standalone-binary-no-nodejs-at-all --- # Getting started @@ -53,6 +56,33 @@ pptxdiff Same behavior as Option B once installed — the formula packages the identical published npm tarball, just via `brew` instead of `npm`. Not yet in a real `brew tap` (so no plain `brew install pptxdiff` yet — the `--formula ` form above works today without one); see the [Homebrew formula](homebrew.md) page for full details and status. +## Option E — standalone binary (no Node.js at all) + +```bash +./pptxdiff-linux # or pptxdiff-mac / pptxdiff-mac-arm64 / pptxdiff-win.exe, etc. +``` + +A native, standalone executable per OS+chip — download one file from a +[GitHub Actions build](https://github.com/sugatoray/pptxdiff/actions/workflows/binaries.yml) +and run it directly. No Node.js install of any kind, not even the `npx`/`npm` +step Options A/B still need. Six targets exist (x64 and arm64 for each of +Windows, macOS, and Linux) — see the [`@pptxdiff/binaries` package +README](https://github.com/sugatoray/pptxdiff/blob/master/src/packages/binaries/README.md) +for exactly which file to pick and how to build them yourself. + +This is a genuine single file (the Node runtime and the app's static assets +are both embedded inside it via [`@yao-pkg/pkg`](https://github.com/yao-pkg/pkg)) +running the exact same, unmodified `bin/cli.js` Options A/B run — same local +server, same loopback binding, same [security properties](cli.md#security-note). + +!!! warning "Not code-signed" + There's no code-signing certificate for this project yet, so Windows + SmartScreen and macOS Gatekeeper will both warn on a freshly-downloaded + copy — see each OS folder's own README for how to proceed anyway + (`pptxdiff-{win,mac,linux}/README.md` in the repo). Not attached to a + tagged GitHub Release yet either; download from the Actions run's + workflow artifacts for now. + ## What happens on first load The app ships a built-in **sample deck** — `sample-pptx.js` generates a Before/After pair on load — so you can try every feature immediately, with nothing to upload. Drop in your own `.pptx` pair whenever you're ready, or click **Reset to sample** to go back to the demo data. @@ -61,7 +91,7 @@ The app ships a built-in **sample deck** — `sample-pptx.js` generates a Before ## Requirements -- **Node.js ≥ 18** if you're using the `npx`/`npm` install paths (see `engines` in `package.json`). +- **Node.js ≥ 18** if you're using the `npx`/`npm` install paths (see `engines` in `package.json`). Not needed at all for Option E (standalone binary) — the Node runtime is embedded inside it. - **No internet connection required, for any install option.** React, ReactDOM, Babel-standalone, `@aiden0z/pptx-renderer`, JSZip, and fonts are all vendored locally under `src/pptxdiff/vendor/` and loaded from disk, not from a CDN. The app works fully offline/air-gapped by default — an opt-in `PPTXDIFF_LITE_MODE` switches back to CDN sourcing if you ever want that; see the [CLI reference](cli.md#lite-mode-cdn-sourcing). - **Your `.pptx` files never leave your machine.** Parsing, rendering, and diffing all happen client-side, in your browser's memory. diff --git a/src/pptxdiff/docs-site/docs/index.md b/src/pptxdiff/docs-site/docs/index.md index 5e28f04..75e8176 100644 --- a/src/pptxdiff/docs-site/docs/index.md +++ b/src/pptxdiff/docs-site/docs/index.md @@ -45,7 +45,7 @@ It's a **single self-contained HTML file**. No server, no build step, no cloud u --- - Open `index.html` directly, or run it with `npx pptxdiff`. + Open `index.html` directly, run it with `npx pptxdiff`, or download a standalone native binary — no Node.js needed for that last one. [:octicons-arrow-right-24: Getting started](getting-started.md) diff --git a/src/pptxdiff/docs-site/docs/limitations.md b/src/pptxdiff/docs-site/docs/limitations.md index 4edf43a..6ee88f1 100644 --- a/src/pptxdiff/docs-site/docs/limitations.md +++ b/src/pptxdiff/docs-site/docs/limitations.md @@ -28,6 +28,8 @@ doc_coverage: quality: complete - id: homebrew-formula-limitations quality: complete + - id: native-binaries-limitations + quality: complete --- # Known limitations @@ -48,7 +50,8 @@ These are documented, accepted trade-offs — not bugs waiting to be fixed. Each | **Batch filename-similarity pairing uses an untuned threshold** | The bigram Dice-coefficient cutoff (0.3) for filename-based pairing hasn't been validated against real-world messy filename patterns, and isn't user-adjustable. See [Batch mode](features/batch-mode.md). | | **On-disk sample fixtures don't cover everything the in-browser sample does** | `docs/assets/sample_*.pptx` (generated by `pptxgenjs`) don't exercise SmartArt/diagrams, slide transitions, embedded fonts, or real video/audio media — `pptxgenjs` has no API surface for any of these. The in-browser "Reset to sample" deck still covers all four, for its own narrower purpose (feeding this app's own parser/renderer, never downloaded as a real file). | | **English-only UI, no full accessibility audit** | `aria-label`s exist on the highest-value icon-only controls and toasts are screen-reader-friendly, but there's no localization and no full keyboard-navigation/focus-ring audit. | -| **The npm CLI opens a browser tab, not a native app window** | No dock/taskbar icon, no standalone app process — `bin/cli.js` is a static file server, not an Electron/Tauri wrapper. See [CLI reference](cli.md). | +| **The npm CLI (and the standalone binaries) open a browser tab, not a native app window** | No dock/taskbar icon, no standalone app process — `bin/cli.js` is a static file server, not an Electron/Tauri wrapper, and the standalone binaries run that exact same server. See [CLI reference](cli.md). | +| **Standalone binaries are unsigned/ad-hoc-signed, and not yet on GitHub Releases** | No code-signing certificate exists for this project — Windows SmartScreen and macOS Gatekeeper both warn on a freshly-downloaded copy. Only available as CI workflow artifacts today, not attached to a tagged release. macOS specifically can't be cross-compiled from Linux/Windows for this reason (an unsigned binary may not even launch on Apple Silicon) — it builds on a real `macos-latest` runner instead. See [Getting Started → Option E](getting-started.md#option-e-standalone-binary-no-nodejs-at-all). | | **Offline vendoring has two known gaps** | `pdfjs-dist` (an optional peer dependency of the rendering library, used only for embedded-PDF-object rendering) isn't vendored — this was never functional even before the app went offline-capable. The vendored Spectral font ships only its latin subset (matching the English-only UI); non-Latin text falls back to the browser's default font. See [Architecture](architecture.md#runtime-dependencies-vendored-locally). | | **`PPTXDIFF_LITE_MODE` is all 5 dependencies at once** | No way to mix vendored and CDN sourcing per-dependency (e.g. vendored React with CDN `pptx-renderer`) without hand-editing the source. See [CLI reference](cli.md#lite-mode-cdn-sourcing). | | **The headless CLI/API aren't published, and `batch`/`report`/`merge` aren't built yet** | `pptxdiff-cli`/`@pptxdiff/server` are source-only (`file:` dependencies on their monorepo siblings) — install from a checkout, not `npm install`. `diff`/`checksum`/git integration (`textconv`/`difftool`/`install-git-integration`) ship; `batch`/`report`/`merge` and server authentication for a non-loopback bind are designed but not built. Every invocation currently pays a real headless-browser boot cost (no native, browser-free engine yet). See [Headless CLI & Web API](headless-cli-api.md). | diff --git a/src/pptxdiff/docs-site/mkdocs.yml b/src/pptxdiff/docs-site/mkdocs.yml index 2a2219a..cdcbd09 100644 --- a/src/pptxdiff/docs-site/mkdocs.yml +++ b/src/pptxdiff/docs-site/mkdocs.yml @@ -124,6 +124,7 @@ nav: - "Changelog: pptxdiff": changelogs/pptxdiff.md - "Changelog: @pptxdiff/cli": changelogs/pptxdiff-cli.md - "Changelog: @pptxdiff/server": changelogs/pptxdiff-server.md + - "Changelog: @pptxdiff/binaries": changelogs/pptxdiff-binaries.md - VS Code Extension(s): - "Changelog: pptxdiff-vscode": changelogs/pptxdiff-vscode.md - Homebrew Formula(s): diff --git a/src/pptxdiff/docs-site/scripts/coverage_registry.yml b/src/pptxdiff/docs-site/scripts/coverage_registry.yml index 79ace1a..945f027 100644 --- a/src/pptxdiff/docs-site/scripts/coverage_registry.yml +++ b/src/pptxdiff/docs-site/scripts/coverage_registry.yml @@ -84,6 +84,9 @@ features: - id: homebrew-formula title: "Homebrew formula for pptxdiff" source: SPEC.md §32-§33 + - id: native-binaries + title: "Standalone native binaries (@pptxdiff/binaries, x64+arm64 for win/mac/linux)" + source: SPEC.md §36 limitations: - id: rendering-limitations @@ -128,3 +131,6 @@ limitations: - id: homebrew-formula-limitations title: "Homebrew formula: not tapped anywhere yet, not verified against real brew locally" source: GAP_ANALYSIS.md "Packaging" + - id: native-binaries-limitations + title: "Standalone binaries: unsigned/ad-hoc-signed, macOS not cross-compiled, not on GitHub Releases" + source: GAP_ANALYSIS.md "Standalone native binaries" From 50fe34544c577e340f3dbc1315f73d2e55204469 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 03:16:43 +0000 Subject: [PATCH 7/7] Fix stray SPEC.md section references after rebasing onto master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebasing the binaries branch onto master (which landed Homebrew and Chocolatey packaging work in the interim) required renumbering the binaries SPEC.md section from the collided §32 to §36, since master's Homebrew formula legitimately owns §32. A few self-references to the binaries feature's own section number, written before the rebase, still said §32 in HANDOFF.md, PLAN.md, and the per-OS CHANGELOG.md files - fixed to §36 (verified every remaining §32 reference in the repo is the real, correct Homebrew one). Also regenerates documentation-coverage.md via sync_doc_coverage.py --write, since the getting-started.md "Option D" naming collision (both this branch and master's Homebrew work independently claimed it) was resolved by renaming this branch's option to "Option E". Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BxwMTp6RQJ6j6K5K8Jjdpm --- docs/.scrolls/HANDOFF.md | 10 +++++----- docs/.scrolls/PLAN.md | 6 +++--- .../binaries/pptxdiff-linux/CHANGELOG.md | 2 +- .../binaries/pptxdiff-mac/CHANGELOG.md | 2 +- .../binaries/pptxdiff-win/CHANGELOG.md | 2 +- .../docs-site/docs/documentation-coverage.md | 18 +++++++++--------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/.scrolls/HANDOFF.md b/docs/.scrolls/HANDOFF.md index 50f4bb6..e053013 100644 --- a/docs/.scrolls/HANDOFF.md +++ b/docs/.scrolls/HANDOFF.md @@ -19,7 +19,7 @@ - **Confirmed the fix actually works, not just that the error message went away**: rebuilt both targets via the real production `buildOne()` path (real asset config, not a bare entry-only smoke test), confirmed via `file` genuine `ELF 64-bit LSB executable, ARM aarch64` and `PE32+ executable ... Aarch64` outputs, and — since this sandbox can't execute either binary (no arm64 emulation) — cross-checked binary SIZE against the known-good x64 builds (72-75MB either way) as evidence real assets are actually embedded, not a truncated/failed artifact. - **`TARGET_MAP` gained `linux-arm64`/`win-arm64` entries**, each sharing its OS's existing `outDirKey` (same mechanism the mac-arm64 addition established) — `.github/workflows/binaries.yml`'s `build-linux-win` job now builds and uploads all four Windows/Linux target/arch combinations. `test_build_e2e.mjs`'s host-detection generalized from "only darwin checks arch" to checking `os.arch()` uniformly across all three platforms, so a future arm64 Linux/Windows CI runner would also genuinely test its native target. - **Genuine RED→GREEN demonstrated on the new `--fallback-to-source` guard**: temporarily removed the flag, confirmed the dedicated regression test caught it (28/29 — this is exactly the kind of regression that would silently break every arm64 target while every x64 test kept passing, hence a dedicated static check rather than trusting "the tests still pass"), restored it, confirmed 29/29. `test_build_config.mjs` now 29 assertions (was 23). -- Scrolls updated to match: SPEC.md §32, PLAN.md (new "Done this session" block, prior "not attempted" ticket closed), GAP_ANALYSIS.md (arm64 gap for win/linux closed, the "unverified end-to-end" gap widened to name all five non-run targets honestly), GAP_CONTEXT.md (prior entry marked superseded, new entry with the full exec-format-failure reproduction), per-OS win/linux `README.md`/`CHANGELOG.md`, top-level `src/packages/binaries/README.md`, root `CHANGELOG.md`. +- Scrolls updated to match: SPEC.md §36, PLAN.md (new "Done this session" block, prior "not attempted" ticket closed), GAP_ANALYSIS.md (arm64 gap for win/linux closed, the "unverified end-to-end" gap widened to name all five non-run targets honestly), GAP_CONTEXT.md (prior entry marked superseded, new entry with the full exec-format-failure reproduction), per-OS win/linux `README.md`/`CHANGELOG.md`, top-level `src/packages/binaries/README.md`, root `CHANGELOG.md`. ## Update (2026-08-05 — binaries: native Apple Silicon build, `pptxdiff-mac-arm64`) - Direct follow-up question on the same-day `@yao-pkg/pkg` switch below: "Does the mac binary work for Apple Silicon MacBooks?" Honest answer given first: yes, but only via Rosetta 2 translation — only the `node22-macos-x64` target existed. Asked whether to add a native arm64 build (real effort: another CI target, another README/CHANGELOG update) via `AskUserQuestion` before doing the work; user said yes. @@ -29,7 +29,7 @@ - **Genuine RED→GREEN demonstrated on the new `outDirKey` guard**: temporarily reverted `buildOne()`'s `outDir` computation back to using the `osKey` argument, confirmed the dedicated regression test caught it (22/23), restored it, confirmed 23/23 — `test_build_config.mjs` now 23 assertions total (was 18). - **Windows/Linux deliberately stay x64-only** — not asked about, and arm64 desktop usage is a much smaller fraction of this app's likely audience for those two OSes than Apple Silicon is of the Mac audience. `pkg` supports `node22-win-arm64`/`node22-linux-arm64` equally well if this is ever revisited. - **`ldid`-based Linux-side signing considered, not pursued**: `pkg`'s own error output suggests it as an alternative to needing a real Mac at all (would let `build-mac` fold into the cross-compiled `build-linux-win` job) — flagged as a real, deliberate non-choice in GAP_ANALYSIS.md rather than silently never considered; a real `macos-latest` runner using Apple's own `codesign` was judged more trustworthy for a first pass. -- Scrolls updated to match: SPEC.md §32, PLAN.md (new "Done this session" block + 2 new tickets), GAP_ANALYSIS.md (arm64 gap closed for mac, new ldid/win-linux-arm64 tickets), GAP_CONTEXT.md (two new entries — why mac got native arm64 but win/linux didn't, why `ldid` wasn't pursued), per-OS mac `README.md`/`CHANGELOG.md`, top-level `src/packages/binaries/README.md`, root `CHANGELOG.md`. +- Scrolls updated to match: SPEC.md §36, PLAN.md (new "Done this session" block + 2 new tickets), GAP_ANALYSIS.md (arm64 gap closed for mac, new ldid/win-linux-arm64 tickets), GAP_CONTEXT.md (two new entries — why mac got native arm64 but win/linux didn't, why `ldid` wasn't pursued), per-OS mac `README.md`/`CHANGELOG.md`, top-level `src/packages/binaries/README.md`, root `CHANGELOG.md`. ## Update (2026-08-05 — binaries: switched from Node SEA to `@yao-pkg/pkg`) - Direct follow-up question on the same-day binaries work below: "Why aren't you using the npm library yao-pkg/pkg?" Honest answer given first: SEA was reached for by default (Node core feature, no added third-party build-tool dependency) without actually evaluating `pkg` first — then investigated hands-on rather than defending the choice abstractly. @@ -40,7 +40,7 @@ - **macOS deliberately NOT cross-compiled**, even though `pkg` technically can produce a mac binary from Linux: `codesign` only exists on macOS, and on Apple Silicon a completely unsigned binary may not even launch (AMFI requires at least an ad-hoc signature — not just a Gatekeeper warning the way Intel Macs work). `buildOne()` only codesigns when `process.platform === "darwin"`, warns loudly otherwise. `.github/workflows/binaries.yml` restructured to 2 jobs: `build-linux-win` on `ubuntu-latest` (genuine cross-compile, both targets in one job), `build-mac` kept on its own `macos-latest` runner. - **Both test files rewritten and re-verified with genuine RED→GREEN**: `test_build_config.mjs` (18 assertions, fast/pure) — demonstrated RED on the sharpest new guard by moving the temp-config write location from `REPO_ROOT` to `__dirname` and confirming the test caught it (17/18), restored, confirmed 18/18. `test_build_e2e.mjs` (10 assertions, slow/real) re-run against the new mechanism: built and ran an actual `pkg`-produced binary, confirmed real HTTP responses, added an explicit assertion that no separate `assets/` folder exists. - Per-OS `README.md`/`CHANGELOG.md` (still unreleased, so amended in place) and root `CHANGELOG.md` updated to match — no remaining mention of Node SEA or the `root` parameter as the current mechanism (both fully retired), though GAP_CONTEXT.md keeps the old reasoning entries with explicit "SUPERSEDED, see below" markers rather than deleting them, per this project's own convention. -- Scrolls updated to match: SPEC.md §32 (rewritten for `pkg`), PLAN.md (new "switched to @yao-pkg/pkg" Done-this-session block, ticket 3 marked done-by-supersession), GAP_ANALYSIS.md (macOS-not-cross-compiled reframed as a real load-bearing constraint, not leftover SEA caution), GAP_CONTEXT.md (two entries marked superseded, two new entries: the switch reasoning and why mac stays on its own runner), WISDOM.md (new trap entry with the full A/B reproduction). +- Scrolls updated to match: SPEC.md §36 (rewritten for `pkg`), PLAN.md (new "switched to @yao-pkg/pkg" Done-this-session block, ticket 3 marked done-by-supersession), GAP_ANALYSIS.md (macOS-not-cross-compiled reframed as a real load-bearing constraint, not leftover SEA caution), GAP_CONTEXT.md (two entries marked superseded, two new entries: the switch reasoning and why mac stays on its own runner), WISDOM.md (new trap entry with the full A/B reproduction). ## Update (2026-08-05 — binaries follow-up: Red/Green TDD + per-OS CHANGELOG.md) - Direct follow-up ask on the same-day binaries work below: "use Red/Green TDD and update documentation and add a CHANGELOG.md for each os specific folder." @@ -50,7 +50,7 @@ - **Real bug found and fixed while writing the e2e test, before it ever touched committed files**: both `build.mjs`'s "clean the output directory before building" step AND the e2e test's own post-run cleanup were a blind `rm -rf ` — fine when that directory held only generated output, but it's the SAME directory as each OS's tracked `README.md` (and now `CHANGELOG.md`). Fixed both with a `cleanGeneratedOutDir()`/equivalent that removes only the specific known-generated entries (binary by exact name, `assets/` folder, `*.zip` files) rather than the whole directory. Verified the fix for real: ran the actual build twice in a row and confirmed `README.md`/`CHANGELOG.md` survived both times. New WISDOM.md trap entry recorded so a future generator touching a mixed generated/tracked-content directory doesn't repeat this. - **`src/packages/binaries/pptxdiff-{win,mac,linux}/CHANGELOG.md`** added (Keep a Changelog format, un-ignored in `.gitignores/user.gitignore` + regenerated `.gitignore` the same way `README.md` already was) — tracks the bundled `pptxdiff` app version per OS folder, since the binary itself has no independent feature set to version separately. - **Root `CHANGELOG.md`**: filled in the previously-empty `[Unreleased]` placeholder with this whole binaries feature (both this update and the earlier same-day one). -- Scrolls updated to match: SPEC.md §32 (testing subsection added), PLAN.md (new "Done this session" bullet + a 4th ticket), WISDOM.md (new "clean the output dir" trap). +- Scrolls updated to match: SPEC.md §36 (testing subsection added), PLAN.md (new "Done this session" bullet + a 4th ticket), WISDOM.md (new "clean the output dir" trap). ## Update (2026-08-05 — standalone native binaries for Windows/macOS/Linux, `src/packages/binaries/`) - Task asked for a "mechanism to create downloadable installers" under `src/packages/binaries/pptxdiff-{win,mac,linux}`, and explicitly asked whether the folder should be named `binaries` or `installers` — flagged this as a real fork (not a naming bikeshed) since it changes scope by an order of magnitude, and asked the user directly via `AskUserQuestion` before building anything: standalone binaries (Node SEA, no install wizard, no signing) vs. true OS installers (`.msi`/`.pkg`/`.deb` with code signing). User picked **standalone binaries** — consistent with a prior session's explicit Electron/Tauri-vs-CLI+browser decision already on record in GAP_CONTEXT.md (picked CLI+browser specifically to avoid installer/signing overhead). @@ -63,7 +63,7 @@ - Output folders `src/packages/binaries/pptxdiff-{win,mac,linux}/` are gitignored (build artifacts, `.gitignores/user.gitignore` + regenerated `.gitignore`, same treatment as `dist/`) except one tracked `README.md` per OS folder describing what a build produces there. - **Verified for real on Linux (this sandbox)**: ran `npm install && node build.mjs` in `src/packages/binaries/`, got a real 120MB ELF executable + a 42MB zip. Then actually RAN the packaged binary directly (not `bin/cli.js`) — it printed `pptxdiff running at http://localhost:`, and real `curl` requests confirmed `index.html`/`support.js`/`vendor/react.production.min.js` all served correctly (200, correct content-type) from its own `assets/` folder resolved via `process.execPath`. macOS/Windows code paths (codesign steps, `.exe` naming) are structurally parallel but genuinely unverified until CI runs them — no non-Linux host available in this sandbox. - **Known, documented gaps** (see GAP_ANALYSIS.md/PLAN.md): unsigned Windows `.exe` / ad-hoc-signed-only macOS binary (no code-signing cert — real ongoing cost, not a code fix); not yet attached to GitHub Releases (workflow artifacts only); ~120MB per binary (SEA embeds the whole Node runtime, inherent to the approach); true single-file binaries via SEA's embedded-asset store (`sea.getAsset()`) not attempted — shipped as binary+`assets/`-folder zipped together instead, to avoid changing `bin/cli.js`'s already-hardened file-serving logic for this first pass. -- Scrolls updated to match: SPEC.md §32 (new), PLAN.md (new "Done this session" entry + 3 new tickets), GAP_ANALYSIS.md (new "Standalone native binaries" section), GAP_CONTEXT.md (three new entries: why standalone-not-installers, why build output stays out of git except a README, why `startServer()` was reused instead of a third server copy, why assets aren't SEA-embedded). +- Scrolls updated to match: SPEC.md §36 (new), PLAN.md (new "Done this session" entry + 3 new tickets), GAP_ANALYSIS.md (new "Standalone native binaries" section), GAP_CONTEXT.md (three new entries: why standalone-not-installers, why build output stays out of git except a README, why `startServer()` was reused instead of a third server copy, why assets aren't SEA-embedded). ## Update (2026-08-09 — fixed the sync-homebrew-tap.yml `brew-audit` job) - Direct ask: "the brew github actions pipeline ... did not succeed. Fix it." The workflow's first real `workflow_dispatch` run (2026-08-08) failed at the `brew-audit` (macOS) job's `brew audit diff --git a/docs/.scrolls/PLAN.md b/docs/.scrolls/PLAN.md index ebcc19f..51bb679 100644 --- a/docs/.scrolls/PLAN.md +++ b/docs/.scrolls/PLAN.md @@ -424,7 +424,7 @@ shim, not sequentially — that plan is what shipped below. confirming both docs files survive. `src/packages/binaries/pptxdiff-{win,mac,linux}/CHANGELOG.md` added (Keep a Changelog format, tracks the bundled `pptxdiff` app version). Root `CHANGELOG.md` `[Unreleased]` section filled in for this whole feature (previously an empty placeholder). See - SPEC.md §32, WISDOM.md's new "clean the output dir" trap entry. + SPEC.md §36, WISDOM.md's new "clean the output dir" trap entry. ## New tickets opened this session 1. **P2 — Attach the built binaries to GitHub Releases**, not just CI workflow artifacts. Needs a @@ -500,7 +500,7 @@ shim, not sequentially — that plan is what shipped below. `buildOne()`'s `outDir` computation to use the `osKey` argument instead of `target.outDirKey`, confirmed the dedicated regression test caught it (22/23), restored it, confirmed 23/23. - [x] Per-OS mac `README.md`/`CHANGELOG.md`, the top-level `src/packages/binaries/README.md`, root - `CHANGELOG.md`, `SPEC.md` §32, `GAP_ANALYSIS.md`, and `GAP_CONTEXT.md` all updated to describe both + `CHANGELOG.md`, `SPEC.md` §36, `GAP_ANALYSIS.md`, and `GAP_CONTEXT.md` all updated to describe both mac targets. ## New tickets opened this session @@ -540,5 +540,5 @@ shim, not sequentially — that plan is what shipped below. checking `os.arch()` for every platform, so an arm64 Linux/Windows CI runner would also genuinely exercise its native target rather than always falling back to x64. - [x] Per-OS win/linux `README.md`/`CHANGELOG.md`, the top-level `src/packages/binaries/README.md`, - root `CHANGELOG.md`, `SPEC.md` §32, `GAP_ANALYSIS.md`, and `GAP_CONTEXT.md` all updated — all six + root `CHANGELOG.md`, `SPEC.md` §36, `GAP_ANALYSIS.md`, and `GAP_CONTEXT.md` all updated — all six targets now documented consistently. diff --git a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md index a1831cb..949d685 100644 --- a/src/packages/binaries/pptxdiff-linux/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-linux/CHANGELOG.md @@ -16,7 +16,7 @@ since the binaries have no independent feature set of their own. ### Added - First standalone Linux executable, built via `@yao-pkg/pkg` (see - `../README.md` and `docs/.scrolls/SPEC.md` §32) — download + `../README.md` and `docs/.scrolls/SPEC.md` §36) — download `pptxdiff-linux`, `chmod +x pptxdiff-linux && ./pptxdiff-linux`. A true single file (Node runtime and the static app files it serves are both embedded inside it) — no separate Node.js install, no companion folder diff --git a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md index 31c0d45..0eb4c05 100644 --- a/src/packages/binaries/pptxdiff-mac/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-mac/CHANGELOG.md @@ -16,7 +16,7 @@ since the binaries have no independent feature set of their own. ### Added - First standalone macOS executables, built via `@yao-pkg/pkg` (see - `../README.md` and `docs/.scrolls/SPEC.md` §32) — download + `../README.md` and `docs/.scrolls/SPEC.md` §36) — download `pptxdiff-mac` (Intel) or `pptxdiff-mac-arm64` (Apple Silicon, native), run it. Each is a true single file (Node runtime and the static app files it serves are both embedded inside it) — no separate Node.js diff --git a/src/packages/binaries/pptxdiff-win/CHANGELOG.md b/src/packages/binaries/pptxdiff-win/CHANGELOG.md index eabe629..5cc8713 100644 --- a/src/packages/binaries/pptxdiff-win/CHANGELOG.md +++ b/src/packages/binaries/pptxdiff-win/CHANGELOG.md @@ -16,7 +16,7 @@ since the binaries have no independent feature set of their own. ### Added - First standalone Windows executable, built via `@yao-pkg/pkg` (see - `../README.md` and `docs/.scrolls/SPEC.md` §32) — download + `../README.md` and `docs/.scrolls/SPEC.md` §36) — download `pptxdiff-win.exe`, run it. A true single file (Node runtime and the static app files it serves are both embedded inside it) — no separate Node.js install, no companion folder needed. diff --git a/src/pptxdiff/docs-site/docs/documentation-coverage.md b/src/pptxdiff/docs-site/docs/documentation-coverage.md index 884c3d9..6e9cea1 100644 --- a/src/pptxdiff/docs-site/docs/documentation-coverage.md +++ b/src/pptxdiff/docs-site/docs/documentation-coverage.md @@ -2,21 +2,21 @@ title: Documentation Coverage render_macros: true coverage_summary: - generated_at: '2026-08-05T06:08:51Z' + generated_at: '2026-08-09T03:15:30Z' totals: overall: - items: 40 - complete: 36 - partial: 4 + items: 42 + complete: 37 + partial: 5 missing: 0 feature: - items: 26 + items: 27 complete: 22 - partial: 4 + partial: 5 missing: 0 limitation: - items: 14 - complete: 14 + items: 15 + complete: 15 partial: 0 missing: 0 items: @@ -129,7 +129,7 @@ coverage_summary: quality: partial locations: - page: getting-started.md - anchor: option-d-standalone-binary-no-nodejs-at-all + anchor: option-e-standalone-binary-no-nodejs-at-all - id: npm-cli-packaging kind: feature title: npm CLI (bin/cli.js)