@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/dist/cli.js +3556 -3504
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +917 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +270 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/config-cli.ts +15 -3
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/backend.ts +3 -3
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/prelude.py +28 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +28 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +26 -4
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +24 -5
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/types.ts +1 -1
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +61 -34
- package/src/session/agent-session.ts +283 -39
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
package/src/eval/py/prelude.py
CHANGED
|
@@ -57,6 +57,27 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
57
57
|
|
|
58
58
|
_OMP_INTERNAL_URL_RE = re.compile(r"^([a-z][a-z0-9+.-]*)://(.*)$", re.IGNORECASE)
|
|
59
59
|
|
|
60
|
+
def _should_delegate_read(path: str | Path) -> bool:
|
|
61
|
+
return (
|
|
62
|
+
isinstance(path, str)
|
|
63
|
+
and _OMP_INTERNAL_URL_RE.match(path) is not None
|
|
64
|
+
and not path.lower().startswith("local://")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _read_line_selector(offset: int, limit: int | None) -> str | None:
|
|
68
|
+
if offset <= 1 and limit is None:
|
|
69
|
+
return None
|
|
70
|
+
start = max(1, offset)
|
|
71
|
+
if limit is None:
|
|
72
|
+
return f"{start}-"
|
|
73
|
+
return f"{start}-{start + limit - 1}"
|
|
74
|
+
|
|
75
|
+
def _read_tool_text(path: str) -> str:
|
|
76
|
+
result = _bridge_call("read", {"path": path})
|
|
77
|
+
if isinstance(result, dict) and "text" in result:
|
|
78
|
+
return result["text"]
|
|
79
|
+
return result
|
|
80
|
+
|
|
60
81
|
def _resolve_omp_path(path: str | Path) -> Path:
|
|
61
82
|
"""Map a helper path to a real filesystem Path.
|
|
62
83
|
|
|
@@ -94,7 +115,13 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
94
115
|
return Path(resolved)
|
|
95
116
|
|
|
96
117
|
def read(path: str | Path, offset: int = 1, limit: int | None = None) -> str:
|
|
97
|
-
"""Read file contents. offset/limit are 1-indexed
|
|
118
|
+
"""Read file or read-tool URI contents. offset/limit are 1-indexed lines."""
|
|
119
|
+
if _should_delegate_read(path):
|
|
120
|
+
if limit is not None and limit <= 0:
|
|
121
|
+
return ""
|
|
122
|
+
selector = _read_line_selector(offset, limit)
|
|
123
|
+
tool_path = path if selector is None else f"{path}:{selector}"
|
|
124
|
+
return _read_tool_text(tool_path)
|
|
98
125
|
p = _resolve_omp_path(path)
|
|
99
126
|
data = p.read_text(encoding="utf-8")
|
|
100
127
|
lines = data.splitlines(keepends=True)
|
|
@@ -40,6 +40,19 @@ export function shouldHideKernelWindow(opts: {
|
|
|
40
40
|
return !opts.hostHasInheritableConsole;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Keep eval kernels outside the host's POSIX terminal session.
|
|
45
|
+
*
|
|
46
|
+
* User code can start an interactive shell which calls `tcsetpgrp(3)`. If the
|
|
47
|
+
* kernel shares OMP's session, that shell can replace OMP as the controlling
|
|
48
|
+
* terminal's foreground process group and the host is then stopped by SIGTTIN
|
|
49
|
+
* on its next stdin read. Bun implements `detached: true` with `setsid(2)` on
|
|
50
|
+
* POSIX, making the kernel a session leader with no controlling terminal.
|
|
51
|
+
*/
|
|
52
|
+
export function shouldDetachKernel(platform: NodeJS.Platform): boolean {
|
|
53
|
+
return platform !== "win32";
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
/**
|
|
44
57
|
* TTY-based fallback used when the Win32 console probe is unavailable.
|
|
45
58
|
*
|
|
@@ -15,6 +15,7 @@ export interface PyToolBridgeEntry {
|
|
|
15
15
|
toolSession: ToolSession;
|
|
16
16
|
signal?: AbortSignal;
|
|
17
17
|
emitStatus?: (event: JsStatusEvent) => void;
|
|
18
|
+
abortRequested?: () => boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface PyToolBridgeInfo {
|
|
@@ -31,23 +32,21 @@ const registrations = new Map<string, PyToolBridgeEntry>();
|
|
|
31
32
|
let serverPromise: Promise<BridgeServer> | null = null;
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
* Forward a bridge call to {@link callSessionTool}
|
|
35
|
-
*
|
|
36
|
-
* to fully tear down.
|
|
35
|
+
* Forward a bridge call to {@link callSessionTool} while respecting eval abort
|
|
36
|
+
* shielding.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* persistent state lost while the subagents were still winding down. Responding
|
|
46
|
-
* immediately on abort lets the kernel raise through the blocked call and settle
|
|
47
|
-
* cleanly (preserving state); the already-signaled call keeps tearing down in
|
|
48
|
-
* the background, its eventual result/rejection swallowed.
|
|
38
|
+
* Python invokes this bridge with blocking `urllib` requests from worker threads
|
|
39
|
+
* (each `agent()` / `tool.*` call). The base executor defers the registered
|
|
40
|
+
* signal while a bridge call is already paused so in-flight subagents can finish
|
|
41
|
+
* and persist output instead of being orphaned. Once an abort has been requested,
|
|
42
|
+
* later bridge calls are rejected before starting; once the shielded signal
|
|
43
|
+
* finally aborts, this handler still resolves the HTTP request promptly so the
|
|
44
|
+
* kernel can unwind without being hard-killed.
|
|
49
45
|
*/
|
|
50
46
|
async function callSessionToolPromptOnAbort(name: string, args: unknown, entry: PyToolBridgeEntry): Promise<unknown> {
|
|
47
|
+
if (entry.abortRequested?.()) {
|
|
48
|
+
throw new Error(`bridge call ${JSON.stringify(name)} aborted: eval cell was interrupted`);
|
|
49
|
+
}
|
|
51
50
|
const call = callSessionTool(name, args, {
|
|
52
51
|
session: entry.toolSession,
|
|
53
52
|
signal: entry.signal,
|
package/src/eval/rb/kernel.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { $ } from "bun";
|
|
|
16
16
|
import { Settings } from "../../config/settings";
|
|
17
17
|
import { BaseKernel, getRemainingTimeMs, type KernelRuntimeEnv, type KernelStartOptions } from "../kernel-base";
|
|
18
18
|
import type { KernelDisplayOutput } from "../py/display";
|
|
19
|
-
import { hostHasInheritableConsole, shouldHideKernelWindow } from "../py/spawn-options";
|
|
19
|
+
import { hostHasInheritableConsole, shouldDetachKernel, shouldHideKernelWindow } from "../py/spawn-options";
|
|
20
20
|
import { RUBY_PRELUDE } from "./prelude";
|
|
21
21
|
import RUNNER_SCRIPT from "./runner.rb" with { type: "text" };
|
|
22
22
|
import {
|
|
@@ -186,6 +186,7 @@ export class RubyKernel extends BaseKernel<KernelExecuteOptions> {
|
|
|
186
186
|
|
|
187
187
|
const proc = Bun.spawn([runtime.rubyPath, scriptPath], {
|
|
188
188
|
cwd: options.cwd,
|
|
189
|
+
detached: shouldDetachKernel(process.platform),
|
|
189
190
|
env: spawnEnv,
|
|
190
191
|
stdin: "pipe",
|
|
191
192
|
stdout: "pipe",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses brush-core via native bindings for shell execution.
|
|
5
5
|
*/
|
|
6
6
|
import { ExponentialYield } from "@oh-my-pi/pi-agent-core/utils/yield";
|
|
7
|
-
import {
|
|
7
|
+
import { type MinimizerOptions, Shell, type ShellRunResult } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { isExecutable, type ShellConfig } from "@oh-my-pi/pi-utils/procmgr";
|
|
9
9
|
import { Settings, type ShellMinimizerSettings } from "../config/settings";
|
|
10
10
|
import { OutputSink } from "../session/streaming-output";
|
|
@@ -250,6 +250,11 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
const shellOptions = {
|
|
254
|
+
sessionEnv: shellEnv,
|
|
255
|
+
snapshotPath: snapshotPath ?? undefined,
|
|
256
|
+
minimizer,
|
|
257
|
+
};
|
|
253
258
|
const sessionKey = buildSessionKey(shell, prefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
|
|
254
259
|
const persistentSessionBroken = brokenShellSessions.has(sessionKey);
|
|
255
260
|
if (persistentSessionBroken) {
|
|
@@ -264,13 +269,10 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
264
269
|
const sessionBusy = shellSessionsInUse.has(sessionKey);
|
|
265
270
|
let shellSession = persistentSessionBroken || sessionBusy ? undefined : shellSessions.get(sessionKey);
|
|
266
271
|
if (!shellSession && !persistentSessionBroken && !sessionBusy) {
|
|
267
|
-
shellSession = new Shell(
|
|
268
|
-
sessionEnv: shellEnv,
|
|
269
|
-
snapshotPath: snapshotPath ?? undefined,
|
|
270
|
-
minimizer,
|
|
271
|
-
});
|
|
272
|
+
shellSession = new Shell(shellOptions);
|
|
272
273
|
shellSessions.set(sessionKey, shellSession);
|
|
273
274
|
}
|
|
275
|
+
const executionShell = shellSession ?? new Shell(shellOptions);
|
|
274
276
|
const ownsPersistentSession = shellSession !== undefined;
|
|
275
277
|
if (ownsPersistentSession) {
|
|
276
278
|
shellSessionsInUse.add(sessionKey);
|
|
@@ -278,13 +280,15 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
278
280
|
const userSignal = options?.signal;
|
|
279
281
|
const runAbortController = new AbortController();
|
|
280
282
|
let abortCleanupPromise: Promise<void> | undefined;
|
|
283
|
+
const abortShell = (): Promise<void> => {
|
|
284
|
+
abortCleanupPromise ??= executionShell.abort().catch(() => undefined);
|
|
285
|
+
return abortCleanupPromise;
|
|
286
|
+
};
|
|
281
287
|
const abortCurrentExecution = () => {
|
|
282
288
|
if (!runAbortController.signal.aborted) {
|
|
283
289
|
runAbortController.abort();
|
|
284
290
|
}
|
|
285
|
-
|
|
286
|
-
abortCleanupPromise = shellSession.abort().catch(() => undefined);
|
|
287
|
-
}
|
|
291
|
+
void abortShell();
|
|
288
292
|
};
|
|
289
293
|
const abortDeferred = Promise.withResolvers<"abort">();
|
|
290
294
|
const abortHandler = () => {
|
|
@@ -317,38 +321,20 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
317
321
|
let resetSession = false;
|
|
318
322
|
|
|
319
323
|
try {
|
|
320
|
-
const runPromise =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
)
|
|
335
|
-
: executeShell(
|
|
336
|
-
{
|
|
337
|
-
command: finalCommand,
|
|
338
|
-
cwd: commandCwd,
|
|
339
|
-
env: commandEnv,
|
|
340
|
-
sessionEnv: shellEnv,
|
|
341
|
-
snapshotPath: snapshotPath ?? undefined,
|
|
342
|
-
minimizer,
|
|
343
|
-
timeoutMs: nativeTimeoutMs,
|
|
344
|
-
signal: runAbortController.signal,
|
|
345
|
-
},
|
|
346
|
-
(err, chunk) => {
|
|
347
|
-
if (!err) {
|
|
348
|
-
enqueueChunk(chunk);
|
|
349
|
-
}
|
|
350
|
-
},
|
|
351
|
-
);
|
|
324
|
+
const runPromise = executionShell.run(
|
|
325
|
+
{
|
|
326
|
+
command: finalCommand,
|
|
327
|
+
cwd: commandCwd,
|
|
328
|
+
env: commandEnv,
|
|
329
|
+
timeoutMs: nativeTimeoutMs,
|
|
330
|
+
signal: runAbortController.signal,
|
|
331
|
+
},
|
|
332
|
+
(err, chunk) => {
|
|
333
|
+
if (!err) {
|
|
334
|
+
enqueueChunk(chunk);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
);
|
|
352
338
|
|
|
353
339
|
const ey = new ExponentialYield();
|
|
354
340
|
const winner = await ey.race<
|
|
@@ -361,11 +347,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
361
347
|
|
|
362
348
|
if (winner.kind === "timeout" || winner.kind === "abort") {
|
|
363
349
|
acceptingChunks = false;
|
|
350
|
+
const cleanupPromise = abortShell();
|
|
364
351
|
if (shellSession) {
|
|
365
352
|
resetSession = true;
|
|
366
|
-
quarantineShellSession(sessionKey, runPromise,
|
|
353
|
+
quarantineShellSession(sessionKey, runPromise, cleanupPromise);
|
|
367
354
|
} else {
|
|
368
|
-
void
|
|
355
|
+
void Promise.allSettled([runPromise, cleanupPromise]);
|
|
369
356
|
}
|
|
370
357
|
return {
|
|
371
358
|
exitCode: undefined,
|
|
@@ -23,6 +23,7 @@ import type { Settings } from "../../config/settings";
|
|
|
23
23
|
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
24
24
|
import type { HookUIContext } from "../../extensibility/hooks/types";
|
|
25
25
|
import type * as PiCodingAgent from "../../index";
|
|
26
|
+
import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
|
|
26
27
|
import type { Theme } from "../../modes/theme/theme";
|
|
27
28
|
import type { ReadonlySessionManager } from "../../session/session-manager";
|
|
28
29
|
import type { TodoItem } from "../../tools/todo";
|
|
@@ -96,6 +97,8 @@ export interface CustomToolContext {
|
|
|
96
97
|
settings?: Settings;
|
|
97
98
|
/** Fetch implementation for outbound HTTP; defaults to global fetch when omitted. */
|
|
98
99
|
fetch?: FetchImpl;
|
|
100
|
+
/** Calling session's `local://` root mapping for tools that bridge out of the OMP process. */
|
|
101
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
99
102
|
/** Whether to auto-approve all destructive tool operations (--auto-approve CLI flag) */
|
|
100
103
|
autoApprove?: boolean;
|
|
101
104
|
}
|
|
@@ -7,6 +7,7 @@ import type { KeyId } from "@oh-my-pi/pi-tui";
|
|
|
7
7
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
9
9
|
import type { Settings } from "../../config/settings";
|
|
10
|
+
import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
|
|
10
11
|
import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
11
12
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
12
13
|
import type { SessionManager } from "../../session/session-manager";
|
|
@@ -256,6 +257,7 @@ export class ExtensionRunner {
|
|
|
256
257
|
private readonly modelRegistry: ModelRegistry,
|
|
257
258
|
getMemory?: () => MemoryRuntimeContext | undefined,
|
|
258
259
|
private readonly settings?: Settings,
|
|
260
|
+
private readonly localProtocolOptions?: LocalProtocolOptions,
|
|
259
261
|
) {
|
|
260
262
|
this.#uiContext = noOpUIContext;
|
|
261
263
|
this.#getMemoryFn = getMemory;
|
|
@@ -538,6 +540,7 @@ export class ExtensionRunner {
|
|
|
538
540
|
hasPendingMessages: () => this.#hasPendingMessagesFn(),
|
|
539
541
|
shutdown: () => this.#shutdownHandler(),
|
|
540
542
|
getSystemPrompt: () => this.#getSystemPromptFn(),
|
|
543
|
+
localProtocolOptions: this.localProtocolOptions,
|
|
541
544
|
memory: this.#getMemoryFn?.(),
|
|
542
545
|
};
|
|
543
546
|
}
|
|
@@ -41,6 +41,7 @@ import type { PythonResult } from "../../eval/py/executor";
|
|
|
41
41
|
import type { BashResult } from "../../exec/bash-executor";
|
|
42
42
|
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
43
43
|
import type * as PiCodingAgent from "../../index";
|
|
44
|
+
import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
|
|
44
45
|
import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
45
46
|
import type { CustomEditor } from "../../modes/components/custom-editor";
|
|
46
47
|
import type { Theme } from "../../modes/theme/theme";
|
|
@@ -420,6 +421,8 @@ export interface ExtensionContext {
|
|
|
420
421
|
sessionManager: ReadonlySessionManager;
|
|
421
422
|
/** Model registry for API key resolution */
|
|
422
423
|
modelRegistry: ModelRegistry;
|
|
424
|
+
/** Calling session's `local://` root mapping for external tool bridges. */
|
|
425
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
423
426
|
/** Current model (may be undefined) */
|
|
424
427
|
model: Model | undefined;
|
|
425
428
|
/** Read-only model query facade: list / current / resolve / family. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool wrappers for extensions.
|
|
3
3
|
*/
|
|
4
|
-
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import type { Settings } from "../../config/settings";
|
|
7
7
|
import type { Theme } from "../../modes/theme/theme";
|
|
@@ -110,7 +110,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
110
110
|
signal?: AbortSignal,
|
|
111
111
|
onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
|
|
112
112
|
context?: AgentToolContext,
|
|
113
|
-
) {
|
|
113
|
+
): Promise<AgentToolResult<TDetails, TParameters>> {
|
|
114
114
|
// 1. Check approval policy (before extension handlers).
|
|
115
115
|
// CLI `--auto-approve` / `--yolo` sets approval mode to yolo.
|
|
116
116
|
// User `tools.approval.<tool>` policies are still applied in all modes.
|
|
@@ -237,23 +237,23 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
237
237
|
const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
|
|
238
238
|
const modifiedDetails = (resultResult.details ?? result.details) as TDetails;
|
|
239
239
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
throw new Error(errorText);
|
|
246
|
-
}
|
|
247
|
-
if (resultResult.isError === false && executionError) {
|
|
248
|
-
// Extension clears the error - return success
|
|
249
|
-
return { content: modifiedContent, details: modifiedDetails };
|
|
250
|
-
}
|
|
240
|
+
// Effective error state: an explicit handler override wins; otherwise the
|
|
241
|
+
// original execution outcome stands. This lets a handler rewrite a failed
|
|
242
|
+
// call's model-visible content/details while keeping it an error, flip a
|
|
243
|
+
// failure to success, or flag a success as an error.
|
|
244
|
+
const effectiveError = resultResult.isError ?? !!executionError;
|
|
251
245
|
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
246
|
+
// Return the (possibly modified) result carrying the error flag rather than
|
|
247
|
+
// rethrowing the original exception. The agent loop honors
|
|
248
|
+
// `AgentToolResult.isError` and surfaces it as a tool error on the wire (see
|
|
249
|
+
// `coerceToolResult` in agent-loop), so replacement failure content reaches
|
|
250
|
+
// the model while the call remains an error — the original exception text is
|
|
251
|
+
// no longer forced through, which previously discarded the replacement.
|
|
252
|
+
return {
|
|
253
|
+
content: modifiedContent,
|
|
254
|
+
details: modifiedDetails,
|
|
255
|
+
...(effectiveError ? { isError: true } : {}),
|
|
256
|
+
};
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import type { GitSource } from "./git-url";
|
|
6
|
+
|
|
7
|
+
interface CommandResult {
|
|
8
|
+
readonly exitCode: number;
|
|
9
|
+
readonly stdout: string;
|
|
10
|
+
readonly stderr: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function runCommand(command: string[], cwd: string): Promise<CommandResult> {
|
|
14
|
+
const proc = Bun.spawn(command, {
|
|
15
|
+
cwd,
|
|
16
|
+
stdin: "ignore",
|
|
17
|
+
stdout: "pipe",
|
|
18
|
+
stderr: "pipe",
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
});
|
|
21
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
22
|
+
proc.exited,
|
|
23
|
+
new Response(proc.stdout).text(),
|
|
24
|
+
new Response(proc.stderr).text(),
|
|
25
|
+
]);
|
|
26
|
+
return { exitCode, stdout, stderr };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRepositoryUrl(repository: string): string {
|
|
30
|
+
const withoutFragment = repository.replace(/^git\+/i, "").replace(/#.*$/, "");
|
|
31
|
+
const scpLike = withoutFragment.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
|
|
32
|
+
if (scpLike && !withoutFragment.includes("://")) {
|
|
33
|
+
const host = scpLike[1]?.toLowerCase() ?? "";
|
|
34
|
+
const repoPath = (scpLike[2] ?? "").replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
35
|
+
return `ssh://${host}/${repoPath}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const parsed = new URL(withoutFragment);
|
|
40
|
+
const repoPath = parsed.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
41
|
+
return `${parsed.protocol.toLowerCase()}//${parsed.host.toLowerCase()}/${repoPath}`;
|
|
42
|
+
} catch {
|
|
43
|
+
return withoutFragment.replace(/\/+$/g, "").replace(/\.git$/i, "");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Fetches current heads and tags into Bun's matching cached bare clone before a plugin update. */
|
|
48
|
+
export async function refreshBunGitCache(source: GitSource, cwd: string): Promise<void> {
|
|
49
|
+
const cacheResult = await runCommand(["bun", "pm", "cache"], cwd);
|
|
50
|
+
if (cacheResult.exitCode !== 0) {
|
|
51
|
+
throw new Error(`bun pm cache failed: ${cacheResult.stderr}`);
|
|
52
|
+
}
|
|
53
|
+
const cacheDir = cacheResult.stdout.trim();
|
|
54
|
+
if (!cacheDir) {
|
|
55
|
+
throw new Error("bun pm cache returned an empty cache path");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let entries: Dirent[];
|
|
59
|
+
try {
|
|
60
|
+
entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (isEnoent(err)) return;
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const repositoryUrl = normalizeRepositoryUrl(source.repo);
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".git")) continue;
|
|
69
|
+
const repositoryDir = path.join(cacheDir, entry.name);
|
|
70
|
+
const originResult = await runCommand(["git", "-C", repositoryDir, "config", "--get", "remote.origin.url"], cwd);
|
|
71
|
+
if (originResult.exitCode !== 0 || normalizeRepositoryUrl(originResult.stdout.trim()) !== repositoryUrl) continue;
|
|
72
|
+
|
|
73
|
+
const fetchResult = await runCommand(
|
|
74
|
+
[
|
|
75
|
+
"git",
|
|
76
|
+
"-C",
|
|
77
|
+
repositoryDir,
|
|
78
|
+
"fetch",
|
|
79
|
+
"--force",
|
|
80
|
+
"--prune",
|
|
81
|
+
"origin",
|
|
82
|
+
"+refs/heads/*:refs/heads/*",
|
|
83
|
+
"+refs/tags/*:refs/tags/*",
|
|
84
|
+
],
|
|
85
|
+
cwd,
|
|
86
|
+
);
|
|
87
|
+
if (fetchResult.exitCode !== 0) {
|
|
88
|
+
throw new Error(`Failed to refresh Bun's git cache for ${source.host}/${source.path}: ${fetchResult.stderr}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1135,10 +1135,9 @@ async function realpathOrSelfUncached(p: string): Promise<string> {
|
|
|
1135
1135
|
* Extension-local bare dependency entries are also included so their relative
|
|
1136
1136
|
* children receive the reload mtime tag; bare imports inside those dependencies
|
|
1137
1137
|
* remain native Bun resolutions to avoid taking over full third-party graphs.
|
|
1138
|
-
* CommonJS
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
* specifier inside `bun build --compile` binaries).
|
|
1138
|
+
* CommonJS modules reached through `require()` stay on Bun's native loader.
|
|
1139
|
+
* The only exception is a module whose bare requires resolve to native addons:
|
|
1140
|
+
* those require a synchronous hook that pins the addon to an absolute path.
|
|
1142
1141
|
*/
|
|
1143
1142
|
async function collectExtensionModules(entryRealPath: string): Promise<Map<string, string>> {
|
|
1144
1143
|
const modules = new Map<string, string>();
|
|
@@ -1159,32 +1158,40 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
|
|
|
1159
1158
|
let source: string;
|
|
1160
1159
|
try {
|
|
1161
1160
|
source = await Bun.file(file).text();
|
|
1162
|
-
if (nativeAddonLoaderModulePaths.has(file)) {
|
|
1163
|
-
// CJS requires cannot await an async onLoad hook. Resolve and
|
|
1164
|
-
// rewrite native-addon paths before installing its sync hook.
|
|
1165
|
-
source = await rewriteExtensionNativeAddonRequires(source, file);
|
|
1166
|
-
}
|
|
1167
1161
|
} catch {
|
|
1168
1162
|
continue;
|
|
1169
1163
|
}
|
|
1170
1164
|
modules.set(file, source);
|
|
1171
1165
|
const dir = path.dirname(file);
|
|
1172
1166
|
const specifiers = new Set<string>();
|
|
1167
|
+
const requiredSpecifiers = new Set<string>();
|
|
1173
1168
|
for (const match of source.matchAll(EXTENSION_GRAPH_SPECIFIER_REGEX)) {
|
|
1174
1169
|
if (match[2]) specifiers.add(match[2]);
|
|
1175
1170
|
}
|
|
1176
1171
|
for (const match of source.matchAll(NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX)) {
|
|
1177
|
-
if (match[2])
|
|
1172
|
+
if (match[2]) {
|
|
1173
|
+
specifiers.add(match[2]);
|
|
1174
|
+
requiredSpecifiers.add(match[2]);
|
|
1175
|
+
}
|
|
1178
1176
|
}
|
|
1179
1177
|
for (const specifier of specifiers) {
|
|
1180
1178
|
try {
|
|
1181
1179
|
let resolved: string | null = null;
|
|
1182
1180
|
let nextFollowsBareDependencies = followBareDependencies;
|
|
1181
|
+
const isRequired = requiredSpecifiers.has(specifier);
|
|
1183
1182
|
if (specifier.startsWith(".")) {
|
|
1184
1183
|
const candidate = Bun.resolveSync(specifier, dir);
|
|
1185
|
-
|
|
1184
|
+
if (
|
|
1185
|
+
hasSourceModuleExtension(candidate) &&
|
|
1186
|
+
(!isRequired || (await moduleRequiresNativeAddon(candidate)))
|
|
1187
|
+
) {
|
|
1188
|
+
resolved = await realpathOrSelf(candidate);
|
|
1189
|
+
}
|
|
1186
1190
|
} else if (specifier.startsWith("#")) {
|
|
1187
|
-
|
|
1191
|
+
const candidate = await resolvePackageImportSpecifier(specifier, file);
|
|
1192
|
+
if (candidate && (!isRequired || (await moduleRequiresNativeAddon(candidate)))) {
|
|
1193
|
+
resolved = candidate;
|
|
1194
|
+
}
|
|
1188
1195
|
} else if (
|
|
1189
1196
|
followBareDependencies &&
|
|
1190
1197
|
isBareExtensionDependencySpecifier(specifier) &&
|
|
@@ -1206,14 +1213,17 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
|
|
|
1206
1213
|
isHookableEntry && isCommonJsEntry && dependencyEntry
|
|
1207
1214
|
? await moduleRequiresNativeAddon(dependencyEntry)
|
|
1208
1215
|
: false;
|
|
1209
|
-
if (isHookableEntry && dependencyEntry && (!isCommonJsEntry || hookCommonJsEntry)) {
|
|
1216
|
+
if (isHookableEntry && dependencyEntry && ((!isRequired && !isCommonJsEntry) || hookCommonJsEntry)) {
|
|
1210
1217
|
resolved = await realpathOrSelf(dependencyEntry);
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1218
|
+
}
|
|
1219
|
+
if (resolved && hookCommonJsEntry) {
|
|
1220
|
+
nativeAddonLoaderModulePaths.add(resolved);
|
|
1214
1221
|
}
|
|
1215
1222
|
nextFollowsBareDependencies = false;
|
|
1216
1223
|
}
|
|
1224
|
+
if (resolved && isRequired) {
|
|
1225
|
+
nativeAddonLoaderModulePaths.add(resolved);
|
|
1226
|
+
}
|
|
1217
1227
|
if (resolved && !modules.has(resolved)) {
|
|
1218
1228
|
const queuedFollowsBareDependencies = queuedFollowBareDependencies.get(resolved) ?? false;
|
|
1219
1229
|
const mergedFollowsBareDependencies = queuedFollowsBareDependencies || nextFollowsBareDependencies;
|
|
@@ -1225,6 +1235,12 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
|
|
|
1225
1235
|
}
|
|
1226
1236
|
}
|
|
1227
1237
|
}
|
|
1238
|
+
for (const modulePath of nativeAddonLoaderModulePaths) {
|
|
1239
|
+
const source = modules.get(modulePath);
|
|
1240
|
+
if (source !== undefined) {
|
|
1241
|
+
modules.set(modulePath, await rewriteExtensionNativeAddonRequires(source, modulePath));
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1228
1244
|
return modules;
|
|
1229
1245
|
}
|
|
1230
1246
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
logger,
|
|
13
13
|
} from "@oh-my-pi/pi-utils";
|
|
14
14
|
import { withExitGuard } from "../utils";
|
|
15
|
+
import { refreshBunGitCache } from "./bun-git-cache";
|
|
15
16
|
import { type GitSource, parseGitUrl } from "./git-url";
|
|
16
17
|
import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "./legacy-pi-compat";
|
|
17
18
|
import { resolvePluginManifestEntries } from "./loader";
|
|
@@ -211,6 +212,16 @@ export class PluginManager {
|
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
async #removeDependencyEntry(pkgJsonPath: string, name: string): Promise<void> {
|
|
216
|
+
const pkgJson: { dependencies?: Record<string, string>; [key: string]: unknown } =
|
|
217
|
+
await Bun.file(pkgJsonPath).json();
|
|
218
|
+
if (!pkgJson.dependencies || !(name in pkgJson.dependencies)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
delete pkgJson.dependencies[name];
|
|
222
|
+
await Bun.write(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
223
|
+
}
|
|
224
|
+
|
|
214
225
|
#collectInstalledNames(deps: Record<string, string>, config: PluginRuntimeConfig): Set<string> {
|
|
215
226
|
const installedNames = new Set<string>();
|
|
216
227
|
for (const name of Object.keys(deps)) {
|
|
@@ -450,6 +461,17 @@ export class PluginManager {
|
|
|
450
461
|
// validation throws.
|
|
451
462
|
let actualName: string | undefined;
|
|
452
463
|
try {
|
|
464
|
+
// Bun treats a dependency replacement from `repo#old-ref` to the same
|
|
465
|
+
// package at `repo`/`repo#new-ref` as a self-edge and bails with
|
|
466
|
+
// DependencyLoop. Remove only the stale manifest edge; rollback restores
|
|
467
|
+
// the original package.json and node_modules snapshot on failure.
|
|
468
|
+
if (gitSource && existingActualName) {
|
|
469
|
+
const installedSource = parseGitUrl(depsBefore[existingActualName] ?? "");
|
|
470
|
+
if (installedSource && installedSource.ref !== gitSource.ref) {
|
|
471
|
+
await this.#removeDependencyEntry(pkgJsonPath, existingActualName);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
453
475
|
// Step 1: write the spec into plugins/package.json + node_modules.
|
|
454
476
|
const installProc = Bun.spawn(["bun", "install", packageInstallSpec], {
|
|
455
477
|
cwd: getPluginsDir(),
|
|
@@ -500,14 +522,13 @@ export class PluginManager {
|
|
|
500
522
|
|
|
501
523
|
// Step 2: refresh the git lockfile pin when re-installing an existing
|
|
502
524
|
// git plugin. `bun install <spec>` is a no-op when the spec matches the
|
|
503
|
-
// lockfile entry
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
// move. First-time installs skip this — the initial `bun install` already
|
|
509
|
-
// fetched HEAD. Rollback is handled by the outer catch.
|
|
525
|
+
// lockfile entry, while `bun update <name>` resolves through Bun's bare
|
|
526
|
+
// clone cache. Fetch the matching cache clone first so a stale cached
|
|
527
|
+
// ref cannot silently preserve the old pin (#3063, #5401). First-time
|
|
528
|
+
// installs skip this because the initial `bun install` populated the
|
|
529
|
+
// cache from the remote. Rollback is handled by the outer catch.
|
|
510
530
|
if (gitSource && existingActualName) {
|
|
531
|
+
await refreshBunGitCache(gitSource, getPluginsDir());
|
|
511
532
|
const updateProc = Bun.spawn(["bun", "update", actualName], {
|
|
512
533
|
cwd: getPluginsDir(),
|
|
513
534
|
stdin: "ignore",
|