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.
- package/LICENSE +191 -0
- package/README.md +234 -0
- package/dist/bin/cli.cjs +2066 -0
- package/dist/bin/cli.d.cts +1 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.mjs +2044 -0
- package/dist/main.cjs +2079 -0
- package/dist/main.d.cts +7 -0
- package/dist/main.d.ts +7 -0
- package/dist/main.mjs +2045 -0
- package/package.json +112 -0
- package/src/bin/cli.ts +4 -0
- package/src/commands/app/index.ts +22 -0
- package/src/commands/auth/index.ts +124 -0
- package/src/commands/auth/login.ts +35 -0
- package/src/commands/auth/logout.ts +23 -0
- package/src/commands/auth/whoami.ts +38 -0
- package/src/commands/config/index.ts +54 -0
- package/src/commands/database/index.ts +881 -0
- package/src/commands/devbox/index.ts +224 -0
- package/src/commands/quota/index.ts +22 -0
- package/src/commands/s3/index.ts +35 -0
- package/src/commands/template/index.ts +314 -0
- package/src/commands/workspace/index.ts +84 -0
- package/src/docs/database_openapi.json +8297 -0
- package/src/docs/template_openapi.json +1 -0
- package/src/generated/database.ts +3969 -0
- package/src/generated/template.ts +1007 -0
- package/src/lib/api-client.ts +64 -0
- package/src/lib/api.ts +83 -0
- package/src/lib/auth.ts +570 -0
- package/src/lib/config.ts +134 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/errors.ts +105 -0
- package/src/lib/oauth.ts +197 -0
- package/src/lib/output.ts +93 -0
- package/src/lib/with-auth.ts +56 -0
- package/src/main.ts +51 -0
- package/src/types/index.ts +56 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
4
|
+
import type { SealosConfig, Context } from '../types/index.ts'
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = join(homedir(), '.sealos')
|
|
7
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure config directory exists
|
|
11
|
+
*/
|
|
12
|
+
export function ensureConfigDir (): void {
|
|
13
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
14
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read config file
|
|
20
|
+
*/
|
|
21
|
+
export function readConfig (): SealosConfig {
|
|
22
|
+
ensureConfigDir()
|
|
23
|
+
|
|
24
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
25
|
+
const defaultConfig: SealosConfig = {
|
|
26
|
+
currentContext: '',
|
|
27
|
+
contexts: []
|
|
28
|
+
}
|
|
29
|
+
return defaultConfig
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8')
|
|
34
|
+
return JSON.parse(content) as SealosConfig
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(`Failed to read config file: ${error}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Write config file
|
|
42
|
+
*/
|
|
43
|
+
export function writeConfig (config: SealosConfig): void {
|
|
44
|
+
ensureConfigDir()
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8')
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Failed to write config file: ${error}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get current context
|
|
55
|
+
*/
|
|
56
|
+
export function getCurrentContext (): Context | null {
|
|
57
|
+
const config = readConfig()
|
|
58
|
+
if (!config.currentContext) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const context = config.contexts.find(ctx => ctx.name === config.currentContext)
|
|
63
|
+
return context || null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set current context
|
|
68
|
+
*/
|
|
69
|
+
export function setCurrentContext (name: string): void {
|
|
70
|
+
const config = readConfig()
|
|
71
|
+
const context = config.contexts.find(ctx => ctx.name === name)
|
|
72
|
+
|
|
73
|
+
if (!context) {
|
|
74
|
+
throw new Error(`Context "${name}" not found`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
config.currentContext = name
|
|
78
|
+
writeConfig(config)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Add or update context
|
|
83
|
+
*/
|
|
84
|
+
export function upsertContext (context: Context): void {
|
|
85
|
+
const config = readConfig()
|
|
86
|
+
const existingIndex = config.contexts.findIndex(ctx => ctx.name === context.name)
|
|
87
|
+
|
|
88
|
+
if (existingIndex >= 0) {
|
|
89
|
+
config.contexts[existingIndex] = context
|
|
90
|
+
} else {
|
|
91
|
+
config.contexts.push(context)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If this is the first context, set it as current automatically
|
|
95
|
+
if (!config.currentContext) {
|
|
96
|
+
config.currentContext = context.name
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
writeConfig(config)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove context
|
|
104
|
+
*/
|
|
105
|
+
export function removeContext (name: string): void {
|
|
106
|
+
const config = readConfig()
|
|
107
|
+
config.contexts = config.contexts.filter(ctx => ctx.name !== name)
|
|
108
|
+
|
|
109
|
+
// If removing current context, clear currentContext
|
|
110
|
+
if (config.currentContext === name) {
|
|
111
|
+
config.currentContext = config.contexts[0]?.name || ''
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
writeConfig(config)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get config value
|
|
119
|
+
*/
|
|
120
|
+
export function getConfigValue (key: string): string | undefined {
|
|
121
|
+
const config = readConfig()
|
|
122
|
+
// TODO: Implement nested key access, e.g. "contexts.0.name"
|
|
123
|
+
return (config as unknown as Record<string, unknown>)[key] as string | undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Set config value
|
|
128
|
+
*/
|
|
129
|
+
export function setConfigValue (key: string, value: string): void {
|
|
130
|
+
const config = readConfig()
|
|
131
|
+
// TODO: Implement nested key setting
|
|
132
|
+
;(config as unknown as Record<string, unknown>)[key] = value
|
|
133
|
+
writeConfig(config)
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLIENT_ID = 'af993c98-d19d-4bdc-b338-79b80dc4f8bf'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI error base class
|
|
5
|
+
*/
|
|
6
|
+
export class CliError extends Error {
|
|
7
|
+
constructor (message: string, public exitCode: number = 1) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.name = 'CliError'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Authentication error
|
|
15
|
+
*/
|
|
16
|
+
export class AuthError extends CliError {
|
|
17
|
+
constructor (message: string = 'Authentication required. Please run "sealos login" first.') {
|
|
18
|
+
super(message, 1)
|
|
19
|
+
this.name = 'AuthError'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration error
|
|
25
|
+
*/
|
|
26
|
+
export class ConfigError extends CliError {
|
|
27
|
+
constructor (message: string) {
|
|
28
|
+
super(message, 1)
|
|
29
|
+
this.name = 'ConfigError'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Standard API error response body
|
|
35
|
+
*/
|
|
36
|
+
export interface ApiErrorBody {
|
|
37
|
+
error?: {
|
|
38
|
+
type?: string
|
|
39
|
+
code?: string
|
|
40
|
+
message?: string
|
|
41
|
+
details?: Array<{ field: string; message: string }> | string
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* API error
|
|
47
|
+
*/
|
|
48
|
+
export class ApiError extends CliError {
|
|
49
|
+
constructor (
|
|
50
|
+
message: string,
|
|
51
|
+
public statusCode?: number,
|
|
52
|
+
public code?: string,
|
|
53
|
+
public details?: Array<{ field: string; message: string }> | string
|
|
54
|
+
) {
|
|
55
|
+
super(message, 1)
|
|
56
|
+
this.name = 'ApiError'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Map an API error response to the appropriate CliError.
|
|
62
|
+
* Supports the unified error format: { error: { type, code, message, details? } }
|
|
63
|
+
*/
|
|
64
|
+
export function mapApiError (status: number, body?: ApiErrorBody): CliError {
|
|
65
|
+
const message = body?.error?.message || `API request failed with status ${status}`
|
|
66
|
+
if (status === 401) {
|
|
67
|
+
return new AuthError(message)
|
|
68
|
+
}
|
|
69
|
+
return new ApiError(message, status, body?.error?.code, body?.error?.details)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Unified error handling
|
|
74
|
+
*/
|
|
75
|
+
export function handleError (error: unknown): never {
|
|
76
|
+
if (error instanceof ApiError) {
|
|
77
|
+
console.error(chalk.red('Error:'), error.message)
|
|
78
|
+
if (error.details) {
|
|
79
|
+
if (Array.isArray(error.details)) {
|
|
80
|
+
for (const d of error.details) {
|
|
81
|
+
console.error(chalk.yellow(` ${d.field}:`), d.message)
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
console.error(chalk.yellow(' Details:'), error.details)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
process.exit(error.exitCode)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (error instanceof CliError) {
|
|
91
|
+
console.error(chalk.red('Error:'), error.message)
|
|
92
|
+
process.exit(error.exitCode)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
console.error(chalk.red('Error:'), error.message)
|
|
97
|
+
if (process.env.DEBUG) {
|
|
98
|
+
console.error(error.stack)
|
|
99
|
+
}
|
|
100
|
+
process.exit(1)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.error(chalk.red('Error:'), 'An unknown error occurred')
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
package/src/lib/oauth.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { platform } from 'node:os'
|
|
3
|
+
import type { Ora } from 'ora'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { CLIENT_ID } from './constants.ts'
|
|
6
|
+
|
|
7
|
+
interface DeviceAuthResponse {
|
|
8
|
+
device_code: string
|
|
9
|
+
user_code: string
|
|
10
|
+
verification_uri: string
|
|
11
|
+
verification_uri_complete?: string
|
|
12
|
+
expires_in: number
|
|
13
|
+
interval?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TokenResponse {
|
|
17
|
+
access_token: string
|
|
18
|
+
token_type: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sleep (ms: number): Promise<void> {
|
|
22
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* POST /api/auth/oauth2/device to start device authorization.
|
|
27
|
+
*/
|
|
28
|
+
export async function requestDeviceAuthorization (region: string): Promise<DeviceAuthResponse> {
|
|
29
|
+
const res = await fetch(`${region}/api/auth/oauth2/device`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
32
|
+
body: new URLSearchParams({
|
|
33
|
+
client_id: CLIENT_ID,
|
|
34
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const contentType = res.headers.get('content-type') || ''
|
|
40
|
+
if (res.status === 404) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'OAuth2 device grant not supported on this host.\n' +
|
|
43
|
+
`'${region}' does not have the device authorization endpoint.`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
const body = contentType.includes('text/html')
|
|
47
|
+
? ''
|
|
48
|
+
: await res.text().catch(() => '')
|
|
49
|
+
throw new Error(`Device authorization request failed (${res.status}): ${body || res.statusText}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return res.json() as Promise<DeviceAuthResponse>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Poll POST /api/auth/oauth2/token until the user authorizes.
|
|
57
|
+
* Handles: authorization_pending, slow_down (+5s per RFC 8628 §3.5),
|
|
58
|
+
* access_denied, expired_token. Hard cap at 10 minutes.
|
|
59
|
+
*/
|
|
60
|
+
export async function pollForToken (
|
|
61
|
+
region: string,
|
|
62
|
+
deviceCode: string,
|
|
63
|
+
interval: number,
|
|
64
|
+
expiresIn: number
|
|
65
|
+
): Promise<TokenResponse> {
|
|
66
|
+
const maxWait = Math.min(expiresIn, 600) * 1000
|
|
67
|
+
const deadline = Date.now() + maxWait
|
|
68
|
+
let pollInterval = interval * 1000
|
|
69
|
+
|
|
70
|
+
while (Date.now() < deadline) {
|
|
71
|
+
await sleep(pollInterval)
|
|
72
|
+
|
|
73
|
+
const res = await fetch(`${region}/api/auth/oauth2/token`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
76
|
+
body: new URLSearchParams({
|
|
77
|
+
client_id: CLIENT_ID,
|
|
78
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
79
|
+
device_code: deviceCode
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (res.ok) {
|
|
84
|
+
return res.json() as Promise<TokenResponse>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const body = await res.json().catch(() => ({})) as { error?: string }
|
|
88
|
+
|
|
89
|
+
switch (body.error) {
|
|
90
|
+
case 'authorization_pending':
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
case 'slow_down':
|
|
94
|
+
pollInterval += 5000
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
case 'access_denied':
|
|
98
|
+
throw new Error('Authorization denied by user')
|
|
99
|
+
|
|
100
|
+
case 'expired_token':
|
|
101
|
+
throw new Error('Device code expired. Please run login again.')
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
throw new Error(`Token request failed: ${body.error || res.statusText}`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error('Authorization timed out (10 minutes). Please run login again.')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Exchange an OAuth access token for a Sealos kubeconfig.
|
|
113
|
+
*/
|
|
114
|
+
export async function exchangeForKubeconfig (region: string, accessToken: string): Promise<string> {
|
|
115
|
+
const res = await fetch(`${region}/api/auth/getDefaultKubeconfig`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: accessToken,
|
|
119
|
+
'Content-Type': 'application/json'
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const body = await res.text().catch(() => '')
|
|
125
|
+
throw new Error(`Kubeconfig exchange failed (${res.status}): ${body || res.statusText}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const data = await res.json() as { data?: { kubeconfig?: string } }
|
|
129
|
+
const kubeconfig = data.data?.kubeconfig
|
|
130
|
+
|
|
131
|
+
if (!kubeconfig) {
|
|
132
|
+
throw new Error('API response missing data.kubeconfig field')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return kubeconfig
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Open a URL in the user's default browser. Swallows errors silently.
|
|
140
|
+
*/
|
|
141
|
+
export function openBrowser (url: string): void {
|
|
142
|
+
try {
|
|
143
|
+
const os = platform()
|
|
144
|
+
const cmd = os === 'darwin' ? 'open' : os === 'win32' ? 'start' : 'xdg-open'
|
|
145
|
+
execSync(`${cmd} "${url}"`, { stdio: 'ignore' })
|
|
146
|
+
} catch {
|
|
147
|
+
// Silently ignore — user can open manually
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Orchestrate the full OAuth2 Device Grant login flow.
|
|
153
|
+
*/
|
|
154
|
+
export async function deviceGrantLogin (
|
|
155
|
+
region: string,
|
|
156
|
+
spinner: Ora
|
|
157
|
+
): Promise<{ kubeconfig: string; region: string }> {
|
|
158
|
+
// Step 1: Request device authorization
|
|
159
|
+
spinner.stop()
|
|
160
|
+
const deviceAuth = await requestDeviceAuthorization(region)
|
|
161
|
+
|
|
162
|
+
const {
|
|
163
|
+
device_code: deviceCode,
|
|
164
|
+
user_code: userCode,
|
|
165
|
+
verification_uri: verificationUri,
|
|
166
|
+
verification_uri_complete: verificationUriComplete,
|
|
167
|
+
expires_in: expiresIn,
|
|
168
|
+
interval = 5
|
|
169
|
+
} = deviceAuth
|
|
170
|
+
|
|
171
|
+
// Step 2: Display verification info
|
|
172
|
+
const url = verificationUriComplete || verificationUri
|
|
173
|
+
console.log()
|
|
174
|
+
console.log(chalk.bold(' Open this URL to authorize:'))
|
|
175
|
+
console.log(` ${chalk.cyan(url)}`)
|
|
176
|
+
console.log()
|
|
177
|
+
console.log(` Code: ${chalk.bold.yellow(userCode)}`)
|
|
178
|
+
console.log()
|
|
179
|
+
|
|
180
|
+
// Step 3: Auto-open browser
|
|
181
|
+
openBrowser(url)
|
|
182
|
+
|
|
183
|
+
// Step 4: Poll for token
|
|
184
|
+
spinner.start('Waiting for authorization...')
|
|
185
|
+
const tokenResponse = await pollForToken(region, deviceCode, interval, expiresIn)
|
|
186
|
+
const accessToken = tokenResponse.access_token
|
|
187
|
+
|
|
188
|
+
if (!accessToken) {
|
|
189
|
+
throw new Error('Token response missing access_token')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Step 5: Exchange for kubeconfig
|
|
193
|
+
spinner.text = 'Exchanging for kubeconfig...'
|
|
194
|
+
const kubeconfig = await exchangeForKubeconfig(region, accessToken)
|
|
195
|
+
|
|
196
|
+
return { kubeconfig, region }
|
|
197
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { table } from 'table'
|
|
3
|
+
import ora, { type Ora } from 'ora'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Output JSON format
|
|
7
|
+
*/
|
|
8
|
+
export function outputJson (data: any): void {
|
|
9
|
+
console.log(JSON.stringify(data, null, 2))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Output YAML format
|
|
14
|
+
*/
|
|
15
|
+
export function outputYaml (data: any): void {
|
|
16
|
+
// TODO: Use yaml library to implement
|
|
17
|
+
console.log('YAML output not implemented yet')
|
|
18
|
+
console.log(data)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Output table
|
|
23
|
+
*/
|
|
24
|
+
export function outputTable (data: string[][]): void {
|
|
25
|
+
console.log(table(data))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format output
|
|
30
|
+
*/
|
|
31
|
+
export function formatOutput (data: any, format: 'json' | 'yaml' | 'table' = 'table'): void {
|
|
32
|
+
switch (format) {
|
|
33
|
+
case 'json':
|
|
34
|
+
outputJson(data)
|
|
35
|
+
break
|
|
36
|
+
case 'yaml':
|
|
37
|
+
outputYaml(data)
|
|
38
|
+
break
|
|
39
|
+
case 'table':
|
|
40
|
+
if (Array.isArray(data)) {
|
|
41
|
+
outputTable(data)
|
|
42
|
+
} else {
|
|
43
|
+
console.log(data)
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
default:
|
|
47
|
+
console.log(data)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Success message
|
|
53
|
+
*/
|
|
54
|
+
export function success (message: string): void {
|
|
55
|
+
console.log(chalk.green('✓'), message)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Error message
|
|
60
|
+
*/
|
|
61
|
+
export function error (message: string): void {
|
|
62
|
+
console.error(chalk.red('✗'), message)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Warning message
|
|
67
|
+
*/
|
|
68
|
+
export function warn (message: string): void {
|
|
69
|
+
console.warn(chalk.yellow('⚠'), message)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Info message
|
|
74
|
+
*/
|
|
75
|
+
export function info (message: string): void {
|
|
76
|
+
console.log(chalk.blue('ℹ'), message)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create loading spinner
|
|
81
|
+
*/
|
|
82
|
+
export function spinner (text: string): Ora {
|
|
83
|
+
return ora(text).start()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Confirmation prompt
|
|
88
|
+
*/
|
|
89
|
+
export async function confirm (message: string): Promise<boolean> {
|
|
90
|
+
// TODO: Use inquirer or other interactive library
|
|
91
|
+
console.log(chalk.yellow('?'), message)
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Ora } from 'ora'
|
|
2
|
+
import { spinner } from './output.ts'
|
|
3
|
+
import { requireAuth } from './auth.ts'
|
|
4
|
+
import { handleError } from './errors.ts'
|
|
5
|
+
|
|
6
|
+
interface AuthContext {
|
|
7
|
+
auth: { Authorization: string }
|
|
8
|
+
spinner: Ora
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ErrorHandlingContext {
|
|
12
|
+
spinner: Ora
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface WithAuthOptions {
|
|
16
|
+
spinnerText: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wraps a command handler that requires authentication.
|
|
21
|
+
* Handles: auth check + spinner + try/catch + error handling
|
|
22
|
+
*/
|
|
23
|
+
export function withAuth<T extends any[]> (
|
|
24
|
+
options: WithAuthOptions,
|
|
25
|
+
fn: (ctx: AuthContext, ...args: T) => Promise<void>
|
|
26
|
+
): (...args: T) => Promise<void> {
|
|
27
|
+
return async (...args: T) => {
|
|
28
|
+
const spin = spinner(options.spinnerText)
|
|
29
|
+
try {
|
|
30
|
+
const auth = requireAuth()
|
|
31
|
+
await fn({ auth, spinner: spin }, ...args)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
spin.fail()
|
|
34
|
+
handleError(error)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wraps a command handler that does NOT require authentication.
|
|
41
|
+
* Handles: spinner + try/catch + error handling
|
|
42
|
+
*/
|
|
43
|
+
export function withErrorHandling<T extends any[]> (
|
|
44
|
+
options: WithAuthOptions,
|
|
45
|
+
fn: (ctx: ErrorHandlingContext, ...args: T) => Promise<void>
|
|
46
|
+
): (...args: T) => Promise<void> {
|
|
47
|
+
return async (...args: T) => {
|
|
48
|
+
const spin = spinner(options.spinnerText)
|
|
49
|
+
try {
|
|
50
|
+
await fn({ spinner: spin }, ...args)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
spin.fail()
|
|
53
|
+
handleError(error)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { registerAuthCommands } from './commands/auth/index.ts'
|
|
4
|
+
import { createWorkspaceCommand } from './commands/workspace/index.ts'
|
|
5
|
+
import { createDevboxCommand } from './commands/devbox/index.ts'
|
|
6
|
+
import { createS3Command } from './commands/s3/index.ts'
|
|
7
|
+
import { createDatabaseCommand } from './commands/database/index.ts'
|
|
8
|
+
import { createTemplateCommand } from './commands/template/index.ts'
|
|
9
|
+
import { createQuotaCommand } from './commands/quota/index.ts'
|
|
10
|
+
import { createAppCommand } from './commands/app/index.ts'
|
|
11
|
+
import { createConfigCommand } from './commands/config/index.ts'
|
|
12
|
+
import { handleError } from './lib/errors.ts'
|
|
13
|
+
|
|
14
|
+
export function createProgram (): Command {
|
|
15
|
+
const program = new Command()
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('sealos')
|
|
19
|
+
.description('Official CLI tool for Sealos Cloud - Manage devbox, applications, databases, and object storage')
|
|
20
|
+
.version('0.0.1')
|
|
21
|
+
|
|
22
|
+
// Register all command modules
|
|
23
|
+
registerAuthCommands(program)
|
|
24
|
+
program.addCommand(createWorkspaceCommand())
|
|
25
|
+
program.addCommand(createDevboxCommand())
|
|
26
|
+
program.addCommand(createS3Command())
|
|
27
|
+
program.addCommand(createDatabaseCommand())
|
|
28
|
+
program.addCommand(createTemplateCommand())
|
|
29
|
+
program.addCommand(createQuotaCommand())
|
|
30
|
+
program.addCommand(createAppCommand())
|
|
31
|
+
program.addCommand(createConfigCommand())
|
|
32
|
+
|
|
33
|
+
return program
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function runCLI (): void {
|
|
37
|
+
const program = createProgram()
|
|
38
|
+
|
|
39
|
+
// Global error handling
|
|
40
|
+
program.exitOverride()
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
program.parse(process.argv)
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error instanceof Error && 'code' in error &&
|
|
46
|
+
typeof error.code === 'string' && error.code.startsWith('commander.')) {
|
|
47
|
+
process.exit(0)
|
|
48
|
+
}
|
|
49
|
+
handleError(error)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Core type definitions
|
|
2
|
+
|
|
3
|
+
export interface SealosConfig {
|
|
4
|
+
currentContext: string
|
|
5
|
+
contexts: Context[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Context {
|
|
9
|
+
name: string
|
|
10
|
+
host: string
|
|
11
|
+
token: string
|
|
12
|
+
workspace: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DevboxConfig {
|
|
16
|
+
name?: string
|
|
17
|
+
template: string
|
|
18
|
+
resources: {
|
|
19
|
+
cpu: string
|
|
20
|
+
memory: string
|
|
21
|
+
storage?: string
|
|
22
|
+
}
|
|
23
|
+
ports?: number[]
|
|
24
|
+
env?: Record<string, string>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OutputOptions {
|
|
28
|
+
format: 'json' | 'yaml' | 'table'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ApiResponse<T = any> {
|
|
32
|
+
success: boolean
|
|
33
|
+
data?: T
|
|
34
|
+
error?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SealosWorkspace {
|
|
38
|
+
uid?: string
|
|
39
|
+
id?: string
|
|
40
|
+
teamName?: string
|
|
41
|
+
role?: string
|
|
42
|
+
nstype?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SealosAuthData {
|
|
46
|
+
region: string
|
|
47
|
+
access_token?: string
|
|
48
|
+
regional_token?: string
|
|
49
|
+
authenticated_at?: string
|
|
50
|
+
auth_method?: string
|
|
51
|
+
current_workspace?: {
|
|
52
|
+
uid?: string
|
|
53
|
+
id?: string
|
|
54
|
+
teamName?: string
|
|
55
|
+
}
|
|
56
|
+
}
|