xuanwu-cli 2.2.0 → 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.
Files changed (41) 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 +29 -4
  18. package/dist/api/client.js +113 -29
  19. package/dist/commands/app.js +44 -0
  20. package/dist/commands/auth/login.js +5 -4
  21. package/dist/commands/deploy.js +77 -49
  22. package/dist/commands/env.js +31 -48
  23. package/dist/commands/project.d.ts +5 -0
  24. package/dist/commands/project.js +134 -0
  25. package/dist/commands/svc.js +36 -0
  26. package/dist/config/types.d.ts +1 -0
  27. package/dist/index.js +2 -0
  28. package/jest.config.js +18 -0
  29. package/package.json +10 -2
  30. package/src/api/client.ts +142 -33
  31. package/src/commands/app.ts +53 -0
  32. package/src/commands/auth/login.ts +6 -4
  33. package/src/commands/deploy.ts +93 -48
  34. package/src/commands/env.ts +35 -52
  35. package/src/commands/project.ts +153 -0
  36. package/src/commands/svc.ts +40 -0
  37. package/src/config/types.ts +1 -0
  38. package/src/index.ts +2 -0
  39. package/test/cli-integration.sh +245 -0
  40. package/test/integration.js +3 -3
  41. package/test/integration.sh +252 -0
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * 项目管理命令
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.makeProjectCommand = makeProjectCommand;
7
+ const commander_1 = require("commander");
8
+ const store_1 = require("../config/store");
9
+ const client_1 = require("../api/client");
10
+ const formatter_1 = require("../output/formatter");
11
+ function getAge(timestamp) {
12
+ if (!timestamp)
13
+ return '-';
14
+ const created = new Date(timestamp);
15
+ const now = new Date();
16
+ const diff = Math.floor((now.getTime() - created.getTime()) / 1000);
17
+ if (diff < 60)
18
+ return `${diff}s`;
19
+ if (diff < 3600)
20
+ return `${Math.floor(diff / 60)}m`;
21
+ if (diff < 86400)
22
+ return `${Math.floor(diff / 3600)}h`;
23
+ return `${Math.floor(diff / 86400)}d`;
24
+ }
25
+ function makeProjectCommand() {
26
+ const cmd = new commander_1.Command('project')
27
+ .description('Manage projects');
28
+ cmd
29
+ .command('ls')
30
+ .alias('list')
31
+ .description('List projects')
32
+ .option('-n, --name <name>', 'Filter by name')
33
+ .option('-c, --code <code>', 'Filter by code')
34
+ .action(async (options) => {
35
+ const conn = store_1.configStore.getDefaultConnection();
36
+ if (!conn) {
37
+ formatter_1.OutputFormatter.error('No connection configured. Run: xw connect add <name> -e <endpoint> -t <token>');
38
+ return;
39
+ }
40
+ const client = (0, client_1.createClient)(conn);
41
+ const result = await client.listProjects({ name: options.name, code: options.code });
42
+ if (!result.success) {
43
+ formatter_1.OutputFormatter.error(result.error.message);
44
+ return;
45
+ }
46
+ const projects = result.data || [];
47
+ if (projects.length === 0) {
48
+ formatter_1.OutputFormatter.info('No projects found');
49
+ return;
50
+ }
51
+ formatter_1.OutputFormatter.table(['Code', 'Name', 'Environments', 'Members', 'Created'], projects.map((p) => [
52
+ p.code,
53
+ p.name,
54
+ p.environments?.length || 0,
55
+ p.members?.length || 0,
56
+ getAge(p.createdAt)
57
+ ]));
58
+ });
59
+ cmd
60
+ .command('get <code>')
61
+ .alias('describe')
62
+ .description('Get project details')
63
+ .action(async (code) => {
64
+ const conn = store_1.configStore.getDefaultConnection();
65
+ if (!conn) {
66
+ formatter_1.OutputFormatter.error('No connection configured');
67
+ return;
68
+ }
69
+ const client = (0, client_1.createClient)(conn);
70
+ const result = await client.getProject(code);
71
+ if (!result.success) {
72
+ formatter_1.OutputFormatter.error(result.error.message);
73
+ return;
74
+ }
75
+ const project = result.data;
76
+ formatter_1.OutputFormatter.info(`Code: ${project.code}`);
77
+ formatter_1.OutputFormatter.info(`Name: ${project.name}`);
78
+ formatter_1.OutputFormatter.info(`Description: ${project.description || '-'}`);
79
+ formatter_1.OutputFormatter.info(`Environments: ${project.environments?.length || 0}`);
80
+ formatter_1.OutputFormatter.info(`Members: ${project.members?.length || 0}`);
81
+ formatter_1.OutputFormatter.info(`Created: ${project.createdAt}`);
82
+ if (project.environments && project.environments.length > 0) {
83
+ formatter_1.OutputFormatter.info('\nEnvironments:');
84
+ project.environments.forEach((env) => {
85
+ formatter_1.OutputFormatter.info(` - ${env.name} (${env.namespace})`);
86
+ });
87
+ }
88
+ if (project.members && project.members.length > 0) {
89
+ formatter_1.OutputFormatter.info('\nMembers:');
90
+ project.members.forEach((m) => {
91
+ formatter_1.OutputFormatter.info(` - ${m.user.name || m.user.email} (${m.role})`);
92
+ });
93
+ }
94
+ });
95
+ cmd
96
+ .command('create')
97
+ .description('Create a new project')
98
+ .requiredOption('-n, --name <name>', 'Project name')
99
+ .requiredOption('-c, --code <code>', 'Project code (unique identifier, cannot be changed after creation)')
100
+ .option('-d, --description <description>', 'Project description')
101
+ .action(async (options) => {
102
+ const conn = store_1.configStore.getDefaultConnection();
103
+ if (!conn) {
104
+ formatter_1.OutputFormatter.error('No connection configured');
105
+ return;
106
+ }
107
+ const client = (0, client_1.createClient)(conn);
108
+ const result = await client.createProject(options.name, options.code, options.description);
109
+ if (!result.success) {
110
+ formatter_1.OutputFormatter.error(result.error.message);
111
+ return;
112
+ }
113
+ formatter_1.OutputFormatter.success(`Project "${options.name}" created with code "${options.code}"`);
114
+ });
115
+ cmd
116
+ .command('rm <code>')
117
+ .alias('delete')
118
+ .description('Delete a project')
119
+ .action(async (code) => {
120
+ const conn = store_1.configStore.getDefaultConnection();
121
+ if (!conn) {
122
+ formatter_1.OutputFormatter.error('No connection configured');
123
+ return;
124
+ }
125
+ const client = (0, client_1.createClient)(conn);
126
+ const result = await client.deleteProject(code);
127
+ if (!result.success) {
128
+ formatter_1.OutputFormatter.error(result.error.message);
129
+ return;
130
+ }
131
+ formatter_1.OutputFormatter.success(`Project "${code}" deleted`);
132
+ });
133
+ return cmd;
134
+ }
@@ -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')
@@ -36,6 +36,7 @@ export interface DeployOptions {
36
36
  namespace: string;
37
37
  serviceName: string;
38
38
  type: 'application' | 'database' | 'image';
39
+ projectCode?: string;
39
40
  port?: number;
40
41
  replicas?: number;
41
42
  cpu?: string;
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const commander_1 = require("commander");
7
7
  const connect_1 = require("./commands/connect");
8
+ const project_1 = require("./commands/project");
8
9
  const env_1 = require("./commands/env");
9
10
  const deploy_1 = require("./commands/deploy");
10
11
  const app_1 = require("./commands/app");
@@ -24,6 +25,7 @@ program
24
25
  .version('2.0.0')
25
26
  .option('-o, --output <format>', 'Output format (human|json)', 'human');
26
27
  program.addCommand((0, connect_1.makeConnectCommand)());
28
+ program.addCommand((0, project_1.makeProjectCommand)());
27
29
  program.addCommand((0, env_1.makeEnvCommand)());
28
30
  program.addCommand((0, app_1.makeAppCommand)());
29
31
  program.addCommand((0, svc_1.makeSvcCommand)());
package/jest.config.js ADDED
@@ -0,0 +1,18 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src', '<rootDir>/__tests__'],
5
+ testMatch: ['**/*.test.ts', '**/*.spec.ts'],
6
+ collectCoverageFrom: [
7
+ 'src/**/*.ts',
8
+ '!src/**/*.d.ts',
9
+ '!src/index.ts'
10
+ ],
11
+ coverageDirectory: 'coverage',
12
+ coverageReporters: ['text', 'lcov', 'html'],
13
+ moduleFileExtensions: ['ts', 'js', 'json', 'node'],
14
+ testTimeout: 30000,
15
+ setupFilesAfterEnv: ['<rootDir>/__tests__/setup.ts'],
16
+ globalSetup: '<rootDir>/__tests__/global-setup.ts',
17
+ globalTeardown: '<rootDir>/__tests__/global-teardown.ts'
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xuanwu-cli",
3
- "version": "2.2.0",
3
+ "version": "2.3.3",
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
  // ===================
@@ -459,6 +554,14 @@ export class APIClient {
459
554
  return this.request('GET', `/api/cli/services/${namespace}/${name}/pods`)
460
555
  }
461
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
+
462
565
  async deleteK8sService(namespace: string, name: string): Promise<CLIResult<void>> {
463
566
  return this.request<void>('DELETE', `/api/cli/services/${namespace}/${name}`)
464
567
  }
@@ -483,6 +586,12 @@ export class APIClient {
483
586
  async cancelBuild(id: string): Promise<CLIResult<void>> {
484
587
  return this.request<void>('POST', `/api/cli/builds/${id}/cancel`)
485
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
+ }
486
595
  }
487
596
 
488
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
  }
@@ -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