Skip to content

Commit 230a15e

Browse files
committed
Fix Claude script authentication and workflow detection
- Add automatic authentication flows for Claude and GitHub CLI - Improve GitHub workflow detection to handle race conditions - Fix ESLint issues with proper comment placement - Use file-level eslint-disable for no-await-in-loop - Update workflow detection to check recent runs for commit SHA
1 parent f8071ee commit 230a15e

File tree

1 file changed

+207
-29
lines changed

1 file changed

+207
-29
lines changed

scripts/claude.mjs

Lines changed: 207 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable no-await-in-loop */
2+
13
/**
24
* @fileoverview Claude Code-powered utilities for Socket projects.
35
* Provides various AI-assisted development tools and automations using Claude Code CLI.
@@ -173,6 +175,140 @@ async function checkClaude() {
173175
return false
174176
}
175177

178+
/**
179+
* Ensure Claude Code is authenticated, prompting for login if needed.
180+
* Returns true if authenticated, false if unable to authenticate.
181+
*/
182+
async function ensureClaudeAuthenticated(claudeCmd) {
183+
let attempts = 0
184+
const maxAttempts = 3
185+
186+
while (attempts < maxAttempts) {
187+
// Check if Claude is working by checking version
188+
log.progress('Checking Claude Code status')
189+
const versionCheck = await runCommandWithOutput(claudeCmd, ['--version'])
190+
191+
if (versionCheck.exitCode === 0) {
192+
// Claude Code is installed and working
193+
// Check if we need to login by looking for specific error patterns
194+
const testPrompt = 'echo "test"'
195+
const testResult = await runCommandWithOutput(claudeCmd, [], {
196+
input: testPrompt,
197+
stdio: ['pipe', 'pipe', 'pipe'],
198+
env: { ...process.env, CLAUDE_OUTPUT_MODE: 'text' },
199+
timeout: 10000
200+
})
201+
202+
// Check for authentication errors
203+
const output = (testResult.stdout + testResult.stderr).toLowerCase()
204+
const needsAuth = output.includes('not logged in') ||
205+
output.includes('authentication') ||
206+
output.includes('unauthorized') ||
207+
output.includes('login required') ||
208+
output.includes('please login')
209+
210+
if (!needsAuth && (testResult.exitCode === 0 || testResult.stdout.length > 0)) {
211+
log.done('Claude Code ready')
212+
return true
213+
}
214+
215+
if (!needsAuth) {
216+
// Claude seems to be working, even if the test had an odd response
217+
log.done('Claude Code ready')
218+
return true
219+
}
220+
}
221+
222+
attempts++
223+
224+
if (attempts >= maxAttempts) {
225+
log.error(`Failed to setup Claude Code after ${maxAttempts} attempts`)
226+
return false
227+
}
228+
229+
// Not authenticated, prompt for login
230+
log.warn('Claude Code login required')
231+
console.log(colors.yellow('\nClaude Code needs to be authenticated.'))
232+
console.log('This will open your browser for authentication.\n')
233+
234+
// Run claude login interactively
235+
log.progress('Starting Claude Code login process')
236+
const loginResult = await runCommand(claudeCmd, ['--login'], {
237+
stdio: 'inherit'
238+
})
239+
240+
if (loginResult === 0) {
241+
log.done('Login process completed')
242+
// Give it a moment for the auth to register
243+
await new Promise(resolve => setTimeout(resolve, 3000))
244+
} else {
245+
log.failed('Login process failed or was cancelled')
246+
247+
if (attempts < maxAttempts) {
248+
console.log(colors.yellow(`\nWould you like to try again?`))
249+
console.log(colors.gray(`Attempt ${attempts} of ${maxAttempts}`))
250+
// Wait a bit before retrying
251+
await new Promise(resolve => setTimeout(resolve, 2000))
252+
}
253+
}
254+
}
255+
256+
return false
257+
}
258+
259+
/**
260+
* Ensure GitHub CLI is authenticated, prompting for login if needed.
261+
* Returns true if authenticated, false if unable to authenticate.
262+
*/
263+
async function ensureGitHubAuthenticated() {
264+
let attempts = 0
265+
const maxAttempts = 3
266+
267+
while (attempts < maxAttempts) {
268+
log.progress('Checking GitHub authentication')
269+
const authCheck = await runCommandWithOutput('gh', ['auth', 'status'])
270+
271+
if (authCheck.exitCode === 0) {
272+
log.done('GitHub CLI authenticated')
273+
return true
274+
}
275+
276+
attempts++
277+
278+
if (attempts >= maxAttempts) {
279+
log.error(`Failed to authenticate with GitHub after ${maxAttempts} attempts`)
280+
return false
281+
}
282+
283+
// Not authenticated, prompt for login
284+
log.warn('GitHub authentication required')
285+
console.log(colors.yellow('\nYou need to authenticate with GitHub.'))
286+
console.log('Follow the prompts to complete authentication.\n')
287+
288+
// Run gh auth login interactively
289+
log.progress('Starting GitHub login process')
290+
const loginResult = await runCommand('gh', ['auth', 'login'], {
291+
stdio: 'inherit'
292+
})
293+
294+
if (loginResult === 0) {
295+
log.done('Login process completed')
296+
// Give it a moment for the auth to register
297+
await new Promise(resolve => setTimeout(resolve, 2000))
298+
} else {
299+
log.failed('Login process failed')
300+
console.log(colors.red('\nLogin failed. Please try again.'))
301+
302+
if (attempts < maxAttempts) {
303+
console.log(colors.yellow(`\nAttempt ${attempts + 1} of ${maxAttempts}`))
304+
await new Promise(resolve => setTimeout(resolve, 1000))
305+
}
306+
}
307+
}
308+
309+
return false
310+
}
311+
176312
/**
177313
* Prepare Claude command arguments for Claude Code.
178314
* Claude Code uses natural language prompts, not the same flags.
@@ -608,7 +744,8 @@ async function scanProjectForIssues(claudeCmd, project, options = {}) {
608744
const extensions = ['.js', '.mjs', '.ts', '.mts', '.jsx', '.tsx']
609745

610746
async function findFiles(dir, depth = 0) {
611-
if (depth > 5) {return} // Limit depth to avoid excessive scanning.
747+
// Limit depth to avoid excessive scanning
748+
if (depth > 5) {return}
612749

613750
const entries = await fs.readdir(dir, { withFileTypes: true })
614751

@@ -691,7 +828,8 @@ Provide ONLY the JSON array, nothing else.`
691828
const result = await runCommandWithOutput(claudeCmd, prepareClaudeArgs([], options), {
692829
input: prompt,
693830
stdio: ['pipe', 'pipe', 'pipe'],
694-
maxBuffer: 1024 * 1024 * 10 // 10MB buffer for large responses.
831+
// 10MB buffer for large responses
832+
maxBuffer: 1024 * 1024 * 10
695833
})
696834

697835
if (result.exitCode !== 0) {
@@ -703,7 +841,7 @@ Provide ONLY the JSON array, nothing else.`
703841

704842
try {
705843
return JSON.parse(result.stdout.trim())
706-
} catch (_e) {
844+
} catch {
707845
log.warn(`Failed to parse scan results for ${name}`)
708846
return null
709847
}
@@ -1665,7 +1803,8 @@ Fix this issue now by making the necessary changes.`
16651803

16661804
// Run Claude non-interactively with timeout and progress
16671805
const startTime = Date.now()
1668-
const timeout = 120000 // 2 minute timeout
1806+
// 2 minute timeout
1807+
const timeout = 120000
16691808
log.substep(`[${repoName}] Analyzing error...`)
16701809

16711810
const claudeProcess = spawn(claudeCmd, prepareClaudeArgs([], opts), {
@@ -1686,7 +1825,8 @@ Fix this issue now by making the necessary changes.`
16861825
} else {
16871826
log.substep(`[${repoName}] Claude working... (${Math.round(elapsed/1000)}s)`)
16881827
}
1689-
}, 10000) // Update every 10 seconds
1828+
}, 10000)
1829+
// Update every 10 seconds
16901830

16911831
await new Promise((resolve) => {
16921832
claudeProcess.on('close', () => {
@@ -1732,7 +1872,8 @@ Let's work through this together to get CI passing.`
17321872
await runCommand(claudeCmd, prepareClaudeArgs([], opts), {
17331873
input: interactivePrompt,
17341874
cwd: rootPath,
1735-
stdio: 'inherit' // Interactive mode
1875+
// Interactive mode
1876+
stdio: 'inherit'
17361877
})
17371878

17381879
// Try once more after interactive session
@@ -1801,7 +1942,7 @@ Let's work through this together to get CI passing.`
18011942
return true
18021943
}
18031944

1804-
// Check for GitHub CLI and authentication
1945+
// Check for GitHub CLI
18051946
const ghCheck = await runCommandWithOutput('which', ['gh'])
18061947
if (ghCheck.exitCode !== 0) {
18071948
log.error('GitHub CLI (gh) is required for CI monitoring')
@@ -1818,25 +1959,14 @@ Let's work through this together to get CI passing.`
18181959
return false
18191960
}
18201961

1821-
// Check if gh is authenticated
1822-
log.progress('Checking GitHub authentication')
1823-
const authCheck = await runCommandWithOutput('gh', ['auth', 'status'])
1824-
if (authCheck.exitCode !== 0) {
1825-
log.failed('GitHub CLI is not authenticated')
1826-
console.log(colors.yellow('\nYou need to authenticate with GitHub:'))
1827-
console.log(` 1. Run: ${colors.green('gh auth login')}`)
1828-
console.log(' 2. Choose "GitHub.com"')
1829-
console.log(' 3. Choose your preferred authentication method')
1830-
console.log(' 4. Follow the prompts to complete authentication')
1831-
console.log(` 5. Try again: ${colors.green('pnpm claude --green')}`)
1832-
1833-
if (authCheck.stderr) {
1834-
console.log(colors.gray('\nError details:'))
1835-
console.log(colors.gray(authCheck.stderr))
1836-
}
1962+
// Ensure GitHub is authenticated (will handle login automatically)
1963+
const isGitHubAuthenticated = await ensureGitHubAuthenticated()
1964+
if (!isGitHubAuthenticated) {
1965+
log.error('Unable to authenticate with GitHub')
1966+
console.log(colors.red('\nGitHub authentication is required for CI monitoring.'))
1967+
console.log('Please ensure you can login to GitHub CLI and try again.')
18371968
return false
18381969
}
1839-
log.done('GitHub CLI authenticated')
18401970

18411971
// Get current commit SHA
18421972
const shaResult = await runCommandWithOutput('git', ['rev-parse', 'HEAD'], {
@@ -1873,16 +2003,33 @@ Let's work through this together to get CI passing.`
18732003
}
18742004

18752005
// Check workflow runs using gh CLI
1876-
const runsResult = await runCommandWithOutput('gh', [
2006+
// First try to find runs for the specific commit
2007+
let runsResult = await runCommandWithOutput('gh', [
18772008
'run', 'list',
18782009
'--repo', `${owner}/${repo}`,
18792010
'--commit', currentSha,
18802011
'--limit', '1',
1881-
'--json', 'databaseId,status,conclusion,name'
2012+
'--json', 'databaseId,status,conclusion,name,headSha'
18822013
], {
18832014
cwd: rootPath
18842015
})
18852016

2017+
// If no runs found for commit, get recent runs and check if any match our SHA
2018+
if (runsResult.exitCode === 0) {
2019+
const runs = JSON.parse(runsResult.stdout || '[]')
2020+
if (runs.length === 0) {
2021+
// Fallback: get latest runs and find our commit
2022+
runsResult = await runCommandWithOutput('gh', [
2023+
'run', 'list',
2024+
'--repo', `${owner}/${repo}`,
2025+
'--limit', '10',
2026+
'--json', 'databaseId,status,conclusion,name,headSha'
2027+
], {
2028+
cwd: rootPath
2029+
})
2030+
}
2031+
}
2032+
18862033
if (runsResult.exitCode !== 0) {
18872034
log.failed('Failed to fetch workflow runs')
18882035

@@ -1916,13 +2063,33 @@ Let's work through this together to get CI passing.`
19162063
return false
19172064
}
19182065

1919-
if (runs.length === 0) {
2066+
// Filter runs to find one matching our commit SHA
2067+
let matchingRun = null
2068+
for (const run of runs) {
2069+
if (run.headSha && run.headSha.startsWith(currentSha.substring(0, 7))) {
2070+
matchingRun = run
2071+
break
2072+
}
2073+
}
2074+
2075+
if (!matchingRun && runs.length > 0) {
2076+
// If no exact match, take the most recent run if it was triggered recently
2077+
// This handles cases where the workflow was triggered by the push but headSha isn't set yet
2078+
const latestRun = runs[0]
2079+
if (retryCount === 0) {
2080+
// On first attempt, assume the latest run might be ours if triggered very recently
2081+
matchingRun = latestRun
2082+
log.substep(`Monitoring latest workflow run: ${latestRun.name}`)
2083+
}
2084+
}
2085+
2086+
if (!matchingRun) {
19202087
log.substep('No workflow runs found yet, waiting...')
19212088
await new Promise(resolve => setTimeout(resolve, 30000))
19222089
continue
19232090
}
19242091

1925-
const run = runs[0]
2092+
const run = matchingRun
19262093
lastRunId = run.databaseId
19272094

19282095
log.substep(`Workflow "${run.name}" status: ${run.status}`)
@@ -1978,7 +2145,8 @@ Fix all CI failures now by making the necessary changes.`
19782145
await runCommand(claudeCmd, prepareClaudeArgs([], opts), {
19792146
input: fixPrompt,
19802147
cwd: rootPath,
1981-
stdio: 'pipe' // Run silently
2148+
// Run silently
2149+
stdio: 'pipe'
19822150
})
19832151

19842152
// Give Claude's changes a moment to complete
@@ -2259,6 +2427,16 @@ async function main() {
22592427
}
22602428
log.done(`Found Claude Code CLI: ${claudeCmd}`)
22612429

2430+
// Ensure Claude is authenticated
2431+
const isClaudeAuthenticated = await ensureClaudeAuthenticated(claudeCmd)
2432+
if (!isClaudeAuthenticated) {
2433+
log.error('Unable to authenticate with Claude Code')
2434+
console.log(colors.red('\nAuthentication is required to use Claude utilities.'))
2435+
console.log('Please ensure you can login to Claude Code and try again.')
2436+
process.exitCode = 1
2437+
return
2438+
}
2439+
22622440
// Execute requested operation.
22632441
let success = true
22642442
const options = { ...values, positionals }

0 commit comments

Comments
 (0)