Skip to content

fix: npm install fails on Windows with Node 24+ (better-sqlite3 native build) - #14

Merged
randhirinsta merged 9 commits into
mainfrom
fix/windows-node24-sqlite
Oct 12, 2026
Merged

randhirinsta merged 9 commits into
mainfrom
fix/windows-node24-sqlite

Conversation

@randhirinsta

@randhirinsta randhirinsta commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Problem

npm install -g @instawp/cli fails on Windows for anyone on Node 24 LTS, 25 or 26. The log ends with npm warn cleanup Failed to remove some directories, which is npm rolling back after a native build error.

Root cause

  • better-sqlite3 ^11.10.0 resolves to 11.10.0, which ships prebuilt Windows binaries only for Node ABI 108/115/127/131 (Node 18–23).
  • On Node 24 (ABI 137), 25 (141) and 26 (147) npm falls back to node-gyp, which needs Visual Studio Build Tools. Most users don't have them, so the whole install fails.
  • local.ts imported it statically and index.ts loads local.ts at startup, so a missing module also broke every command, login included.
  • CI missed it because the Windows smoke test ran only on Node 20, which has a prebuild.

Change

  • better-sqlite3 ^11.10.0 → ^12.11.1, the newest 12.x on npm (12.12.0 exists only as a GitHub tag). It has prebuilds for Node 22/24/25/26 on win32/darwin/linux, x64 and arm64. It is now an optionalDependency, so a failed native build no longer fails the install.
  • New src/lib/sqlite.ts → loadSqlite() loads it on first use (createRequire, so callers stay synchronous). The static imports in local.ts and sqlite-to-mysql.ts are now import type. If the module is missing, only local clone and local push --with-db fail, with a message saying how to fix it. push --with-db checks this before syncing any files.
  • engines.node >=18 → >=22 (12.11.1 has no Node 20 prebuild, and Node 18/20 are EOL). README, CLAUDE.md and the publish job are updated to match.
  • publish.yml Windows smoke test:
    • runs on Node [22, 24, 26];
    • asserts the native module installed and loads;
    • fails if build/config.gypi exists, meaning it was compiled from source (the runner has Visual Studio, so a missing prebuild would otherwise pass silently);
    • asserts the CLI starts with the module hidden.

Evidence

  • Local (Linux, Node 22): tsc clean; vitest 254/254. api-exec.test.ts fails to load in the agent sandbox on main too.
  • Falsifier: with node_modules/better-sqlite3 hidden, main's instawp --version crashes with ERR_MODULE_NOT_FOUND, and this branch exits 0.
  • Windows CI (workflow_dispatch, publish job skipped), this branch @ 4139494: Node 22 / 24 / 26 all green. That covers the native module installing from a prebuild, loading, and the CLI starting with it hidden. Run: https://github.com/InstaWP/cli/actions/runs/37922379716
  • Falsifier: same CI, but with the old better-sqlite3 11.10.0. Node 22 is green. Node 24 red: "better-sqlite3 was compiled from source on Node 24 (no prebuilt binary)" (the runner has Visual Studio; users don't). Node 26 red at npm ci (the source build fails). This reproduces the user's bug and proves the new check catches it. Run: https://github.com/InstaWP/cli/actions/runs/37922383215
  • The final head 4239e5b is a type-only rename (BetterSqlite3 → Database); the compiled dist/lib/sqlite.js is byte-identical. CI was re-dispatched on it anyway.
  • Code review (independent agent): WARN, no blockers. Both CI findings (detect a source compile; run on Windows before merge) and the local push --with-db preflight are applied.

Not in this PR

No version bump or release. Cutting 0.0.1-beta.34 is a separate tag push.

ClickUp: https://app.clickup.com/t/14ypaj0hnkn
Agentric session: https://aos.agents.instawp.net/#/sessions/aos-ses_8ef4990541280f46

Next step (human): /code-review ultra <PR#> before merge.

— Engineer (agent-os)

🤖 Generated with Claude Code

Engineer (agent-os) and others added 2 commits October 9, 2026 11:07
…e build)

better-sqlite3 11.10.0 ships win32 prebuilds only for Node ABI 108-131
(Node 18-23). On Node 24 LTS/25/26 npm falls back to node-gyp, which needs
Visual Studio Build Tools, so the global install fails.

- better-sqlite3 ^11.10.0 -> ^12.11.1 (win32/darwin/linux prebuilds for
  Node 22/24/25/26), moved to optionalDependencies.
- Lazy-load it via src/lib/sqlite.ts so a missing module only affects
  local clone/push, not every command (index.ts loads local.ts eagerly).
