diff --git a/.agents/skills/gen-docs/SKILL.md b/.agents/skills/gen-docs/SKILL.md index 9f39ece2..9980d133 100644 --- a/.agents/skills/gen-docs/SKILL.md +++ b/.agents/skills/gen-docs/SKILL.md @@ -7,7 +7,7 @@ description: Update Pythinker Code CLI user documentation after meaningful code ## Overview -This repository maintains English user documentation under `docs/`. +This repository (`github.com/Pythoughts-labs/pythinker-code`) maintains English user documentation under `docs/`, published at **https://code.pythinker.com**. Use this skill to update the corresponding documentation whenever the codebase has changes that affect product behavior or user experience. @@ -17,7 +17,7 @@ For a **full pre-release audit** of all pages (detecting hallucinations and cove This skill depends on the following being in place. If any are missing, stop and report to the user before continuing: -- `docs/` directory with documentation pages and `docs/.vitepress/config.ts` set up (VitePress site). +- `docs/` directory with documentation pages and `docs/.vitepress/config.ts` set up (VitePress site, deployed to code.pythinker.com). - `docs/AGENTS.md` style guide — defines terminology, typography, and writing style. ## Workflow diff --git a/.agents/skills/sync-changelog/SKILL.md b/.agents/skills/sync-changelog/SKILL.md index 52998b52..55d38a0f 100644 --- a/.agents/skills/sync-changelog/SKILL.md +++ b/.agents/skills/sync-changelog/SKILL.md @@ -15,7 +15,7 @@ apps/pythinker-code/CHANGELOG.md This file is the **only upstream source** for the documentation-site changelog. Internal package changelogs such as `packages/*/CHANGELOG.md` do not go into the documentation site. -After the release flow finishes (Release PR merged → `Version Packages` completed → npm publish succeeded), maintainers manually run this skill to copy the new CLI changelog entries into the docs site. +After the release flow finishes (Release PR merged → `Version Packages` completed → npm publish succeeded), maintainers manually run this skill to copy the new CLI changelog entries into the docs site (published at https://code.pythinker.com). ## When To Use @@ -38,7 +38,7 @@ Core rule: the English docs changelog is the source of truth for user-facing rel Before editing, confirm: -- The released version exists on npm (`npm view @pythoughts/pythinker-code versions --json`) or has a matching GitHub Release tag. +- The released version exists on npm (`npm view @pythoughts/pythinker-code versions --json`) or has a matching GitHub Release tag on `Pythoughts-labs/pythinker-code`. - The top of `apps/pythinker-code/CHANGELOG.md` is that new version. - The current branch is clean, or you are on a dedicated docs-sync branch. @@ -66,7 +66,7 @@ Use upstream order: newest version first. Upstream entries look like this: ```markdown -- [#317](https://github.com/...) [`2f51db4`](https://github.com/...) - Clean up lint warnings ... +- [#317](https://github.com/Pythoughts-labs/pythinker-code/pull/317) [`2f51db4`](https://github.com/Pythoughts-labs/pythinker-code/commit/2f51db4) - Clean up lint warnings ... ``` Keep: diff --git a/.changeset/windows-launcher-execve.md b/.changeset/windows-launcher-execve.md new file mode 100644 index 00000000..6e4dcd71 --- /dev/null +++ b/.changeset/windows-launcher-execve.md @@ -0,0 +1,5 @@ +--- +'@pythoughts/pythinker-code': patch +--- + +Fix the CLI failing to start on Windows with "process.execve is unavailable" by using the spawn fallback instead of calling execve there. diff --git a/apps/pythinker-code/src/launcher.ts b/apps/pythinker-code/src/launcher.ts index 3ef188d9..592ec291 100644 --- a/apps/pythinker-code/src/launcher.ts +++ b/apps/pythinker-code/src/launcher.ts @@ -116,17 +116,21 @@ async function launch(): Promise { ...process.env, [FFI_CHILD_ENV]: '1', }; + // On Windows, process.execve either does not exist or exists but throws + // ERR_FEATURE_UNAVAILABLE_ON_PLATFORM when called — checking for undefined + // is not enough, so always take the spawn fallback there. + if (process.platform === 'win32') { + launchWindowsFallback(nodeArguments, environment); + return; + } + // execve keeps the same pid, process group, session, and controlling // terminal, so Ctrl+C and job-control signals keep flowing to the app and // the child's process group stays the terminal's foreground group. - if (process.execve !== undefined) { - process.execve(process.execPath, [process.execPath, ...nodeArguments], environment); - } - - if (process.platform !== 'win32') { + if (process.execve === undefined) { throw new Error('process.execve is unavailable on this platform'); } - launchWindowsFallback(nodeArguments, environment); + process.execve(process.execPath, [process.execPath, ...nodeArguments], environment); } void launch().catch((error: unknown) => { diff --git a/apps/pythinker-code/test/cli/ffi-launcher.test.ts b/apps/pythinker-code/test/cli/ffi-launcher.test.ts index fc5ee267..4c40144d 100644 --- a/apps/pythinker-code/test/cli/ffi-launcher.test.ts +++ b/apps/pythinker-code/test/cli/ffi-launcher.test.ts @@ -160,6 +160,37 @@ describe('FFI launcher', () => { expect(details.pid === originalPid).toBe(process.platform !== 'win32'); }); + it('uses the spawn fallback on win32 even when process.execve exists but throws', async () => { + // Regression: Windows Node ships process.execve as a defined function that + // throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM when called. The launcher must + // route win32 to the spawn fallback without ever calling execve. + const patchPath = join(fixtureDir, 'patch-win32.mjs'); + await writeFile( + patchPath, + ` + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + process.execve = () => { + throw new Error('The feature process.execve is unavailable on the current platform'); + }; + `, + ); + await writeMain(` + process.stdout.write(JSON.stringify({ imported: true, marker: process.env.PYTHINKER_CODE_FFI_CHILD })); + `); + + const child = spawn( + process.execPath, + ['--import', tsxLoader, '--import', patchPath, launcherPath], + { cwd: fixtureDir, env: { ...process.env }, stdio: 'pipe' }, + ); + const result = await collect(child); + + expect(result.stderr).not.toContain('process.execve is unavailable'); + expect(result.code).toBe(0); + const details = JSON.parse(result.stdout) as { imported: boolean; marker: string }; + expect(details).toMatchObject({ imported: true, marker: '1' }); + }); + it.skipIf(process.platform === 'win32')( 'preserves the parent process group and session across execve', async () => {