@linxiraos/pi-utils 1.1.14 → 1.1.16

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 (51) hide show
  1. package/CHANGELOG.md +2 -52
  2. package/THIRD-PARTY-NOTICES.txt +54 -25
  3. package/dist/types/async.d.ts +18 -0
  4. package/dist/types/browsers.d.ts +2 -30
  5. package/dist/types/color.d.ts +2 -0
  6. package/dist/types/dirs.d.ts +21 -0
  7. package/dist/types/env.d.ts +12 -2
  8. package/dist/types/executable.d.ts +4 -0
  9. package/dist/types/fetch-retry.d.ts +8 -2
  10. package/dist/types/file-lock.d.ts +12 -0
  11. package/dist/types/format.d.ts +7 -0
  12. package/dist/types/index.d.ts +2 -1
  13. package/dist/types/mime.d.ts +1 -0
  14. package/dist/types/path.d.ts +6 -0
  15. package/dist/types/peek-file.d.ts +2 -3
  16. package/dist/types/postmortem.d.ts +26 -5
  17. package/dist/types/procmgr.d.ts +2 -4
  18. package/dist/types/snowflake.d.ts +1 -0
  19. package/dist/types/sqlite.d.ts +27 -7
  20. package/dist/types/stream.d.ts +76 -1
  21. package/dist/types/which.d.ts +8 -2
  22. package/dist/types/yaml-config.d.ts +2 -0
  23. package/package.json +2 -2
  24. package/src/acp/transport.ts +37 -3
  25. package/src/async.ts +31 -0
  26. package/src/browsers.ts +46 -191
  27. package/src/color.ts +1 -1
  28. package/src/dirs.ts +34 -6
  29. package/src/env.ts +81 -34
  30. package/src/executable.ts +17 -0
  31. package/src/fetch-retry.ts +72 -13
  32. package/src/file-lock.ts +36 -2
  33. package/src/format.ts +16 -0
  34. package/src/index.ts +2 -1
  35. package/src/json.ts +12 -1
  36. package/src/logger/rotating-file.ts +40 -3
  37. package/src/logger.ts +16 -4
  38. package/src/mime.ts +3 -7
  39. package/src/path.ts +14 -0
  40. package/src/peek-file.ts +17 -67
  41. package/src/postmortem.ts +95 -47
  42. package/src/procmgr.ts +2 -12
  43. package/src/ptree.ts +36 -5
  44. package/src/snowflake.ts +12 -1
  45. package/src/sqlite.ts +242 -7
  46. package/src/stream.ts +167 -35
  47. package/src/which.ts +38 -14
  48. package/src/xml.ts +16 -0
  49. package/src/yaml-config.ts +8 -0
  50. package/dist/types/glob.d.ts +0 -28
  51. package/src/glob.ts +0 -189
package/src/which.ts CHANGED
@@ -11,8 +11,7 @@
11
11
  import * as fs from "node:fs";
12
12
  import * as os from "node:os";
13
13
  import * as path from "node:path";
14
-
15
- type CacheKey = string | bigint | number;
14
+ import { isFullyQualifiedPath } from "./path";
16
15
 
17
16
  // Tools shipped by Xcode / Command Line Tools that callers actually look up.
18
17
  // Keeps the set small so darwinWhich can fast-reject non-Xcode commands without
@@ -148,12 +147,12 @@ function getMacosToolPaths(): Map<string, string> {
148
147
  }
149
148
 
150
149
  // Map: cache key -> resolved binary path or null (not found)
151
- const toolCache = new Map<CacheKey, string | null>();
150
+ const toolCache = new Map<string, string | null>();
152
151
 
153
152
  /**
154
153
  * Cache policy for which lookups.
155
154
  */