- engines.node >=18 -> >=22.
- Windows smoke test: Node matrix [22, 24, 26]; assert the native module
  installed and loads; assert the CLI starts without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sh --with-db

- Windows smoke: fail if better-sqlite3 has build/config.gypi. The runner
  has Visual Studio, so a missing prebuild would otherwise compile silently
  and pass while users without build tools fail.
- local push --with-db: check the native module before syncing files, so a
  missing module can't fail after files are pushed and an overwrite prompt.
- Error message names Node 26; rename the install step (no native deps now).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

…codebase

Type-only rename; the compiled dist/lib/sqlite.js is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@randhirinsta
randhirinsta marked this pull request as ready for review October 9, 2026 11:29
@randhirinsta

randhirinsta commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Ready at adf2270. QA PASS at 4239e5b, 40efcfe and adf2270 (clone with/without the SQLite binary, npm 12, Node 24). Windows CI green on adf2270 (Node 22/24/26). Code review PASS on all rounds.
Known, not in scope: clone after a failed/empty cloud DB export still ends green (pre-existing); reinstall hint printed up to 3× when the module is missing (cosmetic).
Next (human): /code-review ultra 14 → merge → push tag v0.0.1-beta.34.

@randhirinsta
randhirinsta marked this pull request as draft October 9, 2026 11:31
… to fix it

npm 12 blocks install scripts by default, so a global install keeps the
better-sqlite3 package but never downloads its binary. require() still
succeeds; the first `new Database()` throws a raw "Could not locate the
bindings file". loadSqlite() now opens an in-memory DB once so that case
gets the friendly message, which names --allow-scripts=better-sqlite3.
`instawp upgrade` passes the flag (npm 10/11 accept it), and the README
install command includes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# the native build fails. That must never happen on a supported Node
# version — load it from the global install and open a DB to prove it.
$cli = Join-Path (npm root -g) "@instawp\cli"
node -e "const D=require(require.resolve('better-sqlite3',{paths:[process.argv[1]]}));const db=new D(':memory:');console.log('better-sqlite3', db.prepare('select sqlite_version() v').get().v);db.close()" $cli

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BUG: Wrong process.argv index — should be process.argv[2], not process.argv[1]

When running node -e "script" $cli, the Node process receives:

  • process.argv[0] = 'node'
  • process.argv[1] = the -e script string
  • process.argv[2] = $cli (the CLI path)

The code uses process.argv[1] which is the script itself, not the argument. This works by accident because require.resolve falls back to default resolution, but it's incorrect.

Suggested change
node -e "const D=require(require.resolve('better-sqlite3',{paths:[process.argv[1]]}));const db=new D(':memory:');console.log('better-sqlite3', db.prepare('select sqlite_version() v').get().v);db.close()" $cli
node -e "const D=require(require.resolve('better-sqlite3',{paths:[process.argv[2]]}));const db=new D(':memory:');console.log('better-sqlite3', db.prepare('select sqlite_version() v').get().v);db.close()" $cli

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@randhirinsta

randhirinsta commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

QA: PASS ✅ · Env: Linux x64 QA host, throwaway sandbox qacli14a (deleted) · InstaWP/cli PR #14 @4239e5b · packed tgz sha256 c6093ac5…, npm i -g into an isolated prefix per Node (NODE_ENV=production)

Node 22.22.0 (ABI 127) Node 24.21.0 (137) Node 26.11.1 (147)
1. Global install ✅ exit 0, bs3 12.11.1 loads, prebuilt (no build/config.gypi) ✅ same ✅ same
2. --version / login --token / whoami / sites list ✅ all exit 0 ✅ ✅
3. local clone → edit post 1 + blogname in SQLite → local push --with-db --force ✅ 13 tables imported; edit visible on cloud (wp post get 1) ✅ ✅
4a. bs3 renamed away: commands in (2) ✅ all exit 0 ✅ ✅
4b. bs3 renamed away: local push --with-db ✅ exit 1, "Local-site features need the SQLite native module… (MODULE_NOT_FOUND)" as the only output; no rsync, cloud unchanged ✅ ✅
Control: published beta.33 --version & whoami crash ERR_MODULE_NOT_FOUND, exit 1, with bs3 hidden bs3 11.10.0 compiled from source (config.gypi) npm i -g fails: node-gyp GetPrototype / This errors

