mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat: implement OAuth token setup for Claude CLI
- Added a new SetupTokenModal component for handling OAuth token authentication. - Integrated in-app terminal support using node-pty for seamless user experience. - Updated ClaudeCliDetector to extract tokens from command output and handle authentication flow. - Enhanced README with Windows-specific notes and authentication instructions. - Updated package.json and package-lock.json to include necessary dependencies for the new functionality.
This commit is contained in:
@@ -3,6 +3,22 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
|
||||
let runPtyCommand = null;
|
||||
try {
|
||||
({ runPtyCommand } = require("./pty-runner"));
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[ClaudeCliDetector] node-pty unavailable, will fall back to external terminal:",
|
||||
error?.message || error
|
||||
);
|
||||
}
|
||||
|
||||
const ANSI_REGEX =
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b[@-_]|\u001b\][^\u0007]*\u0007/g;
|
||||
|
||||
const stripAnsi = (text = "") => text.replace(ANSI_REGEX, "");
|
||||
|
||||
/**
|
||||
* Claude CLI Detector
|
||||
*
|
||||
@@ -459,6 +475,231 @@ class ClaudeCliDetector {
|
||||
note: "This token is from your Claude subscription and allows you to use Claude without API charges.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract OAuth token from command output
|
||||
* Tries multiple patterns to find the token
|
||||
* @param {string} output The command output
|
||||
* @returns {string|null} Extracted token or null
|
||||
*/
|
||||
static extractTokenFromOutput(output) {
|
||||
// Pattern 1: CLAUDE_CODE_OAUTH_TOKEN=<token> or CLAUDE_CODE_OAUTH_TOKEN: <token>
|
||||
const envMatch = output.match(
|
||||
/CLAUDE_CODE_OAUTH_TOKEN[=:]\s*["']?([a-zA-Z0-9_\-\.]+)["']?/i
|
||||
);
|
||||
if (envMatch) return envMatch[1];
|
||||
|
||||
// Pattern 2: "Token: <token>" or "token: <token>"
|
||||
const tokenLabelMatch = output.match(
|
||||
/\btoken[:\s]+["']?([a-zA-Z0-9_\-\.]{40,})["']?/i
|
||||
);
|
||||
if (tokenLabelMatch) return tokenLabelMatch[1];
|
||||
|
||||
// Pattern 3: Look for token after success/authenticated message
|
||||
const successMatch = output.match(
|
||||
/(?:success|authenticated|generated|token is)[^\n]*\n\s*([a-zA-Z0-9_\-\.]{40,})/i
|
||||
);
|
||||
if (successMatch) return successMatch[1];
|
||||
|
||||
// Pattern 4: Standalone long alphanumeric string on its own line (last resort)
|
||||
// This catches tokens that are printed on their own line
|
||||
const lines = output.split("\n");
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Token should be 40+ chars, alphanumeric with possible hyphens/underscores/dots
|
||||
if (/^[a-zA-Z0-9_\-\.]{40,}$/.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run claude setup-token command to generate OAuth token
|
||||
* Opens an external terminal window since Claude CLI requires TTY for its Ink-based UI
|
||||
* @param {Function} onProgress Callback for progress updates
|
||||
* @returns {Promise<Object>} Result indicating terminal was opened
|
||||
*/
|
||||
static async runSetupToken(onProgress) {
|
||||
const detection = this.detectClaudeInstallation();
|
||||
|
||||
if (!detection.installed) {
|
||||
throw {
|
||||
success: false,
|
||||
error: "Claude CLI is not installed. Please install it first.",
|
||||
requiresManualAuth: false,
|
||||
};
|
||||
}
|
||||
|
||||
const claudePath = detection.path;
|
||||
const platform = process.platform;
|
||||
const preferPty =
|
||||
(platform === "win32" ||
|
||||
process.env.CLAUDE_AUTH_FORCE_PTY === "1") &&
|
||||
process.env.CLAUDE_AUTH_DISABLE_PTY !== "1";
|
||||
|
||||
const send = (data) => {
|
||||
if (onProgress && data) {
|
||||
onProgress({ type: "stdout", data });
|
||||
}
|
||||
};
|
||||
|
||||
if (preferPty && runPtyCommand) {
|
||||
try {
|
||||
send("Starting in-app terminal session for Claude auth...\n");
|
||||
send("If your browser opens, complete sign-in and return here.\n\n");
|
||||
|
||||
const ptyResult = await runPtyCommand(claudePath, ["setup-token"], {
|
||||
cols: 120,
|
||||
rows: 30,
|
||||
onData: (chunk) => send(chunk),
|
||||
env: {
|
||||
FORCE_COLOR: "1",
|
||||
},
|
||||
});
|
||||
|
||||
const cleanedOutput = stripAnsi(ptyResult.output || "");
|
||||
const token = this.extractTokenFromOutput(cleanedOutput);
|
||||
|
||||
if (ptyResult.success && token) {
|
||||
send("\nCaptured token automatically.\n");
|
||||
return {
|
||||
success: true,
|
||||
token,
|
||||
requiresManualAuth: false,
|
||||
terminalOpened: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (ptyResult.success && !token) {
|
||||
send(
|
||||
"\nCLI completed but token was not detected automatically. You can copy it above or retry.\n"
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
terminalOpened: false,
|
||||
error: "Could not capture token automatically",
|
||||
output: cleanedOutput,
|
||||
};
|
||||
}
|
||||
|
||||
send(
|
||||
`\nClaude CLI exited with code ${ptyResult.exitCode}. Falling back to manual copy.\n`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: `Claude CLI exited with code ${ptyResult.exitCode}`,
|
||||
requiresManualAuth: true,
|
||||
output: cleanedOutput,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[ClaudeCliDetector] PTY auth failed, falling back:", error);
|
||||
send(
|
||||
`In-app terminal failed (${error?.message || "unknown error"}). Falling back to external terminal...\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: external terminal window
|
||||
if (preferPty && !runPtyCommand) {
|
||||
send("In-app terminal unavailable (node-pty not loaded).");
|
||||
} else if (!preferPty) {
|
||||
send("Using system terminal for authentication on this platform.");
|
||||
}
|
||||
send("Opening system terminal for authentication...\n");
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
// Open command in external terminal since Claude CLI requires TTY
|
||||
let command, args;
|
||||
|
||||
if (platform === "win32") {
|
||||
// Windows: Open new cmd window that stays open
|
||||
command = "cmd";
|
||||
args = ["/c", "start", "cmd", "/k", `"${claudePath}" setup-token`];
|
||||
} else if (platform === "darwin") {
|
||||
// macOS: Open Terminal.app
|
||||
command = "osascript";
|
||||
args = [
|
||||
"-e",
|
||||
`tell application "Terminal" to do script "${claudePath} setup-token"`,
|
||||
"-e",
|
||||
'tell application "Terminal" to activate',
|
||||
];
|
||||
} else {
|
||||
// Linux: Try common terminal emulators
|
||||
const terminals = [
|
||||
["gnome-terminal", ["--", claudePath, "setup-token"]],
|
||||
["konsole", ["-e", claudePath, "setup-token"]],
|
||||
["xterm", ["-e", claudePath, "setup-token"]],
|
||||
["x-terminal-emulator", ["-e", `${claudePath} setup-token`]],
|
||||
];
|
||||
|
||||
// Try to find an available terminal
|
||||
for (const [term, termArgs] of terminals) {
|
||||
try {
|
||||
execSync(`which ${term}`, { stdio: "ignore" });
|
||||
command = term;
|
||||
args = termArgs;
|
||||
break;
|
||||
} catch {
|
||||
// Terminal not found, try next
|
||||
}
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
reject({
|
||||
success: false,
|
||||
error:
|
||||
"Could not find a terminal emulator. Please run 'claude setup-token' manually in your terminal.",
|
||||
requiresManualAuth: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[ClaudeCliDetector] Spawning terminal:",
|
||||
command,
|
||||
args.join(" ")
|
||||
);
|
||||
|
||||
const proc = spawn(command, args, {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
shell: platform === "win32",
|
||||
});
|
||||
|
||||
proc.unref();
|
||||
|
||||
proc.on("error", (error) => {
|
||||
console.error("[ClaudeCliDetector] Failed to open terminal:", error);
|
||||
reject({
|
||||
success: false,
|
||||
error: `Failed to open terminal: ${error.message}`,
|
||||
requiresManualAuth: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Give the terminal a moment to open
|
||||
setTimeout(() => {
|
||||
send("Terminal window opened!\n\n");
|
||||
send("1. Complete the sign-in in your browser\n");
|
||||
send("2. Copy the token from the terminal\n");
|
||||
send("3. Paste it below\n");
|
||||
|
||||
// Resolve with manual auth required since we can't capture from external terminal
|
||||
resolve({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
terminalOpened: true,
|
||||
message:
|
||||
"Terminal opened. Complete authentication and paste the token below.",
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClaudeCliDetector;
|
||||
|
||||
Reference in New Issue
Block a user