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,224 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { getCurrentContext } from '../../lib/config.ts'
|
|
3
|
+
import { success, spinner, outputTable, formatOutput, info } from '../../lib/output.ts'
|
|
4
|
+
import { handleError, AuthError } from '../../lib/errors.ts'
|
|
5
|
+
|
|
6
|
+
export function createDevboxCommand (): Command {
|
|
7
|
+
const devboxCmd = new Command('devbox')
|
|
8
|
+
.alias('dev')
|
|
9
|
+
.description('Manage devbox instances')
|
|
10
|
+
|
|
11
|
+
// devbox create
|
|
12
|
+
devboxCmd
|
|
13
|
+
.command('create')
|
|
14
|
+
.description('Create a new devbox')
|
|
15
|
+
.argument('[path]', 'Project path', '.')
|
|
16
|
+
.option('--name <name>', 'Devbox name')
|
|
17
|
+
.option('--template <template>', 'Template to use', 'default')
|
|
18
|
+
.option('--cpu <cpu>', 'CPU configuration', '1c')
|
|
19
|
+
.option('--memory <memory>', 'Memory configuration', '2g')
|
|
20
|
+
.option('--port <port>', 'Port number')
|
|
21
|
+
.option('--config <config>', 'Config file path')
|
|
22
|
+
.action(async (path, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const context = getCurrentContext()
|
|
25
|
+
if (!context) {
|
|
26
|
+
throw new AuthError()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const spin = spinner('Creating devbox...')
|
|
30
|
+
|
|
31
|
+
// TODO: 实现创建 devbox
|
|
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)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// devbox list
|
|
55
|
+
devboxCmd
|
|
56
|
+
.command('list')
|
|
57
|
+
.description('List all devboxes')
|
|
58
|
+
.option('-o, --output <format>', 'Output format: json, yaml, table', 'table')
|
|
59
|
+
.option('--selector <selector>', 'Label selector')
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
try {
|
|
62
|
+
const context = getCurrentContext()
|
|
63
|
+
if (!context) {
|
|
64
|
+
throw new AuthError()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 示例数据
|
|
68
|
+
const data = [
|
|
69
|
+
['NAME', 'STATUS', 'CPU', 'MEMORY', 'CREATED'],
|
|
70
|
+
['my-devbox', 'Running', '2c', '4g', '2h ago'],
|
|
71
|
+
['test-box', 'Stopped', '1c', '2g', '1d ago']
|
|
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)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// devbox get
|
|
87
|
+
devboxCmd
|
|
88
|
+
.command('get')
|
|
89
|
+
.description('Get devbox details')
|
|
90
|
+
.argument('<name>', 'Devbox name')
|
|
91
|
+
.action(async (name) => {
|
|
92
|
+
try {
|
|
93
|
+
const context = getCurrentContext()
|
|
94
|
+
if (!context) {
|
|
95
|
+
throw new AuthError()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const data = [
|
|
99
|
+
['Field', 'Value'],
|
|
100
|
+
['Name', name],
|
|
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)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// devbox start/stop/restart/delete
|
|
114
|
+
const actions = ['start', 'stop', 'restart', 'delete']
|
|
115
|
+
actions.forEach(action => {
|
|
116
|
+
const cmd = devboxCmd
|
|
117
|
+
.command(action)
|
|
118
|
+
.description(`${action.charAt(0).toUpperCase() + action.slice(1)} a devbox`)
|
|
119
|
+
.argument('<name>', 'Devbox name')
|
|
120
|
+
|
|
121
|
+
if (action === 'delete') {
|
|
122
|
+
cmd.option('-f, --force', 'Force delete without confirmation')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
cmd.action(async (name, options) => {
|
|
126
|
+
try {
|
|
127
|
+
const context = getCurrentContext()
|
|
128
|
+
if (!context) {
|
|
129
|
+
throw new AuthError()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const spin = spinner(`${action}ing devbox...`)
|
|
133
|
+
|
|
134
|
+
// TODO: 实现对应操作
|
|
135
|
+
spin.succeed(`Devbox ${action}ed successfully`)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
handleError(error)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// devbox connect
|
|
143
|
+
devboxCmd
|
|
144
|
+
.command('connect')
|
|
145
|
+
.description('Connect to a devbox')
|
|
146
|
+
.argument('<name>', 'Devbox name')
|
|
147
|
+
.option('--ide <ide>', 'IDE type: vscode, cursor', 'vscode')
|
|
148
|
+
.action(async (name, options) => {
|
|
149
|
+
try {
|
|
150
|
+
const context = getCurrentContext()
|
|
151
|
+
if (!context) {
|
|
152
|
+
throw new AuthError()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// TODO: 实现连接 devbox
|
|
156
|
+
// 1. 获取 devbox 信息
|
|
157
|
+
// 2. 根据 IDE 类型打开对应的连接
|
|
158
|
+
|
|
159
|
+
info(`Opening ${name} in ${options.ide}...`)
|
|
160
|
+
success(`Connect URL: ${options.ide}://remote-ssh/devbox-${name}`)
|
|
161
|
+
} catch (error) {
|
|
162
|
+
handleError(error)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// devbox publish
|
|
167
|
+
devboxCmd
|
|
168
|
+
.command('publish')
|
|
169
|
+
.description('Publish a devbox')
|
|
170
|
+
.argument('<name>', 'Devbox name')
|
|
171
|
+
.action(async (name) => {
|
|
172
|
+
try {
|
|
173
|
+
const context = getCurrentContext()
|
|
174
|
+
if (!context) {
|
|
175
|
+
throw new AuthError()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const spin = spinner('Publishing devbox...')
|
|
179
|
+
|
|
180
|
+
// TODO: 实现发布 devbox
|
|
181
|
+
spin.succeed('Devbox published successfully')
|
|
182
|
+
} catch (error) {
|
|
183
|
+
handleError(error)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// devbox template 子命令
|
|
188
|
+
const templateCmd = devboxCmd
|
|
189
|
+
.command('template')
|
|
190
|
+
.description('Manage devbox templates')
|
|
191
|
+
|
|
192
|
+
templateCmd
|
|
193
|
+
.command('build')
|
|
194
|
+
.description('Build a devbox template')
|
|
195
|
+
.option('--name <name>', 'Template name')
|
|
196
|
+
.action(async (options) => {
|
|
197
|
+
try {
|
|
198
|
+
// TODO: 实现构建 template
|
|
199
|
+
info('Building template...')
|
|
200
|
+
} catch (error) {
|
|
201
|
+
handleError(error)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
templateCmd
|
|
206
|
+
.command('list')
|
|
207
|
+
.description('List devbox templates')
|
|
208
|
+
.action(async () => {
|
|
209
|
+
try {
|
|
210
|
+
// TODO: 列出 devbox templates
|
|
211
|
+
const data = [
|
|
212
|
+
['NAME', 'DESCRIPTION', 'VERSION'],
|
|
213
|
+
['nextjs', 'Next.js development environment', '1.0.0'],
|
|
214
|
+
['python', 'Python 3.11 environment', '1.0.0']
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
outputTable(data)
|
|
218
|
+
} catch (error) {
|
|
219
|
+
handleError(error)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
return devboxCmd
|
|
224
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { handleError } from '../../lib/errors.ts'
|
|
3
|
+
|
|
4
|
+
export function createQuotaCommand (): Command {
|
|
5
|
+
const quotaCmd = new Command('quota')
|
|
6
|
+
.description('View resource quotas')
|
|
7
|
+
|
|
8
|
+
// TODO: 实现配额相关命令
|
|
9
|
+
|
|
10
|
+
quotaCmd
|
|
11
|
+
.command('get')
|
|
12
|
+
.description('Get quota information')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
console.log('TODO: Implement quota get')
|
|
16
|
+
} catch (error) {
|
|
17
|
+
handleError(error)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return quotaCmd
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { handleError } from '../../lib/errors.ts'
|
|
3
|
+
|
|
4
|
+
export function createS3Command (): Command {
|
|
5
|
+
const s3Cmd = new Command('s3')
|
|
6
|
+
.description('Manage S3 object storage')
|
|
7
|
+
|
|
8
|
+
// TODO: 实现 S3 相关命令
|
|
9
|
+
// - upload
|
|
10
|
+
// - download
|
|
11
|
+
// - list
|
|
12
|
+
// - delete
|
|
13
|
+
// - sync
|
|
14
|
+
// - bucket (create/list/delete)
|
|
15
|
+
|
|
16
|
+
s3Cmd
|
|
17
|
+
.command('upload')
|
|
18
|
+
.description('Upload files to S3')
|
|
19
|
+
.argument('<source>', 'Source file or directory')
|
|
20
|
+
.argument('[destination]', 'Destination path')
|
|
21
|
+
.option('--bucket <bucket>', 'Bucket name')
|
|
22
|
+
.option('--acl <acl>', 'Access control: private, public-read')
|
|
23
|
+
.action(async (source, destination, options) => {
|
|
24
|
+
try {
|
|
25
|
+
console.log('TODO: Implement s3 upload', { source, destination, options })
|
|
26
|
+
} catch (error) {
|
|
27
|
+
handleError(error)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// 其他命令...
|
|
32
|
+
// 为了简洁,这里只实现 upload 作为示例
|
|
33
|
+
|
|
34
|
+
return s3Cmd
|
|
35
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { createTemplateClient } from '../../lib/api-client.ts'
|
|
5
|
+
import { type ApiErrorBody, mapApiError } from '../../lib/errors.ts'
|
|
6
|
+
import { outputJson, outputTable } from '../../lib/output.ts'
|
|
7
|
+
import { withAuth, withErrorHandling } from '../../lib/with-auth.ts'
|
|
8
|
+
|
|
9
|
+
interface TemplateDeployOptions {
|
|
10
|
+
name?: string
|
|
11
|
+
file?: string
|
|
12
|
+
yaml?: string
|
|
13
|
+
set: string[]
|
|
14
|
+
dryRun?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type TemplateDeployMode = 'catalog' | 'raw'
|
|
18
|
+
|
|
19
|
+
export function parseSetArgs (sets: string[]): Record<string, string> {
|
|
20
|
+
const args: Record<string, string> = {}
|
|
21
|
+
for (const s of sets) {
|
|
22
|
+
const idx = s.indexOf('=')
|
|
23
|
+
if (idx === -1) {
|
|
24
|
+
throw new Error(`Invalid --set format: "${s}". Expected KEY=VALUE`)
|
|
25
|
+
}
|
|
26
|
+
args[s.slice(0, idx)] = s.slice(idx + 1)
|
|
27
|
+
}
|
|
28
|
+
return args
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readStdin (): Promise<string> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
if (process.stdin.isTTY) {
|
|
34
|
+
reject(new Error('No input provided. Use --file, --yaml, or pipe YAML via stdin.'))
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const chunks: Buffer[] = []
|
|
38
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk))
|
|
39
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').trim()))
|
|
40
|
+
process.stdin.on('error', reject)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function resolveYaml (options: { file?: string; yaml?: string }, spinner: { stop: () => void; start: (text?: string) => void }): Promise<string> {
|
|
45
|
+
if (options.file) {
|
|
46
|
+
return readFileSync(options.file, 'utf-8')
|
|
47
|
+
}
|
|
48
|
+
if (options.yaml) {
|
|
49
|
+
return options.yaml
|
|
50
|
+
}
|
|
51
|
+
spinner.stop()
|
|
52
|
+
const content = await readStdin()
|
|
53
|
+
spinner.start('Deploying template...')
|
|
54
|
+
return content
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveTemplateDeployMode (
|
|
58
|
+
template: string | undefined,
|
|
59
|
+
options: TemplateDeployOptions,
|
|
60
|
+
stdinIsTTY: boolean = process.stdin.isTTY
|
|
61
|
+
): TemplateDeployMode {
|
|
62
|
+
const isRaw = !!(options.file || options.yaml || !stdinIsTTY)
|
|
63
|
+
|
|
64
|
+
if (template && isRaw) {
|
|
65
|
+
throw new Error('Cannot specify both a template name and --file/--yaml/stdin. Use one or the other.')
|
|
66
|
+
}
|
|
67
|
+
if (!template && !isRaw) {
|
|
68
|
+
throw new Error('Provide a template name or use --file/--yaml/stdin to supply raw YAML.')
|
|
69
|
+
}
|
|
70
|
+
if (template) {
|
|
71
|
+
if (!options.name) {
|
|
72
|
+
throw new Error('--name is required when deploying from the template catalog.')
|
|
73
|
+
}
|
|
74
|
+
if (options.dryRun) {
|
|
75
|
+
throw new Error('--dry-run is only supported for raw template deploys (--file, --yaml, or stdin).')
|
|
76
|
+
}
|
|
77
|
+
return 'catalog'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return 'raw'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildCatalogTemplateDeployBody (
|
|
84
|
+
template: string,
|
|
85
|
+
options: Pick<TemplateDeployOptions, 'name' | 'set'>
|
|
86
|
+
): { name: string; template: string; args?: Record<string, string> } {
|
|
87
|
+
const body: { name: string; template: string; args?: Record<string, string> } = {
|
|
88
|
+
name: options.name!,
|
|
89
|
+
template
|
|
90
|
+
}
|
|
91
|
+
if (options.set.length > 0) {
|
|
92
|
+
body.args = parseSetArgs(options.set)
|
|
93
|
+
}
|
|
94
|
+
return body
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildRawTemplateDeployBody (
|
|
98
|
+
yaml: string,
|
|
99
|
+
options: Pick<TemplateDeployOptions, 'set' | 'dryRun'>
|
|
100
|
+
): { yaml: string; args?: Record<string, string>; dryRun?: boolean } {
|
|
101
|
+
const body: { yaml: string; args?: Record<string, string>; dryRun?: boolean } = {
|
|
102
|
+
yaml
|
|
103
|
+
}
|
|
104
|
+
if (options.set.length > 0) {
|
|
105
|
+
body.args = parseSetArgs(options.set)
|
|
106
|
+
}
|
|
107
|
+
if (options.dryRun) {
|
|
108
|
+
body.dryRun = true
|
|
109
|
+
}
|
|
110
|
+
return body
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createTemplateCommand (): Command {
|
|
114
|
+
const tplCmd = new Command('template')
|
|
115
|
+
.alias('tpl')
|
|
116
|
+
.description('Manage templates')
|
|
117
|
+
|
|
118
|
+
const deployTemplate = withAuth({
|
|
119
|
+
spinnerText: 'Deploying template...'
|
|
120
|
+
}, async (
|
|
121
|
+
ctx,
|
|
122
|
+
catalogTemplate: string | undefined,
|
|
123
|
+
deployOptions: TemplateDeployOptions,
|
|
124
|
+
deployMode: TemplateDeployMode
|
|
125
|
+
) => {
|
|
126
|
+
const client = createTemplateClient()
|
|
127
|
+
|
|
128
|
+
// ── deploy from catalog ──
|
|
129
|
+
if (deployMode === 'catalog') {
|
|
130
|
+
const body = buildCatalogTemplateDeployBody(catalogTemplate!, deployOptions)
|
|
131
|
+
const { data, error, response } = await client.POST('/templates/instances', {
|
|
132
|
+
headers: ctx.auth,
|
|
133
|
+
body
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
137
|
+
|
|
138
|
+
ctx.spinner.succeed(`Instance "${data.name}" created successfully from catalog template "${catalogTemplate}"`)
|
|
139
|
+
console.log(chalk.dim(` UID: ${data.uid}`))
|
|
140
|
+
console.log(chalk.dim(` Created: ${data.createdAt}`))
|
|
141
|
+
|
|
142
|
+
if (data.resources && data.resources.length > 0) {
|
|
143
|
+
console.log(chalk.dim('\n Resources:'))
|
|
144
|
+
const rows: string[][] = [
|
|
145
|
+
[chalk.bold('Name'), chalk.bold('Type'), chalk.bold('CPU'), chalk.bold('Memory'), chalk.bold('Storage')]
|
|
146
|
+
]
|
|
147
|
+
for (const r of data.resources) {
|
|
148
|
+
rows.push([
|
|
149
|
+
r.name,
|
|
150
|
+
r.resourceType,
|
|
151
|
+
r.quota?.cpu != null ? `${r.quota.cpu} vCPU` : '-',
|
|
152
|
+
r.quota?.memory != null ? `${r.quota.memory} GiB` : '-',
|
|
153
|
+
r.quota?.storage != null ? `${r.quota.storage} GiB` : '-'
|
|
154
|
+
])
|
|
155
|
+
}
|
|
156
|
+
outputTable(rows)
|
|
157
|
+
}
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── deploy from raw YAML ──
|
|
162
|
+
const yamlContent = await resolveYaml(deployOptions, ctx.spinner)
|
|
163
|
+
const body = buildRawTemplateDeployBody(yamlContent, deployOptions)
|
|
164
|
+
|
|
165
|
+
const { data, error, response } = await client.POST('/templates/raw', {
|
|
166
|
+
headers: ctx.auth,
|
|
167
|
+
body
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
171
|
+
|
|
172
|
+
if (deployOptions.dryRun) {
|
|
173
|
+
ctx.spinner.succeed('Raw template validation passed; no resources were created')
|
|
174
|
+
console.log(chalk.dim(` Name: ${data.name}`))
|
|
175
|
+
if (data.resources && data.resources.length > 0) {
|
|
176
|
+
console.log(chalk.dim('\n Resources that would be created:'))
|
|
177
|
+
for (const r of data.resources) {
|
|
178
|
+
console.log(chalk.dim(` - ${r.resourceType}: ${r.name}`))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
ctx.spinner.succeed(`Raw template deployed as "${data.name}"`)
|
|
185
|
+
if ('uid' in data) {
|
|
186
|
+
console.log(chalk.dim(` UID: ${data.uid}`))
|
|
187
|
+
}
|
|
188
|
+
if ('createdAt' in data) {
|
|
189
|
+
console.log(chalk.dim(` Created: ${data.createdAt}`))
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// ── list ─────────────────────────────────────────────────────────
|
|
194
|
+
tplCmd
|
|
195
|
+
.command('list')
|
|
196
|
+
.description('List available templates')
|
|
197
|
+
.option('-c, --category <category>', 'Filter by category')
|
|
198
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
199
|
+
.action(withErrorHandling({ spinnerText: 'Loading templates...' }, async (ctx, options: { category?: string; output: string }) => {
|
|
200
|
+
const client = createTemplateClient()
|
|
201
|
+
const { data, error, response } = await client.GET('/templates', {
|
|
202
|
+
params: { query: {} }
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
206
|
+
|
|
207
|
+
ctx.spinner.stop()
|
|
208
|
+
|
|
209
|
+
let templates = data
|
|
210
|
+
if (options.category) {
|
|
211
|
+
templates = templates.filter((tpl: typeof data[number]) => tpl.category.includes(options.category!))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (options.output === 'json') {
|
|
215
|
+
outputJson(templates)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const rows: string[][] = [
|
|
220
|
+
[chalk.bold('Name'), chalk.bold('Description'), chalk.bold('Category'), chalk.bold('Deploys')]
|
|
221
|
+
]
|
|
222
|
+
for (const tpl of templates) {
|
|
223
|
+
rows.push([
|
|
224
|
+
tpl.name,
|
|
225
|
+
tpl.description.length > 50 ? tpl.description.slice(0, 47) + '...' : tpl.description,
|
|
226
|
+
tpl.category.join(', '),
|
|
227
|
+
String(tpl.deployCount)
|
|
228
|
+
])
|
|
229
|
+
}
|
|
230
|
+
outputTable(rows)
|
|
231
|
+
}))
|
|
232
|
+
|
|
233
|
+
// ── get ──────────────────────────────────────────────────────────
|
|
234
|
+
tplCmd
|
|
235
|
+
.command('get <name>')
|
|
236
|
+
.alias('describe')
|
|
237
|
+
.description('Get template details')
|
|
238
|
+
.option('-o, --output <format>', 'Output format (json|table)', 'table')
|
|
239
|
+
.action(withErrorHandling({ spinnerText: 'Loading template...' }, async (ctx, name: string, options: { output: string }) => {
|
|
240
|
+
const client = createTemplateClient()
|
|
241
|
+
const { data, error, response } = await client.GET('/templates/{name}', {
|
|
242
|
+
params: {
|
|
243
|
+
path: { name }
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
if (error) throw mapApiError(response.status, error as ApiErrorBody)
|
|
248
|
+
|
|
249
|
+
ctx.spinner.stop()
|
|
250
|
+
|
|
251
|
+
if (options.output === 'json') {
|
|
252
|
+
outputJson(data)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log(chalk.bold(`\n ${data.name}\n`))
|
|
257
|
+
console.log(` ${chalk.dim('Description:')} ${data.description}`)
|
|
258
|
+
console.log(` ${chalk.dim('Category:')} ${data.category.join(', ')}`)
|
|
259
|
+
console.log(` ${chalk.dim('Git Repo:')} ${data.gitRepo}`)
|
|
260
|
+
console.log(` ${chalk.dim('Deploys:')} ${data.deployCount}`)
|
|
261
|
+
|
|
262
|
+
if (data.quota) {
|
|
263
|
+
console.log(`\n ${chalk.dim('Resources:')}`)
|
|
264
|
+
console.log(` CPU: ${data.quota.cpu} vCPU`)
|
|
265
|
+
console.log(` Memory: ${data.quota.memory} GiB`)
|
|
266
|
+
console.log(` Storage: ${data.quota.storage} GiB`)
|
|
267
|
+
console.log(` NodePort: ${data.quota.nodeport}`)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const argEntries = Object.entries(data.args)
|
|
271
|
+
if (argEntries.length > 0) {
|
|
272
|
+
console.log(`\n ${chalk.dim('Arguments:')}`)
|
|
273
|
+
const argRows: string[][] = [
|
|
274
|
+
[chalk.bold('Name'), chalk.bold('Type'), chalk.bold('Required'), chalk.bold('Default'), chalk.bold('Description')]
|
|
275
|
+
]
|
|
276
|
+
for (const [key, arg] of argEntries) {
|
|
277
|
+
argRows.push([
|
|
278
|
+
key,
|
|
279
|
+
arg.type,
|
|
280
|
+
arg.required ? chalk.red('yes') : 'no',
|
|
281
|
+
arg.default || chalk.dim('-'),
|
|
282
|
+
arg.description
|
|
283
|
+
])
|
|
284
|
+
}
|
|
285
|
+
outputTable(argRows)
|
|
286
|
+
}
|
|
287
|
+
}))
|
|
288
|
+
|
|
289
|
+
// ── deploy ──────────────────────────────────────────────────────
|
|
290
|
+
tplCmd
|
|
291
|
+
.command('deploy [template]')
|
|
292
|
+
.description('Deploy a template (from catalog or raw YAML)')
|
|
293
|
+
.option('--name <name>', 'Instance name (required when deploying from catalog)')
|
|
294
|
+
.option('--file <path>', 'Path to template YAML file')
|
|
295
|
+
.option('--yaml <yaml>', 'Template YAML string')
|
|
296
|
+
.option('--set <KEY=VALUE...>', 'Set template arguments', (val: string, prev: string[]) => [...prev, val], [] as string[])
|
|
297
|
+
.option('--dry-run', 'Validate raw template YAML without creating resources')
|
|
298
|
+
.addHelpText('after', `
|
|
299
|
+
Examples:
|
|
300
|
+
Catalog:
|
|
301
|
+
sealos template deploy perplexica --name my-app --set OPENAI_API_KEY=xxx
|
|
302
|
+
|
|
303
|
+
Raw:
|
|
304
|
+
sealos template deploy --file ./template.yaml --dry-run
|
|
305
|
+
sealos template deploy --yaml 'apiVersion: app.sealos.io/v1\nkind: Template\n...'
|
|
306
|
+
cat template.yaml | sealos template deploy --dry-run
|
|
307
|
+
`)
|
|
308
|
+
.action(async (template: string | undefined, options: TemplateDeployOptions) => {
|
|
309
|
+
const mode = resolveTemplateDeployMode(template, options)
|
|
310
|
+
await deployTemplate(template, options, mode)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
return tplCmd
|
|
314
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { getCurrentContext } from '../../lib/config.ts'
|
|
3
|
+
import { success, outputTable, info } from '../../lib/output.ts'
|
|
4
|
+
import { handleError, AuthError } from '../../lib/errors.ts'
|
|
5
|
+
|
|
6
|
+
export function createWorkspaceCommand (): Command {
|
|
7
|
+
const workspaceCmd = new Command('workspace')
|
|
8
|
+
.alias('ws')
|
|
9
|
+
.description('Manage workspaces')
|
|
10
|
+
|
|
11
|
+
// workspace switch
|
|
12
|
+
workspaceCmd
|
|
13
|
+
.command('switch')
|
|
14
|
+
.description('Switch to another workspace')
|
|
15
|
+
.argument('<name>', 'Workspace name')
|
|
16
|
+
.action(async (name) => {
|
|
17
|
+
try {
|
|
18
|
+
// TODO: 调用 API 验证 workspace 是否存在
|
|
19
|
+
// const api = createApiClient()
|
|
20
|
+
// await api.get(`/api/v1/workspaces/${name}`)
|
|
21
|
+
|
|
22
|
+
// 更新配置
|
|
23
|
+
const context = getCurrentContext()
|
|
24
|
+
if (context) {
|
|
25
|
+
context.workspace = name
|
|
26
|
+
// TODO: 更新到配置文件
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
success(`Switched to workspace: ${name}`)
|
|
30
|
+
} catch (error) {
|
|
31
|
+
handleError(error)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// workspace list
|
|
36
|
+
workspaceCmd
|
|
37
|
+
.command('list')
|
|
38
|
+
.description('List all workspaces')
|
|
39
|
+
.action(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const context = getCurrentContext()
|
|
42
|
+
if (!context) {
|
|
43
|
+
throw new AuthError()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 示例数据
|
|
47
|
+
const data = [
|
|
48
|
+
['NAME', 'STATUS', 'CURRENT'],
|
|
49
|
+
['default', 'Active', context.workspace === 'default' ? '*' : ''],
|
|
50
|
+
['production', 'Active', context.workspace === 'production' ? '*' : '']
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
outputTable(data)
|
|
54
|
+
info('API integration needed for real data')
|
|
55
|
+
} catch (error) {
|
|
56
|
+
handleError(error)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// workspace current
|
|
61
|
+
workspaceCmd
|
|
62
|
+
.command('current')
|
|
63
|
+
.description('Show current workspace')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
try {
|
|
66
|
+
const context = getCurrentContext()
|
|
67
|
+
if (!context) {
|
|
68
|
+
throw new AuthError()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = [
|
|
72
|
+
['Field', 'Value'],
|
|
73
|
+
['Workspace', context.workspace],
|
|
74
|
+
['Context', context.name]
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
outputTable(data)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
handleError(error)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return workspaceCmd
|
|
84
|
+
}
|