@gajae-code/utils 0.5.0 → 0.5.2
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/env.d.ts +1 -11
- package/dist/types/index.d.ts +1 -0
- package/dist/types/safe-stderr.d.ts +1 -0
- package/dist/types/spawn-env.d.ts +4 -0
- package/package.json +2 -2
- package/src/env.ts +3 -26
- package/src/frontmatter.ts +27 -1
- package/src/index.ts +1 -0
- package/src/postmortem.ts +4 -3
- package/src/safe-stderr.ts +15 -0
- package/src/spawn-env.ts +23 -0
package/dist/types/env.d.ts
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
|
+
export { filterProcessEnv, isSafeEnvName, isSafeEnvValue } from "./spawn-env";
|
|
1
2
|
/**
|
|
2
3
|
* Strict shell-identifier shape. Used for dotenv keys we accept into
|
|
3
4
|
* `Bun.env` — those should be referenceable as `$NAME` from POSIX shells,
|
|
4
5
|
* so we reject anything outside `[A-Za-z_][A-Za-z0-9_]*`.
|
|
5
6
|
*/
|
|
6
7
|
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
8
|
/**
|
|
19
9
|
* Parses simple POSIX shell environment assignments from files such as
|
|
20
10
|
* ~/.zshrc without executing user shell code. Supports `export KEY=value` and
|
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 "./safe-stderr";
|
|
22
23
|
export * from "./sanitize-text";
|
|
23
24
|
export * from "./snowflake";
|
|
24
25
|
export * from "./stream";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function safeStderrWrite(message: string): void;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function isSafeEnvName(name: string): boolean;
|
|
2
|
+
export declare function isSafeEnvValue(value: string): boolean;
|
|
3
|
+
export declare function shouldForwardSpawnEnvName(name: string): boolean;
|
|
4
|
+
export declare function filterProcessEnv(env: Record<string, string | undefined>): Record<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.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
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.5.
|
|
34
|
+
"@gajae-code/natives": "0.5.2",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/env.ts
CHANGED
|
@@ -2,6 +2,9 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getAgentDir, getConfigRootDir } from "./dirs";
|
|
5
|
+
import { isSafeEnvName, isSafeEnvValue } from "./spawn-env";
|
|
6
|
+
|
|
7
|
+
export { filterProcessEnv, isSafeEnvName, isSafeEnvValue } from "./spawn-env";
|
|
5
8
|
|
|
6
9
|
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7
10
|
|
|
@@ -14,32 +17,6 @@ export function isValidEnvName(name: string): boolean {
|
|
|
14
17
|
return ENV_NAME_RE.test(name);
|
|
15
18
|
}
|
|
16
19
|
|
|
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
|
-
|
|
43
20
|
function stripInlineShellComment(value: string): string {
|
|
44
21
|
let quote: '"' | "'" | undefined;
|
|
45
22
|
for (let i = 0; i < value.length; i++) {
|
package/src/frontmatter.ts
CHANGED
|
@@ -6,6 +6,32 @@ function stripHtmlComments(content: string): string {
|
|
|
6
6
|
return content.replace(/<!--[\s\S]*?-->/g, "");
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function stripLooseScalarTrailingCommas(metadata: string): string {
|
|
10
|
+
return metadata
|
|
11
|
+
.split("\n")
|
|
12
|
+
.map(line => {
|
|
13
|
+
const match = line.match(/^(\s*[\w-]+\s*:\s+)(.+),(\s*)$/);
|
|
14
|
+
if (!match || /^[[{|>]/.test(match[2].trimStart())) return line;
|
|
15
|
+
return `${match[1]}${match[2]}${match[3]}`;
|
|
16
|
+
})
|
|
17
|
+
.join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseYamlMetadata(metadata: string): Record<string, unknown> | null {
|
|
21
|
+
const normalized = metadata.replaceAll("\t", " ");
|
|
22
|
+
try {
|
|
23
|
+
return YAML.parse(normalized) as Record<string, unknown> | null;
|
|
24
|
+
} catch (strictError) {
|
|
25
|
+
const loose = stripLooseScalarTrailingCommas(normalized);
|
|
26
|
+
if (loose === normalized) throw strictError;
|
|
27
|
+
try {
|
|
28
|
+
return YAML.parse(loose) as Record<string, unknown> | null;
|
|
29
|
+
} catch {
|
|
30
|
+
throw strictError;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
/** Convert kebab-case to camelCase (e.g. "thinking-level" -> "thinkingLevel") */
|
|
10
36
|
function kebabToCamel(key: string): string {
|
|
11
37
|
if (!key.includes("-")) return key;
|
|
@@ -101,7 +127,7 @@ export function parseFrontmatter(
|
|
|
101
127
|
|
|
102
128
|
try {
|
|
103
129
|
// Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness
|
|
104
|
-
const loaded =
|
|
130
|
+
const loaded = parseYamlMetadata(metadata);
|
|
105
131
|
return { frontmatter: normalizeKeys({ ...frontmatter, ...loaded }), body };
|
|
106
132
|
} catch (error) {
|
|
107
133
|
const err = new FrontmatterError(
|
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 "./safe-stderr";
|
|
22
23
|
export * from "./sanitize-text";
|
|
23
24
|
export * from "./snowflake";
|
|
24
25
|
export * from "./stream";
|
package/src/postmortem.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import inspector from "node:inspector";
|
|
9
9
|
import { isMainThread } from "node:worker_threads";
|
|
10
10
|
import { logger } from ".";
|
|
11
|
+
import { safeStderrWrite } from "./safe-stderr";
|
|
11
12
|
|
|
12
13
|
// Cleanup reasons, in order of priority/meaning.
|
|
13
14
|
export enum Reason {
|
|
@@ -86,17 +87,17 @@ if (isMainThread) {
|
|
|
86
87
|
inspectorOpened = true;
|
|
87
88
|
inspector.open(undefined, undefined, false);
|
|
88
89
|
const url = inspector.url();
|
|
89
|
-
|
|
90
|
+
safeStderrWrite(`Inspector opened: ${url}\n`);
|
|
90
91
|
})
|
|
91
92
|
.on("uncaughtException", async err => {
|
|
92
|
-
|
|
93
|
+
safeStderrWrite(formatFatalError("Uncaught Exception", err));
|
|
93
94
|
logger.error("Uncaught exception", { err, stack: err.stack });
|
|
94
95
|
await runCleanup(Reason.UNCAUGHT_EXCEPTION);
|
|
95
96
|
process.exit(1);
|
|
96
97
|
})
|
|
97
98
|
.on("unhandledRejection", async reason => {
|
|
98
99
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
99
|
-
|
|
100
|
+
safeStderrWrite(formatFatalError("Unhandled Rejection", err));
|
|
100
101
|
logger.error("Unhandled rejection", { err, stack: err.stack });
|
|
101
102
|
await runCleanup(Reason.UNHANDLED_REJECTION);
|
|
102
103
|
process.exit(1);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const CLOSED_STDERR_ERROR_CODES = new Set(["EIO", "EPIPE", "EBADF"]);
|
|
2
|
+
|
|
3
|
+
function isClosedStderrWriteError(error: unknown): boolean {
|
|
4
|
+
if (!(error instanceof Error) || !("code" in error) || typeof error.code !== "string") return false;
|
|
5
|
+
return CLOSED_STDERR_ERROR_CODES.has(error.code);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function safeStderrWrite(message: string): void {
|
|
9
|
+
try {
|
|
10
|
+
process.stderr.write(message);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (isClosedStderrWriteError(error)) return;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/spawn-env.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const MACOS_MALLOC_STACK_LOGGING_ENV = new Set(["MallocStackLogging", "MallocStackLoggingNoCompact"]);
|
|
2
|
+
|
|
3
|
+
export function isSafeEnvName(name: string): boolean {
|
|
4
|
+
return name.length > 0 && !name.includes("=") && !name.includes("\0");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isSafeEnvValue(value: string): boolean {
|
|
8
|
+
return !value.includes("\0");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function shouldForwardSpawnEnvName(name: string): boolean {
|
|
12
|
+
return isSafeEnvName(name) && !MACOS_MALLOC_STACK_LOGGING_ENV.has(name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string> {
|
|
16
|
+
const result: Record<string, string> = {};
|
|
17
|
+
for (const key in env) {
|
|
18
|
+
const value = env[key];
|
|
19
|
+
if (!shouldForwardSpawnEnvName(key) || value === undefined || !isSafeEnvValue(value)) continue;
|
|
20
|
+
result[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|