@agents-at-scale/ark 0.1.42 → 0.1.44

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.
Files changed (51) hide show
  1. package/dist/arkServices.js +12 -18
  2. package/dist/commands/completion/index.js +38 -3
  3. package/dist/commands/evaluation/index.spec.js +1 -6
  4. package/dist/commands/generate/generators/project.js +3 -3
  5. package/dist/commands/generate/generators/team.js +4 -1
  6. package/dist/commands/generate/index.js +2 -2
  7. package/dist/commands/install/index.js +27 -0
  8. package/dist/commands/marketplace/index.d.ts +4 -0
  9. package/dist/commands/marketplace/index.js +50 -0
  10. package/dist/commands/models/create.js +1 -1
  11. package/dist/commands/models/create.spec.js +6 -2
  12. package/dist/commands/models/providers/azure.spec.js +3 -1
  13. package/dist/commands/queries/delete.d.ts +7 -0
  14. package/dist/commands/queries/delete.js +24 -0
  15. package/dist/commands/queries/delete.spec.d.ts +1 -0
  16. package/dist/commands/queries/delete.spec.js +74 -0
  17. package/dist/commands/queries/index.js +42 -4
  18. package/dist/commands/queries/list.d.ts +6 -0
  19. package/dist/commands/queries/list.js +66 -0
  20. package/dist/commands/queries/list.spec.d.ts +1 -0
  21. package/dist/commands/queries/list.spec.js +170 -0
  22. package/dist/commands/queries/validation.d.ts +2 -0
  23. package/dist/commands/queries/validation.js +10 -0
  24. package/dist/commands/queries/validation.spec.d.ts +1 -0
  25. package/dist/commands/queries/validation.spec.js +27 -0
  26. package/dist/commands/query/index.js +2 -0
  27. package/dist/commands/query/index.spec.js +24 -0
  28. package/dist/commands/uninstall/index.js +27 -0
  29. package/dist/components/ChatUI.js +14 -4
  30. package/dist/index.js +2 -0
  31. package/dist/lib/arkApiClient.js +2 -0
  32. package/dist/lib/chatClient.d.ts +4 -0
  33. package/dist/lib/chatClient.js +23 -7
  34. package/dist/lib/chatClient.spec.d.ts +1 -0
  35. package/dist/lib/chatClient.spec.js +108 -0
  36. package/dist/lib/constants.d.ts +3 -0
  37. package/dist/lib/constants.js +8 -0
  38. package/dist/lib/executeQuery.d.ts +1 -4
  39. package/dist/lib/executeQuery.js +103 -104
  40. package/dist/lib/executeQuery.spec.js +218 -99
  41. package/dist/lib/kubectl.d.ts +7 -0
  42. package/dist/lib/kubectl.js +27 -0
  43. package/dist/lib/kubectl.spec.js +89 -1
  44. package/dist/lib/types.d.ts +22 -7
  45. package/dist/marketplaceServices.d.ts +15 -0
  46. package/dist/marketplaceServices.js +51 -0
  47. package/package.json +1 -1
  48. package/templates/models/azure.yaml +1 -1
  49. package/templates/project/Makefile +1 -1
  50. package/templates/project/README.md +1 -1
  51. package/templates/project/scripts/setup.sh +2 -2
