1import * 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.
19const 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.
32const 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
67export 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 */
86export 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
95export interface InteractionCapabilities {
96 readonly input: boolean;
97}
98
99/** Process capabilities as an Effect so callers can replace them in tests. */
100export const processInteractionCapabilities: Effect.Effect<InteractionCapabilities> =
101 Effect.sync(() => ({ input: !isNonInteractive() }));
102
103/** Select user-facing copy from an injected capability Effect. */
104export 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. */
114export const profileCommandHint = (nonInteractiveCommand: string) =>
115 messageForCapabilities(
116 processInteractionCapabilities,
117 "alchemy profile",
118 nonInteractiveCommand,
119 );