| 1 | import * as Effect from "effect/Effect"; |
| 2 | |
| 3 | /** |
| 4 | * Returns true when the current process looks like it's being driven by a |
| 5 | * coding agent, CI runner, test runner, or anything else that won't render an |
| 6 | * interactive TUI / prompt well. |
| 7 | * |
| 8 | * This is the process-level source of truth used by CliKit capability |
| 9 | * detection, auth configuration guards, and spawned-command stdin policy. |
| 10 | * |
| 11 | * Kept in `Util` (rather than `Cli`) so `Auth` can depend on it without |
| 12 | * pulling in the CLI layer. |
| 13 | */ |
| 14 | // An explicit CLI flag beats every env-based heuristic. Checked via argv |
| 15 | // because capability detection runs while the CLI's service layers are |
| 16 | // built, before the command parser has produced flag values. Only scan up |
| 17 | // to a `--` separator so a positional argument that happens to be the |
| 18 | // literal text "--no-input" is not mistaken for the flag. |
| 19 | const hasNoInputFlag = (): boolean => { |
| 20 | const separator = process.argv.indexOf("--"); |
| 21 | const flagArgs = |
| 22 | separator === -1 ? process.argv : process.argv.slice(0, separator); |
| 23 | return flagArgs.includes("--no-input"); |
| 24 | }; |
| 25 | |
| 26 | // Known coding-agent env vars, ported from unjs/std-env (src/agents.ts). |
| 27 | // Best-effort — the isTTY checks at the call sites already catch most cases |
| 28 | // since agents typically pipe stdin/stdout. std-env's kiro check |
| 29 | // (TERM_PROGRAM=kiro gated on no-TTY) is deliberately omitted: both call |
| 30 | // sites only reach this function when stdin/stdout ARE a TTY, where that |
| 31 | // check would never match. |
| 32 | const isAgentEnv = (env: NodeJS.ProcessEnv): boolean => |
| 33 | !!( |
| 34 | // explicit override supported by std-env |
| 35 | env.AI_AGENT || |
| 36 | // claude |
| 37 | env.CLAUDECODE || |
| 38 | env.CLAUDE_CODE || |
| 39 | env.CLAUDE_CODE_ENTRYPOINT || |
| 40 | // replit |
| 41 | env.REPL_ID || |
| 42 | // gemini |
| 43 | env.GEMINI_CLI || |
| 44 | // codex |
| 45 | env.CODEX_SANDBOX || |
| 46 | env.CODEX_THREAD_ID || |
| 47 | env.CODEX_CLI || |
| 48 | // opencode |
| 49 | env.OPENCODE || |
| 50 | // auggie |
| 51 | env.AUGMENT_AGENT || |
| 52 | // goose |
| 53 | env.GOOSE_PROVIDER || |
| 54 | // junie |
| 55 | env.JUNIE_DATA || |
| 56 | env.JUNIE_SHIM_PATH || |
| 57 | // aider |
| 58 | env.AIDER_MODEL || |
| 59 | // pi |
| 60 | /\.pi[\\/]agent/.test(env.PATH ?? "") || |
| 61 | // devin |
| 62 | /devin/.test(env.EDITOR ?? "") || |
| 63 | // cursor |
| 64 | env.CURSOR_AGENT |
| 65 | ); |
| 66 | |
| 67 | export const isNonInteractive = (): boolean => { |
| 68 | const env = process.env; |
| 69 | if (hasNoInputFlag()) return true; |
| 70 | if (env.ALCHEMY_PLAIN === "1" || env.ALCHEMY_NO_TUI === "1") return true; |
| 71 | if (env.ALCHEMY_TUI === "1") return false; |
| 72 | if (!process.stdin.isTTY || !process.stdout.isTTY) return true; |
| 73 | if (env.CI) return true; |
| 74 | if (isAgentEnv(env)) return true; |
| 75 | return false; |
| 76 | }; |
| 77 | |
| 78 | /** |
| 79 | * Whether a plain line-based prompt can still read an answer from stdin. |
| 80 | * |
| 81 | * Distinct from {@link isNonInteractive}: `ALCHEMY_PLAIN` / `ALCHEMY_NO_TUI` |
| 82 | * turn off the TUI *rendering*, not input — a human running plain mode in a |
| 83 | * terminal can still answer a `[y/n]` question. Agents and CI pipe stdin |
| 84 | * (or set their env markers) and land `false`. |
| 85 | */ |
| 86 | export const canPromptOnStdin = (): boolean => { |
| 87 | const env = process.env; |
| 88 | if (hasNoInputFlag()) return false; |
| 89 | if (!process.stdin.isTTY || !process.stdout.isTTY) return false; |
| 90 | if (env.CI) return false; |
| 91 | if (isAgentEnv(env)) return false; |
| 92 | return true; |
| 93 | }; |
| 94 | |
| 95 | export interface InteractionCapabilities { |
| 96 | readonly input: boolean; |
| 97 | } |
| 98 | |
| 99 | /** Process capabilities as an Effect so callers can replace them in tests. */ |
| 100 | export const processInteractionCapabilities: Effect.Effect<InteractionCapabilities> = |
| 101 | Effect.sync(() => ({ input: !isNonInteractive() })); |
| 102 | |
| 103 | /** Select user-facing copy from an injected capability Effect. */ |
| 104 | export const messageForCapabilities = <E, R>( |
| 105 | capabilities: Effect.Effect<InteractionCapabilities, E, R>, |
| 106 | interactive: string, |
| 107 | nonInteractive: string, |
| 108 | ): Effect.Effect<string, E, R> => |
| 109 | Effect.map(capabilities, ({ input }) => |
| 110 | input ? interactive : nonInteractive, |
| 111 | ); |
| 112 | |
| 113 | /** Prefer the profile dashboard when this process can own a TUI screen. */ |
| 114 | export const profileCommandHint = (nonInteractiveCommand: string) => |
| 115 | messageForCapabilities( |
| 116 | processInteractionCapabilities, |
| 117 | "alchemy profile", |
| 118 | nonInteractiveCommand, |
| 119 | ); |