xuanwu-cli 1.0.0 → 2.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.
@@ -0,0 +1,34 @@
1
+ import { Command } from 'commander'
2
+ import { SessionManager } from '../../lib/session'
3
+ import { OutputFormatter } from '../../output/formatter'
4
+
5
+ export function makeLogoutCommand(): Command {
6
+ const cmd = new Command('logout')
7
+ .description('Logout from xuanwu factory')
8
+ .action(async () => {
9
+ const sessionManager = new SessionManager()
10
+ const session = await sessionManager.loadSession()
11
+
12
+ if (!session) {
13
+ OutputFormatter.info('未登录')
14
+ return
15
+ }
16
+
17
+ try {
18
+ await fetch(`${session.apiUrl}/api/cli/auth/logout`, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Authorization': `Bearer ${session.token}`,
22
+ 'Content-Type': 'application/json'
23
+ }
24
+ })
25
+ } catch (error) {
26
+ // Ignore API errors
27
+ }
28
+
29
+ await sessionManager.clearSession()
30
+ OutputFormatter.success('登出成功')
31
+ })
32
+
33
+ return cmd
34
+ }
@@ -0,0 +1,85 @@
1
+ import { Command } from 'commander'
2
+ import { SessionManager } from '../../lib/session'
3
+ import { OutputFormatter } from '../../output/formatter'
4
+
5
+ export function makeTokensCommand(): Command {
6
+ const cmd = new Command('tokens')
7
+ .description('List all tokens')
8
+
9
+ const listCmd = new Command('list')
10
+ .description('List all tokens')
11
+ .action(async () => {
12
+ const sessionManager = new SessionManager()
13
+ const session = await sessionManager.loadSession()
14
+
15
+ if (!session) {
16
+ OutputFormatter.info('未登录')
17
+ return
18
+ }
19
+
20
+ const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens`, {
21
+ headers: {
22
+ 'Authorization': `Bearer ${session.token}`
23
+ }
24
+ })
25
+
26
+ if (!res.ok) {
27
+ OutputFormatter.error('Failed to list tokens')
28
+ return
29
+ }
30
+
31
+ const { tokens } = await res.json() as { tokens: Array<{ id: string; name: string; lastUsedAt?: string; expiresAt?: string }> }
32
+
33
+ if (tokens.length === 0) {
34
+ OutputFormatter.info('没有找到tokens')
35
+ return
36
+ }
37
+
38
+ console.log('Your Tokens:')
39
+ tokens.forEach((token, index) => {
40
+ const lastUsed = token.lastUsedAt
41
+ ? `${Math.floor((Date.now() - new Date(token.lastUsedAt).getTime()) / (60 * 60 * 1000))}h ago`
42
+ : 'never'
43
+
44
+ const expires = token.expiresAt
45
+ ? `${Math.max(0, Math.floor((new Date(token.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))} days`
46
+ : 'never'
47
+
48
+ const expired = token.expiresAt && new Date(token.expiresAt) < new Date()
49
+
50
+ console.log(` ${index + 1}. ${token.name} (last used: ${lastUsed}) - ${expired ? 'expired' : expires}`)
51
+ })
52
+ })
53
+
54
+ const revokeCmd = new Command('revoke')
55
+ .description('Revoke a token')
56
+ .argument('<id>', 'Token ID')
57
+ .action(async (id) => {
58
+ const sessionManager = new SessionManager()
59
+ const session = await sessionManager.loadSession()
60
+
61
+ if (!session) {
62
+ OutputFormatter.info('未登录')
63
+ return
64
+ }
65
+
66
+ const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens/${id}`, {
67
+ method: 'DELETE',
68
+ headers: {
69
+ 'Authorization': `Bearer ${session.token}`
70
+ }
71
+ })
72
+
73
+ if (!res.ok) {
74
+ OutputFormatter.error('Failed to revoke token')
75
+ return
76
+ }
77
+
78
+ OutputFormatter.success('Token已撤销')
79
+ })
80
+
81
+ cmd.addCommand(listCmd)
82
+ cmd.addCommand(revokeCmd)
83
+
84
+ return cmd
85
+ }
@@ -0,0 +1,35 @@
1
+ import { Command } from 'commander'
2
+ import { SessionManager } from '../../lib/session'
3
+ import { OutputFormatter } from '../../output/formatter'
4
+
5
+ export function makeWhoamiCommand(): Command {
6
+ const cmd = new Command('whoami')
7
+ .description('Show current login status')
8
+ .action(async () => {
9
+ const sessionManager = new SessionManager()
10
+ const session = await sessionManager.loadSession()
11
+
12
+ if (!session) {
13
+ OutputFormatter.info('未登录')
14
+ console.log('请使用: xw login')
15
+ return
16
+ }
17
+
18
+ if (sessionManager.isExpired(session)) {
19
+ OutputFormatter.info('登录已过期')
20
+ console.log('请使用: xw login')
21
+ await sessionManager.clearSession()
22
+ return
23
+ }
24
+
25
+ const expiresIn = session.expiresAt
26
+ ? Math.max(0, Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
27
+ : 'never'
28
+
29
+ console.log(`Logged in as: ${session.userName} (${session.userEmail})`)
30
+ console.log(`Device: ${session.deviceId}`)
31
+ console.log(`Expires in: ${expiresIn === 'never' ? 'never' : `${expiresIn} days`}`)
32
+ })
33
+
34
+ return cmd
35
+ }
@@ -7,14 +7,28 @@ import { configStore } from '../config/store'
7
7
  import { createClient } from '../api/client'
8
8
  import { OutputFormatter } from '../output/formatter'
9
9
 
10
+ function getAge(timestamp?: string): string {
11
+ if (!timestamp) return '-'
12
+ const created = new Date(timestamp)
13
+ const now = new Date()
14
+ const diff = Math.floor((now.getTime() - created.getTime()) / 1000)
15
+ if (diff < 60) return `${diff}s`
16
+ if (diff < 3600) return `${Math.floor(diff / 60)}m`
17
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h`
18
+ return `${Math.floor(diff / 86400)}d`
19
+ }
20
+
10
21
  export function makeBuildCommand(): Command {
11
22
  const cmd = new Command('build')
12
- .description('Build applications')
23
+ .description('Manage builds')
13
24
 
14
25
  cmd
15
- .command('<namespace> <service-name>')
16
- .description('Build a service')
17
- .action(async (namespace, serviceName) => {
26
+ .command('ls')
27
+ .alias('list')
28
+ .description('List builds')
29
+ .option('-a, --app <code>', 'Filter by application code')
30
+ .option('-s, --status <status>', 'Filter by status (SUCCESS|FAILED|RUNNING|PENDING)')
31
+ .action(async (options) => {
18
32
  const conn = configStore.getDefaultConnection()
19
33
  if (!conn) {
20
34
  OutputFormatter.error('No connection configured')
@@ -22,22 +36,38 @@ export function makeBuildCommand(): Command {
22
36
  }
23
37
 
24
38
  const client = createClient(conn)
25
- OutputFormatter.info('Starting build...')
26
-
27
- const result = await client.build(namespace, serviceName)
39
+ const result = await client.listBuilds({
40
+ appCode: options.app,
41
+ status: options.status
42
+ })
28
43
 
29
44
  if (!result.success) {
30
45
  OutputFormatter.error(result.error!.message)
31
46
  return
32
47
  }
33
48
 
34
- OutputFormatter.success('Build started')
49
+ const builds = result.data || []
50
+ if (builds.length === 0) {
51
+ OutputFormatter.info('No builds found')
52
+ return
53
+ }
54
+
55
+ OutputFormatter.table(
56
+ ['ID', 'Build', 'Status', 'Image Tag', 'Created'],
57
+ builds.map((b: any) => [
58
+ b.id.substring(0, 12),
59
+ `#${b.buildNumber}`,
60
+ b.status,
61
+ b.imageTag || '-',
62
+ getAge(b.createdAt)
63
+ ])
64
+ )
35
65
  })
