@linxiraos/pi-utils 1.1.27 → 1.2.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +4 -6
  2. package/README.md +1 -1
  3. package/dist/types/ar/index.d.ts +1 -0
  4. package/dist/types/ar/zip-package.d.ts +21 -0
  5. package/dist/types/color.d.ts +2 -0
  6. package/dist/types/dirs.d.ts +31 -12
  7. package/dist/types/docx/converter.d.ts +2 -0
  8. package/dist/types/dom/core.d.ts +15 -4
  9. package/dist/types/dom/parser.d.ts +5 -3
  10. package/dist/types/dom/selector.d.ts +7 -1
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/logger/rotating-file.d.ts +4 -2
  13. package/dist/types/loop-phase.d.ts +12 -7
  14. package/dist/types/marked/core.d.ts +7 -6
  15. package/dist/types/marked-list.d.ts +38 -0
  16. package/dist/types/math-delimiters.d.ts +10 -0
  17. package/dist/types/readability/types.d.ts +2 -0
  18. package/dist/types/release-assets.d.ts +22 -0
  19. package/dist/types/temp.d.ts +7 -0
  20. package/dist/types/turndown/types.d.ts +7 -0
  21. package/dist/types/vterm/buffer.d.ts +16 -5
  22. package/package.json +3 -3
  23. package/src/ar/index.ts +1 -0
  24. package/src/ar/open.ts +29 -8
  25. package/src/ar/zip-package.ts +57 -0
  26. package/src/chalk.ts +66 -23
  27. package/src/color.ts +6 -0
  28. package/src/dirs.ts +84 -24
  29. package/src/docx/converter.ts +26 -12
  30. package/src/dom/core.ts +354 -111
  31. package/src/dom/parser.ts +42 -19
  32. package/src/dom/selector.ts +328 -95
  33. package/src/fetch-retry.ts +2 -0
  34. package/src/format.ts +5 -1
  35. package/src/index.ts +1 -0
  36. package/src/json-lexer.ts +54 -0
  37. package/src/json-parse.ts +21 -6
  38. package/src/logger/rotating-file.ts +5 -3
  39. package/src/logger.ts +39 -23
  40. package/src/loop-phase.ts +66 -21
  41. package/src/marked/core.ts +42 -18
  42. package/src/marked-list.ts +73 -0
  43. package/src/math-delimiters.ts +16 -0
  44. package/src/module-timer.ts +5 -5
  45. package/src/postmortem.ts +1 -1
  46. package/src/process-name.ts +2 -2
  47. package/src/prompt.ts +3 -1
  48. package/src/ptree.ts +28 -35
  49. package/src/readability/readability.ts +349 -39
  50. package/src/readability/types.ts +2 -0
  51. package/src/release-assets.ts +40 -0
  52. package/src/runtime-install.ts +2 -2
  53. package/src/stream.ts +34 -13
  54. package/src/temp.ts +7 -0
  55. package/src/template.ts +7 -4
  56. package/src/timing-buffer.ts +1 -1
  57. package/src/tls-fetch.ts +1 -1
  58. package/src/turndown/gfm.ts +16 -5
  59. package/src/turndown/html.ts +28 -8
  60. package/src/turndown/service.ts +43 -15
  61. package/src/turndown/types.ts +7 -0
  62. package/src/vterm/buffer.ts +31 -11
  63. package/src/vterm/terminal.ts +213 -100
package/CHANGELOG.md CHANGED
@@ -1,14 +1,12 @@
1
- # Changelog
2
-
3
1
  ## [Unreleased]
4
2
 
5
- ## [1.1.27] - 2026-10-07
3
+ ## [1.2.0] - 2026-10-12
6
4
 
7
- - 版本线推进至 1.1.27;本版无独立用户可见变化。
5
+ - Added `getNativeGrammarsDir()`, where the native addon loads downloaded tree-sitter grammars from (`~/.zeta/natives/grammars`).
8
6
 
9
- ### Breaking Changes
7
+ ## [1.1.27] - 2026-10-07
10
8
 
11
- - Replaced the `cursorPosition` option of `TerminalQueryResponder` with `hostCursorHandshake`, which leaves only the PTY host's own session-start cursor query unanswered instead of every cursor-position query
9
+ - 版本线推进至 1.1.27;本版无独立用户可见变化。
12
10
 
13
11
  ## [1.1.25] - 2026-10-03
14
12
 
