From bf87f681b6c9d56a329a36d38218c6eb49f78381 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:29:02 +0530 Subject: [PATCH 1/9] build: use prebuilt @lydell/node-pty for PTY sessions node-pty ships no Linux prebuilds, so every Linux install compiled it with node-gyp, and macOS needed a postinstall to restore spawn-helper's execute bit. @lydell/node-pty publishes per-platform prebuilt packages (including Linux) with correct permissions, so the postinstall goes away. Co-Authored-By: Claude Opus 5.5 --- package.json | 3 +- pnpm-lock.yaml | 81 ++++++++++++++++++++++------ pnpm-workspace.yaml | 1 - scripts/fix-node-pty-permissions.mjs | 24 --------- src/process-sessions.ts | 8 +-- 5 files changed, 69 insertions(+), 48 deletions(-) delete mode 100644 scripts/fix-node-pty-permissions.mjs diff --git a/package.json b/package.json index 5d0ed9be0..fc4fc15a4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "dev": "tsx scripts/dev.ts", "dev:seed": "tsx scripts/dev-state.ts seed", "dev:reset": "tsx scripts/dev-state.ts reset", - "postinstall": "node scripts/fix-node-pty-permissions.mjs", "prepack": "pnpm build", "schema:config": "tsx scripts/generate-config-schema.ts", "start": "node dist/cli.js serve", @@ -101,6 +100,6 @@ "undici": "8.5.0" }, "optionalDependencies": { - "node-pty": "^1.1.0" + "@lydell/node-pty": "1.2.0-beta.18" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a7efa3f7..fed8dbc0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,9 +133,9 @@ importers: specifier: ^8.0.14 version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.3)(yaml@2.9.0) optionalDependencies: - node-pty: - specifier: ^1.1.0 - version: 1.1.0 + '@lydell/node-pty': + specifier: 1.2.0-beta.18 + version: 1.2.0-beta.18 packages: @@ -636,6 +636,39 @@ packages: cpu: [x64] os: [win32] + '@lydell/node-pty-darwin-arm64@1.2.0-beta.18': + resolution: {integrity: sha512-gJizeNHkkKCoggtj5Q0yf4btK6dPfr7E063SrOh1jHXp3IyjiErU4KwT7Cn29qb/OY5pBo/Utctenl03MyBtAg==} + cpu: [arm64] + os: [darwin] + + '@lydell/node-pty-darwin-x64@1.2.0-beta.18': + resolution: {integrity: sha512-xnJQz+i5v7pkR1xo14ffTwvMm2DoK3KqPACQaJqxtMVkM7ojv14/KmkaV2jte7Q0BIzfU+VbKVUL9Zmf23G8VQ==} + cpu: [x64] + os: [darwin] + + '@lydell/node-pty-linux-arm64@1.2.0-beta.18': + resolution: {integrity: sha512-fM8jRcZDZwG7ihEOx/R7N1aGHdT4Fd6IQj1d6Jp7Qb9FeDWlIwLuVlxUkJOsW4V1ZmJe1xZl2+WR0RjqMvb2+Q==} + cpu: [arm64] + os: [linux] + + '@lydell/node-pty-linux-x64@1.2.0-beta.18': + resolution: {integrity: sha512-LGikvfcvoPrxXO7o1gCsd5ErrRWnQFYLxjJfX0uwBpi0oGPiTERUBLzKspzZs7wjXZqF30mnNB/C09uotu5vRA==} + cpu: [x64] + os: [linux] + + '@lydell/node-pty-win32-arm64@1.2.0-beta.18': + resolution: {integrity: sha512-S6h4G3zYGe+pPQxhlNxg+MvJNUIwXFxhnUufx1QT45TgratKMxQfoGPidvnwoLwuXQ3hqQ7SsI93rH8RUoL09g==} + cpu: [arm64] + os: [win32] + + '@lydell/node-pty-win32-x64@1.2.0-beta.18': + resolution: {integrity: sha512-wBSMeAwEL04t694ln+QbLnc98YeF3mAwWm/NP8acYDAobPw/WK7KKpk8z07iyxv0gRH2Z2/L/f6vgtnBgxE0Bg==} + cpu: [x64] + os: [win32] + + '@lydell/node-pty@1.2.0-beta.18': + resolution: {integrity: sha512-ryy4liRcw6QBBKWDIbJgTW7Pw8NI2xjri/5s/LZg/jJ8ZVg8MgA1xI/vjpT5qTvDvPjUpU5NybJBrKRgK/QvlA==} + '@mariozechner/clipboard-darwin-arm64@0.3.9': resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} engines: {node: '>= 10'} @@ -1977,9 +2010,6 @@ packages: resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} engines: {node: '>=10'} - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -1997,9 +2027,6 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true - node-pty@1.1.0: - resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3003,6 +3030,34 @@ snapshots: '@koromix/koffi-win32-x64@3.2.1': optional: true + '@lydell/node-pty-darwin-arm64@1.2.0-beta.18': + optional: true + + '@lydell/node-pty-darwin-x64@1.2.0-beta.18': + optional: true + + '@lydell/node-pty-linux-arm64@1.2.0-beta.18': + optional: true + + '@lydell/node-pty-linux-x64@1.2.0-beta.18': + optional: true + + '@lydell/node-pty-win32-arm64@1.2.0-beta.18': + optional: true + + '@lydell/node-pty-win32-x64@1.2.0-beta.18': + optional: true + + '@lydell/node-pty@1.2.0-beta.18': + optionalDependencies: + '@lydell/node-pty-darwin-arm64': 1.2.0-beta.18 + '@lydell/node-pty-darwin-x64': 1.2.0-beta.18 + '@lydell/node-pty-linux-arm64': 1.2.0-beta.18 + '@lydell/node-pty-linux-x64': 1.2.0-beta.18 + '@lydell/node-pty-win32-arm64': 1.2.0-beta.18 + '@lydell/node-pty-win32-x64': 1.2.0-beta.18 + optional: true + '@mariozechner/clipboard-darwin-arm64@0.3.9': optional: true @@ -4147,9 +4202,6 @@ snapshots: dependencies: semver: 7.8.4 - node-addon-api@7.1.1: - optional: true - node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -4165,11 +4217,6 @@ snapshots: detect-libc: 2.1.2 optional: true - node-pty@1.1.0: - dependencies: - node-addon-api: 7.1.1 - optional: true - object-assign@4.1.1: {} object-inspect@1.13.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f7842d097..d8cfae99e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,5 +4,4 @@ allowBuilds: esbuild: true koffi: true msgpackr-extract: false - node-pty: true protobufjs: false diff --git a/scripts/fix-node-pty-permissions.mjs b/scripts/fix-node-pty-permissions.mjs deleted file mode 100644 index 04fdd8442..000000000 --- a/scripts/fix-node-pty-permissions.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import { chmod } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -if (process.platform === "darwin") { - const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); - - for (const architecture of ["arm64", "x64"]) { - const helper = resolve( - projectRoot, - "node_modules", - "node-pty", - "prebuilds", - `darwin-${architecture}`, - "spawn-helper", - ); - - try { - await chmod(helper, 0o755); - } catch (error) { - if (error.code !== "ENOENT") throw error; - } - } -} diff --git a/src/process-sessions.ts b/src/process-sessions.ts index e8301b5e8..4502c7e54 100644 --- a/src/process-sessions.ts +++ b/src/process-sessions.ts @@ -455,16 +455,16 @@ export class ProcessSessionManager { } private async startPty(session: ProcessSession, input: StartCommandInput): Promise { - let nodePty: typeof import("node-pty"); + let nodePty: typeof import("@lydell/node-pty"); try { - nodePty = await import("node-pty"); + nodePty = await import("@lydell/node-pty"); } catch { - throw new Error("PTY support requires the optional node-pty dependency."); + throw new Error("PTY support requires the optional @lydell/node-pty dependency."); } const shell = resolveShellCommand(input.command); - let pty: import("node-pty").IPty; + let pty: import("@lydell/node-pty").IPty; pty = nodePty.spawn(shell.executable, shell.args, { cwd: input.cwd, From 03baeba13847be2a06a042d714ee22c3ffca2a52 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:46:32 +0530 Subject: [PATCH 2/9] build: replace better-sqlite3 with built-in node:sqlite better-sqlite3 is a native addon that either downloads a prebuild for the active Node ABI or falls back to node-gyp, which broke installs on unusual Node versions and blocked installing with --ignore-scripts. node:sqlite is built into every Node allowed by engines (>=22.19), so DevSpace state no longer needs a compiled dependency. - Drizzle moves to 1.0.0-rc.5 (exact pin) for its node-sqlite driver. - A small transaction() helper replaces better-sqlite3's transaction wrapper, keeping nested calls as savepoints. - node:sqlite still emits an ExperimentalWarning, and built-ins load while the module graph links, so the filter is preloaded: the bin launcher imports it first and the agent daemon gets it via --import. - doctor reports node:sqlite instead of the native module, and the better-sqlite3 rebuild troubleshooting entry is gone. Existing databases open unchanged (same SQLite file format); verified against a backup of a real v1 state database. Co-Authored-By: Claude Opus 5.5 --- bin/run-entrypoint.js | 1 + bin/silence-sqlite-warning.js | 12 ++ package.json | 6 +- pnpm-lock.yaml | 349 ++++++++------------------------ pnpm-workspace.yaml | 1 - scripts/dev-state.ts | 6 +- src/cli-config.test.ts | 2 +- src/cli.test.ts | 4 +- src/cli.ts | 28 +-- src/db/client.ts | 21 +- src/db/migrations.ts | 38 ++-- src/db/sqlite.ts | 47 +++++ src/dev-state.test.ts | 16 +- src/local-agent-client.ts | 8 +- src/local-agent-store.test.ts | 6 +- src/local-agent-store.ts | 30 +-- src/oauth-store.test.ts | 18 +- src/oauth-store.ts | 8 +- src/workflow-store.ts | 6 +- src/workspace-activity-store.ts | 8 +- 20 files changed, 239 insertions(+), 376 deletions(-) create mode 100644 bin/silence-sqlite-warning.js create mode 100644 src/db/sqlite.ts diff --git a/bin/run-entrypoint.js b/bin/run-entrypoint.js index b9eee6fb4..4c10e534c 100644 --- a/bin/run-entrypoint.js +++ b/bin/run-entrypoint.js @@ -1,3 +1,4 @@ +import "./silence-sqlite-warning.js"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; diff --git a/bin/silence-sqlite-warning.js b/bin/silence-sqlite-warning.js new file mode 100644 index 000000000..d5921bf4a --- /dev/null +++ b/bin/silence-sqlite-warning.js @@ -0,0 +1,12 @@ +// node:sqlite is a release candidate but still emits an ExperimentalWarning in every process that loads it. +// Built-ins load while the ES module graph links, before any app code runs, so this filter must be preloaded: +// run-entrypoint.js imports it first, and the agent daemon receives it through `--import`. +const emitWarning = process.emitWarning; + +process.emitWarning = function (warning, ...args) { + const type = typeof args[0] === "string" ? args[0] : args[0]?.type; + const message = typeof warning === "string" ? warning : warning?.message; + + if (type === "ExperimentalWarning" && message?.startsWith("SQLite")) return; + return emitWarning.call(process, warning, ...args); +}; diff --git a/package.json b/package.json index fc4fc15a4..228e3abd4 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "prepack": "pnpm build", "schema:config": "tsx scripts/generate-config-schema.ts", "start": "node dist/cli.js serve", - "test": "tsx --test --test-concurrency=1 \"src/**/*.test.ts\"", + "test": "tsx --import ./bin/silence-sqlite-warning.js --test --test-concurrency=1 \"src/**/*.test.ts\"", "test:package-install": "tsx --test --test-concurrency=1 \"test/package-install-smoke.test.ts\"", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p scripts/qa/tsconfig.json && tsc -p test/agents-replay/tsconfig.json", "lint": "oxlint", @@ -65,10 +65,9 @@ "@pierre/diffs": "^1.3.6", "ajv": "^8.20.0", "better-result": "^2.10.0", - "better-sqlite3": "^12.10.0", "cross-spawn": "^7.0.6", "diff": "^8.0.3", - "drizzle-orm": "^0.45.2", + "drizzle-orm": "1.0.0-rc.5", "express": "^5.2.1", "jsonc-parser": "^3.3.1", "koffi": "3.2.1", @@ -82,7 +81,6 @@ }, "devDependencies": { "@oxlint/plugins": "1.89.0", - "@types/better-sqlite3": "^7.6.13", "@types/express": "^5.0.6", "@types/node": "^25.9.1", "@types/react": "^19.2.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fed8dbc0c..80b5d3a95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,9 +53,6 @@ importers: better-result: specifier: ^2.10.0 version: 2.10.0 - better-sqlite3: - specifier: ^12.10.0 - version: 12.10.0 cross-spawn: specifier: ^7.0.6 version: 7.0.6 @@ -63,8 +60,8 @@ importers: specifier: ^8.0.3 version: 8.0.3 drizzle-orm: - specifier: ^0.45.2 - version: 0.45.2(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.10.0) + specifier: 1.0.0-rc.5 + version: 1.0.0-rc.5(@opentelemetry/api@1.9.0)(zod@4.4.3) express: specifier: ^5.2.1 version: 5.2.1 @@ -99,9 +96,6 @@ importers: '@oxlint/plugins': specifier: 1.89.0 version: 1.89.0 - '@types/better-sqlite3': - specifier: ^7.6.13 - version: 7.6.13 '@types/express': specifier: ^5.0.6 version: 5.0.6 @@ -1213,9 +1207,6 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - '@types/better-sqlite3@7.6.13': - resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} - '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -1314,19 +1305,9 @@ packages: better-result@2.10.0: resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==} - better-sqlite3@12.10.0: - resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} - engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} - bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -1341,9 +1322,6 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1369,9 +1347,6 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -1423,14 +1398,6 @@ packages: supports-color: optional: true - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1458,43 +1425,88 @@ packages: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} - drizzle-orm@0.45.2: - resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + drizzle-orm@1.0.0-rc.5: + resolution: {integrity: sha512-7BMYTsAZbQ9DcQCtLG4G202Hq6nc/e8o1EBKN6nBweRI8EGPFe/VdyvWQCsU9fuZSiRjNpELolUSi//JFBq+lw==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' + '@aws/aurora-dsql-node-postgres-connector': '>=0.1.9' + '@aws/aurora-dsql-postgresjs-connector': '>=0.3.0' '@cloudflare/workers-types': '>=4' + '@drizzle-team/minipg': '>=0.4.2' + '@effect/sql-d1': '>=4.0.0' + '@effect/sql-libsql': '>=4.0.0' + '@effect/sql-mysql2': '>=4.0.0' + '@effect/sql-pg': '>=4.0.0' + '@effect/sql-pglite': '>=4.0.0' + '@effect/sql-sqlite-bun': '>=4.0.0' + '@effect/sql-sqlite-do': '>=4.0.0' + '@effect/sql-sqlite-node': '>=4.0.0' + '@effect/sql-sqlite-wasm': '>=4.0.0' '@electric-sql/pglite': '>=0.2.0' '@libsql/client': '>=0.10.0' '@libsql/client-wasm': '>=0.10.0' '@neondatabase/serverless': '>=0.10.0' - '@op-engineering/op-sqlite': '>=2' + '@op-engineering/op-sqlite': '>=17' '@opentelemetry/api': ^1.4.1 '@planetscale/database': '>=1.13' - '@prisma/client': '*' + '@sinclair/typebox': '>=0.34.8' + '@sqlitecloud/drivers': '>=1.0.653' '@tidbcloud/serverless': '*' + '@tursodatabase/database': '>=0.7.1' + '@tursodatabase/database-common': '>=0.7.1' + '@tursodatabase/database-wasm': '>=0.7.1' + '@tursodatabase/serverless': '>=1.4.0' + '@tursodatabase/sync': '>=0.7.1' '@types/better-sqlite3': '*' + '@types/mssql': ^9.1.4 '@types/pg': '*' '@types/sql.js': '*' '@upstash/redis': '>=1.34.7' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' - better-sqlite3: '>=7' + arktype: '>=2.0.0' + better-sqlite3: '>=9.3.0' bun-types: '*' + effect: '>=4.0.0' expo-sqlite: '>=14.0.0' - gel: '>=2' - knex: '*' - kysely: '*' + mssql: ^11.0.1 mysql2: '>=2' pg: '>=8' postgres: '>=3' - prisma: '*' sql.js: '>=1' sqlite3: '>=5' + typebox: '>=1.2.0' + valibot: '>=1.0.0-beta.7' + zod: ^3.25.0 || ^4.0.0 peerDependenciesMeta: '@aws-sdk/client-rds-data': optional: true + '@aws/aurora-dsql-node-postgres-connector': + optional: true + '@aws/aurora-dsql-postgresjs-connector': + optional: true '@cloudflare/workers-types': optional: true + '@drizzle-team/minipg': + optional: true + '@effect/sql-d1': + optional: true + '@effect/sql-libsql': + optional: true + '@effect/sql-mysql2': + optional: true + '@effect/sql-pg': + optional: true + '@effect/sql-pglite': + optional: true + '@effect/sql-sqlite-bun': + optional: true + '@effect/sql-sqlite-do': + optional: true + '@effect/sql-sqlite-node': + optional: true + '@effect/sql-sqlite-wasm': + optional: true '@electric-sql/pglite': optional: true '@libsql/client': @@ -1509,12 +1521,26 @@ packages: optional: true '@planetscale/database': optional: true - '@prisma/client': + '@sinclair/typebox': + optional: true + '@sqlitecloud/drivers': optional: true '@tidbcloud/serverless': optional: true + '@tursodatabase/database': + optional: true + '@tursodatabase/database-common': + optional: true + '@tursodatabase/database-wasm': + optional: true + '@tursodatabase/serverless': + optional: true + '@tursodatabase/sync': + optional: true '@types/better-sqlite3': optional: true + '@types/mssql': + optional: true '@types/pg': optional: true '@types/sql.js': @@ -1525,17 +1551,17 @@ packages: optional: true '@xata.io/client': optional: true + arktype: + optional: true better-sqlite3: optional: true bun-types: optional: true - expo-sqlite: - optional: true - gel: + effect: optional: true - knex: + expo-sqlite: optional: true - kysely: + mssql: optional: true mysql2: optional: true @@ -1543,12 +1569,16 @@ packages: optional: true postgres: optional: true - prisma: - optional: true sql.js: optional: true sqlite3: optional: true + typebox: + optional: true + valibot: + optional: true + zod: + optional: true dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -1567,9 +1597,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1602,10 +1629,6 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - express-rate-limit@8.5.2: resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} @@ -1661,9 +1684,6 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -1680,9 +1700,6 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1711,9 +1728,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -1777,9 +1791,6 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -1787,9 +1798,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -1966,24 +1974,14 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1999,17 +1997,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - node-abi@3.92.0: - resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} - engines: {node: '>=10'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2114,12 +2105,6 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. - hasBin: true - proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -2134,9 +2119,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - pure-rand@8.4.2: resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} @@ -2162,10 +2144,6 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - react-dom@19.2.6: resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: @@ -2175,10 +2153,6 @@ packages: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -2269,12 +2243,6 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -2292,26 +2260,12 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strnum@2.3.0: resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} - tar-fs@2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -2334,9 +2288,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} @@ -2375,9 +2326,6 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3468,10 +3416,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/better-sqlite3@7.6.13': - dependencies: - '@types/node': 25.9.1 - '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -3566,23 +3510,8 @@ snapshots: better-result@2.10.0: {} - better-sqlite3@12.10.0: - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - bignumber.js@9.3.1: {} - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -3605,11 +3534,6 @@ snapshots: buffer-equal-constant-time@1.0.1: {} - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -3630,8 +3554,6 @@ snapshots: character-entities-legacy@3.0.0: {} - chownr@1.1.4: {} - comma-separated-tokens@2.0.3: {} commander@12.1.0: {} @@ -3665,12 +3587,6 @@ snapshots: dependencies: ms: 2.1.3 - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - deep-extend@0.6.0: {} - depd@2.0.0: {} dequal@2.0.3: {} @@ -3687,11 +3603,10 @@ snapshots: diff@9.0.0: {} - drizzle-orm@0.45.2(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.10.0): + drizzle-orm@1.0.0-rc.5(@opentelemetry/api@1.9.0)(zod@4.4.3): optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/better-sqlite3': 7.6.13 - better-sqlite3: 12.10.0 + zod: 4.4.3 dunder-proto@1.0.1: dependencies: @@ -3712,10 +3627,6 @@ snapshots: encodeurl@2.0.0: {} - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -3763,8 +3674,6 @@ snapshots: dependencies: eventsource-parser: 3.1.0 - expand-template@2.0.3: {} - express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1 @@ -3846,8 +3755,6 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - file-uri-to-path@1.0.0: {} - finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -3867,8 +3774,6 @@ snapshots: fresh@2.0.0: {} - fs-constants@1.0.0: {} - fsevents@2.3.3: optional: true @@ -3910,8 +3815,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 - github-from-package@0.0.0: {} - glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -3995,14 +3898,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: {} - ignore@7.0.5: {} inherits@2.0.4: {} - ini@1.3.8: {} - ip-address@10.2.0: {} ipaddr.js@1.9.1: {} @@ -4162,18 +4061,12 @@ snapshots: dependencies: mime-db: 1.54.0 - mimic-response@3.1.0: {} - minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 - minimist@1.2.8: {} - minipass@7.1.3: {} - mkdirp-classic@0.5.3: {} - ms@2.1.3: {} msgpackr-extract@3.0.4: @@ -4194,14 +4087,8 @@ snapshots: nanoid@3.3.12: {} - napi-build-utils@2.0.0: {} - negotiator@1.0.0: {} - node-abi@3.92.0: - dependencies: - semver: 7.8.4 - node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -4296,21 +4183,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.92.0 - pump: 3.0.4 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.4 - tunnel-agent: 0.6.0 - proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -4338,11 +4210,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - pure-rand@8.4.2: {} qs@6.15.2: @@ -4372,13 +4239,6 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - react-dom@19.2.6(react@19.2.6): dependencies: react: 19.2.6 @@ -4386,12 +4246,6 @@ snapshots: react@19.2.6: {} - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -4523,14 +4377,6 @@ snapshots: signal-exit@3.0.7: {} - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - sisteransi@1.0.5: {} source-map-js@1.2.1: {} @@ -4544,34 +4390,13 @@ snapshots: statuses@2.0.2: {} - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - strip-json-comments@2.0.1: {} - strnum@2.3.0: {} - tar-fs@2.1.4: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.4 - tar-stream: 2.2.0 - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -4591,10 +4416,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - type-is@2.1.0: dependencies: content-type: 2.0.0 @@ -4634,8 +4455,6 @@ snapshots: unpipe@1.0.0: {} - util-deprecate@1.0.2: {} - vary@1.1.2: {} vfile-message@4.0.3: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d8cfae99e..9363492f0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,5 @@ allowBuilds: '@google/genai': false - better-sqlite3: true esbuild: true koffi: true msgpackr-extract: false diff --git a/scripts/dev-state.ts b/scripts/dev-state.ts index 59e0b04db..e82b8569e 100644 --- a/scripts/dev-state.ts +++ b/scripts/dev-state.ts @@ -10,11 +10,11 @@ import { } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; -import Database from "better-sqlite3"; import { parse, type ParseError } from "jsonc-parser"; import { migrateLegacyConfig } from "../src/config-migration.js"; import { devspaceConfigSchema, type DevspaceConfig } from "../src/config-schema.js"; import { databasePath } from "../src/db/client.js"; +import { backupSqlite, openSqlite } from "../src/db/sqlite.js"; import { expandHomePath } from "../src/roots.js"; const checkoutRoot = resolve(process.cwd()); @@ -145,10 +145,10 @@ async function readSourceConfig(configDir: string): Promise<{ config: DevspaceCo async function backupDatabase(sourcePath: string, destinationPath: string): Promise { await mkdir(dirname(destinationPath), { recursive: true }); - const source = new Database(sourcePath, { readonly: true, fileMustExist: true }); + const source = openSqlite(sourcePath, { readOnly: true }); try { - await source.backup(destinationPath); + await backupSqlite(source, destinationPath); await chmod(destinationPath, 0o600); } finally { source.close(); diff --git a/src/cli-config.test.ts b/src/cli-config.test.ts index d5469460d..3b7444934 100644 --- a/src/cli-config.test.ts +++ b/src/cli-config.test.ts @@ -15,7 +15,7 @@ test("config set tells users to restart the running server", () => { const stdout = execFileSync( "node", - ["--import", "tsx", "src/cli.ts", "config", "set", "publicBaseUrl", "https://example.com"], + ["bin/devspace.js", "config", "set", "publicBaseUrl", "https://example.com"], { encoding: "utf8", env }, ); diff --git a/src/cli.test.ts b/src/cli.test.ts index fdc83722d..02fb343be 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -29,7 +29,7 @@ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.me test("-v and --version print the package version", () => { for (const flag of ["-v", "--version"]) { - const output = execFileSync("node", ["--import", "tsx", "src/cli.ts", flag], { + const output = execFileSync("node", ["bin/devspace.js", flag], { encoding: "utf8", env: { ...process.env, DEVSPACE_CONFIG_DIR: "/tmp/devspace-cli-version-test" }, }).trim(); @@ -204,7 +204,7 @@ const currentWorkspaceEnv = { DEVSPACE_WORKSPACE_ID: "ws_current", DEVSPACE_WORK // Runs the source CLI against the fixture config; pass `{}` to omit the current-workspace variables. function execCli(args: string[], workspaceEnv: NodeJS.ProcessEnv = currentWorkspaceEnv) { - return execFileAsync("node", ["--import", "tsx", "src/cli.ts", ...args], { + return execFileAsync("node", ["bin/devspace.js", ...args], { cwd: process.cwd(), encoding: "utf8", env: { ...process.env, ...cliConfigEnv, ...workspaceEnv }, diff --git a/src/cli.ts b/src/cli.ts index 04c6a5668..a5b784987 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -371,20 +371,6 @@ async function runInit({ force }: { force: boolean }): Promise { } async function serve(): Promise { - const sqliteStatus = checkSqliteNative(); - - if (sqliteStatus !== "ok") { - throw new Error( - [ - "better-sqlite3 could not load for this Node runtime.", - sqliteStatus, - "", - "Try reinstalling or rebuilding dependencies under the active Node version:", - " npm rebuild better-sqlite3", - ].join("\n"), - ); - } - const config = loadConfig(); await runStartupWorktreeCleanup(config); const { configureHttpServer, createServer } = await import("./server.js"); @@ -474,11 +460,10 @@ async function runDoctor(): Promise { console.log(`Config file: ${files.configExists ? files.configPath : "missing"}`); console.log(`Auth file: ${files.authExists ? files.authPath : "missing"}`); console.log(`Node: ${process.version} (${nodeVersionStatus()})`); - console.log(`Node ABI: ${process.versions.modules}`); console.log(`Platform: ${process.platform} ${process.arch}`); console.log(`Git: ${checkGitAvailable()}`); console.log(`Bash shell: ${checkBashShell()}`); - console.log(`SQLite native dependency: ${checkSqliteNative()}`); + console.log(`SQLite: ${await checkSqlite()}`); try { const config = loadConfig(); @@ -1250,14 +1235,15 @@ function nodeVersionStatus(): string { class SetupCancelledError extends Error {} -function checkSqliteNative(): string { +async function checkSqlite(): Promise { try { - // SAFETY: The surrounding checks or construction already match this type. - const Database = require("better-sqlite3") as typeof import("better-sqlite3"); - const db = new Database(":memory:"); + const { openSqlite } = await import("./db/sqlite.js"); + const db = openSqlite(":memory:"); + // SAFETY: sqlite_version() always returns one text column. + const { version } = db.prepare("select sqlite_version() as version").get() as { version: string }; db.close(); - return "ok"; + return `ok (node:sqlite ${version})`; } catch (error) { return error instanceof Error ? error.message : String(error); } diff --git a/src/db/client.ts b/src/db/client.ts index 5711ffdd4..eeaf3bb51 100644 --- a/src/db/client.ts +++ b/src/db/client.ts @@ -1,11 +1,10 @@ import { chmodSync, mkdirSync } from "node:fs"; import { join } from "node:path"; -import Database from "better-sqlite3"; -import { drizzle } from "drizzle-orm/better-sqlite3"; -import * as schema from "./schema.js"; +import { drizzle } from "drizzle-orm/node-sqlite"; import { migrateDatabase } from "./migrations.js"; +import { openSqlite, type SqliteDatabase } from "./sqlite.js"; -export type SqliteDatabase = Database.Database; +export { transaction, type SqliteDatabase } from "./sqlite.js"; export type AppDatabase = ReturnType; @@ -23,12 +22,14 @@ export function openDatabase(stateDir: string): DatabaseHandle { mkdirSync(stateDir, { recursive: true, mode: 0o700 }); chmodSync(stateDir, 0o700); const path = databasePath(stateDir); - const sqlite = new Database(path); + const sqlite = openSqlite(path); chmodSync(path, 0o600); - sqlite.pragma("journal_mode = WAL"); - sqlite.pragma("synchronous = NORMAL"); - sqlite.pragma("busy_timeout = 5000"); - sqlite.pragma("foreign_keys = ON"); + sqlite.exec(` + pragma journal_mode = WAL; + pragma synchronous = NORMAL; + pragma busy_timeout = 5000; + pragma foreign_keys = ON; + `); migrateDatabase(sqlite); return { @@ -39,5 +40,5 @@ export function openDatabase(stateDir: string): DatabaseHandle { } function createDrizzleDatabase(sqlite: SqliteDatabase) { - return drizzle(sqlite, { schema }); + return drizzle({ client: sqlite }); } diff --git a/src/db/migrations.ts b/src/db/migrations.ts index ab04c39a9..b00e2590b 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -1,9 +1,9 @@ -import type Database from "better-sqlite3"; +import { transaction, type SqliteDatabase } from "./sqlite.js"; interface Migration { version: number; name: string; - up(sqlite: Database.Database): void; + up(sqlite: SqliteDatabase): void; } const migrations: Migration[] = [ @@ -114,8 +114,8 @@ const migrations: Migration[] = [ }, ]; -export function migrateDatabase(sqlite: Database.Database): void { - const migrate = sqlite.transaction(() => { +export function migrateDatabase(sqlite: SqliteDatabase): void { + transaction(sqlite, () => { sqlite.exec(` create table if not exists devspace_schema_migrations ( version integer primary key, @@ -158,12 +158,10 @@ export function migrateDatabase(sqlite: Database.Database): void { migration.up(sqlite); recordMigration.run(migration.version, migration.name, new Date().toISOString()); } - }); - - migrate.immediate(); + }, "immediate"); } -function migrateWorkspaceState(sqlite: Database.Database): void { +function migrateWorkspaceState(sqlite: SqliteDatabase): void { sqlite.exec(` create table if not exists workspace_sessions ( id text primary key, @@ -208,7 +206,7 @@ function migrateWorkspaceState(sqlite: Database.Database): void { addColumnIfMissing(sqlite, "workspace_sessions", "managed", "text not null default 'false'"); } -function migrateOAuthState(sqlite: Database.Database): void { +function migrateOAuthState(sqlite: SqliteDatabase): void { sqlite.exec(` create table if not exists oauth_clients ( client_id text primary key, @@ -251,7 +249,7 @@ function migrateOAuthState(sqlite: Database.Database): void { `); } -function migrateLocalAgentSessions(sqlite: Database.Database): void { +function migrateLocalAgentSessions(sqlite: SqliteDatabase): void { sqlite.exec(` create table if not exists local_agent_sessions ( id text primary key, @@ -282,7 +280,7 @@ function migrateLocalAgentSessions(sqlite: Database.Database): void { addColumnIfMissing(sqlite, "local_agent_sessions", "effort", "text"); } -function migrateWorkspaceConversationBindings(sqlite: Database.Database): void { +function migrateWorkspaceConversationBindings(sqlite: SqliteDatabase): void { sqlite.exec(` create table if not exists workspace_conversation_bindings ( conversation_scope_id text not null, @@ -301,12 +299,12 @@ function migrateWorkspaceConversationBindings(sqlite: Database.Database): void { `); } -function migrateLocalAgentStructuredErrors(sqlite: Database.Database): void { +function migrateLocalAgentStructuredErrors(sqlite: SqliteDatabase): void { addColumnIfMissing(sqlite, "local_agent_sessions", "error_code", "text"); addColumnIfMissing(sqlite, "local_agent_sessions", "error_retryable", "text"); } -function migrateLocalAgentEffortRename(sqlite: Database.Database): void { +function migrateLocalAgentEffortRename(sqlite: SqliteDatabase): void { // SAFETY: The surrounding checks or construction already match this type. const columns = sqlite.prepare("pragma table_info(local_agent_sessions)").all() as Array<{ name: string; @@ -335,7 +333,7 @@ function migrateLocalAgentEffortRename(sqlite: Database.Database): void { sqlite.exec("alter table local_agent_sessions rename column thinking to effort"); } -function migrateWorkspaceRecoveryState(sqlite: Database.Database): void { +function migrateWorkspaceRecoveryState(sqlite: SqliteDatabase): void { const workspaceStateExists = sqlite .prepare("select 1 from sqlite_master where type = 'table' and name = 'workspace_sessions'") .get(); @@ -345,7 +343,7 @@ function migrateWorkspaceRecoveryState(sqlite: Database.Database): void { addColumnIfMissing(sqlite, "workspace_sessions", "recovery_kind", "text"); } -function migrateLocalAgentTurns(sqlite: Database.Database): void { +function migrateLocalAgentTurns(sqlite: SqliteDatabase): void { sqlite.exec(` create table if not exists local_agent_turns ( id integer primary key autoincrement, @@ -369,7 +367,7 @@ function migrateLocalAgentTurns(sqlite: Database.Database): void { `); } -function migrateLocalAgentProviderInstances(sqlite: Database.Database): void { +function migrateLocalAgentProviderInstances(sqlite: SqliteDatabase): void { // SAFETY: The surrounding checks or construction already match this type. const columns = sqlite.prepare("pragma table_info(local_agent_sessions)").all() as Array<{ name: string; @@ -389,7 +387,7 @@ function migrateLocalAgentProviderInstances(sqlite: Database.Database): void { `); } -function migrateLocalAgentGenericAcpDriver(sqlite: Database.Database): void { +function migrateLocalAgentGenericAcpDriver(sqlite: SqliteDatabase): void { sqlite.exec(` update local_agent_sessions set driver = 'acp' @@ -397,7 +395,7 @@ function migrateLocalAgentGenericAcpDriver(sqlite: Database.Database): void { `); } -function migrateLocalAgentTurnEvents(sqlite: Database.Database): void { +function migrateLocalAgentTurnEvents(sqlite: SqliteDatabase): void { addColumnIfMissing(sqlite, "local_agent_turns", "turn_id", "text"); addColumnIfMissing(sqlite, "local_agent_turns", "error_detail", "text"); addColumnIfMissing(sqlite, "local_agent_sessions", "latest_turn_id", "text"); @@ -428,7 +426,7 @@ function migrateLocalAgentTurnEvents(sqlite: Database.Database): void { `); } -function migrateWorkspaceActivity(sqlite: Database.Database): void { +function migrateWorkspaceActivity(sqlite: SqliteDatabase): void { sqlite.exec(` create table if not exists workspace_turns ( id integer primary key autoincrement, @@ -475,7 +473,7 @@ function migrateWorkspaceActivity(sqlite: Database.Database): void { } function addColumnIfMissing( - sqlite: Database.Database, + sqlite: SqliteDatabase, table: "workspace_sessions" | "local_agent_sessions" | "local_agent_turns", column: string, definition: string, diff --git a/src/db/sqlite.ts b/src/db/sqlite.ts new file mode 100644 index 000000000..f5b17f1cb --- /dev/null +++ b/src/db/sqlite.ts @@ -0,0 +1,47 @@ +import { backup, DatabaseSync, type DatabaseSyncOptions } from "node:sqlite"; + +export type SqliteDatabase = DatabaseSync; + +export function openSqlite(path: string, options: DatabaseSyncOptions = {}): SqliteDatabase { + return new DatabaseSync(path, options); +} + +/** Copies a live database to `path` using SQLite's online backup API. */ +export async function backupSqlite(source: SqliteDatabase, path: string): Promise { + await backup(source, path); +} + +let savepointCount = 0; + +/** Runs `run` atomically; nested calls become savepoints so an inner failure rolls back only its own work. */ +export function transaction( + database: SqliteDatabase, + run: () => T, + mode: "deferred" | "immediate" = "deferred", +): T { + if (database.isTransaction) { + const savepoint = `devspace_${++savepointCount}`; + database.exec(`savepoint ${savepoint}`); + + try { + const result = run(); + database.exec(`release ${savepoint}`); + return result; + } catch (error) { + database.exec(`rollback to ${savepoint}`); + database.exec(`release ${savepoint}`); + throw error; + } + } + + database.exec(mode === "immediate" ? "begin immediate" : "begin"); + + try { + const result = run(); + database.exec("commit"); + return result; + } catch (error) { + if (database.isTransaction) database.exec("rollback"); + throw error; + } +} diff --git a/src/dev-state.test.ts b/src/dev-state.test.ts index cca18933b..aa1027e07 100644 --- a/src/dev-state.test.ts +++ b/src/dev-state.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test, { after } from "node:test"; import { fileURLToPath } from "node:url"; -import Database from "better-sqlite3"; +import { openSqlite } from "./db/sqlite.js"; const root = await mkdtemp(join(tmpdir(), "devspace-dev-state-test-")); const checkoutRoot = join(root, "checkout"); @@ -30,7 +30,7 @@ test("seed copies source config and state, and reset reseeds only once source au })); await writeFile(join(sourceConfigDir, "skills", "example", "SKILL.md"), "example skill\n"); - const sourceDatabase = new Database(join(sourceStateDir, "devspace.sqlite")); + const sourceDatabase = openSqlite(join(sourceStateDir, "devspace.sqlite")); sourceDatabase.exec("create table marker (value text); insert into marker values ('source')"); sourceDatabase.close(); @@ -51,8 +51,8 @@ test("seed copies source config and state, and reset reseeds only once source au assert.equal(existsSync(join(devRoot, "config", "skills", "example", "SKILL.md")), true); const localDatabasePath = join(devRoot, "state", "devspace.sqlite"); - const localDatabase = new Database(localDatabasePath); - assert.equal(localDatabase.prepare("select value from marker").pluck().get(), "source"); + const localDatabase = openSqlite(localDatabasePath); + assert.equal(localDatabase.prepare("select value from marker").get()?.value, "source"); localDatabase.exec("insert into marker values ('local-only')"); localDatabase.close(); @@ -61,11 +61,11 @@ test("seed copies source config and state, and reset reseeds only once source au await rm(join(sourceConfigDir, "auth.json")); await assert.rejects(runDevState("reset"), /No auth\.json found/); - const preservedDatabase = new Database(localDatabasePath, { readonly: true }); + const preservedDatabase = openSqlite(localDatabasePath, { readOnly: true }); try { assert.deepEqual( - preservedDatabase.prepare("select value from marker order by rowid").pluck().all(), + preservedDatabase.prepare("select value from marker order by rowid").all().map((row) => row.value), ["source", "local-only"], ); } finally { @@ -77,8 +77,8 @@ test("seed copies source config and state, and reset reseeds only once source au })); await runDevState("reset"); - const resetDatabase = new Database(localDatabasePath, { readonly: true }); - assert.deepEqual(resetDatabase.prepare("select value from marker order by rowid").pluck().all(), ["source"]); + const resetDatabase = openSqlite(localDatabasePath, { readOnly: true }); + assert.deepEqual(resetDatabase.prepare("select value from marker order by rowid").all().map((row) => row.value), ["source"]); resetDatabase.close(); }); diff --git a/src/local-agent-client.ts b/src/local-agent-client.ts index 36880d927..ef881f77c 100644 --- a/src/local-agent-client.ts +++ b/src/local-agent-client.ts @@ -702,7 +702,13 @@ export function spawnLocalAgentDaemon( ): void { const entrypoint = resolveDaemonEntrypoint(); - const child = spawn(process.execPath, [...daemonExecArgv(process.execArgv), entrypoint], { + const silenceSqliteWarning = new URL("../bin/silence-sqlite-warning.js", import.meta.url).href; + const child = spawn(process.execPath, [ + ...daemonExecArgv(process.execArgv), + "--import", + silenceSqliteWarning, + entrypoint, + ], { detached: true, stdio: "ignore", windowsHide: true, diff --git a/src/local-agent-store.test.ts b/src/local-agent-store.test.ts index c917496c9..fb48ed219 100644 --- a/src/local-agent-store.test.ts +++ b/src/local-agent-store.test.ts @@ -5,7 +5,7 @@ import { mock } from "node:test"; import { syncBuiltinESMExports } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import Database from "better-sqlite3"; +import { openSqlite } from "./db/sqlite.js"; import { databasePath } from "./db/client.js"; import { LocalAgentStore } from "./local-agent-store.js"; @@ -216,7 +216,7 @@ assert.deepEqual(store.list({ workspaceId: "ws_1", workspaceRoot: join(root, "ot const legacyStateDir = join(root, "legacy-state"); mkdirSync(legacyStateDir, { recursive: true }); - const legacy = new Database(databasePath(legacyStateDir)); + const legacy = openSqlite(databasePath(legacyStateDir)); legacy.exec(` create table devspace_schema_migrations ( version integer primary key, @@ -274,7 +274,7 @@ assert.deepEqual(store.list({ workspaceId: "ws_1", workspaceRoot: join(root, "ot assert.equal(legacyRecord?.effort, "high"); assert.equal(legacyRecord?.providerInstanceId, "cursor"); assert.equal(legacyRecord?.driver, "acp"); - const inspect = new Database(databasePath(legacyStateDir), { readonly: true }); + const inspect = openSqlite(databasePath(legacyStateDir), { readOnly: true }); // SAFETY: The surrounding checks or construction already match this type. const driverColumn = (inspect.prepare("pragma table_info(local_agent_sessions)").all() as Array<{ name: string; notnull: number }>) diff --git a/src/local-agent-store.ts b/src/local-agent-store.ts index 1d4294820..88f07a641 100644 --- a/src/local-agent-store.ts +++ b/src/local-agent-store.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import { resolve } from "node:path"; import { Result, type Result as BetterResult } from "better-result"; -import { openDatabase, type DatabaseHandle } from "./db/client.js"; +import { openDatabase, transaction, type DatabaseHandle } from "./db/client.js"; import { AgentStoreError, isProgrammerDefect } from "./local-agent-errors.js"; import type { LocalAgentTurnEvent, LocalAgentTurnEventBody } from "./local-agent-events.js"; import { validateAgentId } from "./local-agent-id.js"; @@ -171,7 +171,7 @@ export class LocalAgentStore { where workspace_id = ? and workspace_root = ? order by updated_at desc`, ) - .all(scope.workspaceId, resolve(scope.workspaceRoot)) as LocalAgentRow[]; + .all(scope.workspaceId, resolve(scope.workspaceRoot)) as unknown as LocalAgentRow[]; } else if (scope.workspaceId) { // SAFETY: The surrounding checks or construction already match this type. rows = this.database.sqlite @@ -180,7 +180,7 @@ export class LocalAgentStore { where workspace_id = ? order by updated_at desc`, ) - .all(scope.workspaceId) as LocalAgentRow[]; + .all(scope.workspaceId) as unknown as LocalAgentRow[]; } else if (scope.workspaceRoot) { // SAFETY: The surrounding checks or construction already match this type. rows = this.database.sqlite @@ -189,12 +189,12 @@ export class LocalAgentStore { where workspace_root = ? order by updated_at desc`, ) - .all(resolve(scope.workspaceRoot)) as LocalAgentRow[]; + .all(resolve(scope.workspaceRoot)) as unknown as LocalAgentRow[]; } else { // SAFETY: The surrounding checks or construction already match this type. rows = this.database.sqlite .prepare("select * from local_agent_sessions order by updated_at desc") - .all() as LocalAgentRow[]; + .all() as unknown as LocalAgentRow[]; } return rows.map(rowToLocalAgentRecord); @@ -209,7 +209,7 @@ export class LocalAgentStore { } private createDispatch(input: CreateLocalAgentRecordInput): CreatedLocalAgentRecord { - return this.database.sqlite.transaction(() => { + return transaction(this.database.sqlite, () => { const generate = () => `agt_${randomUUID().replaceAll("-", "").slice(0, 8)}`; let id = input.id ?? generate(); validateAgentId(id); @@ -282,7 +282,7 @@ export class LocalAgentStore { ); return { record, created: true }; - }).immediate(); + }, "immediate"); } createResult(input: CreateLocalAgentRecordInput): BetterResult { @@ -396,7 +396,7 @@ export class LocalAgentStore { } beginTurn(agentId: string, input: BeginLocalAgentTurnInput): BegunLocalAgentTurn { - return this.database.sqlite.transaction(() => { + return transaction(this.database.sqlite, () => { const current = this.getById(agentId); if (!current) throw new Error(`Unknown subagent id: ${agentId}`); @@ -444,7 +444,7 @@ export class LocalAgentStore { if (!turn) throw new Error(`Unable to load the new turn for subagent ${agentId}.`); return { agent, turn }; - }).immediate(); + }, "immediate"); } beginTurnResult( @@ -459,7 +459,7 @@ export class LocalAgentStore { turnId: number, completion: FinishLocalAgentTurnInput, ): LocalAgentRecord { - return this.database.sqlite.transaction(() => { + return transaction(this.database.sqlite, () => { const turn = this.getTurnById(turnId); if (!turn || turn.agentId !== agentId) { @@ -521,7 +521,7 @@ export class LocalAgentStore { errorRetryable: completion.errorRetryable, errorDetail: completion.errorDetail, }); - }).immediate(); + }, "immediate"); } finishTurnResult( @@ -588,7 +588,7 @@ export class LocalAgentStore { where turn_row_id = ? and seq > ? order by seq asc`, ) - .all(turn.id, afterSeq) as LocalAgentTurnEventRow[]; + .all(turn.id, afterSeq) as unknown as LocalAgentTurnEventRow[]; return rows.map((row) => ({ // SAFETY: The surrounding checks or construction already match this type. @@ -625,13 +625,13 @@ export class LocalAgentStore { // SAFETY: The surrounding checks or construction already match this type. const rows = this.database.sqlite .prepare("select * from local_agent_turns where agent_id = ? order by id asc") - .all(agentId) as LocalAgentTurnRow[]; + .all(agentId) as unknown as LocalAgentTurnRow[]; return rows.map(rowToLocalAgentTurnRecord); } reconcileActiveRuns(message = "DevSpace restarted while this agent turn was running."): number { - return this.database.sqlite.transaction(() => { + return transaction(this.database.sqlite, () => { const now = new Date().toISOString(); this.database.sqlite .prepare( @@ -651,7 +651,7 @@ export class LocalAgentStore { .run(message, now); return Number(result.changes); - }).immediate(); + }, "immediate"); } reconcileActiveRunsResult( diff --git a/src/oauth-store.test.ts b/src/oauth-store.test.ts index edb47bf9f..b580639fc 100644 --- a/src/oauth-store.test.ts +++ b/src/oauth-store.test.ts @@ -57,10 +57,10 @@ async function testDatabaseConfiguration(stateDir: string): Promise { const database = openDatabase(stateDir); try { - assert.equal(database.sqlite.pragma("journal_mode", { simple: true }), "wal"); - assert.equal(database.sqlite.pragma("synchronous", { simple: true }), 1); - assert.equal(database.sqlite.pragma("busy_timeout", { simple: true }), 5000); - assert.equal(database.sqlite.pragma("foreign_keys", { simple: true }), 1); + assert.equal(database.sqlite.prepare("pragma journal_mode").get()?.journal_mode, "wal"); + assert.equal(database.sqlite.prepare("pragma synchronous").get()?.synchronous, 1); + assert.equal(database.sqlite.prepare("pragma busy_timeout").get()?.timeout, 5000); + assert.equal(database.sqlite.prepare("pragma foreign_keys").get()?.foreign_keys, 1); const migrations = database.sqlite .prepare("select version, name from devspace_schema_migrations order by version") @@ -115,17 +115,15 @@ function testPersistenceAndTokenHashing(stateDir: string): void { const database = openDatabase(stateDir); try { - // SAFETY: The surrounding checks or construction already match this type. const accessHashes = database.sqlite .prepare("select token_hash from oauth_access_tokens") - .pluck() - .all() as string[]; + .all() + .map((row) => row.token_hash); - // SAFETY: The surrounding checks or construction already match this type. const refreshHashes = database.sqlite .prepare("select token_hash from oauth_refresh_tokens") - .pluck() - .all() as string[]; + .all() + .map((row) => row.token_hash); assert.deepEqual(accessHashes, [hashToken(accessToken)]); assert.deepEqual(refreshHashes, [hashToken(refreshToken)]); diff --git a/src/oauth-store.ts b/src/oauth-store.ts index 26f6fc43d..a1e548a5d 100644 --- a/src/oauth-store.ts +++ b/src/oauth-store.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import type { OAuthRegisteredClientsStore } from "@modelcontextprotocol/sdk/server/auth/clients.js"; import { InvalidRequestError } from "@modelcontextprotocol/sdk/server/auth/errors.js"; import type { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/shared/auth.js"; -import { openDatabase, type DatabaseHandle } from "./db/client.js"; +import { openDatabase, transaction, type DatabaseHandle } from "./db/client.js"; export interface PersistedAccessTokenRecord { clientId: string; @@ -146,7 +146,7 @@ export class SqliteOAuthStore { } saveTokenPair(pair: PersistedTokenPair, consumedRefreshTokenHash?: string): boolean { - const save = this.database.sqlite.transaction(() => { + return transaction(this.database.sqlite, () => { if (consumedRefreshTokenHash) { const now = Math.floor(Date.now() / 1000); @@ -166,9 +166,7 @@ export class SqliteOAuthStore { this.saveRefreshToken(pair.refreshTokenHash, pair.refreshToken); return true; - }); - - return save.immediate(); + }, "immediate"); } getRefreshToken(tokenHash: string): PersistedRefreshTokenRecord | undefined { diff --git a/src/workflow-store.ts b/src/workflow-store.ts index 0b74bbb0c..72b6f0e4f 100644 --- a/src/workflow-store.ts +++ b/src/workflow-store.ts @@ -1,5 +1,5 @@ import type { WireInput } from "./json-value.js"; -import { openDatabase, type DatabaseHandle } from "./db/client.js"; +import { openDatabase, transaction, type DatabaseHandle } from "./db/client.js"; import type { LocalAgentWorkspaceScope } from "./local-agent-store.js"; import { WorkflowError, type WorkflowCall, type WorkflowEvent, type WorkflowSnapshot } from "./workflow-types.js"; @@ -34,14 +34,14 @@ export class WorkflowStore { return run; } addCall(call: WorkflowCall): void { - this.database.sqlite.transaction(() => { + transaction(this.database.sqlite, () => { const run = this.get(call.runId)!; this.database.sqlite.prepare("insert into workflow_calls (run_id, call_index, agent_id, record_json) values (?, ?, ?, ?)") .run(call.runId, call.index, call.agentId, JSON.stringify(call)); run.callCount++; run.updatedAt = new Date().toISOString(); this.database.sqlite.prepare("update workflow_runs set record_json = ? where id = ?").run(JSON.stringify(run), run.id); - })(); + }); } calls(runId: string): WorkflowCall[] { return this.database.sqlite.prepare("select record_json from workflow_calls where run_id = ? order by call_index").all(runId) diff --git a/src/workspace-activity-store.ts b/src/workspace-activity-store.ts index 03d24bead..699ba8e63 100644 --- a/src/workspace-activity-store.ts +++ b/src/workspace-activity-store.ts @@ -1,6 +1,6 @@ import { optionalFields } from "./json-value.js"; import { and, asc, count, desc, eq, max, sql } from "drizzle-orm"; -import { openDatabase, type DatabaseHandle } from "./db/client.js"; +import { openDatabase, transaction, type DatabaseHandle } from "./db/client.js"; import { workspaceSessions, workspaceTurns, @@ -93,7 +93,7 @@ export class WorkspaceActivityStore { /** Returns the workspace's open turn, starting the next one when none is open. */ openTurn(workspaceId: string, now = new Date()): WorkspaceTurn { - const open = this.database.sqlite.transaction(() => { + const open = transaction(this.database.sqlite, () => { const existing = this.findOpenTurnRow(workspaceId); if (existing) return existing; @@ -114,9 +114,9 @@ export class WorkspaceActivityStore { }) .returning() .get(); - }); + }, "immediate"); - return this.withCallCounts(open.immediate()); + return this.withCallCounts(open); } currentTurn(workspaceId: string): WorkspaceTurn | undefined { From 72904aba0ee40eeba90bc574706f4179e52866eb Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:46:32 +0530 Subject: [PATCH 3/9] fix(cli): derive the supported Node range from package engines The CLI guard and doctor still advertised >=20.12 while package.json requires >=22.19, which node:sqlite now depends on. Read the range from engines so they cannot drift. Co-Authored-By: Claude Opus 5.5 --- src/cli.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index a5b784987..1e837b172 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -83,7 +83,8 @@ type Command = | "version"; const require = createRequire(import.meta.url); -const SUPPORTED_NODE_RANGE = ">=20.12 <27"; +// SAFETY: package.json always declares engines.node; it is the single source for the supported range. +const SUPPORTED_NODE_RANGE = (require("../package.json") as { engines: { node: string } }).engines.node; async function main(argv: string[]): Promise { assertSupportedNode(); From 70c0456ee278df43435e46ac1b59c4523d2af7ad Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:52:19 +0530 Subject: [PATCH 4/9] feat: add one-command installers for macOS, Linux, and Windows install.sh and install.ps1 reuse a compatible Node on PATH or download a checksum-verified Node 24 LTS for DevSpace alone, install the package into a user-owned npm prefix with --ignore-scripts (no sudo, no native builds), write devspace launchers that pin that Node so version managers cannot break it, add the launcher directory to PATH, and run devspace init from the terminal even under curl | sh. Rerunning updates. Co-Authored-By: Claude Opus 5.5 --- install/install.ps1 | 129 ++++++++++++++++++++++++++++++ install/install.sh | 190 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 install/install.ps1 create mode 100755 install/install.sh diff --git a/install/install.ps1 b/install/install.ps1 new file mode 100644 index 000000000..39837dec5 --- /dev/null +++ b/install/install.ps1 @@ -0,0 +1,129 @@ +# DevSpace installer for Windows. +# +# powershell -c "irm https://raw.githubusercontent.com/Waishnav/devspace/main/install/install.ps1 | iex" +# +# Mirrors install.sh: uses a compatible Node already on PATH, otherwise downloads a checksum-verified Node 24 LTS +# for DevSpace alone, installs @waishnav/devspace into a user-owned npm prefix, writes `devspace` launchers that +# pin that Node, then runs `devspace init`. Rerun it to update. DevSpace itself also needs Git Bash on Windows. +# +# Environment overrides: DEVSPACE_VERSION, DEVSPACE_INSTALL_DIR, DEVSPACE_BIN_DIR, DEVSPACE_NO_INIT=1, +# DEVSPACE_NO_MODIFY_PATH=1, DEVSPACE_PACKAGE_SPEC (see install.sh). + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$Package = "@waishnav/devspace" +$Version = if ($env:DEVSPACE_VERSION) { $env:DEVSPACE_VERSION } else { "latest" } +$PackageSpec = if ($env:DEVSPACE_PACKAGE_SPEC) { $env:DEVSPACE_PACKAGE_SPEC } else { "$Package@$Version" } +$InstallDir = if ($env:DEVSPACE_INSTALL_DIR) { $env:DEVSPACE_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "devspace\install" } +$BinDir = if ($env:DEVSPACE_BIN_DIR) { $env:DEVSPACE_BIN_DIR } else { Join-Path $env:LOCALAPPDATA "devspace\bin" } +$NodeMajor = 24 +# Must match package.json engines.node. +$NodeMin = "22.19.0" +$NodeMaxMajor = 27 + +function Test-CompatibleNode([string]$Node) { + $script = 'const [maj,min,patch]=process.versions.node.split(".").map(Number);const [a,b,c]=process.argv[1].split(".").map(Number);const ok=maj>a||(maj===a&&(min>b||(min===b&&patch>=c)));process.exit(ok&&maj $null + return $LASTEXITCODE -eq 0 +} + +function Install-Node { + $arch = switch ($env:PROCESSOR_ARCHITECTURE) { + "AMD64" { "x64" } + "ARM64" { "arm64" } + default { throw "Unsupported CPU architecture: $($env:PROCESSOR_ARCHITECTURE)" } + } + $dist = "https://nodejs.org/dist/latest-v$NodeMajor.x" + $tmp = Join-Path ([IO.Path]::GetTempPath()) ("devspace-node-" + [Guid]::NewGuid()) + New-Item -ItemType Directory -Path $tmp | Out-Null + + try { + Write-Host "Downloading Node $NodeMajor LTS for win-$arch..." + $sums = (Invoke-WebRequest -UseBasicParsing "$dist/SHASUMS256.txt").Content -split "`n" + $line = $sums | Where-Object { $_ -match "\s(node-v[\d.]+-win-$arch\.zip)$" } | Select-Object -First 1 + if (-not $line) { throw "No Node $NodeMajor build is published for win-$arch." } + $expected, $file = $line.Trim() -split "\s+" + $zip = Join-Path $tmp $file + Invoke-WebRequest -UseBasicParsing "$dist/$file" -OutFile $zip + if ((Get-FileHash -Algorithm SHA256 $zip).Hash.ToLower() -ne $expected) { + throw "Checksum mismatch for $file; refusing to install it." + } + + Expand-Archive -Path $zip -DestinationPath $tmp + $nodeDir = Join-Path $InstallDir "node" + if (Test-Path $nodeDir) { Remove-Item -Recurse -Force $nodeDir } + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + Move-Item (Join-Path $tmp ($file -replace "\.zip$", "")) $nodeDir + Write-Host "Installed $($file -replace '\.zip$', '') into $nodeDir" + return (Join-Path $nodeDir "node.exe") + } finally { + Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue + } +} + +function Select-Node { + $managed = Join-Path $InstallDir "node\node.exe" + if ((Test-Path $managed) -and (Test-CompatibleNode $managed)) { return $managed } + + $system = Get-Command node -ErrorAction SilentlyContinue + if ($system -and (Test-CompatibleNode $system.Source)) { return $system.Source } + if ($system) { Write-Host "Found Node $(& $system.Source --version), but DevSpace needs >=$NodeMin <$NodeMaxMajor." } + + return Install-Node +} + +$Node = Select-Node +$NodeDir = Split-Path $Node +$Npm = Join-Path $NodeDir "npm.cmd" +if (-not (Test-Path $Npm)) { throw "npm was not found next to $Node. Install npm for that Node and rerun." } + +Write-Host "Installing $PackageSpec with Node $(& $Node --version)..." +$prefix = Join-Path $InstallDir "npm" +New-Item -ItemType Directory -Path $prefix -Force | Out-Null +# npm resolves `node` from PATH, so put the selected Node first. +# DevSpace and its dependencies ship prebuilt binaries, so no install scripts are needed. +$env:Path = "$NodeDir;$env:Path" +& $Npm install --global --prefix $prefix --ignore-scripts --no-fund --no-audit --no-update-notifier --loglevel=error $PackageSpec +if ($LASTEXITCODE -ne 0) { throw "npm install failed." } + +$packageDir = Join-Path $prefix "node_modules\$Package" +if (-not (Test-Path (Join-Path $packageDir "bin\devspace.js"))) { throw "Install finished but $packageDir\bin\devspace.js is missing." } +New-Item -ItemType Directory -Path $BinDir -Force | Out-Null + +foreach ($command in @("devspace", "devspace-agentd")) { + # Pin the Node that installed DevSpace so a version manager switching `node` cannot break it. + # goto instead of an if (...) block: paths such as "Program Files (x86)" would close the block early. + $launcher = @( + "@echo off", + "rem Generated by the DevSpace installer. Rerun the installer to update.", + "if not exist `"$Node`" goto missing", + "`"$Node`" `"$packageDir\bin\$command.js`" %*", + "exit /b %errorlevel%", + ":missing", + "echo DevSpace's Node runtime is missing: $Node 1>&2", + "echo Rerun the DevSpace installer. 1>&2", + "exit /b 1" + ) -join "`r`n" + Set-Content -Path (Join-Path $BinDir "$command.cmd") -Value $launcher -Encoding ASCII +} + +$userPath = [Environment]::GetEnvironmentVariable("Path", "User") +if (($userPath -split ";") -notcontains $BinDir) { + if ($env:DEVSPACE_NO_MODIFY_PATH -eq "1") { + Write-Host "Add $BinDir to your PATH to run devspace." + } else { + [Environment]::SetEnvironmentVariable("Path", (@($BinDir, $userPath) | Where-Object { $_ }) -join ";", "User") + Write-Host "Added $BinDir to your user PATH (open a new terminal to pick it up)." + } +} +$env:Path = "$BinDir;$env:Path" + +$devspace = Join-Path $BinDir "devspace.cmd" +Write-Host "DevSpace $(& $devspace --version) is installed at $devspace" + +if ($env:DEVSPACE_NO_INIT -ne "1") { + Write-Host "" + & $devspace init + if ($LASTEXITCODE -ne 0) { Write-Host "Setup did not finish. Run ``devspace init`` to continue." } +} diff --git a/install/install.sh b/install/install.sh new file mode 100755 index 000000000..bcdcf2fa6 --- /dev/null +++ b/install/install.sh @@ -0,0 +1,190 @@ +#!/bin/sh +# DevSpace installer for macOS, Linux, and WSL. +# +# curl -fsSL https://raw.githubusercontent.com/Waishnav/devspace/main/install/install.sh | sh +# +# Uses a compatible Node already on PATH, otherwise downloads a checksum-verified Node 24 LTS for DevSpace alone. +# Installs @waishnav/devspace into a user-owned npm prefix (no sudo), writes `devspace` launchers that pin that +# Node, then runs `devspace init`. Rerun it to update. +# +# Environment overrides: +# DEVSPACE_VERSION npm version or dist-tag to install (default: latest) +# DEVSPACE_INSTALL_DIR runtime and package location (default: ~/.local/share/devspace/install) +# DEVSPACE_BIN_DIR launcher location (default: ~/.local/bin) +# DEVSPACE_NO_INIT=1 skip running `devspace init` afterwards +# DEVSPACE_NO_MODIFY_PATH=1 never edit shell profiles +# DEVSPACE_PACKAGE_SPEC npm spec to install instead, such as a local tarball (for testing) + +set -eu + +PACKAGE="@waishnav/devspace" +VERSION="${DEVSPACE_VERSION:-latest}" +PACKAGE_SPEC="${DEVSPACE_PACKAGE_SPEC:-$PACKAGE@$VERSION}" +INSTALL_DIR="${DEVSPACE_INSTALL_DIR:-$HOME/.local/share/devspace/install}" +BIN_DIR="${DEVSPACE_BIN_DIR:-$HOME/.local/bin}" +NODE_MAJOR=24 +# Must match package.json engines.node. +NODE_MIN="22.19.0" +NODE_MAX_MAJOR=27 + +info() { printf '%s\n' "$*"; } +fail() { printf 'error: %s\n' "$*" >&2; exit 1; } + +need() { command -v "$1" >/dev/null 2>&1 || fail "'$1' is required but not installed."; } + +# Succeeds when the given `node` binary satisfies NODE_MIN <= version < NODE_MAX_MAJOR. +node_is_compatible() { + "$1" -e ' + const [maj, min, patch] = process.versions.node.split(".").map(Number); + const [wantMaj, wantMin, wantPatch] = process.argv[1].split(".").map(Number); + const atLeast = maj > wantMaj || (maj === wantMaj && (min > wantMin || (min === wantMin && patch >= wantPatch))); + process.exit(atLeast && maj < Number(process.argv[2]) ? 0 : 1); + ' "$NODE_MIN" "$NODE_MAX_MAJOR" >/dev/null 2>&1 +} + +sha256() { + if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 + elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1 + else fail "sha256sum or shasum is required to verify the Node download." + fi +} + +detect_platform() { + case "$(uname -s)" in + Linux) os=linux ;; + Darwin) os=darwin ;; + MINGW* | MSYS* | CYGWIN*) fail "Use install.ps1 on Windows, or run this script inside WSL." ;; + *) fail "Unsupported OS: $(uname -s)" ;; + esac + + case "$(uname -m)" in + x86_64 | amd64) arch=x64 ;; + arm64 | aarch64) arch=arm64 ;; + *) fail "Unsupported CPU architecture: $(uname -m)" ;; + esac + + # Rosetta reports x64 on Apple silicon; prefer the native build. + if [ "$os" = darwin ] && [ "$arch" = x64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then + arch=arm64 + fi + + if [ "$os" = linux ] && { [ -f /etc/alpine-release ] || ldd --version 2>&1 | grep -qi musl; }; then + musl=1 + else + musl=0 + fi +} + +install_node() { + [ "$musl" = 0 ] || fail "nodejs.org has no musl build. Install Node $NODE_MIN+ with your package manager (apk add nodejs npm) and rerun." + need curl + need tar + + dist="https://nodejs.org/dist/latest-v$NODE_MAJOR.x" + tmp=$(mktemp -d) + trap 'rm -rf "$tmp"' EXIT + + info "Downloading Node $NODE_MAJOR LTS for $os-$arch..." + curl -fsSL "$dist/SHASUMS256.txt" -o "$tmp/SHASUMS256.txt" + file=$(awk -v suffix="-$os-$arch.tar.gz" 'substr($2, length($2) - length(suffix) + 1) == suffix { print $2; exit }' "$tmp/SHASUMS256.txt") + [ -n "$file" ] || fail "No Node $NODE_MAJOR build is published for $os-$arch." + expected=$(awk -v name="$file" '$2 == name { print $1; exit }' "$tmp/SHASUMS256.txt") + + curl -fsSL "$dist/$file" -o "$tmp/$file" + [ "$(sha256 "$tmp/$file")" = "$expected" ] || fail "Checksum mismatch for $file; refusing to install it." + + rm -rf "$INSTALL_DIR/node" + mkdir -p "$INSTALL_DIR/node" + tar -xzf "$tmp/$file" -C "$INSTALL_DIR/node" --strip-components=1 + NODE="$INSTALL_DIR/node/bin/node" + info "Installed ${file%.tar.gz} into $INSTALL_DIR/node" +} + +select_node() { + if [ -x "$INSTALL_DIR/node/bin/node" ] && node_is_compatible "$INSTALL_DIR/node/bin/node"; then + NODE="$INSTALL_DIR/node/bin/node" + elif command -v node >/dev/null 2>&1 && node_is_compatible "$(command -v node)"; then + NODE=$(command -v node) + else + if command -v node >/dev/null 2>&1; then + info "Found Node $(node --version), but DevSpace needs >=$NODE_MIN <$NODE_MAX_MAJOR." + fi + install_node + fi + + NODE_BIN_DIR=$(dirname "$NODE") + [ -x "$NODE_BIN_DIR/npm" ] || fail "npm was not found next to $NODE. Install npm for that Node and rerun." +} + +install_package() { + info "Installing $PACKAGE_SPEC with Node $("$NODE" --version)..." + mkdir -p "$INSTALL_DIR/npm" + # npm's shebang resolves `node` from PATH, so put the selected Node first. + # DevSpace and its dependencies ship prebuilt binaries, so no install scripts are needed. + PATH="$NODE_BIN_DIR:$PATH" "$NODE_BIN_DIR/npm" install --global --prefix "$INSTALL_DIR/npm" \ + --ignore-scripts --no-fund --no-audit --no-update-notifier --loglevel=error "$PACKAGE_SPEC" +} + +write_launchers() { + package_dir="$INSTALL_DIR/npm/lib/node_modules/$PACKAGE" + [ -f "$package_dir/bin/devspace.js" ] || fail "Install finished but $package_dir/bin/devspace.js is missing." + mkdir -p "$BIN_DIR" + + for command in devspace devspace-agentd; do + launcher="$BIN_DIR/$command" + # Pin the Node that installed DevSpace so a version manager switching `node` cannot break it. + cat >"$launcher" <&2 + exit 1 +fi +exec "$NODE" "$package_dir/bin/$command.js" "\$@" +EOF + chmod 755 "$launcher" + done +} + +ensure_path() { + case ":$PATH:" in *":$BIN_DIR:"*) return ;; esac + + if [ "${DEVSPACE_NO_MODIFY_PATH:-0}" = 1 ]; then + info "Add $BIN_DIR to your PATH to run devspace." + return + fi + + case "$(basename "${SHELL:-sh}")" in + zsh) profile="${ZDOTDIR:-$HOME}/.zshrc"; line="export PATH=\"$BIN_DIR:\$PATH\"" ;; + bash) if [ "$os" = darwin ]; then profile="$HOME/.bash_profile"; else profile="$HOME/.bashrc"; fi + line="export PATH=\"$BIN_DIR:\$PATH\"" ;; + fish) profile="$HOME/.config/fish/config.fish"; line="fish_add_path \"$BIN_DIR\"" ;; + *) profile="$HOME/.profile"; line="export PATH=\"$BIN_DIR:\$PATH\"" ;; + esac + + if ! grep -Fqs "$line" "$profile"; then + mkdir -p "$(dirname "$profile")" + printf '\n# DevSpace\n%s\n' "$line" >>"$profile" + info "Added $BIN_DIR to PATH in $profile (open a new shell to pick it up)." + fi +} + +run_init() { + if [ "${DEVSPACE_NO_INIT:-0}" = 1 ]; then return; fi + + # Under `curl | sh` stdin is the script itself, so setup reads answers from the terminal. + if [ -r /dev/tty ] && [ -w /dev/tty ]; then + info "" + "$BIN_DIR/devspace" init /dev/tty 2>&1 || info "Setup did not finish. Run \`devspace init\` to continue." + else + info "No terminal available; run \`devspace init\` to finish setup." + fi +} + +detect_platform +select_node +install_package +write_launchers +ensure_path +info "DevSpace $("$BIN_DIR/devspace" --version) is installed at $BIN_DIR/devspace" +run_init From 34a5ba4a5fca386faa7bbe941ed64fb518e79bb5 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:54:06 +0530 Subject: [PATCH 5/9] style: satisfy lint for the node:sqlite migration Use unsafeNarrow for node:sqlite row results instead of chained casts, avoid typeof in the warning filter, and add the required blank lines. Co-Authored-By: Claude Opus 5.5 --- bin/silence-sqlite-warning.js | 8 +++++--- src/cli.ts | 1 + src/db/sqlite.ts | 2 ++ src/local-agent-client.ts | 1 + src/local-agent-store.ts | 25 +++++++++++++------------ 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/bin/silence-sqlite-warning.js b/bin/silence-sqlite-warning.js index d5921bf4a..a3c1db657 100644 --- a/bin/silence-sqlite-warning.js +++ b/bin/silence-sqlite-warning.js @@ -4,9 +4,11 @@ const emitWarning = process.emitWarning; process.emitWarning = function (warning, ...args) { - const type = typeof args[0] === "string" ? args[0] : args[0]?.type; - const message = typeof warning === "string" ? warning : warning?.message; + // emitWarning(warning, type) and emitWarning(warning, { type }) are both valid call shapes. + const type = args[0]?.type ?? args[0]; + const message = String(warning?.message ?? warning); + + if (type === "ExperimentalWarning" && message.startsWith("SQLite")) return; - if (type === "ExperimentalWarning" && message?.startsWith("SQLite")) return; return emitWarning.call(process, warning, ...args); }; diff --git a/src/cli.ts b/src/cli.ts index 1e837b172..9dd793afe 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -83,6 +83,7 @@ type Command = | "version"; const require = createRequire(import.meta.url); + // SAFETY: package.json always declares engines.node; it is the single source for the supported range. const SUPPORTED_NODE_RANGE = (require("../package.json") as { engines: { node: string } }).engines.node; diff --git a/src/db/sqlite.ts b/src/db/sqlite.ts index f5b17f1cb..653176f17 100644 --- a/src/db/sqlite.ts +++ b/src/db/sqlite.ts @@ -26,6 +26,7 @@ export function transaction( try { const result = run(); database.exec(`release ${savepoint}`); + return result; } catch (error) { database.exec(`rollback to ${savepoint}`); @@ -39,6 +40,7 @@ export function transaction( try { const result = run(); database.exec("commit"); + return result; } catch (error) { if (database.isTransaction) database.exec("rollback"); diff --git a/src/local-agent-client.ts b/src/local-agent-client.ts index ef881f77c..078071344 100644 --- a/src/local-agent-client.ts +++ b/src/local-agent-client.ts @@ -703,6 +703,7 @@ export function spawnLocalAgentDaemon( const entrypoint = resolveDaemonEntrypoint(); const silenceSqliteWarning = new URL("../bin/silence-sqlite-warning.js", import.meta.url).href; + const child = spawn(process.execPath, [ ...daemonExecArgv(process.execArgv), "--import", diff --git a/src/local-agent-store.ts b/src/local-agent-store.ts index 88f07a641..8d0670481 100644 --- a/src/local-agent-store.ts +++ b/src/local-agent-store.ts @@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto"; import { resolve } from "node:path"; import { Result, type Result as BetterResult } from "better-result"; import { openDatabase, transaction, type DatabaseHandle } from "./db/client.js"; +import { unsafeNarrow } from "./unsafe-narrow.js"; import { AgentStoreError, isProgrammerDefect } from "./local-agent-errors.js"; import type { LocalAgentTurnEvent, LocalAgentTurnEventBody } from "./local-agent-events.js"; import { validateAgentId } from "./local-agent-id.js"; @@ -165,36 +166,36 @@ export class LocalAgentStore { if (scope.workspaceId && scope.workspaceRoot) { // SAFETY: The surrounding checks or construction already match this type. - rows = this.database.sqlite + rows = unsafeNarrow(this.database.sqlite .prepare( `select * from local_agent_sessions where workspace_id = ? and workspace_root = ? order by updated_at desc`, ) - .all(scope.workspaceId, resolve(scope.workspaceRoot)) as unknown as LocalAgentRow[]; + .all(scope.workspaceId, resolve(scope.workspaceRoot))); } else if (scope.workspaceId) { // SAFETY: The surrounding checks or construction already match this type. - rows = this.database.sqlite + rows = unsafeNarrow(this.database.sqlite .prepare( `select * from local_agent_sessions where workspace_id = ? order by updated_at desc`, ) - .all(scope.workspaceId) as unknown as LocalAgentRow[]; + .all(scope.workspaceId)); } else if (scope.workspaceRoot) { // SAFETY: The surrounding checks or construction already match this type. - rows = this.database.sqlite + rows = unsafeNarrow(this.database.sqlite .prepare( `select * from local_agent_sessions where workspace_root = ? order by updated_at desc`, ) - .all(resolve(scope.workspaceRoot)) as unknown as LocalAgentRow[]; + .all(resolve(scope.workspaceRoot))); } else { // SAFETY: The surrounding checks or construction already match this type. - rows = this.database.sqlite + rows = unsafeNarrow(this.database.sqlite .prepare("select * from local_agent_sessions order by updated_at desc") - .all() as unknown as LocalAgentRow[]; + .all()); } return rows.map(rowToLocalAgentRecord); @@ -582,13 +583,13 @@ export class LocalAgentStore { listTurnEvents(turn: LocalAgentTurnRecord, afterSeq = 0): LocalAgentTurnEvent[] { // SAFETY: The surrounding checks or construction already match this type. - const rows = this.database.sqlite + const rows = unsafeNarrow(this.database.sqlite .prepare( `select seq, type, data, created_at from local_agent_turn_events where turn_row_id = ? and seq > ? order by seq asc`, ) - .all(turn.id, afterSeq) as unknown as LocalAgentTurnEventRow[]; + .all(turn.id, afterSeq)); return rows.map((row) => ({ // SAFETY: The surrounding checks or construction already match this type. @@ -623,9 +624,9 @@ export class LocalAgentStore { listTurns(agentId: string): LocalAgentTurnRecord[] { // SAFETY: The surrounding checks or construction already match this type. - const rows = this.database.sqlite + const rows = unsafeNarrow(this.database.sqlite .prepare("select * from local_agent_turns where agent_id = ? order by id asc") - .all(agentId) as unknown as LocalAgentTurnRow[]; + .all(agentId)); return rows.map(rowToLocalAgentTurnRecord); } From 5715d89354d28e9022006730f495136dcde5ad93 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:25:03 +0530 Subject: [PATCH 6/9] fix(install): harden launchers against shims and unusual paths Address review findings on the installers: - Pin the real Node binary (process.execPath) instead of a mise, asdf, or Volta shim, which would re-resolve to whatever version is active. - Single-quote paths written into the sh launchers and shell profiles so $, quotes, and spaces stay literal; fish gets its own quoting. - On Windows, write .cmd paths under the profile folders through %LOCALAPPDATA%, %APPDATA%, or %USERPROFILE% so non-ASCII usernames survive cmd.exe's code page, escape literal %, and fail clearly for other non-ASCII paths. - Fail the install when the new launcher cannot report its version. - Preload the node:sqlite warning filter for pnpm dev and npm start too. Co-Authored-By: Claude Opus 5.5 --- install/install.ps1 | 35 ++++++++++++++++++++++++++++++----- install/install.sh | 29 +++++++++++++++++++---------- package.json | 2 +- scripts/dev.ts | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/install/install.ps1 b/install/install.ps1 index 39837dec5..30ff704b7 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -67,12 +67,33 @@ function Select-Node { if ((Test-Path $managed) -and (Test-CompatibleNode $managed)) { return $managed } $system = Get-Command node -ErrorAction SilentlyContinue - if ($system -and (Test-CompatibleNode $system.Source)) { return $system.Source } + # Resolve through version-manager shims so the launcher pins this Node, not the shim. + if ($system -and (Test-CompatibleNode $system.Source)) { return (& $system.Source -p "process.execPath") } if ($system) { Write-Host "Found Node $(& $system.Source --version), but DevSpace needs >=$NodeMin <$NodeMaxMajor." } return Install-Node } +# cmd.exe reads .cmd files in the console code page, so a launcher cannot embed non-ASCII text literally. +# Spell paths under the user's profile folders through their environment variables instead, which cmd expands +# with full Unicode, and keep any literal % from being expanded. +function ConvertTo-CmdPath([string]$Path) { + $prefix = "" + $rest = $Path + foreach ($name in @("LOCALAPPDATA", "APPDATA", "USERPROFILE")) { + $root = [Environment]::GetEnvironmentVariable($name) + if ($root -and $Path.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) { + $prefix = "%$name%" + $rest = $Path.Substring($root.Length) + break + } + } + if ($rest -match "[^\x00-\x7F]") { + throw "Cannot write a launcher for the non-ASCII path $Path. Set DEVSPACE_INSTALL_DIR and DEVSPACE_BIN_DIR to ASCII paths and rerun." + } + return $prefix + $rest.Replace("%", "%%") +} + $Node = Select-Node $NodeDir = Split-Path $Node $Npm = Join-Path $NodeDir "npm.cmd" @@ -91,17 +112,19 @@ $packageDir = Join-Path $prefix "node_modules\$Package" if (-not (Test-Path (Join-Path $packageDir "bin\devspace.js"))) { throw "Install finished but $packageDir\bin\devspace.js is missing." } New-Item -ItemType Directory -Path $BinDir -Force | Out-Null +$cmdNode = ConvertTo-CmdPath $Node foreach ($command in @("devspace", "devspace-agentd")) { + $cmdEntry = ConvertTo-CmdPath (Join-Path $packageDir "bin\$command.js") # Pin the Node that installed DevSpace so a version manager switching `node` cannot break it. # goto instead of an if (...) block: paths such as "Program Files (x86)" would close the block early. $launcher = @( "@echo off", "rem Generated by the DevSpace installer. Rerun the installer to update.", - "if not exist `"$Node`" goto missing", - "`"$Node`" `"$packageDir\bin\$command.js`" %*", + "if not exist `"$cmdNode`" goto missing", + "`"$cmdNode`" `"$cmdEntry`" %*", "exit /b %errorlevel%", ":missing", - "echo DevSpace's Node runtime is missing: $Node 1>&2", + "echo DevSpace's Node runtime is missing: $cmdNode 1>&2", "echo Rerun the DevSpace installer. 1>&2", "exit /b 1" ) -join "`r`n" @@ -120,7 +143,9 @@ if (($userPath -split ";") -notcontains $BinDir) { $env:Path = "$BinDir;$env:Path" $devspace = Join-Path $BinDir "devspace.cmd" -Write-Host "DevSpace $(& $devspace --version) is installed at $devspace" +$version = & $devspace --version +if ($LASTEXITCODE -ne 0) { throw "The installed launcher at $devspace did not run." } +Write-Host "DevSpace $version is installed at $devspace" if ($env:DEVSPACE_NO_INIT -ne "1") { Write-Host "" diff --git a/install/install.sh b/install/install.sh index bcdcf2fa6..6a3475939 100755 --- a/install/install.sh +++ b/install/install.sh @@ -32,6 +32,9 @@ fail() { printf 'error: %s\n' "$*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || fail "'$1' is required but not installed."; } +# Single-quotes a value for POSIX sh, so generated files keep paths containing $, `, ", or spaces literal. +shell_quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; } + # Succeeds when the given `node` binary satisfies NODE_MIN <= version < NODE_MAX_MAJOR. node_is_compatible() { "$1" -e ' @@ -104,7 +107,8 @@ select_node() { if [ -x "$INSTALL_DIR/node/bin/node" ] && node_is_compatible "$INSTALL_DIR/node/bin/node"; then NODE="$INSTALL_DIR/node/bin/node" elif command -v node >/dev/null 2>&1 && node_is_compatible "$(command -v node)"; then - NODE=$(command -v node) + # Resolve through version-manager shims (mise, asdf, Volta) so the launcher pins this Node, not the shim. + NODE=$(node -p process.execPath) else if command -v node >/dev/null 2>&1; then info "Found Node $(node --version), but DevSpace needs >=$NODE_MIN <$NODE_MAX_MAJOR." @@ -136,11 +140,12 @@ write_launchers() { cat >"$launcher" <&2 +node=$(shell_quote "$NODE") +if [ ! -x "\$node" ]; then + echo "DevSpace's Node runtime (\$node) is missing. Rerun the DevSpace installer." >&2 exit 1 fi -exec "$NODE" "$package_dir/bin/$command.js" "\$@" +exec "\$node" $(shell_quote "$package_dir/bin/$command.js") "\$@" EOF chmod 755 "$launcher" done @@ -154,12 +159,15 @@ ensure_path() { return fi + line="export PATH=$(shell_quote "$BIN_DIR"):\"\$PATH\"" + case "$(basename "${SHELL:-sh}")" in - zsh) profile="${ZDOTDIR:-$HOME}/.zshrc"; line="export PATH=\"$BIN_DIR:\$PATH\"" ;; - bash) if [ "$os" = darwin ]; then profile="$HOME/.bash_profile"; else profile="$HOME/.bashrc"; fi - line="export PATH=\"$BIN_DIR:\$PATH\"" ;; - fish) profile="$HOME/.config/fish/config.fish"; line="fish_add_path \"$BIN_DIR\"" ;; - *) profile="$HOME/.profile"; line="export PATH=\"$BIN_DIR:\$PATH\"" ;; + zsh) profile="${ZDOTDIR:-$HOME}/.zshrc" ;; + bash) if [ "$os" = darwin ]; then profile="$HOME/.bash_profile"; else profile="$HOME/.bashrc"; fi ;; + fish) profile="$HOME/.config/fish/config.fish" + # fish single quotes escape only backslash and the quote itself. + line="fish_add_path '$(printf '%s' "$BIN_DIR" | sed "s/[\\\\']/\\\\&/g")'" ;; + *) profile="$HOME/.profile" ;; esac if ! grep -Fqs "$line" "$profile"; then @@ -186,5 +194,6 @@ select_node install_package write_launchers ensure_path -info "DevSpace $("$BIN_DIR/devspace" --version) is installed at $BIN_DIR/devspace" +version=$("$BIN_DIR/devspace" --version) || fail "The installed launcher at $BIN_DIR/devspace did not run." +info "DevSpace $version is installed at $BIN_DIR/devspace" run_init diff --git a/package.json b/package.json index 228e3abd4..1fa9a2718 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dev:reset": "tsx scripts/dev-state.ts reset", "prepack": "pnpm build", "schema:config": "tsx scripts/generate-config-schema.ts", - "start": "node dist/cli.js serve", + "start": "node --import ./bin/silence-sqlite-warning.js dist/cli.js serve", "test": "tsx --import ./bin/silence-sqlite-warning.js --test --test-concurrency=1 \"src/**/*.test.ts\"", "test:package-install": "tsx --test --test-concurrency=1 \"test/package-install-smoke.test.ts\"", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p scripts/qa/tsconfig.json && tsc -p test/agents-replay/tsconfig.json", diff --git a/scripts/dev.ts b/scripts/dev.ts index a8f2ef06e..6c1300806 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -10,7 +10,7 @@ if (!hasConfig) { console.error("Development state is not initialized. Run `pnpm dev:seed` first."); process.exitCode = 1; } else { - const child = spawn("tsx", ["watch", "--clear-screen=false", "src/cli.ts", "serve"], { + const child = spawn("tsx", ["watch", "--clear-screen=false", "--import", "./bin/silence-sqlite-warning.js", "src/cli.ts", "serve"], { cwd: checkoutRoot, env: { ...process.env, From 32b15c746e0b7cc9492d3acb358ec45b1d82e34a Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:38:42 +0530 Subject: [PATCH 7/9] fix(install): keep Homebrew's Node path and default to the beta tag Resolving every Node through process.execPath pinned Homebrew's Cellar keg, which brew upgrade deletes. Only resolve version-manager shims (mise/asdf/nodenv shims, Volta) and keep any other Node path as found. Default to the beta dist-tag: npm latest is 1.0.8, which still needs better-sqlite3's install script and so cannot be installed with --ignore-scripts. Switch back to latest when v1.1 ships as stable. On Windows, quote the Node path in the missing-runtime message so a & cannot end the echo, and explain how to fix a non-ASCII Node path. Co-Authored-By: Claude Opus 5.5 --- install/install.ps1 | 17 +++++++++++------ install/install.sh | 15 +++++++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/install/install.ps1 b/install/install.ps1 index 30ff704b7..5bf0a586b 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -6,14 +6,16 @@ # for DevSpace alone, installs @waishnav/devspace into a user-owned npm prefix, writes `devspace` launchers that # pin that Node, then runs `devspace init`. Rerun it to update. DevSpace itself also needs Git Bash on Windows. # -# Environment overrides: DEVSPACE_VERSION, DEVSPACE_INSTALL_DIR, DEVSPACE_BIN_DIR, DEVSPACE_NO_INIT=1, +# Environment overrides: DEVSPACE_VERSION (default: beta), DEVSPACE_INSTALL_DIR, DEVSPACE_BIN_DIR, DEVSPACE_NO_INIT=1, # DEVSPACE_NO_MODIFY_PATH=1, DEVSPACE_PACKAGE_SPEC (see install.sh). $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $Package = "@waishnav/devspace" -$Version = if ($env:DEVSPACE_VERSION) { $env:DEVSPACE_VERSION } else { "latest" } +# v1.1 is the first release installable without install scripts, and it is still on the beta tag. +# Switch this to latest once v1.1 ships as stable. +$Version = if ($env:DEVSPACE_VERSION) { $env:DEVSPACE_VERSION } else { "beta" } $PackageSpec = if ($env:DEVSPACE_PACKAGE_SPEC) { $env:DEVSPACE_PACKAGE_SPEC } else { "$Package@$Version" } $InstallDir = if ($env:DEVSPACE_INSTALL_DIR) { $env:DEVSPACE_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "devspace\install" } $BinDir = if ($env:DEVSPACE_BIN_DIR) { $env:DEVSPACE_BIN_DIR } else { Join-Path $env:LOCALAPPDATA "devspace\bin" } @@ -67,8 +69,11 @@ function Select-Node { if ((Test-Path $managed) -and (Test-CompatibleNode $managed)) { return $managed } $system = Get-Command node -ErrorAction SilentlyContinue - # Resolve through version-manager shims so the launcher pins this Node, not the shim. - if ($system -and (Test-CompatibleNode $system.Source)) { return (& $system.Source -p "process.execPath") } + if ($system -and (Test-CompatibleNode $system.Source)) { + # A Volta shim re-resolves on every run, so pin the Node behind it; anything else stays as found. + if ($system.Source -match "\\Volta\\") { return (& $system.Source -p "process.execPath") } + return $system.Source + } if ($system) { Write-Host "Found Node $(& $system.Source --version), but DevSpace needs >=$NodeMin <$NodeMaxMajor." } return Install-Node @@ -89,7 +94,7 @@ function ConvertTo-CmdPath([string]$Path) { } } if ($rest -match "[^\x00-\x7F]") { - throw "Cannot write a launcher for the non-ASCII path $Path. Set DEVSPACE_INSTALL_DIR and DEVSPACE_BIN_DIR to ASCII paths and rerun." + throw "Cannot write a launcher for $Path because cmd.exe cannot read its non-ASCII characters. If it is a Node runtime, move that Node to an ASCII path or remove it from PATH so the installer downloads its own; otherwise set DEVSPACE_INSTALL_DIR and DEVSPACE_BIN_DIR to ASCII paths. Then rerun." } return $prefix + $rest.Replace("%", "%%") } @@ -124,7 +129,7 @@ foreach ($command in @("devspace", "devspace-agentd")) { "`"$cmdNode`" `"$cmdEntry`" %*", "exit /b %errorlevel%", ":missing", - "echo DevSpace's Node runtime is missing: $cmdNode 1>&2", + "echo DevSpace's Node runtime is missing: `"$cmdNode`" 1>&2", "echo Rerun the DevSpace installer. 1>&2", "exit /b 1" ) -join "`r`n" diff --git a/install/install.sh b/install/install.sh index 6a3475939..e2191aa0b 100755 --- a/install/install.sh +++ b/install/install.sh @@ -8,7 +8,7 @@ # Node, then runs `devspace init`. Rerun it to update. # # Environment overrides: -# DEVSPACE_VERSION npm version or dist-tag to install (default: latest) +# DEVSPACE_VERSION npm version or dist-tag to install (default: beta) # DEVSPACE_INSTALL_DIR runtime and package location (default: ~/.local/share/devspace/install) # DEVSPACE_BIN_DIR launcher location (default: ~/.local/bin) # DEVSPACE_NO_INIT=1 skip running `devspace init` afterwards @@ -18,7 +18,9 @@ set -eu PACKAGE="@waishnav/devspace" -VERSION="${DEVSPACE_VERSION:-latest}" +# v1.1 is the first release installable without install scripts, and it is still on the beta tag. +# Switch this to latest once v1.1 ships as stable. +VERSION="${DEVSPACE_VERSION:-beta}" PACKAGE_SPEC="${DEVSPACE_PACKAGE_SPEC:-$PACKAGE@$VERSION}" INSTALL_DIR="${DEVSPACE_INSTALL_DIR:-$HOME/.local/share/devspace/install}" BIN_DIR="${DEVSPACE_BIN_DIR:-$HOME/.local/bin}" @@ -107,8 +109,13 @@ select_node() { if [ -x "$INSTALL_DIR/node/bin/node" ] && node_is_compatible "$INSTALL_DIR/node/bin/node"; then NODE="$INSTALL_DIR/node/bin/node" elif command -v node >/dev/null 2>&1 && node_is_compatible "$(command -v node)"; then - # Resolve through version-manager shims (mise, asdf, Volta) so the launcher pins this Node, not the shim. - NODE=$(node -p process.execPath) + NODE=$(command -v node) + + # A version-manager shim (mise, asdf, nodenv, Volta) re-resolves on every run, so pin the Node behind it. + # Anything else stays as found: Homebrew's bin/node symlink survives upgrades, its Cellar target does not. + case "$NODE" in + */shims/node | */.volta/bin/node) NODE=$("$NODE" -p process.execPath) ;; + esac else if command -v node >/dev/null 2>&1; then info "Found Node $(node --version), but DevSpace needs >=$NODE_MIN <$NODE_MAX_MAJOR." From 13a0b48489ab421d2224e0d735b1320e38618f53 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:50:43 +0530 Subject: [PATCH 8/9] fix(install): pin the real Node except where upgrades delete it Special-casing shims kept missing version managers (fnm's multishell links, nvm-windows' NVM_SYMLINK). Invert the rule: pin the real binary, since version managers keep each installed version on disk, and keep the stable link only for Homebrew (Cellar) and Nix (/nix/store), whose upgrades delete old versions. On Windows, follow nvm-windows' symlinked directory explicitly rather than relying on process.execPath. Co-Authored-By: Claude Opus 5.5 --- install/install.ps1 | 8 +++++--- install/install.sh | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/install/install.ps1 b/install/install.ps1 index 5bf0a586b..7d90ac96c 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -70,9 +70,11 @@ function Select-Node { $system = Get-Command node -ErrorAction SilentlyContinue if ($system -and (Test-CompatibleNode $system.Source)) { - # A Volta shim re-resolves on every run, so pin the Node behind it; anything else stays as found. - if ($system.Source -match "\\Volta\\") { return (& $system.Source -p "process.execPath") } - return $system.Source + # Pin the real binary, since version managers switch what `node` points to but keep each version on disk. + # nvm-windows repoints a symlinked directory, which process.execPath may report unresolved, so follow it. + $dir = Get-Item -LiteralPath (Split-Path $system.Source) + if ($dir.LinkType -and $dir.Target) { return Join-Path @($dir.Target)[0] (Split-Path $system.Source -Leaf) } + return (& $system.Source -p "process.execPath") } if ($system) { Write-Host "Found Node $(& $system.Source --version), but DevSpace needs >=$NodeMin <$NodeMaxMajor." } diff --git a/install/install.sh b/install/install.sh index e2191aa0b..994ccd6f2 100755 --- a/install/install.sh +++ b/install/install.sh @@ -109,12 +109,12 @@ select_node() { if [ -x "$INSTALL_DIR/node/bin/node" ] && node_is_compatible "$INSTALL_DIR/node/bin/node"; then NODE="$INSTALL_DIR/node/bin/node" elif command -v node >/dev/null 2>&1 && node_is_compatible "$(command -v node)"; then - NODE=$(command -v node) - - # A version-manager shim (mise, asdf, nodenv, Volta) re-resolves on every run, so pin the Node behind it. - # Anything else stays as found: Homebrew's bin/node symlink survives upgrades, its Cellar target does not. + # Pin the real binary: version managers (nvm, fnm, mise, asdf, Volta) switch what `node` points to but + # keep every installed version on disk. Homebrew and Nix delete old versions on upgrade instead, so for + # them keep the stable link that `command -v` found. + NODE=$(node -p process.execPath) case "$NODE" in - */shims/node | */.volta/bin/node) NODE=$("$NODE" -p process.execPath) ;; + */Cellar/* | /nix/store/*) NODE=$(command -v node) ;; esac else if command -v node >/dev/null 2>&1; then From 2c6c92a9d798ee43aeb27af9713f45e9d691bbaf Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:59:13 +0530 Subject: [PATCH 9/9] fix(install): resolve relative Node directory links from their parent Co-Authored-By: Claude Opus 5.5 --- install/install.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/install/install.ps1 b/install/install.ps1 index 7d90ac96c..b28ee957d 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -73,7 +73,11 @@ function Select-Node { # Pin the real binary, since version managers switch what `node` points to but keep each version on disk. # nvm-windows repoints a symlinked directory, which process.execPath may report unresolved, so follow it. $dir = Get-Item -LiteralPath (Split-Path $system.Source) - if ($dir.LinkType -and $dir.Target) { return Join-Path @($dir.Target)[0] (Split-Path $system.Source -Leaf) } + if ($dir.LinkType -and $dir.Target) { + $target = @($dir.Target)[0] + if (-not [IO.Path]::IsPathRooted($target)) { $target = Join-Path (Split-Path $dir.FullName) $target } + return Join-Path $target (Split-Path $system.Source -Leaf) + } return (& $system.Source -p "process.execPath") } if ($system) { Write-Host "Found Node $(& $system.Source --version), but DevSpace needs >=$NodeMin <$NodeMaxMajor." }