berget 2.2.7 → 2.2.9
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 +6 -6
- package/.github/workflows/test.yml +1 -1
- package/.prettierrc +5 -3
- package/dist/index.js +24 -25
- package/dist/package.json +7 -3
- package/dist/src/agents/app.js +8 -8
- package/dist/src/agents/backend.js +3 -3
- package/dist/src/agents/devops.js +8 -8
- package/dist/src/agents/frontend.js +3 -3
- package/dist/src/agents/fullstack.js +3 -3
- package/dist/src/agents/index.js +18 -18
- package/dist/src/agents/quality.js +8 -8
- package/dist/src/agents/security.js +8 -8
- package/dist/src/client.js +115 -127
- package/dist/src/commands/api-keys.js +181 -202
- package/dist/src/commands/auth.js +16 -25
- package/dist/src/commands/autocomplete.js +8 -8
- package/dist/src/commands/billing.js +10 -19
- package/dist/src/commands/chat.js +139 -170
- package/dist/src/commands/clusters.js +21 -30
- package/dist/src/commands/code/__tests__/auth-sync.test.js +189 -186
- package/dist/src/commands/code/__tests__/fake-api-key-service.js +3 -13
- package/dist/src/commands/code/__tests__/fake-auth-service.js +21 -29
- package/dist/src/commands/code/__tests__/fake-command-runner.js +22 -33
- package/dist/src/commands/code/__tests__/fake-file-store.js +19 -41
- package/dist/src/commands/code/__tests__/fake-prompter.js +81 -97
- package/dist/src/commands/code/__tests__/setup-flow.test.js +295 -295
- package/dist/src/commands/code/adapters/clack-prompter.js +15 -32
- package/dist/src/commands/code/adapters/fs-file-store.js +25 -44
- package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -41
- package/dist/src/commands/code/auth-sync.js +215 -228
- package/dist/src/commands/code/errors.js +15 -12
- package/dist/src/commands/code/setup.js +390 -425
- package/dist/src/commands/code.js +279 -294
- package/dist/src/commands/index.js +5 -5
- package/dist/src/commands/models.js +16 -25
- package/dist/src/commands/users.js +9 -18
- package/dist/src/constants/command-structure.js +138 -138
- package/dist/src/services/api-key-service.js +132 -152
- package/dist/src/services/auth-service.js +81 -95
- package/dist/src/services/browser-auth.js +121 -131
- package/dist/src/services/chat-service.js +369 -386
- package/dist/src/services/cluster-service.js +47 -62
- package/dist/src/services/collaborator-service.js +9 -21
- package/dist/src/services/flux-service.js +13 -25
- package/dist/src/services/helm-service.js +9 -21
- package/dist/src/services/kubectl-service.js +15 -29
- package/dist/src/utils/config-checker.js +8 -8
- package/dist/src/utils/config-loader.js +109 -109
- package/dist/src/utils/default-api-key.js +129 -139
- package/dist/src/utils/env-manager.js +55 -66
- package/dist/src/utils/error-handler.js +62 -62
- package/dist/src/utils/logger.js +74 -67
- package/dist/src/utils/markdown-renderer.js +28 -28
- package/dist/src/utils/opencode-validator.js +67 -69
- package/dist/src/utils/token-manager.js +67 -65
- package/dist/tests/commands/chat.test.js +30 -39
- package/dist/tests/commands/code.test.js +186 -195
- package/dist/tests/utils/config-loader.test.js +107 -107
- package/dist/tests/utils/env-manager.test.js +81 -90
- package/dist/tests/utils/opencode-validator.test.js +42 -41
- package/dist/vitest.config.js +1 -1
- package/eslint.config.mjs +65 -30
- package/index.ts +30 -31
- package/package.json +7 -3
- package/src/agents/app.ts +9 -9
- package/src/agents/backend.ts +4 -4
- package/src/agents/devops.ts +9 -9
- package/src/agents/frontend.ts +4 -4
- package/src/agents/fullstack.ts +4 -4
- package/src/agents/index.ts +27 -25
- package/src/agents/quality.ts +9 -9
- package/src/agents/security.ts +9 -9
- package/src/agents/types.ts +10 -10
- package/src/client.ts +85 -77
- package/src/commands/api-keys.ts +180 -185
- package/src/commands/auth.ts +15 -14
- package/src/commands/autocomplete.ts +10 -10
- package/src/commands/billing.ts +13 -12
- package/src/commands/chat.ts +145 -142
- package/src/commands/clusters.ts +20 -19
- package/src/commands/code/__tests__/auth-sync.test.ts +176 -175
- package/src/commands/code/__tests__/fake-api-key-service.ts +2 -2
- package/src/commands/code/__tests__/fake-auth-service.ts +18 -18
- package/src/commands/code/__tests__/fake-command-runner.ts +28 -22
- package/src/commands/code/__tests__/fake-file-store.ts +15 -15
- package/src/commands/code/__tests__/fake-prompter.ts +86 -85
- package/src/commands/code/__tests__/setup-flow.test.ts +253 -251
- package/src/commands/code/adapters/clack-prompter.ts +32 -30
- package/src/commands/code/adapters/fs-file-store.ts +18 -17
- package/src/commands/code/adapters/spawn-command-runner.ts +20 -15
- package/src/commands/code/auth-sync.ts +210 -210
- package/src/commands/code/errors.ts +11 -11
- package/src/commands/code/ports/auth-services.ts +7 -7
- package/src/commands/code/ports/command-runner.ts +2 -2
- package/src/commands/code/ports/file-store.ts +3 -3
- package/src/commands/code/ports/prompter.ts +13 -13
- package/src/commands/code/setup.ts +408 -406
- package/src/commands/code.ts +288 -287
- package/src/commands/index.ts +11 -10
- package/src/commands/models.ts +19 -18
- package/src/commands/users.ts +11 -10
- package/src/constants/command-structure.ts +159 -159
- package/src/services/api-key-service.ts +85 -85
- package/src/services/auth-service.ts +55 -54
- package/src/services/browser-auth.ts +62 -62
- package/src/services/chat-service.ts +170 -171
- package/src/services/cluster-service.ts +28 -28
- package/src/services/collaborator-service.ts +6 -6
- package/src/services/flux-service.ts +17 -17
- package/src/services/helm-service.ts +11 -11
- package/src/services/kubectl-service.ts +12 -12
- package/src/types/api.d.ts +1933 -1933
- package/src/types/json.d.ts +1 -1
- package/src/utils/config-checker.ts +7 -7
- package/src/utils/config-loader.ts +130 -129
- package/src/utils/default-api-key.ts +81 -80
- package/src/utils/env-manager.ts +37 -37
- package/src/utils/error-handler.ts +64 -64
- package/src/utils/logger.ts +72 -66
- package/src/utils/markdown-renderer.ts +28 -28
- package/src/utils/opencode-validator.ts +72 -71
- package/src/utils/token-manager.ts +69 -68
- package/tests/commands/chat.test.ts +32 -31
- package/tests/commands/code.test.ts +182 -181
- package/tests/utils/config-loader.test.ts +111 -110
- package/tests/utils/env-manager.test.ts +83 -79
- package/tests/utils/opencode-validator.test.ts +43 -42
- package/tsconfig.json +2 -1
- package/vitest.config.ts +2 -2
|
@@ -1,205 +1,57 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import type { ApiKeyServicePort, AuthServicePort } from './ports/auth-services';
|
|
2
|
+
import type { FileStore } from './ports/file-store';
|
|
3
|
+
import type { Prompter } from './ports/prompter';
|
|
4
4
|
|
|
5
|
-
export interface
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export interface AuthDeps {
|
|
6
|
+
apiKeyService: ApiKeyServicePort;
|
|
7
|
+
authService: AuthServicePort;
|
|
8
|
+
files: FileStore;
|
|
9
|
+
homeDir: string;
|
|
10
|
+
prompter: Prompter;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface AuthResult {
|
|
12
14
|
authenticated: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export interface
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
apiKeyService: ApiKeyServicePort;
|
|
20
|
-
homeDir: string;
|
|
17
|
+
export interface CliAuth {
|
|
18
|
+
access_token: string;
|
|
19
|
+
expires_at: number;
|
|
20
|
+
refresh_token: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const CLI_AUTH_PATH = (homeDir: string) => homeDir +
|
|
23
|
+
const CLI_AUTH_PATH = (homeDir: string) => homeDir + '/.berget/auth.json';
|
|
24
24
|
|
|
25
25
|
const TOOL_AUTH_PATHS = {
|
|
26
|
-
opencode: (homeDir: string) => homeDir +
|
|
27
|
-
pi: (homeDir: string) => homeDir +
|
|
26
|
+
opencode: (homeDir: string) => homeDir + '/.local/share/opencode/auth.json',
|
|
27
|
+
pi: (homeDir: string) => homeDir + '/.pi/agent/auth.json',
|
|
28
28
|
} as const;
|
|
29
29
|
|
|
30
|
-
const TOOL_API_KEY_TYPES: Record<
|
|
31
|
-
opencode:
|
|
32
|
-
pi:
|
|
30
|
+
const TOOL_API_KEY_TYPES: Record<'opencode' | 'pi', string> = {
|
|
31
|
+
opencode: 'api',
|
|
32
|
+
pi: 'api_key',
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
const parts = accessToken.split(".");
|
|
38
|
-
if (parts.length !== 3) return 0;
|
|
39
|
-
const payload = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
40
|
-
const decoded = JSON.parse(payload);
|
|
41
|
-
if (typeof decoded.exp === "number") {
|
|
42
|
-
return decoded.exp * 1000; // JWT exp is in seconds, convert to milliseconds
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// If decoding fails, return 0 (treated as expired)
|
|
46
|
-
}
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function readCliAuth(files: FileStore, homeDir: string): Promise<CliAuth | null> {
|
|
51
|
-
const content = await files.readFile(CLI_AUTH_PATH(homeDir));
|
|
52
|
-
if (!content) return null;
|
|
53
|
-
try {
|
|
54
|
-
const parsed = JSON.parse(content);
|
|
55
|
-
if (parsed.access_token && parsed.refresh_token) {
|
|
56
|
-
// Extract the actual expiry time from the JWT token instead of using the stored expires_at
|
|
57
|
-
const jwtExpiresAt = extractJwtExpiresAt(parsed.access_token);
|
|
58
|
-
if (jwtExpiresAt === 0) {
|
|
59
|
-
// Invalid token, return null
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
access_token: parsed.access_token,
|
|
64
|
-
refresh_token: parsed.refresh_token,
|
|
65
|
-
expires_at: jwtExpiresAt,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function isToolAuthenticated(
|
|
75
|
-
files: FileStore,
|
|
76
|
-
homeDir: string,
|
|
77
|
-
tool: "opencode" | "pi"
|
|
78
|
-
): Promise<boolean> {
|
|
79
|
-
const content = await files.readFile(TOOL_AUTH_PATHS[tool](homeDir));
|
|
80
|
-
if (!content) return false;
|
|
81
|
-
try {
|
|
82
|
-
const parsed = JSON.parse(content);
|
|
83
|
-
return typeof parsed.berget === "object" && parsed.berget !== null;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function decodeJwtPayload(token: string): unknown | null {
|
|
90
|
-
try {
|
|
91
|
-
const parts = token.split(".");
|
|
92
|
-
if (parts.length !== 3) return null;
|
|
93
|
-
const payload = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
94
|
-
return JSON.parse(payload);
|
|
95
|
-
} catch {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function hasBergetCodeSeat(accessToken: string): boolean {
|
|
101
|
-
const payload = decodeJwtPayload(accessToken);
|
|
102
|
-
if (!payload || typeof payload !== "object") return false;
|
|
103
|
-
const p = payload as Record<string, unknown>;
|
|
104
|
-
const realmAccess = p.realm_access as Record<string, unknown> | undefined;
|
|
105
|
-
if (!realmAccess) return false;
|
|
106
|
-
const roles = realmAccess.roles as string[] | undefined;
|
|
107
|
-
if (!Array.isArray(roles)) return false;
|
|
108
|
-
return roles.includes("berget_code_seat");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export async function syncOAuthToTool(
|
|
112
|
-
files: FileStore,
|
|
113
|
-
homeDir: string,
|
|
114
|
-
tool: "opencode" | "pi",
|
|
115
|
-
cliAuth: CliAuth
|
|
116
|
-
): Promise<void> {
|
|
117
|
-
const authPath = TOOL_AUTH_PATHS[tool](homeDir);
|
|
118
|
-
let existing: Record<string, unknown> = {};
|
|
119
|
-
|
|
120
|
-
const content = await files.readFile(authPath);
|
|
121
|
-
if (content) {
|
|
122
|
-
try {
|
|
123
|
-
existing = JSON.parse(content) as Record<string, unknown>;
|
|
124
|
-
} catch {
|
|
125
|
-
existing = {};
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Use the JWT's actual expiry time for consistency
|
|
130
|
-
const jwtExpiresAt = extractJwtExpiresAt(cliAuth.access_token);
|
|
131
|
-
|
|
132
|
-
const updated = {
|
|
133
|
-
...existing,
|
|
134
|
-
berget: {
|
|
135
|
-
type: "oauth",
|
|
136
|
-
access: cliAuth.access_token,
|
|
137
|
-
refresh: cliAuth.refresh_token,
|
|
138
|
-
expires: jwtExpiresAt,
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
await files.writeFile(authPath, JSON.stringify(updated, null, 2) + "\n");
|
|
143
|
-
await files.chmod(authPath, 0o600);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export async function syncApiKeyToTool(
|
|
147
|
-
files: FileStore,
|
|
148
|
-
homeDir: string,
|
|
149
|
-
tool: "opencode" | "pi",
|
|
150
|
-
apiKey: string
|
|
151
|
-
): Promise<void> {
|
|
152
|
-
const authPath = TOOL_AUTH_PATHS[tool](homeDir);
|
|
153
|
-
let existing: Record<string, unknown> = {};
|
|
154
|
-
|
|
155
|
-
const content = await files.readFile(authPath);
|
|
156
|
-
if (content) {
|
|
157
|
-
try {
|
|
158
|
-
existing = JSON.parse(content) as Record<string, unknown>;
|
|
159
|
-
} catch {
|
|
160
|
-
existing = {};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const updated = {
|
|
165
|
-
...existing,
|
|
166
|
-
berget: {
|
|
167
|
-
type: TOOL_API_KEY_TYPES[tool],
|
|
168
|
-
key: apiKey,
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
await files.writeFile(authPath, JSON.stringify(updated, null, 2) + "\n");
|
|
173
|
-
await files.chmod(authPath, 0o600);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function isTokenExpired(expiresAt: number): boolean {
|
|
177
|
-
const now = Date.now();
|
|
178
|
-
const timeUntilExpiry = expiresAt - now;
|
|
179
|
-
const buffer = Math.min(30 * 1000, timeUntilExpiry * 0.1);
|
|
180
|
-
return now + buffer >= expiresAt;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export async function configureAuth(deps: AuthDeps, tool: "opencode" | "pi"): Promise<AuthResult> {
|
|
184
|
-
const { prompter, files, authService, apiKeyService, homeDir } = deps;
|
|
35
|
+
export async function configureAuth(deps: AuthDeps, tool: 'opencode' | 'pi'): Promise<AuthResult> {
|
|
36
|
+
const { apiKeyService, authService, files, homeDir, prompter } = deps;
|
|
185
37
|
|
|
186
38
|
const alreadyAuth = await isToolAuthenticated(files, homeDir, tool);
|
|
187
39
|
|
|
188
40
|
if (alreadyAuth) {
|
|
189
|
-
const choice = await prompter.select<
|
|
190
|
-
message: `Account is already connected to Berget AI (${tool ===
|
|
41
|
+
const choice = await prompter.select<'keep' | 'reconfigure'>({
|
|
42
|
+
message: `Account is already connected to Berget AI (${tool === 'opencode' ? 'OpenCode' : 'Pi'}). How do you want to proceed?`,
|
|
191
43
|
options: [
|
|
192
|
-
{
|
|
193
|
-
{
|
|
44
|
+
{ label: 'Keep existing authentication', value: 'keep' },
|
|
45
|
+
{ label: 'Reconfigure — choose a different method', value: 'reconfigure' },
|
|
194
46
|
],
|
|
195
47
|
});
|
|
196
48
|
|
|
197
|
-
if (choice ===
|
|
49
|
+
if (choice === 'keep') {
|
|
198
50
|
return { authenticated: true };
|
|
199
51
|
}
|
|
200
52
|
// Fall through to reconfigure
|
|
201
53
|
} else {
|
|
202
|
-
prompter.note(
|
|
54
|
+
prompter.note('Authentication required to use Berget AI.', 'Connect your account');
|
|
203
55
|
}
|
|
204
56
|
|
|
205
57
|
// Try to reuse existing CLI tokens (from ~/.berget/auth.json)
|
|
@@ -208,31 +60,31 @@ export async function configureAuth(deps: AuthDeps, tool: "opencode" | "pi"): Pr
|
|
|
208
60
|
if (!cliAuth || isTokenExpired(cliAuth.expires_at)) {
|
|
209
61
|
// No valid tokens → full browser login
|
|
210
62
|
const s = prompter.spinner();
|
|
211
|
-
s.start(
|
|
63
|
+
s.start('Waiting for browser login...');
|
|
212
64
|
|
|
213
65
|
const loginResult = await authService.loginInteractive();
|
|
214
66
|
if (!loginResult.success) {
|
|
215
|
-
s.stop(
|
|
67
|
+
s.stop('Login failed.');
|
|
216
68
|
prompter.note(
|
|
217
|
-
`${loginResult.error ||
|
|
218
|
-
|
|
69
|
+
`${loginResult.error || 'Login timed out or was cancelled.'}\n\nPlease run \`berget auth login\` manually, then run \`berget code setup\` again.`,
|
|
70
|
+
'Authentication Failed',
|
|
219
71
|
);
|
|
220
72
|
return { authenticated: false };
|
|
221
73
|
}
|
|
222
74
|
|
|
223
|
-
s.stop(
|
|
75
|
+
s.stop('Successfully logged in to Berget.');
|
|
224
76
|
|
|
225
77
|
const jwtExpiresAt = extractJwtExpiresAt(loginResult.accessToken!);
|
|
226
78
|
if (jwtExpiresAt === 0) {
|
|
227
|
-
s.stop(
|
|
228
|
-
prompter.note(
|
|
79
|
+
s.stop('Login succeeded but received invalid token.');
|
|
80
|
+
prompter.note('Please try logging in again or contact support.', 'Authentication Error');
|
|
229
81
|
return { authenticated: false };
|
|
230
82
|
}
|
|
231
83
|
|
|
232
84
|
cliAuth = {
|
|
233
85
|
access_token: loginResult.accessToken!,
|
|
234
|
-
refresh_token: loginResult.refreshToken!,
|
|
235
86
|
expires_at: jwtExpiresAt,
|
|
87
|
+
refresh_token: loginResult.refreshToken!,
|
|
236
88
|
};
|
|
237
89
|
}
|
|
238
90
|
|
|
@@ -244,50 +96,50 @@ export async function configureAuth(deps: AuthDeps, tool: "opencode" | "pi"): Pr
|
|
|
244
96
|
// we can't verify the subscription role. Let the tool handle authorization.
|
|
245
97
|
if (!jwtPayload) {
|
|
246
98
|
const s = prompter.spinner();
|
|
247
|
-
s.start(
|
|
99
|
+
s.start('Authenticating with Berget AI...');
|
|
248
100
|
await syncOAuthToTool(files, homeDir, tool, cliAuth);
|
|
249
|
-
s.stop(
|
|
101
|
+
s.stop('Authenticated.');
|
|
250
102
|
prompter.note(
|
|
251
|
-
|
|
252
|
-
|
|
103
|
+
'Warning: Could not verify Berget Code subscription status.\nIf you do not have a subscription, the tool may show an authorization error.',
|
|
104
|
+
'Authentication',
|
|
253
105
|
);
|
|
254
106
|
return { authenticated: true };
|
|
255
107
|
}
|
|
256
108
|
|
|
257
109
|
if (hasSeat) {
|
|
258
110
|
// Case B: Has seat — ask how to authenticate
|
|
259
|
-
const method = await prompter.select<
|
|
260
|
-
message:
|
|
111
|
+
const method = await prompter.select<'api_key' | 'subscription'>({
|
|
112
|
+
message: 'You have a Berget Code subscription. How do you want to authenticate?',
|
|
261
113
|
options: [
|
|
262
|
-
{
|
|
263
|
-
{
|
|
114
|
+
{ label: 'Use my Berget Code subscription', value: 'subscription' },
|
|
115
|
+
{ label: 'Use an API key instead', value: 'api_key' },
|
|
264
116
|
],
|
|
265
117
|
});
|
|
266
118
|
|
|
267
|
-
if (method ===
|
|
119
|
+
if (method === 'subscription') {
|
|
268
120
|
const s = prompter.spinner();
|
|
269
|
-
s.start(
|
|
121
|
+
s.start('Authenticating with Berget AI via subscription...');
|
|
270
122
|
await syncOAuthToTool(files, homeDir, tool, cliAuth);
|
|
271
|
-
s.stop(
|
|
123
|
+
s.stop('Authenticated.');
|
|
272
124
|
return { authenticated: true };
|
|
273
125
|
}
|
|
274
126
|
|
|
275
127
|
// Create API key instead
|
|
276
128
|
const s = prompter.spinner();
|
|
277
|
-
s.start(
|
|
129
|
+
s.start('Creating API key...');
|
|
278
130
|
try {
|
|
279
131
|
const { key } = await apiKeyService.create({
|
|
280
|
-
|
|
281
|
-
|
|
132
|
+
description: 'Created by berget code setup',
|
|
133
|
+
name: `${tool === 'opencode' ? 'OpenCode' : 'Pi'} (created by berget CLI)`,
|
|
282
134
|
});
|
|
283
135
|
await syncApiKeyToTool(files, homeDir, tool, key);
|
|
284
|
-
s.stop(
|
|
136
|
+
s.stop('API key created and saved.');
|
|
285
137
|
return { authenticated: true };
|
|
286
138
|
} catch {
|
|
287
|
-
s.stop(
|
|
139
|
+
s.stop('API key creation failed.');
|
|
288
140
|
prompter.note(
|
|
289
|
-
|
|
290
|
-
|
|
141
|
+
'Could not create API key. Please create one manually with `berget api-keys create`.',
|
|
142
|
+
'Error',
|
|
291
143
|
);
|
|
292
144
|
return { authenticated: false };
|
|
293
145
|
}
|
|
@@ -295,26 +147,26 @@ export async function configureAuth(deps: AuthDeps, tool: "opencode" | "pi"): Pr
|
|
|
295
147
|
|
|
296
148
|
// No Berget Code seat — prompt for API key creation
|
|
297
149
|
const shouldCreate = await prompter.confirm({
|
|
298
|
-
message: "You do not have a Berget Code subscription. Would you like to create a new API key?",
|
|
299
150
|
initialValue: true,
|
|
151
|
+
message: 'You do not have a Berget Code subscription. Would you like to create a new API key?',
|
|
300
152
|
});
|
|
301
153
|
|
|
302
154
|
if (shouldCreate) {
|
|
303
155
|
const s = prompter.spinner();
|
|
304
|
-
s.start(
|
|
156
|
+
s.start('Creating API key...');
|
|
305
157
|
try {
|
|
306
158
|
const { key } = await apiKeyService.create({
|
|
307
|
-
|
|
308
|
-
|
|
159
|
+
description: 'Created by berget code setup',
|
|
160
|
+
name: `${tool === 'opencode' ? 'OpenCode' : 'Pi'} (created by berget CLI)`,
|
|
309
161
|
});
|
|
310
162
|
await syncApiKeyToTool(files, homeDir, tool, key);
|
|
311
|
-
s.stop(
|
|
163
|
+
s.stop('API key created and saved.');
|
|
312
164
|
return { authenticated: true };
|
|
313
165
|
} catch {
|
|
314
|
-
s.stop(
|
|
166
|
+
s.stop('API key creation failed.');
|
|
315
167
|
prompter.note(
|
|
316
|
-
|
|
317
|
-
|
|
168
|
+
'Could not create API key. Please create one manually with `berget api-keys create`.',
|
|
169
|
+
'Error',
|
|
318
170
|
);
|
|
319
171
|
return { authenticated: false };
|
|
320
172
|
}
|
|
@@ -323,7 +175,155 @@ export async function configureAuth(deps: AuthDeps, tool: "opencode" | "pi"): Pr
|
|
|
323
175
|
// Case D: Declined
|
|
324
176
|
prompter.note(
|
|
325
177
|
'Authentication skipped. You\'ll need to set up authentication manually:\n1. Run: berget api-keys create --name "My Key"\n2. Set BERGET_API_KEY environment variable, or\n3. Run `berget auth login` and try again',
|
|
326
|
-
|
|
178
|
+
'Authentication',
|
|
327
179
|
);
|
|
328
180
|
return { authenticated: false };
|
|
329
181
|
}
|
|
182
|
+
|
|
183
|
+
export function decodeJwtPayload(token: string): null | unknown {
|
|
184
|
+
try {
|
|
185
|
+
const parts = token.split('.');
|
|
186
|
+
if (parts.length !== 3) return null;
|
|
187
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
188
|
+
return JSON.parse(payload);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function hasBergetCodeSeat(accessToken: string): boolean {
|
|
195
|
+
const payload = decodeJwtPayload(accessToken);
|
|
196
|
+
if (!payload || typeof payload !== 'object') return false;
|
|
197
|
+
const p = payload as Record<string, unknown>;
|
|
198
|
+
const realmAccess = p.realm_access as Record<string, unknown> | undefined;
|
|
199
|
+
if (!realmAccess) return false;
|
|
200
|
+
const roles = realmAccess.roles as string[] | undefined;
|
|
201
|
+
if (!Array.isArray(roles)) return false;
|
|
202
|
+
return roles.includes('berget_code_seat');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function isTokenExpired(expiresAt: number): boolean {
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
const timeUntilExpiry = expiresAt - now;
|
|
208
|
+
const buffer = Math.min(30 * 1000, timeUntilExpiry * 0.1);
|
|
209
|
+
return now + buffer >= expiresAt;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function isToolAuthenticated(
|
|
213
|
+
files: FileStore,
|
|
214
|
+
homeDir: string,
|
|
215
|
+
tool: 'opencode' | 'pi',
|
|
216
|
+
): Promise<boolean> {
|
|
217
|
+
const content = await files.readFile(TOOL_AUTH_PATHS[tool](homeDir));
|
|
218
|
+
if (!content) return false;
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(content);
|
|
221
|
+
return typeof parsed.berget === 'object' && parsed.berget !== null;
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function readCliAuth(files: FileStore, homeDir: string): Promise<CliAuth | null> {
|
|
228
|
+
const content = await files.readFile(CLI_AUTH_PATH(homeDir));
|
|
229
|
+
if (!content) return null;
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(content);
|
|
232
|
+
if (parsed.access_token && parsed.refresh_token) {
|
|
233
|
+
// Extract the actual expiry time from the JWT token instead of using the stored expires_at
|
|
234
|
+
const jwtExpiresAt = extractJwtExpiresAt(parsed.access_token);
|
|
235
|
+
if (jwtExpiresAt === 0) {
|
|
236
|
+
// Invalid token, return null
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
access_token: parsed.access_token,
|
|
241
|
+
expires_at: jwtExpiresAt,
|
|
242
|
+
refresh_token: parsed.refresh_token,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function syncApiKeyToTool(
|
|
252
|
+
files: FileStore,
|
|
253
|
+
homeDir: string,
|
|
254
|
+
tool: 'opencode' | 'pi',
|
|
255
|
+
apiKey: string,
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const authPath = TOOL_AUTH_PATHS[tool](homeDir);
|
|
258
|
+
let existing: Record<string, unknown> = {};
|
|
259
|
+
|
|
260
|
+
const content = await files.readFile(authPath);
|
|
261
|
+
if (content) {
|
|
262
|
+
try {
|
|
263
|
+
existing = JSON.parse(content) as Record<string, unknown>;
|
|
264
|
+
} catch {
|
|
265
|
+
existing = {};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const updated = {
|
|
270
|
+
...existing,
|
|
271
|
+
berget: {
|
|
272
|
+
key: apiKey,
|
|
273
|
+
type: TOOL_API_KEY_TYPES[tool],
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
await files.writeFile(authPath, JSON.stringify(updated, null, 2) + '\n');
|
|
278
|
+
await files.chmod(authPath, 0o600);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function syncOAuthToTool(
|
|
282
|
+
files: FileStore,
|
|
283
|
+
homeDir: string,
|
|
284
|
+
tool: 'opencode' | 'pi',
|
|
285
|
+
cliAuth: CliAuth,
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
const authPath = TOOL_AUTH_PATHS[tool](homeDir);
|
|
288
|
+
let existing: Record<string, unknown> = {};
|
|
289
|
+
|
|
290
|
+
const content = await files.readFile(authPath);
|
|
291
|
+
if (content) {
|
|
292
|
+
try {
|
|
293
|
+
existing = JSON.parse(content) as Record<string, unknown>;
|
|
294
|
+
} catch {
|
|
295
|
+
existing = {};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Use the JWT's actual expiry time for consistency
|
|
300
|
+
const jwtExpiresAt = extractJwtExpiresAt(cliAuth.access_token);
|
|
301
|
+
|
|
302
|
+
const updated = {
|
|
303
|
+
...existing,
|
|
304
|
+
berget: {
|
|
305
|
+
access: cliAuth.access_token,
|
|
306
|
+
expires: jwtExpiresAt,
|
|
307
|
+
refresh: cliAuth.refresh_token,
|
|
308
|
+
type: 'oauth',
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
await files.writeFile(authPath, JSON.stringify(updated, null, 2) + '\n');
|
|
313
|
+
await files.chmod(authPath, 0o600);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function extractJwtExpiresAt(accessToken: string): number {
|
|
317
|
+
try {
|
|
318
|
+
const parts = accessToken.split('.');
|
|
319
|
+
if (parts.length !== 3) return 0;
|
|
320
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
321
|
+
const decoded = JSON.parse(payload);
|
|
322
|
+
if (typeof decoded.exp === 'number') {
|
|
323
|
+
return decoded.exp * 1000; // JWT exp is in seconds, convert to milliseconds
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
// If decoding fails, return 0 (treated as expired)
|
|
327
|
+
}
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
export class PrerequisiteError extends Error {
|
|
2
|
-
constructor(public readonly binary: string) {
|
|
3
|
-
super(`Required binary not found: ${binary}`);
|
|
4
|
-
this.name = "PrerequisiteError";
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
|
|
8
1
|
export class CancelledError extends Error {
|
|
9
2
|
constructor() {
|
|
10
|
-
super(
|
|
11
|
-
this.name =
|
|
3
|
+
super('Wizard cancelled');
|
|
4
|
+
this.name = 'CancelledError';
|
|
12
5
|
}
|
|
13
6
|
}
|
|
14
7
|
|
|
15
8
|
export class CommandFailedError extends Error {
|
|
16
9
|
constructor(
|
|
17
10
|
public readonly command: string,
|
|
18
|
-
public readonly exitCode: number
|
|
11
|
+
public readonly exitCode: number,
|
|
19
12
|
) {
|
|
20
13
|
super(`Command "${command}" failed with exit code ${exitCode}`);
|
|
21
|
-
this.name =
|
|
14
|
+
this.name = 'CommandFailedError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PrerequisiteError extends Error {
|
|
19
|
+
constructor(public readonly binary: string) {
|
|
20
|
+
super(`Required binary not found: ${binary}`);
|
|
21
|
+
this.name = 'PrerequisiteError';
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
export interface ApiKeyServicePort {
|
|
2
|
+
create(options: { description?: string; name: string }): Promise<{ key: string }>;
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
export interface AuthServicePort {
|
|
2
6
|
login(): Promise<boolean>;
|
|
3
7
|
loginInteractive(): Promise<{
|
|
4
|
-
success: boolean;
|
|
5
8
|
accessToken?: string;
|
|
6
|
-
refreshToken?: string;
|
|
7
|
-
expiresIn?: number;
|
|
8
9
|
error?: string;
|
|
10
|
+
expiresIn?: number;
|
|
11
|
+
refreshToken?: string;
|
|
12
|
+
success: boolean;
|
|
9
13
|
}>;
|
|
10
14
|
}
|
|
11
|
-
|
|
12
|
-
export interface ApiKeyServicePort {
|
|
13
|
-
create(options: { name: string; description?: string }): Promise<{ key: string }>;
|
|
14
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface FileStore {
|
|
2
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
2
3
|
exists(path: string): Promise<boolean>;
|
|
3
|
-
readFile(path: string): Promise<string | null>;
|
|
4
|
-
writeFile(path: string, content: string): Promise<void>;
|
|
5
4
|
mkdir(path: string): Promise<void>;
|
|
6
|
-
|
|
5
|
+
readFile(path: string): Promise<null | string>;
|
|
6
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
7
7
|
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
export interface Prompter {
|
|
2
|
+
confirm(options: { initialValue?: boolean; message: string }): Promise<boolean>;
|
|
2
3
|
intro(message: string): void;
|
|
3
|
-
|
|
4
|
-
note(message: string, title?: string): void;
|
|
5
|
-
spinner(): Spinner;
|
|
6
|
-
select<T>(opts: {
|
|
4
|
+
multiselect<T>(options: {
|
|
7
5
|
message: string;
|
|
8
6
|
options: ReadonlyArray<{
|
|
9
|
-
value: T;
|
|
10
|
-
label: string;
|
|
11
7
|
hint?: string;
|
|
8
|
+
label: string;
|
|
9
|
+
value: T;
|
|
12
10
|
}>;
|
|
13
|
-
}): Promise<T>;
|
|
14
|
-
|
|
11
|
+
}): Promise<T[]>;
|
|
12
|
+
note(message: string, title?: string): void;
|
|
13
|
+
outro(message: string): void;
|
|
14
|
+
select<T>(options: {
|
|
15
15
|
message: string;
|
|
16
16
|
options: ReadonlyArray<{
|
|
17
|
-
value: T;
|
|
18
|
-
label: string;
|
|
19
17
|
hint?: string;
|
|
18
|
+
label: string;
|
|
19
|
+
value: T;
|
|
20
20
|
}>;
|
|
21
|
-
}): Promise<T
|
|
22
|
-
|
|
23
|
-
text(
|
|
21
|
+
}): Promise<T>;
|
|
22
|
+
spinner(): Spinner;
|
|
23
|
+
text(options: { message: string; placeholder?: string }): Promise<string>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface Spinner {
|