@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/src/logger.ts
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* transport so a process supervisor (pm2, journald, k8s) captures the logs.
|
|
8
8
|
*
|
|
9
9
|
* Each entry includes `process.pid` so concurrent zeta instances stay
|
|
10
|
-
* traceable
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* traceable, and the fields of the enclosing {@link withContext} (a session's
|
|
11
|
+
* `sessionId`) so sessions sharing one process stay apart. The file is created
|
|
12
|
+
* on the first record written. Records are batched — one write per second or
|
|
13
|
+
* per 64 KiB — while `warn`/`error` records are written at once together with
|
|
14
|
+
* everything buffered before them; exit, fatal, and signal paths flush the
|
|
15
|
+
* rest. `ZETA_LOG_LEVEL` optionally limits the levels written to the file
|
|
16
|
+
* (default: all).
|
|
15
17
|
*/
|
|
16
18
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
19
|
import * as fs from "node:fs";
|
|
@@ -48,7 +50,18 @@ export function registerLogSink(sink: LogSink): () => void {
|
|
|
48
50
|
|
|
49
51
|
function emitToSinks(level: LogLevel, message: string, context: Record<string, unknown> | undefined): void {
|
|
50
52
|
if (logSinks.size === 0) return;
|
|
51
|
-
|
|
53
|
+
// The enclosing {@link withContext} fields reach sinks as they reach the file;
|
|
54
|
+
// call-site fields win, and a record outside any context keeps its own object.
|
|
55
|
+
const scoped = logContextStorage.getStore();
|
|
56
|
+
let merged = context;
|
|
57
|
+
if (scoped) {
|
|
58
|
+
try {
|
|
59
|
+
merged = { ...scoped, ...context };
|
|
60
|
+
} catch {
|
|
61
|
+
// A throwing getter must not turn a log call into a failure; sinks get the call's own fields.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const event: LogEvent = { level, message, context: merged, timestamp: new Date() };
|
|
52
65
|
for (const sink of logSinks) {
|
|
53
66
|
try {
|
|
54
67
|
sink(event);
|
|
@@ -251,6 +264,28 @@ function formatLocalTimestamp(date: Date): string {
|
|
|
251
264
|
|
|
252
265
|
const FORMAT_TOKEN_PATTERN = /%[scdjifoO%]/;
|
|
253
266
|
|
|
267
|
+
const logContextStorage = new AsyncLocalStorage<Readonly<Record<string, unknown>>>();
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Run `fn` with `context`'s fields on every record logged while it runs,
|
|
271
|
+
* including from the promises, timers and callbacks it starts. A nested call
|
|
272
|
+
* replaces the enclosing context, as a subagent's session replaces its
|
|
273
|
+
* parent's; a field passed to the log call itself wins. Fields are read as each
|
|
274
|
+
* record is written, so a getter stays current.
|
|
275
|
+
*/
|
|
276
|
+
export function withContext<T>(context: Readonly<Record<string, unknown>>, fn: () => T): T {
|
|
277
|
+
return logContextStorage.run(context, fn);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Run `fn` outside any {@link withContext}: for process-wide work started on
|
|
282
|
+
* behalf of one caller, such as a shared server's read loop, whose later
|
|
283
|
+
* records belong to no single caller.
|
|
284
|
+
*/
|
|
285
|
+
export function withoutContext<T>(fn: () => T): T {
|
|
286
|
+
return logContextStorage.exit(fn);
|
|
287
|
+
}
|
|
288
|
+
|
|
254
289
|
/** Serialize one log record: timestamp, level, pid, message, then context fields. */
|
|
255
290
|
function formatLogRecord(
|
|
256
291
|
level: LogLevel,
|
|
@@ -266,6 +301,12 @@ function formatLogRecord(
|
|
|
266
301
|
pid: process.pid,
|
|
267
302
|
message: metadata?.message ? `${message} ${metadata.message}` : message,
|
|
268
303
|
};
|
|
304
|
+
const scoped = logContextStorage.getStore();
|
|
305
|
+
if (scoped) {
|
|
306
|
+
for (const key of Object.keys(scoped)) {
|
|
307
|
+
if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = scoped[key];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
269
310
|
if (metadata) {
|
|
270
311
|
for (const key of Object.keys(metadata)) {
|
|
271
312
|
if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = metadata[key];
|
package/src/marked/core.ts
CHANGED
|
@@ -655,39 +655,48 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
|
655
655
|
// The context is shared with the sources nested in this one, which move its end while they are lexed.
|
|
656
656
|
context.end = closers.end;
|
|
657
657
|
let rest = lex.rest;
|
|
658
|
+
// A token the lexer consumed ends the walk, so the backticks left where the next token starts are a string of their
|
|
659
|
+
// own even when the run they belong to began earlier: an extension, an override or an escape token may leave the
|
|
660
|
+
// scan inside a run, and a code span opens there. Only the text step's own advance leaves the scan inside a run
|
|
661
|
+
// without consuming a token, and no span opens at such a position.
|
|
662
|
+
let resumed = false;
|
|
663
|
+
/** Pushes `token` and moves past its raw, which is where the next token starts. */
|
|
664
|
+
const consumeToken = (token: Token): void => {
|
|
665
|
+
output.push(token);
|
|
666
|
+
rest = rest.slice(token.raw.length);
|
|
667
|
+
resumed = true;
|
|
668
|
+
};
|
|
658
669
|
while (rest !== "") {
|
|
670
|
+
const resumedHere = resumed;
|
|
671
|
+
resumed = false;
|
|
659
672
|
let custom: Tokens.Generic | undefined;
|
|
660
673
|
for (const extension of lexer.extensions.inline) {
|
|
661
674
|
custom = extension.tokenizer.call(context, rest, output);
|
|
662
675
|
if (custom?.raw) break;
|
|
663
676
|
}
|
|
664
677
|
if (custom?.raw) {
|
|
665
|
-
|
|
666
|
-
rest = rest.slice(custom.raw.length);
|
|
678
|
+
consumeToken(custom);
|
|
667
679
|
continue;
|
|
668
680
|
}
|
|
669
681
|
|
|
670
682
|
const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/.exec(rest);
|
|
671
683
|
if (escaped) {
|
|
672
|
-
|
|
673
|
-
rest = rest.slice(2);
|
|
684
|
+
consumeToken({ type: "escape", raw: escaped[0], text: escaped[1]! });
|
|
674
685
|
continue;
|
|
675
686
|
}
|
|
676
687
|
const br = lexer.options.breaks ? /^(?: {2,}|\\)?\n/.exec(rest) : /^(?: {2,}|\\)\n/.exec(rest);
|
|
677
688
|
if (br) {
|
|
678
|
-
|
|
679
|
-
rest = rest.slice(br[0].length);
|
|
689
|
+
consumeToken({ type: "br", raw: br[0] });
|
|
680
690
|
continue;
|
|
681
691
|
}
|
|
682
692
|
if (rest[0] === "`") {
|
|
683
|
-
const width = closers.codeOpenerWidth(rest);
|
|
693
|
+
const width = closers.codeOpenerWidth(rest, resumedHere);
|
|
684
694
|
const end = closers.closeCode(rest, width);
|
|
685
695
|
if (end !== -1) {
|
|
686
696
|
const raw = rest.slice(0, end + width);
|
|
687
697
|
let text = rest.slice(width, end).replace(/\n/g, " ");
|
|
688
698
|
if (/^ .* $/.test(text) && text.trim() !== "") text = text.slice(1, -1);
|
|
689
|
-
|
|
690
|
-
rest = rest.slice(raw.length);
|
|
699
|
+
consumeToken({ type: "codespan", raw, text });
|
|
691
700
|
continue;
|
|
692
701
|
}
|
|
693
702
|
}
|
|
@@ -695,14 +704,12 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
|
695
704
|
if (auto) {
|
|
696
705
|
const text = auto.slice(1, -1);
|
|
697
706
|
const href = text.includes("@") && !/^[a-z][a-z+.-]*:\/\//i.test(text) ? `mailto:${text}` : text;
|
|
698
|
-
|
|
699
|
-
rest = rest.slice(auto.length);
|
|
707
|
+
consumeToken({ type: "link", raw: auto, text, href, tokens: [{ type: "text", raw: text, text }] });
|
|
700
708
|
continue;
|
|
701
709
|
}
|
|
702
710
|
const html = inlineHtmlPrefix(rest, closers);
|
|
703
711
|
if (html) {
|
|
704
|
-
|
|
705
|
-
rest = rest.slice(html.length);
|
|
712
|
+
consumeToken({ type: "html", raw: html, inLink: false, inRawBlock: false, block: false, text: html });
|
|
706
713
|
continue;
|
|
707
714
|
}
|
|
708
715
|
const nested = matchLink(rest, lexer, closers) ?? matchEmphasis(rest, output, closers);
|
|
@@ -716,8 +723,7 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
|
716
723
|
if (override) del = override.call(lexer.tokenizer, rest, rest);
|
|
717
724
|
if (del === false || !override) del = lexer.tokenizer.del(rest);
|
|
718
725
|
if (del) {
|
|
719
|
-
|
|
720
|
-
rest = rest.slice(del.raw.length);
|
|
726
|
+
consumeToken(del);
|
|
721
727
|
continue;
|
|
722
728
|
}
|
|
723
729
|
}
|
|
@@ -744,8 +750,7 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
|
|
|
744
750
|
}
|
|
745
751
|
}
|
|
746
752
|
if (url) {
|
|
747
|
-
|
|
748
|
-
rest = rest.slice(url.raw.length);
|
|
753
|
+
consumeToken(url);
|
|
749
754
|
continue;
|
|
750
755
|
}
|
|
751
756
|
|
|
@@ -170,38 +170,30 @@ class EmphasisDelimiters {
|
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
173
|
+
* next run of exactly `width` backticks: a run of any other length is no closer, and the one that closes is the first
|
|
174
|
+
* run of that width at or after the opener. Backslash escapes do not apply inside a code span, so a backslash just
|
|
175
|
+
* before that closing backtick is content and still closes the span.
|
|
176
176
|
*/
|
|
177
177
|
class CodeRuns {
|
|
178
|
-
// Per run, in order: where it starts and ends
|
|
178
|
+
// Per run, in order: where it starts and ends; the starts are the binary search `endOf` and `startsStringAt` need.
|
|
179
179
|
readonly #starts: number[] = [];
|
|
180
180
|
readonly #ends: number[] = [];
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
readonly #
|
|
184
|
-
readonly #leaves: number;
|
|
181
|
+
// Run starts by width, each list ascending: a closer must have the opener's exact width, so finding one is a
|
|
182
|
+
// binary search in the list for that width and no scan of the runs between.
|
|
183
|
+
readonly #byWidth = new Map<number, number[]>();
|
|
185
184
|
|
|
186
185
|
constructor(src: string) {
|
|
187
|
-
const widths: number[] = [];
|
|
188
186
|
for (let at = src.indexOf("`"); at !== -1;) {
|
|
189
187
|
let end = at + 1;
|
|
190
188
|
while (src.charCodeAt(end) === 0x60 /* ` */) end++;
|
|
191
|
-
const escaped = escapedAt(src, at);
|
|
192
189
|
this.#starts.push(at);
|
|
193
190
|
this.#ends.push(end);
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
const width = end - at;
|
|
192
|
+
const starts = this.#byWidth.get(width);
|
|
193
|
+
if (starts === undefined) this.#byWidth.set(width, [at]);
|
|
194
|
+
else starts.push(at);
|
|
196
195
|
at = src.indexOf("`", end);
|
|
197
196
|
}
|
|
198
|
-
let leaves = 1;
|
|
199
|
-
while (leaves < widths.length) leaves <<= 1;
|
|
200
|
-
const widest = new Int32Array(2 * leaves);
|
|
201
|
-
for (let run = 0; run < widths.length; run++) widest[leaves + run] = widths[run]!;
|
|
202
|
-
for (let node = leaves - 1; node >= 1; node--) widest[node] = Math.max(widest[2 * node]!, widest[2 * node + 1]!);
|
|
203
|
-
this.#widest = widest;
|
|
204
|
-
this.#leaves = leaves;
|
|
205
197
|
}
|
|
206
198
|
|
|
207
199
|
/** Where the run of backticks holding the backtick at `at` ends. */
|
|
@@ -209,28 +201,24 @@ class CodeRuns {
|
|
|
209
201
|
return this.#ends[lowerBound(this.#starts, at + 1) - 1]!;
|
|
210
202
|
}
|
|
211
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Whether a backtick string starts at `at` for a scan that walked to `at`: a string is a whole run, so it starts at
|
|
206
|
+
* the run's first backtick and nowhere else. A retry deeper inside a run is not a string, which keeps a run whose
|
|
207
|
+
* opener found no closer from opening a narrower span one backtick later. A position a tokenizer's raw resumed at,
|
|
208
|
+
* or the suffix an escape token left behind, is not a walked position — `codeOpenerWidth` admits those on its own,
|
|
209
|
+
* because it is told the scan resumed there rather than inferring it from the source text.
|
|
210
|
+
*/
|
|
211
|
+
startsStringAt(at: number): boolean {
|
|
212
|
+
const run = lowerBound(this.#starts, at + 1) - 1;
|
|
213
|
+
return this.#starts[run] === at;
|
|
214
|
+
}
|
|
215
|
+
|
|
212
216
|
/** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
|
|
213
217
|
closerAfter(from: number, width: number): number {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
/** The first run at or after `first` that holds a closer of `width` backticks, or -1. */
|
|
219
|
-
#firstHolding(first: number, width: number): number {
|
|
220
|
-
if (first >= this.#starts.length) return -1;
|
|
221
|
-
const widest = this.#widest;
|
|
222
|
-
const leaves = this.#leaves;
|
|
223
|
-
let node = leaves + first;
|
|
224
|
-
if (widest[node]! >= width) return first;
|
|
225
|
-
// Up to the nearest subtree on the right that holds one, then down to its first run that does.
|
|
226
|
-
for (;;) {
|
|
227
|
-
if (node === 1) return -1;
|
|
228
|
-
if ((node & 1) === 0 && widest[node + 1]! >= width) break;
|
|
229
|
-
node >>= 1;
|
|
230
|
-
}
|
|
231
|
-
node++;
|
|
232
|
-
while (node < leaves) node = widest[2 * node]! >= width ? 2 * node : 2 * node + 1;
|
|
233
|
-
return node - leaves;
|
|
218
|
+
const starts = this.#byWidth.get(width);
|
|
219
|
+
if (starts === undefined) return -1;
|
|
220
|
+
const run = lowerBound(starts, from);
|
|
221
|
+
return run < starts.length ? starts[run]! : -1;
|
|
234
222
|
}
|
|
235
223
|
}
|
|
236
224
|
|
|
@@ -388,10 +376,17 @@ export class InlineClosers {
|
|
|
388
376
|
return close === -1 ? -1 : close - pos;
|
|
389
377
|
}
|
|
390
378
|
|
|
391
|
-
/**
|
|
392
|
-
|
|
379
|
+
/**
|
|
380
|
+
* How many backticks start `rest`, which starts with one, or 0 where no code span opens because no backtick string
|
|
381
|
+
* starts there: the opener is a whole string, so the rest of a run whose opener found no closer is text, while the
|
|
382
|
+
* backticks left after a backslash escaped the run's first one do open a span. `resumed` says the scan resumed at
|
|
383
|
+
* `rest` after a tokenizer consumed a raw that ended here, where the backticks are a string wherever they sit.
|
|
384
|
+
*/
|
|
385
|
+
codeOpenerWidth(rest: string, resumed = false): number {
|
|
393
386
|
const pos = this.end - rest.length;
|
|
394
|
-
|
|
387
|
+
const runs = this.#indexes.codeRuns;
|
|
388
|
+
if (!resumed && !runs.startsStringAt(pos)) return 0;
|
|
389
|
+
return Math.min(runs.endOf(pos), this.end) - pos;
|
|
395
390
|
}
|
|
396
391
|
|
|
397
392
|
/** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
|
package/src/mime.ts
CHANGED
|
@@ -11,6 +11,7 @@ const GIF89A = Buffer.from("GIF89a");
|
|
|
11
11
|
const WEBP_VP8X = Buffer.from("VP8X");
|
|
12
12
|
const WEBP_VP8L = Buffer.from("VP8L");
|
|
13
13
|
const WEBP_VP8 = Buffer.from("VP8 ");
|
|
14
|
+
const FTYP = Buffer.from("ftyp");
|
|
14
15
|
|
|
15
16
|
export const SUPPORTED_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
16
17
|
|
|
@@ -143,6 +144,40 @@ export function parseImageMetadata(header: Uint8Array): ImageMetadata | null {
|
|
|
143
144
|
);
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/** Still images in a HEIF (ISO BMFF) container, which the image pipeline cannot decode itself. */
|
|
148
|
+
export type HeifImageMimeType = "image/heic" | "image/heif" | "image/avif";
|
|
149
|
+
|
|
150
|
+
const HEIF_BRAND_MIME_TYPES: Readonly<Record<string, HeifImageMimeType>> = {
|
|
151
|
+
heic: "image/heic",
|
|
152
|
+
heix: "image/heic",
|
|
153
|
+
heim: "image/heic",
|
|
154
|
+
heis: "image/heic",
|
|
155
|
+
hevc: "image/heic",
|
|
156
|
+
hevx: "image/heic",
|
|
157
|
+
avif: "image/avif",
|
|
158
|
+
avis: "image/avif",
|
|
159
|
+
mif1: "image/heif",
|
|
160
|
+
msf1: "image/heif",
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* MIME type of a HEIC, AVIF or other HEIF image from the brands of its leading `ftyp` box:
|
|
165
|
+
* the major brand, then the compatible brands, with the generic `mif1`/`msf1` brands last.
|
|
166
|
+
*/
|
|
167
|
+
export function parseHeifImageMimeType(header: Uint8Array): HeifImageMimeType | null {
|
|
168
|
+
if (!magicEquals(header, 4, FTYP)) return null;
|
|
169
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
170
|
+
const boxEnd = Math.min(view.getUint32(0, false), header.length);
|
|
171
|
+
let generic: HeifImageMimeType | null = null;
|
|
172
|
+
// The major brand sits at 8 and the compatible brands start at 16, after the minor version.
|
|
173
|
+
for (let offset = 8; offset + 4 <= boxEnd; offset += offset === 8 ? 8 : 4) {
|
|
174
|
+
const mimeType = HEIF_BRAND_MIME_TYPES[String.fromCharCode(...header.subarray(offset, offset + 4))];
|
|
175
|
+
if (mimeType === "image/heif") generic ??= mimeType;
|
|
176
|
+
else if (mimeType) return mimeType;
|
|
177
|
+
}
|
|
178
|
+
return generic;
|
|
179
|
+
}
|
|
180
|
+
|
|
146
181
|
/** Why base64 image data is unusable; `undefined` when it is canonical and non-empty. */
|
|
147
182
|
export type Base64ImageDataFailure = "invalid base64 image data" | "empty image data";
|
|
148
183
|
|
package/src/ports.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 const ZETA_WEB_PORT = 43141;
|
|
14
15
|
export const ZETA_GATEWAY_PORT = 43143;
|
|
15
16
|
export const ZETA_STATS_PORT = 43147;
|
|
17
|
+
export const ZETA_BROWSER_RELAY_PORT = 43153;
|