refactor: implemented $PWD > cwd to prevent macOS symlink resolution

- Replaced all direct `process.cwd()` calls with `getProjectDir()` utility function across 40+ files to centralize project directory resolution logic.
- Added `getProjectDir()` and `setProjectDir()` functions to `@oh-my-pi/pi-utils/dirs` module to provide abstracted project directory management.
- Made `SessionManager.list()` method asynchronous to support asynchronous session discovery operations.
- Updated default working directory resolution throughout codebase to use `getProjectDir()` instead of `process.cwd()` for improved project directory detection.
This commit is contained in:
can1357
2026-02-13 23:39:48 +01:00
parent f8e915e14e
commit d1e16d2101
43 changed files with 161 additions and 111 deletions
+3
View File
@@ -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
@@ -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)}…"`);
@@ -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<T>(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);
@@ -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;
@@ -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) {
+2 -2
View File
@@ -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<void> {
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);
@@ -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<void> {
const cwd = process.cwd();
const cwd = getProjectDir();
const git = new ControlledGit(cwd);
const [settings, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
+2 -1
View File
@@ -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<void> {
}
async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
const cwd = process.cwd();
const cwd = getProjectDir();
const settings = await Settings.init();
const commitSettings = settings.getGroup("commit");
const authStorage = await discoverAuthStorage();
+6 -6
View File
@@ -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<string>();
@@ -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<PromptTemplate[]> {
const resolvedCwd = options.cwd ?? process.cwd();
const resolvedCwd = options.cwd ?? getProjectDir();
const resolvedAgentDir = options.agentDir ?? getPromptsDir();
const templates: PromptTemplate[] = [];
+2 -2
View File
@@ -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;
@@ -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<SystemInfo> {
bun: Bun.version,
node: process.version,
},
cwd: process.cwd(),
cwd: getProjectDir(),
shell,
terminal,
};
@@ -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<DiscoverCustomCommandsResult> {
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<string>();
@@ -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<CustomCommandsLoadResult> {
const cwd = options.cwd ?? process.cwd();
const cwd = options.cwd ?? getProjectDir();
const agentDir = options.agentDir ?? getAgentDir();
const { paths } = await discoverCustomCommands({ cwd, agentDir });
@@ -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<InstalledPlugin[]> {
}
export async function linkPlugin(localPath: string): Promise<void> {
const cwd = process.cwd();
const cwd = getProjectDir();
const absolutePath = path.resolve(cwd, localPath);
// Validate that resolved path is within cwd to prevent path traversal
@@ -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;
}
@@ -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<LoadSkillsResult> {
}
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<LoadSkillsResult> {
const {
cwd = process.cwd(),
cwd = getProjectDir(),
enabled = true,
enableCodexUser = true,
enableClaudeUser = true,
@@ -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;
}
+2 -2
View File
@@ -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<PythonResult> {
const cwd = options?.cwd ?? process.cwd();
const cwd = options?.cwd ?? getProjectDir();
await ensureKernelAvailable(cwd);
const kernelMode = options?.kernelMode ?? "session";
+3 -3
View File
@@ -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<PythonModu
* Discover Python prelude extension modules from user and project directories.
*/
export async function discoverPythonModules(options: DiscoverPythonModulesOptions = {}): Promise<PythonModuleEntry[]> {
const cwd = options.cwd ?? process.cwd();
const cwd = options.cwd ?? getProjectDir();
const userDir = getAgentModulesDir(options.agentDir);
const projectDir = getProjectModulesDir(cwd);
+9 -7
View File
@@ -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<void> {
}
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<void> {
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<void> {
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<CreateAgentSessionOptions> {
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<v
process.exit(1);
}
const cwd = process.cwd();
const cwd = getProjectDir();
await Settings.init({ cwd });
debugStartup("main:Settings.init");
time("Settings.init");
@@ -6,6 +6,7 @@
*/
import { readJsonl } from "@oh-my-pi/pi-utils";
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
import { type Subprocess, spawn } from "bun";
import type { JsonRpcResponse, MCPRequestOptions, MCPStdioServerConfig, MCPTransport } from "../../mcp/types";
@@ -54,7 +55,7 @@ export class StdioTransport implements MCPTransport {
this.#process = spawn({
cmd: [this.config.command, ...args],
cwd: this.config.cwd ?? process.cwd(),
cwd: this.config.cwd ?? getProjectDir(),
env,
stdin: "pipe",
stdout: "pipe",
@@ -3,6 +3,7 @@ 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 { isEnoent } from "@oh-my-pi/pi-utils";
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
import { theme } from "../../modes/theme/theme";
import type { AgentSession } from "../../session/agent-session";
import { shortenPath } from "../../tools/render-utils";
@@ -21,7 +22,7 @@ function sanitizeStatusText(text: string): string {
/** Find the git root by walking up from cwd. Returns path and content of .git/HEAD if found. */
async function findGitHeadPath(): Promise<{ path: string; content: string } | null> {
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();
@@ -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)) {
@@ -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);
@@ -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;
@@ -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);
@@ -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<void> {
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<void> {
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);
}
@@ -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<void> {
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();
@@ -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
+11 -11
View File
@@ -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<LoadExtensionsResult> {
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<Array<{ path: string; content: string; depth?: number }>> {
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<PromptTemplate[]> {
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<FileSlashCommand[]> {
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<CustomCommandsLoadResult> {
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<MCPToolsLoadResult> {
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<CreateAgentSessionResult> {
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();
@@ -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<SessionManager> {
// 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;
+6 -6
View File
@@ -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<Array<{ path: string; content: string; depth?: number }>> {
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<string | null> {
const resolvedCwd = options.cwd ?? process.cwd();
const resolvedCwd = options.cwd ?? getProjectDir();
const result = await loadCapability<SystemPromptFile>(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");
+2 -1
View File
@@ -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) {
+2 -1
View File
@@ -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) {
@@ -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);
@@ -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");
+2 -1
View File
@@ -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 });
+2 -1
View File
@@ -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);
+2 -1
View File
@@ -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<string, { entries: fs.Dirent[]; timestamp: number }> = 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;
}
+2 -1
View File
@@ -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 {
+2 -1
View File
@@ -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);
+25 -5
View File
@@ -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");
}
+5 -4
View File
@@ -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<string[]>
/**
* 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<string[]> {
@@ -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