@agents-at-scale/ark 0.1.46 → 0.1.49

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 (65) hide show
  1. package/.arkrc.template.yaml +51 -0
  2. package/README.md +4 -0
  3. package/dist/arkServices.js +22 -8
  4. package/dist/arkServices.spec.js +6 -0
  5. package/dist/commands/agents/index.d.ts +1 -1
  6. package/dist/commands/agents/index.js +4 -2
  7. package/dist/commands/chat/index.js +1 -1
  8. package/dist/commands/completion/index.js +19 -5
  9. package/dist/commands/dashboard/index.d.ts +2 -2
  10. package/dist/commands/dashboard/index.js +5 -4
  11. package/dist/commands/export/index.d.ts +3 -0
  12. package/dist/commands/export/index.js +73 -0
  13. package/dist/commands/export/index.spec.d.ts +1 -0
  14. package/dist/commands/export/index.spec.js +145 -0
  15. package/dist/commands/import/index.d.ts +3 -0
  16. package/dist/commands/import/index.js +27 -0
  17. package/dist/commands/import/index.spec.d.ts +1 -0
  18. package/dist/commands/import/index.spec.js +46 -0
  19. package/dist/commands/install/index.js +20 -10
  20. package/dist/commands/marketplace/index.js +51 -23
  21. package/dist/commands/marketplace/index.spec.d.ts +1 -0
  22. package/dist/commands/marketplace/index.spec.js +88 -0
  23. package/dist/commands/memory/index.js +9 -4
  24. package/dist/commands/models/index.d.ts +1 -1
  25. package/dist/commands/models/index.js +4 -2
  26. package/dist/commands/query/index.d.ts +1 -1
  27. package/dist/commands/query/index.js +6 -2
  28. package/dist/commands/status/index.js +7 -2
  29. package/dist/commands/teams/index.d.ts +1 -1
  30. package/dist/commands/teams/index.js +4 -2
  31. package/dist/commands/uninstall/index.js +20 -10
  32. package/dist/index.js +4 -0
  33. package/dist/lib/arkApiProxy.d.ts +1 -1
  34. package/dist/lib/arkApiProxy.js +2 -2
  35. package/dist/lib/arkServiceProxy.d.ts +3 -1
  36. package/dist/lib/arkServiceProxy.js +34 -1
  37. package/dist/lib/arkServiceProxy.spec.d.ts +1 -0
  38. package/dist/lib/arkServiceProxy.spec.js +100 -0
  39. package/dist/lib/chatClient.d.ts +2 -0
  40. package/dist/lib/chatClient.js +10 -2
  41. package/dist/lib/config.d.ts +17 -1
  42. package/dist/lib/config.js +62 -7
  43. package/dist/lib/config.spec.js +103 -0
  44. package/dist/lib/constants.d.ts +3 -0
  45. package/dist/lib/constants.js +5 -0
  46. package/dist/lib/executeQuery.d.ts +1 -0
  47. package/dist/lib/executeQuery.js +21 -4
  48. package/dist/lib/executeQuery.spec.js +4 -1
  49. package/dist/lib/kubectl.d.ts +3 -0
  50. package/dist/lib/kubectl.js +68 -0
  51. package/dist/lib/kubectl.spec.js +16 -0
  52. package/dist/lib/marketplaceFetcher.d.ts +6 -0
  53. package/dist/lib/marketplaceFetcher.js +80 -0
  54. package/dist/lib/marketplaceFetcher.spec.d.ts +1 -0
  55. package/dist/lib/marketplaceFetcher.spec.js +225 -0
  56. package/dist/lib/types.d.ts +1 -0
  57. package/dist/marketplaceServices.d.ts +15 -6
  58. package/dist/marketplaceServices.js +38 -40
  59. package/dist/marketplaceServices.spec.d.ts +1 -0
  60. package/dist/marketplaceServices.spec.js +74 -0
  61. package/dist/types/marketplace.d.ts +37 -0
  62. package/dist/types/marketplace.js +1 -0
  63. package/dist/ui/MainMenu.js +6 -2
  64. package/package.json +4 -2
  65. package/templates/marketplace/marketplace.json.example +59 -0
