@agents-at-scale/ark 0.1.45 → 0.1.47
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/README.md +2 -0
- package/dist/arkServices.js +13 -1
- package/dist/arkServices.spec.js +6 -0
- package/dist/commands/agents/index.d.ts +1 -1
- package/dist/commands/agents/index.js +4 -2
- package/dist/commands/completion/index.js +2 -4
- package/dist/commands/install/index.js +20 -10
- package/dist/commands/marketplace/index.js +51 -23
- package/dist/commands/marketplace/index.spec.d.ts +1 -0
- package/dist/commands/marketplace/index.spec.js +88 -0
- package/dist/commands/models/index.d.ts +1 -1
- package/dist/commands/models/index.js +4 -2
- package/dist/commands/query/index.d.ts +1 -1
- package/dist/commands/query/index.js +4 -2
- package/dist/commands/status/index.js +7 -2
- package/dist/commands/teams/index.d.ts +1 -1
- package/dist/commands/teams/index.js +4 -2
- package/dist/commands/uninstall/index.js +20 -10
- package/dist/lib/chatClient.d.ts +1 -0
- package/dist/lib/chatClient.js +4 -2
- package/dist/lib/config.d.ts +14 -0
- package/dist/lib/config.js +41 -0
- package/dist/lib/config.spec.js +93 -0
- package/dist/lib/constants.d.ts +3 -0
- package/dist/lib/constants.js +5 -0
- package/dist/lib/executeQuery.js +9 -3
- package/dist/lib/executeQuery.spec.js +4 -1
- package/dist/lib/kubectl.d.ts +1 -0
- package/dist/lib/kubectl.js +62 -0
- package/dist/lib/marketplaceFetcher.d.ts +6 -0
- package/dist/lib/marketplaceFetcher.js +80 -0
- package/dist/lib/marketplaceFetcher.spec.d.ts +1 -0
- package/dist/lib/marketplaceFetcher.spec.js +225 -0
- package/dist/marketplaceServices.d.ts +15 -6
- package/dist/marketplaceServices.js +38 -40
- package/dist/marketplaceServices.spec.d.ts +1 -0
- package/dist/marketplaceServices.spec.js +74 -0
- package/dist/types/marketplace.d.ts +37 -0
- package/dist/types/marketplace.js +1 -0
- package/dist/ui/AgentSelector.d.ts +8 -0
- package/dist/ui/AgentSelector.js +53 -0
- 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/package.json +1 -1
- package/templates/marketplace/marketplace.json.example +59 -0
package/dist/lib/config.spec.js
CHANGED
|
@@ -35,6 +35,10 @@ describe('config', () => {
|
|
|
35
35
|
streaming: true,
|
|
36
36
|
outputFormat: 'text',
|
|
37
37
|
},
|
|
38
|
+
marketplace: {
|
|
39
|
+
repoUrl: 'https://github.com/mckinsey/agents-at-scale-marketplace',
|
|
40
|
+
registry: 'oci://ghcr.io/mckinsey/agents-at-scale-marketplace/charts',
|
|
41
|
+
},
|
|
38
42
|
});
|
|
39
43
|
});
|
|
40
44
|
it('loads and merges configs in order: defaults, user, project', () => {
|
|
@@ -66,6 +70,21 @@ describe('config', () => {
|
|
|
66
70
|
expect(config.chat?.streaming).toBe(true);
|
|
67
71
|
expect(config.chat?.outputFormat).toBe('markdown');
|
|
68
72
|
});
|
|
73
|
+
it('loads queryTimeout from config file', () => {
|
|
74
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
75
|
+
mockFs.readFileSync.mockReturnValue('yaml');
|
|
76
|
+
mockYaml.parse.mockReturnValue({ queryTimeout: '30m' });
|
|
77
|
+
const config = loadConfig();
|
|
78
|
+
expect(config.queryTimeout).toBe('30m');
|
|
79
|
+
});
|
|
80
|
+
it('ARK_QUERY_TIMEOUT environment variable overrides config', () => {
|
|
81
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
82
|
+
mockFs.readFileSync.mockReturnValue('yaml');
|
|
83
|
+
mockYaml.parse.mockReturnValue({ queryTimeout: '5m' });
|
|
84
|
+
process.env.ARK_QUERY_TIMEOUT = '1h';
|
|
85
|
+
const config = loadConfig();
|
|
86
|
+
expect(config.queryTimeout).toBe('1h');
|
|
87
|
+
});
|
|
69
88
|
it('throws error for invalid YAML', () => {
|
|
70
89
|
const userConfigPath = path.join(os.homedir(), '.arkrc.yaml');
|
|
71
90
|
mockFs.existsSync.mockImplementation((path) => path === userConfigPath);
|
|
@@ -96,4 +115,78 @@ describe('config', () => {
|
|
|
96
115
|
expect(mockYaml.stringify).toHaveBeenCalledWith(config);
|
|
97
116
|
expect(result).toBe('formatted');
|
|
98
117
|
});
|
|
118
|
+
it('loads marketplace config from config file', () => {
|
|
119
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
120
|
+
mockFs.readFileSync.mockReturnValue('yaml');
|
|
121
|
+
mockYaml.parse.mockReturnValue({
|
|
122
|
+
marketplace: {
|
|
123
|
+
repoUrl: 'https://example.com/my-marketplace',
|
|
124
|
+
registry: 'oci://example.com/charts',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const config = loadConfig();
|
|
128
|
+
expect(config.marketplace?.repoUrl).toBe('https://example.com/my-marketplace');
|
|
129
|
+
expect(config.marketplace?.registry).toBe('oci://example.com/charts');
|
|
130
|
+
});
|
|
131
|
+
it('marketplace environment variables override config', () => {
|
|
132
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
133
|
+
mockFs.readFileSync.mockReturnValue('yaml');
|
|
134
|
+
mockYaml.parse.mockReturnValue({
|
|
135
|
+
marketplace: {
|
|
136
|
+
repoUrl: 'https://example.com/my-marketplace',
|
|
137
|
+
registry: 'oci://example.com/charts',
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
process.env.ARK_MARKETPLACE_REPO_URL = 'https://custom.com/marketplace';
|
|
141
|
+
process.env.ARK_MARKETPLACE_REGISTRY = 'oci://custom.com/charts';
|
|
142
|
+
const config = loadConfig();
|
|
143
|
+
expect(config.marketplace?.repoUrl).toBe('https://custom.com/marketplace');
|
|
144
|
+
expect(config.marketplace?.registry).toBe('oci://custom.com/charts');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('marketplace helpers', () => {
|
|
148
|
+
const originalEnv = process.env;
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
jest.clearAllMocks();
|
|
151
|
+
process.env = { ...originalEnv };
|
|
152
|
+
});
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
process.env = originalEnv;
|
|
155
|
+
});
|
|
156
|
+
it('getMarketplaceRepoUrl returns custom config value', async () => {
|
|
157
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
158
|
+
mockFs.readFileSync.mockReturnValue('yaml');
|
|
159
|
+
mockYaml.parse.mockReturnValue({
|
|
160
|
+
marketplace: {
|
|
161
|
+
repoUrl: 'https://custom-repo.com/marketplace',
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
jest.resetModules();
|
|
165
|
+
const { getMarketplaceRepoUrl } = await import('./config.js');
|
|
166
|
+
expect(getMarketplaceRepoUrl()).toBe('https://custom-repo.com/marketplace');
|
|
167
|
+
});
|
|
168
|
+
it('getMarketplaceRepoUrl returns default value', async () => {
|
|
169
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
170
|
+
jest.resetModules();
|
|
171
|
+
const { getMarketplaceRepoUrl } = await import('./config.js');
|
|
172
|
+
expect(getMarketplaceRepoUrl()).toBe('https://github.com/mckinsey/agents-at-scale-marketplace');
|
|
173
|
+
});
|
|
174
|
+
it('getMarketplaceRegistry returns custom config value', async () => {
|
|
175
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
176
|
+
mockFs.readFileSync.mockReturnValue('yaml');
|
|
177
|
+
mockYaml.parse.mockReturnValue({
|
|
178
|
+
marketplace: {
|
|
179
|
+
registry: 'oci://custom-registry.com/charts',
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
jest.resetModules();
|
|
183
|
+
const { getMarketplaceRegistry } = await import('./config.js');
|
|
184
|
+
expect(getMarketplaceRegistry()).toBe('oci://custom-registry.com/charts');
|
|
185
|
+
});
|
|
186
|
+
it('getMarketplaceRegistry returns default value', async () => {
|
|
187
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
188
|
+
jest.resetModules();
|
|
189
|
+
const { getMarketplaceRegistry } = await import('./config.js');
|
|
190
|
+
expect(getMarketplaceRegistry()).toBe('oci://ghcr.io/mckinsey/agents-at-scale-marketplace/charts');
|
|
191
|
+
});
|
|
99
192
|
});
|
package/dist/lib/constants.d.ts
CHANGED
package/dist/lib/constants.js
CHANGED
|
@@ -6,3 +6,8 @@ export const QUERY_ANNOTATIONS = {
|
|
|
6
6
|
// A2A context ID annotation (goes to K8s annotations)
|
|
7
7
|
A2A_CONTEXT_ID: `${ARK_PREFIX}a2a-context-id`,
|
|
8
8
|
};
|
|
9
|
+
// Event annotation constants
|
|
10
|
+
export const EVENT_ANNOTATIONS = {
|
|
11
|
+
// Event data annotation for structured event data
|
|
12
|
+
EVENT_DATA: `${ARK_PREFIX}event-data`,
|
|
13
|
+
};
|
package/dist/lib/executeQuery.js
CHANGED
|
@@ -7,6 +7,7 @@ import chalk from 'chalk';
|
|
|
7
7
|
import { ExitCodes } from './errors.js';
|
|
8
8
|
import { ArkApiProxy } from './arkApiProxy.js';
|
|
9
9
|
import { ChatClient } from './chatClient.js';
|
|
10
|
+
import { watchEventsLive } from './kubectl.js';
|
|
10
11
|
export async function executeQuery(options) {
|
|
11
12
|
if (options.outputFormat) {
|
|
12
13
|
return executeQueryWithFormat(options);
|
|
@@ -27,9 +28,8 @@ export async function executeQuery(options) {
|
|
|
27
28
|
let lastAgentName;
|
|
28
29
|
let headerShown = false;
|
|
29
30
|
let firstOutput = true;
|
|
30
|
-
// Get sessionId from option or environment variable
|
|
31
31
|
const sessionId = options.sessionId || process.env.ARK_SESSION_ID;
|
|
32
|
-
await chatClient.sendMessage(targetId, messages, { streamingEnabled: true, sessionId }, (chunk, toolCalls, arkMetadata) => {
|
|
32
|
+
await chatClient.sendMessage(targetId, messages, { streamingEnabled: true, sessionId, queryTimeout: options.timeout }, (chunk, toolCalls, arkMetadata) => {
|
|
33
33
|
if (firstOutput) {
|
|
34
34
|
spinner.stop();
|
|
35
35
|
firstOutput = false;
|
|
@@ -118,6 +118,12 @@ async function executeQueryWithFormat(options) {
|
|
|
118
118
|
input: JSON.stringify(queryManifest),
|
|
119
119
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
120
120
|
});
|
|
121
|
+
// Give Kubernetes a moment to process the resource before watching
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
123
|
+
if (options.outputFormat === 'events') {
|
|
124
|
+
await watchEventsLive(queryName);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
121
127
|
const timeoutSeconds = 300;
|
|
122
128
|
await execa('kubectl', [
|
|
123
129
|
'wait',
|
|
@@ -134,7 +140,7 @@ async function executeQueryWithFormat(options) {
|
|
|
134
140
|
console.log(stdout);
|
|
135
141
|
}
|
|
136
142
|
else {
|
|
137
|
-
console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, or
|
|
143
|
+
console.error(chalk.red(`Invalid output format: ${options.outputFormat}. Use: yaml, json, name, or events`));
|
|
138
144
|
process.exit(ExitCodes.CliError);
|
|
139
145
|
}
|
|
140
146
|
}
|
|
@@ -206,9 +206,12 @@ describe('executeQuery', () => {
|
|
|
206
206
|
expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringMatching(/cli-query-\d+/));
|
|
207
207
|
});
|
|
208
208
|
it('should include sessionId in query manifest when outputFormat is specified', async () => {
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
209
210
|
let appliedManifest = '';
|
|
210
211
|
mockExeca.mockImplementation(async (command, args) => {
|
|
211
|
-
if (args.includes('apply') &&
|
|
212
|
+
if (args.includes('apply') &&
|
|
213
|
+
args.includes('-f') &&
|
|
214
|
+
args.includes('-')) {
|
|
212
215
|
// Capture the stdin input
|
|
213
216
|
const stdinIndex = args.indexOf('-');
|
|
214
217
|
if (stdinIndex >= 0 && args[stdinIndex + 1]) {
|
package/dist/lib/kubectl.d.ts
CHANGED
|
@@ -12,4 +12,5 @@ export declare function deleteResource(resourceType: string, name?: string, opti
|
|
|
12
12
|
all?: boolean;
|
|
13
13
|
}): Promise<void>;
|
|
14
14
|
export declare function replaceResource<T extends K8sResource>(resource: T): Promise<T>;
|
|
15
|
+
export declare function watchEventsLive(queryName: string): Promise<void>;
|
|
15
16
|
export {};
|
package/dist/lib/kubectl.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { EVENT_ANNOTATIONS } from './constants.js';
|
|
2
4
|
export async function getResource(resourceType, name) {
|
|
3
5
|
if (name === '@latest') {
|
|
4
6
|
const result = await execa('kubectl', [
|
|
@@ -45,3 +47,63 @@ export async function replaceResource(resource) {
|
|
|
45
47
|
});
|
|
46
48
|
return JSON.parse(result.stdout);
|
|
47
49
|
}
|
|
50
|
+
export async function watchEventsLive(queryName) {
|
|
51
|
+
const seenEvents = new Set();
|
|
52
|
+
const pollEvents = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execa('kubectl', [
|
|
55
|
+
'get',
|
|
56
|
+
'events',
|
|
57
|
+
'--field-selector',
|
|
58
|
+
`involvedObject.name=${queryName}`,
|
|
59
|
+
'-o',
|
|
60
|
+
'json',
|
|
61
|
+
]);
|
|
62
|
+
const eventsData = JSON.parse(stdout);
|
|
63
|
+
for (const event of eventsData.items || []) {
|
|
64
|
+
const eventId = event.metadata?.uid;
|
|
65
|
+
if (eventId && !seenEvents.has(eventId)) {
|
|
66
|
+
seenEvents.add(eventId);
|
|
67
|
+
const annotations = event.metadata?.annotations || {};
|
|
68
|
+
const eventData = annotations[EVENT_ANNOTATIONS.EVENT_DATA];
|
|
69
|
+
if (eventData) {
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const hours = now.getHours().toString().padStart(2, '0');
|
|
72
|
+
const minutes = now.getMinutes().toString().padStart(2, '0');
|
|
73
|
+
const seconds = now.getSeconds().toString().padStart(2, '0');
|
|
74
|
+
const millis = now.getMilliseconds().toString().padStart(3, '0');
|
|
75
|
+
const timestamp = `${hours}:${minutes}:${seconds}.${millis}`;
|
|
76
|
+
const reason = event.reason || 'Unknown';
|
|
77
|
+
const eventType = event.type || 'Normal';
|
|
78
|
+
const colorCode = eventType === 'Normal' ? 32 : eventType === 'Warning' ? 33 : 31;
|
|
79
|
+
console.log(`${timestamp} \x1b[${colorCode}m${reason}\x1b[0m ${eventData}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// eslint-disable-next-line no-empty, @typescript-eslint/no-unused-vars
|
|
84
|
+
}
|
|
85
|
+
catch (error) { }
|
|
86
|
+
};
|
|
87
|
+
const pollInterval = setInterval(pollEvents, 200);
|
|
88
|
+
const timeoutSeconds = 300;
|
|
89
|
+
const waitProcess = execa('kubectl', [
|
|
90
|
+
'wait',
|
|
91
|
+
'--for=condition=Completed',
|
|
92
|
+
`query/${queryName}`,
|
|
93
|
+
`--timeout=${timeoutSeconds}s`,
|
|
94
|
+
], {
|
|
95
|
+
timeout: timeoutSeconds * 1000,
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
await waitProcess;
|
|
99
|
+
await pollEvents();
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
101
|
+
await pollEvents();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(chalk.red('Query wait failed:', error instanceof Error ? error.message : 'Unknown error'));
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
clearInterval(pollInterval);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ArkService, ServiceCollection } from '../types/arkService.js';
|
|
2
|
+
import type { AnthropicMarketplaceManifest, AnthropicMarketplaceItem } from '../types/marketplace.js';
|
|
3
|
+
export declare function fetchMarketplaceManifest(): Promise<AnthropicMarketplaceManifest | null>;
|
|
4
|
+
export declare function mapMarketplaceItemToArkService(item: AnthropicMarketplaceItem, registry?: string): ArkService;
|
|
5
|
+
export declare function getMarketplaceServicesFromManifest(): Promise<ServiceCollection | null>;
|
|
6
|
+
export declare function getMarketplaceAgentsFromManifest(): Promise<ServiceCollection | null>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { getMarketplaceRepoUrl, getMarketplaceRegistry } from './config.js';
|
|
3
|
+
export async function fetchMarketplaceManifest() {
|
|
4
|
+
const repoUrl = getMarketplaceRepoUrl();
|
|
5
|
+
const manifestUrl = `${repoUrl}/raw/main/marketplace.json`;
|
|
6
|
+
try {
|
|
7
|
+
const response = await axios.get(manifestUrl, {
|
|
8
|
+
timeout: 10000,
|
|
9
|
+
headers: {
|
|
10
|
+
Accept: 'application/json',
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
return response.data;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (axios.isAxiosError(error)) {
|
|
17
|
+
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function mapMarketplaceItemToArkService(item, registry) {
|
|
25
|
+
const defaultRegistry = registry || getMarketplaceRegistry();
|
|
26
|
+
const serviceName = item.name
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '');
|
|
30
|
+
const chartPath = item.ark?.chartPath || `${defaultRegistry}/${serviceName}`;
|
|
31
|
+
return {
|
|
32
|
+
name: serviceName,
|
|
33
|
+
helmReleaseName: item.ark?.helmReleaseName || serviceName,
|
|
34
|
+
description: item.description,
|
|
35
|
+
enabled: true,
|
|
36
|
+
category: 'marketplace',
|
|
37
|
+
namespace: item.ark?.namespace || serviceName,
|
|
38
|
+
chartPath,
|
|
39
|
+
installArgs: item.ark?.installArgs || ['--create-namespace'],
|
|
40
|
+
k8sServiceName: item.ark?.k8sServiceName || serviceName,
|
|
41
|
+
k8sServicePort: item.ark?.k8sServicePort,
|
|
42
|
+
k8sPortForwardLocalPort: item.ark?.k8sPortForwardLocalPort,
|
|
43
|
+
k8sDeploymentName: item.ark?.k8sDeploymentName || serviceName,
|
|
44
|
+
k8sDevDeploymentName: item.ark?.k8sDevDeploymentName,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export async function getMarketplaceServicesFromManifest() {
|
|
48
|
+
const manifest = await fetchMarketplaceManifest();
|
|
49
|
+
if (!manifest || !manifest.items) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const services = {};
|
|
53
|
+
for (const item of manifest.items) {
|
|
54
|
+
if (item.ark && item.type === 'service') {
|
|
55
|
+
const serviceName = item.name
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
58
|
+
.replace(/^-+|-+$/g, '');
|
|
59
|
+
services[serviceName] = mapMarketplaceItemToArkService(item);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Object.keys(services).length > 0 ? services : null;
|
|
63
|
+
}
|
|
64
|
+
export async function getMarketplaceAgentsFromManifest() {
|
|
65
|
+
const manifest = await fetchMarketplaceManifest();
|
|
66
|
+
if (!manifest || !manifest.items) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const agents = {};
|
|
70
|
+
for (const item of manifest.items) {
|
|
71
|
+
if (item.ark && item.type === 'agent') {
|
|
72
|
+
const agentName = item.name
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
75
|
+
.replace(/^-+|-+$/g, '');
|
|
76
|
+
agents[agentName] = mapMarketplaceItemToArkService(item);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return Object.keys(agents).length > 0 ? agents : null;
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
const mockAxiosGet = jest.fn();
|
|
3
|
+
jest.unstable_mockModule('axios', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
get: mockAxiosGet,
|
|
6
|
+
isAxiosError: (error) => {
|
|
7
|
+
return (typeof error === 'object' &&
|
|
8
|
+
error !== null &&
|
|
9
|
+
'isAxiosError' in error &&
|
|
10
|
+
error.isAxiosError === true);
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
const mockGetMarketplaceRepoUrl = jest.fn();
|
|
15
|
+
const mockGetMarketplaceRegistry = jest.fn();
|
|
16
|
+
jest.unstable_mockModule('./config.js', () => ({
|
|
17
|
+
getMarketplaceRepoUrl: mockGetMarketplaceRepoUrl,
|
|
18
|
+
getMarketplaceRegistry: mockGetMarketplaceRegistry,
|
|
19
|
+
}));
|
|
20
|
+
const { fetchMarketplaceManifest, mapMarketplaceItemToArkService, getMarketplaceServicesFromManifest, } = await import('./marketplaceFetcher.js');
|
|
21
|
+
describe('marketplaceFetcher', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
mockAxiosGet.mockClear();
|
|
25
|
+
mockGetMarketplaceRepoUrl.mockReturnValue('https://test-repo.example.com/marketplace');
|
|
26
|
+
mockGetMarketplaceRegistry.mockReturnValue('oci://test-registry.example.com/charts');
|
|
27
|
+
});
|
|
28
|
+
describe('fetchMarketplaceManifest', () => {
|
|
29
|
+
it('fetches and returns manifest successfully', async () => {
|
|
30
|
+
const mockManifest = {
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
marketplace: 'ARK Marketplace',
|
|
33
|
+
items: [
|
|
34
|
+
{
|
|
35
|
+
name: 'test-service',
|
|
36
|
+
description: 'Test service',
|
|
37
|
+
ark: {
|
|
38
|
+
chartPath: 'oci://registry/test-service',
|
|
39
|
+
namespace: 'test',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
mockAxiosGet.mockResolvedValue({
|
|
45
|
+
data: mockManifest,
|
|
46
|
+
status: 200,
|
|
47
|
+
statusText: 'OK',
|
|
48
|
+
headers: {},
|
|
49
|
+
config: {},
|
|
50
|
+
});
|
|
51
|
+
const result = await fetchMarketplaceManifest();
|
|
52
|
+
expect(result).toEqual(mockManifest);
|
|
53
|
+
expect(mockAxiosGet).toHaveBeenCalledWith(expect.stringContaining('marketplace.json'), expect.objectContaining({
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
headers: { Accept: 'application/json' },
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
it('returns null on network error', async () => {
|
|
59
|
+
const networkError = new Error('Network error');
|
|
60
|
+
networkError.code = 'ENOTFOUND';
|
|
61
|
+
networkError.isAxiosError = true;
|
|
62
|
+
mockAxiosGet.mockRejectedValue(networkError);
|
|
63
|
+
const result = await fetchMarketplaceManifest();
|
|
64
|
+
expect(result).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
it('returns null on connection refused', async () => {
|
|
67
|
+
const connectionError = Object.assign(new Error('Connection refused'), {
|
|
68
|
+
code: 'ECONNREFUSED',
|
|
69
|
+
isAxiosError: true,
|
|
70
|
+
});
|
|
71
|
+
mockAxiosGet.mockRejectedValue(connectionError);
|
|
72
|
+
const result = await fetchMarketplaceManifest();
|
|
73
|
+
expect(result).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('mapMarketplaceItemToArkService', () => {
|
|
77
|
+
it('maps marketplace item to ARK service correctly', () => {
|
|
78
|
+
const item = {
|
|
79
|
+
name: 'test-service',
|
|
80
|
+
description: 'Test description',
|
|
81
|
+
ark: {
|
|
82
|
+
chartPath: 'oci://registry/test',
|
|
83
|
+
namespace: 'test-ns',
|
|
84
|
+
helmReleaseName: 'test-release',
|
|
85
|
+
installArgs: ['--create-namespace'],
|
|
86
|
+
k8sServiceName: 'test-svc',
|
|
87
|
+
k8sServicePort: 8080,
|
|
88
|
+
k8sDeploymentName: 'test-deploy',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const result = mapMarketplaceItemToArkService(item);
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
name: 'test-service',
|
|
94
|
+
helmReleaseName: 'test-release',
|
|
95
|
+
description: 'Test description',
|
|
96
|
+
enabled: true,
|
|
97
|
+
category: 'marketplace',
|
|
98
|
+
namespace: 'test-ns',
|
|
99
|
+
chartPath: 'oci://registry/test',
|
|
100
|
+
installArgs: ['--create-namespace'],
|
|
101
|
+
k8sServiceName: 'test-svc',
|
|
102
|
+
k8sServicePort: 8080,
|
|
103
|
+
k8sDeploymentName: 'test-deploy',
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
it('uses defaults when ark fields are missing', () => {
|
|
107
|
+
const item = {
|
|
108
|
+
name: 'simple-service',
|
|
109
|
+
description: 'Simple service',
|
|
110
|
+
ark: {},
|
|
111
|
+
};
|
|
112
|
+
const result = mapMarketplaceItemToArkService(item);
|
|
113
|
+
expect(result.name).toBe('simple-service');
|
|
114
|
+
expect(result.helmReleaseName).toBe('simple-service');
|
|
115
|
+
expect(result.namespace).toBe('simple-service');
|
|
116
|
+
expect(result.chartPath).toContain('simple-service');
|
|
117
|
+
});
|
|
118
|
+
it('sanitizes service name', () => {
|
|
119
|
+
const item = {
|
|
120
|
+
name: 'Test Service 123!',
|
|
121
|
+
description: 'Test',
|
|
122
|
+
ark: {},
|
|
123
|
+
};
|
|
124
|
+
const result = mapMarketplaceItemToArkService(item);
|
|
125
|
+
expect(result.name).toBe('test-service-123');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('getMarketplaceServicesFromManifest', () => {
|
|
129
|
+
it('converts manifest items to service collection', async () => {
|
|
130
|
+
const mockManifest = {
|
|
131
|
+
version: '1.0.0',
|
|
132
|
+
marketplace: 'ARK Marketplace',
|
|
133
|
+
items: [
|
|
134
|
+
{
|
|
135
|
+
name: 'service1',
|
|
136
|
+
description: 'Service 1',
|
|
137
|
+
type: 'service',
|
|
138
|
+
ark: {
|
|
139
|
+
chartPath: 'oci://registry/service1',
|
|
140
|
+
namespace: 'ns1',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'service2',
|
|
145
|
+
description: 'Service 2',
|
|
146
|
+
type: 'service',
|
|
147
|
+
ark: {
|
|
148
|
+
chartPath: 'oci://registry/service2',
|
|
149
|
+
namespace: 'ns2',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
mockAxiosGet.mockResolvedValue({
|
|
155
|
+
data: mockManifest,
|
|
156
|
+
status: 200,
|
|
157
|
+
statusText: 'OK',
|
|
158
|
+
headers: {},
|
|
159
|
+
config: {},
|
|
160
|
+
});
|
|
161
|
+
const result = await getMarketplaceServicesFromManifest();
|
|
162
|
+
expect(result).not.toBeNull();
|
|
163
|
+
expect(result?.['service1']).toBeDefined();
|
|
164
|
+
expect(result?.['service2']).toBeDefined();
|
|
165
|
+
expect(result?.['service1']?.description).toBe('Service 1');
|
|
166
|
+
expect(result?.['service2']?.description).toBe('Service 2');
|
|
167
|
+
});
|
|
168
|
+
it('filters out items without ark field', async () => {
|
|
169
|
+
const mockManifest = {
|
|
170
|
+
version: '1.0.0',
|
|
171
|
+
marketplace: 'ARK Marketplace',
|
|
172
|
+
items: [
|
|
173
|
+
{
|
|
174
|
+
name: 'service1',
|
|
175
|
+
description: 'Service 1',
|
|
176
|
+
type: 'service',
|
|
177
|
+
ark: {
|
|
178
|
+
chartPath: 'oci://registry/service1',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'service2',
|
|
183
|
+
description: 'Service 2',
|
|
184
|
+
type: 'service',
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
mockAxiosGet.mockResolvedValue({
|
|
189
|
+
data: mockManifest,
|
|
190
|
+
status: 200,
|
|
191
|
+
statusText: 'OK',
|
|
192
|
+
headers: {},
|
|
193
|
+
config: {},
|
|
194
|
+
});
|
|
195
|
+
const result = await getMarketplaceServicesFromManifest();
|
|
196
|
+
expect(result).not.toBeNull();
|
|
197
|
+
expect(result?.['service1']).toBeDefined();
|
|
198
|
+
expect(result?.['service2']).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
it('returns null when manifest fetch fails', async () => {
|
|
201
|
+
const networkError = new Error('Network error');
|
|
202
|
+
networkError.code = 'ENOTFOUND';
|
|
203
|
+
networkError.isAxiosError = true;
|
|
204
|
+
mockAxiosGet.mockRejectedValue(networkError);
|
|
205
|
+
const result = await getMarketplaceServicesFromManifest();
|
|
206
|
+
expect(result).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
it('returns null when manifest has no items', async () => {
|
|
209
|
+
const mockManifest = {
|
|
210
|
+
version: '1.0.0',
|
|
211
|
+
marketplace: 'ARK Marketplace',
|
|
212
|
+
items: [],
|
|
213
|
+
};
|
|
214
|
+
mockAxiosGet.mockResolvedValue({
|
|
215
|
+
data: mockManifest,
|
|
216
|
+
status: 200,
|
|
217
|
+
statusText: 'OK',
|
|
218
|
+
headers: {},
|
|
219
|
+
config: {},
|
|
220
|
+
});
|
|
221
|
+
const result = await getMarketplaceServicesFromManifest();
|
|
222
|
+
expect(result).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -2,14 +2,23 @@
|
|
|
2
2
|
* Marketplace service definitions for external ARK marketplace resources
|
|
3
3
|
* Repository: https://github.com/mckinsey/agents-at-scale-marketplace
|
|
4
4
|
* Charts are installed from the public OCI registry
|
|
5
|
+
*
|
|
6
|
+
* Supports Anthropic Marketplace JSON format for dynamic enumeration
|
|
5
7
|
*/
|
|
6
8
|
import type { ArkService, ServiceCollection } from './types/arkService.js';
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Get all marketplace services, fetching from marketplace.json
|
|
11
|
+
* Returns null if marketplace is unavailable
|
|
10
12
|
*/
|
|
11
|
-
export declare
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export declare function getAllMarketplaceServices(): Promise<ServiceCollection | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Get all marketplace agents, fetching from marketplace.json
|
|
16
|
+
* Returns null if marketplace is unavailable
|
|
17
|
+
*/
|
|
18
|
+
export declare function getAllMarketplaceAgents(): Promise<ServiceCollection | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Get a marketplace item by path (supports both services and agents)
|
|
21
|
+
* Returns null if marketplace is unavailable
|
|
22
|
+
*/
|
|
23
|
+
export declare function getMarketplaceItem(path: string): Promise<ArkService | undefined | null>;
|
|
14
24
|
export declare function isMarketplaceService(name: string): boolean;
|
|
15
|
-
export declare function extractMarketplaceServiceName(path: string): string;
|