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
package/dist/api/client.js
CHANGED
|
@@ -10,8 +10,11 @@ exports.APIClient = void 0;
|
|
|
10
10
|
exports.createClient = createClient;
|
|
11
11
|
const axios_1 = __importDefault(require("axios"));
|
|
12
12
|
const formatter_1 = require("../output/formatter");
|
|
13
|
+
const session_1 = require("../lib/session");
|
|
13
14
|
class APIClient {
|
|
14
15
|
constructor(connection) {
|
|
16
|
+
this.connection = connection;
|
|
17
|
+
this.sessionManager = new session_1.SessionManager();
|
|
15
18
|
this.client = axios_1.default.create({
|
|
16
19
|
baseURL: connection.endpoint,
|
|
17
20
|
headers: {
|
|
@@ -40,8 +43,23 @@ class APIClient {
|
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
catch (error) {
|
|
46
|
+
const status = error.response?.status;
|
|
43
47
|
const duration = Date.now() - startTime;
|
|
44
|
-
|
|
48
|
+
if (status === 401) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: {
|
|
52
|
+
code: '401',
|
|
53
|
+
message: '登录已过期,请运行 "xw login" 重新登录',
|
|
54
|
+
details: { needLogin: true }
|
|
55
|
+
},
|
|
56
|
+
meta: {
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
duration
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const message = error.response?.data?.error || error.response?.data?.message || error.message;
|
|
45
63
|
return {
|
|
46
64
|
success: false,
|
|
47
65
|
error: {
|
|
@@ -76,27 +94,6 @@ class APIClient {
|
|
|
76
94
|
async getNamespaceInfo(identifier) {
|
|
77
95
|
return this.request('GET', `/api/deploy-spaces?identifier=${identifier}`);
|
|
78
96
|
}
|
|
79
|
-
async getProjects() {
|
|
80
|
-
return this.request('GET', '/api/projects');
|
|
81
|
-
}
|
|
82
|
-
async createProject(name, description) {
|
|
83
|
-
return this.request('POST', '/api/projects', {
|
|
84
|
-
name,
|
|
85
|
-
description: description || ''
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
async deleteProject(projectId) {
|
|
89
|
-
return this.request('DELETE', `/api/projects/${projectId}`);
|
|
90
|
-
}
|
|
91
|
-
async createNamespaceWithProject(name, projectId, environment = 'development') {
|
|
92
|
-
return this.request('POST', '/api/deploy-spaces', {
|
|
93
|
-
name,
|
|
94
|
-
identifier: name,
|
|
95
|
-
namespace: name,
|
|
96
|
-
environment,
|
|
97
|
-
project_id: projectId
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
97
|
// ===================
|
|
101
98
|
// Service
|
|
102
99
|
// ===================
|
|
@@ -113,14 +110,23 @@ class APIClient {
|
|
|
113
110
|
// Deploy
|
|
114
111
|
// ===================
|
|
115
112
|
async deploy(options) {
|
|
116
|
-
const { namespace, serviceName, type, ...rest } = options;
|
|
113
|
+
const { namespace, serviceName, type, projectCode, ...rest } = options;
|
|
117
114
|
// 获取 namespace 对应的 project_id
|
|
118
115
|
let projectId;
|
|
116
|
+
// 如果提供了 projectCode,直接通过 projectCode 获取项目
|
|
117
|
+
if (projectCode) {
|
|
118
|
+
const projectResult = await this.getProject(projectCode);
|
|
119
|
+
if (projectResult.success && projectResult.data) {
|
|
120
|
+
projectId = projectResult.data.id;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
119
123
|
// 尝试通过 identifier 查询
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
if (!projectId) {
|
|
125
|
+
const envResult = await this.getNamespaceInfo(namespace);
|
|
126
|
+
if (envResult.success && envResult.data) {
|
|
127
|
+
const envData = Array.isArray(envResult.data) ? envResult.data[0] : envResult.data;
|
|
128
|
+
projectId = envData.project?.id || envData.projectId;
|
|
129
|
+
}
|
|
124
130
|
}
|
|
125
131
|
// 如果没找到,尝试从列表中查找
|
|
126
132
|
if (!projectId) {
|
|
@@ -134,7 +140,7 @@ class APIClient {
|
|
|
134
140
|
}
|
|
135
141
|
// 如果还是没有,使用第一个项目的 ID
|
|
136
142
|
if (!projectId) {
|
|
137
|
-
const projectsResult = await this.
|
|
143
|
+
const projectsResult = await this.listProjects();
|
|
138
144
|
if (projectsResult.success && projectsResult.data && projectsResult.data.length > 0) {
|
|
139
145
|
projectId = projectsResult.data[0].id;
|
|
140
146
|
}
|
|
@@ -344,7 +350,7 @@ class APIClient {
|
|
|
344
350
|
return this.request('GET', `/api/cli/apps/${code}`);
|
|
345
351
|
}
|
|
346
352
|
async createApplication(dto) {
|
|
347
|
-
return this.request('POST', '/api/
|
|
353
|
+
return this.request('POST', '/api/cli/apps', dto);
|
|
348
354
|
}
|
|
349
355
|
async updateApplication(code, dto) {
|
|
350
356
|
return this.request('PUT', `/api/cli/apps/${code}`, dto);
|
|
@@ -359,6 +365,75 @@ class APIClient {
|
|
|
359
365
|
return this.request('GET', `/api/cli/apps/${code}/builds`);
|
|
360
366
|
}
|
|
361
367
|
// ===================
|
|
368
|
+
// Project (CLI API - using code)
|
|
369
|
+
// ===================
|
|
370
|
+
async listProjects(options) {
|
|
371
|
+
let url = '/api/cli/projects';
|
|
372
|
+
const params = [];
|
|
373
|
+
if (options?.name)
|
|
374
|
+
params.push(`name=${options.name}`);
|
|
375
|
+
if (options?.code)
|
|
376
|
+
params.push(`code=${options.code}`);
|
|
377
|
+
if (params.length > 0)
|
|
378
|
+
url += '?' + params.join('&');
|
|
379
|
+
return this.request('GET', url);
|
|
380
|
+
}
|
|
381
|
+
async getProject(code) {
|
|
382
|
+
return this.request('GET', `/api/cli/projects/${code}`);
|
|
383
|
+
}
|
|
384
|
+
async createProject(name, code, description) {
|
|
385
|
+
return this.request('POST', '/api/cli/projects', {
|
|
386
|
+
name,
|
|
387
|
+
code,
|
|
388
|
+
description: description || ''
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
async deleteProject(code) {
|
|
392
|
+
return this.request('DELETE', `/api/cli/projects/${code}`);
|
|
393
|
+
}
|
|
394
|
+
// ===================
|
|
395
|
+
// Environment (CLI API - using namespace)
|
|
396
|
+
// ===================
|
|
397
|
+
async listEnvironments(options) {
|
|
398
|
+
let url = '/api/cli/envs';
|
|
399
|
+
const params = [];
|
|
400
|
+
if (options?.project)
|
|
401
|
+
params.push(`project=${options.project}`);
|
|
402
|
+
if (options?.name)
|
|
403
|
+
params.push(`name=${options.name}`);
|
|
404
|
+
if (params.length > 0)
|
|
405
|
+
url += '?' + params.join('&');
|
|
406
|
+
return this.request('GET', url);
|
|
407
|
+
}
|
|
408
|
+
async getEnvironment(namespace) {
|
|
409
|
+
return this.request('GET', `/api/cli/envs/${namespace}`);
|
|
410
|
+
}
|
|
411
|
+
async createEnvironment(name, namespace, projectCode) {
|
|
412
|
+
return this.request('POST', '/api/cli/envs', {
|
|
413
|
+
name,
|
|
414
|
+
namespace,
|
|
415
|
+
projectCode
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async deleteEnvironment(namespace) {
|
|
419
|
+
return this.request('DELETE', `/api/cli/envs/${namespace}`);
|
|
420
|
+
}
|
|
421
|
+
// ===================
|
|
422
|
+
// Deployment (CLI API)
|
|
423
|
+
// ===================
|
|
424
|
+
async deployService(namespace, name, image, options) {
|
|
425
|
+
return this.request('POST', `/api/cli/services/${namespace}/${name}/deploy`, {
|
|
426
|
+
image,
|
|
427
|
+
projectCode: options?.projectCode,
|
|
428
|
+
replicas: options?.replicas,
|
|
429
|
+
port: options?.port,
|
|
430
|
+
env: options?.env
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
async listServiceDeployments(namespace, name) {
|
|
434
|
+
return this.request('GET', `/api/cli/services/${namespace}/${name}/deployments`);
|
|
435
|
+
}
|
|
436
|
+
// ===================
|
|
362
437
|
// Service (CLI API - using ns/name)
|
|
363
438
|
// ===================
|
|
364
439
|
async listK8sServices(namespace) {
|
|
@@ -394,6 +469,9 @@ class APIClient {
|
|
|
394
469
|
async listK8sServicePods(namespace, name) {
|
|
395
470
|
return this.request('GET', `/api/cli/services/${namespace}/${name}/pods`);
|
|
396
471
|
}
|
|
472
|
+
async updateK8sService(namespace, name, options) {
|
|
473
|
+
return this.request('PUT', `/api/cli/services/${namespace}/${name}`, options);
|
|
474
|
+
}
|
|
397
475
|
async deleteK8sService(namespace, name) {
|
|
398
476
|
return this.request('DELETE', `/api/cli/services/${namespace}/${name}`);
|
|
399
477
|
}
|
|
@@ -417,6 +495,12 @@ class APIClient {
|
|
|
417
495
|
async cancelBuild(id) {
|
|
418
496
|
return this.request('POST', `/api/cli/builds/${id}/cancel`);
|
|
419
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
|
+
}
|
|
420
504
|
}
|
|
421
505
|
exports.APIClient = APIClient;
|
|
422
506
|
function createClient(connection) {
|
package/dist/commands/app.js
CHANGED
|
@@ -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
|
}
|
|
@@ -11,7 +11,7 @@ const formatter_1 = require("../../output/formatter");
|
|
|
11
11
|
function makeLoginCommand() {
|
|
12
12
|
const cmd = new commander_1.Command('login')
|
|
13
13
|
.description('Login to xuanwu factory')
|
|
14
|
-
.option('-u, --api-url <url>', 'API server URL', '
|
|
14
|
+
.option('-u, --api-url <url>', 'API server URL', 'http://xw.xuanwu-prod.dev.aimstek.cn')
|
|
15
15
|
.option('-e, --email <email>', 'Email address (for non-interactive login)')
|
|
16
16
|
.option('-p, --password <password>', 'Password (for non-interactive login)')
|
|
17
17
|
.option('--expires-in <duration>', 'Token expiration (30d, 90d, never)', '30d')
|
|
@@ -35,16 +35,17 @@ async function loginWithBrowser(sessionManager, apiUrl) {
|
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
const { sessionId, loginUrl, code } = await deviceAuthRes.json();
|
|
38
|
+
const fullLoginUrl = loginUrl.startsWith('http') ? loginUrl : `${finalApiUrl}${loginUrl}`;
|
|
38
39
|
formatter_1.OutputFormatter.info('正在生成设备授权码...');
|
|
39
40
|
console.log(`授权码: ${code}`);
|
|
40
|
-
console.log(`已打开浏览器: ${
|
|
41
|
+
console.log(`已打开浏览器: ${fullLoginUrl}`);
|
|
41
42
|
formatter_1.OutputFormatter.info('请在浏览器中完成授权');
|
|
42
43
|
formatter_1.OutputFormatter.info('等待授权...');
|
|
43
44
|
try {
|
|
44
|
-
await (0, open_1.default)(
|
|
45
|
+
await (0, open_1.default)(fullLoginUrl);
|
|
45
46
|
}
|
|
46
47
|
catch (error) {
|
|
47
|
-
console.log(`如果浏览器未打开,请手动访问: ${
|
|
48
|
+
console.log(`如果浏览器未打开,请手动访问: ${fullLoginUrl}`);
|
|
48
49
|
}
|
|
49
50
|
const maxAttempts = 60;
|
|
50
51
|
let attempts = 0;
|
package/dist/commands/deploy.js
CHANGED
|
@@ -8,25 +8,73 @@ const commander_1 = require("commander");
|
|
|
8
8
|
const store_1 = require("../config/store");
|
|
9
9
|
const client_1 = require("../api/client");
|
|
10
10
|
const formatter_1 = require("../output/formatter");
|
|
11
|
+
function parseNamespaceName(input) {
|
|
12
|
+
const parts = input.split('/');
|
|
13
|
+
if (parts.length !== 2) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return { namespace: parts[0], name: parts[1] };
|
|
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
|
+
}
|
|
11
32
|
function makeDeployCommand() {
|
|
12
33
|
const cmd = new commander_1.Command('deploy')
|
|
13
|
-
.description('Deploy services to environment')
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
74
|
+
.argument('<ns-name>', 'Target namespace and service name (format: namespace/service-name)')
|
|
75
|
+
.requiredOption('-i, --image <image>', 'Container image (required)')
|
|
76
|
+
.option('-p, --project <code>', 'Project code')
|
|
77
|
+
.option('--port <port>', 'Container port', '80')
|
|
30
78
|
.option('-r, --replicas <num>', 'Number of replicas', '1')
|
|
31
79
|
.option('--cpu <value>', 'CPU limit')
|
|
32
80
|
.option('--memory <value>', 'Memory limit')
|
|
@@ -36,45 +84,25 @@ function makeDeployCommand() {
|
|
|
36
84
|
memo[key] = value;
|
|
37
85
|
return memo;
|
|
38
86
|
}, {})
|
|
39
|
-
.action(async (
|
|
87
|
+
.action(async (nsName, options) => {
|
|
40
88
|
const conn = store_1.configStore.getDefaultConnection();
|
|
41
89
|
if (!conn) {
|
|
42
90
|
formatter_1.OutputFormatter.error('No connection configured');
|
|
43
91
|
return;
|
|
44
92
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
namespace
|
|
48
|
-
|
|
49
|
-
type,
|
|
50
|
-
port: options.port ? parseInt(options.port) : undefined,
|
|
51
|
-
replicas: options.replicas ? parseInt(options.replicas) : undefined,
|
|
52
|
-
cpu: options.cpu,
|
|
53
|
-
memory: options.memory,
|
|
54
|
-
domain: options.domain,
|
|
55
|
-
envVars: options.env
|
|
56
|
-
};
|
|
57
|
-
// 根据类型添加配置
|
|
58
|
-
if (type === 'application') {
|
|
59
|
-
deployOptions.git = options.git;
|
|
60
|
-
deployOptions.gitBranch = options.gitBranch;
|
|
61
|
-
deployOptions.buildType = options.buildType;
|
|
62
|
-
deployOptions.language = options.language;
|
|
63
|
-
deployOptions.dockerfilePath = options.dockerfilePath;
|
|
64
|
-
}
|
|
65
|
-
else if (type === 'database') {
|
|
66
|
-
deployOptions.dbType = options.dbType;
|
|
67
|
-
deployOptions.dbVersion = options.dbVersion;
|
|
68
|
-
deployOptions.rootPassword = options.rootPassword;
|
|
69
|
-
deployOptions.password = options.password;
|
|
70
|
-
deployOptions.user = options.user;
|
|
71
|
-
deployOptions.database = options.database;
|
|
72
|
-
}
|
|
73
|
-
else if (type === 'image') {
|
|
74
|
-
deployOptions.image = options.image;
|
|
93
|
+
const parsed = parseNamespaceName(nsName);
|
|
94
|
+
if (!parsed) {
|
|
95
|
+
formatter_1.OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>');
|
|
96
|
+
return;
|
|
75
97
|
}
|
|
98
|
+
const { namespace, name: serviceName } = parsed;
|
|
76
99
|
const client = (0, client_1.createClient)(conn);
|
|
77
|
-
const result = await client.
|
|
100
|
+
const result = await client.deployService(namespace, serviceName, options.image, {
|
|
101
|
+
projectCode: options.project,
|
|
102
|
+
replicas: options.replicas ? parseInt(options.replicas) : 1,
|
|
103
|
+
port: options.port ? parseInt(options.port) : 80,
|
|
104
|
+
env: options.env
|
|
105
|
+
});
|
|
78
106
|
if (!result.success) {
|
|
79
107
|
formatter_1.OutputFormatter.error(result.error.message);
|
|
80
108
|
return;
|
package/dist/commands/env.js
CHANGED
|
@@ -13,15 +13,17 @@ function makeEnvCommand() {
|
|
|
13
13
|
.description('Manage environments (K8s namespaces)');
|
|
14
14
|
cmd
|
|
15
15
|
.command('ls')
|
|
16
|
+
.alias('list')
|
|
16
17
|
.description('List accessible environments')
|
|
17
|
-
.
|
|
18
|
+
.option('-p, --project <project-code>', 'Filter by project code')
|
|
19
|
+
.action(async (options) => {
|
|
18
20
|
const conn = store_1.configStore.getDefaultConnection();
|
|
19
21
|
if (!conn) {
|
|
20
|
-
formatter_1.OutputFormatter.error('No connection configured. Run:
|
|
22
|
+
formatter_1.OutputFormatter.error('No connection configured. Run: xw connect add <name> -e <endpoint> -t <token>');
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
23
25
|
const client = (0, client_1.createClient)(conn);
|
|
24
|
-
const result = await client.
|
|
26
|
+
const result = await client.listEnvironments({ project: options.project });
|
|
25
27
|
if (!result.success) {
|
|
26
28
|
formatter_1.OutputFormatter.error(result.error.message);
|
|
27
29
|
return;
|
|
@@ -31,71 +33,57 @@ function makeEnvCommand() {
|
|
|
31
33
|
formatter_1.OutputFormatter.info('No environments found');
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
|
-
formatter_1.OutputFormatter.table(['Name', '
|
|
35
|
-
s.name
|
|
36
|
-
s.
|
|
37
|
-
s.
|
|
38
|
-
s.
|
|
36
|
+
formatter_1.OutputFormatter.table(['Name', 'Namespace', 'Project', 'Services'], spaces.map((s) => [
|
|
37
|
+
s.name,
|
|
38
|
+
s.namespace,
|
|
39
|
+
s.project?.name || '-',
|
|
40
|
+
s._count?.services || 0
|
|
39
41
|
]));
|
|
40
42
|
});
|
|
41
43
|
cmd
|
|
42
|
-
.command('
|
|
43
|
-
.description('
|
|
44
|
-
.
|
|
45
|
-
.option('-e, --environment <env>', 'Environment type (development|staging|production)', 'development')
|
|
46
|
-
.action(async (namespace, options) => {
|
|
44
|
+
.command('get <namespace>')
|
|
45
|
+
.description('Get environment details')
|
|
46
|
+
.action(async (namespace) => {
|
|
47
47
|
const conn = store_1.configStore.getDefaultConnection();
|
|
48
48
|
if (!conn) {
|
|
49
49
|
formatter_1.OutputFormatter.error('No connection configured');
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
const client = (0, client_1.createClient)(conn);
|
|
53
|
-
|
|
54
|
-
// 如果没有指定 project-id,尝试获取第一个项目
|
|
55
|
-
if (!projectId) {
|
|
56
|
-
const projectsResult = await client.getProjects();
|
|
57
|
-
if (!projectsResult.success || !projectsResult.data || projectsResult.data.length === 0) {
|
|
58
|
-
formatter_1.OutputFormatter.error('No projects found. Please specify --project-id');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
projectId = projectsResult.data[0].id;
|
|
62
|
-
formatter_1.OutputFormatter.info(`Using project: ${projectsResult.data[0].name}`);
|
|
63
|
-
}
|
|
64
|
-
const result = await client.createNamespaceWithProject(namespace, projectId, options.environment);
|
|
53
|
+
const result = await client.getEnvironment(namespace);
|
|
65
54
|
if (!result.success) {
|
|
66
55
|
formatter_1.OutputFormatter.error(result.error.message);
|
|
67
56
|
return;
|
|
68
57
|
}
|
|
69
|
-
|
|
58
|
+
const env = result.data;
|
|
59
|
+
formatter_1.OutputFormatter.info(`Name: ${env.name}`);
|
|
60
|
+
formatter_1.OutputFormatter.info(`Namespace: ${env.namespace}`);
|
|
61
|
+
formatter_1.OutputFormatter.info(`Project: ${env.project?.name || '-'}`);
|
|
62
|
+
formatter_1.OutputFormatter.info(`Services: ${env.services?.length || 0}`);
|
|
70
63
|
});
|
|
71
64
|
cmd
|
|
72
|
-
.command('
|
|
73
|
-
.description('
|
|
74
|
-
.
|
|
65
|
+
.command('create <namespace>')
|
|
66
|
+
.description('Create a new environment (K8s namespace)')
|
|
67
|
+
.option('-n, --name <name>', 'Environment name')
|
|
68
|
+
.option('-p, --project <project-code>', 'Project code')
|
|
69
|
+
.action(async (namespace, options) => {
|
|
75
70
|
const conn = store_1.configStore.getDefaultConnection();
|
|
76
71
|
if (!conn) {
|
|
77
72
|
formatter_1.OutputFormatter.error('No connection configured');
|
|
78
73
|
return;
|
|
79
74
|
}
|
|
80
75
|
const client = (0, client_1.createClient)(conn);
|
|
81
|
-
|
|
82
|
-
const infoResult = await client.getNamespaceInfo(namespace);
|
|
83
|
-
if (!infoResult.success || !infoResult.data) {
|
|
84
|
-
formatter_1.OutputFormatter.error(`Environment "${namespace}" not found`);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const envData = Array.isArray(infoResult.data) ? infoResult.data[0] : infoResult.data;
|
|
88
|
-
const id = envData.id;
|
|
89
|
-
const result = await client.deleteNamespace(id);
|
|
76
|
+
const result = await client.createEnvironment(options.name || namespace, namespace, options.project);
|
|
90
77
|
if (!result.success) {
|
|
91
78
|
formatter_1.OutputFormatter.error(result.error.message);
|
|
92
79
|
return;
|
|
93
80
|
}
|
|
94
|
-
formatter_1.OutputFormatter.success(`Environment "${namespace}"
|
|
81
|
+
formatter_1.OutputFormatter.success(`Environment "${namespace}" created`);
|
|
95
82
|
});
|
|
96
83
|
cmd
|
|
97
|
-
.command('
|
|
98
|
-
.
|
|
84
|
+
.command('rm <namespace>')
|
|
85
|
+
.alias('delete')
|
|
86
|
+
.description('Delete an environment')
|
|
99
87
|
.action(async (namespace) => {
|
|
100
88
|
const conn = store_1.configStore.getDefaultConnection();
|
|
101
89
|
if (!conn) {
|
|
@@ -103,17 +91,12 @@ function makeEnvCommand() {
|
|
|
103
91
|
return;
|
|
104
92
|
}
|
|
105
93
|
const client = (0, client_1.createClient)(conn);
|
|
106
|
-
const result = await client.
|
|
94
|
+
const result = await client.deleteEnvironment(namespace);
|
|
107
95
|
if (!result.success) {
|
|
108
96
|
formatter_1.OutputFormatter.error(result.error.message);
|
|
109
97
|
return;
|
|
110
98
|
}
|
|
111
|
-
|
|
112
|
-
formatter_1.OutputFormatter.info(`Name: ${info.name}`);
|
|
113
|
-
formatter_1.OutputFormatter.info(`Identifier: ${info.identifier}`);
|
|
114
|
-
formatter_1.OutputFormatter.info(`Namespace: ${info.namespace}`);
|
|
115
|
-
formatter_1.OutputFormatter.info(`Environment: ${info.environment}`);
|
|
116
|
-
formatter_1.OutputFormatter.info(`Status: ${info.status}`);
|
|
99
|
+
formatter_1.OutputFormatter.success(`Environment "${namespace}" deleted`);
|
|
117
100
|
});
|
|
118
101
|
return cmd;
|
|
119
102
|
}
|