156
- export const enum WhichCachePolicy {
155
+ export enum WhichCachePolicy {
157
156
  /**
158
157
  * Use cached result if available.
159
158
  */
@@ -176,9 +175,15 @@ export const enum WhichCachePolicy {
176
175
  export interface WhichOptions extends Bun.WhichOptions {
177
176
  /**
178
177
  * Cache policy for the lookup.
179
- * Defaults to `WhichCachePolicy.Fresh`.
178
+ * Defaults to `WhichCachePolicy.Cached`.
180
179
  */
181
180
  cache?: WhichCachePolicy;
181
+ /**
182
+ * Only search absolute directory entries in PATH, ignoring relative entries
183
+ * (e.g. `.` or `./bin`) and empty components to prevent resolving against
184
+ * an untrusted working directory.
185
+ */
186
+ requireAbsolutePaths?: boolean;
182
187
  }
183
188
 
184
189
  // Darwin-specific "which" shim: consult Xcode/CLT toolchain directories after $PATH.
@@ -192,6 +197,15 @@ function darwinWhich(command: string, options?: Bun.WhichOptions): string | null
192
197
  return null;
193
198
  }
194
199
 
200
+ function filterAbsoluteSearchPath(rawPath: string | undefined): string | null {
201
+ if (!rawPath) return null;
202
+ const safePath = rawPath
203
+ .split(path.delimiter)
204
+ .filter(dir => dir.length > 0 && isFullyQualifiedPath(dir))
205
+ .join(path.delimiter);
206
+ return safePath || null;
207
+ }
208
+
195
209
  // Which function that incorporates Darwin Xcode logic if platform reports as 'darwin'.
196
210
  // Look `Bun.which` up per call rather than capturing it at import, so a `Bun.which`
197
211
  // stub installed later (the per-test seam) is honoured on every platform.
@@ -200,14 +214,14 @@ export const whichFresh =
200
214
  ? darwinWhich
201
215
  : (command: string, options?: Bun.WhichOptions): string | null => Bun.which(command, options);
202
216
 
203
- // Derive stable cache key from command and lookup options
204
- function cacheKey(command: string, options?: Bun.WhichOptions): CacheKey {
217
+ // Length-prefixed (command, cwd, PATH) tuple: exact, unlike the 64-bit hash
218
+ // chain it replaced, and the length prefixes keep `("ab", "c")` and
219
+ // `("a", "bc")` distinct without a separator that cwd/PATH could contain.
220
+ function cacheKey(command: string, options?: Bun.WhichOptions): string {
205
221
  if (!options) return command;
206
- if (!options.cwd && !options.PATH) return command;
207
- let h = Bun.hash(command);
208
- if (options.cwd) h = Bun.hash(options.cwd, h);
209
- if (options.PATH) h = Bun.hash(options.PATH, h);
210
- return h;
222
+ const cwd = options?.cwd ?? "";
223
+ const binPath = options?.PATH ?? "";
224
+ return `${command.length}:${command}${cwd.length}:${cwd}${binPath.length}:${binPath}`;
211
225
  }
212
226
 
213
227
  /**
@@ -219,9 +233,16 @@ function cacheKey(command: string, options?: Bun.WhichOptions): CacheKey {
219
233
  */
220
234
  export function $which(command: string, options?: WhichOptions): string | null {
221
235
  const cachePolicy = options?.cache ?? WhichCachePolicy.Cached;
222
- const lookupOptions =
236
+ let lookupOptions =
223
237
  options?.PATH !== undefined || process.env.PATH === undefined ? options : { ...options, PATH: process.env.PATH };
224
- let key: CacheKey | undefined;
238
+
239
+ if (options?.requireAbsolutePaths) {
240
+ const safePath = filterAbsoluteSearchPath(lookupOptions?.PATH);
241
+ if (!safePath) return null;
242
+ lookupOptions = { ...lookupOptions, PATH: safePath };
243
+ }
244
+
245
+ let key: string | undefined;
225
246
 
226
247
  if (cachePolicy !== WhichCachePolicy.Bypass) {
227
248
  key = cacheKey(command, lookupOptions);
@@ -232,6 +253,9 @@ export function $which(command: string, options?: WhichOptions): string | null {
232
253
  }
233
254
 
234
255
  const result = whichFresh(command, lookupOptions);
256
+ if (result && options?.requireAbsolutePaths && !isFullyQualifiedPath(result)) {
257
+ return null;
258
+ }
235
259
  if (key != null && cachePolicy !== WhichCachePolicy.ReadOnly) {
236
260
  toolCache.set(key, result);
237
261
  }
package/src/xml.ts CHANGED
@@ -104,6 +104,12 @@ class XmlReader {
104
104
  this.#readProcessingInstruction(document);
105
105
  continue;
106
106
  }
107
+ if (this.#xml.startsWith("</", this.#position)) {
108
+ // Stray end tag with no element open: skip it instead of parsing it as a new element,
109
+ // whose empty name would leave #readElement stuck on the `/`.
110
+ this.#skipThrough(">");
111
+ continue;
112
+ }
107
113
  if (this.#xml[this.#position] === "<") {
108
114
  const element = this.#readElement("");
109
115
  this.#addValue(document, element.name, element.name, element.value, element.leaf, null);
@@ -138,6 +144,11 @@ class XmlReader {
138
144
  this.#position++;
139
145
  this.#skipWhitespace();
140
146
  value = this.#readAttributeValue();
147
+ } else if (attributeName === "") {
148
+ // Markup #readName cannot consume (the stray `/` in `<a / >`): skip the character
149
+ // so the loop always makes progress.
150
+ this.#position++;
151
+ continue;
141
152
  }
142
153
  attributes.push([attributeName, value]);
143
154
  }
@@ -153,6 +164,11 @@ class XmlReader {
153
164
  this.#position = close < 0 ? this.#xml.length : close + 1;
154
165
  break;
155
166
  }
167
+ if (this.#xml.startsWith("</", this.#position)) {
168
+ // End tag for an ancestor (an unclosed `<br>` inside `<p>`, or plain mismatched
169
+ // markup): close this element implicitly and leave the tag to its owner.
170
+ break;
171
+ }
156
172
  if (this.#xml.startsWith("<![CDATA[", this.#position)) {
157
173
  const end = this.#xml.indexOf("]]>", this.#position + 9);
158
174
  const raw = this.#xml.slice(this.#position + 9, end < 0 ? this.#xml.length : end);
@@ -0,0 +1,8 @@
1
+ import { YAML } from "bun";
2
+
3
+ const YAML_MAPPING_HEADER_TRAILING_SPACE = /: +$/gm;
4
+
5
+ /** Serialize config YAML without Bun's trailing space on block mapping headers. */
6
+ export function stringifyYamlConfig(value: unknown): string {
7
+ return YAML.stringify(value, null, 2).replace(YAML_MAPPING_HEADER_TRAILING_SPACE, ":");
8
+ }
@@ -1,28 +0,0 @@
1
- export interface GlobPathsOptions {
2
- /** Base directory for glob patterns. Defaults to getProjectDir(). */
3
- cwd?: string;
4
- /** Glob exclusion patterns. */
5
- exclude?: string[];
6
- /** Abort signal to cancel the glob. */
7
- signal?: AbortSignal;
8
- /** Timeout in milliseconds for the glob operation. */
9
- timeoutMs?: number;
10
- /** Include dotfiles when true. */
11
- dot?: boolean;
12
- /** Only return files (skip directories). Default: true. */
13
- onlyFiles?: boolean;
14
- /** Respect .gitignore files when true. Walks up directory tree to find all applicable .gitignore files. */
15
- gitignore?: boolean;
16
- }
17
- /**
18
- * Load .gitignore patterns from a directory and its parents.
19
- * Walks up the directory tree to find all applicable .gitignore files.
20
- * Returns glob-compatible exclude patterns.
21
- */
22
- export declare function loadGitignorePatterns(baseDir: string): Promise<string[]>;
23
- /**
24
- * Resolve filesystem paths matching glob patterns with optional exclude filters.
25
- * Returns paths relative to the provided cwd (or getProjectDir()).
26
- * Errors and abort/timeouts are surfaced to the caller.
27
- */
28
- export declare function globPaths(patterns: string | string[], options?: GlobPathsOptions): Promise<string[]>;
package/src/glob.ts DELETED
@@ -1,189 +0,0 @@
1
- import * as path from "node:path";
2
- import { Glob } from "bun";
3
- import { getProjectDir } from "./dirs";
4
-
5
- export interface GlobPathsOptions {
6
- /** Base directory for glob patterns. Defaults to getProjectDir(). */
7
- cwd?: string;
8
- /** Glob exclusion patterns. */
9
- exclude?: string[];
10
- /** Abort signal to cancel the glob. */
11
- signal?: AbortSignal;
12
- /** Timeout in milliseconds for the glob operation. */
13
- timeoutMs?: number;
14
- /** Include dotfiles when true. */
15
- dot?: boolean;
16
- /** Only return files (skip directories). Default: true. */
17
- onlyFiles?: boolean;
18
- /** Respect .gitignore files when true. Walks up directory tree to find all applicable .gitignore files. */
19
- gitignore?: boolean;
20
- }
21
-
22
- /** Patterns always excluded (.git is never useful in glob results). */
23
- const ALWAYS_IGNORED = ["**/.git", "**/.git/**"];
24
-
25
- /** node_modules exclusion patterns (skipped if pattern explicitly references node_modules). */
26
- const NODE_MODULES_IGNORED = ["**/node_modules", "**/node_modules/**"];
27
-
28
- /**
29
- * Parse a single .gitignore file and return glob-compatible exclude patterns.
30
- * @param content - Raw content of the .gitignore file
31
- * @param gitignoreDir - Absolute path to the directory containing the .gitignore
32
- * @param baseDir - Absolute path to the glob's cwd (for relativizing rooted patterns)
33
- */
34
- function parseGitignorePatterns(content: string, gitignoreDir: string, baseDir: string): string[] {
35
- const patterns: string[] = [];
36
-
37
- for (const rawLine of content.split("\n")) {
38
- const line = rawLine.trim();
39
- // Skip empty lines and comments
40
- if (!line || line.startsWith("#")) {
41
- continue;
42
- }
43
- // Skip negation patterns (unsupported for simple exclude)
44
- if (line.startsWith("!")) {
45
- continue;
46
- }
47
-
48
- let pattern = line;
49
-
50
- // Handle trailing slash (directory-only match)
51
- // For glob exclude, we treat it as matching the dir and its contents
52
- const isDirectoryOnly = pattern.endsWith("/");
53
- if (isDirectoryOnly) {
54
- pattern = pattern.slice(0, -1);
55
- }
56
-
57
- // Handle rooted patterns (start with /)
58
- if (pattern.startsWith("/")) {
59
- // Rooted pattern: relative to the .gitignore location
60
- const absolutePattern = path.join(gitignoreDir, pattern.slice(1));
61
- const relativeToBase = path.relative(baseDir, absolutePattern);
62
- if (relativeToBase.startsWith("..")) {
63
- // Pattern is outside the search directory, skip
64
- continue;
65
- }
66
- pattern = relativeToBase.replace(/\\/g, "/");
67
- if (isDirectoryOnly) {
68
- patterns.push(pattern);
69
- patterns.push(`${pattern}/**`);
70
- } else {
71
- patterns.push(pattern);
72
- }
73
- } else {
74
- // Unrooted pattern: match anywhere in the tree
75
- if (pattern.includes("/")) {
76
- // Contains slash: match from any directory level
77
- patterns.push(`**/${pattern}`);
78
- if (isDirectoryOnly) {
79
- patterns.push(`**/${pattern}/**`);
80
- }
81
- } else {
82
- // No slash: match file/dir name anywhere
83
- patterns.push(`**/${pattern}`);
84
- if (isDirectoryOnly) {
85
- patterns.push(`**/${pattern}/**`);
86
- }
87
- }
88
- }
89
- }
90
-
91
- return patterns;
92
- }
93
-
94
- /**
95
- * Load .gitignore patterns from a directory and its parents.
96
- * Walks up the directory tree to find all applicable .gitignore files.
97
- * Returns glob-compatible exclude patterns.
98
- */
99
- export async function loadGitignorePatterns(baseDir: string): Promise<string[]> {
100
- const patterns: string[] = [];
101
- const absoluteBase = path.resolve(baseDir);
102
-
103
- let current = absoluteBase;
104
- const maxDepth = 50; // Prevent infinite loops
105
-
106
- for (let i = 0; i < maxDepth; i++) {
107
- const gitignorePath = path.join(current, ".gitignore");
108
-
109
- try {
110
- const content = await Bun.file(gitignorePath).text();
111
- const filePatterns = parseGitignorePatterns(content, current, absoluteBase);
112
- patterns.push(...filePatterns);
113
- } catch {
114
- // .gitignore doesn't exist or can't be read, continue
115
- }
116
-
117
- const parent = path.dirname(current);
118
- if (parent === current) {
119
- // Reached filesystem root
120
- break;
121
- }
122
- current = parent;
123
- }
124
-
125
- return patterns;
126
- }
127
-
128
- /**
129
- * Resolve filesystem paths matching glob patterns with optional exclude filters.
130
- * Returns paths relative to the provided cwd (or getProjectDir()).
131
- * Errors and abort/timeouts are surfaced to the caller.
132
- */
133
- export async function globPaths(patterns: string | string[], options: GlobPathsOptions = {}): Promise<string[]> {
134
- const { cwd, exclude, signal, timeoutMs, dot, onlyFiles = true, gitignore } = options;
135
-
136
- // Build exclude list: always exclude .git, exclude node_modules unless pattern references it
137
- const patternArray = Array.isArray(patterns) ? patterns : [patterns];
138
- const mentionsNodeModules = patternArray.some(p => p.includes("node_modules"));
139
-
140
- const baseExclude = mentionsNodeModules ? [...ALWAYS_IGNORED] : [...ALWAYS_IGNORED, ...NODE_MODULES_IGNORED];
141
- let effectiveExclude = exclude ? [...baseExclude, ...exclude] : baseExclude;
142
-
143
- if (gitignore) {
144
- const gitignorePatterns = await loadGitignorePatterns(cwd ?? getProjectDir());
145
- effectiveExclude = [...effectiveExclude, ...gitignorePatterns];
146
- }
147
-
148
- const base = cwd ?? getProjectDir();
149
- const allResults: string[] = [];
150
-
151
- // Combine timeout and abort signals
152
- const timeoutSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
153
- const combinedSignal =
154
- signal && timeoutSignal ? AbortSignal.any([signal, timeoutSignal]) : (signal ?? timeoutSignal);
155
-
156
- for (const pattern of patternArray) {
157
- const glob = new Glob(pattern);
158
- const scanOptions = {
159
- cwd: base,
160
- dot,
161
- onlyFiles,
162
- throwErrorOnBrokenSymlink: false,
163
- };
164
-
165
- for await (const entry of glob.scan(scanOptions)) {
166
- if (combinedSignal?.aborted) {
167
- const reason = combinedSignal.reason;
168
- if (reason instanceof Error) throw reason;
169
- throw new DOMException("Aborted", "AbortError");
170
- }
171
-
172
- // Check exclusion patterns
173
- const normalized = entry.replace(/\\/g, "/");
174
- let excluded = false;
175
- for (const excludePattern of effectiveExclude) {
176
- const excludeGlob = new Glob(excludePattern);
177
- if (excludeGlob.match(normalized)) {
178
- excluded = true;
179
- break;
180
- }
181
- }
182
- if (!excluded) {
183
- allResults.push(normalized);
184
- }
185
- }
186
- }
187
-
188
- return allResults;
189
- }