xuanwu-cli 1.0.0 → 2.1.0
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/README.md +138 -88
- package/bin/xuanwu +0 -0
- package/dist/api/client.d.ts +25 -1
- package/dist/api/client.js +92 -0
- package/dist/commands/app.d.ts +5 -0
- package/dist/commands/app.js +241 -0
- package/dist/commands/auth/login.d.ts +2 -0
- package/dist/commands/auth/login.js +130 -0
- package/dist/commands/auth/logout.d.ts +2 -0
- package/dist/commands/auth/logout.js +33 -0
- package/dist/commands/auth/tokens.d.ts +2 -0
- package/dist/commands/auth/tokens.js +70 -0
- package/dist/commands/auth/whoami.d.ts +2 -0
- package/dist/commands/auth/whoami.js +32 -0
- package/dist/commands/build.js +74 -17
- package/dist/commands/svc.d.ts +1 -1
- package/dist/commands/svc.js +219 -24
- package/dist/config/types.d.ts +92 -0
- package/dist/index.js +13 -116
- package/dist/lib/auth.d.ts +6 -0
- package/dist/lib/auth.js +29 -0
- package/dist/lib/session.d.ts +18 -0
- package/dist/lib/session.js +72 -0
- package/package.json +11 -9
- package/src/api/client.ts +111 -1
- package/src/commands/app.ts +269 -0
- package/src/commands/auth/login.ts +165 -0
- package/src/commands/auth/logout.ts +34 -0
- package/src/commands/auth/tokens.ts +85 -0
- package/src/commands/auth/whoami.ts +35 -0
- package/src/commands/build.ts +78 -18
- package/src/commands/svc.ts +266 -25
- package/src/config/types.ts +105 -0
- package/src/index.ts +13 -97
- package/src/lib/auth.ts +41 -0
- package/src/lib/session.ts +50 -0
- package/test/REPORT.md +78 -0
- package/test/integration.js +431 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { SessionManager } from '../../lib/session'
|
|
3
|
+
import { OutputFormatter } from '../../output/formatter'
|
|
4
|
+
|
|
5
|
+
export function makeLogoutCommand(): Command {
|
|
6
|
+
const cmd = new Command('logout')
|
|
7
|
+
.description('Logout from xuanwu factory')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const sessionManager = new SessionManager()
|
|
10
|
+
const session = await sessionManager.loadSession()
|
|
11
|
+
|
|
12
|
+
if (!session) {
|
|
13
|
+
OutputFormatter.info('未登录')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await fetch(`${session.apiUrl}/api/cli/auth/logout`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Authorization': `Bearer ${session.token}`,
|
|
22
|
+
'Content-Type': 'application/json'
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// Ignore API errors
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await sessionManager.clearSession()
|
|
30
|
+
OutputFormatter.success('登出成功')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return cmd
|
|
34
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { SessionManager } from '../../lib/session'
|
|
3
|
+
import { OutputFormatter } from '../../output/formatter'
|
|
4
|
+
|
|
5
|
+
export function makeTokensCommand(): Command {
|
|
6
|
+
const cmd = new Command('tokens')
|
|
7
|
+
.description('List all tokens')
|
|
8
|
+
|
|
9
|
+
const listCmd = new Command('list')
|
|
10
|
+
.description('List all tokens')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const sessionManager = new SessionManager()
|
|
13
|
+
const session = await sessionManager.loadSession()
|
|
14
|
+
|
|
15
|
+
if (!session) {
|
|
16
|
+
OutputFormatter.info('未登录')
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens`, {
|
|
21
|
+
headers: {
|
|
22
|
+
'Authorization': `Bearer ${session.token}`
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
OutputFormatter.error('Failed to list tokens')
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { tokens } = await res.json() as { tokens: Array<{ id: string; name: string; lastUsedAt?: string; expiresAt?: string }> }
|
|
32
|
+
|
|
33
|
+
if (tokens.length === 0) {
|
|
34
|
+
OutputFormatter.info('没有找到tokens')
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('Your Tokens:')
|
|
39
|
+
tokens.forEach((token, index) => {
|
|
40
|
+
const lastUsed = token.lastUsedAt
|
|
41
|
+
? `${Math.floor((Date.now() - new Date(token.lastUsedAt).getTime()) / (60 * 60 * 1000))}h ago`
|
|
42
|
+
: 'never'
|
|
43
|
+
|
|
44
|
+
const expires = token.expiresAt
|
|
45
|
+
? `${Math.max(0, Math.floor((new Date(token.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))} days`
|
|
46
|
+
: 'never'
|
|
47
|
+
|
|
48
|
+
const expired = token.expiresAt && new Date(token.expiresAt) < new Date()
|
|
49
|
+
|
|
50
|
+
console.log(` ${index + 1}. ${token.name} (last used: ${lastUsed}) - ${expired ? 'expired' : expires}`)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const revokeCmd = new Command('revoke')
|
|
55
|
+
.description('Revoke a token')
|
|
56
|
+
.argument('<id>', 'Token ID')
|
|
57
|
+
.action(async (id) => {
|
|
58
|
+
const sessionManager = new SessionManager()
|
|
59
|
+
const session = await sessionManager.loadSession()
|
|
60
|
+
|
|
61
|
+
if (!session) {
|
|
62
|
+
OutputFormatter.info('未登录')
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens/${id}`, {
|
|
67
|
+
method: 'DELETE',
|
|
68
|
+
headers: {
|
|
69
|
+
'Authorization': `Bearer ${session.token}`
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
OutputFormatter.error('Failed to revoke token')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
OutputFormatter.success('Token已撤销')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
cmd.addCommand(listCmd)
|
|
82
|
+
cmd.addCommand(revokeCmd)
|
|
83
|
+
|
|
84
|
+
return cmd
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { SessionManager } from '../../lib/session'
|
|
3
|
+
import { OutputFormatter } from '../../output/formatter'
|
|
4
|
+
|
|
5
|
+
export function makeWhoamiCommand(): Command {
|
|
6
|
+
const cmd = new Command('whoami')
|
|
7
|
+
.description('Show current login status')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const sessionManager = new SessionManager()
|
|
10
|
+
const session = await sessionManager.loadSession()
|
|
11
|
+
|
|
12
|
+
if (!session) {
|
|
13
|
+
OutputFormatter.info('未登录')
|
|
14
|
+
console.log('请使用: xw login')
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (sessionManager.isExpired(session)) {
|
|
19
|
+
OutputFormatter.info('登录已过期')
|
|
20
|
+
console.log('请使用: xw login')
|
|
21
|
+
await sessionManager.clearSession()
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const expiresIn = session.expiresAt
|
|
26
|
+
? Math.max(0, Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
|
|
27
|
+
: 'never'
|
|
28
|
+
|
|
29
|
+
console.log(`Logged in as: ${session.userName} (${session.userEmail})`)
|
|
30
|
+
console.log(`Device: ${session.deviceId}`)
|
|
31
|
+
console.log(`Expires in: ${expiresIn === 'never' ? 'never' : `${expiresIn} days`}`)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return cmd
|
|
35
|
+
}
|
package/src/commands/build.ts
CHANGED
|
@@ -7,14 +7,28 @@ import { configStore } from '../config/store'
|
|
|
7
7
|
import { createClient } from '../api/client'
|
|
8
8
|
import { OutputFormatter } from '../output/formatter'
|
|
9
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
|
+
|
|
10
21
|
export function makeBuildCommand(): Command {
|
|
11
22
|
const cmd = new Command('build')
|
|
12
|
-
.description('
|
|
23
|
+
.description('Manage builds')
|
|
13
24
|
|
|
14
25
|
cmd
|
|
15
|
-
.command('
|
|
16
|
-
.
|
|
17
|
-
.
|
|
26
|
+
.command('ls')
|
|
27
|
+
.alias('list')
|
|
28
|
+
.description('List builds')
|
|
29
|
+
.option('-a, --app <code>', 'Filter by application code')
|
|
30
|
+
.option('-s, --status <status>', 'Filter by status (SUCCESS|FAILED|RUNNING|PENDING)')
|
|
31
|
+
.action(async (options) => {
|
|
18
32
|
const conn = configStore.getDefaultConnection()
|
|
19
33
|
if (!conn) {
|
|
20
34
|
OutputFormatter.error('No connection configured')
|
|
@@ -22,22 +36,38 @@ export function makeBuildCommand(): Command {
|
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
const client = createClient(conn)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
const result = await client.listBuilds({
|
|
40
|
+
appCode: options.app,
|
|
41
|
+
status: options.status
|
|
42
|
+
})
|
|
28
43
|
|
|
29
44
|
if (!result.success) {
|
|
30
45
|
OutputFormatter.error(result.error!.message)
|
|
31
46
|
return
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
const builds = result.data || []
|
|
50
|
+
if (builds.length === 0) {
|
|
51
|
+
OutputFormatter.info('No builds found')
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
OutputFormatter.table(
|
|
56
|
+
['ID', 'Build', 'Status', 'Image Tag', 'Created'],
|
|
57
|
+
builds.map((b: any) => [
|
|
58
|
+
b.id.substring(0, 12),
|
|
59
|
+
`#${b.buildNumber}`,
|
|
60
|
+
b.status,
|
|
61
|
+
b.imageTag || '-',
|
|
62
|
+
getAge(b.createdAt)
|
|
63
|
+
])
|
|
64
|
+
)
|
|
35
65
|
})
|
|
36
66
|
|
|
37
67
|
cmd
|
|
38
|
-
.command('
|
|
39
|
-
.description('Get build
|
|
40
|
-
.action(async (
|
|
68
|
+
.command('get <id>')
|
|
69
|
+
.description('Get build details')
|
|
70
|
+
.action(async (id) => {
|
|
41
71
|
const conn = configStore.getDefaultConnection()
|
|
42
72
|
if (!conn) {
|
|
43
73
|
OutputFormatter.error('No connection configured')
|
|
@@ -45,22 +75,52 @@ export function makeBuildCommand(): Command {
|
|
|
45
75
|
}
|
|
46
76
|
|
|
47
77
|
const client = createClient(conn)
|
|
48
|
-
const result = await client.
|
|
78
|
+
const result = await client.getBuild(id)
|
|
49
79
|
|
|
50
80
|
if (!result.success) {
|
|
51
81
|
OutputFormatter.error(result.error!.message)
|
|
52
82
|
return
|
|
53
83
|
}
|
|
54
84
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
const build = result.data!
|
|
86
|
+
OutputFormatter.info(`Build: #${build.buildNumber}`)
|
|
87
|
+
OutputFormatter.info(`ID: ${build.id}`)
|
|
88
|
+
OutputFormatter.info(`Status: ${build.status}`)
|
|
89
|
+
OutputFormatter.info(`Image Tag: ${build.imageTag || '-'}`)
|
|
90
|
+
if (build.commitSha) {
|
|
91
|
+
OutputFormatter.info(`Commit: ${build.commitSha.substring(0, 8)}`)
|
|
92
|
+
}
|
|
93
|
+
if (build.commitMessage) {
|
|
94
|
+
OutputFormatter.info(`Message: ${build.commitMessage}`)
|
|
95
|
+
}
|
|
96
|
+
if (build.commitAuthor) {
|
|
97
|
+
OutputFormatter.info(`Author: ${build.commitAuthor}`)
|
|
98
|
+
}
|
|
99
|
+
OutputFormatter.info(`Created: ${build.createdAt}`)
|
|
100
|
+
if (build.completedAt) {
|
|
101
|
+
OutputFormatter.info(`Completed: ${build.completedAt}`)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
cmd
|
|
106
|
+
.command('cancel <id>')
|
|
107
|
+
.description('Cancel a running build')
|
|
108
|
+
.action(async (id) => {
|
|
109
|
+
const conn = configStore.getDefaultConnection()
|
|
110
|
+
if (!conn) {
|
|
111
|
+
OutputFormatter.error('No connection configured')
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const client = createClient(conn)
|
|
116
|
+
const result = await client.cancelBuild(id)
|
|
117
|
+
|
|
118
|
+
if (!result.success) {
|
|
119
|
+
OutputFormatter.error(result.error!.message)
|
|
58
120
|
return
|
|
59
121
|
}
|
|
60
122
|
|
|
61
|
-
|
|
62
|
-
OutputFormatter.info(`Status: ${latest.status}`)
|
|
63
|
-
OutputFormatter.info(`Created: ${latest.created_at}`)
|
|
123
|
+
OutputFormatter.success(`Build "${id}" cancelled`)
|
|
64
124
|
})
|
|
65
125
|
|
|
66
126
|
return cmd
|
package/src/commands/svc.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 服务管理命令
|
|
2
|
+
* 服务管理命令 (K8s)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander'
|
|
@@ -7,6 +7,14 @@ 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
|
+
|
|
10
18
|
function getAge(timestamp?: string): string {
|
|
11
19
|
if (!timestamp) return '-'
|
|
12
20
|
const created = new Date(timestamp)
|
|
@@ -20,12 +28,14 @@ function getAge(timestamp?: string): string {
|
|
|
20
28
|
|
|
21
29
|
export function makeSvcCommand(): Command {
|
|
22
30
|
const cmd = new Command('svc')
|
|
23
|
-
.description('Manage services')
|
|
31
|
+
.description('Manage services (Kubernetes)')
|
|
24
32
|
|
|
25
33
|
cmd
|
|
26
|
-
.command('ls
|
|
34
|
+
.command('ls')
|
|
35
|
+
.alias('list')
|
|
27
36
|
.description('List services in namespace')
|
|
28
|
-
.
|
|
37
|
+
.option('-e, --env <namespace>', 'Namespace/environment')
|
|
38
|
+
.action(async (options) => {
|
|
29
39
|
const conn = configStore.getDefaultConnection()
|
|
30
40
|
if (!conn) {
|
|
31
41
|
OutputFormatter.error('No connection configured')
|
|
@@ -33,7 +43,7 @@ export function makeSvcCommand(): Command {
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
const client = createClient(conn)
|
|
36
|
-
const result = await client.
|
|
46
|
+
const result = await client.listK8sServices(options.env)
|
|
37
47
|
|
|
38
48
|
if (!result.success) {
|
|
39
49
|
OutputFormatter.error(result.error!.message)
|
|
@@ -42,26 +52,33 @@ export function makeSvcCommand(): Command {
|
|
|
42
52
|
|
|
43
53
|
const services = result.data || []
|
|
44
54
|
if (services.length === 0) {
|
|
45
|
-
OutputFormatter.info(`No services in ${
|
|
55
|
+
OutputFormatter.info(`No services found${options.env ? ` in ${options.env}` : ''}`)
|
|
46
56
|
return
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
OutputFormatter.table(
|
|
50
|
-
['Name', '
|
|
60
|
+
['Namespace', 'Name', 'Type', 'Cluster IP', 'Ports', 'Age'],
|
|
51
61
|
services.map((s: any) => [
|
|
52
|
-
s.
|
|
53
|
-
|
|
54
|
-
s.
|
|
55
|
-
s.
|
|
56
|
-
|
|
62
|
+
s.namespace || '-',
|
|
63
|
+
s.name || 'unknown',
|
|
64
|
+
s.type || 'ClusterIP',
|
|
65
|
+
s.clusterIP || '-',
|
|
66
|
+
s.ports?.map((p: any) => `${p.port}`).join(',') || '-',
|
|
67
|
+
getAge(s.createdAt)
|
|
57
68
|
])
|
|
58
69
|
)
|
|
59
70
|
})
|
|
60
71
|
|
|
61
72
|
cmd
|
|
62
|
-
.command('
|
|
63
|
-
.description('Get service
|
|
64
|
-
.action(async (
|
|
73
|
+
.command('get <ns>/<name>')
|
|
74
|
+
.description('Get service details')
|
|
75
|
+
.action(async (nsName) => {
|
|
76
|
+
const parsed = parseNamespaceName(nsName)
|
|
77
|
+
if (!parsed) {
|
|
78
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
const conn = configStore.getDefaultConnection()
|
|
66
83
|
if (!conn) {
|
|
67
84
|
OutputFormatter.error('No connection configured')
|
|
@@ -69,7 +86,45 @@ export function makeSvcCommand(): Command {
|
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
const client = createClient(conn)
|
|
72
|
-
const result = await client.
|
|
89
|
+
const result = await client.getK8sService(parsed.namespace, parsed.name)
|
|
90
|
+
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
OutputFormatter.error(result.error!.message)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const svc = result.data
|
|
97
|
+
OutputFormatter.info(`Service: ${svc.name}`)
|
|
98
|
+
OutputFormatter.info(`Namespace: ${svc.namespace}`)
|
|
99
|
+
OutputFormatter.info(`Type: ${svc.type}`)
|
|
100
|
+
OutputFormatter.info(`Cluster IP: ${svc.clusterIP}`)
|
|
101
|
+
if (svc.externalIP) {
|
|
102
|
+
OutputFormatter.info(`External IP: ${svc.externalIP}`)
|
|
103
|
+
}
|
|
104
|
+
if (svc.ports) {
|
|
105
|
+
OutputFormatter.info(`Ports: ${svc.ports.map((p: any) => `${p.port}:${p.targetPort}/${p.protocol}`).join(', ')}`)
|
|
106
|
+
}
|
|
107
|
+
OutputFormatter.info(`Created: ${svc.createdAt}`)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
cmd
|
|
111
|
+
.command('status <ns>/<name>')
|
|
112
|
+
.description('Get deployment status')
|
|
113
|
+
.action(async (nsName) => {
|
|
114
|
+
const parsed = parseNamespaceName(nsName)
|
|
115
|
+
if (!parsed) {
|
|
116
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const conn = configStore.getDefaultConnection()
|
|
121
|
+
if (!conn) {
|
|
122
|
+
OutputFormatter.error('No connection configured')
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const client = createClient(conn)
|
|
127
|
+
const result = await client.getK8sServiceStatus(parsed.namespace, parsed.name)
|
|
73
128
|
|
|
74
129
|
if (!result.success) {
|
|
75
130
|
OutputFormatter.error(result.error!.message)
|
|
@@ -77,22 +132,208 @@ export function makeSvcCommand(): Command {
|
|
|
77
132
|
}
|
|
78
133
|
|
|
79
134
|
const info = result.data
|
|
80
|
-
OutputFormatter.info(`
|
|
135
|
+
OutputFormatter.info(`Deployment: ${info.name}`)
|
|
81
136
|
OutputFormatter.info(`Namespace: ${info.namespace}`)
|
|
82
|
-
OutputFormatter.info(`Replicas: ${info.replicas}`)
|
|
83
|
-
OutputFormatter.info(`
|
|
84
|
-
OutputFormatter.info(`Available Replicas: ${info.availableReplicas}`)
|
|
137
|
+
OutputFormatter.info(`Replicas: ${info.readyReplicas}/${info.replicas}`)
|
|
138
|
+
OutputFormatter.info(`Available: ${info.availableReplicas}`)
|
|
85
139
|
OutputFormatter.info(`Image: ${info.image}`)
|
|
86
|
-
|
|
87
140
|
if (info.labels) {
|
|
88
141
|
OutputFormatter.info(`Labels: ${JSON.stringify(info.labels)}`)
|
|
89
142
|
}
|
|
90
143
|
})
|
|
91
144
|
|
|
92
145
|
cmd
|
|
93
|
-
.command('
|
|
146
|
+
.command('logs <ns>/<name>')
|
|
147
|
+
.description('Get service logs')
|
|
148
|
+
.option('-f, --follow', 'Follow log output')
|
|
149
|
+
.option('-t, --tail <lines>', 'Number of lines to show', '100')
|
|
150
|
+
.action(async (nsName, options) => {
|
|
151
|
+
const parsed = parseNamespaceName(nsName)
|
|
152
|
+
if (!parsed) {
|
|
153
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const conn = configStore.getDefaultConnection()
|
|
158
|
+
if (!conn) {
|
|
159
|
+
OutputFormatter.error('No connection configured')
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const client = createClient(conn)
|
|
164
|
+
|
|
165
|
+
if (options.follow) {
|
|
166
|
+
OutputFormatter.info('Following logs... (Ctrl+C to exit)')
|
|
167
|
+
await client.streamLogs(parsed.namespace, parsed.name)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = await client.getK8sServiceLogs(
|
|
172
|
+
parsed.namespace,
|
|
173
|
+
parsed.name,
|
|
174
|
+
parseInt(options.tail),
|
|
175
|
+
false
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if (!result.success) {
|
|
179
|
+
OutputFormatter.error(result.error!.message)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(result.data?.logs || 'No logs available')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
cmd
|
|
187
|
+
.command('restart <ns>/<name>')
|
|
188
|
+
.description('Restart a service')
|
|
189
|
+
.action(async (nsName) => {
|
|
190
|
+
const parsed = parseNamespaceName(nsName)
|
|
191
|
+
if (!parsed) {
|
|
192
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const conn = configStore.getDefaultConnection()
|
|
197
|
+
if (!conn) {
|
|
198
|
+
OutputFormatter.error('No connection configured')
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const client = createClient(conn)
|
|
203
|
+
const result = await client.restartK8sService(parsed.namespace, parsed.name)
|
|
204
|
+
|
|
205
|
+
if (!result.success) {
|
|
206
|
+
OutputFormatter.error(result.error!.message)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
OutputFormatter.success(`Service "${nsName}" restarted`)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
cmd
|
|
214
|
+
.command('scale <ns>/<name>')
|
|
215
|
+
.description('Scale a service')
|
|
216
|
+
.requiredOption('-r, --replicas <num>', 'Number of replicas')
|
|
217
|
+
.action(async (nsName, options) => {
|
|
218
|
+
const parsed = parseNamespaceName(nsName)
|
|
219
|
+
if (!parsed) {
|
|
220
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const conn = configStore.getDefaultConnection()
|
|
225
|
+
if (!conn) {
|
|
226
|
+
OutputFormatter.error('No connection configured')
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const client = createClient(conn)
|
|
231
|
+
const replicas = parseInt(options.replicas)
|
|
232
|
+
const result = await client.scaleK8sService(parsed.namespace, parsed.name, replicas)
|
|
233
|
+
|
|
234
|
+
if (!result.success) {
|
|
235
|
+
OutputFormatter.error(result.error!.message)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
OutputFormatter.success(`Service "${nsName}" scaled to ${replicas} replicas`)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
cmd
|
|
243
|
+
.command('exec <ns>/<name>')
|
|
244
|
+
.description('Execute command in service container')
|
|
245
|
+
.option('-c, --command <cmd>', 'Command to execute', 'sh')
|
|
246
|
+
.option('-p, --pod <pod-name>', 'Specific pod name')
|
|
247
|
+
.option('-i, --stdin', 'Keep stdin open')
|
|
248
|
+
.option('-t, --tty', 'Allocate a pseudo-TTY')
|
|
249
|
+
.action(async (nsName, options) => {
|
|
250
|
+
const parsed = parseNamespaceName(nsName)
|
|
251
|
+
if (!parsed) {
|
|
252
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const conn = configStore.getDefaultConnection()
|
|
257
|
+
if (!conn) {
|
|
258
|
+
OutputFormatter.error('No connection configured')
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const client = createClient(conn)
|
|
263
|
+
const result = await client.execK8sService(
|
|
264
|
+
parsed.namespace,
|
|
265
|
+
parsed.name,
|
|
266
|
+
options.command,
|
|
267
|
+
options.pod
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
OutputFormatter.error(result.error!.message)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const data = result.data
|
|
276
|
+
if (data.stdout) console.log(data.stdout)
|
|
277
|
+
if (data.stderr) console.error(data.stderr)
|
|
278
|
+
if (data.exitCode !== 0) {
|
|
279
|
+
process.exit(data.exitCode)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
cmd
|
|
284
|
+
.command('pods <ns>/<name>')
|
|
285
|
+
.description('List pods for a service')
|
|
286
|
+
.action(async (nsName) => {
|
|
287
|
+
const parsed = parseNamespaceName(nsName)
|
|
288
|
+
if (!parsed) {
|
|
289
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const conn = configStore.getDefaultConnection()
|
|
294
|
+
if (!conn) {
|
|
295
|
+
OutputFormatter.error('No connection configured')
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const client = createClient(conn)
|
|
300
|
+
const result = await client.listK8sServicePods(parsed.namespace, parsed.name)
|
|
301
|
+
|
|
302
|
+
if (!result.success) {
|
|
303
|
+
OutputFormatter.error(result.error!.message)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const pods = result.data || []
|
|
308
|
+
if (pods.length === 0) {
|
|
309
|
+
OutputFormatter.info('No pods found')
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
OutputFormatter.table(
|
|
314
|
+
['Name', 'Status', 'Ready', 'Restarts', 'Age', 'IP'],
|
|
315
|
+
pods.map((p: any) => [
|
|
316
|
+
p.name,
|
|
317
|
+
p.status,
|
|
318
|
+
p.ready || '0/0',
|
|
319
|
+
p.restarts || 0,
|
|
320
|
+
getAge(p.createdAt),
|
|
321
|
+
p.ip || '-'
|
|
322
|
+
])
|
|
323
|
+
)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
cmd
|
|
327
|
+
.command('delete <ns>/<name>')
|
|
328
|
+
.alias('rm')
|
|
94
329
|
.description('Delete a service')
|
|
95
|
-
.action(async (
|
|
330
|
+
.action(async (nsName) => {
|
|
331
|
+
const parsed = parseNamespaceName(nsName)
|
|
332
|
+
if (!parsed) {
|
|
333
|
+
OutputFormatter.error('Invalid format. Use: <namespace>/<service-name>')
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
96
337
|
const conn = configStore.getDefaultConnection()
|
|
97
338
|
if (!conn) {
|
|
98
339
|
OutputFormatter.error('No connection configured')
|
|
@@ -100,14 +341,14 @@ export function makeSvcCommand(): Command {
|
|
|
100
341
|
}
|
|
101
342
|
|
|
102
343
|
const client = createClient(conn)
|
|
103
|
-
const result = await client.
|
|
344
|
+
const result = await client.deleteK8sService(parsed.namespace, parsed.name)
|
|
104
345
|
|
|
105
346
|
if (!result.success) {
|
|
106
347
|
OutputFormatter.error(result.error!.message)
|
|
107
348
|
return
|
|
108
349
|
}
|
|
109
350
|
|
|
110
|
-
OutputFormatter.success(`Service "${
|
|
351
|
+
OutputFormatter.success(`Service "${nsName}" deleted`)
|
|
111
352
|
})
|
|
112
353
|
|
|
113
354
|
return cmd
|