xuanwu-cli 2.3.2 → 2.3.3

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.
@@ -62,6 +62,11 @@ export declare class APIClient {
62
62
  scaleK8sService(namespace: string, name: string, replicas: number): Promise<CLIResult<any>>;
63
63
  execK8sService(namespace: string, name: string, command: string, podName?: string): Promise<CLIResult<any>>;
64
64
  listK8sServicePods(namespace: string, name: string): Promise<CLIResult<any>>;
65
+ updateK8sService(namespace: string, name: string, options: {
66
+ image?: string;
67
+ replicas?: number;
68
+ port?: number;
69
+ }): Promise<CLIResult<any>>;
65
70
  deleteK8sService(namespace: string, name: string): Promise<CLIResult<void>>;
66
71
  listBuilds(options?: {
67
72
  appCode?: string;
@@ -69,5 +74,6 @@ export declare class APIClient {
69
74
  }): Promise<CLIResult<Build[]>>;
70
75
  getBuild(id: string): Promise<CLIResult<Build>>;
71
76
  cancelBuild(id: string): Promise<CLIResult<void>>;
77
+ getBuildLogs(id: string, follow?: boolean): Promise<CLIResult<any>>;
72
78
  }
73
79
  export declare function createClient(connection: Connection): APIClient;
@@ -469,6 +469,9 @@ class APIClient {
469
469
  async listK8sServicePods(namespace, name) {
470
470
  return this.request('GET', `/api/cli/services/${namespace}/${name}/pods`);
471
471
  }
472
+ async updateK8sService(namespace, name, options) {
473
+ return this.request('PUT', `/api/cli/services/${namespace}/${name}`, options);
474
+ }
472
475
  async deleteK8sService(namespace, name) {
473
476
  return this.request('DELETE', `/api/cli/services/${namespace}/${name}`);
474
477
  }
@@ -492,6 +495,12 @@ class APIClient {
492
495
  async cancelBuild(id) {
493
496
  return this.request('POST', `/api/cli/builds/${id}/cancel`);
494
497
  }
498
+ async getBuildLogs(id, follow) {
499
+ let url = `/api/cli/builds/${id}/logs`;
500
+ if (follow)
501
+ url += '?follow=true';
502
+ return this.request('GET', url);
503
+ }
495
504
  }
496
505
  exports.APIClient = APIClient;
497
506
  function createClient(connection) {
@@ -237,5 +237,49 @@ function makeAppCommand() {
237
237
  getAge(b.createdAt)
238
238
  ]));
239
239
  });
240
+ cmd
241
+ .command('logs <code>')
242
+ .description('View application build logs')
243
+ .option('-b, --build <buildNumber>', 'Specific build number (default: latest)')
244
+ .option('-f, --follow', 'Follow log output in real-time')
245
+ .action(async (code, options) => {
246
+ const conn = store_1.configStore.getDefaultConnection();
247
+ if (!conn) {
248
+ formatter_1.OutputFormatter.error('No connection configured');
249
+ return;
250
+ }
251
+ const client = (0, client_1.createClient)(conn);
252
+ const buildsResult = await client.listApplicationBuilds(code);
253
+ if (!buildsResult.success) {
254
+ formatter_1.OutputFormatter.error(buildsResult.error.message);
255
+ return;
256
+ }
257
+ const builds = buildsResult.data || [];
258
+ if (builds.length === 0) {
259
+ formatter_1.OutputFormatter.info('No builds found for this application');
260
+ return;
261
+ }
262
+ let targetBuild;
263
+ if (options.build) {
264
+ targetBuild = builds.find((b) => b.buildNumber === parseInt(options.build));
265
+ if (!targetBuild) {
266
+ formatter_1.OutputFormatter.error(`Build #${options.build} not found`);
267
+ return;
268
+ }
269
+ }
270
+ else {
271
+ targetBuild = builds[0];
272
+ }
273
+ formatter_1.OutputFormatter.info(`Viewing logs for build #${targetBuild.buildNumber} (ID: ${targetBuild.id})`);
274
+ if (options.follow) {
275
+ formatter_1.OutputFormatter.info('Following logs... (Ctrl+C to exit)');
276
+ }
277
+ const result = await client.getBuildLogs(targetBuild.id, options.follow);
278
+ if (!result.success) {
279
+ formatter_1.OutputFormatter.error(result.error.message);
280
+ return;
281
+ }
282
+ console.log(result.data?.logs || 'No logs available');
283
+ });
240
284
  return cmd;
241
285
  }