@@ -0,0 +1,51 @@
1
+ # Ark CLI Configuration Template
2
+ # Copy this to ~/.arkrc.yaml (user config) or .arkrc.yaml (project config)
3
+ # All fields are optional - only include what you want to override
4
+
5
+ # Chat configuration
6
+ chat:
7
+ # Enable/disable streaming for chat responses (default: true)
8
+ # streaming: true
9
+
10
+ # Output format: 'text' or 'markdown' (default: text)
11
+ # outputFormat: text
12
+
13
+ # Service configuration
14
+ services:
15
+ # Reuse existing kubectl port-forward processes instead of failing (default: false)
16
+ # reusePortForwards: false
17
+
18
+ # Service overrides - customize specific Ark services
19
+ # Example: Enable localhost-gateway
20
+ # localhost-gateway:
21
+ # enabled: true
22
+
23
+ # Example: Change namespace
24
+ # ark-controller:
25
+ # namespace: custom-namespace
26
+
27
+ # Example: Add custom install arguments
28
+ # ark-api:
29
+ # installArgs:
30
+ # - --set
31
+ # - replicas=3
32
+
33
+ # Example: Configure Agents @ Scale with container registry
34
+ # agents-at-scale:
35
+ # enabled: true
36
+ # installArgs:
37
+ # - --set
38
+ # - containerRegistry.enabled=true
39
+ # - --set
40
+ # - containerRegistry.username=YOUR_USERNAME
41
+ # - --set
42
+ # - containerRegistry.password=YOUR_PASSWORD
43
+
44
+ # Available service properties:
45
+ # enabled: boolean - Enable/disable service installation
46
+ # namespace: string - Kubernetes namespace
47
+ # chartPath: string - Custom Helm chart path
48
+ # installArgs: string[] - Additional Helm install arguments
49
+ # k8sServiceName: string - Kubernetes service name for port forwarding
50
+ # k8sServicePort: number - Kubernetes service port for port forwarding
51
+ # k8sPortForwardLocalPort: number - Local port for port forwarding
package/README.md CHANGED
@@ -46,6 +46,8 @@ To troubleshoot an installation, run `ark status`.
46
46
 
47
47
  You can customize Ark service installations using a `.arkrc.yaml` file in your home directory (`~/.arkrc.yaml`) or project directory. This allows you to override service properties like enabled status, namespace, or chart location.
48
48
 
49
+ See [`.arkrc.template.yaml`](.arkrc.template.yaml) for a complete reference of all available configuration options.
50
+
49
51
  Example `.arkrc.yaml`:
50
52
 
51
53
  ```yaml
@@ -76,3 +78,5 @@ services:
76
78
  ```
77
79
 
78
80
  Replace `YOUR_USERNAME` and `YOUR_PASSWORD` with your JFrog credentials.
81
+
82
+ See [.arkrc.yaml.sample](.arkrc.yaml.sample) for a complete example with all available options.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralized ARK service definitions used by both install and status commands
3
3
  */
4
- import { loadConfig } from './lib/config.js';
4
+ import { loadConfig, getMarketplaceRegistry } from './lib/config.js';
5
5
  const REGISTRY_BASE = 'oci://ghcr.io/mckinsey/agents-at-scale-ark/charts';
