xuanwu-cli 2.2.0 → 2.3.2
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/.env.test.example +14 -0
- package/__tests__/E2E_TEST_REPORT.md +206 -0
- package/__tests__/README.md +322 -0
- package/__tests__/TEST_SUMMARY.md +215 -0
- package/__tests__/global-setup.ts +13 -0
- package/__tests__/global-teardown.ts +3 -0
- package/__tests__/helpers/test-utils.ts +70 -0
- package/__tests__/integration/app.integration.test.ts +363 -0
- package/__tests__/integration/auth.integration.test.ts +243 -0
- package/__tests__/integration/build.integration.test.ts +215 -0
- package/__tests__/integration/e2e.test.ts +267 -0
- package/__tests__/integration/service.integration.test.ts +267 -0
- package/__tests__/integration/webhook.integration.test.ts +246 -0
- package/__tests__/run-e2e.js +360 -0
- package/__tests__/setup.ts +9 -0
- package/bin/xuanwu +0 -0
- package/dist/api/client.d.ts +23 -4
- package/dist/api/client.js +104 -29
- package/dist/commands/auth/login.js +5 -4
- package/dist/commands/deploy.js +25 -49
- package/dist/commands/env.js +31 -48
- package/dist/commands/project.d.ts +5 -0
- package/dist/commands/project.js +134 -0
- package/dist/config/types.d.ts +1 -0
- package/dist/index.js +2 -0
- package/jest.config.js +18 -0
- package/package.json +10 -2
- package/src/api/client.ts +128 -33
- package/src/commands/auth/login.ts +6 -4
- package/src/commands/deploy.ts +32 -49
- package/src/commands/env.ts +35 -52
- package/src/commands/project.ts +153 -0
- package/src/config/types.ts +1 -0
- package/src/index.ts +2 -0
- package/test/cli-integration.sh +245 -0
- package/test/integration.js +3 -3
- package/test/integration.sh +252 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xuanwu-cli",
|
|
3
|
-
"version": "2.2
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "玄武工厂平台 CLI 工具",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
|
-
"start": "node dist/index.js"
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"test:watch": "jest --watch",
|
|
16
|
+
"test:coverage": "jest --coverage",
|
|
17
|
+
"test:integration": "jest --testPathPattern=integration",
|
|
18
|
+
"test:e2e": "jest --testPathPattern=e2e"
|
|
14
19
|
},
|
|
15
20
|
"keywords": [
|
|
16
21
|
"cli",
|
|
@@ -27,7 +32,10 @@
|
|
|
27
32
|
},
|
|
28
33
|
"devDependencies": {
|
|
29
34
|
"@types/inquirer": "^9.0.7",
|
|
35
|
+
"@types/jest": "^29.5.11",
|
|
30
36
|
"@types/node": "^20.11.0",
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"ts-jest": "^29.1.1",
|
|
31
39
|
"typescript": "^5.3.3"
|
|
32
40
|
}
|
|
33
41
|
}
|
package/src/api/client.ts
CHANGED
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
|
6
6
|
import { Connection, CLIResult, ServiceInfo, NamespaceInfo, DeployOptions, Application, Build, CreateApplicationDto, UpdateApplicationDto, TriggerBuildOptions } from '../config/types'
|
|
7
7
|
import { OutputFormatter } from '../output/formatter'
|
|
8
|
+
import { SessionManager } from '../lib/session'
|
|
8
9
|
|
|
9
10
|
export class APIClient {
|
|
10
11
|
private client: AxiosInstance
|
|
12
|
+
private connection: Connection
|
|
13
|
+
private sessionManager: SessionManager
|
|
11
14
|
|
|
12
15
|
constructor(connection: Connection) {
|
|
16
|
+
this.connection = connection
|
|
17
|
+
this.sessionManager = new SessionManager()
|
|
18
|
+
|
|
13
19
|
this.client = axios.create({
|
|
14
20
|
baseURL: connection.endpoint,
|
|
15
21
|
headers: {
|
|
@@ -22,6 +28,7 @@ export class APIClient {
|
|
|
22
28
|
|
|
23
29
|
private async request<T>(method: string, url: string, data?: any, config?: AxiosRequestConfig): Promise<CLIResult<T>> {
|
|
24
30
|
const startTime = Date.now()
|
|
31
|
+
|
|
25
32
|
try {
|
|
26
33
|
const response = await this.client.request({
|
|
27
34
|
method,
|
|
@@ -39,8 +46,25 @@ export class APIClient {
|
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
} catch (error: any) {
|
|
49
|
+
const status = error.response?.status
|
|
42
50
|
const duration = Date.now() - startTime
|
|
43
|
-
|
|
51
|
+
|
|
52
|
+
if (status === 401) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: {
|
|
56
|
+
code: '401',
|
|
57
|
+
message: '登录已过期,请运行 "xw login" 重新登录',
|
|
58
|
+
details: { needLogin: true }
|
|
59
|
+
},
|
|
60
|
+
meta: {
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
duration
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const message = error.response?.data?.error || error.response?.data?.message || error.message
|
|
44
68
|
|
|
45
69
|
return {
|
|
46
70
|
success: false,
|
|
@@ -82,31 +106,6 @@ export class APIClient {
|
|
|
82
106
|
return this.request('GET', `/api/deploy-spaces?identifier=${identifier}`)
|
|
83
107
|
}
|
|
84
108
|
|
|
85
|
-
async getProjects(): Promise<CLIResult<any[]>> {
|
|
86
|
-
return this.request<any[]>('GET', '/api/projects')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async createProject(name: string, description?: string): Promise<CLIResult<any>> {
|
|
90
|
-
return this.request('POST', '/api/projects', {
|
|
91
|
-
name,
|
|
92
|
-
description: description || ''
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async deleteProject(projectId: string): Promise<CLIResult<void>> {
|
|
97
|
-
return this.request('DELETE', `/api/projects/${projectId}`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async createNamespaceWithProject(name: string, projectId: string, environment: string = 'development'): Promise<CLIResult<any>> {
|
|
101
|
-
return this.request('POST', '/api/deploy-spaces', {
|
|
102
|
-
name,
|
|
103
|
-
identifier: name,
|
|
104
|
-
namespace: name,
|
|
105
|
-
environment,
|
|
106
|
-
project_id: projectId
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
109
|
// ===================
|
|
111
110
|
// Service
|
|
112
111
|
// ===================
|
|
@@ -128,16 +127,26 @@ export class APIClient {
|
|
|
128
127
|
// ===================
|
|
129
128
|
|
|
130
129
|
async deploy(options: DeployOptions): Promise<CLIResult<any>> {
|
|
131
|
-
const { namespace, serviceName, type, ...rest } = options
|
|
130
|
+
const { namespace, serviceName, type, projectCode, ...rest } = options
|
|
132
131
|
|
|
133
132
|
// 获取 namespace 对应的 project_id
|
|
134
133
|
let projectId: string | undefined
|
|
135
134
|
|
|
135
|
+
// 如果提供了 projectCode,直接通过 projectCode 获取项目
|
|
136
|
+
if (projectCode) {
|
|
137
|
+
const projectResult = await this.getProject(projectCode)
|
|
138
|
+
if (projectResult.success && projectResult.data) {
|
|
139
|
+
projectId = projectResult.data.id
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
136
143
|
// 尝试通过 identifier 查询
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
if (!projectId) {
|
|
145
|
+
const envResult = await this.getNamespaceInfo(namespace)
|
|
146
|
+
if (envResult.success && envResult.data) {
|
|
147
|
+
const envData = Array.isArray(envResult.data) ? envResult.data[0] : envResult.data
|
|
148
|
+
projectId = envData.project?.id || envData.projectId
|
|
149
|
+
}
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
// 如果没找到,尝试从列表中查找
|
|
@@ -153,7 +162,7 @@ export class APIClient {
|
|
|
153
162
|
|
|
154
163
|
// 如果还是没有,使用第一个项目的 ID
|
|
155
164
|
if (!projectId) {
|
|
156
|
-
const projectsResult = await this.
|
|
165
|
+
const projectsResult = await this.listProjects()
|
|
157
166
|
if (projectsResult.success && projectsResult.data && projectsResult.data.length > 0) {
|
|
158
167
|
projectId = projectsResult.data[0].id
|
|
159
168
|
}
|
|
@@ -398,7 +407,7 @@ export class APIClient {
|
|
|
398
407
|
}
|
|
399
408
|
|
|
400
409
|
async createApplication(dto: CreateApplicationDto): Promise<CLIResult<Application>> {
|
|
401
|
-
return this.request<Application>('POST', '/api/
|
|
410
|
+
return this.request<Application>('POST', '/api/cli/apps', dto)
|
|
402
411
|
}
|
|
403
412
|
|
|
404
413
|
async updateApplication(code: string, dto: UpdateApplicationDto): Promise<CLIResult<Application>> {
|
|
@@ -417,6 +426,92 @@ export class APIClient {
|
|
|
417
426
|
return this.request<Build[]>('GET', `/api/cli/apps/${code}/builds`)
|
|
418
427
|
}
|
|
419
428
|
|
|
429
|
+
// ===================
|
|
430
|
+
// Project (CLI API - using code)
|
|
431
|
+
// ===================
|
|
432
|
+
|
|
433
|
+
async listProjects(options?: { name?: string; code?: string }): Promise<CLIResult<any[]>> {
|
|
434
|
+
let url = '/api/cli/projects'
|
|
435
|
+
const params: string[] = []
|
|
436
|
+
if (options?.name) params.push(`name=${options.name}`)
|
|
437
|
+
if (options?.code) params.push(`code=${options.code}`)
|
|
438
|
+
if (params.length > 0) url += '?' + params.join('&')
|
|
439
|
+
return this.request<any[]>('GET', url)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async getProject(code: string): Promise<CLIResult<any>> {
|
|
443
|
+
return this.request<any>('GET', `/api/cli/projects/${code}`)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async createProject(name: string, code: string, description?: string): Promise<CLIResult<any>> {
|
|
447
|
+
return this.request<any>('POST', '/api/cli/projects', {
|
|
448
|
+
name,
|
|
449
|
+
code,
|
|
450
|
+
description: description || ''
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async deleteProject(code: string): Promise<CLIResult<void>> {
|
|
455
|
+
return this.request<void>('DELETE', `/api/cli/projects/${code}`)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ===================
|
|
459
|
+
// Environment (CLI API - using namespace)
|
|
460
|
+
// ===================
|
|
461
|
+
|
|
462
|
+
async listEnvironments(options?: { project?: string; name?: string }): Promise<CLIResult<any[]>> {
|
|
463
|
+
let url = '/api/cli/envs'
|
|
464
|
+
const params: string[] = []
|
|
465
|
+
if (options?.project) params.push(`project=${options.project}`)
|
|
466
|
+
if (options?.name) params.push(`name=${options.name}`)
|
|
467
|
+
if (params.length > 0) url += '?' + params.join('&')
|
|
468
|
+
return this.request<any[]>('GET', url)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async getEnvironment(namespace: string): Promise<CLIResult<any>> {
|
|
472
|
+
return this.request<any>('GET', `/api/cli/envs/${namespace}`)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async createEnvironment(name: string, namespace: string, projectCode?: string): Promise<CLIResult<any>> {
|
|
476
|
+
return this.request<any>('POST', '/api/cli/envs', {
|
|
477
|
+
name,
|
|
478
|
+
namespace,
|
|
479
|
+
projectCode
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async deleteEnvironment(namespace: string): Promise<CLIResult<void>> {
|
|
484
|
+
return this.request<void>('DELETE', `/api/cli/envs/${namespace}`)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ===================
|
|
488
|
+
// Deployment (CLI API)
|
|
489
|
+
// ===================
|
|
490
|
+
|
|
491
|
+
async deployService(
|
|
492
|
+
namespace: string,
|
|
493
|
+
name: string,
|
|
494
|
+
image: string,
|
|
495
|
+
options?: {
|
|
496
|
+
projectCode?: string
|
|
497
|
+
replicas?: number
|
|
498
|
+
port?: number
|
|
499
|
+
env?: Record<string, string>
|
|
500
|
+
}
|
|
501
|
+
): Promise<CLIResult<any>> {
|
|
502
|
+
return this.request<any>('POST', `/api/cli/services/${namespace}/${name}/deploy`, {
|
|
503
|
+
image,
|
|
504
|
+
projectCode: options?.projectCode,
|
|
505
|
+
replicas: options?.replicas,
|
|
506
|
+
port: options?.port,
|
|
507
|
+
env: options?.env
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async listServiceDeployments(namespace: string, name: string): Promise<CLIResult<any>> {
|
|
512
|
+
return this.request<any>('GET', `/api/cli/services/${namespace}/${name}/deployments`)
|
|
513
|
+
}
|
|
514
|
+
|
|
420
515
|
// ===================
|
|
421
516
|
// Service (CLI API - using ns/name)
|
|
422
517
|
// ===================
|
|
@@ -6,7 +6,7 @@ import { OutputFormatter } from '../../output/formatter'
|
|
|
6
6
|
export function makeLoginCommand(): Command {
|
|
7
7
|
const cmd = new Command('login')
|
|
8
8
|
.description('Login to xuanwu factory')
|
|
9
|
-
.option('-u, --api-url <url>', 'API server URL', '
|
|
9
|
+
.option('-u, --api-url <url>', 'API server URL', 'http://xw.xuanwu-prod.dev.aimstek.cn')
|
|
10
10
|
.option('-e, --email <email>', 'Email address (for non-interactive login)')
|
|
11
11
|
.option('-p, --password <password>', 'Password (for non-interactive login)')
|
|
12
12
|
.option('--expires-in <duration>', 'Token expiration (30d, 90d, never)', '30d')
|
|
@@ -44,16 +44,18 @@ async function loginWithBrowser(sessionManager: SessionManager, apiUrl: string):
|
|
|
44
44
|
|
|
45
45
|
const { sessionId, loginUrl, code } = await deviceAuthRes.json() as { sessionId: string; loginUrl: string; code: string }
|
|
46
46
|
|
|
47
|
+
const fullLoginUrl = loginUrl.startsWith('http') ? loginUrl : `${finalApiUrl}${loginUrl}`
|
|
48
|
+
|
|
47
49
|
OutputFormatter.info('正在生成设备授权码...')
|
|
48
50
|
console.log(`授权码: ${code}`)
|
|
49
|
-
console.log(`已打开浏览器: ${
|
|
51
|
+
console.log(`已打开浏览器: ${fullLoginUrl}`)
|
|
50
52
|
OutputFormatter.info('请在浏览器中完成授权')
|
|
51
53
|
OutputFormatter.info('等待授权...')
|
|
52
54
|
|
|
53
55
|
try {
|
|
54
|
-
await open(
|
|
56
|
+
await open(fullLoginUrl)
|
|
55
57
|
} catch (error) {
|
|
56
|
-
console.log(`如果浏览器未打开,请手动访问: ${
|
|
58
|
+
console.log(`如果浏览器未打开,请手动访问: ${fullLoginUrl}`)
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const maxAttempts = 60
|
package/src/commands/deploy.ts
CHANGED
|
@@ -6,27 +6,22 @@ import { Command } from 'commander'
|
|
|
6
6
|
import { configStore } from '../config/store'
|
|
7
7
|
import { createClient } from '../api/client'
|
|
8
8
|
import { OutputFormatter } from '../output/formatter'
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
function parseNamespaceName(input: string): { namespace: string; name: string } | null {
|
|
11
|
+
const parts = input.split('/')
|
|
12
|
+
if (parts.length !== 2) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
return { namespace: parts[0], name: parts[1] }
|
|
16
|
+
}
|
|
10
17
|
|
|
11
18
|
export function makeDeployCommand(): Command {
|
|
12
19
|
const cmd = new Command('deploy')
|
|
13
|
-
.description('Deploy services to environment')
|
|
14
|
-
.argument('<
|
|
15
|
-
.
|
|
16
|
-
.option('-
|
|
17
|
-
.option('--
|
|
18
|
-
.option('--git-branch <branch>', 'Git branch', 'main')
|
|
19
|
-
.option('--build-type <type>', 'Build type (template|dockerfile)', 'template')
|
|
20
|
-
.option('--language <lang>', 'Language (java-springboot|nodejs|python|golang)')
|
|
21
|
-
.option('--dockerfile-path <path>', 'Dockerfile path')
|
|
22
|
-
.option('--db-type <type>', 'Database type (mysql|redis|postgres|elasticsearch)')
|
|
23
|
-
.option('--db-version <version>', 'Database version')
|
|
24
|
-
.option('--root-password <password>', 'Root password')
|
|
25
|
-
.option('--password <password>', 'Password')
|
|
26
|
-
.option('--user <user>', 'Username')
|
|
27
|
-
.option('--database <name>', 'Database name')
|
|
28
|
-
.option('--image <image>', 'Container image')
|
|
29
|
-
.option('-p, --port <port>', 'Container port')
|
|
20
|
+
.description('Deploy services to environment (auto-creates service if not exists)')
|
|
21
|
+
.argument('<ns-name>', 'Target namespace and service name (format: namespace/service-name)')
|
|
22
|
+
.requiredOption('-i, --image <image>', 'Container image (required)')
|
|
23
|
+
.option('-p, --project <code>', 'Project code')
|
|
24
|
+
.option('--port <port>', 'Container port', '80')
|
|
30
25
|
.option('-r, --replicas <num>', 'Number of replicas', '1')
|
|
31
26
|
.option('--cpu <value>', 'CPU limit')
|
|
32
27
|
.option('--memory <value>', 'Memory limit')
|
|
@@ -36,47 +31,35 @@ export function makeDeployCommand(): Command {
|
|
|
36
31
|
memo[key] = value
|
|
37
32
|
return memo
|
|
38
33
|
}, {})
|
|
39
|
-
.action(async (
|
|
34
|
+
.action(async (nsName, options) => {
|
|
40
35
|
const conn = configStore.getDefaultConnection()
|
|
41
36
|
if (!conn) {
|
|
42
37
|
OutputFormatter.error('No connection configured')
|
|
43
38
|
return
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
serviceName,
|
|
51
|
-
type,
|
|
52
|
-
port: options.port ? parseInt(options.port) : undefined,
|
|
53
|
-
replicas: options.replicas ? parseInt(options.replicas) : undefined,
|
|
54
|
-
cpu: options.cpu,
|
|
55
|
-
memory: options.memory,
|
|
56
|
-
domain: options.domain,
|
|
57
|
-
envVars: options.env
|
|
41
|
+
const parsed = parseNamespaceName(nsName)
|
|
42
|
+
if (!parsed) {
|
|
43
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
44
|
+
return
|
|
58
45
|
}
|
|
59
46
|
|
|
60
|
-
|
|
61
|
-
if (type === 'application') {
|
|
62
|
-
deployOptions.git = options.git
|
|
63
|
-
deployOptions.gitBranch = options.gitBranch
|
|
64
|
-
deployOptions.buildType = options.buildType
|
|
65
|
-
deployOptions.language = options.language
|
|
66
|
-
deployOptions.dockerfilePath = options.dockerfilePath
|
|
67
|
-
} else if (type === 'database') {
|
|
68
|
-
deployOptions.dbType = options.dbType
|
|
69
|
-
deployOptions.dbVersion = options.dbVersion
|
|
70
|
-
deployOptions.rootPassword = options.rootPassword
|
|
71
|
-
deployOptions.password = options.password
|
|
72
|
-
deployOptions.user = options.user
|
|
73
|
-
deployOptions.database = options.database
|
|
74
|
-
} else if (type === 'image') {
|
|
75
|
-
deployOptions.image = options.image
|
|
76
|
-
}
|
|
47
|
+
const { namespace, name: serviceName } = parsed
|
|
77
48
|
|
|
78
49
|
const client = createClient(conn)
|
|
79
|
-
|
|
50
|
+
|
|
51
|
+
// 使用 CLI API 进行部署
|
|
52
|
+
const result = await client.deployService(
|
|
53
|
+
namespace,
|
|
54
|
+
serviceName,
|
|
55
|
+
options.image,
|
|
56
|
+
{
|
|
57
|
+
projectCode: options.project,
|
|
58
|
+
replicas: options.replicas ? parseInt(options.replicas) : 1,
|
|
59
|
+
port: options.port ? parseInt(options.port) : 80,
|
|
60
|
+
env: options.env
|
|
61
|
+
}
|
|
62
|
+
)
|
|
80
63
|
|
|
81
64
|
if (!result.success) {
|
|
82
65
|
OutputFormatter.error(result.error!.message)
|
package/src/commands/env.ts
CHANGED
|
@@ -13,16 +13,18 @@ export function makeEnvCommand(): Command {
|
|
|
13
13
|
|
|
14
14
|
cmd
|
|
15
15
|
.command('ls')
|
|
16
|
+
.alias('list')
|
|
16
17
|
.description('List accessible environments')
|
|
17
|
-
.
|
|
18
|
+
.option('-p, --project <project-code>', 'Filter by project code')
|
|
19
|
+
.action(async (options) => {
|
|
18
20
|
const conn = configStore.getDefaultConnection()
|
|
19
21
|
if (!conn) {
|
|
20
|
-
OutputFormatter.error('No connection configured. Run:
|
|
22
|
+
OutputFormatter.error('No connection configured. Run: xw connect add <name> -e <endpoint> -t <token>')
|
|
21
23
|
return
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const client = createClient(conn)
|
|
25
|
-
const result = await client.
|
|
27
|
+
const result = await client.listEnvironments({ project: options.project })
|
|
26
28
|
|
|
27
29
|
if (!result.success) {
|
|
28
30
|
OutputFormatter.error(result.error!.message)
|
|
@@ -36,22 +38,20 @@ export function makeEnvCommand(): Command {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
OutputFormatter.table(
|
|
39
|
-
['Name', '
|
|
41
|
+
['Name', 'Namespace', 'Project', 'Services'],
|
|
40
42
|
spaces.map((s: any) => [
|
|
41
|
-
s.name
|
|
42
|
-
s.
|
|
43
|
-
s.
|
|
44
|
-
s.
|
|
43
|
+
s.name,
|
|
44
|
+
s.namespace,
|
|
45
|
+
s.project?.name || '-',
|
|
46
|
+
s._count?.services || 0
|
|
45
47
|
])
|
|
46
48
|
)
|
|
47
49
|
})
|
|
48
50
|
|
|
49
51
|
cmd
|
|
50
|
-
.command('
|
|
51
|
-
.description('
|
|
52
|
-
.
|
|
53
|
-
.option('-e, --environment <env>', 'Environment type (development|staging|production)', 'development')
|
|
54
|
-
.action(async (namespace, options) => {
|
|
52
|
+
.command('get <namespace>')
|
|
53
|
+
.description('Get environment details')
|
|
54
|
+
.action(async (namespace) => {
|
|
55
55
|
const conn = configStore.getDefaultConnection()
|
|
56
56
|
if (!conn) {
|
|
57
57
|
OutputFormatter.error('No connection configured')
|
|
@@ -59,34 +59,26 @@ export function makeEnvCommand(): Command {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const client = createClient(conn)
|
|
62
|
-
|
|
63
|
-
let projectId = options.projectId
|
|
64
|
-
|
|
65
|
-
// 如果没有指定 project-id,尝试获取第一个项目
|
|
66
|
-
if (!projectId) {
|
|
67
|
-
const projectsResult = await client.getProjects()
|
|
68
|
-
if (!projectsResult.success || !projectsResult.data || projectsResult.data.length === 0) {
|
|
69
|
-
OutputFormatter.error('No projects found. Please specify --project-id')
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
projectId = projectsResult.data[0].id
|
|
73
|
-
OutputFormatter.info(`Using project: ${projectsResult.data[0].name}`)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const result = await client.createNamespaceWithProject(namespace, projectId, options.environment)
|
|
62
|
+
const result = await client.getEnvironment(namespace)
|
|
77
63
|
|
|
78
64
|
if (!result.success) {
|
|
79
65
|
OutputFormatter.error(result.error!.message)
|
|
80
66
|
return
|
|
81
67
|
}
|
|
82
68
|
|
|
83
|
-
|
|
69
|
+
const env = result.data
|
|
70
|
+
OutputFormatter.info(`Name: ${env.name}`)
|
|
71
|
+
OutputFormatter.info(`Namespace: ${env.namespace}`)
|
|
72
|
+
OutputFormatter.info(`Project: ${env.project?.name || '-'}`)
|
|
73
|
+
OutputFormatter.info(`Services: ${env.services?.length || 0}`)
|
|
84
74
|
})
|
|
85
75
|
|
|
86
76
|
cmd
|
|
87
|
-
.command('
|
|
88
|
-
.description('
|
|
89
|
-
.
|
|
77
|
+
.command('create <namespace>')
|
|
78
|
+
.description('Create a new environment (K8s namespace)')
|
|
79
|
+
.option('-n, --name <name>', 'Environment name')
|
|
80
|
+
.option('-p, --project <project-code>', 'Project code')
|
|
81
|
+
.action(async (namespace, options) => {
|
|
90
82
|
const conn = configStore.getDefaultConnection()
|
|
91
83
|
if (!conn) {
|
|
92
84
|
OutputFormatter.error('No connection configured')
|
|
@@ -95,28 +87,24 @@ export function makeEnvCommand(): Command {
|
|
|
95
87
|
|
|
96
88
|
const client = createClient(conn)
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const envData = Array.isArray(infoResult.data) ? infoResult.data[0] : infoResult.data
|
|
106
|
-
const id = envData.id
|
|
107
|
-
const result = await client.deleteNamespace(id)
|
|
90
|
+
const result = await client.createEnvironment(
|
|
91
|
+
options.name || namespace,
|
|
92
|
+
namespace,
|
|
93
|
+
options.project
|
|
94
|
+
)
|
|
108
95
|
|
|
109
96
|
if (!result.success) {
|
|
110
97
|
OutputFormatter.error(result.error!.message)
|
|
111
98
|
return
|
|
112
99
|
}
|
|
113
100
|
|
|
114
|
-
OutputFormatter.success(`Environment "${namespace}"
|
|
101
|
+
OutputFormatter.success(`Environment "${namespace}" created`)
|
|
115
102
|
})
|
|
116
103
|
|
|
117
104
|
cmd
|
|
118
|
-
.command('
|
|
119
|
-
.
|
|
105
|
+
.command('rm <namespace>')
|
|
106
|
+
.alias('delete')
|
|
107
|
+
.description('Delete an environment')
|
|
120
108
|
.action(async (namespace) => {
|
|
121
109
|
const conn = configStore.getDefaultConnection()
|
|
122
110
|
if (!conn) {
|
|
@@ -125,19 +113,14 @@ export function makeEnvCommand(): Command {
|
|
|
125
113
|
}
|
|
126
114
|
|
|
127
115
|
const client = createClient(conn)
|
|
128
|
-
const result = await client.
|
|
116
|
+
const result = await client.deleteEnvironment(namespace)
|
|
129
117
|
|
|
130
118
|
if (!result.success) {
|
|
131
119
|
OutputFormatter.error(result.error!.message)
|
|
132
120
|
return
|
|
133
121
|
}
|
|
134
122
|
|
|
135
|
-
|
|
136
|
-
OutputFormatter.info(`Name: ${info.name}`)
|
|
137
|
-
OutputFormatter.info(`Identifier: ${info.identifier}`)
|
|
138
|
-
OutputFormatter.info(`Namespace: ${info.namespace}`)
|
|
139
|
-
OutputFormatter.info(`Environment: ${info.environment}`)
|
|
140
|
-
OutputFormatter.info(`Status: ${info.status}`)
|
|
123
|
+
OutputFormatter.success(`Environment "${namespace}" deleted`)
|
|
141
124
|
})
|
|
142
125
|
|
|
143
126
|
return cmd
|