berget 2.2.6 → 2.2.8

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.
Files changed (145) hide show
  1. package/.github/workflows/publish.yml +2 -2
  2. package/.github/workflows/test.yml +10 -4
  3. package/.husky/pre-commit +1 -0
  4. package/.prettierignore +15 -0
  5. package/.prettierrc +7 -3
  6. package/CONTRIBUTING.md +38 -0
  7. package/README.md +2 -148
  8. package/dist/index.js +10 -11
  9. package/dist/package.json +30 -2
  10. package/dist/src/agents/app.js +28 -0
  11. package/dist/src/agents/backend.js +25 -0
  12. package/dist/src/agents/devops.js +34 -0
  13. package/dist/src/agents/frontend.js +25 -0
  14. package/dist/src/agents/fullstack.js +25 -0
  15. package/dist/src/agents/index.js +61 -0
  16. package/dist/src/agents/quality.js +70 -0
  17. package/dist/src/agents/security.js +26 -0
  18. package/dist/src/agents/types.js +2 -0
  19. package/dist/src/client.js +97 -117
  20. package/dist/src/commands/api-keys.js +75 -90
  21. package/dist/src/commands/auth.js +7 -16
  22. package/dist/src/commands/autocomplete.js +1 -1
  23. package/dist/src/commands/billing.js +6 -17
  24. package/dist/src/commands/chat.js +68 -101
  25. package/dist/src/commands/clusters.js +9 -18
  26. package/dist/src/commands/code/__tests__/auth-sync.test.js +351 -0
  27. package/dist/src/commands/code/__tests__/fake-api-key-service.js +13 -0
  28. package/dist/src/commands/code/__tests__/fake-auth-service.js +47 -0
  29. package/dist/src/commands/code/__tests__/fake-command-runner.js +21 -34
  30. package/dist/src/commands/code/__tests__/fake-file-store.js +20 -33
  31. package/dist/src/commands/code/__tests__/fake-prompter.js +83 -57
  32. package/dist/src/commands/code/__tests__/setup-flow.test.js +359 -92
  33. package/dist/src/commands/code/adapters/clack-prompter.js +15 -22
  34. package/dist/src/commands/code/adapters/fs-file-store.js +26 -40
  35. package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -37
  36. package/dist/src/commands/code/auth-sync.js +270 -0
  37. package/dist/src/commands/code/errors.js +12 -9
  38. package/dist/src/commands/code/ports/auth-services.js +2 -0
  39. package/dist/src/commands/code/setup.js +387 -281
  40. package/dist/src/commands/code.js +205 -332
  41. package/dist/src/commands/index.js +5 -5
  42. package/dist/src/commands/models.js +6 -17
  43. package/dist/src/commands/users.js +5 -16
  44. package/dist/src/constants/command-structure.js +104 -104
  45. package/dist/src/services/api-key-service.js +132 -157
  46. package/dist/src/services/auth-service.js +89 -342
  47. package/dist/src/services/browser-auth.js +268 -0
  48. package/dist/src/services/chat-service.js +371 -401
  49. package/dist/src/services/cluster-service.js +47 -62
  50. package/dist/src/services/collaborator-service.js +10 -25
  51. package/dist/src/services/flux-service.js +14 -29
  52. package/dist/src/services/helm-service.js +10 -25
  53. package/dist/src/services/kubectl-service.js +16 -33
  54. package/dist/src/utils/config-checker.js +3 -3
  55. package/dist/src/utils/config-loader.js +95 -95
  56. package/dist/src/utils/default-api-key.js +124 -134
  57. package/dist/src/utils/env-manager.js +55 -66
  58. package/dist/src/utils/error-handler.js +20 -21
  59. package/dist/src/utils/logger.js +72 -65
  60. package/dist/src/utils/markdown-renderer.js +27 -27
  61. package/dist/src/utils/opencode-validator.js +63 -68
  62. package/dist/src/utils/token-manager.js +74 -45
  63. package/dist/tests/commands/chat.test.js +16 -25
  64. package/dist/tests/commands/code.test.js +95 -104
  65. package/dist/tests/utils/config-loader.test.js +48 -48
  66. package/dist/tests/utils/env-manager.test.js +43 -52
  67. package/dist/tests/utils/opencode-validator.test.js +22 -21
  68. package/dist/vitest.config.js +1 -1
  69. package/eslint.config.mjs +67 -0
  70. package/index.ts +35 -42
  71. package/package.json +30 -2
  72. package/src/agents/app.ts +27 -0
  73. package/src/agents/backend.ts +24 -0
  74. package/src/agents/devops.ts +33 -0
  75. package/src/agents/frontend.ts +24 -0
  76. package/src/agents/fullstack.ts +24 -0
  77. package/src/agents/index.ts +73 -0
  78. package/src/agents/quality.ts +69 -0
  79. package/src/agents/security.ts +26 -0
  80. package/src/agents/types.ts +17 -0
  81. package/src/client.ts +118 -152
  82. package/src/commands/api-keys.ts +241 -333
  83. package/src/commands/auth.ts +22 -27
  84. package/src/commands/autocomplete.ts +9 -9
  85. package/src/commands/billing.ts +20 -24
  86. package/src/commands/chat.ts +248 -338
  87. package/src/commands/clusters.ts +27 -26
  88. package/src/commands/code/__tests__/auth-sync.test.ts +482 -0
  89. package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
  90. package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
  91. package/src/commands/code/__tests__/fake-command-runner.ts +45 -42
  92. package/src/commands/code/__tests__/fake-file-store.ts +32 -23
  93. package/src/commands/code/__tests__/fake-prompter.ts +116 -77
  94. package/src/commands/code/__tests__/setup-flow.test.ts +624 -268
  95. package/src/commands/code/adapters/clack-prompter.ts +53 -39
  96. package/src/commands/code/adapters/fs-file-store.ts +32 -27
  97. package/src/commands/code/adapters/spawn-command-runner.ts +38 -29
  98. package/src/commands/code/auth-sync.ts +329 -0
  99. package/src/commands/code/errors.ts +18 -18
  100. package/src/commands/code/ports/auth-services.ts +14 -0
  101. package/src/commands/code/ports/command-runner.ts +8 -4
  102. package/src/commands/code/ports/file-store.ts +5 -4
  103. package/src/commands/code/ports/prompter.ts +24 -18
  104. package/src/commands/code/setup.ts +570 -340
  105. package/src/commands/code.ts +338 -539
  106. package/src/commands/index.ts +20 -19
  107. package/src/commands/models.ts +28 -32
  108. package/src/commands/users.ts +15 -21
  109. package/src/constants/command-structure.ts +134 -157
  110. package/src/services/api-key-service.ts +105 -122
  111. package/src/services/auth-service.ts +99 -345
  112. package/src/services/browser-auth.ts +296 -0
  113. package/src/services/chat-service.ts +265 -299
  114. package/src/services/cluster-service.ts +42 -45
  115. package/src/services/collaborator-service.ts +14 -19
  116. package/src/services/flux-service.ts +23 -25
  117. package/src/services/helm-service.ts +19 -21
  118. package/src/services/kubectl-service.ts +17 -19
  119. package/src/types/api.d.ts +1905 -1907
  120. package/src/types/json.d.ts +2 -2
  121. package/src/utils/config-checker.ts +10 -10
  122. package/src/utils/config-loader.ts +162 -178
  123. package/src/utils/default-api-key.ts +114 -125
  124. package/src/utils/env-manager.ts +53 -57
  125. package/src/utils/error-handler.ts +61 -56
  126. package/src/utils/logger.ts +79 -73
  127. package/src/utils/markdown-renderer.ts +31 -31
  128. package/src/utils/opencode-validator.ts +85 -89
  129. package/src/utils/token-manager.ts +108 -87
  130. package/templates/agents/app.md +1 -0
  131. package/templates/agents/backend.md +1 -0
  132. package/templates/agents/devops.md +2 -0
  133. package/templates/agents/frontend.md +1 -0
  134. package/templates/agents/fullstack.md +1 -0
  135. package/templates/agents/quality.md +45 -40
  136. package/templates/agents/security.md +1 -0
  137. package/tests/commands/chat.test.ts +53 -62
  138. package/tests/commands/code.test.ts +265 -310
  139. package/tests/utils/config-loader.test.ts +189 -188
  140. package/tests/utils/env-manager.test.ts +110 -113
  141. package/tests/utils/opencode-validator.test.ts +52 -56
  142. package/tsconfig.json +4 -3
  143. package/vitest.config.ts +3 -3
  144. package/AGENTS.md +0 -374
  145. package/TODO.md +0 -19