6
6
  /**
7
7
  * Dependencies that should be installed before ARK services
@@ -119,17 +119,17 @@ const defaultArkServices = {
119
119
  k8sDeploymentName: 'ark-mcp',
120
120
  k8sDevDeploymentName: 'ark-mcp-devspace',
121
121
  },
122
- 'ark-cluster-memory': {
123
- name: 'ark-cluster-memory',
124
- helmReleaseName: 'ark-cluster-memory',
122
+ 'ark-broker': {
123
+ name: 'ark-broker',
124
+ helmReleaseName: 'ark-broker',
125
125
  description: 'In-memory storage service with streaming support for Ark queries',
126
126
  enabled: true,
127
127
  category: 'service',
128
128
  // namespace: undefined - uses current context namespace
129
- chartPath: `${REGISTRY_BASE}/ark-cluster-memory`,
129
+ chartPath: `${REGISTRY_BASE}/ark-broker`,
130
130
  installArgs: [],
131
- k8sDeploymentName: 'ark-cluster-memory',
132
- k8sDevDeploymentName: 'ark-cluster-memory-devspace',
131
+ k8sDeploymentName: 'ark-broker',
132
+ k8sDevDeploymentName: 'ark-broker-devspace',
133
133
  },
134
134
  'mcp-filesystem': {
135
135
  name: 'mcp-filesystem',
@@ -153,13 +153,27 @@ const defaultArkServices = {
153
153
  chartPath: `${REGISTRY_BASE}/localhost-gateway`,
154
154
  installArgs: [],
155
155
  },
156
+ 'noah': {
157
+ name: 'noah',
158
+ helmReleaseName: 'noah',
159
+ description: 'Runtime administration agent with cluster privileges',
160
+ enabled: true,
161
+ category: 'service',
162
+ chartPath: `${getMarketplaceRegistry()}/noah`,
163
+ installArgs: [],
164
+ k8sServiceName: 'noah-mcp',
165
+ k8sServicePort: 8639,
166
+ k8sDeploymentName: 'noah-mcp',
167
+ },
156
168
  };
157
169
  function applyConfigOverrides(defaults) {
158
170
  const config = loadConfig();
159
171
  const overrides = config?.services || {};
160
172
  const result = {};
161
173
  for (const [key, service] of Object.entries(defaults)) {
162
- result[key] = overrides[key] ? { ...service, ...overrides[key] } : service;
174
+ const override = overrides[key];
175
+ result[key] =
176
+ override && typeof override === 'object' ? { ...service, ...override } : service;
163
177
  }
164
178
  return result;
165
179
  }
@@ -1,12 +1,18 @@
1
1
  import { jest } from '@jest/globals';
2
2
  const mockLoadConfig = jest.fn();
3
+ const mockGetMarketplaceRegistry = jest.fn();
3
4
  jest.unstable_mockModule('./lib/config.js', () => ({
4
5
  loadConfig: mockLoadConfig,
6
+ getMarketplaceRegistry: mockGetMarketplaceRegistry,
5
7
  }));
8
+ mockLoadConfig.mockReturnValue({});
9
+ mockGetMarketplaceRegistry.mockReturnValue('oci://test-registry/charts');
6
10
  const { arkDependencies, arkServices: originalArkServices, getInstallableServices, } = await import('./arkServices.js');
7
11
  describe('arkServices', () => {
8
12
  beforeEach(() => {
9
13
  jest.clearAllMocks();
14
+ mockLoadConfig.mockReturnValue({});
15
+ mockGetMarketplaceRegistry.mockReturnValue('oci://test-registry/charts');
10
16
  });
11
17
  it('exports arkDependencies with expected structure', () => {
12
18
  expect(arkDependencies).toBeDefined();
@@ -1,3 +1,3 @@
1
1
  import { Command } from 'commander';
2
2
  import type { ArkConfig } from '../../lib/config.js';
3
- export declare function createAgentsCommand(_: ArkConfig): Command;
3
+ export declare function createAgentsCommand(config: ArkConfig): Command;
@@ -30,7 +30,7 @@ async function listAgents(options) {
30
30
  process.exit(1);
31
31
  }
32
32
  }
33
- export function createAgentsCommand(_) {
33
+ export function createAgentsCommand(config) {
34
34
  const agentsCommand = new Command('agents');
35
35
  agentsCommand
36
36
  .description('list available agents')
@@ -54,11 +54,13 @@ export function createAgentsCommand(_) {
54
54
  .description('Query an agent')
55
55
  .argument('<name>', 'Agent name')
56
56
  .argument('<message>', 'Message to send')
57
- .action(async (name, message) => {
57
+ .option('--timeout <timeout>', 'Query timeout (e.g., 30s, 5m, 1h)')
58
+ .action(async (name, message, options) => {
58
59
  await executeQuery({
59
60
  targetType: 'agent',
60
61
  targetName: name,
61
62
  message,
63
+ timeout: options.timeout || config.queryTimeout,
62
64
  });
63
65
  });
64
66
  return agentsCommand;
@@ -14,7 +14,7 @@ export function createChatCommand(config) {
14
14
  const initialTargetId = targetArg;
15
15
  // Config is passed from main
16
16
  try {
17
- const proxy = new ArkApiProxy();
17
+ const proxy = new ArkApiProxy(undefined, config.services?.reusePortForwards ?? false);
18
18
  const arkApiClient = await proxy.start();
19
19
  render(_jsx(ChatUI, { initialTargetId: initialTargetId, arkApiClient: arkApiClient, arkApiProxy: proxy, config: config }));
20
20
  }
@@ -28,7 +28,7 @@ _ark_completion() {
28
28
 
29
29
  case \${COMP_CWORD} in
30
30
  1)
31
- opts="agents chat cluster completion config dashboard docs generate install marketplace models queries query routes status targets teams tools uninstall help"
31
+ opts="agents chat cluster completion config dashboard docs evaluation export generate import install marketplace memory models queries query routes status targets teams tools uninstall help"
32
32
  COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
33
33
  return 0
34
34
  ;;
@@ -84,15 +84,18 @@ _ark_completion() {
84
84
  COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
85
85
  return 0
86
86
  ;;
87
+ memory)
88
+ opts="list ls delete reset"
89
+ COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
90
+ return 0
91
+ ;;
87
92
  install)
88
- # Suggest marketplace services with marketplace/services/ prefix
89
- opts="marketplace/services/phoenix marketplace/services/langfuse"
93
+ opts="marketplace/services/phoenix marketplace/services/langfuse marketplace/agents/noah"
90
94
  COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
91
95
  return 0
92
96
  ;;
93
97
  uninstall)
94
- # Suggest marketplace services with marketplace/services/ prefix
95
- opts="marketplace/services/phoenix marketplace/services/langfuse"
98
+ opts="marketplace/services/phoenix marketplace/services/langfuse marketplace/agents/noah"
96
99
  COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
97
100
  return 0
98
101
  ;;
@@ -157,9 +160,13 @@ _ark() {
157
160
  'config[Configuration management]' \\
158
161
  'dashboard[Open ARK dashboard]' \\
159
162
  'docs[Open ARK documentation]' \\
163
+ 'evaluation[Execute evaluations against evaluators]' \\
164
+ 'export[Export ARK resources to a file]' \\
160
165
  'generate[Generate ARK resources]' \\
166
+ 'import[Import ARK resources from a file]' \\
161
167
  'install[Install ARK services]' \\
162
168
  'marketplace[Manage marketplace services]' \\
169
+ 'memory[Manage memory sessions and queries]' \\
163
170
  'models[List available models]' \\
164
171
  'queries[Manage query resources]' \\
165
172
  'query[Execute a single query]' \\
@@ -227,6 +234,13 @@ _ark() {
227
234
  'list[List available marketplace services]' \\
228
235
  'ls[List available marketplace services]'
229
236
  ;;
237
+ memory)
238
+ _values 'memory commands' \\
239
+ 'list[List all sessions]' \\
240
+ 'ls[List all sessions]' \\
241
+ 'delete[Delete memory data]' \\
242
+ 'reset[Delete memory data]'
243
+ ;;
230
244
  install)
231
245
  _values 'services to install' \\
232
246
  'marketplace/services/phoenix[Phoenix observability platform]' \\
@@ -1,4 +1,4 @@
1
1
  import { Command } from 'commander';
2
2
  import type { ArkConfig } from '../../lib/config.js';
3
- export declare function openDashboard(): Promise<void>;
4
- export declare function createDashboardCommand(_: ArkConfig): Command;
3
+ export declare function openDashboard(config: ArkConfig): Promise<void>;
4
+ export declare function createDashboardCommand(config: ArkConfig): Command;
@@ -4,11 +4,12 @@ import open from 'open';
4
4
  import ora from 'ora';
5
5
  import { ArkServiceProxy } from '../../lib/arkServiceProxy.js';
6
6
  import { arkServices } from '../../arkServices.js';
7
- export async function openDashboard() {
7
+ export async function openDashboard(config) {
8
8
  const spinner = ora('Connecting to dashboard').start();
9
9
  try {
10
10
  const dashboardService = arkServices['ark-dashboard'];
11
- const proxy = new ArkServiceProxy(dashboardService, 3274); // DASH on phone keypad
11
+ const proxy = new ArkServiceProxy(dashboardService, 3274, // DASH on phone keypad
12
+ config.services?.reusePortForwards ?? false);
12
13
  const url = await proxy.start();
13
14
  spinner.succeed('Dashboard connected');
14
15
  console.log(`ARK dashboard running on: ${chalk.green(url)}`);
@@ -30,10 +31,10 @@ export async function openDashboard() {
30
31
  process.exit(1);
31
32
  }
32
33
  }
33
- export function createDashboardCommand(_) {
34
+ export function createDashboardCommand(config) {
34
35
  const dashboardCommand = new Command('dashboard');
35
36
  dashboardCommand
36
37
  .description('Open the ARK dashboard in your browser')
37
- .action(openDashboard);
38
+ .action(() => openDashboard(config));
38
39
  return dashboardCommand;
39
40
  }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { ArkConfig } from '../../lib/config.js';
3
+ export declare function createExportCommand(config: ArkConfig): Command;
@@ -0,0 +1,73 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs/promises';
3
+ import yaml from 'yaml';
4
+ import { listResources } from '../../lib/kubectl.js';
5
+ import output from '../../lib/output.js';
6
+ // resource types in dependency order so that they can be loaded correctly
7
+ // by default these will all be exported if not specified; can be overridden with defaultExportTypes in config
8
+ const RESOURCE_ORDER = [
9
+ 'secrets',
10
+ 'tools',
11
+ 'models',
12
+ 'agents',
13
+ 'teams',
14
+ 'evaluators',
15
+ 'mcpservers',
16
+ 'a2aservers',
17
+ ];
18
+ async function exportResources(options, config) {
19
+ try {
20
+ const allResourceTypes = config.defaultExportTypes || RESOURCE_ORDER;
21
+ const outputPath = options.output || 'ark-export.yaml';
22
+ let resourceTypes = options.types
23
+ ? (options.types.split(','))
24
+ : allResourceTypes;
25
+ // ensure that we get resources in the correct order; e.g. agents before teams that use the agents
26
+ resourceTypes.sort((a, b) => {
27
+ return RESOURCE_ORDER.indexOf(a) - RESOURCE_ORDER.indexOf(b);
28
+ });
29
+ output.info(`exporting ark resources to ${outputPath}...`);
30
+ const allResources = [];
31
+ let allResourceCount = 0;
32
+ for (const resourceType of resourceTypes) {
33
+ if (!RESOURCE_ORDER.includes(resourceType)) {
34
+ output.warning(`unknown resource type: ${resourceType}, skipping`);
35
+ continue;
36
+ }
37
+ output.info(`fetching ${resourceType}...`);
38
+ const resources = await listResources(resourceType, {
39
+ namespace: options.namespace,
40
+ labels: options.labels
41
+ });
42
+ const resourceCount = resources.length;
43
+ if (resources.length > 0) {
44
+ output.success(`found ${resourceCount} ${resourceType}`);
45
+ allResources.push(...resources);
46
+ allResourceCount += resourceCount;
47
+ }
48
+ }
49
+ if (allResourceCount === 0) {
50
+ output.warning('no resources found to export');
51
+ return;
52
+ }
53
+ const yamlContent = allResources.map((resource) => yaml.stringify(resource)).join("\n---\n");
54
+ await fs.writeFile(outputPath, yamlContent, 'utf-8');
55
+ output.success(`exported ${allResourceCount} resources to ${outputPath}`);
56
+ }
57
+ catch (error) {
58
+ output.error('export failed:', error instanceof Error ? error.message : error);
59
+ }
60
+ }
61
+ export function createExportCommand(config) {
62
+ const exportCommand = new Command('export');
63
+ exportCommand
64
+ .description('export ARK resources to a file')
65
+ .option('-o, --output <file>', 'output file path', 'ark-export.yaml')
66
+ .option('-n, --namespace <namespace>', 'namespace to export from')
67
+ .option('-t, --types <types>', 'comma-separated list of resource types to export')
68
+ .option('-l, --labels <labels>', 'label selector to filter resources')
69
+ .action(async (options) => {
70
+ await exportResources(options, config);
71
+ });
72
+ return exportCommand;
73
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,145 @@
1
+ import { jest } from '@jest/globals';
2
+ import { Command } from 'commander';
3
+ const mockExeca = jest.fn();
4
+ jest.unstable_mockModule('execa', () => ({
5
+ execa: mockExeca,
6
+ }));
7
+ const mockWriteFile = jest.fn();
8
+ jest.unstable_mockModule('fs/promises', () => ({
9
+ writeFile: mockWriteFile,
10
+ }));
11
+ const mockOutput = {
12
+ info: jest.fn(),
13
+ success: jest.fn(),
14
+ warning: jest.fn(),
15
+ error: jest.fn(),
16
+ };
17
+ jest.unstable_mockModule('../../lib/output.js', () => ({
18
+ default: mockOutput,
19
+ }));
20
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
21
+ throw new Error('process.exit called');
22
+ }));
23
+ const mockKubectlGetResponse = {
24
+ "apiVersion": "v1",
25
+ "items": [{ "spec": "foo" }],
26
+ "kind": "List",
27
+ "metadata": {
28
+ "resourceVersion": ""
29
+ }
30
+ };
31
+ const { createExportCommand } = await import('./index.js');
32
+ describe('export command', () => {
33
+ const mockConfig = {};
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ });
37
+ it('should create export command with correct description', () => {
38
+ const command = createExportCommand(mockConfig);
39
+ expect(command).toBeInstanceOf(Command);
40
+ expect(command.name()).toBe('export');
41
+ expect(command.description()).toBe('export ARK resources to a file');
42
+ });
43
+ it('should export all resource types by default', async () => {
44
+ mockExeca.mockResolvedValue({
45
+ stdout: JSON.stringify(mockKubectlGetResponse),
46
+ });
47
+ mockWriteFile.mockResolvedValue(undefined);
48
+ const command = createExportCommand(mockConfig);
49
+ await command.parseAsync(['node', 'test', '-o', 'test.yaml']);
50
+ const expectedResourceTypes = [
51
+ 'secrets',
52
+ 'tools',
53
+ 'models',
54
+ 'agents',
55
+ 'teams',
56
+ 'evaluators',
57
+ 'mcpservers',
58
+ 'a2aservers',
59
+ ];
60
+ expect(mockExeca).toHaveBeenCalledTimes(expectedResourceTypes.length);
61
+ for (const resourceType of expectedResourceTypes) {
62
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', expect.arrayContaining(['get', resourceType, '-o', 'json']), expect.any(Object));
63
+ expect(mockOutput.success).toHaveBeenCalledWith(`found 1 ${resourceType}`);
64
+ }
65
+ expect(mockWriteFile).toHaveBeenCalledTimes(1);
66
+ });
67
+ it('should export types specified in config in dependency order', async () => {
68
+ mockExeca.mockResolvedValue({
69
+ stdout: JSON.stringify(mockKubectlGetResponse),
70
+ });
71
+ mockWriteFile.mockResolvedValue(undefined);
72
+ const newDefaultTypes = ['teams', 'agents'];
73
+ const modifiedConfig = { defaultExportTypes: newDefaultTypes };
74
+ const command = createExportCommand(modifiedConfig);
75
+ await command.parseAsync(['node', 'test', '-o', 'test.yaml']);
76
+ expect(mockExeca.mock.calls).toEqual([
77
+ ['kubectl', expect.arrayContaining(['get', 'agents']), expect.any(Object)],
78
+ ['kubectl', expect.arrayContaining(['get', 'teams']), expect.any(Object)],
79
+ ]);
80
+ expect(mockWriteFile).toHaveBeenCalledTimes(1);
81
+ });
82
+ it('should filter by resource types when specified and export in order', async () => {
83
+ mockExeca.mockResolvedValue({
84
+ stdout: JSON.stringify(mockKubectlGetResponse),
85
+ });
86
+ mockWriteFile.mockResolvedValue(undefined);
87
+ const command = createExportCommand(mockConfig);
88
+ await command.parseAsync([
89
+ 'node',
90
+ 'test',
91
+ '-t',
92
+ 'agents,models',
93
+ '-o',
94
+ 'test.yaml',
95
+ ]);
96
+ expect(mockExeca.mock.calls).toEqual([
97
+ ['kubectl', expect.arrayContaining(['get', 'models']), expect.any(Object)],
98
+ ['kubectl', expect.arrayContaining(['get', 'agents']), expect.any(Object)],
99
+ ]);
100
+ });
101
+ it('should use namespace filter when specified', async () => {
102
+ mockExeca.mockResolvedValue({
103
+ stdout: JSON.stringify(mockKubectlGetResponse),
104
+ });
105
+ mockWriteFile.mockResolvedValue(undefined);
106
+ const command = createExportCommand(mockConfig);
107
+ await command.parseAsync([
108
+ 'node',
109
+ 'test',
110
+ '-n',
111
+ 'custom-namespace',
112
+ '-t',
113
+ 'agents',
114
+ '-o',
115
+ 'test.yaml',
116
+ ]);
117
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', expect.arrayContaining(['-n', 'custom-namespace']), expect.any(Object));
118
+ expect(mockWriteFile).toHaveBeenCalledTimes(1);
119
+ });
120
+ it('should use label selector when specified', async () => {
121
+ mockExeca.mockResolvedValue({
122
+ stdout: JSON.stringify(mockKubectlGetResponse),
123
+ });
124
+ mockWriteFile.mockResolvedValue(undefined);
125
+ const command = createExportCommand(mockConfig);
126
+ await command.parseAsync([
127
+ 'node',
128
+ 'test',
129
+ '-l',
130
+ 'app=test',
131
+ '-t',
132
+ 'agents',
133
+ '-o',
134
+ 'test.yaml',
135
+ ]);
136
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', expect.arrayContaining(['-l', 'app=test']), expect.any(Object));
137
+ expect(mockWriteFile).toHaveBeenCalledTimes(1);
138
+ });
139
+ it('fails if kubectl get fails for a resource type', async () => {
140
+ mockExeca.mockRejectedValue('Export broke');
141
+ const command = createExportCommand(mockConfig);
142
+ await command.parseAsync(['node', 'test']);
143
+ expect(mockOutput.error).toHaveBeenCalledWith('export failed:', 'Export broke');
144
+ });
145
+ });
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { ArkConfig } from '../../lib/config.js';
3
+ export declare function createImportCommand(_: ArkConfig): Command;
@@ -0,0 +1,27 @@
1
+ import { Command } from 'commander';
2
+ import { execa } from 'execa';
3
+ import output from '../../lib/output.js';
4
+ async function importResources(filepath) {
5
+ try {
6
+ output.info(`importing ark resources from ${filepath}...`);
7
+ const args = ['create', '-f', filepath];
8
+ const result = await execa('kubectl', args, {
9
+ stdio: 'pipe',
10
+ });
11
+ output.success(`imported resources from ${filepath}`);
12
+ }
13
+ catch (error) {
14
+ output.error('import failed:', error instanceof Error ? error.message : error);
15
+ process.exit(1);
16
+ }
17
+ }
18
+ export function createImportCommand(_) {
19
+ const importCommand = new Command('import');
20
+ importCommand
21
+ .description('import ARK resources from a file')
22
+ .argument('<filepath>', 'input file path')
23
+ .action(async (filepath) => {
24
+ await importResources(filepath);
25
+ });
26
+ return importCommand;
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { jest } from '@jest/globals';
2
+ import { Command } from 'commander';
3
+ const mockExeca = jest.fn();
4
+ jest.unstable_mockModule('execa', () => ({
5
+ execa: mockExeca,
6
+ }));
7
+ const mockOutput = {
8
+ info: jest.fn(),
9
+ success: jest.fn(),
10
+ warning: jest.fn(),
11
+ error: jest.fn(),
12
+ };
13
+ jest.unstable_mockModule('../../lib/output.js', () => ({
14
+ default: mockOutput,
15
+ }));
16
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
17
+ throw new Error('process.exit called');
18
+ }));
19
+ const { createImportCommand } = await import('./index.js');
20
+ describe('import command', () => {
21
+ const mockConfig = {};
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ });
25
+ it('should create import command with correct description', () => {
26
+ const command = createImportCommand(mockConfig);
27
+ expect(command).toBeInstanceOf(Command);
28
+ expect(command.name()).toBe('import');
29
+ expect(command.description()).toBe('import ARK resources from a file');
30
+ });
31
+ it('should use kubectl to import', async () => {
32
+ mockExeca.mockResolvedValue({
33
+ stdout: JSON.stringify({ items: [] }),
34
+ });
35
+ const command = createImportCommand(mockConfig);
36
+ await command.parseAsync(['node', 'test', 'test.yaml']);
37
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['create', '-f', 'test.yaml'], expect.any(Object));
38
+ });
39
+ it('exits with error when kubectl create has error', async () => {
40
+ mockExeca.mockRejectedValue('Import broke');
41
+ const command = createImportCommand(mockConfig);
42
+ await expect(command.parseAsync(['node', 'test', 'test.yaml'])).rejects.toThrow('process.exit called');
43
+ expect(mockOutput.error).toHaveBeenCalledWith('import failed:', 'Import broke');
44
+ expect(mockExit).toHaveBeenCalledWith(1);
45
+ });
46
+ });
@@ -5,7 +5,7 @@ import inquirer from 'inquirer';
5
5
  import { showNoClusterError } from '../../lib/startup.js';
6
6
  import output from '../../lib/output.js';
7
7
  import { getInstallableServices, arkDependencies, arkServices, } from '../../arkServices.js';
8
- import { isMarketplaceService, extractMarketplaceServiceName, getMarketplaceService, getAllMarketplaceServices, } from '../../marketplaceServices.js';
8
+ import { isMarketplaceService, getMarketplaceItem, getAllMarketplaceServices, getAllMarketplaceAgents, } from '../../marketplaceServices.js';
9
9
  import { printNextSteps } from '../../lib/nextSteps.js';
10
10
  import ora from 'ora';
11
11
  import { waitForServicesReady, } from '../../lib/waitForReady.js';
@@ -42,20 +42,30 @@ export async function installArk(config, serviceName, options = {}) {
42
42
  console.log(); // Add blank line after cluster info
43
43
  // If a specific service is requested, install only that service
44
44
  if (serviceName) {
45
- // Check if it's a marketplace service
45
+ // Check if it's a marketplace item
46
46
  if (isMarketplaceService(serviceName)) {
47
- const marketplaceServiceName = extractMarketplaceServiceName(serviceName);
48
- const service = getMarketplaceService(marketplaceServiceName);
47
+ const service = await getMarketplaceItem(serviceName);
49
48
  if (!service) {
50
- output.error(`marketplace service '${marketplaceServiceName}' not found`);
51
- output.info('available marketplace services:');
52
- const marketplaceServices = getAllMarketplaceServices();
53
- for (const serviceName of Object.keys(marketplaceServices)) {
54
- output.info(` marketplace/services/${serviceName}`);
49
+ output.error(`marketplace item '${serviceName}' not found`);
50
+ output.info('available marketplace items:');
51
+ const marketplaceServices = await getAllMarketplaceServices();
52
+ if (marketplaceServices) {
53
+ for (const name of Object.keys(marketplaceServices)) {
54
+ output.info(` marketplace/services/${name}`);
55
+ }
56
+ }
57
+ const marketplaceAgents = await getAllMarketplaceAgents();
58
+ if (marketplaceAgents) {
59
+ for (const name of Object.keys(marketplaceAgents)) {
60
+ output.info(` marketplace/agents/${name}`);
61
+ }
62
+ }
63
+ if (!marketplaceServices && !marketplaceAgents) {
64
+ output.warning('Marketplace unavailable');
55
65
  }
56
66
  process.exit(1);
57
67
  }
58
- output.info(`installing marketplace service ${service.name}...`);
68
+ output.info(`installing marketplace item ${service.name}...`);
59
69
  try {
60
70
  await installService(service, options.verbose);
61
71
  output.success(`${service.name} installed successfully`);