36
66
 
37
67
  cmd
38
- .command('status <namespace> <service-name>')
39
- .description('Get build status')
40
- .action(async (namespace, serviceName) => {
68
+ .command('get <id>')
69
+ .description('Get build details')
70
+ .action(async (id) => {
41
71
  const conn = configStore.getDefaultConnection()
42
72
  if (!conn) {
43
73
  OutputFormatter.error('No connection configured')
@@ -45,22 +75,52 @@ export function makeBuildCommand(): Command {
45
75
  }
46
76
 
47
77
  const client = createClient(conn)
48
- const result = await client.getBuildStatus(namespace, serviceName)
78
+ const result = await client.getBuild(id)
49
79
 
50
80
  if (!result.success) {
51
81
  OutputFormatter.error(result.error!.message)
52
82
  return
53
83
  }
54
84
 
55
- const builds = result.data || []
56
- if (builds.length === 0) {
57
- OutputFormatter.info('No builds found')
85
+ const build = result.data!
86
+ OutputFormatter.info(`Build: #${build.buildNumber}`)
87
+ OutputFormatter.info(`ID: ${build.id}`)
88
+ OutputFormatter.info(`Status: ${build.status}`)
89
+ OutputFormatter.info(`Image Tag: ${build.imageTag || '-'}`)
90
+ if (build.commitSha) {
91
+ OutputFormatter.info(`Commit: ${build.commitSha.substring(0, 8)}`)
92
+ }
93
+ if (build.commitMessage) {
94
+ OutputFormatter.info(`Message: ${build.commitMessage}`)
95
+ }
96
+ if (build.commitAuthor) {
97
+ OutputFormatter.info(`Author: ${build.commitAuthor}`)
98
+ }
99
+ OutputFormatter.info(`Created: ${build.createdAt}`)
100
+ if (build.completedAt) {
101
+ OutputFormatter.info(`Completed: ${build.completedAt}`)
102
+ }
103
+ })
104
+
105
+ cmd
106
+ .command('cancel <id>')
107
+ .description('Cancel a running build')
108
+ .action(async (id) => {
109
+ const conn = configStore.getDefaultConnection()
110
+ if (!conn) {
111
+ OutputFormatter.error('No connection configured')
112
+ return
113
+ }
114
+
115
+ const client = createClient(conn)
116
+ const result = await client.cancelBuild(id)
117
+
118
+ if (!result.success) {
119
+ OutputFormatter.error(result.error!.message)
58
120
  return
59
121
  }
60
122
 
61
- const latest = builds[0]
62
- OutputFormatter.info(`Status: ${latest.status}`)
63
- OutputFormatter.info(`Created: ${latest.created_at}`)
123
+ OutputFormatter.success(`Build "${id}" cancelled`)
64
124
  })
65
125
 
66
126
  return cmd
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 服务管理命令
2
+ * 服务管理命令 (K8s)
3
3
  */
4
4
 
5
5
  import { Command } from 'commander'
@@ -7,6 +7,14 @@ 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
+ }
17
+
10
18
  function getAge(timestamp?: string): string {
11
19
  if (!timestamp) return '-'
12
20
  const created = new Date(timestamp)
@@ -20,12 +28,14 @@ function getAge(timestamp?: string): string {
20
28
 
21
29
  export function makeSvcCommand(): Command {
22
30
  const cmd = new Command('svc')
23
- .description('Manage services')
31
+ .description('Manage services (Kubernetes)')
24
32
 
25
33
  cmd
26
- .command('ls <namespace>')
34
+ .command('ls')
35
+ .alias('list')
27
36
  .description('List services in namespace')
28
- .action(async (namespace) => {
37
+ .option('-e, --env <namespace>', 'Namespace/environment')
38
+ .action(async (options) => {
29
39
  const conn = configStore.getDefaultConnection()
30
40
  if (!conn) {
31
41
  OutputFormatter.error('No connection configured')
@@ -33,7 +43,7 @@ export function makeSvcCommand(): Command {
33
43
  }
34
44
 
35
45
  const client = createClient(conn)
36
- const result = await client.listServices(namespace)
46
+ const result = await client.listK8sServices(options.env)
37
47
 
38
48
  if (!result.success) {
39
49
  OutputFormatter.error(result.error!.message)
@@ -42,26 +52,33 @@ export function makeSvcCommand(): Command {
42
52
 
43
53
  const services = result.data || []
44
54
  if (services.length === 0) {
45
- OutputFormatter.info(`No services in ${namespace}`)
55
+ OutputFormatter.info(`No services found${options.env ? ` in ${options.env}` : ''}`)
46
56
  return
47
57
  }
48
58
 
49
59
  OutputFormatter.table(
50
- ['Name', 'Ready', 'Up-to-date', 'Available', 'Age'],
60
+ ['Namespace', 'Name', 'Type', 'Cluster IP', 'Ports', 'Age'],
51
61
  services.map((s: any) => [
52
- s.metadata?.name || 'unknown',
53
- `${s.status?.readyReplicas || 0}/${s.status?.replicas || 0}`,
54
- s.status?.updatedReplicas || 0,
55
- s.status?.availableReplicas || 0,
56
- getAge(s.metadata?.creationTimestamp)
62
+ s.namespace || '-',
63
+ s.name || 'unknown',
64
+ s.type || 'ClusterIP',
65
+ s.clusterIP || '-',
66
+ s.ports?.map((p: any) => `${p.port}`).join(',') || '-',
67
+ getAge(s.createdAt)
57
68
  ])
58
69
  )
59
70
  })
60
71
 
61
72
  cmd
62
- .command('status <namespace> <service-name>')
63
- .description('Get service status')
64
- .action(async (namespace, serviceName) => {
73
+ .command('get <ns>/<name>')
74
+ .description('Get service details')
75
+ .action(async (nsName) => {
76
+ const parsed = parseNamespaceName(nsName)
77
+ if (!parsed) {
78
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
79
+ return
80
+ }
81
+
65
82
  const conn = configStore.getDefaultConnection()
66
83
  if (!conn) {
67
84
  OutputFormatter.error('No connection configured')
@@ -69,7 +86,45 @@ export function makeSvcCommand(): Command {
69
86
  }
70
87
 
71
88
  const client = createClient(conn)
72
- const result = await client.getServiceStatus(namespace, serviceName)
89
+ const result = await client.getK8sService(parsed.namespace, parsed.name)
90
+
91
+ if (!result.success) {
92
+ OutputFormatter.error(result.error!.message)
93
+ return
94
+ }
95
+
96
+ const svc = result.data
97
+ OutputFormatter.info(`Service: ${svc.name}`)
98
+ OutputFormatter.info(`Namespace: ${svc.namespace}`)
99
+ OutputFormatter.info(`Type: ${svc.type}`)
100
+ OutputFormatter.info(`Cluster IP: ${svc.clusterIP}`)
101
+ if (svc.externalIP) {
102
+ OutputFormatter.info(`External IP: ${svc.externalIP}`)
103
+ }
104
+ if (svc.ports) {
105
+ OutputFormatter.info(`Ports: ${svc.ports.map((p: any) => `${p.port}:${p.targetPort}/${p.protocol}`).join(', ')}`)
106
+ }
107
+ OutputFormatter.info(`Created: ${svc.createdAt}`)
108
+ })
109
+
110
+ cmd
111
+ .command('status <ns>/<name>')
112
+ .description('Get deployment status')
113
+ .action(async (nsName) => {
114
+ const parsed = parseNamespaceName(nsName)
115
+ if (!parsed) {
116
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
117
+ return
118
+ }
119
+
120
+ const conn = configStore.getDefaultConnection()
121
+ if (!conn) {
122
+ OutputFormatter.error('No connection configured')
123
+ return
124
+ }
125
+
126
+ const client = createClient(conn)
127
+ const result = await client.getK8sServiceStatus(parsed.namespace, parsed.name)
73
128
 
74
129
  if (!result.success) {
75
130
  OutputFormatter.error(result.error!.message)
@@ -77,22 +132,208 @@ export function makeSvcCommand(): Command {
77
132
  }
78
133
 
79
134
  const info = result.data
80
- OutputFormatter.info(`Service: ${info.name}`)
135
+ OutputFormatter.info(`Deployment: ${info.name}`)
81
136
  OutputFormatter.info(`Namespace: ${info.namespace}`)
82
- OutputFormatter.info(`Replicas: ${info.replicas}`)
83
- OutputFormatter.info(`Ready Replicas: ${info.readyReplicas}`)
84
- OutputFormatter.info(`Available Replicas: ${info.availableReplicas}`)
137
+ OutputFormatter.info(`Replicas: ${info.readyReplicas}/${info.replicas}`)
138
+ OutputFormatter.info(`Available: ${info.availableReplicas}`)
85
139
  OutputFormatter.info(`Image: ${info.image}`)
86
-
87
140
  if (info.labels) {
88
141
  OutputFormatter.info(`Labels: ${JSON.stringify(info.labels)}`)
89
142
  }
90
143
  })
91
144
 
92
145
  cmd
93
- .command('rm <namespace> <service-name>')
146
+ .command('logs <ns>/<name>')
147
+ .description('Get service logs')
148
+ .option('-f, --follow', 'Follow log output')
149
+ .option('-t, --tail <lines>', 'Number of lines to show', '100')
150
+ .action(async (nsName, options) => {
151
+ const parsed = parseNamespaceName(nsName)
152
+ if (!parsed) {
153
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
154
+ return
155
+ }
156
+
157
+ const conn = configStore.getDefaultConnection()
158
+ if (!conn) {
159
+ OutputFormatter.error('No connection configured')
160
+ return
161
+ }
162
+
163
+ const client = createClient(conn)
164
+
165
+ if (options.follow) {
166
+ OutputFormatter.info('Following logs... (Ctrl+C to exit)')
167
+ await client.streamLogs(parsed.namespace, parsed.name)
168
+ return
169
+ }
170
+
171
+ const result = await client.getK8sServiceLogs(
172
+ parsed.namespace,
173
+ parsed.name,
174
+ parseInt(options.tail),
175
+ false
176
+ )
177
+
178
+ if (!result.success) {
179
+ OutputFormatter.error(result.error!.message)
180
+ return
181
+ }
182
+
183
+ console.log(result.data?.logs || 'No logs available')
184
+ })
185
+
186
+ cmd
187
+ .command('restart <ns>/<name>')
188
+ .description('Restart a service')
189
+ .action(async (nsName) => {
190
+ const parsed = parseNamespaceName(nsName)
191
+ if (!parsed) {
192
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
193
+ return
194
+ }
195
+
196
+ const conn = configStore.getDefaultConnection()
197
+ if (!conn) {
198
+ OutputFormatter.error('No connection configured')
199
+ return
200
+ }
201
+
202
+ const client = createClient(conn)
203
+ const result = await client.restartK8sService(parsed.namespace, parsed.name)
204
+
205
+ if (!result.success) {
206
+ OutputFormatter.error(result.error!.message)
207
+ return
208
+ }
209
+
210
+ OutputFormatter.success(`Service "${nsName}" restarted`)
211
+ })
212
+
213
+ cmd
214
+ .command('scale <ns>/<name>')
215
+ .description('Scale a service')
216
+ .requiredOption('-r, --replicas <num>', 'Number of replicas')
217
+ .action(async (nsName, options) => {
218
+ const parsed = parseNamespaceName(nsName)
219
+ if (!parsed) {
220
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
221
+ return
222
+ }
223
+
224
+ const conn = configStore.getDefaultConnection()
225
+ if (!conn) {
226
+ OutputFormatter.error('No connection configured')
227
+ return
228
+ }
229
+
230
+ const client = createClient(conn)
231
+ const replicas = parseInt(options.replicas)
232
+ const result = await client.scaleK8sService(parsed.namespace, parsed.name, replicas)
233
+
234
+ if (!result.success) {
235
+ OutputFormatter.error(result.error!.message)
236
+ return
237
+ }
238
+
239
+ OutputFormatter.success(`Service "${nsName}" scaled to ${replicas} replicas`)
240
+ })
241
+
242
+ cmd
243
+ .command('exec <ns>/<name>')
244
+ .description('Execute command in service container')
245
+ .option('-c, --command <cmd>', 'Command to execute', 'sh')
246
+ .option('-p, --pod <pod-name>', 'Specific pod name')
247
+ .option('-i, --stdin', 'Keep stdin open')
248
+ .option('-t, --tty', 'Allocate a pseudo-TTY')
249
+ .action(async (nsName, options) => {
250
+ const parsed = parseNamespaceName(nsName)
251
+ if (!parsed) {
252
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
253
+ return
254
+ }
255
+
256
+ const conn = configStore.getDefaultConnection()
257
+ if (!conn) {
258
+ OutputFormatter.error('No connection configured')
259
+ return
260
+ }
261
+
262
+ const client = createClient(conn)
263
+ const result = await client.execK8sService(
264
+ parsed.namespace,
265
+ parsed.name,
266
+ options.command,
267
+ options.pod
268
+ )
269
+
270
+ if (!result.success) {
271
+ OutputFormatter.error(result.error!.message)
272
+ return
273
+ }
274
+
275
+ const data = result.data
276
+ if (data.stdout) console.log(data.stdout)
277
+ if (data.stderr) console.error(data.stderr)
278
+ if (data.exitCode !== 0) {
279
+ process.exit(data.exitCode)
280
+ }
281
+ })
282
+
283
+ cmd
284
+ .command('pods <ns>/<name>')
285
+ .description('List pods for a service')
286
+ .action(async (nsName) => {
287
+ const parsed = parseNamespaceName(nsName)
288
+ if (!parsed) {
289
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
290
+ return
291
+ }
292
+
293
+ const conn = configStore.getDefaultConnection()
294
+ if (!conn) {
295
+ OutputFormatter.error('No connection configured')
296
+ return
297
+ }
298
+
299
+ const client = createClient(conn)
300
+ const result = await client.listK8sServicePods(parsed.namespace, parsed.name)
301
+
302
+ if (!result.success) {
303
+ OutputFormatter.error(result.error!.message)
304
+ return
305
+ }
306
+
307
+ const pods = result.data || []
308
+ if (pods.length === 0) {
309
+ OutputFormatter.info('No pods found')
310
+ return
311
+ }
312
+
313
+ OutputFormatter.table(
314
+ ['Name', 'Status', 'Ready', 'Restarts', 'Age', 'IP'],
315
+ pods.map((p: any) => [
316
+ p.name,
317
+ p.status,
318
+ p.ready || '0/0',
319
+ p.restarts || 0,
320
+ getAge(p.createdAt),
321
+ p.ip || '-'
322
+ ])
323
+ )
324
+ })
325
+
326
+ cmd
327
+ .command('delete <ns>/<name>')
328
+ .alias('rm')
94
329
  .description('Delete a service')
95
- .action(async (namespace, serviceName) => {
330
+ .action(async (nsName) => {
331
+ const parsed = parseNamespaceName(nsName)
332
+ if (!parsed) {
333
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
334
+ return
335
+ }
336
+
96
337
  const conn = configStore.getDefaultConnection()
97
338
  if (!conn) {
98
339
  OutputFormatter.error('No connection configured')
@@ -100,14 +341,14 @@ export function makeSvcCommand(): Command {
100
341
  }
101
342
 
102
343
  const client = createClient(conn)
103
- const result = await client.deleteService(namespace, serviceName)
344
+ const result = await client.deleteK8sService(parsed.namespace, parsed.name)
104
345
 
105
346
  if (!result.success) {
106
347
  OutputFormatter.error(result.error!.message)
107
348
  return
108
349
  }
109
350
 
110
- OutputFormatter.success(`Service "${serviceName}" deleted`)
351
+ OutputFormatter.success(`Service "${nsName}" deleted`)
111
352
  })
112
353
 
113
354
  return cmd