From 046fe86fca5ffacb6cd90c6138e60d14381955c8 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 10 Sep 2026 15:40:15 +0200 Subject: [PATCH 1/7] fix: handle CLAUDE.md symlink on Windows Add CLAUDE.md to .prettierignore so Prettier skips the symlink target, and enable core.symlinks in the prepare script so Windows clones checkout the symlink correctly after pnpm install. Co-Authored-By: Claude Opus 4.6 (1M context) --- .prettierignore | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index 32405c6..5380627 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ pnpm-lock.yaml dist/ node_modules/ CHANGELOG.md +CLAUDE.md diff --git a/package.json b/package.json index 7525d4a..2e26108 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "typecheck": "tsc --noEmit", "qa": "pnpm typecheck && pnpm lint && pnpm test", "clear": "./scripts/clean-dev-env.sh", - "prepare": "husky" + "prepare": "git config --local core.symlinks true && husky" }, "dependencies": { "@inkjs/ui": "^2.0.0", From b15013d594eeb003b750afe8fd69e6ecc37cdedf Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 10 Sep 2026 15:42:15 +0200 Subject: [PATCH 2/7] fix: use fileURLToPath in vitest configs for Windows compatibility URL.pathname returns /C:/Users/... on Windows, breaking path alias resolution. fileURLToPath() handles this correctly cross-platform. Co-Authored-By: Claude Opus 4.6 (1M context) --- vitest.config.e2e.ts | 13 +++++++------ vitest.config.ts | 17 +++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index 0481f0b..393a2b6 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -1,14 +1,15 @@ +import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitest/config'; export default defineConfig({ resolve: { alias: { - '@commands': new URL('./src/commands', import.meta.url).pathname, - '@frameworks': new URL('./src/frameworks', import.meta.url).pathname, - '@integrations': new URL('./src/integrations', import.meta.url).pathname, - '@providers': new URL('./src/providers', import.meta.url).pathname, - '@ui': new URL('./src/ui', import.meta.url).pathname, - '@lib': new URL('./src/lib', import.meta.url).pathname, + '@commands': fileURLToPath(new URL('./src/commands', import.meta.url)), + '@frameworks': fileURLToPath(new URL('./src/frameworks', import.meta.url)), + '@integrations': fileURLToPath(new URL('./src/integrations', import.meta.url)), + '@providers': fileURLToPath(new URL('./src/providers', import.meta.url)), + '@ui': fileURLToPath(new URL('./src/ui', import.meta.url)), + '@lib': fileURLToPath(new URL('./src/lib', import.meta.url)), }, }, test: { diff --git a/vitest.config.ts b/vitest.config.ts index 7369bcf..338847d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,4 @@ +import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitest/config'; const isCI = !!process.env.CI; @@ -5,14 +6,14 @@ const isCI = !!process.env.CI; export default defineConfig({ resolve: { alias: { - '@commands': new URL('./src/commands', import.meta.url).pathname, - '@features': new URL('./src/features', import.meta.url).pathname, - '@frameworks': new URL('./src/frameworks', import.meta.url).pathname, - '@integrations': new URL('./src/integrations', import.meta.url).pathname, - '@providers': new URL('./src/providers', import.meta.url).pathname, - '@shared-kernel': new URL('./src/shared-kernel', import.meta.url).pathname, - '@ui': new URL('./src/ui', import.meta.url).pathname, - '@lib': new URL('./src/lib', import.meta.url).pathname, + '@commands': fileURLToPath(new URL('./src/commands', import.meta.url)), + '@features': fileURLToPath(new URL('./src/features', import.meta.url)), + '@frameworks': fileURLToPath(new URL('./src/frameworks', import.meta.url)), + '@integrations': fileURLToPath(new URL('./src/integrations', import.meta.url)), + '@providers': fileURLToPath(new URL('./src/providers', import.meta.url)), + '@shared-kernel': fileURLToPath(new URL('./src/shared-kernel', import.meta.url)), + '@ui': fileURLToPath(new URL('./src/ui', import.meta.url)), + '@lib': fileURLToPath(new URL('./src/lib', import.meta.url)), }, }, test: { From bb5e6d2a1b1c7c9df3c111b93f26d1b856985b72 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 10 Sep 2026 16:02:26 +0200 Subject: [PATCH 3/7] fix: use os.tmpdir() on Windows for test project directories /tmp/ doesn't exist on Windows. Use perPlatform() to pick os.tmpdir() on Windows while keeping /tmp/ on Unix for stable e2e snapshot column alignment. Co-Authored-By: Claude Opus 4.6 (1M context) --- __tests__/shared/platform.ts | 5 +++++ __tests__/shared/project-scaffold/index.ts | 23 +++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 __tests__/shared/platform.ts diff --git a/__tests__/shared/platform.ts b/__tests__/shared/platform.ts new file mode 100644 index 0000000..7dbb967 --- /dev/null +++ b/__tests__/shared/platform.ts @@ -0,0 +1,5 @@ +export const isWindows = process.platform === 'win32'; + +export function perPlatform(options: { windows: T; unix: T }): T { + return isWindows ? options.windows : options.unix; +} diff --git a/__tests__/shared/project-scaffold/index.ts b/__tests__/shared/project-scaffold/index.ts index a170981..4bed870 100644 --- a/__tests__/shared/project-scaffold/index.ts +++ b/__tests__/shared/project-scaffold/index.ts @@ -1,4 +1,7 @@ import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { perPlatform } from '../platform.js'; import { SCAFFOLDS } from './scaffolds.js'; import type { ProjectType } from './types.js'; @@ -9,10 +12,15 @@ export type { ProjectType }; * project scaffold. Supports `Symbol.dispose` for automatic cleanup. * * @remarks - * Uses a hardcoded `/tmp/` prefix instead of `os.tmpdir()`. On macOS - * `tmpdir()` returns `/var/folders/…` which is longer than Linux's `/tmp/`, - * shifting column alignment in the VT100 screen buffer and breaking e2e - * snapshot assertions across platforms. + * Uses `/tmp/` on Unix instead of `os.tmpdir()`. On macOS `tmpdir()` + * returns `/var/folders/…` which is longer than Linux's `/tmp/`, shifting + * column alignment in the VT100 screen buffer and breaking e2e snapshot + * assertions across platforms. On Windows, `/tmp/` doesn't exist so we + * fall back to `os.tmpdir()`. + * + * @todo The Windows `tmpdir()` path is longer than `/tmp/`, producing + * different VT100 column alignment and incompatible e2e snapshots. When + * Windows CI is added, use a fixed-length prefix on all platforms. * * @param type - A named scaffold, or `null` for an empty directory. * @defaultValue `'react'` @@ -26,7 +34,12 @@ export type { ProjectType }; * ``` */ export function createProjectDir(type: ProjectType = 'react') { - const dir = mkdtempSync('/tmp/wizard-test-'); + const prefix = perPlatform({ + windows: join(tmpdir(), 'wizard-test-'), + unix: '/tmp/wizard-test-', + }); + + const dir = mkdtempSync(prefix); SCAFFOLDS[type](dir); return { From a4934c95c19f5810ed3540e1a97e8a8d49319817 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 10 Sep 2026 16:11:42 +0200 Subject: [PATCH 4/7] fix: make e2e mock binaries work on Windows On Windows, scripts need a .cmd launcher instead of a Unix shebang and chmod. Split binary writing into writeUnixBinary/writeWindowsBinary in a new fs.ts module and add IS_WINDOWS to the e2e env constants. Co-Authored-By: Claude Opus 4.6 (1M context) --- __tests__/e2e/testing-framework/env.ts | 3 +++ .../testing-framework/mocks/binaries/fs.ts | 21 +++++++++++++++++++ .../testing-framework/mocks/binaries/index.ts | 12 ++++------- 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 __tests__/e2e/testing-framework/mocks/binaries/fs.ts diff --git a/__tests__/e2e/testing-framework/env.ts b/__tests__/e2e/testing-framework/env.ts index 4055bff..3678749 100644 --- a/__tests__/e2e/testing-framework/env.ts +++ b/__tests__/e2e/testing-framework/env.ts @@ -1,5 +1,8 @@ export { AUTH_CALLBACK_PORT } from '@lib/auth.js'; +/** `true` when the test suite is running on Windows. */ +export const IS_WINDOWS = process.platform === 'win32'; + /** * Baseline environment variables injected into every e2e terminal session. * diff --git a/__tests__/e2e/testing-framework/mocks/binaries/fs.ts b/__tests__/e2e/testing-framework/mocks/binaries/fs.ts new file mode 100644 index 0000000..86340dc --- /dev/null +++ b/__tests__/e2e/testing-framework/mocks/binaries/fs.ts @@ -0,0 +1,21 @@ +import { writeFileSync, chmodSync } from 'node:fs'; +import { join } from 'node:path'; +import { IS_WINDOWS } from '../../env.js'; + +export const writeMockBinary = IS_WINDOWS ? writeWindowsBinary : writeUnixBinary; + +function writeUnixBinary(dir: string, name: string, script: string): void { + const filePath = join(dir, name); + writeFileSync(filePath, script, 'utf-8'); + chmodSync(filePath, 0o755); +} + +function writeWindowsBinary(dir: string, name: string, script: string): void { + const jsPath = join(dir, `${name}.js`); + writeFileSync(jsPath, script.replace(/^#!.*\n/, ''), 'utf-8'); + writeFileSync(join(dir, `${name}.cmd`), `@node "%~dp0${name}.js" %*\r\n`, 'utf-8'); +} + +export const writeMockOpenStub = IS_WINDOWS + ? (dir: string) => writeFileSync(join(dir, 'open.cmd'), '@exit /b 0\r\n', 'utf-8') + : (dir: string) => writeUnixBinary(dir, 'open', '#!/bin/sh\nexit 0\n'); diff --git a/__tests__/e2e/testing-framework/mocks/binaries/index.ts b/__tests__/e2e/testing-framework/mocks/binaries/index.ts index 0a6a082..34be236 100644 --- a/__tests__/e2e/testing-framework/mocks/binaries/index.ts +++ b/__tests__/e2e/testing-framework/mocks/binaries/index.ts @@ -1,8 +1,9 @@ -import { writeFileSync, mkdirSync, chmodSync } from 'node:fs'; +import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { CLAUDE_SCRIPT } from './claude.js'; import { CURSOR_SCRIPT } from './cursor.js'; import { CODEX_SCRIPT } from './codex.js'; +import { writeMockBinary, writeMockOpenStub } from './fs.js'; /** Filename the mock IDE binary writes the chat prompt to. */ export const CHAT_PROMPT_FILE = '.e2e-chat-prompt'; @@ -10,12 +11,6 @@ export const CHAT_PROMPT_FILE = '.e2e-chat-prompt'; /** Filename the mock IDE binary writes the onboarding invocation JSON to. */ export const ONBOARDING_INVOCATION_FILE = '.e2e-onboarding-invocation'; -function writeMockBinary(dir: string, name: string, script: string): void { - const filePath = join(dir, name); - writeFileSync(filePath, script, 'utf-8'); - chmodSync(filePath, 0o755); -} - /** * Creates a directory of executable mock IDE binaries (`claude`, `cursor`, * `codex`, `open`) that the wizard will find on `PATH` during e2e tests. @@ -34,7 +29,8 @@ export function createMockBinDir(dir: string): string { writeMockBinary(binDir, 'claude', CLAUDE_SCRIPT); writeMockBinary(binDir, 'cursor', CURSOR_SCRIPT); writeMockBinary(binDir, 'codex', CODEX_SCRIPT); - writeMockBinary(binDir, 'open', '#!/bin/sh\nexit 0\n'); + + writeMockOpenStub(binDir); return binDir; } From dabf0057381739398d028f94184252865a8118dc Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 10 Sep 2026 16:17:18 +0200 Subject: [PATCH 5/7] fix: use path.delimiter for PATH separator in e2e sessions Windows uses ; not : as the PATH separator. Co-Authored-By: Claude Opus 4.6 (1M context) --- __tests__/e2e/testing-framework/session-factory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/e2e/testing-framework/session-factory.ts b/__tests__/e2e/testing-framework/session-factory.ts index d2c706f..e91af91 100644 --- a/__tests__/e2e/testing-framework/session-factory.ts +++ b/__tests__/e2e/testing-framework/session-factory.ts @@ -1,6 +1,6 @@ import { mkdtempSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; import { tmpdir } from 'node:os'; +import { delimiter, join } from 'node:path'; import { TerminalSession } from './terminal/index.js'; import { createProjectDir, type ProjectType } from '../../shared/project-scaffold/index.js'; @@ -56,7 +56,7 @@ export function createSession({ const { path: projectDir } = createProjectDir(project); const sessionEnv: Record = { - PATH: `${mockBinDir}:${systemPath ?? process.env.PATH}`, + PATH: `${mockBinDir}${delimiter}${systemPath ?? process.env.PATH}`, ...env, }; From a651a07116454a8b6b04e613048e6ce038aea8e2 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 10 Sep 2026 16:20:49 +0200 Subject: [PATCH 6/7] fix: set platform-correct home and temp env vars in e2e sessions Windows uses USERPROFILE/TEMP/TMP instead of HOME/TMPDIR. Set the Windows equivalents alongside the Unix vars so both are available. Co-Authored-By: Claude Opus 4.6 (1M context) --- __tests__/e2e/testing-framework/terminal/session.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/__tests__/e2e/testing-framework/terminal/session.ts b/__tests__/e2e/testing-framework/terminal/session.ts index 50d12fa..30dea01 100644 --- a/__tests__/e2e/testing-framework/terminal/session.ts +++ b/__tests__/e2e/testing-framework/terminal/session.ts @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { stripAnsi } from './strip-ansi.js'; import { renderScreen, normalizeSnapshot } from './screen-buffer.js'; -import { E2E_BASE_ENV } from '../env.js'; +import { E2E_BASE_ENV, IS_WINDOWS } from '../env.js'; import { resolveKey, type Modifiers } from '../../../shared/key-map.js'; const CLI_PATH = resolve(import.meta.dirname, '../../../../dist/bin/cli.js'); @@ -85,8 +85,15 @@ export class TerminalSession { ...process.env, ...E2E_BASE_ENV, ...env, + HOME: isolatedTmpDir, TMPDIR: isolatedTmpDir, + + ...(IS_WINDOWS && { + USERPROFILE: isolatedTmpDir, + TEMP: isolatedTmpDir, + TMP: isolatedTmpDir, + }), }, }); From 3f27bbee53d67b42fa4ebab8beb9b3438847a5e8 Mon Sep 17 00:00:00 2001 From: Alex Bespoyasov Date: Thu, 10 Sep 2026 16:22:32 +0200 Subject: [PATCH 7/7] fix: skip /usr/bin system-check e2e test on Windows The test relies on /usr/bin as a PATH with git but not node, which has no portable Windows equivalent. Other system-check tests already validate the failure UI. Co-Authored-By: Claude Opus 4.6 (1M context) --- __tests__/e2e/system-check-failure.e2e.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/__tests__/e2e/system-check-failure.e2e.ts b/__tests__/e2e/system-check-failure.e2e.ts index 9defccf..be58b24 100644 --- a/__tests__/e2e/system-check-failure.e2e.ts +++ b/__tests__/e2e/system-check-failure.e2e.ts @@ -1,4 +1,5 @@ import { createSession } from './testing-framework/index.js'; +import { IS_WINDOWS } from './testing-framework/env.js'; import { dirname } from 'node:path'; describe('when system check fails', () => { @@ -18,8 +19,13 @@ describe('when system check fails', () => { expect(session.snapshot()).toMatchSnapshot('system-check-failure'); }); - it('shows error when node is not on PATH', async () => { - // PATH with git but not node — CLI still runs via absolute path + /** + * @todo Make this test work on Windows. It currently relies on `/usr/bin` + * as a PATH that contains git but not node, which has no direct Windows + * equivalent. Constructing one portably is non-trivial because git's + * install location varies across Windows setups. + */ + it.skipIf(IS_WINDOWS)('shows error when node is not on PATH', async () => { using session = createSession({ systemPath: '/usr/bin' }); await session.waitForText('Start setup');