diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ee1cf8c..aa59ccb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,14 +10,22 @@ on: jobs: smoke-windows: - name: Windows smoke test + name: Windows smoke test (Node ${{ matrix.node }}) runs-on: windows-latest + strategy: + fail-fast: false + matrix: + # Every Node line a user is likely to have. better-sqlite3 is native: a + # Node version with no prebuilt win32 binary falls back to node-gyp, which + # needs Visual Studio Build Tools — users don't have them. Node 20 alone + # (the old single entry) had a prebuild, so it could never catch that. + node: [22, 24, 26] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: ${{ matrix.node }} - run: npm ci - run: npm run build @@ -46,7 +54,7 @@ jobs: & $busybox awk 'BEGIN { print "awk works" }' if ($LASTEXITCODE -ne 0) { throw "busybox awk failed (exit $LASTEXITCODE)" } - - name: Install globally + verify CLI loads (incl. SFTP + native deps) + - name: Install globally + verify CLI loads (incl. SFTP deps) shell: pwsh run: | npm i -g (Get-ChildItem -Filter "instawp-cli-*.tgz").FullName @@ -54,12 +62,41 @@ jobs: # `instawp --version` eagerly loads the full command import graph: # sync -> ssh-connection -> sftp-sync -> ssh2-sftp-client + ssh2 - # local -> better-sqlite3 (native) - # so a clean exit here proves all of those load on Windows. - Write-Host "--- instawp --version (exercises SFTP + native deps) ---" + # so a clean exit here proves those load on Windows. better-sqlite3 is + # NOT in that graph (lazy-loaded by src/lib/sqlite.ts) — checked below. + Write-Host "--- instawp --version (exercises SFTP deps) ---" instawp --version if ($LASTEXITCODE -ne 0) { throw "instawp --version failed (exit $LASTEXITCODE) — a dependency failed to load" } + - name: Verify the native SQLite module installed and loads + shell: pwsh + run: | + # better-sqlite3 is an optionalDependency, so npm SKIPS it silently when + # 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 + if ($LASTEXITCODE -ne 0) { throw "better-sqlite3 is missing or failed to load on Node ${{ matrix.node }}" } + # The runner HAS Visual Studio, so a missing prebuild would be compiled + # silently and still load — while users without build tools fail. A + # prebuild ships only build\Release\better_sqlite3.node; node-gyp + # writes build\config.gypi. Its presence means no prebuild was found. + if (Test-Path (Join-Path $cli "node_modules\better-sqlite3\build\config.gypi")) { + throw "better-sqlite3 was compiled from source on Node ${{ matrix.node }} (no prebuilt binary) — users without VS Build Tools would fail to install" + } + + - name: Verify the CLI still starts without the native module + shell: pwsh + run: | + # Simulates a machine where the optional native build was skipped: core + # commands must keep working; only local clone/push may fail. + $mod = Join-Path (npm root -g) "@instawp\cli\node_modules\better-sqlite3" + if (-not (Test-Path $mod)) { throw "expected $mod to exist" } + Rename-Item $mod "$mod.hidden" + instawp --version + if ($LASTEXITCODE -ne 0) { throw "instawp --version failed without better-sqlite3 (exit $LASTEXITCODE) — it is being loaded eagerly again" } + Rename-Item "$mod.hidden" $mod + publish: name: Publish to npm needs: smoke-windows @@ -74,7 +111,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 registry-url: https://registry.npmjs.org - run: npm ci diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f7db95..e99fb5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.0.1-beta.34 (2026-10-09) + +### Fixed — `npm install -g @instawp/cli` fails on Windows with Node 24+ + +The install aborted on Windows for anyone on Node 24 LTS, 25 or 26 (`npm warn cleanup Failed to remove some directories` after a native build error). + +- `better-sqlite3` 11.10.0 ships prebuilt Windows binaries only for Node 18–23. On newer Node, npm fell back to compiling it with node-gyp, which needs Visual Studio Build Tools — so the whole install failed. +- Upgraded to `better-sqlite3` ^12.11.1 (prebuilt for Node 22, 24, 25 and 26 on Windows, macOS and Linux) and made it an **optional** dependency: if the native module still can't be installed, the CLI installs anyway. +- The module is now loaded only by the commands that use it (`local clone` / `local push`). Before, `local.ts` imported it at startup, so a missing module broke every command, including `login`. If it's missing, those two commands now say how to fix it. +- **Requires Node.js 22+** (was 18+). Node 18 and 20 are end-of-life. +- **npm 12** blocks packages' install scripts unless allowed, so the SQLite binary is never downloaded. The CLI now detects that on first use (not just a missing package) and says to reinstall with `--allow-scripts=better-sqlite3`. `instawp upgrade` passes that flag. The plain install command is unchanged. +- `local clone` checks for the SQLite module before downloading anything and warns (without stopping) if it is missing. When the database couldn't be imported, it ends with "Clone complete — but the database was NOT imported" and the reason, instead of a plain "Clone complete!" over an empty site. +- CI: the Windows smoke test now runs on Node 22, 24 and 26 (it only ran on Node 20, which had a prebuilt binary, so it could never catch this), checks the native module installed and loads, and checks the CLI still starts without it. + ## 0.0.1-beta.33 (2026-07-10) ### Fixed — remote docroot resolved as a full path (chroot/jailed SSH nodes) diff --git a/CLAUDE.md b/CLAUDE.md index cec3ad3..c6318a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ TypeScript CLI for InstaWP. Lets users create/manage WordPress sites from the terminal. -- **Stack**: TypeScript, ESM, Commander.js, Axios, Node 18+ +- **Stack**: TypeScript, ESM, Commander.js, Axios, Node 22+ - **Package**: `@instawp/cli` on npm (scoped, public) - **Entry**: `src/index.ts` → compiled to `dist/index.js` - **Binary**: `instawp` (registered via `bin` in package.json) @@ -209,7 +209,7 @@ The standalone-CLI replacement for the plugin's `wp instawp local push`. Mirrors Windows ships with `ssh`/`scp` but not `rsync`, `awk`, or `sqlite3`. The CLI works on Windows with zero extra installs via: -- **`better-sqlite3`** (npm dep) — replaces the sqlite3 CLI. Native module, prebuilt binaries for win32-x64. +- **`better-sqlite3`** (optional npm dep) — replaces the sqlite3 CLI. Native module with prebuilt binaries per Node ABI: a Node version with no prebuild falls back to node-gyp, which fails without Visual Studio Build Tools, so it is an `optionalDependency` and lazy-loaded via `loadSqlite()` (`src/lib/sqlite.ts`) — only `local clone`/`local push --with-db` need it. **Never import it statically** from anything `index.ts` loads, or a skipped install breaks every command. When bumping it, check the release's `node-v-win32-x64` assets cover every Node line in `publish.yml`'s smoke matrix. - **`vendor/win32/busybox.exe`** — provides `awk` for the `mysql2sqlite` script (invoked as `busybox awk -f ...`). Statically linked, no DLLs. This is the **only** bundled binary. - **Pure-JS SFTP** (`ssh2-sftp-client`) for file transfers — see below. - **`cross-spawn`** for launching WordPress Playground (`local create/start/clone`). `npx`/`wp-playground-cli` are `.cmd` shims on Windows; Node won't spawn `.cmd` without `shell:true` (CVE-2024-27980), so `local-env.ts` uses `cross-spawn` (resolves the shim + quotes mount-path args safely). Never use bare `child_process.spawn` for npx/.cmd on Windows. diff --git a/README.md b/README.md index 4aa0897..73cf9d0 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,8 @@ The CLI checks for a new version **once a day** and prints a one-line hint (to s ## Requirements -- Node.js 18+ +- Node.js 22+ +- **npm 12:** npm blocks packages' install scripts unless you allow them, so the SQLite module used by `local clone` / `local push --with-db` is never downloaded. Everything else works; for those two commands, install with `npm install -g @instawp/cli --allow-scripts=better-sqlite3`. The CLI tells you this if it happens. (`instawp upgrade` passes the flag for you.) - `ssh` and `ssh-keygen` (for SSH / `exec` / `sync` / `db` over SSH) - `rsync` (for `sync` on macOS/Linux; Windows uses built-in SFTP) - Local development uses WordPress Playground (auto-fetched via `npx`) — no Docker required diff --git a/package-lock.json b/package-lock.json index 92526b4..91b65eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,16 @@ { "name": "@instawp/cli", - "version": "0.0.1-beta.33", + "version": "0.0.1-beta.34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@instawp/cli", - "version": "0.0.1-beta.33", + "version": "0.0.1-beta.34", "license": "MIT", "dependencies": { "archiver": "^7.0.1", "axios": "^1.7.0", - "better-sqlite3": "^11.10.0", "chalk": "^5.3.0", "cli-table3": "^0.6.5", "commander": "^12.1.0", @@ -35,7 +34,10 @@ "vitest": "^4.0.18" }, "engines": { - "node": ">=18" + "node": ">=22" + }, + "optionalDependencies": { + "better-sqlite3": "^12.11.1" } }, "node_modules/@colors/colors": { @@ -1518,14 +1520,18 @@ } }, "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" } }, "node_modules/bindings": { @@ -1533,6 +1539,7 @@ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", + "optional": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -1542,6 +1549,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", + "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1576,6 +1584,7 @@ } ], "license": "MIT", + "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -1659,7 +1668,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/cli-cursor": { "version": "5.0.0", @@ -1955,6 +1965,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", + "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -1970,6 +1981,7 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", + "optional": true, "engines": { "node": ">=4.0.0" } @@ -2028,6 +2040,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", + "optional": true, "engines": { "node": ">=8" } @@ -2078,6 +2091,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", + "optional": true, "dependencies": { "once": "^1.4.0" } @@ -2236,6 +2250,7 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "license": "(MIT OR WTFPL)", + "optional": true, "engines": { "node": ">=6" } @@ -2300,7 +2315,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/follow-redirects": { "version": "1.15.11", @@ -2358,7 +2374,8 @@ "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==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -2437,7 +2454,8 @@ "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==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/glob": { "version": "10.5.0", @@ -2547,7 +2565,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/is-docker": { "version": "3.0.0", @@ -2820,6 +2839,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", + "optional": true, "engines": { "node": ">=10" }, @@ -2847,6 +2867,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", + "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2864,7 +2885,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/nan": { "version": "2.27.0", @@ -2896,13 +2918,15 @@ "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==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/node-abi": { "version": "3.92.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "license": "MIT", + "optional": true, "dependencies": { "semver": "^7.3.5" }, @@ -2935,6 +2959,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", + "optional": true, "dependencies": { "wrappy": "1" } @@ -3111,6 +3136,7 @@ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "license": "MIT", + "optional": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -3171,6 +3197,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", + "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -3181,6 +3208,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -3413,7 +3441,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/simple-get": { "version": "4.0.1", @@ -3434,6 +3463,7 @@ } ], "license": "MIT", + "optional": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -3644,6 +3674,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } @@ -3668,6 +3699,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", + "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -3680,6 +3712,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "license": "MIT", + "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -3758,6 +3791,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "license": "Apache-2.0", + "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -4117,7 +4151,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/wsl-utils": { "version": "0.1.0", diff --git a/package.json b/package.json index e93ac54..5cfa70d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@instawp/cli", - "version": "0.0.1-beta.33", + "version": "0.0.1-beta.34", "description": "InstaWP CLI - Create and manage WordPress sites from the terminal", "type": "module", "bin": { @@ -21,7 +21,7 @@ "test:watch": "vitest" }, "engines": { - "node": ">=18" + "node": ">=22" }, "keywords": [ "instawp", @@ -44,7 +44,6 @@ "dependencies": { "archiver": "^7.0.1", "axios": "^1.7.0", - "better-sqlite3": "^11.10.0", "chalk": "^5.3.0", "cli-table3": "^0.6.5", "commander": "^12.1.0", @@ -63,5 +62,8 @@ "@types/ssh2-sftp-client": "^9.0.6", "typescript": "^5.5.0", "vitest": "^4.0.18" + }, + "optionalDependencies": { + "better-sqlite3": "^12.11.1" } } diff --git a/src/commands/local.ts b/src/commands/local.ts index 72e64fa..9d4a1f4 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -6,7 +6,6 @@ import { tmpdir } from 'node:os'; import { randomBytes } from 'node:crypto'; import chalk from 'chalk'; import open from 'open'; -import Database from 'better-sqlite3'; import { resolveFromModule } from '../lib/paths.js'; import { bundledBusybox } from '../lib/windows-binaries.js'; import { @@ -35,6 +34,7 @@ import { syncFiles, execViaSsh, execViaSshToFile, scpUpload } from '../lib/ssh-c import { listLocalFiles } from '../lib/sftp-sync.js'; import { sanitizeName, defaultInstanceName, pushTargetRef, parseTablePrefix, parseSqlTableNames } from '../lib/local-instance.js'; import { generateMysqlDump } from '../lib/sqlite-to-mysql.js'; +import { loadSqlite } from '../lib/sqlite.js'; import { success, error, table, spinner, info, isJsonMode } from '../lib/output.js'; import type { LocalInstance, SshConnection } from '../types.js'; @@ -241,6 +241,18 @@ export function registerLocalCommand(program: Command): void { process.exit(1); } + // --with-db needs the native SQLite module. Check it before anything is + // pushed, so a missing module can't fail halfway (files already synced, + // and an overwrite confirmation for a DB push that cannot happen). + if (opts.withDb) { + try { + loadSqlite(); + } catch (err: any) { + error(err.message); + process.exit(1); + } + } + const localWpContent = join(instance.path, 'wp-content') + '/'; // Where does this push go? Explicit arg → the site this instance was @@ -492,6 +504,14 @@ export function registerLocalCommand(program: Command): void { process.exit(1); } + // The database import needs the native SQLite module. Check it now, before + // anything is downloaded, and warn instead of stopping: the files still clone. + try { + loadSqlite(); + } catch (err: any) { + info(chalk.yellow("The site's files will be cloned, but its database can't be imported. ") + err.message); + } + // 2. Create local instance const instances = getLocalInstances(); const name = opts.name ? sanitizeName(opts.name) : defaultInstanceName(site); @@ -595,6 +615,7 @@ export function registerLocalCommand(program: Command): void { // 7. Convert MySQL dump → SQLite, import directly, fix URLs and table prefix const hasDump = existsSync(dumpPath) && statSync(dumpPath).size > 0; let adminUsername = 'admin'; + let dbImportError: string | null = null; if (hasDump) { const dbSpin2 = spinner('Importing database...'); dbSpin2.start(); @@ -639,7 +660,7 @@ export function registerLocalCommand(program: Command): void { ); // Import directly via better-sqlite3 (no external sqlite3 CLI needed) - const db = new Database(sqliteDbPath); + const db = new (loadSqlite())(sqliteDbPath); try { db.exec(sqliteSql); @@ -707,6 +728,7 @@ export function registerLocalCommand(program: Command): void { db.close(); } } catch (err: any) { + dbImportError = err.message; dbSpin2.fail('Database import failed: ' + err.message); } } @@ -737,8 +759,16 @@ export function registerLocalCommand(program: Command): void { writeFileSync(join(muDir, '0-suppress-errors.php'), "` (inherits stdio). Returns npm's exit code. */ +/** + * Run `npm install -g @instawp/cli@` (inherits stdio). Returns npm's exit code. + * `--allow-scripts=better-sqlite3`: npm 12 blocks install scripts by default, which + * would leave the SQLite native binary undownloaded (local clone/push then fail). + * npm 10/11 accept it (some 11.x versions print an unknown-config warning). + */ export function performUpgrade(versionTag = 'latest'): number { - const res = spawnSync('npm', ['install', '-g', `${PKG}@${versionTag}`], { stdio: 'inherit' }); + const res = spawnSync('npm', ['install', '-g', `${PKG}@${versionTag}`, '--allow-scripts=better-sqlite3'], { stdio: 'inherit' }); return res.status ?? 1; }