@rubytech/create-maxy 0.4.1 → 0.4.3
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 — it polls Anthropic for completion
|
|
5
5
|
let loginProcess: ChildProcess | null = null
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -26,51 +26,24 @@ export async function GET() {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* POST /api/onboarding/claude-auth
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* Start the Claude Code OAuth login flow.
|
|
30
|
+
* Returns the auth URL. The user visits it in their browser,
|
|
31
|
+
* authenticates with Anthropic, and the process completes automatically
|
|
32
|
+
* via PKCE polling. No code pasting needed.
|
|
31
33
|
*/
|
|
32
|
-
export async function POST(
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
body = await req.json()
|
|
36
|
-
} catch { /* empty body = start flow */ }
|
|
37
|
-
|
|
38
|
-
// If code is provided, feed it to the running login process
|
|
39
|
-
if (body.code && loginProcess && loginProcess.stdin) {
|
|
40
|
-
loginProcess.stdin.write(body.code + '\n')
|
|
41
|
-
loginProcess.stdin.end()
|
|
42
|
-
|
|
43
|
-
// Wait a moment for the process to complete
|
|
44
|
-
await new Promise((r) => setTimeout(r, 3000))
|
|
45
|
-
|
|
46
|
-
// Check if auth succeeded
|
|
47
|
-
try {
|
|
48
|
-
const output = execFileSync('claude', ['auth', 'status', '--json'], {
|
|
49
|
-
encoding: 'utf-8',
|
|
50
|
-
timeout: 5000,
|
|
51
|
-
})
|
|
52
|
-
const status = JSON.parse(output)
|
|
53
|
-
loginProcess = null
|
|
54
|
-
return NextResponse.json({
|
|
55
|
-
authenticated: status.loggedIn === true,
|
|
56
|
-
email: status.email ?? null,
|
|
57
|
-
})
|
|
58
|
-
} catch {
|
|
59
|
-
loginProcess = null
|
|
60
|
-
return NextResponse.json({ authenticated: false, error: 'Authentication failed. Check the code and try again.' })
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Start a new login flow
|
|
34
|
+
export async function POST() {
|
|
35
|
+
// Kill any existing login process
|
|
65
36
|
if (loginProcess) {
|
|
66
37
|
loginProcess.kill()
|
|
67
38
|
loginProcess = null
|
|
68
39
|
}
|
|
69
40
|
|
|
70
41
|
return new Promise<Response>((resolve) => {
|
|
42
|
+
// Don't suppress the browser — but on a headless Pi it won't open.
|
|
43
|
+
// We capture the auth URL from the output and present it in the web UI.
|
|
71
44
|
const child = spawn('claude', ['auth', 'login'], {
|
|
72
45
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
73
|
-
env: { ...process.env, BROWSER: '
|
|
46
|
+
env: { ...process.env, BROWSER: '' },
|
|
74
47
|
})
|
|
75
48
|
|
|
76
49
|
loginProcess = child
|
|
@@ -97,16 +70,21 @@ export async function POST(req: Request) {
|
|
|
97
70
|
tryResolve()
|
|
98
71
|
})
|
|
99
72
|
|
|
73
|
+
// The process stays alive — it polls Anthropic for auth completion.
|
|
74
|
+
// Don't kill it. It will exit on its own when auth succeeds.
|
|
75
|
+
child.on('exit', () => {
|
|
76
|
+
loginProcess = null
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Only timeout if we can't even get the URL
|
|
100
80
|
setTimeout(() => {
|
|
101
81
|
if (!resolved) {
|
|
102
82
|
resolved = true
|
|
103
|
-
child.kill()
|
|
104
|
-
loginProcess = null
|
|
105
83
|
resolve(NextResponse.json(
|
|
106
|
-
{ error: '
|
|
84
|
+
{ error: 'Could not start auth flow. Is Claude Code installed?' },
|
|
107
85
|
{ status: 500 },
|
|
108
86
|
))
|
|
109
87
|
}
|
|
110
|
-
},
|
|
88
|
+
}, 15000)
|
|
111
89
|
})
|
|
112
90
|
}
|
|
@@ -1415,6 +1415,18 @@ a:hover {
|
|
|
1415
1415
|
border-color: var(--sage);
|
|
1416
1416
|
}
|
|
1417
1417
|
|
|
1418
|
+
.auth-retry-link {
|
|
1419
|
+
font-family: var(--font-body);
|
|
1420
|
+
font-size: 13px;
|
|
1421
|
+
color: var(--text-tertiary);
|
|
1422
|
+
text-decoration: none;
|
|
1423
|
+
margin-top: 12px;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.auth-retry-link:hover {
|
|
1427
|
+
color: var(--sage);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1418
1430
|
.admin-pin-error {
|
|
1419
1431
|
color: #c44;
|
|
1420
1432
|
font-size: 14px;
|
|
@@ -19,7 +19,6 @@ 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('')
|
|
23
22
|
const [authLoading, setAuthLoading] = useState(false)
|
|
24
23
|
const [sessionKey, setSessionKey] = useState<string | null>(null)
|
|
25
24
|
const [messages, setMessages] = useState<Message[]>([])
|
|
@@ -275,6 +274,8 @@ export default function AdminPage() {
|
|
|
275
274
|
if (data.authUrl) {
|
|
276
275
|
setAuthUrl(data.authUrl)
|
|
277
276
|
window.open(data.authUrl, '_blank')
|
|
277
|
+
// Start polling for auth completion
|
|
278
|
+
pollForAuth()
|
|
278
279
|
} else if (data.error) {
|
|
279
280
|
setPinError(data.error)
|
|
280
281
|
}
|
|
@@ -284,27 +285,18 @@ export default function AdminPage() {
|
|
|
284
285
|
setAuthLoading(false)
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
async function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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.')
|
|
288
|
+
async function pollForAuth() {
|
|
289
|
+
for (let i = 0; i < 60; i++) { // Poll for up to 2 minutes
|
|
290
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
291
|
+
try {
|
|
292
|
+
const res = await fetch('/api/onboarding/claude-auth')
|
|
293
|
+
const data = await res.json()
|
|
294
|
+
if (data.authenticated) {
|
|
295
|
+
setAppState('enter-pin')
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
} catch { /* keep polling */ }
|
|
306
299
|
}
|
|
307
|
-
setAuthLoading(false)
|
|
308
300
|
}
|
|
309
301
|
|
|
310
302
|
return (
|
|
@@ -326,27 +318,14 @@ export default function AdminPage() {
|
|
|
326
318
|
) : (
|
|
327
319
|
<>
|
|
328
320
|
<p className="chat-intro" style={{ fontSize: '14px', marginTop: 0 }}>
|
|
329
|
-
|
|
321
|
+
Complete sign-in on the Anthropic page. This screen will update automatically.
|
|
330
322
|
</p>
|
|
331
|
-
<
|
|
332
|
-
<
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
placeholder="Paste auth code"
|
|
338
|
-
className="chat-input"
|
|
339
|
-
autoFocus
|
|
340
|
-
/>
|
|
341
|
-
<button type="submit" className="chat-send" disabled={!authCode.trim() || authLoading}>
|
|
342
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
343
|
-
<line x1="5" y1="12" x2="19" y2="12" />
|
|
344
|
-
<polyline points="12 5 19 12 12 19" />
|
|
345
|
-
</svg>
|
|
346
|
-
</button>
|
|
347
|
-
</div>
|
|
348
|
-
</form>
|
|
349
|
-
<a href={authUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary" style={{ marginTop: '8px' }}>
|
|
323
|
+
<span className="typing-indicator" style={{ margin: '16px auto' }}>
|
|
324
|
+
<span className="typing-dot" />
|
|
325
|
+
<span className="typing-dot" />
|
|
326
|
+
<span className="typing-dot" />
|
|
327
|
+
</span>
|
|
328
|
+
<a href={authUrl} target="_blank" rel="noopener noreferrer" className="auth-retry-link">
|
|
350
329
|
Open sign-in page again
|
|
351
330
|
</a>
|
|
352
331
|
</>
|