@rubytech/create-maxy 0.4.2 → 0.4.4

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Install Maxy — your personal AI assistant",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -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 between requests
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
- * Body: {} starts the login flow, returns auth URL
30
- * Body: { code: "..." } feeds the auth code to the running login process
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(req: Request) {
33
- let body: { code?: string } = {}
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: 'echo' },
46
+ env: { ...process.env, BROWSER: '' },
74
47
  })
75
48
 
76
49
  loginProcess = child
@@ -83,7 +56,10 @@ export async function POST(req: Request) {
83
56
  const match = output.match(/(https:\/\/claude\.ai\/oauth\/authorize[^\s]+)/)
84
57
  if (match) {
85
58
  resolved = true
86
- resolve(NextResponse.json({ authUrl: match[1] }))
59
+ // Remove code=true& from the URL — that triggers the fallback flow
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 }))
87
63
  }
88
64
  }
89
65
 
@@ -97,16 +73,21 @@ export async function POST(req: Request) {
97
73
  tryResolve()
98
74
  })
99
75
 
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
+ child.on('exit', () => {
79
+ loginProcess = null
80
+ })
81
+
82
+ // Only timeout if we can't even get the URL
100
83
  setTimeout(() => {
101
84
  if (!resolved) {
102
85
  resolved = true
103
- child.kill()
104
- loginProcess = null
105
86
  resolve(NextResponse.json(
106
- { error: 'Timed out waiting for auth URL.' },
87
+ { error: 'Could not start auth flow. Is Claude Code installed?' },
107
88
  { status: 500 },
108
89
  ))
109
90
  }
110
- }, 10000)
91
+ }, 15000)
111
92
  })
112
93
  }
@@ -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 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.')
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,28 +318,15 @@ export default function AdminPage() {
326
318
  ) : (
327
319
  <>
328
320
  <p className="chat-intro" style={{ fontSize: '14px', marginTop: 0 }}>
329
- Sign in on the Anthropic page, then paste the code here.
321
+ Complete sign-in on the Anthropic page. This screen will update automatically.
330
322
  </p>
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 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>
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>
349
328
  <a href={authUrl} target="_blank" rel="noopener noreferrer" className="auth-retry-link">
350
- Try again
329
+ Open sign-in page again
351
330
  </a>
352
331
  </>
353
332
  )}