sealos-cli 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.
@@ -0,0 +1,64 @@
1
+ import createClient from 'openapi-fetch'
2
+ import type { paths as TemplatePaths } from '../generated/template.ts'
3
+ import type { paths as DatabasePaths } from '../generated/database.ts'
4
+ import { DEFAULT_SEALOS_REGION, loadAuth } from './auth.ts'
5
+ import { ConfigError } from './errors.ts'
6
+
7
+ function resolveHost (options?: { baseUrl?: string }): string {
8
+ let authRegion: string | undefined
9
+ try {
10
+ authRegion = loadAuth().region
11
+ } catch {
12
+ authRegion = undefined
13
+ }
14
+
15
+ const host = options?.baseUrl || process.env.SEALOS_REGION || authRegion || DEFAULT_SEALOS_REGION
16
+ if (!host) {
17
+ throw new ConfigError('No Sealos Cloud host configured. Run "sealos login <host>" first.')
18
+ }
19
+ return host.replace(/\/+$/, '')
20
+ }
21
+
22
+ export function resolveDbproviderHost (host: string): string {
23
+ return resolvePrefixedHost(host, 'dbprovider')
24
+ }
25
+
26
+ export function resolveTemplateProviderHost (host: string): string {
27
+ return resolvePrefixedHost(host, 'template')
28
+ }
29
+
30
+ function resolvePrefixedHost (host: string, prefix: string): string {
31
+ const url = new URL(host)
32
+
33
+ if (
34
+ url.hostname === 'localhost' ||
35
+ url.hostname === '127.0.0.1' ||
36
+ url.hostname.startsWith(`${prefix}.`)
37
+ ) {
38
+ return url.toString().replace(/\/+$/, '')
39
+ }
40
+
41
+ url.hostname = `${prefix}.${url.hostname}`
42
+ return url.toString().replace(/\/+$/, '')
43
+ }
44
+
45
+ function resolveDatabaseHost (options?: { baseUrl?: string }): string {
46
+ const override = process.env.SEALOS_DATABASE_HOST?.trim()
47
+ if (override) {
48
+ return override.replace(/\/+$/, '')
49
+ }
50
+
51
+ return resolveDbproviderHost(resolveHost(options))
52
+ }
53
+
54
+ function resolveTemplateHost (options?: { baseUrl?: string }): string {
55
+ return resolveTemplateProviderHost(resolveHost(options))
56
+ }
57
+
58
+ export function createTemplateClient (options?: { baseUrl?: string }) {
59
+ return createClient<TemplatePaths>({ baseUrl: `${resolveTemplateHost(options)}/api/v2alpha` })
60
+ }
61
+
62
+ export function createDatabaseClient (options?: { baseUrl?: string }) {
63
+ return createClient<DatabasePaths>({ baseUrl: `${resolveDatabaseHost(options)}/api/v2alpha` })
64
+ }
package/src/lib/api.ts ADDED
@@ -0,0 +1,83 @@
1
+ import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
2
+ import { getCurrentContext } from './config.ts'
3
+
4
+ /**
5
+ * API client base class
6
+ */
7
+ export class ApiClient {
8
+ private client: AxiosInstance
9
+
10
+ constructor (baseURL?: string) {
11
+ const context = getCurrentContext()
12
+
13
+ this.client = axios.create({
14
+ baseURL: baseURL || context?.host || '',
15
+ timeout: 30000,
16
+ headers: {
17
+ 'Content-Type': 'application/json'
18
+ }
19
+ })
20
+
21
+ // Request interceptor - add auth token
22
+ this.client.interceptors.request.use(
23
+ (config) => {
24
+ const context = getCurrentContext()
25
+ if (context?.token) {
26
+ config.headers.Authorization = `Bearer ${context.token}`
27
+ }
28
+
29
+ // Support KUBECONFIG environment variable
30
+ const kubeconfig = process.env.KUBECONFIG
31
+ if (kubeconfig) {
32
+ config.headers['X-Kubeconfig'] = kubeconfig
33
+ }
34
+
35
+ return config
36
+ },
37
+ async (error) => await Promise.reject(error)
38
+ )
39
+
40
+ // Response interceptor - unified error handling
41
+ this.client.interceptors.response.use(
42
+ (response) => response,
43
+ async (error) => {
44
+ if (error.response?.status === 401) {
45
+ throw new Error('Authentication failed. Please run "sealos login" first.')
46
+ }
47
+ return await Promise.reject(error)
48
+ }
49
+ )
50
+ }
51
+
52
+ async get<T = any> (url: string, config?: AxiosRequestConfig): Promise<T> {
53
+ const response = await this.client.get<T>(url, config)
54
+ return response.data
55
+ }
56
+
57
+ async post<T = any> (url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
58
+ const response = await this.client.post<T>(url, data, config)
59
+ return response.data
60
+ }
61
+
62
+ async put<T = any> (url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
63
+ const response = await this.client.put<T>(url, data, config)
64
+ return response.data
65
+ }
66
+
67
+ async delete<T = any> (url: string, config?: AxiosRequestConfig): Promise<T> {
68
+ const response = await this.client.delete<T>(url, config)
69
+ return response.data
70
+ }
71
+
72
+ async patch<T = any> (url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
73
+ const response = await this.client.patch<T>(url, data, config)
74
+ return response.data
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Create API client instance
80
+ */
81
+ export function createApiClient (baseURL?: string): ApiClient {
82
+ return new ApiClient(baseURL)
83
+ }
@@ -0,0 +1,570 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { homedir, platform } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import type { SealosAuthData, SealosWorkspace } from '../types/index.ts'
6
+
7
+ export const SEALOS_AUTH_CLIENT_ID = 'af993c98-d19d-4bdc-b338-79b80dc4f8bf'
8
+ export const DEFAULT_SEALOS_REGION = 'https://usw-1.sealos.io'
9
+ export const AUTH_METHOD_DEVICE_GRANT = 'oauth2_device_grant'
10
+ export const AUTH_METHOD_TOKEN = 'token'
11
+
12
+ const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code'
13
+
14
+ export interface SealosAuthPaths {
15
+ sealosDir: string
16
+ authPath: string
17
+ kubeconfigPath: string
18
+ }
19
+
20
+ export interface AuthDependencies {
21
+ fetch?: typeof fetch
22
+ sleep?: (ms: number) => Promise<void>
23
+ openBrowser?: (url: string) => void
24
+ now?: () => Date
25
+ paths?: SealosAuthPaths
26
+ stderr?: Pick<NodeJS.WriteStream, 'write'>
27
+ }
28
+
29
+ interface DeviceAuthorizationResponse {
30
+ device_code: string
31
+ user_code: string
32
+ verification_uri?: string
33
+ verification_uri_complete?: string
34
+ expires_in: number
35
+ interval?: number
36
+ }
37
+
38
+ interface TokenResponse {
39
+ access_token?: string
40
+ }
41
+
42
+ interface RegionTokenResponse {
43
+ data?: {
44
+ token?: string
45
+ kubeconfig?: string
46
+ }
47
+ }
48
+
49
+ interface NamespaceListResponse {
50
+ data?: {
51
+ namespaces?: SealosWorkspace[]
52
+ } | SealosWorkspace[]
53
+ }
54
+
55
+ interface SwitchWorkspaceResponse {
56
+ data?: {
57
+ token?: string
58
+ }
59
+ }
60
+
61
+ interface KubeconfigResponse {
62
+ data?: {
63
+ kubeconfig?: string
64
+ }
65
+ }
66
+
67
+ export interface LoginResult {
68
+ kubeconfig_path?: string
69
+ region: string
70
+ workspace: string
71
+ limited?: boolean
72
+ }
73
+
74
+ export interface AuthStatus {
75
+ authenticated: boolean
76
+ kubeconfig_path?: string
77
+ region?: string
78
+ workspace?: string
79
+ }
80
+
81
+ export interface AuthInfo extends AuthStatus {
82
+ auth_method?: string
83
+ authenticated_at?: string
84
+ current_workspace?: SealosAuthData['current_workspace'] | null
85
+ }
86
+
87
+ export interface WorkspaceListResult {
88
+ current: string | null
89
+ workspaces: SealosWorkspace[]
90
+ }
91
+
92
+ export interface SwitchWorkspaceResult {
93
+ workspace: {
94
+ uid?: string
95
+ id?: string
96
+ teamName?: string
97
+ }
98
+ kubeconfig_path: string
99
+ }
100
+
101
+ export function getAuthPaths (sealosDir = join(homedir(), '.sealos')): SealosAuthPaths {
102
+ return {
103
+ sealosDir,
104
+ authPath: join(sealosDir, 'auth.json'),
105
+ kubeconfigPath: join(sealosDir, 'kubeconfig')
106
+ }
107
+ }
108
+
109
+ export function normalizeRegion (region?: string): string {
110
+ return (region || process.env.SEALOS_REGION || DEFAULT_SEALOS_REGION).replace(/\/+$/, '')
111
+ }
112
+
113
+ export function createDefaultAuthDependencies (): Required<AuthDependencies> {
114
+ return {
115
+ fetch,
116
+ sleep: async (ms: number) => await new Promise(resolve => setTimeout(resolve, ms)),
117
+ openBrowser,
118
+ now: () => new Date(),
119
+ paths: getAuthPaths(),
120
+ stderr: process.stderr
121
+ }
122
+ }
123
+
124
+ function withDeps (deps: AuthDependencies = {}): Required<AuthDependencies> {
125
+ return {
126
+ ...createDefaultAuthDependencies(),
127
+ ...deps
128
+ }
129
+ }
130
+
131
+ function ensureSealosDir (paths: SealosAuthPaths): void {
132
+ mkdirSync(paths.sealosDir, { recursive: true })
133
+ }
134
+
135
+ export function saveAuth (auth: SealosAuthData, deps: AuthDependencies = {}): void {
136
+ const { paths } = withDeps(deps)
137
+ ensureSealosDir(paths)
138
+ writeFileSync(paths.authPath, JSON.stringify(auth, null, 2), { mode: 0o600 })
139
+ }
140
+
141
+ export function loadAuth (deps: AuthDependencies = {}): SealosAuthData {
142
+ const { paths } = withDeps(deps)
143
+ if (!existsSync(paths.authPath)) {
144
+ throw new Error('Not authenticated. Please run: sealos login')
145
+ }
146
+
147
+ return JSON.parse(readFileSync(paths.authPath, 'utf-8')) as SealosAuthData
148
+ }
149
+
150
+ export function getToken (deps: AuthDependencies = {}): string | null {
151
+ try {
152
+ return loadAuth(deps).regional_token || null
153
+ } catch {
154
+ return null
155
+ }
156
+ }
157
+
158
+ export function getAuthHeaders (deps: AuthDependencies = {}): { Authorization: string } | null {
159
+ const token = getToken(deps)
160
+ return token ? { Authorization: token } : null
161
+ }
162
+
163
+ export function requireAuth (deps: AuthDependencies = {}): { Authorization: string } {
164
+ const headers = getAuthHeaders(deps)
165
+ if (!headers) {
166
+ throw new Error('Authentication required. Please run "sealos login" first.')
167
+ }
168
+ return headers
169
+ }
170
+
171
+ export function saveKubeconfig (kubeconfig: string, deps: AuthDependencies = {}): void {
172
+ const { paths } = withDeps(deps)
173
+ ensureSealosDir(paths)
174
+ writeFileSync(paths.kubeconfigPath, kubeconfig, { mode: 0o600 })
175
+ }
176
+
177
+ export function clearAuth (deps: AuthDependencies = {}): void {
178
+ const { paths } = withDeps(deps)
179
+ rmSync(paths.authPath, { force: true })
180
+ rmSync(paths.kubeconfigPath, { force: true })
181
+ }
182
+
183
+ export function checkAuth (deps: AuthDependencies = {}): AuthStatus {
184
+ const { paths } = withDeps(deps)
185
+ if (!existsSync(paths.kubeconfigPath)) {
186
+ return { authenticated: false }
187
+ }
188
+
189
+ try {
190
+ const kubeconfig = readFileSync(paths.kubeconfigPath, 'utf-8')
191
+ if (!kubeconfig.includes('server:') || (!kubeconfig.includes('token:') && !kubeconfig.includes('client-certificate'))) {
192
+ return { authenticated: false }
193
+ }
194
+
195
+ const auth: Partial<SealosAuthData> = existsSync(paths.authPath)
196
+ ? JSON.parse(readFileSync(paths.authPath, 'utf-8')) as SealosAuthData
197
+ : {}
198
+
199
+ return {
200
+ authenticated: true,
201
+ kubeconfig_path: paths.kubeconfigPath,
202
+ region: auth.region || 'unknown',
203
+ workspace: auth.current_workspace?.id || 'unknown'
204
+ }
205
+ } catch {
206
+ return { authenticated: false }
207
+ }
208
+ }
209
+
210
+ export function getAuthInfo (deps: AuthDependencies = {}): AuthInfo {
211
+ const { paths } = withDeps(deps)
212
+ const status = checkAuth(deps)
213
+ if (!status.authenticated) {
214
+ return { authenticated: false }
215
+ }
216
+
217
+ const auth: Partial<SealosAuthData> = existsSync(paths.authPath)
218
+ ? JSON.parse(readFileSync(paths.authPath, 'utf-8')) as SealosAuthData
219
+ : {}
220
+
221
+ return {
222
+ ...status,
223
+ auth_method: auth.auth_method || 'unknown',
224
+ authenticated_at: auth.authenticated_at || 'unknown',
225
+ current_workspace: auth.current_workspace || null
226
+ }
227
+ }
228
+
229
+ async function parseResponse<T> (res: Response): Promise<T> {
230
+ return await res.json() as T
231
+ }
232
+
233
+ async function readErrorBody (res: Response): Promise<string> {
234
+ return await res.text().catch(() => '')
235
+ }
236
+
237
+ export async function requestDeviceAuthorization (region: string, deps: AuthDependencies = {}): Promise<DeviceAuthorizationResponse> {
238
+ const { fetch: fetchImpl } = withDeps(deps)
239
+ const res = await fetchImpl(`${region}/api/auth/oauth2/device`, {
240
+ method: 'POST',
241
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
242
+ body: new URLSearchParams({
243
+ client_id: SEALOS_AUTH_CLIENT_ID,
244
+ grant_type: DEVICE_GRANT_TYPE
245
+ })
246
+ })
247
+
248
+ if (!res.ok) {
249
+ const body = await readErrorBody(res)
250
+ throw new Error(`Device authorization request failed (${res.status}): ${body || res.statusText}`)
251
+ }
252
+
253
+ return await parseResponse<DeviceAuthorizationResponse>(res)
254
+ }
255
+
256
+ export async function pollForToken (
257
+ region: string,
258
+ deviceCode: string,
259
+ interval: number,
260
+ expiresIn: number,
261
+ deps: AuthDependencies = {}
262
+ ): Promise<TokenResponse> {
263
+ const { fetch: fetchImpl, sleep, now, stderr } = withDeps(deps)
264
+ const maxWait = Math.min(expiresIn, 600) * 1000
265
+ const deadline = now().getTime() + maxWait
266
+ let pollInterval = interval * 1000
267
+ let lastLoggedMinute = -1
268
+
269
+ while (now().getTime() < deadline) {
270
+ await sleep(pollInterval)
271
+
272
+ const remaining = Math.ceil((deadline - now().getTime()) / 60000)
273
+ if (remaining !== lastLoggedMinute && remaining > 0) {
274
+ lastLoggedMinute = remaining
275
+ stderr.write(` Waiting for authorization... (${remaining} min remaining)\n`)
276
+ }
277
+
278
+ const res = await fetchImpl(`${region}/api/auth/oauth2/token`, {
279
+ method: 'POST',
280
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
281
+ body: new URLSearchParams({
282
+ client_id: SEALOS_AUTH_CLIENT_ID,
283
+ grant_type: DEVICE_GRANT_TYPE,
284
+ device_code: deviceCode
285
+ })
286
+ })
287
+
288
+ if (res.ok) {
289
+ return await parseResponse<TokenResponse>(res)
290
+ }
291
+
292
+ const body = await res.json().catch(() => ({})) as { error?: string }
293
+ switch (body.error) {
294
+ case 'authorization_pending':
295
+ break
296
+ case 'slow_down':
297
+ pollInterval += 5000
298
+ break
299
+ case 'access_denied':
300
+ throw new Error('Authorization denied by user')
301
+ case 'expired_token':
302
+ throw new Error('Device code expired. Please run login again.')
303
+ default:
304
+ throw new Error(`Token request failed: ${body.error || res.statusText}`)
305
+ }
306
+ }
307
+
308
+ throw new Error('Authorization timed out (10 minutes). Please run login again.')
309
+ }
310
+
311
+ export async function getRegionToken (region: string, globalToken: string, deps: AuthDependencies = {}): Promise<RegionTokenResponse> {
312
+ const { fetch: fetchImpl } = withDeps(deps)
313
+ const res = await fetchImpl(`${region}/api/auth/regionToken`, {
314
+ method: 'POST',
315
+ headers: {
316
+ Authorization: globalToken,
317
+ 'Content-Type': 'application/json'
318
+ }
319
+ })
320
+
321
+ if (!res.ok) {
322
+ const body = await readErrorBody(res)
323
+ throw new Error(`Region token exchange failed (${res.status}): ${body || res.statusText}`)
324
+ }
325
+
326
+ return await parseResponse<RegionTokenResponse>(res)
327
+ }
328
+
329
+ function normalizeWorkspaces (data: NamespaceListResponse): SealosWorkspace[] {
330
+ const namespaces = Array.isArray(data.data) ? data.data : data.data?.namespaces
331
+ return Array.isArray(namespaces) ? namespaces : []
332
+ }
333
+
334
+ export async function listRemoteWorkspaces (region: string, regionalToken: string, deps: AuthDependencies = {}): Promise<SealosWorkspace[]> {
335
+ const { fetch: fetchImpl } = withDeps(deps)
336
+ const res = await fetchImpl(`${region}/api/auth/namespace/list`, {
337
+ headers: { Authorization: regionalToken }
338
+ })
339
+
340
+ if (!res.ok) {
341
+ const body = await readErrorBody(res)
342
+ throw new Error(`List workspaces failed (${res.status}): ${body || res.statusText}`)
343
+ }
344
+
345
+ return normalizeWorkspaces(await parseResponse<NamespaceListResponse>(res))
346
+ }
347
+
348
+ export async function switchRemoteWorkspace (
349
+ region: string,
350
+ regionalToken: string,
351
+ nsUid: string,
352
+ deps: AuthDependencies = {}
353
+ ): Promise<SwitchWorkspaceResponse> {
354
+ const { fetch: fetchImpl } = withDeps(deps)
355
+ const res = await fetchImpl(`${region}/api/auth/namespace/switch`, {
356
+ method: 'POST',
357
+ headers: {
358
+ Authorization: regionalToken,
359
+ 'Content-Type': 'application/json'
360
+ },
361
+ body: JSON.stringify({ ns_uid: nsUid })
362
+ })
363
+
364
+ if (!res.ok) {
365
+ const body = await readErrorBody(res)
366
+ throw new Error(`Switch workspace failed (${res.status}): ${body || res.statusText}`)
367
+ }
368
+
369
+ return await parseResponse<SwitchWorkspaceResponse>(res)
370
+ }
371
+
372
+ export async function getKubeconfig (region: string, regionalToken: string, deps: AuthDependencies = {}): Promise<KubeconfigResponse> {
373
+ const { fetch: fetchImpl } = withDeps(deps)
374
+ const res = await fetchImpl(`${region}/api/auth/getKubeconfig`, {
375
+ headers: { Authorization: regionalToken }
376
+ })
377
+
378
+ if (!res.ok) {
379
+ const body = await readErrorBody(res)
380
+ throw new Error(`Get kubeconfig failed (${res.status}): ${body || res.statusText}`)
381
+ }
382
+
383
+ return await parseResponse<KubeconfigResponse>(res)
384
+ }
385
+
386
+ export function openBrowser (url: string): void {
387
+ const command = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'cmd' : 'xdg-open'
388
+ const args = platform() === 'win32' ? ['/c', 'start', '', url] : [url]
389
+ execFileSync(command, args, { stdio: 'ignore' })
390
+ }
391
+
392
+ export async function loginWithToken (region: string, token: string, deps: AuthDependencies = {}): Promise<LoginResult> {
393
+ const { now } = withDeps(deps)
394
+ const normalizedRegion = normalizeRegion(region)
395
+ saveAuth({
396
+ region: normalizedRegion,
397
+ regional_token: token,
398
+ authenticated_at: now().toISOString(),
399
+ auth_method: AUTH_METHOD_TOKEN
400
+ }, deps)
401
+
402
+ return {
403
+ region: normalizedRegion,
404
+ workspace: 'unknown',
405
+ limited: true
406
+ }
407
+ }
408
+
409
+ export async function loginWithDeviceFlow (region?: string, deps: AuthDependencies = {}): Promise<LoginResult> {
410
+ const fullDeps = withDeps(deps)
411
+ const normalizedRegion = normalizeRegion(region)
412
+ const deviceAuth = await requestDeviceAuthorization(normalizedRegion, fullDeps)
413
+ const {
414
+ device_code: deviceCode,
415
+ user_code: userCode,
416
+ verification_uri: verificationUri,
417
+ verification_uri_complete: verificationUriComplete,
418
+ expires_in: expiresIn,
419
+ interval = 5
420
+ } = deviceAuth
421
+
422
+ const url = verificationUriComplete || verificationUri
423
+ fullDeps.stderr.write(`\nPlease open the following URL in your browser to authorize:\n\n ${url}\n\nAuthorization code: ${userCode}\nExpires in: ${Math.floor(expiresIn / 60)} minutes\n\nWaiting for authorization...\n`)
424
+
425
+ if (url) {
426
+ try {
427
+ fullDeps.openBrowser(url)
428
+ fullDeps.stderr.write('Browser opened automatically.\n')
429
+ } catch {
430
+ fullDeps.stderr.write('Could not open browser automatically. Please open the URL manually.\n')
431
+ }
432
+ }
433
+
434
+ const tokenResponse = await pollForToken(normalizedRegion, deviceCode, interval, expiresIn, fullDeps)
435
+ const accessToken = tokenResponse.access_token
436
+ if (!accessToken) {
437
+ throw new Error('Token response missing access_token')
438
+ }
439
+
440
+ fullDeps.stderr.write('Authorization received. Exchanging for regional token...\n')
441
+
442
+ const regionData = await getRegionToken(normalizedRegion, accessToken, fullDeps)
443
+ const regionalToken = regionData.data?.token
444
+ const kubeconfig = regionData.data?.kubeconfig
445
+ if (!regionalToken) {
446
+ throw new Error('Region token response missing data.token field')
447
+ }
448
+ if (!kubeconfig) {
449
+ throw new Error('Region token response missing data.kubeconfig field')
450
+ }
451
+
452
+ let currentWorkspace: SealosWorkspace | undefined
453
+ try {
454
+ const workspaces = await listRemoteWorkspaces(normalizedRegion, regionalToken, fullDeps)
455
+ currentWorkspace = workspaces.find(workspace => workspace.nstype === 'private') || workspaces[0]
456
+ } catch {
457
+ currentWorkspace = undefined
458
+ }
459
+
460
+ saveKubeconfig(kubeconfig, fullDeps)
461
+ saveAuth({
462
+ region: normalizedRegion,
463
+ access_token: accessToken,
464
+ regional_token: regionalToken,
465
+ authenticated_at: fullDeps.now().toISOString(),
466
+ auth_method: AUTH_METHOD_DEVICE_GRANT,
467
+ ...(currentWorkspace
468
+ ? {
469
+ current_workspace: {
470
+ uid: currentWorkspace.uid,
471
+ id: currentWorkspace.id,
472
+ teamName: currentWorkspace.teamName
473
+ }
474
+ }
475
+ : {})
476
+ }, fullDeps)
477
+
478
+ fullDeps.stderr.write('Authentication successful!\n')
479
+
480
+ return {
481
+ kubeconfig_path: fullDeps.paths.kubeconfigPath,
482
+ region: normalizedRegion,
483
+ workspace: currentWorkspace?.id || 'default'
484
+ }
485
+ }
486
+
487
+ export async function listWorkspaces (deps: AuthDependencies = {}): Promise<WorkspaceListResult> {
488
+ const auth = loadAuth(deps)
489
+ if (!auth.regional_token) {
490
+ throw new Error('No regional_token found. Please run: sealos login')
491
+ }
492
+
493
+ const workspaces = await listRemoteWorkspaces(auth.region, auth.regional_token, deps)
494
+ return {
495
+ current: auth.current_workspace?.id || null,
496
+ workspaces: workspaces.map(workspace => ({
497
+ uid: workspace.uid,
498
+ id: workspace.id,
499
+ teamName: workspace.teamName,
500
+ role: workspace.role,
501
+ nstype: workspace.nstype
502
+ }))
503
+ }
504
+ }
505
+
506
+ export async function switchWorkspace (target: string, deps: AuthDependencies = {}): Promise<SwitchWorkspaceResult> {
507
+ if (!target) {
508
+ throw new Error('Usage: sealos auth switch <namespace-id-or-uid>')
509
+ }
510
+
511
+ const fullDeps = withDeps(deps)
512
+ const auth = loadAuth(fullDeps)
513
+ if (!auth.regional_token) {
514
+ throw new Error('No regional_token found. Please run: sealos login')
515
+ }
516
+
517
+ const workspaces = await listRemoteWorkspaces(auth.region, auth.regional_token, fullDeps)
518
+ if (workspaces.length === 0) {
519
+ throw new Error('No workspaces found')
520
+ }
521
+
522
+ const targetLower = target.toLowerCase()
523
+ const match = workspaces.find(workspace =>
524
+ workspace.id === target ||
525
+ workspace.uid === target ||
526
+ workspace.id?.toLowerCase().includes(targetLower) ||
527
+ workspace.teamName?.toLowerCase().includes(targetLower)
528
+ )
529
+
530
+ if (!match?.uid) {
531
+ const available = workspaces.map(workspace => ` ${workspace.id || 'unknown'} (${workspace.teamName || 'unknown'})`).join('\n')
532
+ throw new Error(`No workspace matching "${target}". Available:\n${available}`)
533
+ }
534
+
535
+ fullDeps.stderr.write(`Switching to workspace: ${match.id || match.uid} (${match.teamName || 'unknown'})...\n`)
536
+ const switchData = await switchRemoteWorkspace(auth.region, auth.regional_token, match.uid, fullDeps)
537
+ const newToken = switchData.data?.token
538
+ if (!newToken) {
539
+ throw new Error('Switch response missing data.token')
540
+ }
541
+
542
+ const kubeconfigData = await getKubeconfig(auth.region, newToken, fullDeps)
543
+ const kubeconfig = kubeconfigData.data?.kubeconfig
544
+ if (!kubeconfig) {
545
+ throw new Error('Kubeconfig response missing data.kubeconfig')
546
+ }
547
+
548
+ const nextAuth: SealosAuthData = {
549
+ ...auth,
550
+ regional_token: newToken,
551
+ current_workspace: {
552
+ uid: match.uid,
553
+ id: match.id,
554
+ teamName: match.teamName
555
+ }
556
+ }
557
+ saveAuth(nextAuth, fullDeps)
558
+ saveKubeconfig(kubeconfig, fullDeps)
559
+
560
+ fullDeps.stderr.write(`Switched to workspace: ${match.id || match.uid}\n`)
561
+
562
+ return {
563
+ workspace: {
564
+ uid: match.uid,
565
+ id: match.id,
566
+ teamName: match.teamName
567
+ },
568
+ kubeconfig_path: fullDeps.paths.kubeconfigPath
569
+ }
570
+ }