xuanwu-cli 1.0.0 → 2.0.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.
@@ -84,3 +84,95 @@ export interface CLIResult<T = any> {
84
84
  duration?: number;
85
85
  };
86
86
  }
87
+ export interface Application {
88
+ id: string;
89
+ code: string;
90
+ name: string;
91
+ gitRepo: string;
92
+ gitBranch: string;
93
+ buildPath: string;
94
+ dockerfile: string;
95
+ buildArgs?: Record<string, string>;
96
+ imageName: string;
97
+ jenkinsConfigId?: string;
98
+ jenkinsJobName?: string;
99
+ createdAt: string;
100
+ updatedAt: string;
101
+ }
102
+ export interface CreateApplicationDto {
103
+ name: string;
104
+ code?: string;
105
+ gitRepo: string;
106
+ gitBranch?: string;
107
+ imageName?: string;
108
+ buildPath?: string;
109
+ dockerfile?: string;
110
+ buildArgs?: Record<string, string>;
111
+ jenkinsConfigId?: string;
112
+ jenkinsJobName?: string;
113
+ }
114
+ export interface UpdateApplicationDto {
115
+ name?: string;
116
+ gitRepo?: string;
117
+ gitBranch?: string;
118
+ imageName?: string;
119
+ buildPath?: string;
120
+ dockerfile?: string;
121
+ buildArgs?: Record<string, string>;
122
+ jenkinsConfigId?: string;
123
+ jenkinsJobName?: string;
124
+ }
125
+ export interface Build {
126
+ id: string;
127
+ buildNumber: number;
128
+ status: string;
129
+ imageTag: string;
130
+ commitSha?: string;
131
+ commitMessage?: string;
132
+ commitAuthor?: string;
133
+ createdAt: string;
134
+ completedAt?: string;
135
+ }
136
+ export interface TriggerBuildOptions {
137
+ gitBranch?: string;
138
+ commitSha?: string;
139
+ commitMessage?: string;
140
+ commitAuthor?: string;
141
+ }
142
+ export interface K8sService {
143
+ name: string;
144
+ namespace: string;
145
+ type: string;
146
+ clusterIP: string;
147
+ externalIP?: string;
148
+ ports: {
149
+ port: number;
150
+ targetPort: number;
151
+ protocol: string;
152
+ }[];
153
+ age: string;
154
+ }
155
+ export interface K8sDeployment {
156
+ name: string;
157
+ namespace: string;
158
+ replicas: number;
159
+ readyReplicas: number;
160
+ availableReplicas: number;
161
+ image: string;
162
+ labels?: Record<string, string>;
163
+ createdAt: string;
164
+ }
165
+ export interface K8sPod {
166
+ name: string;
167
+ namespace: string;
168
+ status: string;
169
+ ready: string;
170
+ restarts: number;
171
+ age: string;
172
+ ip: string;
173
+ node: string;
174
+ containers: {
175
+ name: string;
176
+ image: string;
177
+ }[];
178
+ }
package/dist/index.js CHANGED
@@ -2,44 +2,12 @@
2
2
  /**
3
3
  * xuanwu-cli 主入口
4
4
  */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  const commander_1 = require("commander");
40
7
  const connect_1 = require("./commands/connect");
41
8
  const env_1 = require("./commands/env");
42
9
  const deploy_1 = require("./commands/deploy");
10
+ const app_1 = require("./commands/app");
43
11
  const svc_1 = require("./commands/svc");
44
12
  const build_1 = require("./commands/build");
45
13
  const scale_1 = require("./commands/scale");
@@ -47,96 +15,17 @@ const logs_1 = require("./commands/logs");
47
15
  const pods_1 = require("./commands/pods");
48
16
  const program = new commander_1.Command();
49
17
  program
50
- .name('xuanwu')
18
+ .name('xw')
51
19
  .description('玄武工厂平台 CLI 工具')
52
- .version('1.0.0')
20
+ .version('2.0.0')
53
21
  .option('-o, --output <format>', 'Output format (human|json)', 'human');
