From d0735279860a7a3589e6945922b20c02816aea90 Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Sat, 30 May 2026 15:24:11 +0200 Subject: [PATCH] feat(init): opt-in agent selection with interactive picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit init no longer installs files for every agent (or the global skill) by default. Nothing is written unless the user chooses it. - TTY, no flags: interactive checkbox picker (AGENTS.md + CLAUDE.md preselected; skill toggle in-list). Zero new dependency (readline raw mode). - Non-interactive (--yes/--json/no TTY): defaults to AGENTS.md + CLAUDE.md. - --agents explicit, --all for every agent. - Global skill is opt-in via --skill (removed --no-skill; no longer default-on). Pure resolveInitPlan() + prompt helpers are unit-tested (16 new tests). Pre-release change to the canary-only init command — no stable users affected. --- CHANGELOG.md | 12 ++-- README.md | 22 +++--- docs/cli-reference.md | 21 ++++-- llms-full.txt | 16 +++-- src/cli.ts | 68 ++++++++++++------- src/install/index.test.ts | 53 +++++++++++++++ src/install/index.ts | 69 +++++++++++++++++++ src/install/prompt.test.ts | 74 ++++++++++++++++++++ src/install/prompt.ts | 134 +++++++++++++++++++++++++++++++++++++ 9 files changed, 416 insertions(+), 53 deletions(-) create mode 100644 src/install/prompt.test.ts create mode 100644 src/install/prompt.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a6483e8..8ae2587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`init` command** — agent adoption layer. `codebase-intelligence init [path]` writes an idempotent, marked instruction block ("query CI before grep/read") into each - agent's repo file (`AGENTS.md`, `CLAUDE.md`, + selected agent's repo file (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`, `.github/copilot-instructions.md`, - `GEMINI.md`, `CONVENTIONS.md`) and installs a portable skill to + `GEMINI.md`, `CONVENTIONS.md`) and optionally installs a portable skill to `~/.claude/skills/codebase-intelligence/SKILL.md`. - - `--agents ` to target a subset of agents (default: all). - - `--no-skill` to skip the global skill install. - - `--json` for machine-readable output. + - **Opt-in by design** — nothing is written unless chosen. On a TTY, an interactive + picker (`AGENTS.md` + `CLAUDE.md` preselected); non-interactively, those two by + default. The global skill installs only with `--skill`. + - `--agents ` to select explicitly, `--all` for every agent, `--yes` for + non-interactive defaults, `--json` for machine-readable output. - Writes are idempotent — only content between the `codebase-intelligence:start`/`:end` markers is ever touched; existing user content is preserved. diff --git a/README.md b/README.md index f042487..ff3d9cb 100644 --- a/README.md +++ b/README.md @@ -125,18 +125,22 @@ codebase-intelligence has the data — but AI agents only benefit if they actual *query* it instead of defaulting to grep/read. `init` closes that gap. ```bash -codebase-intelligence init # current repo, all agents + skill -codebase-intelligence init ./repo --agents claude,agents -codebase-intelligence init --no-skill +codebase-intelligence init # interactive picker (TTY) +codebase-intelligence init --agents claude,cursor +codebase-intelligence init --all --skill # every agent + global skill +codebase-intelligence init --yes # non-interactive defaults ``` -It writes an idempotent, marked instruction block ("query CI before grep/read") into -each agent's native file, and installs a portable skill: +Nothing is written unless you choose it. On a terminal, `init` shows an interactive +picker (`AGENTS.md` + `CLAUDE.md` preselected); non-interactively it defaults to those +two. The global skill is **opt-in** (`--skill`). It writes an idempotent, marked +instruction block ("query CI before grep/read") into each selected agent's native file: -| Layer | Target | -|---|---| -| Repo instructions | `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`, `.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md` (Aider) | -| Portable skill | `~/.claude/skills/codebase-intelligence/SKILL.md` | +| Layer | Target | Default | +|---|---|---| +| Repo instructions | `AGENTS.md`, `CLAUDE.md` | selected | +| Repo instructions | `.cursor/rules/*.mdc`, `.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md` (Aider) | opt-in | +| Portable skill | `~/.claude/skills/codebase-intelligence/SKILL.md` | opt-in (`--skill`) | Writes are idempotent — only content between the `` / `:end` markers is ever touched, so re-running diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 339291c..538e2f0 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -158,14 +158,19 @@ codebase-intelligence clusters [--min-files ] [--json] [--force] ### init -Make AI agents use codebase-intelligence: write a managed instruction block into each -agent's repo file (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`, -`.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md`) and install the -portable skill to `~/.claude/skills/`. Idempotent — only content between the +Set up AI agents to use codebase-intelligence by writing a managed instruction block +into each selected agent's repo file (`AGENTS.md`, `CLAUDE.md`, +`.cursor/rules/codebase-intelligence.mdc`, `.github/copilot-instructions.md`, +`GEMINI.md`, `CONVENTIONS.md`) and optionally installing the portable skill to +`~/.claude/skills/`. Idempotent — only content between the `codebase-intelligence:start`/`:end` markers is touched. +Opt-in by design: on a TTY it shows an interactive picker (`AGENTS.md` + `CLAUDE.md` +preselected). Non-interactively (or with `--yes`/`--json`) it defaults to those two. +The global skill is never installed unless `--skill` is passed. + ```bash -codebase-intelligence init [path] [--agents ] [--no-skill] [--json] +codebase-intelligence init [path] [--agents ] [--all] [--skill] [--yes] [--json] ``` **Output:** per-file actions (created / updated / unchanged) and skill install status. @@ -187,8 +192,10 @@ codebase-intelligence init [path] [--agents ] [--no-skill] [--json] | `--entry ` | processes | Filter by entry point name | | `--min-files ` | clusters | Min files per cluster | | `--no-dry-run` | rename | Actually perform the rename (default: dry run) | -| `--agents ` | init | Comma-separated agents (default: all) | -| `--no-skill` | init | Skip installing the global Claude skill | +| `--agents ` | init | Comma-separated agents, non-interactive (default: agents,claude) | +| `--all` | init | Target every agent (non-interactive) | +| `--skill` | init | Also install the global Claude skill (opt-in) | +| `-y, --yes` | init | Accept defaults without prompting | ## Behavior diff --git a/llms-full.txt b/llms-full.txt index 49897ec..8ea9dd9 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -379,15 +379,17 @@ Community-detected file clusters (Louvain algorithm). ### init ```bash -codebase-intelligence init [path] [--agents ] [--no-skill] [--json] +codebase-intelligence init [path] [--agents ] [--all] [--skill] [--yes] [--json] ``` -Make AI agents actually use codebase-intelligence. Writes an idempotent, marked -instruction block ("query CI before grep/read") into each agent's repo file — +Set up AI agents to use codebase-intelligence. Writes an idempotent, marked +instruction block ("query CI before grep/read") into each selected agent's repo file — `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`, -`.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md` — and installs the -portable skill to `~/.claude/skills/codebase-intelligence/SKILL.md`. Only content -between the `codebase-intelligence:start`/`:end` markers is ever touched, so re-running -is safe. `--agents` limits targets (default: all); `--no-skill` skips the skill. +`.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md`. Opt-in: on a TTY it +shows an interactive picker (`AGENTS.md` + `CLAUDE.md` preselected); non-interactively +(or `--yes`/`--json`) it defaults to those two. `--agents` selects explicitly, `--all` +targets every agent, `--skill` also installs the portable skill to +`~/.claude/skills/codebase-intelligence/SKILL.md` (never installed otherwise). Only +content between the `codebase-intelligence:start`/`:end` markers is ever touched. ## Global Behavior diff --git a/src/cli.ts b/src/cli.ts index fe9f908..0899afb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -43,11 +43,11 @@ import { import { installRepoFiles, installGlobalSkill, - isAgentId, + resolveInitPlan, ALL_AGENT_IDS, } from "./install/index.js"; +import { promptSelection } from "./install/prompt.js"; import type { CodebaseGraph } from "./types/index.js"; -import type { AgentId } from "./install/index.js"; const INDEX_DIR_NAME = ".code-visualizer"; @@ -188,7 +188,9 @@ interface McpOptions { interface InitOptions { agents?: string; + all?: boolean; skill?: boolean; + yes?: boolean; json?: boolean; } @@ -953,36 +955,50 @@ program program .command("init") - .description("Make AI agents use codebase-intelligence: write per-agent instruction files + install the skill") + .description("Set up AI agents to use codebase-intelligence: write per-agent instruction files (+ optional skill)") .argument("[path]", "Repo root (default: current directory)", ".") - .option("--agents ", `Comma-separated agents to target (default: all). Available: ${ALL_AGENT_IDS.join(", ")}`) - .option("--no-skill", "Skip installing the global Claude skill") - .option("--json", "Output as JSON") - .action((targetPath: string, options: InitOptions) => { + .option("--agents ", `Comma-separated agents, non-interactive. Available: ${ALL_AGENT_IDS.join(", ")}`) + .option("--all", "Target every agent (non-interactive)") + .option("--skill", "Also install the global Claude skill (opt-in)") + .option("-y, --yes", "Accept defaults without prompting") + .option("--json", "Output as JSON (implies non-interactive)") + .action(async (targetPath: string, options: InitOptions) => { const resolved = path.resolve(targetPath); if (!fs.existsSync(resolved)) { process.stderr.write(`Error: Path does not exist: ${targetPath}\n`); process.exit(1); } - let agents: AgentId[] | undefined; - if (options.agents) { - const requested = options.agents - .split(",") - .map((a) => a.trim()) - .filter(Boolean); - const invalid = requested.filter((a) => !isAgentId(a)); - if (invalid.length > 0) { - process.stderr.write( - `Error: Unknown agents: ${invalid.join(", ")}. Available: ${ALL_AGENT_IDS.join(", ")}\n`, - ); - process.exit(2); + const isTty = process.stdin.isTTY && process.stdout.isTTY; + const plan = resolveInitPlan(options, isTty); + + if (plan.invalidAgents.length > 0) { + process.stderr.write( + `Error: Unknown agents: ${plan.invalidAgents.join(", ")}. Available: ${ALL_AGENT_IDS.join(", ")}\n`, + ); + process.exit(2); + } + + let agents = plan.agents; + let installSkill = plan.installSkill; + + if (plan.mode === "interactive") { + const selection = await promptSelection(agents, installSkill); + if (!selection) { + output("Cancelled — nothing written."); + return; } - agents = requested.filter(isAgentId); + agents = selection.agents; + installSkill = selection.skill; + } + + if (agents.length === 0 && !installSkill) { + output("Nothing selected — nothing to do."); + return; } const repoResults = installRepoFiles(resolved, { agents }); - const skillResult = options.skill === false ? undefined : installGlobalSkill(); + const skillResult = installSkill ? installGlobalSkill() : undefined; if (options.json) { outputJson({ repoFiles: repoResults, skill: skillResult ?? null }); @@ -991,9 +1007,11 @@ program output(`Codebase Intelligence — agent adoption`); output(`──────────────────────────────────────`); - output(`Repo instruction files (${resolved}):`); - for (const r of repoResults) { - output(` ${r.action.padEnd(9)} ${r.path}`); + if (repoResults.length > 0) { + output(`Repo instruction files (${resolved}):`); + for (const r of repoResults) { + output(` ${r.action.padEnd(9)} ${r.path}`); + } } if (skillResult) { output(``); @@ -1001,7 +1019,7 @@ program output(` ${skillResult.action.padEnd(9)} ${skillResult.path}`); } output(``); - output(`Done. Agents in this repo will now be told to query codebase-intelligence first.`); + output(`Done. Selected agents will be told to query codebase-intelligence first.`); output(`Re-run anytime — writes are idempotent (managed blocks only).`); }); diff --git a/src/install/index.test.ts b/src/install/index.test.ts index c07eb22..c5a234c 100644 --- a/src/install/index.test.ts +++ b/src/install/index.test.ts @@ -11,8 +11,10 @@ import { installRepoFiles, installGlobalSkill, isAgentId, + resolveInitPlan, AGENT_TARGETS, ALL_AGENT_IDS, + DEFAULT_AGENTS, DEFAULT_MARKERS, } from "./index.js"; @@ -194,3 +196,54 @@ describe("installGlobalSkill", () => { expect(second.action).toBe("unchanged"); }); }); + +describe("resolveInitPlan", () => { + it("defaults to AGENTS.md + CLAUDE.md only", () => { + expect([...DEFAULT_AGENTS]).toEqual(["agents", "claude"]); + }); + + it("--all selects every agent (explicit, non-interactive)", () => { + const plan = resolveInitPlan({ all: true }, true); + expect(plan.mode).toBe("explicit"); + expect(plan.agents).toEqual([...ALL_AGENT_IDS]); + expect(plan.installSkill).toBe(false); + }); + + it("--agents picks the listed agents", () => { + const plan = resolveInitPlan({ agents: "claude,gemini" }, true); + expect(plan.mode).toBe("explicit"); + expect(plan.agents).toEqual(["claude", "gemini"]); + expect(plan.invalidAgents).toEqual([]); + }); + + it("--agents reports unknown ids and keeps valid ones", () => { + const plan = resolveInitPlan({ agents: "claude,bogus" }, true); + expect(plan.agents).toEqual(["claude"]); + expect(plan.invalidAgents).toEqual(["bogus"]); + }); + + it("no flags on a TTY → interactive, seeded with the default set", () => { + const plan = resolveInitPlan({}, true); + expect(plan.mode).toBe("interactive"); + expect(plan.agents).toEqual([...DEFAULT_AGENTS]); + }); + + it("no flags without a TTY → non-interactive default", () => { + const plan = resolveInitPlan({}, false); + expect(plan.mode).toBe("default"); + expect(plan.agents).toEqual([...DEFAULT_AGENTS]); + }); + + it("--json forces non-interactive even on a TTY", () => { + expect(resolveInitPlan({ json: true }, true).mode).toBe("default"); + }); + + it("--yes forces non-interactive even on a TTY", () => { + expect(resolveInitPlan({ yes: true }, true).mode).toBe("default"); + }); + + it("--skill is opt-in across modes", () => { + expect(resolveInitPlan({ skill: true }, false).installSkill).toBe(true); + expect(resolveInitPlan({}, false).installSkill).toBe(false); + }); +}); diff --git a/src/install/index.ts b/src/install/index.ts index 00ff831..f4c8623 100644 --- a/src/install/index.ts +++ b/src/install/index.ts @@ -112,6 +112,75 @@ export function isAgentId(value: string): value is AgentId { return (ALL_AGENT_IDS as readonly string[]).includes(value); } +/** + * Default selection when the user doesn't choose explicitly: the universal + * `AGENTS.md` standard plus `CLAUDE.md`. Everything else is opt-in. + */ +export const DEFAULT_AGENTS: readonly AgentId[] = ["agents", "claude"]; + +// ── Init planning (pure) ──────────────────────────────────── + +export interface InitFlags { + /** Comma-separated agent ids from `--agents`. */ + agents?: string; + /** `--all`: every agent. */ + all?: boolean; + /** `--skill`: install the global skill (opt-in). */ + skill?: boolean; + /** `--json`: machine output, implies non-interactive. */ + json?: boolean; + /** `--yes`: accept defaults without prompting. */ + yes?: boolean; +} + +export type InitMode = "explicit" | "interactive" | "default"; + +export interface InitPlan { + /** Agents to write (preselection when mode is "interactive"). */ + agents: AgentId[]; + /** Whether to install the global skill (default when mode is "interactive"). */ + installSkill: boolean; + /** How the selection was decided. "interactive" → caller should prompt. */ + mode: InitMode; + /** Unknown ids passed to `--agents`. */ + invalidAgents: string[]; +} + +/** + * Decide what `init` should do from its flags and whether stdout is a TTY. + * Pure — no prompting or filesystem. When `mode` is "interactive" the caller + * presents a picker seeded with `agents`/`installSkill`; otherwise the plan is + * final. + */ +export function resolveInitPlan(flags: InitFlags, isTty: boolean): InitPlan { + const installSkill = flags.skill === true; + + if (flags.all === true) { + return { agents: [...ALL_AGENT_IDS], installSkill, mode: "explicit", invalidAgents: [] }; + } + + if (flags.agents !== undefined) { + const requested = flags.agents + .split(",") + .map((a) => a.trim()) + .filter(Boolean); + return { + agents: requested.filter(isAgentId), + installSkill, + mode: "explicit", + invalidAgents: requested.filter((a) => !isAgentId(a)), + }; + } + + const interactive = isTty && flags.json !== true && flags.yes !== true; + return { + agents: [...DEFAULT_AGENTS], + installSkill, + mode: interactive ? "interactive" : "default", + invalidAgents: [], + }; +} + // ── Content (single source of truth) ──────────────────────── /** diff --git a/src/install/prompt.test.ts b/src/install/prompt.test.ts new file mode 100644 index 0000000..1dfc473 --- /dev/null +++ b/src/install/prompt.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from "vitest"; +import { + buildPromptItems, + toggleItem, + toggleAll, + collectSelection, + renderMenu, +} from "./prompt.js"; +import { AGENT_TARGETS } from "./index.js"; + +describe("buildPromptItems", () => { + it("has one row per agent plus a skill row", () => { + const items = buildPromptItems(["agents", "claude"], false); + expect(items).toHaveLength(AGENT_TARGETS.length + 1); + expect(items[items.length - 1].id).toBe("skill"); + }); + + it("preselects the given agents and skill", () => { + const items = buildPromptItems(["agents", "claude"], true); + const checked = new Set(items.filter((i) => i.checked).map((i) => i.id)); + expect(checked.has("agents")).toBe(true); + expect(checked.has("claude")).toBe(true); + expect(checked.has("gemini")).toBe(false); + expect(checked.has("skill")).toBe(true); + }); +}); + +describe("collectSelection", () => { + it("reduces checked rows to agents + skill", () => { + const items = buildPromptItems(["claude"], false); + toggleItem(items, items.findIndex((i) => i.id === "gemini")); + toggleItem(items, items.findIndex((i) => i.id === "skill")); + const sel = collectSelection(items); + expect(sel.agents.sort()).toEqual(["claude", "gemini"]); + expect(sel.skill).toBe(true); + }); + + it("empty when nothing is checked", () => { + const items = buildPromptItems([], false); + const sel = collectSelection(items); + expect(sel.agents).toEqual([]); + expect(sel.skill).toBe(false); + }); +}); + +describe("toggleItem / toggleAll", () => { + it("toggleItem flips a single row", () => { + const items = buildPromptItems([], false); + expect(items[0].checked).toBe(false); + toggleItem(items, 0); + expect(items[0].checked).toBe(true); + }); + + it("toggleAll checks all when some are unchecked, then clears", () => { + const items = buildPromptItems(["agents"], false); + toggleAll(items); + expect(items.every((i) => i.checked)).toBe(true); + toggleAll(items); + expect(items.every((i) => !i.checked)).toBe(true); + }); +}); + +describe("renderMenu", () => { + it("marks the cursor row and renders checkboxes", () => { + const items = buildPromptItems(["agents"], false); + const out = renderMenu(items, 0); + const lines = out.split("\n"); + expect(lines).toHaveLength(items.length); + expect(lines[0].startsWith(">")).toBe(true); + expect(lines[1].startsWith(" ")).toBe(true); + expect(out).toContain("[x]"); + expect(out).toContain("[ ]"); + }); +}); diff --git a/src/install/prompt.ts b/src/install/prompt.ts new file mode 100644 index 0000000..964df33 --- /dev/null +++ b/src/install/prompt.ts @@ -0,0 +1,134 @@ +import readline from "readline"; +import { AGENT_TARGETS } from "./index.js"; +import type { AgentId } from "./index.js"; + +// ── Interactive multiselect (zero-dependency) ─────────────── +// +// A readline raw-mode checkbox picker. The pure helpers +// (buildPromptItems / toggle / collectSelection / renderMenu) hold the logic +// and are unit-tested; promptSelection is the thin terminal I/O shell. + +export type PromptItemId = AgentId | "skill"; + +export interface PromptItem { + id: PromptItemId; + label: string; + checked: boolean; +} + +export interface PromptSelection { + agents: AgentId[]; + skill: boolean; +} + +const SKILL_LABEL = "Install global skill (~/.claude/skills/codebase-intelligence/SKILL.md)"; + +/** Build the picker rows, preselecting the given agents / skill. */ +export function buildPromptItems(preAgents: readonly AgentId[], preSkill: boolean): PromptItem[] { + const items: PromptItem[] = AGENT_TARGETS.map((t) => ({ + id: t.id, + label: `${t.id.padEnd(7)} ${t.file}`, + checked: preAgents.includes(t.id), + })); + items.push({ id: "skill", label: SKILL_LABEL, checked: preSkill }); + return items; +} + +/** Flip the checkbox at `index` (mutates and returns the same array). */ +export function toggleItem(items: PromptItem[], index: number): PromptItem[] { + const item = items[index]; + item.checked = !item.checked; + return items; +} + +/** Check all if any are unchecked, otherwise clear all. */ +export function toggleAll(items: PromptItem[]): PromptItem[] { + const allChecked = items.every((i) => i.checked); + for (const item of items) item.checked = !allChecked; + return items; +} + +/** Reduce the picker rows to a selection. */ +export function collectSelection(items: readonly PromptItem[]): PromptSelection { + const agents: AgentId[] = []; + let skill = false; + for (const item of items) { + if (!item.checked) continue; + if (item.id === "skill") skill = true; + else agents.push(item.id); + } + return { agents, skill }; +} + +/** Render the menu body (no trailing newline). */ +export function renderMenu(items: readonly PromptItem[], cursor: number): string { + return items + .map((item, i) => { + const pointer = i === cursor ? ">" : " "; + const box = item.checked ? "[x]" : "[ ]"; + return `${pointer} ${box} ${item.label}`; + }) + .join("\n"); +} + +/** + * Present the interactive picker. Returns the selection, or `null` if the user + * cancelled (Esc / Ctrl-C). Falls back to the preselection when stdin is not a + * TTY (no way to read keystrokes). + */ +export async function promptSelection( + preAgents: readonly AgentId[], + preSkill: boolean, +): Promise { + const items = buildPromptItems(preAgents, preSkill); + const input = process.stdin; + const out = process.stderr; + + if (!input.isTTY) { + return collectSelection(items); + } + + out.write("Select what to set up (↑/↓ move · space toggle · a all · enter confirm · esc cancel):\n"); + readline.emitKeypressEvents(input); + input.setRawMode(true); + input.resume(); + + let cursor = 0; + const draw = (first: boolean): void => { + if (!first) out.write(`\x1b[${items.length}A\x1b[0J`); + out.write(`${renderMenu(items, cursor)}\n`); + }; + draw(true); + + return await new Promise((resolve) => { + const cleanup = (): void => { + input.setRawMode(false); + input.removeListener("keypress", onKey); + input.pause(); + out.write("\n"); + }; + + const onKey = (str: string | undefined, key: readline.Key): void => { + if (key.name === "up" || str === "k") { + cursor = (cursor - 1 + items.length) % items.length; + } else if (key.name === "down" || str === "j") { + cursor = (cursor + 1) % items.length; + } else if (str === " ") { + toggleItem(items, cursor); + } else if (str === "a") { + toggleAll(items); + } else if (key.name === "return" || key.name === "enter") { + cleanup(); + resolve(collectSelection(items)); + return; + } else if (key.name === "escape" || (key.ctrl === true && key.name === "c")) { + cleanup(); + resolve(null); + return; + } + draw(false); + }; + + input.on("keypress", onKey); + }); +}