@howaboua/pi-codex-conversion 1.0.8 → 1.0.9-dev.0

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,62 +1,30 @@
1
- import { parseShellPart, nextCwd } from "./parse.ts";
2
- import { splitOnConnectors, normalizeTokens, shellSplit } from "./tokenize.ts";
1
+ import { parseCommandString, type ParsedShellCommand } from "./parse-command.ts";
3
2
  import type { CommandSummary, ShellAction } from "./types.ts";
4
3
 
5
4
  export type { CommandSummary, ShellAction } from "./types.ts";
6
5
 
7
- // The adapter only masks commands when every parsed segment still looks like
8
- // repository exploration. The moment we see an actual side-effectful run, we
9
- // fall back to raw command rendering so the UI does not hide meaningful work.
10
6
  export function summarizeShellCommand(command: string): CommandSummary {
11
- const normalized = normalizeTokens(shellSplit(command));
12
- const parts = splitOnConnectors(normalized);
13
- const fallback = runSummary(command);
14
-
15
- if (parts.length === 0) {
16
- return fallback;
17
- }
18
-
19
- const actions: ShellAction[] = [];
20
- let cwd: string | undefined;
21
-
22
- for (const part of parts) {
23
- if (part.length === 0) continue;
24
-
25
- cwd = nextCwd(cwd, part);
26
- const parsed = parseShellPart(part, cwd);
27
- if (parsed === null) continue;
28
- if (parsed.kind === "run") {
29
- return fallback;
30
- }
31
- actions.push(parsed);
32
- }
33
-
34
- const deduped = dedupeActions(actions);
35
- if (deduped.length === 0) {
36
- return fallback;
37
- }
38
-
7
+ const actions = parseCommandString(command).map(parsedToAction);
39
8
  return {
40
- maskAsExplored: deduped.every((action) => action.kind !== "run"),
41
- actions: deduped,
9
+ maskAsExplored: actions.every((action) => action.kind !== "run"),
10
+ actions,
42
11
  };
43
12
  }
44
13
 
