@gajae-code/utils 0.17.2 → 0.17.4
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/dist/types/canonical-log-dir.d.ts +20 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/crash-fingerprint.d.ts +9 -0
- package/dist/types/error-classification.d.ts +12 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/postmortem.d.ts +2 -0
- package/dist/types/sanitize-text.d.ts +2 -1
- package/package.json +2 -2
- package/src/canonical-log-dir.ts +59 -0
- package/src/cli.ts +15 -0
- package/src/crash-fingerprint.ts +30 -6
- package/src/dirs.ts +13 -1
- package/src/error-classification.ts +31 -0
- package/src/index.ts +1 -0
- package/src/postmortem.ts +18 -4
- package/src/sanitize-text.ts +4 -2
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface ProjectEnvSnapshotLike {
|
|
2
|
+
values: Record<string, string>;
|
|
3
|
+
dynamic: Set<string>;
|
|
4
|
+
}
|
|
5
|
+
interface CanonicalLogDirInput {
|
|
6
|
+
home: string;
|
|
7
|
+
env: Record<string, string | undefined>;
|
|
8
|
+
projectEnv: ProjectEnvSnapshotLike;
|
|
9
|
+
xdgEligible: boolean;
|
|
10
|
+
pathExists?: (target: string) => boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the canonical user log directory without mutating the environment.
|
|
14
|
+
*
|
|
15
|
+
* The caller supplies filesystem existence checks so this leaf stays free of
|
|
16
|
+
* filesystem and resolver side effects. Both the production directory resolver
|
|
17
|
+
* and the test preload use this exact path selection logic.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveCanonicalLogsDir(input: CanonicalLogDirInput): string;
|
|
20
|
+
export {};
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -111,6 +111,14 @@ export interface CommandEntry {
|
|
|
111
111
|
name: string;
|
|
112
112
|
load: () => Promise<CommandCtor>;
|
|
113
113
|
aliases?: string[];
|
|
114
|
+
/** Owns parsing, help, loading and failures for this family when supplied. */
|
|
115
|
+
dispatch?: (argv: string[], context: CommandEntryContext) => Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
export interface CommandEntryContext {
|
|
118
|
+
bin: string;
|
|
119
|
+
version: string;
|
|
120
|
+
/** Canonical registered entry name, including when invoked through an alias. */
|
|
121
|
+
command: string;
|
|
114
122
|
}
|
|
115
123
|
export interface RunOptions {
|
|
116
124
|
bin: string;
|
|
@@ -59,6 +59,15 @@ export declare function normalizeCrashMessage(message: string, options?: CrashFi
|
|
|
59
59
|
export declare function normalizeCrashFrames(stack: string, options?: CrashFingerprintOptions): string[];
|
|
60
60
|
/** Compute the v1 fingerprint of an already-captured fatal diagnostic. */
|
|
61
61
|
export declare function computeCrashFingerprint(input: CrashFingerprintInput, options?: CrashFingerprintOptions): CrashFingerprint;
|
|
62
|
+
/**
|
|
63
|
+
* Compute the stable identity used for a handled tool error.
|
|
64
|
+
*
|
|
65
|
+
* Tool failures often carry command output or other per-occurrence detail in
|
|
66
|
+
* their message. That text is useful in the record body but is not the failure
|
|
67
|
+
* identity: handled errors group by error class and the first in-app frame
|
|
68
|
+
* where the failure originated, rather than the full wrapper stack.
|
|
69
|
+
*/
|
|
70
|
+
export declare function computeHandledErrorFingerprint(input: CrashFingerprintInput, options?: CrashFingerprintOptions): CrashFingerprint;
|
|
62
71
|
/** The machine-readable identity line appended to every new crash record. */
|
|
63
72
|
export declare function formatCrashRecordMarker(fingerprint: string, version: number, recordId: string): string;
|
|
64
73
|
export interface CrashRecordMarker {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-package classification for errors that describe an expected outcome
|
|
3
|
+
* rather than an unexpected failure.
|
|
4
|
+
*
|
|
5
|
+
* The symbol is registered globally so the marker survives package duplication
|
|
6
|
+
* and worker/VM boundaries without relying on a particular Error constructor.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DESIGNED_ERROR: unique symbol;
|
|
9
|
+
/** Mark an Error as a designed outcome before it crosses package boundaries. */
|
|
10
|
+
export declare function markDesignedError<T extends Error>(error: T): T;
|
|
11
|
+
/** Return true when a throwable carries the trusted designed-outcome marker. */
|
|
12
|
+
export declare function isDesignedError(error: unknown): boolean;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* in response to process exit, signals, or fatal exceptions. It is intended to
|
|
6
6
|
* allow reliably releasing resources or shutting down subprocesses, files, sockets, etc.
|
|
7
7
|
*/
|
|
8
|
+
import { type CrashFingerprint } from "./crash-fingerprint";
|
|
8
9
|
import type { CrashProvenance } from "./crash-journal";
|
|
9
10
|
import { redactCrashSecrets } from "./crash-redaction";
|
|
10
11
|
export declare enum Reason {
|
|
@@ -63,6 +64,7 @@ interface CrashRecordOptions {
|
|
|
63
64
|
path?: string;
|
|
64
65
|
now?: Date;
|
|
65
66
|
provenance?: CrashProvenance;
|
|
67
|
+
fingerprint?: CrashFingerprint;
|
|
66
68
|
}
|
|
67
69
|
/**
|
|
68
70
|
* Register a process cleanup callback, to be run on shutdown, signal, or fatal error.
|
|
@@ -18,6 +18,7 @@ export declare function sanitizeText(text: string): string;
|
|
|
18
18
|
* {@link sanitizeText} deliberately preserves `\n`, and width-based truncation
|
|
19
19
|
* treats it as zero-width, so a value carrying line breaks can still inject
|
|
20
20
|
* extra rows and evade a single-line width budget. Flatten every CR/LF run to a
|
|
21
|
-
* single space before the usual control/ANSI strip
|
|
21
|
+
* single space before the usual control/ANSI strip, then remove directional
|
|
22
|
+
* format controls that can visually reorder an otherwise safe row.
|
|
22
23
|
*/
|
|
23
24
|
export declare function sanitizeDisplayLine(text: string): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/utils",
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.4",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@gajae-code/natives": "0.17.
|
|
34
|
+
"@gajae-code/natives": "0.17.4",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
interface ProjectEnvSnapshotLike {
|
|
4
|
+
values: Record<string, string>;
|
|
5
|
+
dynamic: Set<string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface CanonicalLogDirInput {
|
|
9
|
+
home: string;
|
|
10
|
+
env: Record<string, string | undefined>;
|
|
11
|
+
projectEnv: ProjectEnvSnapshotLike;
|
|
12
|
+
xdgEligible: boolean;
|
|
13
|
+
pathExists?: (target: string) => boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const APP_NAME = "gjc";
|
|
17
|
+
const DEFAULT_CONFIG_DIR_NAME = ".gjc";
|
|
18
|
+
|
|
19
|
+
function canonicalEnvKey(name: string): string {
|
|
20
|
+
return process.platform === "win32" ? name.toUpperCase() : name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sanitizeConfigDirName(value: string | undefined): string | undefined {
|
|
24
|
+
const trimmed = value?.trim();
|
|
25
|
+
if (!trimmed || path.normalize(trimmed).split(/[\\/]/).includes("..")) return undefined;
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve a caller environment value only when its dotenv provenance is trusted. */
|
|
30
|
+
function trustedValue(name: string, input: CanonicalLogDirInput): string | undefined {
|
|
31
|
+
const value = input.env[name];
|
|
32
|
+
if (!value) return undefined;
|
|
33
|
+
const key = canonicalEnvKey(name);
|
|
34
|
+
if (input.projectEnv.dynamic.has(key) || input.projectEnv.values[key] === value) return undefined;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the canonical user log directory without mutating the environment.
|
|
40
|
+
*
|
|
41
|
+
* The caller supplies filesystem existence checks so this leaf stays free of
|
|
42
|
+
* filesystem and resolver side effects. Both the production directory resolver
|
|
43
|
+
* and the test preload use this exact path selection logic.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveCanonicalLogsDir(input: CanonicalLogDirInput): string {
|
|
46
|
+
const configDirName =
|
|
47
|
+
sanitizeConfigDirName(trustedValue("GJC_CONFIG_DIR", input)) ??
|
|
48
|
+
sanitizeConfigDirName(trustedValue("PI_CONFIG_DIR", input)) ??
|
|
49
|
+
DEFAULT_CONFIG_DIR_NAME;
|
|
50
|
+
const xdgStateHome =
|
|
51
|
+
input.xdgEligible && (process.platform === "linux" || process.platform === "darwin")
|
|
52
|
+
? trustedValue("XDG_STATE_HOME", input)?.trim()
|
|
53
|
+
: undefined;
|
|
54
|
+
if (xdgStateHome) {
|
|
55
|
+
const xdgRoot = path.join(xdgStateHome, APP_NAME);
|
|
56
|
+
if (input.pathExists?.(xdgRoot) === true) return path.join(xdgRoot, "logs");
|
|
57
|
+
}
|
|
58
|
+
return path.join(input.home, configDirName, "logs");
|
|
59
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -394,6 +394,15 @@ export interface CommandEntry {
|
|
|
394
394
|
name: string;
|
|
395
395
|
load: () => Promise<CommandCtor>;
|
|
396
396
|
aliases?: string[];
|
|
397
|
+
/** Owns parsing, help, loading and failures for this family when supplied. */
|
|
398
|
+
dispatch?: (argv: string[], context: CommandEntryContext) => Promise<void>;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface CommandEntryContext {
|
|
402
|
+
bin: string;
|
|
403
|
+
version: string;
|
|
404
|
+
/** Canonical registered entry name, including when invoked through an alias. */
|
|
405
|
+
command: string;
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
export interface RunOptions {
|
|
@@ -439,6 +448,12 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
439
448
|
return;
|
|
440
449
|
}
|
|
441
450
|
|
|
451
|
+
const dispatchedEntry = findEntry(opts.commands, commandId);
|
|
452
|
+
if (dispatchedEntry?.dispatch) {
|
|
453
|
+
await dispatchedEntry.dispatch(commandArgv, { bin, version, command: dispatchedEntry.name });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
442
457
|
// Per-command help. Commands with nested subcommands can opt into receiving
|
|
443
458
|
// help flags themselves so `cmd subcommand --help` can render subcommand help.
|
|
444
459
|
const delimiterIndex = commandArgv.indexOf("--");
|
package/src/crash-fingerprint.ts
CHANGED
|
@@ -224,17 +224,18 @@ function canonicalSerialization(fields: readonly string[]): Buffer {
|
|
|
224
224
|
return Buffer.concat(parts);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
export function computeCrashFingerprint(
|
|
227
|
+
function computeFingerprint(
|
|
229
228
|
input: CrashFingerprintInput,
|
|
230
|
-
options: CrashFingerprintOptions
|
|
229
|
+
options: CrashFingerprintOptions,
|
|
230
|
+
includeMessage: boolean,
|
|
231
231
|
): CrashFingerprint {
|
|
232
232
|
const errorName = truncateUtf8(normalizeCrashMessage(input.name, options) || "Error", 128);
|
|
233
233
|
const messageClass = normalizeCrashMessage(input.message, options);
|
|
234
234
|
const frames = normalizeCrashFrames(input.stack, options);
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
.
|
|
235
|
+
const identity = includeMessage
|
|
236
|
+
? ["gjc-crash-fp.v1", errorName, messageClass, ...frames]
|
|
237
|
+
: ["gjc-crash-fp.v1", "handled", errorName, frames[0] ?? NO_APP_FRAME];
|
|
238
|
+
const digest = createHash("sha256").update(canonicalSerialization(identity)).digest();
|
|
238
239
|
return {
|
|
239
240
|
fingerprint: digest.subarray(0, CRASH_FINGERPRINT_HEX_LENGTH / 2).toString("hex"),
|
|
240
241
|
version: CRASH_FINGERPRINT_VERSION,
|
|
@@ -244,6 +245,29 @@ export function computeCrashFingerprint(
|
|
|
244
245
|
};
|
|
245
246
|
}
|
|
246
247
|
|
|
248
|
+
/** Compute the v1 fingerprint of an already-captured fatal diagnostic. */
|
|
249
|
+
export function computeCrashFingerprint(
|
|
250
|
+
input: CrashFingerprintInput,
|
|
251
|
+
options: CrashFingerprintOptions = {},
|
|
252
|
+
): CrashFingerprint {
|
|
253
|
+
return computeFingerprint(input, options, true);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Compute the stable identity used for a handled tool error.
|
|
258
|
+
*
|
|
259
|
+
* Tool failures often carry command output or other per-occurrence detail in
|
|
260
|
+
* their message. That text is useful in the record body but is not the failure
|
|
261
|
+
* identity: handled errors group by error class and the first in-app frame
|
|
262
|
+
* where the failure originated, rather than the full wrapper stack.
|
|
263
|
+
*/
|
|
264
|
+
export function computeHandledErrorFingerprint(
|
|
265
|
+
input: CrashFingerprintInput,
|
|
266
|
+
options: CrashFingerprintOptions = {},
|
|
267
|
+
): CrashFingerprint {
|
|
268
|
+
return computeFingerprint(input, options, false);
|
|
269
|
+
}
|
|
270
|
+
|
|
247
271
|
/** The machine-readable identity line appended to every new crash record. */
|
|
248
272
|
export function formatCrashRecordMarker(fingerprint: string, version: number, recordId: string): string {
|
|
249
273
|
return `${CRASH_RECORD_MARKER} fp:${fingerprint} fpv:${version} id:${recordId}`;
|
package/src/dirs.ts
CHANGED
|
@@ -16,6 +16,7 @@ import * as fs from "node:fs";
|
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import { engines, version } from "../package.json" with { type: "json" };
|
|
19
|
+
import { resolveCanonicalLogsDir } from "./canonical-log-dir";
|
|
19
20
|
import { canonicalEnvKey, type ProjectEnvSnapshot, projectEnvSnapshot } from "./env-file";
|
|
20
21
|
|
|
21
22
|
// The provenance snapshot and its key fold live in the leaf `env-file` module so
|
|
@@ -701,7 +702,18 @@ export function getReportsDir(): string {
|
|
|
701
702
|
|
|
702
703
|
/** Get the logs directory (~/.gjc/logs). */
|
|
703
704
|
export function getLogsDir(): string {
|
|
704
|
-
|
|
705
|
+
const home = getTrustedHomeDir();
|
|
706
|
+
return resolveCanonicalLogsDir({
|
|
707
|
+
home,
|
|
708
|
+
env: {
|
|
709
|
+
GJC_CONFIG_DIR: process.env.GJC_CONFIG_DIR,
|
|
710
|
+
PI_CONFIG_DIR: process.env.PI_CONFIG_DIR,
|
|
711
|
+
XDG_STATE_HOME: process.env.XDG_STATE_HOME,
|
|
712
|
+
},
|
|
713
|
+
projectEnv: dirs.trustSnapshot,
|
|
714
|
+
xdgEligible: dirs.profileAuthority === "default",
|
|
715
|
+
pathExists: fs.existsSync,
|
|
716
|
+
});
|
|
705
717
|
}
|
|
706
718
|
|
|
707
719
|
/** Dated log file name, shared by the canonical and effective log paths so they cannot drift. */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-package classification for errors that describe an expected outcome
|
|
3
|
+
* rather than an unexpected failure.
|
|
4
|
+
*
|
|
5
|
+
* The symbol is registered globally so the marker survives package duplication
|
|
6
|
+
* and worker/VM boundaries without relying on a particular Error constructor.
|
|
7
|
+
*/
|
|
8
|
+
export const DESIGNED_ERROR = Symbol.for("gajae-code.designed-error");
|
|
9
|
+
|
|
10
|
+
type MarkedError = object & { readonly [DESIGNED_ERROR]?: unknown };
|
|
11
|
+
|
|
12
|
+
/** Mark an Error as a designed outcome before it crosses package boundaries. */
|
|
13
|
+
export function markDesignedError<T extends Error>(error: T): T {
|
|
14
|
+
Object.defineProperty(error, DESIGNED_ERROR, {
|
|
15
|
+
configurable: false,
|
|
16
|
+
enumerable: false,
|
|
17
|
+
value: true,
|
|
18
|
+
writable: false,
|
|
19
|
+
});
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Return true when a throwable carries the trusted designed-outcome marker. */
|
|
24
|
+
export function isDesignedError(error: unknown): boolean {
|
|
25
|
+
if ((typeof error !== "object" && typeof error !== "function") || error === null) return false;
|
|
26
|
+
try {
|
|
27
|
+
return (error as MarkedError)[DESIGNED_ERROR] === true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
CHANGED
package/src/postmortem.ts
CHANGED
|
@@ -12,11 +12,17 @@ import inspector from "node:inspector";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { isMainThread } from "node:worker_threads";
|
|
14
14
|
import { BROKEN_PIPE_EXIT_CODE, createProcessStdoutEpipeClassifier } from "./broken-pipe";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type CrashFingerprint,
|
|
17
|
+
computeCrashFingerprint,
|
|
18
|
+
computeHandledErrorFingerprint,
|
|
19
|
+
formatCrashRecordMarker,
|
|
20
|
+
} from "./crash-fingerprint";
|
|
16
21
|
import type { CrashProvenance } from "./crash-journal";
|
|
17
22
|
import { appendCrashEvent, appendFatalCrashEvent, detectCrashProvenance } from "./crash-journal";
|
|
18
23
|
import { redactCrashSecrets } from "./crash-redaction";
|
|
19
24
|
import { getCrashEventsPath, getCrashLogPath, getHandledErrorEventsPath, getHandledErrorLogPath } from "./dirs";
|
|
25
|
+
import { isDesignedError } from "./error-classification";
|
|
20
26
|
import * as logger from "./logger";
|
|
21
27
|
import { safeStderrWrite } from "./safe-stderr";
|
|
22
28
|
|
|
@@ -512,9 +518,15 @@ export function recordHandledError(
|
|
|
512
518
|
options: HandledErrorRecordOptions = {},
|
|
513
519
|
): string | undefined {
|
|
514
520
|
try {
|
|
515
|
-
if (
|
|
521
|
+
if (
|
|
522
|
+
!(error instanceof Error) ||
|
|
523
|
+
isDesignedError(error) ||
|
|
524
|
+
typeof error.stack !== "string" ||
|
|
525
|
+
error.stack.length === 0
|
|
526
|
+
)
|
|
527
|
+
return undefined;
|
|
516
528
|
const fatal = describeFatal(error);
|
|
517
|
-
const fingerprint =
|
|
529
|
+
const fingerprint = computeHandledErrorFingerprint(fatal).fingerprint;
|
|
518
530
|
if (handledErrorFingerprints.has(fingerprint)) {
|
|
519
531
|
// Still hot: dedupe, but refresh recency so an actively failing class
|
|
520
532
|
// is not the one evicted under pressure.
|
|
@@ -530,6 +542,7 @@ export function recordHandledError(
|
|
|
530
542
|
const written = writeCrashRecord(label, fatal, {
|
|
531
543
|
path: options.path ?? getHandledErrorLogPath(),
|
|
532
544
|
now: options.now,
|
|
545
|
+
fingerprint: computeHandledErrorFingerprint(fatal),
|
|
533
546
|
});
|
|
534
547
|
if (!written) {
|
|
535
548
|
handledErrorFingerprints.delete(fingerprint);
|
|
@@ -562,6 +575,7 @@ interface CrashRecordOptions {
|
|
|
562
575
|
path?: string;
|
|
563
576
|
now?: Date;
|
|
564
577
|
provenance?: CrashProvenance;
|
|
578
|
+
fingerprint?: CrashFingerprint;
|
|
565
579
|
}
|
|
566
580
|
|
|
567
581
|
interface WrittenCrashRecord {
|
|
@@ -595,7 +609,7 @@ function writeCrashRecord(
|
|
|
595
609
|
const payload = fatal.payload ? `${redactCrashSecrets(fatal.payload)}\n` : "";
|
|
596
610
|
// Identity is computed from the already-captured diagnostic text only; the
|
|
597
611
|
// throwable is never read again here.
|
|
598
|
-
const fingerprint = computeCrashFingerprint(fatal);
|
|
612
|
+
const fingerprint = options.fingerprint ?? computeCrashFingerprint(fatal);
|
|
599
613
|
const recordId = randomBytes(8).toString("hex");
|
|
600
614
|
const markerLine = `${formatCrashRecordMarker(fingerprint.fingerprint, fingerprint.version, recordId)}\n`;
|
|
601
615
|
// The marker is the record's identity, so it is budgeted first and appended
|
package/src/sanitize-text.ts
CHANGED
|
@@ -17,6 +17,7 @@ const ESC_CHAR = "\x1b";
|
|
|
17
17
|
// Well-formed strings only need control/ANSI detection: C0 (excl. \t \n),
|
|
18
18
|
// CR, DEL, and C1. ESC (0x1B) is in \x0B-\x1F.
|
|
19
19
|
const CONTROL_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g;
|
|
20
|
+
const DISPLAY_FORMAT_RE = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu;
|
|
20
21
|
|
|
21
22
|
const REPLACEMENT_CHAR = "\ufffd";
|
|
22
23
|
|
|
@@ -34,10 +35,11 @@ export function sanitizeText(text: string): string {
|
|
|
34
35
|
* {@link sanitizeText} deliberately preserves `\n`, and width-based truncation
|
|
35
36
|
* treats it as zero-width, so a value carrying line breaks can still inject
|
|
36
37
|
* extra rows and evade a single-line width budget. Flatten every CR/LF run to a
|
|
37
|
-
* single space before the usual control/ANSI strip
|
|
38
|
+
* single space before the usual control/ANSI strip, then remove directional
|
|
39
|
+
* format controls that can visually reorder an otherwise safe row.
|
|
38
40
|
*/
|
|
39
41
|
export function sanitizeDisplayLine(text: string): string {
|
|
40
|
-
return sanitizeText(text.replace(/[\r\n]+/gu, " "));
|
|
42
|
+
return sanitizeText(text.replace(/[\r\n]+/gu, " ")).replace(DISPLAY_FORMAT_RE, "");
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
function sanitizeWellFormedText(text: string): string {
|