@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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
/**
|
|
3
|
+
* Check if a port is available
|
|
4
|
+
*/
|
|
5
|
+
export async function isPortAvailable(port) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const server = net.createServer();
|
|
8
|
+
server.once('error', (err) => {
|
|
9
|
+
if (err.code === 'EADDRINUSE') {
|
|
10
|
+
resolve(false);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
resolve(false);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
server.once('listening', () => {
|
|
17
|
+
server.close();
|
|
18
|
+
resolve(true);
|
|
19
|
+
});
|
|
20
|
+
server.listen(port, '127.0.0.1');
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find an available port, starting from the preferred port
|
|
25
|
+
*/
|
|
26
|
+
export async function findAvailablePort(preferredPort, maxAttempts = 10) {
|
|
27
|
+
// First try the preferred port
|
|
28
|
+
if (await isPortAvailable(preferredPort)) {
|
|
29
|
+
return preferredPort;
|
|
30
|
+
}
|
|
31
|
+
// Try random ports
|
|
32
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
33
|
+
const randomPort = Math.floor(Math.random() * (65535 - 1024) + 1024);
|
|
34
|
+
if (await isPortAvailable(randomPort)) {
|
|
35
|
+
return randomPort;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Could not find an available port after ${maxAttempts} attempts`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ArkConfig } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Show error message when no cluster is detected
|
|
4
|
+
*/
|
|
5
|
+
export declare function showNoClusterError(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Initialize the CLI by checking requirements and loading config
|
|
8
|
+
*/
|
|
9
|
+
export declare function startup(): Promise<ArkConfig>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { checkCommandExists } from './commands.js';
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { getArkVersion } from './arkStatus.js';
|
|
5
|
+
import { getClusterInfo } from './cluster.js';
|
|
6
|
+
const REQUIRED_COMMANDS = [
|
|
7
|
+
{
|
|
8
|
+
name: 'kubectl',
|
|
9
|
+
command: 'kubectl',
|
|
10
|
+
args: ['version', '--client'],
|
|
11
|
+
installUrl: 'https://kubernetes.io/docs/tasks/tools/',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'helm',
|
|
15
|
+
command: 'helm',
|
|
16
|
+
args: ['version', '--short'],
|
|
17
|
+
installUrl: 'https://helm.sh/docs/intro/install/',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
async function checkRequirements() {
|
|
21
|
+
const missing = [];
|
|
22
|
+
for (const cmd of REQUIRED_COMMANDS) {
|
|
23
|
+
const exists = await checkCommandExists(cmd.command, cmd.args);
|
|
24
|
+
if (!exists) {
|
|
25
|
+
missing.push(cmd);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (missing.length > 0) {
|
|
29
|
+
for (const cmd of missing) {
|
|
30
|
+
console.error(chalk.red('error:') + ` ${cmd.name} is required`);
|
|
31
|
+
console.error(' ' + chalk.blue(cmd.installUrl));
|
|
32
|
+
}
|
|
33
|
+
process.exit(1);
|
|
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
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fetch version information (non-blocking)
|
|
52
|
+
*/
|
|
53
|
+
async function fetchVersionInfo(config) {
|
|
54
|
+
// Fetch latest version from GitHub
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch('https://api.github.com/repos/mckinsey/agents-at-scale-ark/releases/latest');
|
|
57
|
+
if (response.ok) {
|
|
58
|
+
const data = (await response.json());
|
|
59
|
+
// Remove 'v' prefix if present for consistent comparison
|
|
60
|
+
config.latestVersion = data.tag_name.replace(/^v/, '');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Silently fail - latestVersion will remain undefined
|
|
65
|
+
}
|
|
66
|
+
// Fetch current installed version (already without 'v' from helm)
|
|
67
|
+
try {
|
|
68
|
+
const currentVersion = await getArkVersion();
|
|
69
|
+
if (currentVersion) {
|
|
70
|
+
config.currentVersion = currentVersion;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Silently fail - currentVersion will remain undefined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Initialize the CLI by checking requirements and loading config
|
|
79
|
+
*/
|
|
80
|
+
export async function startup() {
|
|
81
|
+
// Check required commands
|
|
82
|
+
await checkRequirements();
|
|
83
|
+
// Load config
|
|
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
|
+
}
|
|
90
|
+
// Fetch version info synchronously so it's available immediately
|
|
91
|
+
await fetchVersionInfo(config);
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
// Mock chalk to avoid ANSI codes in tests
|
|
3
|
+
jest.unstable_mockModule('chalk', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
red: (str) => str,
|
|
6
|
+
yellow: (str) => str,
|
|
7
|
+
gray: (str) => str,
|
|
8
|
+
blue: (str) => str,
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
// Mock commands module
|
|
12
|
+
jest.unstable_mockModule('./commands.js', () => ({
|
|
13
|
+
checkCommandExists: jest.fn(),
|
|
14
|
+
}));
|
|
15
|
+
// Mock config module
|
|
16
|
+
jest.unstable_mockModule('./config.js', () => ({
|
|
17
|
+
loadConfig: jest.fn(),
|
|
18
|
+
}));
|
|
19
|
+
// Dynamic imports after mocks
|
|
20
|
+
const { checkCommandExists } = await import('./commands.js');
|
|
21
|
+
const { loadConfig } = await import('./config.js');
|
|
22
|
+
const { startup } = await import('./startup.js');
|
|
23
|
+
// Type the mocks
|
|
24
|
+
const mockCheckCommandExists = checkCommandExists;
|
|
25
|
+
const mockLoadConfig = loadConfig;
|
|
26
|
+
// Mock fetch globally
|
|
27
|
+
globalThis.fetch = jest.fn();
|
|
28
|
+
describe('startup', () => {
|
|
29
|
+
let mockExit;
|
|
30
|
+
let mockConsoleError;
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
globalThis.fetch.mockClear();
|
|
34
|
+
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
35
|
+
throw new Error('process.exit');
|
|
36
|
+
});
|
|
37
|
+
mockConsoleError = jest
|
|
38
|
+
.spyOn(console, 'error')
|
|
39
|
+
.mockImplementation(() => { });
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
mockExit.mockRestore();
|
|
43
|
+
mockConsoleError.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
it('returns config when all required commands are installed', async () => {
|
|
46
|
+
const expectedConfig = {
|
|
47
|
+
chat: {
|
|
48
|
+
streaming: true,
|
|
49
|
+
outputFormat: 'text',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
// Mock all commands as available
|
|
53
|
+
mockCheckCommandExists.mockResolvedValue(true);
|
|
54
|
+
mockLoadConfig.mockReturnValue(expectedConfig);
|
|
55
|
+
const config = await startup();
|
|
56
|
+
expect(config).toEqual(expectedConfig);
|
|
57
|
+
expect(mockCheckCommandExists).toHaveBeenCalledWith('kubectl', [
|
|
58
|
+
'version',
|
|
59
|
+
'--client',
|
|
60
|
+
]);
|
|
61
|
+
expect(mockCheckCommandExists).toHaveBeenCalledWith('helm', [
|
|
62
|
+
'version',
|
|
63
|
+
'--short',
|
|
64
|
+
]);
|
|
65
|
+
expect(mockLoadConfig).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(mockExit).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('exits with error when kubectl is missing', async () => {
|
|
69
|
+
// Mock kubectl as missing, helm as available
|
|
70
|
+
mockCheckCommandExists
|
|
71
|
+
.mockResolvedValueOnce(false) // kubectl
|
|
72
|
+
.mockResolvedValueOnce(true); // helm
|
|
73
|
+
await expect(startup()).rejects.toThrow('process.exit');
|
|
74
|
+
expect(mockConsoleError).toHaveBeenCalledWith('error: kubectl is required');
|
|
75
|
+
expect(mockConsoleError).toHaveBeenCalledWith(' https://kubernetes.io/docs/tasks/tools/');
|
|
76
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
77
|
+
});
|
|
78
|
+
it('exits with error when helm is missing', async () => {
|
|
79
|
+
// Mock kubectl as available, helm as missing
|
|
80
|
+
mockCheckCommandExists
|
|
81
|
+
.mockResolvedValueOnce(true) // kubectl
|
|
82
|
+
.mockResolvedValueOnce(false); // helm
|
|
83
|
+
await expect(startup()).rejects.toThrow('process.exit');
|
|
84
|
+
expect(mockConsoleError).toHaveBeenCalledWith('error: helm is required');
|
|
85
|
+
expect(mockConsoleError).toHaveBeenCalledWith(' https://helm.sh/docs/intro/install/');
|
|
86
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
87
|
+
});
|
|
88
|
+
it('exits with error when both commands are missing', async () => {
|
|
89
|
+
// Mock both commands as missing
|
|
90
|
+
mockCheckCommandExists.mockResolvedValue(false);
|
|
91
|
+
await expect(startup()).rejects.toThrow('process.exit');
|
|
92
|
+
expect(mockConsoleError).toHaveBeenCalledWith('error: kubectl is required');
|
|
93
|
+
expect(mockConsoleError).toHaveBeenCalledWith(' https://kubernetes.io/docs/tasks/tools/');
|
|
94
|
+
expect(mockConsoleError).toHaveBeenCalledWith('error: helm is required');
|
|
95
|
+
expect(mockConsoleError).toHaveBeenCalledWith(' https://helm.sh/docs/intro/install/');
|
|
96
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
97
|
+
});
|
|
98
|
+
it('checks commands with correct arguments', async () => {
|
|
99
|
+
mockCheckCommandExists.mockResolvedValue(true);
|
|
100
|
+
mockLoadConfig.mockReturnValue({ chat: {} });
|
|
101
|
+
await startup();
|
|
102
|
+
expect(mockCheckCommandExists).toHaveBeenCalledTimes(2);
|
|
103
|
+
expect(mockCheckCommandExists).toHaveBeenNthCalledWith(1, 'kubectl', [
|
|
104
|
+
'version',
|
|
105
|
+
'--client',
|
|
106
|
+
]);
|
|
107
|
+
expect(mockCheckCommandExists).toHaveBeenNthCalledWith(2, 'helm', [
|
|
108
|
+
'version',
|
|
109
|
+
'--short',
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
it('loads config after checking requirements', async () => {
|
|
113
|
+
mockCheckCommandExists.mockResolvedValue(true);
|
|
114
|
+
const expectedConfig = { chat: { streaming: false } };
|
|
115
|
+
mockLoadConfig.mockReturnValue(expectedConfig);
|
|
116
|
+
const config = await startup();
|
|
117
|
+
// Verify order - checkCommandExists should be called before loadConfig
|
|
118
|
+
const checkCallOrder = mockCheckCommandExists.mock.invocationCallOrder[0];
|
|
119
|
+
const loadCallOrder = mockLoadConfig.mock.invocationCallOrder[0];
|
|
120
|
+
expect(checkCallOrder).toBeLessThan(loadCallOrder);
|
|
121
|
+
expect(config).toEqual(expectedConfig);
|
|
122
|
+
});
|
|
123
|
+
describe('version fetching', () => {
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
// Setup successful requirements check and config
|
|
126
|
+
mockCheckCommandExists.mockResolvedValue(true);
|
|
127
|
+
mockLoadConfig.mockReturnValue({ chat: { streaming: true } });
|
|
128
|
+
});
|
|
129
|
+
it('fetches latest version from GitHub API', async () => {
|
|
130
|
+
globalThis.fetch.mockResolvedValue({
|
|
131
|
+
ok: true,
|
|
132
|
+
json: async () => ({ tag_name: 'v0.1.35' }),
|
|
133
|
+
});
|
|
134
|
+
const config = await startup();
|
|
135
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('https://api.github.com/repos/mckinsey/agents-at-scale-ark/releases/latest');
|
|
136
|
+
// Wait for async fetch to complete
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
138
|
+
expect(config.latestVersion).toBe('0.1.35');
|
|
139
|
+
});
|
|
140
|
+
it('handles GitHub API failure gracefully', async () => {
|
|
141
|
+
globalThis.fetch.mockRejectedValue(new Error('Network error'));
|
|
142
|
+
const config = await startup();
|
|
143
|
+
// Wait for async fetch attempt
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
145
|
+
// Should not have latestVersion set
|
|
146
|
+
expect(config.latestVersion).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
it('handles non-OK response from GitHub API', async () => {
|
|
149
|
+
globalThis.fetch.mockResolvedValue({
|
|
150
|
+
ok: false,
|
|
151
|
+
status: 403,
|
|
152
|
+
});
|
|
153
|
+
const config = await startup();
|
|
154
|
+
// Wait for async fetch to complete
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
156
|
+
// Should not have latestVersion set
|
|
157
|
+
expect(config.latestVersion).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
it('continues startup even if version fetch fails', async () => {
|
|
160
|
+
globalThis.fetch.mockRejectedValue(new Error('API Error'));
|
|
161
|
+
const config = await startup();
|
|
162
|
+
// Startup should complete successfully
|
|
163
|
+
expect(config).toBeDefined();
|
|
164
|
+
expect(config.chat).toBeDefined();
|
|
165
|
+
expect(mockExit).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type ServiceStatus = {
|
|
|
22
22
|
version?: string;
|
|
23
23
|
revision?: string;
|
|
24
24
|
details?: string;
|
|
25
|
+
isDev?: boolean;
|
|
26
|
+
namespace?: string;
|
|
25
27
|
};
|
|
26
28
|
export interface DependencyStatus {
|
|
27
29
|
name: string;
|
|
@@ -29,11 +31,18 @@ export interface DependencyStatus {
|
|
|
29
31
|
version?: string;
|
|
30
32
|
details?: string;
|
|
31
33
|
}
|
|
34
|
+
export interface ModelStatus {
|
|
35
|
+
exists: boolean;
|
|
36
|
+
available?: boolean;
|
|
37
|
+
provider?: string;
|
|
38
|
+
details?: string;
|
|
39
|
+
}
|
|
32
40
|
export interface StatusData {
|
|
33
41
|
services: ServiceStatus[];
|
|
34
42
|
dependencies: DependencyStatus[];
|
|
35
43
|
arkReady?: boolean;
|
|
36
44
|
defaultModelExists?: boolean;
|
|
45
|
+
defaultModel?: ModelStatus;
|
|
37
46
|
}
|
|
38
47
|
export interface CommandVersionConfig {
|
|
39
48
|
command: string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Agent, ArkApiClient } from '../lib/arkApiClient.js';
|
|
2
|
+
interface AgentSelectorProps {
|
|
3
|
+
arkApiClient: ArkApiClient;
|
|
4
|
+
onSelect: (agent: Agent) => void;
|
|
5
|
+
onExit: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function AgentSelector({ arkApiClient, onSelect, onExit, }: AgentSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
export function AgentSelector({ arkApiClient, onSelect, onExit, }) {
|
|
5
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
6
|
+
const [agents, setAgents] = useState([]);
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
const [error, setError] = useState(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
arkApiClient
|
|
11
|
+
.getAgents()
|
|
12
|
+
.then((fetchedAgents) => {
|
|
13
|
+
setAgents(fetchedAgents);
|
|
14
|
+
setLoading(false);
|
|
15
|
+
})
|
|
16
|
+
.catch((err) => {
|
|
17
|
+
setError(err.message || 'Failed to fetch agents');
|
|
18
|
+
setLoading(false);
|
|
19
|
+
});
|
|
20
|
+
}, [arkApiClient]);
|
|
21
|
+
useInput((input, key) => {
|
|
22
|
+
if (key.escape) {
|
|
23
|
+
onExit();
|
|
24
|
+
}
|
|
25
|
+
else if (key.upArrow || input === 'k') {
|
|
26
|
+
setSelectedIndex((prev) => (prev === 0 ? agents.length - 1 : prev - 1));
|
|
27
|
+
}
|
|
28
|
+
else if (key.downArrow || input === 'j') {
|
|
29
|
+
setSelectedIndex((prev) => (prev === agents.length - 1 ? 0 : prev + 1));
|
|
30
|
+
}
|
|
31
|
+
else if (key.return) {
|
|
32
|
+
onSelect(agents[selectedIndex]);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Handle number keys for quick selection
|
|
36
|
+
const num = parseInt(input, 10);
|
|
37
|
+
if (!isNaN(num) && num >= 1 && num <= agents.length) {
|
|
38
|
+
onSelect(agents[num - 1]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
if (loading) {
|
|
43
|
+
return (_jsx(Box, { children: _jsx(Text, { children: "Loading agents..." }) }));
|
|
44
|
+
}
|
|
45
|
+
if (error) {
|
|
46
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) }));
|
|
47
|
+
}
|
|
48
|
+
if (agents.length === 0) {
|
|
49
|
+
return (_jsx(Box, { children: _jsx(Text, { children: "No agents available" }) }));
|
|
50
|
+
}
|
|
51
|
+
const selectedAgent = agents[selectedIndex];
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Select Agent" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Choose an agent to start a conversation with" }) }), _jsx(Box, { flexDirection: "column", children: agents.map((agent, index) => (_jsx(Box, { marginBottom: 0, children: _jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', index + 1, ". ", agent.name] }) }, agent.name))) }), selectedAgent.description && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: selectedAgent.description }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Number to select \u00B7 Esc to exit" }) })] }));
|
|
53
|
+
}
|
package/dist/ui/MainMenu.d.ts
CHANGED
package/dist/ui/MainMenu.js
CHANGED
|
@@ -1,38 +1,68 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Text, Box, render, useInput } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
3
4
|
import * as React from 'react';
|
|
5
|
+
import { isArkReady } from '../lib/arkStatus.js';
|
|
4
6
|
// Helper function to unmount the main ink app - used when we move from a
|
|
5
7
|
// React TUI app to basic input/output.
|
|
6
8
|
async function unmountInkApp() {
|
|
7
9
|
const app = globalThis.inkApp;
|
|
8
10
|
if (app) {
|
|
11
|
+
// Unmount the Ink app
|
|
9
12
|
app.unmount();
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
process.removeAllListeners('SIGQUIT');
|
|
14
|
-
process.removeAllListeners('exit');
|
|
15
|
-
// Reset stdin completely
|
|
13
|
+
// Clear the global reference
|
|
14
|
+
delete globalThis.inkApp;
|
|
15
|
+
// Reset terminal to normal mode
|
|
16
16
|
if (process.stdin.isTTY) {
|
|
17
17
|
process.stdin.setRawMode(false);
|
|
18
18
|
process.stdin.pause();
|
|
19
|
-
process.stdin.removeAllListeners();
|
|
20
|
-
process.stdin.resume();
|
|
21
19
|
}
|
|
22
|
-
//
|
|
23
|
-
process.stdout.removeAllListeners();
|
|
24
|
-
process.stderr.removeAllListeners();
|
|
20
|
+
// Clear screen
|
|
25
21
|
console.clear();
|
|
26
|
-
//
|
|
27
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
22
|
+
// Small delay to ensure everything is flushed
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
28
24
|
}
|
|
29
25
|
}
|
|
30
|
-
const MainMenu = () => {
|
|
26
|
+
const MainMenu = ({ config }) => {
|
|
31
27
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
32
|
-
const
|
|
28
|
+
const [arkReady, setArkReady] = React.useState(null);
|
|
29
|
+
const [isChecking, setIsChecking] = React.useState(true);
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
const checkArkStatus = async () => {
|
|
32
|
+
setIsChecking(true);
|
|
33
|
+
const ready = await isArkReady();
|
|
34
|
+
setArkReady(ready);
|
|
35
|
+
setIsChecking(false);
|
|
36
|
+
// Reset selected index to 0 after status check
|
|
37
|
+
setSelectedIndex(0);
|
|
38
|
+
};
|
|
39
|
+
checkArkStatus();
|
|
40
|
+
}, []);
|
|
41
|
+
// Handle Ctrl+C to properly unmount Ink and restore terminal
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
const handleExit = () => {
|
|
44
|
+
const app = globalThis.inkApp;
|
|
45
|
+
if (app) {
|
|
46
|
+
app.unmount();
|
|
47
|
+
}
|
|
48
|
+
process.exit(0);
|
|
49
|
+
};
|
|
50
|
+
process.on('SIGINT', handleExit);
|
|
51
|
+
return () => {
|
|
52
|
+
process.removeListener('SIGINT', handleExit);
|
|
53
|
+
};
|
|
54
|
+
}, []);
|
|
55
|
+
// Check if upgrade is available
|
|
56
|
+
const hasUpgrade = React.useMemo(() => {
|
|
57
|
+
if (!config.currentVersion || !config.latestVersion)
|
|
58
|
+
return false;
|
|
59
|
+
// Simple version comparison
|
|
60
|
+
return config.currentVersion !== config.latestVersion;
|
|
61
|
+
}, [config.currentVersion, config.latestVersion]);
|
|
62
|
+
const allChoices = [
|
|
33
63
|
{
|
|
34
64
|
label: 'Chat',
|
|
35
|
-
description: 'Interactive chat with
|
|
65
|
+
description: 'Interactive chat with Ark agents',
|
|
36
66
|
value: 'chat',
|
|
37
67
|
command: 'ark chat',
|
|
38
68
|
},
|
|
@@ -42,27 +72,57 @@ const MainMenu = () => {
|
|
|
42
72
|
value: 'install',
|
|
43
73
|
command: 'ark install',
|
|
44
74
|
},
|
|
75
|
+
{
|
|
76
|
+
label: 'Upgrade',
|
|
77
|
+
description: `Upgrade Ark from ${config.currentVersion} to ${config.latestVersion}`,
|
|
78
|
+
value: 'upgrade',
|
|
79
|
+
command: 'ark install -y',
|
|
80
|
+
},
|
|
45
81
|
{
|
|
46
82
|
label: 'Dashboard',
|
|
47
|
-
description: 'Open
|
|
83
|
+
description: 'Open Ark dashboard in browser',
|
|
48
84
|
value: 'dashboard',
|
|
49
85
|
command: 'ark dashboard',
|
|
50
86
|
},
|
|
51
87
|
{
|
|
52
|
-
label: 'Status
|
|
53
|
-
description: 'Check
|
|
88
|
+
label: 'Status',
|
|
89
|
+
description: 'Check Ark services status',
|
|
54
90
|
value: 'status',
|
|
55
91
|
command: 'ark status',
|
|
56
92
|
},
|
|
57
93
|
{
|
|
58
94
|
label: 'Generate',
|
|
59
|
-
description: 'Generate new
|
|
95
|
+
description: 'Generate new Ark components',
|
|
60
96
|
value: 'generate',
|
|
61
97
|
command: 'ark generate',
|
|
62
98
|
},
|
|
63
|
-
{ label: 'Exit', description: 'Exit
|
|
99
|
+
{ label: 'Exit', description: 'Exit Ark CLI', value: 'exit' },
|
|
64
100
|
];
|
|
101
|
+
// Filter choices based on Ark readiness
|
|
102
|
+
const choices = React.useMemo(() => {
|
|
103
|
+
// Don't return any choices while checking
|
|
104
|
+
if (isChecking)
|
|
105
|
+
return [];
|
|
106
|
+
if (!arkReady) {
|
|
107
|
+
// Only show Install, Status, and Exit when Ark is not ready
|
|
108
|
+
return allChoices.filter((choice) => ['install', 'status', 'exit'].includes(choice.value));
|
|
109
|
+
}
|
|
110
|
+
// Ark is ready - filter out install (already installed) and conditionally show upgrade
|
|
111
|
+
const filteredChoices = allChoices.filter((choice) => {
|
|
112
|
+
// Never show install when Ark is ready (it's already installed)
|
|
113
|
+
if (choice.value === 'install')
|
|
114
|
+
return false;
|
|
115
|
+
// Only show upgrade if there's actually an upgrade available
|
|
116
|
+
if (choice.value === 'upgrade' && !hasUpgrade)
|
|
117
|
+
return false;
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
return filteredChoices;
|
|
121
|
+
}, [arkReady, isChecking, hasUpgrade, allChoices]);
|
|
65
122
|
useInput((input, key) => {
|
|
123
|
+
// Don't process input while checking status
|
|
124
|
+
if (isChecking)
|
|
125
|
+
return;
|
|
66
126
|
if (key.upArrow || input === 'k') {
|
|
67
127
|
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : choices.length - 1));
|
|
68
128
|
}
|
|
@@ -102,7 +162,7 @@ const MainMenu = () => {
|
|
|
102
162
|
const output = (await import('../lib/output.js')).default;
|
|
103
163
|
output.error(error instanceof Error
|
|
104
164
|
? error.message
|
|
105
|
-
: 'Failed to connect to
|
|
165
|
+
: 'Failed to connect to Ark API');
|
|
106
166
|
process.exit(1);
|
|
107
167
|
}
|
|
108
168
|
break;
|
|
@@ -110,36 +170,38 @@ const MainMenu = () => {
|
|
|
110
170
|
case 'install': {
|
|
111
171
|
// Unmount fullscreen app and clear screen.
|
|
112
172
|
await unmountInkApp();
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const child = spawn(process.execPath, [process.argv[1], 'install'], {
|
|
120
|
-
stdio: 'inherit',
|
|
121
|
-
});
|
|
122
|
-
await new Promise((resolve, reject) => {
|
|
123
|
-
child.on('close', (code) => {
|
|
124
|
-
if (code === 0) {
|
|
125
|
-
resolve();
|
|
126
|
-
}
|
|
127
|
-
else if (code === 130) {
|
|
128
|
-
// 130 is the exit code for SIGINT (Ctrl+C)
|
|
129
|
-
process.exit(130);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
reject(new Error(`Install command failed with code ${code}`));
|
|
133
|
-
}
|
|
173
|
+
// Spawn as a new process to avoid Ink/inquirer signal conflicts
|
|
174
|
+
const { execFileSync } = await import('child_process');
|
|
175
|
+
try {
|
|
176
|
+
execFileSync(process.execPath, [process.argv[1], 'install'], {
|
|
177
|
+
stdio: 'inherit',
|
|
178
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
134
179
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
process
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
// execFileSync throws if the process exits with non-zero
|
|
183
|
+
process.exit(error.status || 1);
|
|
184
|
+
}
|
|
185
|
+
process.exit(0);
|
|
186
|
+
break; // Add break even though process.exit prevents reaching here
|
|
187
|
+
}
|
|
188
|
+
case 'upgrade': {
|
|
189
|
+
// Unmount fullscreen app and clear screen.
|
|
190
|
+
await unmountInkApp();
|
|
191
|
+
// Spawn as a new process with -y flag for automatic upgrade
|
|
192
|
+
const { execFileSync } = await import('child_process');
|
|
193
|
+
try {
|
|
194
|
+
execFileSync(process.execPath, [process.argv[1], 'install', '-y'], {
|
|
195
|
+
stdio: 'inherit',
|
|
196
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
140
197
|
});
|
|
141
|
-
}
|
|
142
|
-
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
// execFileSync throws if the process exits with non-zero
|
|
201
|
+
process.exit(error.status || 1);
|
|
202
|
+
}
|
|
203
|
+
process.exit(0);
|
|
204
|
+
break; // Add break even though process.exit prevents reaching here
|
|
143
205
|
}
|
|
144
206
|
case 'dashboard': {
|
|
145
207
|
// Unmount fullscreen app and clear screen.
|
|
@@ -152,8 +214,9 @@ const MainMenu = () => {
|
|
|
152
214
|
// Unmount fullscreen app and clear screen.
|
|
153
215
|
await unmountInkApp();
|
|
154
216
|
const { checkStatus } = await import('../commands/status/index.js');
|
|
155
|
-
await checkStatus();
|
|
156
|
-
|
|
217
|
+
await checkStatus(config);
|
|
218
|
+
process.exit(0);
|
|
219
|
+
break; // Add break even though process.exit prevents reaching here
|
|
157
220
|
}
|
|
158
221
|
case 'generate': {
|
|
159
222
|
const GeneratorUI = (await import('../components/GeneratorUI.js'))
|
|
@@ -176,9 +239,9 @@ const MainMenu = () => {
|
|
|
176
239
|
║ Agents at Scale Platform ║
|
|
177
240
|
║ ║
|
|
178
241
|
╚═══════════════════════════════════════╝
|
|
179
|
-
` }), _jsx(Text, { color: "green", bold: true, 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) => {
|
|
180
243
|
const isSelected = index === selectedIndex;
|
|
181
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));
|
|
182
|
-
}) })] }));
|
|
245
|
+
}) }))] }));
|
|
183
246
|
};
|
|
184
247
|
export default MainMenu;
|