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:
@@ -97,7 +97,10 @@ describe("penguin config model add/list (--root plus provider / model_id stored
|
||||
|
||||
const file = projectConfigPath(tmpRoot, DEFAULT_PROJECT_ID);
|
||||
expect(path.basename(file)).toBe(".project_config.toml");
|
||||
expect((await fs.stat(file)).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 fs.stat(file)).mode & 0o777).toBe(0o600);
|
||||
}
|
||||
const parsed = parseToml(await fs.readFile(file, "utf8")) as {
|
||||
models: Array<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
@@ -64,7 +64,10 @@ describe("penguin config vault", () => {
|
||||
|
||||
const file = agentVaultPath(tmpRoot, DEFAULT_PROJECT_ID, "default_agent");
|
||||
expect(path.basename(file)).toBe(".vault.toml");
|
||||
expect((await fs.stat(file)).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 fs.stat(file)).mode & 0o777).toBe(0o600);
|
||||
}
|
||||
expect(await fs.readFile(file, "utf8")).toContain("vault-secret-9876");
|
||||
|
||||
const list = await runVault(["list"]);
|
||||
|
||||
@@ -5,26 +5,29 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
import { applyLanguageToRc, resolveShellRc, upsertBlock } from "../src/lang-config.js";
|
||||
|
||||
describe("resolveShellRc", () => {
|
||||
// rcPath is built with path.join, so the expectations join too (backslashes on Windows —
|
||||
// where the `config lang` command refuses before ever calling this, but the pure function
|
||||
// stays coherent).
|
||||
it("maps zsh / bash / fish to their startup files and syntax", () => {
|
||||
const zsh = resolveShellRc("/bin/zsh", "/home/u");
|
||||
expect(zsh.kind).toBe("zsh");
|
||||
expect(zsh.rcPath).toBe("/home/u/.zshrc");
|
||||
expect(zsh.rcPath).toBe(join("/home/u", ".zshrc"));
|
||||
expect(zsh.body("zh")).toBe("export PENGUIN_LANG=zh");
|
||||
|
||||
const bash = resolveShellRc("/usr/bin/bash", "/home/u");
|
||||
expect(bash.kind).toBe("bash");
|
||||
expect(bash.rcPath).toBe("/home/u/.bashrc");
|
||||
expect(bash.rcPath).toBe(join("/home/u", ".bashrc"));
|
||||
|
||||
const fish = resolveShellRc("/usr/local/bin/fish", "/home/u");
|
||||
expect(fish.kind).toBe("fish");
|
||||
expect(fish.rcPath).toBe("/home/u/.config/fish/config.fish");
|
||||
expect(fish.rcPath).toBe(join("/home/u", ".config", "fish", "config.fish"));
|
||||
expect(fish.body("en")).toBe("set -gx PENGUIN_LANG en");
|
||||
});
|
||||
|
||||
it("falls back to ~/.profile for an unknown shell", () => {
|
||||
const rc = resolveShellRc(undefined, "/home/u");
|
||||
expect(rc.kind).toBe("unknown");
|
||||
expect(rc.rcPath).toBe("/home/u/.profile");
|
||||
expect(rc.rcPath).toBe(join("/home/u", ".profile"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -22,58 +22,64 @@ import {
|
||||
} from "../src/commands/update.js";
|
||||
import { getMessages } from "../src/i18n.js";
|
||||
|
||||
describe("detectInstall (how this CLI was installed, from its own real path)", () => {
|
||||
it("tarball: <installDir>/lib/dist/index.js, the layout install.sh unpacks", () => {
|
||||
expect(detectInstall("/home/me/.penguin/lib/dist/index.js")).toEqual({
|
||||
kind: "tarball",
|
||||
installDir: "/home/me/.penguin",
|
||||
// POSIX-only: the fixtures are POSIX install layouts and detectInstall normalizes through
|
||||
// path.* (backslashes on Windows) — where in-place `penguin update` is refused anyway
|
||||
// (the documented Windows upgrade path is re-running install.ps1).
|
||||
describe.skipIf(process.platform === "win32")(
|
||||
"detectInstall (how this CLI was installed, from its own real path)",
|
||||
() => {
|
||||
it("tarball: <installDir>/lib/dist/index.js, the layout install.sh unpacks", () => {
|
||||
expect(detectInstall("/home/me/.penguin/lib/dist/index.js")).toEqual({
|
||||
kind: "tarball",
|
||||
installDir: "/home/me/.penguin",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("tarball: a non-default PENGUIN_INSTALL_DIR is read off the path, not the environment", () => {
|
||||
expect(detectInstall("/opt/tools/penguin/lib/dist/index.js")).toEqual({
|
||||
kind: "tarball",
|
||||
installDir: "/opt/tools/penguin",
|
||||
it("tarball: a non-default PENGUIN_INSTALL_DIR is read off the path, not the environment", () => {
|
||||
expect(detectInstall("/opt/tools/penguin/lib/dist/index.js")).toEqual({
|
||||
kind: "tarball",
|
||||
installDir: "/opt/tools/penguin",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("npm global: npm's own prefix layout", () => {
|
||||
expect(
|
||||
detectInstall("/usr/local/lib/node_modules/@prismshadow/penguin-cli/dist/index.js"),
|
||||
).toEqual({ kind: "npm", globalRoot: "/usr/local/lib/node_modules" });
|
||||
});
|
||||
|
||||
it("npm global: pnpm's global store, through the .pnpm virtual dir", () => {
|
||||
const p =
|
||||
"/home/me/.local/share/pnpm/global/5/node_modules/.pnpm/@prismshadow+penguin-cli@0.1.1/node_modules/@prismshadow/penguin-cli/dist/index.js";
|
||||
const info = detectInstall(p);
|
||||
expect(info.kind).toBe("npm");
|
||||
expect(info.globalRoot).toContain(".pnpm");
|
||||
});
|
||||
|
||||
it("source checkout: the built dist inside the monorepo", () => {
|
||||
expect(detectInstall("/home/me/code/penguin-harness/packages/cli/dist/index.js")).toEqual({
|
||||
kind: "source",
|
||||
it("npm global: npm's own prefix layout", () => {
|
||||
expect(
|
||||
detectInstall("/usr/local/lib/node_modules/@prismshadow/penguin-cli/dist/index.js"),
|
||||
).toEqual({ kind: "npm", globalRoot: "/usr/local/lib/node_modules" });
|
||||
});
|
||||
});
|
||||
|
||||
it("source checkout: tsx running src directly", () => {
|
||||
expect(detectInstall("/home/me/code/penguin-harness/packages/cli/src/index.ts")).toEqual({
|
||||
kind: "source",
|
||||
it("npm global: pnpm's global store, through the .pnpm virtual dir", () => {
|
||||
const p =
|
||||
"/home/me/.local/share/pnpm/global/5/node_modules/.pnpm/@prismshadow+penguin-cli@0.1.1/node_modules/@prismshadow/penguin-cli/dist/index.js";
|
||||
const info = detectInstall(p);
|
||||
expect(info.kind).toBe("npm");
|
||||
expect(info.globalRoot).toContain(".pnpm");
|
||||
});
|
||||
});
|
||||
|
||||
it("a checkout wins over the tarball shape, so a repo under a lib/ dir is never mistaken for an install", () => {
|
||||
expect(detectInstall("/srv/lib/penguin-harness/packages/cli/dist/index.js")).toEqual({
|
||||
kind: "source",
|
||||
it("source checkout: the built dist inside the monorepo", () => {
|
||||
expect(detectInstall("/home/me/code/penguin-harness/packages/cli/dist/index.js")).toEqual({
|
||||
kind: "source",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("anything else is unknown rather than guessed", () => {
|
||||
expect(detectInstall("/random/place/index.js").kind).toBe("unknown");
|
||||
expect(detectInstall("/home/me/.penguin/bin/penguin").kind).toBe("unknown");
|
||||
});
|
||||
});
|
||||
it("source checkout: tsx running src directly", () => {
|
||||
expect(detectInstall("/home/me/code/penguin-harness/packages/cli/src/index.ts")).toEqual({
|
||||
kind: "source",
|
||||
});
|
||||
});
|
||||
|
||||
it("a checkout wins over the tarball shape, so a repo under a lib/ dir is never mistaken for an install", () => {
|
||||
expect(detectInstall("/srv/lib/penguin-harness/packages/cli/dist/index.js")).toEqual({
|
||||
kind: "source",
|
||||
});
|
||||
});
|
||||
|
||||
it("anything else is unknown rather than guessed", () => {
|
||||
expect(detectInstall("/random/place/index.js").kind).toBe("unknown");
|
||||
expect(detectInstall("/home/me/.penguin/bin/penguin").kind).toBe("unknown");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("detectPackageManager (which manager owns a global node_modules root)", () => {
|
||||
it("pnpm: the global store or the .pnpm virtual dir", () => {
|
||||
|
||||
Reference in New Issue
Block a user