* feat(cli): catalog listing, auth flags, and OAuth browser UX Add servers/list and servers/show, --relogin / --stored-auth-only, browser OAuth navigation with OSC 8 links, and extract method handlers under clients/cli/src/handlers. Tracks #1781. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): address #1782 review — relogin clear, stored-auth-only, docs Clear both raw and normalized OAuth keys on --relogin; check the shared store before bailing under --stored-auth-only mid-session; restore the tests README link target; document servers/show redaction limits; keep mcpi groundwork with TODO(#1432) markers. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): gate OAuth browser open and harden #1782 review follow-ups Arm auto-open only for CLI-owned interactive OAuth, honor MCP_AUTO_OPEN_ENABLED, move --relogin clear after validation, skip keychain on servers/list, and cover secret redaction after rehydrate. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): silence disarmed OAuth URLs and round-3 review nits Suppress authorize URL print when navigation is disarmed or --stored-auth-only; document MCP_AUTO_OPEN_ENABLED; redact Cookie/auth headers; keep servers/show unit tests off the OS keychain. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): cover withArmedAutoOpen for the ≥90 branch gate CI failed on cliOAuth.ts branch coverage after the auto-open arming helper landed; exercise the armed path through runCliInteractiveOAuth. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): mid-RPC unauthorized recovery and clear round-4 nits Mirror connect's UnauthorizedError path in withCliAuthRecoveryRetry, let MCP_AUTO_OPEN_ENABLED=true force open off-TTY, mock open-url in the in-process runner, reject --relogin on catalog methods, restore the __tests__ README inventory, and narrow OAuth helpers to structural client types. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): reconnect after mid-RPC OAuth and finish round-5 nits Reconnect before retrying after unauthorized recovery, guard non-OAuth configs, decouple forceAutoOpen, move open-url mock to vitest setupFiles, and reject --relogin with stored-auth short-circuits. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail fast on non-TTY interactive OAuth and round-6 nits Skip interactive OAuth without a TTY unless MCP_AUTO_OPEN_ENABLED=true; document --stored-auth-only for CI; redact settings.metadata; reject --relogin for stdio; clarify AuthRecoveryRequired reconnect comments. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): admit interactive OAuth when stdin is a TTY (round-7) Gate on stdin||stderr so `2>&1 | tee` still works; fold confirm/auto-open/ TTY into CliOAuthConnectOptions; rename authRequiredFailure; document MCP_AUTO_OPEN_ENABLED=true as non-TTY admit. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail fast on step-up without stdin (round-8) Decline [y/N] on readline EOF/close, require a stdin TTY for the default step-up confirmer (force-open admit is not enough), document stderr-only browser auto-open vs stdin||stderr flow admit, and drop the alias wrapper. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): allow piped step-up answers; bound silent pipes (round-9) Drop the hard stdin-TTY gate that broke `echo y | …`. Keep EOF decline and add a short non-TTY timeout so an open pipe that never writes cannot hang. Clarify isTTY as admit-only; extract the readline mock factory. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): keep partial piped step-up answers (round-10) Capture readline `line` before close so `printf y` without a newline is not treated as decline; timeout is auth_required with a distinct message; document the 5s bound and post-confirm callback wait; pin the TTY no-timeout arm. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): tighten step-up timeout messaging and TTY test (round-11) Make the TTY no-timeout assertion use a settled flag (the Promise.race check was vacuous), say "timed out" instead of "declined" on the pipe bound, and document that piped answers need a newline or stdin close. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
84 lines
2.7 KiB
TypeScript
84 lines
2.7 KiB
TypeScript
import type { OutputFormat } from "./handlers/format-output.js";
|
|
|
|
/**
|
|
* ANSI / OSC 8 styling helpers.
|
|
*
|
|
* TODO(#1432): the CLI OAuth path only needs {@link Style.link} today; bold /
|
|
* color helpers and {@link styleFromOpts} are used by the experimental session
|
|
* CLI (`mcpi`) human formatter.
|
|
*/
|
|
export type Style = {
|
|
/** Whether ANSI styling is enabled. */
|
|
readonly ansi: boolean;
|
|
bold: (s: string) => string;
|
|
dim: (s: string) => string;
|
|
red: (s: string) => string;
|
|
yellow: (s: string) => string;
|
|
green: (s: string) => string;
|
|
cyan: (s: string) => string;
|
|
/** OSC 8 hyperlink when ansi; otherwise plain label (defaulting to uri). */
|
|
link: (uri: string, label?: string) => string;
|
|
};
|
|
|
|
const identity = (s: string): string => s;
|
|
|
|
function sgr(enabled: boolean, open: number, close: number) {
|
|
if (!enabled) return identity;
|
|
return (s: string): string => `\u001b[${open}m${s}\u001b[${close}m`;
|
|
}
|
|
|
|
/**
|
|
* Build a style helper. When `ansi` is false, all methods are identity
|
|
* (and `link` returns the label only — no OSC 8).
|
|
*/
|
|
export function createStyle(ansi: boolean): Style {
|
|
return {
|
|
ansi,
|
|
bold: sgr(ansi, 1, 22),
|
|
dim: sgr(ansi, 2, 22),
|
|
red: sgr(ansi, 31, 39),
|
|
yellow: sgr(ansi, 33, 39),
|
|
green: sgr(ansi, 32, 39),
|
|
cyan: sgr(ansi, 36, 39),
|
|
link: (uri: string, label?: string): string => {
|
|
const text = label ?? uri;
|
|
if (!ansi || !uri) return text;
|
|
// OSC 8 hyperlink: ESC ] 8 ; ; URI BEL text ESC ] 8 ; ; BEL
|
|
return `\u001b]8;;${uri}\u0007${text}\u001b]8;;\u0007`;
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Plain (no ANSI) style — default for tests and `--plain`. */
|
|
export const PLAIN: Style = createStyle(false);
|
|
|
|
export type ResolveAnsiOptions = {
|
|
/** `--plain` — force no ANSI. */
|
|
plain?: boolean;
|
|
/** `--format`; json never uses ANSI for the payload path. */
|
|
format?: OutputFormat;
|
|
/** Override TTY detection (tests). */
|
|
isTTY?: boolean;
|
|
/** Override `NO_COLOR` (tests). */
|
|
noColorEnv?: string | undefined;
|
|
};
|
|
|
|
/**
|
|
* Decide whether session human output should use ANSI.
|
|
* Off when: `--plain`, `NO_COLOR` set, `--format json`, or the caller's stream
|
|
* is not a TTY (override via `isTTY`; default checks `process.stdout`).
|
|
*/
|
|
export function resolveAnsiEnabled(opts: ResolveAnsiOptions = {}): boolean {
|
|
if (opts.plain) return false;
|
|
if (opts.format === "json") return false;
|
|
const noColor =
|
|
opts.noColorEnv !== undefined ? opts.noColorEnv : process.env.NO_COLOR;
|
|
if (noColor !== undefined && noColor !== "") return false;
|
|
const tty =
|
|
opts.isTTY !== undefined ? opts.isTTY : process.stdout.isTTY === true;
|
|
return tty;
|
|
}
|
|
|
|
export function styleFromOpts(opts: ResolveAnsiOptions = {}): Style {
|
|
return createStyle(resolveAnsiEnabled(opts));
|
|
}
|