@agents-at-scale/ark 0.1.35-rc.1 → 0.1.35-rc1
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/dist/arkServices.d.ts +4 -12
- package/dist/arkServices.js +19 -34
- package/dist/arkServices.spec.d.ts +1 -0
- package/dist/arkServices.spec.js +24 -0
- package/dist/commands/agents/index.d.ts +2 -1
- package/dist/commands/agents/index.js +2 -7
- package/dist/commands/agents/index.spec.d.ts +1 -0
- package/dist/commands/agents/index.spec.js +67 -0
- package/dist/commands/chat/index.d.ts +2 -1
- package/dist/commands/chat/index.js +5 -21
- package/dist/commands/cluster/get.spec.d.ts +1 -0
- package/dist/commands/cluster/get.spec.js +92 -0
- package/dist/commands/cluster/index.d.ts +2 -1
- package/dist/commands/cluster/index.js +1 -1
- package/dist/commands/cluster/index.spec.d.ts +1 -0
- package/dist/commands/cluster/index.spec.js +24 -0
- package/dist/commands/completion/index.d.ts +2 -1
- package/dist/commands/completion/index.js +1 -1
- package/dist/commands/completion/index.spec.d.ts +1 -0
- package/dist/commands/completion/index.spec.js +34 -0
- package/dist/commands/config/index.d.ts +2 -1
- package/dist/commands/config/index.js +2 -2
- package/dist/commands/config/index.spec.d.ts +1 -0
- package/dist/commands/config/index.spec.js +78 -0
- package/dist/commands/dashboard/index.d.ts +2 -1
- package/dist/commands/dashboard/index.js +1 -1
- package/dist/commands/dev/index.d.ts +2 -1
- package/dist/commands/dev/index.js +1 -1
- package/dist/commands/dev/tool-generate.spec.d.ts +1 -0
- package/dist/commands/dev/tool-generate.spec.js +163 -0
- package/dist/commands/dev/tool.spec.d.ts +1 -0
- package/dist/commands/dev/tool.spec.js +48 -0
- package/dist/commands/generate/generators/project.js +22 -41
- package/dist/commands/generate/index.d.ts +2 -1
- package/dist/commands/generate/index.js +1 -1
- package/dist/commands/install/index.d.ts +4 -2
- package/dist/commands/install/index.js +215 -78
- package/dist/commands/install/index.spec.d.ts +1 -0
- package/dist/commands/install/index.spec.js +135 -0
- package/dist/commands/models/create.spec.d.ts +1 -0
- package/dist/commands/models/create.spec.js +125 -0
- package/dist/commands/models/index.d.ts +2 -1
- package/dist/commands/models/index.js +2 -7
- package/dist/commands/models/index.spec.d.ts +1 -0
- package/dist/commands/models/index.spec.js +76 -0
- package/dist/commands/routes/index.d.ts +2 -1
- package/dist/commands/routes/index.js +1 -9
- package/dist/commands/status/index.d.ts +3 -2
- package/dist/commands/status/index.js +210 -11
- package/dist/commands/targets/index.d.ts +2 -1
- package/dist/commands/targets/index.js +1 -1
- package/dist/commands/targets/index.spec.d.ts +1 -0
- package/dist/commands/targets/index.spec.js +105 -0
- package/dist/commands/teams/index.d.ts +2 -1
- package/dist/commands/teams/index.js +2 -7
- package/dist/commands/teams/index.spec.d.ts +1 -0
- package/dist/commands/teams/index.spec.js +70 -0
- package/dist/commands/tools/index.d.ts +2 -1
- package/dist/commands/tools/index.js +2 -7
- package/dist/commands/tools/index.spec.d.ts +1 -0
- package/dist/commands/tools/index.spec.js +70 -0
- package/dist/commands/uninstall/index.d.ts +2 -1
- package/dist/commands/uninstall/index.js +61 -38
- package/dist/commands/uninstall/index.spec.d.ts +1 -0
- package/dist/commands/uninstall/index.spec.js +117 -0
- package/dist/components/ChatUI.js +4 -4
- package/dist/components/statusChecker.d.ts +5 -12
- package/dist/components/statusChecker.js +172 -89
- package/dist/config.d.ts +3 -22
- package/dist/config.js +7 -151
- package/dist/index.js +22 -19
- package/dist/lib/arkServiceProxy.js +4 -2
- package/dist/lib/arkStatus.d.ts +5 -0
- package/dist/lib/arkStatus.js +61 -2
- package/dist/lib/arkStatus.spec.d.ts +1 -0
- package/dist/lib/arkStatus.spec.js +49 -0
- package/dist/lib/chatClient.js +1 -3
- package/dist/lib/cluster.js +11 -14
- package/dist/lib/cluster.spec.d.ts +1 -0
- package/dist/lib/cluster.spec.js +338 -0
- package/dist/lib/commandUtils.js +7 -7
- package/dist/lib/commands.d.ts +16 -0
- package/dist/lib/commands.js +29 -0
- package/dist/lib/commands.spec.d.ts +1 -0
- package/dist/lib/commands.spec.js +146 -0
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.js +6 -4
- package/dist/lib/config.spec.d.ts +1 -0
- package/dist/lib/config.spec.js +99 -0
- package/dist/lib/consts.d.ts +0 -1
- package/dist/lib/consts.js +0 -2
- package/dist/lib/consts.spec.d.ts +1 -0
- package/dist/lib/consts.spec.js +15 -0
- package/dist/lib/errors.js +1 -1
- package/dist/lib/errors.spec.d.ts +1 -0
- package/dist/lib/errors.spec.js +221 -0
- package/dist/lib/exec.d.ts +0 -4
- package/dist/lib/exec.js +0 -11
- package/dist/lib/output.spec.d.ts +1 -0
- package/dist/lib/output.spec.js +123 -0
- package/dist/lib/portUtils.d.ts +8 -0
- package/dist/lib/portUtils.js +39 -0
- package/dist/lib/startup.d.ts +5 -0
- package/dist/lib/startup.js +73 -0
- package/dist/lib/startup.spec.d.ts +1 -0
- package/dist/lib/startup.spec.js +168 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/ui/AgentSelector.d.ts +8 -0
- package/dist/ui/AgentSelector.js +53 -0
- package/dist/ui/MainMenu.d.ts +5 -1
- package/dist/ui/MainMenu.js +117 -54
- package/dist/ui/ModelSelector.d.ts +8 -0
- package/dist/ui/ModelSelector.js +53 -0
- package/dist/ui/TeamSelector.d.ts +8 -0
- package/dist/ui/TeamSelector.js +55 -0
- package/dist/ui/ToolSelector.d.ts +8 -0
- package/dist/ui/ToolSelector.js +53 -0
- package/dist/ui/statusFormatter.d.ts +22 -10
- package/dist/ui/statusFormatter.js +37 -109
- package/dist/ui/statusFormatter.spec.d.ts +1 -0
- package/dist/ui/statusFormatter.spec.js +58 -0
- package/package.json +3 -3
|
@@ -18,9 +18,11 @@ export class ArkServiceProxy {
|
|
|
18
18
|
'port-forward',
|
|
19
19
|
`service/${this.service.k8sServiceName}`,
|
|
20
20
|
`${this.localPort}:${this.service.k8sServicePort}`,
|
|
21
|
-
'--namespace',
|
|
22
|
-
this.service.namespace,
|
|
23
21
|
];
|
|
22
|
+
// Add namespace flag only if namespace is defined
|
|
23
|
+
if (this.service.namespace) {
|
|
24
|
+
args.push('--namespace', this.service.namespace);
|
|
25
|
+
}
|
|
24
26
|
this.kubectlProcess = spawn('kubectl', args, {
|
|
25
27
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
26
28
|
});
|
package/dist/lib/arkStatus.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get current installed ARK version
|
|
3
|
+
* @returns version string if found, undefined otherwise
|
|
4
|
+
*/
|
|
5
|
+
export declare function getArkVersion(): Promise<string | undefined>;
|
|
1
6
|
/**
|
|
2
7
|
* Check if ARK is ready by verifying the ark-controller is running
|
|
3
8
|
* @returns true if ark-controller deployment exists and has ready replicas
|
package/dist/lib/arkStatus.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
|
+
import { arkServices } from '../arkServices.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get current installed ARK version
|
|
5
|
+
* @returns version string if found, undefined otherwise
|
|
6
|
+
*/
|
|
7
|
+
export async function getArkVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const controller = arkServices['ark-controller'];
|
|
10
|
+
const { stdout } = await execa('helm', ['list', '-n', controller.namespace, '-o', 'json'], { stdio: 'pipe' });
|
|
11
|
+
const releases = JSON.parse(stdout);
|
|
12
|
+
const arkController = releases.find((r) => r.name === controller.helmReleaseName);
|
|
13
|
+
return arkController?.app_version;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
2
19
|
/**
|
|
3
20
|
* Check if ARK is ready by verifying the ark-controller is running
|
|
4
21
|
* @returns true if ark-controller deployment exists and has ready replicas
|
|
@@ -10,11 +27,53 @@ export async function isArkReady() {
|
|
|
10
27
|
const deployment = JSON.parse(result.stdout);
|
|
11
28
|
const readyReplicas = deployment.status?.readyReplicas || 0;
|
|
12
29
|
const replicas = deployment.spec?.replicas || 0;
|
|
30
|
+
// If main deployment has 0 replicas, check devspace deployment
|
|
31
|
+
if (replicas === 0) {
|
|
32
|
+
try {
|
|
33
|
+
const devResult = await execa('kubectl', [
|
|
34
|
+
'get',
|
|
35
|
+
'deployment',
|
|
36
|
+
'ark-controller-devspace',
|
|
37
|
+
'-n',
|
|
38
|
+
'ark-system',
|
|
39
|
+
'-o',
|
|
40
|
+
'json',
|
|
41
|
+
], { stdio: 'pipe' });
|
|
42
|
+
const devDeployment = JSON.parse(devResult.stdout);
|
|
43
|
+
const devReadyReplicas = devDeployment.status?.readyReplicas || 0;
|
|
44
|
+
const devReplicas = devDeployment.spec?.replicas || 0;
|
|
45
|
+
// ARK is ready if devspace deployment has ready replicas
|
|
46
|
+
return devReadyReplicas > 0 && devReadyReplicas === devReplicas;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Devspace deployment doesn't exist
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
13
53
|
// ARK is ready if deployment exists and has at least one ready replica
|
|
14
54
|
return readyReplicas > 0 && readyReplicas === replicas;
|
|
15
55
|
}
|
|
16
56
|
catch {
|
|
17
|
-
//
|
|
18
|
-
|
|
57
|
+
// Main deployment doesn't exist, try devspace deployment
|
|
58
|
+
try {
|
|
59
|
+
const devResult = await execa('kubectl', [
|
|
60
|
+
'get',
|
|
61
|
+
'deployment',
|
|
62
|
+
'ark-controller-devspace',
|
|
63
|
+
'-n',
|
|
64
|
+
'ark-system',
|
|
65
|
+
'-o',
|
|
66
|
+
'json',
|
|
67
|
+
], { stdio: 'pipe' });
|
|
68
|
+
const devDeployment = JSON.parse(devResult.stdout);
|
|
69
|
+
const devReadyReplicas = devDeployment.status?.readyReplicas || 0;
|
|
70
|
+
const devReplicas = devDeployment.spec?.replicas || 0;
|
|
71
|
+
// ARK is ready if devspace deployment has ready replicas
|
|
72
|
+
return devReadyReplicas > 0 && devReadyReplicas === devReplicas;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Neither deployment exists or kubectl failed
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
19
78
|
}
|
|
20
79
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
2
|
+
// Mock execa using unstable_mockModule
|
|
3
|
+
jest.unstable_mockModule('execa', () => ({
|
|
4
|
+
execa: jest.fn(),
|
|
5
|
+
}));
|
|
6
|
+
// Dynamic imports after mock
|
|
7
|
+
const { execa } = await import('execa');
|
|
8
|
+
const { isArkReady } = await import('./arkStatus.js');
|
|
9
|
+
describe('arkStatus with __mocks__', () => {
|
|
10
|
+
describe('isArkReady', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
it('should return true when ark-controller is deployed and ready', async () => {
|
|
15
|
+
// Mock successful kubectl response with ready deployment
|
|
16
|
+
const mockDeployment = {
|
|
17
|
+
metadata: { name: 'ark-controller' },
|
|
18
|
+
spec: { replicas: 3 },
|
|
19
|
+
status: {
|
|
20
|
+
readyReplicas: 3,
|
|
21
|
+
availableReplicas: 3,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
execa.mockResolvedValue({
|
|
25
|
+
stdout: JSON.stringify(mockDeployment),
|
|
26
|
+
stderr: '',
|
|
27
|
+
exitCode: 0,
|
|
28
|
+
failed: false,
|
|
29
|
+
});
|
|
30
|
+
const result = await isArkReady();
|
|
31
|
+
expect(result).toBe(true);
|
|
32
|
+
expect(execa).toHaveBeenCalledWith('kubectl', [
|
|
33
|
+
'get',
|
|
34
|
+
'deployment',
|
|
35
|
+
'ark-controller',
|
|
36
|
+
'-n',
|
|
37
|
+
'ark-system',
|
|
38
|
+
'-o',
|
|
39
|
+
'json',
|
|
40
|
+
], { stdio: 'pipe' });
|
|
41
|
+
});
|
|
42
|
+
it('should return false when kubectl fails', async () => {
|
|
43
|
+
// Mock kubectl failure
|
|
44
|
+
execa.mockRejectedValue(new Error('kubectl not found'));
|
|
45
|
+
const result = await isArkReady();
|
|
46
|
+
expect(result).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
package/dist/lib/chatClient.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import output from './output.js';
|
|
2
1
|
export class ChatClient {
|
|
3
2
|
constructor(arkApiClient) {
|
|
4
3
|
this.arkApiClient = arkApiClient;
|
|
@@ -93,8 +92,7 @@ export class ChatClient {
|
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
catch (error) {
|
|
96
|
-
|
|
97
|
-
output.error('failed to call openai api:', errorMessage);
|
|
95
|
+
// Don't log here - error will be displayed in the message thread
|
|
98
96
|
throw error;
|
|
99
97
|
}
|
|
100
98
|
}
|
package/dist/lib/cluster.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execa } from 'execa';
|
|
2
2
|
export async function detectClusterType() {
|
|
3
3
|
try {
|
|
4
|
-
const { stdout } = await
|
|
5
|
-
'config',
|
|
6
|
-
'current-context',
|
|
7
|
-
]);
|
|
4
|
+
const { stdout } = await execa('kubectl', ['config', 'current-context']);
|
|
8
5
|
const context = stdout.trim();
|
|
9
6
|
if (context.includes('minikube')) {
|
|
10
7
|
return { type: 'minikube', context };
|
|
@@ -39,7 +36,7 @@ export async function getClusterInfo(context) {
|
|
|
39
36
|
// If context is provided, use it
|
|
40
37
|
const contextArgs = context ? ['--context', context] : [];
|
|
41
38
|
// Get all config info in one command
|
|
42
|
-
const { stdout: configJson } = await
|
|
39
|
+
const { stdout: configJson } = await execa('kubectl', [
|
|
43
40
|
'config',
|
|
44
41
|
'view',
|
|
45
42
|
'--minify',
|
|
@@ -62,12 +59,12 @@ export async function getClusterInfo(context) {
|
|
|
62
59
|
switch (clusterInfo.type) {
|
|
63
60
|
case 'minikube':
|
|
64
61
|
try {
|
|
65
|
-
const { stdout } = await
|
|
62
|
+
const { stdout } = await execa('minikube', ['ip']);
|
|
66
63
|
ip = stdout.trim();
|
|
67
64
|
}
|
|
68
65
|
catch {
|
|
69
66
|
// Fallback to kubectl if minikube command fails
|
|
70
|
-
const { stdout } = await
|
|
67
|
+
const { stdout } = await execa('kubectl', [
|
|
71
68
|
'get',
|
|
72
69
|
'nodes',
|
|
73
70
|
'-o',
|
|
@@ -77,7 +74,7 @@ export async function getClusterInfo(context) {
|
|
|
77
74
|
}
|
|
78
75
|
break;
|
|
79
76
|
case 'kind': {
|
|
80
|
-
const { stdout: kindOutput } = await
|
|
77
|
+
const { stdout: kindOutput } = await execa('kubectl', [
|
|
81
78
|
'get',
|
|
82
79
|
'nodes',
|
|
83
80
|
'-o',
|
|
@@ -90,7 +87,7 @@ export async function getClusterInfo(context) {
|
|
|
90
87
|
ip = 'localhost';
|
|
91
88
|
break;
|
|
92
89
|
case 'k3s': {
|
|
93
|
-
const { stdout: k3sOutput } = await
|
|
90
|
+
const { stdout: k3sOutput } = await execa('kubectl', [
|
|
94
91
|
'get',
|
|
95
92
|
'nodes',
|
|
96
93
|
'-o',
|
|
@@ -102,7 +99,7 @@ export async function getClusterInfo(context) {
|
|
|
102
99
|
case 'cloud':
|
|
103
100
|
// For cloud clusters, try to get the external IP or load balancer IP
|
|
104
101
|
try {
|
|
105
|
-
const { stdout: lbOutput } = await
|
|
102
|
+
const { stdout: lbOutput } = await execa('kubectl', [
|
|
106
103
|
'get',
|
|
107
104
|
'svc',
|
|
108
105
|
'-n',
|
|
@@ -113,7 +110,7 @@ export async function getClusterInfo(context) {
|
|
|
113
110
|
]);
|
|
114
111
|
ip = lbOutput.trim();
|
|
115
112
|
if (!ip) {
|
|
116
|
-
const { stdout: hostnameOutput } = await
|
|
113
|
+
const { stdout: hostnameOutput } = await execa('kubectl', [
|
|
117
114
|
'get',
|
|
118
115
|
'svc',
|
|
119
116
|
'-n',
|
|
@@ -127,7 +124,7 @@ export async function getClusterInfo(context) {
|
|
|
127
124
|
}
|
|
128
125
|
catch {
|
|
129
126
|
// Fallback to node IP
|
|
130
|
-
const { stdout: nodeOutput } = await
|
|
127
|
+
const { stdout: nodeOutput } = await execa('kubectl', [
|
|
131
128
|
'get',
|
|
132
129
|
'nodes',
|
|
133
130
|
'-o',
|
|
@@ -137,7 +134,7 @@ export async function getClusterInfo(context) {
|
|
|
137
134
|
}
|
|
138
135
|
break;
|
|
139
136
|
default: {
|
|
140
|
-
const { stdout: defaultOutput } = await
|
|
137
|
+
const { stdout: defaultOutput } = await execa('kubectl', [
|
|
141
138
|
'get',
|
|
142
139
|
'nodes',
|
|
143
140
|
'-o',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
const mockExeca = jest.fn();
|
|
3
|
+
jest.unstable_mockModule('execa', () => ({
|
|
4
|
+
execa: mockExeca,
|
|
5
|
+
}));
|
|
6
|
+
const { getClusterInfo, detectClusterType } = await import('./cluster.js');
|
|
7
|
+
describe('cluster', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
});
|
|
11
|
+
describe('detectClusterType', () => {
|
|
12
|
+
it('detects minikube cluster', async () => {
|
|
13
|
+
mockExeca.mockResolvedValue({ stdout: 'minikube' });
|
|
14
|
+
const result = await detectClusterType();
|
|
15
|
+
expect(result).toEqual({ type: 'minikube', context: 'minikube' });
|
|
16
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', [
|
|
17
|
+
'config',
|
|
18
|
+
'current-context',
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
it('detects kind cluster', async () => {
|
|
22
|
+
mockExeca.mockResolvedValue({ stdout: 'kind-kind' });
|
|
23
|
+
const result = await detectClusterType();
|
|
24
|
+
expect(result).toEqual({ type: 'kind', context: 'kind-kind' });
|
|
25
|
+
});
|
|
26
|
+
it('detects k3s cluster', async () => {
|
|
27
|
+
mockExeca.mockResolvedValue({ stdout: 'k3s-default' });
|
|
28
|
+
const result = await detectClusterType();
|
|
29
|
+
expect(result).toEqual({ type: 'k3s', context: 'k3s-default' });
|
|
30
|
+
});
|
|
31
|
+
it('detects docker-desktop cluster', async () => {
|
|
32
|
+
mockExeca.mockResolvedValue({ stdout: 'docker-desktop' });
|
|
33
|
+
const result = await detectClusterType();
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
type: 'docker-desktop',
|
|
36
|
+
context: 'docker-desktop',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('detects gke cloud cluster', async () => {
|
|
40
|
+
mockExeca.mockResolvedValue({ stdout: 'gke_project_zone_cluster' });
|
|
41
|
+
const result = await detectClusterType();
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
type: 'cloud',
|
|
44
|
+
context: 'gke_project_zone_cluster',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('detects eks cloud cluster', async () => {
|
|
48
|
+
mockExeca.mockResolvedValue({
|
|
49
|
+
stdout: 'arn:aws:eks:region:account:cluster/name',
|
|
50
|
+
});
|
|
51
|
+
const result = await detectClusterType();
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
type: 'cloud',
|
|
54
|
+
context: 'arn:aws:eks:region:account:cluster/name',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it('detects aks cloud cluster', async () => {
|
|
58
|
+
mockExeca.mockResolvedValue({ stdout: 'aks-cluster-name' });
|
|
59
|
+
const result = await detectClusterType();
|
|
60
|
+
expect(result).toEqual({ type: 'cloud', context: 'aks-cluster-name' });
|
|
61
|
+
});
|
|
62
|
+
it('returns unknown for unrecognized cluster', async () => {
|
|
63
|
+
mockExeca.mockResolvedValue({ stdout: 'some-other-cluster' });
|
|
64
|
+
const result = await detectClusterType();
|
|
65
|
+
expect(result).toEqual({ type: 'unknown', context: 'some-other-cluster' });
|
|
66
|
+
});
|
|
67
|
+
it('handles kubectl error', async () => {
|
|
68
|
+
mockExeca.mockRejectedValue(new Error('kubectl not found'));
|
|
69
|
+
const result = await detectClusterType();
|
|
70
|
+
expect(result).toEqual({ type: 'unknown', error: 'kubectl not found' });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('getClusterInfo', () => {
|
|
74
|
+
const mockConfig = {
|
|
75
|
+
'current-context': 'minikube',
|
|
76
|
+
contexts: [
|
|
77
|
+
{
|
|
78
|
+
name: 'minikube',
|
|
79
|
+
context: {
|
|
80
|
+
namespace: 'default',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
it('gets minikube cluster info with IP', async () => {
|
|
86
|
+
mockExeca
|
|
87
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(mockConfig) })
|
|
88
|
+
.mockResolvedValueOnce({ stdout: 'minikube' })
|
|
89
|
+
.mockResolvedValueOnce({ stdout: '192.168.49.2' });
|
|
90
|
+
const result = await getClusterInfo();
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
type: 'minikube',
|
|
93
|
+
context: 'minikube',
|
|
94
|
+
namespace: 'default',
|
|
95
|
+
ip: '192.168.49.2',
|
|
96
|
+
});
|
|
97
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', [
|
|
98
|
+
'config',
|
|
99
|
+
'view',
|
|
100
|
+
'--minify',
|
|
101
|
+
'-o',
|
|
102
|
+
'json',
|
|
103
|
+
]);
|
|
104
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', [
|
|
105
|
+
'config',
|
|
106
|
+
'current-context',
|
|
107
|
+
]);
|
|
108
|
+
expect(mockExeca).toHaveBeenCalledWith('minikube', ['ip']);
|
|
109
|
+
});
|
|
110
|
+
it('falls back to kubectl for minikube IP if minikube command fails', async () => {
|
|
111
|
+
mockExeca
|
|
112
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(mockConfig) })
|
|
113
|
+
.mockResolvedValueOnce({ stdout: 'minikube' })
|
|
114
|
+
.mockRejectedValueOnce(new Error('minikube not found'))
|
|
115
|
+
.mockResolvedValueOnce({ stdout: '192.168.49.2' });
|
|
116
|
+
const result = await getClusterInfo();
|
|
117
|
+
expect(result.ip).toBe('192.168.49.2');
|
|
118
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', [
|
|
119
|
+
'get',
|
|
120
|
+
'nodes',
|
|
121
|
+
'-o',
|
|
122
|
+
'jsonpath={.items[0].status.addresses[?(@.type=="InternalIP")].address}',
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
it('gets kind cluster info with IP', async () => {
|
|
126
|
+
const kindConfig = {
|
|
127
|
+
'current-context': 'kind-kind',
|
|
128
|
+
contexts: [
|
|
129
|
+
{
|
|
130
|
+
name: 'kind-kind',
|
|
131
|
+
context: {
|
|
132
|
+
namespace: 'kube-system',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
mockExeca
|
|
138
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(kindConfig) })
|
|
139
|
+
.mockResolvedValueOnce({ stdout: 'kind-kind' })
|
|
140
|
+
.mockResolvedValueOnce({ stdout: '172.18.0.2' });
|
|
141
|
+
const result = await getClusterInfo();
|
|
142
|
+
expect(result).toEqual({
|
|
143
|
+
type: 'kind',
|
|
144
|
+
context: 'kind-kind',
|
|
145
|
+
namespace: 'kube-system',
|
|
146
|
+
ip: '172.18.0.2',
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
it('gets docker-desktop cluster info', async () => {
|
|
150
|
+
const dockerConfig = {
|
|
151
|
+
'current-context': 'docker-desktop',
|
|
152
|
+
contexts: [
|
|
153
|
+
{
|
|
154
|
+
name: 'docker-desktop',
|
|
155
|
+
context: {},
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
mockExeca
|
|
160
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(dockerConfig) })
|
|
161
|
+
.mockResolvedValueOnce({ stdout: 'docker-desktop' });
|
|
162
|
+
const result = await getClusterInfo();
|
|
163
|
+
expect(result).toEqual({
|
|
164
|
+
type: 'docker-desktop',
|
|
165
|
+
context: 'docker-desktop',
|
|
166
|
+
namespace: 'default',
|
|
167
|
+
ip: 'localhost',
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
it('gets cloud cluster info with load balancer IP', async () => {
|
|
171
|
+
const cloudConfig = {
|
|
172
|
+
'current-context': 'gke_project_zone_cluster',
|
|
173
|
+
contexts: [
|
|
174
|
+
{
|
|
175
|
+
name: 'gke_project_zone_cluster',
|
|
176
|
+
context: {
|
|
177
|
+
namespace: 'production',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
mockExeca
|
|
183
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(cloudConfig) })
|
|
184
|
+
.mockResolvedValueOnce({ stdout: 'gke_project_zone_cluster' })
|
|
185
|
+
.mockResolvedValueOnce({ stdout: '35.201.125.17' });
|
|
186
|
+
const result = await getClusterInfo();
|
|
187
|
+
expect(result).toEqual({
|
|
188
|
+
type: 'cloud',
|
|
189
|
+
context: 'gke_project_zone_cluster',
|
|
190
|
+
namespace: 'production',
|
|
191
|
+
ip: '35.201.125.17',
|
|
192
|
+
});
|
|
193
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', [
|
|
194
|
+
'get',
|
|
195
|
+
'svc',
|
|
196
|
+
'-n',
|
|
197
|
+
'istio-system',
|
|
198
|
+
'istio-ingressgateway',
|
|
199
|
+
'-o',
|
|
200
|
+
'jsonpath={.status.loadBalancer.ingress[0].ip}',
|
|
201
|
+
]);
|
|
202
|
+
});
|
|
203
|
+
it('falls back to hostname for cloud cluster if no IP', async () => {
|
|
204
|
+
const cloudConfig = {
|
|
205
|
+
'current-context': 'eks-cluster',
|
|
206
|
+
contexts: [
|
|
207
|
+
{
|
|
208
|
+
name: 'eks-cluster',
|
|
209
|
+
context: {},
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
mockExeca
|
|
214
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(cloudConfig) })
|
|
215
|
+
.mockResolvedValueOnce({ stdout: 'eks-cluster' })
|
|
216
|
+
.mockResolvedValueOnce({ stdout: '' })
|
|
217
|
+
.mockResolvedValueOnce({ stdout: 'a1234.elb.amazonaws.com' });
|
|
218
|
+
const result = await getClusterInfo();
|
|
219
|
+
expect(result.ip).toBe('a1234.elb.amazonaws.com');
|
|
220
|
+
});
|
|
221
|
+
it('falls back to external node IP for cloud cluster', async () => {
|
|
222
|
+
const cloudConfig = {
|
|
223
|
+
'current-context': 'gke-cluster',
|
|
224
|
+
contexts: [
|
|
225
|
+
{
|
|
226
|
+
name: 'gke-cluster',
|
|
227
|
+
context: {},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
mockExeca
|
|
232
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(cloudConfig) })
|
|
233
|
+
.mockResolvedValueOnce({ stdout: 'gke-cluster' })
|
|
234
|
+
.mockRejectedValueOnce(new Error('service not found'))
|
|
235
|
+
.mockResolvedValueOnce({ stdout: '35.201.125.18' });
|
|
236
|
+
const result = await getClusterInfo();
|
|
237
|
+
expect(result.ip).toBe('35.201.125.18');
|
|
238
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', [
|
|
239
|
+
'get',
|
|
240
|
+
'nodes',
|
|
241
|
+
'-o',
|
|
242
|
+
'jsonpath={.items[0].status.addresses[?(@.type=="ExternalIP")].address}',
|
|
243
|
+
]);
|
|
244
|
+
});
|
|
245
|
+
it('gets k3s cluster info', async () => {
|
|
246
|
+
const k3sConfig = {
|
|
247
|
+
'current-context': 'k3s-default',
|
|
248
|
+
contexts: [
|
|
249
|
+
{
|
|
250
|
+
name: 'k3s-default',
|
|
251
|
+
context: {},
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
mockExeca
|
|
256
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(k3sConfig) })
|
|
257
|
+
.mockResolvedValueOnce({ stdout: 'k3s-default' })
|
|
258
|
+
.mockResolvedValueOnce({ stdout: '10.0.0.5' });
|
|
259
|
+
const result = await getClusterInfo();
|
|
260
|
+
expect(result).toEqual({
|
|
261
|
+
type: 'k3s',
|
|
262
|
+
context: 'k3s-default',
|
|
263
|
+
namespace: 'default',
|
|
264
|
+
ip: '10.0.0.5',
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
it('uses provided context parameter', async () => {
|
|
268
|
+
const multiConfig = {
|
|
269
|
+
'current-context': 'kind-staging',
|
|
270
|
+
contexts: [
|
|
271
|
+
{
|
|
272
|
+
name: 'kind-staging',
|
|
273
|
+
context: {
|
|
274
|
+
namespace: 'staging-ns',
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
mockExeca
|
|
280
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(multiConfig) })
|
|
281
|
+
.mockResolvedValueOnce({ stdout: 'kind-staging' })
|
|
282
|
+
.mockResolvedValueOnce({ stdout: '172.18.0.3' });
|
|
283
|
+
const result = await getClusterInfo('kind-staging');
|
|
284
|
+
expect(result.context).toBe('kind-staging');
|
|
285
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', [
|
|
286
|
+
'config',
|
|
287
|
+
'view',
|
|
288
|
+
'--minify',
|
|
289
|
+
'-o',
|
|
290
|
+
'json',
|
|
291
|
+
'--context',
|
|
292
|
+
'kind-staging',
|
|
293
|
+
]);
|
|
294
|
+
});
|
|
295
|
+
it('handles unknown cluster type', async () => {
|
|
296
|
+
const unknownConfig = {
|
|
297
|
+
'current-context': 'custom-cluster',
|
|
298
|
+
contexts: [
|
|
299
|
+
{
|
|
300
|
+
name: 'custom-cluster',
|
|
301
|
+
context: {},
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
mockExeca
|
|
306
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(unknownConfig) })
|
|
307
|
+
.mockResolvedValueOnce({ stdout: 'custom-cluster' })
|
|
308
|
+
.mockResolvedValueOnce({ stdout: '10.0.0.1' });
|
|
309
|
+
const result = await getClusterInfo();
|
|
310
|
+
expect(result).toEqual({
|
|
311
|
+
type: 'unknown',
|
|
312
|
+
context: 'custom-cluster',
|
|
313
|
+
namespace: 'default',
|
|
314
|
+
ip: '10.0.0.1',
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
it('handles kubectl config error', async () => {
|
|
318
|
+
mockExeca.mockRejectedValue(new Error('kubectl not configured'));
|
|
319
|
+
const result = await getClusterInfo();
|
|
320
|
+
expect(result).toEqual({
|
|
321
|
+
type: 'unknown',
|
|
322
|
+
error: 'kubectl not configured',
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
it('handles missing context in config', async () => {
|
|
326
|
+
const emptyConfig = {
|
|
327
|
+
contexts: [],
|
|
328
|
+
};
|
|
329
|
+
mockExeca
|
|
330
|
+
.mockResolvedValueOnce({ stdout: JSON.stringify(emptyConfig) })
|
|
331
|
+
.mockResolvedValueOnce({ stdout: '' })
|
|
332
|
+
.mockResolvedValueOnce({ stdout: '10.0.0.1' });
|
|
333
|
+
const result = await getClusterInfo();
|
|
334
|
+
expect(result.context).toBe('');
|
|
335
|
+
expect(result.namespace).toBe('default');
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
package/dist/lib/commandUtils.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from 'util';
|
|
3
|
-
const execAsync = promisify(exec);
|
|
1
|
+
import { execa } from 'execa';
|
|
4
2
|
/**
|
|
5
3
|
* Check if a command is available in the system
|
|
6
4
|
*/
|
|
7
5
|
export async function isCommandAvailable(command) {
|
|
8
6
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
await execa('where', [command]);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
await execa('sh', ['-c', `command -v ${command}`]);
|
|
12
|
+
}
|
|
13
13
|
return true;
|
|
14
14
|
}
|
|
15
15
|
catch (_error) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Options } from 'execa';
|
|
2
|
+
/**
|
|
3
|
+
* Check if a command exists and is executable by running it with specified args
|
|
4
|
+
*/
|
|
5
|
+
export declare function checkCommandExists(command: string, args?: string[]): Promise<boolean>;
|
|
6
|
+
export { checkCommandExists as isCommandAvailable };
|
|
7
|
+
/**
|
|
8
|
+
* Execute a command with optional verbose output
|
|
9
|
+
* @param command The command to execute
|
|
10
|
+
* @param args Array of arguments
|
|
11
|
+
* @param execaOptions Standard execa options
|
|
12
|
+
* @param additionalOptions Additional options for execute (e.g., verbose)
|
|
13
|
+
*/
|
|
14
|
+
export declare function execute(command: string, args?: string[], execaOptions?: Options, additionalOptions?: {
|
|
15
|
+
verbose?: boolean;
|
|
16
|
+
}): Promise<import("execa").Result<Options>>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
/**
|
|
4
|
+
* Check if a command exists and is executable by running it with specified args
|
|
5
|
+
*/
|
|
6
|
+
export async function checkCommandExists(command, args = ['--version']) {
|
|
7
|
+
try {
|
|
8
|
+
await execa(command, args);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export { checkCommandExists as isCommandAvailable };
|
|
16
|
+
/**
|
|
17
|
+
* Execute a command with optional verbose output
|
|
18
|
+
* @param command The command to execute
|
|
19
|
+
* @param args Array of arguments
|
|
20
|
+
* @param execaOptions Standard execa options
|
|
21
|
+
* @param additionalOptions Additional options for execute (e.g., verbose)
|
|
22
|
+
*/
|
|
23
|
+
export async function execute(command, args = [], execaOptions = {}, additionalOptions = {}) {
|
|
24
|
+
const { verbose = false } = additionalOptions;
|
|
25
|
+
if (verbose) {
|
|
26
|
+
console.log(chalk.gray(`$ ${command} ${args.join(' ')}`));
|
|
27
|
+
}
|
|
28
|
+
return await execa(command, args, execaOptions);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|