@agents-at-scale/ark 0.1.41 → 0.1.43
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 +0 -9
- package/dist/commands/completion/index.js +46 -1
- package/dist/commands/evaluation/index.d.ts +3 -0
- package/dist/commands/evaluation/index.js +60 -0
- package/dist/commands/evaluation/index.spec.d.ts +1 -0
- package/dist/commands/evaluation/index.spec.js +161 -0
- package/dist/commands/generate/generators/team.js +4 -1
- package/dist/commands/install/index.js +27 -0
- package/dist/commands/marketplace/index.d.ts +4 -0
- package/dist/commands/marketplace/index.js +50 -0
- package/dist/commands/memory/index.d.ts +15 -0
- package/dist/commands/memory/index.js +130 -0
- package/dist/commands/memory/index.spec.d.ts +1 -0
- package/dist/commands/memory/index.spec.js +124 -0
- package/dist/commands/models/create.d.ts +5 -6
- package/dist/commands/models/create.js +14 -119
- package/dist/commands/models/create.spec.js +51 -0
- package/dist/commands/models/kubernetes/manifest-builder.d.ts +11 -0
- package/dist/commands/models/kubernetes/manifest-builder.js +109 -0
- package/dist/commands/models/kubernetes/secret-manager.d.ts +7 -0
- package/dist/commands/models/kubernetes/secret-manager.js +20 -0
- package/dist/commands/models/providers/azure.d.ts +31 -0
- package/dist/commands/models/providers/azure.js +82 -0
- package/dist/commands/models/providers/azure.spec.d.ts +1 -0
- package/dist/commands/models/providers/azure.spec.js +232 -0
- package/dist/commands/models/providers/bedrock.d.ts +37 -0
- package/dist/commands/models/providers/bedrock.js +105 -0
- package/dist/commands/models/providers/bedrock.spec.d.ts +1 -0
- package/dist/commands/models/providers/bedrock.spec.js +241 -0
- package/dist/commands/models/providers/factory.d.ts +18 -0
- package/dist/commands/models/providers/factory.js +31 -0
- package/dist/commands/models/providers/index.d.ts +17 -0
- package/dist/commands/models/providers/index.js +9 -0
- package/dist/commands/models/providers/openai.d.ts +28 -0
- package/dist/commands/models/providers/openai.js +68 -0
- package/dist/commands/models/providers/openai.spec.d.ts +1 -0
- package/dist/commands/models/providers/openai.spec.js +180 -0
- package/dist/commands/models/providers/types.d.ts +51 -0
- package/dist/commands/models/providers/types.js +1 -0
- package/dist/commands/queries/delete.d.ts +7 -0
- package/dist/commands/queries/delete.js +24 -0
- package/dist/commands/queries/delete.spec.d.ts +1 -0
- package/dist/commands/queries/delete.spec.js +74 -0
- package/dist/commands/queries/index.d.ts +3 -0
- package/dist/commands/queries/index.js +108 -0
- package/dist/commands/queries/list.d.ts +6 -0
- package/dist/commands/queries/list.js +66 -0
- package/dist/commands/queries/list.spec.d.ts +1 -0
- package/dist/commands/queries/list.spec.js +170 -0
- package/dist/commands/queries/validation.d.ts +2 -0
- package/dist/commands/queries/validation.js +10 -0
- package/dist/commands/queries/validation.spec.d.ts +1 -0
- package/dist/commands/queries/validation.spec.js +27 -0
- package/dist/commands/query/index.js +3 -1
- package/dist/commands/query/index.spec.js +24 -0
- package/dist/commands/uninstall/index.js +27 -0
- package/dist/components/ChatUI.js +2 -0
- package/dist/index.js +8 -0
- package/dist/lib/arkApiClient.d.ts +4 -0
- package/dist/lib/arkApiClient.js +57 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +1 -0
- package/dist/lib/executeEvaluation.d.ts +16 -0
- package/dist/lib/executeEvaluation.js +155 -0
- package/dist/lib/executeQuery.d.ts +1 -4
- package/dist/lib/executeQuery.js +98 -68
- package/dist/lib/executeQuery.spec.js +176 -99
- package/dist/lib/kubectl.d.ts +15 -0
- package/dist/lib/kubectl.js +47 -0
- package/dist/lib/kubectl.spec.d.ts +1 -0
- package/dist/lib/kubectl.spec.js +176 -0
- package/dist/lib/stdin.d.ts +1 -0
- package/dist/lib/stdin.js +16 -0
- package/dist/lib/stdin.spec.d.ts +1 -0
- package/dist/lib/stdin.spec.js +82 -0
- package/dist/lib/types.d.ts +39 -0
- package/dist/marketplaceServices.d.ts +15 -0
- package/dist/marketplaceServices.js +51 -0
- package/package.json +2 -1
package/dist/lib/executeQuery.js
CHANGED
|
@@ -4,21 +4,91 @@
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import output from './output.js';
|
|
8
7
|
import { ExitCodes } from './errors.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
* Execute a query against any ARK target (model, agent, team)
|
|
12
|
-
* This is the shared implementation used by all query commands
|
|
13
|
-
*/
|
|
8
|
+
import { ArkApiProxy } from './arkApiProxy.js';
|
|
9
|
+
import { ChatClient } from './chatClient.js';
|
|
14
10
|
export async function executeQuery(options) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
if (options.outputFormat) {
|
|
12
|
+
return executeQueryWithFormat(options);
|
|
13
|
+
}
|
|
14
|
+
let arkApiProxy;
|
|
15
|
+
const spinner = ora('Connecting to Ark API...').start();
|
|
16
|
+
try {
|
|
17
|
+
arkApiProxy = new ArkApiProxy();
|
|
18
|
+
const arkApiClient = await arkApiProxy.start();
|
|
19
|
+
const chatClient = new ChatClient(arkApiClient);
|
|
20
|
+
spinner.text = 'Executing query...';
|
|
21
|
+
const targetId = `${options.targetType}/${options.targetName}`;
|
|
22
|
+
const messages = [{ role: 'user', content: options.message }];
|
|
23
|
+
const state = {
|
|
24
|
+
toolCalls: new Map(),
|
|
25
|
+
content: '',
|
|
26
|
+
};
|
|
27
|
+
let lastAgentName;
|
|
28
|
+
let headerShown = false;
|
|
29
|
+
let firstOutput = true;
|
|
30
|
+
await chatClient.sendMessage(targetId, messages, { streamingEnabled: true }, (chunk, toolCalls, arkMetadata) => {
|
|
31
|
+
if (firstOutput) {
|
|
32
|
+
spinner.stop();
|
|
33
|
+
firstOutput = false;
|
|
34
|
+
}
|
|
35
|
+
const agentName = arkMetadata?.agent || arkMetadata?.team;
|
|
36
|
+
if (agentName && agentName !== lastAgentName) {
|
|
37
|
+
if (lastAgentName) {
|
|
38
|
+
if (state.content) {
|
|
39
|
+
process.stdout.write('\n');
|
|
40
|
+
}
|
|
41
|
+
process.stdout.write('\n');
|
|
42
|
+
}
|
|
43
|
+
const prefix = arkMetadata?.team ? '◆' : '●';
|
|
44
|
+
const color = arkMetadata?.team ? 'green' : 'cyan';
|
|
45
|
+
process.stdout.write(chalk[color](`${prefix} ${agentName}\n`));
|
|
46
|
+
lastAgentName = agentName;
|
|
47
|
+
state.content = '';
|
|
48
|
+
state.toolCalls.clear();
|
|
49
|
+
headerShown = true;
|
|
50
|
+
}
|
|
51
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
52
|
+
for (const toolCall of toolCalls) {
|
|
53
|
+
if (!state.toolCalls.has(toolCall.id)) {
|
|
54
|
+
state.toolCalls.set(toolCall.id, toolCall);
|
|
55
|
+
if (state.content) {
|
|
56
|
+
process.stdout.write('\n');
|
|
57
|
+
}
|
|
58
|
+
process.stdout.write(chalk.magenta(`🔧 ${toolCall.function.name}\n`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (chunk) {
|
|
63
|
+
if (state.toolCalls.size > 0 && !state.content) {
|
|
64
|
+
process.stdout.write('\n');
|
|
65
|
+
}
|
|
66
|
+
process.stdout.write(chunk);
|
|
67
|
+
state.content += chunk;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (spinner.isSpinning) {
|
|
71
|
+
spinner.stop();
|
|
72
|
+
}
|
|
73
|
+
if ((state.content || state.toolCalls.size > 0) && headerShown) {
|
|
74
|
+
process.stdout.write('\n');
|
|
75
|
+
}
|
|
76
|
+
if (arkApiProxy) {
|
|
77
|
+
arkApiProxy.stop();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (spinner.isSpinning) {
|
|
82
|
+
spinner.stop();
|
|
83
|
+
}
|
|
84
|
+
if (arkApiProxy) {
|
|
85
|
+
arkApiProxy.stop();
|
|
86
|
+
}
|
|
87
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
88
|
+
process.exit(ExitCodes.CliError);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function executeQueryWithFormat(options) {
|
|
22
92
|
const timestamp = Date.now();
|
|
23
93
|
const queryName = `cli-query-${timestamp}`;
|
|
24
94
|
const queryManifest = {
|
|
@@ -39,71 +109,31 @@ export async function executeQuery(options) {
|
|
|
39
109
|
},
|
|
40
110
|
};
|
|
41
111
|
try {
|
|
42
|
-
// Apply the query
|
|
43
|
-
spinner.text = 'Submitting query...';
|
|
44
112
|
await execa('kubectl', ['apply', '-f', '-'], {
|
|
45
113
|
input: JSON.stringify(queryManifest),
|
|
46
114
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
115
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
116
|
+
const timeoutSeconds = 300;
|
|
117
|
+
await execa('kubectl', [
|
|
118
|
+
'wait',
|
|
119
|
+
'--for=condition=Completed',
|
|
120
|
+
`query/${queryName}`,
|
|
121
|
+
`--timeout=${timeoutSeconds}s`,
|
|
122
|
+
], { timeout: timeoutSeconds * 1000 });
|
|
123
|
+
if (options.outputFormat === 'name') {
|
|
124
|
+
console.log(queryName);
|
|
57
125
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
error.message.includes('timed out waiting')) {
|
|
63
|
-
console.error(chalk.red(`Query did not complete within ${options.watchTimeout ?? `${Math.floor(watchTimeoutMs / 1000)}s`}`));
|
|
64
|
-
process.exit(ExitCodes.Timeout);
|
|
65
|
-
}
|
|
66
|
-
// For other errors, fetch the query to check status
|
|
67
|
-
}
|
|
68
|
-
spinner.stop();
|
|
69
|
-
// Fetch final query state
|
|
70
|
-
try {
|
|
71
|
-
const { stdout } = await execa('kubectl', ['get', 'query', queryName, '-o', 'json'], { stdio: 'pipe' });
|
|
72
|
-
const query = JSON.parse(stdout);
|
|
73
|
-
const phase = query.status?.phase;
|
|
74
|
-
// Check if query completed successfully or with error
|
|
75
|
-
if (phase === 'done') {
|
|
76
|
-
// Extract and display the response from responses array
|
|
77
|
-
if (query.status?.responses && query.status.responses.length > 0) {
|
|
78
|
-
const response = query.status.responses[0];
|
|
79
|
-
console.log(response.content || response);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
output.warning('No response received');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else if (phase === 'error') {
|
|
86
|
-
const response = query.status?.responses?.[0];
|
|
87
|
-
console.error(chalk.red(response?.content || 'Query failed with unknown error'));
|
|
88
|
-
process.exit(ExitCodes.OperationError);
|
|
89
|
-
}
|
|
90
|
-
else if (phase === 'canceled') {
|
|
91
|
-
spinner.warn('Query canceled');
|
|
92
|
-
if (query.status?.message) {
|
|
93
|
-
output.warning(query.status.message);
|
|
94
|
-
}
|
|
95
|
-
process.exit(ExitCodes.OperationError);
|
|
96
|
-
}
|
|
126
|
+
else if (options.outputFormat === 'json' ||
|
|
127
|
+
options.outputFormat === 'yaml') {
|
|
128
|
+
const { stdout } = await execa('kubectl', ['get', 'query', queryName, '-o', options.outputFormat], { stdio: 'pipe' });
|
|
129
|
+
console.log(stdout);
|
|
97
130
|
}
|
|
98
|
-
|
|
99
|
-
console.error(chalk.red(
|
|
100
|
-
? error.message
|
|
101
|
-
: 'Failed to fetch query result'));
|
|
131
|
+
else {
|
|
132
|
+
console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, or name`));
|
|
102
133
|
process.exit(ExitCodes.CliError);
|
|
103
134
|
}
|
|
104
135
|
}
|
|
105
136
|
catch (error) {
|
|
106
|
-
spinner.stop();
|
|
107
137
|
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
108
138
|
process.exit(ExitCodes.CliError);
|
|
109
139
|
}
|
|
@@ -10,17 +10,26 @@ const mockSpinner = {
|
|
|
10
10
|
warn: jest.fn(),
|
|
11
11
|
stop: jest.fn(),
|
|
12
12
|
text: '',
|
|
13
|
+
isSpinning: false,
|
|
13
14
|
};
|
|
14
15
|
const mockOra = jest.fn(() => mockSpinner);
|
|
15
16
|
jest.unstable_mockModule('ora', () => ({
|
|
16
17
|
default: mockOra,
|
|
17
18
|
}));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
let mockSendMessage = jest.fn();
|
|
20
|
+
const mockChatClient = jest.fn(() => ({
|
|
21
|
+
sendMessage: mockSendMessage,
|
|
22
|
+
}));
|
|
23
|
+
let mockArkApiProxyInstance = {
|
|
24
|
+
start: jest.fn(),
|
|
25
|
+
stop: jest.fn(),
|
|
21
26
|
};
|
|
22
|
-
jest.
|
|
23
|
-
|
|
27
|
+
const mockArkApiProxy = jest.fn(() => mockArkApiProxyInstance);
|
|
28
|
+
jest.unstable_mockModule('./arkApiProxy.js', () => ({
|
|
29
|
+
ArkApiProxy: mockArkApiProxy,
|
|
30
|
+
}));
|
|
31
|
+
jest.unstable_mockModule('./chatClient.js', () => ({
|
|
32
|
+
ChatClient: mockChatClient,
|
|
24
33
|
}));
|
|
25
34
|
const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
26
35
|
throw new Error('process.exit called');
|
|
@@ -29,12 +38,25 @@ const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
|
29
38
|
const mockConsoleError = jest
|
|
30
39
|
.spyOn(console, 'error')
|
|
31
40
|
.mockImplementation(() => { });
|
|
41
|
+
const mockStdoutWrite = jest
|
|
42
|
+
.spyOn(process.stdout, 'write')
|
|
43
|
+
.mockImplementation(() => true);
|
|
32
44
|
const { executeQuery, parseTarget } = await import('./executeQuery.js');
|
|
33
45
|
const { ExitCodes } = await import('./errors.js');
|
|
34
46
|
describe('executeQuery', () => {
|
|
35
47
|
beforeEach(() => {
|
|
36
48
|
jest.clearAllMocks();
|
|
37
49
|
mockSpinner.start.mockReturnValue(mockSpinner);
|
|
50
|
+
mockSpinner.isSpinning = false;
|
|
51
|
+
mockSendMessage = jest.fn();
|
|
52
|
+
mockChatClient.mockReturnValue({ sendMessage: mockSendMessage });
|
|
53
|
+
const startMock = jest.fn();
|
|
54
|
+
startMock.mockResolvedValue({});
|
|
55
|
+
mockArkApiProxyInstance = {
|
|
56
|
+
start: startMock,
|
|
57
|
+
stop: jest.fn(),
|
|
58
|
+
};
|
|
59
|
+
mockArkApiProxy.mockReturnValue(mockArkApiProxyInstance);
|
|
38
60
|
});
|
|
39
61
|
describe('parseTarget', () => {
|
|
40
62
|
it('should parse valid target strings', () => {
|
|
@@ -57,24 +79,107 @@ describe('executeQuery', () => {
|
|
|
57
79
|
expect(parseTarget('model/default/extra')).toBeNull();
|
|
58
80
|
});
|
|
59
81
|
});
|
|
60
|
-
describe('executeQuery', () => {
|
|
61
|
-
it('should
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
describe('executeQuery with streaming', () => {
|
|
83
|
+
it('should execute query with streaming and display chunks', async () => {
|
|
84
|
+
mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
|
|
85
|
+
callback('Hello', undefined, { agent: 'test-agent' });
|
|
86
|
+
callback(' world', undefined, { agent: 'test-agent' });
|
|
87
|
+
});
|
|
88
|
+
await executeQuery({
|
|
89
|
+
targetType: 'model',
|
|
90
|
+
targetName: 'default',
|
|
91
|
+
message: 'Hello',
|
|
92
|
+
});
|
|
93
|
+
expect(mockArkApiProxy).toHaveBeenCalled();
|
|
94
|
+
expect(mockArkApiProxyInstance.start).toHaveBeenCalled();
|
|
95
|
+
expect(mockChatClient).toHaveBeenCalled();
|
|
96
|
+
expect(mockSendMessage).toHaveBeenCalledWith('model/default', [{ role: 'user', content: 'Hello' }], { streamingEnabled: true }, expect.any(Function));
|
|
97
|
+
expect(mockSpinner.stop).toHaveBeenCalled();
|
|
98
|
+
expect(mockArkApiProxyInstance.stop).toHaveBeenCalled();
|
|
99
|
+
expect(mockStdoutWrite).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
it('should display agent names with correct formatting', async () => {
|
|
102
|
+
mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
|
|
103
|
+
callback('Response 1', undefined, { agent: 'agent-1' });
|
|
104
|
+
callback('Response 2', undefined, { agent: 'agent-2' });
|
|
105
|
+
});
|
|
106
|
+
await executeQuery({
|
|
107
|
+
targetType: 'agent',
|
|
108
|
+
targetName: 'test-agent',
|
|
109
|
+
message: 'Hello',
|
|
110
|
+
});
|
|
111
|
+
expect(mockStdoutWrite).toHaveBeenCalled();
|
|
112
|
+
const calls = mockStdoutWrite.mock.calls.map((call) => String(call[0]));
|
|
113
|
+
expect(calls.some((call) => call.includes('agent-1'))).toBe(true);
|
|
114
|
+
expect(calls.some((call) => call.includes('agent-2'))).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('should display team names with diamond prefix', async () => {
|
|
117
|
+
mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
|
|
118
|
+
callback('Team response', undefined, { team: 'my-team' });
|
|
119
|
+
});
|
|
120
|
+
await executeQuery({
|
|
121
|
+
targetType: 'team',
|
|
122
|
+
targetName: 'my-team',
|
|
123
|
+
message: 'Hello',
|
|
124
|
+
});
|
|
125
|
+
const calls = mockStdoutWrite.mock.calls.map((call) => String(call[0]));
|
|
126
|
+
expect(calls.some((call) => call.includes('◆'))).toBe(true);
|
|
127
|
+
expect(calls.some((call) => call.includes('my-team'))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('should display tool calls', async () => {
|
|
130
|
+
mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
|
|
131
|
+
callback('', [{ id: 1, function: { name: 'get_weather' } }], {
|
|
132
|
+
agent: 'weather-agent',
|
|
133
|
+
});
|
|
134
|
+
callback('The weather is sunny', undefined, {
|
|
135
|
+
agent: 'weather-agent',
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
await executeQuery({
|
|
139
|
+
targetType: 'agent',
|
|
140
|
+
targetName: 'weather-agent',
|
|
141
|
+
message: 'What is the weather?',
|
|
142
|
+
});
|
|
143
|
+
const calls = mockStdoutWrite.mock.calls.map((call) => String(call[0]));
|
|
144
|
+
expect(calls.some((call) => call.includes('get_weather'))).toBe(true);
|
|
145
|
+
expect(calls.some((call) => call.includes('The weather is sunny'))).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
it('should handle errors and exit with CliError', async () => {
|
|
148
|
+
mockSpinner.isSpinning = true;
|
|
149
|
+
const startMock = jest.fn();
|
|
150
|
+
startMock.mockRejectedValue(new Error('Connection failed'));
|
|
151
|
+
mockArkApiProxyInstance.start = startMock;
|
|
152
|
+
await expect(executeQuery({
|
|
153
|
+
targetType: 'model',
|
|
154
|
+
targetName: 'default',
|
|
155
|
+
message: 'Hello',
|
|
156
|
+
})).rejects.toThrow('process.exit called');
|
|
157
|
+
expect(mockSpinner.stop).toHaveBeenCalled();
|
|
158
|
+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Connection failed'));
|
|
159
|
+
expect(mockExit).toHaveBeenCalledWith(ExitCodes.CliError);
|
|
160
|
+
expect(mockArkApiProxyInstance.stop).toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
it('should stop spinner when first output arrives', async () => {
|
|
163
|
+
mockSpinner.isSpinning = true;
|
|
164
|
+
mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
|
|
165
|
+
callback('First chunk', undefined, { agent: 'test-agent' });
|
|
166
|
+
});
|
|
167
|
+
await executeQuery({
|
|
168
|
+
targetType: 'model',
|
|
169
|
+
targetName: 'default',
|
|
170
|
+
message: 'Hello',
|
|
171
|
+
});
|
|
172
|
+
expect(mockSpinner.stop).toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('executeQuery with output format', () => {
|
|
176
|
+
it('should create query and output name format', async () => {
|
|
68
177
|
mockExeca.mockImplementation(async (command, args) => {
|
|
69
178
|
if (args.includes('apply')) {
|
|
70
179
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
71
180
|
}
|
|
72
|
-
if (args.includes('
|
|
73
|
-
return {
|
|
74
|
-
stdout: JSON.stringify(mockQueryResponse),
|
|
75
|
-
stderr: '',
|
|
76
|
-
exitCode: 0,
|
|
77
|
-
};
|
|
181
|
+
if (args.includes('wait')) {
|
|
182
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
78
183
|
}
|
|
79
184
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
80
185
|
});
|
|
@@ -82,83 +187,67 @@ describe('executeQuery', () => {
|
|
|
82
187
|
targetType: 'model',
|
|
83
188
|
targetName: 'default',
|
|
84
189
|
message: 'Hello',
|
|
190
|
+
outputFormat: 'name',
|
|
85
191
|
});
|
|
86
|
-
expect(
|
|
87
|
-
expect(
|
|
88
|
-
expect(mockConsoleLog).toHaveBeenCalledWith(
|
|
192
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', expect.arrayContaining(['apply', '-f', '-']), expect.any(Object));
|
|
193
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', expect.arrayContaining(['wait', '--for=condition=Completed']), expect.any(Object));
|
|
194
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringMatching(/cli-query-\d+/));
|
|
89
195
|
});
|
|
90
|
-
it('should
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
196
|
+
it('should output json format', async () => {
|
|
197
|
+
const mockQuery = {
|
|
198
|
+
apiVersion: 'ark.mckinsey.com/v1alpha1',
|
|
199
|
+
kind: 'Query',
|
|
200
|
+
metadata: { name: 'test-query' },
|
|
96
201
|
};
|
|
97
202
|
mockExeca.mockImplementation(async (command, args) => {
|
|
98
203
|
if (args.includes('apply')) {
|
|
99
204
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
100
205
|
}
|
|
101
|
-
if (args.includes('
|
|
102
|
-
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
206
|
+
if (args.includes('wait')) {
|
|
207
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
208
|
+
}
|
|
209
|
+
if (args.includes('get') && args.includes('-o')) {
|
|
210
|
+
return { stdout: JSON.stringify(mockQuery), stderr: '', exitCode: 0 };
|
|
107
211
|
}
|
|
108
212
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
109
213
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
catch (error) {
|
|
118
|
-
expect(error.message).toBe('process.exit called');
|
|
119
|
-
}
|
|
120
|
-
expect(mockSpinner.stop).toHaveBeenCalled();
|
|
121
|
-
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Query failed with test error'));
|
|
122
|
-
expect(mockExit).toHaveBeenCalledWith(ExitCodes.OperationError);
|
|
214
|
+
await executeQuery({
|
|
215
|
+
targetType: 'model',
|
|
216
|
+
targetName: 'default',
|
|
217
|
+
message: 'Hello',
|
|
218
|
+
outputFormat: 'json',
|
|
219
|
+
});
|
|
220
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(JSON.stringify(mockQuery));
|
|
123
221
|
});
|
|
124
|
-
it('should
|
|
125
|
-
const
|
|
126
|
-
status: {
|
|
127
|
-
phase: 'canceled',
|
|
128
|
-
message: 'Query was canceled',
|
|
129
|
-
},
|
|
130
|
-
};
|
|
222
|
+
it('should output yaml format', async () => {
|
|
223
|
+
const mockYaml = 'apiVersion: ark.mckinsey.com/v1alpha1\nkind: Query';
|
|
131
224
|
mockExeca.mockImplementation(async (command, args) => {
|
|
132
225
|
if (args.includes('apply')) {
|
|
133
226
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
134
227
|
}
|
|
135
|
-
if (args.includes('
|
|
136
|
-
return {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
};
|
|
228
|
+
if (args.includes('wait')) {
|
|
229
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
230
|
+
}
|
|
231
|
+
if (args.includes('get') && args.includes('yaml')) {
|
|
232
|
+
return { stdout: mockYaml, stderr: '', exitCode: 0 };
|
|
141
233
|
}
|
|
142
234
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
143
235
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
catch (error) {
|
|
152
|
-
expect(error.message).toBe('process.exit called');
|
|
153
|
-
}
|
|
154
|
-
expect(mockSpinner.warn).toHaveBeenCalledWith('Query canceled');
|
|
155
|
-
expect(mockOutput.warning).toHaveBeenCalledWith('Query was canceled');
|
|
156
|
-
expect(mockExit).toHaveBeenCalledWith(ExitCodes.OperationError);
|
|
236
|
+
await executeQuery({
|
|
237
|
+
targetType: 'model',
|
|
238
|
+
targetName: 'default',
|
|
239
|
+
message: 'Hello',
|
|
240
|
+
outputFormat: 'yaml',
|
|
241
|
+
});
|
|
242
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(mockYaml);
|
|
157
243
|
});
|
|
158
|
-
it('should
|
|
244
|
+
it('should reject invalid output format', async () => {
|
|
159
245
|
mockExeca.mockImplementation(async (command, args) => {
|
|
160
246
|
if (args.includes('apply')) {
|
|
161
|
-
|
|
247
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
248
|
+
}
|
|
249
|
+
if (args.includes('wait')) {
|
|
250
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
162
251
|
}
|
|
163
252
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
164
253
|
});
|
|
@@ -166,38 +255,26 @@ describe('executeQuery', () => {
|
|
|
166
255
|
targetType: 'model',
|
|
167
256
|
targetName: 'default',
|
|
168
257
|
message: 'Hello',
|
|
258
|
+
outputFormat: 'invalid',
|
|
169
259
|
})).rejects.toThrow('process.exit called');
|
|
170
|
-
expect(
|
|
171
|
-
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Failed to apply'));
|
|
260
|
+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Invalid output format'));
|
|
172
261
|
expect(mockExit).toHaveBeenCalledWith(ExitCodes.CliError);
|
|
173
262
|
});
|
|
174
|
-
it('should handle
|
|
263
|
+
it('should handle kubectl errors', async () => {
|
|
175
264
|
mockExeca.mockImplementation(async (command, args) => {
|
|
176
265
|
if (args.includes('apply')) {
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
if (args.includes('wait')) {
|
|
180
|
-
// Simulate kubectl wait timeout
|
|
181
|
-
const error = new Error('timed out waiting for the condition');
|
|
182
|
-
throw error;
|
|
266
|
+
throw new Error('kubectl apply failed');
|
|
183
267
|
}
|
|
184
268
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
185
269
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
expect(error.message).toBe('process.exit called');
|
|
197
|
-
}
|
|
198
|
-
expect(mockSpinner.stop).toHaveBeenCalled();
|
|
199
|
-
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Query did not complete within 200ms'));
|
|
200
|
-
expect(mockExit).toHaveBeenCalledWith(ExitCodes.Timeout);
|
|
270
|
+
await expect(executeQuery({
|
|
271
|
+
targetType: 'model',
|
|
272
|
+
targetName: 'default',
|
|
273
|
+
message: 'Hello',
|
|
274
|
+
outputFormat: 'name',
|
|
275
|
+
})).rejects.toThrow('process.exit called');
|
|
276
|
+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('kubectl apply failed'));
|
|
277
|
+
expect(mockExit).toHaveBeenCalledWith(ExitCodes.CliError);
|
|
201
278
|
});
|
|
202
279
|
});
|
|
203
280
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface K8sResource {
|
|
2
|
+
metadata: {
|
|
3
|
+
name: string;
|
|
4
|
+
creationTimestamp?: string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export declare function getResource<T extends K8sResource>(resourceType: string, name: string): Promise<T>;
|
|
8
|
+
export declare function listResources<T extends K8sResource>(resourceType: string, options?: {
|
|
9
|
+
sortBy?: string;
|
|
10
|
+
}): Promise<T[]>;
|
|
11
|
+
export declare function deleteResource(resourceType: string, name?: string, options?: {
|
|
12
|
+
all?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
export declare function replaceResource<T extends K8sResource>(resource: T): Promise<T>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
export async function getResource(resourceType, name) {
|
|
3
|
+
if (name === '@latest') {
|
|
4
|
+
const result = await execa('kubectl', [
|
|
5
|
+
'get',
|
|
6
|
+
resourceType,
|
|
7
|
+
'--sort-by=.metadata.creationTimestamp',
|
|
8
|
+
'-o',
|
|
9
|
+
'json',
|
|
10
|
+
], { stdio: 'pipe' });
|
|
11
|
+
const data = JSON.parse(result.stdout);
|
|
12
|
+
const resources = data.items || [];
|
|
13
|
+
if (resources.length === 0) {
|
|
14
|
+
throw new Error(`No ${resourceType} found`);
|
|
15
|
+
}
|
|
16
|
+
return resources[resources.length - 1];
|
|
17
|
+
}
|
|
18
|
+
const result = await execa('kubectl', ['get', resourceType, name, '-o', 'json'], { stdio: 'pipe' });
|
|
19
|
+
return JSON.parse(result.stdout);
|
|
20
|
+
}
|
|
21
|
+
export async function listResources(resourceType, options) {
|
|
22
|
+
const args = ['get', resourceType];
|
|
23
|
+
if (options?.sortBy) {
|
|
24
|
+
args.push(`--sort-by=${options.sortBy}`);
|
|
25
|
+
}
|
|
26
|
+
args.push('-o', 'json');
|
|
27
|
+
const result = await execa('kubectl', args, { stdio: 'pipe' });
|
|
28
|
+
const data = JSON.parse(result.stdout);
|
|
29
|
+
return data.items || [];
|
|
30
|
+
}
|
|
31
|
+
export async function deleteResource(resourceType, name, options) {
|
|
32
|
+
const args = ['delete', resourceType];
|
|
33
|
+
if (options?.all) {
|
|
34
|
+
args.push('--all');
|
|
35
|
+
}
|
|
36
|
+
else if (name) {
|
|
37
|
+
args.push(name);
|
|
38
|
+
}
|
|
39
|
+
await execa('kubectl', args, { stdio: 'pipe' });
|
|
40
|
+
}
|
|
41
|
+
export async function replaceResource(resource) {
|
|
42
|
+
const result = await execa('kubectl', ['replace', '-f', '-', '-o', 'json'], {
|
|
43
|
+
input: JSON.stringify(resource),
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
});
|
|
46
|
+
return JSON.parse(result.stdout);
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|