45
- function runSummary(command: string): CommandSummary {
46
- return {
47
- maskAsExplored: false,
48
- actions: [{ kind: "run", command: command.trim() || command }],
49
- };
50
- }
51
-
52
- function dedupeActions(actions: ShellAction[]): ShellAction[] {
53
- const deduped: ShellAction[] = [];
54
- for (const action of actions) {
55
- const previous = deduped[deduped.length - 1];
56
- if (previous && JSON.stringify(previous) === JSON.stringify(action)) {
57
- continue;
58
- }
59
- deduped.push(action);
14
+ function parsedToAction(command: ParsedShellCommand): ShellAction {
15
+ if (command.kind === "unknown") {
16
+ return { kind: "run", command: command.command };
17
+ }
18
+ if (command.kind === "list") {
19
+ return command.path ? { kind: "list", command: command.command, path: command.path } : { kind: "list", command: command.command };
20
+ }
21
+ if (command.kind === "search") {
22
+ return {
23
+ kind: "search",
24
+ command: command.command,
25
+ ...(command.query ? { query: command.query } : {}),
26
+ ...(command.path ? { path: command.path } : {}),
27
+ };
60
28
  }
61
- return deduped;
29
+ return { kind: "read", command: command.command, name: command.name, path: command.path };
62
30
  }
@@ -23,9 +23,19 @@ export function shellSplit(input: string): string[] {
23
23
  continue;
24
24
  }
25
25
 
26
- if (char === "\\" && quote !== "'") {
27
- escaping = true;
28
- continue;
26
+ if (char === "\\") {
27
+ if (!quote) {
28
+ escaping = true;
29
+ continue;
30
+ }
31
+ if (quote === '"') {
32
+ if (next && (next === "\\" || next === '"' || next === "$" || next === "`")) {
33
+ escaping = true;
34
+ continue;
35
+ }
36
+ current += char;
37
+ continue;
38
+ }
29
39
  }
30
40
 
31
41
  if (quote) {
@@ -72,13 +82,26 @@ export function shellSplit(input: string): string[] {
72
82
  return tokens;
73
83
  }
74
84
 
85
+ export function shellQuote(token: string): string {
86
+ if (token.length === 0) return "''";
87
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(token)) return token;
88
+ return `'${token.replace(/'/g, `'"'"'`)}'`;
89
+ }
90
+
91
+ export function joinCommandTokens(tokens: string[]): string {
92
+ return tokens
93
+ .map((token) => (token === "&&" || token === "||" || token === "|" || token === ";" ? token : shellQuote(token)))
94
+ .join(" ");
95
+ }
96
+
75
97
  export function normalizeTokens(tokens: string[]): string[] {
76
98
  if (tokens.length >= 3 && (tokens[0] === "yes" || tokens[0] === "y" || tokens[0] === "no" || tokens[0] === "n") && tokens[1] === "|") {
77
99
  return normalizeTokens(tokens.slice(2));
78
100
  }
101
+ const shell = tokens[0]?.replace(/\\/g, "/").split("/").pop();
79
102
  if (
80
103
  tokens.length === 3 &&
81
- (tokens[0] === "bash" || tokens[0] === "zsh" || tokens[0].endsWith("/bash") || tokens[0].endsWith("/zsh")) &&
104
+ (shell === "bash" || shell === "zsh" || shell === "sh") &&
82
105
  (tokens[1] === "-c" || tokens[1] === "-lc")
83
106
  ) {
84
107
  return normalizeTokens(shellSplit(tokens[2]));
@@ -109,7 +132,7 @@ export function shortDisplayPath(path: string): string {
109
132
  const normalized = path.replace(/\\/g, "/").replace(/\/$/, "");
110
133
  const parts = normalized
111
134
  .split("/")
112
- .filter((part) => part.length > 0 && part !== "." && part !== "src" && part !== "dist" && part !== "build" && part !== "node_modules");
135
+ .filter((part) => part.length > 0 && part !== "src" && part !== "dist" && part !== "build" && part !== "node_modules");
113
136
  return parts[parts.length - 1] ?? normalized;
114
137
  }
115
138
 
@@ -3,6 +3,7 @@ import { spawn, type ChildProcessByStdio } from "node:child_process";
3
3
  import { resolve } from "node:path";
4
4
  import type { Readable } from "node:stream";
5
5
  import * as pty from "node-pty";
6
+ import { CODEX_FALLBACK_SHELL, getCodexRuntimeShell, isFishShell } from "../adapter/runtime-shell.ts";
6
7
 
7
8
  export interface UnifiedExecResult {
8
9
  chunk_id: string;
@@ -77,7 +78,60 @@ function resolveWorkdir(baseCwd: string, workdir?: string): string {
77
78
  }
78
79
 
79
80
  function resolveShell(shell?: string): string {
80
- return shell || process.env.SHELL || "/bin/bash";
81
+ return getCodexRuntimeShell(shell || process.env.SHELL);
82
+ }
83
+
84
+ const BASH_SYNC_ENV_KEYS = [
85
+ "PATH",
86
+ "SHELL",
87
+ "HOME",
88
+ "XDG_CONFIG_HOME",
89
+ "XDG_DATA_HOME",
90
+ "XDG_CACHE_HOME",
91
+ "BUN_INSTALL",
92
+ "PNPM_HOME",
93
+ "MISE_DATA_DIR",
94
+ "MISE_CONFIG_DIR",
95
+ "MISE_SHIMS_DIR",
96
+ "CARGO_HOME",
97
+ "GOPATH",
98
+ "ANDROID_HOME",
99
+ "ANDROID_NDK_HOME",
100
+ "JAVA_HOME",
101
+ ];
102
+
103
+ function shellEscape(value: string): string {
104
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
105
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
106
+ }
107
+
108
+ function shouldSyncBashEnv(requestedShell: string | undefined, effectiveShell: string): boolean {
109
+ return effectiveShell === CODEX_FALLBACK_SHELL && isFishShell(requestedShell || process.env.SHELL);
110
+ }
111
+
112
+ function buildSyncedBashCommand(command: string, env: NodeJS.ProcessEnv): string {
113
+ const assignments: string[] = [];
114
+ for (const key of BASH_SYNC_ENV_KEYS) {
115
+ const value = key === "SHELL" ? CODEX_FALLBACK_SHELL : env[key];
116
+ if (typeof value !== "string") continue;
117
+ assignments.push(`export ${key}=${shellEscape(value)}`);
118
+ }
119
+ if (assignments.length === 0) return command;
120
+ return `${assignments.join("; ")}; ${command}`;
121
+ }
122
+
123
+ function resolveExecution(requestedShell: string | undefined, command: string): { shell: string; command: string; env: NodeJS.ProcessEnv } {
124
+ const shell = resolveShell(requestedShell);
125
+ const env: NodeJS.ProcessEnv = { ...process.env };
126
+ if (!shouldSyncBashEnv(requestedShell, shell)) {
127
+ return { shell, command, env };
128
+ }
129
+ env.SHELL = CODEX_FALLBACK_SHELL;
130
+ return {
131
+ shell,
132
+ command: buildSyncedBashCommand(command, env),
133
+ env,
134
+ };
81
135
  }
82
136
 
83
137
  function clampYieldTime(yieldTimeMs: number | undefined, fallback: number): number {
@@ -341,11 +395,12 @@ export function createExecSessionManager(): ExecSessionManager {
341
395
 
342
396
  function createPipeSession(input: ExecCommandInput, workdir: string, shell: string, signal?: AbortSignal): PipeExecSession {
343
397
  const login = input.login ?? true;
344
- const shellArgs = login ? ["-lc", input.cmd] : ["-c", input.cmd];
398
+ const execution = resolveExecution(input.shell, input.cmd);
399
+ const shellArgs = login ? ["-lc", execution.command] : ["-c", execution.command];
345
400
  const child = spawn(shell, shellArgs, {
346
401
  cwd: workdir,
347
402
  stdio: ["ignore", "pipe", "pipe"],
348
- env: process.env,
403
+ env: execution.env,
349
404
  });
350
405
 
351
406
  const session: PipeExecSession = {
@@ -387,10 +442,11 @@ export function createExecSessionManager(): ExecSessionManager {
387
442
 
388
443
  function createPtySession(input: ExecCommandInput, workdir: string, shell: string, signal?: AbortSignal): PtyExecSession {
389
444
  const login = input.login ?? true;
390
- const shellArgs = login ? ["-lc", input.cmd] : ["-c", input.cmd];
445
+ const execution = resolveExecution(input.shell, input.cmd);
446
+ const shellArgs = login ? ["-lc", execution.command] : ["-c", execution.command];
391
447
  const child = pty.spawn(shell, shellArgs, {
392
448
  cwd: workdir,
393
- env: process.env,
449
+ env: execution.env,
394
450
  name: process.env.TERM || "xterm-256color",
395
451
  cols: 80,
396
452
  rows: 24,