diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e63d083d3..d3c6c7d55 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Added - Added real-time authentication validation in OAuth provider selector with visual status indicators (checking, valid, invalid) @@ -8,6 +9,8 @@ ### Changed +- Replaced `process.cwd()` with `getProjectDir()` throughout codebase for improved project directory detection and handling +- Made `SessionManager.list()` async to support asynchronous session discovery operations - Preserved internal whitespace and indentation in bash command normalization to support heredocs and indentation-sensitive scripts - Improved git context loading performance with configurable timeouts and parallel status/commit queries - Enhanced git context reliability with better error handling for timeout and command failures diff --git a/packages/coding-agent/examples/sdk/11-sessions.ts b/packages/coding-agent/examples/sdk/11-sessions.ts index f94f2c126..17c9c0c19 100644 --- a/packages/coding-agent/examples/sdk/11-sessions.ts +++ b/packages/coding-agent/examples/sdk/11-sessions.ts @@ -25,7 +25,7 @@ if (modelFallbackMessage) console.log("Note:", modelFallbackMessage); console.log("Continued session:", continued.sessionFile); // List and open specific session -const sessions = SessionManager.list(process.cwd()); +const sessions = await SessionManager.list(process.cwd()); console.log(`\nFound ${sessions.length} sessions:`); for (const info of sessions.slice(0, 3)) { console.log(` ${info.id.slice(0, 8)}… - "${info.firstMessage.slice(0, 30)}…"`); diff --git a/packages/coding-agent/src/capability/index.ts b/packages/coding-agent/src/capability/index.ts index 9747ac3b0..88591bc60 100644 --- a/packages/coding-agent/src/capability/index.ts +++ b/packages/coding-agent/src/capability/index.ts @@ -9,6 +9,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { $env } from "@oh-my-pi/pi-utils"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; /** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */ const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {}; @@ -221,7 +222,7 @@ export async function loadCapability(capabilityId: string, options: LoadOptio throw new Error(`Unknown capability: "${capabilityId}"`); } - const cwd = options.cwd ?? process.cwd(); + const cwd = options.cwd ?? getProjectDir(); const home = os.homedir(); const ctx: LoadContext = { cwd, home }; const providers = filterProviders(capability, options); diff --git a/packages/coding-agent/src/capability/types.ts b/packages/coding-agent/src/capability/types.ts index 8fba6553d..277afb2b0 100644 --- a/packages/coding-agent/src/capability/types.ts +++ b/packages/coding-agent/src/capability/types.ts @@ -62,7 +62,7 @@ export interface LoadOptions { providers?: string[]; /** Exclude these providers (by ID). Default: none */ excludeProviders?: string[]; - /** Custom cwd. Default: process.cwd() */ + /** Custom cwd. Default: getProjectDir() */ cwd?: string; /** Include items even if they fail validation. Default: false */ includeInvalid?: boolean; diff --git a/packages/coding-agent/src/cli/file-processor.ts b/packages/coding-agent/src/cli/file-processor.ts index 8cba61759..e266fbbfd 100644 --- a/packages/coding-agent/src/cli/file-processor.ts +++ b/packages/coding-agent/src/cli/file-processor.ts @@ -5,6 +5,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import type { ImageContent } from "@oh-my-pi/pi-ai"; import { isEnoent } from "@oh-my-pi/pi-utils"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import chalk from "chalk"; import { resolveReadPath } from "../tools/path-utils"; import { formatSize } from "../tools/truncate"; @@ -34,7 +35,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process for (const fileArg of fileArgs) { // Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces) - const absolutePath = path.resolve(resolveReadPath(fileArg, process.cwd())); + const absolutePath = path.resolve(resolveReadPath(fileArg, getProjectDir())); const stat = fs.statSync(absolutePath, { throwIfNoEntry: false }); if (!stat) { diff --git a/packages/coding-agent/src/cli/shell-cli.ts b/packages/coding-agent/src/cli/shell-cli.ts index c6254deb0..87a85dfd5 100644 --- a/packages/coding-agent/src/cli/shell-cli.ts +++ b/packages/coding-agent/src/cli/shell-cli.ts @@ -6,7 +6,7 @@ import * as path from "node:path"; import { createInterface } from "node:readline/promises"; import { Shell } from "@oh-my-pi/pi-natives"; -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import chalk from "chalk"; import { Settings } from "../config/settings"; import { getOrCreateSnapshot } from "../utils/shell-snapshot"; @@ -47,7 +47,7 @@ export async function runShellCommand(cmd: ShellCommandArgs): Promise { process.exit(1); } - const cwd = cmd.cwd ? path.resolve(cmd.cwd) : process.cwd(); + const cwd = cmd.cwd ? path.resolve(cmd.cwd) : getProjectDir(); const settings = await Settings.init({ cwd }); const { shell, env: shellEnv } = settings.getShellConfig(); const snapshotPath = cmd.noSnapshot || !shell.includes("bash") ? null : await getOrCreateSnapshot(shell, shellEnv); diff --git a/packages/coding-agent/src/commit/agentic/index.ts b/packages/coding-agent/src/commit/agentic/index.ts index a7e281776..ee1f0788b 100644 --- a/packages/coding-agent/src/commit/agentic/index.ts +++ b/packages/coding-agent/src/commit/agentic/index.ts @@ -1,6 +1,7 @@ import * as path from "node:path"; import { createInterface } from "node:readline/promises"; import { $env } from "@oh-my-pi/pi-utils"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { applyChangelogProposals } from "../../commit/changelog"; import { detectChangelogBoundaries } from "../../commit/changelog/detect"; import { parseUnreleasedSection } from "../../commit/changelog/parse"; @@ -26,7 +27,7 @@ interface CommitExecutionContext { } export async function runAgenticCommit(args: CommitCommandArgs): Promise { - const cwd = process.cwd(); + const cwd = getProjectDir(); const git = new ControlledGit(cwd); const [settings, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]); diff --git a/packages/coding-agent/src/commit/pipeline.ts b/packages/coding-agent/src/commit/pipeline.ts index ce75b783e..cd72d89fe 100644 --- a/packages/coding-agent/src/commit/pipeline.ts +++ b/packages/coding-agent/src/commit/pipeline.ts @@ -1,6 +1,7 @@ import * as path from "node:path"; import type { Api, Model } from "@oh-my-pi/pi-ai"; import { logger } from "@oh-my-pi/pi-utils"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { ModelRegistry } from "../config/model-registry"; import { renderPromptTemplate } from "../config/prompt-templates"; import { Settings } from "../config/settings"; @@ -38,7 +39,7 @@ export async function runCommitCommand(args: CommitCommandArgs): Promise { } async function runLegacyCommitCommand(args: CommitCommandArgs): Promise { - const cwd = process.cwd(); + const cwd = getProjectDir(); const settings = await Settings.init(); const commitSettings = settings.getGroup("commit"); const authStorage = await discoverAuthStorage(); diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 67223e77b..088e042c9 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { CONFIG_DIR_NAME, getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { CONFIG_DIR_NAME, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import type { TSchema } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; import { Ajv, type ErrorObject, type ValidateFunction } from "ajv"; @@ -39,8 +39,8 @@ export function getPackageDir(): string { } dir = path.dirname(dir); } - // Fallback to cwd (docs/examples won't be found, but that's fine) - return process.cwd(); + // Fallback to project dir (docs/examples won't be found, but that's fine) + return getProjectDir(); } /** Get path to CHANGELOG.md (optional, may not exist in binary) */ @@ -273,7 +273,7 @@ export interface GetConfigDirsOptions { user?: boolean; /** Include project-level directories (.omp/...). Default: true */ project?: boolean; - /** Current working directory for project paths. Default: process.cwd() */ + /** Current working directory for project paths. Default: getProjectDir() */ cwd?: string; /** Only return directories that exist. Default: false */ existingOnly?: boolean; @@ -296,7 +296,7 @@ export interface GetConfigDirsOptions { * getConfigDirs("skills", { user: false, existingOnly: true }) */ export function getConfigDirs(subpath: string, options: GetConfigDirsOptions = {}): ConfigDirEntry[] { - const { user = true, project = true, cwd = process.cwd(), existingOnly = false } = options; + const { user = true, project = true, cwd = getProjectDir(), existingOnly = false } = options; const results: ConfigDirEntry[] = []; // User-level directories (highest priority) @@ -382,7 +382,7 @@ export function findConfigFileWithMeta( * Returns one entry per config base (.omp, .claude) - the nearest one found. * Results are in priority order (highest first). */ -export function findAllNearestProjectConfigDirs(subpath: string, cwd: string = process.cwd()): ConfigDirEntry[] { +export function findAllNearestProjectConfigDirs(subpath: string, cwd: string = getProjectDir()): ConfigDirEntry[] { const results: ConfigDirEntry[] = []; const foundBases = new Set(); diff --git a/packages/coding-agent/src/config/prompt-templates.ts b/packages/coding-agent/src/config/prompt-templates.ts index 05482485c..d6110d574 100644 --- a/packages/coding-agent/src/config/prompt-templates.ts +++ b/packages/coding-agent/src/config/prompt-templates.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { logger } from "@oh-my-pi/pi-utils"; -import { getProjectPromptsDir, getPromptsDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, getProjectPromptsDir, getPromptsDir } from "@oh-my-pi/pi-utils/dirs"; import Handlebars from "handlebars"; import { computeLineHash } from "../patch/hashline"; import { jtdToTypeScript } from "../tools/jtd-to-typescript"; @@ -459,7 +459,7 @@ async function loadTemplatesFromDir( } export interface LoadPromptTemplatesOptions { - /** Working directory for project-local templates. Default: process.cwd() */ + /** Working directory for project-local templates. Default: getProjectDir() */ cwd?: string; /** Agent config directory for global templates. Default: from getPromptsDir() */ agentDir?: string; @@ -471,7 +471,7 @@ export interface LoadPromptTemplatesOptions { * 2. Project: cwd/.omp/prompts/ */ export async function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): Promise { - const resolvedCwd = options.cwd ?? process.cwd(); + const resolvedCwd = options.cwd ?? getProjectDir(); const resolvedAgentDir = options.agentDir ?? getPromptsDir(); const templates: PromptTemplate[] = []; diff --git a/packages/coding-agent/src/config/settings.ts b/packages/coding-agent/src/config/settings.ts index d68705c14..6b4afb983 100644 --- a/packages/coding-agent/src/config/settings.ts +++ b/packages/coding-agent/src/config/settings.ts @@ -14,7 +14,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { YAML } from "bun"; import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings"; import type { ModelRole } from "../config/model-registry"; @@ -146,7 +146,7 @@ export class Settings { #persist: boolean; private constructor(options: SettingsOptions = {}) { - this.#cwd = path.normalize(options.cwd ?? process.cwd()); + this.#cwd = path.normalize(options.cwd ?? getProjectDir()); this.#agentDir = path.normalize(options.agentDir ?? getAgentDir()); this.#configPath = options.inMemory ? null : path.join(this.#agentDir, "config.yml"); this.#persist = !options.inMemory; diff --git a/packages/coding-agent/src/debug/system-info.ts b/packages/coding-agent/src/debug/system-info.ts index e594a1861..669b5554e 100644 --- a/packages/coding-agent/src/debug/system-info.ts +++ b/packages/coding-agent/src/debug/system-info.ts @@ -2,7 +2,7 @@ * System information collection for debug reports. */ import * as os from "node:os"; -import { VERSION } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs"; export interface SystemInfo { os: string; @@ -65,7 +65,7 @@ export async function collectSystemInfo(): Promise { bun: Bun.version, node: process.version, }, - cwd: process.cwd(), + cwd: getProjectDir(), shell, terminal, }; diff --git a/packages/coding-agent/src/extensibility/custom-commands/loader.ts b/packages/coding-agent/src/extensibility/custom-commands/loader.ts index 3d541dda1..d5e1f21f0 100644 --- a/packages/coding-agent/src/extensibility/custom-commands/loader.ts +++ b/packages/coding-agent/src/extensibility/custom-commands/loader.ts @@ -8,7 +8,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import * as piCodingAgent from "@oh-my-pi/pi-coding-agent"; import { isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import * as typebox from "@sinclair/typebox"; import { getConfigDirs } from "../../config"; import { execCommand } from "../../exec/exec"; @@ -62,7 +62,7 @@ async function loadCommandModule( } export interface DiscoverCustomCommandsOptions { - /** Current working directory. Default: process.cwd() */ + /** Current working directory. Default: getProjectDir() */ cwd?: string; /** Agent config directory. Default: from getAgentDir() */ agentDir?: string; @@ -80,7 +80,7 @@ export interface DiscoverCustomCommandsResult { export async function discoverCustomCommands( options: DiscoverCustomCommandsOptions = {}, ): Promise { - const cwd = options.cwd ?? process.cwd(); + const cwd = options.cwd ?? getProjectDir(); const agentDir = options.agentDir ?? getAgentDir(); const paths: Array<{ path: string; source: CustomCommandSource }> = []; const seen = new Set(); @@ -136,7 +136,7 @@ export async function discoverCustomCommands( } export interface LoadCustomCommandsOptions { - /** Current working directory. Default: process.cwd() */ + /** Current working directory. Default: getProjectDir() */ cwd?: string; /** Agent config directory. Default: from getAgentDir() */ agentDir?: string; @@ -163,7 +163,7 @@ function loadBundledCommands(sharedApi: CustomCommandAPI): LoadedCustomCommand[] * Discover and load custom commands from standard locations. */ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}): Promise { - const cwd = options.cwd ?? process.cwd(); + const cwd = options.cwd ?? getProjectDir(); const agentDir = options.agentDir ?? getAgentDir(); const { paths } = await discoverCustomCommands({ cwd, agentDir }); diff --git a/packages/coding-agent/src/extensibility/plugins/installer.ts b/packages/coding-agent/src/extensibility/plugins/installer.ts index 586cde57e..ffcc73a57 100644 --- a/packages/coding-agent/src/extensibility/plugins/installer.ts +++ b/packages/coding-agent/src/extensibility/plugins/installer.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; import { isEnoent } from "@oh-my-pi/pi-utils"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import type { InstalledPlugin } from "./types"; const PLUGINS_DIR = path.join(getAgentDir(), "plugins"); @@ -131,7 +131,7 @@ export async function listPlugins(): Promise { } export async function linkPlugin(localPath: string): Promise { - const cwd = process.cwd(); + const cwd = getProjectDir(); const absolutePath = path.resolve(cwd, localPath); // Validate that resolved path is within cwd to prevent path traversal diff --git a/packages/coding-agent/src/extensibility/plugins/manager.ts b/packages/coding-agent/src/extensibility/plugins/manager.ts index eb639364a..26cdeb620 100644 --- a/packages/coding-agent/src/extensibility/plugins/manager.ts +++ b/packages/coding-agent/src/extensibility/plugins/manager.ts @@ -6,6 +6,7 @@ import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson, + getProjectDir, getProjectPluginOverridesPath, } from "@oh-my-pi/pi-utils/dirs"; import { extractPackageName, parsePluginSpec } from "./parser"; @@ -50,7 +51,7 @@ export class PluginManager { #runtimeConfig: PluginRuntimeConfig | null = null; #cwd: string; - constructor(cwd: string = process.cwd()) { + constructor(cwd: string = getProjectDir()) { this.#cwd = cwd; } diff --git a/packages/coding-agent/src/extensibility/skills.ts b/packages/coding-agent/src/extensibility/skills.ts index a377f1718..e37a8950b 100644 --- a/packages/coding-agent/src/extensibility/skills.ts +++ b/packages/coding-agent/src/extensibility/skills.ts @@ -2,6 +2,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { logger } from "@oh-my-pi/pi-utils"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { skillCapability } from "../capability/skill"; import type { SourceMeta } from "../capability/types"; import type { SkillsSettings } from "../config/settings"; @@ -207,7 +208,7 @@ async function scanDirectoryForSkills(dir: string): Promise { } export interface LoadSkillsOptions extends SkillsSettings { - /** Working directory for project-local skills. Default: process.cwd() */ + /** Working directory for project-local skills. Default: getProjectDir() */ cwd?: string; } @@ -217,7 +218,7 @@ export interface LoadSkillsOptions extends SkillsSettings { */ export async function loadSkills(options: LoadSkillsOptions = {}): Promise { const { - cwd = process.cwd(), + cwd = getProjectDir(), enabled = true, enableCodexUser = true, enableClaudeUser = true, diff --git a/packages/coding-agent/src/extensibility/slash-commands.ts b/packages/coding-agent/src/extensibility/slash-commands.ts index 4910ff2ae..acb7d8558 100644 --- a/packages/coding-agent/src/extensibility/slash-commands.ts +++ b/packages/coding-agent/src/extensibility/slash-commands.ts @@ -281,7 +281,7 @@ export function substituteArgs(content: string, args: string[]): string { } export interface LoadSlashCommandsOptions { - /** Working directory for project-local commands. Default: process.cwd() */ + /** Working directory for project-local commands. Default: getProjectDir() */ cwd?: string; } diff --git a/packages/coding-agent/src/ipy/executor.ts b/packages/coding-agent/src/ipy/executor.ts index d0a73c99d..86086bdea 100644 --- a/packages/coding-agent/src/ipy/executor.ts +++ b/packages/coding-agent/src/ipy/executor.ts @@ -1,6 +1,6 @@ import * as path from "node:path"; import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { OutputSink } from "../session/streaming-output"; import { time } from "../utils/timings"; import { shutdownSharedGateway } from "./gateway-coordinator"; @@ -523,7 +523,7 @@ export async function executePythonWithKernel( } export async function executePython(code: string, options?: PythonExecutorOptions): Promise { - const cwd = options?.cwd ?? process.cwd(); + const cwd = options?.cwd ?? getProjectDir(); await ensureKernelAvailable(cwd); const kernelMode = options?.kernelMode ?? "session"; diff --git a/packages/coding-agent/src/ipy/modules.ts b/packages/coding-agent/src/ipy/modules.ts index 7bc605bc7..af023a172 100644 --- a/packages/coding-agent/src/ipy/modules.ts +++ b/packages/coding-agent/src/ipy/modules.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { getAgentModulesDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentModulesDir, getProjectDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs"; export type PythonModuleSource = "user" | "project"; @@ -24,7 +24,7 @@ export interface PythonModuleExecutor { } export interface DiscoverPythonModulesOptions { - /** Working directory for project-level modules. Default: process.cwd() */ + /** Working directory for project-level modules. Default: getProjectDir() */ cwd?: string; /** Agent directory for user-level modules. Default: from getAgentDir() */ agentDir?: string; @@ -65,7 +65,7 @@ async function readModuleContent(candidate: ModuleCandidate): Promise { - const cwd = options.cwd ?? process.cwd(); + const cwd = options.cwd ?? getProjectDir(); const userDir = getAgentModulesDir(options.agentDir); const projectDir = getProjectModulesDir(cwd); diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index e32fbc3fd..1b26f0844 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -4,13 +4,15 @@ * This file handles CLI argument parsing and translates them into * createAgentSession() options. The SDK does the heavy lifting. */ + +import { realpathSync } from "node:fs"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { createInterface } from "node:readline/promises"; import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai"; import { $env, postmortem } from "@oh-my-pi/pi-utils"; -import { VERSION } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, setProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs"; import chalk from "chalk"; import type { Args } from "./cli/args"; import { processFileArguments } from "./cli/file-processor"; @@ -278,11 +280,11 @@ async function maybeAutoChdir(parsed: Args): Promise { } const normalizePath = (value: string) => { - const resolved = path.resolve(value); + const resolved = realpathSync(path.resolve(value)); return process.platform === "win32" ? resolved.toLowerCase() : resolved; }; - const cwd = normalizePath(process.cwd()); + const cwd = normalizePath(getProjectDir()); const normalizedHome = normalizePath(home); if (cwd !== normalizedHome) { return; @@ -303,7 +305,7 @@ async function maybeAutoChdir(parsed: Args): Promise { if (!(await isDirectory(candidate))) { continue; } - process.chdir(candidate); + setProjectDir(candidate); return; } catch { // Try next candidate. @@ -313,7 +315,7 @@ async function maybeAutoChdir(parsed: Args): Promise { try { const fallback = os.tmpdir(); if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) { - process.chdir(fallback); + setProjectDir(fallback); } } catch { // Ignore fallback errors. @@ -355,7 +357,7 @@ async function buildSessionOptions( modelRegistry: ModelRegistry, ): Promise { const options: CreateAgentSessionOptions = { - cwd: parsed.cwd ?? process.cwd(), + cwd: parsed.cwd ?? getProjectDir(), }; // Auto-discover SYSTEM.md if no CLI system prompt provided @@ -524,7 +526,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise { - let dir = process.cwd(); + let dir = getProjectDir(); while (true) { const gitHeadPath = path.join(dir, ".git", "HEAD"); try { @@ -201,7 +202,7 @@ export class FooterComponent implements Component { }; // Replace home directory with ~ - let pwd = shortenPath(process.cwd()); + let pwd = shortenPath(getProjectDir()); // Add git branch if available const branch = this.#getCurrentBranch(); diff --git a/packages/coding-agent/src/modes/components/status-line.ts b/packages/coding-agent/src/modes/components/status-line.ts index 5432841c8..f82eef68b 100644 --- a/packages/coding-agent/src/modes/components/status-line.ts +++ b/packages/coding-agent/src/modes/components/status-line.ts @@ -2,6 +2,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import type { AssistantMessage } from "@oh-my-pi/pi-ai"; import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { $ } from "bun"; import { settings } from "../../config/settings"; import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema"; @@ -41,7 +42,7 @@ function sanitizeStatusText(text: string): string { /** Find the git root directory by walking up from cwd */ function findGitHeadPath(): string | null { - let dir = process.cwd(); + let dir = getProjectDir(); while (true) { const gitHeadPath = path.join(dir, ".git", "HEAD"); if (fs.existsSync(gitHeadPath)) { diff --git a/packages/coding-agent/src/modes/components/status-line/segments.ts b/packages/coding-agent/src/modes/components/status-line/segments.ts index 318ecf37d..12ee857f6 100644 --- a/packages/coding-agent/src/modes/components/status-line/segments.ts +++ b/packages/coding-agent/src/modes/components/status-line/segments.ts @@ -1,4 +1,5 @@ import * as os from "node:os"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { theme } from "../../../modes/theme/theme"; import { shortenPath } from "../../../tools/render-utils"; import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types"; @@ -91,7 +92,7 @@ const pathSegment: StatusLineSegment = { render(ctx) { const opts = ctx.options.path ?? {}; - let pwd = process.cwd(); + let pwd = getProjectDir(); if (opts.abbreviate !== false) { pwd = shortenPath(pwd); diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index b20e51bf8..5768ed40a 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -13,6 +13,7 @@ import { type TUI, } from "@oh-my-pi/pi-tui"; import { logger, sanitizeText } from "@oh-my-pi/pi-utils"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import type { Theme } from "../../modes/theme/theme"; import { theme } from "../../modes/theme/theme"; import { @@ -129,7 +130,7 @@ export class ToolExecutionComponent extends Container { options: ToolExecutionOptions = {}, tool: AgentTool | undefined, ui: TUI, - cwd: string = process.cwd(), + cwd: string = getProjectDir(), ) { super(); this.#toolName = toolName; diff --git a/packages/coding-agent/src/modes/controllers/command-controller.ts b/packages/coding-agent/src/modes/controllers/command-controller.ts index 7372469fd..b1b76f540 100644 --- a/packages/coding-agent/src/modes/controllers/command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/command-controller.ts @@ -5,6 +5,7 @@ import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai"; import { copyToClipboard } from "@oh-my-pi/pi-natives"; import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui"; import { Snowflake } from "@oh-my-pi/pi-utils"; +import { setProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { $ } from "bun"; import { reset as resetCapabilities } from "../../capability"; import { loadCustomShare } from "../../export/custom-share"; @@ -466,7 +467,7 @@ export class CommandController { try { await this.ctx.sessionManager.flush(); await this.ctx.sessionManager.moveTo(resolvedPath); - process.chdir(resolvedPath); + setProjectDir(resolvedPath); resetCapabilities(); await this.ctx.refreshSlashCommandState(resolvedPath); diff --git a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts index 303bbe34c..78103555d 100644 --- a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts @@ -4,7 +4,7 @@ * Handles /mcp subcommands for managing MCP servers. */ import { Spacer, Text } from "@oh-my-pi/pi-tui"; -import { getMCPConfigPath } from "@oh-my-pi/pi-utils/dirs"; +import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import type { SourceMeta } from "../../capability/types"; import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp"; import { connectToServer, disconnectServer, listTools } from "../../mcp/client"; @@ -528,7 +528,7 @@ export class MCPCommandController { if (this.ctx.mcpManager) { resolvedConfig = await this.ctx.mcpManager.prepareConfig(config); } else { - const tempManager = new MCPManager(process.cwd()); + const tempManager = new MCPManager(getProjectDir()); tempManager.setAuthStorage(this.ctx.session.modelRegistry.authStorage); resolvedConfig = await tempManager.prepareConfig(config); } @@ -540,7 +540,7 @@ export class MCPCommandController { async #findConfiguredServer( name: string, ): Promise<{ filePath: string; scope: "user" | "project"; config: MCPServerConfig } | null> { - const cwd = process.cwd(); + const cwd = getProjectDir(); const userPath = getMCPConfigPath("user", cwd); const projectPath = getMCPConfigPath("project", cwd); @@ -665,7 +665,7 @@ export class MCPCommandController { async #handleWizardComplete(name: string, config: MCPServerConfig, scope: "user" | "project"): Promise { try { // Determine file path - const cwd = process.cwd(); + const cwd = getProjectDir(); const filePath = getMCPConfigPath(scope, cwd); // Add server to config @@ -747,7 +747,7 @@ export class MCPCommandController { */ async #handleList(): Promise { try { - const cwd = process.cwd(); + const cwd = getProjectDir(); // Load from both user and project configs const userPath = getMCPConfigPath("user", cwd); @@ -913,7 +913,7 @@ export class MCPCommandController { } try { - const cwd = process.cwd(); + const cwd = getProjectDir(); const userPath = getMCPConfigPath("user", cwd); const projectPath = getMCPConfigPath("project", cwd); const filePath = scope === "user" ? userPath : projectPath; @@ -957,7 +957,7 @@ export class MCPCommandController { let connection: MCPServerConnection | undefined; try { - const cwd = process.cwd(); + const cwd = getProjectDir(); const userPath = getMCPConfigPath("user", cwd); const projectPath = getMCPConfigPath("project", cwd); @@ -989,7 +989,7 @@ export class MCPCommandController { if (this.ctx.mcpManager) { resolvedConfig = await this.ctx.mcpManager.prepareConfig(config); } else { - const tempManager = new MCPManager(process.cwd()); + const tempManager = new MCPManager(getProjectDir()); tempManager.setAuthStorage(this.ctx.session.modelRegistry.authStorage); resolvedConfig = await tempManager.prepareConfig(config); } diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index 7da5bed99..77f52ba25 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -2,7 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { MODEL_ROLES } from "../../config/model-registry"; import { settings } from "../../config/settings"; import { DebugSelectorComponent } from "../../debug"; @@ -67,7 +67,7 @@ export class SelectorController { availableThinkingLevels: this.ctx.session.getAvailableThinkingLevels(), thinkingLevel: this.ctx.session.thinkingLevel, availableThemes, - cwd: process.cwd(), + cwd: getProjectDir(), }, { onChange: (id, value) => this.handleSettingChange(id, value), @@ -146,7 +146,7 @@ export class SelectorController { * Replaces /status with a unified view of all providers and extensions. */ async showExtensionsDashboard(): Promise { - const dashboard = await ExtensionDashboard.create(process.cwd(), this.ctx.settings, this.ctx.ui.terminal.rows); + const dashboard = await ExtensionDashboard.create(getProjectDir(), this.ctx.settings, this.ctx.ui.terminal.rows); this.showSelector(done => { dashboard.onClose = () => { done(); diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 99ab2570a..bd0ec5346 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -16,7 +16,7 @@ import { TUI, } from "@oh-my-pi/pi-tui"; import { $env, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils"; -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import chalk from "chalk"; import { KeybindingsManager } from "../config/keybindings"; import { renderPromptTemplate } from "../config/prompt-templates"; @@ -249,7 +249,7 @@ export class InteractiveMode implements InteractiveModeContext { this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush()); debugStartup("InteractiveMode.init:cleanupRegistered"); - await this.refreshSlashCommandState(process.cwd()); + await this.refreshSlashCommandState(getProjectDir()); debugStartup("InteractiveMode.init:slashCommands"); // Get current model info for welcome screen diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 700d44b1c..4d7e8685f 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -2,7 +2,7 @@ import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type Thinkin import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; import { $env, logger, postmortem } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import chalk from "chalk"; import { loadCapability } from "./capability"; import { type Rule, ruleCapability } from "./capability/rule"; @@ -85,7 +85,7 @@ const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.w // Types export interface CreateAgentSessionOptions { - /** Working directory for project-local discovery. Default: process.cwd() */ + /** Working directory for project-local discovery. Default: getProjectDir() */ cwd?: string; /** Global config directory. Default: ~/.omp/agent */ agentDir?: string; @@ -240,7 +240,7 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir( * Discover extensions from cwd. */ export async function discoverExtensions(cwd?: string): Promise { - const resolvedCwd = cwd ?? process.cwd(); + const resolvedCwd = cwd ?? getProjectDir(); return discoverAndLoadExtensions([], resolvedCwd); } @@ -255,7 +255,7 @@ export async function discoverSkills( ): Promise<{ skills: Skill[]; warnings: SkillWarning[] }> { return await loadSkillsInternal({ ...settings, - cwd: cwd ?? process.cwd(), + cwd: cwd ?? getProjectDir(), }); } @@ -268,7 +268,7 @@ export async function discoverContextFiles( _agentDir?: string, ): Promise> { return await loadContextFilesInternal({ - cwd: cwd ?? process.cwd(), + cwd: cwd ?? getProjectDir(), }); } @@ -277,7 +277,7 @@ export async function discoverContextFiles( */ export async function discoverPromptTemplates(cwd?: string, agentDir?: string): Promise { return await loadPromptTemplatesInternal({ - cwd: cwd ?? process.cwd(), + cwd: cwd ?? getProjectDir(), agentDir: agentDir ?? getDefaultAgentDir(), }); } @@ -286,14 +286,14 @@ export async function discoverPromptTemplates(cwd?: string, agentDir?: string): * Discover file-based slash commands from commands/ directories. */ export async function discoverSlashCommands(cwd?: string): Promise { - return loadSlashCommandsInternal({ cwd: cwd ?? process.cwd() }); + return loadSlashCommandsInternal({ cwd: cwd ?? getProjectDir() }); } /** * Discover custom commands (TypeScript slash commands) from cwd and agentDir. */ export async function discoverCustomTSCommands(cwd?: string, agentDir?: string): Promise { - const resolvedCwd = cwd ?? process.cwd(); + const resolvedCwd = cwd ?? getProjectDir(); const resolvedAgentDir = agentDir ?? getDefaultAgentDir(); return loadCustomCommandsInternal({ @@ -307,7 +307,7 @@ export async function discoverCustomTSCommands(cwd?: string, agentDir?: string): * Returns the manager and loaded tools. */ export async function discoverMCPServers(cwd?: string): Promise { - const resolvedCwd = cwd ?? process.cwd(); + const resolvedCwd = cwd ?? getProjectDir(); return discoverAndLoadMCPTools(resolvedCwd); } @@ -469,7 +469,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory { * model: myModel, * getApiKey: async () => Bun.env.MY_KEY, * systemPrompt: 'You are helpful.', - * tools: codingTools({ cwd: process.cwd() }), + * tools: codingTools({ cwd: getProjectDir() }), * skills: [], * sessionManager: SessionManager.inMemory(), * }); @@ -477,7 +477,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory { */ export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise { debugStartup("sdk:createAgentSession:entry"); - const cwd = options.cwd ?? process.cwd(); + const cwd = options.cwd ?? getProjectDir(); const agentDir = options.agentDir ?? getDefaultAgentDir(); const eventBus = options.eventBus ?? new EventBus(); diff --git a/packages/coding-agent/src/session/session-manager.ts b/packages/coding-agent/src/session/session-manager.ts index d3b2cf25d..c97fb2da2 100644 --- a/packages/coding-agent/src/session/session-manager.ts +++ b/packages/coding-agent/src/session/session-manager.ts @@ -3,7 +3,7 @@ import * as path from "node:path"; import type { AgentMessage } from "@oh-my-pi/pi-agent-core"; import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai"; import { isEnoent, logger, parseJsonlLenient, Snowflake } from "@oh-my-pi/pi-utils"; -import { getBlobsDir, getAgentDir as getDefaultAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getBlobsDir, getAgentDir as getDefaultAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { type BlobPutResult, BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store"; import { type BashExecutionMessage, @@ -2153,10 +2153,10 @@ export class SessionManager { sessionDir?: string, storage: SessionStorage = new FileSessionStorage(), ): Promise { - // Extract cwd from session header if possible, otherwise use process.cwd() + // Extract cwd from session header if possible, otherwise use getProjectDir() const entries = await loadEntriesFromFile(filePath, storage); const header = entries.find(e => e.type === "session") as SessionHeader | undefined; - const cwd = header?.cwd ?? process.cwd(); + const cwd = header?.cwd ?? getProjectDir(); // If no sessionDir provided, derive from file's parent directory const dir = sessionDir ?? path.resolve(filePath, ".."); const manager = new SessionManager(cwd, dir, true, storage); @@ -2188,7 +2188,10 @@ export class SessionManager { } /** Create an in-memory session (no file persistence) */ - static inMemory(cwd: string = process.cwd(), storage: SessionStorage = new MemorySessionStorage()): SessionManager { + static inMemory( + cwd: string = getProjectDir(), + storage: SessionStorage = new MemorySessionStorage(), + ): SessionManager { const manager = new SessionManager(cwd, "", false, storage); manager.#initNewSession(); return manager; diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index 2679e47ea..a45929ba4 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -4,7 +4,7 @@ import * as os from "node:os"; import { getSystemInfo as getNativeSystemInfo, type SystemInfo } from "@oh-my-pi/pi-natives"; import { $env, logger } from "@oh-my-pi/pi-utils"; -import { getGpuCachePath } from "@oh-my-pi/pi-utils/dirs"; +import { getGpuCachePath, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { $ } from "bun"; import { contextFileCapability } from "./capability/context-file"; import { systemPromptCapability } from "./capability/system-prompt"; @@ -362,7 +362,7 @@ export async function resolvePromptInput(input: string | undefined, description: } export interface LoadContextFilesOptions { - /** Working directory to start walking up from. Default: process.cwd() */ + /** Working directory to start walking up from. Default: getProjectDir() */ cwd?: string; } @@ -374,7 +374,7 @@ export interface LoadContextFilesOptions { export async function loadProjectContextFiles( options: LoadContextFilesOptions = {}, ): Promise> { - const resolvedCwd = options.cwd ?? process.cwd(); + const resolvedCwd = options.cwd ?? getProjectDir(); const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd }); @@ -404,7 +404,7 @@ export async function loadProjectContextFiles( * Returns combined content from all discovered SYSTEM.md files. */ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise { - const resolvedCwd = options.cwd ?? process.cwd(); + const resolvedCwd = options.cwd ?? getProjectDir(); const result = await loadCapability(systemPromptCapability.id, { cwd: resolvedCwd }); @@ -433,7 +433,7 @@ export interface BuildSystemPromptOptions { appendSystemPrompt?: string; /** Skills settings for discovery. */ skillsSettings?: SkillsSettings; - /** Working directory. Default: process.cwd() */ + /** Working directory. Default: getProjectDir() */ cwd?: string; /** Pre-loaded context files (skips discovery if provided). */ contextFiles?: Array<{ path: string; content: string; depth?: number }>; @@ -463,7 +463,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): preloadedSkills: providedPreloadedSkills, rules, } = options; - const resolvedCwd = cwd ?? process.cwd(); + const resolvedCwd = cwd ?? getProjectDir(); const resolvedCustomPrompt = await resolvePromptInput(customPrompt, "system prompt"); const resolvedAppendPrompt = await resolvePromptInput(appendSystemPrompt, "append system prompt"); diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 767ef9afa..01b9be652 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -4,6 +4,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb import type { Component } from "@oh-my-pi/pi-tui"; import { Text } from "@oh-my-pi/pi-tui"; import { $env, isEnoent } from "@oh-my-pi/pi-utils"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { type Static, Type } from "@sinclair/typebox"; import { renderPromptTemplate } from "../config/prompt-templates"; import { type BashResult, executeBash } from "../exec/bash-executor"; @@ -205,7 +206,7 @@ interface BashRenderContext { function formatBashCommand(args: BashRenderArgs, _uiTheme: Theme): string { const command = args.command || "…"; const prompt = "$"; - const cwd = process.cwd(); + const cwd = getProjectDir(); let displayWorkdir = args.cwd; if (displayWorkdir) { diff --git a/packages/coding-agent/src/tools/python.ts b/packages/coding-agent/src/tools/python.ts index 1c6944832..9a11bae99 100644 --- a/packages/coding-agent/src/tools/python.ts +++ b/packages/coding-agent/src/tools/python.ts @@ -4,6 +4,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb import type { ImageContent } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; import { Text } from "@oh-my-pi/pi-tui"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { type Static, Type } from "@sinclair/typebox"; import { renderPromptTemplate } from "../config/prompt-templates"; import type { RenderResultOptions } from "../extensibility/custom-tools/types"; @@ -836,7 +837,7 @@ export const pythonToolRenderer = { renderCall(args: PythonRenderArgs, uiTheme: Theme): Component { const ui = new ToolUIKit(uiTheme); const cells = args.cells ?? []; - const cwd = process.cwd(); + const cwd = getProjectDir(); let displayWorkdir = args.cwd; if (displayWorkdir) { diff --git a/packages/coding-agent/test/core/python-executor-lifecycle.test.ts b/packages/coding-agent/test/core/python-executor-lifecycle.test.ts index 59b05d4bb..3dc659c41 100644 --- a/packages/coding-agent/test/core/python-executor-lifecycle.test.ts +++ b/packages/coding-agent/test/core/python-executor-lifecycle.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "bun:test"; import { disposeAllKernelSessions, executePython } from "@oh-my-pi/pi-coding-agent/ipy/executor"; import type { KernelExecuteResult } from "@oh-my-pi/pi-coding-agent/ipy/kernel"; import * as pythonKernel from "@oh-my-pi/pi-coding-agent/ipy/kernel"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; class FakeKernel { execute = vi.fn(async () => this.result); @@ -36,7 +37,7 @@ describe("executePython lifecycle", () => { .spyOn(pythonKernel.PythonKernel, "start") .mockResolvedValue(kernel as unknown as pythonKernel.PythonKernel); - await executePython("print('hi')", { kernelMode: "per-call", cwd: process.cwd() }); + await executePython("print('hi')", { kernelMode: "per-call", cwd: getProjectDir() }); expect(startSpy).toHaveBeenCalledTimes(1); expect(kernel.execute).toHaveBeenCalledTimes(1); @@ -50,8 +51,8 @@ describe("executePython lifecycle", () => { .spyOn(pythonKernel.PythonKernel, "start") .mockResolvedValue(kernel as unknown as pythonKernel.PythonKernel); - await executePython("1 + 1", { kernelMode: "session", sessionId: "test-session", cwd: process.cwd() }); - await executePython("2 + 2", { kernelMode: "session", sessionId: "test-session", cwd: process.cwd() }); + await executePython("1 + 1", { kernelMode: "session", sessionId: "test-session", cwd: getProjectDir() }); + await executePython("2 + 2", { kernelMode: "session", sessionId: "test-session", cwd: getProjectDir() }); expect(startSpy).toHaveBeenCalledTimes(1); expect(kernel.execute).toHaveBeenCalledTimes(2); @@ -66,12 +67,12 @@ describe("executePython lifecycle", () => { .mockResolvedValueOnce(kernel as unknown as pythonKernel.PythonKernel) .mockResolvedValueOnce(kernelNext as unknown as pythonKernel.PythonKernel); - await executePython("1 + 1", { kernelMode: "session", sessionId: "reset-session", cwd: process.cwd() }); + await executePython("1 + 1", { kernelMode: "session", sessionId: "reset-session", cwd: getProjectDir() }); await executePython("2 + 2", { kernelMode: "session", sessionId: "reset-session", reset: true, - cwd: process.cwd(), + cwd: getProjectDir(), }); expect(startSpy).toHaveBeenCalledTimes(2); @@ -89,7 +90,7 @@ describe("executePython lifecycle", () => { .mockResolvedValueOnce(kernel as unknown as pythonKernel.PythonKernel) .mockResolvedValueOnce(kernelNext as unknown as pythonKernel.PythonKernel); - await executePython("1 + 1", { kernelMode: "session", sessionId: "dead-session", cwd: process.cwd() }); + await executePython("1 + 1", { kernelMode: "session", sessionId: "dead-session", cwd: getProjectDir() }); expect(startSpy).toHaveBeenCalledTimes(2); expect(kernel.shutdown).toHaveBeenCalledTimes(1); diff --git a/packages/coding-agent/test/core/python-prelude.test.ts b/packages/coding-agent/test/core/python-prelude.test.ts index 486beb689..4a5dafa27 100644 --- a/packages/coding-agent/test/core/python-prelude.test.ts +++ b/packages/coding-agent/test/core/python-prelude.test.ts @@ -4,10 +4,11 @@ import * as path from "node:path"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { resetPreludeDocsCache, warmPythonEnvironment } from "@oh-my-pi/pi-coding-agent/ipy/executor"; import { getPythonToolDescription, PythonTool } from "@oh-my-pi/pi-coding-agent/tools/python"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; const resolvePythonPath = (): string | null => { const venvPath = Bun.env.VIRTUAL_ENV; - const candidates = [venvPath, path.join(process.cwd(), ".venv"), path.join(process.cwd(), "venv")].filter( + const candidates = [venvPath, path.join(getProjectDir(), ".venv"), path.join(getProjectDir(), "venv")].filter( Boolean, ) as string[]; for (const candidate of candidates) { @@ -42,7 +43,7 @@ describe.skipIf(!shouldRun)("PYTHON_PRELUDE integration", () => { const helpers = ["env", "read", "write", "append", "rm", "mv", "cp", "find", "grep"]; const session = { - cwd: process.cwd(), + cwd: getProjectDir(), hasUI: false, getSessionFile: () => null, getSessionSpawns: () => null, @@ -75,7 +76,7 @@ describe.skipIf(!shouldRun)("PYTHON_PRELUDE integration", () => { it("exposes prelude docs via warmup", async () => { resetPreludeDocsCache(); - const result = await warmPythonEnvironment(process.cwd()); + const result = await warmPythonEnvironment(getProjectDir()); expect(result.ok).toBe(true); const names = result.docs.map(doc => doc.name); expect(names).toContain("read"); @@ -83,7 +84,7 @@ describe.skipIf(!shouldRun)("PYTHON_PRELUDE integration", () => { it("renders prelude docs in python tool description", async () => { resetPreludeDocsCache(); - const result = await warmPythonEnvironment(process.cwd()); + const result = await warmPythonEnvironment(getProjectDir()); expect(result.ok).toBe(true); const description = getPythonToolDescription(); expect(description).toContain("read"); diff --git a/packages/natives/test/native.test.ts b/packages/natives/test/native.test.ts index 5e42ea7d3..9ec91c810 100644 --- a/packages/natives/test/native.test.ts +++ b/packages/natives/test/native.test.ts @@ -2,6 +2,7 @@ import { beforeAll, describe, expect, it } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import { FileType, fuzzyFind, type GlobMatch, glob, grep, htmlToMarkdown, invalidateFsScanCache } from "../src/index"; let testDir: string; @@ -135,7 +136,7 @@ describe("pi-natives", () => { const newFile = path.join(testDir, "newly-added.ts"); await fs.writeFile(newFile, "export const newer = true;\n"); - const relativePath = path.relative(process.cwd(), newFile); + const relativePath = path.relative(getProjectDir(), newFile); invalidateFsScanCache(relativePath); const result = await glob({ pattern: "newly-added.ts", path: testDir, cache: true }); diff --git a/packages/tui/README.md b/packages/tui/README.md index ec438b65e..bf7f9114c 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -443,6 +443,7 @@ Supports both slash commands and file paths. ```typescript import { CombinedAutocompleteProvider } from "@oh-my-pi/pi-tui"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; const provider = new CombinedAutocompleteProvider( [ @@ -450,7 +451,7 @@ const provider = new CombinedAutocompleteProvider( { name: "clear", description: "Clear screen" }, { name: "delete", description: "Delete last message" }, ], - process.cwd() // base path for file completion + getProjectDir() // base path for file completion ); editor.setAutocompleteProvider(provider); diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 2e0323619..2e2a574f3 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -2,6 +2,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { fuzzyFind } from "@oh-my-pi/pi-natives"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]); @@ -203,7 +204,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { #dirCache: Map = new Map(); readonly #DIR_CACHE_TTL = 2000; // 2 seconds - constructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd()) { + constructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = getProjectDir()) { this.#commands = commands; this.#basePath = basePath; } diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 046e7e108..7fbc02098 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -1,3 +1,4 @@ +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete"; import { type EditorKeybindingsManager, getEditorKeybindings } from "../keybindings"; import { matchesKey } from "../keys"; @@ -419,7 +420,7 @@ export class Editor implements Component, Focusable { this.#history.pop(); } - this.#historyStorage?.add(trimmed, process.cwd()); + this.#historyStorage?.add(trimmed, getProjectDir()); } #isEditorEmpty(): boolean { diff --git a/packages/tui/test/chat-simple.ts b/packages/tui/test/chat-simple.ts index 1b71ad42c..66742b716 100644 --- a/packages/tui/test/chat-simple.ts +++ b/packages/tui/test/chat-simple.ts @@ -8,6 +8,7 @@ import { Markdown } from "@oh-my-pi/pi-tui/components/markdown"; import { Text } from "@oh-my-pi/pi-tui/components/text"; import { ProcessTerminal } from "@oh-my-pi/pi-tui/terminal"; import { TUI } from "@oh-my-pi/pi-tui/tui"; +import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; import chalk from "chalk"; import { defaultEditorTheme, defaultMarkdownTheme } from "./test-themes"; @@ -31,7 +32,7 @@ const autocompleteProvider = new CombinedAutocompleteProvider( { name: "delete", description: "Delete the last message" }, { name: "clear", description: "Clear all messages" }, ], - process.cwd(), + getProjectDir(), ); editor.setAutocompleteProvider(autocompleteProvider); diff --git a/packages/utils/src/dirs.ts b/packages/utils/src/dirs.ts index 2f8a2a721..0fdfeadf7 100644 --- a/packages/utils/src/dirs.ts +++ b/packages/utils/src/dirs.ts @@ -4,6 +4,8 @@ * Uses PI_CONFIG_DIR (default ".omp") for the config root and * PI_CODING_AGENT_DIR to override the agent directory. */ + +import { realpathSync } from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { version } from "../package.json" with { type: "json" }; @@ -22,6 +24,24 @@ export const VERSION: string = version; // Root directories // ============================================================================= +let projectDir = process.cwd(); +if ($env.PWD) { + if (realpathSync($env.PWD) === projectDir) { + projectDir = $env.PWD; + } +} + +/** Get the project directory. */ +export function getProjectDir(): string { + return projectDir; +} + +/** Set the project directory. */ +export function setProjectDir(dir: string): void { + projectDir = path.resolve(dir); + process.chdir(projectDir); +} + /** Get the config root directory (~/.omp). */ export function getConfigRootDir(): string { return path.join(os.homedir(), $env.PI_CONFIG_DIR || CONFIG_DIR_NAME); @@ -42,7 +62,7 @@ export function getAgentDir(): string { } /** Get the project-local config directory (.omp). */ -export function getProjectAgentDir(cwd: string = process.cwd()): string { +export function getProjectAgentDir(cwd: string = getProjectDir()): string { return path.join(cwd, CONFIG_DIR_NAME); } @@ -225,17 +245,17 @@ export function getDebugLogPath(agentDir?: string): string { // ============================================================================= /** Get the project-level Python modules directory (.omp/modules). */ -export function getProjectModulesDir(cwd: string = process.cwd()): string { +export function getProjectModulesDir(cwd: string = getProjectDir()): string { return path.join(getProjectAgentDir(cwd), "modules"); } /** Get the project-level prompts directory (.omp/prompts). */ -export function getProjectPromptsDir(cwd: string = process.cwd()): string { +export function getProjectPromptsDir(cwd: string = getProjectDir()): string { return path.join(getProjectAgentDir(cwd), "prompts"); } /** Get the project-level plugin overrides path (.omp/plugin-overrides.json). */ -export function getProjectPluginOverridesPath(cwd: string = process.cwd()): string { +export function getProjectPluginOverridesPath(cwd: string = getProjectDir()): string { return path.join(getProjectAgentDir(cwd), "plugin-overrides.json"); } @@ -244,7 +264,7 @@ export function getProjectPluginOverridesPath(cwd: string = process.cwd()): stri // ============================================================================= /** Get the primary MCP config file path (first candidate). */ -export function getMCPConfigPath(scope: "user" | "project", cwd: string = process.cwd()): string { +export function getMCPConfigPath(scope: "user" | "project", cwd: string = getProjectDir()): string { if (scope === "user") { return path.join(getAgentDir(), "mcp.json"); } diff --git a/packages/utils/src/glob.ts b/packages/utils/src/glob.ts index 1b95d7d00..96201eb62 100644 --- a/packages/utils/src/glob.ts +++ b/packages/utils/src/glob.ts @@ -1,8 +1,9 @@ import * as path from "node:path"; import { Glob } from "bun"; +import { getProjectDir } from "./dirs"; export interface GlobPathsOptions { - /** Base directory for glob patterns. Defaults to process.cwd(). */ + /** Base directory for glob patterns. Defaults to getProjectDir(). */ cwd?: string; /** Glob exclusion patterns. */ exclude?: string[]; @@ -126,7 +127,7 @@ export async function loadGitignorePatterns(baseDir: string): Promise /** * Resolve filesystem paths matching glob patterns with optional exclude filters. - * Returns paths relative to the provided cwd (or process.cwd()). + * Returns paths relative to the provided cwd (or getProjectDir()). * Errors and abort/timeouts are surfaced to the caller. */ export async function globPaths(patterns: string | string[], options: GlobPathsOptions = {}): Promise { @@ -140,11 +141,11 @@ export async function globPaths(patterns: string | string[], options: GlobPathsO let effectiveExclude = exclude ? [...baseExclude, ...exclude] : baseExclude; if (gitignore) { - const gitignorePatterns = await loadGitignorePatterns(cwd ?? process.cwd()); + const gitignorePatterns = await loadGitignorePatterns(cwd ?? getProjectDir()); effectiveExclude = [...effectiveExclude, ...gitignorePatterns]; } - const base = cwd ?? process.cwd(); + const base = cwd ?? getProjectDir(); const allResults: string[] = []; // Combine timeout and abort signals