package/README.md CHANGED
@@ -8,7 +8,7 @@ Shared utilities for [omp](https://github.com/can1357/oh-my-pi) packages. Zero c
8
8
  | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
9
9
  | `logger` | Centralized logger writing to `~/.zeta/logs/` with rotation (TUI-safe — never stdout) |
10
10
  | `prompt` | Handlebars-based prompt templating and formatting helpers |
11
- | `dirs` | Path helpers for omp config directories (`~/.zeta`, XDG-aware on Linux) |
11
+ | `dirs` | Path helpers for zetacode config directories (`~/.zeta`, XDG-aware on Linux) |
12
12
  | `stream` | `readStream` / `readLines` helpers over `ReadableStream` |
13
13
  | `ptree` / `procmgr` | Process trees, `ChildProcess` wrapper, process lifecycle management |
14
14
  | `postmortem` | Cleanup callbacks on exit, signals, and fatal exceptions |
@@ -9,3 +9,4 @@ export * from "./registry.js";
9
9
  export * from "./source.js";
10
10
  export * from "./types.js";
11
11
  export * from "./write.js";
12
+ export * from "./zip-package.js";
@@ -0,0 +1,21 @@
1
+ import type { ArchiveReader } from "./reader.js";
2
+ /**
3
+ * A ZIP-based document package (OOXML, EPUB) whose members are inflated only
4
+ * when read. Lookups use exact member paths, so a converter sees the same
5
+ * members a fully materialized `path → bytes` map would expose. Each member's
6
+ * size counts once toward a cap on total inflated bytes, like a fully
7
+ * materialized archive, so a crafted package cannot amplify past the in-memory
8
+ * limit one member at a time.
9
+ */
10
+ export declare class ZipPackage {
11
+ #private;
12
+ constructor(archive: ArchiveReader, maxTotalBytes?: number);
13
+ /** Index a ZIP buffer without inflating any member. */
14
+ static open(bytes: Uint8Array, maxTotalBytes?: number): Promise<ZipPackage>;
15
+ /** Exact paths of every file member. */
16
+ get members(): ReadonlySet<string>;
17
+ /** Inflate one member, or `undefined` when absent. Throws once distinct inflated bytes exceed the cap. */
18
+ readBytes(memberPath: string): Promise<Uint8Array | undefined>;
19
+ /** Inflate one member as UTF-8 text, or `undefined` when absent. */
20
+ readText(memberPath: string): Promise<string | undefined>;
21
+ }
@@ -111,6 +111,8 @@ export declare function oklchCusp(h: number): {
111
111
  l: number;
112
112
  c: number;
113
113
  };
114
+ /** Angular distance between two hues in degrees (0–180), wrapping at 360. */
115
+ export declare function hueDistance(a: number, b: number): number;
114
116
  /**
115
117
  * Convert OKLCH to a CSS hex string, gamut-mapping by chroma reduction.
116
118
  *
@@ -1,22 +1,23 @@
1
1
  /**
2
- * Centralized path helpers for zeta-c config directories.
2
+ * Centralized path helpers for zetacode config directories.
3
3
  *
4
4
  * Uses PI_CONFIG_DIR (default ".zeta") for the config root and
5
+ * Uses ZETA_CONFIG_DIR (default ".zeta") for the config root and
5
6
  * ZETA_CODING_AGENT_DIR to override the agent directory.
6
7
  *
7
8
  * On Linux, if XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME environment
8
9
  * variables are set, paths are redirected to XDG-compliant locations under
9
- * $XDG_*_HOME/zeta/. This requires running `zeta-c config migrate` first to
10
+ * $XDG_*_HOME/zeta/. This requires running `zetacode config migrate` first to
10
11
  * move data to the new locations. No filesystem existence checks are performed
11
12
  * — if the env var is set, zeta trusts that the migration has been done.
12
13
  */
13
14
  /** App name (e.g. "zeta") — product-level identity: config-dir root, log file
14
15
  * names, provider attribution headers, splash wordmark. NOT the CLI command. */
15
16
  export declare const APP_NAME: string;
16
- /** CLI binary name (`npm i -g @linxiraos/zeta` installs `zeta-c`, with
17
- * `zeta-cli` and `zetacode` as aliases). Every user-facing "run this command"
18
- * string and process title uses this; the bare `zeta` name belongs to the
19
- * @linxiraos/main workspace product, not this package. */
17
+ /** CLI binary name (`npm i -g @linxiraos/zeta` installs `zetacode`, with
18
+ * `zetacode` and `zeta-cli` as compatibility aliases). Every user-facing "run
19
+ * this command" string and process title uses this; the bare `zeta` name
20
+ * belongs to the @linxiraos/main workspace product, not this package. */
20
21
  export declare const CLI_BIN_NAME: string;
21
22
  /** Public homepage that inference gateways (OpenRouter, Vercel AI Gateway) credit our traffic to. */
22
23
  export declare const APP_URL: string;
@@ -91,7 +92,7 @@ export declare function directoryIsEnterableSync(dir: string): boolean;
91
92
  * Used by spawns that must preserve project-relative behavior when healthy.
92
93
  */
93
94
  export declare function getSafeProjectCwd(): string;
94
- /** Get the config directory name relative to home (e.g. ".zeta" or PI_CONFIG_DIR override). */
95
+ /** Get the config directory name relative to home (e.g. ".zeta" or ZETA_CONFIG_DIR override). */
95
96
  export declare function getConfigDirName(): string;
96
97
  /** Get the config agent directory name relative to home (e.g. ".zeta/agent" or PI_CONFIG_DIR + "/agent"). */
97
98
  export declare function getConfigAgentDirName(): string;
@@ -166,10 +167,21 @@ export declare function getPluginsDir(home?: string): string;
166
167
  export declare function getPluginsNodeModules(home?: string): string;
167
168
  /** Plugin manifest (~/.zeta/plugins/package.json). */
168
169
  export declare function getPluginsPackageJson(home?: string): string;
169
- /** Plugin lock file (~/.zeta/plugins/omp-plugins.lock.json). The on-disk
170
- * filename stays upstream-compatible so existing installs keep their runtime
171
- * state (enabled features, versions) across upgrades — never rebrand it. */
170
+ /**
171
+ * Plugin lock file (~/.zeta/plugins/omp-plugins.lock.json).
172
+ *
173
+ * The on-disk filename stays upstream-compatible: it carries per-plugin runtime
174
+ * state (enabled flag, version), so renaming it would reset every existing
175
+ * install's plugins on upgrade. That is the reason for the compat, not a
176
+ * general rule — see `document/brand-surface-rules.md`, which classifies this
177
+ * surface as DUAL (read both spellings, write the Zeta one) rather than
178
+ * permanently frozen.
179
+ */
172
180
  export declare function getPluginsLockfile(home?: string): string;
181
+ /** Zeta-named lockfile, written going forward; `getPluginsLockfile` is read first. */
182
+ export declare const PLUGINS_LOCKFILE_CANONICAL = "zeta-plugins.lock.json";
183
+ /** Pre-rename lockfile name still produced by earlier installs. */
184
+ export declare const PLUGINS_LOCKFILE_LEGACY = "omp-plugins.lock.json";
173
185
  /**
174
186
  * Normalize a plugin id into a safe single path segment (spec §4.2): lowercased,
175
187
  * every character outside `[a-z0-9._-]` (including `/` and `\`, so scoped npm
@@ -204,11 +216,11 @@ export declare function adoptLegacyFileOnce(legacyPath: string, targetPath: stri
204
216
  export declare function getRemoteDir(): string;
205
217
  /**
206
218
  * Relocate the base directory for agent-managed worktrees (PR checkouts, task
207
- * isolation, and `zeta worktree` cleanup all read the same base). Driven by the
219
+ * isolation, and `zetacode worktree` cleanup all read the same base). Driven by the
208
220
  * `worktree.base` setting in coding-agent; pass `undefined`/empty to clear and
209
221
  * fall back to `OMP_WORKTREE_DIR` or the `~/.zeta/wt` default.
210
222
  *
211
- * `~` is expanded and a relative path is rejected (see {@link resolveWorktreeBase}).
223
+ * `~` is expanded and a relative path is rejected (see {@link resolveAbsoluteDir}).
212
224
  * Returns the absolute path that took effect, or `undefined` if the input was
213
225
  * cleared or rejected — callers can warn on a non-empty input that returns
214
226
  * `undefined`.
@@ -283,6 +295,13 @@ export declare function getFastembedCacheDir(): string;
283
295
  /** Get the on-demand fastembed runtime install root (~/.zeta/cache/fastembed-runtime). */
284
296
  export declare function getFastembedRuntimeDir(): string;
285
297
  /** Get the natives directory (~/.zeta/natives). */
298
+ /**
299
+ * Get the directory the native addon loads downloaded tree-sitter wasm
300
+ * grammars from (`<natives dir>/grammars`, `~/.zeta/natives/grammars`). The
301
+ * natives loader configures the addon with the same path.
302
+ */
303
+ export declare function getNativeGrammarsDir(): string;
304
+ /** Get the natives directory. PI_NATIVES_DIR overrides the usual cache root; relative values are ignored. */
286
305
  export declare function getNativesDir(): string;
287
306
  /** Get the stats database path (~/.zeta/stats.db). */
288
307
  export declare function getStatsDbPath(): string;
@@ -22,6 +22,8 @@ export interface DocxImage {
22
22
  readonly altText: string;
23
23
  /** Read the image payload using mammoth's used encoding surface. */
24
24
  read(encoding: "base64"): Promise<string>;
25
+ /** Read the raw image bytes without a base64 round trip. */
26
+ readBytes(): Promise<Uint8Array>;
25
27
  }
26
28
  /** HTML attributes returned by a custom image converter. */
27
29
  export type ImageAttributes = Readonly<Record<string, string>>;
@@ -59,6 +59,7 @@ export declare class EventTarget {
59
59
  }
60
60
  /** Base class for the implemented DOM tree. */
61
61
  export declare class Node extends EventTarget {
62
+ #private;
62
63
  static readonly ELEMENT_NODE = NodeType.ELEMENT;
63
64
  static readonly ATTRIBUTE_NODE = NodeType.ATTRIBUTE;
64
65
  static readonly TEXT_NODE = NodeType.TEXT;
@@ -69,8 +70,12 @@ export declare class Node extends EventTarget {
69
70
  readonly nodeName: string;
70
71
  parentNode: Node | null;
71
72
  ownerDocument: Document | null;
72
- childNodes: Node[];
73
73
  constructor(nodeType: number, nodeName: string, ownerDocument?: Document | null);
74
+ /** Child nodes in tree order. */
75
+ get childNodes(): Node[];
76
+ set childNodes(value: Node[]);
77
+ /** Whether this node has any children (without allocating a child list). */
78
+ hasChildNodes(): boolean;
74
79
  /** First child node, if present. */
75
80
  get firstChild(): Node | null;
76
81
  /** Last child node, if present. */
@@ -196,14 +201,20 @@ export declare class CSSStyleDeclaration {
196
201
  }
197
202
  /** DOM element with attributes, selectors, and HTML serialization. */
198
203
  export declare class Element extends Node {
204
+ #private;
199
205
  readonly localName: string;
200
206
  readonly tagName: string;
201
207
  readonly namespaceURI: string | null;
202
208
  readonly qualifiedName: string;
203
- readonly attributes: NamedNodeMap;
204
- readonly classList: DOMTokenList;
205
- readonly style: CSSStyleDeclaration;
206
209
  constructor(tagName: string, ownerDocument?: Document | null, namespaceURI?: string | null);
210
+ /** Attribute collection. */
211
+ get attributes(): NamedNodeMap;
212
+ /** Class token list over the `class` attribute. */
213
+ get classList(): DOMTokenList;
214
+ /** Inline style declaration. */
215
+ get style(): CSSStyleDeclaration;
216
+ /** Whether the element has any attribute objects (without allocating the collection). */
217
+ hasAttributes(): boolean;
207
218
  /** Element children. */
208
219
  get children(): Element[];
209
220
  /** First element child. */
@@ -1,7 +1,9 @@
1
- import { Document, type DocumentFragment } from "./core.js";
1
+ import { Document, type DocumentFragment, Element } from "./core.js";
2
2
  /** Decode common named entities and all valid numeric character references. */
3
3
  export declare function decodeEntities(value: string): string;
4
- /** Parse markup into a document fragment. */
5
- export declare function parseFragment(html: string, document: Document, contextTag?: string): DocumentFragment;
4
+ /** Parse markup, appending the resulting nodes to `root` (a document, fragment, or element). */
5
+ export declare function parseInto(html: string, document: Document, root: Document | DocumentFragment | Element): void;
6
+ /** Parse markup into a document fragment. `_contextTag` is accepted for compatibility and ignored. */
7
+ export declare function parseFragment(html: string, document: Document, _contextTag?: string): DocumentFragment;
6
8
  /** Parse markup directly into a document. */
7
9
  export declare function parseDocument(html: string): Document;
@@ -1,5 +1,11 @@
1
1
  import { type Document, type DocumentFragment, Element } from "./core.js";
2
+ /** Whether a whitespace-separated class string contains a token, without splitting it. */
3
+ export declare function hasClassToken(className: string, token: string): boolean;
2
4
  /** Whether an element matches a CSS selector list. */
3
5
  export declare function matchesSelector(element: Element, selector: string): boolean;
6
+ /** Nearest inclusive ancestor of an element matching a CSS selector list. */
7
+ export declare function closestMatching(element: Element, selector: string): Element | null;
4
8
  /** Query descendants of a node in document order. */
5
- export declare function querySelectorAllFrom(root: Document | DocumentFragment | Element, selector: string, includeRoot: boolean): Element[];
9
+ export declare function querySelectorAllFrom(root: Document | DocumentFragment | Element, selector: string, _includeRoot: boolean): Element[];
10
+ /** First descendant of a node in document order matching a selector, stopping at the first match. */
11
+ export declare function querySelectorFrom(root: Document | DocumentFragment | Element, selector: string): Element | null;
@@ -29,6 +29,7 @@ export * as procmgr from "./procmgr.js";
29
29
  export * as prompt from "./prompt.js";
30
30
  export * as ptree from "./ptree.js";
31
31
  export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree.js";
32
+ export * from "./release-assets.js";
32
33
  export * from "./runtime-install.js";
33
34
  export * from "./sanitize-text.js";
34
35
  export * from "./snowflake.js";
@@ -47,9 +47,11 @@ export declare class RotatingFileSink {
47
47
  /**
48
48
  * Append one already-formatted log record. A batching sink buffers it unless
49
49
  * `flushNow` is set or the buffer reached its size threshold; either way the
50
- * whole buffer, this record included, is then written in order.
50
+ * whole buffer, this record included, is then written in order. `day` is the
51
+ * record's `localDay` key, so callers that already took the record's `Date`
52
+ * reuse it.
51
53
  */
52
- write(line: string, flushNow?: boolean): void;
54
+ write(line: string, flushNow?: boolean, day?: string): void;
53
55
  /** Write every buffered record to the active file now. */
54
56
  flush(): void;
55
57
  /** Write buffered records, then stop accepting records. */
@@ -1,10 +1,15 @@
1
+ /** A label and its exclusive in-window time, as named by {@link takeLoopPhaseAttribution}. */
2
+ export interface LoopPhaseAttribution {
3
+ readonly label: string;
4
+ /** Total time this label was the innermost active phase inside the late window [start, end]. */
5
+ readonly ms: number;
6
+ }
1
7
  export declare function pushLoopPhase(label: string): void;
2
8
  export declare function popLoopPhase(): void;
3
9
  export declare function currentLoopPhase(): string | undefined;
4
- /**
5
- * Phase to blame for a just-detected loop block: the live top phase if one is
6
- * still held, else the most recent phase pushed since the last call. Clears the
7
- * recent slot so a block in a later, phase-less interval is not misattributed
8
- * to a phase that already finished.
9
- */
10
- export declare function takeRecentLoopPhase(): string | undefined;
10
+ /** Run `fn` under `label`. For an async `fn`, only its synchronous prefix (up to its first await) is labeled; label each continuation separately. */
11
+ export declare function withLoopPhase<T>(label: string, fn: () => T): T;
12
+ /** Arm an attribution window starting at `start` (a deadline on `now`'s clock). Called with no arguments it disarms and clears. */
13
+ export declare function resetLoopPhaseWindow(start?: number, now?: () => number): void;
14
+ /** Attribute the window [start, end] and disarm it. Returns undefined when no window is armed or no label outweighs unlabeled time. */
15
+ export declare function takeLoopPhaseAttribution(end?: number): LoopPhaseAttribution | undefined;
@@ -185,14 +185,15 @@ export type TokensList = Token[] & {
185
185
  export interface TokenizerThis {
186
186
  lexer: Lexer;
187
187
  /**
188
- * For an inline tokenizer, the whole inline source, such as a paragraph's text. The `src` it receives is the part
189
- * of it before `end` that starts at `end - src.length`: inside a link label or the text of emphasis, `end` is where
190
- * that text ends, right before its closer, and a token there must not reach past it. Inline tokenizers get one
191
- * context object per inline source, the same for every call while the lexer works through it and the labels and
192
- * emphasis inside it, so a tokenizer can keep state for it in a `WeakMap`.
188
+ * The whole source the lexer works through. The `src` a tokenizer receives is the part of it before `end` that
189
+ * starts at `end - src.length`. For a block tokenizer, `source` is the text passed to one `blockTokens` call and
190
+ * `end` its length. For an inline tokenizer, it is the inline source, such as a paragraph's text: inside a link
191
+ * label or the text of emphasis, `end` is where that text ends, right before its closer, and a token there must
192
+ * not reach past it. Tokenizers get one context object per source, the same for every call while the lexer works
193
+ * through it (and, inline, the labels and emphasis inside it), so a tokenizer can keep state for it in a `WeakMap`.
193
194
  */
194
195
  source?: string;
195
- /** For an inline tokenizer, where the text the lexer works through ends in `source` (see `source`). */
196
+ /** Where the text the lexer works through ends in `source` (see `source`). */
196
197
  end?: number;
197
198
  }
198
199
  /** A tokenizer extension callback. */
@@ -0,0 +1,38 @@
1
+ /**
2
+ * List-continuation grammar of `@linxiraos/pi-utils/marked`, for streaming
3
+ * renderers that freeze a lexed prefix at blank-line boundaries (TUI and
4
+ * collab web).
5
+ *
6
+ * marked's list tokenizer (Tokenizer.list, marked v18) continues a list across
7
+ * blank lines only when the remaining source matches
8
+ * `listItemRegex(marker)` = `^( {0,3}${marker})((?:[\t ][^\n]*)?(?:\n|$))`,
9
+ * where `marker` is the exact bullet char for unordered lists (`\${char}`) or
10
+ * 1-9 digits plus the exact delimiter for ordered lists (`\d{1,9}\${delim}`).
11
+ * The marker is derived from the list's FIRST item (`n = t[1].trim()`), which
12
+ * sits at the start of a top-level list token's raw.
13
+ */
14
+ /**
15
+ * Whether a same-marker item could still continue the list `listRaw` at
16
+ * `tailStart` (directly after the blank line ending the list), now or after
17
+ * any append to `text`.
18
+ *
19
+ * Streaming-freeze equivalence invariant: lex(prefix) ++ lex(tail) must equal
20
+ * lex(full text) for the current text and every append-only extension of it,
21
+ * because a frozen prefix is sticky. At a blank-line cut directly after a
22
+ * top-level `list` token, the only construct that can straddle the cut is a
23
+ * continuation item of that list: marked consumed the blank line into the last
24
+ * item's raw and re-ran `listItemRegex` at exactly `tailStart`, merging a
25
+ * same-marker item into one renumbered loose list. The cut is safe only when
26
+ * that regex can NEVER match at `tailStart`, no matter what is appended later.
27
+ *
28
+ * Append-only growth means "closed" may only be concluded from a present
29
+ * character that contradicts every possible continuation (tail "1x" can never
30
+ * grow into an ordered item, but tail "1" can become "1. c"). Running out of
31
+ * text mid-marker therefore answers "may continue".
32
+ *
33
+ * Returns true when the tail could still continue the list (or the list's
34
+ * marker is unrecognizable) — the conservative "don't freeze" answer. marked
35
+ * may break the list anyway when the matching line is also an hr (`- - -`);
36
+ * treating that as "may continue" merely skips a freeze, never corrupts one.
37
+ */
38
+ export declare function listMayContinueAt(text: string, tailStart: number, listRaw: string): boolean;
@@ -80,6 +80,16 @@ export declare class MathBlockScan {
80
80
  /** The own-line display block starting at `from`, or `undefined`. */
81
81
  at(from: number): MathBlock | undefined;
82
82
  }
83
+ /**
84
+ * The own-line display block at the start of `src`, for a marked block tokenizer: `src` is the part of
85
+ * `context.source` that starts at `context.end - src.length`. One {@link MathBlockScan} is kept per context, which
86
+ * marked keeps for one `blockTokens` source, so asking at every block start takes linear time even when an opener
87
+ * never closes. Without `context.source` it is {@link mathBlockAt}.
88
+ */
89
+ export declare function mathBlockInContext(context: {
90
+ source?: string;
91
+ end?: number;
92
+ }, src: string): MathBlock | undefined;
83
93
  /**
84
94
  * The display opener alone on the line at `from` (up to 3 leading spaces,
85
95
  * trailing spaces or tabs; the source's last line may be unterminated), or
@@ -13,6 +13,8 @@ export interface ReadabilityNode {
13
13
  appendChild(node: ReadabilityNode): ReadabilityNode;
14
14
  cloneNode(deep?: boolean): ReadabilityNode;
15
15
  remove(): void;
16
+ /** Detach all children; used when present to move or reset children in one step. */
17
+ replaceChildren?(): void;
16
18
  }
17
19
  /** Minimal standards-shaped DOM element accepted by the readability extractor. */
18
20
  export interface ReadabilityElement extends ReadabilityNode {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Published CLI release-asset naming — the one contract shared by the release
3
+ * build (`scripts/ci-release-build-binaries.ts` outfiles), the CI upload matrix
4
+ * (`.github/workflows/ci.yml` `release_binary.binary_path`), the installers
5
+ * (`scripts/install.sh` / `install.ps1`) and the updater's binary fallback.
6
+ *
7
+ * Assets are `zeta-cli-<os>-<arch>[.exe]`. `zeta-cli` is the *file-name* face
8
+ * of the package and deliberately does NOT follow {@link CLI_BIN_NAME}
9
+ * (`zetacode`, the user-visible command face): the brand cutover moved the
10
+ * command name, and the updater's asset name drifted with it, so every binary
11
+ * fallback asked GitHub for `zetacode-*` assets no release carries (v1.1.27 /
12
+ * main). `scripts/check-release-asset-names.ts` pins both sides together.
13
+ */
14
+ /**
15
+ * Leading token of every published CLI binary asset, separator included:
16
+ * assets are `<prefix><os>[-musl]-<arch>[.exe]`.
17
+ */
18
+ export declare const RELEASE_CLI_ASSET_PREFIX = "zeta-cli-";
19
+ /** Release asset for a CLI binary, e.g. `zeta-cli-windows-x64.exe`. */
20
+ export declare function releaseCliAssetName(platform: NodeJS.Platform, arch: string, options?: {
21
+ musl?: boolean;
22
+ }): string;
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Owned temporary directory, removed via `remove()` or `using`/`await using`.
3
+ *
4
+ * Prefixes follow `mkdtemp`: `"@name-"` creates under the OS temp dir, any other
5
+ * prefix is a path (a bare `"name-"` lands in the process cwd), and no prefix
6
+ * means `"@pi-temp-"`.
7
+ */
1
8
  export declare class TempDir {
2
9
  #private;
3
10
  private constructor();
@@ -12,6 +12,8 @@ export interface TurndownNode {
12
12
  readonly previousSibling: TurndownNode | null;
13
13
  readonly textContent: string | null;
14
14
  readonly outerHTML?: string;
15
+ /** Position among the parent's element children, when the node implementation tracks it. */
16
+ readonly elementIndex?: number;
15
17
  getAttribute(name: string): string | null;
16
18
  hasAttribute(name: string): boolean;
17
19
  }
@@ -55,6 +57,11 @@ export type ReplacementFunction = (content: string, node: TurndownNode, options:
55
57
  export interface TurndownRule {
56
58
  filter: RuleFilter;
57
59
  replacement: ReplacementFunction;
60
+ /**
61
+ * Set when `replacement` ignores its `content` argument; the service then skips
62
+ * converting the node's children before calling it.
63
+ */
64
+ skipContent?: boolean;
58
65
  }
59
66
  /** A plugin that installs one or more conversion rules. */
60
67
  export type TurndownPlugin = (service: TurndownServiceLike) => void;
@@ -12,16 +12,27 @@ export interface CellAttributes {
12
12
  bgMode: 0 | 1 | 2;
13
13
  bg: number;
14
14
  }
15
- /** A mutable terminal grid cell. */
15
+ /**
16
+ * A terminal grid cell. Cells written by the terminal are frozen and may be
17
+ * shared between grid positions (blank cells, repeated ASCII glyphs), so the
18
+ * type is readonly: replace a cell instead of mutating it.
19
+ */
16
20
  export interface CellData {
17
- chars: string;
18
- width: number;
19
- attrs: CellAttributes;
21
+ readonly chars: string;
22
+ readonly width: number;
23
+ readonly attrs: Readonly<CellAttributes>;
20
24
  }
21
25
  /** Creates the default rendition. */
22
26
  export declare function defaultAttributes(): CellAttributes;
27
+ /** Frozen default rendition shared by every unstyled cell. */
28
+ export declare const DEFAULT_CELL_ATTRIBUTES: Readonly<CellAttributes>;
23
29
  /** Creates an empty cell with the supplied rendition. */
24
30
  export declare function blankCell(attrs?: CellAttributes): CellData;
31
+ /**
32
+ * Returns the frozen empty cell shared by every blank position with `attrs`.
33
+ * Frozen renditions are interned; a mutable one is snapshotted first.
34
+ */
35
+ export declare function sharedBlankCell(attrs?: Readonly<CellAttributes>): CellData;
25
36
  /** Proposed xterm-compatible cell readback object. */
26
37
  export declare class BufferCell {
27
38
  #private;
@@ -62,7 +73,7 @@ export declare class BufferCell {
62
73
  export declare class BufferLine {
63
74
  cells: CellData[];
64
75
  isWrapped: boolean;
65
- constructor(columns: number, attrs?: CellAttributes);
76
+ constructor(columns: number, attrs?: Readonly<CellAttributes>);
66
77
  /** Number of grid columns in the line. */
67
78
  get length(): number;
68
79
  /** Reads a cell, optionally reusing the caller's object. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-utils",
3
- "version": "1.1.27",
3
+ "version": "1.2.1",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "utilities",
@@ -54,10 +54,10 @@
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.1.27"
57
+ "@linxiraos/pi-natives": "1.2.1"
58
58
  },
59
59
  "devDependencies": {
60
- "@types/bun": "^1.3.14"
60
+ "@types/bun": "1.4.3"
61
61
  },
62
62
  "engines": {
63
63
  "bun": ">=1.3.14"
package/src/ar/index.ts CHANGED
@@ -14,3 +14,4 @@ export * from "./registry";
14
14
  export * from "./source";
15
15
  export * from "./types";
16
16
  export * from "./write";
17
+ export * from "./zip-package";
package/src/ar/open.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { formatBytes } from "../format";
4
+ import { hasFsCode } from "../fs-error";
4
5
  import { UTF8_DECODER } from "./bytes";
5
6
  import { ArchiveError } from "./error";
6
7
  import { type ArchiveLimits, assertInMemorySize, DEFAULT_ARCHIVE_LIMITS } from "./limits";
@@ -179,17 +180,15 @@ export async function extractArchive(
179
180
  if (outputPath !== extractRoot && !outputPath.startsWith(extractRoot + path.sep)) {
180
181
  throw new ArchiveError(`Archive entry escapes extraction dir: ${entry.path}`);
181
182
  }
182
- if (entry.isDirectory) {
183
- if (entry.storage?.type !== "link") {
184
- await fs.mkdir(outputPath, { recursive: true });
185
- count++;
186
- }
187
- continue;
188
- }
189
183
  if (entry.storage?.type === "link") {
190
184
  links.push({ path: entry.path, target: entry.storage.targetPath });
191
185
  continue;
192
186
  }
187
+ if (entry.isDirectory) {
188
+ await fs.mkdir(outputPath, { recursive: true });
189
+ count++;
190
+ continue;
191
+ }
193
192
  files.push({ path: entry.path, mode: entry.mode });
194
193
  }
195
194
 
@@ -217,7 +216,29 @@ export async function extractArchive(
217
216
  throw new ArchiveError(`Archive symlink escapes extraction dir: ${link.path} -> ${link.target}`);
218
217
  }
219
218
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
220
- await fs.symlink(path.relative(path.dirname(outputPath), resolvedTarget) || ".", outputPath);
219
+ const relativeTarget = path.relative(path.dirname(outputPath), resolvedTarget) || ".";
220
+ const linkType = process.platform === "win32" && archive.getNode(link.path)?.isDirectory ? "dir" : "file";
221
+ try {
222
+ await fs.symlink(relativeTarget, outputPath, linkType);
223
+ } catch (error) {
224
+ if (process.platform !== "win32" || !hasFsCode(error, "EPERM")) throw error;
225
+ // Windows without the symlink privilege: degrade to a directory
226
+ // junction or a file copy so extraction still yields usable
227
+ // content instead of failing the whole archive.
228
+ if (linkType === "dir") {
229
+ await fs.symlink(resolvedTarget, outputPath, "junction");
230
+ } else {
231
+ // Resolve through the archive, not the extraction order: this
232
+ // link may target another link that has not materialized yet.
233
+ // A dangling target cannot be copied without the symlink
234
+ // privilege; skip the entry so the rest still extracts.
235
+ const extracted = await archive.readFile(link.path).catch(() => undefined);
236
+ if (!extracted) continue;
237
+ await Bun.write(outputPath, extracted.bytes);
238
+ const permissions = (extracted.mode ?? 0) & 0o777;
239
+ if (permissions) await fs.chmod(outputPath, permissions);
240
+ }
241
+ }
221
242
  count++;
222
243
  }
223
244
 
@@ -0,0 +1,57 @@
1
+ import { UTF8_DECODER } from "./bytes";
2
+ import { type ArchiveLimits, assertInMemorySize, DEFAULT_ARCHIVE_LIMITS } from "./limits";
3
+ import { openArchive } from "./open";
4
+ import type { ArchiveReader } from "./reader";
5
+
6
+ /**
7
+ * A ZIP-based document package (OOXML, EPUB) whose members are inflated only
8
+ * when read. Lookups use exact member paths, so a converter sees the same
9
+ * members a fully materialized `path → bytes` map would expose. Each member's
10
+ * size counts once toward a cap on total inflated bytes, like a fully
11
+ * materialized archive, so a crafted package cannot amplify past the in-memory
12
+ * limit one member at a time.
13
+ */
14
+ export class ZipPackage {
15
+ #archive: ArchiveReader;
16
+ #members = new Set<string>();
17
+ #limits: ArchiveLimits;
18
+ /** Members already counted toward {@link #inflated}; re-reading one is free against the cap. */
19
+ #counted = new Set<string>();
20
+ #inflated = 0;
21
+
22
+ constructor(archive: ArchiveReader, maxTotalBytes = DEFAULT_ARCHIVE_LIMITS.maxInMemorySize) {
23
+ this.#archive = archive;
24
+ this.#limits = { ...DEFAULT_ARCHIVE_LIMITS, maxInMemorySize: maxTotalBytes };
25
+ for (const entry of archive.indexEntries()) {
26
+ if (!entry.isDirectory) this.#members.add(entry.path);
27
+ }
28
+ }
29
+
30
+ /** Index a ZIP buffer without inflating any member. */
31
+ static async open(bytes: Uint8Array, maxTotalBytes?: number): Promise<ZipPackage> {
32
+ return new ZipPackage(await openArchive({ bytes, format: "zip" }), maxTotalBytes);
33
+ }
34
+
35
+ /** Exact paths of every file member. */
36
+ get members(): ReadonlySet<string> {
37
+ return this.#members;
38
+ }
39
+
40
+ /** Inflate one member, or `undefined` when absent. Throws once distinct inflated bytes exceed the cap. */
41
+ async readBytes(memberPath: string): Promise<Uint8Array | undefined> {
42
+ if (!this.#members.has(memberPath)) return undefined;
43
+ const { bytes } = await this.#archive.readFile(memberPath);
44
+ if (!this.#counted.has(memberPath)) {
45
+ this.#counted.add(memberPath);
46
+ this.#inflated += bytes.length;
47
+ assertInMemorySize(this.#inflated, this.#limits);
48
+ }
49
+ return bytes;
50
+ }
51
+
52
+ /** Inflate one member as UTF-8 text, or `undefined` when absent. */
53
+ async readText(memberPath: string): Promise<string | undefined> {
54
+ const bytes = await this.readBytes(memberPath);
55
+ return bytes ? UTF8_DECODER.decode(bytes) : undefined;
56
+ }
57
+ }