源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
export type PathEnv = Record<string, string | undefined>;
|
||||
|
||||
export interface PathOptions {
|
||||
env?: PathEnv;
|
||||
homeDir?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
userDataDir?: string;
|
||||
}
|
||||
|
||||
export interface OpencodeDbPathOptions extends PathOptions {
|
||||
/** Additional OpenCode data roots to search before the platform defaults. */
|
||||
dataDirs?: string[];
|
||||
/** Channel used when OPENCODE_CHANNEL is unset. */
|
||||
defaultChannel?: string;
|
||||
}
|
||||
|
||||
export declare const MAX_CONFIG_ROOT_LENGTH: 4096;
|
||||
|
||||
export declare function normalizeWorkspaceRootPath(value: unknown, opts?: PathOptions): string;
|
||||
export declare function openworkConfigDir(opts?: PathOptions): string;
|
||||
export declare function openworkServerConfigPath(opts?: PathOptions): string;
|
||||
export declare function openworkEnvStorePath(opts?: PathOptions): string;
|
||||
export declare function globalOpencodeConfigDir(opts?: PathOptions): string;
|
||||
export declare function resolveGlobalOpencodeConfigPath(opts?: PathOptions): string;
|
||||
export declare function workspaceOpencodeConfigCandidates(workspaceRoot: string): string[];
|
||||
export declare function resolveWorkspaceOpencodeConfigPath(workspaceRoot: string): string;
|
||||
export declare function desktopBootstrapPath(opts?: PathOptions): string;
|
||||
export declare function legacyDesktopBootstrapPath(opts?: PathOptions): string;
|
||||
export declare function expandHomePath(value: string, opts?: PathOptions): string;
|
||||
export declare function openworkServerDataDir(opts?: PathOptions): string;
|
||||
export declare function opencodeDataDirs(opts?: PathOptions): string[];
|
||||
export declare function opencodeDbCandidates(opts?: OpencodeDbPathOptions): string[];
|
||||
export declare function opencodeCacheDirs(opts?: PathOptions): string[];
|
||||
@@ -0,0 +1,283 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export const MAX_CONFIG_ROOT_LENGTH = 4_096;
|
||||
const FORBIDDEN_CONFIG_ROOT_CHARS = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
||||
|
||||
function pathApi(platform) {
|
||||
return platform === "win32" ? path.win32 : path.posix;
|
||||
}
|
||||
|
||||
function invalidWindowsWorkspaceRoot(value) {
|
||||
const error = new TypeError(`Invalid Windows workspace root: ${value}`);
|
||||
Object.defineProperty(error, "code", { value: "invalid_workspace_root" });
|
||||
return error;
|
||||
}
|
||||
|
||||
function isWindowsDeviceNamespace(value) {
|
||||
return /^\\\\[?.]\\/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes Windows verbatim drive and UNC paths without consulting the
|
||||
* filesystem. Missing drives and disconnected shares must remain unchanged so
|
||||
* callers can report their real accessibility error instead of resolving them
|
||||
* against another drive.
|
||||
*/
|
||||
export function normalizeWorkspaceRootPath(value, opts) {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
if (!trimmed || optionPlatform(opts) !== "win32") return trimmed;
|
||||
|
||||
const windowsPath = trimmed.replace(/\//g, "\\");
|
||||
let normalized = windowsPath;
|
||||
if (windowsPath.startsWith("\\\\?\\")) {
|
||||
const verbatimPath = windowsPath.slice(4);
|
||||
if (/^UNC(?:\\|$)/i.test(verbatimPath)) {
|
||||
const uncPath = verbatimPath.slice(3).replace(/^\\/, "");
|
||||
const [server, share] = uncPath.split("\\");
|
||||
if (!server || !share) throw invalidWindowsWorkspaceRoot(trimmed);
|
||||
normalized = `\\\\${uncPath}`;
|
||||
} else {
|
||||
if (!/^[A-Za-z]:\\/.test(verbatimPath)) throw invalidWindowsWorkspaceRoot(trimmed);
|
||||
normalized = verbatimPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (isWindowsDeviceNamespace(normalized)) {
|
||||
throw invalidWindowsWorkspaceRoot(trimmed);
|
||||
}
|
||||
|
||||
if (/^[A-Za-z]:(?!\\)/.test(normalized)) {
|
||||
throw invalidWindowsWorkspaceRoot(trimmed);
|
||||
}
|
||||
if (normalized.startsWith("\\")) {
|
||||
if (!normalized.startsWith("\\\\")) throw invalidWindowsWorkspaceRoot(trimmed);
|
||||
const [server, share] = normalized.slice(2).split("\\");
|
||||
if (!server || !share) throw invalidWindowsWorkspaceRoot(trimmed);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function optionEnv(opts) {
|
||||
return opts?.env ?? process.env;
|
||||
}
|
||||
|
||||
function optionPlatform(opts) {
|
||||
return opts?.platform ?? process.platform;
|
||||
}
|
||||
|
||||
function envValue(env, key) {
|
||||
return String(env?.[key] ?? "").trim();
|
||||
}
|
||||
|
||||
function envRawValue(env, key) {
|
||||
const value = env?.[key];
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function optionHomeDir(opts) {
|
||||
const configured = opts?.homeDir?.trim();
|
||||
if (configured) return configured;
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const fromEnv = platform === "win32"
|
||||
? envValue(env, "USERPROFILE") || envValue(env, "HOME")
|
||||
: envValue(env, "HOME");
|
||||
return fromEnv || homedir();
|
||||
}
|
||||
|
||||
function defaultOpenworkConfigDir(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const homeDir = optionHomeDir(opts);
|
||||
if (platform === "win32") {
|
||||
const appData = envValue(env, "APPDATA");
|
||||
const root = appData || paths.join(homeDir, "AppData", "Roaming");
|
||||
return paths.join(root, "openwork");
|
||||
}
|
||||
const xdgConfigHome = envValue(env, "XDG_CONFIG_HOME");
|
||||
const root = xdgConfigHome || paths.join(homeDir, ".config");
|
||||
return paths.join(root, "openwork");
|
||||
}
|
||||
|
||||
export function openworkConfigDir(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const override = envValue(env, "OPENWORK_SERVER_CONFIG");
|
||||
if (override) return paths.dirname(paths.resolve(override));
|
||||
return defaultOpenworkConfigDir(opts);
|
||||
}
|
||||
|
||||
export function openworkServerConfigPath(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const override = envValue(env, "OPENWORK_SERVER_CONFIG");
|
||||
if (override) return paths.resolve(override);
|
||||
return paths.join(defaultOpenworkConfigDir(opts), "server.json");
|
||||
}
|
||||
|
||||
export function openworkEnvStorePath(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const override = envValue(env, "OPENWORK_ENV_STORE");
|
||||
if (override) return paths.resolve(override);
|
||||
return paths.join(defaultOpenworkConfigDir(opts), "env.json");
|
||||
}
|
||||
|
||||
function safeConfigRoot(value, paths) {
|
||||
return value.length > 0
|
||||
&& value.length <= MAX_CONFIG_ROOT_LENGTH
|
||||
&& paths.isAbsolute(value)
|
||||
&& !FORBIDDEN_CONFIG_ROOT_CHARS.test(value);
|
||||
}
|
||||
|
||||
export function globalOpencodeConfigDir(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const configuredDirectory = envRawValue(env, "OPENCODE_CONFIG_DIR");
|
||||
if (safeConfigRoot(configuredDirectory, paths)) return configuredDirectory;
|
||||
|
||||
const configuredRoot = envRawValue(env, "XDG_CONFIG_HOME");
|
||||
const configRoot = safeConfigRoot(configuredRoot, paths)
|
||||
? configuredRoot
|
||||
: paths.join(optionHomeDir(opts), ".config");
|
||||
// OpenCode accepts OPENCODE_CONFIG_DIR as the directory containing its
|
||||
// opencode.json(c) files. It is not an XDG parent directory.
|
||||
return paths.join(configRoot, "opencode");
|
||||
}
|
||||
|
||||
export function resolveGlobalOpencodeConfigPath(opts) {
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const base = globalOpencodeConfigDir(opts);
|
||||
const jsonc = paths.join(base, "opencode.jsonc");
|
||||
const json = paths.join(base, "opencode.json");
|
||||
if (existsSync(jsonc)) return jsonc;
|
||||
if (existsSync(json)) return json;
|
||||
return jsonc;
|
||||
}
|
||||
|
||||
export function workspaceOpencodeConfigCandidates(workspaceRoot) {
|
||||
return [
|
||||
path.join(workspaceRoot, "opencode.jsonc"),
|
||||
path.join(workspaceRoot, "opencode.json"),
|
||||
path.join(workspaceRoot, ".opencode", "opencode.jsonc"),
|
||||
path.join(workspaceRoot, ".opencode", "opencode.json"),
|
||||
];
|
||||
}
|
||||
|
||||
export function resolveWorkspaceOpencodeConfigPath(workspaceRoot) {
|
||||
const candidates = workspaceOpencodeConfigCandidates(workspaceRoot);
|
||||
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
||||
}
|
||||
|
||||
function desktopConfigDir(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const xdgConfigHome = envValue(env, "XDG_CONFIG_HOME");
|
||||
if (xdgConfigHome) return xdgConfigHome;
|
||||
if (platform === "win32") {
|
||||
const localAppData = envValue(env, "LOCALAPPDATA");
|
||||
if (localAppData) return localAppData;
|
||||
return paths.join(optionHomeDir(opts), "AppData", "Local");
|
||||
}
|
||||
return paths.join(optionHomeDir(opts), ".config");
|
||||
}
|
||||
|
||||
export function desktopBootstrapPath(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const override = envValue(env, "OPENWORK_DESKTOP_BOOTSTRAP_PATH");
|
||||
if (override) return override;
|
||||
if (envValue(env, "OPENWORK_DEV_MODE") === "1" && opts?.userDataDir) {
|
||||
return paths.join(opts.userDataDir, "openwork-dev-data", "home", ".config", "openwork", "desktop-bootstrap.json");
|
||||
}
|
||||
return paths.join(desktopConfigDir(opts), "openwork", "desktop-bootstrap.json");
|
||||
}
|
||||
|
||||
export function legacyDesktopBootstrapPath(opts) {
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
// Existing installers may have written this file using USERPROFILE/HOME while
|
||||
// Electron used os.homedir(). optionHomeDir accepts an explicit homeDir but
|
||||
// otherwise checks the same env variables before os.homedir(), so both legacy
|
||||
// locations continue to resolve for normal installs.
|
||||
return paths.join(optionHomeDir(opts), ".config", "openwork", "desktop-bootstrap.json");
|
||||
}
|
||||
|
||||
export function expandHomePath(value, opts) {
|
||||
if (value === "~") return optionHomeDir(opts);
|
||||
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
||||
return pathApi(optionPlatform(opts)).join(optionHomeDir(opts), value.slice(2));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function openworkServerDataDir(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const override = envValue(env, "OPENWORK_DATA_DIR");
|
||||
if (override) return expandHomePath(override, opts);
|
||||
return paths.join(optionHomeDir(opts), ".openwork", "openwork-server");
|
||||
}
|
||||
|
||||
export function opencodeDataDirs(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const homeDir = optionHomeDir(opts);
|
||||
const dirs = [];
|
||||
const xdgDataHome = envValue(env, "XDG_DATA_HOME");
|
||||
if (xdgDataHome) dirs.push(paths.join(xdgDataHome, "opencode"));
|
||||
dirs.push(paths.join(homeDir, ".local", "share", "opencode"));
|
||||
if (platform === "darwin") dirs.push(paths.join(homeDir, "Library", "Application Support", "opencode"));
|
||||
if (platform === "win32") {
|
||||
const appData = envValue(env, "APPDATA") || paths.join(homeDir, "AppData", "Roaming");
|
||||
dirs.push(paths.join(appData, "opencode"));
|
||||
}
|
||||
return Array.from(new Set(dirs));
|
||||
}
|
||||
|
||||
function truthy(value) {
|
||||
const normalized = String(value ?? "").trim().toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
}
|
||||
|
||||
/** Candidate OpenCode databases in the same override/channel order used by OpenWork. */
|
||||
export function opencodeDbCandidates(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const dirs = Array.from(new Set([...(opts?.dataDirs ?? []), ...opencodeDataDirs(opts)]));
|
||||
const override = envValue(env, "OPENCODE_DB");
|
||||
if (override) {
|
||||
if (paths.isAbsolute(override)) return [override];
|
||||
return dirs.map((dir) => paths.join(dir, override));
|
||||
}
|
||||
|
||||
const channel = envValue(env, "OPENCODE_CHANNEL") || opts?.defaultChannel || "local";
|
||||
const names = channel === "latest" || channel === "beta" || truthy(envValue(env, "OPENCODE_DISABLE_CHANNEL_DB"))
|
||||
? ["opencode.db"]
|
||||
: [`opencode-${channel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`, "opencode.db"];
|
||||
return dirs.flatMap((dir) => names.map((name) => paths.join(dir, name)));
|
||||
}
|
||||
|
||||
export function opencodeCacheDirs(opts) {
|
||||
const env = optionEnv(opts);
|
||||
const platform = optionPlatform(opts);
|
||||
const paths = pathApi(platform);
|
||||
const dirs = [];
|
||||
const xdgCacheHome = envValue(env, "XDG_CACHE_HOME");
|
||||
if (xdgCacheHome) dirs.push(paths.join(xdgCacheHome, "opencode"));
|
||||
dirs.push(paths.join(optionHomeDir(opts), ".cache", "opencode"));
|
||||
return Array.from(new Set(dirs));
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@openwork/paths",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"index.d.ts",
|
||||
"index.mjs"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
desktopBootstrapPath,
|
||||
globalOpencodeConfigDir,
|
||||
legacyDesktopBootstrapPath,
|
||||
MAX_CONFIG_ROOT_LENGTH,
|
||||
normalizeWorkspaceRootPath,
|
||||
opencodeDbCandidates,
|
||||
openworkEnvStorePath,
|
||||
openworkServerConfigPath,
|
||||
resolveGlobalOpencodeConfigPath,
|
||||
resolveWorkspaceOpencodeConfigPath,
|
||||
workspaceOpencodeConfigCandidates,
|
||||
} from "../index.mjs";
|
||||
|
||||
async function withTempDir(callback) {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "openwork-paths-"));
|
||||
try {
|
||||
await callback(root);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("workspace root paths", () => {
|
||||
test("normalizes valid Windows verbatim drive and UNC paths cross-platform", () => {
|
||||
const opts = { platform: "win32" };
|
||||
expect(normalizeWorkspaceRootPath("\\\\?\\C:\\Users\\Ada\\Workspace", opts))
|
||||
.toBe("C:\\Users\\Ada\\Workspace");
|
||||
expect(normalizeWorkspaceRootPath("\\\\?\\C:\\", opts)).toBe("C:\\");
|
||||
expect(normalizeWorkspaceRootPath("//?/UNC/server/share/Workspace", opts))
|
||||
.toBe("\\\\server\\share\\Workspace");
|
||||
expect(normalizeWorkspaceRootPath("\\\\?\\UNC\\server\\share", opts))
|
||||
.toBe("\\\\server\\share");
|
||||
});
|
||||
|
||||
test("preserves valid normal drives and UNC shares without checking availability", () => {
|
||||
const opts = { platform: "win32" };
|
||||
expect(normalizeWorkspaceRootPath("Z:\\Disconnected\\Workspace", opts))
|
||||
.toBe("Z:\\Disconnected\\Workspace");
|
||||
expect(normalizeWorkspaceRootPath("\\\\offline-server\\share\\Workspace", opts))
|
||||
.toBe("\\\\offline-server\\share\\Workspace");
|
||||
expect(normalizeWorkspaceRootPath("\\\\offline-server\\pipe\\Workspace", opts))
|
||||
.toBe("\\\\offline-server\\pipe\\Workspace");
|
||||
});
|
||||
|
||||
test("rejects Win32 device namespace roots", () => {
|
||||
const opts = { platform: "win32" };
|
||||
for (const value of [
|
||||
"\\\\.\\pipe\\openwork",
|
||||
"//./PIPE/openwork",
|
||||
"\\\\.\\PhysicalDrive0",
|
||||
"\\\\?\\UNC\\.\\pipe\\openwork",
|
||||
"\\\\?\\UNC\\?\\PhysicalDrive0",
|
||||
]) {
|
||||
expect(() => normalizeWorkspaceRootPath(value, opts)).toThrow("Invalid Windows workspace root");
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects incomplete Windows drive and UNC roots", () => {
|
||||
const opts = { platform: "win32" };
|
||||
for (const value of [
|
||||
"C:",
|
||||
"\\\\?\\",
|
||||
"\\\\?\\C:",
|
||||
"\\\\?\\C:Workspace",
|
||||
"\\\\?\\UNC",
|
||||
"\\\\?\\UNC\\server",
|
||||
"\\\\server",
|
||||
]) {
|
||||
expect(() => normalizeWorkspaceRootPath(value, opts)).toThrow("Invalid Windows workspace root");
|
||||
}
|
||||
});
|
||||
|
||||
test("applies Windows validation only when Windows is injected", () => {
|
||||
expect(normalizeWorkspaceRootPath("\\\\?\\C:", { platform: "linux" })).toBe("\\\\?\\C:");
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenCode database paths", () => {
|
||||
test("uses the production channel name and honors explicit overrides", () => {
|
||||
expect(opencodeDbCandidates({
|
||||
env: {},
|
||||
homeDir: "/Users/ada",
|
||||
platform: "darwin",
|
||||
defaultChannel: "latest",
|
||||
})).toContain("/Users/ada/Library/Application Support/opencode/opencode.db");
|
||||
expect(opencodeDbCandidates({
|
||||
env: { OPENCODE_CHANNEL: "preview" },
|
||||
dataDirs: ["/tmp/opencode"],
|
||||
homeDir: "/Users/ada",
|
||||
platform: "darwin",
|
||||
})[0]).toBe("/tmp/opencode/opencode-preview.db");
|
||||
expect(opencodeDbCandidates({
|
||||
env: { OPENCODE_DB: "/tmp/production.db" },
|
||||
homeDir: "/Users/ada",
|
||||
platform: "darwin",
|
||||
})).toEqual(["/tmp/production.db"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openwork server config paths", () => {
|
||||
test("uses APPDATA on Windows", () => {
|
||||
expect(openworkServerConfigPath({
|
||||
env: { APPDATA: "C:\\Users\\Ada\\AppData\\Roaming" },
|
||||
homeDir: "C:\\Users\\Ada",
|
||||
platform: "win32",
|
||||
})).toBe("C:\\Users\\Ada\\AppData\\Roaming\\openwork\\server.json");
|
||||
});
|
||||
|
||||
test("uses XDG_CONFIG_HOME on Unix", () => {
|
||||
expect(openworkServerConfigPath({
|
||||
env: { XDG_CONFIG_HOME: "/tmp/xdg" },
|
||||
homeDir: "/home/ada",
|
||||
platform: "linux",
|
||||
})).toBe("/tmp/xdg/openwork/server.json");
|
||||
});
|
||||
|
||||
test("falls back to ~/.config", () => {
|
||||
expect(openworkServerConfigPath({ env: {}, homeDir: "/home/ada", platform: "linux" }))
|
||||
.toBe("/home/ada/.config/openwork/server.json");
|
||||
});
|
||||
|
||||
test("honors OPENWORK_SERVER_CONFIG", () => {
|
||||
expect(openworkServerConfigPath({
|
||||
env: { OPENWORK_SERVER_CONFIG: "/tmp/openwork/server.json" },
|
||||
homeDir: "/home/ada",
|
||||
platform: "linux",
|
||||
})).toBe("/tmp/openwork/server.json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("openwork env store and desktop bootstrap paths", () => {
|
||||
test("honors OPENWORK_ENV_STORE", () => {
|
||||
expect(openworkEnvStorePath({
|
||||
env: { OPENWORK_ENV_STORE: "/tmp/openwork/env.json" },
|
||||
homeDir: "/home/ada",
|
||||
platform: "linux",
|
||||
})).toBe("/tmp/openwork/env.json");
|
||||
});
|
||||
|
||||
test("uses the same openwork config layout for env.json", () => {
|
||||
expect(openworkEnvStorePath({
|
||||
env: { XDG_CONFIG_HOME: "/tmp/xdg" },
|
||||
homeDir: "/home/ada",
|
||||
platform: "linux",
|
||||
})).toBe("/tmp/xdg/openwork/env.json");
|
||||
});
|
||||
|
||||
test("honors OPENWORK_DESKTOP_BOOTSTRAP_PATH", () => {
|
||||
expect(desktopBootstrapPath({
|
||||
env: { OPENWORK_DESKTOP_BOOTSTRAP_PATH: "/tmp/bootstrap.json" },
|
||||
homeDir: "/home/ada",
|
||||
platform: "linux",
|
||||
})).toBe("/tmp/bootstrap.json");
|
||||
});
|
||||
|
||||
test("preserves dev-data desktop bootstrap path when userDataDir is injected", () => {
|
||||
expect(desktopBootstrapPath({
|
||||
env: { OPENWORK_DEV_MODE: "1" },
|
||||
homeDir: "/Users/ada",
|
||||
platform: "darwin",
|
||||
userDataDir: "/tmp/openwork-userdata",
|
||||
})).toBe("/tmp/openwork-userdata/openwork-dev-data/home/.config/openwork/desktop-bootstrap.json");
|
||||
});
|
||||
|
||||
test("resolves the legacy desktop bootstrap path from the chosen home", () => {
|
||||
expect(legacyDesktopBootstrapPath({ env: {}, homeDir: "/Users/ada", platform: "darwin" }))
|
||||
.toBe("/Users/ada/.config/openwork/desktop-bootstrap.json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("global OpenCode config paths", () => {
|
||||
test("accepts safe OPENCODE_CONFIG_DIR as the config directory", async () => {
|
||||
await withTempDir(async (root) => {
|
||||
const opencodeConfigDir = path.join(root, "explicit-opencode");
|
||||
await mkdir(opencodeConfigDir, { recursive: true });
|
||||
const json = path.join(opencodeConfigDir, "opencode.json");
|
||||
await writeFile(json, "{}", "utf8");
|
||||
|
||||
const opts = {
|
||||
env: { OPENCODE_CONFIG_DIR: opencodeConfigDir, XDG_CONFIG_HOME: path.join(root, "xdg") },
|
||||
homeDir: path.join(root, "home"),
|
||||
platform: "linux",
|
||||
};
|
||||
expect(globalOpencodeConfigDir(opts)).toBe(opencodeConfigDir);
|
||||
expect(resolveGlobalOpencodeConfigPath(opts)).toBe(json);
|
||||
});
|
||||
});
|
||||
|
||||
test("prefers opencode.jsonc over opencode.json and falls back to jsonc", async () => {
|
||||
await withTempDir(async (root) => {
|
||||
const dir = path.join(root, "xdg", "opencode");
|
||||
await mkdir(dir, { recursive: true });
|
||||
const opts = { env: { XDG_CONFIG_HOME: path.join(root, "xdg") }, homeDir: path.join(root, "home"), platform: "linux" };
|
||||
const jsonc = path.join(dir, "opencode.jsonc");
|
||||
const json = path.join(dir, "opencode.json");
|
||||
|
||||
expect(resolveGlobalOpencodeConfigPath(opts)).toBe(jsonc);
|
||||
await writeFile(json, "{}", "utf8");
|
||||
expect(resolveGlobalOpencodeConfigPath(opts)).toBe(json);
|
||||
await writeFile(jsonc, "{}", "utf8");
|
||||
expect(resolveGlobalOpencodeConfigPath(opts)).toBe(jsonc);
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects relative OPENCODE_CONFIG_DIR", () => {
|
||||
const opts = {
|
||||
env: { OPENCODE_CONFIG_DIR: "relative/opencode", XDG_CONFIG_HOME: "/tmp/xdg" },
|
||||
homeDir: "/home/ada",
|
||||
platform: "linux",
|
||||
};
|
||||
expect(globalOpencodeConfigDir(opts)).toBe("/tmp/xdg/opencode");
|
||||
});
|
||||
|
||||
test("rejects over-long OPENCODE_CONFIG_DIR", () => {
|
||||
const opts = {
|
||||
env: { OPENCODE_CONFIG_DIR: `/${"a".repeat(MAX_CONFIG_ROOT_LENGTH)}`, XDG_CONFIG_HOME: "/tmp/xdg" },
|
||||
homeDir: "/home/ada",
|
||||
platform: "linux",
|
||||
};
|
||||
expect(globalOpencodeConfigDir(opts)).toBe("/tmp/xdg/opencode");
|
||||
});
|
||||
|
||||
test("rejects forbidden control characters in OPENCODE_CONFIG_DIR", () => {
|
||||
const opts = {
|
||||
env: { OPENCODE_CONFIG_DIR: "/tmp/opencode\n", XDG_CONFIG_HOME: "/tmp/xdg" },
|
||||
homeDir: "/home/ada",
|
||||
platform: "linux",
|
||||
};
|
||||
expect(globalOpencodeConfigDir(opts)).toBe("/tmp/xdg/opencode");
|
||||
});
|
||||
});
|
||||
|
||||
describe("workspace OpenCode config paths", () => {
|
||||
test("returns the four server candidates in order", () => {
|
||||
expect(workspaceOpencodeConfigCandidates("/repo/workspace")).toEqual([
|
||||
"/repo/workspace/opencode.jsonc",
|
||||
"/repo/workspace/opencode.json",
|
||||
"/repo/workspace/.opencode/opencode.jsonc",
|
||||
"/repo/workspace/.opencode/opencode.json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("resolves the first existing workspace candidate", async () => {
|
||||
await withTempDir(async (root) => {
|
||||
await mkdir(path.join(root, ".opencode"), { recursive: true });
|
||||
const hiddenJsonc = path.join(root, ".opencode", "opencode.jsonc");
|
||||
const hiddenJson = path.join(root, ".opencode", "opencode.json");
|
||||
await writeFile(hiddenJson, "{}", "utf8");
|
||||
expect(resolveWorkspaceOpencodeConfigPath(root)).toBe(hiddenJson);
|
||||
await writeFile(hiddenJsonc, "{}", "utf8");
|
||||
expect(resolveWorkspaceOpencodeConfigPath(root)).toBe(hiddenJsonc);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"noEmit": true,
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["index.d.ts", "index.mjs"]
|
||||
}
|
||||
Reference in New Issue
Block a user