feat: add GSD_HOME env var to override global ~/.gsd directory (#1566)
Centralise all ~/.gsd path construction through app-paths.ts (compiled code) or a module-level `gsdHome` const (runtime extensions that cannot import app-paths). When GSD_HOME is set, every path that previously resolved under ~/.gsd now resolves under the override. Existing overrides (GSD_STATE_DIR, GSD_CODING_AGENT_DIR) continue to take precedence when set.
This commit is contained in:
parent
21a9ab2bcf
commit
869e037202
17 changed files with 52 additions and 21 deletions
|
|
@ -151,6 +151,14 @@ Recommended verification order:
|
|||
- If a server is team-shared and safe to commit, `.mcp.json` is usually the better home.
|
||||
- If a server depends on machine-local paths, personal services, or local-only secrets, prefer `.gsd/mcp.json`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GSD_HOME` | `~/.gsd` | Global GSD directory. All paths derive from this unless individually overridden. |
|
||||
| `GSD_STATE_DIR` | `$GSD_HOME` | Per-project state root. Controls where `projects/<repo-hash>/` directories are created. Takes precedence over `GSD_HOME` for project state. |
|
||||
| `GSD_CODING_AGENT_DIR` | `$GSD_HOME/agent` | Agent directory containing managed resources, extensions, and auth. Takes precedence over `GSD_HOME` for agent paths. |
|
||||
|
||||
## All Settings
|
||||
|
||||
### `models`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
export const appRoot = join(homedir(), '.gsd')
|
||||
export const appRoot = process.env.GSD_HOME || join(homedir(), '.gsd')
|
||||
export const agentDir = join(appRoot, 'agent')
|
||||
export const sessionsDir = join(appRoot, 'sessions')
|
||||
export const authFilePath = join(agentDir, 'auth.json')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { appRoot } from "./app-paths.js";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -66,7 +66,7 @@ function isManifest(data: unknown): data is ExtensionManifest {
|
|||
// ─── Registry Path ──────────────────────────────────────────────────────────
|
||||
|
||||
export function getRegistryPath(): string {
|
||||
return join(homedir(), ".gsd", "extensions", "registry.json");
|
||||
return join(appRoot, "extensions", "registry.json");
|
||||
}
|
||||
|
||||
// ─── Registry I/O ───────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@
|
|||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { appRoot } from "./app-paths.js";
|
||||
|
||||
// Inlined from preferences.ts to avoid crossing the compiled/uncompiled
|
||||
// boundary — this file is compiled by tsc, but preferences.ts is loaded
|
||||
// via jiti at runtime. Importing it as .js fails because no .js exists
|
||||
// in dist/. See #592, #1110.
|
||||
const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
|
||||
const GLOBAL_PREFERENCES_PATH = join(appRoot, "preferences.md");
|
||||
|
||||
export function saveRemoteQuestionsConfig(channel: "slack" | "discord" | "telegram", channelId: string): void {
|
||||
const prefsPath = GLOBAL_PREFERENCES_PATH;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { join, sep as pathSep } from "node:path";
|
|||
import { homedir } from "node:os";
|
||||
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
|
||||
// ─── Project Root → Worktree Sync ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -111,7 +113,7 @@ export function syncStateToProjectRoot(
|
|||
*/
|
||||
export function readResourceVersion(): string | null {
|
||||
const agentDir =
|
||||
process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
|
||||
process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
|
||||
const manifestPath = join(agentDir, "managed-resources.json");
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
|
|||
import { dirname, join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
|
||||
// ─── Types (mirrored from extension-registry.ts) ────────────────────────────
|
||||
|
||||
interface ExtensionManifest {
|
||||
|
|
@ -48,11 +50,11 @@ interface ExtensionRegistry {
|
|||
// ─── Registry I/O ───────────────────────────────────────────────────────────
|
||||
|
||||
function getRegistryPath(): string {
|
||||
return join(homedir(), ".gsd", "extensions", "registry.json");
|
||||
return join(gsdHome, "extensions", "registry.json");
|
||||
}
|
||||
|
||||
function getAgentExtensionsDir(): string {
|
||||
return join(homedir(), ".gsd", "agent", "extensions");
|
||||
return join(gsdHome, "agent", "extensions");
|
||||
}
|
||||
|
||||
function loadRegistry(): ExtensionRegistry {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { gsdRoot } from "./paths.js";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
import { enableDebug } from "./debug-logger.js";
|
||||
import { deriveState } from "./state.js";
|
||||
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
||||
|
|
@ -482,7 +484,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
if (parts.length === 3 && ["enable", "disable", "info"].includes(parts[1])) {
|
||||
const idPrefix = parts[2] ?? "";
|
||||
try {
|
||||
const extDir = join(homedir(), ".gsd", "agent", "extensions");
|
||||
const extDir = join(gsdHome, "agent", "extensions");
|
||||
const ids: { id: string; name: string }[] = [];
|
||||
for (const entry of readdirSync(extDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { join } from "node:path";
|
|||
import { homedir } from "node:os";
|
||||
import { gsdRoot } from "./paths.js";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ProjectDetection {
|
||||
|
|
@ -400,7 +402,6 @@ function detectVerificationCommands(
|
|||
* Check if global GSD setup exists (has ~/.gsd/ with preferences).
|
||||
*/
|
||||
export function hasGlobalSetup(): boolean {
|
||||
const gsdHome = join(homedir(), ".gsd");
|
||||
return (
|
||||
existsSync(join(gsdHome, "preferences.md")) ||
|
||||
existsSync(join(gsdHome, "PREFERENCES.md"))
|
||||
|
|
@ -412,7 +413,6 @@ export function hasGlobalSetup(): boolean {
|
|||
* Returns true if ~/.gsd/ doesn't exist or has no preferences or auth.
|
||||
*/
|
||||
export function isFirstEverLaunch(): boolean {
|
||||
const gsdHome = join(homedir(), ".gsd");
|
||||
if (!existsSync(gsdHome)) return true;
|
||||
|
||||
// If we have preferences, not first launch
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ import { join } from "node:path";
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { shortcutDesc } from "../shared/mod.js";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
import { Text } from "@gsd/pi-tui";
|
||||
import { pauseAutoForProviderError, classifyProviderError } from "./provider-error-pause.js";
|
||||
import { toPosixPath } from "../shared/mod.js";
|
||||
|
|
@ -73,7 +75,7 @@ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js"
|
|||
|
||||
function warnDeprecatedAgentInstructions(): void {
|
||||
const paths = [
|
||||
join(homedir(), ".gsd", "agent-instructions.md"),
|
||||
join(gsdHome, "agent-instructions.md"),
|
||||
join(process.cwd(), ".gsd", "agent-instructions.md"),
|
||||
];
|
||||
for (const p of paths) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
import { gsdRoot } from "./paths.js";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
|
||||
|
|
@ -82,14 +84,14 @@ export {
|
|||
|
||||
// ─── Path Constants & Getters ───────────────────────────────────────────────
|
||||
|
||||
const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
|
||||
const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
|
||||
const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
|
||||
function projectPreferencesPath(): string {
|
||||
return join(gsdRoot(process.cwd()), "preferences.md");
|
||||
}
|
||||
// Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
|
||||
// Check uppercase as a fallback so those files aren't silently ignored.
|
||||
const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(homedir(), ".gsd", "PREFERENCES.md");
|
||||
const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
|
||||
function projectPreferencesPathUppercase(): string {
|
||||
return join(gsdRoot(process.cwd()), "PREFERENCES.md");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, s
|
|||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
|
||||
// ─── Repo Identity ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -113,7 +115,7 @@ export function repoIdentity(basePath: string): string {
|
|||
* otherwise `~/.gsd/projects/<hash>`.
|
||||
*/
|
||||
export function externalGsdRoot(basePath: string): string {
|
||||
const base = process.env.GSD_STATE_DIR || join(homedir(), ".gsd");
|
||||
const base = process.env.GSD_STATE_DIR || gsdHome;
|
||||
return join(base, "projects", repoIdentity(basePath));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { join } from "node:path";
|
|||
import { homedir } from "node:os";
|
||||
import { resolveProjectRoot } from "./worktree.js";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
|
||||
// ─── Resource Staleness ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -23,7 +25,7 @@ function isManifestWithVersion(data: unknown): data is { gsdVersion: string } {
|
|||
}
|
||||
|
||||
export function readResourceVersion(): string | null {
|
||||
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
|
||||
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
|
||||
const manifestPath = join(agentDir, "managed-resources.json");
|
||||
const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
|
||||
return manifest?.gsdVersion ?? null;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { join } from "node:path";
|
|||
import { homedir } from "node:os";
|
||||
import { readPromptRecord } from "./store.js";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
|
||||
export interface LatestPromptSummary {
|
||||
id: string;
|
||||
status: string;
|
||||
|
|
@ -14,7 +16,7 @@ export interface LatestPromptSummary {
|
|||
}
|
||||
|
||||
export function getLatestPromptSummary(): LatestPromptSummary | null {
|
||||
const runtimeDir = join(homedir(), ".gsd", "runtime", "remote-questions");
|
||||
const runtimeDir = join(gsdHome, "runtime", "remote-questions");
|
||||
if (!existsSync(runtimeDir)) return null;
|
||||
const files = readdirSync(runtimeDir).filter((f) => f.endsWith(".json"));
|
||||
if (files.length === 0) return null;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import { join } from "node:path";
|
|||
import { homedir } from "node:os";
|
||||
import type { RemotePrompt, RemotePromptRecord, RemotePromptRef, RemoteAnswer, RemotePromptStatus } from "./types.js";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
|
||||
function runtimeDir(): string {
|
||||
return join(homedir(), ".gsd", "runtime", "remote-questions");
|
||||
return join(gsdHome, "runtime", "remote-questions");
|
||||
}
|
||||
|
||||
function recordPath(id: string): string {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js'
|
|||
// Compute authFilePath locally instead of importing from app-paths.ts,
|
||||
// because extensions are copied to ~/.gsd/agent/extensions/ at runtime
|
||||
// where the relative import '../../../app-paths.ts' doesn't resolve.
|
||||
const authFilePath = join(homedir(), '.gsd', 'agent', 'auth.json')
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd')
|
||||
const authFilePath = join(gsdHome, 'agent', 'auth.json')
|
||||
|
||||
export type SearchProvider = 'tavily' | 'brave' | 'ollama'
|
||||
export type SearchProviderPreference = SearchProvider | 'auto'
|
||||
|
|
|
|||
|
|
@ -57,8 +57,10 @@ function encodeCwd(cwd: string): string {
|
|||
return cwd.replace(/\//g, "--");
|
||||
}
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || path.join(os.homedir(), ".gsd");
|
||||
|
||||
function getIsolationBaseDir(cwd: string, taskId: string): string {
|
||||
return path.join(os.homedir(), ".gsd", "wt", encodeCwd(cwd), taskId);
|
||||
return path.join(gsdHome, "wt", encodeCwd(cwd), taskId);
|
||||
}
|
||||
|
||||
// Track active isolation dirs for cleanup on exit
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
||||
import { join, basename } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
||||
import type { Rule } from "./ttsr-manager.js";
|
||||
import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
|
||||
|
||||
|
|
@ -59,7 +61,7 @@ function scanDir(dir: string): Rule[] {
|
|||
* Project rules override global rules with the same name.
|
||||
*/
|
||||
export function loadRules(cwd: string): Rule[] {
|
||||
const globalDir = join(homedir(), ".gsd", "agent", "rules");
|
||||
const globalDir = join(gsdHome, "agent", "rules");
|
||||
const projectDir = join(cwd, ".gsd", "rules");
|
||||
|
||||
const globalRules = scanDir(globalDir);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue