@rubytech/create-maxy 0.4.5 → 0.4.7

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.5",
3
+ "version": "0.4.7",
4
4
  "description": "Install Maxy — your personal AI assistant",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -1,8 +1,53 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { spawn, execFileSync, type ChildProcess } from 'node:child_process'
2
+ import { execFileSync } from 'node:child_process'
3
+ import { loginAnthropic, type OAuthCredentials } from '@mariozechner/pi-ai/dist/utils/oauth/anthropic.js'
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
5
+ import { resolve } from 'node:path'
6
+ import { homedir } from 'node:os'
7
+
8
+ // --- Active OAuth session (same pattern as v1 gateway/server-methods/auth.ts) ---
9
+
10
+ type OAuthSession = {
11
+ authUrl: string | null
12
+ codeResolver: ((code: string) => void) | null
13
+ codePromise: Promise<string> | null
14
+ completed: boolean
15
+ error: string | null
16
+ credentials: OAuthCredentials | null
17
+ }
18
+
19
+ let activeSession: OAuthSession | null = null
20
+
21
+ function resetSession(): void {
22
+ activeSession = null
23
+ }
24
+
25
+ // --- Claude CLI credentials ---
26
+
27
+ const CLAUDE_CREDS_PATH = resolve(homedir(), '.claude/.credentials.json')
28
+
29
+ function writeClaudeCredentials(creds: OAuthCredentials): void {
30
+ const claudeDir = resolve(homedir(), '.claude')
31
+ mkdirSync(claudeDir, { recursive: true })
32
+
33
+ let data: Record<string, unknown> = {}
34
+ if (existsSync(CLAUDE_CREDS_PATH)) {
35
+ try {
36
+ data = JSON.parse(readFileSync(CLAUDE_CREDS_PATH, 'utf-8'))
37
+ } catch { /* start fresh */ }
38
+ }
3
39
 
4
- // Keep the login process alive between requests so we can feed it the code
5
- let loginProcess: ChildProcess | null = null
40
+ data.claudeAiOauth = {
41
+ ...(typeof data.claudeAiOauth === 'object' && data.claudeAiOauth ? data.claudeAiOauth : {}),
42
+ accessToken: creds.access,
43
+ refreshToken: creds.refresh,
44
+ expiresAt: creds.expires,
45
+ }
46
+
47
+ writeFileSync(CLAUDE_CREDS_PATH, JSON.stringify(data, null, 2), { mode: 0o600 })
48
+ }
49
+
50
+ // --- Endpoints ---
6
51
 
7
52
  /**
8
53
  * GET /api/onboarding/claude-auth
@@ -26,112 +71,118 @@ export async function GET() {
26
71
 
27
72
  /**
28
73
  * POST /api/onboarding/claude-auth
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
74
+ * Three actions:
75
+ * - {} — start OAuth flow, returns authUrl
76
+ * - { code: "..." } — submit the auth code
77
+ * - { action: "wait" } — poll for completion
31
78
  */
