Skip to content

Commit e1e8422

Browse files
authored
Merge pull request #31 from AutoMaker-Org/feat/enchance-welcome-page-setup
feat: enchance welcome page setup
2 parents 7ff97a4 + 13de313 commit e1e8422

28 files changed

Lines changed: 3682 additions & 1819 deletions

app/README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ cd automaker
3333
npm install
3434
```
3535

36+
### Windows notes (in-app Claude auth)
37+
38+
- Node.js 22.x
39+
- Prebuilt PTY is bundled; Visual Studio build tools are not required for Claude auth.
40+
- If you prefer the external terminal flow, set `CLAUDE_AUTH_DISABLE_PTY=1`.
41+
- If you later add native modules beyond the prebuilt PTY, you may still need VS Build Tools + Python to rebuild those.
42+
3643
**Step 3:** Run the Claude Code setup token command:
3744

3845
```bash
@@ -55,7 +62,15 @@ npm run dev:electron
5562

5663
This will start both the Next.js development server and the Electron application.
5764

58-
**Step 6:** MOST IMPORANT: Run the Following after all is setup
65+
### Auth smoke test (Windows)
66+
67+
1. Ensure dependencies are installed (prebuilt pty is included).
68+
2. Run `npm run dev:electron` and open the Setup modal.
69+
3. Click Start on Claude auth; watch the embedded terminal stream logs.
70+
4. Successful runs show “Token captured automatically.”; otherwise copy/paste the token from the log.
71+
5. Optional: `node --test tests/claude-cli-detector.test.js` to verify token parsing.
72+
73+
**Step 6:** MOST IMPORTANT: Run the Following after all is setup
5974