54
- // 添加子命令
55
22
  program.addCommand((0, connect_1.makeConnectCommand)());
56
23
  program.addCommand((0, env_1.makeEnvCommand)());
57
- program.addCommand((0, deploy_1.makeDeployCommand)());
24
+ program.addCommand((0, app_1.makeAppCommand)());
58
25
  program.addCommand((0, svc_1.makeSvcCommand)());
59
26
  program.addCommand((0, build_1.makeBuildCommand)());
27
+ program.addCommand((0, deploy_1.makeDeployCommand)());
60
28
  program.addCommand((0, scale_1.makeScaleCommand)());
61
29
  program.addCommand((0, logs_1.makeLogsCommand)());
62
30
  program.addCommand((0, pods_1.makePodsCommand)());
63
- // 快捷命令:直接 deploy 也支持
64
- program
65
- .command('up <namespace> <service-name>')
66
- .description('Build and deploy a service')
67
- .action(async (namespace, serviceName) => {
68
- const { makeBuildCommand } = await Promise.resolve().then(() => __importStar(require('./commands/build')));
69
- const buildCmd = makeBuildCommand();
70
- // 简化:只调用部署
71
- console.log('Use: xuanwu build <namespace> <service-name> && xuanwu deploy <namespace> <service-name>');
72
- });
73
- // restart 命令
74
- program
75
- .command('restart <namespace> <service-name>')
76
- .description('Restart a service')
77
- .action(async (namespace, serviceName) => {
78
- console.log('Restarting service... (not implemented)');
79
- });
80
- // exec 命令
81
- program
82
- .command('exec <namespace> <service-name>')
83
- .description('Execute command in container')
84
- .option('-c, --command <cmd>', 'Command to execute', 'ls -la')
85
- .option('-p, --pod <pod-name>', 'Specific pod name')
86
- .action(async (namespace, serviceName, options) => {
87
- const { configStore } = await Promise.resolve().then(() => __importStar(require('./config/store')));
88
- const { createClient } = await Promise.resolve().then(() => __importStar(require('./api/client')));
89
- const { OutputFormatter } = await Promise.resolve().then(() => __importStar(require('./output/formatter')));
90
- const conn = configStore.getDefaultConnection();
91
- if (!conn) {
92
- OutputFormatter.error('No connection configured');
93
- return;
94
- }
95
- const client = createClient(conn);
96
- const result = await client.exec(namespace, serviceName, options.command, options.pod);
97
- if (!result.success) {
98
- OutputFormatter.error(result.error.message);
99
- return;
100
- }
101
- const data = result.data;
102
- if (data.stdout)
103
- console.log(data.stdout);
104
- if (data.stderr)
105
- console.error(data.stderr);
106
- if (data.exitCode !== 0) {
107
- process.exit(data.exitCode);
108
- }
109
- });
110
- // top 命令
111
- program
112
- .command('top <namespace> <service-name>')
113
- .description('Show resource usage')
114
- .option('-p, --pod <pod-name>', 'Specific pod name')
115
- .action(async (namespace, serviceName, options) => {
116
- const { configStore } = await Promise.resolve().then(() => __importStar(require('./config/store')));
117
- const { createClient } = await Promise.resolve().then(() => __importStar(require('./api/client')));
118
- const { OutputFormatter } = await Promise.resolve().then(() => __importStar(require('./output/formatter')));
119
- const conn = configStore.getDefaultConnection();
120
- if (!conn) {
121
- OutputFormatter.error('No connection configured');
122
- return;
123
- }
124
- const client = createClient(conn);
125
- const result = await client.getMetrics(namespace, serviceName, options.pod);
126
- if (!result.success) {
127
- OutputFormatter.error(result.error.message);
128
- return;
129
- }
130
- const pods = result.data?.pods || [];
131
- if (pods.length === 0) {
132
- OutputFormatter.info('No pod metrics found');
133
- return;
134
- }
135
- OutputFormatter.table(['Pod Name', 'CPU', 'Memory'], pods.map((p) => [
136
- p.name,
137
- p.containers?.[0]?.cpuRaw || 'N/A',
138
- p.containers?.[0]?.memoryRaw || 'N/A'
139
- ]));
140
- });
141
- // 解析参数
142
31
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "xuanwu-cli",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "玄武工厂平台 CLI 工具",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "xuanwu": "./bin/xuanwu"
7
+ "xuanwu": "./bin/xuanwu",
8
+ "xw": "./bin/xuanwu"
8
9
  },
