Skip to content

Commit 9b1b195

Browse files
authored
fix: CLI fails to start on Windows with process.execve unavailable (#8)
## Related Issue No linked issue — problem explained below. ## Problem On Windows, `pythinker` installed via npm fails immediately: ``` Failed to start Pythinker Code: The feature process.execve is unavailable on the current platform, which is being used to run Node.js ``` Windows Node defines `process.execve` as a function that throws `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` when called. The launcher's existence check (`process.execve !== undefined`) therefore routed Windows into the execve path and crashed before the existing spawn fallback could run. ## What changed The launcher checks the platform first: win32 always uses the spawn fallback; the execve path (which preserves pid, process group, and controlling terminal) remains for POSIX. Added a regression test that fakes win32 with a throwing `execve` and asserts the fallback child completes — verified it fails against the previous launcher. ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/Pythoughts-labs/pythinker-code/blob/main/CONTRIBUTING.md) document. - [x] I have linked a related issue, or explained the problem above. - [x] I have added tests that prove my feature works. - [x] Ran `gen-changesets` skill, or this PR needs no changeset. - [x] Ran `gen-docs` skill, or this PR needs no doc update.
1 parent d396320 commit 9b1b195

5 files changed

Lines changed: 51 additions & 11 deletions

File tree

.agents/skills/gen-docs/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ description: Update Pythinker Code CLI user documentation after meaningful code
77

88
## Overview
99

10-
This repository maintains English user documentation under `docs/`.
10+
This repository (`github.com/Pythoughts-labs/pythinker-code`) maintains English user documentation under `docs/`, published at **https://code.pythinker.com**.
1111

1212
Use this skill to update the corresponding documentation whenever the codebase has changes that affect product behavior or user experience.
1313

@@ -17,7 +17,7 @@ For a **full pre-release audit** of all pages (detecting hallucinations and cove
1717

1818
This skill depends on the following being in place. If any are missing, stop and report to the user before continuing:
1919

20-
- `docs/` directory with documentation pages and `docs/.vitepress/config.ts` set up (VitePress site).
20+
- `docs/` directory with documentation pages and `docs/.vitepress/config.ts` set up (VitePress site, deployed to code.pythinker.com).
2121
- `docs/AGENTS.md` style guide — defines terminology, typography, and writing style.
2222

2323
## Workflow

.agents/skills/sync-changelog/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ apps/pythinker-code/CHANGELOG.md
1515

1616
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.
1717

18-
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.
18+
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).
1919

2020
## When To Use
2121

@@ -38,7 +38,7 @@ Core rule: the English docs changelog is the source of truth for user-facing rel
3838

3939
Before editing, confirm:
4040

41-
- The released version exists on npm (`npm view @pythoughts/pythinker-code versions --json`) or has a matching GitHub Release tag.
41+
- 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`.
4242
- The top of `apps/pythinker-code/CHANGELOG.md` is that new version.
4343
- The current branch is clean, or you are on a dedicated docs-sync branch.
4444

@@ -66,7 +66,7 @@ Use upstream order: newest version first.
6666
Upstream entries look like this:
6767

6868
```markdown
69-
- [#317](https://github.com/...) [`2f51db4`](https://github.com/...) - Clean up lint warnings ...
69+
- [#317](https://github.com/Pythoughts-labs/pythinker-code/pull/317) [`2f51db4`](https://github.com/Pythoughts-labs/pythinker-code/commit/2f51db4) - Clean up lint warnings ...
7070
```
7171

7272
Keep:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pythoughts/pythinker-code': patch
3+
---
4+
5+
Fix the CLI failing to start on Windows with "process.execve is unavailable" by using the spawn fallback instead of calling execve there.

apps/pythinker-code/src/launcher.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,21 @@ async function launch(): Promise<void> {
116116
...process.env,
117117
[FFI_CHILD_ENV]: '1',
118118
};
119+
// On Windows, process.execve either does not exist or exists but throws
120+
// ERR_FEATURE_UNAVAILABLE_ON_PLATFORM when called — checking for undefined
121+
// is not enough, so always take the spawn fallback there.
122+
if (process.platform === 'win32') {
123+
launchWindowsFallback(nodeArguments, environment);
124+
return;
125+
}
126+
119127
// execve keeps the same pid, process group, session, and controlling
120128
// terminal, so Ctrl+C and job-control signals keep flowing to the app and
121129
// the child's process group stays the terminal's foreground group.
122-
if (process.execve !== undefined) {
123-
process.execve(process.execPath, [process.execPath, ...nodeArguments], environment);
124-
}
125-
126-
if (process.platform !== 'win32') {
130+
if (process.execve === undefined) {
127131
throw new Error('process.execve is unavailable on this platform');
128132
}
129-
launchWindowsFallback(nodeArguments, environment);
133+
process.execve(process.execPath, [process.execPath, ...nodeArguments], environment);
130134
}
131135

132136
void launch().catch((error: unknown) => {

apps/pythinker-code/test/cli/ffi-launcher.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,37 @@ describe('FFI launcher', () => {
160160
expect(details.pid === originalPid).toBe(process.platform !== 'win32');
161161
});
162162

163+
it('uses the spawn fallback on win32 even when process.execve exists but throws', async () => {
164+
// Regression: Windows Node ships process.execve as a defined function that
165+
// throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM when called. The launcher must
166+
// route win32 to the spawn fallback without ever calling execve.
167+
const patchPath = join(fixtureDir, 'patch-win32.mjs');
168+
await writeFile(
169+
patchPath,
170+
`
171+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
172+
process.execve = () => {
173+
throw new Error('The feature process.execve is unavailable on the current platform');
174+
};
175+
`,
176+
);
177+
await writeMain(`
178+
process.stdout.write(JSON.stringify({ imported: true, marker: process.env.PYTHINKER_CODE_FFI_CHILD }));
179+
`);
180+
181+
const child = spawn(
182+
process.execPath,
183+
['--import', tsxLoader, '--import', patchPath, launcherPath],
184+
{ cwd: fixtureDir, env: { ...process.env }, stdio: 'pipe' },
185+
);
186+
const result = await collect(child);
187+
188+
expect(result.stderr).not.toContain('process.execve is unavailable');
189+
expect(result.code).toBe(0);
190+
const details = JSON.parse(result.stdout) as { imported: boolean; marker: string };
191+
expect(details).toMatchObject({ imported: true, marker: '1' });
192+
});
193+
163194
it.skipIf(process.platform === 'win32')(
164195
'preserves the parent process group and session across execve',
165196
async () => {

0 commit comments

Comments
 (0)