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/2] 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/2] 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; }