32
79
  export async function POST(req: Request) {
33
- let body: { code?: string } = {}
80
+ let body: { code?: string; action?: string } = {}
34
81
  try {
35
82
  body = await req.json()
36
83
  } catch { /* empty body = start flow */ }
37
84
 
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
85
+ // --- Submit code ---
86
+ if (body.code) {
87
+ if (!activeSession?.codeResolver) {
88
+ return NextResponse.json(
89
+ { error: 'No active OAuth session. Click "Sign in with Claude" to start again.' },
90
+ { status: 400 },
91
+ )
92
+ }
52
93
 
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 { /* */ }
94
+ activeSession.codeResolver(body.code.trim())
64
95
 
65
- return NextResponse.json({
66
- authenticated: false,
67
- error: 'Authentication failed. Check the code and try again.',
68
- })
96
+ return NextResponse.json({ message: 'Code received — verifying...' })
69
97
  }
70
98
 
71
- // --- Start new login flow ---
72
- if (loginProcess) {
73
- loginProcess.kill()
74
- loginProcess = null
75
- }
99
+ // --- Poll for completion ---
100
+ if (body.action === 'wait') {
101
+ if (!activeSession) {
102
+ return NextResponse.json({ completed: false, error: 'No active session.' })
103
+ }
76
104
 
77
- return new Promise<Response>((resolve) => {
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
- })
91
-
92
- loginProcess = child
93
-
94
- let output = ''
95
- let resolved = false
96
-
97
- function tryResolve() {
98
- if (resolved) return
99
- const match = output.match(/(https:\/\/claude\.ai\/oauth\/authorize[^\s]+)/)
100
- if (match) {
101
- resolved = true
102
- resolve(NextResponse.json({ authUrl: match[1] }))
105
+ // Poll for up to 5 seconds
106
+ const start = Date.now()
107
+ while (Date.now() - start < 5000) {
108
+ if (activeSession.completed) {
109
+ if (activeSession.error) {
110
+ const error = activeSession.error
111
+ resetSession()
112
+ return NextResponse.json({ completed: true, authenticated: false, error })
113
+ }
114
+
115
+ resetSession()
116
+ return NextResponse.json({ completed: true, authenticated: true })
103
117
  }
118
+ await new Promise(r => setTimeout(r, 500))
104
119
  }
105
120
 
106
- child.stdout?.on('data', (chunk: Buffer) => {
107
- output += chunk.toString()
108
- tryResolve()
109
- })
121
+ return NextResponse.json({ completed: false })
122
+ }
110
123
 
111
- child.stderr?.on('data', (chunk: Buffer) => {
112
- output += chunk.toString()
113
- tryResolve()
114
- })
124
+ // --- Start OAuth flow ---
125
+ resetSession()
126
+
127
+ activeSession = {
128
+ authUrl: null,
129
+ codeResolver: null,
130
+ codePromise: null,
131
+ completed: false,
132
+ error: null,
133
+ credentials: null,
134
+ }
135
+
136
+ // Set up code promise (resolved when user submits code via POST { code })
137
+ activeSession.codePromise = new Promise<string>((resolve) => {
138
+ activeSession!.codeResolver = resolve
139
+ })
115
140
 
116
- child.on('exit', () => {
117
- if (!resolved) {
118
- resolved = true
119
- loginProcess = null
120
- resolve(NextResponse.json(
121
- { error: 'Login process exited unexpectedly.' },
122
- { status: 500 },
123
- ))
141
+ // Start OAuth flow in background (same as v1)
142
+ const oauthPromise = loginAnthropic(
143
+ // onAuthUrl — called when we have the URL
144
+ (url: string) => {
145
+ if (activeSession) {
146
+ activeSession.authUrl = url
147
+ }
148
+ },
149
+ // onPromptCode — called when the library needs the code
150
+ async (): Promise<string> => {
151
+ if (!activeSession?.codePromise) {
152
+ throw new Error('OAuth session expired')
153
+ }
154
+ return activeSession.codePromise
155
+ },
156
+ )
157
+
158
+ // Handle completion in background
159
+ oauthPromise
160
+ .then((credentials) => {
161
+ if (activeSession) {
162
+ activeSession.credentials = credentials
163
+ activeSession.completed = true
164
+ writeClaudeCredentials(credentials)
124
165
  }
125
166
  })
126
-
127
- setTimeout(() => {
128
- if (!resolved) {
129
- resolved = true
130
- resolve(NextResponse.json(
131
- { error: 'Could not start auth flow. Is Claude Code installed?' },
132
- { status: 500 },
133
- ))
167
+ .catch((err) => {
168
+ if (activeSession) {
169
+ activeSession.error = err instanceof Error ? err.message : String(err)
170
+ activeSession.completed = true
134
171
  }
135
- }, 15000)
136
- })
172
+ })
173
+
174
+ // Wait for the auth URL to appear
175
+ await new Promise(r => setTimeout(r, 500))
176
+ if (!activeSession?.authUrl) {
177
+ await new Promise(r => setTimeout(r, 1500))
178
+ }
179
+
180
+ if (!activeSession?.authUrl) {
181
+ return NextResponse.json(
182
+ { error: 'Failed to start OAuth flow.' },
183
+ { status: 500 },
184
+ )
185
+ }
186
+
187
+ return NextResponse.json({ authUrl: activeSession.authUrl })
137
188
  }
@@ -290,17 +290,35 @@ export default function AdminPage() {
290
290
  setAuthLoading(true)
291
291
  setPinError('')
292
292
  try {
293
- const res = await fetch('/api/onboarding/claude-auth', {
293
+ // Submit the code
294
+ await fetch('/api/onboarding/claude-auth', {
294
295
  method: 'POST',
295
296
  headers: { 'Content-Type': 'application/json' },
296
297
  body: JSON.stringify({ code: authCode.trim() }),
297
298
  })
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.')
299
+
300
+ // Poll for completion
301
+ for (let i = 0; i < 12; i++) {
302
+ const waitRes = await fetch('/api/onboarding/claude-auth', {
303
+ method: 'POST',
304
+ headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify({ action: 'wait' }),
306
+ })
307
+ const waitData = await waitRes.json()
308
+
309
+ if (waitData.completed) {
310
+ if (waitData.authenticated) {
311
+ setAppState('enter-pin')
312
+ return
313
+ } else {
314
+ setPinError(waitData.error || 'Authentication failed. Check the code and try again.')
315
+ setAuthLoading(false)
316
+ return
317
+ }
318
+ }
303
319
  }
320
+
321
+ setPinError('Timed out waiting for verification. Try again.')
304
322
  } catch {
305
323
  setPinError('Could not verify code.')
306
324
  }