@@ -15,9 +15,62 @@ function parseNamespaceName(input) {
15
15
  }
16
16
  return { namespace: parts[0], name: parts[1] };
17
17
  }
18
+ function getAge(timestamp) {
19
+ if (!timestamp)
20
+ return '-';
21
+ const created = new Date(timestamp);
22
+ const now = new Date();
23
+ const diff = Math.floor((now.getTime() - created.getTime()) / 1000);
24
+ if (diff < 60)
25
+ return `${diff}s`;
26
+ if (diff < 3600)
27
+ return `${Math.floor(diff / 60)}m`;
28
+ if (diff < 86400)
29
+ return `${Math.floor(diff / 3600)}h`;
30
+ return `${Math.floor(diff / 86400)}d`;
31
+ }
18
32
  function makeDeployCommand() {
19
33
  const cmd = new commander_1.Command('deploy')
20
- .description('Deploy services to environment (auto-creates service if not exists)')
34
+ .description('Deploy services to environment');
35
+ cmd
36
+ .command('history <ns-name>')
37
+ .description('Show deployment history for a service')
38
+ .action(async (nsName) => {
39
+ const conn = store_1.configStore.getDefaultConnection();
40
+ if (!conn) {
41
+ formatter_1.OutputFormatter.error('No connection configured');
42
+ return;
43
+ }
44
+ const parsed = parseNamespaceName(nsName);
45
+ if (!parsed) {
46
+ formatter_1.OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>');
47
+ return;
48
+ }
49
+ const { namespace, name: serviceName } = parsed;
50
+ const client = (0, client_1.createClient)(conn);
51
+ const result = await client.listServiceDeployments(namespace, serviceName);
52
+ if (!result.success) {
53
+ formatter_1.OutputFormatter.error(result.error.message);
54
+ return;
55
+ }
56
+ const data = result.data;
57
+ const deployments = data?.deployments || data || [];
58
+ if (deployments.length === 0) {
59
+ formatter_1.OutputFormatter.info('No deployments found');
60
+ return;
61
+ }
62
+ formatter_1.OutputFormatter.table(['ID', 'Image', 'Status', 'Build', 'Created'], deployments.map((d) => [
63
+ d.id.substring(0, 8),
64
+ d.image?.substring(0, 40) || '-',
65
+ d.status || '-',
66
+ d.build?.buildNumber ? `#${d.build.buildNumber}` : '-',
67
+ getAge(d.createdAt)
68
+ ]));
69
+ if (data?.pagination) {
70
+ formatter_1.OutputFormatter.info(`Page ${data.pagination.page}/${data.pagination.totalPages}, Total: ${data.pagination.total}`);
71
+ }
72
+ });
73
+ cmd
21
74
  .argument('<ns-name>', 'Target namespace and service name (format: namespace/service-name)')
22
75
  .requiredOption('-i, --image <image>', 'Container image (required)')
23
76
  .option('-p, --project <code>', 'Project code')
@@ -44,7 +97,6 @@ function makeDeployCommand() {
44
97
  }
45
98
  const { namespace, name: serviceName } = parsed;
46
99
  const client = (0, client_1.createClient)(conn);
47
- // 使用 CLI API 进行部署
48
100
  const result = await client.deployService(namespace, serviceName, options.image, {
49
101
  projectCode: options.project,
50
102
  replicas: options.replicas ? parseInt(options.replicas) : 1,
@@ -268,6 +268,42 @@ function makeSvcCommand() {
268
268
  p.ip || '-'
269
269
  ]));
270
270
  });
271
+ cmd
272
+ .command('update <ns>/<name>')
273
+ .description('Update service configuration')
274
+ .option('-i, --image <image>', 'Container image')
275
+ .option('-r, --replicas <num>', 'Number of replicas')
276
+ .option('--port <port>', 'Container port')
277
+ .action(async (nsName, options) => {
278
+ const parsed = parseNamespaceName(nsName);
279
+ if (!parsed) {
280
+ formatter_1.OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>');
281
+ return;
282
+ }
283
+ const conn = store_1.configStore.getDefaultConnection();
284
+ if (!conn) {
285
+ formatter_1.OutputFormatter.error('No connection configured');
286
+ return;
287
+ }
288
+ const updateData = {};
289
+ if (options.image)
290
+ updateData.image = options.image;
291
+ if (options.replicas)
292
+ updateData.replicas = parseInt(options.replicas);
293
+ if (options.port)
294
+ updateData.port = parseInt(options.port);
295
+ if (Object.keys(updateData).length === 0) {
296
+ formatter_1.OutputFormatter.error('No update options provided. Use --image, --replicas, or --port');
297
+ return;
298
+ }
299
+ const client = (0, client_1.createClient)(conn);
300
+ const result = await client.updateK8sService(parsed.namespace, parsed.name, updateData);
301
+ if (!result.success) {
302
+ formatter_1.OutputFormatter.error(result.error.message);
303
+ return;
304
+ }
305
+ formatter_1.OutputFormatter.success(`Service "${nsName}" updated`);
306
+ });
271
307
  cmd