Repro-before-fix: yes. The beta.33 row reproduces the user's bug on Linux, and the PR fixes all of it.
Environment disclosure (pre-existing, not this PR): on every fleet site I probed (OVH 167.114.x, 15.204.x, DO 188.166.x), jailed SSH has no mariadb-dump and no MySQL socket, so wp db export/import fails. Both commands need it, on base as well. For leg 3, on my throwaway site only, I set DB_HOST=127.0.0.1 and symlinked mariadb-*→mysql* in its ~/.local/bin. I also used --no-backup, because the backup path is hard-coded to /home/<user>/…, which doesn't exist in the jail (same line on main). Worth a separate ticket: today local clone/push --with-db cannot work against the fleet at all.
Non-blocking: with bs3 missing, local clone still exports the DB and rsyncs the whole site. It then prints the fix message as "Database import failed" and ends with "Clone complete!", exit 0, leaving an empty DB. The PR body says clone "fails"; a loadSqlite() preflight like push's would make that true.

— QA

@kilo-code-bot

kilo-code-bot Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/sqlite-to-mysql.ts 86 Incorrect type annotation — Database.Database should be InstanceType<Database>
Files Reviewed (10 files)
  • .github/workflows/publish.yml - CI matrix expanded to Node 22/24/26, native module verification added
  • CHANGELOG.md - Release notes for 0.0.1-beta.34
  • CLAUDE.md - Updated Node requirement to 22+, Windows docs updated
  • README.md - Node 22+ requirement, npm 12 note added
  • package.json - Node >=22, better-sqlite3 moved to optionalDependencies
  • src/commands/local.ts - Lazy SQLite loading, pre-checks for clone/push, better error messages
  • src/lib/sqlite-to-mysql.ts - Uses lazy-loaded SQLite module (type annotation issue persists)
  • src/lib/sqlite.ts - New file: Lazy loading wrapper for better-sqlite3
  • src/lib/update-notifier.ts - Added --allow-scripts=better-sqlite3 to upgrade
  • src/commands/upgrade.ts - Updated error message

Fix these issues in Kilo Cloud

Previous Review Summaries (2 snapshots, latest commit 8d995b8)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 8d995b8)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/sqlite-to-mysql.ts 86 Incorrect type annotation — Database.Database should be Database
Files Reviewed (6 files)
  • CHANGELOG.md
  • README.md
  • package-lock.json
  • package.json
  • src/lib/sqlite.ts
  • src/lib/update-notifier.ts

Fix these issues in Kilo Cloud

Previous review (commit 4239e5b)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
.github/workflows/publish.yml 78 Wrong process.argv index — should be process.argv[2], not process.argv[1]
src/lib/sqlite-to-mysql.ts 86 Incorrect type annotation — Database.Database should be Database
Files Reviewed (9 files)
  • .github/workflows/publish.yml - 1 issue
  • CHANGELOG.md
  • CLAUDE.md
  • README.md
  • package-lock.json
  • package.json
  • src/commands/local.ts
  • src/lib/sqlite-to-mysql.ts - 1 issue (outside diff hunk, but caused by import change)
  • src/lib/sqlite.ts

Issue Details

1. .github/workflows/publish.yml:78 — The inline Node script uses process.argv[1] to access the CLI path argument, but process.argv[1] is the -e script string itself. The argument $cli is at process.argv[2]. This works by accident due to require.resolve fallback but is incorrect.

2. src/lib/sqlite-to-mysql.ts:86 — The type annotation Database.Database | null is wrong. The import import type Database from 'better-sqlite3' brings in the class (default export), not a namespace. The variable db holds an instance, so its type should be Database | null. This was directly caused by changing the import from import Database to import type Database in this PR. Note: this line is outside the diff hunk so no inline comment was posted.

Fix these issues in Kilo Cloud


Reviewed by nemotron-3-ultra-550b-a55b:free · Input: 537.6K · Output: 12K · Cached: 829.4K

…qlite3 12.x, optional + lazy-loaded)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

Engineer (agent-os) and others added 2 commits October 9, 2026 11:46
… CI install; accurate npm 11 comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lure message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@randhirinsta

Copy link
Copy Markdown
Contributor Author

QA: PASS ✅ · Env: Linux x64 QA host, throwaway sandbox qacli14b (deleted) · InstaWP/cli PR #14 @40efcfe (re-checked before posting) · packed tgz sha256 1bb26418…, npm i -g <tgz> into an isolated prefix per leg. Supersedes nothing: the Node 22/24/26 matrix above was at 4239e5b.

