@agents-at-scale/ark 0.1.35-rc.1 → 0.1.35-rc2

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 (130) 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 +24 -2
  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/docs/index.d.ts +4 -0
  34. package/dist/commands/docs/index.js +18 -0
  35. package/dist/commands/generate/generators/project.js +22 -41
  36. package/dist/commands/generate/index.d.ts +2 -1
  37. package/dist/commands/generate/index.js +1 -1
  38. package/dist/commands/install/index.d.ts +4 -2
  39. package/dist/commands/install/index.js +225 -90
  40. package/dist/commands/install/index.spec.d.ts +1 -0
  41. package/dist/commands/install/index.spec.js +143 -0
  42. package/dist/commands/models/create.spec.d.ts +1 -0
  43. package/dist/commands/models/create.spec.js +125 -0
  44. package/dist/commands/models/index.d.ts +2 -1
  45. package/dist/commands/models/index.js +2 -7
  46. package/dist/commands/models/index.spec.d.ts +1 -0
  47. package/dist/commands/models/index.spec.js +76 -0
  48. package/dist/commands/query/index.d.ts +3 -0
  49. package/dist/commands/query/index.js +131 -0
  50. package/dist/commands/routes/index.d.ts +2 -1
  51. package/dist/commands/routes/index.js +1 -9
  52. package/dist/commands/status/index.d.ts +3 -2
  53. package/dist/commands/status/index.js +240 -11
  54. package/dist/commands/targets/index.d.ts +2 -1
  55. package/dist/commands/targets/index.js +1 -1
  56. package/dist/commands/targets/index.spec.d.ts +1 -0
  57. package/dist/commands/targets/index.spec.js +105 -0
  58. package/dist/commands/teams/index.d.ts +2 -1
  59. package/dist/commands/teams/index.js +2 -7
  60. package/dist/commands/teams/index.spec.d.ts +1 -0
  61. package/dist/commands/teams/index.spec.js +70 -0
  62. package/dist/commands/tools/index.d.ts +2 -1
  63. package/dist/commands/tools/index.js +2 -7
  64. package/dist/commands/tools/index.spec.d.ts +1 -0
  65. package/dist/commands/tools/index.spec.js +70 -0
  66. package/dist/commands/uninstall/index.d.ts +2 -1
  67. package/dist/commands/uninstall/index.js +66 -44
  68. package/dist/commands/uninstall/index.spec.d.ts +1 -0
  69. package/dist/commands/uninstall/index.spec.js +125 -0
  70. package/dist/components/ChatUI.js +4 -4
  71. package/dist/components/statusChecker.d.ts +5 -12
  72. package/dist/components/statusChecker.js +193 -90
  73. package/dist/config.d.ts +3 -22
  74. package/dist/config.js +7 -151
  75. package/dist/index.js +26 -19
  76. package/dist/lib/arkServiceProxy.js +4 -2
  77. package/dist/lib/arkStatus.d.ts +5 -0
  78. package/dist/lib/arkStatus.js +61 -2
  79. package/dist/lib/arkStatus.spec.d.ts +1 -0
  80. package/dist/lib/arkStatus.spec.js +49 -0
  81. package/dist/lib/chatClient.js +1 -3
  82. package/dist/lib/cluster.js +11 -14
  83. package/dist/lib/cluster.spec.d.ts +1 -0
  84. package/dist/lib/cluster.spec.js +338 -0
  85. package/dist/lib/commandUtils.js +7 -7
  86. package/dist/lib/commands.d.ts +16 -0
  87. package/dist/lib/commands.js +29 -0
  88. package/dist/lib/commands.spec.d.ts +1 -0
  89. package/dist/lib/commands.spec.js +146 -0
  90. package/dist/lib/config.d.ts +4 -0
  91. package/dist/lib/config.js +6 -4
  92. package/dist/lib/config.spec.d.ts +1 -0
  93. package/dist/lib/config.spec.js +99 -0
  94. package/dist/lib/consts.d.ts +0 -1
  95. package/dist/lib/consts.js +0 -2
  96. package/dist/lib/consts.spec.d.ts +1 -0
  97. package/dist/lib/consts.spec.js +15 -0
  98. package/dist/lib/errors.js +1 -1
  99. package/dist/lib/errors.spec.d.ts +1 -0
  100. package/dist/lib/errors.spec.js +221 -0
  101. package/dist/lib/exec.d.ts +0 -4
  102. package/dist/lib/exec.js +0 -11
  103. package/dist/lib/nextSteps.d.ts +4 -0
  104. package/dist/lib/nextSteps.js +20 -0
  105. package/dist/lib/nextSteps.spec.d.ts +1 -0
  106. package/dist/lib/nextSteps.spec.js +59 -0
  107. package/dist/lib/output.spec.d.ts +1 -0
  108. package/dist/lib/output.spec.js +123 -0
  109. package/dist/lib/portUtils.d.ts +8 -0
  110. package/dist/lib/portUtils.js +39 -0
  111. package/dist/lib/startup.d.ts +9 -0
  112. package/dist/lib/startup.js +93 -0
  113. package/dist/lib/startup.spec.d.ts +1 -0
  114. package/dist/lib/startup.spec.js +168 -0
  115. package/dist/lib/types.d.ts +9 -0
  116. package/dist/ui/AgentSelector.d.ts +8 -0
  117. package/dist/ui/AgentSelector.js +53 -0
  118. package/dist/ui/MainMenu.d.ts +5 -1
  119. package/dist/ui/MainMenu.js +117 -54
  120. package/dist/ui/ModelSelector.d.ts +8 -0
  121. package/dist/ui/ModelSelector.js +53 -0
  122. package/dist/ui/TeamSelector.d.ts +8 -0
  123. package/dist/ui/TeamSelector.js +55 -0
  124. package/dist/ui/ToolSelector.d.ts +8 -0
  125. package/dist/ui/ToolSelector.js +53 -0
  126. package/dist/ui/statusFormatter.d.ts +22 -10
  127. package/dist/ui/statusFormatter.js +37 -109
  128. package/dist/ui/statusFormatter.spec.d.ts +1 -0
  129. package/dist/ui/statusFormatter.spec.js +58 -0
  130. package/package.json +3 -3
