@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.
- 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 +24 -2
- 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/docs/index.d.ts +4 -0
- package/dist/commands/docs/index.js +18 -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 +225 -90
- package/dist/commands/install/index.spec.d.ts +1 -0
- package/dist/commands/install/index.spec.js +143 -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/query/index.d.ts +3 -0
- package/dist/commands/query/index.js +131 -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 +240 -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 +66 -44
- package/dist/commands/uninstall/index.spec.d.ts +1 -0
- package/dist/commands/uninstall/index.spec.js +125 -0
- package/dist/components/ChatUI.js +4 -4
- package/dist/components/statusChecker.d.ts +5 -12
- package/dist/components/statusChecker.js +193 -90
- package/dist/config.d.ts +3 -22
- package/dist/config.js +7 -151
- package/dist/index.js +26 -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 +4 -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/nextSteps.d.ts +4 -0
- package/dist/lib/nextSteps.js +20 -0
- package/dist/lib/nextSteps.spec.d.ts +1 -0
- package/dist/lib/nextSteps.spec.js +59 -0
- 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 +9 -0
- package/dist/lib/startup.js +93 -0
- package/dist/lib/startup.spec.d.ts +1 -0
- package/dist/lib/startup.spec.js +168 -0
- package/dist/lib/types.d.ts +9 -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,46 +1,40 @@
|
|
|
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 {
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
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);
|
|
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
|
-
//
|
|
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
|
|
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('
|
|
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
|
|
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(`
|
|
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
|
|
161
|
-
.
|
|
162
|
-
|
|
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
|
+
});
|