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.
Files changed (37) hide show
  1. package/.env.test.example +14 -0
  2. package/__tests__/E2E_TEST_REPORT.md +206 -0
  3. package/__tests__/README.md +322 -0
  4. package/__tests__/TEST_SUMMARY.md +215 -0
  5. package/__tests__/global-setup.ts +13 -0
  6. package/__tests__/global-teardown.ts +3 -0
  7. package/__tests__/helpers/test-utils.ts +70 -0
  8. package/__tests__/integration/app.integration.test.ts +363 -0
  9. package/__tests__/integration/auth.integration.test.ts +243 -0
  10. package/__tests__/integration/build.integration.test.ts +215 -0
  11. package/__tests__/integration/e2e.test.ts +267 -0
  12. package/__tests__/integration/service.integration.test.ts +267 -0
  13. package/__tests__/integration/webhook.integration.test.ts +246 -0
  14. package/__tests__/run-e2e.js +360 -0
  15. package/__tests__/setup.ts +9 -0
  16. package/bin/xuanwu +0 -0
  17. package/dist/api/client.d.ts +23 -4
  18. package/dist/api/client.js +104 -29
  19. package/dist/commands/auth/login.js +5 -4
  20. package/dist/commands/deploy.js +25 -49
  21. package/dist/commands/env.js +31 -48
  22. package/dist/commands/project.d.ts +5 -0
  23. package/dist/commands/project.js +134 -0
  24. package/dist/config/types.d.ts +1 -0
  25. package/dist/index.js +2 -0
  26. package/jest.config.js +18 -0
  27. package/package.json +10 -2
  28. package/src/api/client.ts +128 -33
  29. package/src/commands/auth/login.ts +6 -4
  30. package/src/commands/deploy.ts +32 -49
  31. package/src/commands/env.ts +35 -52
  32. package/src/commands/project.ts +153 -0
  33. package/src/config/types.ts +1 -0
  34. package/src/index.ts +2 -0
  35. package/test/cli-integration.sh +245 -0
  36. package/test/integration.js +3 -3
  37. 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.0",
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
- const message = error.response?.data?.message || error.message
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
- const envResult = await this.getNamespaceInfo(namespace)
138
- if (envResult.success && envResult.data) {
139
- const envData = Array.isArray(envResult.data) ? envResult.data[0] : envResult.data
140
- projectId = envData.project?.id || envData.projectId
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.getProjects()
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/applications', dto)
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', 'https://i.xuanwu.dev.aimstek.cn')
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(`已打开浏览器: ${loginUrl}`)
51
+ console.log(`已打开浏览器: ${fullLoginUrl}`)
50
52
  OutputFormatter.info('请在浏览器中完成授权')
51
53
  OutputFormatter.info('等待授权...')
52
54
 
53
55
  try {
54
- await open(loginUrl)
56
+ await open(fullLoginUrl)
55
57
  } catch (error) {
56
- console.log(`如果浏览器未打开,请手动访问: ${loginUrl}`)
58
+ console.log(`如果浏览器未打开,请手动访问: ${fullLoginUrl}`)
57
59
  }
58
60
 
59
61
  const maxAttempts = 60
@@ -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
- import { DeployOptions } from '../config/types'
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('<namespace>', 'Target namespace')
15
- .argument('<service-name>', 'Service name')
16
- .option('-t, --type <type>', 'Service type (application|database|image)', 'image')
17
- .option('--git <url>', 'Git repository URL')
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 (namespace, serviceName, options) => {
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 type = options.type as 'application' | 'database' | 'image'
47
-
48
- const deployOptions: DeployOptions = {
49
- namespace,
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
- const result = await client.deploy(deployOptions)
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)
@@ -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
- .action(async () => {
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: xuanwu connect add <name> -e <endpoint> -t <token>')
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.listNamespaces()
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', 'Identifier', 'Environment', 'Status'],
41
+ ['Name', 'Namespace', 'Project', 'Services'],
40
42
  spaces.map((s: any) => [
41
- s.name || s.identifier,
42
- s.identifier,
43
- s.environment,
44
- s.status || 'active'
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('create <namespace>')
51
- .description('Create a new environment (K8s namespace)')
52
- .option('-p, --project-id <id>', 'Project ID')
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
- OutputFormatter.success(`Environment "${namespace}" created`)
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('rm <namespace>')
88
- .description('Delete an environment')
89
- .action(async (namespace) => {
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
- // 先获取 namespace ID
99
- const infoResult = await client.getNamespaceInfo(namespace)
100
- if (!infoResult.success || !infoResult.data) {
101
- OutputFormatter.error(`Environment "${namespace}" not found`)
102
- return
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}" deleted`)
101
+ OutputFormatter.success(`Environment "${namespace}" created`)
115
102
  })
116
103
 
117
104
  cmd
118
- .command('info <namespace>')
119
- .description('Show environment details')
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.getNamespaceInfo(namespace)
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
- const info = result.data
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