@oh-my-pi/pi-utils 15.1.3 → 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.
@@ -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)` or `HTTP 503`.
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/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.3",
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,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "15.1.3",
34
+ "@oh-my-pi/pi-natives": "15.1.4",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
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 [key, value] of Object.entries(file)) {
102
+ for (const key in file) {
56
103
  if (!Bun.env[key]) {
57
- Bun.env[key] = value;
104
+ Bun.env[key] = file[key];
58
105
  }
59
106
  }
60
107
  }
@@ -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)` or `HTTP 503`.
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/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",