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/src/commands/deploy.ts
CHANGED
|
@@ -6,27 +6,85 @@ import { Command } from 'commander'
|
|
|
6
6
|
import { configStore } from '../config/store'
|
|
7
7
|
import { createClient } from '../api/client'
|
|
8
8
|
import { OutputFormatter } from '../output/formatter'
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
function parseNamespaceName(input: string): { namespace: string; name: string } | null {
|
|
11
|
+
const parts = input.split('/')
|
|
12
|
+
if (parts.length !== 2) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
return { namespace: parts[0], name: parts[1] }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getAge(timestamp?: string): string {
|
|
19
|
+
if (!timestamp) return '-'
|
|
20
|
+
const created = new Date(timestamp)
|
|
21
|
+
const now = new Date()
|
|
22
|
+
const diff = Math.floor((now.getTime() - created.getTime()) / 1000)
|
|
23
|
+
if (diff < 60) return `${diff}s`
|
|
24
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m`
|
|
25
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h`
|
|
26
|
+
return `${Math.floor(diff / 86400)}d`
|
|
27
|
+
}
|
|
10
28
|
|
|
11
29
|
export function makeDeployCommand(): Command {
|
|
12
30
|
const cmd = new Command('deploy')
|
|
13
31
|
.description('Deploy services to environment')
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
|
|
33
|
+
cmd
|
|
34
|
+
.command('history <ns-name>')
|
|
35
|
+
.description('Show deployment history for a service')
|
|
36
|
+
.action(async (nsName) => {
|
|
37
|
+
const conn = configStore.getDefaultConnection()
|
|
38
|
+
if (!conn) {
|
|
39
|
+
OutputFormatter.error('No connection configured')
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parsed = parseNamespaceName(nsName)
|
|
44
|
+
if (!parsed) {
|
|
45
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { namespace, name: serviceName } = parsed
|
|
50
|
+
|
|
51
|
+
const client = createClient(conn)
|
|
52
|
+
const result = await client.listServiceDeployments(namespace, serviceName)
|
|
53
|
+
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
OutputFormatter.error(result.error!.message)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = result.data as any
|
|
60
|
+
const deployments = data?.deployments || data || []
|
|
61
|
+
|
|
62
|
+
if (deployments.length === 0) {
|
|
63
|
+
OutputFormatter.info('No deployments found')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
OutputFormatter.table(
|
|
68
|
+
['ID', 'Image', 'Status', 'Build', 'Created'],
|
|
69
|
+
deployments.map((d: any) => [
|
|
70
|
+
d.id.substring(0, 8),
|
|
71
|
+
d.image?.substring(0, 40) || '-',
|
|
72
|
+
d.status || '-',
|
|
73
|
+
d.build?.buildNumber ? `#${d.build.buildNumber}` : '-',
|
|
74
|
+
getAge(d.createdAt)
|
|
75
|
+
])
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if (data?.pagination) {
|
|
79
|
+
OutputFormatter.info(`Page ${data.pagination.page}/${data.pagination.totalPages}, Total: ${data.pagination.total}`)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
cmd
|
|
84
|
+
.argument('<ns-name>', 'Target namespace and service name (format: namespace/service-name)')
|
|
85
|
+
.requiredOption('-i, --image <image>', 'Container image (required)')
|
|
86
|
+
.option('-p, --project <code>', 'Project code')
|
|
87
|
+
.option('--port <port>', 'Container port', '80')
|
|
30
88
|
.option('-r, --replicas <num>', 'Number of replicas', '1')
|
|
31
89
|
.option('--cpu <value>', 'CPU limit')
|
|
32
90
|
.option('--memory <value>', 'Memory limit')
|
|
@@ -36,47 +94,34 @@ export function makeDeployCommand(): Command {
|
|
|
36
94
|
memo[key] = value
|
|
37
95
|
return memo
|
|
38
96
|
}, {})
|
|
39
|
-
.action(async (
|
|
97
|
+
.action(async (nsName, options) => {
|
|
40
98
|
const conn = configStore.getDefaultConnection()
|
|
41
99
|
if (!conn) {
|
|
42
100
|
OutputFormatter.error('No connection configured')
|
|
43
101
|
return
|
|
44
102
|
}
|
|
45
103
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
serviceName,
|
|
51
|
-
type,
|
|
52
|
-
port: options.port ? parseInt(options.port) : undefined,
|
|
53
|
-
replicas: options.replicas ? parseInt(options.replicas) : undefined,
|
|
54
|
-
cpu: options.cpu,
|
|
55
|
-
memory: options.memory,
|
|
56
|
-
domain: options.domain,
|
|
57
|
-
envVars: options.env
|
|
104
|
+
const parsed = parseNamespaceName(nsName)
|
|
105
|
+
if (!parsed) {
|
|
106
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
107
|
+
return
|
|
58
108
|
}
|
|
59
109
|
|
|
60
|
-
|
|
61
|
-
if (type === 'application') {
|
|
62
|
-
deployOptions.git = options.git
|
|
63
|
-
deployOptions.gitBranch = options.gitBranch
|
|
64
|
-
deployOptions.buildType = options.buildType
|
|
65
|
-
deployOptions.language = options.language
|
|
66
|
-
deployOptions.dockerfilePath = options.dockerfilePath
|
|
67
|
-
} else if (type === 'database') {
|
|
68
|
-
deployOptions.dbType = options.dbType
|
|
69
|
-
deployOptions.dbVersion = options.dbVersion
|
|
70
|
-
deployOptions.rootPassword = options.rootPassword
|
|
71
|
-
deployOptions.password = options.password
|
|
72
|
-
deployOptions.user = options.user
|
|
73
|
-
deployOptions.database = options.database
|
|
74
|
-
} else if (type === 'image') {
|
|
75
|
-
deployOptions.image = options.image
|
|
76
|
-
}
|
|
110
|
+
const { namespace, name: serviceName } = parsed
|
|
77
111
|
|
|
78
112
|
const client = createClient(conn)
|
|
79
|
-
|
|
113
|
+
|
|
114
|
+
const result = await client.deployService(
|
|
115
|
+
namespace,
|
|
116
|
+
serviceName,
|
|
117
|
+
options.image,
|
|
118
|
+
{
|
|
119
|
+
projectCode: options.project,
|
|
120
|
+
replicas: options.replicas ? parseInt(options.replicas) : 1,
|
|
121
|
+
port: options.port ? parseInt(options.port) : 80,
|
|
122
|
+
env: options.env
|
|
123
|
+
}
|
|
124
|
+
)
|
|
80
125
|
|
|
81
126
|
if (!result.success) {
|
|
82
127
|
OutputFormatter.error(result.error!.message)
|
package/src/commands/env.ts
CHANGED
|
@@ -13,16 +13,18 @@ export function makeEnvCommand(): Command {
|
|
|
13
13
|
|
|
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 = configStore.getDefaultConnection()
|
|
19
21
|
if (!conn) {
|
|
20
|
-
OutputFormatter.error('No connection configured. Run:
|
|
22
|
+
OutputFormatter.error('No connection configured. Run: xw connect add <name> -e <endpoint> -t <token>')
|
|
21
23
|
return
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const client = createClient(conn)
|
|
25
|
-
const result = await client.
|
|
27
|
+
const result = await client.listEnvironments({ project: options.project })
|
|
26
28
|
|
|
27
29
|
if (!result.success) {
|
|
28
30
|
OutputFormatter.error(result.error!.message)
|
|
@@ -36,22 +38,20 @@ export function makeEnvCommand(): Command {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
OutputFormatter.table(
|
|
39
|
-
['Name', '
|
|
41
|
+
['Name', 'Namespace', 'Project', 'Services'],
|
|
40
42
|
spaces.map((s: any) => [
|
|
41
|
-
s.name
|
|
42
|
-
s.
|
|
43
|
-
s.
|
|
44
|
-
s.
|
|
43
|
+
s.name,
|
|
44
|
+
s.namespace,
|
|
45
|
+
s.project?.name || '-',
|
|
46
|
+
s._count?.services || 0
|
|
45
47
|
])
|
|
46
48
|
)
|
|
47
49
|
})
|
|
48
50
|
|
|
49
51
|
cmd
|
|
50
|
-
.command('
|
|
51
|
-
.description('
|
|
52
|
-
.
|
|
53
|
-
.option('-e, --environment <env>', 'Environment type (development|staging|production)', 'development')
|
|
54
|
-
.action(async (namespace, options) => {
|
|
52
|
+
.command('get <namespace>')
|
|
53
|
+
.description('Get environment details')
|
|
54
|
+
.action(async (namespace) => {
|
|
55
55
|
const conn = configStore.getDefaultConnection()
|
|
56
56
|
if (!conn) {
|
|
57
57
|
OutputFormatter.error('No connection configured')
|
|
@@ -59,34 +59,26 @@ export function makeEnvCommand(): Command {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const client = createClient(conn)
|
|
62
|
-
|
|
63
|
-
let projectId = options.projectId
|
|
64
|
-
|
|
65
|
-
// 如果没有指定 project-id,尝试获取第一个项目
|
|
66
|
-
if (!projectId) {
|
|
67
|
-
const projectsResult = await client.getProjects()
|
|
68
|
-
if (!projectsResult.success || !projectsResult.data || projectsResult.data.length === 0) {
|
|
69
|
-
OutputFormatter.error('No projects found. Please specify --project-id')
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
projectId = projectsResult.data[0].id
|
|
73
|
-
OutputFormatter.info(`Using project: ${projectsResult.data[0].name}`)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const result = await client.createNamespaceWithProject(namespace, projectId, options.environment)
|
|
62
|
+
const result = await client.getEnvironment(namespace)
|
|
77
63
|
|
|
78
64
|
if (!result.success) {
|
|
79
65
|
OutputFormatter.error(result.error!.message)
|
|
80
66
|
return
|
|
81
67
|
}
|
|
82
68
|
|
|
83
|
-
|
|
69
|
+
const env = result.data
|
|
70
|
+
OutputFormatter.info(`Name: ${env.name}`)
|
|
71
|
+
OutputFormatter.info(`Namespace: ${env.namespace}`)
|
|
72
|
+
OutputFormatter.info(`Project: ${env.project?.name || '-'}`)
|
|
73
|
+
OutputFormatter.info(`Services: ${env.services?.length || 0}`)
|
|
84
74
|
})
|
|
85
75
|
|
|
86
76
|
cmd
|
|
87
|
-
.command('
|
|
88
|
-
.description('
|
|
89
|
-
.
|
|
77
|
+
.command('create <namespace>')
|
|
78
|
+
.description('Create a new environment (K8s namespace)')
|
|
79
|
+
.option('-n, --name <name>', 'Environment name')
|
|
80
|
+
.option('-p, --project <project-code>', 'Project code')
|
|
81
|
+
.action(async (namespace, options) => {
|
|
90
82
|
const conn = configStore.getDefaultConnection()
|
|
91
83
|
if (!conn) {
|
|
92
84
|
OutputFormatter.error('No connection configured')
|
|
@@ -95,28 +87,24 @@ export function makeEnvCommand(): Command {
|
|
|
95
87
|
|
|
96
88
|
const client = createClient(conn)
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const envData = Array.isArray(infoResult.data) ? infoResult.data[0] : infoResult.data
|
|
106
|
-
const id = envData.id
|
|
107
|
-
const result = await client.deleteNamespace(id)
|
|
90
|
+
const result = await client.createEnvironment(
|
|
91
|
+
options.name || namespace,
|
|
92
|
+
namespace,
|
|
93
|
+
options.project
|
|
94
|
+
)
|
|
108
95
|
|
|
109
96
|
if (!result.success) {
|
|
110
97
|
OutputFormatter.error(result.error!.message)
|
|
111
98
|
return
|
|
112
99
|
}
|
|
113
100
|
|
|
114
|
-
OutputFormatter.success(`Environment "${namespace}"
|
|
101
|
+
OutputFormatter.success(`Environment "${namespace}" created`)
|
|
115
102
|
})
|
|
116
103
|
|
|
117
104
|
cmd
|
|
118
|
-
.command('
|
|
119
|
-
.
|
|
105
|
+
.command('rm <namespace>')
|
|
106
|
+
.alias('delete')
|
|
107
|
+
.description('Delete an environment')
|
|
120
108
|
.action(async (namespace) => {
|
|
121
109
|
const conn = configStore.getDefaultConnection()
|
|
122
110
|
if (!conn) {
|
|
@@ -125,19 +113,14 @@ export function makeEnvCommand(): Command {
|
|
|
125
113
|
}
|
|
126
114
|
|
|
127
115
|
const client = createClient(conn)
|
|
128
|
-
const result = await client.
|
|
116
|
+
const result = await client.deleteEnvironment(namespace)
|
|
129
117
|
|
|
130
118
|
if (!result.success) {
|
|
131
119
|
OutputFormatter.error(result.error!.message)
|
|
132
120
|
return
|
|
133
121
|
}
|
|
134
122
|
|
|
135
|
-
|
|
136
|
-
OutputFormatter.info(`Name: ${info.name}`)
|
|
137
|
-
OutputFormatter.info(`Identifier: ${info.identifier}`)
|
|
138
|
-
OutputFormatter.info(`Namespace: ${info.namespace}`)
|
|
139
|
-
OutputFormatter.info(`Environment: ${info.environment}`)
|
|
140
|
-
OutputFormatter.info(`Status: ${info.status}`)
|
|
123
|
+
OutputFormatter.success(`Environment "${namespace}" deleted`)
|
|
141
124
|
})
|
|
142
125
|
|
|
143
126
|
return cmd
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目管理命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { configStore } from '../config/store'
|
|
7
|
+
import { createClient } from '../api/client'
|
|
8
|
+
import { OutputFormatter } from '../output/formatter'
|
|
9
|
+
|
|
10
|
+
function getAge(timestamp?: string): string {
|
|
11
|
+
if (!timestamp) return '-'
|
|
12
|
+
const created = new Date(timestamp)
|
|
13
|
+
const now = new Date()
|
|
14
|
+
const diff = Math.floor((now.getTime() - created.getTime()) / 1000)
|
|
15
|
+
if (diff < 60) return `${diff}s`
|
|
16
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m`
|
|
17
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h`
|
|
18
|
+
return `${Math.floor(diff / 86400)}d`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function makeProjectCommand(): Command {
|
|
22
|
+
const cmd = new Command('project')
|
|
23
|
+
.description('Manage projects')
|
|
24
|
+
|
|
25
|
+
cmd
|
|
26
|
+
.command('ls')
|
|
27
|
+
.alias('list')
|
|
28
|
+
.description('List projects')
|
|
29
|
+
.option('-n, --name <name>', 'Filter by name')
|
|
30
|
+
.option('-c, --code <code>', 'Filter by code')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
const conn = configStore.getDefaultConnection()
|
|
33
|
+
if (!conn) {
|
|
34
|
+
OutputFormatter.error('No connection configured. Run: xw connect add <name> -e <endpoint> -t <token>')
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const client = createClient(conn)
|
|
39
|
+
const result = await client.listProjects({ name: options.name, code: options.code })
|
|
40
|
+
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
OutputFormatter.error(result.error!.message)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const projects = result.data || []
|
|
47
|
+
if (projects.length === 0) {
|
|
48
|
+
OutputFormatter.info('No projects found')
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
OutputFormatter.table(
|
|
53
|
+
['Code', 'Name', 'Environments', 'Members', 'Created'],
|
|
54
|
+
projects.map((p: any) => [
|
|
55
|
+
p.code,
|
|
56
|
+
p.name,
|
|
57
|
+
p.environments?.length || 0,
|
|
58
|
+
p.members?.length || 0,
|
|
59
|
+
getAge(p.createdAt)
|
|
60
|
+
])
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
cmd
|
|
65
|
+
.command('get <code>')
|
|
66
|
+
.alias('describe')
|
|
67
|
+
.description('Get project details')
|
|
68
|
+
.action(async (code) => {
|
|
69
|
+
const conn = configStore.getDefaultConnection()
|
|
70
|
+
if (!conn) {
|
|
71
|
+
OutputFormatter.error('No connection configured')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const client = createClient(conn)
|
|
76
|
+
const result = await client.getProject(code)
|
|
77
|
+
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
OutputFormatter.error(result.error!.message)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const project = result.data
|
|
84
|
+
OutputFormatter.info(`Code: ${project.code}`)
|
|
85
|
+
OutputFormatter.info(`Name: ${project.name}`)
|
|
86
|
+
OutputFormatter.info(`Description: ${project.description || '-'}`)
|
|
87
|
+
OutputFormatter.info(`Environments: ${project.environments?.length || 0}`)
|
|
88
|
+
OutputFormatter.info(`Members: ${project.members?.length || 0}`)
|
|
89
|
+
OutputFormatter.info(`Created: ${project.createdAt}`)
|
|
90
|
+
|
|
91
|
+
if (project.environments && project.environments.length > 0) {
|
|
92
|
+
OutputFormatter.info('\nEnvironments:')
|
|
93
|
+
project.environments.forEach((env: any) => {
|
|
94
|
+
OutputFormatter.info(` - ${env.name} (${env.namespace})`)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (project.members && project.members.length > 0) {
|
|
99
|
+
OutputFormatter.info('\nMembers:')
|
|
100
|
+
project.members.forEach((m: any) => {
|
|
101
|
+
OutputFormatter.info(` - ${m.user.name || m.user.email} (${m.role})`)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
cmd
|
|
107
|
+
.command('create')
|
|
108
|
+
.description('Create a new project')
|
|
109
|
+
.requiredOption('-n, --name <name>', 'Project name')
|
|
110
|
+
.requiredOption('-c, --code <code>', 'Project code (unique identifier, cannot be changed after creation)')
|
|
111
|
+
.option('-d, --description <description>', 'Project description')
|
|
112
|
+
.action(async (options) => {
|
|
113
|
+
const conn = configStore.getDefaultConnection()
|
|
114
|
+
if (!conn) {
|
|
115
|
+
OutputFormatter.error('No connection configured')
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const client = createClient(conn)
|
|
120
|
+
const result = await client.createProject(options.name, options.code, options.description)
|
|
121
|
+
|
|
122
|
+
if (!result.success) {
|
|
123
|
+
OutputFormatter.error(result.error!.message)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
OutputFormatter.success(`Project "${options.name}" created with code "${options.code}"`)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
cmd
|
|
131
|
+
.command('rm <code>')
|
|
132
|
+
.alias('delete')
|
|
133
|
+
.description('Delete a project')
|
|
134
|
+
.action(async (code) => {
|
|
135
|
+
const conn = configStore.getDefaultConnection()
|
|
136
|
+
if (!conn) {
|
|
137
|
+
OutputFormatter.error('No connection configured')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const client = createClient(conn)
|
|
142
|
+
const result = await client.deleteProject(code)
|
|
143
|
+
|
|
144
|
+
if (!result.success) {
|
|
145
|
+
OutputFormatter.error(result.error!.message)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
OutputFormatter.success(`Project "${code}" deleted`)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return cmd
|
|
153
|
+
}
|
package/src/commands/svc.ts
CHANGED
|
@@ -323,6 +323,46 @@ export function makeSvcCommand(): Command {
|
|
|
323
323
|
)
|
|
324
324
|
})
|
|
325
325
|
|
|
326
|
+
cmd
|
|
327
|
+
.command('update <ns>/<name>')
|
|
328
|
+
.description('Update service configuration')
|
|
329
|
+
.option('-i, --image <image>', 'Container image')
|
|
330
|
+
.option('-r, --replicas <num>', 'Number of replicas')
|
|
331
|
+
.option('--port <port>', 'Container port')
|
|
332
|
+
.action(async (nsName, options) => {
|
|
333
|
+
const parsed = parseNamespaceName(nsName)
|
|
334
|
+
if (!parsed) {
|
|
335
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const conn = configStore.getDefaultConnection()
|
|
340
|
+
if (!conn) {
|
|
341
|
+
OutputFormatter.error('No connection configured')
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const updateData: any = {}
|
|
346
|
+
if (options.image) updateData.image = options.image
|
|
347
|
+
if (options.replicas) updateData.replicas = parseInt(options.replicas)
|
|
348
|
+
if (options.port) updateData.port = parseInt(options.port)
|
|
349
|
+
|
|
350
|
+
if (Object.keys(updateData).length === 0) {
|
|
351
|
+
OutputFormatter.error('No update options provided. Use --image, --replicas, or --port')
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const client = createClient(conn)
|
|
356
|
+
const result = await client.updateK8sService(parsed.namespace, parsed.name, updateData)
|
|
357
|
+
|
|
358
|
+
if (!result.success) {
|
|
359
|
+
OutputFormatter.error(result.error!.message)
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
OutputFormatter.success(`Service "${nsName}" updated`)
|
|
364
|
+
})
|
|
365
|
+
|
|
326
366
|
cmd
|
|
327
367
|
.command('delete <ns>/<name>')
|
|
328
368
|
.alias('rm')
|
package/src/config/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Command } from 'commander'
|
|
6
6
|
import { configStore } from './config/store'
|
|
7
7
|
import { makeConnectCommand } from './commands/connect'
|
|
8
|
+
import { makeProjectCommand } from './commands/project'
|
|
8
9
|
import { makeEnvCommand } from './commands/env'
|
|
9
10
|
import { makeDeployCommand } from './commands/deploy'
|
|
10
11
|
import { makeAppCommand } from './commands/app'
|
|
@@ -27,6 +28,7 @@ program
|
|
|
27
28
|
.option('-o, --output <format>', 'Output format (human|json)', 'human')
|
|
28
29
|
|
|
29
30
|
program.addCommand(makeConnectCommand())
|
|
31
|
+
program.addCommand(makeProjectCommand())
|
|
30
32
|
program.addCommand(makeEnvCommand())
|
|
31
33
|
program.addCommand(makeAppCommand())
|
|
32
34
|
program.addCommand(makeSvcCommand())
|