@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.
- package/dist/arkServices.d.ts +4 -12
- package/dist/arkServices.js +19 -34
- package/dist/arkServices.spec.d.ts +1 -0
- package/dist/arkServices.spec.js +24 -0
- package/dist/commands/agents/index.d.ts +2 -1
- package/dist/commands/agents/index.js +2 -7
- package/dist/commands/agents/index.spec.d.ts +1 -0
- package/dist/commands/agents/index.spec.js +67 -0
- package/dist/commands/chat/index.d.ts +2 -1
- package/dist/commands/chat/index.js +5 -21
- package/dist/commands/cluster/get.spec.d.ts +1 -0
- package/dist/commands/cluster/get.spec.js +92 -0
- package/dist/commands/cluster/index.d.ts +2 -1
- package/dist/commands/cluster/index.js +1 -1
- package/dist/commands/cluster/index.spec.d.ts +1 -0
- package/dist/commands/cluster/index.spec.js +24 -0
- package/dist/commands/completion/index.d.ts +2 -1
- package/dist/commands/completion/index.js +1 -1
- package/dist/commands/completion/index.spec.d.ts +1 -0
- package/dist/commands/completion/index.spec.js +34 -0
- package/dist/commands/config/index.d.ts +2 -1
- package/dist/commands/config/index.js +2 -2
- package/dist/commands/config/index.spec.d.ts +1 -0
- package/dist/commands/config/index.spec.js +78 -0
- package/dist/commands/dashboard/index.d.ts +2 -1
- package/dist/commands/dashboard/index.js +1 -1
- package/dist/commands/dev/index.d.ts +2 -1
- package/dist/commands/dev/index.js +1 -1
- package/dist/commands/dev/tool-generate.spec.d.ts +1 -0
- package/dist/commands/dev/tool-generate.spec.js +163 -0
- package/dist/commands/dev/tool.spec.d.ts +1 -0
- package/dist/commands/dev/tool.spec.js +48 -0
- package/dist/commands/generate/generators/project.js +22 -41
- package/dist/commands/generate/index.d.ts +2 -1
- package/dist/commands/generate/index.js +1 -1
- package/dist/commands/install/index.d.ts +4 -2
- package/dist/commands/install/index.js +215 -78
- package/dist/commands/install/index.spec.d.ts +1 -0
- package/dist/commands/install/index.spec.js +135 -0
- package/dist/commands/models/create.spec.d.ts +1 -0
- package/dist/commands/models/create.spec.js +125 -0
- package/dist/commands/models/index.d.ts +2 -1
- package/dist/commands/models/index.js +2 -7
- package/dist/commands/models/index.spec.d.ts +1 -0
- package/dist/commands/models/index.spec.js +76 -0
- package/dist/commands/routes/index.d.ts +2 -1
- package/dist/commands/routes/index.js +1 -9
- package/dist/commands/status/index.d.ts +3 -2
- package/dist/commands/status/index.js +210 -11
- package/dist/commands/targets/index.d.ts +2 -1
- package/dist/commands/targets/index.js +1 -1
- package/dist/commands/targets/index.spec.d.ts +1 -0
- package/dist/commands/targets/index.spec.js +105 -0
- package/dist/commands/teams/index.d.ts +2 -1
- package/dist/commands/teams/index.js +2 -7
- package/dist/commands/teams/index.spec.d.ts +1 -0
- package/dist/commands/teams/index.spec.js +70 -0
- package/dist/commands/tools/index.d.ts +2 -1
- package/dist/commands/tools/index.js +2 -7
- package/dist/commands/tools/index.spec.d.ts +1 -0
- package/dist/commands/tools/index.spec.js +70 -0
- package/dist/commands/uninstall/index.d.ts +2 -1
- package/dist/commands/uninstall/index.js +61 -38
- package/dist/commands/uninstall/index.spec.d.ts +1 -0
- package/dist/commands/uninstall/index.spec.js +117 -0
- package/dist/components/ChatUI.js +4 -4
- package/dist/components/statusChecker.d.ts +5 -12
- package/dist/components/statusChecker.js +172 -89
- package/dist/config.d.ts +3 -22
- package/dist/config.js +7 -151
- package/dist/index.js +22 -19
- package/dist/lib/arkServiceProxy.js +4 -2
- package/dist/lib/arkStatus.d.ts +5 -0
- package/dist/lib/arkStatus.js +61 -2
- package/dist/lib/arkStatus.spec.d.ts +1 -0
- package/dist/lib/arkStatus.spec.js +49 -0
- package/dist/lib/chatClient.js +1 -3
- package/dist/lib/cluster.js +11 -14
- package/dist/lib/cluster.spec.d.ts +1 -0
- package/dist/lib/cluster.spec.js +338 -0
- package/dist/lib/commandUtils.js +7 -7
- package/dist/lib/commands.d.ts +16 -0
- package/dist/lib/commands.js +29 -0
- package/dist/lib/commands.spec.d.ts +1 -0
- package/dist/lib/commands.spec.js +146 -0
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.js +6 -4
- package/dist/lib/config.spec.d.ts +1 -0
- package/dist/lib/config.spec.js +99 -0
- package/dist/lib/consts.d.ts +0 -1
- package/dist/lib/consts.js +0 -2
- package/dist/lib/consts.spec.d.ts +1 -0
- package/dist/lib/consts.spec.js +15 -0
- package/dist/lib/errors.js +1 -1
- package/dist/lib/errors.spec.d.ts +1 -0
- package/dist/lib/errors.spec.js +221 -0
- package/dist/lib/exec.d.ts +0 -4
- package/dist/lib/exec.js +0 -11
- package/dist/lib/output.spec.d.ts +1 -0
- package/dist/lib/output.spec.js +123 -0
- package/dist/lib/portUtils.d.ts +8 -0
- package/dist/lib/portUtils.js +39 -0
- package/dist/lib/startup.d.ts +5 -0
- package/dist/lib/startup.js +73 -0
- package/dist/lib/startup.spec.d.ts +1 -0
- package/dist/lib/startup.spec.js +168 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/ui/AgentSelector.d.ts +8 -0
- package/dist/ui/AgentSelector.js +53 -0
- package/dist/ui/MainMenu.d.ts +5 -1
- package/dist/ui/MainMenu.js +117 -54
- package/dist/ui/ModelSelector.d.ts +8 -0
- package/dist/ui/ModelSelector.js +53 -0
- package/dist/ui/TeamSelector.d.ts +8 -0
- package/dist/ui/TeamSelector.js +55 -0
- package/dist/ui/ToolSelector.d.ts +8 -0
- package/dist/ui/ToolSelector.js +53 -0
- package/dist/ui/statusFormatter.d.ts +22 -10
- package/dist/ui/statusFormatter.js +37 -109
- package/dist/ui/statusFormatter.spec.d.ts +1 -0
- package/dist/ui/statusFormatter.spec.js +58 -0
- package/package.json +3 -3
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
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(`
|
|
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
|
|
161
|
-
.
|
|
162
|
-
|
|
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
|
+
});
|
|
@@ -26,16 +26,11 @@ async function listModels(options) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
catch (error) {
|
|
29
|
-
|
|
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 {};
|