272
308
  .command('delete <ns>/<name>')
273
309
  .alias('rm')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xuanwu-cli",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "玄武工厂平台 CLI 工具",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/api/client.ts CHANGED
@@ -554,6 +554,14 @@ export class APIClient {
554
554
  return this.request('GET', `/api/cli/services/${namespace}/${name}/pods`)
555
555
  }
556
556
 
557
+ async updateK8sService(namespace: string, name: string, options: {
558
+ image?: string
559
+ replicas?: number
560
+ port?: number
561
+ }): Promise<CLIResult<any>> {
562
+ return this.request('PUT', `/api/cli/services/${namespace}/${name}`, options)
563
+ }
564
+
557
565
  async deleteK8sService(namespace: string, name: string): Promise<CLIResult<void>> {
558
566
  return this.request<void>('DELETE', `/api/cli/services/${namespace}/${name}`)
559
567
  }
@@ -578,6 +586,12 @@ export class APIClient {
578
586
  async cancelBuild(id: string): Promise<CLIResult<void>> {
579
587
  return this.request<void>('POST', `/api/cli/builds/${id}/cancel`)
580
588
  }
589
+
590
+ async getBuildLogs(id: string, follow?: boolean): Promise<CLIResult<any>> {
591
+ let url = `/api/cli/builds/${id}/logs`
592
+ if (follow) url += '?follow=true'
593
+ return this.request('GET', url)
594
+ }
581
595
  }
582
596
 
583
597
  export function createClient(connection: Connection): APIClient {
@@ -265,5 +265,58 @@ export function makeAppCommand(): Command {
265
265
  )
266
266
  })
267
267
 
268
+ cmd
269
+ .command('logs <code>')
270
+ .description('View application build logs')
271
+ .option('-b, --build <buildNumber>', 'Specific build number (default: latest)')
272
+ .option('-f, --follow', 'Follow log output in real-time')
273
+ .action(async (code, options) => {
274
+ const conn = configStore.getDefaultConnection()
275
+ if (!conn) {
276
+ OutputFormatter.error('No connection configured')
277
+ return
278
+ }
279
+
280
+ const client = createClient(conn)
281
+
282
+ const buildsResult = await client.listApplicationBuilds(code)
283
+ if (!buildsResult.success) {
284
+ OutputFormatter.error(buildsResult.error!.message)
285
+ return
286
+ }
287
+
288
+ const builds = buildsResult.data || []
289
+ if (builds.length === 0) {
290
+ OutputFormatter.info('No builds found for this application')
291
+ return
292
+ }
293
+
294
+ let targetBuild: any
295
+ if (options.build) {
296
+ targetBuild = builds.find((b: any) => b.buildNumber === parseInt(options.build))
297
+ if (!targetBuild) {
298
+ OutputFormatter.error(`Build #${options.build} not found`)
299
+ return
300
+ }
301
+ } else {
302
+ targetBuild = builds[0]
303
+ }
304
+
305
+ OutputFormatter.info(`Viewing logs for build #${targetBuild.buildNumber} (ID: ${targetBuild.id})`)
306
+
307
+ if (options.follow) {
308
+ OutputFormatter.info('Following logs... (Ctrl+C to exit)')
309
+ }
310
+
311
+ const result = await client.getBuildLogs(targetBuild.id, options.follow)
312
+
313
+ if (!result.success) {
314
+ OutputFormatter.error(result.error!.message)
315
+ return
316
+ }
317
+
318
+ console.log(result.data?.logs || 'No logs available')
319
+ })
320
+
268
321
  return cmd
269
322
  }
@@ -15,9 +15,72 @@ function parseNamespaceName(input: string): { namespace: string; name: string }
15
15
  return { namespace: parts[0], name: parts[1] }
16
16
  }
17
17
 
