@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.
Files changed (122) hide show
  1. package/dist/arkServices.d.ts +4 -12
  2. package/dist/arkServices.js +19 -34
  3. package/dist/arkServices.spec.d.ts +1 -0
  4. package/dist/arkServices.spec.js +24 -0
  5. package/dist/commands/agents/index.d.ts +2 -1
  6. package/dist/commands/agents/index.js +2 -7
  7. package/dist/commands/agents/index.spec.d.ts +1 -0
  8. package/dist/commands/agents/index.spec.js +67 -0
  9. package/dist/commands/chat/index.d.ts +2 -1
  10. package/dist/commands/chat/index.js +5 -21
  11. package/dist/commands/cluster/get.spec.d.ts +1 -0
  12. package/dist/commands/cluster/get.spec.js +92 -0
  13. package/dist/commands/cluster/index.d.ts +2 -1
  14. package/dist/commands/cluster/index.js +1 -1
  15. package/dist/commands/cluster/index.spec.d.ts +1 -0
  16. package/dist/commands/cluster/index.spec.js +24 -0
  17. package/dist/commands/completion/index.d.ts +2 -1
  18. package/dist/commands/completion/index.js +1 -1
  19. package/dist/commands/completion/index.spec.d.ts +1 -0
  20. package/dist/commands/completion/index.spec.js +34 -0
  21. package/dist/commands/config/index.d.ts +2 -1
  22. package/dist/commands/config/index.js +2 -2
  23. package/dist/commands/config/index.spec.d.ts +1 -0
  24. package/dist/commands/config/index.spec.js +78 -0
  25. package/dist/commands/dashboard/index.d.ts +2 -1
  26. package/dist/commands/dashboard/index.js +1 -1
  27. package/dist/commands/dev/index.d.ts +2 -1
  28. package/dist/commands/dev/index.js +1 -1
  29. package/dist/commands/dev/tool-generate.spec.d.ts +1 -0
  30. package/dist/commands/dev/tool-generate.spec.js +163 -0
  31. package/dist/commands/dev/tool.spec.d.ts +1 -0
  32. package/dist/commands/dev/tool.spec.js +48 -0
  33. package/dist/commands/generate/generators/project.js +22 -41
  34. package/dist/commands/generate/index.d.ts +2 -1
  35. package/dist/commands/generate/index.js +1 -1
  36. package/dist/commands/install/index.d.ts +4 -2
  37. package/dist/commands/install/index.js +215 -78
  38. package/dist/commands/install/index.spec.d.ts +1 -0
  39. package/dist/commands/install/index.spec.js +135 -0
  40. package/dist/commands/models/create.spec.d.ts +1 -0
  41. package/dist/commands/models/create.spec.js +125 -0
  42. package/dist/commands/models/index.d.ts +2 -1
  43. package/dist/commands/models/index.js +2 -7
  44. package/dist/commands/models/index.spec.d.ts +1 -0
  45. package/dist/commands/models/index.spec.js +76 -0
  46. package/dist/commands/routes/index.d.ts +2 -1
  47. package/dist/commands/routes/index.js +1 -9
  48. package/dist/commands/status/index.d.ts +3 -2
  49. package/dist/commands/status/index.js +210 -11
  50. package/dist/commands/targets/index.d.ts +2 -1
  51. package/dist/commands/targets/index.js +1 -1
  52. package/dist/commands/targets/index.spec.d.ts +1 -0
  53. package/dist/commands/targets/index.spec.js +105 -0
  54. package/dist/commands/teams/index.d.ts +2 -1
  55. package/dist/commands/teams/index.js +2 -7
  56. package/dist/commands/teams/index.spec.d.ts +1 -0
  57. package/dist/commands/teams/index.spec.js +70 -0
  58. package/dist/commands/tools/index.d.ts +2 -1
  59. package/dist/commands/tools/index.js +2 -7
  60. package/dist/commands/tools/index.spec.d.ts +1 -0
  61. package/dist/commands/tools/index.spec.js +70 -0
  62. package/dist/commands/uninstall/index.d.ts +2 -1
  63. package/dist/commands/uninstall/index.js +61 -38
  64. package/dist/commands/uninstall/index.spec.d.ts +1 -0
  65. package/dist/commands/uninstall/index.spec.js +117 -0
  66. package/dist/components/ChatUI.js +4 -4
  67. package/dist/components/statusChecker.d.ts +5 -12
  68. package/dist/components/statusChecker.js +172 -89
  69. package/dist/config.d.ts +3 -22
  70. package/dist/config.js +7 -151
  71. package/dist/index.js +22 -19
  72. package/dist/lib/arkServiceProxy.js +4 -2
  73. package/dist/lib/arkStatus.d.ts +5 -0
  74. package/dist/lib/arkStatus.js +61 -2
  75. package/dist/lib/arkStatus.spec.d.ts +1 -0
  76. package/dist/lib/arkStatus.spec.js +49 -0
  77. package/dist/lib/chatClient.js +1 -3
  78. package/dist/lib/cluster.js +11 -14
  79. package/dist/lib/cluster.spec.d.ts +1 -0
  80. package/dist/lib/cluster.spec.js +338 -0
  81. package/dist/lib/commandUtils.js +7 -7
  82. package/dist/lib/commands.d.ts +16 -0
  83. package/dist/lib/commands.js +29 -0
  84. package/dist/lib/commands.spec.d.ts +1 -0
  85. package/dist/lib/commands.spec.js +146 -0
  86. package/dist/lib/config.d.ts +2 -0
  87. package/dist/lib/config.js +6 -4
  88. package/dist/lib/config.spec.d.ts +1 -0
  89. package/dist/lib/config.spec.js +99 -0
  90. package/dist/lib/consts.d.ts +0 -1
  91. package/dist/lib/consts.js +0 -2
  92. package/dist/lib/consts.spec.d.ts +1 -0
  93. package/dist/lib/consts.spec.js +15 -0
  94. package/dist/lib/errors.js +1 -1
  95. package/dist/lib/errors.spec.d.ts +1 -0
  96. package/dist/lib/errors.spec.js +221 -0
  97. package/dist/lib/exec.d.ts +0 -4
  98. package/dist/lib/exec.js +0 -11
  99. package/dist/lib/output.spec.d.ts +1 -0
  100. package/dist/lib/output.spec.js +123 -0
  101. package/dist/lib/portUtils.d.ts +8 -0
  102. package/dist/lib/portUtils.js +39 -0
  103. package/dist/lib/startup.d.ts +5 -0
  104. package/dist/lib/startup.js +73 -0
  105. package/dist/lib/startup.spec.d.ts +1 -0
  106. package/dist/lib/startup.spec.js +168 -0
  107. package/dist/lib/types.d.ts +2 -0
  108. package/dist/ui/AgentSelector.d.ts +8 -0
  109. package/dist/ui/AgentSelector.js +53 -0
  110. package/dist/ui/MainMenu.d.ts +5 -1
  111. package/dist/ui/MainMenu.js +117 -54
  112. package/dist/ui/ModelSelector.d.ts +8 -0
  113. package/dist/ui/ModelSelector.js +53 -0
  114. package/dist/ui/TeamSelector.d.ts +8 -0
  115. package/dist/ui/TeamSelector.js +55 -0
  116. package/dist/ui/ToolSelector.d.ts +8 -0
  117. package/dist/ui/ToolSelector.js +53 -0
  118. package/dist/ui/statusFormatter.d.ts +22 -10
  119. package/dist/ui/statusFormatter.js +37 -109
  120. package/dist/ui/statusFormatter.spec.d.ts +1 -0
  121. package/dist/ui/statusFormatter.spec.js +58 -0
  122. 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
  });
@@ -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
@@ -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
- // Deployment doesn't exist or kubectl failed
18
- return false;
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
+ });
@@ -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
- const errorMessage = error instanceof Error ? error.message : String(error);
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
  }
@@ -1,10 +1,7 @@
1
- import { executeCommand } from './exec.js';
1
+ import { execa } from 'execa';
2
2
  export async function detectClusterType() {
3
3
  try {
4
- const { stdout } = await executeCommand('kubectl', [
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 executeCommand('kubectl', [
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 executeCommand('minikube', ['ip']);
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 executeCommand('kubectl', [
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 executeCommand('kubectl', [
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 executeCommand('kubectl', [
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 executeCommand('kubectl', [
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 executeCommand('kubectl', [
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 executeCommand('kubectl', [
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 executeCommand('kubectl', [
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
+ });
@@ -1,15 +1,15 @@
1
- import { exec } from 'child_process';
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
- const checkCommand = process.platform === 'win32'
10
- ? `where ${command}`
11
- : `command -v ${command}`;
12
- await execAsync(checkCommand);
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 {};