From 89738917aed2c82f66c89203c05fda27a734e929 Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Wed, 29 Apr 2026 08:18:29 +0000 Subject: [PATCH 1/4] offer to install and authenticate Claude CLI before diagnosis When setup fails and claude-assist kicks in, instead of silently skipping when the CLI is missing or unauthenticated, interactively offer to install it (via install-claude.sh) and sign in (via claude setup-token) so the user can get diagnostic help immediately. Co-Authored-By: Claude Opus 4.7 (1M context) --- setup/lib/claude-assist.ts | 73 +++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/setup/lib/claude-assist.ts b/setup/lib/claude-assist.ts index 1651a9c..9cc3e5d 100644 --- a/setup/lib/claude-assist.ts +++ b/setup/lib/claude-assist.ts @@ -2,8 +2,10 @@ * Offer Claude-assisted debugging when a setup step fails. * * Flow: - * 1. Check `claude` is on PATH and has a working credential. If not, - * silently skip — pre-auth failures can't use this path. + * 1. Check `claude` is on PATH — if not, offer to install it via + * setup/install-claude.sh. Then check auth via `claude auth status` + * — if not signed in, offer to run `claude setup-token` (browser + * OAuth). If either is declined or fails, silently skip. * 2. Ask the user for consent ("Want me to ask Claude for a fix?"). * 3. Build a minimal prompt: the one-paragraph situation, the failing * step's name/message/hint, and a short list of *file references* @@ -16,7 +18,7 @@ * * Skippable with NANOCLAW_SKIP_CLAUDE_ASSIST=1 for CI/scripted runs. */ -import { execSync, spawn } from 'child_process'; +import { execSync, spawn, spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; @@ -90,7 +92,7 @@ export async function offerClaudeAssist( projectRoot: string = process.cwd(), ): Promise { if (process.env.NANOCLAW_SKIP_CLAUDE_ASSIST === '1') return false; - if (!isClaudeUsable()) return false; + if (!(await ensureClaudeReady(projectRoot))) return false; const want = ensureAnswer( await p.confirm({ @@ -128,15 +130,70 @@ export async function offerClaudeAssist( return true; } -function isClaudeUsable(): boolean { +function isClaudeInstalled(): boolean { try { execSync('command -v claude', { stdio: 'ignore' }); + return true; } catch { return false; } - // Availability without auth is half the story; a real query will still - // fail if the token isn't registered. We try first and surface the error - // rather than pre-checking auth with a separate round trip. +} + +function isClaudeAuthenticated(): boolean { + try { + execSync('claude auth status', { stdio: 'ignore', timeout: 5_000 }); + return true; + } catch { + return false; + } +} + +async function ensureClaudeReady(projectRoot: string): Promise { + if (!isClaudeInstalled()) { + const install = ensureAnswer( + await p.confirm({ + message: + 'Claude CLI is needed to diagnose this. Install it now?', + initialValue: true, + }), + ); + if (!install) return false; + + const code = spawnSync('bash', ['setup/install-claude.sh'], { + cwd: projectRoot, + stdio: 'inherit', + }).status; + if (code !== 0 || !isClaudeInstalled()) { + p.log.error("Couldn't install the Claude CLI."); + return false; + } + p.log.success('Claude CLI installed.'); + } + + if (!isClaudeAuthenticated()) { + const auth = ensureAnswer( + await p.confirm({ + message: + "Claude CLI isn't signed in. Sign in now? (a browser will open)", + initialValue: true, + }), + ); + if (!auth) return false; + + const code = await new Promise((resolve) => { + const child = spawn('claude', ['setup-token'], { + stdio: 'inherit', + }); + child.on('close', (c) => resolve(c ?? 1)); + child.on('error', () => resolve(1)); + }); + if (code !== 0 || !isClaudeAuthenticated()) { + p.log.error("Couldn't complete Claude sign-in."); + return false; + } + p.log.success('Claude CLI signed in.'); + } + return true; } From 93be2d15f0d1e78797f085733fba65026cdae19e Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Wed, 29 Apr 2026 10:18:38 +0000 Subject: [PATCH 2/4] fix claude setup-token flow for headless/remote systems Use script(1) to capture PTY output and extract OAuth token when browser-based auth isn't available, with fallback code-paste flow. Co-Authored-By: Claude Opus 4.6 --- setup/lib/claude-assist.ts | 47 ++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/setup/lib/claude-assist.ts b/setup/lib/claude-assist.ts index 9cc3e5d..dbc5082 100644 --- a/setup/lib/claude-assist.ts +++ b/setup/lib/claude-assist.ts @@ -5,7 +5,8 @@ * 1. Check `claude` is on PATH — if not, offer to install it via * setup/install-claude.sh. Then check auth via `claude auth status` * — if not signed in, offer to run `claude setup-token` (browser - * OAuth). If either is declined or fails, silently skip. + * OAuth with code-paste fallback for headless/remote systems). + * If either is declined or fails, silently skip. * 2. Ask the user for consent ("Want me to ask Claude for a fix?"). * 3. Build a minimal prompt: the one-paragraph situation, the failing * step's name/message/hint, and a short list of *file references* @@ -20,6 +21,7 @@ */ import { execSync, spawn, spawnSync } from 'child_process'; import fs from 'fs'; +import os from 'os'; import path from 'path'; import * as p from '@clack/prompts'; @@ -180,14 +182,45 @@ async function ensureClaudeReady(projectRoot: string): Promise { ); if (!auth) return false; - const code = await new Promise((resolve) => { - const child = spawn('claude', ['setup-token'], { + // setup-token has an interactive TUI; reset terminal to cooked mode + // so its prompts render correctly after clack's raw-mode prompts. + spawnSync('stty', ['sane'], { stdio: 'inherit' }); + + // Run under script(1) to capture the OAuth token from PTY output + // while preserving interactive TTY for the browser OAuth flow. + // Same approach as register-claude-token.sh, but we set the env var + // instead of writing to OneCLI. + const tmpfile = path.join(os.tmpdir(), `claude-setup-token-${process.pid}`); + try { + const isUtilLinux = (() => { + try { + return execSync('script --version 2>&1', { encoding: 'utf-8' }).includes('util-linux'); + } catch { return false; } + })(); + const scriptArgs = isUtilLinux + ? ['-q', '-c', 'claude setup-token', tmpfile] + : ['-q', tmpfile, 'claude', 'setup-token']; + + spawnSync('script', scriptArgs, { + cwd: projectRoot, stdio: 'inherit', }); - child.on('close', (c) => resolve(c ?? 1)); - child.on('error', () => resolve(1)); - }); - if (code !== 0 || !isClaudeAuthenticated()) { + + if (!isClaudeAuthenticated() && fs.existsSync(tmpfile)) { + const raw = fs.readFileSync(tmpfile, 'utf-8'); + const stripped = raw + .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') + .replace(/[\n\r]/g, ''); + const matches = stripped.match(/(sk-ant-oat[A-Za-z0-9_-]{80,500}AA)/g); + if (matches) { + process.env.CLAUDE_CODE_OAUTH_TOKEN = matches[matches.length - 1]; + } + } + } finally { + try { fs.unlinkSync(tmpfile); } catch {} + } + + if (!isClaudeAuthenticated()) { p.log.error("Couldn't complete Claude sign-in."); return false; } From aa390b3fd0466af30e5cc19113bb9e52944d3684 Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Wed, 29 Apr 2026 10:20:54 +0000 Subject: [PATCH 3/4] detect existing .env and credentials on setup re-run When re-running setup on a machine that already has a .env with channel tokens or OneCLI config, detect them early and offer to reuse instead of prompting the user to paste everything again. - Add detectExistingEnv() to parse .env and group known keys - Add detectExistingDisplayName() to read display name from v2.db - Defer display name prompt until actually needed (cli-agent or channel) - Skip cli-agent and first-chat when groups are already wired - Add token reuse checks to Telegram, Discord, Slack, Teams, iMessage Co-Authored-By: Claude Opus 4.6 (1M context) --- setup/auto.ts | 98 ++++++++++++++++++++++++++++++++++++-- setup/channels/discord.ts | 12 +++++ setup/channels/imessage.ts | 13 +++++ setup/channels/slack.ts | 24 ++++++++++ setup/channels/teams.ts | 22 +++++++++ setup/channels/telegram.ts | 12 +++++ setup/environment.ts | 18 +++++++ 7 files changed, 195 insertions(+), 4 deletions(-) diff --git a/setup/auto.ts b/setup/auto.ts index 4dee7c8..01d7f3a 100644 --- a/setup/auto.ts +++ b/setup/auto.ts @@ -46,6 +46,7 @@ import { } from './lib/setup-config-parse.js'; import { runAdvancedScreen } from './lib/setup-config-screen.js'; import { runWindowedStep } from './lib/windowed-runner.js'; +import { detectRegisteredGroups, detectExistingDisplayName } from './environment.js'; import { pollHealth } from './onecli.js'; import { getLaunchdLabel, getSystemdUnit } from '../src/install-slug.js'; import { claudeCliAvailable, resolveTimezoneViaClaude } from './lib/tz-from-claude.js'; @@ -121,6 +122,39 @@ async function main(): Promise { } } + // Detect existing .env and offer to reuse it so the user doesn't have to + // paste credentials again on a re-run. + const existingEnv = detectExistingEnv(); + if (existingEnv) { + const lines = Object.values(existingEnv.groups).map( + (g) => ` ${k.green('✓')} ${g.label}`, + ); + p.note(lines.join('\n'), 'Found existing configuration'); + + const reuseChoice = ensureAnswer( + await brightSelect({ + message: 'Use this existing environment?', + options: [ + { value: 'reuse', label: 'Yes, use what I already have', hint: 'recommended' }, + { value: 'fresh', label: 'No, start fresh' }, + ], + initialValue: 'reuse', + }), + ) as 'reuse' | 'fresh'; + setupLog.userInput('existing_env_choice', reuseChoice); + + if (reuseChoice === 'reuse') { + for (const [key, value] of Object.entries(existingEnv.raw)) { + if (!process.env[key]) process.env[key] = value; + } + if (existingEnv.groups.onecli) skip.add('onecli'); + if (detectRegisteredGroups(process.cwd())) { + skip.add('cli-agent'); + skip.add('first-chat'); + } + } + } + if (!skip.has('container')) { p.log.message(dimWrap('Your assistant lives in its own sandbox. It can only see what you explicitly share.', 4)); p.log.message( @@ -295,14 +329,17 @@ async function main(): Promise { } let displayName: string | undefined; - const needsDisplayName = !skip.has('cli-agent') || !skip.has('channel'); - if (needsDisplayName) { - const fallback = process.env.USER?.trim() || 'Operator'; + async function resolveDisplayName(): Promise { + if (displayName) return displayName; const preset = process.env.NANOCLAW_DISPLAY_NAME?.trim(); - displayName = preset || (await askDisplayName(fallback)); + const existing = detectExistingDisplayName(process.cwd()); + const fallback = process.env.USER?.trim() || 'Operator'; + displayName = preset || existing || (await askDisplayName(fallback)); + return displayName; } if (!skip.has('cli-agent')) { + await resolveDisplayName(); const res = await runQuietStep( 'cli-agent', { @@ -371,6 +408,9 @@ async function main(): Promise { let channelChoice: ChannelChoice = 'skip'; if (!skip.has('channel')) { channelChoice = await askChannelChoice(); + if (channelChoice !== 'skip') { + await resolveDisplayName(); + } if (channelChoice === 'telegram') { await runTelegramChannel(displayName!); } else if (channelChoice === 'discord') { @@ -1010,6 +1050,56 @@ async function askChannelChoice(): Promise { // ─── interactive / env helpers ───────────────────────────────────────── +interface ExistingEnvGroup { + label: string; + keys: string[]; +} + +const ENV_KEY_GROUPS: Record = { + onecli: { label: 'OneCLI', keys: ['ONECLI_URL'] }, + telegram: { label: 'Telegram', keys: ['TELEGRAM_BOT_TOKEN'] }, + discord: { label: 'Discord', keys: ['DISCORD_BOT_TOKEN', 'DISCORD_APPLICATION_ID', 'DISCORD_PUBLIC_KEY'] }, + slack: { label: 'Slack', keys: ['SLACK_BOT_TOKEN', 'SLACK_SIGNING_SECRET'] }, + signal: { label: 'Signal', keys: ['SIGNAL_ACCOUNT'] }, + teams: { label: 'Teams', keys: ['TEAMS_APP_ID', 'TEAMS_APP_PASSWORD', 'TEAMS_APP_TENANT_ID', 'TEAMS_APP_TYPE'] }, + whatsapp: { label: 'WhatsApp', keys: ['ASSISTANT_HAS_OWN_NUMBER'] }, + imessage: { label: 'iMessage', keys: ['IMESSAGE_LOCAL', 'IMESSAGE_ENABLED', 'IMESSAGE_SERVER_URL', 'IMESSAGE_API_KEY'] }, +}; + +function detectExistingEnv(): { groups: Record; raw: Record } | null { + const envPath = path.join(process.cwd(), '.env'); + if (!fs.existsSync(envPath)) return null; + + let content: string; + try { + content = fs.readFileSync(envPath, 'utf-8'); + } catch { + return null; + } + + const raw: Record = {}; + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq < 1) continue; + raw[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); + } + + if (Object.keys(raw).length === 0) return null; + + const groups: Record = {}; + for (const [id, def] of Object.entries(ENV_KEY_GROUPS)) { + const found = def.keys.filter((key) => raw[key] !== undefined); + if (found.length > 0) { + groups[id] = { label: def.label, keys: found }; + } + } + + if (Object.keys(groups).length === 0) return null; + return { groups, raw }; +} + function anthropicSecretExists(): boolean { try { const res = spawnSync('onecli', ['secrets', 'list'], { diff --git a/setup/channels/discord.ts b/setup/channels/discord.ts index 3668686..dd17bc2 100644 --- a/setup/channels/discord.ts +++ b/setup/channels/discord.ts @@ -239,6 +239,18 @@ async function walkThroughServerCreation(): Promise { } async function collectDiscordToken(): Promise { + const existing = process.env.DISCORD_BOT_TOKEN?.trim(); + if (existing && /^[A-Za-z0-9._-]{50,}$/.test(existing)) { + const reuse = ensureAnswer(await p.confirm({ + message: `Found an existing Discord bot token (${existing.slice(0, 10)}…). Use it?`, + initialValue: true, + })); + if (reuse) { + setupLog.userInput('discord_token', 'reused-existing'); + return existing; + } + } + const answer = ensureAnswer( await p.password({ message: 'Paste your bot token', diff --git a/setup/channels/imessage.ts b/setup/channels/imessage.ts index d8b129f..89d2efe 100644 --- a/setup/channels/imessage.ts +++ b/setup/channels/imessage.ts @@ -222,6 +222,19 @@ async function walkThroughFullDiskAccess(): Promise { } async function collectRemoteCreds(): Promise { + const existingUrl = process.env.IMESSAGE_SERVER_URL?.trim(); + const existingKey = process.env.IMESSAGE_API_KEY?.trim(); + if (existingUrl && existingKey && /^https?:\/\//i.test(existingUrl)) { + const reuse = ensureAnswer(await p.confirm({ + message: `Found existing Photon credentials (${existingUrl}). Use them?`, + initialValue: true, + })); + if (reuse) { + setupLog.userInput('imessage_remote_creds', 'reused-existing'); + return { serverUrl: existingUrl, apiKey: existingKey }; + } + } + p.note( [ "Photon is a separate service that owns an iMessage account and", diff --git a/setup/channels/slack.ts b/setup/channels/slack.ts index 6d1ff56..cfbd988 100644 --- a/setup/channels/slack.ts +++ b/setup/channels/slack.ts @@ -151,6 +151,18 @@ async function walkThroughAppCreation(): Promise { } async function collectBotToken(): Promise { + const existing = process.env.SLACK_BOT_TOKEN?.trim(); + if (existing && existing.startsWith('xoxb-') && existing.length >= 24) { + const reuse = ensureAnswer(await p.confirm({ + message: `Found an existing Slack bot token (${existing.slice(0, 10)}…). Use it?`, + initialValue: true, + })); + if (reuse) { + setupLog.userInput('slack_bot_token', 'reused-existing'); + return existing; + } + } + const answer = ensureAnswer( await p.password({ message: 'Paste your Slack bot token', @@ -172,6 +184,18 @@ async function collectBotToken(): Promise { } async function collectSigningSecret(): Promise { + const existing = process.env.SLACK_SIGNING_SECRET?.trim(); + if (existing && /^[a-f0-9]{16,}$/i.test(existing)) { + const reuse = ensureAnswer(await p.confirm({ + message: 'Found an existing Slack signing secret. Use it?', + initialValue: true, + })); + if (reuse) { + setupLog.userInput('slack_signing_secret', 'reused-existing'); + return existing; + } + } + const answer = ensureAnswer( await p.password({ message: 'Paste your Slack signing secret', diff --git a/setup/channels/teams.ts b/setup/channels/teams.ts index fb4d878..91a91d9 100644 --- a/setup/channels/teams.ts +++ b/setup/channels/teams.ts @@ -59,6 +59,28 @@ export async function runTeamsChannel(_displayName: string): Promise { const collected: Collected = {}; const completed: string[] = []; + const existingAppId = process.env.TEAMS_APP_ID?.trim(); + const existingPassword = process.env.TEAMS_APP_PASSWORD?.trim(); + if (existingAppId && existingPassword) { + const reuse = ensureAnswer(await p.confirm({ + message: `Found existing Teams credentials (App ID: ${existingAppId.slice(0, 8)}…). Use them?`, + initialValue: true, + })); + if (reuse) { + collected.appId = existingAppId; + collected.appPassword = existingPassword; + collected.appType = (process.env.TEAMS_APP_TYPE?.trim() as 'SingleTenant' | 'MultiTenant') || 'MultiTenant'; + if (collected.appType === 'SingleTenant') { + collected.tenantId = process.env.TEAMS_APP_TENANT_ID?.trim(); + } + setupLog.userInput('teams_credentials', 'reused-existing'); + await installAdapter(collected); + completed.push('Adapter installed and service restarted (reused existing credentials).'); + await finishWithHandoff(collected, completed); + return; + } + } + printIntro(); await confirmPrereqs({ collected, completed }); diff --git a/setup/channels/telegram.ts b/setup/channels/telegram.ts index df97fcf..4659bd6 100644 --- a/setup/channels/telegram.ts +++ b/setup/channels/telegram.ts @@ -132,6 +132,18 @@ export async function runTelegramChannel(displayName: string): Promise { } async function collectTelegramToken(): Promise { + const existing = process.env.TELEGRAM_BOT_TOKEN?.trim(); + if (existing && /^[0-9]+:[A-Za-z0-9_-]{35,}$/.test(existing)) { + const reuse = ensureAnswer(await p.confirm({ + message: `Found an existing Telegram bot token (${existing.slice(0, 8)}…). Use it?`, + initialValue: true, + })); + if (reuse) { + setupLog.userInput('telegram_token', 'reused-existing'); + return existing; + } + } + p.note( [ "Your assistant talks to you through a Telegram bot you create.", diff --git a/setup/environment.ts b/setup/environment.ts index 6986396..c351023 100644 --- a/setup/environment.ts +++ b/setup/environment.ts @@ -11,6 +11,24 @@ import { log } from '../src/log.js'; import { commandExists, getPlatform, isHeadless, isWSL } from './platform.js'; import { emitStatus } from './status.js'; +export function detectExistingDisplayName(projectRoot: string): string | null { + const dbPath = path.join(projectRoot, 'data', 'v2.db'); + if (!fs.existsSync(dbPath)) return null; + + let db: Database.Database | null = null; + try { + db = new Database(dbPath, { readonly: true }); + const row = db + .prepare(`SELECT display_name FROM users WHERE id = 'cli:local'`) + .get() as { display_name: string } | undefined; + return row?.display_name?.trim() || null; + } catch { + return null; + } finally { + db?.close(); + } +} + export function detectRegisteredGroups(projectRoot: string): boolean { if (fs.existsSync(path.join(projectRoot, 'data', 'registered_groups.json'))) { return true; From a014a675561a9d8f893a02b50ee374675b3ed602 Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Wed, 29 Apr 2026 10:34:58 +0000 Subject: [PATCH 4/4] fix password fields not clearing after validation error When pasting an invalid token, the old value stayed in the input field. Pasting a new token appended to the old one instead of replacing it, causing repeated validation failures. Add clearOnError: true to all 8 password prompts across setup. Co-Authored-By: Claude Opus 4.6 (1M context) --- setup/auto.ts | 1 + setup/channels/discord.ts | 1 + setup/channels/imessage.ts | 1 + setup/channels/slack.ts | 2 ++ setup/channels/teams.ts | 1 + setup/channels/telegram.ts | 1 + setup/lib/setup-config-screen.ts | 2 +- 7 files changed, 8 insertions(+), 1 deletion(-) diff --git a/setup/auto.ts b/setup/auto.ts index 4dee7c8..2f333a3 100644 --- a/setup/auto.ts +++ b/setup/auto.ts @@ -706,6 +706,7 @@ async function runPasteAuth(method: 'oauth' | 'api'): Promise { const answer = ensureAnswer( await p.password({ message: `Paste your ${label}`, + clearOnError: true, validate: (v) => { if (!v || !v.trim()) return 'Required'; if (!v.trim().startsWith(prefix)) { diff --git a/setup/channels/discord.ts b/setup/channels/discord.ts index 3668686..74bc9af 100644 --- a/setup/channels/discord.ts +++ b/setup/channels/discord.ts @@ -242,6 +242,7 @@ async function collectDiscordToken(): Promise { const answer = ensureAnswer( await p.password({ message: 'Paste your bot token', + clearOnError: true, validate: (v) => { const t = (v ?? '').trim(); if (!t) return 'Token is required'; diff --git a/setup/channels/imessage.ts b/setup/channels/imessage.ts index d8b129f..fae9fe4 100644 --- a/setup/channels/imessage.ts +++ b/setup/channels/imessage.ts @@ -250,6 +250,7 @@ async function collectRemoteCreds(): Promise { const keyAnswer = ensureAnswer( await p.password({ message: 'Photon API key', + clearOnError: true, validate: (v) => ((v ?? '').trim() ? undefined : 'API key is required'), }), ); diff --git a/setup/channels/slack.ts b/setup/channels/slack.ts index 6d1ff56..9ae86ae 100644 --- a/setup/channels/slack.ts +++ b/setup/channels/slack.ts @@ -154,6 +154,7 @@ async function collectBotToken(): Promise { const answer = ensureAnswer( await p.password({ message: 'Paste your Slack bot token', + clearOnError: true, validate: (v) => { const t = (v ?? '').trim(); if (!t) return 'Token is required'; @@ -175,6 +176,7 @@ async function collectSigningSecret(): Promise { const answer = ensureAnswer( await p.password({ message: 'Paste your Slack signing secret', + clearOnError: true, validate: (v) => { const t = (v ?? '').trim(); if (!t) return 'Signing secret is required'; diff --git a/setup/channels/teams.ts b/setup/channels/teams.ts index fb4d878..2b892bf 100644 --- a/setup/channels/teams.ts +++ b/setup/channels/teams.ts @@ -276,6 +276,7 @@ async function stepClientSecret(args: { const answer = ensureAnswer( await p.password({ message: 'Paste the client secret Value', + clearOnError: true, validate: validateWithHelpEscape((v) => { const t = (v ?? '').trim(); if (!t) return 'Required'; diff --git a/setup/channels/telegram.ts b/setup/channels/telegram.ts index df97fcf..3c670e6 100644 --- a/setup/channels/telegram.ts +++ b/setup/channels/telegram.ts @@ -150,6 +150,7 @@ async function collectTelegramToken(): Promise { const answer = ensureAnswer( await p.password({ message: 'Paste your bot token', + clearOnError: true, validate: (v) => { if (!v || !v.trim()) return "Token is required"; if (!/^[0-9]+:[A-Za-z0-9_-]{35,}$/.test(v.trim())) { diff --git a/setup/lib/setup-config-screen.ts b/setup/lib/setup-config-screen.ts index ad8ae62..88b10d5 100644 --- a/setup/lib/setup-config-screen.ts +++ b/setup/lib/setup-config-screen.ts @@ -115,7 +115,7 @@ async function promptOne(e: Entry, values: ConfigValues): Promise { }; const ans = ensureAnswer( e.secret - ? await p.password({ message: e.label, validate }) + ? await p.password({ message: e.label, clearOnError: true, validate }) : await p.text({ message: e.label, placeholder: e.placeholder ?? e.default,