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