@linxiraos/pi-utils 1.4.4 → 1.4.7
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 +18 -0
- package/README.md +1 -1
- package/dist/types/abortable.d.ts +2 -1
- package/dist/types/acp/protocol.d.ts +1 -0
- package/dist/types/ar/tar.d.ts +1 -1
- package/dist/types/ar/types.d.ts +1 -1
- package/dist/types/format.d.ts +2 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/json.d.ts +7 -0
- package/dist/types/logger.d.ts +14 -0
- package/dist/types/marked/inline-closers.d.ts +18 -5
- package/dist/types/mime.d.ts +7 -0
- package/dist/types/ports.d.ts +4 -2
- package/dist/types/postmortem.d.ts +19 -2
- package/dist/types/process-name.d.ts +2 -1
- package/dist/types/which.d.ts +13 -0
- package/package.json +4 -4
- package/src/abortable.ts +6 -2
- package/src/acp/protocol.ts +1 -0
- package/src/ar/tar.ts +66 -10
- package/src/ar/types.ts +1 -1
- package/src/dirs.ts +15 -1
- package/src/format.ts +6 -2
- package/src/index.ts +0 -1
- package/src/json.ts +25 -0
- package/src/logger/rotating-file.ts +1 -2
- package/src/logger.ts +47 -6
- package/src/marked/core.ts +22 -17
- package/src/marked/inline-closers.ts +37 -42
- package/src/mime.ts +35 -0
- package/src/ports.ts +4 -2
- package/src/postmortem.ts +174 -25
- package/src/process-name.ts +10 -5
- package/src/stream.ts +16 -9
- package/src/which.ts +27 -0
- package/dist/types/fs-open.d.ts +0 -17
- package/src/fs-open.ts +0 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.4.7] - 2026-10-30
|
|
4
|
+
|
|
5
|
+
- Synced with upstream OMP v18.14.0; no package-specific user-visible changes.
|
|
6
|
+
|
|
7
|
+
## [1.4.6] - 2026-10-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `logger.withContext(fields, fn)` to attach contextual fields to all records produced during a function and its asynchronous work, plus `logger.withoutContext(fn)` for work that should not inherit caller context.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed `untilAborted` so promises passed to an already-aborted signal are still observed, preventing later rejections from being reported as unhandled.
|
|
16
|
+
|
|
17
|
+
## [1.4.5] - 2026-10-28
|
|
18
|
+
|
|
19
|
+
- Synced with upstream OMP v18.13.2 (shared utilities and runtime fixes); user-visible changes are in UPDATE-LOG.md under 1.4.5.
|
|
20
|
+
|
|
3
21
|
## [1.4.4] - 2026-10-26
|
|
4
22
|
|
|
5
23
|
- Released as part of Zeta 1.4.4; no package-specific user-visible changes.
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Import from the root barrel or per-module subpaths (`@linxiraos/pi-utils/<module
|
|
|
29
29
|
bun add @linxiraos/pi-utils
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Ships TypeScript source directly (no build step); requires Bun ≥ 1.
|
|
32
|
+
Ships TypeScript source directly (no build step); requires Bun ≥ 1.4.4.
|
|
33
33
|
|
|
34
34
|
## References
|
|
35
35
|
|
|
@@ -16,7 +16,8 @@ export declare class AbortError extends Error {
|
|
|
16
16
|
export declare function abortableSource<T>(stream: ReadableStream<T>, signal?: AbortSignal): AsyncGenerator<T>;
|
|
17
17
|
/**
|
|
18
18
|
* Runs a promise-returning function (`pr`). If the given AbortSignal is aborted before or during
|
|
19
|
-
* execution, the promise is rejected with a standard error.
|
|
19
|
+
* execution, the promise is rejected with a standard error. A promise passed directly stays
|
|
20
|
+
* observed, so its later rejection is never unhandled.
|
|
20
21
|
*
|
|
21
22
|
* @param signal - Optional AbortSignal to cancel the operation
|
|
22
23
|
* @param pr - Function returning a promise to run
|
|
@@ -93,6 +93,7 @@ export interface ToolCallLocation extends Meta {
|
|
|
93
93
|
export interface ToolCall extends Meta {
|
|
94
94
|
toolCallId: ToolCallId;
|
|
95
95
|
title: string;
|
|
96
|
+
name?: string | null;
|
|
96
97
|
kind?: ToolKind | null;
|
|
97
98
|
status?: ToolCallStatus | null;
|
|
98
99
|
content?: ToolCallContent[] | null;
|
package/dist/types/ar/tar.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function readTarEntriesFromBuffer(buffer: Uint8Array, options: Fo
|
|
|
6
6
|
* pass over `open()` walks the headers, and member reads open it again.
|
|
7
7
|
*/
|
|
8
8
|
export declare function readTarEntriesFromStream(open: () => AsyncIterable<Uint8Array>, options: FormatReadOptions): Promise<ArchiveIndexEntry[]>;
|
|
9
|
-
/**
|
|
9
|
+
/** Index a plain tar by ranging over headers and retaining lazy member offsets. */
|
|
10
10
|
export declare const readTar: FormatReader;
|
|
11
11
|
/** Detect a tar header, including legacy pre-ustar archives, by its checksum. */
|
|
12
12
|
export declare function sniffTar(bytes: Uint8Array): boolean;
|
package/dist/types/ar/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type WritableArchiveFormat = "zip" | "tar" | "tar.gz" | "tar.zst" | "asar
|
|
|
8
8
|
* Where to read an archive from: an extension-inferred filesystem path, a
|
|
9
9
|
* format-tagged filesystem path, in-memory bytes, or any caller-provided
|
|
10
10
|
* {@link ByteSource} (e.g. `httpByteSource` for ranged remote reads).
|
|
11
|
-
* File- and source-backed ZIP/ASAR/RAR/7z/ISO are read lazily.
|
|
11
|
+
* File- and source-backed TAR/ZIP/ASAR/RAR/7z/ISO are read lazily.
|
|
12
12
|
*/
|
|
13
13
|
export type ArchiveSource = string | {
|
|
14
14
|
bytes: Uint8Array;
|
package/dist/types/format.d.ts
CHANGED
|
@@ -6,9 +6,10 @@ export declare function formatDuration(ms: number): string;
|
|
|
6
6
|
/**
|
|
7
7
|
* Format a number with K/M/B suffix for compact display.
|
|
8
8
|
* Uses 1 decimal for small leading digits when non-zero, rounded otherwise.
|
|
9
|
+
* Values below 1,000 print as-is unless `maxFractionDigits` caps their decimals.
|
|
9
10
|
* Examples: "999", "1K", "1.5K", "25K", "1M", "1.5M", "25M", "1.5B"
|
|
10
11
|
*/
|
|
11
|
-
export declare function formatNumber(n: number): string;
|
|
12
|
+
export declare function formatNumber(n: number, maxFractionDigits?: number): string;
|
|
12
13
|
/**
|
|
13
14
|
* Format a byte count to a human-readable string.
|
|
14
15
|
* Examples: "512B", "1.5KB", "2.3MB", "1.2GB"
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export * from "./file-lock.js";
|
|
|
10
10
|
export * from "./format.js";
|
|
11
11
|
export * from "./frontmatter.js";
|
|
12
12
|
export * from "./fs-error.js";
|
|
13
|
-
export * from "./fs-open.js";
|
|
14
13
|
export * from "./incoming-json.js";
|
|
15
14
|
export * from "./json.js";
|
|
16
15
|
export * from "./json-parse.js";
|
package/dist/types/json.d.ts
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
* Try to parse JSON, returning null on failure.
|
|
3
3
|
*/
|
|
4
4
|
export declare function tryParseJson<T = unknown>(content: string): T | null;
|
|
5
|
+
/**
|
|
6
|
+
* Index just past the JSON object or array that opens at `text[start]`, or -1
|
|
7
|
+
* when it does not close within `text`. String literals are skipped, honoring
|
|
8
|
+
* escapes, so brackets inside values do not count. Malformed input yields some
|
|
9
|
+
* end that a parser then rejects.
|
|
10
|
+
*/
|
|
11
|
+
export declare function jsonContainerEnd(text: string, start: number): number;
|
|
5
12
|
/**
|
|
6
13
|
* Serialize JSON while preserving bigint precision as decimal strings.
|
|
7
14
|
*
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ export interface LogEvent {
|
|
|
11
11
|
export type LogSink = (event: LogEvent) => void;
|
|
12
12
|
/** Register an out-of-band log sink and return a disposer. */
|
|
13
13
|
export declare function registerLogSink(sink: LogSink): () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Run `fn` with `context`'s fields on every record logged while it runs,
|
|
16
|
+
* including from the promises, timers and callbacks it starts. A nested call
|
|
17
|
+
* replaces the enclosing context, as a subagent's session replaces its
|
|
18
|
+
* parent's; a field passed to the log call itself wins. Fields are read as each
|
|
19
|
+
* record is written, so a getter stays current.
|
|
20
|
+
*/
|
|
21
|
+
export declare function withContext<T>(context: Readonly<Record<string, unknown>>, fn: () => T): T;
|
|
22
|
+
/**
|
|
23
|
+
* Run `fn` outside any {@link withContext}: for process-wide work started on
|
|
24
|
+
* behalf of one caller, such as a shared server's read loop, whose later
|
|
25
|
+
* records belong to no single caller.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withoutContext<T>(fn: () => T): T;
|
|
14
28
|
/**
|
|
15
29
|
* Write buffered file records to disk now. The file transport batches records
|
|
16
30
|
* (one write per second or per 64 KiB) and writes `warn`/`error` records
|
|
@@ -45,15 +45,23 @@ declare class EmphasisDelimiters {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* next run of exactly `width` backticks: a run of any other length is no closer, and the one that closes is the first
|
|
49
|
+
* run of that width at or after the opener. Backslash escapes do not apply inside a code span, so a backslash just
|
|
50
|
+
* before that closing backtick is content and still closes the span.
|
|
51
51
|
*/
|
|
52
52
|
declare class CodeRuns {
|
|
53
53
|
#private;
|
|
54
54
|
constructor(src: string);
|
|
55
55
|
/** Where the run of backticks holding the backtick at `at` ends. */
|
|
56
56
|
endOf(at: number): number;
|
|
57
|
+
/**
|
|
58
|
+
* Whether a backtick string starts at `at` for a scan that walked to `at`: a string is a whole run, so it starts at
|
|
59
|
+
* the run's first backtick and nowhere else. A retry deeper inside a run is not a string, which keeps a run whose
|
|
60
|
+
* opener found no closer from opening a narrower span one backtick later. A position a tokenizer's raw resumed at,
|
|
61
|
+
* or the suffix an escape token left behind, is not a walked position — `codeOpenerWidth` admits those on its own,
|
|
62
|
+
* because it is told the scan resumed there rather than inferring it from the source text.
|
|
63
|
+
*/
|
|
64
|
+
startsStringAt(at: number): boolean;
|
|
57
65
|
/** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
|
|
58
66
|
closerAfter(from: number, width: number): number;
|
|
59
67
|
}
|
|
@@ -102,8 +110,13 @@ export declare class InlineClosers {
|
|
|
102
110
|
escapeWithin(rest: string, from: number, to: number): boolean;
|
|
103
111
|
/** Where the emphasis opened by the `width` markers at the start of `rest` closes, or -1. */
|
|
104
112
|
closeEmphasis(rest: string, marker: string, width: number): number;
|
|
105
|
-
/**
|
|
106
|
-
|
|
113
|
+
/**
|
|
114
|
+
* How many backticks start `rest`, which starts with one, or 0 where no code span opens because no backtick string
|
|
115
|
+
* starts there: the opener is a whole string, so the rest of a run whose opener found no closer is text, while the
|
|
116
|
+
* backticks left after a backslash escaped the run's first one do open a span. `resumed` says the scan resumed at
|
|
117
|
+
* `rest` after a tokenizer consumed a raw that ended here, where the backticks are a string wherever they sit.
|
|
118
|
+
*/
|
|
119
|
+
codeOpenerWidth(rest: string, resumed?: boolean): number;
|
|
107
120
|
/** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
|
|
108
121
|
closeCode(rest: string, width: number): number;
|
|
109
122
|
/**
|
package/dist/types/mime.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export type ImageMetadata = {
|
|
|
26
26
|
hasAlpha?: boolean;
|
|
27
27
|
};
|
|
28
28
|
export declare function parseImageMetadata(header: Uint8Array): ImageMetadata | null;
|
|
29
|
+
/** Still images in a HEIF (ISO BMFF) container, which the image pipeline cannot decode itself. */
|
|
30
|
+
export type HeifImageMimeType = "image/heic" | "image/heif" | "image/avif";
|
|
31
|
+
/**
|
|
32
|
+
* MIME type of a HEIC, AVIF or other HEIF image from the brands of its leading `ftyp` box:
|
|
33
|
+
* the major brand, then the compatible brands, with the generic `mif1`/`msf1` brands last.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseHeifImageMimeType(header: Uint8Array): HeifImageMimeType | null;
|
|
29
36
|
/** Why base64 image data is unusable; `undefined` when it is canonical and non-empty. */
|
|
30
37
|
export type Base64ImageDataFailure = "invalid base64 image data" | "empty image data";
|
|
31
38
|
/**
|
package/dist/types/ports.d.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* OMP installs on the same machine use 30141/30142/3847, and common developer
|
|
5
5
|
* tools squat on 3000/8080/8787/3847. Zeta therefore owns a dedicated block:
|
|
6
|
-
*
|
|
7
|
-
* unrelated services cannot take the gateway
|
|
6
|
+
* uncommon ports, at least two apart, so a side-by-side OMP install and
|
|
7
|
+
* unrelated services cannot take the gateway, stats dashboard, or browser
|
|
8
|
+
* relay away.
|
|
8
9
|
*
|
|
9
10
|
* Every default in the repository derives from these constants. The standalone
|
|
10
11
|
* Electron shell (`desktop/src/main.ts`, not a Bun workspace package) carries
|
|
@@ -13,3 +14,4 @@
|
|
|
13
14
|
export declare const ZETA_WEB_PORT = 43141;
|
|
14
15
|
export declare const ZETA_GATEWAY_PORT = 43143;
|
|
15
16
|
export declare const ZETA_STATS_PORT = 43147;
|
|
17
|
+
export declare const ZETA_BROWSER_RELAY_PORT = 43153;
|
|
@@ -52,7 +52,7 @@ export declare const NATIVE_PROCESS_EXIT: unique symbol;
|
|
|
52
52
|
* signal and fatal exit.
|
|
53
53
|
*/
|
|
54
54
|
export declare function exitProcess(code: number): never;
|
|
55
|
-
/** User-facing command printed
|
|
55
|
+
/** User-facing command printed after fatal or SIGTERM cleanup so interrupted work can be resumed. */
|
|
56
56
|
export interface FatalRecoveryHint {
|
|
57
57
|
/** Stable label identifying the recoverable session or process. */
|
|
58
58
|
label: string;
|
|
@@ -168,7 +168,10 @@ export declare function isExpectedCleanupError(reason: unknown): boolean;
|
|
|
168
168
|
export declare function interceptUnhandledRejections(interceptor: (reason: unknown) => boolean): () => void;
|
|
169
169
|
/**
|
|
170
170
|
* Register a synchronous recovery command to print when the process exits
|
|
171
|
-
* through an uncaught exception
|
|
171
|
+
* through an uncaught exception, an unhandled rejection, or SIGTERM.
|
|
172
|
+
*
|
|
173
|
+
* Providers are read once cleanup finishes (or hits its deadline), so they
|
|
174
|
+
* report what teardown left behind: a session it saved or one it dropped.
|
|
172
175
|
*/
|
|
173
176
|
export declare function registerFatalRecoveryHint(provider: FatalRecoveryHintProvider): () => void;
|
|
174
177
|
/**
|
|
@@ -182,6 +185,20 @@ export interface CleanupRegistrationOptions {
|
|
|
182
185
|
* The registration remains armed when a keep-alive pass skips it.
|
|
183
186
|
*/
|
|
184
187
|
exitOnly?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* How long a pass waits for this callback, when that is longer than the
|
|
190
|
+
* default 10 s cleanup deadline. It extends the pass only while the callback
|
|
191
|
+
* is still pending: one that settles early ends its hold at once.
|
|
192
|
+
*/
|
|
193
|
+
deadlineMs?: number;
|
|
194
|
+
/**
|
|
195
|
+
* With `deadlineMs`: a line printed to stderr, with the remaining wait
|
|
196
|
+
* appended, if exit is still waiting on this callback once every other
|
|
197
|
+
* callback has finished (a terminal owner has then released the screen), or
|
|
198
|
+
* at the default 10 s deadline when another callback is still running.
|
|
199
|
+
* Nothing is printed when the callback finishes first.
|
|
200
|
+
*/
|
|
201
|
+
waitMessage?: string;
|
|
185
202
|
}
|
|
186
203
|
/**
|
|
187
204
|
* Registers a cleanup callback for shutdown, signals, fatal errors, and
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Set
|
|
2
|
+
* Set the JS `process.title` (except on Windows, see above) and — on Linux —
|
|
3
|
+
* the kernel `comm` name.
|
|
3
4
|
*
|
|
4
5
|
* Never throws: `bun:ffi` unavailability or a failed syscall degrades silently
|
|
5
6
|
* to the `process.title`-only behavior, so it is safe to call at startup.
|
package/dist/types/which.d.ts
CHANGED
|
@@ -42,4 +42,17 @@ export declare const whichFresh: typeof darwinWhich;
|
|
|
42
42
|
* @returns Filesystem path if found, else null
|
|
43
43
|
*/
|
|
44
44
|
export declare function $which(command: string, options?: WhichOptions): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Locate every match for `command` on PATH, in PATH order, uncached.
|
|
47
|
+
*
|
|
48
|
+
* Callers that probe candidates (e.g. the Python eval backend) use this so a broken
|
|
49
|
+
* first hit — a version-manager shim with no version selected, a Windows Store alias —
|
|
50
|
+
* does not hide a working binary further down PATH. On macOS the Xcode/CLT toolchain
|
|
51
|
+
* match is appended last, mirroring {@link $which}'s fallback.
|
|
52
|
+
*
|
|
53
|
+
* @param command - Binary name to resolve
|
|
54
|
+
* @param options - `PATH` (defaults to `process.env.PATH`) and `cwd` for relative entries
|
|
55
|
+
* @returns Distinct filesystem paths; empty when nothing matches
|
|
56
|
+
*/
|
|
57
|
+
export declare function $whichAll(command: string, options?: Bun.WhichOptions): string[];
|
|
45
58
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxiraos/pi-utils",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"utilities",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@linxiraos/pi-natives": "1.4.
|
|
57
|
+
"@linxiraos/pi-natives": "1.4.7"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@types/bun": "1.4.
|
|
60
|
+
"@types/bun": "1.4.4"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
|
-
"bun": ">=1.
|
|
63
|
+
"bun": ">=1.4.4"
|
|
64
64
|
}
|
|
65
65
|
}
|
package/src/abortable.ts
CHANGED
|
@@ -60,7 +60,8 @@ export async function* abortableSource<T>(stream: ReadableStream<T>, signal?: Ab
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Runs a promise-returning function (`pr`). If the given AbortSignal is aborted before or during
|
|
63
|
-
* execution, the promise is rejected with a standard error.
|
|
63
|
+
* execution, the promise is rejected with a standard error. A promise passed directly stays
|
|
64
|
+
* observed, so its later rejection is never unhandled.
|
|
64
65
|
*
|
|
65
66
|
* @param signal - Optional AbortSignal to cancel the operation
|
|
66
67
|
* @param pr - Function returning a promise to run
|
|
@@ -71,7 +72,10 @@ export function untilAborted<T>(
|
|
|
71
72
|
pr: Promise<T> | (() => Promise<T>),
|
|
72
73
|
): Promise<T> {
|
|
73
74
|
if (!signal) return typeof pr === "function" ? pr() : pr;
|
|
74
|
-
if (signal.aborted)
|
|
75
|
+
if (signal.aborted) {
|
|
76
|
+
if (typeof pr !== "function") void pr.catch(() => {});
|
|
77
|
+
return Promise.reject(new AbortError(signal));
|
|
78
|
+
}
|
|
75
79
|
|
|
76
80
|
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
77
81
|
const onAbort = () => reject(new AbortError(signal));
|
package/src/acp/protocol.ts
CHANGED
|
@@ -102,6 +102,7 @@ export interface ToolCallLocation extends Meta {
|
|
|
102
102
|
export interface ToolCall extends Meta {
|
|
103
103
|
toolCallId: ToolCallId;
|
|
104
104
|
title: string;
|
|
105
|
+
name?: string | null;
|
|
105
106
|
kind?: ToolKind | null;
|
|
106
107
|
status?: ToolCallStatus | null;
|
|
107
108
|
content?: ToolCallContent[] | null;
|
package/src/ar/tar.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
normalizeArchiveEntryPath,
|
|
17
17
|
normalizeArchiveLookupPath,
|
|
18
18
|
} from "./paths";
|
|
19
|
-
import {
|
|
19
|
+
import type { ByteSource } from "./source";
|
|
20
20
|
import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
|
|
21
21
|
|
|
22
22
|
const BLOCK_SIZE = 512;
|
|
@@ -53,8 +53,44 @@ interface PendingTarLink {
|
|
|
53
53
|
targetPath: string;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
interface TarCursor {
|
|
57
|
+
readonly position: number;
|
|
58
|
+
read(length: number): Promise<Uint8Array>;
|
|
59
|
+
skip(length: number): Promise<boolean>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Seek over plain tar payloads without fetching their bytes. */
|
|
63
|
+
class TarSourceCursor implements TarCursor {
|
|
64
|
+
readonly #source: ByteSource;
|
|
65
|
+
#position = 0;
|
|
66
|
+
|
|
67
|
+
constructor(source: ByteSource) {
|
|
68
|
+
this.#source = source;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get position(): number {
|
|
72
|
+
return this.#position;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async read(length: number): Promise<Uint8Array> {
|
|
76
|
+
const size = Math.min(length, this.#source.size - this.#position);
|
|
77
|
+
if (size === 0) return new Uint8Array(0);
|
|
78
|
+
const end = this.#position + size;
|
|
79
|
+
const bytes = await this.#source.read(this.#position, end);
|
|
80
|
+
if (bytes.byteLength !== size) throw new ArchiveError("Invalid archive: truncated data");
|
|
81
|
+
this.#position = end;
|
|
82
|
+
return bytes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async skip(length: number): Promise<boolean> {
|
|
86
|
+
if (length > this.#source.size - this.#position) return false;
|
|
87
|
+
this.#position += length;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
56
92
|
/** Forward-only reader over tar bytes that arrive in chunks: one buffer, or a decompressing stream. */
|
|
57
|
-
class TarByteStream {
|
|
93
|
+
class TarByteStream implements TarCursor {
|
|
58
94
|
readonly #chunks: AsyncIterator<Uint8Array> | Iterator<Uint8Array>;
|
|
59
95
|
#chunk: Uint8Array = new Uint8Array(0);
|
|
60
96
|
#position = 0;
|
|
@@ -119,6 +155,27 @@ interface TarPayload {
|
|
|
119
155
|
read(offset: number, size: number, memberPath: string): Promise<Uint8Array>;
|
|
120
156
|
}
|
|
121
157
|
|
|
158
|
+
class TarSourcePayload implements TarPayload {
|
|
159
|
+
readonly #source: ByteSource;
|
|
160
|
+
|
|
161
|
+
constructor(source: ByteSource) {
|
|
162
|
+
this.#source = source;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async read(offset: number, size: number, memberPath: string): Promise<Uint8Array> {
|
|
166
|
+
try {
|
|
167
|
+
const bytes = await this.#source.read(offset, offset + size);
|
|
168
|
+
if (bytes.byteLength !== size) {
|
|
169
|
+
throw new ArchiveError(`Archive member '${formatArchivePathForError(memberPath)}' is truncated`);
|
|
170
|
+
}
|
|
171
|
+
return bytes;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof ArchiveError) throw error;
|
|
174
|
+
throw new ArchiveError(error instanceof Error ? error.message : "Failed to read tar member");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
122
179
|
class TarBufferPayload implements TarPayload {
|
|
123
180
|
readonly #buffer: Uint8Array;
|
|
124
181
|
|
|
@@ -533,7 +590,7 @@ function resolvePendingLinks(
|
|
|
533
590
|
|
|
534
591
|
/** Read one metadata record's payload, bounded by `maxIndexSize`, and step over its padding. */
|
|
535
592
|
async function readTarMetadata(
|
|
536
|
-
stream:
|
|
593
|
+
stream: TarCursor,
|
|
537
594
|
size: number,
|
|
538
595
|
limits: ArchiveLimits,
|
|
539
596
|
what: string,
|
|
@@ -551,7 +608,7 @@ async function readTarMetadata(
|
|
|
551
608
|
* stepped over, never held: entries point into `payload` by stream offset.
|
|
552
609
|
*/
|
|
553
610
|
async function readTarEntries(
|
|
554
|
-
stream:
|
|
611
|
+
stream: TarCursor,
|
|
555
612
|
payload: TarPayload,
|
|
556
613
|
options: FormatReadOptions,
|
|
557
614
|
): Promise<ArchiveIndexEntry[]> {
|
|
@@ -737,18 +794,17 @@ export async function readTarEntriesFromStream(
|
|
|
737
794
|
}
|
|
738
795
|
}
|
|
739
796
|
|
|
740
|
-
/**
|
|
797
|
+
/** Index a plain tar by ranging over headers and retaining lazy member offsets. */
|
|
741
798
|
export const readTar: FormatReader = async (source, options) => {
|
|
742
|
-
|
|
743
|
-
|
|
799
|
+
if (!Number.isSafeInteger(source.size) || source.size < 0) {
|
|
800
|
+
throw new ArchiveError("Archive is too large to read safely");
|
|
801
|
+
}
|
|
744
802
|
try {
|
|
745
|
-
|
|
803
|
+
return await readTarEntries(new TarSourceCursor(source), new TarSourcePayload(source), options);
|
|
746
804
|
} catch (error) {
|
|
747
805
|
if (error instanceof ArchiveError) throw error;
|
|
748
806
|
throw new ArchiveError(error instanceof Error ? error.message : "Failed to read tar archive");
|
|
749
807
|
}
|
|
750
|
-
if (bytes.byteLength !== source.size) throw new ArchiveError("Invalid archive: truncated data");
|
|
751
|
-
return readTarEntriesFromBuffer(bytes, options);
|
|
752
808
|
};
|
|
753
809
|
|
|
754
810
|
/** Detect a tar header, including legacy pre-ustar archives, by its checksum. */
|
package/src/ar/types.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type WritableArchiveFormat = "zip" | "tar" | "tar.gz" | "tar.zst" | "asar
|
|
|
37
37
|
* Where to read an archive from: an extension-inferred filesystem path, a
|
|
38
38
|
* format-tagged filesystem path, in-memory bytes, or any caller-provided
|
|
39
39
|
* {@link ByteSource} (e.g. `httpByteSource` for ranged remote reads).
|
|
40
|
-
* File- and source-backed ZIP/ASAR/RAR/7z/ISO are read lazily.
|
|
40
|
+
* File- and source-backed TAR/ZIP/ASAR/RAR/7z/ISO are read lazily.
|
|
41
41
|
*/
|
|
42
42
|
export type ArchiveSource =
|
|
43
43
|
| string
|
package/src/dirs.ts
CHANGED
|
@@ -333,7 +333,21 @@ export function getSafeProjectCwd(): string {
|
|
|
333
333
|
|
|
334
334
|
/** Get the config directory name relative to home (e.g. ".zeta" or ZETA_CONFIG_DIR override). */
|
|
335
335
|
export function getConfigDirName(): string {
|
|
336
|
-
|
|
336
|
+
const configured = process.env.ZETA_CONFIG_DIR;
|
|
337
|
+
if (!configured) return CONFIG_DIR_NAME;
|
|
338
|
+
// This value is a directory *name* under the user's home, never a path to
|
|
339
|
+
// adopt directly. Absolute paths would be re-joined to home by every
|
|
340
|
+
// consumer (`getBaseConfigRoot`, project discovery, plugin roots) and
|
|
341
|
+
// produce inconsistent trees; `..` would escape home entirely. Reject
|
|
342
|
+
// both at the boundary instead of scattering files.
|
|
343
|
+
if (path.isAbsolute(configured) || path.parse(configured).root !== "") {
|
|
344
|
+
throw new Error("ZETA_CONFIG_DIR must be a relative directory name under the home directory");
|
|
345
|
+
}
|
|
346
|
+
const normalized = path.normalize(configured);
|
|
347
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
|
|
348
|
+
throw new Error("ZETA_CONFIG_DIR must stay within the home directory");
|
|
349
|
+
}
|
|
350
|
+
return normalized;
|
|
337
351
|
}
|
|
338
352
|
|
|
339
353
|
/** Get the config agent directory name relative to home (e.g. ".zeta/agent" or PI_CONFIG_DIR + "/agent"). */
|
package/src/format.ts
CHANGED
|
@@ -33,10 +33,14 @@ export function formatDuration(ms: number): string {
|
|
|
33
33
|
/**
|
|
34
34
|
* Format a number with K/M/B suffix for compact display.
|
|
35
35
|
* Uses 1 decimal for small leading digits when non-zero, rounded otherwise.
|
|
36
|
+
* Values below 1,000 print as-is unless `maxFractionDigits` caps their decimals.
|
|
36
37
|
* Examples: "999", "1K", "1.5K", "25K", "1M", "1.5M", "25M", "1.5B"
|
|
37
38
|
*/
|
|
38
|
-
export function formatNumber(n: number): string {
|
|
39
|
-
if (n < 1_000)
|
|
39
|
+
export function formatNumber(n: number, maxFractionDigits?: number): string {
|
|
40
|
+
if (n < 1_000) {
|
|
41
|
+
if (maxFractionDigits === undefined || Number.isInteger(n)) return n.toString();
|
|
42
|
+
return Number(n.toFixed(maxFractionDigits)).toString();
|
|
43
|
+
}
|
|
40
44
|
if (n < 10_000) return `${trim1(n / 1_000)}K`;
|
|
41
45
|
if (n < 1_000_000) return `${Math.round(n / 1_000)}K`;
|
|
42
46
|
if (n < 10_000_000) return `${trim1(n / 1_000_000)}M`;
|
package/src/index.ts
CHANGED
package/src/json.ts
CHANGED
|
@@ -9,6 +9,31 @@ export function tryParseJson<T = unknown>(content: string): T | null {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Index just past the JSON object or array that opens at `text[start]`, or -1
|
|
14
|
+
* when it does not close within `text`. String literals are skipped, honoring
|
|
15
|
+
* escapes, so brackets inside values do not count. Malformed input yields some
|
|
16
|
+
* end that a parser then rejects.
|
|
17
|
+
*/
|
|
18
|
+
export function jsonContainerEnd(text: string, start: number): number {
|
|
19
|
+
let depth = 0;
|
|
20
|
+
for (let index = start; index < text.length; index++) {
|
|
21
|
+
const code = text.charCodeAt(index);
|
|
22
|
+
if (code === 0x22) {
|
|
23
|
+
for (index++; index < text.length; index++) {
|
|
24
|
+
const inner = text.charCodeAt(index);
|
|
25
|
+
if (inner === 0x5c) index++;
|
|
26
|
+
else if (inner === 0x22) break;
|
|
27
|
+
}
|
|
28
|
+
} else if (code === 0x7b || code === 0x5b) {
|
|
29
|
+
depth++;
|
|
30
|
+
} else if ((code === 0x7d || code === 0x5d) && --depth === 0) {
|
|
31
|
+
return index + 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
|
|
12
37
|
/**
|
|
13
38
|
* Serialize JSON while preserving bigint precision as decimal strings.
|
|
14
39
|
*
|
|
@@ -3,7 +3,6 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { localDay } from "../dirs";
|
|
6
|
-
import { openCloexecSync } from "../fs-open";
|
|
7
6
|
|
|
8
7
|
/** Write batching for {@link RotatingFileSink}. */
|
|
9
8
|
export interface RotatingFileBatchOptions {
|
|
@@ -87,7 +86,7 @@ export class RotatingFileSink {
|
|
|
87
86
|
|
|
88
87
|
#openFd(filePath: string): void {
|
|
89
88
|
this.#closeFd();
|
|
90
|
-
this.#fd =
|
|
89
|
+
this.#fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
|
|
91
90
|
try {
|
|
92
91
|
this.#onRotate?.(filePath);
|
|
93
92
|
} catch {
|