Leg (Node 24.21.0 unless noted) Install bs3 binary --version / login / whoami / sites list / wp / exec local clone local push --with-db
npm 12.2.0, no flag exit 0; npm warns bs3 + ssh2 + cpu-features scripts blocked ❌ absent ✅ beta.34, all exit 0 (ssh works without ssh2's native crypto) ⚠️ still exit 0 "Clone complete!" with an empty database/ dir; prints ✗ Database import failed: …--allow-scripts=better-sqlite3 ✅ exit 1, the friendly message (Could not locate the bindings file. + --allow-scripts=better-sqlite3) is the only output; no rsync (marker file absent on cloud), post 1 unchanged
npm 12.2.0 + --allow-scripts=better-sqlite3 exit 0 (only ssh2/cpu-features blocked) ✅ prebuilt better_sqlite3.node, opens :memory: ✅ ✅ 13 tables imported ✅ edit post 1 + blogname in SQLite → push → cloud wp post get 1 = QA14 npm12 edit, blogname = QA14 blog npm12
Node 22.23.3 + npm 12.2.0, no flag / flag exit 0 / exit 0 ❌ / ✅ beta.34; loadSqlite() throws the friendly message / ok n/a n/a
npm 11.19.0 + flag exit 0, no error (warns only about ssh2/cpu-features) ✅ beta.34, loadSqlite() ok
npm 10.9.4 (Node 22.22.0) + flag exit 0, no warning at all (flag accepted silently) ✅ beta.34, loadSqlite() ok
instawp upgrade under npm 12 (beta.34 relabelled beta.1) → Updated to 0.0.1-beta.33, exit 0; bs3 not in npm's blocked list ✅

Answer to the clone question: yes, it is still true. Without bs3, local clone exits 0 with "Clone complete!" and leaves no SQLite DB. The error line now names the fix, so it is non-blocking. Moving push's loadSqlite() preflight into clone would make clone fail before the export/rsync.
Disclosure (same as before, pre-existing): for the round trip I set DB_HOST=127.0.0.1 on the throwaway site, symlinked mariadb-*→mysql* in its ~/.local/bin, and pushed with --no-backup.

— QA

@claude

claude Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@randhirinsta
randhirinsta marked this pull request as ready for review October 9, 2026 12:02
…a plain success without the DB

Before, a missing better-sqlite3 (npm 12 without --allow-scripts) made the
DB import fail on one easily-missed line and the command still ended with
a green "Clone complete!" over an empty site. Now clone checks the module
right after resolving the site (try/catch, warns, continues), and if the
import failed for any reason the completion header says so and why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@randhirinsta
randhirinsta marked this pull request as draft October 9, 2026 12:06
@claude

claude Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@randhirinsta

Copy link
Copy Markdown
Contributor Author

QA: PASS ✅ · Env: Linux x64 QA host, Node 24.21.0 + npm 12.2.0, throwaway sandbox qacli14c (deleted) · InstaWP/cli PR #14 @adf2270 (re-checked before posting) · attached tgz sha256 2daf5a67…, npm i -g into an isolated prefix per leg

local clone leg Warning before download? Files Final header Exit
A. no flag (bs3 binary absent, require fails "Could not locate the bindings file") ✅ yellow line right after "Cloud site:", before "Local instance created" / export / rsync ✅ wp-content cloned ✅ "Clone complete — but the database was NOT imported." + Reason = reinstall message; database/ empty 0
B. --allow-scripts=better-sqlite3 (binary present) ✅ no warning ✅ ✅ green "Clone complete!", 13 tables imported; SQLite holds the cloud edits I made first (blogname QA14c blog, post 1 QA14c post) 0
A, un-rigged site (cloud export fails: no mariadb-dump) ✅ ✅ ✅ NOT imported, Reason = reinstall message 0
B, un-rigged site n/a ✅ ✅ NOT imported, Reason = no such table: wp_options; 0-byte .ht.sqlite 0

Answers: 1) yes / yes / yes / exit 0. 2) unchanged: green, DB imported with the right content. 3) Two non-blocking wording points:

  • Leg A prints the reinstall message 3 times (warning, ✗ Database import failed, Reason:).
  • On an un-rigged site the Reason is misleading. It says no such table: wp_options; the real cause is the failed export just above (mariadb-dump: No such file). That is today's default on fleet sites (jailed SSH, see my first comment). It only hits the yellow header because the failed export wrote the server's 95-byte SSH MOTD into database.sql, so hasDump was true. From reading the code, not measured: an export failure that leaves the file empty skips the import, dbImportError stays null, and the green "Clone complete!" prints over an empty DB. Setting dbImportError in the export-failure branch would cover both cases.
    Rig disclosure (same as before): for the rigged rows, on the throwaway site only, DB_HOST=127.0.0.1 + mariadb-*→mysql* symlinks.

— QA

@randhirinsta
randhirinsta marked this pull request as ready for review October 9, 2026 13:02
@randhirinsta
randhirinsta merged commit 640c27c into main Oct 12, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant