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
@@ -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
- const message = error.response?.data?.message || error.message;
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
- const envResult = await this.getNamespaceInfo(namespace);
121
- if (envResult.success && envResult.data) {
122
- const envData = Array.isArray(envResult.data) ? envResult.data[0] : envResult.data;
123
- projectId = envData.project?.id || envData.projectId;
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.getProjects();
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/applications', dto);
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) {
@@ -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', 'https://i.xuanwu.dev.aimstek.cn')
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(`已打开浏览器: ${loginUrl}`);
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)(loginUrl);
45
+ await (0, open_1.default)(fullLoginUrl);
45
46
  }
46
47
  catch (error) {
47
- console.log(`如果浏览器未打开,请手动访问: ${loginUrl}`);
48
+ console.log(`如果浏览器未打开,请手动访问: ${fullLoginUrl}`);
48
49
  }
49
50
  const maxAttempts = 60;
50
51
  let attempts = 0;
@@ -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
- .argument('<namespace>', 'Target namespace')
15
- .argument('<service-name>', 'Service name')
16
- .option('-t, --type <type>', 'Service type (application|database|image)', 'image')
17
- .option('--git <url>', 'Git repository URL')
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')
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 (namespace, serviceName, options) => {
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 type = options.type;
46
- const deployOptions = {
47
- namespace,
48
- serviceName,
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.deploy(deployOptions);
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;
@@ -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
- .action(async () => {
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: xuanwu connect add <name> -e <endpoint> -t <token>');
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.listNamespaces();
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', 'Identifier', 'Environment', 'Status'], spaces.map((s) => [
35
- s.name || s.identifier,
36
- s.identifier,
37
- s.environment,
38
- s.status || 'active'
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('create <namespace>')
43
- .description('Create a new environment (K8s namespace)')
44
- .option('-p, --project-id <id>', 'Project ID')
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
- let projectId = options.projectId;
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
- formatter_1.OutputFormatter.success(`Environment "${namespace}" created`);
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('rm <namespace>')
73
- .description('Delete an environment')
74
- .action(async (namespace) => {
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
- // 先获取 namespace ID
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}" deleted`);
81
+ formatter_1.OutputFormatter.success(`Environment "${namespace}" created`);
95
82
  });
96
83
  cmd
97
- .command('info <namespace>')
98
- .description('Show environment details')
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.getNamespaceInfo(namespace);
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
- const info = result.data;
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,5 @@
1
+ /**
2
+ * 项目管理命令
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare function makeProjectCommand(): Command;