diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 552e4a23..b9170455 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,8 +1,7 @@ import { render } from "ink"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; -import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; +import { getProjectCode, getUserDataDir, setShellIfWindows } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check"; import { AppContainer } from "./ui"; import { parseArguments } from "./cli-args"; @@ -36,7 +35,7 @@ async function main(): Promise { // Resolve --last to the most recent session ID for the current project if (parsed.last) { const projectCode = getProjectCode(projectRoot); - const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json"); + const indexPath = join(getUserDataDir(), "projects", projectCode, "sessions-index.json"); try { const index = JSON.parse(readFileSync(indexPath, "utf-8")); const entries: { id: string; updateTime: string }[] = Array.isArray(index?.entries) ? index.entries : []; @@ -54,7 +53,7 @@ async function main(): Promise { if (forkSessionId === true) { const projectCode = getProjectCode(projectRoot); - const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json"); + const indexPath = join(getUserDataDir(), "projects", projectCode, "sessions-index.json"); try { const index = JSON.parse(readFileSync(indexPath, "utf-8")); const entries: { id: string; updateTime: string }[] = Array.isArray(index?.entries) ? index.entries : []; @@ -88,7 +87,7 @@ async function main(): Promise { // Validate --resume before entering TUI if (typeof resumeSessionId === "string") { const projectCode = getProjectCode(projectRoot); - const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json"); + const indexPath = join(getUserDataDir(), "projects", projectCode, "sessions-index.json"); try { const index = JSON.parse(readFileSync(indexPath, "utf-8")); const found = @@ -105,7 +104,7 @@ async function main(): Promise { if (typeof forkSessionId === "string") { const projectCode = getProjectCode(projectRoot); - const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json"); + const indexPath = join(getUserDataDir(), "projects", projectCode, "sessions-index.json"); try { const index = JSON.parse(readFileSync(indexPath, "utf-8")); const found = Array.isArray(index?.entries) && index.entries.some((e: { id: string }) => e.id === forkSessionId); diff --git a/packages/cli/src/common/update-check.ts b/packages/cli/src/common/update-check.ts index 2dad85f8..76e2a4ac 100644 --- a/packages/cli/src/common/update-check.ts +++ b/packages/cli/src/common/update-check.ts @@ -1,11 +1,10 @@ import { spawn, type ChildProcess, type SpawnOptions } from "child_process"; import React from "react"; import * as fs from "fs"; -import * as os from "os"; import * as path from "path"; import { render, type Instance } from "ink"; import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; -import { killProcessTree } from "@vegamo/deepcode-core"; +import { getUserDataDir, killProcessTree } from "@vegamo/deepcode-core"; import type { PackageJson } from "../utils/package"; type UpdateState = { @@ -118,7 +117,7 @@ export function compareVersions(a: string, b: string): number { } export function getUpdateStatePath(): string { - return path.join(os.homedir(), ".deepcode", UPDATE_STATE_FILE); + return path.join(getUserDataDir(), UPDATE_STATE_FILE); } async function promptUpdateChoice({ diff --git a/packages/core/src/common/debug-logger.ts b/packages/core/src/common/debug-logger.ts index 124049e1..4ad615ef 100644 --- a/packages/core/src/common/debug-logger.ts +++ b/packages/core/src/common/debug-logger.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; -import * as os from "os"; import * as path from "path"; +import { getUserDataDir } from "../settings"; const DEBUG_LOG_FILE = "debug.log"; @@ -34,7 +34,7 @@ export function logOpenAIChatCompletionDebug(entry: OpenAIChatCompletionDebugEnt } export function getDebugLogPath(): string { - return path.join(os.homedir(), ".deepcode", "logs", DEBUG_LOG_FILE); + return path.join(getUserDataDir(), "logs", DEBUG_LOG_FILE); } export function normalizeDebugError(error: unknown): { name: string; message: string; stack?: string } { diff --git a/packages/core/src/common/error-logger.ts b/packages/core/src/common/error-logger.ts index 694a331a..de793afe 100644 --- a/packages/core/src/common/error-logger.ts +++ b/packages/core/src/common/error-logger.ts @@ -1,9 +1,9 @@ import * as fs from "fs"; import * as path from "path"; -import * as os from "os"; import type { LlmErrorDetails } from "./llm-error"; +import { getUserDataDir } from "../settings"; -const LOG_DIR = path.join(os.homedir(), ".deepcode", "logs"); +const LOG_DIR = path.join(getUserDataDir(), "logs"); const ERROR_LOG_PATH = path.join(LOG_DIR, "error.log"); function ensureLogDir(): void { diff --git a/packages/core/src/common/openai-client.ts b/packages/core/src/common/openai-client.ts index d3b56c08..495f2f5f 100644 --- a/packages/core/src/common/openai-client.ts +++ b/packages/core/src/common/openai-client.ts @@ -3,7 +3,7 @@ import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; import { Agent, fetch as undiciFetch } from "undici"; -import { resolveCurrentSettings } from "../settings"; +import { getUserDataDir, resolveCurrentSettings } from "../settings"; // Custom undici Agent with a 180-second keepAlive timeout. The default // global fetch (undici) only keeps connections alive for 4 seconds, which @@ -108,7 +108,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { function getMachineId(): string | undefined { try { - const idPath = path.join(os.homedir(), ".deepcode", "machine-id"); + const idPath = path.join(getUserDataDir(), "machine-id"); if (fs.existsSync(idPath)) { const raw = fs.readFileSync(idPath, "utf8").trim(); if (raw) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d389348c..1b6249c2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,6 +13,8 @@ export { applyModelConfigSelection, modelConfigKey, getUserSettingsPath, + getUserConfigDir, + getUserDataDir, getProjectSettingsPath, getDefaultContextWindow, getDefaultAutoCompactWindow, diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index b9252eaa..b54acf48 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -30,6 +30,7 @@ import { import { McpManager } from "./mcp/mcp-manager"; import { getDefaultAutoCompactWindow, + getUserDataDir, type McpServerConfig, type PermissionScope, type PermissionSettings, @@ -845,7 +846,7 @@ ${agentInstructions} return [ { root: path.join(this.projectRoot, ".deepcode", "skills"), displayRoot: "./.deepcode/skills" }, { root: path.join(this.projectRoot, ".agents", "skills"), displayRoot: "./.agents/skills" }, - { root: path.join(homeDir, ".deepcode", "skills"), displayRoot: "~/.deepcode/skills" }, + { root: path.join(getUserDataDir(), "skills"), displayRoot: "~/.deepcode/skills" }, { root: path.join(homeDir, ".agents", "skills"), displayRoot: "~/.agents/skills" }, { root: this.getBundledSkillsRoot(), displayRoot: "bundled:" }, ]; @@ -2008,7 +2009,7 @@ ${agentInstructions} sessionsIndexPath: string; } { const projectCode = getProjectCode(this.projectRoot); - const projectDir = path.join(os.homedir(), ".deepcode", "projects", projectCode); + const projectDir = path.join(getUserDataDir(), "projects", projectCode); const sessionsIndexPath = path.join(projectDir, "sessions-index.json"); return { projectCode, projectDir, sessionsIndexPath }; } @@ -2400,7 +2401,7 @@ ${agentInstructions} return projectInstructions.content; } - return this.readNonEmptyFile(path.join(os.homedir(), ".deepcode", "AGENTS.md")); + return this.readNonEmptyFile(path.join(getUserDataDir(), "AGENTS.md")); } private buildSystemMessage( diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index da0e2d94..4ed33d80 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -655,8 +655,24 @@ export const DEFAULT_BASE_URL = "https://api.deepseek.com"; // Settings file I/O // --------------------------------------------------------------------------- +/** User-level config directory. Honors $XDG_CONFIG_HOME/deepcode on Linux; defaults to ~/.deepcode. (#226) */ +export function getUserConfigDir(): string { + if (process.platform === "linux" && process.env.XDG_CONFIG_HOME) { + return path.join(process.env.XDG_CONFIG_HOME, "deepcode"); + } + return path.join(os.homedir(), ".deepcode"); +} + +/** User-level data directory (projects, logs, machine-id). Honors $XDG_DATA_HOME/deepcode on Linux; defaults to ~/.deepcode. (#226) */ +export function getUserDataDir(): string { + if (process.platform === "linux" && process.env.XDG_DATA_HOME) { + return path.join(process.env.XDG_DATA_HOME, "deepcode"); + } + return path.join(os.homedir(), ".deepcode"); +} + export function getUserSettingsPath(): string { - return path.join(os.homedir(), ".deepcode", "settings.json"); + return path.join(getUserConfigDir(), "settings.json"); } export function getProjectSettingsPath(projectRoot: string): string { diff --git a/packages/core/src/tests/settings-and-notify.test.ts b/packages/core/src/tests/settings-and-notify.test.ts index 93e8dc29..ebc4f1ef 100644 --- a/packages/core/src/tests/settings-and-notify.test.ts +++ b/packages/core/src/tests/settings-and-notify.test.ts @@ -8,9 +8,46 @@ import { type NotifySpawn, } from "../common/notify"; import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } from "../settings"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { getUserConfigDir, getUserDataDir, getUserSettingsPath } from "../settings"; const TEST_PROCESS_ENV = {}; +test("getUserConfigDir and getUserDataDir honor XDG env vars on Linux (#226)", () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); + const originalConfig = process.env.XDG_CONFIG_HOME; + const originalData = process.env.XDG_DATA_HOME; + try { + Object.defineProperty(process, "platform", { value: "linux" }); + process.env.XDG_CONFIG_HOME = "/tmp/xdg-config"; + process.env.XDG_DATA_HOME = "/tmp/xdg-data"; + assert.equal(getUserConfigDir(), path.join("/tmp/xdg-config", "deepcode")); + assert.equal(getUserDataDir(), path.join("/tmp/xdg-data", "deepcode")); + assert.equal(getUserSettingsPath(), path.join("/tmp/xdg-config", "deepcode", "settings.json")); + + delete process.env.XDG_CONFIG_HOME; + delete process.env.XDG_DATA_HOME; + assert.equal(getUserConfigDir(), path.join(os.homedir(), ".deepcode")); + assert.equal(getUserDataDir(), path.join(os.homedir(), ".deepcode")); + } finally { + if (originalConfig === undefined) { + delete process.env.XDG_CONFIG_HOME; + } else { + process.env.XDG_CONFIG_HOME = originalConfig; + } + if (originalData === undefined) { + delete process.env.XDG_DATA_HOME; + } else { + process.env.XDG_DATA_HOME = originalData; + } + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform); + } + } +}); + test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool", () => { const resolved = resolveSettings( { diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 37243c93..b64791ee 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; import * as fs from "fs"; import * as path from "path"; -import * as os from "os"; import OpenAI from "openai"; import MarkdownIt from "markdown-it"; import type { SessionMessage } from "@vegamo/deepcode-core"; @@ -17,6 +16,7 @@ import { type DeepcodingSettings, type ReasoningEffort, type ResolvedDeepcodingSettings, + getUserSettingsPath, setShellIfWindows, } from "@vegamo/deepcode-core"; import { getNonce } from "./utils.js"; @@ -438,7 +438,7 @@ export class DeepCodeViewProvider implements vscode.WebviewViewProvider { private readUserSettings(): DeepcodingSettings | null { try { - const settingsPath = path.join(os.homedir(), ".deepcode", "settings.json"); + const settingsPath = getUserSettingsPath(); if (!fs.existsSync(settingsPath)) { return null; }