Skip to content

Commit 0577eaa

Browse files
authored
Merge pull request #74 from moazbuilds/fix/claude_auth
feat(auth): Claude - add credentials watcher with process termination on detection
2 parents 5f7e670 + 599ade2 commit 0577eaa

1 file changed

Lines changed: 96 additions & 25 deletions

File tree

  • src/infra/engines/providers/claude

src/infra/engines/providers/claude/auth.ts

Lines changed: 96 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
displayCliNotInstalledError,
99
isCommandNotFoundError,
1010
ensureAuthDirectory,
11-
createCredentialFile,
1211
cleanupAuthFiles,
1312
getNextAuthAction,
1413
} from '../../core/auth.js';
@@ -74,6 +73,86 @@ export async function isAuthenticated(options?: ClaudeAuthOptions): Promise<bool
7473
}
7574
}
7675

76+
/**
77+
* Polls for credentials file and terminates process when found
78+
*/
79+
function watchForCredentials(
80+
credPath: string,
81+
proc: ReturnType<typeof Bun.spawn>,
82+
timeoutMs: number = 600000,
83+
pollIntervalMs: number = 500
84+
): Promise<boolean> {
85+
return new Promise((resolve) => {
86+
let resolved = false;
87+
let pollInterval: ReturnType<typeof setInterval> | null = null;
88+
89+
const cleanup = () => {
90+
if (pollInterval) {
91+
clearInterval(pollInterval);
92+
pollInterval = null;
93+
}
94+
clearTimeout(timeout);
95+
};
96+
97+
const finish = (success: boolean) => {
98+
if (resolved) return;
99+
resolved = true;
100+
cleanup();
101+
resolve(success);
102+
};
103+
104+
// Overall timeout
105+
const timeout = setTimeout(async () => {
106+
if (resolved) return;
107+
proc.kill('SIGTERM');
108+
finish(false);
109+
}, timeoutMs);
110+
111+
// Poll for credential file
112+
pollInterval = setInterval(async () => {
113+
if (resolved) return;
114+
115+
try {
116+
await stat(credPath);
117+
} catch {
118+
return; // File doesn't exist yet
119+
}
120+
121+
// File exists - wait for it to be fully written
122+
await new Promise((r) => setTimeout(r, 200));
123+
124+
// Verify file still exists
125+
try {
126+
await stat(credPath);
127+
} catch {
128+
return; // File disappeared
129+
}
130+
131+
console.log('\nAuthentication detected, closing Claude...\n');
132+
proc.kill('SIGTERM');
133+
134+
// Fallback to SIGKILL if process doesn't exit
135+
const killTimeout = setTimeout(() => proc.kill('SIGKILL'), 2000);
136+
await proc.exited;
137+
clearTimeout(killTimeout);
138+
139+
finish(true);
140+
}, pollIntervalMs);
141+
142+
// Also resolve if process exits on its own
143+
proc.exited.then(async () => {
144+
if (resolved) return;
145+
// Check if credentials exist after natural exit
146+
try {
147+
await stat(credPath);
148+
finish(true);
149+
} catch {
150+
finish(false);
151+
}
152+
});
153+
});
154+
}
155+
77156
/**
78157
* Ensures Claude is authenticated, running setup-token if needed
79158
*/
@@ -101,25 +180,36 @@ export async function ensureAuth(options?: ClaudeAuthOptions): Promise<boolean>
101180
throw new Error(`${metadata.name} CLI is not installed.`);
102181
}
103182

104-
// Run interactive setup-token via Claude CLI with proper env
183+
// Ensure config directory exists for watcher
184+
await ensureAuthDirectory(configDir);
185+
105186
console.log(`\nRunning Claude authentication...\n`);
106187
console.log(`Config directory: ${configDir}\n`);
107188

108189
try {
109-
// Resolve claude command to handle Windows .cmd files
110190
const resolvedClaude = Bun.which('claude') ?? 'claude';
111191

112-
const proc = Bun.spawn([resolvedClaude, 'setup-token'], {
192+
// Spawn claude (not setup-token) - interactive
193+
const proc = Bun.spawn([resolvedClaude], {
113194
env: { ...process.env, CLAUDE_CONFIG_DIR: configDir },
114195
stdio: ['inherit', 'inherit', 'inherit'],
115196
});
116-
await proc.exited;
197+
198+
// Poll for credentials and terminate on success
199+
const success = await watchForCredentials(credPath, proc);
200+
201+
if (success) {
202+
return true;
203+
}
204+
205+
// Auth failed or timed out
206+
throw new Error('Authentication timed out or was not completed.');
117207
} catch (error) {
118208
if (isCommandNotFoundError(error)) {
119209
console.error(`\n────────────────────────────────────────────────────────────`);
120210
console.error(` ⚠️ ${metadata.name} CLI Not Found`);
121211
console.error(`────────────────────────────────────────────────────────────`);
122-
console.error(`\n'${metadata.cliBinary} setup-token' failed because the CLI is missing.`);
212+
console.error(`\n'${metadata.cliBinary}' failed because the CLI is missing.`);
123213
console.error(`Please install ${metadata.name} CLI before trying again:\n`);
124214
console.error(` ${metadata.installCommand}\n`);
125215
console.error(`────────────────────────────────────────────────────────────\n`);
@@ -128,25 +218,6 @@ export async function ensureAuth(options?: ClaudeAuthOptions): Promise<boolean>
128218

129219
throw error;
130220
}
131-
132-
// Verify the credentials were created
133-
try {
134-
await stat(credPath);
135-
return true;
136-
} catch {
137-
// Credentials file wasn't created - Claude CLI returned token instead
138-
console.error(`\n────────────────────────────────────────────────────────────`);
139-
console.error(` ℹ️ Claude CLI Authentication Notice`);
140-
console.error(`────────────────────────────────────────────────────────────`);
141-
console.error(`\nYour Claude CLI installation uses token-based authentication.`);
142-
console.error(`Please set the token you received as an environment variable:\n`);
143-
console.error(` export CODEMACHINE_CLAUDE_OAUTH_TOKEN=<your-token>\n`);
144-
console.error(`For persistence, add this line to your shell configuration:`);
145-
console.error(` ~/.bashrc (Bash) or ~/.zshrc (Zsh)\n`);
146-
console.error(`────────────────────────────────────────────────────────────\n`);
147-
148-
throw new Error('Authentication incomplete. Please set CODEMACHINE_CLAUDE_OAUTH_TOKEN environment variable.');
149-
}
150221
}
151222

152223
/**

0 commit comments

Comments
 (0)