diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 307b02ca4d..456d6c3ee3 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -762,7 +762,14 @@ export const layer = Layer.effect( result.permission = mergeDeep(perms, result.permission ?? {}) } - if (!result.username) result.username = os.userInfo().username + if (!result.username) { + try { + result.username = os.userInfo().username || "user" + } catch (err) { + log.warn("failed to read system username, using fallback", { err }) + result.username = "user" + } + } if (result.autoshare === true && !result.share) { result.share = "auto" diff --git a/packages/opencode/src/config/managed.ts b/packages/opencode/src/config/managed.ts index 5b04884208..c5348afaf7 100644 --- a/packages/opencode/src/config/managed.ts +++ b/packages/opencode/src/config/managed.ts @@ -46,7 +46,14 @@ export function parseManagedPlist(json: string): string { export async function readManagedPreferences() { if (process.platform !== "darwin") return - const user = os.userInfo().username + const user = (() => { + try { + return os.userInfo().username || "user" + } catch (err) { + log.warn("failed to read system username, using fallback", { err }) + return "user" + } + })() const paths = [ path.join("/Library/Managed Preferences", user, `${MANAGED_PLIST_DOMAIN}.plist`), path.join("/Library/Managed Preferences", `${MANAGED_PLIST_DOMAIN}.plist`), diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 6ce0acdb2a..4d5aaf6fe1 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1,4 +1,4 @@ -import { test, expect, describe, afterEach, beforeEach } from "bun:test" +import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test" import { Effect, Exit, Layer, Option } from "effect" import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http" import { NodeFileSystem, NodePath } from "@effect/platform-node" @@ -28,6 +28,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { testEffect } from "../lib/effect" import path from "path" import fs from "fs/promises" +import os from "os" import { pathToFileURL } from "url" import { Global } from "@opencode-ai/core/global" import { ProjectID } from "../../src/project/schema" @@ -291,6 +292,20 @@ it.instance("loads config with defaults when no files exist", () => }), ) +it.instance("falls back to generic username when system user info is unavailable", () => + Effect.gen(function* () { + const userInfo = spyOn(os, "userInfo").mockImplementation(() => { + throw Object.assign(new Error("missing passwd entry"), { code: "ENOENT" }) + }) + try { + const config = yield* Config.use.get() + expect(config.username).toBe("user") + } finally { + userInfo.mockRestore() + } + }), +) + it.effect("creates global jsonc config with schema when no global configs exist", () => withGlobalConfig({}, ({ dir }) => Effect.gen(function* () {