18
+ function getAge(timestamp?: string): string {
19
+ if (!timestamp) return '-'
20
+ const created = new Date(timestamp)
21
+ const now = new Date()
22
+ const diff = Math.floor((now.getTime() - created.getTime()) / 1000)
23
+ if (diff < 60) return `${diff}s`
24
+ if (diff < 3600) return `${Math.floor(diff / 60)}m`
25
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h`
26
+ return `${Math.floor(diff / 86400)}d`
27
+ }
28
+
18
29
  export function makeDeployCommand(): Command {
19
30
  const cmd = new Command('deploy')
20
- .description('Deploy services to environment (auto-creates service if not exists)')
31
+ .description('Deploy services to environment')
32
+
33
+ cmd
34
+ .command('history <ns-name>')
35
+ .description('Show deployment history for a service')
36
+ .action(async (nsName) => {
37
+ const conn = configStore.getDefaultConnection()
38
+ if (!conn) {
39
+ OutputFormatter.error('No connection configured')
40
+ return
41
+ }
42
+
43
+ const parsed = parseNamespaceName(nsName)
44
+ if (!parsed) {
45
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
46
+ return
47
+ }
48
+
49
+ const { namespace, name: serviceName } = parsed
50
+
51
+ const client = createClient(conn)
52
+ const result = await client.listServiceDeployments(namespace, serviceName)
53
+
54
+ if (!result.success) {
55
+ OutputFormatter.error(result.error!.message)
56
+ return
57
+ }
58
+
59
+ const data = result.data as any
60
+ const deployments = data?.deployments || data || []
61
+
62
+ if (deployments.length === 0) {
63
+ OutputFormatter.info('No deployments found')
64
+ return
65
+ }
66
+
67
+ OutputFormatter.table(
68
+ ['ID', 'Image', 'Status', 'Build', 'Created'],
69
+ deployments.map((d: any) => [
70
+ d.id.substring(0, 8),
71
+ d.image?.substring(0, 40) || '-',
72
+ d.status || '-',
73
+ d.build?.buildNumber ? `#${d.build.buildNumber}` : '-',
74
+ getAge(d.createdAt)
75
+ ])
76
+ )
77
+
78
+ if (data?.pagination) {
79
+ OutputFormatter.info(`Page ${data.pagination.page}/${data.pagination.totalPages}, Total: ${data.pagination.total}`)
80
+ }
81
+ })
82
+
83
+ cmd
21
84
  .argument('<ns-name>', 'Target namespace and service name (format: namespace/service-name)')
22
85
  .requiredOption('-i, --image <image>', 'Container image (required)')
23
86
  .option('-p, --project <code>', 'Project code')
@@ -48,7 +111,6 @@ export function makeDeployCommand(): Command {
48
111
 
49
112
  const client = createClient(conn)
50
113
 
51
- // 使用 CLI API 进行部署
52
114
  const result = await client.deployService(
53
115
  namespace,
54
116
  serviceName,
@@ -323,6 +323,46 @@ export function makeSvcCommand(): Command {
323
323
  )
324
324
  })
325
325
 
326
+ cmd
327
+ .command('update <ns>/<name>')
328
+ .description('Update service configuration')
329
+ .option('-i, --image <image>', 'Container image')
330
+ .option('-r, --replicas <num>', 'Number of replicas')
331
+ .option('--port <port>', 'Container port')
332
+ .action(async (nsName, options) => {
333
+ const parsed = parseNamespaceName(nsName)
334
+ if (!parsed) {
335
+ OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
336
+ return
337
+ }
338
+
339
+ const conn = configStore.getDefaultConnection()
340
+ if (!conn) {
341
+ OutputFormatter.error('No connection configured')
342
+ return
343
+ }
344
+
345
+ const updateData: any = {}
346
+ if (options.image) updateData.image = options.image
347
+ if (options.replicas) updateData.replicas = parseInt(options.replicas)
348
+ if (options.port) updateData.port = parseInt(options.port)
349
+
350
+ if (Object.keys(updateData).length === 0) {
351
+ OutputFormatter.error('No update options provided. Use --image, --replicas, or --port')
352
+ return
353
+ }
354
+
355
+ const client = createClient(conn)
356
+ const result = await client.updateK8sService(parsed.namespace, parsed.name, updateData)
357
+
358
+ if (!result.success) {
359
+ OutputFormatter.error(result.error!.message)
360
+ return
361
+ }
362
+
363
+ OutputFormatter.success(`Service "${nsName}" updated`)
364
+ })
365
+
326
366
  cmd
327
367
  .command('delete <ns>/<name>')
328
368
  .alias('rm')