berget 0.0.2 → 0.0.4

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,123 @@
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 = process.env.BERGET_API_URL ||
16
+ (isLocalMode ? 'http://localhost:3000' : 'https://api.berget.ai');
17
+
18
+ if (isLocalMode && !process.env.BERGET_API_URL) {
19
+ console.log(chalk.yellow('Using local API endpoint: http://localhost:3000'));
20
+ }
21
+
22
+ // Create a typed client for the Berget API
23
+ export const apiClient = createClient<paths>({
24
+ baseUrl: API_BASE_URL,
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'Accept': 'application/json'
28
+ }
29
+ });
30
+
31
+ // Authentication functions
32
+ export const getAuthToken = (): string | null => {
33
+ try {
34
+ if (fs.existsSync(TOKEN_FILE)) {
35
+ const tokenData = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'));
36
+ return tokenData.accessToken;
37
+ }
38
+ } catch (error) {
39
+ console.error('Error reading auth token:', error);
40
+ }
41
+ return null;
42
+ };
43
+
44
+ // Check if token is expired (JWT tokens have an exp claim)
45
+ export const isTokenExpired = (token: string): boolean => {
46
+ try {
47
+ const base64Url = token.split('.')[1];
48
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
49
+ const jsonPayload = decodeURIComponent(
50
+ atob(base64)
51
+ .split('')
52
+ .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
53
+ .join('')
54
+ );
55
+ const payload = JSON.parse(jsonPayload);
56
+
57
+ // Check if token has expired
58
+ if (payload.exp) {
59
+ return payload.exp * 1000 < Date.now();
60
+ }
61
+ } catch (error) {
62
+ // If we can't decode the token, assume it's expired
63
+ return true;
64
+ }
65
+
66
+ // If there's no exp claim, assume it's valid
67
+ return false;
68
+ };
69
+
70
+ export const saveAuthToken = (token: string): void => {
71
+ try {
72
+ if (!fs.existsSync(CONFIG_DIR)) {
73
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
74
+ }
75
+ fs.writeFileSync(TOKEN_FILE, JSON.stringify({ accessToken: token }), 'utf8');
76
+ // Set file permissions to be readable only by the owner
77
+ fs.chmodSync(TOKEN_FILE, 0o600);
78
+ } catch (error) {
79
+ console.error('Error saving auth token:', error);
80
+ }
81
+ };
82
+
83
+ export const clearAuthToken = (): void => {
84
+ try {
85
+ if (fs.existsSync(TOKEN_FILE)) {
86
+ fs.unlinkSync(TOKEN_FILE);
87
+ }
88
+ } catch (error) {
89
+ console.error('Error clearing auth token:', error);
90
+ }
91
+ };
92
+
93
+ // Create an authenticated client
94
+ export const createAuthenticatedClient = () => {
95
+ const token = getAuthToken();
96
+
97
+ if (!token) {
98
+ console.warn(chalk.yellow('No authentication token found. Please run `berget login` first.'));
99
+ } else if (isTokenExpired(token)) {
100
+ console.warn(chalk.yellow('Your authentication token has expired. Please run `berget login` to get a new token.'));
101
+ // Optionally clear the expired token
102
+ clearAuthToken();
103
+ return createClient<paths>({
104
+ baseUrl: API_BASE_URL,
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ 'Accept': 'application/json'
108
+ }
109
+ });
110
+ }
111
+
112
+ return createClient<paths>({
113
+ baseUrl: API_BASE_URL,
114
+ headers: token ? {
115
+ 'Authorization': `Bearer ${token}`,
116
+ 'Content-Type': 'application/json',
117
+ 'Accept': 'application/json'
118
+ } : {
119
+ 'Content-Type': 'application/json',
120
+ 'Accept': 'application/json'
121
+ },
122
+ });
123
+ };
@@ -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,159 @@
1
+ import { createAuthenticatedClient, saveAuthToken, clearAuthToken, apiClient } from '../client'
2
+ import open from 'open'
3
+ import chalk from 'chalk'
4
+ import { handleError } from '../utils/error-handler'
5
+
6
+ export class AuthService {
7
+ private static instance: AuthService
8
+ private client = createAuthenticatedClient()
9
+
10
+ private constructor() {}
11
+
12
+ public static getInstance(): AuthService {
13
+ if (!AuthService.instance) {
14
+ AuthService.instance = new AuthService()
15
+ }
16
+ return AuthService.instance
17
+ }
18
+
19
+ public async login(): Promise<boolean> {
20
+ try {
21
+ // Clear any existing token to ensure a fresh login
22
+ clearAuthToken()
23
+
24
+ console.log(chalk.blue('Initiating login process...'))
25
+
26
+ // Step 1: Initiate device authorization
27
+ const { data: deviceData, error: deviceError } = await apiClient.POST('/v1/auth/device', {})
28
+
29
+ if (deviceError || !deviceData) {
30
+ throw new Error(deviceError ? JSON.stringify(deviceError) : 'Failed to get device authorization data')
31
+ }
32
+
33
+ // Display information to user
34
+ console.log(chalk.cyan('\nTo complete login:'))
35
+ console.log(chalk.cyan(`1. Open this URL: ${chalk.bold(deviceData.verification_url || 'https://auth.berget.ai/device')}`))
36
+ console.log(chalk.cyan(`2. Enter this code: ${chalk.bold(deviceData.user_code || '')}\n`))
37
+
38
+ // Try to open browser automatically
39
+ try {
40
+ if (deviceData.verification_url) {
41
+ await open(deviceData.verification_url)
42
+ console.log(chalk.dim('Browser opened automatically. If it didn\'t open, please use the URL above.'))
43
+ }
44
+ } catch (error) {
45
+ console.log(chalk.yellow('Could not open browser automatically. Please open the URL manually.'))
46
+ }
47
+
48
+ console.log(chalk.dim('\nWaiting for authentication to complete...'))
49
+
50
+ // Step 2: Poll for completion
51
+ const startTime = Date.now()
52
+ const expiresIn = deviceData.expires_in !== undefined ? deviceData.expires_in : 900
53
+ const expiresAt = startTime + (expiresIn * 1000)
54
+ let pollInterval = (deviceData.interval || 5) * 1000
55
+
56
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
57
+ let spinnerIdx = 0
58
+
59
+ while (Date.now() < expiresAt) {
60
+ // Wait for the polling interval
61
+ await new Promise(resolve => setTimeout(resolve, pollInterval))
62
+
63
+ // Update spinner
64
+ process.stdout.write(`\r${chalk.blue(spinner[spinnerIdx])} Waiting for authentication...`)
65
+ spinnerIdx = (spinnerIdx + 1) % spinner.length
66
+
67
+ // Check if authentication is complete
68
+ const deviceCode = deviceData.device_code || ''
69
+ const { data: tokenData, error: tokenError } = await apiClient.POST('/v1/auth/device/token', {
70
+ body: {
71
+ device_code: deviceCode
72
+ }
73
+ })
74
+
75
+ if (tokenError) {
76
+ // Parse the error to get status and other details
77
+ const errorObj = typeof tokenError === 'string'
78
+ ? JSON.parse(tokenError)
79
+ : tokenError;
80
+
81
+ const status = errorObj.status || 0;
82
+ const errorCode = errorObj.code || '';
83
+
84
+ if (status === 401 || errorCode === 'AUTHORIZATION_PENDING') {
85
+ // Still waiting for user to complete authorization
86
+ continue
87
+ } else if (status === 429) {
88
+ // Slow down
89
+ pollInterval *= 2
90
+ continue
91
+ } else if (status === 400) {
92
+ // Error or expired
93
+ if (errorCode === 'EXPIRED_TOKEN') {
94
+ console.log(chalk.red('\n\nAuthentication timed out. Please try again.'))
95
+ } else if (errorCode !== 'AUTHORIZATION_PENDING') {
96
+ // Only show error if it's not the expected "still waiting" error
97
+ const errorMessage = errorObj.message || JSON.stringify(errorObj);
98
+ console.log(chalk.red(`\n\nError: ${errorMessage}`))
99
+ return false
100
+ } else {
101
+ // If it's AUTHORIZATION_PENDING, continue polling
102
+ continue
103
+ }
104
+ return false
105
+ } else {
106
+ // For any other error, log it but continue polling
107
+ // This makes the flow more resilient to temporary issues
108
+ if (process.env.DEBUG) {
109
+ console.log(chalk.yellow(`\n\nReceived error: ${JSON.stringify(errorObj)}`))
110
+ console.log(chalk.yellow('Continuing to wait for authentication...'))
111
+ process.stdout.write(`\r${chalk.blue(spinner[spinnerIdx])} Waiting for authentication...`)
112
+ }
113
+ continue
114
+ }
115
+ } else if (tokenData && tokenData.token) {
116
+ // Success!
117
+ saveAuthToken(tokenData.token)
118
+
119
+ process.stdout.write('\r' + ' '.repeat(50) + '\r') // Clear the spinner line
120
+ console.log(chalk.green('✓ Successfully logged in to Berget'))
121
+
122
+ if (tokenData.user) {
123
+ const user = tokenData.user as any
124
+ console.log(chalk.green(`Logged in as ${user.name || user.email || 'User'}`))
125
+ }
126
+
127
+ return true
128
+ }
129
+ }
130
+
131
+ console.log(chalk.red('\n\nAuthentication timed out. Please try again.'))
132
+ return false
133
+ } catch (error) {
134
+ handleError('Login failed', error)
135
+ return false
136
+ }
137
+ }
138
+
139
+ public async isAuthenticated(): Promise<boolean> {
140
+ try {
141
+ // Call an API endpoint that requires authentication
142
+ const { data, error } = await this.client.GET('/v1/users/me')
143
+ return !!data && !error
144
+ } catch {
145
+ return false
146
+ }
147
+ }
148
+
149
+ public async getUserProfile() {
150
+ try {
151
+ const { data, error } = await this.client.GET('/v1/users/me')
152
+ if (error) throw new Error(JSON.stringify(error))
153
+ return data
154
+ } catch (error) {
155
+ handleError('Failed to get user profile', error)
156
+ throw error
157
+ }
158
+ }
159
+ }
@@ -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
+ }