@rubytech/create-maxy 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { spawn, execFileSync, type ChildProcess } from 'node:child_process'
|
|
3
3
|
|
|
4
|
-
// Keep the login process alive
|
|
4
|
+
// Keep the login process alive between requests so we can feed it the code
|
|
5
5
|
let loginProcess: ChildProcess | null = null
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -26,25 +26,68 @@ export async function GET() {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* POST /api/onboarding/claude-auth
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* authenticates with Anthropic, and the process completes automatically
|
|
32
|
-
* via PKCE polling. No code pasting needed.
|
|
29
|
+
* Body: {} — starts the login flow, returns the auth URL (with code=true for headless)
|
|
30
|
+
* Body: { code: "..." } — feeds the auth code to the waiting login process
|
|
33
31
|
*/
|
|
34
|
-
export async function POST() {
|
|
35
|
-
|
|
32
|
+
export async function POST(req: Request) {
|
|
33
|
+
let body: { code?: string } = {}
|
|
34
|
+
try {
|
|
35
|
+
body = await req.json()
|
|
36
|
+
} catch { /* empty body = start flow */ }
|
|
37
|
+
|
|
38
|
+
// --- Submit code to running process ---
|
|
39
|
+
if (body.code && loginProcess) {
|
|
40
|
+
loginProcess.stdin?.write(body.code + '\n')
|
|
41
|
+
|
|
42
|
+
// Wait for process to handle the code
|
|
43
|
+
await new Promise<void>((resolve) => {
|
|
44
|
+
const timeout = setTimeout(() => resolve(), 8000)
|
|
45
|
+
loginProcess?.on('exit', () => {
|
|
46
|
+
clearTimeout(timeout)
|
|
47
|
+
resolve()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
loginProcess = null
|
|
52
|
+
|
|
53
|
+
// Check result
|
|
54
|
+
try {
|
|
55
|
+
const output = execFileSync('claude', ['auth', 'status', '--json'], {
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
})
|
|
59
|
+
const status = JSON.parse(output)
|
|
60
|
+
if (status.loggedIn) {
|
|
61
|
+
return NextResponse.json({ authenticated: true, email: status.email })
|
|
62
|
+
}
|
|
63
|
+
} catch { /* */ }
|
|
64
|
+
|
|
65
|
+
return NextResponse.json({
|
|
66
|
+
authenticated: false,
|
|
67
|
+
error: 'Authentication failed. Check the code and try again.',
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Start new login flow ---
|
|
36
72
|
if (loginProcess) {
|
|
37
73
|
loginProcess.kill()
|
|
38
74
|
loginProcess = null
|
|
39
75
|
}
|
|
40
76
|
|
|
41
77
|
return new Promise<Response>((resolve) => {
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
// Use `script` to provide a PTY so claude auth login gets its interactive prompt.
|
|
79
|
+
// On Linux (Pi): script -qc "command" /dev/null
|
|
80
|
+
// On macOS: script -q /dev/null command args
|
|
81
|
+
const isLinux = process.platform === 'linux'
|
|
82
|
+
const child = isLinux
|
|
83
|
+
? spawn('script', ['-qc', 'claude auth login', '/dev/null'], {
|
|
84
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
+
env: { ...process.env, BROWSER: 'echo', TERM: 'dumb' },
|
|
86
|
+
})
|
|
87
|
+
: spawn('script', ['-q', '/dev/null', 'claude', 'auth', 'login'], {
|
|
88
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
89
|
+
env: { ...process.env, BROWSER: 'echo', TERM: 'dumb' },
|
|
90
|
+
})
|
|
48
91
|
|
|
49
92
|
loginProcess = child
|
|
50
93
|
|
|
@@ -56,10 +99,7 @@ export async function POST() {
|
|
|
56
99
|
const match = output.match(/(https:\/\/claude\.ai\/oauth\/authorize[^\s]+)/)
|
|
57
100
|
if (match) {
|
|
58
101
|
resolved = true
|
|
59
|
-
|
|
60
|
-
// which shows an auth code. Without it, the flow completes automatically.
|
|
61
|
-
const autoUrl = match[1].replace(/code=true&?/, '').replace(/\?&/, '?')
|
|
62
|
-
resolve(NextResponse.json({ authUrl: autoUrl }))
|
|
102
|
+
resolve(NextResponse.json({ authUrl: match[1] }))
|
|
63
103
|
}
|
|
64
104
|
}
|
|
65
105
|
|
|
@@ -73,13 +113,17 @@ export async function POST() {
|
|
|
73
113
|
tryResolve()
|
|
74
114
|
})
|
|
75
115
|
|
|
76
|
-
// The process stays alive — it polls Anthropic for auth completion.
|
|
77
|
-
// Don't kill it. It will exit on its own when auth succeeds.
|
|
78
116
|
child.on('exit', () => {
|
|
79
|
-
|
|
117
|
+
if (!resolved) {
|
|
118
|
+
resolved = true
|
|
119
|
+
loginProcess = null
|
|
120
|
+
resolve(NextResponse.json(
|
|
121
|
+
{ error: 'Login process exited unexpectedly.' },
|
|
122
|
+
{ status: 500 },
|
|
123
|
+
))
|
|
124
|
+
}
|
|
80
125
|
})
|
|
81
126
|
|
|
82
|
-
// Only timeout if we can't even get the URL
|
|
83
127
|
setTimeout(() => {
|
|
84
128
|
if (!resolved) {
|
|
85
129
|
resolved = true
|
|
@@ -19,6 +19,7 @@ export default function AdminPage() {
|
|
|
19
19
|
const [pinError, setPinError] = useState('')
|
|
20
20
|
const [showPin, setShowPin] = useState(false)
|
|
21
21
|
const [authUrl, setAuthUrl] = useState<string | null>(null)
|
|
22
|
+
const [authCode, setAuthCode] = useState('')
|
|
22
23
|
const [authLoading, setAuthLoading] = useState(false)
|
|
23
24
|
const [sessionKey, setSessionKey] = useState<string | null>(null)
|
|
24
25
|
const [messages, setMessages] = useState<Message[]>([])
|
|
@@ -274,8 +275,6 @@ export default function AdminPage() {
|
|
|
274
275
|
if (data.authUrl) {
|
|
275
276
|
setAuthUrl(data.authUrl)
|
|
276
277
|
window.open(data.authUrl, '_blank')
|
|
277
|
-
// Start polling for auth completion
|
|
278
|
-
pollForAuth()
|
|
279
278
|
} else if (data.error) {
|
|
280
279
|
setPinError(data.error)
|
|
281
280
|
}
|
|
@@ -285,18 +284,27 @@ export default function AdminPage() {
|
|
|
285
284
|
setAuthLoading(false)
|
|
286
285
|
}
|
|
287
286
|
|
|
288
|
-
async function
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
}
|
|
287
|
+
async function submitCode(e: FormEvent) {
|
|
288
|
+
e.preventDefault()
|
|
289
|
+
if (!authCode.trim()) return
|
|
290
|
+
setAuthLoading(true)
|
|
291
|
+
setPinError('')
|
|
292
|
+
try {
|
|
293
|
+
const res = await fetch('/api/onboarding/claude-auth', {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
296
|
+
body: JSON.stringify({ code: authCode.trim() }),
|
|
297
|
+
})
|
|
298
|
+
const data = await res.json()
|
|
299
|
+
if (data.authenticated) {
|
|
300
|
+
setAppState('enter-pin')
|
|
301
|
+
} else {
|
|
302
|
+
setPinError(data.error || 'Authentication failed. Check the code and try again.')
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
setPinError('Could not verify code.')
|
|
299
306
|
}
|
|
307
|
+
setAuthLoading(false)
|
|
300
308
|
}
|
|
301
309
|
|
|
302
310
|
return (
|
|
@@ -318,15 +326,29 @@ export default function AdminPage() {
|
|
|
318
326
|
) : (
|
|
319
327
|
<>
|
|
320
328
|
<p className="chat-intro" style={{ fontSize: '14px', marginTop: 0 }}>
|
|
321
|
-
|
|
329
|
+
Sign in on the Anthropic page, then paste the code here.
|
|
322
330
|
</p>
|
|
323
|
-
<
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
331
|
+
<form onSubmit={submitCode}>
|
|
332
|
+
<div className="pin-input-row">
|
|
333
|
+
<input
|
|
334
|
+
type="text"
|
|
335
|
+
value={authCode}
|
|
336
|
+
onChange={e => setAuthCode(e.target.value)}
|
|
337
|
+
placeholder="Paste authentication code"
|
|
338
|
+
className="chat-input"
|
|
339
|
+
autoFocus
|
|
340
|
+
autoComplete="off"
|
|
341
|
+
/>
|
|
342
|
+
<button type="submit" className="chat-send" disabled={!authCode.trim() || authLoading}>
|
|
343
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
344
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
345
|
+
<polyline points="12 5 19 12 12 19" />
|
|
346
|
+
</svg>
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
349
|
+
</form>
|
|
328
350
|
<a href={authUrl} target="_blank" rel="noopener noreferrer" className="auth-retry-link">
|
|
329
|
-
|
|
351
|
+
Try again
|
|
330
352
|
</a>
|
|
331
353
|
</>
|
|
332
354
|
)}
|