@agents-at-scale/ark 0.1.35-rc1 → 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/commands/completion/index.js +23 -1
- package/dist/commands/docs/index.d.ts +4 -0
- package/dist/commands/docs/index.js +18 -0
- package/dist/commands/install/index.d.ts +2 -2
- package/dist/commands/install/index.js +13 -15
- package/dist/commands/install/index.spec.js +14 -6
- package/dist/commands/query/index.d.ts +3 -0
- package/dist/commands/query/index.js +131 -0
- package/dist/commands/status/index.js +33 -3
- package/dist/commands/uninstall/index.d.ts +1 -1
- package/dist/commands/uninstall/index.js +8 -9
- package/dist/commands/uninstall/index.spec.js +14 -6
- package/dist/components/statusChecker.js +22 -2
- package/dist/index.js +4 -0
- package/dist/lib/config.d.ts +2 -0
- 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/startup.d.ts +4 -0
- package/dist/lib/startup.js +20 -0
- package/dist/lib/startup.spec.js +1 -1
- package/dist/lib/types.d.ts +7 -0
- package/dist/ui/MainMenu.js +1 -1
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ _ark_completion() {
|
|
|
28
28
|
|
|
29
29
|
case \${COMP_CWORD} in
|
|
30
30
|
1)
|
|
31
|
-
opts="agents chat cluster completion config dashboard dev generate install models routes status targets teams tools uninstall help"
|
|
31
|
+
opts="agents chat cluster completion config dashboard dev docs generate install models query routes status targets teams tools uninstall help"
|
|
32
32
|
COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
|
|
33
33
|
return 0
|
|
34
34
|
;;
|
|
@@ -95,6 +95,17 @@ _ark_completion() {
|
|
|
95
95
|
COMPREPLY=( $(compgen -W "\${targets}" -- \${cur}) )
|
|
96
96
|
return 0
|
|
97
97
|
;;
|
|
98
|
+
query)
|
|
99
|
+
# Dynamically fetch available targets for query command
|
|
100
|
+
local targets
|
|
101
|
+
targets=$(ark targets list 2>/dev/null)
|
|
102
|
+
if [ -z "$targets" ]; then
|
|
103
|
+
# Fallback to common targets if API is not available
|
|
104
|
+
targets="model/default agent/sample-agent"
|
|
105
|
+
fi
|
|
106
|
+
COMPREPLY=( $(compgen -W "\${targets}" -- \${cur}) )
|
|
107
|
+
return 0
|
|
108
|
+
;;
|
|
98
109
|
esac
|
|
99
110
|
;;
|
|
100
111
|
3)
|
|
@@ -159,9 +170,11 @@ _ark() {
|
|
|
159
170
|
'config[Configuration management]' \\
|
|
160
171
|
'dashboard[Open ARK dashboard]' \\
|
|
161
172
|
'dev[Development tools for ARK]' \\
|
|
173
|
+
'docs[Open ARK documentation]' \\
|
|
162
174
|
'generate[Generate ARK resources]' \\
|
|
163
175
|
'install[Install ARK services]' \\
|
|
164
176
|
'models[List available models]' \\
|
|
177
|
+
'query[Execute a single query]' \\
|
|
165
178
|
'routes[List available routes]' \\
|
|
166
179
|
'status[Check system status]' \\
|
|
167
180
|
'targets[List available query targets]' \\
|
|
@@ -234,6 +247,15 @@ _ark() {
|
|
|
234
247
|
fi
|
|
235
248
|
_values 'available targets' \${targets[@]}
|
|
236
249
|
;;
|
|
250
|
+
query)
|
|
251
|
+
# Get available targets dynamically for query
|
|
252
|
+
local -a targets
|
|
253
|
+
targets=($(ark targets list 2>/dev/null))
|
|
254
|
+
if [ \${#targets[@]} -eq 0 ]; then
|
|
255
|
+
targets=('model/default' 'agent/sample-agent')
|
|
256
|
+
fi
|
|
257
|
+
_values 'available targets' \${targets[@]}
|
|
258
|
+
;;
|
|
237
259
|
esac
|
|
238
260
|
;;
|
|
239
261
|
args)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
const DOCS_URL = 'https://mckinsey.github.io/agents-at-scale-ark/';
|
|
5
|
+
export async function openDocs() {
|
|
6
|
+
console.log(`Opening ARK documentation: ${chalk.blue(DOCS_URL)}`);
|
|
7
|
+
// Brief pause before opening browser
|
|
8
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
9
|
+
// Open browser
|
|
10
|
+
await open(DOCS_URL);
|
|
11
|
+
}
|
|
12
|
+
export function createDocsCommand(_) {
|
|
13
|
+
const docsCommand = new Command('docs');
|
|
14
|
+
docsCommand
|
|
15
|
+
.description('Open the ARK documentation in your browser')
|
|
16
|
+
.action(openDocs);
|
|
17
|
+
return docsCommand;
|
|
18
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { ArkConfig } from '../../lib/config.js';
|
|
3
|
-
export declare function installArk(serviceName?: string, options?: {
|
|
3
|
+
export declare function installArk(config: ArkConfig, serviceName?: string, options?: {
|
|
4
4
|
yes?: boolean;
|
|
5
5
|
waitForReady?: string;
|
|
6
6
|
verbose?: boolean;
|
|
7
7
|
}): Promise<void>;
|
|
8
|
-
export declare function createInstallCommand(
|
|
8
|
+
export declare function createInstallCommand(config: ArkConfig): Command;
|
|
@@ -2,10 +2,11 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { execute } from '../../lib/commands.js';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
-
import {
|
|
5
|
+
import { showNoClusterError } from '../../lib/startup.js';
|
|
6
6
|
import output from '../../lib/output.js';
|
|
7
7
|
import { getInstallableServices, arkDependencies } from '../../arkServices.js';
|
|
8
8
|
import { isArkReady } from '../../lib/arkStatus.js';
|
|
9
|
+
import { printNextSteps } from '../../lib/nextSteps.js';
|
|
9
10
|
import ora from 'ora';
|
|
10
11
|
async function installService(service, verbose = false) {
|
|
11
12
|
const helmArgs = [
|
|
@@ -22,25 +23,18 @@ async function installService(service, verbose = false) {
|
|
|
22
23
|
helmArgs.push(...(service.installArgs || []));
|
|
23
24
|
await execute('helm', helmArgs, { stdio: 'inherit' }, { verbose });
|
|
24
25
|
}
|
|
25
|
-
export async function installArk(serviceName, options = {}) {
|
|
26
|
+
export async function installArk(config, serviceName, options = {}) {
|
|
26
27
|
// Validate that --wait-for-ready requires -y
|
|
27
28
|
if (options.waitForReady && !options.yes) {
|
|
28
29
|
output.error('--wait-for-ready requires -y flag for non-interactive mode');
|
|
29
30
|
process.exit(1);
|
|
30
31
|
}
|
|
31
|
-
// Check cluster connectivity
|
|
32
|
-
|
|
33
|
-
|
|
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}`);
|
|
@@ -250,6 +244,10 @@ export async function installArk(serviceName, options = {}) {
|
|
|
250
244
|
}
|
|
251
245
|
}
|
|
252
246
|
}
|
|
247
|
+
// Show next steps after successful installation
|
|
248
|
+
if (!serviceName || serviceName === 'all') {
|
|
249
|
+
printNextSteps();
|
|
250
|
+
}
|
|
253
251
|
// Wait for Ark to be ready if requested
|
|
254
252
|
if (options.waitForReady) {
|
|
255
253
|
// Parse timeout value (e.g., '30s', '2m', '60')
|
|
@@ -287,7 +285,7 @@ export async function installArk(serviceName, options = {}) {
|
|
|
287
285
|
}
|
|
288
286
|
}
|
|
289
287
|
}
|
|
290
|
-
export function createInstallCommand(
|
|
288
|
+
export function createInstallCommand(config) {
|
|
291
289
|
const command = new Command('install');
|
|
292
290
|
command
|
|
293
291
|
.description('Install ARK components using Helm')
|
|
@@ -296,7 +294,7 @@ export function createInstallCommand(_) {
|
|
|
296
294
|
.option('--wait-for-ready <timeout>', 'wait for Ark to be ready after installation (e.g., 30s, 2m)')
|
|
297
295
|
.option('-v, --verbose', 'show commands being executed')
|
|
298
296
|
.action(async (service, options) => {
|
|
299
|
-
await installArk(service, options);
|
|
297
|
+
await installArk(config, service, options);
|
|
300
298
|
});
|
|
301
299
|
return command;
|
|
302
300
|
}
|
|
@@ -9,9 +9,11 @@ jest.unstable_mockModule('../../lib/cluster.js', () => ({
|
|
|
9
9
|
getClusterInfo: mockGetClusterInfo,
|
|
10
10
|
}));
|
|
11
11
|
const mockGetInstallableServices = jest.fn();
|
|
12
|
+
const mockArkServices = {};
|
|
12
13
|
const mockArkDependencies = {};
|
|
13
14
|
jest.unstable_mockModule('../../arkServices.js', () => ({
|
|
14
15
|
getInstallableServices: mockGetInstallableServices,
|
|
16
|
+
arkServices: mockArkServices,
|
|
15
17
|
arkDependencies: mockArkDependencies,
|
|
16
18
|
}));
|
|
17
19
|
const mockOutput = {
|
|
@@ -30,6 +32,13 @@ jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
|
30
32
|
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
31
33
|
const { createInstallCommand } = await import('./index.js');
|
|
32
34
|
describe('install command', () => {
|
|
35
|
+
const mockConfig = {
|
|
36
|
+
clusterInfo: {
|
|
37
|
+
context: 'test-cluster',
|
|
38
|
+
type: 'minikube',
|
|
39
|
+
namespace: 'default',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
33
42
|
beforeEach(() => {
|
|
34
43
|
jest.clearAllMocks();
|
|
35
44
|
mockGetClusterInfo.mockResolvedValue({
|
|
@@ -39,7 +48,7 @@ describe('install command', () => {
|
|
|
39
48
|
});
|
|
40
49
|
});
|
|
41
50
|
it('creates command with correct structure', () => {
|
|
42
|
-
const command = createInstallCommand(
|
|
51
|
+
const command = createInstallCommand(mockConfig);
|
|
43
52
|
expect(command).toBeInstanceOf(Command);
|
|
44
53
|
expect(command.name()).toBe('install');
|
|
45
54
|
});
|
|
@@ -54,7 +63,7 @@ describe('install command', () => {
|
|
|
54
63
|
mockGetInstallableServices.mockReturnValue({
|
|
55
64
|
'ark-api': mockService,
|
|
56
65
|
});
|
|
57
|
-
const command = createInstallCommand(
|
|
66
|
+
const command = createInstallCommand(mockConfig);
|
|
58
67
|
await command.parseAsync(['node', 'test', 'ark-api']);
|
|
59
68
|
expect(mockExeca).toHaveBeenCalledWith('helm', [
|
|
60
69
|
'upgrade',
|
|
@@ -73,7 +82,7 @@ describe('install command', () => {
|
|
|
73
82
|
'ark-api': { name: 'ark-api' },
|
|
74
83
|
'ark-controller': { name: 'ark-controller' },
|
|
75
84
|
});
|
|
76
|
-
const command = createInstallCommand(
|
|
85
|
+
const command = createInstallCommand(mockConfig);
|
|
77
86
|
await expect(command.parseAsync(['node', 'test', 'invalid-service'])).rejects.toThrow('process.exit called');
|
|
78
87
|
expect(mockOutput.error).toHaveBeenCalledWith("service 'invalid-service' not found");
|
|
79
88
|
expect(mockOutput.info).toHaveBeenCalledWith('available services:');
|
|
@@ -92,7 +101,7 @@ describe('install command', () => {
|
|
|
92
101
|
mockGetInstallableServices.mockReturnValue({
|
|
93
102
|
'ark-dashboard': mockService,
|
|
94
103
|
});
|
|
95
|
-
const command = createInstallCommand(
|
|
104
|
+
const command = createInstallCommand(mockConfig);
|
|
96
105
|
await command.parseAsync(['node', 'test', 'ark-dashboard']);
|
|
97
106
|
// Should NOT include --namespace flag
|
|
98
107
|
expect(mockExeca).toHaveBeenCalledWith('helm', [
|
|
@@ -114,7 +123,7 @@ describe('install command', () => {
|
|
|
114
123
|
mockGetInstallableServices.mockReturnValue({
|
|
115
124
|
'simple-service': mockService,
|
|
116
125
|
});
|
|
117
|
-
const command = createInstallCommand(
|
|
126
|
+
const command = createInstallCommand(mockConfig);
|
|
118
127
|
await command.parseAsync(['node', 'test', 'simple-service']);
|
|
119
128
|
expect(mockExeca).toHaveBeenCalledWith('helm', [
|
|
120
129
|
'upgrade',
|
|
@@ -129,7 +138,6 @@ describe('install command', () => {
|
|
|
129
138
|
mockGetClusterInfo.mockResolvedValue({ error: true });
|
|
130
139
|
const command = createInstallCommand({});
|
|
131
140
|
await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
|
|
132
|
-
expect(mockOutput.error).toHaveBeenCalledWith('no kubernetes cluster detected');
|
|
133
141
|
expect(mockExit).toHaveBeenCalledWith(1);
|
|
134
142
|
});
|
|
135
143
|
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import output from '../../lib/output.js';
|
|
5
|
+
async function runQuery(target, message) {
|
|
6
|
+
const spinner = ora('Creating query...').start();
|
|
7
|
+
// Generate a unique query name
|
|
8
|
+
const timestamp = Date.now();
|
|
9
|
+
const queryName = `cli-query-${timestamp}`;
|
|
10
|
+
// Parse the target format (e.g., model/default -> type: model, name: default)
|
|
11
|
+
const [targetType, targetName] = target.split('/');
|
|
12
|
+
// Create the Query resource
|
|
13
|
+
const queryManifest = {
|
|
14
|
+
apiVersion: 'ark.mckinsey.com/v1alpha1',
|
|
15
|
+
kind: 'Query',
|
|
16
|
+
metadata: {
|
|
17
|
+
name: queryName,
|
|
18
|
+
},
|
|
19
|
+
spec: {
|
|
20
|
+
input: message,
|
|
21
|
+
targets: [
|
|
22
|
+
{
|
|
23
|
+
type: targetType,
|
|
24
|
+
name: targetName,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
// Apply the query
|
|
31
|
+
spinner.text = 'Submitting query...';
|
|
32
|
+
await execa('kubectl', ['apply', '-f', '-'], {
|
|
33
|
+
input: JSON.stringify(queryManifest),
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
+
});
|
|
36
|
+
// Watch for query completion
|
|
37
|
+
spinner.text = 'Query status: initializing';
|
|
38
|
+
let queryComplete = false;
|
|
39
|
+
let attempts = 0;
|
|
40
|
+
const maxAttempts = 300; // 5 minutes with 1 second intervals
|
|
41
|
+
while (!queryComplete && attempts < maxAttempts) {
|
|
42
|
+
attempts++;
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execa('kubectl', [
|
|
45
|
+
'get',
|
|
46
|
+
'query',
|
|
47
|
+
queryName,
|
|
48
|
+
'-o',
|
|
49
|
+
'json',
|
|
50
|
+
], { stdio: 'pipe' });
|
|
51
|
+
const query = JSON.parse(stdout);
|
|
52
|
+
const phase = query.status?.phase;
|
|
53
|
+
// Update spinner with current phase
|
|
54
|
+
if (phase) {
|
|
55
|
+
spinner.text = `Query status: ${phase}`;
|
|
56
|
+
}
|
|
57
|
+
// Check if query is complete based on phase
|
|
58
|
+
if (phase === 'done') {
|
|
59
|
+
queryComplete = true;
|
|
60
|
+
spinner.succeed('Query completed');
|
|
61
|
+
// Extract and display the response from responses array
|
|
62
|
+
if (query.status?.responses && query.status.responses.length > 0) {
|
|
63
|
+
const response = query.status.responses[0];
|
|
64
|
+
console.log('\n' + (response.content || response));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
output.warning('No response received');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (phase === 'error') {
|
|
71
|
+
queryComplete = true;
|
|
72
|
+
spinner.fail('Query failed');
|
|
73
|
+
// Try to get error message from conditions or status
|
|
74
|
+
const errorCondition = query.status?.conditions?.find((c) => {
|
|
75
|
+
const condition = c;
|
|
76
|
+
return condition.type === 'Complete' && condition.status === 'False';
|
|
77
|
+
});
|
|
78
|
+
if (errorCondition?.message) {
|
|
79
|
+
output.error(errorCondition.message);
|
|
80
|
+
}
|
|
81
|
+
else if (query.status?.error) {
|
|
82
|
+
output.error(query.status.error);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
output.error('Query failed with unknown error');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (phase === 'canceled') {
|
|
89
|
+
queryComplete = true;
|
|
90
|
+
spinner.warn('Query canceled');
|
|
91
|
+
// Try to get cancellation reason if available
|
|
92
|
+
if (query.status?.message) {
|
|
93
|
+
output.warning(query.status.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Query might not exist yet, continue waiting
|
|
99
|
+
spinner.text = 'Running query: waiting for query to be created';
|
|
100
|
+
}
|
|
101
|
+
if (!queryComplete) {
|
|
102
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!queryComplete) {
|
|
106
|
+
spinner.fail('Query timed out');
|
|
107
|
+
output.error('Query did not complete within 5 minutes');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
spinner.fail('Query failed');
|
|
112
|
+
output.error(error instanceof Error ? error.message : 'Unknown error');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function createQueryCommand(_) {
|
|
117
|
+
const queryCommand = new Command('query');
|
|
118
|
+
queryCommand
|
|
119
|
+
.description('Execute a single query against a model or agent')
|
|
120
|
+
.argument('<target>', 'Query target (e.g., model/default, agent/my-agent)')
|
|
121
|
+
.argument('<message>', 'Message to send')
|
|
122
|
+
.action(async (target, message) => {
|
|
123
|
+
// Validate target format
|
|
124
|
+
if (!target.includes('/')) {
|
|
125
|
+
output.error('Invalid target format. Use: model/name or agent/name etc');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
await runQuery(target, message);
|
|
129
|
+
});
|
|
130
|
+
return queryCommand;
|
|
131
|
+
}
|
|
@@ -158,9 +158,6 @@ function buildStatusSections(data, config) {
|
|
|
158
158
|
statusColor: statusInfo.color,
|
|
159
159
|
name: displayName,
|
|
160
160
|
details: details,
|
|
161
|
-
subtext: controller.status === 'healthy' && !data.defaultModelExists
|
|
162
|
-
? '(no default model configured)'
|
|
163
|
-
: undefined,
|
|
164
161
|
});
|
|
165
162
|
// Add version update status as separate line
|
|
166
163
|
if (controller.status === 'healthy' && controller.version && config) {
|
|
@@ -199,6 +196,39 @@ function buildStatusSections(data, config) {
|
|
|
199
196
|
}
|
|
200
197
|
}
|
|
201
198
|
}
|
|
199
|
+
// Add default model status
|
|
200
|
+
if (data.defaultModel) {
|
|
201
|
+
if (!data.defaultModel.exists) {
|
|
202
|
+
arkStatusLines.push({
|
|
203
|
+
icon: '○',
|
|
204
|
+
iconColor: 'yellow',
|
|
205
|
+
status: 'default model',
|
|
206
|
+
statusColor: 'yellow',
|
|
207
|
+
name: '',
|
|
208
|
+
details: 'not configured',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else if (data.defaultModel.available) {
|
|
212
|
+
arkStatusLines.push({
|
|
213
|
+
icon: '●',
|
|
214
|
+
iconColor: 'green',
|
|
215
|
+
status: 'default model',
|
|
216
|
+
statusColor: 'green',
|
|
217
|
+
name: '',
|
|
218
|
+
details: data.defaultModel.provider || 'configured',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
arkStatusLines.push({
|
|
223
|
+
icon: '●',
|
|
224
|
+
iconColor: 'yellow',
|
|
225
|
+
status: 'default model',
|
|
226
|
+
statusColor: 'yellow',
|
|
227
|
+
name: '',
|
|
228
|
+
details: 'not available',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
202
232
|
}
|
|
203
233
|
}
|
|
204
234
|
sections.push({ title: 'ark status:', lines: arkStatusLines });
|
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { execute } from '../../lib/commands.js';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
-
import {
|
|
5
|
+
import { showNoClusterError } from '../../lib/startup.js';
|
|
6
6
|
import output from '../../lib/output.js';
|
|
7
7
|
import { getInstallableServices } from '../../arkServices.js';
|
|
8
8
|
async function uninstallService(service, verbose = false) {
|
|
@@ -13,14 +13,13 @@ async function uninstallService(service, verbose = false) {
|
|
|
13
13
|
}
|
|
14
14
|
await execute('helm', helmArgs, { stdio: 'inherit' }, { verbose });
|
|
15
15
|
}
|
|
16
|
-
async function uninstallArk(serviceName, options = {}) {
|
|
17
|
-
// Check cluster connectivity
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
output.error('no kubernetes cluster detected');
|
|
21
|
-
output.info('please ensure you have a running cluster and kubectl is configured.');
|
|
16
|
+
async function uninstallArk(config, serviceName, options = {}) {
|
|
17
|
+
// Check cluster connectivity from config
|
|
18
|
+
if (!config.clusterInfo) {
|
|
19
|
+
showNoClusterError();
|
|
22
20
|
process.exit(1);
|
|
23
21
|
}
|
|
22
|
+
const clusterInfo = config.clusterInfo;
|
|
24
23
|
// Show cluster info
|
|
25
24
|
output.success(`connected to cluster: ${chalk.bold(clusterInfo.context)}`);
|
|
26
25
|
output.info(`type: ${clusterInfo.type}`);
|
|
@@ -93,7 +92,7 @@ async function uninstallArk(serviceName, options = {}) {
|
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
|
-
export function createUninstallCommand(
|
|
95
|
+
export function createUninstallCommand(config) {
|
|
97
96
|
const command = new Command('uninstall');
|
|
98
97
|
command
|
|
99
98
|
.description('Uninstall ARK components using Helm')
|
|
@@ -101,7 +100,7 @@ export function createUninstallCommand(_) {
|
|
|
101
100
|
.option('-y, --yes', 'automatically confirm all uninstallations')
|
|
102
101
|
.option('-v, --verbose', 'show commands being executed')
|
|
103
102
|
.action(async (service, options) => {
|
|
104
|
-
await uninstallArk(service, options);
|
|
103
|
+
await uninstallArk(config, service, options);
|
|
105
104
|
});
|
|
106
105
|
return command;
|
|
107
106
|
}
|
|
@@ -9,8 +9,10 @@ jest.unstable_mockModule('../../lib/cluster.js', () => ({
|
|
|
9
9
|
getClusterInfo: mockGetClusterInfo,
|
|
10
10
|
}));
|
|
11
11
|
const mockGetInstallableServices = jest.fn();
|
|
12
|
+
const mockArkServices = {};
|
|
12
13
|
jest.unstable_mockModule('../../arkServices.js', () => ({
|
|
13
14
|
getInstallableServices: mockGetInstallableServices,
|
|
15
|
+
arkServices: mockArkServices,
|
|
14
16
|
}));
|
|
15
17
|
const mockOutput = {
|
|
16
18
|
error: jest.fn(),
|
|
@@ -28,6 +30,13 @@ jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
|
28
30
|
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
29
31
|
const { createUninstallCommand } = await import('./index.js');
|
|
30
32
|
describe('uninstall command', () => {
|
|
33
|
+
const mockConfig = {
|
|
34
|
+
clusterInfo: {
|
|
35
|
+
context: 'test-cluster',
|
|
36
|
+
type: 'minikube',
|
|
37
|
+
namespace: 'default',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
31
40
|
beforeEach(() => {
|
|
32
41
|
jest.clearAllMocks();
|
|
33
42
|
mockGetClusterInfo.mockResolvedValue({
|
|
@@ -37,7 +46,7 @@ describe('uninstall command', () => {
|
|
|
37
46
|
});
|
|
38
47
|
});
|
|
39
48
|
it('creates command with correct structure', () => {
|
|
40
|
-
const command = createUninstallCommand(
|
|
49
|
+
const command = createUninstallCommand(mockConfig);
|
|
41
50
|
expect(command).toBeInstanceOf(Command);
|
|
42
51
|
expect(command.name()).toBe('uninstall');
|
|
43
52
|
});
|
|
@@ -50,7 +59,7 @@ describe('uninstall command', () => {
|
|
|
50
59
|
mockGetInstallableServices.mockReturnValue({
|
|
51
60
|
'ark-api': mockService,
|
|
52
61
|
});
|
|
53
|
-
const command = createUninstallCommand(
|
|
62
|
+
const command = createUninstallCommand(mockConfig);
|
|
54
63
|
await command.parseAsync(['node', 'test', 'ark-api']);
|
|
55
64
|
expect(mockExeca).toHaveBeenCalledWith('helm', [
|
|
56
65
|
'uninstall',
|
|
@@ -68,7 +77,7 @@ describe('uninstall command', () => {
|
|
|
68
77
|
'ark-api': { name: 'ark-api' },
|
|
69
78
|
'ark-controller': { name: 'ark-controller' },
|
|
70
79
|
});
|
|
71
|
-
const command = createUninstallCommand(
|
|
80
|
+
const command = createUninstallCommand(mockConfig);
|
|
72
81
|
await expect(command.parseAsync(['node', 'test', 'invalid-service'])).rejects.toThrow('process.exit called');
|
|
73
82
|
expect(mockOutput.error).toHaveBeenCalledWith("service 'invalid-service' not found");
|
|
74
83
|
expect(mockOutput.info).toHaveBeenCalledWith('available services:');
|
|
@@ -85,7 +94,7 @@ describe('uninstall command', () => {
|
|
|
85
94
|
mockGetInstallableServices.mockReturnValue({
|
|
86
95
|
'ark-dashboard': mockService,
|
|
87
96
|
});
|
|
88
|
-
const command = createUninstallCommand(
|
|
97
|
+
const command = createUninstallCommand(mockConfig);
|
|
89
98
|
await command.parseAsync(['node', 'test', 'ark-dashboard']);
|
|
90
99
|
// Should NOT include --namespace flag
|
|
91
100
|
expect(mockExeca).toHaveBeenCalledWith('helm', ['uninstall', 'ark-dashboard', '--ignore-not-found'], {
|
|
@@ -102,7 +111,7 @@ describe('uninstall command', () => {
|
|
|
102
111
|
'ark-api': mockService,
|
|
103
112
|
});
|
|
104
113
|
mockExeca.mockRejectedValue(new Error('helm failed'));
|
|
105
|
-
const command = createUninstallCommand(
|
|
114
|
+
const command = createUninstallCommand(mockConfig);
|
|
106
115
|
await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
|
|
107
116
|
expect(mockOutput.error).toHaveBeenCalledWith('failed to uninstall ark-api');
|
|
108
117
|
expect(mockExit).toHaveBeenCalledWith(1);
|
|
@@ -111,7 +120,6 @@ describe('uninstall command', () => {
|
|
|
111
120
|
mockGetClusterInfo.mockResolvedValue({ error: true });
|
|
112
121
|
const command = createUninstallCommand({});
|
|
113
122
|
await expect(command.parseAsync(['node', 'test', 'ark-api'])).rejects.toThrow('process.exit called');
|
|
114
|
-
expect(mockOutput.error).toHaveBeenCalledWith('no kubernetes cluster detected');
|
|
115
123
|
expect(mockExit).toHaveBeenCalledWith(1);
|
|
116
124
|
});
|
|
117
125
|
});
|
|
@@ -372,16 +372,35 @@ export class StatusChecker {
|
|
|
372
372
|
// Check if ARK is ready (controller is running)
|
|
373
373
|
let arkReady = false;
|
|
374
374
|
let defaultModelExists = false;
|
|
375
|
+
let defaultModel;
|
|
375
376
|
if (clusterAccess) {
|
|
376
377
|
arkReady = await isArkReady();
|
|
377
|
-
// Check for default model
|
|
378
|
+
// Check for default model with detailed status
|
|
378
379
|
if (arkReady) {
|
|
379
380
|
try {
|
|
380
|
-
await execa('kubectl', [
|
|
381
|
+
const { stdout } = await execa('kubectl', [
|
|
382
|
+
'get',
|
|
383
|
+
'model',
|
|
384
|
+
'default',
|
|
385
|
+
'-o',
|
|
386
|
+
'json',
|
|
387
|
+
]);
|
|
388
|
+
const model = JSON.parse(stdout);
|
|
381
389
|
defaultModelExists = true;
|
|
390
|
+
// Extract model details
|
|
391
|
+
const available = model.status?.conditions?.find((c) => c.type === 'Available')?.status === 'True';
|
|
392
|
+
defaultModel = {
|
|
393
|
+
exists: true,
|
|
394
|
+
available,
|
|
395
|
+
provider: model.spec?.provider,
|
|
396
|
+
details: model.spec?.model || model.spec?.apiEndpoint,
|
|
397
|
+
};
|
|
382
398
|
}
|
|
383
399
|
catch {
|
|
384
400
|
defaultModelExists = false;
|
|
401
|
+
defaultModel = {
|
|
402
|
+
exists: false,
|
|
403
|
+
};
|
|
385
404
|
}
|
|
386
405
|
}
|
|
387
406
|
}
|
|
@@ -392,6 +411,7 @@ export class StatusChecker {
|
|
|
392
411
|
clusterInfo,
|
|
393
412
|
arkReady,
|
|
394
413
|
defaultModelExists,
|
|
414
|
+
defaultModel,
|
|
395
415
|
};
|
|
396
416
|
}
|
|
397
417
|
}
|
package/dist/index.js
CHANGED
|
@@ -12,10 +12,12 @@ import { createChatCommand } from './commands/chat/index.js';
|
|
|
12
12
|
import { createClusterCommand } from './commands/cluster/index.js';
|
|
13
13
|
import { createCompletionCommand } from './commands/completion/index.js';
|
|
14
14
|
import { createDashboardCommand } from './commands/dashboard/index.js';
|
|
15
|
+
import { createDocsCommand } from './commands/docs/index.js';
|
|
15
16
|
import { createDevCommand } from './commands/dev/index.js';
|
|
16
17
|
import { createGenerateCommand } from './commands/generate/index.js';
|
|
17
18
|
import { createInstallCommand } from './commands/install/index.js';
|
|
18
19
|
import { createModelsCommand } from './commands/models/index.js';
|
|
20
|
+
import { createQueryCommand } from './commands/query/index.js';
|
|
19
21
|
import { createUninstallCommand } from './commands/uninstall/index.js';
|
|
20
22
|
import { createStatusCommand } from './commands/status/index.js';
|
|
21
23
|
import { createConfigCommand } from './commands/config/index.js';
|
|
@@ -41,10 +43,12 @@ async function main() {
|
|
|
41
43
|
program.addCommand(createClusterCommand(config));
|
|
42
44
|
program.addCommand(createCompletionCommand(config));
|
|
43
45
|
program.addCommand(createDashboardCommand(config));
|
|
46
|
+
program.addCommand(createDocsCommand(config));
|
|
44
47
|
program.addCommand(createDevCommand(config));
|
|
45
48
|
program.addCommand(createGenerateCommand(config));
|
|
46
49
|
program.addCommand(createInstallCommand(config));
|
|
47
50
|
program.addCommand(createModelsCommand(config));
|
|
51
|
+
program.addCommand(createQueryCommand(config));
|
|
48
52
|
program.addCommand(createUninstallCommand(config));
|
|
49
53
|
program.addCommand(createStatusCommand(config));
|
|
50
54
|
program.addCommand(createConfigCommand(config));
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClusterInfo } from './cluster.js';
|
|
1
2
|
export interface ChatConfig {
|
|
2
3
|
streaming?: boolean;
|
|
3
4
|
outputFormat?: 'text' | 'markdown';
|
|
@@ -6,6 +7,7 @@ export interface ArkConfig {
|
|
|
6
7
|
chat?: ChatConfig;
|
|
7
8
|
latestVersion?: string;
|
|
8
9
|
currentVersion?: string;
|
|
10
|
+
clusterInfo?: ClusterInfo;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Load configuration from multiple sources with proper precedence:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
/**
|
|
3
|
+
* Print helpful next steps after successful ARK installation
|
|
4
|
+
*/
|
|
5
|
+
export function printNextSteps() {
|
|
6
|
+
console.log();
|
|
7
|
+
console.log(chalk.green.bold('✓ ARK installed successfully!'));
|
|
8
|
+
console.log();
|
|
9
|
+
console.log(chalk.gray('Next steps:'));
|
|
10
|
+
console.log();
|
|
11
|
+
console.log(` ${chalk.gray('docs:')} ${chalk.blue('https://mckinsey.github.io/agents-at-scale-ark/')}`);
|
|
12
|
+
console.log(` ${chalk.gray('create model:')} ${chalk.white.bold('ark models create default')}`);
|
|
13
|
+
console.log(` ${chalk.gray('open dashboard:')} ${chalk.white.bold('ark dashboard')}`);
|
|
14
|
+
console.log(` ${chalk.gray('show agents:')} ${chalk.white.bold('kubectl get agents')}`);
|
|
15
|
+
console.log(` ${chalk.gray('run a query:')} ${chalk.white.bold('ark query model/default "What are large language models?"')}`);
|
|
16
|
+
console.log(` ${chalk.gray('interactive chat:')} ${chalk.white.bold('ark')} ${chalk.gray('# then choose \'Chat\'')}`);
|
|
17
|
+
console.log(` ${chalk.gray('new project:')} ${chalk.white.bold('ark generate project my-agents')}`);
|
|
18
|
+
console.log(` ${chalk.gray('install fark:')} ${chalk.blue('https://mckinsey.github.io/agents-at-scale-ark/developer-guide/cli-tools/')}`);
|
|
19
|
+
console.log();
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { printNextSteps } from './nextSteps.js';
|
|
3
|
+
describe('printNextSteps', () => {
|
|
4
|
+
let consoleLogSpy;
|
|
5
|
+
let output = [];
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
output = [];
|
|
8
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation((...args) => {
|
|
9
|
+
output.push(args.join(' '));
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
consoleLogSpy.mockRestore();
|
|
14
|
+
});
|
|
15
|
+
it('prints successful installation message', () => {
|
|
16
|
+
printNextSteps();
|
|
17
|
+
const fullOutput = output.join('\n');
|
|
18
|
+
expect(fullOutput).toContain('ARK installed successfully!');
|
|
19
|
+
});
|
|
20
|
+
it('includes all required commands', () => {
|
|
21
|
+
printNextSteps();
|
|
22
|
+
const fullOutput = output.join('\n');
|
|
23
|
+
// Check for each command
|
|
24
|
+
expect(fullOutput).toContain('ark models create default');
|
|
25
|
+
expect(fullOutput).toContain('ark dashboard');
|
|
26
|
+
expect(fullOutput).toContain('kubectl get agents');
|
|
27
|
+
expect(fullOutput).toContain('ark query model/default "What are large language models?"');
|
|
28
|
+
expect(fullOutput).toContain('ark');
|
|
29
|
+
expect(fullOutput).toContain("# then choose 'Chat'");
|
|
30
|
+
expect(fullOutput).toContain('ark generate project my-agents');
|
|
31
|
+
});
|
|
32
|
+
it('includes all required links', () => {
|
|
33
|
+
printNextSteps();
|
|
34
|
+
const fullOutput = output.join('\n');
|
|
35
|
+
// Check for documentation links
|
|
36
|
+
expect(fullOutput).toContain('https://mckinsey.github.io/agents-at-scale-ark/');
|
|
37
|
+
expect(fullOutput).toContain('https://mckinsey.github.io/agents-at-scale-ark/developer-guide/cli-tools/');
|
|
38
|
+
});
|
|
39
|
+
it('includes all section labels', () => {
|
|
40
|
+
printNextSteps();
|
|
41
|
+
const fullOutput = output.join('\n');
|
|
42
|
+
// Check for labels
|
|
43
|
+
expect(fullOutput).toContain('Next steps:');
|
|
44
|
+
expect(fullOutput).toContain('docs:');
|
|
45
|
+
expect(fullOutput).toContain('create model:');
|
|
46
|
+
expect(fullOutput).toContain('open dashboard:');
|
|
47
|
+
expect(fullOutput).toContain('show agents:');
|
|
48
|
+
expect(fullOutput).toContain('run a query:');
|
|
49
|
+
expect(fullOutput).toContain('interactive chat:');
|
|
50
|
+
expect(fullOutput).toContain('new project:');
|
|
51
|
+
expect(fullOutput).toContain('install fark:');
|
|
52
|
+
});
|
|
53
|
+
it('has correct structure with empty lines', () => {
|
|
54
|
+
printNextSteps();
|
|
55
|
+
// Should have empty lines for formatting
|
|
56
|
+
expect(output[0]).toBe('');
|
|
57
|
+
expect(output[output.length - 1]).toBe('');
|
|
58
|
+
});
|
|
59
|
+
});
|
package/dist/lib/startup.d.ts
CHANGED
package/dist/lib/startup.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { checkCommandExists } from './commands.js';
|
|
3
3
|
import { loadConfig } from './config.js';
|
|
4
4
|
import { getArkVersion } from './arkStatus.js';
|
|
5
|
+
import { getClusterInfo } from './cluster.js';
|
|
5
6
|
const REQUIRED_COMMANDS = [
|
|
6
7
|
{
|
|
7
8
|
name: 'kubectl',
|
|
@@ -32,6 +33,20 @@ async function checkRequirements() {
|
|
|
32
33
|
process.exit(1);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Show error message when no cluster is detected
|
|
38
|
+
*/
|
|
39
|
+
export function showNoClusterError() {
|
|
40
|
+
console.log(chalk.red.bold('\n✗ No Kubernetes cluster detected\n'));
|
|
41
|
+
console.log('Please ensure you have configured a connection to a Kubernetes cluster.');
|
|
42
|
+
console.log('For local development, you can use:');
|
|
43
|
+
console.log(` • Minikube: ${chalk.blue('https://minikube.sigs.k8s.io/docs/start')}`);
|
|
44
|
+
console.log(` • Docker Desktop: ${chalk.blue('https://docs.docker.com/desktop/kubernetes/')}`);
|
|
45
|
+
console.log(` • Kind: ${chalk.blue('https://kind.sigs.k8s.io/docs/user/quick-start/')}`);
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('And more. For help, check the Quickstart guide:');
|
|
48
|
+
console.log(chalk.blue(' https://mckinsey.github.io/agents-at-scale-ark/quickstart/'));
|
|
49
|
+
}
|
|
35
50
|
/**
|
|
36
51
|
* Fetch version information (non-blocking)
|
|
37
52
|
*/
|
|
@@ -67,6 +82,11 @@ export async function startup() {
|
|
|
67
82
|
await checkRequirements();
|
|
68
83
|
// Load config
|
|
69
84
|
const config = loadConfig();
|
|
85
|
+
// Get cluster info - if no error, we have cluster access
|
|
86
|
+
const clusterInfo = await getClusterInfo();
|
|
87
|
+
if (!clusterInfo.error) {
|
|
88
|
+
config.clusterInfo = clusterInfo;
|
|
89
|
+
}
|
|
70
90
|
// Fetch version info synchronously so it's available immediately
|
|
71
91
|
await fetchVersionInfo(config);
|
|
72
92
|
return config;
|
package/dist/lib/startup.spec.js
CHANGED
|
@@ -135,7 +135,7 @@ describe('startup', () => {
|
|
|
135
135
|
expect(globalThis.fetch).toHaveBeenCalledWith('https://api.github.com/repos/mckinsey/agents-at-scale-ark/releases/latest');
|
|
136
136
|
// Wait for async fetch to complete
|
|
137
137
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
138
|
-
expect(config.latestVersion).toBe('
|
|
138
|
+
expect(config.latestVersion).toBe('0.1.35');
|
|
139
139
|
});
|
|
140
140
|
it('handles GitHub API failure gracefully', async () => {
|
|
141
141
|
globalThis.fetch.mockRejectedValue(new Error('Network error'));
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -31,11 +31,18 @@ export interface DependencyStatus {
|
|
|
31
31
|
version?: string;
|
|
32
32
|
details?: string;
|
|
33
33
|
}
|
|
34
|
+
export interface ModelStatus {
|
|
35
|
+
exists: boolean;
|
|
36
|
+
available?: boolean;
|
|
37
|
+
provider?: string;
|
|
38
|
+
details?: string;
|
|
39
|
+
}
|
|
34
40
|
export interface StatusData {
|
|
35
41
|
services: ServiceStatus[];
|
|
36
42
|
dependencies: DependencyStatus[];
|
|
37
43
|
arkReady?: boolean;
|
|
38
44
|
defaultModelExists?: boolean;
|
|
45
|
+
defaultModel?: ModelStatus;
|
|
39
46
|
}
|
|
40
47
|
export interface CommandVersionConfig {
|
|
41
48
|
command: string;
|
package/dist/ui/MainMenu.js
CHANGED
|
@@ -239,7 +239,7 @@ const MainMenu = ({ config }) => {
|
|
|
239
239
|
║ Agents at Scale Platform ║
|
|
240
240
|
║ ║
|
|
241
241
|
╚═══════════════════════════════════════╝
|
|
242
|
-
` }), isChecking ? (_jsxs(Text, { color: "gray", children: [_jsx(Spinner, { type: "dots" }), " Checking Ark status..."] })) : arkReady ? (_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: "\u25CF Ark is ready" }), config.currentVersion && (_jsxs(Text, { color: "gray", children: [" (", config.currentVersion, ")"] }))] })) : (_jsx(Text, { color: "yellow", bold: true, children: "\u25CF Ark is not installed" })), _jsx(Text, { color: "gray", children: "
|
|
242
|
+
` }), isChecking ? (_jsxs(Text, { color: "gray", children: [_jsx(Spinner, { type: "dots" }), " Checking Ark status..."] })) : arkReady ? (_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: "\u25CF Ark is ready" }), config.currentVersion && (_jsxs(Text, { color: "gray", children: [" (", config.currentVersion, ")"] }))] })) : (_jsx(Text, { color: "yellow", bold: true, children: "\u25CF Ark is not installed" })), config.clusterInfo?.context ? (_jsxs(Text, { children: [_jsx(Text, { color: "gray", children: "Current context: " }), _jsx(Text, { color: "white", bold: true, children: config.clusterInfo.context })] })) : (_jsx(Text, { color: "gray", children: "No Kubernetes context configured" }))] }), !isChecking && (_jsx(Box, { flexDirection: "column", paddingX: 4, marginTop: 1, children: choices.map((choice, index) => {
|
|
243
243
|
const isSelected = index === selectedIndex;
|
|
244
244
|
return (_jsxs(Box, { flexDirection: "row", paddingY: 0, children: [_jsx(Text, { color: "gray", dimColor: true, children: isSelected ? '❯ ' : ' ' }), _jsxs(Text, { color: "gray", dimColor: true, children: [index + 1, "."] }), _jsx(Box, { marginLeft: 1, width: 20, children: _jsx(Text, { color: isSelected ? 'green' : 'white', bold: isSelected, children: choice.label }) }), _jsx(Text, { color: "gray", children: choice.description })] }, choice.value));
|
|
245
245
|
}) }))] }));
|