@@ -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 {};
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ // Mock chalk to avoid ANSI codes in tests
3
+ jest.unstable_mockModule('chalk', () => ({
4
+ default: {
5
+ gray: (str) => str,
6
+ },
7
+ }));
8
+ // Mock execa using unstable_mockModule
9
+ jest.unstable_mockModule('execa', () => ({
10
+ execa: jest.fn(),
11
+ }));
12
+ // Dynamic imports after mock
13
+ const { execa } = await import('execa');
14
+ const { checkCommandExists, execute } = await import('./commands.js');
15
+ // Type the mock properly
16
+ const mockExeca = execa;
17
+ describe('commands', () => {
18
+ describe('checkCommandExists', () => {
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+ it('returns true when command executes successfully', async () => {
23
+ mockExeca.mockResolvedValue({
24
+ stdout: 'v1.0.0',
25
+ stderr: '',
26
+ exitCode: 0,
27
+ });
28
+ const result = await checkCommandExists('helm', ['version']);
29
+ expect(result).toBe(true);
30
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['version']);
31
+ });
32
+ it('returns false when command fails', async () => {
33
+ mockExeca.mockRejectedValue(new Error('Command not found'));
34
+ const result = await checkCommandExists('nonexistent', ['--version']);
35
+ expect(result).toBe(false);
36
+ expect(mockExeca).toHaveBeenCalledWith('nonexistent', ['--version']);
37
+ });
38
+ it('uses default --version arg when no args provided', async () => {
39
+ mockExeca.mockResolvedValue({
40
+ stdout: '1.0.0',
41
+ stderr: '',
42
+ exitCode: 0,
43
+ });
44
+ const result = await checkCommandExists('node');
45
+ expect(result).toBe(true);
46
+ expect(mockExeca).toHaveBeenCalledWith('node', ['--version']);
47
+ });
48
+ it('uses custom args when provided', async () => {
49
+ mockExeca.mockResolvedValue({
50
+ stdout: 'Client Version: v1.28.0',
51
+ stderr: '',
52
+ exitCode: 0,
53
+ });
54
+ const result = await checkCommandExists('kubectl', [
55
+ 'version',
56
+ '--client',
57
+ ]);
58
+ expect(result).toBe(true);
59
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', [
60
+ 'version',
61
+ '--client',
62
+ ]);
63
+ });
64
+ it('handles empty args array', async () => {
65
+ mockExeca.mockResolvedValue({
66
+ stdout: '',
67
+ stderr: '',
68
+ exitCode: 0,
69
+ });
70
+ const result = await checkCommandExists('echo', []);
71
+ expect(result).toBe(true);
72
+ expect(mockExeca).toHaveBeenCalledWith('echo', []);
73
+ });
74
+ });
75
+ describe('execute', () => {
76
+ let mockConsoleLog;
77
+ beforeEach(() => {
78
+ jest.clearAllMocks();
79
+ mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => { });
80
+ });
81
+ afterEach(() => {
82
+ mockConsoleLog.mockRestore();
83
+ });
84
+ it('executes command without verbose output by default', async () => {
85
+ mockExeca.mockResolvedValue({
86
+ stdout: 'success',
87
+ stderr: '',
88
+ exitCode: 0,
89
+ });
90
+ await execute('helm', ['install', 'test'], { stdio: 'inherit' });
91
+ expect(mockConsoleLog).not.toHaveBeenCalled();
92
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['install', 'test'], {
93
+ stdio: 'inherit',
94
+ });
95
+ });
96
+ it('prints command when verbose is true', async () => {
97
+ mockExeca.mockResolvedValue({
98
+ stdout: 'success',
99
+ stderr: '',
100
+ exitCode: 0,
101
+ });
102
+ await execute('helm', ['install', 'test'], { stdio: 'inherit' }, { verbose: true });
103
+ expect(mockConsoleLog).toHaveBeenCalledWith('$ helm install test');
104
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['install', 'test'], {
105
+ stdio: 'inherit',
106
+ });
107
+ });
108
+ it('works with empty args array', async () => {
109
+ mockExeca.mockResolvedValue({
110
+ stdout: '',
111
+ stderr: '',
112
+ exitCode: 0,
113
+ });
114
+ await execute('ls', [], {}, { verbose: true });
115
+ expect(mockConsoleLog).toHaveBeenCalledWith('$ ls ');
116
+ expect(mockExeca).toHaveBeenCalledWith('ls', [], {});
117
+ });
118
+ it('passes through execa options correctly', async () => {
119
+ mockExeca.mockResolvedValue({
120
+ stdout: '',
121
+ stderr: '',
122
+ exitCode: 0,
123
+ });
124
+ const execaOpts = { stdio: 'pipe', timeout: 5000, cwd: '/tmp' };
125
+ await execute('kubectl', ['get', 'pods'], execaOpts);
126
+ expect(mockConsoleLog).not.toHaveBeenCalled();
127
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'pods'], execaOpts);
128
+ });
129
+ it('handles command failure', async () => {
130
+ const error = new Error('Command failed');
131
+ mockExeca.mockRejectedValue(error);
132
+ await expect(execute('fail', ['now'])).rejects.toThrow('Command failed');
133
+ expect(mockExeca).toHaveBeenCalledWith('fail', ['now'], {});
134
+ });
135
+ it('defaults to no verbose when additionalOptions not provided', async () => {
136
+ mockExeca.mockResolvedValue({
137
+ stdout: 'ok',
138
+ stderr: '',
139
+ exitCode: 0,
140
+ });
141
+ await execute('echo', ['test']);
142
+ expect(mockConsoleLog).not.toHaveBeenCalled();
143
+ expect(mockExeca).toHaveBeenCalledWith('echo', ['test'], {});
144
+ });
145
+ });
146
+ });
@@ -1,9 +1,13 @@
1
+ import type { ClusterInfo } from './cluster.js';
1
2
  export interface ChatConfig {
2
3
  streaming?: boolean;
3
4
  outputFormat?: 'text' | 'markdown';
4
5
  }