@@ -1,397 +1,151 @@
1
- import {
2
- createAuthenticatedClient,
3
- saveAuthToken,
4
- clearAuthToken,
5
- apiClient,
6
- API_BASE_URL,
7
- } from '../client'
8
- // We'll use dynamic import for 'open' to support ESM modules in CommonJS
9
- import chalk from 'chalk'
10
- import { handleError } from '../utils/error-handler'
11
- import { COMMAND_GROUPS, SUBCOMMANDS } from '../constants/command-structure'
12
- import * as http from 'http'
13
- import * as crypto from 'crypto'
14
- import * as url from 'url'
1
+ import chalk from 'chalk';
15
2
 
16
- // Keycloak configuration based on environment
17
- const isStageMode = process.argv.includes('--stage')
18
- const isLocalMode = process.argv.includes('--local')
19
- const KEYCLOAK_URL = (isStageMode || isLocalMode)
20
- ? 'https://keycloak.stage.berget.ai'
21
- : 'https://keycloak.berget.ai'
22
- const KEYCLOAK_REALM = 'berget'
23
- const KEYCLOAK_CLIENT_ID = 'berget-code'
24
- const CALLBACK_PORT = 8787
25
-
26
- /**
27
- * Generate a random string for PKCE code_verifier
28
- */
29
- function generateCodeVerifier(): string {
30
- return crypto.randomBytes(32).toString('base64url')
31
- }
3
+ import { clearAuthToken, createAuthenticatedClient, saveAuthToken } from '../client';
4
+ import { COMMAND_GROUPS, SUBCOMMANDS } from '../constants/command-structure';
5
+ import { handleError } from '../utils/error-handler';
6
+ import { BrowserAuth } from './browser-auth';
32
7
 
