@rubytech/create-maxy 0.4.3 → 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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 it polls Anthropic for completion
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
- * 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.
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
- // Kill any existing login process
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
- // 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.
44
- const child = spawn('claude', ['auth', 'login'], {
45
- stdio: ['pipe', 'pipe', 'pipe'],
46
- env: { ...process.env, BROWSER: '' },
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
 
@@ -70,13 +113,17 @@ export async function POST() {
70
113
  tryResolve()
71
114
  })
72
115
 
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
116
  child.on('exit', () => {
76
- loginProcess = null
117
+ if (!resolved) {
118
+ resolved = true
119
+ loginProcess = null
120
+ resolve(NextResponse.json(
121
+ { error: 'Login process exited unexpectedly.' },
122
+ { status: 500 },
123
+ ))
124
+ }
77
125
  })
78
126
 
79
- // Only timeout if we can't even get the URL
80
127
  setTimeout(() => {
81
128
  if (!resolved) {
82
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 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 */ }
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
- Complete sign-in on the Anthropic page. This screen will update automatically.
329
+ Sign in on the Anthropic page, then paste the code here.
322
330
  </p>
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>
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
- Open sign-in page again
351
+ Try again
330
352
  </a>
331
353
  </>
332
354
  )}