Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/gen-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .agents/skills/sync-changelog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions .changeset/windows-launcher-execve.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 10 additions & 6 deletions apps/pythinker-code/src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,21 @@ async function launch(): Promise<void> {
...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) => {
Expand Down
31 changes: 31 additions & 0 deletions apps/pythinker-code/test/cli/ffi-launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading