berget 0.0.3 → 0.1.0

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/src/client.ts ADDED
@@ -0,0 +1,133 @@
1
+ import createClient from 'openapi-fetch'
2
+ import type { paths } from './types/api'
3
+ import * as fs from 'fs'
4
+ import * as path from 'path'
5
+ import * as os from 'os'
6
+ import chalk from 'chalk'
7
+
8
+ // Configuration directory
9
+ const CONFIG_DIR = path.join(os.homedir(), '.berget')
10
+ const TOKEN_FILE = path.join(CONFIG_DIR, 'token.json')
11
+
12
+ // API Base URL
13
+ // Use --local flag to test against local API
14
+ const isLocalMode = process.argv.includes('--local')
15
+ const API_BASE_URL =
16
+ process.env.BERGET_API_URL ||
17
+ (isLocalMode ? 'http://localhost:3000' : 'https://api.berget.ai')
18
+
19
+ if (isLocalMode && !process.env.BERGET_API_URL) {
20
+ console.log(chalk.yellow('Using local API endpoint: http://localhost:3000'))
21
+ }
22
+
23
+ // Create a typed client for the Berget API
24
+ export const apiClient = createClient<paths>({
25
+ baseUrl: API_BASE_URL,
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ Accept: 'application/json',
29
+ },
30
+ })
31
+
32
+ // Authentication functions
33
+ export const getAuthToken = (): string | null => {
34
+ try {
35
+ if (fs.existsSync(TOKEN_FILE)) {
36
+ const tokenData = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'))
37
+ return tokenData.accessToken
38
+ }
39
+ } catch (error) {
40
+ console.error('Error reading auth token:', error)
41
+ }
42
+ return null
43
+ }
44
+
45
+ // Check if token is expired (JWT tokens have an exp claim)
46
+ export const isTokenExpired = (token: string): boolean => {
47
+ try {
48
+ const base64Url = token.split('.')[1]
49
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
50
+ const jsonPayload = decodeURIComponent(
51
+ atob(base64)
52
+ .split('')
53
+ .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
54
+ .join('')
55
+ )
56
+ const payload = JSON.parse(jsonPayload)
57
+
58
+ // Check if token has expired
59
+ if (payload.exp) {
60
+ return payload.exp * 1000 < Date.now()
61
+ }
62
+ } catch (error) {
63
+ // If we can't decode the token, assume it's expired
64
+ return true
65
+ }
66
+
67
+ // If there's no exp claim, assume it's valid
68
+ return false
69
+ }
70
+
71
+ export const saveAuthToken = (token: string): void => {
72
+ try {
73
+ if (!fs.existsSync(CONFIG_DIR)) {
74
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
75
+ }
76
+ fs.writeFileSync(TOKEN_FILE, JSON.stringify({ accessToken: token }), 'utf8')
77
+ // Set file permissions to be readable only by the owner
78
+ fs.chmodSync(TOKEN_FILE, 0o600)
79
+ } catch (error) {
80
+ console.error('Error saving auth token:', error)
81
+ }
82
+ }
83
+
84
+ export const clearAuthToken = (): void => {
85
+ try {
86
+ if (fs.existsSync(TOKEN_FILE)) {
87
+ fs.unlinkSync(TOKEN_FILE)
88
+ }
89
+ } catch (error) {
90
+ console.error('Error clearing auth token:', error)
91
+ }
92
+ }
93
+
94
+ // Create an authenticated client
95
+ export const createAuthenticatedClient = () => {
96
+ const token = getAuthToken()
97
+ if (!token) {
98
+ console.warn(
99
+ chalk.yellow(
100
+ 'No authentication token found. Please run `berget login` first.'
101
+ )
102
+ )
103
+ } else if (isTokenExpired(token)) {
104
+ console.warn(
105
+ chalk.yellow(
106
+ 'Your authentication token has expired. Please run `berget login` to get a new token.'
107
+ )
108
+ )
109
+ // Optionally clear the expired token
110
+ clearAuthToken()
111
+ return createClient<paths>({
112
+ baseUrl: API_BASE_URL,
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ Accept: 'application/json',
116
+ },
117
+ })
118
+ }
119
+
120
+ return createClient<paths>({
121
+ baseUrl: API_BASE_URL,
122
+ headers: token
123
+ ? {
124
+ Authorization: `Bearer ${token}`,
125
+ 'Content-Type': 'application/json',
126
+ Accept: 'application/json',
127
+ }
128
+ : {
129
+ 'Content-Type': 'application/json',
130
+ Accept: 'application/json',
131
+ },
132
+ })
133
+ }
@@ -0,0 +1,114 @@
1
+ import { createAuthenticatedClient } from '../client'
2
+ import { handleError } from '../utils/error-handler'
3
+
4
+ export interface ApiKey {
5
+ id: number
6
+ name: string
7
+ description: string | null
8
+ created: string
9
+ lastUsed: string | null
10
+ prefix: string
11
+ active: boolean
12
+ modified: string
13
+ }
14
+
15
+ export interface CreateApiKeyOptions {
16
+ name: string
17
+ description?: string
18
+ }
19
+
20
+ export interface ApiKeyResponse {
21
+ id: number
22
+ name: string
23
+ description: string | null
24
+ key: string
25
+ created: string
26
+ }
27
+
28
+ export class ApiKeyService {
29
+ private static instance: ApiKeyService
30
+ private client = createAuthenticatedClient()
31
+
32
+ private constructor() {}
33
+
34
+ public static getInstance(): ApiKeyService {
35
+ if (!ApiKeyService.instance) {
36
+ ApiKeyService.instance = new ApiKeyService()
37
+ }
38
+ return ApiKeyService.instance
39
+ }
40
+
41
+ public async listApiKeys(): Promise<ApiKey[]> {
42
+ try {
43
+ const { data, error } = await this.client.GET('/v1/api-keys')
44
+ if (error) {
45
+ // Check if this is an authentication error
46
+ const errorObj = typeof error === 'string' ? JSON.parse(error) : error;
47
+ if (errorObj.status === 401) {
48
+ throw new Error(JSON.stringify({
49
+ error: "Authentication failed. Your session may have expired.",
50
+ code: "AUTH_FAILED",
51
+ details: "Please run 'berget login' to authenticate again."
52
+ }))
53
+ }
54
+ throw new Error(JSON.stringify(error))
55
+ }
56
+ return data || []
57
+ } catch (error) {
58
+ handleError('Failed to list API keys', error)
59
+ throw error
60
+ }
61
+ }
62
+
63
+ public async createApiKey(options: CreateApiKeyOptions): Promise<ApiKeyResponse> {
64
+ try {
65
+ const { data, error } = await this.client.POST('/v1/api-keys', {
66
+ body: options
67
+ })
68
+ if (error) throw new Error(JSON.stringify(error))
69
+ return data!
70
+ } catch (error) {
71
+ console.error('Failed to create API key:', error)
72
+ throw error
73
+ }
74
+ }
75
+
76
+ public async deleteApiKey(id: string): Promise<boolean> {
77
+ try {
78
+ const { error } = await this.client.DELETE('/v1/api-keys/{id}', {
79
+ params: { path: { id } }
80
+ })
81
+ if (error) throw new Error(JSON.stringify(error))
82
+ return true
83
+ } catch (error) {
84
+ console.error('Failed to delete API key:', error)
85
+ throw error
86
+ }
87
+ }
88
+
89
+ public async rotateApiKey(id: string): Promise<ApiKeyResponse> {
90
+ try {
91
+ const { data, error } = await this.client.PUT('/v1/api-keys/{id}/rotate', {
92
+ params: { path: { id } }
93
+ })
94
+ if (error) throw new Error(JSON.stringify(error))
95
+ return data!
96
+ } catch (error) {
97
+ console.error('Failed to rotate API key:', error)
98
+ throw error
99
+ }
100
+ }
101
+
102
+ public async getApiKeyUsage(id: string): Promise<any> {
103
+ try {
104
+ const { data, error } = await this.client.GET('/v1/api-keys/{id}/usage', {
105
+ params: { path: { id } }
106
+ })
107
+ if (error) throw new Error(JSON.stringify(error))
108
+ return data
109
+ } catch (error) {
110
+ console.error('Failed to get API key usage:', error)
111
+ throw error
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,206 @@
1
+ import {
2
+ createAuthenticatedClient,
3
+ saveAuthToken,
4
+ clearAuthToken,
5
+ apiClient,
6
+ } from '../client'
7
+ import open from 'open'
8
+ import chalk from 'chalk'
9
+ import { handleError } from '../utils/error-handler'
10
+
11
+ export class AuthService {
12
+ private static instance: AuthService
13
+ private client = createAuthenticatedClient()
14
+
15
+ private constructor() {}
16
+
17
+ public static getInstance(): AuthService {
18
+ if (!AuthService.instance) {
19
+ AuthService.instance = new AuthService()
20
+ }
21
+ return AuthService.instance
22
+ }
23
+
24
+ public async login(): Promise<boolean> {
25
+ try {
26
+ // Clear any existing token to ensure a fresh login
27
+ clearAuthToken()
28
+
29
+ console.log(chalk.blue('Initiating login process...'))
30
+
31
+ // Step 1: Initiate device authorization
32
+ const { data: deviceData, error: deviceError } = await apiClient.POST(
33
+ '/v1/auth/device',
34
+ {}
35
+ )
36
+
37
+ if (deviceError || !deviceData) {
38
+ throw new Error(
39
+ deviceError
40
+ ? JSON.stringify(deviceError)
41
+ : 'Failed to get device authorization data'
42
+ )
43
+ }
44
+
45
+ // Display information to user
46
+ console.log(chalk.cyan('\nTo complete login:'))
47
+ console.log(
48
+ chalk.cyan(
49
+ `1. Open this URL: ${chalk.bold(
50
+ deviceData.verification_url || 'https://auth.berget.ai/device'
51
+ )}`
52
+ )
53
+ )
54
+ console.log(
55
+ chalk.cyan(
56
+ `2. Enter this code: ${chalk.bold(deviceData.user_code || '')}\n`
57
+ )
58
+ )
59
+
60
+ // Try to open browser automatically
61
+ try {
62
+ if (deviceData.verification_url) {
63
+ await open(deviceData.verification_url)
64
+ console.log(
65
+ chalk.dim(
66
+ "Browser opened automatically. If it didn't open, please use the URL above."
67
+ )
68
+ )
69
+ }
70
+ } catch (error) {
71
+ console.log(
72
+ chalk.yellow(
73
+ 'Could not open browser automatically. Please open the URL manually.'
74
+ )
75
+ )
76
+ }
77
+
78
+ console.log(chalk.dim('\nWaiting for authentication to complete...'))
79
+
80
+ // Step 2: Poll for completion
81
+ const startTime = Date.now()
82
+ const expiresIn =
83
+ deviceData.expires_in !== undefined ? deviceData.expires_in : 900
84
+ const expiresAt = startTime + expiresIn * 1000
85
+ let pollInterval = (deviceData.interval || 5) * 1000
86
+
87
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
88
+ let spinnerIdx = 0
89
+
90
+ while (Date.now() < expiresAt) {
91
+ // Wait for the polling interval
92
+ await new Promise((resolve) => setTimeout(resolve, pollInterval))
93
+
94
+ // Update spinner
95
+ process.stdout.write(
96
+ `\r${chalk.blue(spinner[spinnerIdx])} Waiting for authentication...`
97
+ )
98
+ spinnerIdx = (spinnerIdx + 1) % spinner.length
99
+
100
+ // Check if authentication is complete
101
+ const deviceCode = deviceData.device_code || ''
102
+ const { data: tokenData, error: tokenError } = await apiClient.POST(
103
+ '/v1/auth/device/token',
104
+ {
105
+ body: {
106
+ device_code: deviceCode,
107
+ },
108
+ }
109
+ )
110
+
111
+ if (tokenError) {
112
+ // Parse the error to get status and other details
113
+ const errorObj =
114
+ typeof tokenError === 'string' ? JSON.parse(tokenError) : tokenError
115
+
116
+ const status = errorObj.status || 0
117
+ const errorCode = errorObj.code || ''
118
+
119
+ if (status === 401 || errorCode === 'AUTHORIZATION_PENDING') {
120
+ // Still waiting for user to complete authorization
121
+ continue
122
+ } else if (status === 429) {
123
+ // Slow down
124
+ pollInterval *= 2
125
+ continue
126
+ } else if (status === 400) {
127
+ // Error or expired
128
+ if (errorCode === 'EXPIRED_TOKEN') {
129
+ console.log(
130
+ chalk.red('\n\nAuthentication timed out. Please try again.')
131
+ )
132
+ } else if (errorCode !== 'AUTHORIZATION_PENDING') {
133
+ // Only show error if it's not the expected "still waiting" error
134
+ const errorMessage = errorObj.message || JSON.stringify(errorObj)
135
+ console.log(chalk.red(`\n\nError: ${errorMessage}`))
136
+ return false
137
+ } else {
138
+ // If it's AUTHORIZATION_PENDING, continue polling
139
+ continue
140
+ }
141
+ return false
142
+ } else {
143
+ // For any other error, log it but continue polling
144
+ // This makes the flow more resilient to temporary issues
145
+ if (process.env.DEBUG) {
146
+ console.log(
147
+ chalk.yellow(`\n\nReceived error: ${JSON.stringify(errorObj)}`)
148
+ )
149
+ console.log(
150
+ chalk.yellow('Continuing to wait for authentication...')
151
+ )
152
+ process.stdout.write(
153
+ `\r${chalk.blue(
154
+ spinner[spinnerIdx]
155
+ )} Waiting for authentication...`
156
+ )
157
+ }
158
+ continue
159
+ }
160
+ } else if (tokenData && tokenData.token) {
161
+ // Success!
162
+ saveAuthToken(tokenData.token)
163
+
164
+ process.stdout.write('\r' + ' '.repeat(50) + '\r') // Clear the spinner line
165
+ console.log(chalk.green('✓ Successfully logged in to Berget'))
166
+
167
+ if (tokenData.user) {
168
+ const user = tokenData.user as any
169
+ console.log(
170
+ chalk.green(`Logged in as ${user.name || user.email || 'User'}`)
171
+ )
172
+ }
173
+
174
+ return true
175
+ }
176
+ }
177
+
178
+ console.log(chalk.red('\n\nAuthentication timed out. Please try again.'))
179
+ return false
180
+ } catch (error) {
181
+ handleError('Login failed', error)
182
+ return false
183
+ }
184
+ }
185
+
186
+ public async isAuthenticated(): Promise<boolean> {
187
+ try {
188
+ // Call an API endpoint that requires authentication
189
+ const { data, error } = await this.client.GET('/v1/users/me')
190
+ return !!data && !error
191
+ } catch {
192
+ return false
193
+ }
194
+ }
195
+
196
+ public async getUserProfile() {
197
+ try {
198
+ const { data, error } = await this.client.GET('/v1/users/me')
199
+ if (error) throw new Error(JSON.stringify(error))
200
+ return data
201
+ } catch (error) {
202
+ handleError('Failed to get user profile', error)
203
+ throw error
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,50 @@
1
+ import { createAuthenticatedClient } from '../client'
2
+
3
+ export interface Cluster {
4
+ id: string
5
+ name: string
6
+ status: string
7
+ nodes: number
8
+ created: string
9
+ }
10
+
11
+ export class ClusterService {
12
+ private static instance: ClusterService
13
+ private client = createAuthenticatedClient()
14
+
15
+ private constructor() {}
16
+
17
+ public static getInstance(): ClusterService {
18
+ if (!ClusterService.instance) {
19
+ ClusterService.instance = new ClusterService()
20
+ }
21
+ return ClusterService.instance
22
+ }
23
+
24
+ public async getClusterUsage(clusterId: string): Promise<any> {
25
+ try {
26
+ const { data, error } = await this.client.GET(
27
+ '/v1/clusters/{clusterId}/usage',
28
+ {
29
+ params: { path: { clusterId } },
30
+ }
31
+ )
32
+ if (error) throw new Error(JSON.stringify(error))
33
+ return data
34
+ } catch (error) {
35
+ console.error('Failed to get cluster usage:', error)
36
+ throw error
37
+ }
38
+ }
39
+
40
+ public async listClusters(): Promise<Cluster[]> {
41
+ try {
42
+ const { data, error } = await this.client.GET('/v1/clusters')
43
+ if (error) throw new Error(JSON.stringify(error))
44
+ return data?.data || []
45
+ } catch (error) {
46
+ console.error('Failed to list clusters:', error)
47
+ throw error
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,34 @@
1
+ import { createAuthenticatedClient } from '../client'
2
+
3
+ export interface Collaborator {
4
+ username: string
5
+ role: string
6
+ status: string
7
+ }
8
+
9
+ export class CollaboratorService {
10
+ private static instance: CollaboratorService
11
+ private client = createAuthenticatedClient()
12
+
13
+ private constructor() {}
14
+
15
+ public static getInstance(): CollaboratorService {
16
+ if (!CollaboratorService.instance) {
17
+ CollaboratorService.instance = new CollaboratorService()
18
+ }
19
+ return CollaboratorService.instance
20
+ }
21
+
22
+ // This endpoint is not available in the API
23
+ public async addCollaborator(
24
+ clusterId: string,
25
+ githubUsername: string
26
+ ): Promise<Collaborator[]> {
27
+ throw new Error('This functionality is not available in the API')
28
+ }
29
+
30
+ // This endpoint is not available in the API
31
+ public async listCollaborators(clusterId: string): Promise<Collaborator[]> {
32
+ throw new Error('This functionality is not available in the API')
33
+ }
34
+ }
@@ -0,0 +1,37 @@
1
+ import { createAuthenticatedClient } from '../client'
2
+
3
+ export interface FluxInstallOptions {
4
+ cluster: string
5
+ }
6
+
7
+ export interface FluxBootstrapOptions {
8
+ provider: string
9
+ owner?: string
10
+ repository?: string
11
+ path?: string
12
+ personal?: boolean
13
+ }
14
+
15
+ export class FluxService {
16
+ private static instance: FluxService
17
+ private client = createAuthenticatedClient()
18
+
19
+ private constructor() {}
20
+
21
+ public static getInstance(): FluxService {
22
+ if (!FluxService.instance) {
23
+ FluxService.instance = new FluxService()
24
+ }
25
+ return FluxService.instance
26
+ }
27
+
28
+ // This endpoint is not available in the API
29
+ public async installFlux(options: FluxInstallOptions): Promise<boolean> {
30
+ throw new Error('This functionality is not available in the API')
31
+ }
32
+
33
+ // This endpoint is not available in the API
34
+ public async bootstrapFlux(options: FluxBootstrapOptions): Promise<boolean> {
35
+ throw new Error('This functionality is not available in the API')
36
+ }
37
+ }
@@ -0,0 +1,37 @@
1
+ import { createAuthenticatedClient } from '../client'
2
+
3
+ export interface HelmRepoAddOptions {
4
+ name: string
5
+ url: string
6
+ }
7
+
8
+ export interface HelmInstallOptions {
9
+ name: string
10
+ chart: string
11
+ namespace?: string
12
+ values?: Record<string, string>
13
+ }
14
+
15
+ export class HelmService {
16
+ private static instance: HelmService
17
+ private client = createAuthenticatedClient()
18
+
19
+ private constructor() {}
20
+
21
+ public static getInstance(): HelmService {
22
+ if (!HelmService.instance) {
23
+ HelmService.instance = new HelmService()
24
+ }
25
+ return HelmService.instance
26
+ }
27
+
28
+ // This endpoint is not available in the API
29
+ public async addRepo(options: HelmRepoAddOptions): Promise<boolean> {
30
+ throw new Error('This functionality is not available in the API')
31
+ }
32
+
33
+ // This endpoint is not available in the API
34
+ public async installChart(options: HelmInstallOptions): Promise<any> {
35
+ throw new Error('This functionality is not available in the API')
36
+ }
37
+ }
@@ -0,0 +1,33 @@
1
+ import { createAuthenticatedClient } from '../client'
2
+
3
+ export class KubectlService {
4
+ private static instance: KubectlService
5
+ private client = createAuthenticatedClient()
6
+
7
+ private constructor() {}
8
+
9
+ public static getInstance(): KubectlService {
10
+ if (!KubectlService.instance) {
11
+ KubectlService.instance = new KubectlService()
12
+ }
13
+ return KubectlService.instance
14
+ }
15
+
16
+ // This endpoint is not available in the API
17
+ public async createNamespace(name: string): Promise<boolean> {
18
+ throw new Error('This functionality is not available in the API')
19
+ }
20
+
21
+ // This endpoint is not available in the API
22
+ public async applyConfiguration(filename: string): Promise<boolean> {
23
+ throw new Error('This functionality is not available in the API')
24
+ }
25
+
26
+ // This endpoint is not available in the API
27
+ public async getResources(
28
+ resource: string,
29
+ namespace?: string
30
+ ): Promise<any[]> {
31
+ throw new Error('This functionality is not available in the API')
32
+ }
33
+ }