@gajae-code/utils 0.16.7 → 0.17.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/dist/types/dirs.d.ts +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/logger.d.ts +10 -8
- package/dist/types/safe-error.d.ts +20 -0
- package/package.json +2 -2
- package/src/crash-redaction.ts +12 -0
- package/src/dirs.ts +20 -1
- package/src/index.ts +1 -0
- package/src/logger.ts +23 -89
- package/src/safe-error.ts +40 -0
package/dist/types/dirs.d.ts
CHANGED
|
@@ -80,6 +80,13 @@ export declare function getTrustedConfigRootDir(): string;
|
|
|
80
80
|
* selection and resolve the same storage lane.
|
|
81
81
|
*/
|
|
82
82
|
export declare function setAgentDir(dir: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* Rebuild the resolver from the current trusted environment. Callers that
|
|
85
|
+
* temporarily used {@link setAgentDir} must first restore the original
|
|
86
|
+
* GJC_CODING_AGENT_DIR / PI_CODING_AGENT_DIR values, then call this function;
|
|
87
|
+
* an originally absent override remains absent and keeps following HOME.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resetAgentDirFromEnvironment(): void;
|
|
83
90
|
/** Get the agent config directory (~/.gjc/agent). */
|
|
84
91
|
export declare function getAgentDir(): string;
|
|
85
92
|
/** Resolver-owned profile classification, stable across HOME refreshes. */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * as procmgr from "./procmgr";
|
|
|
24
24
|
export * as prompt from "./prompt";
|
|
25
25
|
export * as ptree from "./ptree";
|
|
26
26
|
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
|
|
27
|
+
export * from "./safe-error";
|
|
27
28
|
export * from "./safe-stderr";
|
|
28
29
|
export * from "./sanitize-text";
|
|
29
30
|
export * from "./snowflake";
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -39,16 +39,18 @@ export declare function debug(message: string, context?: Record<string, unknown>
|
|
|
39
39
|
export declare function printTimings(): void;
|
|
40
40
|
/**
|
|
41
41
|
* Begin recording startup timings under a new root span.
|
|
42
|
-
* Idempotent: a second call while already recording is a no-op so that
|
|
43
|
-
*
|
|
42
|
+
* Idempotent: a second call while already recording is a no-op so that the
|
|
43
|
+
* early starter (cli.ts, the first CLI statement) and the explicit starter
|
|
44
|
+
* (main.ts) can coexist.
|
|
45
|
+
*
|
|
46
|
+
* The root is anchored at the process-start origin rather than at the call:
|
|
47
|
+
* `performance.now()` counts milliseconds since `performance.timeOrigin`
|
|
48
|
+
* (process start) on Bun, so `start: 0` makes `Total` the true
|
|
49
|
+
* time-since-process-start. Runtime bootstrap plus static module linking and
|
|
50
|
+
* evaluation then land in the root's self time instead of being invisible
|
|
51
|
+
* before the first statement of cli.ts.
|
|
44
52
|
*/
|
|
45
53
|
export declare function startTiming(): void;
|
|
46
|
-
/**
|
|
47
|
-
* Record an externally-measured span as a leaf child of the active span (or root
|
|
48
|
-
* when no span is active). Used by the module-load timing plugin to splice load
|
|
49
|
-
* events into the tree retroactively.
|
|
50
|
-
*/
|
|
51
|
-
export declare function recordModuleLoadSpan(path: string, start: number, durationMs: number): void;
|
|
52
54
|
/**
|
|
53
55
|
* End timing window and clear buffers.
|
|
54
56
|
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throw-proof error description.
|
|
3
|
+
*
|
|
4
|
+
* Hostile or exotic thrown values can make `String(value)` throw (a `toString`
|
|
5
|
+
* that throws, a revoked proxy, a null-prototype object) and a `message` getter
|
|
6
|
+
* can throw too. Logging or recording a failure must never become a second
|
|
7
|
+
* failure, so every failure path that stringifies a caught value uses this.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { safeErrorDescription } from "@gajae-code/utils";
|
|
12
|
+
*
|
|
13
|
+
* try {
|
|
14
|
+
* await risky();
|
|
15
|
+
* } catch (error) {
|
|
16
|
+
* logger.warn("risky failed", { error: safeErrorDescription(error) });
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function safeErrorDescription(value: unknown): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/utils",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.1",
|
|
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.
|
|
34
|
+
"@gajae-code/natives": "0.17.1",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/crash-redaction.ts
CHANGED
|
@@ -25,6 +25,16 @@ export function redactCrashSecrets(text: string): string {
|
|
|
25
25
|
redacted = redacted.replace(/\bgh[opsur]_[A-Za-z0-9]{16,}\b/g, "«redacted-github-token»");
|
|
26
26
|
redacted = redacted.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "«redacted-github-token»");
|
|
27
27
|
redacted = redacted.replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}\b/g, "«redacted-slack-token»");
|
|
28
|
+
// The five shapes below are the ones `crash/upstream/envelope.ts` already
|
|
29
|
+
// classifies as credential-like and refuses to transmit. That refusal only
|
|
30
|
+
// guards the Sentry frame fields; the persisted crash log and the
|
|
31
|
+
// `gjc crash report` body — which the user files as a public issue — reach
|
|
32
|
+
// egress through this function alone, so the same shapes have to be named here.
|
|
33
|
+
redacted = redacted.replace(/\bnpm_[A-Za-z0-9]{20,}\b/g, "«redacted-npm-token»");
|
|
34
|
+
redacted = redacted.replace(/\bglpat-[A-Za-z0-9_-]{20,}\b/g, "«redacted-gitlab-token»");
|
|
35
|
+
// Stripe separates with `_`, so the `sk-` rule above never matched one.
|
|
36
|
+
redacted = redacted.replace(/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, "«redacted-api-key»");
|
|
37
|
+
redacted = redacted.replace(/\bhf_[A-Za-z0-9]{20,}\b/g, "«redacted-api-key»");
|
|
28
38
|
// A PEM block carries the key material itself, so it is redacted whole rather
|
|
29
39
|
// than line by line. It runs before the narrower rules because they would
|
|
30
40
|
// otherwise chew on the base64 body and leave a truncated key behind.
|
|
@@ -70,6 +80,8 @@ export const CRASH_REDACTION_MARKERS: readonly string[] = [
|
|
|
70
80
|
"«redacted-api-key»",
|
|
71
81
|
"«redacted-github-token»",
|
|
72
82
|
"«redacted-slack-token»",
|
|
83
|
+
"«redacted-npm-token»",
|
|
84
|
+
"«redacted-gitlab-token»",
|
|
73
85
|
"«redacted-aws-key»",
|
|
74
86
|
"«redacted-private-key»",
|
|
75
87
|
"«redacted-google-api-key»",
|
package/src/dirs.ts
CHANGED
|
@@ -673,6 +673,18 @@ export function setAgentDir(dir: string): void {
|
|
|
673
673
|
process.env.GJC_CODING_AGENT_DIR = dir;
|
|
674
674
|
}
|
|
675
675
|
|
|
676
|
+
/**
|
|
677
|
+
* Rebuild the resolver from the current trusted environment. Callers that
|
|
678
|
+
* temporarily used {@link setAgentDir} must first restore the original
|
|
679
|
+
* GJC_CODING_AGENT_DIR / PI_CODING_AGENT_DIR values, then call this function;
|
|
680
|
+
* an originally absent override remains absent and keeps following HOME.
|
|
681
|
+
*/
|
|
682
|
+
export function resetAgentDirFromEnvironment(): void {
|
|
683
|
+
const snapshot = dirs.trustSnapshot;
|
|
684
|
+
const override = trustedValue("GJC_CODING_AGENT_DIR", snapshot) ?? trustedValue("PI_CODING_AGENT_DIR", snapshot);
|
|
685
|
+
dirs = new DirResolver(override, snapshot);
|
|
686
|
+
}
|
|
687
|
+
|
|
676
688
|
/** Get the agent config directory (~/.gjc/agent). */
|
|
677
689
|
export function getAgentDir(): string {
|
|
678
690
|
dirs.refreshConfigDirOverride();
|
|
@@ -743,7 +755,14 @@ export function getPluginsDir(home?: string): string {
|
|
|
743
755
|
if (home !== undefined) {
|
|
744
756
|
const explicitPath = () => path.join(home, resolveConfigDirName(dirs.trustSnapshot), "plugins");
|
|
745
757
|
try {
|
|
746
|
-
|
|
758
|
+
const canonical = (value: string): string => {
|
|
759
|
+
try {
|
|
760
|
+
return fs.realpathSync.native(value);
|
|
761
|
+
} catch {
|
|
762
|
+
return path.resolve(value);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
if (canonical(home) !== canonical(dirs.trustedHome)) return explicitPath();
|
|
747
766
|
} catch {
|
|
748
767
|
// An explicit home is the caller's documented escape hatch. If the
|
|
749
768
|
// authoritative home is unavailable, do not let its fail-closed resolver
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * as procmgr from "./procmgr";
|
|
|
24
24
|
export * as prompt from "./prompt";
|
|
25
25
|
export * as ptree from "./ptree";
|
|
26
26
|
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
|
|
27
|
+
export * from "./safe-error";
|
|
27
28
|
export * from "./safe-stderr";
|
|
28
29
|
export * from "./sanitize-text";
|
|
29
30
|
export * from "./snowflake";
|
package/src/logger.ts
CHANGED
|
@@ -94,6 +94,7 @@ function applyTransports(
|
|
|
94
94
|
modules: { winston: WinstonModule; DailyRotateFile: DailyRotateFileCtor },
|
|
95
95
|
): void {
|
|
96
96
|
logger.clear();
|
|
97
|
+
logger.silent = !transportOptions.console && !transportOptions.file;
|
|
97
98
|
if (transportOptions.file) {
|
|
98
99
|
logger.add(
|
|
99
100
|
makeFileTransport(
|
|
@@ -249,21 +250,21 @@ export function printTimings(): void {
|
|
|
249
250
|
}
|
|
250
251
|
|
|
251
252
|
gRootSpan.end = performance.now();
|
|
253
|
+
// Close still-open spans (e.g. cli:dispatch, which wraps the very run that
|
|
254
|
+
// prints) so they report elapsed-at-print instead of a misleading 0ms.
|
|
255
|
+
const root = gRootSpan;
|
|
256
|
+
const printNow = root.end;
|
|
257
|
+
const closeOpenSpans = (span: Span): void => {
|
|
258
|
+
for (const child of span.children) closeOpenSpans(child);
|
|
259
|
+
if (span.end === undefined && !span.point) span.end = printNow;
|
|
260
|
+
};
|
|
261
|
+
closeOpenSpans(root);
|
|
252
262
|
const lines: string[] = [];
|
|
253
263
|
lines.push("");
|
|
254
264
|
lines.push("--- Startup timings (hierarchical) ---");
|
|
255
|
-
const
|
|
256
|
-
const loads: Span[] = [];
|
|
257
|
-
for (const child of gRootSpan.children) {
|
|
258
|
-
if (isModuleLoadSpan(child)) loads.push(child);
|
|
259
|
-
else work.push(child);
|
|
260
|
-
}
|
|
261
|
-
for (const child of work.sort((a, b) => a.start - b.start)) {
|
|
265
|
+
for (const child of [...gRootSpan.children].sort((a, b) => a.start - b.start)) {
|
|
262
266
|
printSpan(child, 0, lines);
|
|
263
267
|
}
|
|
264
|
-
if (loads.length > 0) {
|
|
265
|
-
printModuleLoadSummary(loads, 0, lines);
|
|
266
|
-
}
|
|
267
268
|
const totalMs = (gRootSpan.end - gRootSpan.start).toFixed(1);
|
|
268
269
|
lines.push(`Total: ${totalMs}ms`);
|
|
269
270
|
lines.push("--------------------------------------");
|
|
@@ -274,46 +275,28 @@ export function printTimings(): void {
|
|
|
274
275
|
|
|
275
276
|
/**
|
|
276
277
|
* Begin recording startup timings under a new root span.
|
|
277
|
-
* Idempotent: a second call while already recording is a no-op so that
|
|
278
|
-
*
|
|
278
|
+
* Idempotent: a second call while already recording is a no-op so that the
|
|
279
|
+
* early starter (cli.ts, the first CLI statement) and the explicit starter
|
|
280
|
+
* (main.ts) can coexist.
|
|
281
|
+
*
|
|
282
|
+
* The root is anchored at the process-start origin rather than at the call:
|
|
283
|
+
* `performance.now()` counts milliseconds since `performance.timeOrigin`
|
|
284
|
+
* (process start) on Bun, so `start: 0` makes `Total` the true
|
|
285
|
+
* time-since-process-start. Runtime bootstrap plus static module linking and
|
|
286
|
+
* evaluation then land in the root's self time instead of being invisible
|
|
287
|
+
* before the first statement of cli.ts.
|
|
279
288
|
*/
|
|
280
289
|
export function startTiming(): void {
|
|
281
290
|
if (gRecordTimings) return;
|
|
282
291
|
gRootSpan = {
|
|
283
292
|
op: "(root)",
|
|
284
|
-
start:
|
|
293
|
+
start: 0,
|
|
285
294
|
parent: undefined,
|
|
286
295
|
children: [],
|
|
287
296
|
};
|
|
288
297
|
gRecordTimings = true;
|
|
289
298
|
}
|
|
290
299
|
|
|
291
|
-
/**
|
|
292
|
-
* Record an externally-measured span as a leaf child of the active span (or root
|
|
293
|
-
* when no span is active). Used by the module-load timing plugin to splice load
|
|
294
|
-
* events into the tree retroactively.
|
|
295
|
-
*/
|
|
296
|
-
export function recordModuleLoadSpan(path: string, start: number, durationMs: number): void {
|
|
297
|
-
if (!gRecordTimings || !gRootSpan) return;
|
|
298
|
-
const parent = spanStorage.getStore() ?? gRootSpan;
|
|
299
|
-
const span: Span = {
|
|
300
|
-
op: `load:${shortenLoadPath(path)}`,
|
|
301
|
-
start,
|
|
302
|
-
end: start + durationMs,
|
|
303
|
-
parent,
|
|
304
|
-
children: [],
|
|
305
|
-
};
|
|
306
|
-
parent.children.push(span);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function shortenLoadPath(p: string): string {
|
|
310
|
-
const cwd = process.cwd();
|
|
311
|
-
if (p.startsWith(`${cwd}/`)) return p.slice(cwd.length + 1);
|
|
312
|
-
const home = process.env.HOME;
|
|
313
|
-
if (home && p.startsWith(`${home}/`)) return `~/${p.slice(home.length + 1)}`;
|
|
314
|
-
return p;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
300
|
/**
|
|
318
301
|
* End timing window and clear buffers.
|
|
319
302
|
*/
|
|
@@ -359,13 +342,6 @@ function fmtMs(ms: number): string {
|
|
|
359
342
|
return `${ms.toFixed(0)}ms`;
|
|
360
343
|
}
|
|
361
344
|
|
|
362
|
-
const MODULE_LOAD_PREFIX = "load:";
|
|
363
|
-
const MODULE_LOAD_VERBOSE_TOP = 10;
|
|
364
|
-
|
|
365
|
-
function isModuleLoadSpan(span: Span): boolean {
|
|
366
|
-
return span.op.startsWith(MODULE_LOAD_PREFIX);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
345
|
function printSpan(span: Span, depth: number, lines: string[]): void {
|
|
370
346
|
const indent = " ".repeat(depth);
|
|
371
347
|
if (span.point) {
|
|
@@ -380,51 +356,9 @@ function printSpan(span: Span, depth: number, lines: string[]): void {
|
|
|
380
356
|
const selfStr = span.children.length > 0 && self > LOGGED_TIMING_THRESHOLD_MS ? ` (self ${fmtMs(self)})` : "";
|
|
381
357
|
lines.push(`${indent}${span.op}: ${fmtMs(dur)}${selfStr}${tag}`);
|
|
382
358
|
|
|
383
|
-
|
|
384
|
-
const work: Span[] = [];
|
|
385
|
-
const loads: Span[] = [];
|
|
386
|
-
for (const child of span.children) {
|
|
387
|
-
if (isModuleLoadSpan(child)) loads.push(child);
|
|
388
|
-
else work.push(child);
|
|
389
|
-
}
|
|
390
|
-
for (const child of work.sort((a, b) => a.start - b.start)) {
|
|
359
|
+
for (const child of [...span.children].sort((a, b) => a.start - b.start)) {
|
|
391
360
|
printSpan(child, depth + 1, lines);
|
|
392
361
|
}
|
|
393
|
-
if (loads.length > 0) {
|
|
394
|
-
printModuleLoadSummary(loads, depth + 1, lines);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/** Collapse the (typically hundreds of) module-load spans into one summary line. */
|
|
399
|
-
function printModuleLoadSummary(loads: Span[], depth: number, lines: string[]): void {
|
|
400
|
-
const childIndent = " ".repeat(depth);
|
|
401
|
-
const grandIndent = " ".repeat(depth + 1);
|
|
402
|
-
let unionStart = Number.POSITIVE_INFINITY;
|
|
403
|
-
let unionEnd = 0;
|
|
404
|
-
let totalSelf = 0;
|
|
405
|
-
for (const span of loads) {
|
|
406
|
-
if (span.end === undefined) continue;
|
|
407
|
-
if (span.start < unionStart) unionStart = span.start;
|
|
408
|
-
if (span.end > unionEnd) unionEnd = span.end;
|
|
409
|
-
totalSelf += span.end - span.start;
|
|
410
|
-
}
|
|
411
|
-
const wall = unionEnd > unionStart ? unionEnd - unionStart : 0;
|
|
412
|
-
lines.push(`${childIndent}(modules): ${loads.length} loaded, wall ${fmtMs(wall)}, sum ${fmtMs(totalSelf)}`);
|
|
413
|
-
// Resolve GJC-first with PI fallback inline (no ./env import) so this
|
|
414
|
-
// foundational logger stays off the env module's dependency graph, which the
|
|
415
|
-
// tab-worker native-free runtime contract (issue-2598-repro) walks.
|
|
416
|
-
const showAll = (process.env.GJC_TIMING?.trim() || process.env.PI_TIMING?.trim()) === "full";
|
|
417
|
-
const sorted = [...loads].sort((a, b) => durationOf(b) - durationOf(a));
|
|
418
|
-
const visible = showAll ? sorted : sorted.slice(0, MODULE_LOAD_VERBOSE_TOP);
|
|
419
|
-
for (const span of visible) {
|
|
420
|
-
const dur = durationOf(span);
|
|
421
|
-
if (dur < LOGGED_TIMING_THRESHOLD_MS) break;
|
|
422
|
-
const tag = isParallel(span) ? " [parallel]" : "";
|
|
423
|
-
lines.push(`${grandIndent}${span.op}: ${fmtMs(dur)}${tag}`);
|
|
424
|
-
}
|
|
425
|
-
if (!showAll && sorted.length > MODULE_LOAD_VERBOSE_TOP) {
|
|
426
|
-
lines.push(`${grandIndent}… ${sorted.length - MODULE_LOAD_VERBOSE_TOP} more (PI_TIMING=full to show all)`);
|
|
427
|
-
}
|
|
428
362
|
}
|
|
429
363
|
|
|
430
364
|
/** A span is parallel if it overlaps a sibling that started before it. */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throw-proof error description.
|
|
3
|
+
*
|
|
4
|
+
* Hostile or exotic thrown values can make `String(value)` throw (a `toString`
|
|
5
|
+
* that throws, a revoked proxy, a null-prototype object) and a `message` getter
|
|
6
|
+
* can throw too. Logging or recording a failure must never become a second
|
|
7
|
+
* failure, so every failure path that stringifies a caught value uses this.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { safeErrorDescription } from "@gajae-code/utils";
|
|
12
|
+
*
|
|
13
|
+
* try {
|
|
14
|
+
* await risky();
|
|
15
|
+
* } catch (error) {
|
|
16
|
+
* logger.warn("risky failed", { error: safeErrorDescription(error) });
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function safeErrorDescription(value: unknown): string {
|
|
21
|
+
let isError = false;
|
|
22
|
+
try {
|
|
23
|
+
isError = value instanceof Error;
|
|
24
|
+
} catch {
|
|
25
|
+
// Hostile proxies can throw from getPrototypeOf during instanceof.
|
|
26
|
+
}
|
|
27
|
+
if (isError) {
|
|
28
|
+
try {
|
|
29
|
+
const message = (value as { message?: unknown }).message;
|
|
30
|
+
if (typeof message === "string") return message;
|
|
31
|
+
} catch {
|
|
32
|
+
// Hostile error getters must not replace the primary failure.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return String(value);
|
|
37
|
+
} catch {
|
|
38
|
+
return "<unprintable error>";
|
|
39
|
+
}
|
|
40
|
+
}
|