@@ -4,24 +4,93 @@
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 { parseDuration } from './duration.js';
10
- import { getResource } from './kubectl.js';
11
- /**
12
- * Execute a query against any ARK target (model, agent, team)
13
- * This is the shared implementation used by all query commands
14
- */
8
+ import { ArkApiProxy } from './arkApiProxy.js';
9
+ import { ChatClient } from './chatClient.js';
15
10
  export async function executeQuery(options) {
16
- const spinner = options.outputFormat
17
- ? null
18
- : ora('Creating query...').start();
19
- const queryTimeoutMs = options.timeout
20
- ? parseDuration(options.timeout)
21
- : parseDuration('5m');
22
- const watchTimeoutMs = options.watchTimeout
23
- ? parseDuration(options.watchTimeout)
24
- : queryTimeoutMs + 60000;
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
+ // Get sessionId from option or environment variable
31
+ const sessionId = options.sessionId || process.env.ARK_SESSION_ID;
32
+ await chatClient.sendMessage(targetId, messages, { streamingEnabled: true, sessionId }, (chunk, toolCalls, arkMetadata) => {
33
+ if (firstOutput) {
34
+ spinner.stop();
35
+ firstOutput = false;
36
+ }
37
+ const agentName = arkMetadata?.agent || arkMetadata?.team;
38
+ if (agentName && agentName !== lastAgentName) {
39
+ if (lastAgentName) {
40
+ if (state.content) {
41
+ process.stdout.write('\n');
42
+ }
43
+ process.stdout.write('\n');
44
+ }
45
+ const prefix = arkMetadata?.team ? '◆' : '●';
46
+ const color = arkMetadata?.team ? 'green' : 'cyan';
47
+ process.stdout.write(chalk[color](`${prefix} ${agentName}\n`));
48
+ lastAgentName = agentName;
49
+ state.content = '';
50
+ state.toolCalls.clear();
51
+ headerShown = true;
52
+ }
53
+ if (toolCalls && toolCalls.length > 0) {
54
+ for (const toolCall of toolCalls) {
55
+ if (!state.toolCalls.has(toolCall.id)) {
56
+ state.toolCalls.set(toolCall.id, toolCall);
57
+ if (state.content) {
58
+ process.stdout.write('\n');
59
+ }
60
+ process.stdout.write(chalk.magenta(`🔧 ${toolCall.function.name}\n`));
61
+ }
62
+ }
63
+ }
64
+ if (chunk) {
65
+ if (state.toolCalls.size > 0 && !state.content) {
66
+ process.stdout.write('\n');
67
+ }
68
+ process.stdout.write(chunk);
69
+ state.content += chunk;
70
+ }
71
+ });
72
+ if (spinner.isSpinning) {
73
+ spinner.stop();
74
+ }
75
+ if ((state.content || state.toolCalls.size > 0) && headerShown) {
76
+ process.stdout.write('\n');
77
+ }
78
+ if (arkApiProxy) {
79
+ arkApiProxy.stop();
80
+ }
81
+ }
82
+ catch (error) {
83
+ if (spinner.isSpinning) {
84
+ spinner.stop();
85
+ }
86
+ if (arkApiProxy) {
87
+ arkApiProxy.stop();
88
+ }
89
+ console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
90
+ process.exit(ExitCodes.CliError);
91
+ }
92
+ }
93
+ async function executeQueryWithFormat(options) {
25
94
  const timestamp = Date.now();
26
95
  const queryName = `cli-query-${timestamp}`;
27
96
  const queryManifest = {
@@ -33,6 +102,9 @@ export async function executeQuery(options) {
33
102
  spec: {
34
103
  input: options.message,
35
104
  ...(options.timeout && { timeout: options.timeout }),
105
+ ...((options.sessionId || process.env.ARK_SESSION_ID) && {
106
+ sessionId: options.sessionId || process.env.ARK_SESSION_ID,
107
+ }),
36
108
  targets: [
37
109
  {
38
110
  type: options.targetType,
@@ -42,104 +114,31 @@ export async function executeQuery(options) {
42
114
  },
43
115
  };
44
116
  try {
45
- // Apply the query
46
- if (spinner)
47
- spinner.text = 'Submitting query...';
48
117
  await execa('kubectl', ['apply', '-f', '-'], {
49
118
  input: JSON.stringify(queryManifest),
50
119
  stdio: ['pipe', 'pipe', 'pipe'],
51
120
  });
52
- // Watch for query completion using kubectl wait
53
- if (spinner)
54
- spinner.text = 'Waiting for query completion...';
55
- try {
56
- await execa('kubectl', [
57
- 'wait',
58
- '--for=condition=Completed',
59
- `query/${queryName}`,
60
- `--timeout=${Math.floor(watchTimeoutMs / 1000)}s`,
61
- ], { timeout: watchTimeoutMs });
62
- }
63
- catch (error) {
64
- if (spinner)
65
- spinner.stop();
66
- // Check if it's a timeout or other error
67
- if (error instanceof Error &&
68
- error.message.includes('timed out waiting')) {
69
- console.error(chalk.red(`Query did not complete within ${options.watchTimeout ?? `${Math.floor(watchTimeoutMs / 1000)}s`}`));
70
- process.exit(ExitCodes.Timeout);
71
- }
72
- // For other errors, fetch the query to check status
73
- }
74
- if (spinner)
75
- spinner.stop();
76
- // If output format is specified, output the resource and return
77
- if (options.outputFormat) {
78
- try {
79
- if (options.outputFormat === 'name') {
80
- console.log(queryName);
81
- }
82
- else if (options.outputFormat === 'json' ||
83
- options.outputFormat === 'yaml') {
84
- const { stdout } = await execa('kubectl', ['get', 'query', queryName, '-o', options.outputFormat], { stdio: 'pipe' });
85
- console.log(stdout);
86
- }
87
- else {
88
- console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, or name`));
89
- process.exit(ExitCodes.CliError);
90
- }
91
- return;
92
- }
93
- catch (error) {
94
- console.error(chalk.red(error instanceof Error
95
- ? error.message
96
- : 'Failed to fetch query resource'));
97
- process.exit(ExitCodes.CliError);
98
- }
121
+ const timeoutSeconds = 300;
122
+ await execa('kubectl', [
123
+ 'wait',
124
+ '--for=condition=Completed',
125
+ `query/${queryName}`,
126
+ `--timeout=${timeoutSeconds}s`,
127
+ ], { timeout: timeoutSeconds * 1000 });
128
+ if (options.outputFormat === 'name') {
129
+ console.log(queryName);
99
130
  }
100
- // Fetch final query state
101
- try {
102
- const query = await getResource('queries', queryName);
103
- const phase = query.status?.phase;
104
- // Check if query completed successfully or with error
105
- if (phase === 'done') {
106
- // Extract and display the response from responses array
107
- if (query.status?.responses && query.status.responses.length > 0) {
108
- const response = query.status.responses[0];
109
- console.log(response.content || response);
110
- }
111
- else {
112
- output.warning('No response received');
113
- }
114
- }
115
- else if (phase === 'error') {
116
- const response = query.status?.responses?.[0];
117
- console.error(chalk.red(response?.content || 'Query failed with unknown error'));
118
- process.exit(ExitCodes.OperationError);
119
- }
120
- else if (phase === 'canceled') {
121
- if (spinner) {
122
- spinner.warn('Query canceled');
123
- }
124
- else {
125
- output.warning('Query canceled');
126
- }
127
- if (query.status?.message) {
128
- output.warning(query.status.message);
129
- }
130
- process.exit(ExitCodes.OperationError);
131
- }
131
+ else if (options.outputFormat === 'json' ||
132
+ options.outputFormat === 'yaml') {
133
+ const { stdout } = await execa('kubectl', ['get', 'query', queryName, '-o', options.outputFormat], { stdio: 'pipe' });
134
+ console.log(stdout);
132
135
  }
133
- catch (error) {
134
- console.error(chalk.red(error instanceof Error
135
- ? error.message
136
- : 'Failed to fetch query result'));
136
+ else {
137
+ console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, or name`));
137
138
  process.exit(ExitCodes.CliError);
138
139
  }
139
140
  }
140
141
  catch (error) {
141
- if (spinner)
142
- spinner.stop();
143
142
  console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
144
143
  process.exit(ExitCodes.CliError);
145
144
  }
@@ -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
- const mockOutput = {
19
- warning: jest.fn(),
20
- error: jest.fn(),
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.unstable_mockModule('./output.js', () => ({
23
- default: mockOutput,
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,119 @@ describe('executeQuery', () => {
57
79
  expect(parseTarget('model/default/extra')).toBeNull();
58
80
  });
59
81
  });
60
- describe('executeQuery', () => {
61
- it('should create and apply a query manifest', async () => {
62
- const mockQueryResponse = {
63
- status: {
64
- phase: 'done',
65
- responses: [{ content: 'Test response' }],
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
+ });
98
+ it('should pass sessionId to sendMessage when provided', async () => {
99
+ mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
100
+ callback('Hello', undefined, { agent: 'test-agent' });
101
+ });
102
+ await executeQuery({
103
+ targetType: 'model',
104
+ targetName: 'default',
105
+ message: 'Hello',
106
+ sessionId: 'test-session-123',
107
+ });
108
+ expect(mockSendMessage).toHaveBeenCalledWith('model/default', [{ role: 'user', content: 'Hello' }], { streamingEnabled: true, sessionId: 'test-session-123' }, expect.any(Function));
109
+ expect(mockSpinner.stop).toHaveBeenCalled();
110
+ expect(mockArkApiProxyInstance.stop).toHaveBeenCalled();
111
+ expect(mockStdoutWrite).toHaveBeenCalled();
112
+ });
113
+ it('should display agent names with correct formatting', async () => {
114
+ mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
115
+ callback('Response 1', undefined, { agent: 'agent-1' });
116
+ callback('Response 2', undefined, { agent: 'agent-2' });
117
+ });
118
+ await executeQuery({
119
+ targetType: 'agent',
120
+ targetName: 'test-agent',
121
+ message: 'Hello',
122
+ });
123
+ expect(mockStdoutWrite).toHaveBeenCalled();
124
+ const calls = mockStdoutWrite.mock.calls.map((call) => String(call[0]));
125
+ expect(calls.some((call) => call.includes('agent-1'))).toBe(true);
126
+ expect(calls.some((call) => call.includes('agent-2'))).toBe(true);
127
+ });
128
+ it('should display team names with diamond prefix', async () => {
129
+ mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
130
+ callback('Team response', undefined, { team: 'my-team' });
131
+ });
132
+ await executeQuery({
133
+ targetType: 'team',
134
+ targetName: 'my-team',
135
+ message: 'Hello',
136
+ });
137
+ const calls = mockStdoutWrite.mock.calls.map((call) => String(call[0]));
138
+ expect(calls.some((call) => call.includes('◆'))).toBe(true);
139
+ expect(calls.some((call) => call.includes('my-team'))).toBe(true);
140
+ });
141
+ it('should display tool calls', async () => {
142
+ mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
143
+ callback('', [{ id: 1, function: { name: 'get_weather' } }], {
144
+ agent: 'weather-agent',
145
+ });
146
+ callback('The weather is sunny', undefined, {
147
+ agent: 'weather-agent',
148
+ });
149
+ });
150
+ await executeQuery({
151
+ targetType: 'agent',
152
+ targetName: 'weather-agent',
153
+ message: 'What is the weather?',
154
+ });
155
+ const calls = mockStdoutWrite.mock.calls.map((call) => String(call[0]));
156
+ expect(calls.some((call) => call.includes('get_weather'))).toBe(true);
157
+ expect(calls.some((call) => call.includes('The weather is sunny'))).toBe(true);
158
+ });
159
+ it('should handle errors and exit with CliError', async () => {
160
+ mockSpinner.isSpinning = true;
161
+ const startMock = jest.fn();
162
+ startMock.mockRejectedValue(new Error('Connection failed'));
163
+ mockArkApiProxyInstance.start = startMock;
164
+ await expect(executeQuery({
165
+ targetType: 'model',
166
+ targetName: 'default',
167
+ message: 'Hello',
168
+ })).rejects.toThrow('process.exit called');
169
+ expect(mockSpinner.stop).toHaveBeenCalled();
170
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Connection failed'));
171
+ expect(mockExit).toHaveBeenCalledWith(ExitCodes.CliError);
172
+ expect(mockArkApiProxyInstance.stop).toHaveBeenCalled();
173
+ });
174
+ it('should stop spinner when first output arrives', async () => {
175
+ mockSpinner.isSpinning = true;
176
+ mockSendMessage.mockImplementation(async (targetId, messages, options, callback) => {
177
+ callback('First chunk', undefined, { agent: 'test-agent' });
178
+ });
179
+ await executeQuery({
180
+ targetType: 'model',
181
+ targetName: 'default',
182
+ message: 'Hello',
183
+ });
184
+ expect(mockSpinner.stop).toHaveBeenCalled();
185
+ });
186
+ });
187
+ describe('executeQuery with output format', () => {
188
+ it('should create query and output name format', async () => {
68
189
  mockExeca.mockImplementation(async (command, args) => {
69
190
  if (args.includes('apply')) {
70
191
  return { stdout: '', stderr: '', exitCode: 0 };
71
192
  }
72
- if (args.includes('get') && args.includes('queries')) {
73
- return {
74
- stdout: JSON.stringify(mockQueryResponse),
75
- stderr: '',
76
- exitCode: 0,
77
- };
193
+ if (args.includes('wait')) {
194
+ return { stdout: '', stderr: '', exitCode: 0 };
78
195
  }
79
196
  return { stdout: '', stderr: '', exitCode: 0 };
80
197
  });
@@ -82,83 +199,97 @@ describe('executeQuery', () => {
82
199
  targetType: 'model',
83
200
  targetName: 'default',
84
201
  message: 'Hello',
202
+ outputFormat: 'name',
85
203
  });
86
- expect(mockSpinner.start).toHaveBeenCalled();
87
- expect(mockSpinner.stop).toHaveBeenCalled();
88
- expect(mockConsoleLog).toHaveBeenCalledWith('Test response');
204
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', expect.arrayContaining(['apply', '-f', '-']), expect.any(Object));
205
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', expect.arrayContaining(['wait', '--for=condition=Completed']), expect.any(Object));
206
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringMatching(/cli-query-\d+/));
89
207
  });
90
- it('should handle query error phase and exit with code 2', async () => {
91
- const mockQueryResponse = {
92
- status: {
93
- phase: 'error',
94
- responses: [{ content: 'Query failed with test error' }],
95
- },
208
+ it('should include sessionId in query manifest when outputFormat is specified', async () => {
209
+ let appliedManifest = '';
210
+ mockExeca.mockImplementation(async (command, args) => {
211
+ if (args.includes('apply') && args.includes('-f') && args.includes('-')) {
212
+ // Capture the stdin input
213
+ const stdinIndex = args.indexOf('-');
214
+ if (stdinIndex >= 0 && args[stdinIndex + 1]) {
215
+ appliedManifest = args[stdinIndex + 1];
216
+ }
217
+ return { stdout: '', stderr: '', exitCode: 0 };
218
+ }
219
+ if (args.includes('wait')) {
220
+ return { stdout: '', stderr: '', exitCode: 0 };
221
+ }
222
+ return { stdout: '', stderr: '', exitCode: 0 };
223
+ });
224
+ await executeQuery({
225
+ targetType: 'model',
226
+ targetName: 'default',
227
+ message: 'Hello',
228
+ outputFormat: 'name',
229
+ sessionId: 'test-session-456',
230
+ });
231
+ // Check that the manifest includes sessionId in spec
232
+ const applyCall = mockExeca.mock.calls.find((call) => call[1]?.includes('apply'));
233
+ expect(applyCall).toBeDefined();
234
+ // The manifest should be passed via stdin, so we need to check the actual call
235
+ // Since execa handles stdin separately, we verify the call was made
236
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', expect.arrayContaining(['apply', '-f', '-']), expect.any(Object));
237
+ });
238
+ it('should output json format', async () => {
239
+ const mockQuery = {
240
+ apiVersion: 'ark.mckinsey.com/v1alpha1',
241
+ kind: 'Query',
242
+ metadata: { name: 'test-query' },
96
243
  };
97
244
  mockExeca.mockImplementation(async (command, args) => {
98
245
  if (args.includes('apply')) {
99
246
  return { stdout: '', stderr: '', exitCode: 0 };
100
247
  }
101
- if (args.includes('get') && args.includes('queries')) {
102
- return {
103
- stdout: JSON.stringify(mockQueryResponse),
104
- stderr: '',
105
- exitCode: 0,
106
- };
248
+ if (args.includes('wait')) {
249
+ return { stdout: '', stderr: '', exitCode: 0 };
250
+ }
251
+ if (args.includes('get') && args.includes('-o')) {
252
+ return { stdout: JSON.stringify(mockQuery), stderr: '', exitCode: 0 };
107
253
  }
108
254
  return { stdout: '', stderr: '', exitCode: 0 };
109
255
  });
110
- try {
111
- await executeQuery({
112
- targetType: 'model',
113
- targetName: 'default',
114
- message: 'Hello',
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);
256
+ await executeQuery({
257
+ targetType: 'model',
258
+ targetName: 'default',
259
+ message: 'Hello',
260
+ outputFormat: 'json',
261
+ });
262
+ expect(mockConsoleLog).toHaveBeenCalledWith(JSON.stringify(mockQuery));
123
263
  });
124
- it('should handle query canceled phase and exit with code 2', async () => {
125
- const mockQueryResponse = {
126
- status: {
127
- phase: 'canceled',
128
- message: 'Query was canceled',
129
- },
130
- };
264
+ it('should output yaml format', async () => {
265
+ const mockYaml = 'apiVersion: ark.mckinsey.com/v1alpha1\nkind: Query';
131
266
  mockExeca.mockImplementation(async (command, args) => {
132
267
  if (args.includes('apply')) {
133
268
  return { stdout: '', stderr: '', exitCode: 0 };
134
269
  }
135
- if (args.includes('get') && args.includes('queries')) {
136
- return {
137
- stdout: JSON.stringify(mockQueryResponse),
138
- stderr: '',
139
- exitCode: 0,
140
- };
270
+ if (args.includes('wait')) {
271
+ return { stdout: '', stderr: '', exitCode: 0 };
272
+ }
273
+ if (args.includes('get') && args.includes('yaml')) {
274
+ return { stdout: mockYaml, stderr: '', exitCode: 0 };
141
275
  }
142
276
  return { stdout: '', stderr: '', exitCode: 0 };
143
277
  });
144
- try {
145
- await executeQuery({
146
- targetType: 'agent',
147
- targetName: 'test-agent',
148
- message: 'Hello',
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);
278
+ await executeQuery({
279
+ targetType: 'model',
280
+ targetName: 'default',
281
+ message: 'Hello',
282
+ outputFormat: 'yaml',
283
+ });
284
+ expect(mockConsoleLog).toHaveBeenCalledWith(mockYaml);
157
285
  });
158
- it('should handle kubectl apply failures with exit code 1', async () => {
286
+ it('should reject invalid output format', async () => {
159
287
  mockExeca.mockImplementation(async (command, args) => {
160
288
  if (args.includes('apply')) {
161
- throw new Error('Failed to apply');
289
+ return { stdout: '', stderr: '', exitCode: 0 };
290
+ }
291
+ if (args.includes('wait')) {
292
+ return { stdout: '', stderr: '', exitCode: 0 };
162
293
  }
163
294
  return { stdout: '', stderr: '', exitCode: 0 };
164
295
  });
@@ -166,38 +297,26 @@ describe('executeQuery', () => {
166
297
  targetType: 'model',
167
298
  targetName: 'default',
168
299
  message: 'Hello',
300
+ outputFormat: 'invalid',
169
301
  })).rejects.toThrow('process.exit called');
170
- expect(mockSpinner.stop).toHaveBeenCalled();
171
- expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Failed to apply'));
302
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Invalid output format'));
172
303
  expect(mockExit).toHaveBeenCalledWith(ExitCodes.CliError);
173
304
  });
174
- it('should handle query timeout and exit with code 3', async () => {
305
+ it('should handle kubectl errors', async () => {
175
306
  mockExeca.mockImplementation(async (command, args) => {
176
307
  if (args.includes('apply')) {
177
- return { stdout: '', stderr: '', exitCode: 0 };
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;
308
+ throw new Error('kubectl apply failed');
183
309
  }
184
310
  return { stdout: '', stderr: '', exitCode: 0 };
185
311
  });
186
- try {
187
- await executeQuery({
188
- targetType: 'model',
189
- targetName: 'default',
190
- message: 'Hello',
191
- timeout: '100ms',
192
- watchTimeout: '200ms',
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);
312
+ await expect(executeQuery({
313
+ targetType: 'model',
314
+ targetName: 'default',
315
+ message: 'Hello',
316
+ outputFormat: 'name',
317
+ })).rejects.toThrow('process.exit called');
318
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('kubectl apply failed'));
319
+ expect(mockExit).toHaveBeenCalledWith(ExitCodes.CliError);
201
320
  });
202
321
  });
203
322
  });
@@ -5,4 +5,11 @@ interface K8sResource {
5
5
  };
6
6
  }
7
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>;
8
15
  export {};