@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10
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 +94 -1
- package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
- package/dist/cli.js +13216 -15614
- package/dist/types/async/job-manager.d.ts +12 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +13 -0
- package/dist/types/discovery/agents.d.ts +11 -0
- package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -8
- package/dist/types/extensibility/hooks/types.d.ts +5 -5
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
- package/dist/types/hindsight/state.d.ts +0 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/lsp/utils.d.ts +8 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
- package/dist/types/modes/acp/acp-mode.d.ts +1 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
- package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
- package/dist/types/modes/components/agent-hub.d.ts +12 -5
- package/dist/types/modes/components/model-browser.d.ts +9 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/registry/agent-lifecycle.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +47 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +8 -4
- package/dist/types/session/session-entries.d.ts +10 -0
- package/dist/types/session/session-loader.d.ts +11 -1
- package/dist/types/session/session-manager.d.ts +9 -1
- package/dist/types/session/session-tools.d.ts +10 -1
- package/dist/types/session/turn-recovery.d.ts +6 -2
- package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
- package/dist/types/slash-commands/available-commands.d.ts +1 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +2 -3
- package/dist/types/task/read-only-policy.d.ts +3 -0
- package/dist/types/task/structured-subagent.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +3 -3
- package/dist/types/tools/browser/launch.d.ts +3 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -2
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/read.d.ts +3 -5
- package/dist/types/tools/run-scope.d.ts +27 -1
- package/dist/types/tools/shell-tokenize.d.ts +21 -3
- package/dist/types/tools/terminal-output.d.ts +1 -1
- package/dist/types/utils/changelog.d.ts +3 -2
- package/dist/types/utils/late-cleanup.d.ts +2 -0
- package/dist/types/utils/turndown.d.ts +1 -1
- package/dist/types/vibe/runtime.d.ts +4 -3
- package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
- package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
- package/examples/custom-tools/README.md +1 -1
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/api-demo.ts +5 -6
- package/examples/extensions/chalk-logger.ts +1 -1
- package/examples/extensions/hello.ts +2 -2
- package/examples/extensions/reload-runtime.ts +1 -1
- package/examples/extensions/tools.ts +2 -2
- package/examples/extensions/with-deps/index.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/package.json +15 -31
- package/scripts/bundle-dist.ts +1 -11
- package/scripts/omp +11 -2
- package/src/advisor/runtime.ts +54 -0
- package/src/async/job-manager.ts +26 -0
- package/src/cli/agents-cli.ts +1 -1
- package/src/cli/args.ts +24 -2
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/claude-trace-cli.ts +1 -1
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/flag-tables.ts +4 -0
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/grievances-cli.ts +1 -1
- package/src/cli/help-extra.ts +1 -1
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/read-cli.ts +1 -1
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/shell-cli.ts +1 -1
- package/src/cli/ssh-cli.ts +1 -1
- package/src/cli/stats-cli.ts +4 -4
- package/src/cli/tiny-models-cli.ts +1 -1
- package/src/cli/ttsr-cli.ts +1 -1
- package/src/cli/update-cli.ts +3 -57
- package/src/cli/usage-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli/worktree-cli.ts +1 -1
- package/src/commands/say.ts +1 -1
- package/src/commands/token.ts +1 -1
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/cli.ts +1 -1
- package/src/config/config-file.ts +2 -2
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-discovery.ts +36 -2
- package/src/config/model-registry.ts +4 -1
- package/src/config/model-resolver.ts +54 -7
- package/src/discovery/agents.ts +73 -3
- package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
- package/src/discovery/claude.ts +8 -5
- package/src/discovery/cursor.ts +8 -5
- package/src/discovery/gemini.ts +11 -8
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +4 -2
- package/src/edit/hashline/filesystem.ts +7 -7
- package/src/eval/agent-bridge.ts +1 -3
- package/src/eval/jl/prelude.jl +4 -4
- package/src/eval/js/shared/prelude.txt +2 -2
- package/src/eval/py/prelude.py +0 -3
- package/src/eval/py/runner.py +38 -1
- package/src/eval/rb/prelude.rb +1 -2
- package/src/extensibility/custom-commands/loader.ts +4 -4
- package/src/extensibility/custom-commands/types.ts +5 -5
- package/src/extensibility/custom-tools/loader.ts +4 -4
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -6
- package/src/extensibility/extensions/types.ts +39 -8
- package/src/extensibility/hooks/loader.ts +4 -5
- package/src/extensibility/hooks/types.ts +5 -5
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
- package/src/hindsight/state.ts +2 -22
- package/src/index.ts +2 -2
- package/src/internal-urls/memory-protocol.ts +33 -0
- package/src/launch/terminal-output.ts +1 -1
- package/src/lsp/client.ts +2 -2
- package/src/lsp/utils.ts +29 -0
- package/src/main.ts +85 -21
- package/src/markit/converters/docx.ts +1 -1
- package/src/markit/converters/epub.ts +1 -1
- package/src/markit/converters/pptx.ts +1 -1
- package/src/markit/converters/xlsx.ts +1 -1
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/acp/acp-mode.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +248 -0
- package/src/modes/components/agent-hub-renderer.ts +194 -0
- package/src/modes/components/agent-hub.ts +670 -192
- package/src/modes/components/agent-transcript-viewer.ts +1 -3
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/custom-editor.ts +2 -2
- package/src/modes/components/extensions/inspector-panel.ts +11 -6
- package/src/modes/components/footer.ts +1 -3
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-browser.ts +11 -3
- package/src/modes/components/model-hub.ts +4 -1
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/components/status-line/segments.ts +4 -3
- package/src/modes/controllers/event-controller.ts +188 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/mcp-command-controller.ts +81 -20
- package/src/modes/controllers/selector-controller.ts +40 -49
- package/src/modes/controllers/streaming-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/print-mode.ts +29 -12
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/session-observer-registry.ts +5 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/tools/computer.md +1 -1
- package/src/registry/agent-lifecycle.ts +35 -10
- package/src/registry/agent-registry.ts +65 -2
- package/src/registry/persisted-agents.ts +341 -16
- package/src/sdk.ts +11 -5
- package/src/session/agent-session-types.ts +8 -0
- package/src/session/agent-session.ts +177 -188
- package/src/session/session-entries.ts +10 -0
- package/src/session/session-listing.ts +1 -1
- package/src/session/session-loader.ts +74 -8
- package/src/session/session-manager.ts +22 -2
- package/src/session/session-paths.ts +105 -110
- package/src/session/session-tools.ts +28 -1
- package/src/session/settings-stream-fn.ts +7 -3
- package/src/session/turn-recovery.ts +236 -18
- package/src/session/unexpected-stop-classifier.ts +12 -3
- package/src/slash-commands/acp-builtins.ts +1 -1
- package/src/slash-commands/available-commands.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +173 -47
- package/src/task/index.ts +9 -28
- package/src/task/isolation-runner.ts +43 -12
- package/src/task/persisted-revive.ts +12 -5
- package/src/task/read-only-policy.ts +27 -0
- package/src/task/structured-subagent.ts +31 -6
- package/src/task/types.ts +4 -0
- package/src/tiny/worker.ts +1 -1
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/bash-interactive.ts +4 -4
- package/src/tools/bash-interceptor.ts +8 -4
- package/src/tools/bash.ts +6 -6
- package/src/tools/browser/cmux/cmux-tab.ts +66 -18
- package/src/tools/browser/launch.ts +76 -19
- package/src/tools/browser/readable.ts +9 -9
- package/src/tools/browser/tab-supervisor.ts +12 -3
- package/src/tools/browser/tab-worker-entry.ts +1 -1
- package/src/tools/browser/tab-worker.ts +101 -12
- package/src/tools/fetch.ts +1 -1
- package/src/tools/glob.ts +4 -2
- package/src/tools/path-utils.ts +22 -5
- package/src/tools/read.ts +22 -5
- package/src/tools/run-scope.ts +290 -4
- package/src/tools/shell-tokenize.ts +38 -9
- package/src/tools/terminal-output.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/utils/changelog.ts +5 -4
- package/src/utils/clipboard.ts +10 -2
- package/src/utils/external-editor.ts +4 -2
- package/src/utils/jj.ts +1 -1
- package/src/utils/late-cleanup.ts +17 -0
- package/src/utils/turndown.ts +1 -2
- package/src/vibe/runtime.ts +202 -58
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/hackage.ts +1 -12
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +2 -1
- package/src/web/scrapers/twitter.ts +2 -2
- package/src/web/scrapers/types.ts +1 -1
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +12 -39
- package/src/web/search/providers/codex.ts +3 -26
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/ecosia.ts +2 -2
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/firecrawl.ts +1 -1
- package/src/web/search/providers/google.ts +1 -1
- package/src/web/search/providers/mojeek.ts +2 -2
- package/src/web/search/providers/perplexity.ts +1 -1
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/startpage.ts +2 -2
- package/src/markit/converters/mammoth.d.ts +0 -24
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { $which, getPuppeteerDir, logger, removeWithRetries } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import type * as BrowsersNs from "@
|
|
5
|
+
import type * as BrowsersNs from "@oh-my-pi/pi-utils/browsers";
|
|
6
6
|
import type { Browser, CDPSession, Page, default as Puppeteer, Target } from "puppeteer-core";
|
|
7
7
|
import stealthTamperingScript from "../puppeteer/00_stealth_tampering.txt" with { type: "text" };
|
|
8
8
|
import stealthActivityScript from "../puppeteer/01_stealth_activity.txt" with { type: "text" };
|
|
@@ -115,15 +115,15 @@ export async function loadPuppeteerInWorker(safeDir: string): Promise<typeof Pup
|
|
|
115
115
|
let browsersModule: typeof BrowsersNs | undefined;
|
|
116
116
|
async function loadBrowsers(): Promise<typeof BrowsersNs> {
|
|
117
117
|
if (!browsersModule) {
|
|
118
|
-
browsersModule = await import("@
|
|
118
|
+
browsersModule = await import("@oh-my-pi/pi-utils/browsers");
|
|
119
119
|
}
|
|
120
120
|
return browsersModule;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
* Resolve the Chromium executable puppeteer will launch,
|
|
125
|
-
*
|
|
126
|
-
*
|
|
124
|
+
* Resolve the Chromium executable puppeteer will launch, honoring
|
|
125
|
+
* PUPPETEER_EXECUTABLE_PATH before system browser detection and lazily
|
|
126
|
+
* downloading Chromium otherwise. The browser is cached under
|
|
127
127
|
* ~/.omp/puppeteer (getPuppeteerDir). Returns undefined when platform
|
|
128
128
|
* detection fails (puppeteer default resolution takes over). Exported so
|
|
129
129
|
* real-browser tests can probe launchability and skip on hosts missing
|
|
@@ -131,10 +131,10 @@ async function loadBrowsers(): Promise<typeof BrowsersNs> {
|
|
|
131
131
|
*/
|
|
132
132
|
let chromiumExecutablePromise: Promise<string | undefined> | undefined;
|
|
133
133
|
export async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
134
|
-
const sysChrome = resolveSystemChromium();
|
|
135
|
-
if (sysChrome) return sysChrome;
|
|
136
134
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
137
135
|
if (envPath) return envPath;
|
|
136
|
+
const sysChrome = await resolveSystemChromium();
|
|
137
|
+
if (sysChrome) return sysChrome;
|
|
138
138
|
if (chromiumExecutablePromise) return chromiumExecutablePromise;
|
|
139
139
|
|
|
140
140
|
chromiumExecutablePromise = (async () => {
|
|
@@ -166,13 +166,13 @@ export async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
|
166
166
|
buildId,
|
|
167
167
|
cacheDir,
|
|
168
168
|
platform,
|
|
169
|
-
downloadProgressCallback: (
|
|
170
|
-
if (
|
|
171
|
-
const pct = Math.floor((
|
|
172
|
-
if (pct >= lastReportedPercent + 10 ||
|
|
169
|
+
downloadProgressCallback: ({ downloadedBytes, totalBytes }) => {
|
|
170
|
+
if (totalBytes <= 0) return;
|
|
171
|
+
const pct = Math.floor((downloadedBytes / totalBytes) * 100);
|
|
172
|
+
if (pct >= lastReportedPercent + 10 || downloadedBytes === totalBytes) {
|
|
173
173
|
lastReportedPercent = pct;
|
|
174
174
|
logger.debug(
|
|
175
|
-
`Chromium download: ${pct}% (${Math.round(
|
|
175
|
+
`Chromium download: ${pct}% (${Math.round(downloadedBytes / 1_000_000)} / ${Math.round(totalBytes / 1_000_000)} MB)`,
|
|
176
176
|
);
|
|
177
177
|
}
|
|
178
178
|
},
|
|
@@ -193,16 +193,47 @@ let resolvedChromium: string | null | undefined; // undefined = unchecked; null
|
|
|
193
193
|
function isExecutableFile(p: string): boolean {
|
|
194
194
|
try {
|
|
195
195
|
const st = fs.statSync(p);
|
|
196
|
-
|
|
196
|
+
if (!st.isFile()) return false;
|
|
197
|
+
if (process.platform === "win32") return true;
|
|
198
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function isChromiumExecutable(p: string): Promise<boolean> {
|
|
206
|
+
if (!isExecutableFile(p)) return false;
|
|
207
|
+
try {
|
|
208
|
+
const probeTimeoutMs = 3000;
|
|
209
|
+
const proc = Bun.spawn([p, "--version"], {
|
|
210
|
+
stdout: "pipe",
|
|
211
|
+
stderr: "ignore",
|
|
212
|
+
signal: AbortSignal.timeout(probeTimeoutMs),
|
|
213
|
+
killSignal: "SIGKILL",
|
|
214
|
+
});
|
|
215
|
+
const stdout = await Promise.race([
|
|
216
|
+
new Response(proc.stdout).text(),
|
|
217
|
+
Bun.sleep(probeTimeoutMs + 500).then(() => null),
|
|
218
|
+
]);
|
|
219
|
+
if (stdout === null) return false;
|
|
220
|
+
await proc.exited;
|
|
221
|
+
return proc.exitCode === 0 && /Chrom|Edg/i.test(stdout);
|
|
197
222
|
} catch {
|
|
198
223
|
return false;
|
|
199
224
|
}
|
|
200
225
|
}
|
|
201
226
|
|
|
202
|
-
|
|
203
|
-
|
|
227
|
+
/** Flatpak application id published by the Ungoogled Chromium project. */
|
|
228
|
+
const UNGOOGLED_CHROMIUM_FLATPAK_ID = "io.github.ungoogled_software.ungoogled_chromium";
|
|
229
|
+
|
|
230
|
+
function systemChromiumCandidates(
|
|
231
|
+
platform: NodeJS.Platform = process.platform,
|
|
232
|
+
home = os.homedir(),
|
|
233
|
+
which: (name: string) => string | null | undefined = $which,
|
|
234
|
+
): string[] {
|
|
204
235
|
const candidates: string[] = [];
|
|
205
|
-
switch (
|
|
236
|
+
switch (platform) {
|
|
206
237
|
case "darwin": {
|
|
207
238
|
for (const root of ["/Applications", path.join(home, "Applications")]) {
|
|
208
239
|
candidates.push(
|
|
@@ -219,7 +250,7 @@ function systemChromiumCandidates(): string[] {
|
|
|
219
250
|
case "linux": {
|
|
220
251
|
const names = ["google-chrome-stable", "google-chrome", "chromium", "chromium-browser", "chrome"];
|
|
221
252
|
for (const name of names) {
|
|
222
|
-
const found =
|
|
253
|
+
const found = which(name);
|
|
223
254
|
if (found) candidates.push(found);
|
|
224
255
|
}
|
|
225
256
|
candidates.push(
|
|
@@ -238,6 +269,19 @@ function systemChromiumCandidates(): string[] {
|
|
|
238
269
|
if (onNixos) {
|
|
239
270
|
candidates.push(path.join(home, ".nix-profile/bin/chromium"), "/run/current-system/sw/bin/chromium");
|
|
240
271
|
}
|
|
272
|
+
for (const name of ["ungoogled-chromium", "ungoogled-chromium-browser"]) {
|
|
273
|
+
const found = which(name);
|
|
274
|
+
if (found) candidates.push(found);
|
|
275
|
+
}
|
|
276
|
+
candidates.push(
|
|
277
|
+
// Ungoogled Chromium. Distro and AUR packages that keep the plain
|
|
278
|
+
// `chromium` name are already covered above; these are the paths
|
|
279
|
+
// unique to it, including the system and per-user Flatpak shims.
|
|
280
|
+
"/usr/bin/ungoogled-chromium",
|
|
281
|
+
"/usr/bin/ungoogled-chromium-browser",
|
|
282
|
+
`/var/lib/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`,
|
|
283
|
+
path.join(home, ".local/share/flatpak/exports/bin", UNGOOGLED_CHROMIUM_FLATPAK_ID),
|
|
284
|
+
);
|
|
241
285
|
break;
|
|
242
286
|
}
|
|
243
287
|
case "win32": {
|
|
@@ -259,13 +303,13 @@ function systemChromiumCandidates(): string[] {
|
|
|
259
303
|
return candidates;
|
|
260
304
|
}
|
|
261
305
|
|
|
262
|
-
function resolveSystemChromium(): string | undefined {
|
|
306
|
+
async function resolveSystemChromium(): Promise<string | undefined> {
|
|
263
307
|
if (resolvedChromium !== undefined) return resolvedChromium ?? undefined;
|
|
264
308
|
const seen = new Set<string>();
|
|
265
309
|
for (const candidate of systemChromiumCandidates()) {
|
|
266
310
|
if (!candidate || seen.has(candidate)) continue;
|
|
267
311
|
seen.add(candidate);
|
|
268
|
-
if (
|
|
312
|
+
if (await isChromiumExecutable(candidate)) {
|
|
269
313
|
resolvedChromium = candidate;
|
|
270
314
|
logger.debug("Using system Chrome/Chromium", { path: candidate });
|
|
271
315
|
return candidate;
|
|
@@ -866,6 +910,19 @@ export async function applyStealthPatches(
|
|
|
866
910
|
await injectStealthScripts(page);
|
|
867
911
|
}
|
|
868
912
|
|
|
913
|
+
/** Exposes executable candidates for detection tests. */
|
|
914
|
+
export function systemChromiumCandidatesForTest(
|
|
915
|
+
platform: NodeJS.Platform = process.platform,
|
|
916
|
+
home?: string,
|
|
917
|
+
which?: (name: string) => string | null | undefined,
|
|
918
|
+
): string[] {
|
|
919
|
+
return systemChromiumCandidates(platform, home, which);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export async function chromiumExecutableProbeForTest(executablePath: string): Promise<boolean> {
|
|
923
|
+
return isChromiumExecutable(executablePath);
|
|
924
|
+
}
|
|
925
|
+
|
|
869
926
|
export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] {
|
|
870
927
|
return stealthIgnoreDefaultArgs(executablePath);
|
|
871
928
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type * as
|
|
2
|
-
import type * as
|
|
1
|
+
import type * as DomNs from "@oh-my-pi/pi-utils/dom";
|
|
2
|
+
import type * as ReadabilityNs from "@oh-my-pi/pi-utils/readability";
|
|
3
3
|
import { htmlToBasicMarkdown } from "../../web/scrapers/types";
|
|
4
4
|
|
|
5
5
|
export type ReadableFormat = "text" | "markdown";
|
|
@@ -23,17 +23,17 @@ function normalize(text: string | null | undefined): string | undefined {
|
|
|
23
23
|
let readabilityModule: typeof ReadabilityNs | undefined;
|
|
24
24
|
async function loadReadability(): Promise<typeof ReadabilityNs> {
|
|
25
25
|
if (!readabilityModule) {
|
|
26
|
-
readabilityModule = await import("@
|
|
26
|
+
readabilityModule = await import("@oh-my-pi/pi-utils/readability");
|
|
27
27
|
}
|
|
28
28
|
return readabilityModule;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
let
|
|
32
|
-
async function
|
|
33
|
-
if (!
|
|
34
|
-
|
|
31
|
+
let domModule: typeof DomNs | undefined;
|
|
32
|
+
async function loadDom(): Promise<typeof DomNs> {
|
|
33
|
+
if (!domModule) {
|
|
34
|
+
domModule = await import("@oh-my-pi/pi-utils/dom");
|
|
35
35
|
}
|
|
36
|
-
return
|
|
36
|
+
return domModule;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -47,7 +47,7 @@ export async function extractReadableFromHtml(
|
|
|
47
47
|
url: string,
|
|
48
48
|
format: ReadableFormat,
|
|
49
49
|
): Promise<ReadableResult | null> {
|
|
50
|
-
const [{ parseHTML }, { Readability }] = await Promise.all([
|
|
50
|
+
const [{ parseHTML }, { Readability }] = await Promise.all([loadDom(), loadReadability()]);
|
|
51
51
|
const { document } = parseHTML(html);
|
|
52
52
|
|
|
53
53
|
// --- Primary: Readability article extraction ---
|
|
@@ -134,6 +134,7 @@ const tabs = new Map<string, TabSession>();
|
|
|
134
134
|
// awaits) cannot interleave and leak a worker + browser refCount.
|
|
135
135
|
const acquireChains = new Map<string, Promise<void>>();
|
|
136
136
|
const GRACE_MS = 750;
|
|
137
|
+
const WORKER_INIT_TIMEOUT_MS = 15_000;
|
|
137
138
|
// Names of tabs the supervisor force-killed (timeout past grace, failed recycle),
|
|
138
139
|
// mapped to the kill reason. Lets the next `run` on that name explain WHY the tab
|
|
139
140
|
// vanished instead of a bare "not alive". Cleared when the name is opened again.
|
|
@@ -272,7 +273,11 @@ async function acquireTabImpl(
|
|
|
272
273
|
}
|
|
273
274
|
let info: ReadyInfo;
|
|
274
275
|
try {
|
|
275
|
-
info = await initializeTabWorker(
|
|
276
|
+
info = await initializeTabWorker(
|
|
277
|
+
worker,
|
|
278
|
+
initPayload,
|
|
279
|
+
Math.max(WORKER_INIT_TIMEOUT_MS, opts.timeoutMs + GRACE_MS),
|
|
280
|
+
);
|
|
276
281
|
} catch (error) {
|
|
277
282
|
// `BuildMessage`-class failures arrive asynchronously via the worker's `error` event,
|
|
278
283
|
// after `spawnTabWorker`'s synchronous try/catch has already returned. Fall back to
|
|
@@ -287,7 +292,11 @@ async function acquireTabImpl(
|
|
|
287
292
|
});
|
|
288
293
|
worker = await spawnInlineWorker();
|
|
289
294
|
try {
|
|
290
|
-
info = await initializeTabWorker(
|
|
295
|
+
info = await initializeTabWorker(
|
|
296
|
+
worker,
|
|
297
|
+
initPayload,
|
|
298
|
+
Math.max(WORKER_INIT_TIMEOUT_MS, opts.timeoutMs + GRACE_MS),
|
|
299
|
+
);
|
|
291
300
|
} catch (inlineError) {
|
|
292
301
|
await worker.terminate().catch(() => undefined);
|
|
293
302
|
if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
@@ -1014,7 +1023,7 @@ async function spawnInlineWorker(): Promise<WorkerHandle> {
|
|
|
1014
1023
|
close: () => {},
|
|
1015
1024
|
};
|
|
1016
1025
|
const { WorkerCore } = await import("./tab-worker");
|
|
1017
|
-
new WorkerCore(workerTransport);
|
|
1026
|
+
new WorkerCore(workerTransport, false);
|
|
1018
1027
|
return {
|
|
1019
1028
|
mode: "inline",
|
|
1020
1029
|
send: msg =>
|
|
@@ -3,7 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { postmortem, Snowflake, untilAborted, withTimeout } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import type { HTMLElement } from "
|
|
6
|
+
import type { HTMLElement } from "@oh-my-pi/pi-utils/dom";
|
|
7
7
|
import type {
|
|
8
8
|
Browser,
|
|
9
9
|
CDPSession,
|
|
@@ -23,10 +23,15 @@ import { formatScreenshot } from "../render-utils";
|
|
|
23
23
|
import {
|
|
24
24
|
bindRunFacade,
|
|
25
25
|
CELL_BUDGET_SLACK_MS,
|
|
26
|
+
installBrowserWorkerRejectionGuard,
|
|
27
|
+
isBrowserRunOwnedRejection,
|
|
28
|
+
markBrowserRunRejection,
|
|
26
29
|
markHandled,
|
|
30
|
+
observeBrowserRunPromise,
|
|
27
31
|
resolvePredicateTimeout,
|
|
28
32
|
type WaitPredicateOptions,
|
|
29
33
|
waitForRun,
|
|
34
|
+
withBrowserPromiseCombinatorTracking,
|
|
30
35
|
} from "../run-scope";
|
|
31
36
|
import { ToolAbortError, ToolError, throwIfAborted } from "../tool-errors";
|
|
32
37
|
import {
|
|
@@ -44,6 +49,7 @@ import {
|
|
|
44
49
|
loadPuppeteerInWorker,
|
|
45
50
|
} from "./launch";
|
|
46
51
|
import { extractReadableFromHtml, type ReadableFormat } from "./readable";
|
|
52
|
+
|
|
47
53
|
import { cloneSafe, RunOutput } from "./run-output";
|
|
48
54
|
import type {
|
|
49
55
|
Observation,
|
|
@@ -701,6 +707,9 @@ interface ActiveRun {
|
|
|
701
707
|
output: RunOutput;
|
|
702
708
|
screenshots: ScreenshotResult[];
|
|
703
709
|
pendingTools: Map<string, { resolve(value: unknown): void; reject(error: Error): void }>;
|
|
710
|
+
rejectionOwner: object;
|
|
711
|
+
floatingRejections: unknown[];
|
|
712
|
+
floatingFailure: { promise: Promise<never>; reject(reason?: unknown): void };
|
|
704
713
|
/** Helper invocations currently awaiting the page/network, keyed by op id. */
|
|
705
714
|
inflight: Map<number, InflightOp>;
|
|
706
715
|
opCounter: number;
|
|
@@ -748,17 +757,75 @@ export class WorkerCore {
|
|
|
748
757
|
#active: ActiveRun | null = null;
|
|
749
758
|
#runtime: JsRuntime | null = null;
|
|
750
759
|
#unsub: () => void;
|
|
760
|
+
#isolated: boolean;
|
|
761
|
+
#uninstallRejectionGuard: () => void;
|
|
751
762
|
#mode?: WorkerInitPayload["mode"];
|
|
752
763
|
#activateForScreenshot = true;
|
|
753
764
|
#dialogPolicy?: DialogPolicy;
|
|
754
765
|
#dialogHandler?: (dialog: Dialog) => void;
|
|
755
766
|
#openDialog?: OpenDialogInfo;
|
|
756
767
|
|
|
757
|
-
constructor(transport: Transport) {
|
|
768
|
+
constructor(transport: Transport, isolated: boolean) {
|
|
758
769
|
this.#transport = transport;
|
|
770
|
+
this.#isolated = isolated;
|
|
759
771
|
this.#unsub = this.#transport.onMessage(msg => {
|
|
760
772
|
void this.#handleMessage(msg as WorkerInbound);
|
|
761
773
|
});
|
|
774
|
+
this.#uninstallRejectionGuard = this.#installRejectionGuard();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
#installRejectionGuard(): () => void {
|
|
778
|
+
if (!this.#isolated) {
|
|
779
|
+
return postmortem.interceptUnhandledRejections(reason => this.#consumeUnhandledRejection(reason));
|
|
780
|
+
}
|
|
781
|
+
return installBrowserWorkerRejectionGuard(reason => this.#consumeUnhandledRejection(reason));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
#consumeUnhandledRejection(reason: unknown): boolean {
|
|
785
|
+
const active = this.#active;
|
|
786
|
+
if (!active) return false;
|
|
787
|
+
if (!isBrowserRunOwnedRejection(reason, active.rejectionOwner, `browser-run-${active.id}.js`)) return false;
|
|
788
|
+
this.#recordFloatingRejection(active, reason);
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
#recordFloatingRejection(active: ActiveRun, reason: unknown): void {
|
|
793
|
+
if (postmortem.isExpectedCleanupError(reason)) return;
|
|
794
|
+
if (this.#active !== active) {
|
|
795
|
+
this.#log("warn", "Unhandled rejection after browser run ended", {
|
|
796
|
+
runId: active.id,
|
|
797
|
+
error: reason instanceof Error ? reason.message : String(reason),
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const isFirst = active.floatingRejections.length === 0;
|
|
802
|
+
active.floatingRejections.push(reason);
|
|
803
|
+
if (isFirst) active.floatingFailure.reject(this.#floatingRejectionError(reason));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
#floatingRejectionError(reason: unknown): Error {
|
|
807
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
808
|
+
const error = new Error(`Unhandled rejection (missing await?): ${message}`, { cause: reason });
|
|
809
|
+
if (reason instanceof Error) error.name = reason.name;
|
|
810
|
+
return error;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
#foldFloatingRejections(active: ActiveRun, failure: { error: unknown } | undefined): { error: unknown } | undefined {
|
|
814
|
+
const rejections = active.floatingRejections;
|
|
815
|
+
if (rejections.length === 0) return failure;
|
|
816
|
+
let reported = rejections;
|
|
817
|
+
if (!failure) {
|
|
818
|
+
failure = { error: this.#floatingRejectionError(rejections[0]) };
|
|
819
|
+
reported = rejections.slice(1);
|
|
820
|
+
} else if (failure.error instanceof Error && failure.error.cause === rejections[0]) {
|
|
821
|
+
reported = rejections.slice(1);
|
|
822
|
+
}
|
|
823
|
+
for (const reason of reported) {
|
|
824
|
+
this.#log("warn", "Additional unhandled browser-run rejection", {
|
|
825
|
+
error: reason instanceof Error ? reason.message : String(reason),
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
return failure;
|
|
762
829
|
}
|
|
763
830
|
|
|
764
831
|
nextElementId(): number {
|
|
@@ -973,6 +1040,7 @@ export class WorkerCore {
|
|
|
973
1040
|
const signal = AbortSignal.any([timeoutSignal, ac.signal, runAc.signal]);
|
|
974
1041
|
const output = new RunOutput();
|
|
975
1042
|
const screenshots: ScreenshotResult[] = [];
|
|
1043
|
+
const floatingFailure = Promise.withResolvers<never>();
|
|
976
1044
|
const active: ActiveRun = {
|
|
977
1045
|
id: msg.id,
|
|
978
1046
|
ac,
|
|
@@ -980,6 +1048,9 @@ export class WorkerCore {
|
|
|
980
1048
|
output,
|
|
981
1049
|
screenshots,
|
|
982
1050
|
pendingTools: new Map(),
|
|
1051
|
+
rejectionOwner: {},
|
|
1052
|
+
floatingRejections: [],
|
|
1053
|
+
floatingFailure,
|
|
983
1054
|
inflight: new Map(),
|
|
984
1055
|
opCounter: 0,
|
|
985
1056
|
};
|
|
@@ -995,10 +1066,11 @@ export class WorkerCore {
|
|
|
995
1066
|
const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session, output, screenshots, active);
|
|
996
1067
|
const runtime = this.#ensureRuntime(msg.session);
|
|
997
1068
|
runtime.setCwd(msg.session.cwd);
|
|
1069
|
+
const onFloatingRejection = (reason: unknown): void => this.#recordFloatingRejection(active, reason);
|
|
998
1070
|
runtime.setRunScope({
|
|
999
|
-
page: bindRunFacade(runPage.page, signal),
|
|
1000
|
-
browser: bindRunFacade(browser, signal),
|
|
1001
|
-
tab: bindRunFacade(tabApi, signal),
|
|
1071
|
+
page: bindRunFacade(runPage.page, signal, active.rejectionOwner, onFloatingRejection),
|
|
1072
|
+
browser: bindRunFacade(browser, signal, active.rejectionOwner, onFloatingRejection),
|
|
1073
|
+
tab: bindRunFacade(tabApi, signal, active.rejectionOwner, onFloatingRejection),
|
|
1002
1074
|
assert: (cond: unknown, text?: string): void => {
|
|
1003
1075
|
if (!cond) throw new ToolError(text ?? "Assertion failed");
|
|
1004
1076
|
},
|
|
@@ -1010,10 +1082,12 @@ export class WorkerCore {
|
|
|
1010
1082
|
typeof msOrPredicate === "number"
|
|
1011
1083
|
? undefined
|
|
1012
1084
|
: { timeout: resolvePredicateTimeout(msg.timeoutMs, opts?.timeout), interval: opts?.interval };
|
|
1013
|
-
return
|
|
1085
|
+
return observeBrowserRunPromise(
|
|
1014
1086
|
this.#runOp(active, label, signal, Number.POSITIVE_INFINITY, sig =>
|
|
1015
1087
|
waitForRun(msOrPredicate, sig, resolved),
|
|
1016
1088
|
),
|
|
1089
|
+
active.rejectionOwner,
|
|
1090
|
+
onFloatingRejection,
|
|
1017
1091
|
);
|
|
1018
1092
|
},
|
|
1019
1093
|
});
|
|
@@ -1051,10 +1125,19 @@ export class WorkerCore {
|
|
|
1051
1125
|
try {
|
|
1052
1126
|
const hooks = this.#hooksForActiveRun();
|
|
1053
1127
|
if (!hooks) throw new ToolError("Browser runtime started without an active run");
|
|
1054
|
-
returnValue = await
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1128
|
+
returnValue = await withBrowserPromiseCombinatorTracking(
|
|
1129
|
+
active.rejectionOwner,
|
|
1130
|
+
onFloatingRejection,
|
|
1131
|
+
async () =>
|
|
1132
|
+
await Promise.race([
|
|
1133
|
+
runtime.run(msg.code, `browser-run-${msg.id}.js`, hooks, {
|
|
1134
|
+
runId: msg.id,
|
|
1135
|
+
cwd: msg.session.cwd,
|
|
1136
|
+
}),
|
|
1137
|
+
cancelRejection,
|
|
1138
|
+
floatingFailure.promise,
|
|
1139
|
+
]),
|
|
1140
|
+
);
|
|
1058
1141
|
completed = true;
|
|
1059
1142
|
} finally {
|
|
1060
1143
|
signal.removeEventListener("abort", onCancel);
|
|
@@ -1063,11 +1146,13 @@ export class WorkerCore {
|
|
|
1063
1146
|
failure = { error };
|
|
1064
1147
|
} finally {
|
|
1065
1148
|
runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
|
|
1149
|
+
await Bun.sleep(0);
|
|
1066
1150
|
try {
|
|
1067
1151
|
await runPage?.cleanup();
|
|
1068
1152
|
} catch (error) {
|
|
1069
1153
|
failure = { error };
|
|
1070
1154
|
}
|
|
1155
|
+
failure = this.#foldFloatingRejections(active, failure);
|
|
1071
1156
|
if (this.#active?.id === msg.id) this.#active = null;
|
|
1072
1157
|
}
|
|
1073
1158
|
if (failure) {
|
|
@@ -1182,9 +1267,12 @@ export class WorkerCore {
|
|
|
1182
1267
|
(opTimeout?.aborted || (err instanceof Error && err.name === "TimeoutError"))
|
|
1183
1268
|
) {
|
|
1184
1269
|
const hint = selector ? await this.#selectorTimeoutHint(selector) : "";
|
|
1185
|
-
throw
|
|
1270
|
+
throw markBrowserRunRejection(
|
|
1271
|
+
new ToolError(`${label} timed out after ${perOpTimeoutMs}ms${hint}`),
|
|
1272
|
+
active.rejectionOwner,
|
|
1273
|
+
);
|
|
1186
1274
|
}
|
|
1187
|
-
throw err;
|
|
1275
|
+
throw markBrowserRunRejection(err, active.rejectionOwner);
|
|
1188
1276
|
} finally {
|
|
1189
1277
|
earlyAc.abort();
|
|
1190
1278
|
active.inflight.delete(opId);
|
|
@@ -1868,6 +1956,7 @@ export class WorkerCore {
|
|
|
1868
1956
|
|
|
1869
1957
|
async #close(): Promise<void> {
|
|
1870
1958
|
this.#unsub();
|
|
1959
|
+
this.#uninstallRejectionGuard();
|
|
1871
1960
|
this.#clearElementCache();
|
|
1872
1961
|
const page = this.#page;
|
|
1873
1962
|
if (this.#dialogHandler && page && !page.isClosed()) page.off("dialog", this.#dialogHandler);
|
package/src/tools/fetch.ts
CHANGED
|
@@ -516,7 +516,7 @@ function cleanFeedText(text: string): string {
|
|
|
516
516
|
* Parse RSS/Atom feed to markdown
|
|
517
517
|
*/
|
|
518
518
|
async function parseFeedToMarkdown(content: string, maxItems = 10): Promise<string> {
|
|
519
|
-
const { parseHTML } = await import("
|
|
519
|
+
const { parseHTML } = await import("@oh-my-pi/pi-utils/dom");
|
|
520
520
|
try {
|
|
521
521
|
const doc = parseHTML(content).document;
|
|
522
522
|
|
package/src/tools/glob.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
|
6
6
|
import * as natives from "@oh-my-pi/pi-natives";
|
|
7
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
|
-
import { formatGroupedPaths, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { formatGroupedPaths, hasFsCode, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import { InternalUrlRouter } from "../internal-urls";
|
|
12
12
|
import { splitMemoryGlobPattern } from "../internal-urls/memory-protocol";
|
|
@@ -415,7 +415,9 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
415
415
|
try {
|
|
416
416
|
stat = await fs.promises.stat(target.searchPath);
|
|
417
417
|
} catch (err) {
|
|
418
|
-
|
|
418
|
+
// ENAMETOOLONG can never name a real target; surface a clean
|
|
419
|
+
// "Path not found" instead of leaking the raw errno (issue #7597).
|
|
420
|
+
if (isEnoent(err) || hasFsCode(err, "ENAMETOOLONG")) {
|
|
419
421
|
if (isSingle) throw new ToolError(`Path not found: ${scopePath}`);
|
|
420
422
|
return [];
|
|
421
423
|
}
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
5
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
6
|
-
import { isEnoent, isEnotdir, stripWindowsExtendedLengthPathPrefix, untilAborted } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { hasFsCode, isEnoent, isEnotdir, stripWindowsExtendedLengthPathPrefix, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import type { Skill } from "../extensibility/skills";
|
|
8
8
|
import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
|
|
9
9
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
@@ -337,6 +337,13 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
|
|
|
337
337
|
* plus selector `1-2` (issue #4618). `lstat` inspects the entry itself, so a
|
|
338
338
|
* dangling symlink is still detected as present; ambiguous errors resolve to
|
|
339
339
|
* `"unknown"` so callers keep the raw path instead of guessing.
|
|
340
|
+
*
|
|
341
|
+
* `ENAMETOOLONG` resolves to `"missing"` rather than `"unknown"`: a path whose
|
|
342
|
+
* component or whole length exceeds the OS limit can never name a real single
|
|
343
|
+
* entry, so it is strictly stronger evidence of non-existence than `ENOENT`.
|
|
344
|
+
* Without this, a semicolon-joined `path` list long enough to trip the limit
|
|
345
|
+
* (bare filenames past `NAME_MAX`, or a total past `PATH_MAX`) was read as one
|
|
346
|
+
* literal path and the delimited split was suppressed (issue #7597).
|
|
340
347
|
*/
|
|
341
348
|
export async function probeLiteralPathExists(filePath: string, cwd: string): Promise<"exists" | "missing" | "unknown"> {
|
|
342
349
|
const resolved = resolveReadPath(filePath, cwd);
|
|
@@ -344,7 +351,7 @@ export async function probeLiteralPathExists(filePath: string, cwd: string): Pro
|
|
|
344
351
|
await fs.promises.lstat(resolved);
|
|
345
352
|
return "exists";
|
|
346
353
|
} catch (err) {
|
|
347
|
-
if (isEnoent(err) || isEnotdir(err)) return "missing";
|
|
354
|
+
if (isEnoent(err) || isEnotdir(err) || hasFsCode(err, "ENAMETOOLONG")) return "missing";
|
|
348
355
|
return "unknown";
|
|
349
356
|
}
|
|
350
357
|
}
|
|
@@ -762,7 +769,10 @@ async function delimitedPathPartResolves(entry: string, cwd: string, splitter: P
|
|
|
762
769
|
await fs.promises.stat(absoluteBasePath);
|
|
763
770
|
return true;
|
|
764
771
|
} catch (err) {
|
|
765
|
-
|
|
772
|
+
// ENOENT and ENAMETOOLONG both mean this string cannot name an existing
|
|
773
|
+
// path, so the whole entry does not resolve and the delimited split may
|
|
774
|
+
// proceed (issue #7597). Other errors (EACCES, transient I/O) stay fatal.
|
|
775
|
+
if (isEnoent(err) || hasFsCode(err, "ENAMETOOLONG")) return false;
|
|
766
776
|
throw err;
|
|
767
777
|
}
|
|
768
778
|
}
|
|
@@ -806,10 +816,18 @@ async function tryDelimitedPathSplit(
|
|
|
806
816
|
export async function splitDelimitedPathEntry(
|
|
807
817
|
entry: string,
|
|
808
818
|
cwd: string,
|
|
809
|
-
options: {
|
|
819
|
+
options: {
|
|
820
|
+
splitter?: PathEntrySplitter;
|
|
821
|
+
routedUrlPredicate?: (entry: string) => boolean;
|
|
822
|
+
} = {},
|
|
810
823
|
): Promise<string[] | null> {
|
|
811
824
|
const normalizedEntry = normalizePathLikeInput(entry);
|
|
812
825
|
if (!hasTopLevelPathDelimiter(normalizedEntry)) return null;
|
|
826
|
+
const splitter = options.splitter ?? parseSearchPath;
|
|
827
|
+
if (options.routedUrlPredicate?.(normalizedEntry)) {
|
|
828
|
+
const parts = await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "semicolon", "none");
|
|
829
|
+
return parts?.every(options.routedUrlPredicate) ? parts : null;
|
|
830
|
+
}
|
|
813
831
|
if (isInternalUrlPath(normalizedEntry)) return null;
|
|
814
832
|
// A real POSIX file may contain the delimiter and a selector-shaped tail
|
|
815
833
|
// (`a;b:1-2`, `a b:1-2`). Preserve the raw entry whenever the full literal
|
|
@@ -817,7 +835,6 @@ export async function splitDelimitedPathEntry(
|
|
|
817
835
|
// splitters see it before delimiter expansion peels or splits (issue #4618
|
|
818
836
|
// reviewer feedback: delimited expansion ran before the literal check).
|
|
819
837
|
if ((await probeLiteralPathExists(normalizedEntry, cwd)) !== "missing") return null;
|
|
820
|
-
const splitter = options.splitter ?? parseSearchPath;
|
|
821
838
|
const peeledEntry = splitPathAndSel(normalizedEntry).path;
|
|
822
839
|
if (!hasGlobPathChars(peeledEntry) && (await delimitedPathPartResolves(normalizedEntry, cwd, splitter))) {
|
|
823
840
|
return null;
|