Merge pull request #2098 from Koshkoshinsk/setup-token-headless
fix claude setup-token flow for headless/remote systems
This commit is contained in:
@@ -2,8 +2,11 @@
|
||||
* 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 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*
|
||||
@@ -16,8 +19,9 @@
|
||||
*
|
||||
* 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 os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import * as p from '@clack/prompts';
|
||||
@@ -90,7 +94,7 @@ export async function offerClaudeAssist(
|
||||
projectRoot: string = process.cwd(),
|
||||
): Promise<boolean> {
|
||||
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 +132,101 @@ 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<boolean> {
|
||||
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;
|
||||
|
||||
// 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',
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
p.log.success('Claude CLI signed in.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user