sealos-cli 0.1.0 → 1.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/README.md +77 -76
- package/dist/bin/cli.cjs +5120 -775
- package/dist/bin/cli.mjs +5120 -775
- package/dist/main.cjs +5116 -775
- package/dist/main.mjs +5120 -775
- package/package.json +6 -5
- package/src/commands/app/index.ts +5 -4
- package/src/commands/database/index.ts +12 -12
- package/src/commands/devbox/index.ts +676 -183
- package/src/commands/quota/index.ts +4 -3
- package/src/commands/s3/index.ts +5 -5
- package/src/commands/template/index.ts +107 -41
- package/src/commands/workspace/index.ts +49 -39
- package/src/docs/database_openapi.json +21 -38
- package/src/docs/devbox_openapi.json +5760 -0
- package/src/docs/template_openapi.json +2661 -1
- package/src/generated/database.ts +1 -5
- package/src/generated/devbox.ts +2500 -0
- package/src/generated/template.ts +228 -0
- package/src/lib/api-client.ts +19 -1
- package/src/lib/auth.ts +17 -8
- package/src/lib/errors.ts +1 -1
- package/src/lib/output.ts +5 -6
- package/src/main.ts +4 -11
- package/src/types/index.ts +0 -12
- package/src/commands/config/index.ts +0 -54
- package/src/lib/api.ts +0 -83
- package/src/lib/config.ts +0 -134
|
@@ -1,224 +1,717 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { createDevboxClient } from '../../lib/api-client.ts'
|
|
4
|
+
import { type ApiErrorBody, mapApiError } from '../../lib/errors.ts'
|
|
5
|
+
import { outputJson, outputTable } from '../../lib/output.ts'
|
|
6
|
+
import { withAuth } from '../../lib/with-auth.ts'
|
|
7
|
+
|
|
8
|
+
const PORT_PROTOCOLS = ['http', 'grpc', 'ws'] as const
|
|
9
|
+
|
|
10
|
+
type PortProtocol = typeof PORT_PROTOCOLS[number]
|
|
11
|
+
|
|
12
|
+
interface DevboxPortOptions {
|
|
13
|
+
portName?: string
|
|
14
|
+
number?: number
|
|
15
|
+
protocol?: PortProtocol
|
|
16
|
+
isPublic?: boolean
|
|
17
|
+
customDomain?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DevboxCreateOptions {
|
|
21
|
+
name: string
|
|
22
|
+
runtime?: string
|
|
23
|
+
template?: string
|
|
24
|
+
cpu: string
|
|
25
|
+
memory: string
|
|
26
|
+
port: string[]
|
|
27
|
+
env: string[]
|
|
28
|
+
secretEnv: string[]
|
|
29
|
+
autostart?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DevboxUpdateOptions {
|
|
33
|
+
cpu?: string
|
|
34
|
+
memory?: string
|
|
35
|
+
port: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface DevboxReleaseOptions {
|
|
39
|
+
tag: string
|
|
40
|
+
description?: string
|
|
41
|
+
execCommand?: string
|
|
42
|
+
noStart?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatValue (value: unknown): string {
|
|
46
|
+
if (value === undefined || value === null || value === '') return '-'
|
|
47
|
+
return String(value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function collectOption (value: string, previous: string[]): string[] {
|
|
51
|
+
return [...previous, value]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseNumericValue (value: string, field: string): number {
|
|
55
|
+
const normalized = value.trim().toLowerCase()
|
|
56
|
+
let raw = normalized
|
|
57
|
+
|
|
58
|
+
if (field === 'cpu' && normalized.endsWith('c')) {
|
|
59
|
+
raw = normalized.slice(0, -1)
|
|
60
|
+
}
|
|
61
|
+
if (field === 'memory' && /gi?$|gb$|g$/i.test(normalized)) {
|
|
62
|
+
raw = normalized.replace(/gi?$|gb$|g$/i, '')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parsed = Number(raw)
|
|
66
|
+
if (!Number.isFinite(parsed)) {
|
|
67
|
+
throw new Error(`Invalid ${field} value "${value}"`)
|
|
68
|
+
}
|
|
69
|
+
if (parsed < 0.1 || parsed > 32) {
|
|
70
|
+
throw new Error(`${field} must be between 0.1 and 32`)
|
|
71
|
+
}
|
|
72
|
+
return parsed
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseBooleanValue (value: string, field: string): boolean {
|
|
76
|
+
const normalized = value.trim().toLowerCase()
|
|
77
|
+
if (['true', 'yes', '1', 'public'].includes(normalized)) return true
|
|
78
|
+
if (['false', 'no', '0', 'private'].includes(normalized)) return false
|
|
79
|
+
throw new Error(`Invalid ${field} boolean value "${value}"`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeProtocol (value?: string): PortProtocol | undefined {
|
|
83
|
+
if (!value) return undefined
|
|
84
|
+
const normalized = value.trim().toLowerCase()
|
|
85
|
+
if (!PORT_PROTOCOLS.includes(normalized as PortProtocol)) {
|
|
86
|
+
throw new Error(`Invalid port protocol "${value}". Use one of: ${PORT_PROTOCOLS.join(', ')}`)
|
|
87
|
+
}
|
|
88
|
+
return normalized as PortProtocol
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function parsePortSpec (spec: string): DevboxPortOptions {
|
|
92
|
+
if (!spec) {
|
|
93
|
+
throw new Error('Port spec is required.')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (spec.includes('=')) {
|
|
97
|
+
const port: DevboxPortOptions = {}
|
|
98
|
+
for (const pair of spec.split(',')) {
|
|
99
|
+
const index = pair.indexOf('=')
|
|
100
|
+
if (index === -1) {
|
|
101
|
+
throw new Error(`Invalid port spec "${spec}". Expected comma-separated KEY=VALUE pairs.`)
|
|
102
|
+
}
|
|
103
|
+
const key = pair.slice(0, index).trim()
|
|
104
|
+
const value = pair.slice(index + 1).trim()
|
|
105
|
+
switch (key) {
|
|
106
|
+
case 'portName':
|
|
107
|
+
port.portName = value
|
|
108
|
+
break
|
|
109
|
+
case 'number':
|
|
110
|
+
port.number = parsePortNumber(value)
|
|
111
|
+
break
|
|
112
|
+
case 'protocol':
|
|
113
|
+
port.protocol = normalizeProtocol(value)
|
|
114
|
+
break
|
|
115
|
+
case 'isPublic':
|
|
116
|
+
port.isPublic = parseBooleanValue(value, 'isPublic')
|
|
117
|
+
break
|
|
118
|
+
case 'customDomain':
|
|
119
|
+
port.customDomain = value
|
|
120
|
+
break
|
|
121
|
+
default:
|
|
122
|
+
throw new Error(`Unsupported port field "${key}"`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return port
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const [rawNumber, rawProtocol, rawVisibility, customDomain] = spec.split(':')
|
|
129
|
+
const port: DevboxPortOptions = {
|
|
130
|
+
number: parsePortNumber(rawNumber || '')
|
|
131
|
+
}
|
|
132
|
+
const protocol = normalizeProtocol(rawProtocol)
|
|
133
|
+
if (protocol) port.protocol = protocol
|
|
134
|
+
if (rawVisibility) port.isPublic = parseBooleanValue(rawVisibility, 'visibility')
|
|
135
|
+
if (customDomain) port.customDomain = customDomain
|
|
136
|
+
return port
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parsePortNumber (value: string): number {
|
|
140
|
+
const parsed = Number(value)
|
|
141
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
142
|
+
throw new Error(`Invalid port number "${value}"`)
|
|
143
|
+
}
|
|
144
|
+
return parsed
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseEnvArgs (envs: string[]): Array<{ name: string; value: string }> {
|
|
148
|
+
return envs.map(env => {
|
|
149
|
+
const index = env.indexOf('=')
|
|
150
|
+
if (index === -1) {
|
|
151
|
+
throw new Error(`Invalid --env format: "${env}". Expected NAME=VALUE`)
|
|
152
|
+
}
|
|
153
|
+
const name = env.slice(0, index)
|
|
154
|
+
if (!name) {
|
|
155
|
+
throw new Error(`Invalid --env format: "${env}". Environment name is required.`)
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
value: env.slice(index + 1)
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseSecretEnvArgs (envs: string[]): Array<{ name: string; valueFrom: { secretKeyRef: { name: string; key: string } } }> {
|
|
165
|
+
return envs.map(env => {
|
|
166
|
+
const index = env.indexOf('=')
|
|
167
|
+
if (index === -1) {
|
|
168
|
+
throw new Error(`Invalid --secret-env format: "${env}". Expected NAME=SECRET:KEY`)
|
|
169
|
+
}
|
|
170
|
+
const name = env.slice(0, index)
|
|
171
|
+
const [secretName, secretKey] = env.slice(index + 1).split(':')
|
|
172
|
+
if (!name || !secretName || !secretKey) {
|
|
173
|
+
throw new Error(`Invalid --secret-env format: "${env}". Expected NAME=SECRET:KEY`)
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
name,
|
|
177
|
+
valueFrom: {
|
|
178
|
+
secretKeyRef: {
|
|
179
|
+
name: secretName,
|
|
180
|
+
key: secretKey
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function requireRuntime (options: Pick<DevboxCreateOptions, 'runtime' | 'template'>): string {
|
|
188
|
+
const runtime = options.runtime || options.template
|
|
189
|
+
if (!runtime) {
|
|
190
|
+
throw new Error('Devbox runtime is required. Use --runtime <runtime>.')
|
|
191
|
+
}
|
|
192
|
+
return runtime
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function buildCreateDevboxBody (options: DevboxCreateOptions) {
|
|
196
|
+
return {
|
|
197
|
+
name: options.name,
|
|
198
|
+
runtime: requireRuntime(options),
|
|
199
|
+
quota: {
|
|
200
|
+
cpu: parseNumericValue(options.cpu, 'cpu'),
|
|
201
|
+
memory: parseNumericValue(options.memory, 'memory')
|
|
202
|
+
},
|
|
203
|
+
ports: options.port.map(parsePortSpec),
|
|
204
|
+
env: [
|
|
205
|
+
...parseEnvArgs(options.env),
|
|
206
|
+
...parseSecretEnvArgs(options.secretEnv)
|
|
207
|
+
],
|
|
208
|
+
autostart: options.autostart ?? false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function buildUpdateDevboxBody (options: DevboxUpdateOptions) {
|
|
213
|
+
const body: {
|
|
214
|
+
quota?: { cpu?: number; memory?: number }
|
|
215
|
+
ports?: DevboxPortOptions[]
|
|
216
|
+
} = {}
|
|
217
|
+
|
|
218
|
+
if (options.cpu || options.memory) {
|
|
219
|
+
body.quota = {}
|
|
220
|
+
if (options.cpu) body.quota.cpu = parseNumericValue(options.cpu, 'cpu')
|
|
221
|
+
if (options.memory) body.quota.memory = parseNumericValue(options.memory, 'memory')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (options.port.length > 0) {
|
|
225
|
+
body.ports = options.port.map(parsePortSpec)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!body.quota && !body.ports) {
|
|
229
|
+
throw new Error('Provide at least one of --cpu, --memory, or --port.')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return body
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function buildReleaseBody (options: DevboxReleaseOptions) {
|
|
236
|
+
const body: {
|
|
237
|
+
tag: string
|
|
238
|
+
releaseDescription?: string
|
|
239
|
+
execCommand?: string
|
|
240
|
+
startDevboxAfterRelease?: boolean
|
|
241
|
+
} = {
|
|
242
|
+
tag: options.tag
|
|
243
|
+
}
|
|
244
|
+
if (options.description !== undefined) body.releaseDescription = options.description
|
|
245
|
+
if (options.execCommand !== undefined) body.execCommand = options.execCommand
|
|
246
|
+
if (options.noStart) body.startDevboxAfterRelease = false
|
|
247
|
+
return body
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function printDevboxList (items: any[]): void {
|
|
251
|
+
if (items.length === 0) {
|
|
252
|
+
console.log('No devboxes found.')
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
outputTable([
|
|
256
|
+
[chalk.bold('Name'), chalk.bold('Runtime'), chalk.bold('Status'), chalk.bold('CPU'), chalk.bold('Memory'), chalk.bold('UID')],
|
|
257
|
+
...items.map(item => [
|
|
258
|
+
formatValue(item.name),
|
|
259
|
+
formatValue(item.runtime),
|
|
260
|
+
formatValue(item.status),
|
|
261
|
+
item.quota?.cpu != null ? `${item.quota.cpu} vCPU` : '-',
|
|
262
|
+
item.quota?.memory != null ? `${item.quota.memory} GB` : '-',
|
|
263
|
+
formatValue(item.uid)
|
|
264
|
+
])
|
|
265
|
+
])
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function printDevboxDetail (devbox: any): void {
|
|
269
|
+
outputTable([
|
|
270
|
+
[chalk.bold('Field'), chalk.bold('Value')],
|
|
271
|
+
['Name', formatValue(devbox.name)],
|
|
272
|
+
['Runtime', formatValue(devbox.runtime)],
|
|
273
|
+
['Status', formatValue(devbox.status)],
|
|
274
|
+
['CPU', devbox.quota?.cpu != null ? `${devbox.quota.cpu} vCPU` : '-'],
|
|
275
|
+
['Memory', devbox.quota?.memory != null ? `${devbox.quota.memory} GB` : '-'],
|
|
276
|
+
['Working Dir', formatValue(devbox.workingDir)],
|
|
277
|
+
['SSH User', formatValue(devbox.userName)],
|
|
278
|
+
['SSH Port', formatValue(devbox.sshPort)],
|
|
279
|
+
['Domain', formatValue(devbox.domain)]
|
|
280
|
+
])
|
|
281
|
+
|
|
282
|
+
if (devbox.ports?.length > 0) {
|
|
283
|
+
console.log(chalk.dim('\n Ports:'))
|
|
284
|
+
outputTable([
|
|
285
|
+
[chalk.bold('Name'), chalk.bold('Number'), chalk.bold('Protocol'), chalk.bold('Public'), chalk.bold('Public Domain'), chalk.bold('Private Address')],
|
|
286
|
+
...devbox.ports.map((port: any) => [
|
|
287
|
+
formatValue(port.portName),
|
|
288
|
+
formatValue(port.number),
|
|
289
|
+
formatValue(port.protocol),
|
|
290
|
+
formatValue(port.isPublic),
|
|
291
|
+
formatValue(port.publicDomain),
|
|
292
|
+
formatValue(port.privateAddress)
|
|
293
|
+
])
|
|
294
|
+
])
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (devbox.pods?.length > 0) {
|
|
298
|
+
console.log(chalk.dim('\n Pods:'))
|
|
299
|
+
outputTable([
|
|
300
|
+
[chalk.bold('Name'), chalk.bold('Status')],
|
|
301
|
+
...devbox.pods.map((pod: any) => [formatValue(pod.name), formatValue(pod.status)])
|
|
302
|
+
])
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function printCreateResult (devbox: any): void {
|
|
307
|
+
console.log(chalk.dim(` Name: ${formatValue(devbox.name)}`))
|
|
308
|
+
console.log(chalk.dim(` SSH: ${formatValue(devbox.userName)}@${formatValue(devbox.domain)}:${formatValue(devbox.sshPort)}`))
|
|
309
|
+
console.log(chalk.dim(` Working Dir: ${formatValue(devbox.workingDir)}`))
|
|
310
|
+
if (devbox.autostarted !== undefined) {
|
|
311
|
+
console.log(chalk.dim(` Autostarted: ${formatValue(devbox.autostarted)}`))
|
|
312
|
+
}
|
|
313
|
+
if (devbox.ports?.length > 0) {
|
|
314
|
+
console.log(chalk.dim('\n Ports:'))
|
|
315
|
+
outputTable([
|
|
316
|
+
[chalk.bold('Number'), chalk.bold('Protocol'), chalk.bold('Public'), chalk.bold('Public Domain'), chalk.bold('Private Address')],
|
|
317
|
+
...devbox.ports.map((port: any) => [
|
|
318
|
+
formatValue(port.number),
|
|
319
|
+
formatValue(port.protocol),
|
|
320
|
+
formatValue(port.isPublic),
|
|
321
|
+
formatValue(port.publicDomain),
|
|
322
|
+
formatValue(port.privateAddress)
|
|
323
|
+
])
|
|
324
|
+
])
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function printReleases (releases: any[]): void {
|
|
329
|
+
if (releases.length === 0) {
|
|
330
|
+
console.log('No releases found.')
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
outputTable([
|
|
334
|
+
[chalk.bold('Tag'), chalk.bold('Name'), chalk.bold('Created'), chalk.bold('Image'), chalk.bold('Description')],
|
|
335
|
+
...releases.map(release => [
|
|
336
|
+
formatValue(release.tag),
|
|
337
|
+
formatValue(release.name),
|
|
338
|
+
formatValue(release.createdAt),
|
|
339
|
+
formatValue(release.image),
|
|
340
|
+
formatValue(release.description)
|
|
341
|
+
])
|
|
342
|
+
])
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function printDeployments (deployments: any[]): void {
|
|
346
|
+
if (deployments.length === 0) {
|
|
347
|
+
console.log('No deployments found.')
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
outputTable([
|
|
351
|
+
[chalk.bold('Name'), chalk.bold('Type'), chalk.bold('Tag')],
|
|
352
|
+
...deployments.map(deployment => [
|
|
353
|
+
formatValue(deployment.name),
|
|
354
|
+
formatValue(deployment.resourceType),
|
|
355
|
+
formatValue(deployment.tag)
|
|
356
|
+
])
|
|
357
|
+
])
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function printMonitor (samples: any[]): void {
|
|
361
|
+
if (samples.length === 0) {
|
|
362
|
+
console.log('No monitor samples found.')
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
outputTable([
|
|
366
|
+
[chalk.bold('Time'), chalk.bold('CPU %'), chalk.bold('Memory %'), chalk.bold('Timestamp')],
|
|
367
|
+
...samples.map(sample => [
|
|
368
|
+
formatValue(sample.readableTime),
|
|
369
|
+
formatValue(sample.cpu),
|
|
370
|
+
formatValue(sample.memory),
|
|
371
|
+
formatValue(sample.timestamp)
|
|
372
|
+
])
|
|
373
|
+
])
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function printTemplates (templates: any[]): void {
|
|
377
|
+
if (templates.length === 0) {
|
|
378
|
+
console.log('No devbox templates found.')
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
outputTable([
|
|
382
|
+
[chalk.bold('Runtime'), chalk.bold('User'), chalk.bold('Working Dir'), chalk.bold('App Ports'), chalk.bold('Ports')],
|
|
383
|
+
...templates.map(template => [
|
|
384
|
+
formatValue(template.runtime),
|
|
385
|
+
formatValue(template.config?.user),
|
|
386
|
+
formatValue(template.config?.workingDir),
|
|
387
|
+
Array.isArray(template.config?.appPorts) ? String(template.config.appPorts.length) : '-',
|
|
388
|
+
Array.isArray(template.config?.ports) ? String(template.config.ports.length) : '-'
|
|
389
|
+
])
|
|
390
|
+
])
|
|
391
|
+
}
|
|
5
392
|
|
|
6
393
|
export function createDevboxCommand (): Command {
|
|
7
394
|
const devboxCmd = new Command('devbox')
|
|
8
395
|
.alias('dev')
|
|
9
396
|
.description('Manage devbox instances')
|
|
10
397
|
|
|
11
|
-
// devbox create
|
|
12
398
|
devboxCmd
|
|
13
|
-
.command('
|
|
14
|
-
.description('
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const context = getCurrentContext()
|
|
25
|
-
if (!context) {
|
|
26
|
-
throw new AuthError()
|
|
27
|
-
}
|
|
399
|
+
.command('list')
|
|
400
|
+
.description('List all devboxes')
|
|
401
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
402
|
+
.action(withAuth({ spinnerText: 'Loading devboxes...' }, async (ctx, options: { output: string }) => {
|
|
403
|
+
const client = createDevboxClient()
|
|
404
|
+
const { data, error, response } = await client.GET('/devbox', {
|
|
405
|
+
headers: ctx.auth
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
409
|
+
ctx.spinner.stop()
|
|
28
410
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// 1. 读取配置文件(如果有)
|
|
33
|
-
// 2. 调用 API 创建 devbox
|
|
34
|
-
// const result = await api.post('/api/v1/devbox', {
|
|
35
|
-
// name: options.name,
|
|
36
|
-
// template: options.template,
|
|
37
|
-
// resources: {
|
|
38
|
-
// cpu: options.cpu,
|
|
39
|
-
// memory: options.memory
|
|
40
|
-
// }
|
|
41
|
-
// })
|
|
42
|
-
|
|
43
|
-
// 3. 等待创建完成
|
|
44
|
-
// 4. 返回访问地址
|
|
45
|
-
|
|
46
|
-
spin.succeed('Devbox created successfully')
|
|
47
|
-
success('Devbox URL: https://example.sealos.run')
|
|
48
|
-
info(`Run "sealos devbox connect ${options.name || 'devbox'}" to connect`)
|
|
49
|
-
} catch (error) {
|
|
50
|
-
handleError(error)
|
|
411
|
+
if (options.output === 'json') {
|
|
412
|
+
outputJson(data)
|
|
413
|
+
return
|
|
51
414
|
}
|
|
52
|
-
|
|
415
|
+
printDevboxList(data)
|
|
416
|
+
}))
|
|
53
417
|
|
|
54
|
-
// devbox list
|
|
55
418
|
devboxCmd
|
|
56
|
-
.command('
|
|
57
|
-
.description('
|
|
58
|
-
.
|
|
59
|
-
.option('--
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
419
|
+
.command('create')
|
|
420
|
+
.description('Create a new devbox')
|
|
421
|
+
.requiredOption('--name <name>', 'Devbox name')
|
|
422
|
+
.option('--runtime <runtime>', 'Runtime environment name')
|
|
423
|
+
.option('--template <runtime>', 'Alias for --runtime')
|
|
424
|
+
.option('--cpu <cpu>', 'CPU cores', '1')
|
|
425
|
+
.option('--memory <memory>', 'Memory in GB', '2')
|
|
426
|
+
.option('--port <spec>', 'Port spec, e.g. 8080:http:public or number=8080,protocol=http', collectOption, [] as string[])
|
|
427
|
+
.option('--env <NAME=VALUE>', 'Environment variable', collectOption, [] as string[])
|
|
428
|
+
.option('--secret-env <NAME=SECRET:KEY>', 'Environment variable from secret', collectOption, [] as string[])
|
|
429
|
+
.option('--autostart', 'Auto start devbox after creation')
|
|
430
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
431
|
+
.action(withAuth({ spinnerText: 'Creating devbox...' }, async (ctx, options: DevboxCreateOptions & { output: string }) => {
|
|
432
|
+
const client = createDevboxClient()
|
|
433
|
+
const { data, error, response } = await client.POST('/devbox', {
|
|
434
|
+
headers: ctx.auth,
|
|
435
|
+
body: buildCreateDevboxBody(options) as any
|
|
436
|
+
})
|
|
66
437
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (options.output === 'json') {
|
|
75
|
-
formatOutput({ items: data.slice(1) }, 'json')
|
|
76
|
-
} else if (options.output === 'yaml') {
|
|
77
|
-
formatOutput({ items: data.slice(1) }, 'yaml')
|
|
78
|
-
} else {
|
|
79
|
-
outputTable(data)
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
handleError(error)
|
|
438
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
439
|
+
|
|
440
|
+
if (options.output === 'json') {
|
|
441
|
+
ctx.spinner.stop()
|
|
442
|
+
outputJson(data)
|
|
443
|
+
return
|
|
83
444
|
}
|
|
84
|
-
|
|
445
|
+
ctx.spinner.succeed(`Devbox "${data.name}" created`)
|
|
446
|
+
printCreateResult(data)
|
|
447
|
+
}))
|
|
85
448
|
|
|
86
|
-
// devbox get
|
|
87
449
|
devboxCmd
|
|
88
|
-
.command('get')
|
|
450
|
+
.command('get <name>')
|
|
89
451
|
.description('Get devbox details')
|
|
90
|
-
.
|
|
91
|
-
.action(async (name) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
452
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
453
|
+
.action(withAuth({ spinnerText: 'Loading devbox...' }, async (ctx, name: string, options: { output: string }) => {
|
|
454
|
+
const client = createDevboxClient()
|
|
455
|
+
const { data, error, response } = await client.GET('/devbox/{name}', {
|
|
456
|
+
headers: ctx.auth,
|
|
457
|
+
params: {
|
|
458
|
+
path: { name }
|
|
96
459
|
}
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
463
|
+
ctx.spinner.stop()
|
|
97
464
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
['Status', 'Running'],
|
|
102
|
-
['CPU', '2c'],
|
|
103
|
-
['Memory', '4g'],
|
|
104
|
-
['URL', 'https://example.sealos.run']
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
outputTable(data)
|
|
108
|
-
} catch (error) {
|
|
109
|
-
handleError(error)
|
|
465
|
+
if (options.output === 'json') {
|
|
466
|
+
outputJson(data)
|
|
467
|
+
return
|
|
110
468
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
469
|
+
printDevboxDetail(data)
|
|
470
|
+
}))
|
|
471
|
+
|
|
472
|
+
devboxCmd
|
|
473
|
+
.command('update <name>')
|
|
474
|
+
.description('Update devbox resources or ports')
|
|
475
|
+
.option('--cpu <cpu>', 'CPU cores')
|
|
476
|
+
.option('--memory <memory>', 'Memory in GB')
|
|
477
|
+
.option('--port <spec>', 'Port spec. Existing ports can include portName=...', collectOption, [] as string[])
|
|
478
|
+
.action(withAuth({ spinnerText: 'Updating devbox...' }, async (ctx, name: string, options: DevboxUpdateOptions) => {
|
|
479
|
+
const client = createDevboxClient()
|
|
480
|
+
const { error, response } = await client.PATCH('/devbox/{name}', {
|
|
481
|
+
headers: ctx.auth,
|
|
482
|
+
params: {
|
|
483
|
+
path: { name }
|
|
484
|
+
},
|
|
485
|
+
body: buildUpdateDevboxBody(options) as any
|
|
486
|
+
})
|
|
124
487
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
488
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
489
|
+
ctx.spinner.succeed(`Devbox "${name}" update requested`)
|
|
490
|
+
}))
|
|
491
|
+
|
|
492
|
+
devboxCmd
|
|
493
|
+
.command('delete <name>')
|
|
494
|
+
.description('Delete a devbox')
|
|
495
|
+
.alias('rm')
|
|
496
|
+
.action(withAuth({ spinnerText: 'Deleting devbox...' }, async (ctx, name: string) => {
|
|
497
|
+
const client = createDevboxClient()
|
|
498
|
+
const { error, response } = await client.DELETE('/devbox/{name}', {
|
|
499
|
+
headers: ctx.auth,
|
|
500
|
+
params: {
|
|
501
|
+
path: { name }
|
|
130
502
|
}
|
|
503
|
+
})
|
|
131
504
|
|
|
132
|
-
|
|
505
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
506
|
+
ctx.spinner.succeed(`Devbox "${name}" deleted`)
|
|
507
|
+
}))
|
|
133
508
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
509
|
+
const lifecycleActions = [
|
|
510
|
+
{ name: 'start', endpoint: '/devbox/{name}/start' as const, spinnerText: 'Starting devbox...', done: 'start requested' },
|
|
511
|
+
{ name: 'pause', endpoint: '/devbox/{name}/pause' as const, spinnerText: 'Pausing devbox...', done: 'pause requested', alias: 'stop' },
|
|
512
|
+
{ name: 'shutdown', endpoint: '/devbox/{name}/shutdown' as const, spinnerText: 'Shutting down devbox...', done: 'shutdown requested' },
|
|
513
|
+
{ name: 'restart', endpoint: '/devbox/{name}/restart' as const, spinnerText: 'Restarting devbox...', done: 'restart requested' }
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
for (const action of lifecycleActions) {
|
|
517
|
+
const command = devboxCmd
|
|
518
|
+
.command(`${action.name} <name>`)
|
|
519
|
+
.description(`${action.name.charAt(0).toUpperCase()}${action.name.slice(1)} a devbox`)
|
|
520
|
+
|
|
521
|
+
if (action.alias) command.alias(action.alias)
|
|
522
|
+
|
|
523
|
+
command.action(withAuth({ spinnerText: action.spinnerText }, async (ctx, name: string) => {
|
|
524
|
+
const client = createDevboxClient()
|
|
525
|
+
const { error, response } = await client.POST(action.endpoint, {
|
|
526
|
+
headers: ctx.auth,
|
|
527
|
+
params: {
|
|
528
|
+
path: { name }
|
|
529
|
+
},
|
|
530
|
+
body: {}
|
|
531
|
+
} as any)
|
|
532
|
+
|
|
533
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
534
|
+
ctx.spinner.succeed(`Devbox "${name}" ${action.done}`)
|
|
535
|
+
}))
|
|
536
|
+
}
|
|
141
537
|
|
|
142
|
-
// devbox connect
|
|
143
538
|
devboxCmd
|
|
144
|
-
.command('
|
|
145
|
-
.description('
|
|
146
|
-
.
|
|
147
|
-
.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
539
|
+
.command('autostart <name>')
|
|
540
|
+
.description('Configure devbox autostart')
|
|
541
|
+
.option('--exec-command <command>', 'Command to execute when the devbox starts')
|
|
542
|
+
.action(withAuth({ spinnerText: 'Configuring autostart...' }, async (ctx, name: string, options: { execCommand?: string }) => {
|
|
543
|
+
const client = createDevboxClient()
|
|
544
|
+
const body = options.execCommand ? { execCommand: options.execCommand } : {}
|
|
545
|
+
const { error, response } = await client.POST('/devbox/{name}/autostart', {
|
|
546
|
+
headers: ctx.auth,
|
|
547
|
+
params: {
|
|
548
|
+
path: { name }
|
|
549
|
+
},
|
|
550
|
+
body
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
554
|
+
ctx.spinner.succeed(`Autostart configured for "${name}"`)
|
|
555
|
+
}))
|
|
556
|
+
|
|
557
|
+
devboxCmd
|
|
558
|
+
.command('monitor <name>')
|
|
559
|
+
.description('Get devbox monitoring data')
|
|
560
|
+
.option('--start <timestamp>', 'Start timestamp in seconds or milliseconds')
|
|
561
|
+
.option('--end <timestamp>', 'End timestamp in seconds or milliseconds')
|
|
562
|
+
.option('--step <step>', 'Sampling interval, e.g. 2m')
|
|
563
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
564
|
+
.action(withAuth({ spinnerText: 'Loading monitor data...' }, async (
|
|
565
|
+
ctx,
|
|
566
|
+
name: string,
|
|
567
|
+
options: { start?: string; end?: string; step?: string; output: string }
|
|
568
|
+
) => {
|
|
569
|
+
const client = createDevboxClient()
|
|
570
|
+
const { data, error, response } = await client.GET('/devbox/{name}/monitor', {
|
|
571
|
+
headers: ctx.auth,
|
|
572
|
+
params: {
|
|
573
|
+
path: { name },
|
|
574
|
+
query: {
|
|
575
|
+
...(options.start ? { start: options.start } : {}),
|
|
576
|
+
...(options.end ? { end: options.end } : {}),
|
|
577
|
+
...(options.step ? { step: options.step } : {})
|
|
578
|
+
}
|
|
153
579
|
}
|
|
580
|
+
})
|
|
154
581
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// 2. 根据 IDE 类型打开对应的连接
|
|
582
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
583
|
+
ctx.spinner.stop()
|
|
158
584
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
handleError(error)
|
|
585
|
+
if (options.output === 'json') {
|
|
586
|
+
outputJson(data)
|
|
587
|
+
return
|
|
163
588
|
}
|
|
164
|
-
|
|
589
|
+
printMonitor(data)
|
|
590
|
+
}))
|
|
165
591
|
|
|
166
|
-
// devbox publish
|
|
167
592
|
devboxCmd
|
|
168
|
-
.command('
|
|
169
|
-
.description('
|
|
170
|
-
.
|
|
171
|
-
.action(async (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
593
|
+
.command('templates')
|
|
594
|
+
.description('List available devbox templates')
|
|
595
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
596
|
+
.action(withAuth({ spinnerText: 'Loading devbox templates...' }, async (ctx, options: { output: string }) => {
|
|
597
|
+
const client = createDevboxClient()
|
|
598
|
+
const { data, error, response } = await client.GET('/devbox/templates', {
|
|
599
|
+
headers: ctx.auth
|
|
600
|
+
})
|
|
177
601
|
|
|
178
|
-
|
|
602
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
603
|
+
ctx.spinner.stop()
|
|
179
604
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
handleError(error)
|
|
605
|
+
if (options.output === 'json') {
|
|
606
|
+
outputJson(data)
|
|
607
|
+
return
|
|
184
608
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
.command('
|
|
190
|
-
.description('Manage devbox
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.command('
|
|
194
|
-
.description('
|
|
195
|
-
.option('--
|
|
196
|
-
.action(async (options) => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
609
|
+
printTemplates(data)
|
|
610
|
+
}))
|
|
611
|
+
|
|
612
|
+
const releasesCommand = devboxCmd
|
|
613
|
+
.command('releases')
|
|
614
|
+
.description('Manage devbox releases')
|
|
615
|
+
|
|
616
|
+
releasesCommand
|
|
617
|
+
.command('list <name>')
|
|
618
|
+
.description('List devbox releases')
|
|
619
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
620
|
+
.action(withAuth({ spinnerText: 'Loading releases...' }, async (ctx, name: string, options: { output: string }) => {
|
|
621
|
+
const client = createDevboxClient()
|
|
622
|
+
const { data, error, response } = await client.GET('/devbox/{name}/releases', {
|
|
623
|
+
headers: ctx.auth,
|
|
624
|
+
params: {
|
|
625
|
+
path: { name }
|
|
626
|
+
}
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
630
|
+
ctx.spinner.stop()
|
|
631
|
+
|
|
632
|
+
if (options.output === 'json') {
|
|
633
|
+
outputJson(data)
|
|
634
|
+
return
|
|
202
635
|
}
|
|
203
|
-
|
|
636
|
+
printReleases(data)
|
|
637
|
+
}))
|
|
204
638
|
|
|
205
|
-
|
|
206
|
-
.command('
|
|
207
|
-
.description('
|
|
208
|
-
.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
639
|
+
releasesCommand
|
|
640
|
+
.command('create <name>')
|
|
641
|
+
.description('Create a devbox release')
|
|
642
|
+
.requiredOption('--tag <tag>', 'Release tag')
|
|
643
|
+
.option('--description <description>', 'Release description')
|
|
644
|
+
.option('--exec-command <command>', 'Autostart command after release restart')
|
|
645
|
+
.option('--no-start', 'Keep devbox stopped after the release build completes')
|
|
646
|
+
.action(withAuth({ spinnerText: 'Creating release...' }, async (ctx, name: string, options: DevboxReleaseOptions) => {
|
|
647
|
+
const client = createDevboxClient()
|
|
648
|
+
const { data, error, response } = await client.POST('/devbox/{name}/releases', {
|
|
649
|
+
headers: ctx.auth,
|
|
650
|
+
params: {
|
|
651
|
+
path: { name }
|
|
652
|
+
},
|
|
653
|
+
body: buildReleaseBody(options)
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
657
|
+
ctx.spinner.succeed(`Release "${options.tag}" accepted for "${data.name}" (${data.status})`)
|
|
658
|
+
}))
|
|
659
|
+
|
|
660
|
+
releasesCommand
|
|
661
|
+
.command('delete <name> <tag>')
|
|
662
|
+
.alias('rm')
|
|
663
|
+
.description('Delete a devbox release')
|
|
664
|
+
.action(withAuth({ spinnerText: 'Deleting release...' }, async (ctx, name: string, tag: string) => {
|
|
665
|
+
const client = createDevboxClient()
|
|
666
|
+
const { error, response } = await client.DELETE('/devbox/{name}/releases/{tag}', {
|
|
667
|
+
headers: ctx.auth,
|
|
668
|
+
params: {
|
|
669
|
+
path: { name, tag }
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
674
|
+
ctx.spinner.succeed(`Release "${tag}" deleted for "${name}"`)
|
|
675
|
+
}))
|
|
676
|
+
|
|
677
|
+
releasesCommand
|
|
678
|
+
.command('deploy <name> <tag>')
|
|
679
|
+
.description('Deploy a release to AppLaunchpad')
|
|
680
|
+
.action(withAuth({ spinnerText: 'Deploying release...' }, async (ctx, name: string, tag: string) => {
|
|
681
|
+
const client = createDevboxClient()
|
|
682
|
+
const { error, response } = await client.POST('/devbox/{name}/releases/{tag}/deploy', {
|
|
683
|
+
headers: ctx.auth,
|
|
684
|
+
params: {
|
|
685
|
+
path: { name, tag }
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
690
|
+
ctx.spinner.succeed(`Release "${tag}" deployed for "${name}"`)
|
|
691
|
+
}))
|
|
692
|
+
|
|
693
|
+
devboxCmd
|
|
694
|
+
.command('deployments <name>')
|
|
695
|
+
.description('List deployed applications from a devbox')
|
|
696
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
697
|
+
.action(withAuth({ spinnerText: 'Loading deployments...' }, async (ctx, name: string, options: { output: string }) => {
|
|
698
|
+
const client = createDevboxClient()
|
|
699
|
+
const { data, error, response } = await client.GET('/devbox/{name}/deployments', {
|
|
700
|
+
headers: ctx.auth,
|
|
701
|
+
params: {
|
|
702
|
+
path: { name }
|
|
703
|
+
}
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
707
|
+
ctx.spinner.stop()
|
|
708
|
+
|
|
709
|
+
if (options.output === 'json') {
|
|
710
|
+
outputJson(data)
|
|
711
|
+
return
|
|
220
712
|
}
|
|
221
|
-
|
|
713
|
+
printDeployments(data)
|
|
714
|
+
}))
|
|
222
715
|
|
|
223
716
|
return devboxCmd
|
|
224
717
|
}
|