9
10
  "keywords": [
10
11
  "cli",
package/src/api/client.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
6
- import { Connection, CLIResult, ServiceInfo, NamespaceInfo, DeployOptions } from '../config/types'
6
+ import { Connection, CLIResult, ServiceInfo, NamespaceInfo, DeployOptions, Application, Build, CreateApplicationDto, UpdateApplicationDto, TriggerBuildOptions } from '../config/types'
7
7
  import { OutputFormatter } from '../output/formatter'
8
8
 
9
9
  export class APIClient {
@@ -86,6 +86,17 @@ export class APIClient {
86
86
  return this.request<any[]>('GET', '/api/projects')
87
87
  }
88
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
+
89
100
  async createNamespaceWithProject(name: string, projectId: string, environment: string = 'development'): Promise<CLIResult<any>> {
90
101
  return this.request('POST', '/api/deploy-spaces', {
91
102
  name,
@@ -373,6 +384,105 @@ export class APIClient {
373
384
  command
374
385
  })
375
386
  }
387
+
388
+ // ===================
389
+ // Application (CLI API - using code)
390
+ // ===================
391
+
392
+ async listApplications(): Promise<CLIResult<Application[]>> {
393
+ return this.request<Application[]>('GET', '/api/cli/apps')
394
+ }
395
+
396
+ async getApplication(code: string): Promise<CLIResult<Application>> {
397
+ return this.request<Application>('GET', `/api/cli/apps/${code}`)
398
+ }
399
+
400
+ async createApplication(dto: CreateApplicationDto): Promise<CLIResult<Application>> {
401
+ return this.request<Application>('POST', '/api/applications', dto)
402
+ }
403
+
404
+ async updateApplication(code: string, dto: UpdateApplicationDto): Promise<CLIResult<Application>> {
405
+ return this.request<Application>('PUT', `/api/cli/apps/${code}`, dto)
406
+ }
407
+
408
+ async deleteApplication(code: string): Promise<CLIResult<void>> {
409
+ return this.request<void>('DELETE', `/api/cli/apps/${code}`)
410
+ }
411
+
412
+ async triggerApplicationBuild(code: string, options?: TriggerBuildOptions): Promise<CLIResult<Build>> {
413
+ return this.request<Build>('POST', `/api/cli/apps/${code}/build`, options)
414
+ }
415
+
416
+ async listApplicationBuilds(code: string): Promise<CLIResult<Build[]>> {
417
+ return this.request<Build[]>('GET', `/api/cli/apps/${code}/builds`)
418
+ }
419
+
420
+ // ===================
421
+ // Service (CLI API - using ns/name)
422
+ // ===================
423
+
424
+ async listK8sServices(namespace?: string): Promise<CLIResult<any>> {
425
+ const url = namespace ? `/api/cli/services?namespace=${namespace}` : '/api/cli/services'
426
+ return this.request('GET', url)
427
+ }
428
+
429
+ async getK8sService(namespace: string, name: string): Promise<CLIResult<any>> {
430
+ return this.request('GET', `/api/cli/services/${namespace}/${name}`)
431
+ }
432
+
433
+ async getK8sServiceStatus(namespace: string, name: string): Promise<CLIResult<any>> {
434
+ return this.request('GET', `/api/cli/services/${namespace}/${name}/status`)
435
+ }
436
+
437
+ async getK8sServiceLogs(namespace: string, name: string, lines?: number, follow?: boolean): Promise<CLIResult<any>> {
438
+ let url = `/api/cli/services/${namespace}/${name}/logs`
439
+ const params: string[] = []
440
+ if (lines) params.push(`lines=${lines}`)
441
+ if (follow) params.push(`follow=true`)
442
+ if (params.length > 0) url += '?' + params.join('&')
443
+ return this.request('GET', url)
444
+ }
445
+
446
+ async restartK8sService(namespace: string, name: string): Promise<CLIResult<any>> {
447
+ return this.request('POST', `/api/cli/services/${namespace}/${name}/restart`)
448
+ }
449
+
450
+ async scaleK8sService(namespace: string, name: string, replicas: number): Promise<CLIResult<any>> {
451
+ return this.request('POST', `/api/cli/services/${namespace}/${name}/scale`, { replicas })
452
+ }
453
+
454
+ async execK8sService(namespace: string, name: string, command: string, podName?: string): Promise<CLIResult<any>> {
455
+ return this.request('POST', `/api/cli/services/${namespace}/${name}/exec`, { command, podName })
456
+ }
457
+
458
+ async listK8sServicePods(namespace: string, name: string): Promise<CLIResult<any>> {
459
+ return this.request('GET', `/api/cli/services/${namespace}/${name}/pods`)
460
+ }
461
+
462
+ async deleteK8sService(namespace: string, name: string): Promise<CLIResult<void>> {
463
+ return this.request<void>('DELETE', `/api/cli/services/${namespace}/${name}`)
464
+ }
465
+
466
+ // ===================
467
+ // Build (CLI API)
468
+ // ===================
469
+
470
+ async listBuilds(options?: { appCode?: string; status?: string }): Promise<CLIResult<Build[]>> {
471
+ let url = '/api/cli/builds'
472
+ const params: string[] = []
473
+ if (options?.appCode) params.push(`appCode=${options.appCode}`)
474
+ if (options?.status) params.push(`status=${options.status}`)
475
+ if (params.length > 0) url += '?' + params.join('&')
476
+ return this.request<Build[]>('GET', url)
477
+ }
478
+
479
+ async getBuild(id: string): Promise<CLIResult<Build>> {
480
+ return this.request<Build>('GET', `/api/cli/builds/${id}`)
481
+ }
482
+
483
+ async cancelBuild(id: string): Promise<CLIResult<void>> {
484
+ return this.request<void>('POST', `/api/cli/builds/${id}/cancel`)
485
+ }
376
486
  }
377
487
 
378
488
  export function createClient(connection: Connection): APIClient {
@@ -0,0 +1,269 @@
1
+ /**
2
+ * 应用管理命令
3
+ */
4
+
5
+ import { Command } from 'commander'
6
+ import { configStore } from '../config/store'
7
+ import { createClient } from '../api/client'
8
+ import { OutputFormatter } from '../output/formatter'
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
+
21
+ export function makeAppCommand(): Command {
22
+ const cmd = new Command('app')
23
+ .description('Manage applications')
24
+
25
+ cmd
26
+ .command('ls')
27
+ .alias('list')
28
+ .description('List applications')
29
+ .option('-n, --name <name>', 'Filter by name')
30
+ .action(async (options) => {
31
+ const conn = configStore.getDefaultConnection()
32
+ if (!conn) {
33
+ OutputFormatter.error('No connection configured')
34
+ return
35
+ }
36
+
37
+ const client = createClient(conn)
38
+ const result = await client.listApplications()
39
+
40
+ if (!result.success) {
41
+ OutputFormatter.error(result.error!.message)
42
+ return
43
+ }
44
+
45
+ let apps = result.data || []
46
+ if (options.name) {
47
+ apps = apps.filter((app: any) => app.name.includes(options.name) || app.code.includes(options.name))
48
+ }
49
+
50
+ if (apps.length === 0) {
51
+ OutputFormatter.info('No applications found')
52
+ return
53
+ }
54
+
55
+ OutputFormatter.table(
56
+ ['Code', 'Name', 'Git Repo', 'Image Name', 'Updated'],
57
+ apps.map((app: any) => [
58
+ app.code,
59
+ app.name,
60
+ app.gitRepo.substring(0, 40) + (app.gitRepo.length > 40 ? '...' : ''),
61
+ app.imageName,
62
+ getAge(app.updatedAt)
63
+ ])
64
+ )
65
+ })
66
+
67
+ cmd
68
+ .command('get <code>')
69
+ .alias('describe')
70
+ .description('Get application details')
71
+ .action(async (code) => {
72
+ const conn = configStore.getDefaultConnection()
73
+ if (!conn) {
74
+ OutputFormatter.error('No connection configured')
75
+ return
76
+ }
77
+
78
+ const client = createClient(conn)
79
+ const result = await client.getApplication(code)
80
+
81
+ if (!result.success) {
82
+ OutputFormatter.error(result.error!.message)
83
+ return
84
+ }
85
+
86
+ const app = result.data!
87
+ OutputFormatter.info(`Application: ${app.name}`)
88
+ OutputFormatter.info(`Code: ${app.code}`)
89
+ OutputFormatter.info(`Git Repo: ${app.gitRepo}`)
90
+ OutputFormatter.info(`Git Branch: ${app.gitBranch}`)
91
+ OutputFormatter.info(`Image Name: ${app.imageName}`)
92
+ OutputFormatter.info(`Build Path: ${app.buildPath}`)
93
+ OutputFormatter.info(`Dockerfile: ${app.dockerfile}`)
94
+ if (app.jenkinsJobName) {
95
+ OutputFormatter.info(`Jenkins Job: ${app.jenkinsJobName}`)
96
+ }
97
+ OutputFormatter.info(`Created: ${app.createdAt}`)
98
+ OutputFormatter.info(`Updated: ${app.updatedAt}`)
99
+ })
100
+
101
+ cmd
102
+ .command('create')
103
+ .description('Create a new application')
104
+ .requiredOption('-n, --name <name>', 'Application name')
105
+ .option('-c, --code <code>', 'Application code (technical identifier)')
106
+ .option('-g, --git <url>', 'Git repository URL')
107
+ .option('-b, --branch <branch>', 'Git branch', 'main')
108
+ .option('-i, --image <image>', 'Container image name')
109
+ .option('--build-path <path>', 'Build path', '.')
110
+ .option('--dockerfile <path>', 'Dockerfile path', 'Dockerfile')
111
+ .action(async (options) => {
112
+ const conn = configStore.getDefaultConnection()
113
+ if (!conn) {
114
+ OutputFormatter.error('No connection configured')
115
+ return
116
+ }
117
+
118
+ const client = createClient(conn)
119
+ const result = await client.createApplication({
120
+ name: options.name,
121
+ code: options.code,
122
+ gitRepo: options.git,
123
+ gitBranch: options.branch,
124
+ imageName: options.image,
125
+ buildPath: options.buildPath,
126
+ dockerfile: options.dockerfile
127
+ })
128
+
129
+ if (!result.success) {
130
+ OutputFormatter.error(result.error!.message)
131
+ return
132
+ }
133
+
134
+ OutputFormatter.success(`Application "${result.data!.name}" created with code "${result.data!.code}"`)
135
+ })
136
+
137
+ cmd
138
+ .command('update <code>')
139
+ .description('Update an application')
140
+ .option('-n, --name <name>', 'Application name')
141
+ .option('-g, --git <url>', 'Git repository URL')
142
+ .option('-b, --branch <branch>', 'Git branch')
143
+ .option('-i, --image <image>', 'Container image name')
144
+ .option('--build-path <path>', 'Build path')
145
+ .option('--dockerfile <path>', 'Dockerfile path')
146
+ .action(async (code, options) => {
147
+ const conn = configStore.getDefaultConnection()
148
+ if (!conn) {
149
+ OutputFormatter.error('No connection configured')
150
+ return
151
+ }
152
+
153
+ const updateData: any = {}
154
+ if (options.name) updateData.name = options.name
155
+ if (options.git) updateData.gitRepo = options.git
156
+ if (options.branch) updateData.gitBranch = options.branch
157
+ if (options.image) updateData.imageName = options.image
158
+ if (options.buildPath) updateData.buildPath = options.buildPath
159
+ if (options.dockerfile) updateData.dockerfile = options.dockerfile
160
+
161
+ const client = createClient(conn)
162
+ const result = await client.updateApplication(code, updateData)
163
+
164
+ if (!result.success) {
165
+ OutputFormatter.error(result.error!.message)
166
+ return
167
+ }
168
+
169
+ OutputFormatter.success(`Application "${code}" updated`)
170
+ })
171
+
172
+ cmd
173
+ .command('rm <code>')
174
+ .alias('delete')
175
+ .description('Delete an application')
176
+ .action(async (code) => {
177
+ const conn = configStore.getDefaultConnection()
178
+ if (!conn) {
179
+ OutputFormatter.error('No connection configured')
180
+ return
181
+ }
182
+
183
+ const client = createClient(conn)
184
+ const result = await client.deleteApplication(code)
185
+
186
+ if (!result.success) {
187
+ OutputFormatter.error(result.error!.message)
188
+ return
189
+ }
190
+
191
+ OutputFormatter.success(`Application "${code}" deleted`)
192
+ })
193
+
194
+ cmd
195
+ .command('build <code>')
196
+ .description('Trigger a build for application')
197
+ .option('-b, --branch <branch>', 'Git branch')
198
+ .option('--sha <sha>', 'Commit SHA')
199
+ .option('--message <message>', 'Commit message')
200
+ .option('--author <author>', 'Commit author')
201
+ .action(async (code, options) => {
202
+ const conn = configStore.getDefaultConnection()
203
+ if (!conn) {
204
+ OutputFormatter.error('No connection configured')
205
+ return
206
+ }
207
+
208
+ const client = createClient(conn)
209
+ OutputFormatter.info('Starting build...')
210
+
211
+ const buildOptions: any = {}
212
+ if (options.branch) buildOptions.gitBranch = options.branch
213
+ if (options.sha) buildOptions.commitSha = options.sha
214
+ if (options.message) buildOptions.commitMessage = options.message
215
+ if (options.author) buildOptions.commitAuthor = options.author
216
+
217
+ const result = await client.triggerApplicationBuild(code, buildOptions)
218
+
219
+ if (!result.success) {
220
+ OutputFormatter.error(result.error!.message)
221
+ return
222
+ }
223
+
224
+ const build = result.data!
225
+ OutputFormatter.success(`Build #${build.buildNumber} started (ID: ${build.id})`)
226
+ })
227
+
228
+ cmd
229
+ .command('builds <code>')
230
+ .description('List builds for application')
231
+ .option('-s, --status <status>', 'Filter by status (SUCCESS|FAILED|RUNNING|PENDING)')
232
+ .action(async (code, options) => {
233
+ const conn = configStore.getDefaultConnection()
234
+ if (!conn) {
235
+ OutputFormatter.error('No connection configured')
236
+ return
237
+ }
238
+
239
+ const client = createClient(conn)
240
+ const result = await client.listApplicationBuilds(code)
241
+
242
+ if (!result.success) {
243
+ OutputFormatter.error(result.error!.message)
244
+ return
245
+ }
246
+
247
+ let builds = result.data || []
248
+ if (options.status) {
249
+ builds = builds.filter((b: any) => b.status === options.status)
250
+ }
251
+
252
+ if (builds.length === 0) {
253
+ OutputFormatter.info('No builds found')
254
+ return
255
+ }
256
+
257
+ OutputFormatter.table(
258
+ ['Build', 'Status', 'Image Tag', 'Created'],
259
+ builds.map((b: any) => [
260
+ `#${b.buildNumber}`,
261
+ b.status,
262
+ b.imageTag || '-',
263
+ getAge(b.createdAt)
264
+ ])
265
+ )
266
+ })
267
+
268
+ return cmd
269
+ }