5
6
  export interface ArkConfig {
6
7
  chat?: ChatConfig;
8
+ latestVersion?: string;
9
+ currentVersion?: string;
10
+ clusterInfo?: ClusterInfo;
7
11
  }
8
12
  /**
9
13
  * Load configuration from multiple sources with proper precedence:
@@ -24,8 +24,9 @@ export function loadConfig() {
24
24
  const userConfig = yaml.parse(fs.readFileSync(userConfigPath, 'utf-8'));
25
25
  mergeConfig(config, userConfig);
26
26
  }
27
- catch (_e) {
28
- // Silently ignore invalid config files
27
+ catch (e) {
28
+ const message = e instanceof Error ? e.message : 'Unknown error';
29
+ throw new Error(`Invalid YAML in ${userConfigPath}: ${message}`);
29
30
  }
30
31
  }
31
32
  // Load project config from current directory
@@ -35,8 +36,9 @@ export function loadConfig() {
35
36
  const projectConfig = yaml.parse(fs.readFileSync(projectConfigPath, 'utf-8'));
36
37
  mergeConfig(config, projectConfig);
37
38
  }
38
- catch (_e) {
39
- // Silently ignore invalid config files
39
+ catch (e) {
40
+ const message = e instanceof Error ? e.message : 'Unknown error';
41
+ throw new Error(`Invalid YAML in ${projectConfigPath}: ${message}`);
40
42
  }
41
43
  }
42
44
  // Apply environment variable overrides
@@ -0,0 +1 @@
1
+ export {};