xuanwu-cli 2.2.0 → 2.3.2
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 +23 -4
- package/dist/api/client.js +104 -29
- package/dist/commands/auth/login.js +5 -4
- package/dist/commands/deploy.js +25 -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/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 +128 -33
- package/src/commands/auth/login.ts +6 -4
- package/src/commands/deploy.ts +32 -49
- package/src/commands/env.ts +35 -52
- package/src/commands/project.ts +153 -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) {
|
|
@@ -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,20 @@ 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
|
+
}
|
|
11
18
|
function makeDeployCommand() {
|
|
12
19
|
const cmd = new commander_1.Command('deploy')
|
|
13
|
-
.description('Deploy services to environment')
|
|
14
|
-
.argument('<
|
|
15
|
-
.
|
|
16
|
-
.option('-
|
|
17
|
-
.option('--
|
|
18
|
-
.option('--git-branch <branch>', 'Git branch', 'main')
|
|
19
|
-
.option('--build-type <type>', 'Build type (template|dockerfile)', 'template')
|
|
20
|
-
.option('--language <lang>', 'Language (java-springboot|nodejs|python|golang)')
|
|
21
|
-
.option('--dockerfile-path <path>', 'Dockerfile path')
|
|
22
|
-
.option('--db-type <type>', 'Database type (mysql|redis|postgres|elasticsearch)')
|
|
23
|
-
.option('--db-version <version>', 'Database version')
|
|
24
|
-
.option('--root-password <password>', 'Root password')
|
|
25
|
-
.option('--password <password>', 'Password')
|
|
26
|
-
.option('--user <user>', 'Username')
|
|
27
|
-
.option('--database <name>', 'Database name')
|
|
28
|
-
.option('--image <image>', 'Container image')
|
|
29
|
-
.option('-p, --port <port>', 'Container port')
|
|
20
|
+
.description('Deploy services to environment (auto-creates service if not exists)')
|
|
21
|
+
.argument('<ns-name>', 'Target namespace and service name (format: namespace/service-name)')
|
|
22
|
+
.requiredOption('-i, --image <image>', 'Container image (required)')
|
|
23
|
+
.option('-p, --project <code>', 'Project code')
|
|
24
|
+
.option('--port <port>', 'Container port', '80')
|
|
30
25
|
.option('-r, --replicas <num>', 'Number of replicas', '1')
|
|
31
26
|
.option('--cpu <value>', 'CPU limit')
|
|
32
27
|
.option('--memory <value>', 'Memory limit')
|
|
@@ -36,45 +31,26 @@ function makeDeployCommand() {
|
|
|
36
31
|
memo[key] = value;
|
|
37
32
|
return memo;
|
|
38
33
|
}, {})
|
|
39
|
-
.action(async (
|
|
34
|
+
.action(async (nsName, options) => {
|
|
40
35
|
const conn = store_1.configStore.getDefaultConnection();
|
|
41
36
|
if (!conn) {
|
|
42
37
|
formatter_1.OutputFormatter.error('No connection configured');
|
|
43
38
|
return;
|
|
44
39
|
}
|
|
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;
|
|
40
|
+
const parsed = parseNamespaceName(nsName);
|
|
41
|
+
if (!parsed) {
|
|
42
|
+
formatter_1.OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>');
|
|
43
|
+
return;
|
|
75
44
|
}
|
|
45
|
+
const { namespace, name: serviceName } = parsed;
|
|
76
46
|
const client = (0, client_1.createClient)(conn);
|
|
77
|
-
|
|
47
|
+
// 使用 CLI API 进行部署
|
|
48
|
+
const result = await client.deployService(namespace, serviceName, options.image, {
|
|
49
|
+
projectCode: options.project,
|
|
50
|
+
replicas: options.replicas ? parseInt(options.replicas) : 1,
|
|
51
|
+
port: options.port ? parseInt(options.port) : 80,
|
|
52
|
+
env: options.env
|
|
53
|
+
});
|
|
78
54
|
if (!result.success) {
|
|
79
55
|
formatter_1.OutputFormatter.error(result.error.message);
|
|
80
56
|
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
|
}
|
|
@@ -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/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
|
+
}
|