33
- /**
34
- * Generate code_challenge from code_verifier using S256 method
35
- */
36
- function generateCodeChallenge(verifier: string): string {
37
- return crypto.createHash('sha256').update(verifier).digest('base64url')
38
- }
8
+ // Keycloak configuration based on environment
9
+ const isStageMode = process.argv.includes('--stage');
10
+ const isLocalMode = process.argv.includes('--local');
11
+ const KEYCLOAK_URL =
12
+ isStageMode || isLocalMode ? 'https://keycloak.stage.berget.ai' : 'https://keycloak.berget.ai';
13
+ const KEYCLOAK_REALM = 'berget';
14
+ const KEYCLOAK_CLIENT_ID = 'berget-code';
15
+ const CALLBACK_PORT = 8787;
39
16
 
40
17
  /**
41
18
  * Service for authentication operations
42
19
  * Command group: auth
43
20
  */
44
21
  export class AuthService {
45
- private static instance: AuthService
46
- private client = createAuthenticatedClient()
47
-
48
22
  // Command group name for this service
49
- public static readonly COMMAND_GROUP = COMMAND_GROUPS.AUTH
23
+ public static readonly COMMAND_GROUP = COMMAND_GROUPS.AUTH;
50
24
 
51
25
  // Subcommands for this service
52
- public static readonly COMMANDS = SUBCOMMANDS.AUTH
26
+ public static readonly COMMANDS = SUBCOMMANDS.AUTH;
27
+
28
+ private static instance: AuthService;
53
29
 
54
30
  private constructor() {}
55
31
 
56
32
  public static getInstance(): AuthService {
57
33
  if (!AuthService.instance) {
58
- AuthService.instance = new AuthService()
59
- }
60
- return AuthService.instance
61
- }
62
-
63
- public async whoami(): Promise<any> {
64
- try {
65
- // Create fresh client to ensure we have the latest token
66
- const client = createAuthenticatedClient()
67
- const { data: profile, error } = await client.GET('/v1/users/me')
68
- if (error) {
69
- return null
70
- }
71
- return profile
72
- } catch (error) {
73
- return null
34
+ AuthService.instance = new AuthService();
74
35
  }
36
+ return AuthService.instance;
75
37
  }
76
38
 
39
+ /**
40
+ * Browser-based PKCE login for interactive CLI use.
41
+ * Prints status to stdout/stderr. Use loginInteractive() when you need
42
+ * a silent, UI-agnostic result (e.g. inside the setup wizard).
43
+ */
77
44
  public async login(): Promise<boolean> {
78
45
  try {
79
- // Clear any existing token to ensure a fresh login
80
- clearAuthToken()
81
-
82
- console.log(chalk.blue('Initiating login process...'))
83
-
84
- // Generate PKCE code verifier and challenge
85
- const codeVerifier = generateCodeVerifier()
86
- const codeChallenge = generateCodeChallenge(codeVerifier)
87
- const state = crypto.randomBytes(16).toString('hex')
88
-
89
- const redirectUri = `http://localhost:${CALLBACK_PORT}/callback`
90
-
91
- // Build authorization URL
92
- const authUrl = new URL(
93
- `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth`,
94
- )
95
- authUrl.searchParams.set('client_id', KEYCLOAK_CLIENT_ID)
96
- authUrl.searchParams.set('response_type', 'code')
97
- authUrl.searchParams.set('redirect_uri', redirectUri)
98
- authUrl.searchParams.set('scope', 'openid email profile')
99
- authUrl.searchParams.set('state', state)
100
- authUrl.searchParams.set('code_challenge', codeChallenge)
101
- authUrl.searchParams.set('code_challenge_method', 'S256')
102
-
103
- // Create a promise that resolves when we receive the callback
104
- const authResult = await new Promise<{
105
- success: boolean
106
- code?: string
107
- error?: string
108
- }>((resolve) => {
109
- const server = http.createServer(async (req, res) => {
110
- const parsedUrl = url.parse(req.url || '', true)
111
-
112
- if (parsedUrl.pathname === '/callback') {
113
- const receivedState = parsedUrl.query.state as string
114
- const code = parsedUrl.query.code as string
115
- const error = parsedUrl.query.error as string
116
-
117
- const errorPage = (title: string, message: string) => `
118
- <!DOCTYPE html>
119
- <html lang="en">
120
- <head>
121
- <meta charset="UTF-8">
122
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
123
- <title>Berget - Authentication Failed</title>
124
- <style>
125
- * { margin: 0; padding: 0; box-sizing: border-box; }
126
- body {
127
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
128
- display: flex;
129
- justify-content: center;
130
- align-items: center;
131
- min-height: 100vh;
132
- background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
133
- color: #fff;
134
- }
135
- .container {
136
- text-align: center;
137
- padding: 3rem;
138
- max-width: 400px;
139
- }
140
- .icon {
141
- width: 80px;
142
- height: 80px;
143
- background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
144
- border-radius: 50%;
145
- display: flex;
146
- align-items: center;
147
- justify-content: center;
148
- margin: 0 auto 1.5rem;
149
- box-shadow: 0 4px 20px rgba(248, 113, 113, 0.3);
150
- }
151
- .icon svg {
152
- width: 40px;
153
- height: 40px;
154
- stroke: #fff;
155
- stroke-width: 3;
156
- }
157
- h1 {
158
- font-size: 1.5rem;
159
- font-weight: 600;
160
- margin-bottom: 0.75rem;
161
- color: #fff;
162
- }
163
- p {
164
- color: #94a3b8;
165
- font-size: 0.95rem;
166
- line-height: 1.5;
167
- }
168
- .brand {
169
- margin-top: 2rem;
170
- opacity: 0.5;
171
- font-size: 0.8rem;
172
- letter-spacing: 0.05em;
173
- }
174
- </style>
175
- </head>
176
- <body>
177
- <div class="container">
178
- <div class="icon">
179
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
180
- <line x1="18" y1="6" x2="6" y2="18"></line>
181
- <line x1="6" y1="6" x2="18" y2="18"></line>
182
- </svg>
183
- </div>
184
- <h1>${title}</h1>
185
- <p>${message}</p>
186
- <div class="brand">BERGET</div>
187
- </div>
188
- </body>
189
- </html>
190
- `
191
-
192
- if (error) {
193
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
194
- res.end(errorPage('Authentication Failed', String(parsedUrl.query.error_description || error)))
195
- server.close()
196
- resolve({ success: false, error })
197
- return
198
- }
199
-
200
- if (receivedState !== state) {
201
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
202
- res.end(errorPage('Authentication Failed', 'Invalid state parameter. Please try again.'))
203
- server.close()
204
- resolve({ success: false, error: 'Invalid state parameter' })
205
- return
206
- }
46
+ clearAuthToken();
207
47
 
208
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
209
- res.end(`
210
- <!DOCTYPE html>
211
- <html lang="en">
212
- <head>
213
- <meta charset="UTF-8">
214
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
215
- <title>Berget - Authentication Successful</title>
216
- <style>
217
- * { margin: 0; padding: 0; box-sizing: border-box; }
218
- body {
219
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
220
- display: flex;
221
- justify-content: center;
222
- align-items: center;
223
- min-height: 100vh;
224
- background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
225
- color: #fff;
226
- }
227
- .container {
228
- text-align: center;
229
- padding: 3rem;
230
- max-width: 400px;
231
- }
232
- .icon {
233
- width: 80px;
234
- height: 80px;
235
- background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
236
- border-radius: 50%;
237
- display: flex;
238
- align-items: center;
239
- justify-content: center;
240
- margin: 0 auto 1.5rem;
241
- box-shadow: 0 4px 20px rgba(74, 222, 128, 0.3);
242
- }
243
- .icon svg {
244
- width: 40px;
245
- height: 40px;
246
- stroke: #fff;
247
- stroke-width: 3;
248
- }
249
- h1 {
250
- font-size: 1.5rem;
251
- font-weight: 600;
252
- margin-bottom: 0.75rem;
253
- color: #fff;
254
- }
255
- p {
256
- color: #94a3b8;
257
- font-size: 0.95rem;
258
- line-height: 1.5;
259
- }
260
- .brand {
261
- margin-top: 2rem;
262
- opacity: 0.5;
263
- font-size: 0.8rem;
264
- letter-spacing: 0.05em;
265
- }
266
- </style>
267
- </head>
268
- <body>
269
- <div class="container">
270
- <div class="icon">
271
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
272
- <polyline points="20 6 9 17 4 12"></polyline>
273
- </svg>
274
- </div>
275
- <h1>Authentication Successful</h1>
276
- <p>You can close this window and return to your terminal.</p>
277
- <div class="brand">BERGET</div>
278
- </div>
279
- </body>
280
- </html>
281
- `)
282
- server.close()
283
- resolve({ success: true, code })
284
- }
285
- })
48
+ console.log(chalk.blue('Initiating login process...'));
286
49
 
287
- server.listen(CALLBACK_PORT, () => {
288
- if (process.argv.includes('--debug')) {
289
- console.log(
290
- chalk.dim(`Callback server listening on port ${CALLBACK_PORT}`),
291
- )
292
- }
293
- })
50
+ const auth = makeBrowserAuth(process.argv.includes('--debug'));
51
+ const result = await auth.start();
294
52
 
295
- // Set timeout for the server
296
- setTimeout(() => {
297
- server.close()
298
- resolve({ success: false, error: 'Authentication timed out' })
299
- }, 5 * 60 * 1000) // 5 minute timeout
300
-
301
- // Open browser
302
- ;(async () => {
303
- try {
304
- const open = await import('open').then((m) => m.default)
305
- await open(authUrl.toString())
306
- console.log(chalk.dim('Browser opened for authentication...'))
307
- } catch {
308
- console.log(chalk.cyan('\nPlease open this URL in your browser:'))
309
- console.log(chalk.bold(authUrl.toString()))
310
- }
311
- })()
312
- })
313
-
314
- if (!authResult.success || !authResult.code) {
315
- console.log(
316
- chalk.red(`\nAuthentication failed: ${authResult.error || 'Unknown error'}`),
317
- )
318
- return false
319
- }
320
-
321
- // Exchange authorization code for tokens
322
- console.log(chalk.dim('Exchanging authorization code for tokens...'))
323
-
324
- const tokenUrl = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`
325
- const tokenResponse = await fetch(tokenUrl, {
326
- method: 'POST',
327
- headers: {
328
- 'Content-Type': 'application/x-www-form-urlencoded',
329
- },
330
- body: new URLSearchParams({
331
- grant_type: 'authorization_code',
332
- client_id: KEYCLOAK_CLIENT_ID,
333
- code: authResult.code,
334
- redirect_uri: redirectUri,
335
- code_verifier: codeVerifier,
336
- }).toString(),
337
- })
338
-
339
- if (!tokenResponse.ok) {
340
- const errorText = await tokenResponse.text()
341
- console.log(chalk.red(`\nFailed to exchange code for tokens: ${errorText}`))
342
- return false
53
+ if (!result.success) {
54
+ console.log(chalk.red(`\nAuthentication failed: ${result.error || 'Unknown error'}`));
55
+ return false;
343
56
  }
344
57
 
345
- const tokenData = (await tokenResponse.json()) as {
346
- access_token: string
347
- refresh_token: string
348
- expires_in: number
349
- refresh_expires_in?: number
350
- }
351
-
352
- // Save tokens
353
- saveAuthToken(
354
- tokenData.access_token,
355
- tokenData.refresh_token,
356
- tokenData.expires_in,
357
- )
58
+ saveAuthToken(result.accessToken!, result.refreshToken!, result.expiresIn!);
358
59
 
359
60
  if (process.argv.includes('--debug')) {
360
- console.log(chalk.yellow('DEBUG: Token data received:'))
61
+ console.log(chalk.yellow('DEBUG: Token data received:'));
361
62
  console.log(
362
63
  chalk.yellow(
363
64
  JSON.stringify(
364
65
  {
365
- expires_in: tokenData.expires_in,
366
- refresh_expires_in: tokenData.refresh_expires_in,
66
+ expires_in: result.expiresIn,
367
67
  },
368
68
  null,
369
69
  2,
370
70
  ),
371
71
  ),
372
- )
72
+ );
373
73
  }
374
74
 
375
- console.log(chalk.green('\n✓ Successfully logged in to Berget'))
75
+ console.log(chalk.green('\n✓ Successfully logged in to Berget'));
376
76
 
377
- // Try to get user info
378
77
  try {
379
- const profile = await this.whoami()
78
+ const profile = await this.whoami();
380
79
  if (profile?.email) {
381
- console.log(chalk.green(`Logged in as ${profile.name || profile.email}`))
80
+ console.log(chalk.green(`Logged in as ${profile.name || profile.email}`));
382
81
  }
383
82
  } catch {
384
83
  // Ignore errors fetching profile
385
84
  }
386
85
 
387
- console.log(chalk.cyan('\nNext steps:'))
388
- console.log(chalk.cyan(' • Create an API key: berget api-keys create'))
389
- console.log(chalk.cyan(' • Setup OpenCode: berget code init'))
86
+ console.log(chalk.cyan('\nNext steps:'));
87
+ console.log(chalk.cyan(' • Create an API key: berget api-keys create'));
88
+ console.log(chalk.cyan(' • Setup OpenCode: berget code init'));
89
+
90
+ return true;
91
+ } catch (error) {
92
+ handleError('Login failed', error);
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Browser-based PKCE login for wizard / programmatic use.
99
+ * Does NOT print to stdout — returns tokens so callers can display
100
+ * their own UI (e.g. via clack/prompts).
101
+ */
102
+ public async loginInteractive(): Promise<{
103
+ accessToken?: string;
104
+ error?: string;
105
+ expiresIn?: number;
106
+ refreshToken?: string;
107
+ success: boolean;
108
+ }> {
109
+ try {
110
+ clearAuthToken();
111
+
112
+ const auth = makeBrowserAuth(process.argv.includes('--debug'));
113
+ const result = await auth.start();
114
+
115
+ if (result.success) {
116
+ saveAuthToken(result.accessToken!, result.refreshToken!, result.expiresIn!);
117
+ }
390
118
 
391
- return true
119
+ return result;
392
120
  } catch (error) {
393
- handleError('Login failed', error)
394
- return false
121
+ return {
122
+ error: error instanceof Error ? error.message : String(error),
123
+ success: false,
124
+ };
125
+ }
126
+ }
127
+
128
+ public async whoami(): Promise<any> {
129
+ try {
130
+ // Create fresh client to ensure we have the latest token
131
+ const client = createAuthenticatedClient();
132
+ const { data: profile, error } = await client.GET('/v1/users/me');
133
+ if (error) {
134
+ return null;
135
+ }
136
+ return profile;
137
+ } catch {
138
+ return null;
395
139
  }
396
140
  }
397
141
  }
142
+
143
+ function makeBrowserAuth(debug?: boolean): BrowserAuth {
144
+ return new BrowserAuth({
145
+ callbackPort: CALLBACK_PORT,
146
+ clientId: KEYCLOAK_CLIENT_ID,
147
+ debug,
148
+ keycloakUrl: KEYCLOAK_URL,
149
+ realm: KEYCLOAK_REALM,
150
+ });
151
+ }