summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-24 02:04:46 +0900
committerAdam Malczewski <[email protected]>2026-06-24 02:04:46 +0900
commit41ca91a959ae245e76e50f1b55c8b21592bc6c50 (patch)
tree289be7c0df654e75b6bc7552dd38bf0b9bda852d
parent3a688edee373cbbc291a149e50e1d2802e2e29a4 (diff)
downloaddispatch-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.ts80
-rw-r--r--packages/system-prompt/src/resolver.ts50
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:* ────────────────────────────────────────────────────────────