@agents-at-scale/ark 0.1.35-rc2 → 0.1.36-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/arkServices.js +7 -7
- package/dist/commands/agents/index.js +14 -0
- package/dist/commands/completion/index.js +1 -61
- package/dist/commands/dev/tool/shared.js +3 -1
- package/dist/commands/generate/generators/agent.js +2 -2
- package/dist/commands/generate/generators/team.js +2 -2
- package/dist/commands/install/index.js +1 -6
- package/dist/commands/models/index.js +15 -0
- package/dist/commands/models/index.spec.js +20 -0
- package/dist/commands/query/index.js +9 -116
- package/dist/commands/query/index.spec.d.ts +1 -0
- package/dist/commands/query/index.spec.js +53 -0
- package/dist/commands/status/index.d.ts +2 -3
- package/dist/commands/status/index.js +36 -17
- package/dist/commands/targets/index.js +26 -19
- package/dist/commands/targets/index.spec.js +95 -46
- package/dist/commands/teams/index.js +15 -0
- package/dist/commands/uninstall/index.js +0 -5
- package/dist/components/statusChecker.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/lib/chatClient.js +70 -76
- package/dist/lib/config.d.ts +0 -2
- package/dist/lib/executeQuery.d.ts +20 -0
- package/dist/lib/executeQuery.js +135 -0
- package/dist/lib/executeQuery.spec.d.ts +1 -0
- package/dist/lib/executeQuery.spec.js +170 -0
- package/dist/lib/nextSteps.js +1 -1
- package/dist/lib/queryRunner.d.ts +22 -0
- package/dist/lib/queryRunner.js +142 -0
- package/dist/lib/startup.d.ts +1 -1
- package/dist/lib/startup.js +25 -31
- package/dist/lib/startup.spec.js +29 -45
- package/dist/lib/types.d.ts +70 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +51 -0
- package/dist/ui/MainMenu.js +15 -11
- package/package.json +1 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared query execution logic for both universal and resource-specific query commands
|
|
3
|
+
*/
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import output from './output.js';
|
|
7
|
+
/**
|
|
8
|
+
* Execute a query against any ARK target (model, agent, team)
|
|
9
|
+
* This is the shared implementation used by all query commands
|
|
10
|
+
*/
|
|
11
|
+
export async function executeQuery(options) {
|
|
12
|
+
const spinner = ora('Creating query...').start();
|
|
13
|
+
// Generate a unique query name
|
|
14
|
+
const timestamp = Date.now();
|
|
15
|
+
const queryName = `cli-query-${timestamp}`;
|
|
16
|
+
// Create the Query resource
|
|
17
|
+
const queryManifest = {
|
|
18
|
+
apiVersion: 'ark.mckinsey.com/v1alpha1',
|
|
19
|
+
kind: 'Query',
|
|
20
|
+
metadata: {
|
|
21
|
+
name: queryName,
|
|
22
|
+
},
|
|
23
|
+
spec: {
|
|
24
|
+
input: options.message,
|
|
25
|
+
targets: [
|
|
26
|
+
{
|
|
27
|
+
type: options.targetType,
|
|
28
|
+
name: options.targetName,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
// Apply the query
|
|
35
|
+
spinner.text = 'Submitting query...';
|
|
36
|
+
await execa('kubectl', ['apply', '-f', '-'], {
|
|
37
|
+
input: JSON.stringify(queryManifest),
|
|
38
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
});
|
|
40
|
+
// Watch for query completion
|
|
41
|
+
spinner.text = 'Query status: initializing';
|
|
42
|
+
let queryComplete = false;
|
|
43
|
+
let attempts = 0;
|
|
44
|
+
const maxAttempts = 300; // 5 minutes with 1 second intervals
|
|
45
|
+
while (!queryComplete && attempts < maxAttempts) {
|
|
46
|
+
attempts++;
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execa('kubectl', ['get', 'query', queryName, '-o', 'json'], { stdio: 'pipe' });
|
|
49
|
+
const query = JSON.parse(stdout);
|
|
50
|
+
const phase = query.status?.phase;
|
|
51
|
+
// Update spinner with current phase
|
|
52
|
+
if (phase) {
|
|
53
|
+
spinner.text = `Query status: ${phase}`;
|
|
54
|
+
}
|
|
55
|
+
// Check if query is complete based on phase
|
|
56
|
+
if (phase === 'done') {
|
|
57
|
+
queryComplete = true;
|
|
58
|
+
spinner.succeed('Query completed');
|
|
59
|
+
// Extract and display the response from responses array
|
|
60
|
+
if (query.status?.responses && query.status.responses.length > 0) {
|
|
61
|
+
const response = query.status.responses[0];
|
|
62
|
+
console.log('\n' + (response.content || response));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
output.warning('No response received');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (phase === 'error') {
|
|
69
|
+
queryComplete = true;
|
|
70
|
+
spinner.fail('Query failed');
|
|
71
|
+
// Try to get error message from conditions or status
|
|
72
|
+
const errorCondition = query.status?.conditions?.find((c) => {
|
|
73
|
+
return c.type === 'Complete' && c.status === 'False';
|
|
74
|
+
});
|
|
75
|
+
if (errorCondition?.message) {
|
|
76
|
+
output.error(errorCondition.message);
|
|
77
|
+
}
|
|
78
|
+
else if (query.status?.error) {
|
|
79
|
+
output.error(query.status.error);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
output.error('Query failed with unknown error');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (phase === 'canceled') {
|
|
86
|
+
queryComplete = true;
|
|
87
|
+
spinner.warn('Query canceled');
|
|
88
|
+
// Try to get cancellation reason if available
|
|
89
|
+
if (query.status?.message) {
|
|
90
|
+
output.warning(query.status.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Query might not exist yet, continue waiting
|
|
96
|
+
spinner.text = 'Query status: waiting for query to be created';
|
|
97
|
+
}
|
|
98
|
+
if (!queryComplete) {
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!queryComplete) {
|
|
103
|
+
spinner.fail('Query timed out');
|
|
104
|
+
output.error('Query did not complete within 5 minutes');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
spinner.fail('Query failed');
|
|
109
|
+
output.error(error instanceof Error ? error.message : 'Unknown error');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
// Clean up the query resource
|
|
114
|
+
try {
|
|
115
|
+
await execa('kubectl', ['delete', 'query', queryName], { stdio: 'pipe' });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Ignore cleanup errors
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Parse a target string like "model/default" or "agent/weather"
|
|
124
|
+
* Returns QueryTarget or null if invalid
|
|
125
|
+
*/
|
|
126
|
+
export function parseTarget(target) {
|
|
127
|
+
const parts = target.split('/');
|
|
128
|
+
if (parts.length !== 2) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
type: parts[0],
|
|
133
|
+
name: parts[1],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
const mockExeca = jest.fn();
|
|
3
|
+
jest.unstable_mockModule('execa', () => ({
|
|
4
|
+
execa: mockExeca,
|
|
5
|
+
}));
|
|
6
|
+
const mockSpinner = {
|
|
7
|
+
start: jest.fn(),
|
|
8
|
+
succeed: jest.fn(),
|
|
9
|
+
fail: jest.fn(),
|
|
10
|
+
warn: jest.fn(),
|
|
11
|
+
text: '',
|
|
12
|
+
};
|
|
13
|
+
const mockOra = jest.fn(() => mockSpinner);
|
|
14
|
+
jest.unstable_mockModule('ora', () => ({
|
|
15
|
+
default: mockOra,
|
|
16
|
+
}));
|
|
17
|
+
const mockOutput = {
|
|
18
|
+
warning: jest.fn(),
|
|
19
|
+
error: jest.fn(),
|
|
20
|
+
};
|
|
21
|
+
jest.unstable_mockModule('./output.js', () => ({
|
|
22
|
+
default: mockOutput,
|
|
23
|
+
}));
|
|
24
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
25
|
+
throw new Error('process.exit called');
|
|
26
|
+
}));
|
|
27
|
+
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
28
|
+
const { executeQuery, parseTarget } = await import('./executeQuery.js');
|
|
29
|
+
describe('executeQuery', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
mockSpinner.start.mockReturnValue(mockSpinner);
|
|
33
|
+
});
|
|
34
|
+
describe('parseTarget', () => {
|
|
35
|
+
it('should parse valid target strings', () => {
|
|
36
|
+
expect(parseTarget('model/default')).toEqual({
|
|
37
|
+
type: 'model',
|
|
38
|
+
name: 'default',
|
|
39
|
+
});
|
|
40
|
+
expect(parseTarget('agent/weather-agent')).toEqual({
|
|
41
|
+
type: 'agent',
|
|
42
|
+
name: 'weather-agent',
|
|
43
|
+
});
|
|
44
|
+
expect(parseTarget('team/my-team')).toEqual({
|
|
45
|
+
type: 'team',
|
|
46
|
+
name: 'my-team',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it('should return null for invalid target strings', () => {
|
|
50
|
+
expect(parseTarget('invalid')).toBeNull();
|
|
51
|
+
expect(parseTarget('')).toBeNull();
|
|
52
|
+
expect(parseTarget('model/default/extra')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('executeQuery', () => {
|
|
56
|
+
it('should create and apply a query manifest', async () => {
|
|
57
|
+
const mockQueryResponse = {
|
|
58
|
+
status: {
|
|
59
|
+
phase: 'done',
|
|
60
|
+
responses: [{ content: 'Test response' }],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
mockExeca.mockImplementation(async (command, args) => {
|
|
64
|
+
if (args.includes('apply')) {
|
|
65
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
66
|
+
}
|
|
67
|
+
if (args.includes('get') && args.includes('query')) {
|
|
68
|
+
return {
|
|
69
|
+
stdout: JSON.stringify(mockQueryResponse),
|
|
70
|
+
stderr: '',
|
|
71
|
+
exitCode: 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (args.includes('delete')) {
|
|
75
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
76
|
+
}
|
|
77
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
78
|
+
});
|
|
79
|
+
await executeQuery({
|
|
80
|
+
targetType: 'model',
|
|
81
|
+
targetName: 'default',
|
|
82
|
+
message: 'Hello',
|
|
83
|
+
});
|
|
84
|
+
expect(mockSpinner.start).toHaveBeenCalled();
|
|
85
|
+
expect(mockSpinner.succeed).toHaveBeenCalledWith('Query completed');
|
|
86
|
+
expect(mockConsoleLog).toHaveBeenCalledWith('\nTest response');
|
|
87
|
+
});
|
|
88
|
+
it('should handle query error phase', async () => {
|
|
89
|
+
const mockQueryResponse = {
|
|
90
|
+
status: {
|
|
91
|
+
phase: 'error',
|
|
92
|
+
error: 'Query failed with test error',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
mockExeca.mockImplementation(async (command, args) => {
|
|
96
|
+
if (args.includes('apply')) {
|
|
97
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
98
|
+
}
|
|
99
|
+
if (args.includes('get') && args.includes('query')) {
|
|
100
|
+
return {
|
|
101
|
+
stdout: JSON.stringify(mockQueryResponse),
|
|
102
|
+
stderr: '',
|
|
103
|
+
exitCode: 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (args.includes('delete')) {
|
|
107
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
108
|
+
}
|
|
109
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
110
|
+
});
|
|
111
|
+
await executeQuery({
|
|
112
|
+
targetType: 'model',
|
|
113
|
+
targetName: 'default',
|
|
114
|
+
message: 'Hello',
|
|
115
|
+
});
|
|
116
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith('Query failed');
|
|
117
|
+
expect(mockOutput.error).toHaveBeenCalledWith('Query failed with test error');
|
|
118
|
+
});
|
|
119
|
+
it('should handle query canceled phase', async () => {
|
|
120
|
+
const mockQueryResponse = {
|
|
121
|
+
status: {
|
|
122
|
+
phase: 'canceled',
|
|
123
|
+
message: 'Query was canceled',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
mockExeca.mockImplementation(async (command, args) => {
|
|
127
|
+
if (args.includes('apply')) {
|
|
128
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
129
|
+
}
|
|
130
|
+
if (args.includes('get') && args.includes('query')) {
|
|
131
|
+
return {
|
|
132
|
+
stdout: JSON.stringify(mockQueryResponse),
|
|
133
|
+
stderr: '',
|
|
134
|
+
exitCode: 0,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (args.includes('delete')) {
|
|
138
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
139
|
+
}
|
|
140
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
141
|
+
});
|
|
142
|
+
await executeQuery({
|
|
143
|
+
targetType: 'agent',
|
|
144
|
+
targetName: 'test-agent',
|
|
145
|
+
message: 'Hello',
|
|
146
|
+
});
|
|
147
|
+
expect(mockSpinner.warn).toHaveBeenCalledWith('Query canceled');
|
|
148
|
+
expect(mockOutput.warning).toHaveBeenCalledWith('Query was canceled');
|
|
149
|
+
});
|
|
150
|
+
it('should clean up query resource even on failure', async () => {
|
|
151
|
+
mockExeca.mockImplementation(async (command, args) => {
|
|
152
|
+
if (args.includes('apply')) {
|
|
153
|
+
throw new Error('Failed to apply');
|
|
154
|
+
}
|
|
155
|
+
if (args.includes('delete')) {
|
|
156
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
157
|
+
}
|
|
158
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
159
|
+
});
|
|
160
|
+
await expect(executeQuery({
|
|
161
|
+
targetType: 'model',
|
|
162
|
+
targetName: 'default',
|
|
163
|
+
message: 'Hello',
|
|
164
|
+
})).rejects.toThrow('process.exit called');
|
|
165
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith('Query failed');
|
|
166
|
+
expect(mockOutput.error).toHaveBeenCalledWith('Failed to apply');
|
|
167
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
package/dist/lib/nextSteps.js
CHANGED
|
@@ -13,7 +13,7 @@ export function printNextSteps() {
|
|
|
13
13
|
console.log(` ${chalk.gray('open dashboard:')} ${chalk.white.bold('ark dashboard')}`);
|
|
14
14
|
console.log(` ${chalk.gray('show agents:')} ${chalk.white.bold('kubectl get agents')}`);
|
|
15
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(
|
|
16
|
+
console.log(` ${chalk.gray('interactive chat:')} ${chalk.white.bold('ark')} ${chalk.gray("# then choose 'Chat'")}`);
|
|
17
17
|
console.log(` ${chalk.gray('new project:')} ${chalk.white.bold('ark generate project my-agents')}`);
|
|
18
18
|
console.log(` ${chalk.gray('install fark:')} ${chalk.blue('https://mckinsey.github.io/agents-at-scale-ark/developer-guide/cli-tools/')}`);
|
|
19
19
|
console.log();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared query execution logic for both universal and resource-specific query commands
|
|
3
|
+
*/
|
|
4
|
+
export interface QueryOptions {
|
|
5
|
+
targetType: string;
|
|
6
|
+
targetName: string;
|
|
7
|
+
message: string;
|
|
8
|
+
verbose?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Execute a query against any ARK target (model, agent, team)
|
|
12
|
+
* This is the shared implementation used by all query commands
|
|
13
|
+
*/
|
|
14
|
+
export declare function executeQuery(options: QueryOptions): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a target string like "model/default" or "agent/weather"
|
|
17
|
+
* Returns { targetType, targetName } or null if invalid
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseTarget(target: string): {
|
|
20
|
+
targetType: string;
|
|
21
|
+
targetName: string;
|
|
22
|
+
} | null;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared query execution logic for both universal and resource-specific query commands
|
|
3
|
+
*/
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import output from './output.js';
|
|
7
|
+
/**
|
|
8
|
+
* Execute a query against any ARK target (model, agent, team)
|
|
9
|
+
* This is the shared implementation used by all query commands
|
|
10
|
+
*/
|
|
11
|
+
export async function executeQuery(options) {
|
|
12
|
+
const spinner = ora('Creating query...').start();
|
|
13
|
+
// Generate a unique query name
|
|
14
|
+
const timestamp = Date.now();
|
|
15
|
+
const queryName = `cli-query-${timestamp}`;
|
|
16
|
+
// Create the Query resource
|
|
17
|
+
const queryManifest = {
|
|
18
|
+
apiVersion: 'ark.mckinsey.com/v1alpha1',
|
|
19
|
+
kind: 'Query',
|
|
20
|
+
metadata: {
|
|
21
|
+
name: queryName,
|
|
22
|
+
},
|
|
23
|
+
spec: {
|
|
24
|
+
input: options.message,
|
|
25
|
+
targets: [
|
|
26
|
+
{
|
|
27
|
+
type: options.targetType,
|
|
28
|
+
name: options.targetName,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
// Apply the query
|
|
35
|
+
spinner.text = 'Submitting query...';
|
|
36
|
+
await execa('kubectl', ['apply', '-f', '-'], {
|
|
37
|
+
input: JSON.stringify(queryManifest),
|
|
38
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
});
|
|
40
|
+
// Watch for query completion
|
|
41
|
+
spinner.text = 'Query status: initializing';
|
|
42
|
+
let queryComplete = false;
|
|
43
|
+
let attempts = 0;
|
|
44
|
+
const maxAttempts = 300; // 5 minutes with 1 second intervals
|
|
45
|
+
while (!queryComplete && attempts < maxAttempts) {
|
|
46
|
+
attempts++;
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execa('kubectl', [
|
|
49
|
+
'get',
|
|
50
|
+
'query',
|
|
51
|
+
queryName,
|
|
52
|
+
'-o',
|
|
53
|
+
'json',
|
|
54
|
+
], { stdio: 'pipe' });
|
|
55
|
+
const query = JSON.parse(stdout);
|
|
56
|
+
const phase = query.status?.phase;
|
|
57
|
+
// Update spinner with current phase
|
|
58
|
+
if (phase) {
|
|
59
|
+
spinner.text = `Query status: ${phase}`;
|
|
60
|
+
}
|
|
61
|
+
// Check if query is complete based on phase
|
|
62
|
+
if (phase === 'done') {
|
|
63
|
+
queryComplete = true;
|
|
64
|
+
spinner.succeed('Query completed');
|
|
65
|
+
// Extract and display the response from responses array
|
|
66
|
+
if (query.status?.responses && query.status.responses.length > 0) {
|
|
67
|
+
const response = query.status.responses[0];
|
|
68
|
+
console.log('\n' + (response.content || response));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
output.warning('No response received');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (phase === 'error') {
|
|
75
|
+
queryComplete = true;
|
|
76
|
+
spinner.fail('Query failed');
|
|
77
|
+
// Try to get error message from conditions or status
|
|
78
|
+
const errorCondition = query.status?.conditions?.find((c) => {
|
|
79
|
+
const condition = c;
|
|
80
|
+
return condition.type === 'Complete' && condition.status === 'False';
|
|
81
|
+
});
|
|
82
|
+
if (errorCondition?.message) {
|
|
83
|
+
output.error(errorCondition.message);
|
|
84
|
+
}
|
|
85
|
+
else if (query.status?.error) {
|
|
86
|
+
output.error(query.status.error);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
output.error('Query failed with unknown error');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (phase === 'canceled') {
|
|
93
|
+
queryComplete = true;
|
|
94
|
+
spinner.warn('Query canceled');
|
|
95
|
+
// Try to get cancellation reason if available
|
|
96
|
+
if (query.status?.message) {
|
|
97
|
+
output.warning(query.status.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Query might not exist yet, continue waiting
|
|
103
|
+
spinner.text = 'Query status: waiting for query to be created';
|
|
104
|
+
}
|
|
105
|
+
if (!queryComplete) {
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!queryComplete) {
|
|
110
|
+
spinner.fail('Query timed out');
|
|
111
|
+
output.error('Query did not complete within 5 minutes');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
spinner.fail('Query failed');
|
|
116
|
+
output.error(error instanceof Error ? error.message : 'Unknown error');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
// Clean up the query resource
|
|
121
|
+
try {
|
|
122
|
+
await execa('kubectl', ['delete', 'query', queryName], { stdio: 'pipe' });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Ignore cleanup errors
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Parse a target string like "model/default" or "agent/weather"
|
|
131
|
+
* Returns { targetType, targetName } or null if invalid
|
|
132
|
+
*/
|
|
133
|
+
export function parseTarget(target) {
|
|
134
|
+
const parts = target.split('/');
|
|
135
|
+
if (parts.length !== 2) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
targetType: parts[0],
|
|
140
|
+
targetName: parts[1],
|
|
141
|
+
};
|
|
142
|
+
}
|
package/dist/lib/startup.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ import type { ArkConfig } from './config.js';
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function showNoClusterError(): void;
|
|
6
6
|
/**
|
|
7
|
-
* Initialize the CLI
|
|
7
|
+
* Initialize the CLI with minimal checks for fast startup
|
|
8
8
|
*/
|
|
9
9
|
export declare function startup(): Promise<ArkConfig>;
|
package/dist/lib/startup.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { execa } from 'execa';
|
|
2
3
|
import { checkCommandExists } from './commands.js';
|
|
3
4
|
import { loadConfig } from './config.js';
|
|
4
|
-
import { getArkVersion } from './arkStatus.js';
|
|
5
|
-
import { getClusterInfo } from './cluster.js';
|
|
6
5
|
const REQUIRED_COMMANDS = [
|
|
7
6
|
{
|
|
8
7
|
name: 'kubectl',
|
|
@@ -48,46 +47,41 @@ export function showNoClusterError() {
|
|
|
48
47
|
console.log(chalk.blue(' https://mckinsey.github.io/agents-at-scale-ark/quickstart/'));
|
|
49
48
|
}
|
|
50
49
|
/**
|
|
51
|
-
*
|
|
50
|
+
* Check if a Kubernetes context is configured
|
|
51
|
+
* This is a fast local check that doesn't hit the cluster
|
|
52
52
|
*/
|
|
53
|
-
async function
|
|
54
|
-
// Fetch latest version from GitHub
|
|
53
|
+
async function hasKubernetesContext() {
|
|
55
54
|
try {
|
|
56
|
-
const
|
|
57
|
-
|
|
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
|
-
}
|
|
55
|
+
const { stdout } = await execa('kubectl', ['config', 'current-context']);
|
|
56
|
+
return stdout.trim().length > 0;
|
|
72
57
|
}
|
|
73
58
|
catch {
|
|
74
|
-
|
|
59
|
+
return false;
|
|
75
60
|
}
|
|
76
61
|
}
|
|
77
62
|
/**
|
|
78
|
-
* Initialize the CLI
|
|
63
|
+
* Initialize the CLI with minimal checks for fast startup
|
|
79
64
|
*/
|
|
80
65
|
export async function startup() {
|
|
81
|
-
// Check required commands
|
|
66
|
+
// Check required commands (kubectl, helm) - fast local checks
|
|
82
67
|
await checkRequirements();
|
|
83
|
-
// Load config
|
|
68
|
+
// Load config from disk (fast - just file I/O)
|
|
84
69
|
const config = loadConfig();
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
70
|
+
// Check if we have a kubernetes context configured (fast local check)
|
|
71
|
+
// We don't check cluster connectivity here - that's expensive
|
|
72
|
+
const hasContext = await hasKubernetesContext();
|
|
73
|
+
if (hasContext) {
|
|
74
|
+
try {
|
|
75
|
+
const { stdout } = await execa('kubectl', ['config', 'current-context']);
|
|
76
|
+
config.clusterInfo = {
|
|
77
|
+
type: 'unknown', // We don't detect cluster type here - too slow
|
|
78
|
+
context: stdout.trim(),
|
|
79
|
+
// We don't fetch namespace or cluster details here - too slow
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore - no context
|
|
84
|
+
}
|
|
89
85
|
}
|
|
90
|
-
// Fetch version info synchronously so it's available immediately
|
|
91
|
-
await fetchVersionInfo(config);
|
|
92
86
|
return config;
|
|
93
87
|
}
|
package/dist/lib/startup.spec.js
CHANGED
|
@@ -16,21 +16,26 @@ jest.unstable_mockModule('./commands.js', () => ({
|
|
|
16
16
|
jest.unstable_mockModule('./config.js', () => ({
|
|
17
17
|
loadConfig: jest.fn(),
|
|
18
18
|
}));
|
|
19
|
+
// Mock execa module
|
|
20
|
+
jest.unstable_mockModule('execa', () => ({
|
|
21
|
+
execa: jest.fn(),
|
|
22
|
+
}));
|
|
19
23
|
// Dynamic imports after mocks
|
|
20
24
|
const { checkCommandExists } = await import('./commands.js');
|
|
21
25
|
const { loadConfig } = await import('./config.js');
|
|
26
|
+
const { execa } = await import('execa');
|
|
22
27
|
const { startup } = await import('./startup.js');
|
|
23
28
|
// Type the mocks
|
|
24
29
|
const mockCheckCommandExists = checkCommandExists;
|
|
25
30
|
const mockLoadConfig = loadConfig;
|
|
26
|
-
|
|
27
|
-
globalThis.fetch = jest.fn();
|
|
31
|
+
const mockExeca = execa;
|
|
28
32
|
describe('startup', () => {
|
|
29
33
|
let mockExit;
|
|
30
34
|
let mockConsoleError;
|
|
31
35
|
beforeEach(() => {
|
|
32
36
|
jest.clearAllMocks();
|
|
33
|
-
|
|
37
|
+
// Mock execa to reject by default (no kubectl context)
|
|
38
|
+
mockExeca.mockRejectedValue(new Error('No context'));
|
|
34
39
|
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
35
40
|
throw new Error('process.exit');
|
|
36
41
|
});
|
|
@@ -120,49 +125,28 @@ describe('startup', () => {
|
|
|
120
125
|
expect(checkCallOrder).toBeLessThan(loadCallOrder);
|
|
121
126
|
expect(config).toEqual(expectedConfig);
|
|
122
127
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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();
|
|
128
|
+
it('includes cluster context when available', async () => {
|
|
129
|
+
mockCheckCommandExists.mockResolvedValue(true);
|
|
130
|
+
mockLoadConfig.mockReturnValue({ chat: { streaming: true } });
|
|
131
|
+
// Mock successful kubectl context check
|
|
132
|
+
mockExeca.mockResolvedValue({
|
|
133
|
+
stdout: 'minikube',
|
|
134
|
+
stderr: '',
|
|
158
135
|
});
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
expect(config).toBeDefined();
|
|
164
|
-
expect(config.chat).toBeDefined();
|
|
165
|
-
expect(mockExit).not.toHaveBeenCalled();
|
|
136
|
+
const config = await startup();
|
|
137
|
+
expect(config.clusterInfo).toEqual({
|
|
138
|
+
type: 'unknown',
|
|
139
|
+
context: 'minikube',
|
|
166
140
|
});
|
|
141
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['config', 'current-context']);
|
|
142
|
+
});
|
|
143
|
+
it('handles missing kubectl context gracefully', async () => {
|
|
144
|
+
mockCheckCommandExists.mockResolvedValue(true);
|
|
145
|
+
const expectedConfig = { chat: { streaming: false } };
|
|
146
|
+
mockLoadConfig.mockReturnValue(expectedConfig);
|
|
147
|
+
// mockExeca already mocked to reject in beforeEach
|
|
148
|
+
const config = await startup();
|
|
149
|
+
expect(config).toEqual(expectedConfig);
|
|
150
|
+
expect(config.clusterInfo).toBeUndefined();
|
|
167
151
|
});
|
|
168
152
|
});
|