feat(core,tooling): Windows support — shell selection, install.ps1, win-x64 release package, Windows CI (#79)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-27 22:17:35 +08:00
committed by GitHub
parent c23f0bc2f0
commit c369e089a7
46 changed files with 1315 additions and 217 deletions
+1 -1
View File
@@ -25,7 +25,7 @@
"node": ">=24"
},
"scripts": {
"dev": "node ../../scripts/dev-prebuild.mjs && PENGUIN_HOME=\"${PENGUIN_HOME:-$HOME/.penguin/dev-data}\" tsx watch src/index.ts",
"dev": "node ../../scripts/dev-prebuild.mjs && node ../../scripts/run-with-env.mjs PENGUIN_HOME=~/.penguin/dev-data -- tsx watch src/index.ts",
"start": "node --disable-warning=ExperimentalWarning dist/index.js",
"typecheck": "tsc --noEmit -p tsconfig.json",
"test": "vitest run --passWithNoTests",
@@ -262,6 +262,15 @@ export class WorkspaceFilesService {
}
const { dir, name } = await this.resolveWriteParent(workspace, rel);
const file = path.join(dir, name);
// Windows has no O_NOFOLLOW (the `?? 0` below erases it), so the atomic ELOOP guard never
// fires there — refuse a final-segment symlink via lstat instead. Best effort (a link
// created between this check and the open wins the race), but it closes the practical
// "preset a symlink, overwrite an outside file by upload" escape; POSIX keeps the atomic
// open-time guarantee.
if (process.platform === "win32") {
const st = await fs.lstat(file).catch(() => null);
if (st?.isSymbolicLink()) throw badRequest("path must not be a symlink.");
}
// O_NOFOLLOW: open reports ELOOP if the final segment is a symlink, refusing to use it as leverage to overwrite a file outside the sandbox.
const flags = fsc.O_WRONLY | fsc.O_CREAT | fsc.O_TRUNC | (fsc.O_NOFOLLOW ?? 0);
let handle;
+4 -1
View File
@@ -82,7 +82,10 @@ describe("models preset & catalog enrichment", () => {
expect(cfgRaw).toContain('provider = "custom"');
expect(cfgRaw).toContain('model_id = "m-inline"');
expect(cfgRaw).not.toContain("custom/m-inline");
expect((await stat(cfgFile)).mode & 0o777).toBe(0o600);
// POSIX-only: Windows has no owner-only mode bits (chmod maps to the read-only attribute).
if (process.platform !== "win32") {
expect((await stat(cfgFile)).mode & 0o777).toBe(0o600);
}
// No more separate .credentials.toml / project_config.toml files.
await expect(readFile(path.join(projectDir, ".credentials.toml"), "utf8")).rejects.toThrow();
await expect(readFile(path.join(projectDir, "project_config.toml"), "utf8")).rejects.toThrow();