refactor: remove pi-ref lazy barrel import, fix test mocks for Bun 1.3.11

- Remove pi-ref.ts deferred barrel import (no longer needed after circular dep fix)
- Update extension/hook/custom-tool/custom-command loaders to use direct imports
- Fix warmPythonEnvironment mock return type in python tool tests (add docs field)
This commit is contained in:
can1357
2026-04-08 12:21:17 +02:00
parent 003eccce52
commit edab058993
8 changed files with 30 additions and 76 deletions
@@ -10,7 +10,6 @@ import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils
import * as typebox from "@sinclair/typebox";
import { getConfigDirs } from "../../config";
import { execCommand } from "../../exec/exec";
import { getPiRef, initPiRef } from "../pi-ref";
import { GreenCommand } from "./bundled/ci-green";
import { ReviewCommand } from "./bundled/review";
import type {
@@ -169,7 +168,6 @@ function loadBundledCommands(sharedApi: CustomCommandAPI): LoadedCustomCommand[]
* Discover and load custom commands from standard locations.
*/
export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}): Promise<CustomCommandsLoadResult> {
await initPiRef();
const cwd = options.cwd ?? getProjectDir();
const agentDir = options.agentDir ?? getAgentDir();
@@ -185,7 +183,7 @@ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}
exec: (command: string, args: string[], execOptions) =>
execCommand(command, args, execOptions?.cwd ?? cwd, execOptions),
typebox,
pi: getPiRef(),
pi: await import("@oh-my-pi/pi-coding-agent"),
};
// 1. Load bundled commands first (lowest priority - can be overridden)
@@ -14,7 +14,6 @@ import { execCommand } from "../../exec/exec";
import type { HookUIContext } from "../../extensibility/hooks/types";
import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
import type { PendingActionStore } from "../../tools/pending-action";
import { getPiRef, initPiRef } from "../pi-ref";
import { createNoOpUIContext, resolvePath } from "../utils";
import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
@@ -85,7 +84,12 @@ export class CustomToolLoader {
#sharedApi: CustomToolAPI;
#seenNames: Set<string>;
constructor(cwd: string, builtInToolNames: string[], pendingActionStore?: PendingActionStore) {
constructor(
pi: typeof import("@oh-my-pi/pi-coding-agent"),
cwd: string,
builtInToolNames: string[],
pendingActionStore?: PendingActionStore,
) {
this.#sharedApi = {
cwd,
exec: (command: string, args: string[], options?: ExecOptions) =>
@@ -94,7 +98,7 @@ export class CustomToolLoader {
hasUI: false,
logger,
typebox,
pi: getPiRef(),
pi,
pushPendingAction: action => {
if (!pendingActionStore) {
throw new Error("Pending action store unavailable for custom tools in this runtime.");
@@ -157,8 +161,12 @@ export async function loadCustomTools(
builtInToolNames: string[],
pendingActionStore?: PendingActionStore,
) {
await initPiRef();
const loader = new CustomToolLoader(cwd, builtInToolNames, pendingActionStore);
const loader = new CustomToolLoader(
await import("@oh-my-pi/pi-coding-agent"),
cwd,
builtInToolNames,
pendingActionStore,
);
await loader.load(pathsWithSources);
return {
tools: loader.tools,
@@ -185,7 +193,6 @@ export async function discoverAndLoadCustomTools(
builtInToolNames: string[],
pendingActionStore?: PendingActionStore,
) {
await initPiRef();
const allPathsWithSources: ToolPathWithSource[] = [];
const seen = new Set<string>();
@@ -17,7 +17,6 @@ import type { ExecOptions } from "../../exec/exec";
import { execCommand } from "../../exec/exec";
import type { CustomMessage } from "../../session/messages";
import { EventBus } from "../../utils/event-bus";
import { getPiRef, initPiRef } from "../pi-ref";
import { getAllPluginExtensionPaths } from "../plugins/loader";
import { resolvePath } from "../utils";
import type {
@@ -102,7 +101,6 @@ export class ExtensionRuntime implements IExtensionRuntime {
class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
readonly logger = logger;
readonly typebox = TypeBox;
readonly pi = getPiRef();
readonly flagValues = new Map<string, boolean | string>();
readonly pendingProviderRegistrations: Array<{
name: string;
@@ -111,6 +109,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
}> = [];
constructor(
public readonly pi: typeof import("@oh-my-pi/pi-coding-agent"),
private readonly extension: Extension,
private readonly runtime: IExtensionRuntime,
private readonly cwd: string,
@@ -265,7 +264,13 @@ async function loadExtension(
}
const extension = createExtension(extensionPath, resolvedPath);
const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus);
const api = new ConcreteExtensionAPI(
await import("@oh-my-pi/pi-coding-agent"),
extension,
runtime,
cwd,
eventBus,
);
await factory(api);
return { extension, error: null };
@@ -285,9 +290,8 @@ export async function loadExtensionFromFactory(
runtime: IExtensionRuntime,
name = "<inline>",
): Promise<Extension> {
await initPiRef();
const extension = createExtension(name, name);
const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus);
const api = new ConcreteExtensionAPI(await import("@oh-my-pi/pi-coding-agent"), extension, runtime, cwd, eventBus);
await factory(api);
return extension;
}
@@ -296,7 +300,6 @@ export async function loadExtensionFromFactory(
* Load extensions from paths.
*/
export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
await initPiRef();
const extensions: Extension[] = [];
const errors: Array<{ path: string; error: string }> = [];
const resolvedEventBus = eventBus ?? new EventBus();
@@ -455,7 +458,6 @@ export async function discoverAndLoadExtensions(
eventBus?: EventBus,
disabledExtensionIds: string[] = [],
): Promise<LoadExtensionsResult> {
await initPiRef();
const allPaths: string[] = [];
const seen = new Set<string>();
const disabled = new Set(disabledExtensionIds);
@@ -9,7 +9,6 @@ import type { Hook } from "../../discovery";
import { loadCapability } from "../../discovery";
import type { HookMessage } from "../../session/messages";
import type { SessionManager } from "../../session/session-manager";
import { getPiRef, initPiRef } from "../pi-ref";
import { resolvePath } from "../utils";
import { execCommand } from "./runner";
import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types";
@@ -87,16 +86,16 @@ export interface LoadHooksResult {
* Create a HookAPI instance that collects handlers, renderers, and commands.
* Returns the API, maps, and functions to set handlers later.
*/
function createHookAPI(
async function createHookAPI(
handlers: Map<string, HandlerFn[]>,
cwd: string,
): {
): Promise<{
api: HookAPI;
messageRenderers: Map<string, HookMessageRenderer>;
commands: Map<string, RegisteredCommand>;
setSendMessageHandler: (handler: SendMessageHandler) => void;
setAppendEntryHandler: (handler: AppendEntryHandler) => void;
} {
}> {
let sendMessageHandler: SendMessageHandler | null = null;
let appendEntryHandler: AppendEntryHandler | null = null;
const messageRenderers = new Map<string, HookMessageRenderer>();
@@ -137,7 +136,7 @@ function createHookAPI(
},
logger,
typebox,
pi: getPiRef(),
pi: await import("@oh-my-pi/pi-coding-agent"),
} as HookAPI;
return {
@@ -170,7 +169,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
// Create handlers map and API
const handlers = new Map<string, HandlerFn[]>();
const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI(
const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = await createHookAPI(
handlers,
cwd,
);
@@ -202,7 +201,6 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
* @param cwd - Current working directory for resolving relative paths
*/
export async function loadHooks(paths: string[], cwd: string): Promise<LoadHooksResult> {
await initPiRef();
const hooks: LoadedHook[] = [];
const errors: Array<{ path: string; error: string }> = [];
@@ -232,7 +230,6 @@ export async function loadHooks(paths: string[], cwd: string): Promise<LoadHooks
* Plus any explicitly configured paths from settings.
*/
export async function discoverAndLoadHooks(configuredPaths: string[], cwd: string): Promise<LoadHooksResult> {
await initPiRef();
const allPaths: string[] = [];
const seen = new Set<string>();
@@ -1,47 +0,0 @@
/**
* Lazy reference to the package barrel.
*
* Extension/hook/custom-tool/custom-command loaders expose the full
* `@oh-my-pi/pi-coding-agent` namespace to user code. A static
* `import * as piCodingAgent from "@oh-my-pi/pi-coding-agent"` from any of
* those loaders creates a self-referential cycle during module init:
*
* tools/index -> task -> sdk -> <loader> -> package barrel
* -> modes/components -> tool-execution -> renderers -> tools/read
*
* Combined with top-level await transitively pulled in by lru-cache, ESM
* interleaves the cyclic branches and leaves `readToolRenderer` in its
* temporal dead zone when `renderers.ts` reaches the `read` entry.
*
* This module breaks the cycle by deferring the barrel import.
* `initPiRef()` must be awaited once, after the package has finished its
* own initialization, before any loader reads the reference via
* `getPiRef()`. `createAgentSession` (and `discoverCustomTSCommands`) are
* responsible for this single call; both run entirely after module eval.
*/
import type * as PiCodingAgentNs from "@oh-my-pi/pi-coding-agent";
type PiCodingAgent = typeof PiCodingAgentNs;
let cached: PiCodingAgent | undefined;
/**
* Resolve the package barrel once. Safe to call from any async entry point
* that runs after all modules have finished evaluating.
*/
export async function initPiRef(): Promise<void> {
if (cached) return;
cached = (await import("@oh-my-pi/pi-coding-agent")) as PiCodingAgent;
}
/**
* Return the cached package barrel. `initPiRef()` must have resolved
* before this is called; loaders that run inside `createAgentSession`
* or `discoverCustomTSCommands` satisfy that requirement automatically.
*/
export function getPiRef(): PiCodingAgent {
if (!cached) {
throw new Error("pi-ref not initialized; await initPiRef() before instantiating loaders that expose `pi`.");
}
return cached;
}
-3
View File
@@ -58,7 +58,6 @@ import {
type ToolDefinition,
wrapRegisteredTools,
} from "./extensibility/extensions";
import { initPiRef } from "./extensibility/pi-ref";
import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from "./extensibility/skills";
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands";
import {
@@ -350,7 +349,6 @@ export async function discoverSlashCommands(cwd?: string): Promise<FileSlashComm
* Discover custom commands (TypeScript slash commands) from cwd and agentDir.
*/
export async function discoverCustomTSCommands(cwd?: string, agentDir?: string): Promise<CustomCommandsLoadResult> {
await initPiRef();
const resolvedCwd = cwd ?? getProjectDir();
const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
@@ -642,7 +640,6 @@ function buildMCPPromptCommands(manager: MCPManager): LoadedCustomCommand[] {
* ```
*/
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
await initPiRef();
const cwd = options.cwd ?? getProjectDir();
const agentDir = options.agentDir ?? getDefaultAgentDir();
const eventBus = options.eventBus ?? new EventBus();
@@ -56,7 +56,7 @@ describe("python tool settings", () => {
});
it("passes kernel mode from settings to executor", async () => {
vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true });
vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true, docs: [] });
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
output: "ok",
exitCode: 0,
@@ -23,7 +23,7 @@ function createSession(cwd: string): ToolSession {
describe("python tool execution", () => {
it("passes kernel options from settings and args", async () => {
const tempDir = TempDir.createSync("@python-tool-");
vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true });
vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true, docs: [] });
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
output: "ok",
exitCode: 0,