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.
Files changed (37) 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 +23 -4
  18. package/dist/api/client.js +104 -29
  19. package/dist/commands/auth/login.js +5 -4
  20. package/dist/commands/deploy.js +25 -49
  21. package/dist/commands/env.js +31 -48
  22. package/dist/commands/project.d.ts +5 -0
  23. package/dist/commands/project.js +134 -0
  24. package/dist/config/types.d.ts +1 -0
  25. package/dist/index.js +2 -0
  26. package/jest.config.js +18 -0
  27. package/package.json +10 -2
  28. package/src/api/client.ts +128 -33
  29. package/src/commands/auth/login.ts +6 -4
  30. package/src/commands/deploy.ts +32 -49
  31. package/src/commands/env.ts +35 -52
  32. package/src/commands/project.ts +153 -0
  33. package/src/config/types.ts +1 -0
  34. package/src/index.ts +2 -0
  35. package/test/cli-integration.sh +245 -0
  36. package/test/integration.js +3 -3
  37. 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) {
@@ -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,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('<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')
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 (namespace, serviceName, options) => {
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 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;
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
- const result = await client.deploy(deployOptions);
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;
@@ -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;
@@ -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
+ }
@@ -36,6 +36,7 @@ export interface DeployOptions {
36
36
  namespace: string;
37
37
  serviceName: string;
38
38
  type: 'application' | 'database' | 'image';
39
+ projectCode?: string;
39
40
  port?: number;
40
41
  replicas?: number;
41
42
  cpu?: string;
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const commander_1 = require("commander");
7
7
  const connect_1 = require("./commands/connect");
8
+ const project_1 = require("./commands/project");
8
9
  const env_1 = require("./commands/env");
9
10
  const deploy_1 = require("./commands/deploy");
10
11
  const app_1 = require("./commands/app");
@@ -24,6 +25,7 @@ program
24
25
  .version('2.0.0')
25
26
  .option('-o, --output <format>', 'Output format (human|json)', 'human');
26
27
  program.addCommand((0, connect_1.makeConnectCommand)());
28
+ program.addCommand((0, project_1.makeProjectCommand)());
27
29
  program.addCommand((0, env_1.makeEnvCommand)());
28
30
  program.addCommand((0, app_1.makeAppCommand)());
29
31
  program.addCommand((0, svc_1.makeSvcCommand)());
package/jest.config.js ADDED
@@ -0,0 +1,18 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src', '<rootDir>/__tests__'],
5
+ testMatch: ['**/*.test.ts', '**/*.spec.ts'],
6
+ collectCoverageFrom: [
7
+ 'src/**/*.ts',
8
+ '!src/**/*.d.ts',
9
+ '!src/index.ts'
10
+ ],
11
+ coverageDirectory: 'coverage',
12
+ coverageReporters: ['text', 'lcov', 'html'],
13
+ moduleFileExtensions: ['ts', 'js', 'json', 'node'],
14
+ testTimeout: 30000,
15
+ setupFilesAfterEnv: ['<rootDir>/__tests__/setup.ts'],
16
+ globalSetup: '<rootDir>/__tests__/global-setup.ts',
17
+ globalTeardown: '<rootDir>/__tests__/global-teardown.ts'
18
+ }