@linxiraos/pi-utils 1.1.26 → 1.2.0
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 +6 -4
- package/README.md +1 -1
- package/dist/types/ar/index.d.ts +1 -0
- package/dist/types/ar/zip-package.d.ts +21 -0
- package/dist/types/color.d.ts +2 -0
- package/dist/types/dirs.d.ts +37 -12
- package/dist/types/docx/converter.d.ts +2 -0
- package/dist/types/dom/core.d.ts +15 -4
- package/dist/types/dom/parser.d.ts +5 -3
- package/dist/types/dom/selector.d.ts +7 -1
- package/dist/types/logger/rotating-file.d.ts +4 -2
- package/dist/types/loop-phase.d.ts +12 -7
- package/dist/types/marked/core.d.ts +7 -6
- package/dist/types/marked-list.d.ts +38 -0
- package/dist/types/math-delimiters.d.ts +48 -0
- package/dist/types/procmgr.d.ts +14 -20
- package/dist/types/readability/types.d.ts +2 -0
- package/dist/types/temp.d.ts +7 -0
- package/dist/types/turndown/types.d.ts +7 -0
- package/dist/types/vterm/buffer.d.ts +16 -5
- package/dist/types/vterm/query-responder.d.ts +15 -0
- package/package.json +2 -2
- package/src/ar/index.ts +1 -0
- package/src/ar/open.ts +29 -8
- package/src/ar/zip-package.ts +57 -0
- package/src/chalk.ts +66 -23
- package/src/color.ts +6 -0
- package/src/dirs.ts +93 -24
- package/src/docx/converter.ts +26 -12
- package/src/dom/core.ts +354 -111
- package/src/dom/parser.ts +42 -19
- package/src/dom/selector.ts +328 -95
- package/src/fetch-retry.ts +2 -0
- package/src/format.ts +5 -1
- package/src/json-lexer.ts +54 -0
- package/src/json-parse.ts +21 -6
- package/src/logger/rotating-file.ts +5 -3
- package/src/logger.ts +39 -23
- package/src/loop-phase.ts +66 -21
- package/src/marked/core.ts +45 -19
- package/src/marked-list.ts +73 -0
- package/src/math-delimiters.ts +125 -5
- package/src/module-timer.ts +5 -5
- package/src/postmortem.ts +1 -1
- package/src/process-name.ts +2 -2
- package/src/procmgr.ts +58 -4
- package/src/prompt.ts +3 -1
- package/src/ptree.ts +28 -35
- package/src/readability/readability.ts +349 -39
- package/src/readability/types.ts +2 -0
- package/src/runtime-install.ts +2 -2
- package/src/sqlite.ts +58 -1
- package/src/stream.ts +34 -13
- package/src/temp.ts +13 -0
- package/src/template.ts +7 -4
- package/src/timing-buffer.ts +1 -1
- package/src/tls-fetch.ts +1 -1
- package/src/turndown/gfm.ts +16 -5
- package/src/turndown/html.ts +28 -8
- package/src/turndown/service.ts +43 -15
- package/src/turndown/types.ts +7 -0
- package/src/vterm/buffer.ts +31 -11
- package/src/vterm/query-responder.ts +31 -2
- package/src/vterm/terminal.ts +213 -100
package/src/math-delimiters.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
/** Opening delimiter. Each closer (`$`, `$$`, `\)`, `\]`) is as wide as its opener. */
|
|
9
9
|
export type MathOpener = "$" | "$$" | "\\(" | "\\[";
|
|
10
10
|
|
|
11
|
+
/** Opener of an own-line display block. */
|
|
12
|
+
export type MathBlockOpener = "$$" | "\\[";
|
|
13
|
+
|
|
11
14
|
/** A closed math span found in the source. */
|
|
12
15
|
export interface MathSpan {
|
|
13
16
|
opener: MathOpener;
|
|
@@ -33,8 +36,8 @@ export interface MathBlock {
|
|
|
33
36
|
// it. The own-line requirement leaves inline `$$…$$` inside prose to the span
|
|
34
37
|
// grammar below. `\r?\n` at each line boundary keeps the grammar CRLF-safe for
|
|
35
38
|
// direct callers; marked-fed renderers already normalize line endings first.
|
|
36
|
-
const MATH_BLOCK_DOLLAR =
|
|
37
|
-
const MATH_BLOCK_BRACKET =
|
|
39
|
+
const MATH_BLOCK_DOLLAR = / {0,3}\$\$[ \t]*\r?\n([\s\S]+?)\r?\n {0,3}\$\$[ \t]*(?:\r?\n|$)/y;
|
|
40
|
+
const MATH_BLOCK_BRACKET = / {0,3}\\\[[ \t]*\r?\n([\s\S]+?)\r?\n {0,3}\\\][ \t]*(?:\r?\n|$)/y;
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* Leftmost offset at or after `from` where an opener could begin. A scan hint,
|
|
@@ -182,9 +185,126 @@ function spanOf(source: string, opener: MathOpener, bodyStart: number, closeAt:
|
|
|
182
185
|
|
|
183
186
|
/** The own-line display block starting at offset 0, or `undefined`. */
|
|
184
187
|
export function mathBlockAt(source: string): MathBlock | undefined {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return
|
|
188
|
+
MATH_BLOCK_DOLLAR.lastIndex = 0;
|
|
189
|
+
MATH_BLOCK_BRACKET.lastIndex = 0;
|
|
190
|
+
return blockOf(MATH_BLOCK_DOLLAR.exec(source) ?? MATH_BLOCK_BRACKET.exec(source));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The block a match of the block grammar describes; a whitespace-only body is no block. */
|
|
194
|
+
function blockOf(match: RegExpExecArray | null): MathBlock | undefined {
|
|
195
|
+
return match === null || match[1].trim() === "" ? undefined : { raw: match[0], body: match[1] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// An own-line display opener line (`$$` or `\[` after up to 3 spaces), capturing
|
|
199
|
+
// the opener and the line's end: "" when it is the source's unterminated last line.
|
|
200
|
+
const MATH_BLOCK_OPENER_LINE_RE = / {0,3}(\$\$|\\\[)[ \t]*(\r?\n|$)/y;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* {@link mathBlockAt} for many offsets of one `source`. An own-line opener
|
|
204
|
+
* whose closer line is missing means every later opener of the same kind
|
|
205
|
+
* misses one too, since its search covers a suffix of that search. The scan
|
|
206
|
+
* keeps that answer, so asking at every block start takes linear time instead
|
|
207
|
+
* of a scan to the end of `source` per unclosed opener.
|
|
208
|
+
*/
|
|
209
|
+
export class MathBlockScan {
|
|
210
|
+
readonly #source: string;
|
|
211
|
+
// Offset of the first `$$` / `\[` opener line found with no closer line after it.
|
|
212
|
+
#unclosedDollarFrom = Number.POSITIVE_INFINITY;
|
|
213
|
+
#unclosedBracketFrom = Number.POSITIVE_INFINITY;
|
|
214
|
+
|
|
215
|
+
constructor(source: string) {
|
|
216
|
+
this.#source = source;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The own-line display block starting at `from`, or `undefined`. */
|
|
220
|
+
at(from: number): MathBlock | undefined {
|
|
221
|
+
MATH_BLOCK_OPENER_LINE_RE.lastIndex = from;
|
|
222
|
+
const line = MATH_BLOCK_OPENER_LINE_RE.exec(this.#source);
|
|
223
|
+
// An unterminated opener line opens no block.
|
|
224
|
+
if (line === null || line[2] === "") return undefined;
|
|
225
|
+
const dollar = line[1] === "$$";
|
|
226
|
+
if (from >= (dollar ? this.#unclosedDollarFrom : this.#unclosedBracketFrom)) return undefined;
|
|
227
|
+
const grammar = dollar ? MATH_BLOCK_DOLLAR : MATH_BLOCK_BRACKET;
|
|
228
|
+
grammar.lastIndex = from;
|
|
229
|
+
const match = grammar.exec(this.#source);
|
|
230
|
+
if (match === null) {
|
|
231
|
+
if (dollar) this.#unclosedDollarFrom = from;
|
|
232
|
+
else this.#unclosedBracketFrom = from;
|
|
233
|
+
}
|
|
234
|
+
return blockOf(match);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const blockScansByContext = new WeakMap<object, MathBlockScan>();
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The own-line display block at the start of `src`, for a marked block tokenizer: `src` is the part of
|
|
242
|
+
* `context.source` that starts at `context.end - src.length`. One {@link MathBlockScan} is kept per context, which
|
|
243
|
+
* marked keeps for one `blockTokens` source, so asking at every block start takes linear time even when an opener
|
|
244
|
+
* never closes. Without `context.source` it is {@link mathBlockAt}.
|
|
245
|
+
*/
|
|
246
|
+
export function mathBlockInContext(context: { source?: string; end?: number }, src: string): MathBlock | undefined {
|
|
247
|
+
const source = context.source;
|
|
248
|
+
if (source === undefined) return mathBlockAt(src);
|
|
249
|
+
let scan = blockScansByContext.get(context);
|
|
250
|
+
if (!scan) blockScansByContext.set(context, (scan = new MathBlockScan(source)));
|
|
251
|
+
return scan.at((context.end ?? source.length) - src.length);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The display opener alone on the line at `from` (up to 3 leading spaces,
|
|
256
|
+
* trailing spaces or tabs; the source's last line may be unterminated), or
|
|
257
|
+
* `undefined`.
|
|
258
|
+
*/
|
|
259
|
+
export function mathBlockOpenerAt(source: string, from = 0): MathBlockOpener | undefined {
|
|
260
|
+
MATH_BLOCK_OPENER_LINE_RE.lastIndex = from;
|
|
261
|
+
const line = MATH_BLOCK_OPENER_LINE_RE.exec(source);
|
|
262
|
+
return line === null ? undefined : line[1] === "$$" ? "$$" : "\\[";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Whether the own-line display block opened at `from` in `source` is closed,
|
|
267
|
+
* or could still close once more text is appended: the text from `from`
|
|
268
|
+
* onward is a streaming prefix whose real closer may not have arrived. A
|
|
269
|
+
* closer on the unterminated last line has not arrived either, since the next
|
|
270
|
+
* append may extend it into text (`$$ E = mc^2 $$`). With no opener line at
|
|
271
|
+
* `from` it returns false before scanning, so a caller can ask at every block
|
|
272
|
+
* start.
|
|
273
|
+
*/
|
|
274
|
+
export function mathBlockMayCloseAt(source: string, from = 0): boolean {
|
|
275
|
+
MATH_BLOCK_OPENER_LINE_RE.lastIndex = from;
|
|
276
|
+
const line = MATH_BLOCK_OPENER_LINE_RE.exec(source);
|
|
277
|
+
if (line === null) return false;
|
|
278
|
+
// The opener's own line is still being written.
|
|
279
|
+
if (line[2] === "") return true;
|
|
280
|
+
const grammar = line[1] === "$$" ? MATH_BLOCK_DOLLAR : MATH_BLOCK_BRACKET;
|
|
281
|
+
grammar.lastIndex = from;
|
|
282
|
+
const match = grammar.exec(source);
|
|
283
|
+
// No closer line yet, or only on the last line, which is still being written.
|
|
284
|
+
if (match === null || (from + match[0].length === source.length && !match[0].endsWith("\n"))) return true;
|
|
285
|
+
return blockOf(match) !== undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// A display closer alone on its line, captured; the last line counts unterminated.
|
|
289
|
+
const MATH_BLOCK_CLOSER_LINE_RE = /(?<=^|\n) {0,3}(\$\$|\\\])[ \t]*(?=\r?\n|$)/g;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Offset of the first line of `source`, from the line start `from` on, that
|
|
293
|
+
* holds the closer of `opener` alone (up to 3 leading spaces, trailing spaces
|
|
294
|
+
* or tabs), the unterminated last line included, or `undefined`: the first
|
|
295
|
+
* line at which a block `opener` opened before `from` could close.
|
|
296
|
+
*/
|
|
297
|
+
export function mathBlockCloserIndex(source: string, from: number, opener: MathBlockOpener): number | undefined {
|
|
298
|
+
const closer = opener === "$$" ? "$$" : "\\]";
|
|
299
|
+
MATH_BLOCK_CLOSER_LINE_RE.lastIndex = from;
|
|
300
|
+
for (
|
|
301
|
+
let line = MATH_BLOCK_CLOSER_LINE_RE.exec(source);
|
|
302
|
+
line !== null;
|
|
303
|
+
line = MATH_BLOCK_CLOSER_LINE_RE.exec(source)
|
|
304
|
+
) {
|
|
305
|
+
if (line[1] === closer) return line.index;
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
188
308
|
}
|
|
189
309
|
|
|
190
310
|
/**
|
package/src/module-timer.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* **Coverage limits:**
|
|
38
38
|
* - TS/TSX only — intercepting `node_modules` CJS `.js`/`.cjs` and forcing ESM
|
|
39
39
|
* breaks their default-export detection, so they are left to Bun's default path.
|
|
40
|
-
* - **Dev runs only.** In the compiled `
|
|
40
|
+
* - **Dev runs only.** In the compiled `zetacode` binary every module is pre-bundled
|
|
41
41
|
* into bunfs, so `onLoad` never fires; profile with a `bun --preload` dev run.
|
|
42
42
|
*/
|
|
43
43
|
import { readFileSync } from "node:fs";
|
|
@@ -51,19 +51,19 @@ import { moduleLoadBuffer } from "./timing-buffer";
|
|
|
51
51
|
// timing lives) is uniformly TypeScript, so a TS-only filter is both safe and
|
|
52
52
|
// sufficient. Declaration files are imported as text assets, never evaluated.
|
|
53
53
|
const MODULE_LOADER_FILTER = /^(?!.*\.d\.[mc]?ts$).*\.[mc]?tsx?$/;
|
|
54
|
-
const MODULE_COMPLETE_KEY: symbol = Symbol.for("
|
|
55
|
-
const MODULE_BODY_START_KEY: symbol = Symbol.for("
|
|
54
|
+
const MODULE_COMPLETE_KEY: symbol = Symbol.for("zeta.moduleLoadComplete");
|
|
55
|
+
const MODULE_BODY_START_KEY: symbol = Symbol.for("zeta.moduleBodyStart");
|
|
56
56
|
const STATIC_IMPORT_PATTERN =
|
|
57
57
|
/\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
58
58
|
|
|
59
59
|
type CompleteStore = Record<symbol, ((path: string) => void) | undefined>;
|
|
60
60
|
|
|
61
61
|
function bodyStartMarker(path: string): string {
|
|
62
|
-
return `;globalThis[Symbol.for("
|
|
62
|
+
return `;globalThis[Symbol.for("zeta.moduleBodyStart")]?.(${JSON.stringify(path)});\n`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function completionMarker(path: string): string {
|
|
66
|
-
return `\n;globalThis[Symbol.for("
|
|
66
|
+
return `\n;globalThis[Symbol.for("zeta.moduleLoadComplete")]?.(${JSON.stringify(path)});\n`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function instrumentContents(path: string, contents: string): string {
|
package/src/postmortem.ts
CHANGED
|
@@ -332,7 +332,7 @@ function faultWorkerIpcChannels(err: Error): void {
|
|
|
332
332
|
/**
|
|
333
333
|
* Graceful shutdown driven by `process.stdout`'s own `error` event.
|
|
334
334
|
*
|
|
335
|
-
* A closed stdout consumer (`
|
|
335
|
+
* A closed stdout consumer (`zetacode --help | head`, an ACP client dropping the
|
|
336
336
|
* pipe) delivers the broken-pipe write here — attributable to stdout by
|
|
337
337
|
* construction, unlike a process-wide `syscall: "write"` match that a closed
|
|
338
338
|
* subprocess stdin or socket would also satisfy — so it runs cleanup and exits
|
package/src/process-name.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Set the OS-visible process name (`/proc/self/comm`) so `
|
|
3
|
-
* `
|
|
2
|
+
* Set the OS-visible process name (`/proc/self/comm`) so `zetacode` shows up as
|
|
3
|
+
* `zetacode` — not `bun` — in `ps`, `pgrep`, `killall`, `top`, `htop`, and systemd.
|
|
4
4
|
*
|
|
5
5
|
* Bun's `process.title` setter only stores the value on the JS side; unlike
|
|
6
6
|
* Node/libuv it never calls `prctl(PR_SET_NAME)`, so the kernel's `comm` stays
|
package/src/procmgr.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { Process, ProcessStatus } from "@linxiraos/pi-natives";
|
|
4
4
|
import type { Subprocess } from "bun";
|
|
5
|
-
import { getAgentDir, MAIN_CONFIG_FILENAMES } from "./dirs";
|
|
5
|
+
import { getAgentDir, getProjectDir, MAIN_CONFIG_FILENAMES } from "./dirs";
|
|
6
6
|
import { $env, filterChildShellEnv } from "./env";
|
|
7
7
|
import { isExecutable } from "./executable";
|
|
8
8
|
import { $which } from "./which";
|
|
@@ -20,18 +20,31 @@ export interface ShellConfigOptions {
|
|
|
20
20
|
/** File path or runtime layer that supplied the active shell setting. */
|
|
21
21
|
configSource?: string;
|
|
22
22
|
}
|
|
23
|
-
/** Auto-resolved shell (no custom path);
|
|
23
|
+
/** Auto-resolved shell (no custom path); kept until {@link refreshShellConfigCache}. */
|
|
24
24
|
let cachedDefaultShellConfig: ShellConfig | null = null;
|
|
25
25
|
/** Config for the most recent custom `shellPath`; never served once the path is cleared. */
|
|
26
26
|
let cachedCustomShellConfig: ShellConfig | null = null;
|
|
27
|
+
/**
|
|
28
|
+
* The child-shell environment {@link refreshShellConfigCache} last captured. Unset
|
|
29
|
+
* until the first refresh, so spawn environments are built from the live environment.
|
|
30
|
+
*/
|
|
31
|
+
let capturedChildShellEnv: Record<string, string> | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Project whose dotenv values the spawn environment filters out: the one current
|
|
34
|
+
* when it was first built or captured.
|
|
35
|
+
* That is normally the launch project, whose dotenv files Bun and omp load into
|
|
36
|
+
* `process.env`; a later session in another project must not receive them.
|
|
37
|
+
*/
|
|
38
|
+
let spawnEnvProjectDir: string | undefined;
|
|
27
39
|
|
|
28
40
|
/**
|
|
29
41
|
* Build the spawn environment (cached).
|
|
30
42
|
*/
|
|
31
43
|
function buildSpawnEnv(shell: string): Record<string, string> {
|
|
32
44
|
const noCI = $env.PI_BASH_NO_CI || $env.CLAUDE_BASH_NO_CI;
|
|
45
|
+
spawnEnvProjectDir ??= getProjectDir();
|
|
33
46
|
return {
|
|
34
|
-
...filterChildShellEnv(Bun.env),
|
|
47
|
+
...(capturedChildShellEnv ?? filterChildShellEnv(Bun.env, spawnEnvProjectDir)),
|
|
35
48
|
SHELL: shell,
|
|
36
49
|
GIT_EDITOR: "true",
|
|
37
50
|
GPG_TTY: "not a tty",
|
|
@@ -145,6 +158,17 @@ export function resolveBasicShell(): string | undefined {
|
|
|
145
158
|
*
|
|
146
159
|
* Exported for tests; `env` overrides Bun.env-based discovery.
|
|
147
160
|
*/
|
|
161
|
+
|
|
162
|
+
/** True for the WSL launcher bash.exe variants, which are not Windows shells. */
|
|
163
|
+
function isWslLauncher(bashPath: string): boolean {
|
|
164
|
+
const normalized = bashPath.toLowerCase().replaceAll("/", "\\");
|
|
165
|
+
return (
|
|
166
|
+
normalized.endsWith("\\system32\\bash.exe") ||
|
|
167
|
+
normalized.endsWith("\\windowsapps\\bash.exe") ||
|
|
168
|
+
normalized.endsWith("\\system32\\wsl.exe")
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
148
172
|
export function resolveWindowsShell(env: Record<string, string | undefined> = Bun.env): string {
|
|
149
173
|
const gitRoots = [
|
|
150
174
|
env.ProgramFiles && path.join(env.ProgramFiles, "Git"),
|
|
@@ -160,8 +184,12 @@ export function resolveWindowsShell(env: Record<string, string | undefined> = Bu
|
|
|
160
184
|
if (fs.existsSync(candidate)) return candidate;
|
|
161
185
|
}
|
|
162
186
|
|
|
187
|
+
// The WSL launcher (`System32\bash.exe` / `WindowsApps\bash.exe`) is not a
|
|
188
|
+
// Windows shell: it runs inside Linux without the spawn environment, so
|
|
189
|
+
// scripted `-c` snapshots break. Skip it and let the sh.exe sibling (Git
|
|
190
|
+
// Bash) or cmd.exe win instead.
|
|
163
191
|
const bashOnPath = $which("bash.exe");
|
|
164
|
-
if (bashOnPath) return bashOnPath;
|
|
192
|
+
if (bashOnPath && !isWslLauncher(bashOnPath)) return bashOnPath;
|
|
165
193
|
|
|
166
194
|
const shOnPath = $which("sh.exe");
|
|
167
195
|
if (shOnPath) {
|
|
@@ -202,6 +230,32 @@ export function getShellConfig(customShellPath?: string, options: ShellConfigOpt
|
|
|
202
230
|
return cachedDefaultShellConfig;
|
|
203
231
|
}
|
|
204
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Capture the current process environment for child shells and drop both cached
|
|
235
|
+
* shell configs, so the next {@link getShellConfig} builds the spawn environment
|
|
236
|
+
* from that capture. Call right after code that changed `process.env` in a way
|
|
237
|
+
* child shells must see, such as extension session lifecycle handlers exporting
|
|
238
|
+
* session-scoped variables; later writes reach child shells only at the next call.
|
|
239
|
+
*/
|
|
240
|
+
export function refreshShellConfigCache(): void {
|
|
241
|
+
spawnEnvProjectDir ??= getProjectDir();
|
|
242
|
+
capturedChildShellEnv = filterChildShellEnv(Bun.env, spawnEnvProjectDir);
|
|
243
|
+
cachedDefaultShellConfig = null;
|
|
244
|
+
cachedCustomShellConfig = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Test-only: forget the capture and both cached shell configs, so the next
|
|
249
|
+
* {@link getShellConfig} builds from the live environment again. Production code
|
|
250
|
+
* MUST NOT call this; the capture's lifecycle is owned by {@link refreshShellConfigCache}.
|
|
251
|
+
*/
|
|
252
|
+
export function __resetShellConfigCacheForTests(): void {
|
|
253
|
+
capturedChildShellEnv = undefined;
|
|
254
|
+
spawnEnvProjectDir = undefined;
|
|
255
|
+
cachedDefaultShellConfig = null;
|
|
256
|
+
cachedCustomShellConfig = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
205
259
|
/** Platform shell discovery used when no custom `shellPath` is configured. */
|
|
206
260
|
function resolveDefaultShell(): string {
|
|
207
261
|
if (process.platform === "win32") {
|
package/src/prompt.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LRUCache } from "./lru";
|
|
1
2
|
import type { HelperDelegate, Template } from "./template";
|
|
2
3
|
import * as Handlebars from "./template";
|
|
3
4
|
|
|
@@ -526,7 +527,8 @@ export function registerPartial(name: string, fn: Template): void {
|
|
|
526
527
|
handlebars.registerPartial(name, fn);
|
|
527
528
|
}
|
|
528
529
|
|
|
529
|
-
|
|
530
|
+
// Bounded: some callers compile template text that already embeds user arguments.
|
|
531
|
+
const compiledTemplateCache = new LRUCache<string, (context: TemplateContext) => string>({ max: 256 });
|
|
530
532
|
|
|
531
533
|
export function compile(template: string): (context: TemplateContext) => string {
|
|
532
534
|
// Keyed on the raw template so repeat renders skip parsing.
|
package/src/ptree.ts
CHANGED
|
@@ -183,10 +183,12 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
183
183
|
#stderrDone: Promise<void>;
|
|
184
184
|
#exited: Promise<number>;
|
|
185
185
|
#openPipeReaders = 1;
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
#
|
|
186
|
+
// Set by attachTimeout() at the command deadline; untimed commands keep
|
|
187
|
+
// complete EOF-based capture. Active pipe readers are cancelled at cutoff so
|
|
188
|
+
// their pending read() settles as done and partial output is kept.
|
|
189
|
+
#cutoff = false;
|
|
190
|
+
/** Readers of piped stdio; the timeout cancels them so pending reads settle. */
|
|
191
|
+
#pipeReaders = new Set<{ cancel(reason?: unknown): Promise<void> }>();
|
|
190
192
|
#timeoutTimer?: NodeJS.Timeout;
|
|
191
193
|
#stderrStream?: ReadableStream<Uint8Array>;
|
|
192
194
|
// Termination in flight after kill(); aborted exits await it before reporting.
|
|
@@ -224,31 +226,24 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
224
226
|
// Normalize Bun's exited promise into our exitReason / exitedCleanly model.
|
|
225
227
|
const { promise, resolve, reject } = Promise.withResolvers<number>();
|
|
226
228
|
this.#exited = promise;
|
|
227
|
-
const drainCutoff = Promise.withResolvers<void>();
|
|
228
|
-
this.#drainCutoff = drainCutoff.promise;
|
|
229
|
-
this.#resolveDrainCutoff = drainCutoff.resolve;
|
|
230
|
-
// The cutoff remains pending for untimed commands, preserving complete
|
|
231
|
-
// EOF-based capture. attachTimeout() resolves it at the command deadline.
|
|
232
229
|
|
|
233
|
-
const pipeCutoff = this.#drainCutoff;
|
|
234
230
|
this.#stderrDone = (async () => {
|
|
235
231
|
const reader = stderrStream.getReader();
|
|
232
|
+
this.#pipeReaders.add(reader);
|
|
236
233
|
try {
|
|
237
234
|
for (;;) {
|
|
238
|
-
|
|
239
|
-
reader.read().then(r => ({ cutoff: false as const, r })),
|
|
240
|
-
pipeCutoff.then(() => ({ cutoff: true as const })),
|
|
241
|
-
]);
|
|
242
|
-
if (chunk.cutoff) {
|
|
235
|
+
if (this.#cutoff) {
|
|
243
236
|
await reader.cancel().catch(() => {});
|
|
244
237
|
break;
|
|
245
238
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
this.#
|
|
239
|
+
const r = await reader.read();
|
|
240
|
+
if (r.done) break;
|
|
241
|
+
this.#stderrChunks?.push(r.value);
|
|
242
|
+
this.#stderrTail += dec.decode(r.value, { stream: true });
|
|
249
243
|
trim();
|
|
250
244
|
}
|
|
251
245
|
} catch {}
|
|
246
|
+
this.#pipeReaders.delete(reader);
|
|
252
247
|
this.#openPipeReaders--;
|
|
253
248
|
this.#stderrTail += dec.decode();
|
|
254
249
|
trim();
|
|
@@ -440,24 +435,23 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
440
435
|
async #readStream(stream: ReadableStream<Uint8Array>): Promise<string> {
|
|
441
436
|
this.#openPipeReaders++;
|
|
442
437
|
const reader = stream.getReader();
|
|
438
|
+
this.#pipeReaders.add(reader);
|
|
443
439
|
const dec = new TextDecoder();
|
|
444
440
|
let out = "";
|
|
445
441
|
try {
|
|
446
442
|
for (;;) {
|
|
447
|
-
|
|
448
|
-
reader.read().then(r => ({ cutoff: false as const, r })),
|
|
449
|
-
this.#drainCutoff.then(() => ({ cutoff: true as const })),
|
|
450
|
-
]);
|
|
451
|
-
if (chunk.cutoff) {
|
|
443
|
+
if (this.#cutoff) {
|
|
452
444
|
await reader.cancel().catch(() => {});
|
|
453
445
|
break;
|
|
454
446
|
}
|
|
455
|
-
|
|
456
|
-
|
|
447
|
+
const r = await reader.read();
|
|
448
|
+
if (r.done) break;
|
|
449
|
+
out += dec.decode(r.value, { stream: true });
|
|
457
450
|
}
|
|
458
451
|
} catch {
|
|
459
452
|
// A cancelled or failed read keeps whatever was already collected.
|
|
460
453
|
}
|
|
454
|
+
this.#pipeReaders.delete(reader);
|
|
461
455
|
this.#openPipeReaders--;
|
|
462
456
|
return out + dec.decode();
|
|
463
457
|
}
|
|
@@ -465,29 +459,27 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
465
459
|
async #readBytes(): Promise<Uint8Array<ArrayBuffer>> {
|
|
466
460
|
const reader = this.proc.stdout.getReader();
|
|
467
461
|
this.#openPipeReaders++;
|
|
462
|
+
this.#pipeReaders.add(reader);
|
|
468
463
|
const chunks: Uint8Array[] = [];
|
|
469
464
|
let length = 0;
|
|
470
465
|
try {
|
|
471
466
|
for (;;) {
|
|
472
|
-
|
|
473
|
-
reader.read().then(r => ({ cutoff: false as const, r })),
|
|
474
|
-
this.#drainCutoff.then(() => ({ cutoff: true as const })),
|
|
475
|
-
]);
|
|
476
|
-
if (chunk.cutoff) {
|
|
467
|
+
if (this.#cutoff) {
|
|
477
468
|
await reader.cancel().catch(() => {});
|
|
478
469
|
break;
|
|
479
470
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
471
|
+
const r = await reader.read();
|
|
472
|
+
if (r.done) break;
|
|
473
|
+
chunks.push(r.value);
|
|
474
|
+
length += r.value.byteLength;
|
|
483
475
|
}
|
|
484
476
|
} catch {
|
|
485
477
|
// A cancelled or failed read keeps whatever was already collected.
|
|
486
478
|
} finally {
|
|
479
|
+
this.#pipeReaders.delete(reader);
|
|
487
480
|
this.#openPipeReaders--;
|
|
488
481
|
reader.releaseLock();
|
|
489
482
|
}
|
|
490
|
-
|
|
491
483
|
const bytes = new Uint8Array(length);
|
|
492
484
|
let offset = 0;
|
|
493
485
|
for (const chunk of chunks) {
|
|
@@ -597,7 +589,8 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
597
589
|
) {
|
|
598
590
|
this.kill(new TimeoutError(ms, this.#stderrTail), -1);
|
|
599
591
|
}
|
|
600
|
-
this.#
|
|
592
|
+
this.#cutoff = true;
|
|
593
|
+
for (const reader of this.#pipeReaders) reader.cancel().catch(() => {});
|
|
601
594
|
}, ms);
|
|
602
595
|
timer.unref?.();
|
|
603
596
|
this.#timeoutTimer = timer;
|