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.
- package/.env.test.example +14 -0
- package/__tests__/E2E_TEST_REPORT.md +206 -0
- package/__tests__/README.md +322 -0
- package/__tests__/TEST_SUMMARY.md +215 -0
- package/__tests__/global-setup.ts +13 -0
- package/__tests__/global-teardown.ts +3 -0
- package/__tests__/helpers/test-utils.ts +70 -0
- package/__tests__/integration/app.integration.test.ts +363 -0
- package/__tests__/integration/auth.integration.test.ts +243 -0
- package/__tests__/integration/build.integration.test.ts +215 -0
- package/__tests__/integration/e2e.test.ts +267 -0
- package/__tests__/integration/service.integration.test.ts +267 -0
- package/__tests__/integration/webhook.integration.test.ts +246 -0
- package/__tests__/run-e2e.js +360 -0
- package/__tests__/setup.ts +9 -0
- package/bin/xuanwu +0 -0
- package/dist/api/client.d.ts +29 -4
- package/dist/api/client.js +113 -29
- package/dist/commands/app.js +44 -0
- package/dist/commands/auth/login.js +5 -4
- package/dist/commands/deploy.js +77 -49
- package/dist/commands/env.js +31 -48
- package/dist/commands/project.d.ts +5 -0
- package/dist/commands/project.js +134 -0
- package/dist/commands/svc.js +36 -0
- package/dist/config/types.d.ts +1 -0
- package/dist/index.js +2 -0
- package/jest.config.js +18 -0
- package/package.json +10 -2
- package/src/api/client.ts +142 -33
- package/src/commands/app.ts +53 -0
- package/src/commands/auth/login.ts +6 -4
- package/src/commands/deploy.ts +93 -48
- package/src/commands/env.ts +35 -52
- package/src/commands/project.ts +153 -0
- package/src/commands/svc.ts +40 -0
- package/src/config/types.ts +1 -0
- package/src/index.ts +2 -0
- package/test/cli-integration.sh +245 -0
- package/test/integration.js +3 -3
- 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
|
+
}
|
package/dist/commands/svc.js
CHANGED
|
@@ -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/dist/config/types.d.ts
CHANGED
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.
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
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/
|
|
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 {
|
package/src/commands/app.ts
CHANGED
|
@@ -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', '
|
|
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(`已打开浏览器: ${
|
|
51
|
+
console.log(`已打开浏览器: ${fullLoginUrl}`)
|
|
50
52
|
OutputFormatter.info('请在浏览器中完成授权')
|
|
51
53
|
OutputFormatter.info('等待授权...')
|
|
52
54
|
|
|
53
55
|
try {
|
|
54
|
-
await open(
|
|
56
|
+
await open(fullLoginUrl)
|
|
55
57
|
} catch (error) {
|
|
56
|
-
console.log(`如果浏览器未打开,请手动访问: ${
|
|
58
|
+
console.log(`如果浏览器未打开,请手动访问: ${fullLoginUrl}`)
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const maxAttempts = 60
|