@oh-my-pi/pi-utils 18.11.3 → 18.12.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.
- package/CHANGELOG.md +10 -0
- package/dist/types/sanitize-text.d.ts +11 -0
- package/package.json +2 -2
- package/src/ar/open.ts +2 -1
- package/src/fetch-retry.ts +5 -1
- package/src/sanitize-text.ts +70 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.12.0] - 2026-10-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `sanitizeTextDeep()`, which applies `sanitizeText()` to every string in a JSON-shaped value without mutating it ([#15963](https://github.com/can1357/oh-my-pi/issues/15963))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed Windows archive extraction overwriting existing files or following existing symlinks when copying a symlink entry without symlink privileges ([#15931](https://github.com/can1357/oh-my-pi/pull/15931) by [@jchanghong023](https://github.com/jchanghong023)).
|
|
14
|
+
|
|
5
15
|
## [18.11.3] - 2026-10-16
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
* string after the control probe.
|
|
13
13
|
*/
|
|
14
14
|
export declare function sanitizeText(text: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Apply {@link sanitizeText} to every string value inside a JSON-shaped value.
|
|
17
|
+
* Only plain objects and arrays are walked; class instances, typed arrays, and
|
|
18
|
+
* cyclic back-references pass through untouched. Object keys are kept as-is.
|
|
19
|
+
*
|
|
20
|
+
* Returns the input reference when nothing changed, so reference-keyed caches
|
|
21
|
+
* stay warm; changed containers are copied and the input is never mutated.
|
|
22
|
+
* The iterative walk also handles arbitrarily deep tool-result `details`
|
|
23
|
+
* without exhausting the TUI's call stack.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sanitizeTextDeep(value: unknown): unknown;
|
|
15
26
|
/**
|
|
16
27
|
* Escape the three XML-significant characters (`&`, `<`, `>`) in text destined
|
|
17
28
|
* for an XML/markup element body. Allocation-conscious: returns the input
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.12.1",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@oh-my-pi/pi-natives": "18.
|
|
60
|
+
"@oh-my-pi/pi-natives": "18.12.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14"
|
package/src/ar/open.ts
CHANGED
|
@@ -234,7 +234,8 @@ export async function extractArchive(
|
|
|
234
234
|
// privilege; skip the entry so the rest still extracts.
|
|
235
235
|
const extracted = await archive.readFile(link.path).catch(() => undefined);
|
|
236
236
|
if (!extracted) continue;
|
|
237
|
-
|
|
237
|
+
// Keep symlink creation's exclusive destination semantics.
|
|
238
|
+
await fs.writeFile(outputPath, extracted.bytes, { flag: "wx" });
|
|
238
239
|
const permissions = (extracted.mode ?? 0) & 0o777;
|
|
239
240
|
if (permissions) await fs.chmod(outputPath, permissions);
|
|
240
241
|
}
|
package/src/fetch-retry.ts
CHANGED
|
@@ -416,7 +416,11 @@ function wrapNetworkError(error: unknown): Error {
|
|
|
416
416
|
return new Error("Request was aborted");
|
|
417
417
|
}
|
|
418
418
|
if (error.message === "fetch failed" && error.cause instanceof Error) {
|
|
419
|
-
|
|
419
|
+
// Keep the original rejection as `cause`: undici nests the errno under
|
|
420
|
+
// `fetch failed` (often through an AggregateError), and
|
|
421
|
+
// classifyNetworkCause (packages/ai/src/error/network-cause.ts) walks
|
|
422
|
+
// `cause`/`errors` off this error. Flattening to text alone loses it.
|
|
423
|
+
return new Error(`Network error: ${error.cause.message}`, { cause: error });
|
|
420
424
|
}
|
|
421
425
|
return error;
|
|
422
426
|
}
|
package/src/sanitize-text.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* string after the control probe.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { isRecord } from "./type-guards";
|
|
16
|
+
|
|
15
17
|
const ESC_CHAR = "\x1b";
|
|
16
18
|
|
|
17
19
|
// Well-formed strings only need control/ANSI detection: C0 (excl. \t \n),
|
|
@@ -37,6 +39,74 @@ function sanitizeWellFormedText(text: string): string {
|
|
|
37
39
|
return stripped.replace(CONTROL_RE, "");
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Apply {@link sanitizeText} to every string value inside a JSON-shaped value.
|
|
44
|
+
* Only plain objects and arrays are walked; class instances, typed arrays, and
|
|
45
|
+
* cyclic back-references pass through untouched. Object keys are kept as-is.
|
|
46
|
+
*
|
|
47
|
+
* Returns the input reference when nothing changed, so reference-keyed caches
|
|
48
|
+
* stay warm; changed containers are copied and the input is never mutated.
|
|
49
|
+
* The iterative walk also handles arbitrarily deep tool-result `details`
|
|
50
|
+
* without exhausting the TUI's call stack.
|
|
51
|
+
*/
|
|
52
|
+
export function sanitizeTextDeep(value: unknown): unknown {
|
|
53
|
+
if (typeof value === "string") return sanitizeText(value);
|
|
54
|
+
const root = sanitizerFrame(value);
|
|
55
|
+
if (!root) return value;
|
|
56
|
+
const stack: SanitizerFrame[] = [root];
|
|
57
|
+
const ancestors = new Set<object>([root.value]);
|
|
58
|
+
while (true) {
|
|
59
|
+
const frame = stack[stack.length - 1]!;
|
|
60
|
+
const length = frame.kind === "array" ? frame.value.length : frame.keys.length;
|
|
61
|
+
if (frame.index === length) {
|
|
62
|
+
const clean = frame.copy ?? frame.value;
|
|
63
|
+
ancestors.delete(frame.value);
|
|
64
|
+
stack.pop();
|
|
65
|
+
if (stack.length === 0) return clean;
|
|
66
|
+
acceptSanitizedValue(stack[stack.length - 1]!, clean);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const child = frame.kind === "array" ? frame.value[frame.index++] : frame.value[frame.keys[frame.index++]!];
|
|
70
|
+
if (typeof child === "string") {
|
|
71
|
+
acceptSanitizedValue(frame, sanitizeText(child));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (child === null || typeof child !== "object" || ancestors.has(child)) continue;
|
|
75
|
+
const next = sanitizerFrame(child);
|
|
76
|
+
if (!next) continue;
|
|
77
|
+
ancestors.add(child);
|
|
78
|
+
stack.push(next);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type SanitizerFrame =
|
|
83
|
+
| { kind: "array"; value: unknown[]; index: number; copy?: unknown[] }
|
|
84
|
+
| { kind: "record"; value: Record<string, unknown>; keys: string[]; index: number; copy?: Record<string, unknown> };
|
|
85
|
+
|
|
86
|
+
function sanitizerFrame(value: unknown): SanitizerFrame | undefined {
|
|
87
|
+
if (Array.isArray(value)) return { kind: "array", value, index: 0 };
|
|
88
|
+
if (!isRecord(value)) return undefined;
|
|
89
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
90
|
+
if (proto !== Object.prototype && proto !== null) return undefined;
|
|
91
|
+
return { kind: "record", value, keys: Object.keys(value), index: 0 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function acceptSanitizedValue(frame: SanitizerFrame, clean: unknown): void {
|
|
95
|
+
if (frame.kind === "array") {
|
|
96
|
+
const index = frame.index - 1;
|
|
97
|
+
if (clean !== frame.value[index]) (frame.copy ??= frame.value.slice())[index] = clean;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const key = frame.keys[frame.index - 1]!;
|
|
101
|
+
if (clean === frame.value[key]) return;
|
|
102
|
+
const copy = (frame.copy ??= { ...frame.value });
|
|
103
|
+
if (key === "__proto__") {
|
|
104
|
+
Object.defineProperty(copy, key, { value: clean, writable: true, enumerable: true, configurable: true });
|
|
105
|
+
} else {
|
|
106
|
+
copy[key] = clean;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
40
110
|
/**
|
|
41
111
|
* Escape the three XML-significant characters (`&`, `<`, `>`) in text destined
|
|
42
112
|
* for an XML/markup element body. Allocation-conscious: returns the input
|