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.
- package/.github/workflows/publish.yml +2 -2
- package/.github/workflows/test.yml +10 -4
- package/.husky/pre-commit +1 -0
- package/.prettierignore +15 -0
- package/.prettierrc +7 -3
- package/CONTRIBUTING.md +38 -0
- package/README.md +2 -148
- package/dist/index.js +10 -11
- package/dist/package.json +30 -2
- package/dist/src/agents/app.js +28 -0
- package/dist/src/agents/backend.js +25 -0
- package/dist/src/agents/devops.js +34 -0
- package/dist/src/agents/frontend.js +25 -0
- package/dist/src/agents/fullstack.js +25 -0
- package/dist/src/agents/index.js +61 -0
- package/dist/src/agents/quality.js +70 -0
- package/dist/src/agents/security.js +26 -0
- package/dist/src/agents/types.js +2 -0
- package/dist/src/client.js +97 -117
- package/dist/src/commands/api-keys.js +75 -90
- package/dist/src/commands/auth.js +7 -16
- package/dist/src/commands/autocomplete.js +1 -1
- package/dist/src/commands/billing.js +6 -17
- package/dist/src/commands/chat.js +68 -101
- package/dist/src/commands/clusters.js +9 -18
- package/dist/src/commands/code/__tests__/auth-sync.test.js +351 -0
- package/dist/src/commands/code/__tests__/fake-api-key-service.js +13 -0
- package/dist/src/commands/code/__tests__/fake-auth-service.js +47 -0
- package/dist/src/commands/code/__tests__/fake-command-runner.js +21 -34
- package/dist/src/commands/code/__tests__/fake-file-store.js +20 -33
- package/dist/src/commands/code/__tests__/fake-prompter.js +83 -57
- package/dist/src/commands/code/__tests__/setup-flow.test.js +359 -92
- package/dist/src/commands/code/adapters/clack-prompter.js +15 -22
- package/dist/src/commands/code/adapters/fs-file-store.js +26 -40
- package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -37
- package/dist/src/commands/code/auth-sync.js +270 -0
- package/dist/src/commands/code/errors.js +12 -9
- package/dist/src/commands/code/ports/auth-services.js +2 -0
- package/dist/src/commands/code/setup.js +387 -281
- package/dist/src/commands/code.js +205 -332
- package/dist/src/commands/index.js +5 -5
- package/dist/src/commands/models.js +6 -17
- package/dist/src/commands/users.js +5 -16
- package/dist/src/constants/command-structure.js +104 -104
- package/dist/src/services/api-key-service.js +132 -157
- package/dist/src/services/auth-service.js +89 -342
- package/dist/src/services/browser-auth.js +268 -0
- package/dist/src/services/chat-service.js +371 -401
- package/dist/src/services/cluster-service.js +47 -62
- package/dist/src/services/collaborator-service.js +10 -25
- package/dist/src/services/flux-service.js +14 -29
- package/dist/src/services/helm-service.js +10 -25
- package/dist/src/services/kubectl-service.js +16 -33
- package/dist/src/utils/config-checker.js +3 -3
- package/dist/src/utils/config-loader.js +95 -95
- package/dist/src/utils/default-api-key.js +124 -134
- package/dist/src/utils/env-manager.js +55 -66
- package/dist/src/utils/error-handler.js +20 -21
- package/dist/src/utils/logger.js +72 -65
- package/dist/src/utils/markdown-renderer.js +27 -27
- package/dist/src/utils/opencode-validator.js +63 -68
- package/dist/src/utils/token-manager.js +74 -45
- package/dist/tests/commands/chat.test.js +16 -25
- package/dist/tests/commands/code.test.js +95 -104
- package/dist/tests/utils/config-loader.test.js +48 -48
- package/dist/tests/utils/env-manager.test.js +43 -52
- package/dist/tests/utils/opencode-validator.test.js +22 -21
- package/dist/vitest.config.js +1 -1
- package/eslint.config.mjs +67 -0
- package/index.ts +35 -42
- package/package.json +30 -2
- package/src/agents/app.ts +27 -0
- package/src/agents/backend.ts +24 -0
- package/src/agents/devops.ts +33 -0
- package/src/agents/frontend.ts +24 -0
- package/src/agents/fullstack.ts +24 -0
- package/src/agents/index.ts +73 -0
- package/src/agents/quality.ts +69 -0
- package/src/agents/security.ts +26 -0
- package/src/agents/types.ts +17 -0
- package/src/client.ts +118 -152
- package/src/commands/api-keys.ts +241 -333
- package/src/commands/auth.ts +22 -27
- package/src/commands/autocomplete.ts +9 -9
- package/src/commands/billing.ts +20 -24
- package/src/commands/chat.ts +248 -338
- package/src/commands/clusters.ts +27 -26
- package/src/commands/code/__tests__/auth-sync.test.ts +482 -0
- package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
- package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
- package/src/commands/code/__tests__/fake-command-runner.ts +45 -42
- package/src/commands/code/__tests__/fake-file-store.ts +32 -23
- package/src/commands/code/__tests__/fake-prompter.ts +116 -77
- package/src/commands/code/__tests__/setup-flow.test.ts +624 -268
- package/src/commands/code/adapters/clack-prompter.ts +53 -39
- package/src/commands/code/adapters/fs-file-store.ts +32 -27
- package/src/commands/code/adapters/spawn-command-runner.ts +38 -29
- package/src/commands/code/auth-sync.ts +329 -0
- package/src/commands/code/errors.ts +18 -18
- package/src/commands/code/ports/auth-services.ts +14 -0
- package/src/commands/code/ports/command-runner.ts +8 -4
- package/src/commands/code/ports/file-store.ts +5 -4
- package/src/commands/code/ports/prompter.ts +24 -18
- package/src/commands/code/setup.ts +570 -340
- package/src/commands/code.ts +338 -539
- package/src/commands/index.ts +20 -19
- package/src/commands/models.ts +28 -32
- package/src/commands/users.ts +15 -21
- package/src/constants/command-structure.ts +134 -157
- package/src/services/api-key-service.ts +105 -122
- package/src/services/auth-service.ts +99 -345
- package/src/services/browser-auth.ts +296 -0
- package/src/services/chat-service.ts +265 -299
- package/src/services/cluster-service.ts +42 -45
- package/src/services/collaborator-service.ts +14 -19
- package/src/services/flux-service.ts +23 -25
- package/src/services/helm-service.ts +19 -21
- package/src/services/kubectl-service.ts +17 -19
- package/src/types/api.d.ts +1905 -1907
- package/src/types/json.d.ts +2 -2
- package/src/utils/config-checker.ts +10 -10
- package/src/utils/config-loader.ts +162 -178
- package/src/utils/default-api-key.ts +114 -125
- package/src/utils/env-manager.ts +53 -57
- package/src/utils/error-handler.ts +61 -56
- package/src/utils/logger.ts +79 -73
- package/src/utils/markdown-renderer.ts +31 -31
- package/src/utils/opencode-validator.ts +85 -89
- package/src/utils/token-manager.ts +108 -87
- package/templates/agents/app.md +1 -0
- package/templates/agents/backend.md +1 -0
- package/templates/agents/devops.md +2 -0
- package/templates/agents/frontend.md +1 -0
- package/templates/agents/fullstack.md +1 -0
- package/templates/agents/quality.md +45 -40
- package/templates/agents/security.md +1 -0
- package/tests/commands/chat.test.ts +53 -62
- package/tests/commands/code.test.ts +265 -310
- package/tests/utils/config-loader.test.ts +189 -188
- package/tests/utils/env-manager.test.ts +110 -113
- package/tests/utils/opencode-validator.test.ts +52 -56
- package/tsconfig.json +4 -3
- package/vitest.config.ts +3 -3
- package/AGENTS.md +0 -374
- 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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
119
|
+
return result;
|
|
392
120
|
} catch (error) {
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
}
|