@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
@@ -1,46 +1,40 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { execa } from 'execa';
3
+ import { execute } from '../../lib/commands.js';
4
4
  import inquirer from 'inquirer';
5
- import { isCommandAvailable } from '../../lib/commandUtils.js';
6
- import { getClusterInfo } from '../../lib/cluster.js';
5
+ import { showNoClusterError } from '../../lib/startup.js';
7
6
  import output from '../../lib/output.js';
8
7
  import { getInstallableServices, arkDependencies } from '../../arkServices.js';
9
8
  import { isArkReady } from '../../lib/arkStatus.js';
9
+ import { printNextSteps } from '../../lib/nextSteps.js';
10
10
  import ora from 'ora';
11
- export async function installArk(options = {}) {
11
+ async function installService(service, verbose = false) {
12
+ const helmArgs = [
13
+ 'upgrade',
14
+ '--install',
15
+ service.helmReleaseName,
16
+ service.chartPath,
17
+ ];
18
+ // Only add namespace flag if service has explicit namespace
19
+ if (service.namespace) {
20
+ helmArgs.push('--namespace', service.namespace);
21
+ }
22
+ // Add any additional install args
23
+ helmArgs.push(...(service.installArgs || []));
24
+ await execute('helm', helmArgs, { stdio: 'inherit' }, { verbose });
25
+ }
26
+ export async function installArk(config, serviceName, options = {}) {
12
27
  // Validate that --wait-for-ready requires -y
13
28
  if (options.waitForReady && !options.yes) {
14
29
  output.error('--wait-for-ready requires -y flag for non-interactive mode');
15
30
  process.exit(1);
16
31
  }
17
- // Check if helm is installed
18
- const helmInstalled = await isCommandAvailable('helm');
19
- if (!helmInstalled) {
20
- output.error('helm is not installed. please install helm first:');
21
- output.info('https://helm.sh/docs/intro/install/');
22
- process.exit(1);
23
- }
24
- // Check if kubectl is installed (needed for some dependencies)
25
- const kubectlInstalled = await isCommandAvailable('kubectl');
26
- if (!kubectlInstalled) {
27
- output.error('kubectl is not installed. please install kubectl first:');
28
- output.info('https://kubernetes.io/docs/tasks/tools/');
29
- process.exit(1);
30
- }
31
- // Check cluster connectivity
32
- const clusterInfo = await getClusterInfo();
33
- if (clusterInfo.error) {
34
- output.error('no kubernetes cluster detected');
35
- output.info('please ensure you have a running cluster and kubectl is configured.');
36
- output.info('');
37
- output.info('for local development, we recommend minikube:');
38
- output.info('• install: https://minikube.sigs.k8s.io/docs/start');
39
- output.info('• start cluster: minikube start');
40
- output.info('');
41
- output.info('alternatively, you can use kind or docker desktop.');
32
+ // Check cluster connectivity from config
33
+ if (!config.clusterInfo) {
34
+ showNoClusterError();
42
35
  process.exit(1);
43
36
  }
37
+ const clusterInfo = config.clusterInfo;
44
38
  // Show cluster info
45
39
  output.success(`connected to cluster: ${chalk.bold(clusterInfo.context)}`);
46
40
  output.info(`type: ${clusterInfo.type}`);
@@ -49,22 +43,185 @@ export async function installArk(options = {}) {
49
43
  output.info(`ip: ${clusterInfo.ip}`);
50
44
  }
51
45
  console.log(); // Add blank line after cluster info
52
- // Ask about installing dependencies
53
- const shouldInstallDeps = options.yes || (await inquirer.prompt([
54
- {
55
- type: 'confirm',
56
- name: 'shouldInstallDeps',
57
- message: 'install required dependencies (cert-manager, gateway api)?',
58
- default: true,
59
- },
60
- ])).shouldInstallDeps;
61
- if (shouldInstallDeps) {
46
+ // If a specific service is requested, install only that service
47
+ if (serviceName) {
48
+ const services = getInstallableServices();
49
+ const service = Object.values(services).find((s) => s.name === serviceName);
50
+ if (!service) {
51
+ output.error(`service '${serviceName}' not found`);
52
+ output.info('available services:');
53
+ for (const s of Object.values(services)) {
54
+ output.info(` ${s.name}`);
55
+ }
56
+ process.exit(1);
57
+ }
58
+ output.info(`installing ${service.name}...`);
59
+ try {
60
+ await installService(service, options.verbose);
61
+ output.success(`${service.name} installed successfully`);
62
+ }
63
+ catch (error) {
64
+ output.error(`failed to install ${service.name}`);
65
+ console.error(error);
66
+ process.exit(1);
67
+ }
68
+ return;
69
+ }
70
+ // If not using -y flag, show checklist interface
71
+ if (!options.yes) {
72
+ console.log(chalk.cyan.bold('\nSelect components to install:'));
73
+ console.log(chalk.gray('Use arrow keys to navigate, space to toggle, enter to confirm\n'));
74
+ // Build choices for the checkbox prompt
75
+ const allChoices = [
76
+ new inquirer.Separator(chalk.bold('──── Dependencies ────')),
77
+ {
78
+ name: `cert-manager ${chalk.gray('- Certificate management')}`,
79
+ value: 'cert-manager',
80
+ checked: true,
81
+ },
82
+ {
83
+ name: `gateway-api ${chalk.gray('- Gateway API CRDs')}`,
84
+ value: 'gateway-api',
85
+ checked: true,
86
+ },
87
+ new inquirer.Separator(chalk.bold('──── Ark Core ────')),
88
+ {
89
+ name: `ark-controller ${chalk.gray('- Core Ark controller')}`,
90
+ value: 'ark-controller',
91
+ checked: true,
92
+ },
93
+ new inquirer.Separator(chalk.bold('──── Ark Services ────')),
94
+ {
95
+ name: `ark-api ${chalk.gray('- API service')}`,
96
+ value: 'ark-api',
97
+ checked: true,
98
+ },
99
+ {
100
+ name: `ark-dashboard ${chalk.gray('- Web dashboard')}`,
101
+ value: 'ark-dashboard',
102
+ checked: true,
103
+ },
104
+ {
105
+ name: `ark-mcp ${chalk.gray('- MCP services')}`,
106
+ value: 'ark-mcp',
107
+ checked: true,
108
+ },
109
+ {
110
+ name: `localhost-gateway ${chalk.gray('- Gateway for local access')}`,
111
+ value: 'localhost-gateway',
112
+ checked: true,
113
+ },
114
+ ];
115
+ let selectedComponents = [];
116
+ try {
117
+ const answers = await inquirer.prompt([
118
+ {
119
+ type: 'checkbox',
120
+ name: 'components',
121
+ message: 'Components to install:',
122
+ choices: allChoices,
123
+ pageSize: 15,
124
+ },
125
+ ]);
126
+ selectedComponents = answers.components;
127
+ if (selectedComponents.length === 0) {
128
+ output.warning('No components selected. Exiting.');
129
+ process.exit(0);
130
+ }
131
+ }
132
+ catch (error) {
133
+ // Handle Ctrl-C gracefully
134
+ if (error && error.name === 'ExitPromptError') {
135
+ console.log('\nInstallation cancelled');
136
+ process.exit(130);
137
+ }
138
+ throw error;
139
+ }
140
+ // Install dependencies if selected
141
+ const shouldInstallDeps = selectedComponents.includes('cert-manager') ||
142
+ selectedComponents.includes('gateway-api');
143
+ // Install selected dependencies
144
+ if (shouldInstallDeps) {
145
+ // Always install cert-manager repo and update if installing any dependency
146
+ if (selectedComponents.includes('cert-manager') ||
147
+ selectedComponents.includes('gateway-api')) {
148
+ for (const depKey of ['cert-manager-repo', 'helm-repo-update']) {
149
+ const dep = arkDependencies[depKey];
150
+ output.info(`installing ${dep.description || dep.name}...`);
151
+ try {
152
+ await execute(dep.command, dep.args, {
153
+ stdio: 'inherit',
154
+ }, { verbose: options.verbose });
155
+ output.success(`${dep.name} completed`);
156
+ console.log();
157
+ }
158
+ catch {
159
+ console.log();
160
+ process.exit(1);
161
+ }
162
+ }
163
+ }
164
+ // Install cert-manager if selected
165
+ if (selectedComponents.includes('cert-manager')) {
166
+ const dep = arkDependencies['cert-manager'];
167
+ output.info(`installing ${dep.description || dep.name}...`);
168
+ try {
169
+ await execute(dep.command, dep.args, {
170
+ stdio: 'inherit',
171
+ }, { verbose: options.verbose });
172
+ output.success(`${dep.name} completed`);
173
+ console.log();
174
+ }
175
+ catch {
176
+ console.log();
177
+ process.exit(1);
178
+ }
179
+ }
180
+ // Install gateway-api if selected
181
+ if (selectedComponents.includes('gateway-api')) {
182
+ const dep = arkDependencies['gateway-api-crds'];
183
+ output.info(`installing ${dep.description || dep.name}...`);
184
+ try {
185
+ await execute(dep.command, dep.args, {
186
+ stdio: 'inherit',
187
+ }, { verbose: options.verbose });
188
+ output.success(`${dep.name} completed`);
189
+ console.log();
190
+ }
191
+ catch {
192
+ console.log();
193
+ process.exit(1);
194
+ }
195
+ }
196
+ }
197
+ // Install selected services
198
+ const services = getInstallableServices();
199
+ for (const service of Object.values(services)) {
200
+ // Check if this service was selected
201
+ const serviceKey = service.helmReleaseName;
202
+ if (!selectedComponents.includes(serviceKey)) {
203
+ continue;
204
+ }
205
+ output.info(`installing ${service.name}...`);
206
+ try {
207
+ await installService(service, options.verbose);
208
+ console.log(); // Add blank line after command output
209
+ }
210
+ catch {
211
+ // Continue with remaining services on error
212
+ console.log(); // Add blank line after error output
213
+ }
214
+ }
215
+ }
216
+ else {
217
+ // -y flag was used, install everything
218
+ // Install all dependencies
62
219
  for (const dep of Object.values(arkDependencies)) {
63
220
  output.info(`installing ${dep.description || dep.name}...`);
64
221
  try {
65
- await execa(dep.command, dep.args, {
222
+ await execute(dep.command, dep.args, {
66
223
  stdio: 'inherit',
67
- });
224
+ }, { verbose: options.verbose });
68
225
  output.success(`${dep.name} completed`);
69
226
  console.log(); // Add blank line after dependency
70
227
  }
@@ -73,49 +230,25 @@ export async function installArk(options = {}) {
73
230
  process.exit(1);
74
231
  }
75
232
  }
76
- }
77
- // Get installable services and iterate through them
78
- const services = getInstallableServices();
79
- for (const service of Object.values(services)) {
80
- // Ask for confirmation
81
- const shouldInstall = options.yes || (await inquirer.prompt([
82
- {
83
- type: 'confirm',
84
- name: 'shouldInstall',
85
- message: `install ${chalk.bold(service.name)}? ${service.description ? chalk.gray(`(${service.description.toLowerCase()})`) : ''}`,
86
- default: true,
87
- },
88
- ])).shouldInstall;
89
- if (!shouldInstall) {
90
- output.warning(`skipping ${service.name}`);
91
- continue;
92
- }
93
- try {
94
- // Build helm arguments
95
- const helmArgs = [
96
- 'upgrade',
97
- '--install',
98
- service.helmReleaseName,
99
- service.chartPath,
100
- '--namespace',
101
- service.namespace,
102
- ];
103
- // Add any additional args from the service definition
104
- if (service.installArgs) {
105
- helmArgs.push(...service.installArgs);
233
+ // Install all services
234
+ const services = getInstallableServices();
235
+ for (const service of Object.values(services)) {
236
+ output.info(`installing ${service.name}...`);
237
+ try {
238
+ await installService(service, options.verbose);
239
+ console.log(); // Add blank line after command output
240
+ }
241
+ catch {
242
+ // Continue with remaining services on error
243
+ console.log(); // Add blank line after error output
106
244
  }
107
- // Run helm upgrade --install with streaming output
108
- await execa('helm', helmArgs, {
109
- stdio: 'inherit',
110
- });
111
- console.log(); // Add blank line after command output
112
- }
113
- catch {
114
- // Continue with remaining services on error
115
- console.log(); // Add blank line after error output
116
245
  }
117
246
  }
118
- // Wait for ARK to be ready if requested
247
+ // Show next steps after successful installation
248
+ if (!serviceName || serviceName === 'all') {
249
+ printNextSteps();
250
+ }
251
+ // Wait for Ark to be ready if requested
119
252
  if (options.waitForReady) {
120
253
  // Parse timeout value (e.g., '30s', '2m', '60')
121
254
  const parseTimeout = (value) => {
@@ -131,19 +264,19 @@ export async function installArk(options = {}) {
131
264
  const timeoutSeconds = parseTimeout(options.waitForReady);
132
265
  const startTime = Date.now();
133
266
  const endTime = startTime + timeoutSeconds * 1000;
134
- const spinner = ora(`Waiting for ARK to be ready (timeout: ${timeoutSeconds}s)...`).start();
267
+ const spinner = ora(`Waiting for Ark to be ready (timeout: ${timeoutSeconds}s)...`).start();
135
268
  while (Date.now() < endTime) {
136
269
  if (await isArkReady()) {
137
- spinner.succeed('ARK is ready!');
270
+ spinner.succeed('Ark is ready!');
138
271
  return;
139
272
  }
140
273
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
141
- spinner.text = `Waiting for ARK to be ready (${elapsed}/${timeoutSeconds}s)...`;
274
+ spinner.text = `Waiting for Ark to be ready (${elapsed}/${timeoutSeconds}s)...`;
142
275
  // Wait 2 seconds before checking again
143
- await new Promise(resolve => setTimeout(resolve, 2000));
276
+ await new Promise((resolve) => setTimeout(resolve, 2000));
144
277
  }
145
278
  // Timeout reached
146
- spinner.fail(`ARK did not become ready within ${timeoutSeconds} seconds`);
279
+ spinner.fail(`Ark did not become ready within ${timeoutSeconds} seconds`);
147
280
  process.exit(1);
148
281
  }
149
282
  catch (error) {
@@ -152,14 +285,16 @@ export async function installArk(options = {}) {
152
285
  }
153
286
  }
154
287
  }
155
- export function createInstallCommand() {
288
+ export function createInstallCommand(config) {
156
289
  const command = new Command('install');
157
290
  command
158
291
  .description('Install ARK components using Helm')
292
+ .argument('[service]', 'specific service to install, or all if omitted')
159
293
  .option('-y, --yes', 'automatically confirm all installations')
160
- .option('--wait-for-ready <timeout>', 'wait for ARK to be ready after installation (e.g., 30s, 2m)')
161
- .action(async (options) => {
162
- await installArk(options);
294
+ .option('--wait-for-ready <timeout>', 'wait for Ark to be ready after installation (e.g., 30s, 2m)')
295
+ .option('-v, --verbose', 'show commands being executed')
296
+ .action(async (service, options) => {
297
+ await installArk(config, service, options);
163
298
  });
164
299
  return command;
165
300
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,143 @@
1
+ import { jest } from '@jest/globals';
2
+ import { Command } from 'commander';
3
+ const mockExeca = jest.fn(() => Promise.resolve());
4
+ jest.unstable_mockModule('execa', () => ({
5
+ execa: mockExeca,
6
+ }));
7
+ const mockGetClusterInfo = jest.fn();
8
+ jest.unstable_mockModule('../../lib/cluster.js', () => ({
9
+ getClusterInfo: mockGetClusterInfo,
10
+ }));
11
+ const mockGetInstallableServices = jest.fn();
12
+ const mockArkServices = {};
13
+ const mockArkDependencies = {};
14
+ jest.unstable_mockModule('../../arkServices.js', () => ({
15
+ getInstallableServices: mockGetInstallableServices,
16
+ arkServices: mockArkServices,
17
+ arkDependencies: mockArkDependencies,
18
+ }));
19
+ const mockOutput = {
20
+ error: jest.fn(),
21
+ info: jest.fn(),
22
+ success: jest.fn(),
23
+ warning: jest.fn(),
24
+ };
25
+ jest.unstable_mockModule('../../lib/output.js', () => ({
26
+ default: mockOutput,
27
+ }));
28
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
29
+ throw new Error('process.exit called');
30
+ }));
31
+ jest.spyOn(console, 'log').mockImplementation(() => { });
32
+ jest.spyOn(console, 'error').mockImplementation(() => { });
33
+ const { createInstallCommand } = await import('./index.js');
34
+ describe('install command', () => {
35
+ const mockConfig = {
36
+ clusterInfo: {
37
+ context: 'test-cluster',
38
+ type: 'minikube',
39
+ namespace: 'default',
40
+ },
41
+ };
42
+ beforeEach(() => {
43
+ jest.clearAllMocks();
44
+ mockGetClusterInfo.mockResolvedValue({
45
+ context: 'test-cluster',
46
+ type: 'minikube',
47
+ namespace: 'default',
48
+ });
49
+ });
50
+ it('creates command with correct structure', () => {
51
+ const command = createInstallCommand(mockConfig);
52
+ expect(command).toBeInstanceOf(Command);
53
+ expect(command.name()).toBe('install');
54
+ });
55
+ it('installs single service with correct helm parameters', async () => {
56
+ const mockService = {
57
+ name: 'ark-api',
58
+ helmReleaseName: 'ark-api',
59
+ chartPath: './charts/ark-api',
60
+ namespace: 'ark-system',
61
+ installArgs: ['--set', 'image.tag=latest'],
62
+ };
63
+ mockGetInstallableServices.mockReturnValue({
64
+ 'ark-api': mockService,
65
+ });
66
+ const command = createInstallCommand(mockConfig);
67
+ await command.parseAsync(['node', 'test', 'ark-api']);
68
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
69
+ 'upgrade',
70
+ '--install',
71
+ 'ark-api',
72
+ './charts/ark-api',
73
+ '--namespace',
74
+ 'ark-system',
75
+ '--set',
76
+ 'image.tag=latest',
77
+ ], { stdio: 'inherit' });
78
+ expect(mockOutput.success).toHaveBeenCalledWith('ark-api installed successfully');
79
+ });
80
+ it('shows error when service not found', async () => {
81
+ mockGetInstallableServices.mockReturnValue({
82
+ 'ark-api': { name: 'ark-api' },
83
+ 'ark-controller': { name: 'ark-controller' },
84
+ });
85
+ const command = createInstallCommand(mockConfig);
86
+ await expect(command.parseAsync(['node', 'test', 'invalid-service'])).rejects.toThrow('process.exit called');
87
+ expect(mockOutput.error).toHaveBeenCalledWith("service 'invalid-service' not found");
88
+ expect(mockOutput.info).toHaveBeenCalledWith('available services:');
89
+ expect(mockOutput.info).toHaveBeenCalledWith(' ark-api');
90
+ expect(mockOutput.info).toHaveBeenCalledWith(' ark-controller');
91
+ expect(mockExit).toHaveBeenCalledWith(1);
92
+ });
93
+ it('handles service without namespace (uses current context)', async () => {
94
+ const mockService = {
95
+ name: 'ark-dashboard',
96
+ helmReleaseName: 'ark-dashboard',
97
+ chartPath: './charts/ark-dashboard',
98
+ // namespace is undefined - should use current context
99
+ installArgs: ['--set', 'replicas=2'],
100
+ };
101
+ mockGetInstallableServices.mockReturnValue({
102
+ 'ark-dashboard': mockService,
103
+ });
104
+ const command = createInstallCommand(mockConfig);
105
+ await command.parseAsync(['node', 'test', 'ark-dashboard']);
106
+ // Should NOT include --namespace flag
107
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
108
+ 'upgrade',
109
+ '--install',
110
+ 'ark-dashboard',
111
+ './charts/ark-dashboard',
112
+ '--set',
113
+ 'replicas=2',
114
+ ], { stdio: 'inherit' });
115
+ });
116
+ it('handles service without installArgs', async () => {
117
+ const mockService = {
118
+ name: 'simple-service',
119
+ helmReleaseName: 'simple-service',
120
+ chartPath: './charts/simple',
121
+ namespace: 'default',
122
+ };
123
+ mockGetInstallableServices.mockReturnValue({
124
+ 'simple-service': mockService,
125
+ });
126
+ const command = createInstallCommand(mockConfig);
127
+ await command.parseAsync(['node', 'test', 'simple-service']);
128
+ expect(mockExeca).toHaveBeenCalledWith('helm', [
129
+ 'upgrade',
130
+ '--install',
131
+ 'simple-service',
132
+ './charts/simple',
133
+ '--namespace',
134
+ 'default',
135
+ ], { stdio: 'inherit' });
136
+ });
137
+ it('exits when cluster not connected', async () => {
138
+ mockGetClusterInfo.mockResolvedValue({ error: true });
139
+ const command = createInstallCommand({});
140
+ await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
141
+ expect(mockExit).toHaveBeenCalledWith(1);
142
+ });
143
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ import { jest } from '@jest/globals';
2
+ const mockExeca = jest.fn();
3
+ jest.unstable_mockModule('execa', () => ({
4
+ execa: mockExeca,
5
+ }));
6
+ const mockInquirer = {
7
+ prompt: jest.fn(),
8
+ };
9
+ jest.unstable_mockModule('inquirer', () => ({
10
+ default: mockInquirer,
11
+ }));
12
+ const mockOutput = {
13
+ info: jest.fn(),
14
+ warning: jest.fn(),
15
+ error: jest.fn(),
16
+ success: jest.fn(),
17
+ };
18
+ jest.unstable_mockModule('../../lib/output.js', () => ({
19
+ default: mockOutput,
20
+ }));
21
+ jest.spyOn(console, 'log').mockImplementation(() => { });
22
+ jest.spyOn(console, 'error').mockImplementation(() => { });
23
+ const { createModel } = await import('./create.js');
24
+ describe('createModel', () => {
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ });
28
+ it('creates new model with provided name', async () => {
29
+ // Model doesn't exist
30
+ mockExeca.mockRejectedValueOnce(new Error('not found'));
31
+ // Prompts for model details
32
+ mockInquirer.prompt
33
+ .mockResolvedValueOnce({ modelType: 'openai' })
34
+ .mockResolvedValueOnce({
35
+ modelVersion: 'gpt-4',
36
+ baseUrl: 'https://api.openai.com/',
37
+ })
38
+ .mockResolvedValueOnce({ apiKey: 'secret-key' });
39
+ // Secret operations succeed
40
+ mockExeca.mockResolvedValueOnce({}); // delete secret (may not exist)
41
+ mockExeca.mockResolvedValueOnce({}); // create secret
42
+ mockExeca.mockResolvedValueOnce({}); // apply model
43
+ const result = await createModel('test-model');
44
+ expect(result).toBe(true);
45
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'model', 'test-model'], { stdio: 'pipe' });
46
+ expect(mockOutput.success).toHaveBeenCalledWith('model test-model created successfully');
47
+ });
48
+ it('prompts for name when not provided', async () => {
49
+ mockInquirer.prompt
50
+ .mockResolvedValueOnce({ modelName: 'prompted-model' })
51
+ .mockResolvedValueOnce({ modelType: 'azure' })
52
+ .mockResolvedValueOnce({
53
+ modelVersion: 'gpt-4',
54
+ baseUrl: 'https://azure.com',
55
+ })
56
+ .mockResolvedValueOnce({ apiVersion: '2024-12-01' })
57
+ .mockResolvedValueOnce({ apiKey: 'secret' });
58
+ mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
59
+ mockExeca.mockResolvedValue({}); // all kubectl ops succeed
60
+ const result = await createModel();
61
+ expect(result).toBe(true);
62
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([
63
+ expect.objectContaining({
64
+ name: 'modelName',
65
+ message: 'model name:',
66
+ }),
67
+ ]);
68
+ });
69
+ it('handles overwrite confirmation when model exists', async () => {
70
+ // Model exists
71
+ mockExeca.mockResolvedValueOnce({});
72
+ mockInquirer.prompt
73
+ .mockResolvedValueOnce({ overwrite: true })
74
+ .mockResolvedValueOnce({ modelType: 'openai' })
75
+ .mockResolvedValueOnce({
76
+ modelVersion: 'gpt-4',
77
+ baseUrl: 'https://api.openai.com',
78
+ })
79
+ .mockResolvedValueOnce({ apiKey: 'secret' });
80
+ mockExeca.mockResolvedValue({}); // remaining kubectl ops
81
+ const result = await createModel('existing-model');
82
+ expect(result).toBe(true);
83
+ expect(mockOutput.warning).toHaveBeenCalledWith('model existing-model already exists');
84
+ });
85
+ it('cancels when user declines overwrite', async () => {
86
+ mockExeca.mockResolvedValueOnce({}); // model exists
87
+ mockInquirer.prompt.mockResolvedValueOnce({ overwrite: false });
88
+ const result = await createModel('existing-model');
89
+ expect(result).toBe(false);
90
+ expect(mockOutput.info).toHaveBeenCalledWith('model creation cancelled');
91
+ });
92
+ it('handles secret creation failure', async () => {
93
+ mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
94
+ mockInquirer.prompt
95
+ .mockResolvedValueOnce({ modelType: 'openai' })
96
+ .mockResolvedValueOnce({
97
+ modelVersion: 'gpt-4',
98
+ baseUrl: 'https://api.openai.com',
99
+ })
100
+ .mockResolvedValueOnce({ apiKey: 'secret' });
101
+ mockExeca.mockRejectedValueOnce(new Error('delete failed')); // delete secret may fail
102
+ mockExeca.mockRejectedValueOnce(new Error('secret creation failed')); // create secret fails
103
+ const result = await createModel('test-model');
104
+ expect(result).toBe(false);
105
+ expect(mockOutput.error).toHaveBeenCalledWith('failed to create secret');
106
+ });
107
+ it('cleans up secret if model creation fails', async () => {
108
+ mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
109
+ mockInquirer.prompt
110
+ .mockResolvedValueOnce({ modelType: 'openai' })
111
+ .mockResolvedValueOnce({
112
+ modelVersion: 'gpt-4',
113
+ baseUrl: 'https://api.openai.com',
114
+ })
115
+ .mockResolvedValueOnce({ apiKey: 'secret' });
116
+ mockExeca.mockResolvedValueOnce({}); // delete secret
117
+ mockExeca.mockResolvedValueOnce({}); // create secret
118
+ mockExeca.mockRejectedValueOnce(new Error('apply failed')); // apply model fails
119
+ mockExeca.mockResolvedValueOnce({}); // cleanup secret
120
+ const result = await createModel('test-model');
121
+ expect(result).toBe(false);
122
+ expect(mockOutput.error).toHaveBeenCalledWith('failed to create model');
123
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['delete', 'secret', 'test-model-model-api-key'], { stdio: 'pipe' });
124
+ });
125
+ });
@@ -1,2 +1,3 @@
1
1
  import { Command } from 'commander';
2
- export declare function createModelsCommand(): Command;
2
+ import type { ArkConfig } from '../../lib/config.js';
3
+ export declare function createModelsCommand(_: ArkConfig): Command;