@linxiraos/pi-utils 1.1.6 → 1.1.8
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 +5 -6
- package/dist/types/async.d.ts +3 -3
- package/dist/types/env.d.ts +9 -0
- package/dist/types/fetch-retry.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/materialize-string.d.ts +7 -0
- package/dist/types/path.d.ts +2 -0
- package/dist/types/postmortem.d.ts +4 -0
- package/dist/types/runtime-install.d.ts +8 -1
- package/dist/types/temp.d.ts +7 -0
- package/dist/types/vterm/query-responder.d.ts +24 -0
- package/dist/types/vterm.d.ts +1 -0
- package/package.json +2 -2
- package/src/acp/connection.ts +6 -10
- package/src/ar/entries.ts +1 -1
- package/src/ar/sevenzip.ts +17 -2
- package/src/async.ts +9 -4
- package/src/browsers.ts +11 -5
- package/src/dom/core.ts +4 -4
- package/src/env.ts +12 -0
- package/src/fetch-retry.ts +36 -1
- package/src/format.ts +1 -1
- package/src/frontmatter.ts +1 -0
- package/src/index.ts +1 -0
- package/src/marked/core.ts +8 -1
- package/src/materialize-string.ts +10 -0
- package/src/path.ts +14 -0
- package/src/postmortem.ts +33 -6
- package/src/prompt.ts +1 -0
- package/src/ring.ts +1 -0
- package/src/runtime-install.ts +77 -26
- package/src/temp.ts +16 -4
- package/src/template.ts +2 -2
- package/src/tls-fetch.ts +1 -1
- package/src/turndown/html.ts +1 -1
- package/src/vterm/query-responder.ts +76 -0
- package/src/vterm.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.1.8] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
- Added `symlinkDirectorySync` — Windows-correct directory links (junction) for tests and tooling that must link temp trees without elevation.
|
|
8
|
+
- Extended the temp-removal retry window to 7.5s: measured Windows handle-release latency after a child process dies reaches ~5s, and the previous 2s window flaked suites cleaning up trees containing SQLite databases.
|
|
9
|
+
|
|
5
10
|
## [1.1.6] - 2026-08-30
|
|
6
11
|
|
|
7
12
|
- 同步上游 OMP v18.0.10(`33cc6b9a043a`)。
|
|
@@ -11,12 +16,6 @@
|
|
|
11
16
|
|
|
12
17
|
- 同步上游 OMP v18.0.5 / v18.0.6:新增 browsers / json 工具,SHA-2/SHA-3 在 ARM64 上加速。
|
|
13
18
|
|
|
14
|
-
## [1.1.3] - 2026-08-25
|
|
15
|
-
|
|
16
|
-
### Fixed
|
|
17
|
-
|
|
18
|
-
- Republished as 1.1.3 to reset the latest tag after the broken 1.1.2 (no functional change over 1.1.1).
|
|
19
|
-
|
|
20
19
|
## [1.1.2] - 2026-08-25
|
|
21
20
|
|
|
22
21
|
### Fixed
|
package/dist/types/async.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wrap a promise with a timeout and optional abort signal.
|
|
3
|
-
* Rejects with the given
|
|
4
|
-
* Cleans up all listeners on settlement.
|
|
3
|
+
* Rejects with the given error or a new error containing the given message if
|
|
4
|
+
* the timeout fires first. Cleans up all listeners on settlement.
|
|
5
5
|
*/
|
|
6
|
-
export declare function withTimeout<T>(promise: Promise<T>, ms: number,
|
|
6
|
+
export declare function withTimeout<T>(promise: Promise<T>, ms: number, timeout: string | Error, signal?: AbortSignal): Promise<T>;
|
|
7
7
|
/**
|
|
8
8
|
* Coalesces rapid-fire writes into one deferred batch. `push` queues a value
|
|
9
9
|
* and returns a promise for the batch flush; the first push of a batch arms a
|
package/dist/types/env.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ export declare function isValidEnvName(name: string): boolean;
|
|
|
16
16
|
export declare function isSafeEnvName(name: string): boolean;
|
|
17
17
|
export declare function isSafeEnvValue(value: string): boolean;
|
|
18
18
|
export declare function isMacosMallocStackLoggingEnvName(name: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* True when running inside a WSL (Windows Subsystem for Linux) distribution.
|
|
21
|
+
*
|
|
22
|
+
* WSL reports `linux` for `process.platform`, so the only reliable signal is
|
|
23
|
+
* the `WSL_DISTRO_NAME`/`WSL_INTEROP` variables the interop layer injects.
|
|
24
|
+
* Callers use this to translate Windows drive paths to their `/mnt/<drive>`
|
|
25
|
+
* mounts and to route clipboard access through `powershell.exe`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isWsl(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
|
|
19
28
|
export declare function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string>;
|
|
20
29
|
/** Filters process env for child shells without launch-cwd dotenv values. */
|
|
21
30
|
export declare function filterChildShellEnv(env: Record<string, string | undefined>, cwd?: string): Record<string, string>;
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* - `Please retry in 250ms` / `Please retry in 12s`
|
|
15
15
|
* - `"retryDelay": "34.074824224s"` (JSON error detail field)
|
|
16
16
|
* - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
|
|
17
|
+
* - `retry-after-ms=98497000`
|
|
18
|
+
* - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
|
|
17
19
|
*
|
|
18
20
|
* Returns `undefined` if no signal is found.
|
|
19
21
|
*/
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./json.js";
|
|
|
14
14
|
export * from "./json-parse.js";
|
|
15
15
|
export * as logger from "./logger.js";
|
|
16
16
|
export * from "./loop-phase.js";
|
|
17
|
+
export * from "./materialize-string.js";
|
|
17
18
|
export * from "./math-delimiters.js";
|
|
18
19
|
export * from "./mermaid-ascii.js";
|
|
19
20
|
export * from "./mime.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy text into independent UTF-16 backing storage before a bounded substring outlives its source.
|
|
3
|
+
*
|
|
4
|
+
* The UTF-16 round trip preserves every JavaScript code unit, including lone
|
|
5
|
+
* surrogates that a UTF-8 round trip would replace.
|
|
6
|
+
*/
|
|
7
|
+
export declare function materializeString(text: string): string;
|
package/dist/types/path.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/** Maps an absolute Windows drive path to the drive's default WSL mount. */
|
|
2
|
+
export declare function windowsPathToWslMount(filePath: string): string | undefined;
|
|
1
3
|
/** Removes Win32 extended-length prefixes before passing paths to Bun APIs. */
|
|
2
4
|
export declare function stripWindowsExtendedLengthPathPrefix(filePath: string, platform?: NodeJS.Platform): string;
|
|
@@ -123,6 +123,10 @@ export declare function interceptUnhandledRejections(interceptor: (reason: unkno
|
|
|
123
123
|
* through an uncaught exception or unhandled rejection.
|
|
124
124
|
*/
|
|
125
125
|
export declare function registerFatalRecoveryHint(provider: FatalRecoveryHintProvider): () => void;
|
|
126
|
+
/**
|
|
127
|
+
* Reports a caught top-level failure after terminal owners restore their display, then exits.
|
|
128
|
+
*/
|
|
129
|
+
export declare function fatal(error: unknown): Promise<never>;
|
|
126
130
|
/** Controls when a registered cleanup callback participates in cleanup passes. */
|
|
127
131
|
export interface CleanupRegistrationOptions {
|
|
128
132
|
/**
|
|
@@ -73,6 +73,13 @@ export interface EnsureRuntimeInstalledOptions {
|
|
|
73
73
|
export declare function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void>;
|
|
74
74
|
/**
|
|
75
75
|
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
76
|
-
* cross-process safe
|
|
76
|
+
* cross-process safe). Returns `runtimeDir`.
|
|
77
|
+
*
|
|
78
|
+
* Serialization uses the OS-backed {@link withFileLock} at
|
|
79
|
+
* `${runtimeDir}.install.lock`, which the kernel releases on process death, so
|
|
80
|
+
* a crashed installer cannot wedge later attempts (issue #10120). The path is
|
|
81
|
+
* deliberately distinct from the legacy `${runtimeDir}.lock` mkdir directory;
|
|
82
|
+
* {@link withLegacyInstallLock} atomically reserves that namespace during the
|
|
83
|
+
* new install so older processes cannot cross the migration boundary.
|
|
77
84
|
*/
|
|
78
85
|
export declare function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string>;
|
package/dist/types/temp.d.ts
CHANGED
|
@@ -15,3 +15,10 @@ export declare class TempDir {
|
|
|
15
15
|
/** Removes a path recursively, retrying transient Windows deletion failures. */
|
|
16
16
|
export declare function removeWithRetries(target: string): Promise<void>;
|
|
17
17
|
export declare function removeSyncWithRetries(target: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Create a directory symlink in a platform-correct way. Windows denies
|
|
20
|
+
* unprivileged file/dir symlinks (EPERM on `fs.symlink` without a type), but
|
|
21
|
+
* allows directory *junctions*, which every test that links temp directories
|
|
22
|
+
* should use instead. POSIX ignores the type argument.
|
|
23
|
+
*/
|
|
24
|
+
export declare function symlinkDirectorySync(target: string, linkPath: string): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answers terminal capability queries emitted by programs on a headless PTY.
|
|
3
|
+
*
|
|
4
|
+
* A PTY that advertises `TERM=xterm-256color` but has no terminal behind it
|
|
5
|
+
* leaves capability probes unanswered: a program writes a query escape to
|
|
6
|
+
* stdout and blocks on stdin until the reply arrives (or its timeout expires,
|
|
7
|
+
* seconds later). This scanner watches raw PTY output for the standard queries
|
|
8
|
+
* and returns the bytes a real xterm-class terminal would send back, so the
|
|
9
|
+
* caller can write them into the PTY. It keeps no screen state — cursor
|
|
10
|
+
* position reports are answered with the home position — which makes it cheap
|
|
11
|
+
* enough to run on every byte of a long-lived supervised process. Use the full
|
|
12
|
+
* {@link Terminal} when the caller also renders the output.
|
|
13
|
+
*
|
|
14
|
+
* Queries can straddle chunk boundaries, so an unfinished trailing escape is
|
|
15
|
+
* carried into the next {@link feed}.
|
|
16
|
+
*/
|
|
17
|
+
export declare class TerminalQueryResponder {
|
|
18
|
+
#private;
|
|
19
|
+
/**
|
|
20
|
+
* Feed one raw PTY output chunk. Returns the reply bytes to write back into
|
|
21
|
+
* the PTY, or an empty string when the chunk held no answerable query.
|
|
22
|
+
*/
|
|
23
|
+
feed(chunk: string): string;
|
|
24
|
+
}
|
package/dist/types/vterm.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Behavior-compatible reimplementation of @xterm/headless's used surface. */
|
|
2
2
|
export * from "./vterm/buffer.js";
|
|
3
|
+
export * from "./vterm/query-responder.js";
|
|
3
4
|
export * from "./vterm/terminal.js";
|
|
4
5
|
import { Terminal } from "./vterm/terminal.js";
|
|
5
6
|
declare const vterm: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-utils",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.8",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@linxiraos/pi-natives": "1.1.
|
|
34
|
+
"@linxiraos/pi-natives": "1.1.8"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "^1.3.14"
|
package/src/acp/connection.ts
CHANGED
|
@@ -67,13 +67,11 @@ export class AgentSideConnection {
|
|
|
67
67
|
#connection: RpcConnection;
|
|
68
68
|
|
|
69
69
|
constructor(toAgent: (connection: AgentSideConnection) => Agent, stream: Stream) {
|
|
70
|
-
let agent: Agent | undefined;
|
|
71
70
|
this.#connection = new RpcConnection(stream, async (method, params, notification) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return dispatchAgent(target, method, params, notification);
|
|
71
|
+
if (!agent) throw RequestError.internalError(undefined, "Agent is not initialized");
|
|
72
|
+
return dispatchAgent(agent, method, params, notification);
|
|
75
73
|
});
|
|
76
|
-
agent = toAgent(this);
|
|
74
|
+
const agent = toAgent(this);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
/** Signal aborted when the transport closes. */
|
|
@@ -170,13 +168,11 @@ export class ClientSideConnection {
|
|
|
170
168
|
#connection: RpcConnection;
|
|
171
169
|
|
|
172
170
|
constructor(toClient: (connection: ClientSideConnection) => Client, stream: Stream) {
|
|
173
|
-
let client: Client | undefined;
|
|
174
171
|
this.#connection = new RpcConnection(stream, async (method, params, notification) => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return dispatchClient(target, method, params, notification);
|
|
172
|
+
if (!client) throw RequestError.internalError(undefined, "Client is not initialized");
|
|
173
|
+
return dispatchClient(client, method, params, notification);
|
|
178
174
|
});
|
|
179
|
-
client = toClient(this);
|
|
175
|
+
const client = toClient(this);
|
|
180
176
|
}
|
|
181
177
|
/** Signal aborted when the transport closes. */
|
|
182
178
|
get signal(): AbortSignal {
|
package/src/ar/entries.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function upsertArchiveEntry(
|
|
|
45
45
|
*/
|
|
46
46
|
export function ensureParentDirectories(map: Map<string, ArchiveIndexEntry>, limits: ArchiveLimits): void {
|
|
47
47
|
assertEntryCount(map.size, limits);
|
|
48
|
-
for (const entry of
|
|
48
|
+
for (const entry of Array.from(map.values())) {
|
|
49
49
|
const parts = entry.path.split("/");
|
|
50
50
|
const stop = parts.length - 1;
|
|
51
51
|
for (let index = 1; index <= stop; index++) {
|
package/src/ar/sevenzip.ts
CHANGED
|
@@ -107,6 +107,7 @@ interface FileMetadata {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function readBoolVector(reader: HeaderReader, count: number): boolean[] {
|
|
110
|
+
// [suppressed] length preallocation
|
|
110
111
|
const result = new Array<boolean>(count);
|
|
111
112
|
let byte = 0;
|
|
112
113
|
let mask = 0;
|
|
@@ -122,12 +123,16 @@ function readBoolVector(reader: HeaderReader, count: number): boolean[] {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
function readDefinedVector(reader: HeaderReader, count: number): boolean[] {
|
|
125
|
-
if (reader.readByte() !== 0)
|
|
126
|
+
if (reader.readByte() !== 0) {
|
|
127
|
+
// [suppressed] length preallocation
|
|
128
|
+
return new Array<boolean>(count).fill(true);
|
|
129
|
+
}
|
|
126
130
|
return readBoolVector(reader, count);
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
function readDigests(reader: HeaderReader, count: number): Digests {
|
|
130
134
|
const defined = readDefinedVector(reader, count);
|
|
135
|
+
// [suppressed] length preallocation
|
|
131
136
|
const values = new Array<number | undefined>(count);
|
|
132
137
|
for (let index = 0; index < count; index++) if (defined[index]) values[index] = reader.readUInt32();
|
|
133
138
|
return { defined, values };
|
|
@@ -229,7 +234,10 @@ function parsePackInfo(reader: HeaderReader, streams: SevenZipStreams, options:
|
|
|
229
234
|
} else reader.skipSizedProperty();
|
|
230
235
|
}
|
|
231
236
|
if (streams.packSizes.length !== count) throw new ArchiveError("Invalid 7z PackInfo without complete sizes");
|
|
232
|
-
if (streams.packCrcs.length === 0)
|
|
237
|
+
if (streams.packCrcs.length === 0) {
|
|
238
|
+
// [suppressed] length preallocation
|
|
239
|
+
streams.packCrcs = new Array(count).fill(undefined);
|
|
240
|
+
}
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
function parseUnpackInfo(reader: HeaderReader, streams: SevenZipStreams, options: FormatReadOptions): void {
|
|
@@ -254,6 +262,7 @@ function parseUnpackInfo(reader: HeaderReader, streams: SevenZipStreams, options
|
|
|
254
262
|
}
|
|
255
263
|
|
|
256
264
|
function parseSubStreamsInfo(reader: HeaderReader, streams: SevenZipStreams): void {
|
|
265
|
+
// [suppressed] length preallocation
|
|
257
266
|
const counts = new Array<number>(streams.folders.length).fill(1);
|
|
258
267
|
let sizes: number[] | undefined;
|
|
259
268
|
let rawDigests: Digests | undefined;
|
|
@@ -398,6 +407,7 @@ function parseNames(reader: HeaderReader, count: number, external: Uint8Array[],
|
|
|
398
407
|
function parseTimes(reader: HeaderReader, count: number, external: Uint8Array[]): Array<number | undefined> {
|
|
399
408
|
const defined = readDefinedVector(reader, count);
|
|
400
409
|
const values = selectPropertyStream(reader, external);
|
|
410
|
+
// [suppressed] length preallocation
|
|
401
411
|
const result = new Array<number | undefined>(count);
|
|
402
412
|
for (let index = 0; index < count; index++)
|
|
403
413
|
if (defined[index]) {
|
|
@@ -411,6 +421,7 @@ function parseTimes(reader: HeaderReader, count: number, external: Uint8Array[])
|
|
|
411
421
|
function parseAttributes(reader: HeaderReader, count: number, external: Uint8Array[]): Array<number | undefined> {
|
|
412
422
|
const defined = readDefinedVector(reader, count);
|
|
413
423
|
const values = selectPropertyStream(reader, external);
|
|
424
|
+
// [suppressed] length preallocation
|
|
414
425
|
const result = new Array<number | undefined>(count);
|
|
415
426
|
for (let index = 0; index < count; index++) if (defined[index]) result[index] = values.readUInt32();
|
|
416
427
|
return result;
|
|
@@ -420,11 +431,15 @@ function parseFilesInfo(reader: HeaderReader, external: Uint8Array[], options: F
|
|
|
420
431
|
const count = reader.readNumber("file count");
|
|
421
432
|
assertEntryCount(count, options.limits);
|
|
422
433
|
const metadata: FileMetadata = {
|
|
434
|
+
// [suppressed] length preallocation
|
|
423
435
|
names: new Array<string>(count).fill(""),
|
|
436
|
+
// [suppressed] length preallocation
|
|
424
437
|
emptyStreams: new Array<boolean>(count).fill(false),
|
|
425
438
|
emptyFiles: [],
|
|
426
439
|
antiFiles: [],
|
|
440
|
+
// [suppressed] length preallocation
|
|
427
441
|
mtimes: new Array<number | undefined>(count),
|
|
442
|
+
// [suppressed] length preallocation
|
|
428
443
|
attributes: new Array<number | undefined>(count),
|
|
429
444
|
};
|
|
430
445
|
for (;;) {
|
package/src/async.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wrap a promise with a timeout and optional abort signal.
|
|
3
|
-
* Rejects with the given
|
|
4
|
-
* Cleans up all listeners on settlement.
|
|
3
|
+
* Rejects with the given error or a new error containing the given message if
|
|
4
|
+
* the timeout fires first. Cleans up all listeners on settlement.
|
|
5
5
|
*/
|
|
6
|
-
export function withTimeout<T>(
|
|
6
|
+
export function withTimeout<T>(
|
|
7
|
+
promise: Promise<T>,
|
|
8
|
+
ms: number,
|
|
9
|
+
timeout: string | Error,
|
|
10
|
+
signal?: AbortSignal,
|
|
11
|
+
): Promise<T> {
|
|
7
12
|
if (signal?.aborted) {
|
|
8
13
|
const reason = signal.reason instanceof Error ? signal.reason : new Error("Aborted");
|
|
9
14
|
return Promise.reject(reason);
|
|
@@ -15,7 +20,7 @@ export function withTimeout<T>(promise: Promise<T>, ms: number, message: string,
|
|
|
15
20
|
if (settled) return;
|
|
16
21
|
settled = true;
|
|
17
22
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
18
|
-
reject(new Error(
|
|
23
|
+
reject(typeof timeout === "string" ? new Error(timeout) : timeout);
|
|
19
24
|
}, ms);
|
|
20
25
|
|
|
21
26
|
const onAbort = () => {
|
package/src/browsers.ts
CHANGED
|
@@ -39,6 +39,13 @@ export enum BrowserPlatform {
|
|
|
39
39
|
WIN32 = "win32",
|
|
40
40
|
WIN64 = "win64",
|
|
41
41
|
}
|
|
42
|
+
type ChromeForTestingPlatform = Exclude<BrowserPlatform, BrowserPlatform.LINUX_ARM>;
|
|
43
|
+
|
|
44
|
+
function requireChromeForTestingPlatform(platform: BrowserPlatform): ChromeForTestingPlatform {
|
|
45
|
+
if (platform === BrowserPlatform.LINUX_ARM)
|
|
46
|
+
throw new Error("Chrome for Testing does not provide linux/arm64 builds");
|
|
47
|
+
return platform;
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
const BROWSERS = [
|
|
44
51
|
Browser.CHROME,
|
|
@@ -163,13 +170,13 @@ export function getDownloadUrl(
|
|
|
163
170
|
|
|
164
171
|
/** Compute the executable path in Puppeteer's cache layout. */
|
|
165
172
|
export function computeExecutablePath(options: ComputeExecutablePathOptions): string {
|
|
166
|
-
const
|
|
167
|
-
if (!
|
|
173
|
+
const detectedPlatform = options.platform ?? detectBrowserPlatform();
|
|
174
|
+
if (!detectedPlatform) throw new Error("Cannot determine a browser platform for this host");
|
|
168
175
|
if (options.browser !== Browser.CHROME) throw new Error(`Unsupported browser executable: ${options.browser}`);
|
|
176
|
+
const platform = requireChromeForTestingPlatform(detectedPlatform);
|
|
169
177
|
const installDir = installationDir(options.cacheDir, options.browser, platform, options.buildId);
|
|
170
178
|
switch (platform) {
|
|
171
179
|
case BrowserPlatform.LINUX:
|
|
172
|
-
case BrowserPlatform.LINUX_ARM:
|
|
173
180
|
return path.join(installDir, "chrome-linux64", "chrome");
|
|
174
181
|
case BrowserPlatform.MAC:
|
|
175
182
|
return path.join(
|
|
@@ -292,9 +299,8 @@ async function fetchMetadata<T>(filename: string): Promise<T> {
|
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
function chromeArchivePlatform(platform: BrowserPlatform): string {
|
|
295
|
-
switch (platform) {
|
|
302
|
+
switch (requireChromeForTestingPlatform(platform)) {
|
|
296
303
|
case BrowserPlatform.LINUX:
|
|
297
|
-
case BrowserPlatform.LINUX_ARM:
|
|
298
304
|
return "linux64";
|
|
299
305
|
case BrowserPlatform.MAC:
|
|
300
306
|
return "mac-x64";
|
package/src/dom/core.ts
CHANGED
|
@@ -105,7 +105,7 @@ export class EventTarget {
|
|
|
105
105
|
dispatchEvent(event: Event): boolean {
|
|
106
106
|
if (!event.target) event.target = this;
|
|
107
107
|
event.currentTarget = this;
|
|
108
|
-
for (const listener of
|
|
108
|
+
for (const listener of Array.from(this.#listeners.get(event.type) ?? [])) {
|
|
109
109
|
if (typeof listener === "function") listener.call(this, event);
|
|
110
110
|
else listener.handleEvent(event);
|
|
111
111
|
if (event.propagationStopped) break;
|
|
@@ -196,7 +196,7 @@ export class Node extends EventTarget {
|
|
|
196
196
|
const node: Node = child;
|
|
197
197
|
if (node === this || node.contains(this)) throw new Error("The new child is an ancestor of this node");
|
|
198
198
|
if (child instanceof DocumentFragment) {
|
|
199
|
-
for (const nested of
|
|
199
|
+
for (const nested of Array.from(child.childNodes)) this.appendChild(nested);
|
|
200
200
|
return child;
|
|
201
201
|
}
|
|
202
202
|
child.parentNode?.removeChild(child);
|
|
@@ -212,7 +212,7 @@ export class Node extends EventTarget {
|
|
|
212
212
|
const index = this.childNodes.indexOf(reference);
|
|
213
213
|
if (index < 0) throw new Error("The reference node is not a child of this node");
|
|
214
214
|
if (child instanceof DocumentFragment) {
|
|
215
|
-
for (const nested of
|
|
215
|
+
for (const nested of Array.from(child.childNodes)) this.insertBefore(nested, reference);
|
|
216
216
|
return child;
|
|
217
217
|
}
|
|
218
218
|
child.parentNode?.removeChild(child);
|
|
@@ -681,7 +681,7 @@ export class Element extends Node {
|
|
|
681
681
|
const parent = this.parentNode;
|
|
682
682
|
if (!parent) return;
|
|
683
683
|
const fragment = parseFragment(value, this.documentForCreation(), this.parentElement?.localName);
|
|
684
|
-
for (const child of
|
|
684
|
+
for (const child of Array.from(fragment.childNodes)) parent.insertBefore(child, this);
|
|
685
685
|
parent.removeChild(this);
|
|
686
686
|
}
|
|
687
687
|
|
package/src/env.ts
CHANGED
|
@@ -36,6 +36,18 @@ export function isMacosMallocStackLoggingEnvName(name: string): boolean {
|
|
|
36
36
|
return name === "MallocStackLogging" || name === "MallocStackLoggingNoCompact";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* True when running inside a WSL (Windows Subsystem for Linux) distribution.
|
|
41
|
+
*
|
|
42
|
+
* WSL reports `linux` for `process.platform`, so the only reliable signal is
|
|
43
|
+
* the `WSL_DISTRO_NAME`/`WSL_INTEROP` variables the interop layer injects.
|
|
44
|
+
* Callers use this to translate Windows drive paths to their `/mnt/<drive>`
|
|
45
|
+
* mounts and to route clipboard access through `powershell.exe`.
|
|
46
|
+
*/
|
|
47
|
+
export function isWsl(platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
48
|
+
return platform === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
export function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string> {
|
|
40
52
|
const result: Record<string, string> = {};
|
|
41
53
|
for (const key in env) {
|
package/src/fetch-retry.ts
CHANGED
|
@@ -12,6 +12,12 @@ const RETRY_DELAY_FIELD_PATTERN = /"retryDelay":\s*"([0-9.]+)(ms|s)"/i;
|
|
|
12
12
|
const TRY_AGAIN_PATTERN = /try again in\s+~?\s*([0-9.]+)\s*(ms|sec|s|minutes?|mins?|m|hours?|hrs?|h)\b/i;
|
|
13
13
|
// "Your limit will reset in 13 minutes" / "reset in 13 minutes" / "will reset in 2h"
|
|
14
14
|
const WILL_RESET_IN_PATTERN = /(?:will\s+)?reset in\s+~?\s*([0-9.]+)\s*(ms|sec|s|minutes?|mins?|m|hours?|hrs?|h)\b/i;
|
|
15
|
+
// "Your limit will reset at 2026-09-01 09:44:51" / "reset at 2026-09-01T09:44:51Z"
|
|
16
|
+
const WILL_RESET_AT_PATTERN =
|
|
17
|
+
/(?:will\s+)?reset at\s+([0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:?[0-9]{2})?)/i;
|
|
18
|
+
const CN_RESET_AT_PATTERN = /将在\s*([0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}:[0-9]{2})\s*重置/;
|
|
19
|
+
// "retry-after-ms=98497000"
|
|
20
|
+
const RETRY_AFTER_MS_BODY_PATTERN = /\bretry-after-ms=([0-9]+)\b/i;
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* Server-suggested retry delay extraction. Merges the patterns historically used
|
|
@@ -29,6 +35,8 @@ const WILL_RESET_IN_PATTERN = /(?:will\s+)?reset in\s+~?\s*([0-9.]+)\s*(ms|sec|s
|
|
|
29
35
|
* - `Please retry in 250ms` / `Please retry in 12s`
|
|
30
36
|
* - `"retryDelay": "34.074824224s"` (JSON error detail field)
|
|
31
37
|
* - `try again in 250ms` / `try again in 12s` / `try again in 5 min` / `try again in ~158 min`
|
|
38
|
+
* - `retry-after-ms=98497000`
|
|
39
|
+
* - `Your limit will reset at 2026-09-01 09:44:51` / `将在 2026-09-01 09:44:51 重置`
|
|
32
40
|
*
|
|
33
41
|
* Returns `undefined` if no signal is found.
|
|
34
42
|
*/
|
|
@@ -85,11 +93,38 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
85
93
|
if (totalMs > 0) return totalMs;
|
|
86
94
|
}
|
|
87
95
|
}
|
|
96
|
+
for (const pattern of [WILL_RESET_AT_PATTERN, CN_RESET_AT_PATTERN]) {
|
|
97
|
+
const match = pattern.exec(body);
|
|
98
|
+
if (match?.[1]) {
|
|
99
|
+
// Provider timestamps without an explicit offset are interpreted as UTC.
|
|
100
|
+
const normalized = match[1].replace(" ", "T");
|
|
101
|
+
const hasOffset = /(?:Z|[+-][0-9]{2}:?[0-9]{2})$/i.test(normalized);
|
|
102
|
+
const parsed = Date.parse(hasOffset ? normalized : `${normalized}Z`);
|
|
103
|
+
if (!Number.isNaN(parsed) && parsed > Date.now()) {
|
|
104
|
+
return parsed - Date.now();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
88
108
|
// Account-reset hints ("will reset in …") take precedence over short
|
|
89
109
|
// retry hints ("please retry in 5s"): a body carrying both must honour the
|
|
90
110
|
// longer account window, not the shorter generic one. QUOTA_RESET_PATTERN
|
|
91
111
|
// ("reset after …") above already runs first and stays first.
|
|
92
|
-
|
|
112
|
+
const accountResetMatch = WILL_RESET_IN_PATTERN.exec(body);
|
|
113
|
+
if (accountResetMatch?.[1]) {
|
|
114
|
+
const value = Number.parseFloat(accountResetMatch[1]);
|
|
115
|
+
if (Number.isFinite(value) && value > 0) {
|
|
116
|
+
const unitMs = unitToMs(accountResetMatch[2]!);
|
|
117
|
+
if (unitMs !== undefined) return value * unitMs;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const retryAfterMsMatch = RETRY_AFTER_MS_BODY_PATTERN.exec(body);
|
|
122
|
+
if (retryAfterMsMatch?.[1]) {
|
|
123
|
+
const ms = Number(retryAfterMsMatch[1]);
|
|
124
|
+
if (Number.isFinite(ms) && ms > 0) return ms;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const pattern of [PLEASE_RETRY_PATTERN, RETRY_DELAY_FIELD_PATTERN, TRY_AGAIN_PATTERN]) {
|
|
93
128
|
const match = pattern.exec(body);
|
|
94
129
|
if (match?.[1]) {
|
|
95
130
|
const value = Number.parseFloat(match[1]);
|
package/src/format.ts
CHANGED
|
@@ -9,7 +9,7 @@ const DAY = 24 * HOUR;
|
|
|
9
9
|
*/
|
|
10
10
|
export function formatDuration(ms: number): string {
|
|
11
11
|
if (!Number.isFinite(ms) || ms <= 0) return "0ms";
|
|
12
|
-
if (ms < SEC) return `${ms}ms`;
|
|
12
|
+
if (ms < SEC) return `${Math.floor(ms)}ms`;
|
|
13
13
|
if (ms < MIN) return `${(ms / SEC).toFixed(1)}s`;
|
|
14
14
|
if (ms < HOUR) {
|
|
15
15
|
const mins = Math.floor(ms / MIN);
|
package/src/frontmatter.ts
CHANGED
|
@@ -22,6 +22,7 @@ export function normalizeFrontmatterKeys<T>(obj: T): T {
|
|
|
22
22
|
if (obj === null || typeof obj !== "object") return obj;
|
|
23
23
|
if (Array.isArray(obj)) {
|
|
24
24
|
let changed = false;
|
|
25
|
+
// [suppressed] length preallocation
|
|
25
26
|
const out: unknown[] = new Array(obj.length);
|
|
26
27
|
for (let i = 0; i < obj.length; i++) {
|
|
27
28
|
const v = obj[i];
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./json";
|
|
|
14
14
|
export * from "./json-parse";
|
|
15
15
|
export * as logger from "./logger";
|
|
16
16
|
export * from "./loop-phase";
|
|
17
|
+
export * from "./materialize-string";
|
|
17
18
|
export * from "./math-delimiters";
|
|
18
19
|
export * from "./mermaid-ascii";
|
|
19
20
|
export * from "./mime";
|
package/src/marked/core.ts
CHANGED
|
@@ -343,8 +343,15 @@ const DEFAULTS: MarkedOptions = {
|
|
|
343
343
|
};
|
|
344
344
|
const PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
|
|
345
345
|
|
|
346
|
-
function tokenList(links: Links =
|
|
346
|
+
function tokenList(links: Links = Object.create(null)): TokensList {
|
|
347
347
|
const list = [] as unknown as TokensList;
|
|
348
|
+
// The reference-definition map is keyed by user-controlled labels. A plain
|
|
349
|
+
// `{}` inherits `Object.prototype`, so a reference-style link whose label is
|
|
350
|
+
// an inherited member (`[x][constructor]`, `[x][__proto__]`) resolves to a
|
|
351
|
+
// truthy non-definition and yields a link token with `href: undefined`
|
|
352
|
+
// (issue #10283). A null-prototype map makes such lookups miss, so the link
|
|
353
|
+
// correctly falls back to literal text, and label writes cannot pollute the
|
|
354
|
+
// prototype.
|
|
348
355
|
list.links = links;
|
|
349
356
|
return list;
|
|
350
357
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy text into independent UTF-16 backing storage before a bounded substring outlives its source.
|
|
3
|
+
*
|
|
4
|
+
* The UTF-16 round trip preserves every JavaScript code unit, including lone
|
|
5
|
+
* surrogates that a UTF-8 round trip would replace.
|
|
6
|
+
*/
|
|
7
|
+
export function materializeString(text: string): string {
|
|
8
|
+
if (text.length === 0) return "";
|
|
9
|
+
return Buffer.from(text, "utf16le").toString("utf16le");
|
|
10
|
+
}
|
package/src/path.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
const WINDOWS_DRIVE_PATH = /^([A-Za-z]):[\\/](.*)$/;
|
|
4
|
+
|
|
5
|
+
/** Maps an absolute Windows drive path to the drive's default WSL mount. */
|
|
6
|
+
export function windowsPathToWslMount(filePath: string): string | undefined {
|
|
7
|
+
const normalized = path.win32.normalize(filePath.trim());
|
|
8
|
+
const match = WINDOWS_DRIVE_PATH.exec(normalized);
|
|
9
|
+
if (!match) return undefined;
|
|
10
|
+
const [, drive, rest] = match;
|
|
11
|
+
const segments = rest.split("\\").filter(Boolean);
|
|
12
|
+
return path.posix.join("/mnt", drive!.toLowerCase(), ...segments);
|
|
13
|
+
}
|
|
14
|
+
|
|
1
15
|
const WINDOWS_DRIVE_EXTENDED_PREFIX = /^\\\\[?]\\([A-Za-z]:[\\/].*)$/;
|
|
2
16
|
const WINDOWS_UNC_EXTENDED_PREFIX = /^\\\\[?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
|
|
3
17
|
const WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/([A-Za-z]:\/.*)$/;
|
package/src/postmortem.ts
CHANGED
|
@@ -222,7 +222,7 @@ export function isInternalSocketClosedError(err: unknown): boolean {
|
|
|
222
222
|
const internal = frames.every(frame => {
|
|
223
223
|
const trimmed = frame.trim();
|
|
224
224
|
if (trimmed === "" || trimmed === "at unknown" || trimmed === "at native") return true;
|
|
225
|
-
if (!/\(node:[^)]*\)$/.test(trimmed) &&
|
|
225
|
+
if (!/\(node:[^)]*\)$/.test(trimmed) && !trimmed.startsWith("at node:")) return false;
|
|
226
226
|
hasNetFrame ||= trimmed.includes("node:net:");
|
|
227
227
|
return true;
|
|
228
228
|
});
|
|
@@ -395,23 +395,40 @@ function formatFatalError(label: string, err: Error): string {
|
|
|
395
395
|
return `\n[${label}] ${name}: ${message}${formattedStack}\n`;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
async function exitAfterFatal(
|
|
398
|
+
async function exitAfterFatal(output: string, logMessage: string, err: Error, reason: Reason): Promise<never> {
|
|
399
399
|
const forcedExit = setTimeout(() => exitProcess(1), CLEANUP_DEADLINE_MS);
|
|
400
400
|
try {
|
|
401
|
+
// Cleanup callbacks are invoked synchronously before runCleanup returns its
|
|
402
|
+
// completion promise. TUI owners therefore hand the cursor back before the
|
|
403
|
+
// fatal report is written, while slower resource cleanup continues afterward.
|
|
404
|
+
const cleanup = runCleanup(reason);
|
|
401
405
|
restoreTerminalStderr();
|
|
402
406
|
// A revoked terminal can make stream writes raise another fatal error. Use
|
|
403
407
|
// the descriptor directly so failure stays synchronous and contained.
|
|
404
408
|
try {
|
|
405
|
-
fs.writeSync(2,
|
|
409
|
+
fs.writeSync(2, output);
|
|
406
410
|
} catch {}
|
|
407
411
|
logger.error(logMessage, { err });
|
|
408
|
-
await
|
|
412
|
+
await cleanup;
|
|
409
413
|
} finally {
|
|
410
414
|
clearTimeout(forcedExit);
|
|
411
415
|
exitProcess(1);
|
|
412
416
|
}
|
|
413
417
|
}
|
|
414
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Reports a caught top-level failure after terminal owners restore their display, then exits.
|
|
421
|
+
*/
|
|
422
|
+
export async function fatal(error: unknown): Promise<never> {
|
|
423
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
424
|
+
const output = `${Bun.inspect(error, { colors: process.stderr.isTTY === true })}\n${formatFatalRecoveryHints()}`;
|
|
425
|
+
if (!isMainThread) {
|
|
426
|
+
process.stderr.write(output);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
return exitAfterFatal(output, "Fatal error", err, Reason.UNHANDLED_REJECTION);
|
|
430
|
+
}
|
|
431
|
+
|
|
415
432
|
if (isMainThread) {
|
|
416
433
|
process
|
|
417
434
|
.on("SIGINT", async () => {
|
|
@@ -461,7 +478,12 @@ if (isMainThread) {
|
|
|
461
478
|
});
|
|
462
479
|
return;
|
|
463
480
|
}
|
|
464
|
-
await exitAfterFatal(
|
|
481
|
+
await exitAfterFatal(
|
|
482
|
+
`${formatFatalError("Uncaught Exception", err)}${formatFatalRecoveryHints()}`,
|
|
483
|
+
"Uncaught exception",
|
|
484
|
+
err,
|
|
485
|
+
Reason.UNCAUGHT_EXCEPTION,
|
|
486
|
+
);
|
|
465
487
|
})
|
|
466
488
|
.on("unhandledRejection", async reason => {
|
|
467
489
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
@@ -497,7 +519,12 @@ if (isMainThread) {
|
|
|
497
519
|
});
|
|
498
520
|
}
|
|
499
521
|
}
|
|
500
|
-
await exitAfterFatal(
|
|
522
|
+
await exitAfterFatal(
|
|
523
|
+
`${formatFatalError("Unhandled Rejection", err)}${formatFatalRecoveryHints()}`,
|
|
524
|
+
"Unhandled rejection",
|
|
525
|
+
err,
|
|
526
|
+
Reason.UNHANDLED_REJECTION,
|
|
527
|
+
);
|
|
501
528
|
})
|
|
502
529
|
.on("exit", async () => {
|
|
503
530
|
void runCleanup(Reason.EXIT); // fire and forget (exit imminent)
|
package/src/prompt.ts
CHANGED
|
@@ -196,6 +196,7 @@ export function format(content: string, options: PromptFormatOptions = {}): stri
|
|
|
196
196
|
} = options;
|
|
197
197
|
const isPreRender = renderPhase === "pre-render";
|
|
198
198
|
const lines = content.split("\n");
|
|
199
|
+
// [suppressed] length preallocation
|
|
199
200
|
const result: string[] = new Array(lines.length);
|
|
200
201
|
let n = 0; // logical length of `result` (pops are n--)
|
|
201
202
|
let inCodeBlock = false;
|
package/src/ring.ts
CHANGED
|
@@ -15,6 +15,7 @@ export class RingBuffer<T> {
|
|
|
15
15
|
* @param capacity - The maximum number of elements the buffer can hold. Must be positive.
|
|
16
16
|
*/
|
|
17
17
|
constructor(public readonly capacity: number) {
|
|
18
|
+
// [suppressed] length preallocation
|
|
18
19
|
this.#buf = new Array(capacity);
|
|
19
20
|
}
|
|
20
21
|
|
package/src/runtime-install.ts
CHANGED
|
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as fsp from "node:fs/promises";
|
|
3
3
|
import * as Module from "node:module";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { withFileLock } from "./file-lock";
|
|
6
|
+
import { isEexist, isEnoent } from "./fs-error";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* On-demand runtime dependency support for native-heavy optional packages
|
|
@@ -303,25 +305,62 @@ export interface EnsureRuntimeInstalledOptions {
|
|
|
303
305
|
lockSleepMs?: number;
|
|
304
306
|
}
|
|
305
307
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
308
|
+
/** No runtime install plausibly runs this long, so older legacy lock directories are crash orphans. */
|
|
309
|
+
const STALE_LEGACY_LOCK_MS = 10 * 60_000;
|
|
309
310
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Run `fn` while reserving the pre-crash-safe `${runtimeDir}.lock` namespace.
|
|
313
|
+
*
|
|
314
|
+
* Versions through 18.0.10 serialized installs with a bare lock *directory*
|
|
315
|
+
* that only its creator removed; an installer killed outside that window
|
|
316
|
+
* (SIGKILL/OOM/Ctrl-C) left it unreleasable, wedging every later install for
|
|
317
|
+
* the full wait envelope (issue #10120). During an in-flight upgrade a legacy
|
|
318
|
+
* process may still legitimately own this directory, so poll until it is
|
|
319
|
+
* released and only force-reclaim once the directory is older than any
|
|
320
|
+
* plausible install ({@link STALE_LEGACY_LOCK_MS}) — never merely because a
|
|
321
|
+
* retry budget elapsed, which would delete a still-active legacy lock and let
|
|
322
|
+
* two installers race the same tree. Once the namespace is free, atomically
|
|
323
|
+
* create and retain a regular file through `fn`: an older process cannot
|
|
324
|
+
* acquire it between the handoff check and the new install. A file left by a
|
|
325
|
+
* crashed new installer can be reused immediately because the outer OS lock
|
|
326
|
+
* proves its owner is gone, unlike a legacy directory whose owner is unknown.
|
|
327
|
+
*/
|
|
328
|
+
async function withLegacyInstallLock<T>(runtimeDir: string, sleepMs: number, fn: () => Promise<T>): Promise<T> {
|
|
329
|
+
const legacy = `${runtimeDir}.lock`;
|
|
330
|
+
for (;;) {
|
|
314
331
|
try {
|
|
315
|
-
await fsp.
|
|
316
|
-
|
|
317
|
-
await fsp.rm(lockDir, { recursive: true, force: true });
|
|
318
|
-
};
|
|
332
|
+
const reservation = await fsp.open(legacy, "wx");
|
|
333
|
+
await reservation.close();
|
|
319
334
|
} catch (error) {
|
|
320
|
-
if (!
|
|
321
|
-
|
|
335
|
+
if (!isEexist(error)) throw error;
|
|
336
|
+
let stat: fs.Stats;
|
|
337
|
+
try {
|
|
338
|
+
stat = await fsp.stat(legacy);
|
|
339
|
+
} catch (statError) {
|
|
340
|
+
if (isEnoent(statError)) continue; // released between open and stat; retry
|
|
341
|
+
throw statError;
|
|
342
|
+
}
|
|
343
|
+
// A non-directory is a reservation left by a newer installer. The
|
|
344
|
+
// outer OS lock proves that installer is gone, so reuse it at once.
|
|
345
|
+
if (!stat.isDirectory()) break;
|
|
346
|
+
// A fresh directory may still belong to a live pre-18.x installer, so
|
|
347
|
+
// wait for it to finish; only a crash orphan (older than any plausible
|
|
348
|
+
// install) is force-reclaimed.
|
|
349
|
+
if (Date.now() - stat.mtimeMs > STALE_LEGACY_LOCK_MS) {
|
|
350
|
+
await fsp.rm(legacy, { recursive: true, force: true });
|
|
351
|
+
} else {
|
|
352
|
+
await Bun.sleep(sleepMs);
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
322
355
|
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
// Retain the regular-file reservation across the install.
|
|
359
|
+
try {
|
|
360
|
+
return await fn();
|
|
361
|
+
} finally {
|
|
362
|
+
await fsp.rm(legacy, { force: true });
|
|
323
363
|
}
|
|
324
|
-
throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
|
|
325
364
|
}
|
|
326
365
|
|
|
327
366
|
export async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
|
|
@@ -363,7 +402,14 @@ async function runRuntimeInstall(runtimeDir: string): Promise<void> {
|
|
|
363
402
|
|
|
364
403
|
/**
|
|
365
404
|
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
366
|
-
* cross-process safe
|
|
405
|
+
* cross-process safe). Returns `runtimeDir`.
|
|
406
|
+
*
|
|
407
|
+
* Serialization uses the OS-backed {@link withFileLock} at
|
|
408
|
+
* `${runtimeDir}.install.lock`, which the kernel releases on process death, so
|
|
409
|
+
* a crashed installer cannot wedge later attempts (issue #10120). The path is
|
|
410
|
+
* deliberately distinct from the legacy `${runtimeDir}.lock` mkdir directory;
|
|
411
|
+
* {@link withLegacyInstallLock} atomically reserves that namespace during the
|
|
412
|
+
* new install so older processes cannot cross the migration boundary.
|
|
367
413
|
*/
|
|
368
414
|
export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string> {
|
|
369
415
|
const { runtimeDir, install, onPhase, lockAttempts = 240, lockSleepMs = 250 } = options;
|
|
@@ -379,15 +425,20 @@ export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOpti
|
|
|
379
425
|
if (await probeManifest.exists()) return runtimeDir;
|
|
380
426
|
|
|
381
427
|
onPhase?.("initiate");
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
428
|
+
// withFileLock does not create parent directories; the runtime cache dir may
|
|
429
|
+
// not exist yet on the very first install.
|
|
430
|
+
await fsp.mkdir(path.dirname(runtimeDir), { recursive: true });
|
|
431
|
+
return withFileLock(
|
|
432
|
+
`${runtimeDir}.install`,
|
|
433
|
+
() =>
|
|
434
|
+
withLegacyInstallLock(runtimeDir, lockSleepMs, async () => {
|
|
435
|
+
if (await probeManifest.exists()) return runtimeDir;
|
|
436
|
+
await writeRuntimeManifest(runtimeDir, install);
|
|
437
|
+
onPhase?.("download");
|
|
438
|
+
await runRuntimeInstall(runtimeDir);
|
|
439
|
+
onPhase?.("done");
|
|
440
|
+
return runtimeDir;
|
|
441
|
+
}),
|
|
442
|
+
{ retries: lockAttempts, retryDelayMs: lockSleepMs },
|
|
443
|
+
);
|
|
393
444
|
}
|
package/src/temp.ts
CHANGED
|
@@ -78,10 +78,12 @@ function normalizePrefix(prefix?: string): string {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const kRemoveOptions = { recursive: true, force: true } as const;
|
|
81
|
-
const kRemoveRetries =
|
|
82
|
-
// 50ms ×
|
|
83
|
-
// SQLite DBs
|
|
84
|
-
//
|
|
81
|
+
const kRemoveRetries = 150;
|
|
82
|
+
// 50ms × 150 retries = 7.5s total retry window. Windows releases file locks on
|
|
83
|
+
// SQLite DBs (and spawned-process CWDs) asynchronously after close(); the
|
|
84
|
+
// measured release latency reaches ~730ms–5s when a child process just died
|
|
85
|
+
// holding the directory, and the previous 2s window flaked suites that clean
|
|
86
|
+
// up temp trees containing agent.db.
|
|
85
87
|
const kRemoveRetryDelayMs = 50;
|
|
86
88
|
const kRetryableRemoveErrorCodes = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
|
|
87
89
|
const kSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -132,3 +134,13 @@ function sleepSync(ms: number): void {
|
|
|
132
134
|
}
|
|
133
135
|
Atomics.wait(kSleepBuffer, 0, 0, ms);
|
|
134
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a directory symlink in a platform-correct way. Windows denies
|
|
140
|
+
* unprivileged file/dir symlinks (EPERM on `fs.symlink` without a type), but
|
|
141
|
+
* allows directory *junctions*, which every test that links temp directories
|
|
142
|
+
* should use instead. POSIX ignores the type argument.
|
|
143
|
+
*/
|
|
144
|
+
export function symlinkDirectorySync(target: string, linkPath: string): void {
|
|
145
|
+
fs.symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
|
|
146
|
+
}
|
package/src/template.ts
CHANGED
|
@@ -549,7 +549,7 @@ export function compile<T = unknown>(source: string, options: CompileOptions = {
|
|
|
549
549
|
const nodes = parseTemplate(stripStandalone(source));
|
|
550
550
|
return (context, runtime = {}) => {
|
|
551
551
|
const root = context ?? {};
|
|
552
|
-
const frame: Frame = { context: root, parents: [], root, data: { root, ...
|
|
552
|
+
const frame: Frame = { context: root, parents: [], root, data: { root, ...runtime.data } };
|
|
553
553
|
return renderNodes(nodes, frame, { helpers, partials, options, runtime });
|
|
554
554
|
};
|
|
555
555
|
}
|
|
@@ -579,7 +579,7 @@ export class TemplateEngine {
|
|
|
579
579
|
const nodes = parseTemplate(stripStandalone(source));
|
|
580
580
|
return (context, runtime = {}) => {
|
|
581
581
|
const root = context ?? {};
|
|
582
|
-
const frame: Frame = { context: root, parents: [], root, data: { root, ...
|
|
582
|
+
const frame: Frame = { context: root, parents: [], root, data: { root, ...runtime.data } };
|
|
583
583
|
return renderNodes(nodes, frame, { helpers: this.#helpers, partials: this.#partials, options, runtime });
|
|
584
584
|
};
|
|
585
585
|
}
|
package/src/tls-fetch.ts
CHANGED
|
@@ -174,5 +174,5 @@ export function withExtraCaFetch<T extends { fetch?: FetchImpl } | undefined>(op
|
|
|
174
174
|
const fetchImpl = options?.fetch ?? (globalThis.fetch as FetchImpl);
|
|
175
175
|
const wrapped = wrapFetchForExtraCa(fetchImpl);
|
|
176
176
|
if (wrapped === fetchImpl && options?.fetch !== undefined) return options;
|
|
177
|
-
return { ...
|
|
177
|
+
return { ...options, fetch: wrapped } as T;
|
|
178
178
|
}
|
package/src/turndown/html.ts
CHANGED
|
@@ -222,7 +222,7 @@ export function parseHtmlFragment(html: string): TurndownNode {
|
|
|
222
222
|
const stack: HtmlNode[] = [root];
|
|
223
223
|
for (const token of htmlTokens(html)) {
|
|
224
224
|
const parent = stack[stack.length - 1] ?? root;
|
|
225
|
-
if (token.startsWith("<!--") ||
|
|
225
|
+
if (token.startsWith("<!--") || token.startsWith("<!")) continue;
|
|
226
226
|
if (!token.startsWith("<")) {
|
|
227
227
|
parent.append(new HtmlText(decodeEntities(token)));
|
|
228
228
|
continue;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answers terminal capability queries emitted by programs on a headless PTY.
|
|
3
|
+
*
|
|
4
|
+
* A PTY that advertises `TERM=xterm-256color` but has no terminal behind it
|
|
5
|
+
* leaves capability probes unanswered: a program writes a query escape to
|
|
6
|
+
* stdout and blocks on stdin until the reply arrives (or its timeout expires,
|
|
7
|
+
* seconds later). This scanner watches raw PTY output for the standard queries
|
|
8
|
+
* and returns the bytes a real xterm-class terminal would send back, so the
|
|
9
|
+
* caller can write them into the PTY. It keeps no screen state — cursor
|
|
10
|
+
* position reports are answered with the home position — which makes it cheap
|
|
11
|
+
* enough to run on every byte of a long-lived supervised process. Use the full
|
|
12
|
+
* {@link Terminal} when the caller also renders the output.
|
|
13
|
+
*
|
|
14
|
+
* Queries can straddle chunk boundaries, so an unfinished trailing escape is
|
|
15
|
+
* carried into the next {@link feed}.
|
|
16
|
+
*/
|
|
17
|
+
export class TerminalQueryResponder {
|
|
18
|
+
/** Trailing bytes that may be the start of an unfinished query escape. */
|
|
19
|
+
#residual = "";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Feed one raw PTY output chunk. Returns the reply bytes to write back into
|
|
23
|
+
* the PTY, or an empty string when the chunk held no answerable query.
|
|
24
|
+
*/
|
|
25
|
+
feed(chunk: string): string {
|
|
26
|
+
const buffer = this.#residual + chunk;
|
|
27
|
+
let replies = "";
|
|
28
|
+
let lastEnd = 0;
|
|
29
|
+
QUERY.lastIndex = 0;
|
|
30
|
+
for (let match = QUERY.exec(buffer); match !== null; match = QUERY.exec(buffer)) {
|
|
31
|
+
lastEnd = match.index + match[0].length;
|
|
32
|
+
replies += replyFor(match);
|
|
33
|
+
}
|
|
34
|
+
// Keep only a short unmatched trailing escape: a query split across
|
|
35
|
+
// chunks completes on the next feed, while a long tail is ordinary output
|
|
36
|
+
// that can never become a query.
|
|
37
|
+
const tailEscape = buffer.lastIndexOf("\x1b");
|
|
38
|
+
this.#residual =
|
|
39
|
+
tailEscape >= lastEnd && buffer.length - tailEscape <= MAX_PARTIAL_QUERY ? buffer.slice(tailEscape) : "";
|
|
40
|
+
return replies;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Longest query escape we answer, bounding the cross-chunk residual. */
|
|
45
|
+
const MAX_PARTIAL_QUERY = 32;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* CSI DSR/DA queries (final byte `n` or `c`) and OSC 10/11 color queries. Only
|
|
49
|
+
* forms with canned answers are matched; everything else stays plain output.
|
|
50
|
+
*/
|
|
51
|
+
const QUERY = /\x1b\[([?>=]?)([0-9;]*)([nc])|\x1b\](10|11);\?(\x07|\x1b\\)/gu;
|
|
52
|
+
|
|
53
|
+
/** Reply a real xterm-class terminal would send for one matched query. */
|
|
54
|
+
function replyFor(match: RegExpExecArray): string {
|
|
55
|
+
const final = match[3];
|
|
56
|
+
if (final !== undefined) {
|
|
57
|
+
const intermediate = match[1];
|
|
58
|
+
const params = match[2] ?? "";
|
|
59
|
+
if (final === "c") {
|
|
60
|
+
if (intermediate === ">") return "\x1b[>0;10;1c"; // secondary DA: VT100-class, firmware 10
|
|
61
|
+
if (intermediate === "" || intermediate === "0") return "\x1b[?1;2c"; // primary DA: VT100 with AVO
|
|
62
|
+
return ""; // tertiary (`=`) DA has no widely expected reply
|
|
63
|
+
}
|
|
64
|
+
if (intermediate !== "") return ""; // private DSR forms (DECXCPR, appearance) stay unanswered
|
|
65
|
+
const selector = params.split(";", 1)[0];
|
|
66
|
+
if (selector === "6") return "\x1b[1;1R"; // cursor position: home, there is no screen
|
|
67
|
+
if (selector === "5") return "\x1b[0n"; // device status: OK
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
// OSC color queries: neutral colors, terminated the way the request was.
|
|
71
|
+
const selector = match[4];
|
|
72
|
+
const terminator = match[5] ?? "\x07";
|
|
73
|
+
if (selector === "10") return `\x1b]10;rgb:ffff/ffff/ffff${terminator}`; // foreground
|
|
74
|
+
if (selector === "11") return `\x1b]11;rgb:0000/0000/0000${terminator}`; // background
|
|
75
|
+
return "";
|
|
76
|
+
}
|