@oh-my-pi/pi-utils 15.1.2 → 15.1.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/dirs.d.ts +12 -0
- package/dist/types/env.d.ts +24 -0
- package/dist/types/fetch-retry.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/logger.d.ts +15 -0
- package/dist/types/sanitize-text.d.ts +14 -0
- package/package.json +3 -3
- package/src/dirs.ts +73 -0
- package/src/env.ts +50 -3
- package/src/fetch-retry.ts +2 -1
- package/src/index.ts +1 -0
- package/src/logger.ts +56 -20
- package/src/procmgr.ts +2 -2
- package/src/sanitize-text.ts +38 -0
package/dist/types/dirs.d.ts
CHANGED
|
@@ -137,3 +137,15 @@ export declare function getProjectPluginOverridesPath(cwd?: string): string;
|
|
|
137
137
|
export declare function getMCPConfigPath(scope: "user" | "project", cwd?: string): string;
|
|
138
138
|
/** Get the SSH config file path. */
|
|
139
139
|
export declare function getSSHConfigPath(scope: "user" | "project", cwd?: string): string;
|
|
140
|
+
/**
|
|
141
|
+
* Persistent per-install UUID stored at `~/.omp/install-id`.
|
|
142
|
+
*
|
|
143
|
+
* Generated lazily on first call and persisted with `O_CREAT|O_EXCL` so
|
|
144
|
+
* concurrent first-call races don't clobber each other (loser re-reads the
|
|
145
|
+
* winner's id). Survives independently of agent state: deleting
|
|
146
|
+
* `~/.omp/agent/` does not regenerate it. Server-side dedup for grievance
|
|
147
|
+
* pushes (and similar telemetry) keys on this id.
|
|
148
|
+
*/
|
|
149
|
+
export declare function getInstallId(): string;
|
|
150
|
+
/** Test-only: clear cached install id. Never call from production code. */
|
|
151
|
+
export declare function __resetInstallIdCacheForTests(): void;
|
package/dist/types/env.d.ts
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict shell-identifier shape. Used for dotenv keys we accept into
|
|
3
|
+
* `Bun.env` — those should be referenceable as `$NAME` from POSIX shells,
|
|
4
|
+
* so we reject anything outside `[A-Za-z_][A-Za-z0-9_]*`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isValidEnvName(name: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The only names that are genuinely unsafe to forward to a native `execve`
|
|
9
|
+
* spawn: empty, containing `=` (would corrupt the `KEY=VALUE` framing) or
|
|
10
|
+
* NUL (terminates the C string mid-entry). Windows ships standard variables
|
|
11
|
+
* whose names contain parentheses (e.g. `ProgramFiles(x86)`, `CommonProgramFiles(x86)`)
|
|
12
|
+
* — those MUST survive the scrub so downstream resolvers (Git Bash discovery
|
|
13
|
+
* in `procmgr.ts`, etc.) can still read them.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isSafeEnvName(name: string): boolean;
|
|
16
|
+
export declare function isSafeEnvValue(value: string): boolean;
|
|
17
|
+
export declare function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Parses a .env file synchronously and extracts key-value string pairs.
|
|
20
|
+
* Ignores lines that are empty or start with '#'. Trims whitespace.
|
|
21
|
+
* Allows values to be quoted with single or double quotes.
|
|
22
|
+
* Returns an object of key-value pairs.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseEnvFile(filePath: string): Record<string, string>;
|
|
1
25
|
/**
|
|
2
26
|
* Intentional re-export of Bun.env.
|
|
3
27
|
*
|
|
@@ -58,7 +58,7 @@ export declare function fetchWithRetry(url: string | URL | ((attempt: number) =>
|
|
|
58
58
|
* Inspect an arbitrary error value (or its `cause` chain, up to depth 2) for an
|
|
59
59
|
* HTTP status code. Reads `status`, `statusCode`, and `response.status` fields,
|
|
60
60
|
* coerces string values, and falls back to scanning the error message for
|
|
61
|
-
* common patterns like `error (429)
|
|
61
|
+
* common patterns like `Error: 401`, `error (429)`, or `HTTP 503`.
|
|
62
62
|
*/
|
|
63
63
|
export declare function extractHttpStatusFromError(error: unknown): number | undefined;
|
|
64
64
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * as procmgr from "./procmgr";
|
|
|
19
19
|
export * as prompt from "./prompt";
|
|
20
20
|
export * as ptree from "./ptree";
|
|
21
21
|
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
|
|
22
|
+
export * from "./sanitize-text";
|
|
22
23
|
export * from "./snowflake";
|
|
23
24
|
export * from "./stream";
|
|
24
25
|
export * from "./tab-spacing";
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace the active log transports. Pass `console: true, file: false` for
|
|
3
|
+
* long-running services (the auth broker, etc.) that want their structured
|
|
4
|
+
* logs piped into a process supervisor instead of the rotating file.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setTransports(opts: {
|
|
7
|
+
console?: boolean;
|
|
8
|
+
file?: boolean | string;
|
|
9
|
+
}): void;
|
|
1
10
|
/**
|
|
2
11
|
* Log an error message.
|
|
3
12
|
* @param message - The message to log.
|
|
@@ -10,6 +19,12 @@ export declare function error(message: string, context?: Record<string, unknown>
|
|
|
10
19
|
* @param context - The context to log.
|
|
11
20
|
*/
|
|
12
21
|
export declare function warn(message: string, context?: Record<string, unknown>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Log an informational message.
|
|
24
|
+
* @param message - The message to log.
|
|
25
|
+
* @param context - The context to log.
|
|
26
|
+
*/
|
|
27
|
+
export declare function info(message: string, context?: Record<string, unknown>): void;
|
|
13
28
|
/**
|
|
14
29
|
* Log a debug message.
|
|
15
30
|
* @param message - The message to log.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip ANSI escape sequences, remove control characters / lone surrogates,
|
|
3
|
+
* and normalize line endings.
|
|
4
|
+
*
|
|
5
|
+
* Bun-native implementation of the former native `sanitizeText` (see
|
|
6
|
+
* `crates/pi-natives/src/text.rs::sanitize_text`). JavaScript strings are
|
|
7
|
+
* already UTF-16 code-unit arrays. `toWellFormed()` handles the uncommon
|
|
8
|
+
* malformed path; when it changes the input, replacement characters are
|
|
9
|
+
* dropped and the normalized result goes through the well-formed sanitizer.
|
|
10
|
+
*
|
|
11
|
+
* Fast path: well-formed input with no controls or ANSI returns the original
|
|
12
|
+
* string after the control probe.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sanitizeText(text: string): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "15.1.
|
|
4
|
+
"version": "15.1.4",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@oh-my-pi/pi-natives": "15.1.4",
|
|
34
35
|
"beautiful-mermaid": "^1.1.3",
|
|
35
36
|
"handlebars": "^4.7.9",
|
|
36
37
|
"winston": "^3.19.0",
|
|
37
38
|
"winston-daily-rotate-file": "^5.0.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@types/bun": "^1.3.14"
|
|
41
|
-
"@oh-my-pi/pi-natives": "15.1.2"
|
|
41
|
+
"@types/bun": "^1.3.14"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"bun": ">=1.3.14"
|
package/src/dirs.ts
CHANGED
|
@@ -477,3 +477,76 @@ export function getSSHConfigPath(scope: "user" | "project", cwd: string = getPro
|
|
|
477
477
|
}
|
|
478
478
|
return path.join(getProjectAgentDir(cwd), "ssh.json");
|
|
479
479
|
}
|
|
480
|
+
|
|
481
|
+
// =============================================================================
|
|
482
|
+
// Install identity
|
|
483
|
+
// =============================================================================
|
|
484
|
+
|
|
485
|
+
let cachedInstallId: string | null = null;
|
|
486
|
+
|
|
487
|
+
const INSTALL_ID_FILE = "install-id";
|
|
488
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Persistent per-install UUID stored at `~/.omp/install-id`.
|
|
492
|
+
*
|
|
493
|
+
* Generated lazily on first call and persisted with `O_CREAT|O_EXCL` so
|
|
494
|
+
* concurrent first-call races don't clobber each other (loser re-reads the
|
|
495
|
+
* winner's id). Survives independently of agent state: deleting
|
|
496
|
+
* `~/.omp/agent/` does not regenerate it. Server-side dedup for grievance
|
|
497
|
+
* pushes (and similar telemetry) keys on this id.
|
|
498
|
+
*/
|
|
499
|
+
export function getInstallId(): string {
|
|
500
|
+
if (cachedInstallId) return cachedInstallId;
|
|
501
|
+
const filePath = path.join(getConfigRootDir(), INSTALL_ID_FILE);
|
|
502
|
+
|
|
503
|
+
let observedInvalid = false;
|
|
504
|
+
try {
|
|
505
|
+
const existing = fs.readFileSync(filePath, "utf8").trim();
|
|
506
|
+
if (UUID_RE.test(existing)) {
|
|
507
|
+
cachedInstallId = existing;
|
|
508
|
+
return existing;
|
|
509
|
+
}
|
|
510
|
+
// File present but unparseable — fall through and overwrite below.
|
|
511
|
+
observedInvalid = existing.length > 0;
|
|
512
|
+
} catch {}
|
|
513
|
+
|
|
514
|
+
const next = crypto.randomUUID();
|
|
515
|
+
try {
|
|
516
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
517
|
+
// If we already saw garbage in the file, unlink first so O_EXCL doesn't
|
|
518
|
+
// trip on it. Ignored if the unlink races against another writer.
|
|
519
|
+
if (observedInvalid) {
|
|
520
|
+
try {
|
|
521
|
+
fs.unlinkSync(filePath);
|
|
522
|
+
} catch {}
|
|
523
|
+
}
|
|
524
|
+
const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
525
|
+
try {
|
|
526
|
+
fs.writeSync(fd, `${next}\n`);
|
|
527
|
+
} finally {
|
|
528
|
+
fs.closeSync(fd);
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
// Lost the create race — re-read whatever the winner wrote.
|
|
532
|
+
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
|
|
533
|
+
try {
|
|
534
|
+
const existing = fs.readFileSync(filePath, "utf8").trim();
|
|
535
|
+
if (UUID_RE.test(existing)) {
|
|
536
|
+
cachedInstallId = existing;
|
|
537
|
+
return existing;
|
|
538
|
+
}
|
|
539
|
+
} catch {}
|
|
540
|
+
}
|
|
541
|
+
// Any other failure: keep the generated id in-memory so the rest of
|
|
542
|
+
// this process has a stable value; future processes will retry.
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
cachedInstallId = next;
|
|
546
|
+
return next;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Test-only: clear cached install id. Never call from production code. */
|
|
550
|
+
export function __resetInstallIdCacheForTests(): void {
|
|
551
|
+
cachedInstallId = null;
|
|
552
|
+
}
|
package/src/env.ts
CHANGED
|
@@ -3,13 +3,50 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getAgentDir, getConfigRootDir } from "./dirs";
|
|
5
5
|
|
|
6
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Strict shell-identifier shape. Used for dotenv keys we accept into
|
|
10
|
+
* `Bun.env` — those should be referenceable as `$NAME` from POSIX shells,
|
|
11
|
+
* so we reject anything outside `[A-Za-z_][A-Za-z0-9_]*`.
|
|
12
|
+
*/
|
|
13
|
+
export function isValidEnvName(name: string): boolean {
|
|
14
|
+
return ENV_NAME_RE.test(name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The only names that are genuinely unsafe to forward to a native `execve`
|
|
19
|
+
* spawn: empty, containing `=` (would corrupt the `KEY=VALUE` framing) or
|
|
20
|
+
* NUL (terminates the C string mid-entry). Windows ships standard variables
|
|
21
|
+
* whose names contain parentheses (e.g. `ProgramFiles(x86)`, `CommonProgramFiles(x86)`)
|
|
22
|
+
* — those MUST survive the scrub so downstream resolvers (Git Bash discovery
|
|
23
|
+
* in `procmgr.ts`, etc.) can still read them.
|
|
24
|
+
*/
|
|
25
|
+
export function isSafeEnvName(name: string): boolean {
|
|
26
|
+
return name.length > 0 && !name.includes("=") && !name.includes("\0");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isSafeEnvValue(value: string): boolean {
|
|
30
|
+
return !value.includes("\0");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string> {
|
|
34
|
+
const result: Record<string, string> = {};
|
|
35
|
+
for (const key in env) {
|
|
36
|
+
const value = env[key];
|
|
37
|
+
if (!isSafeEnvName(key) || value === undefined || !isSafeEnvValue(value)) continue;
|
|
38
|
+
result[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
6
43
|
/**
|
|
7
44
|
* Parses a .env file synchronously and extracts key-value string pairs.
|
|
8
45
|
* Ignores lines that are empty or start with '#'. Trims whitespace.
|
|
9
46
|
* Allows values to be quoted with single or double quotes.
|
|
10
47
|
* Returns an object of key-value pairs.
|
|
11
48
|
*/
|
|
12
|
-
function parseEnvFile(filePath: string): Record<string, string> {
|
|
49
|
+
export function parseEnvFile(filePath: string): Record<string, string> {
|
|
13
50
|
const result: Record<string, string> = {};
|
|
14
51
|
try {
|
|
15
52
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -22,12 +59,15 @@ function parseEnvFile(filePath: string): Record<string, string> {
|
|
|
22
59
|
if (eqIndex === -1) continue;
|
|
23
60
|
|
|
24
61
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
62
|
+
if (!isValidEnvName(key)) continue;
|
|
63
|
+
|
|
25
64
|
let value = trimmed.slice(eqIndex + 1).trim();
|
|
26
65
|
|
|
27
66
|
// Remove surrounding quotes (" or ')
|
|
28
67
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
29
68
|
value = value.slice(1, -1);
|
|
30
69
|
}
|
|
70
|
+
if (!isSafeEnvValue(value)) continue;
|
|
31
71
|
|
|
32
72
|
result[key] = value;
|
|
33
73
|
}
|
|
@@ -51,10 +91,17 @@ const piEnv = parseEnvFile(path.join(getConfigRootDir(), ".env"));
|
|
|
51
91
|
const agentEnv = parseEnvFile(path.join(getAgentDir(), ".env"));
|
|
52
92
|
const projectEnv = parseEnvFile(path.join(process.cwd(), ".env"));
|
|
53
93
|
|
|
94
|
+
for (const key of Object.keys(Bun.env)) {
|
|
95
|
+
const value = Bun.env[key];
|
|
96
|
+
if (!isSafeEnvName(key) || value === undefined || !isSafeEnvValue(value)) {
|
|
97
|
+
delete Bun.env[key];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
54
101
|
for (const file of [projectEnv, agentEnv, piEnv, homeEnv]) {
|
|
55
|
-
for (const
|
|
102
|
+
for (const key in file) {
|
|
56
103
|
if (!Bun.env[key]) {
|
|
57
|
-
Bun.env[key] =
|
|
104
|
+
Bun.env[key] = file[key];
|
|
58
105
|
}
|
|
59
106
|
}
|
|
60
107
|
}
|
package/src/fetch-retry.ts
CHANGED
|
@@ -198,7 +198,7 @@ function resolveDefaultDelay(
|
|
|
198
198
|
* Inspect an arbitrary error value (or its `cause` chain, up to depth 2) for an
|
|
199
199
|
* HTTP status code. Reads `status`, `statusCode`, and `response.status` fields,
|
|
200
200
|
* coerces string values, and falls back to scanning the error message for
|
|
201
|
-
* common patterns like `error (429)
|
|
201
|
+
* common patterns like `Error: 401`, `error (429)`, or `HTTP 503`.
|
|
202
202
|
*/
|
|
203
203
|
export function extractHttpStatusFromError(error: unknown): number | undefined {
|
|
204
204
|
return extractHttpStatusFromErrorInternal(error, 0);
|
|
@@ -236,6 +236,7 @@ function extractHttpStatusFromErrorInternal(error: unknown, depth: number): numb
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
const STATUS_MESSAGE_PATTERNS = [
|
|
239
|
+
/\berror\s*[:=]\s*(\d{3})\b/i,
|
|
239
240
|
/error\s*\((\d{3})\)/i,
|
|
240
241
|
/status\s*[:=]?\s*(\d{3})/i,
|
|
241
242
|
/\bhttp\s*(\d{3})\b/i,
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * as procmgr from "./procmgr";
|
|
|
19
19
|
export * as prompt from "./prompt";
|
|
20
20
|
export * as ptree from "./ptree";
|
|
21
21
|
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
|
|
22
|
+
export * from "./sanitize-text";
|
|
22
23
|
export * from "./snowflake";
|
|
23
24
|
export * from "./stream";
|
|
24
25
|
export * from "./tab-spacing";
|
package/src/logger.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Centralized
|
|
2
|
+
* Centralized logger for omp.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Default: rotating `~/.omp/logs/omp.<DATE>.log`, no console output (writing
|
|
5
|
+
* to stdout/stderr would corrupt the TUI). Long-running headless services
|
|
6
|
+
* (the auth broker, etc.) call {@link setTransports} to swap in a console
|
|
7
|
+
* transport so a process supervisor (pm2, journald, k8s) captures the logs.
|
|
8
|
+
*
|
|
9
|
+
* Each entry includes `process.pid` so concurrent omp instances stay
|
|
10
|
+
* traceable.
|
|
6
11
|
*/
|
|
7
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
13
|
import * as fs from "node:fs";
|
|
@@ -10,13 +15,12 @@ import winston from "winston";
|
|
|
10
15
|
import DailyRotateFile from "winston-daily-rotate-file";
|
|
11
16
|
import { getLogsDir } from "./dirs";
|
|
12
17
|
|
|
13
|
-
/** Ensure logs directory exists */
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
18
|
+
/** Ensure a logs directory exists; return the resolved path. */
|
|
19
|
+
function ensureDir(dir: string): string {
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
22
|
}
|
|
19
|
-
return
|
|
23
|
+
return dir;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/** Custom format that includes pid and flattens metadata */
|
|
@@ -39,25 +43,44 @@ const logFormat = winston.format.combine(
|
|
|
39
43
|
}),
|
|
40
44
|
);
|
|
41
45
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
/** Build a rotating file transport, materializing the target directory lazily. */
|
|
47
|
+
function makeFileTransport(dir?: string): winston.transport {
|
|
48
|
+
return new DailyRotateFile({
|
|
49
|
+
dirname: ensureDir(dir ?? getLogsDir()),
|
|
50
|
+
filename: "omp.%DATE%.log",
|
|
51
|
+
datePattern: "YYYY-MM-DD",
|
|
52
|
+
maxSize: "10m",
|
|
53
|
+
maxFiles: 5,
|
|
54
|
+
zippedArchive: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeConsoleTransport(): winston.transport {
|
|
59
|
+
return new winston.transports.Console({ format: logFormat });
|
|
60
|
+
}
|
|
51
61
|
|
|
52
|
-
/** The winston logger instance */
|
|
62
|
+
/** The winston logger instance. Default: file ON (TUI-safe), console OFF. */
|
|
53
63
|
const winstonLogger = winston.createLogger({
|
|
54
64
|
level: "debug",
|
|
55
65
|
format: logFormat,
|
|
56
|
-
transports: [
|
|
66
|
+
transports: [makeFileTransport()],
|
|
57
67
|
// Don't exit on error - logging failures shouldn't crash the app
|
|
58
68
|
exitOnError: false,
|
|
59
69
|
});
|
|
60
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Replace the active log transports. Pass `console: true, file: false` for
|
|
73
|
+
* long-running services (the auth broker, etc.) that want their structured
|
|
74
|
+
* logs piped into a process supervisor instead of the rotating file.
|
|
75
|
+
*/
|
|
76
|
+
export function setTransports(opts: { console?: boolean; file?: boolean | string }): void {
|
|
77
|
+
winstonLogger.clear();
|
|
78
|
+
if (opts.file) {
|
|
79
|
+
winstonLogger.add(makeFileTransport(typeof opts.file === "string" ? opts.file : undefined));
|
|
80
|
+
}
|
|
81
|
+
if (opts.console) winstonLogger.add(makeConsoleTransport());
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
/**
|
|
62
85
|
* Log an error message.
|
|
63
86
|
* @param message - The message to log.
|
|
@@ -84,6 +107,19 @@ export function warn(message: string, context?: Record<string, unknown>): void {
|
|
|
84
107
|
}
|
|
85
108
|
}
|
|
86
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Log an informational message.
|
|
112
|
+
* @param message - The message to log.
|
|
113
|
+
* @param context - The context to log.
|
|
114
|
+
*/
|
|
115
|
+
export function info(message: string, context?: Record<string, unknown>): void {
|
|
116
|
+
try {
|
|
117
|
+
winstonLogger.info(message, context);
|
|
118
|
+
} catch {
|
|
119
|
+
// Silently ignore logging failures
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
/**
|
|
88
124
|
* Log a debug message.
|
|
89
125
|
* @param message - The message to log.
|
package/src/procmgr.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { Process, ProcessStatus } from "@oh-my-pi/pi-natives";
|
|
4
4
|
import type { Subprocess } from "bun";
|
|
5
|
-
import { $env } from "./env";
|
|
5
|
+
import { $env, filterProcessEnv } from "./env";
|
|
6
6
|
import { $which } from "./which";
|
|
7
7
|
|
|
8
8
|
export interface ShellConfig {
|
|
@@ -45,7 +45,7 @@ function isExecutable(path: string): boolean {
|
|
|
45
45
|
function buildSpawnEnv(shell: string): Record<string, string> {
|
|
46
46
|
const noCI = $env.PI_BASH_NO_CI || $env.CLAUDE_BASH_NO_CI;
|
|
47
47
|
return {
|
|
48
|
-
...Bun.env,
|
|
48
|
+
...filterProcessEnv(Bun.env),
|
|
49
49
|
SHELL: shell,
|
|
50
50
|
GIT_EDITOR: "true",
|
|
51
51
|
GPG_TTY: "not a tty",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip ANSI escape sequences, remove control characters / lone surrogates,
|
|
3
|
+
* and normalize line endings.
|
|
4
|
+
*
|
|
5
|
+
* Bun-native implementation of the former native `sanitizeText` (see
|
|
6
|
+
* `crates/pi-natives/src/text.rs::sanitize_text`). JavaScript strings are
|
|
7
|
+
* already UTF-16 code-unit arrays. `toWellFormed()` handles the uncommon
|
|
8
|
+
* malformed path; when it changes the input, replacement characters are
|
|
9
|
+
* dropped and the normalized result goes through the well-formed sanitizer.
|
|
10
|
+
*
|
|
11
|
+
* Fast path: well-formed input with no controls or ANSI returns the original
|
|
12
|
+
* string after the control probe.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ESC_CHAR = "\x1b";
|
|
16
|
+
|
|
17
|
+
// Well-formed strings only need control/ANSI detection: C0 (excl. \t \n),
|
|
18
|
+
// CR, DEL, and C1. ESC (0x1B) is in \x0B-\x1F.
|
|
19
|
+
const CONTROL_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g;
|
|
20
|
+
|
|
21
|
+
const REPLACEMENT_CHAR = "\ufffd";
|
|
22
|
+
|
|
23
|
+
export function sanitizeText(text: string): string {
|
|
24
|
+
const wellFormed = text.toWellFormed();
|
|
25
|
+
if (wellFormed !== text) {
|
|
26
|
+
return sanitizeWellFormedText(wellFormed.replaceAll(REPLACEMENT_CHAR, ""));
|
|
27
|
+
}
|
|
28
|
+
return sanitizeWellFormedText(text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeWellFormedText(text: string): string {
|
|
32
|
+
CONTROL_RE.lastIndex = 0;
|
|
33
|
+
if (CONTROL_RE.exec(text) === null) return text;
|
|
34
|
+
|
|
35
|
+
const stripped = text.indexOf(ESC_CHAR) === -1 ? text : Bun.stripANSI(text);
|
|
36
|
+
CONTROL_RE.lastIndex = 0;
|
|
37
|
+
return stripped.replace(CONTROL_RE, "");
|
|
38
|
+
}
|