diff options
| author | Adam Malczewski <[email protected]> | 2026-06-24 02:04:46 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-24 02:04:46 +0900 |
| commit | 41ca91a959ae245e76e50f1b55c8b21592bc6c50 (patch) | |
| tree | 289be7c0df654e75b6bc7552dd38bf0b9bda852d | |
| parent | 3a688edee373cbbc291a149e50e1d2802e2e29a4 (diff) | |
| download | dispatch-41ca91a959ae245e76e50f1b55c8b21592bc6c50.tar.gz dispatch-41ca91a959ae245e76e50f1b55c8b21592bc6c50.zip | |
feat(system-prompt): rich system:os with WSL detection + Linux distro
system:os now returns a descriptive string instead of the raw platform:
- Linux: reads /etc/os-release for distro name (PRETTY_NAME or NAME+VERSION_ID)
- WSL detection: checks /proc/sys/fs/binfmt_misc/WSLInterop or 'microsoft'
in /proc/version — appends (WSL) to the distro string
- Non-Linux: returns process.platform as-is (darwin, win32, etc.)
Examples: 'Ubuntu 22.04 LTS', 'Ubuntu 22.04 LTS (WSL)', 'Debian 12',
'Linux (WSL)', 'darwin'. All file reads use injected fs adapters (testable).
7 new resolver tests. 1403 vitest pass. FE CR-9.
| -rw-r--r-- | packages/system-prompt/src/resolver.test.ts | 80 | ||||
| -rw-r--r-- | packages/system-prompt/src/resolver.ts | 50 |
2 files changed, 129 insertions, 1 deletions
diff --git a/packages/system-prompt/src/resolver.test.ts b/packages/system-prompt/src/resolver.test.ts index 0585a33..474c79a 100644 --- a/packages/system-prompt/src/resolver.test.ts +++ b/packages/system-prompt/src/resolver.test.ts @@ -75,6 +75,86 @@ describe("resolver", () => { }); }); + describe("system:os rich resolution", () => { + it("returns distro from /etc/os-release PRETTY_NAME on Linux", async () => { + const files = new Map<string, string>([ + ["/etc/os-release", 'PRETTY_NAME="Ubuntu 22.04 LTS"\nNAME="Ubuntu"\n'], + ]); + const map = await resolveVariables("/proj", { + spawn: failSpawn(), + fs: fakeFs(files), + platform: () => "linux", + }); + expect(map.get("system:os")).toBe("Ubuntu 22.04 LTS"); + }); + + it("falls back to NAME + VERSION_ID when no PRETTY_NAME", async () => { + const files = new Map<string, string>([ + ["/etc/os-release", 'NAME="Debian"\nVERSION_ID="12"\n'], + ]); + const map = await resolveVariables("/proj", { + spawn: failSpawn(), + fs: fakeFs(files), + platform: () => "linux", + }); + expect(map.get("system:os")).toBe("Debian 12"); + }); + + it("appends (WSL) when WSLInterop exists", async () => { + const files = new Map<string, string>([ + ["/etc/os-release", 'PRETTY_NAME="Ubuntu 22.04 LTS"\n'], + ["/proc/sys/fs/binfmt_misc/WSLInterop", "enabled\n"], + ]); + const map = await resolveVariables("/proj", { + spawn: failSpawn(), + fs: fakeFs(files), + platform: () => "linux", + }); + expect(map.get("system:os")).toBe("Ubuntu 22.04 LTS (WSL)"); + }); + + it("detects WSL via 'microsoft' in /proc/version", async () => { + const files = new Map<string, string>([ + ["/etc/os-release", 'PRETTY_NAME="Ubuntu 22.04 LTS"\n'], + ["/proc/version", "Linux version 5.15.153.1-microsoft-standard-WSL2\n"], + ]); + const map = await resolveVariables("/proj", { + spawn: failSpawn(), + fs: fakeFs(files), + platform: () => "linux", + }); + expect(map.get("system:os")).toBe("Ubuntu 22.04 LTS (WSL)"); + }); + + it("returns 'Linux (WSL)' when WSL detected but no distro info", async () => { + const files = new Map<string, string>([["/proc/sys/fs/binfmt_misc/WSLInterop", "enabled\n"]]); + const map = await resolveVariables("/proj", { + spawn: failSpawn(), + fs: fakeFs(files), + platform: () => "linux", + }); + expect(map.get("system:os")).toBe("Linux (WSL)"); + }); + + it("returns plain 'linux' when no os-release and no WSL", async () => { + const map = await resolveVariables("/proj", { + spawn: failSpawn(), + fs: fakeFs(new Map()), + platform: () => "linux", + }); + expect(map.get("system:os")).toBe("linux"); + }); + + it("returns platform as-is for non-Linux (darwin)", async () => { + const map = await resolveVariables("/proj", { + spawn: failSpawn(), + fs: fakeFs(new Map()), + platform: () => "darwin", + }); + expect(map.get("system:os")).toBe("darwin"); + }); + }); + describe("file variables", () => { it("reads a file relative to cwd", async () => { // 12. file variable reads relative path; missing → null diff --git a/packages/system-prompt/src/resolver.ts b/packages/system-prompt/src/resolver.ts index 1ef9028..f271f47 100644 --- a/packages/system-prompt/src/resolver.ts +++ b/packages/system-prompt/src/resolver.ts @@ -91,6 +91,53 @@ async function readFile(filePath: string, cwd: string, fs: ResolverFs): Promise< } /** + * Resolve a rich OS description string. + * + * - **Linux:** reads `/etc/os-release` for the distro name (PRETTY_NAME or + * NAME+VERSION_ID). Detects WSL via `/proc/sys/fs/binfmt_misc/WSLInterop` or + * "microsoft" in `/proc/version`. Returns e.g. `"Ubuntu 22.04 (WSL)"` or + * `"Ubuntu 22.04"`. + * - **Other platforms:** returns `process.platform` (e.g. `"darwin"`, `"win32"`). + * + * All file reads use the injected `fs` adapter — failures are non-fatal (fall back + * to the base platform string). The `platform` override is honored for tests. + */ +async function resolveOs(platform: string, fs: ResolverFs): Promise<string> { + if (platform !== "linux") return platform; + + let distro: string | null = null; + const osRelease = await readFile("/etc/os-release", "/", fs); + if (osRelease !== null) { + const pretty = osRelease.match(/^PRETTY_NAME="(.+)"/m); + if (pretty?.[1] !== undefined) { + distro = pretty[1]; + } else { + const name = osRelease.match(/^NAME="(.+)"/m); + const version = osRelease.match(/^VERSION_ID="(.+)"/m); + if (name?.[1] !== undefined) { + distro = version?.[1] !== undefined ? `${name[1]} ${version[1]}` : name[1]; + } + } + } + + let isWsl = false; + const wslInterop = await readFile("/proc/sys/fs/binfmt_misc/WSLInterop", "/", fs); + if (wslInterop !== null) { + isWsl = true; + } else { + const procVersion = await readFile("/proc/version", "/", fs); + if (procVersion !== null && /microsoft/i.test(procVersion)) { + isWsl = true; + } + } + + if (distro !== null) { + return isWsl ? `${distro} (WSL)` : distro; + } + return isWsl ? "Linux (WSL)" : "linux"; +} + +/** * Resolve all variables for a construction. * * Always resolves the fixed catalog (`system:*`, `prompt:*`, `git:*`), plus any @@ -110,7 +157,8 @@ export async function resolveVariables( // ── system:* ──────────────────────────────────────────────────────────── vars.set("system:time", now.toISOString()); vars.set("system:date", now.toISOString().slice(0, 10)); - vars.set("system:os", adapters.platform?.() ?? process.platform); + const platform = adapters.platform?.() ?? process.platform; + vars.set("system:os", await resolveOs(platform, adapters.fs)); vars.set("system:hostname", adapters.hostname?.() ?? osHostname()); // ── prompt:* ──────────────────────────────────────────────────────────── |