6075
```bash
6176
echo "W"

app/electron/services/claude-cli-detector.js

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ const fs = require("fs");
33
const path = require("path");
44
const os = require("os");
55

6+
let runPtyCommand = null;
7+
try {
8+
({ runPtyCommand } = require("./pty-runner"));
9+
} catch (error) {
10+
console.warn(
11+
"[ClaudeCliDetector] node-pty unavailable, will fall back to external terminal:",
12+
error?.message || error
13+
);
14+
}
15+
16+
const ANSI_REGEX =
17+
// eslint-disable-next-line no-control-regex
18+
/\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b[@-_]|\u001b\][^\u0007]*\u0007/g;
19+
20+
const stripAnsi = (text = "") => text.replace(ANSI_REGEX, "");
21+
622
/**
723
* Claude CLI Detector
824
*
@@ -459,6 +475,247 @@ class ClaudeCliDetector {
459475
note: "This token is from your Claude subscription and allows you to use Claude without API charges.",
460476
};
461477
}
478+
479+
/**
480+
* Extract OAuth token from command output
481+
* Tries multiple patterns to find the token
482+
* @param {string} output The command output
483+
* @returns {string|null} Extracted token or null
484+
*/
485+
static extractTokenFromOutput(output) {
486+
// Pattern 1: CLAUDE_CODE_OAUTH_TOKEN=<token> or CLAUDE_CODE_OAUTH_TOKEN: <token>
487+
const envMatch = output.match(
488+
/CLAUDE_CODE_OAUTH_TOKEN[=:]\s*["']?([a-zA-Z0-9_\-\.]+)["']?/i
489+
);
490+
if (envMatch) return envMatch[1];
491+
492+
// Pattern 2: "Token: <token>" or "token: <token>"
493+
const tokenLabelMatch = output.match(
494+
/\btoken[:\s]+["']?([a-zA-Z0-9_\-\.]{40,})["']?/i
495+
);
496+
if (tokenLabelMatch) return tokenLabelMatch[1];
497+
498+
// Pattern 3: Look for token after success/authenticated message
499+
const successMatch = output.match(
500+
/(?:success|authenticated|generated|token is)[^\n]*\n\s*([a-zA-Z0-9_\-\.]{40,})/i
501+
);
502+
if (successMatch) return successMatch[1];
503+
504+
// Pattern 4: Standalone long alphanumeric string on its own line (last resort)
505+
// This catches tokens that are printed on their own line
506+
const lines = output.split("\n");
507+
for (const line of lines) {
508+
const trimmed = line.trim();
509+
// Token should be 40+ chars, alphanumeric with possible hyphens/underscores/dots
510+
if (/^[a-zA-Z0-9_\-\.]{40,}$/.test(trimmed)) {
511+
return trimmed;
512+
}
513+
}
514+
515+
return null;
516+
}
517+
518+
/**
519+
* Run claude setup-token command to generate OAuth token
520+
* Opens an external terminal window since Claude CLI requires TTY for its Ink-based UI
521+
* @param {Function} onProgress Callback for progress updates
522+
* @returns {Promise<Object>} Result indicating terminal was opened
523+
*/
524+
static async runSetupToken(onProgress) {
525+
const detection = this.detectClaudeInstallation();
526+
527+
if (!detection.installed) {
528+
throw {
529+
success: false,
530+
error: "Claude CLI is not installed. Please install it first.",
531+
requiresManualAuth: false,
532+
};
533+
}
534+
535+
const claudePath = detection.path;
536+
const platform = process.platform;
537+
const preferPty =
538+
(platform === "win32" ||
539+
platform === "darwin" ||
540+
process.env.CLAUDE_AUTH_FORCE_PTY === "1") &&
541+
process.env.CLAUDE_AUTH_DISABLE_PTY !== "1";
542+
543+
const send = (data) => {
544+
if (onProgress && data) {
545+
onProgress({ type: "stdout", data });
546+
}
547+
};
548+
549+
if (preferPty && runPtyCommand) {
550+
try {
551+
send("Starting in-app terminal session for Claude auth...\n");
552+
send("If your browser opens, complete sign-in and return here.\n\n");
553+
554+
const ptyResult = await runPtyCommand(claudePath, ["setup-token"], {
555+
cols: 120,
556+
rows: 30,
557+
onData: (chunk) => send(chunk),
558+
env: {
559+
FORCE_COLOR: "1",
560+
},
561+
});
562+
563+
const cleanedOutput = stripAnsi(ptyResult.output || "");
564+
const token = this.extractTokenFromOutput(cleanedOutput);
565+
566+
if (ptyResult.success && token) {
567+
send("\nCaptured token automatically.\n");
568+
return {
569+
success: true,
570+
token,
571+
requiresManualAuth: false,
572+
terminalOpened: false,
573+
};
574+
}
575+
576+
if (ptyResult.success && !token) {
577+
send(
578+
"\nCLI completed but token was not detected automatically. You can copy it above or retry.\n"
579+
);
580+
return {
581+
success: true,
582+
requiresManualAuth: true,
583+
terminalOpened: false,
584+
error: "Could not capture token automatically",
585+
output: cleanedOutput,
586+
};
587+
}
588+
589+
send(
590+
`\nClaude CLI exited with code ${ptyResult.exitCode}. Falling back to manual copy.\n`
591+
);
592+
return {
593+
success: false,
594+
error: `Claude CLI exited with code ${ptyResult.exitCode}`,
595+
requiresManualAuth: true,
596+
output: cleanedOutput,
597+
};
598+
} catch (error) {
599+
console.error("[ClaudeCliDetector] PTY auth failed, falling back:", error);
600+
send(
601+
`In-app terminal failed (${error?.message || "unknown error"}). Falling back to external terminal...\n`
602+
);
603+
}
604+
}
605+
606+
// Fallback: external terminal window
607+
if (preferPty && !runPtyCommand) {
608+
send("In-app terminal unavailable (node-pty not loaded).");
609+
} else if (!preferPty) {
610+
send("Using system terminal for authentication on this platform.");
611+
}
612+
send("Opening system terminal for authentication...\n");
613+
614+
// Helper function to check if a command exists asynchronously
615+
const commandExists = (cmd) => {
616+
return new Promise((resolve) => {
617+
require("child_process").exec(
618+
`which ${cmd}`,
619+
{ timeout: 1000 },
620+
(error) => {
621+
resolve(!error);
622+
}
623+
);
624+
});
625+
};
626+
627+
// For Linux, find available terminal first (async)
628+
let linuxTerminal = null;
629+
if (platform !== "win32" && platform !== "darwin") {
630+
const terminals = [
631+
["gnome-terminal", ["--", claudePath, "setup-token"]],
632+
["konsole", ["-e", claudePath, "setup-token"]],
633+
["xterm", ["-e", claudePath, "setup-token"]],
634+
["x-terminal-emulator", ["-e", `${claudePath} setup-token`]],
635+
];
636+
637+
for (const [term, termArgs] of terminals) {
638+
const exists = await commandExists(term);
639+
if (exists) {
640+
linuxTerminal = { command: term, args: termArgs };
641+
break;
642+
}
643+
}
644+
}
645+
646+
return new Promise((resolve, reject) => {
647+
// Open command in external terminal since Claude CLI requires TTY
648+
let command, args;
649+
650+
if (platform === "win32") {
651+
// Windows: Open new cmd window that stays open
652+
command = "cmd";
653+
args = ["/c", "start", "cmd", "/k", `"${claudePath}" setup-token`];
654+
} else if (platform === "darwin") {
655+
// macOS: Open Terminal.app
656+
command = "osascript";
657+
args = [
658+
"-e",
659+
`tell application "Terminal" to do script "${claudePath} setup-token"`,
660+
"-e",
661+
'tell application "Terminal" to activate',
662+
];
663+
} else {
664+
// Linux: Use the terminal we found earlier
665+
if (!linuxTerminal) {
666+
reject({
667+
success: false,
668+
error:
669+
"Could not find a terminal emulator. Please run 'claude setup-token' manually in your terminal.",
670+
requiresManualAuth: true,
671+
});
672+
return;
673+
}
674+
command = linuxTerminal.command;
675+
args = linuxTerminal.args;
676+
}
677+
678+
console.log(
679+
"[ClaudeCliDetector] Spawning terminal:",
680+
command,
681+
args.join(" ")
682+
);
683+
684+
const proc = spawn(command, args, {
685+
detached: true,
686+
stdio: "ignore",
687+
shell: platform === "win32",
688+
});
689+
690+
proc.unref();
691+
692+
proc.on("error", (error) => {
693+
console.error("[ClaudeCliDetector] Failed to open terminal:", error);
694+
reject({
695+
success: false,
696+
error: `Failed to open terminal: ${error.message}`,
697+
requiresManualAuth: true,
698+
});
699+
});
700+
701+
// Give the terminal a moment to open
702+
setTimeout(() => {
703+
send("Terminal window opened!\n\n");
704+
send("1. Complete the sign-in in your browser\n");
705+
send("2. Copy the token from the terminal\n");
706+
send("3. Paste it below\n");
707+
708+
// Resolve with manual auth required since we can't capture from external terminal
709+
resolve({
710+
success: true,
711+
requiresManualAuth: true,
712+
terminalOpened: true,
713+
message:
714+
"Terminal opened. Complete authentication and paste the token below.",
715+
});
716+
}, 500);
717+
});
718+
}
462719
}
463720

464721
module.exports = ClaudeCliDetector;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const os = require("os");
2+
3+
// Prefer prebuilt to avoid native build issues.
4+
const pty = require("@homebridge/node-pty-prebuilt-multiarch");
5+
6+
/**
7+
* Minimal PTY helper to run CLI commands with a pseudo-terminal.
8+
* Useful for CLIs (like Claude) that need raw mode on Windows.
9+
*
10+
* @param {string} command Executable path
11+
* @param {string[]} args Arguments for the executable
12+
* @param {Object} options Additional spawn options
13+
* @param {(chunk: string) => void} [options.onData] Data callback
14+
* @param {string} [options.cwd] Working directory
15+
* @param {Object} [options.env] Extra env vars
16+
* @param {number} [options.cols] Terminal columns
17+
* @param {number} [options.rows] Terminal rows
18+
* @returns {Promise<{ success: boolean, exitCode: number, signal?: number, output: string, errorOutput: string }>}
19+
*/
20+
function runPtyCommand(command, args = [], options = {}) {
21+
const {
22+
onData,
23+
cwd = process.cwd(),
24+
env = {},
25+
cols = 120,
26+
rows = 30,
27+
} = options;
28+
29+
const mergedEnv = {
30+
...process.env,
31+
TERM: process.env.TERM || "xterm-256color",
32+
...env,
33+
};
34+
35+
return new Promise((resolve, reject) => {
36+
let ptyProcess;
37+
38+
try {
39+
ptyProcess = pty.spawn(command, args, {
40+
name: os.platform() === "win32" ? "Windows.Terminal" : "xterm-color",
41+
cols,
42+
rows,
43+
cwd,
44+
env: mergedEnv,
45+
useConpty: true,
46+
});
47+
} catch (error) {
48+
return reject(error);
49+
}
50+
51+
let output = "";
52+
let errorOutput = "";
53+
54+
ptyProcess.onData((data) => {
55+
output += data;
56+
if (typeof onData === "function") {
57+
onData(data);
58+
}
59+
});
60+
61+
// node-pty does not emit 'error' in practice, but guard anyway
62+
if (ptyProcess.on) {
63+
ptyProcess.on("error", (err) => {
64+
errorOutput += err?.message || "";
65+
reject(err);
66+
});
67+
}
68+
69+
ptyProcess.onExit(({ exitCode, signal }) => {
70+
resolve({
71+
success: exitCode === 0,
72+
exitCode,
73+
signal,
74+
output,
75+
errorOutput,
76+
});
77+
});
78+
});
79+
}
80+
81+
module.exports = {
82+
runPtyCommand,
83+
};
84+

0 commit comments

Comments
 (0)