@agents-at-scale/ark 0.1.35 → 0.1.36
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 +42 -0
- package/dist/arkServices.js +138 -0
- package/dist/arkServices.spec.d.ts +1 -0
- package/dist/arkServices.spec.js +24 -0
- package/dist/commands/agents/index.d.ts +3 -0
- package/dist/commands/agents/index.js +65 -0
- 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 +3 -0
- package/dist/commands/chat/index.js +29 -0
- package/dist/commands/cluster/get.d.ts +2 -0
- package/dist/commands/cluster/get.js +39 -0
- 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 +3 -5
- 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 +3 -0
- package/dist/commands/completion/index.js +230 -0
- 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 +3 -0
- package/dist/commands/config/index.js +42 -0
- 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 +4 -0
- package/dist/commands/dashboard/index.js +39 -0
- package/dist/commands/docs/index.d.ts +4 -0
- package/dist/commands/docs/index.js +18 -0
- package/dist/commands/generate/config.js +5 -24
- package/dist/commands/generate/generators/mcpserver.d.ts +2 -1
- package/dist/commands/generate/generators/mcpserver.js +26 -5
- 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 +8 -0
- package/dist/commands/install/index.js +295 -0
- package/dist/commands/install/index.spec.d.ts +1 -0
- package/dist/commands/install/index.spec.js +143 -0
- package/dist/commands/models/create.d.ts +1 -0
- package/dist/commands/models/create.js +213 -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 +3 -0
- package/dist/commands/models/index.js +75 -0
- package/dist/commands/models/index.spec.d.ts +1 -0
- package/dist/commands/models/index.spec.js +96 -0
- package/dist/commands/query/index.d.ts +3 -0
- package/dist/commands/query/index.js +24 -0
- package/dist/commands/query/index.spec.d.ts +1 -0
- package/dist/commands/query/index.spec.js +53 -0
- package/dist/commands/routes/index.d.ts +3 -0
- package/dist/commands/routes/index.js +93 -0
- package/dist/commands/status/index.d.ts +3 -0
- package/dist/commands/status/index.js +281 -0
- package/dist/commands/targets/index.d.ts +3 -0
- package/dist/commands/targets/index.js +72 -0
- package/dist/commands/targets/index.spec.d.ts +1 -0
- package/dist/commands/targets/index.spec.js +154 -0
- package/dist/commands/teams/index.d.ts +3 -0
- package/dist/commands/teams/index.js +64 -0
- 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 +3 -0
- package/dist/commands/tools/index.js +49 -0
- 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 +3 -0
- package/dist/commands/uninstall/index.js +101 -0
- package/dist/commands/uninstall/index.spec.d.ts +1 -0
- package/dist/commands/uninstall/index.spec.js +125 -0
- package/dist/components/ChatUI.d.ts +16 -0
- package/dist/components/ChatUI.js +801 -0
- package/dist/components/statusChecker.d.ts +14 -24
- package/dist/components/statusChecker.js +295 -129
- package/dist/index.d.ts +1 -1
- package/dist/index.js +42 -42
- package/dist/lib/arkApiClient.d.ts +53 -0
- package/dist/lib/arkApiClient.js +102 -0
- package/dist/lib/arkApiProxy.d.ts +9 -0
- package/dist/lib/arkApiProxy.js +22 -0
- package/dist/lib/arkServiceProxy.d.ts +14 -0
- package/dist/lib/arkServiceProxy.js +95 -0
- package/dist/lib/arkStatus.d.ts +10 -0
- package/dist/lib/arkStatus.js +79 -0
- package/dist/lib/arkStatus.spec.d.ts +1 -0
- package/dist/lib/arkStatus.spec.js +49 -0
- package/dist/lib/chatClient.d.ts +33 -0
- package/dist/lib/chatClient.js +93 -0
- package/dist/lib/cluster.d.ts +2 -1
- package/dist/lib/cluster.js +37 -16
- package/dist/lib/cluster.spec.d.ts +1 -0
- package/dist/lib/cluster.spec.js +338 -0
- 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 +26 -80
- package/dist/lib/config.js +70 -205
- package/dist/lib/config.spec.d.ts +1 -0
- package/dist/lib/config.spec.js +99 -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/executeQuery.d.ts +20 -0
- package/dist/lib/executeQuery.js +135 -0
- package/dist/lib/executeQuery.spec.d.ts +1 -0
- package/dist/lib/executeQuery.spec.js +170 -0
- package/dist/lib/nextSteps.d.ts +4 -0
- package/dist/lib/nextSteps.js +20 -0
- package/dist/lib/nextSteps.spec.d.ts +1 -0
- package/dist/lib/nextSteps.spec.js +59 -0
- package/dist/lib/output.d.ts +36 -0
- package/dist/lib/output.js +89 -0
- package/dist/lib/output.spec.d.ts +1 -0
- package/dist/lib/output.spec.js +123 -0
- package/dist/lib/startup.d.ts +9 -0
- package/dist/lib/startup.js +87 -0
- package/dist/lib/startup.spec.d.ts +1 -0
- package/dist/lib/startup.spec.js +152 -0
- package/dist/lib/types.d.ts +87 -3
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +51 -0
- package/dist/types/types.d.ts +40 -0
- package/dist/types/types.js +1 -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 +226 -91
- 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 -7
- package/dist/ui/statusFormatter.js +39 -39
- package/dist/ui/statusFormatter.spec.d.ts +1 -0
- package/dist/ui/statusFormatter.spec.js +58 -0
- package/package.json +16 -5
- package/dist/commands/cluster/get-ip.d.ts +0 -2
- package/dist/commands/cluster/get-ip.js +0 -32
- package/dist/commands/cluster/get-type.d.ts +0 -2
- package/dist/commands/cluster/get-type.js +0 -26
- package/dist/commands/completion.d.ts +0 -2
- package/dist/commands/completion.js +0 -108
- package/dist/commands/config.d.ts +0 -5
- package/dist/commands/config.js +0 -327
- package/dist/components/DashboardCLI.d.ts +0 -3
- package/dist/components/DashboardCLI.js +0 -149
- package/dist/config.d.ts +0 -42
- package/dist/config.js +0 -243
- package/dist/lib/arkClient.d.ts +0 -32
- package/dist/lib/arkClient.js +0 -43
- package/dist/lib/consts.d.ts +0 -10
- package/dist/lib/consts.js +0 -15
- package/dist/lib/exec.d.ts +0 -5
- package/dist/lib/exec.js +0 -20
- package/dist/lib/gatewayManager.d.ts +0 -24
- package/dist/lib/gatewayManager.js +0 -85
- package/dist/lib/kubernetes.d.ts +0 -28
- package/dist/lib/kubernetes.js +0 -122
- package/dist/lib/progress.d.ts +0 -128
- package/dist/lib/progress.js +0 -273
- package/dist/lib/wrappers/git.d.ts +0 -2
- package/dist/lib/wrappers/git.js +0 -43
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export class ChatClient {
|
|
2
|
+
constructor(arkApiClient) {
|
|
3
|
+
this.arkApiClient = arkApiClient;
|
|
4
|
+
}
|
|
5
|
+
async getQueryTargets() {
|
|
6
|
+
return await this.arkApiClient.getQueryTargets();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Send a chat completion request
|
|
10
|
+
*/
|
|
11
|
+
async sendMessage(targetId, messages, config, onChunk, signal) {
|
|
12
|
+
const shouldStream = config.streamingEnabled && !!onChunk;
|
|
13
|
+
const params = {
|
|
14
|
+
model: targetId,
|
|
15
|
+
messages: messages,
|
|
16
|
+
signal: signal,
|
|
17
|
+
};
|
|
18
|
+
if (shouldStream) {
|
|
19
|
+
let fullResponse = '';
|
|
20
|
+
const toolCallsById = new Map();
|
|
21
|
+
const stream = this.arkApiClient.createChatCompletionStream(params);
|
|
22
|
+
for await (const chunk of stream) {
|
|
23
|
+
if (signal?.aborted) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
const delta = chunk.choices[0]?.delta;
|
|
27
|
+
// Extract ARK metadata if present
|
|
28
|
+
const arkMetadata = chunk.ark;
|
|
29
|
+
// Handle regular content
|
|
30
|
+
const content = delta?.content || '';
|
|
31
|
+
if (content) {
|
|
32
|
+
fullResponse += content;
|
|
33
|
+
if (onChunk) {
|
|
34
|
+
onChunk(content, undefined, arkMetadata);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Handle tool calls
|
|
38
|
+
if (delta?.tool_calls) {
|
|
39
|
+
for (const toolCallDelta of delta.tool_calls) {
|
|
40
|
+
const index = toolCallDelta.index;
|
|
41
|
+
// Initialize tool call if this is the first chunk for this index
|
|
42
|
+
if (!toolCallsById.has(index)) {
|
|
43
|
+
toolCallsById.set(index, {
|
|
44
|
+
id: toolCallDelta.id || '',
|
|
45
|
+
type: toolCallDelta.type || 'function',
|
|
46
|
+
function: {
|
|
47
|
+
name: toolCallDelta.function?.name || '',
|
|
48
|
+
arguments: '',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Accumulate function arguments
|
|
53
|
+
const toolCall = toolCallsById.get(index);
|
|
54
|
+
if (toolCallDelta.function?.arguments) {
|
|
55
|
+
toolCall.function.arguments += toolCallDelta.function.arguments;
|
|
56
|
+
}
|
|
57
|
+
// Send the current state of all tool calls
|
|
58
|
+
if (onChunk) {
|
|
59
|
+
const toolCallsArray = Array.from(toolCallsById.values());
|
|
60
|
+
onChunk('', toolCallsArray, arkMetadata);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return fullResponse;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const response = await this.arkApiClient.createChatCompletion(params);
|
|
69
|
+
const message = response.choices[0]?.message;
|
|
70
|
+
const content = message?.content || '';
|
|
71
|
+
// Handle tool calls in non-streaming mode
|
|
72
|
+
if (message?.tool_calls && message.tool_calls.length > 0) {
|
|
73
|
+
const toolCalls = message.tool_calls.map((tc) => ({
|
|
74
|
+
id: tc.id,
|
|
75
|
+
type: tc.type || 'function',
|
|
76
|
+
function: {
|
|
77
|
+
name: tc.function?.name || '',
|
|
78
|
+
arguments: tc.function?.arguments || '',
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
// Send tool calls first
|
|
82
|
+
if (onChunk) {
|
|
83
|
+
onChunk('', toolCalls);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Send content after tool calls
|
|
87
|
+
if (content && onChunk) {
|
|
88
|
+
onChunk(content);
|
|
89
|
+
}
|
|
90
|
+
return content;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/lib/cluster.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export interface ClusterInfo {
|
|
|
2
2
|
type: 'minikube' | 'kind' | 'k3s' | 'docker-desktop' | 'cloud' | 'unknown';
|
|
3
3
|
ip?: string;
|
|
4
4
|
context?: string;
|
|
5
|
+
namespace?: string;
|
|
5
6
|
error?: string;
|
|
6
7
|
}
|
|
7
8
|
export declare function detectClusterType(): Promise<ClusterInfo>;
|
|
8
|
-
export declare function
|
|
9
|
+
export declare function getClusterInfo(context?: string): Promise<ClusterInfo>;
|
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 };
|
|
@@ -28,12 +25,33 @@ export async function detectClusterType() {
|
|
|
28
25
|
}
|
|
29
26
|
}
|
|
30
27
|
catch (error) {
|
|
31
|
-
return {
|
|
28
|
+
return {
|
|
29
|
+
type: 'unknown',
|
|
30
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
31
|
+
};
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
export async function
|
|
34
|
+
export async function getClusterInfo(context) {
|
|
35
35
|
try {
|
|
36
|
+
// If context is provided, use it
|
|
37
|
+
const contextArgs = context ? ['--context', context] : [];
|
|
38
|
+
// Get all config info in one command
|
|
39
|
+
const { stdout: configJson } = await execa('kubectl', [
|
|
40
|
+
'config',
|
|
41
|
+
'view',
|
|
42
|
+
'--minify',
|
|
43
|
+
'-o',
|
|
44
|
+
'json',
|
|
45
|
+
...contextArgs,
|
|
46
|
+
]);
|
|
47
|
+
const config = JSON.parse(configJson);
|
|
48
|
+
const currentContext = config['current-context'] || '';
|
|
49
|
+
const contextData = config.contexts?.find((c) => c.name === currentContext);
|
|
50
|
+
const namespace = contextData?.context?.namespace || 'default';
|
|
51
|
+
// Detect cluster type from context name
|
|
36
52
|
const clusterInfo = await detectClusterType();
|
|
53
|
+
clusterInfo.context = currentContext;
|
|
54
|
+
clusterInfo.namespace = namespace;
|
|
37
55
|
if (clusterInfo.error) {
|
|
38
56
|
return clusterInfo;
|
|
39
57
|
}
|
|
@@ -41,12 +59,12 @@ export async function getClusterIp(_context) {
|
|
|
41
59
|
switch (clusterInfo.type) {
|
|
42
60
|
case 'minikube':
|
|
43
61
|
try {
|
|
44
|
-
const { stdout } = await
|
|
62
|
+
const { stdout } = await execa('minikube', ['ip']);
|
|
45
63
|
ip = stdout.trim();
|
|
46
64
|
}
|
|
47
65
|
catch {
|
|
48
66
|
// Fallback to kubectl if minikube command fails
|
|
49
|
-
const { stdout } = await
|
|
67
|
+
const { stdout } = await execa('kubectl', [
|
|
50
68
|
'get',
|
|
51
69
|
'nodes',
|
|
52
70
|
'-o',
|
|
@@ -56,7 +74,7 @@ export async function getClusterIp(_context) {
|
|
|
56
74
|
}
|
|
57
75
|
break;
|
|
58
76
|
case 'kind': {
|
|
59
|
-
const { stdout: kindOutput } = await
|
|
77
|
+
const { stdout: kindOutput } = await execa('kubectl', [
|
|
60
78
|
'get',
|
|
61
79
|
'nodes',
|
|
62
80
|
'-o',
|
|
@@ -69,7 +87,7 @@ export async function getClusterIp(_context) {
|
|
|
69
87
|
ip = 'localhost';
|
|
70
88
|
break;
|
|
71
89
|
case 'k3s': {
|
|
72
|
-
const { stdout: k3sOutput } = await
|
|
90
|
+
const { stdout: k3sOutput } = await execa('kubectl', [
|
|
73
91
|
'get',
|
|
74
92
|
'nodes',
|
|
75
93
|
'-o',
|
|
@@ -81,7 +99,7 @@ export async function getClusterIp(_context) {
|
|
|
81
99
|
case 'cloud':
|
|
82
100
|
// For cloud clusters, try to get the external IP or load balancer IP
|
|
83
101
|
try {
|
|
84
|
-
const { stdout: lbOutput } = await
|
|
102
|
+
const { stdout: lbOutput } = await execa('kubectl', [
|
|
85
103
|
'get',
|
|
86
104
|
'svc',
|
|
87
105
|
'-n',
|
|
@@ -92,7 +110,7 @@ export async function getClusterIp(_context) {
|
|
|
92
110
|
]);
|
|
93
111
|
ip = lbOutput.trim();
|
|
94
112
|
if (!ip) {
|
|
95
|
-
const { stdout: hostnameOutput } = await
|
|
113
|
+
const { stdout: hostnameOutput } = await execa('kubectl', [
|
|
96
114
|
'get',
|
|
97
115
|
'svc',
|
|
98
116
|
'-n',
|
|
@@ -106,7 +124,7 @@ export async function getClusterIp(_context) {
|
|
|
106
124
|
}
|
|
107
125
|
catch {
|
|
108
126
|
// Fallback to node IP
|
|
109
|
-
const { stdout: nodeOutput } = await
|
|
127
|
+
const { stdout: nodeOutput } = await execa('kubectl', [
|
|
110
128
|
'get',
|
|
111
129
|
'nodes',
|
|
112
130
|
'-o',
|
|
@@ -116,7 +134,7 @@ export async function getClusterIp(_context) {
|
|
|
116
134
|
}
|
|
117
135
|
break;
|
|
118
136
|
default: {
|
|
119
|
-
const { stdout: defaultOutput } = await
|
|
137
|
+
const { stdout: defaultOutput } = await execa('kubectl', [
|
|
120
138
|
'get',
|
|
121
139
|
'nodes',
|
|
122
140
|
'-o',
|
|
@@ -129,6 +147,9 @@ export async function getClusterIp(_context) {
|
|
|
129
147
|
return { ...clusterInfo, ip };
|
|
130
148
|
}
|
|
131
149
|
catch (error) {
|
|
132
|
-
return {
|
|
150
|
+
return {
|
|
151
|
+
type: 'unknown',
|
|
152
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
153
|
+
};
|
|
133
154
|
}
|
|
134
155
|
}
|
|
@@ -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
|
+
});
|
|
@@ -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 {};
|