@agents-at-scale/ark 0.1.35-rc.1 → 0.1.35-rc2
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.d.ts +4 -12
- package/dist/arkServices.js +19 -34
- package/dist/arkServices.spec.d.ts +1 -0
- package/dist/arkServices.spec.js +24 -0
- package/dist/commands/agents/index.d.ts +2 -1
- package/dist/commands/agents/index.js +2 -7
- package/dist/commands/agents/index.spec.d.ts +1 -0
- package/dist/commands/agents/index.spec.js +67 -0
- package/dist/commands/chat/index.d.ts +2 -1
- package/dist/commands/chat/index.js +5 -21
- package/dist/commands/cluster/get.spec.d.ts +1 -0
- package/dist/commands/cluster/get.spec.js +92 -0
- package/dist/commands/cluster/index.d.ts +2 -1
- package/dist/commands/cluster/index.js +1 -1
- package/dist/commands/cluster/index.spec.d.ts +1 -0
- package/dist/commands/cluster/index.spec.js +24 -0
- package/dist/commands/completion/index.d.ts +2 -1
- package/dist/commands/completion/index.js +24 -2
- package/dist/commands/completion/index.spec.d.ts +1 -0
- package/dist/commands/completion/index.spec.js +34 -0
- package/dist/commands/config/index.d.ts +2 -1
- package/dist/commands/config/index.js +2 -2
- package/dist/commands/config/index.spec.d.ts +1 -0
- package/dist/commands/config/index.spec.js +78 -0
- package/dist/commands/dashboard/index.d.ts +2 -1
- package/dist/commands/dashboard/index.js +1 -1
- package/dist/commands/dev/index.d.ts +2 -1
- package/dist/commands/dev/index.js +1 -1
- package/dist/commands/dev/tool-generate.spec.d.ts +1 -0
- package/dist/commands/dev/tool-generate.spec.js +163 -0
- package/dist/commands/dev/tool.spec.d.ts +1 -0
- package/dist/commands/dev/tool.spec.js +48 -0
- package/dist/commands/docs/index.d.ts +4 -0
- package/dist/commands/docs/index.js +18 -0
- package/dist/commands/generate/generators/project.js +22 -41
- package/dist/commands/generate/index.d.ts +2 -1
- package/dist/commands/generate/index.js +1 -1
- package/dist/commands/install/index.d.ts +4 -2
- package/dist/commands/install/index.js +225 -90
- package/dist/commands/install/index.spec.d.ts +1 -0
- package/dist/commands/install/index.spec.js +143 -0
- package/dist/commands/models/create.spec.d.ts +1 -0
- package/dist/commands/models/create.spec.js +125 -0
- package/dist/commands/models/index.d.ts +2 -1
- package/dist/commands/models/index.js +2 -7
- package/dist/commands/models/index.spec.d.ts +1 -0
- package/dist/commands/models/index.spec.js +76 -0
- package/dist/commands/query/index.d.ts +3 -0
- package/dist/commands/query/index.js +131 -0
- package/dist/commands/routes/index.d.ts +2 -1
- package/dist/commands/routes/index.js +1 -9
- package/dist/commands/status/index.d.ts +3 -2
- package/dist/commands/status/index.js +240 -11
- package/dist/commands/targets/index.d.ts +2 -1
- package/dist/commands/targets/index.js +1 -1
- package/dist/commands/targets/index.spec.d.ts +1 -0
- package/dist/commands/targets/index.spec.js +105 -0
- package/dist/commands/teams/index.d.ts +2 -1
- package/dist/commands/teams/index.js +2 -7
- package/dist/commands/teams/index.spec.d.ts +1 -0
- package/dist/commands/teams/index.spec.js +70 -0
- package/dist/commands/tools/index.d.ts +2 -1
- package/dist/commands/tools/index.js +2 -7
- package/dist/commands/tools/index.spec.d.ts +1 -0
- package/dist/commands/tools/index.spec.js +70 -0
- package/dist/commands/uninstall/index.d.ts +2 -1
- package/dist/commands/uninstall/index.js +66 -44
- package/dist/commands/uninstall/index.spec.d.ts +1 -0
- package/dist/commands/uninstall/index.spec.js +125 -0
- package/dist/components/ChatUI.js +4 -4
- package/dist/components/statusChecker.d.ts +5 -12
- package/dist/components/statusChecker.js +193 -90
- package/dist/config.d.ts +3 -22
- package/dist/config.js +7 -151
- package/dist/index.js +26 -19
- package/dist/lib/arkServiceProxy.js +4 -2
- package/dist/lib/arkStatus.d.ts +5 -0
- package/dist/lib/arkStatus.js +61 -2
- package/dist/lib/arkStatus.spec.d.ts +1 -0
- package/dist/lib/arkStatus.spec.js +49 -0
- package/dist/lib/chatClient.js +1 -3
- package/dist/lib/cluster.js +11 -14
- package/dist/lib/cluster.spec.d.ts +1 -0
- package/dist/lib/cluster.spec.js +338 -0
- package/dist/lib/commandUtils.js +7 -7
- package/dist/lib/commands.d.ts +16 -0
- package/dist/lib/commands.js +29 -0
- package/dist/lib/commands.spec.d.ts +1 -0
- package/dist/lib/commands.spec.js +146 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.js +6 -4
- package/dist/lib/config.spec.d.ts +1 -0
- package/dist/lib/config.spec.js +99 -0
- package/dist/lib/consts.d.ts +0 -1
- package/dist/lib/consts.js +0 -2
- package/dist/lib/consts.spec.d.ts +1 -0
- package/dist/lib/consts.spec.js +15 -0
- package/dist/lib/errors.js +1 -1
- package/dist/lib/errors.spec.d.ts +1 -0
- package/dist/lib/errors.spec.js +221 -0
- package/dist/lib/exec.d.ts +0 -4
- package/dist/lib/exec.js +0 -11
- package/dist/lib/nextSteps.d.ts +4 -0
- package/dist/lib/nextSteps.js +20 -0
- package/dist/lib/nextSteps.spec.d.ts +1 -0
- package/dist/lib/nextSteps.spec.js +59 -0
- package/dist/lib/output.spec.d.ts +1 -0
- package/dist/lib/output.spec.js +123 -0
- package/dist/lib/portUtils.d.ts +8 -0
- package/dist/lib/portUtils.js +39 -0
- package/dist/lib/startup.d.ts +9 -0
- package/dist/lib/startup.js +93 -0
- package/dist/lib/startup.spec.d.ts +1 -0
- package/dist/lib/startup.spec.js +168 -0
- package/dist/lib/types.d.ts +9 -0
- package/dist/ui/AgentSelector.d.ts +8 -0
- package/dist/ui/AgentSelector.js +53 -0
- package/dist/ui/MainMenu.d.ts +5 -1
- package/dist/ui/MainMenu.js +117 -54
- 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/dist/ui/statusFormatter.d.ts +22 -10
- package/dist/ui/statusFormatter.js +37 -109
- package/dist/ui/statusFormatter.spec.d.ts +1 -0
- package/dist/ui/statusFormatter.spec.js +58 -0
- package/package.json +3 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const mockFs = {
|
|
5
|
+
existsSync: jest.fn(),
|
|
6
|
+
readFileSync: jest.fn(),
|
|
7
|
+
};
|
|
8
|
+
jest.unstable_mockModule('fs', () => ({
|
|
9
|
+
default: mockFs,
|
|
10
|
+
...mockFs,
|
|
11
|
+
}));
|
|
12
|
+
const mockYaml = {
|
|
13
|
+
parse: jest.fn(),
|
|
14
|
+
stringify: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
jest.unstable_mockModule('yaml', () => ({
|
|
17
|
+
default: mockYaml,
|
|
18
|
+
...mockYaml,
|
|
19
|
+
}));
|
|
20
|
+
const { loadConfig, getConfigPaths, formatConfig } = await import('./config.js');
|
|
21
|
+
describe('config', () => {
|
|
22
|
+
const originalEnv = process.env;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
process.env = { ...originalEnv };
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
process.env = originalEnv;
|
|
29
|
+
});
|
|
30
|
+
it('returns default config when no files exist', () => {
|
|
31
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
expect(config).toEqual({
|
|
34
|
+
chat: {
|
|
35
|
+
streaming: true,
|
|
36
|
+
outputFormat: 'text',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it('loads and merges configs in order: defaults, user, project', () => {
|
|
41
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
42
|
+
mockFs.readFileSync
|
|
43
|
+
.mockReturnValueOnce('user yaml')
|
|
44
|
+
.mockReturnValueOnce('project yaml');
|
|
45
|
+
mockYaml.parse
|
|
46
|
+
.mockReturnValueOnce({
|
|
47
|
+
chat: {
|
|
48
|
+
streaming: false,
|
|
49
|
+
outputFormat: 'markdown',
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
.mockReturnValueOnce({
|
|
53
|
+
chat: {
|
|
54
|
+
streaming: true,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const config = loadConfig();
|
|
58
|
+
expect(config.chat?.streaming).toBe(true);
|
|
59
|
+
expect(config.chat?.outputFormat).toBe('markdown');
|
|
60
|
+
});
|
|
61
|
+
it('environment variables override all configs', () => {
|
|
62
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
63
|
+
process.env.ARK_CHAT_STREAMING = '1';
|
|
64
|
+
process.env.ARK_CHAT_OUTPUT_FORMAT = 'MARKDOWN';
|
|
65
|
+
const config = loadConfig();
|
|
66
|
+
expect(config.chat?.streaming).toBe(true);
|
|
67
|
+
expect(config.chat?.outputFormat).toBe('markdown');
|
|
68
|
+
});
|
|
69
|
+
it('throws error for invalid YAML', () => {
|
|
70
|
+
const userConfigPath = path.join(os.homedir(), '.arkrc.yaml');
|
|
71
|
+
mockFs.existsSync.mockImplementation((path) => path === userConfigPath);
|
|
72
|
+
mockFs.readFileSync.mockReturnValue('invalid yaml');
|
|
73
|
+
mockYaml.parse.mockImplementation(() => {
|
|
74
|
+
throw new Error('YAML parse error');
|
|
75
|
+
});
|
|
76
|
+
expect(() => loadConfig()).toThrow(`Invalid YAML in ${userConfigPath}: YAML parse error`);
|
|
77
|
+
});
|
|
78
|
+
it('handles non-Error exceptions', () => {
|
|
79
|
+
const userConfigPath = path.join(os.homedir(), '.arkrc.yaml');
|
|
80
|
+
mockFs.existsSync.mockImplementation((path) => path === userConfigPath);
|
|
81
|
+
mockFs.readFileSync.mockReturnValue('invalid yaml');
|
|
82
|
+
mockYaml.parse.mockImplementation(() => {
|
|
83
|
+
throw 'string error';
|
|
84
|
+
});
|
|
85
|
+
expect(() => loadConfig()).toThrow(`Invalid YAML in ${userConfigPath}: Unknown error`);
|
|
86
|
+
});
|
|
87
|
+
it('getConfigPaths returns correct paths', () => {
|
|
88
|
+
const paths = getConfigPaths();
|
|
89
|
+
expect(paths.user).toBe(path.join(os.homedir(), '.arkrc.yaml'));
|
|
90
|
+
expect(paths.project).toBe(path.join(process.cwd(), '.arkrc.yaml'));
|
|
91
|
+
});
|
|
92
|
+
it('formatConfig uses yaml.stringify', () => {
|
|
93
|
+
const config = { chat: { streaming: true, outputFormat: 'text' } };
|
|
94
|
+
mockYaml.stringify.mockReturnValue('formatted');
|
|
95
|
+
const result = formatConfig(config);
|
|
96
|
+
expect(mockYaml.stringify).toHaveBeenCalledWith(config);
|
|
97
|
+
expect(result).toBe('formatted');
|
|
98
|
+
});
|
|
99
|
+
});
|
package/dist/lib/consts.d.ts
CHANGED
|
@@ -7,4 +7,3 @@ export declare const DEFAULT_ARK_DASHBOARD_URL = "http://localhost:3000";
|
|
|
7
7
|
export declare const DEFAULT_ARK_A2A_URL = "http://localhost:8080";
|
|
8
8
|
export declare const DEFAULT_ARK_MEMORY_URL = "http://localhost:8081";
|
|
9
9
|
export declare const DEFAULT_ARK_OTEL_URL = "http://localhost:4318";
|
|
10
|
-
export declare const ARK_REPO_ERROR_MESSAGE = "Error: This command must be run inside the ARK repository.";
|
package/dist/lib/consts.js
CHANGED
|
@@ -11,5 +11,3 @@ export const DEFAULT_ARK_DASHBOARD_URL = 'http://localhost:3000';
|
|
|
11
11
|
export const DEFAULT_ARK_A2A_URL = 'http://localhost:8080';
|
|
12
12
|
export const DEFAULT_ARK_MEMORY_URL = 'http://localhost:8081';
|
|
13
13
|
export const DEFAULT_ARK_OTEL_URL = 'http://localhost:4318';
|
|
14
|
-
// Error message for different ARK repo
|
|
15
|
-
export const ARK_REPO_ERROR_MESSAGE = 'Error: This command must be run inside the ARK repository.';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { DEFAULT_ADDRESS_ARK_API, DEFAULT_TIMEOUT_MS, DEFAULT_CONNECTION_TEST_TIMEOUT_MS, CONFIG_DIR_NAME, CONFIG_FILE_NAME, DEFAULT_ARK_DASHBOARD_URL, DEFAULT_ARK_A2A_URL, DEFAULT_ARK_MEMORY_URL, DEFAULT_ARK_OTEL_URL, } from './consts.js';
|
|
3
|
+
describe('Constants', () => {
|
|
4
|
+
it('defines correct default values', () => {
|
|
5
|
+
expect(DEFAULT_ADDRESS_ARK_API).toBe('http://localhost:8000');
|
|
6
|
+
expect(DEFAULT_TIMEOUT_MS).toBe(30000);
|
|
7
|
+
expect(DEFAULT_CONNECTION_TEST_TIMEOUT_MS).toBe(5000);
|
|
8
|
+
expect(CONFIG_DIR_NAME).toBe('ark');
|
|
9
|
+
expect(CONFIG_FILE_NAME).toBe('ark-cli.json');
|
|
10
|
+
expect(DEFAULT_ARK_DASHBOARD_URL).toBe('http://localhost:3000');
|
|
11
|
+
expect(DEFAULT_ARK_A2A_URL).toBe('http://localhost:8080');
|
|
12
|
+
expect(DEFAULT_ARK_MEMORY_URL).toBe('http://localhost:8081');
|
|
13
|
+
expect(DEFAULT_ARK_OTEL_URL).toBe('http://localhost:4318');
|
|
14
|
+
});
|
|
15
|
+
});
|
package/dist/lib/errors.js
CHANGED
|
@@ -162,9 +162,9 @@ export class InputValidator {
|
|
|
162
162
|
if (!kebabRegex.test(trimmed)) {
|
|
163
163
|
const suggestions = [];
|
|
164
164
|
const normalized = trimmed
|
|
165
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2') // Handle camelCase first
|
|
165
166
|
.toLowerCase()
|
|
166
167
|
.replace(/[\s_]+/g, '-')
|
|
167
|
-
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
168
168
|
.replace(/-{2,}/g, '-') // Replace 2+ consecutive hyphens with single hyphen (ReDoS-safe)
|
|
169
169
|
.replace(/^-/, '') // Remove single leading hyphen (ReDoS-safe)
|
|
170
170
|
.replace(/-$/, ''); // Remove single trailing hyphen (ReDoS-safe)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import { ErrorCode, ArkError, ValidationError, TemplateError, ProjectStructureError, ErrorHandler, InputValidator, } from './errors.js';
|
|
3
|
+
jest.mock('fs');
|
|
4
|
+
describe('Error Classes', () => {
|
|
5
|
+
describe('ArkError', () => {
|
|
6
|
+
it('creates error with default code', () => {
|
|
7
|
+
const error = new ArkError('test message');
|
|
8
|
+
expect(error.message).toBe('test message');
|
|
9
|
+
expect(error.code).toBe(ErrorCode.UNKNOWN_ERROR);
|
|
10
|
+
expect(error.name).toBe('ArkError');
|
|
11
|
+
expect(error.details).toBeUndefined();
|
|
12
|
+
expect(error.suggestions).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
it('creates error with all properties', () => {
|
|
15
|
+
const error = new ArkError('test error', ErrorCode.INVALID_INPUT, { field: 'name' }, ['Check the input', 'Try again']);
|
|
16
|
+
expect(error.message).toBe('test error');
|
|
17
|
+
expect(error.code).toBe(ErrorCode.INVALID_INPUT);
|
|
18
|
+
expect(error.details).toEqual({ field: 'name' });
|
|
19
|
+
expect(error.suggestions).toEqual(['Check the input', 'Try again']);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('ValidationError', () => {
|
|
23
|
+
it('creates validation error without field', () => {
|
|
24
|
+
const error = new ValidationError('validation failed');
|
|
25
|
+
expect(error.message).toBe('validation failed');
|
|
26
|
+
expect(error.code).toBe(ErrorCode.VALIDATION_ERROR);
|
|
27
|
+
expect(error.name).toBe('ValidationError');
|
|
28
|
+
expect(error.details).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
it('creates validation error with field and suggestions', () => {
|
|
31
|
+
const error = new ValidationError('invalid email', 'email', [
|
|
32
|
+
'Use valid format',
|
|
33
|
+
]);
|
|
34
|
+
expect(error.message).toBe('invalid email');
|
|
35
|
+
expect(error.code).toBe(ErrorCode.VALIDATION_ERROR);
|
|
36
|
+
expect(error.details).toEqual({ field: 'email' });
|
|
37
|
+
expect(error.suggestions).toEqual(['Use valid format']);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('TemplateError', () => {
|
|
41
|
+
it('creates template error', () => {
|
|
42
|
+
const error = new TemplateError('template failed', 'template.yaml');
|
|
43
|
+
expect(error.message).toBe('template failed');
|
|
44
|
+
expect(error.code).toBe(ErrorCode.TEMPLATE_ERROR);
|
|
45
|
+
expect(error.name).toBe('TemplateError');
|
|
46
|
+
expect(error.details).toEqual({ templatePath: 'template.yaml' });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('ProjectStructureError', () => {
|
|
50
|
+
it('creates project structure error with defaults', () => {
|
|
51
|
+
const error = new ProjectStructureError('project invalid');
|
|
52
|
+
expect(error.message).toBe('project invalid');
|
|
53
|
+
expect(error.code).toBe(ErrorCode.PROJECT_STRUCTURE_INVALID);
|
|
54
|
+
expect(error.name).toBe('ProjectStructureError');
|
|
55
|
+
expect(error.suggestions).toEqual([
|
|
56
|
+
'Ensure you are in a valid ARK project directory',
|
|
57
|
+
'Run "ark generate project" to create a new project',
|
|
58
|
+
'Check that Chart.yaml and agents/ directory exist',
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
it('creates project structure error with path', () => {
|
|
62
|
+
const error = new ProjectStructureError('project invalid', '/path/to/project');
|
|
63
|
+
expect(error.message).toBe('project invalid');
|
|
64
|
+
expect(error.code).toBe(ErrorCode.PROJECT_STRUCTURE_INVALID);
|
|
65
|
+
expect(error.details).toEqual({ projectPath: '/path/to/project' });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('ErrorHandler', () => {
|
|
69
|
+
it('formats basic error', () => {
|
|
70
|
+
const error = new Error('simple error');
|
|
71
|
+
const formatted = ErrorHandler.formatError(error);
|
|
72
|
+
expect(formatted).toContain('❌ simple error');
|
|
73
|
+
});
|
|
74
|
+
it('formats ArkError with details and suggestions', () => {
|
|
75
|
+
const error = new ArkError('test error', ErrorCode.INVALID_INPUT, { field: 'name', value: 'test' }, ['Fix the input', 'Try again']);
|
|
76
|
+
const formatted = ErrorHandler.formatError(error);
|
|
77
|
+
expect(formatted).toContain('❌ test error');
|
|
78
|
+
expect(formatted).toContain('Details:');
|
|
79
|
+
expect(formatted).toContain('field: name');
|
|
80
|
+
expect(formatted).toContain('value: test');
|
|
81
|
+
expect(formatted).toContain('💡 Suggestions:');
|
|
82
|
+
expect(formatted).toContain('• Fix the input');
|
|
83
|
+
expect(formatted).toContain('• Try again');
|
|
84
|
+
});
|
|
85
|
+
it('includes stack trace in debug mode', () => {
|
|
86
|
+
process.env.DEBUG = 'true';
|
|
87
|
+
const error = new Error('debug error');
|
|
88
|
+
const formatted = ErrorHandler.formatError(error);
|
|
89
|
+
expect(formatted).toContain('Stack trace:');
|
|
90
|
+
delete process.env.DEBUG;
|
|
91
|
+
});
|
|
92
|
+
it('handles missing stack trace', () => {
|
|
93
|
+
process.env.NODE_ENV = 'development';
|
|
94
|
+
const error = new Error('no stack');
|
|
95
|
+
error.stack = undefined;
|
|
96
|
+
const formatted = ErrorHandler.formatError(error);
|
|
97
|
+
expect(formatted).toContain('No stack trace available');
|
|
98
|
+
delete process.env.NODE_ENV;
|
|
99
|
+
});
|
|
100
|
+
describe('handleAndExit', () => {
|
|
101
|
+
let mockExit;
|
|
102
|
+
let mockConsoleError;
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
105
|
+
throw new Error('process.exit');
|
|
106
|
+
});
|
|
107
|
+
mockConsoleError = jest
|
|
108
|
+
.spyOn(console, 'error')
|
|
109
|
+
.mockImplementation(() => { });
|
|
110
|
+
});
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
mockExit.mockRestore();
|
|
113
|
+
mockConsoleError.mockRestore();
|
|
114
|
+
});
|
|
115
|
+
it('exits with code 22 for validation errors', () => {
|
|
116
|
+
const error = new ValidationError('invalid');
|
|
117
|
+
expect(() => ErrorHandler.handleAndExit(error)).toThrow('process.exit');
|
|
118
|
+
expect(mockExit).toHaveBeenCalledWith(22);
|
|
119
|
+
});
|
|
120
|
+
it('exits with code 2 for file not found', () => {
|
|
121
|
+
const error = new ArkError('not found', ErrorCode.FILE_NOT_FOUND);
|
|
122
|
+
expect(() => ErrorHandler.handleAndExit(error)).toThrow('process.exit');
|
|
123
|
+
expect(mockExit).toHaveBeenCalledWith(2);
|
|
124
|
+
});
|
|
125
|
+
it('exits with code 13 for permission denied', () => {
|
|
126
|
+
const error = new ArkError('denied', ErrorCode.PERMISSION_DENIED);
|
|
127
|
+
expect(() => ErrorHandler.handleAndExit(error)).toThrow('process.exit');
|
|
128
|
+
expect(mockExit).toHaveBeenCalledWith(13);
|
|
129
|
+
});
|
|
130
|
+
it('exits with code 127 for missing dependency', () => {
|
|
131
|
+
const error = new ArkError('missing', ErrorCode.DEPENDENCY_MISSING);
|
|
132
|
+
expect(() => ErrorHandler.handleAndExit(error)).toThrow('process.exit');
|
|
133
|
+
expect(mockExit).toHaveBeenCalledWith(127);
|
|
134
|
+
});
|
|
135
|
+
it('exits with code 1 for unknown errors', () => {
|
|
136
|
+
const error = new Error('unknown');
|
|
137
|
+
expect(() => ErrorHandler.handleAndExit(error)).toThrow('process.exit');
|
|
138
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
139
|
+
});
|
|
140
|
+
it('exits with code 1 for default ArkError', () => {
|
|
141
|
+
const error = new ArkError('general', ErrorCode.UNKNOWN_ERROR);
|
|
142
|
+
expect(() => ErrorHandler.handleAndExit(error)).toThrow('process.exit');
|
|
143
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('catchAndHandle', () => {
|
|
147
|
+
it('returns successful promise result', async () => {
|
|
148
|
+
const result = await ErrorHandler.catchAndHandle(async () => 'success');
|
|
149
|
+
expect(result).toBe('success');
|
|
150
|
+
});
|
|
151
|
+
it('rethrows ArkError unchanged', async () => {
|
|
152
|
+
const arkError = new ValidationError('test');
|
|
153
|
+
await expect(ErrorHandler.catchAndHandle(async () => {
|
|
154
|
+
throw arkError;
|
|
155
|
+
})).rejects.toThrow(arkError);
|
|
156
|
+
});
|
|
157
|
+
it('wraps generic errors with context', async () => {
|
|
158
|
+
const error = new Error('generic');
|
|
159
|
+
await expect(ErrorHandler.catchAndHandle(async () => {
|
|
160
|
+
throw error;
|
|
161
|
+
}, 'context')).rejects.toThrow('context: generic');
|
|
162
|
+
});
|
|
163
|
+
it('wraps non-Error objects', async () => {
|
|
164
|
+
await expect(ErrorHandler.catchAndHandle(async () => {
|
|
165
|
+
throw 'string error';
|
|
166
|
+
})).rejects.toThrow('string error');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe('InputValidator', () => {
|
|
171
|
+
describe('validateName', () => {
|
|
172
|
+
it('accepts valid names', () => {
|
|
173
|
+
expect(() => InputValidator.validateName('valid-name')).not.toThrow();
|
|
174
|
+
expect(() => InputValidator.validateName('test123')).not.toThrow();
|
|
175
|
+
expect(() => InputValidator.validateName('a-b-c-123')).not.toThrow();
|
|
176
|
+
});
|
|
177
|
+
it('rejects empty names', () => {
|
|
178
|
+
expect(() => InputValidator.validateName('')).toThrow('name cannot be empty');
|
|
179
|
+
expect(() => InputValidator.validateName(' ')).toThrow('name cannot be empty');
|
|
180
|
+
});
|
|
181
|
+
it('rejects names over 63 characters', () => {
|
|
182
|
+
const longName = 'a'.repeat(64);
|
|
183
|
+
expect(() => InputValidator.validateName(longName)).toThrow('must be 63 characters or less');
|
|
184
|
+
});
|
|
185
|
+
it('rejects invalid characters', () => {
|
|
186
|
+
expect(() => InputValidator.validateName('Invalid Name')).toThrow('Invalid name');
|
|
187
|
+
expect(() => InputValidator.validateName('test_name')).toThrow('Invalid name');
|
|
188
|
+
expect(() => InputValidator.validateName('-start')).toThrow('Invalid name');
|
|
189
|
+
expect(() => InputValidator.validateName('end-')).toThrow('Invalid name');
|
|
190
|
+
});
|
|
191
|
+
it('suggests normalized names', () => {
|
|
192
|
+
try {
|
|
193
|
+
InputValidator.validateName('TestName');
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
expect(e.suggestions).toContain('Try: "test-name"');
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('validatePath', () => {
|
|
201
|
+
it('accepts valid paths', () => {
|
|
202
|
+
expect(() => InputValidator.validatePath('/valid/path')).not.toThrow();
|
|
203
|
+
expect(() => InputValidator.validatePath('./relative')).not.toThrow();
|
|
204
|
+
expect(() => InputValidator.validatePath('simple')).not.toThrow();
|
|
205
|
+
});
|
|
206
|
+
it('rejects empty paths', () => {
|
|
207
|
+
expect(() => InputValidator.validatePath('')).toThrow('path cannot be empty');
|
|
208
|
+
});
|
|
209
|
+
it('rejects dangerous paths', () => {
|
|
210
|
+
expect(() => InputValidator.validatePath('../parent')).toThrow('unsafe characters');
|
|
211
|
+
expect(() => InputValidator.validatePath('~/home')).toThrow('unsafe characters');
|
|
212
|
+
expect(() => InputValidator.validatePath('$HOME/test')).toThrow('unsafe characters');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('validateDirectory', () => {
|
|
216
|
+
it('validates path first', () => {
|
|
217
|
+
expect(() => InputValidator.validateDirectory('')).toThrow('directory cannot be empty');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
package/dist/lib/exec.d.ts
CHANGED
package/dist/lib/exec.js
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import { execa } from 'execa';
|
|
3
|
-
export async function executeCommand(command, args = []) {
|
|
4
|
-
try {
|
|
5
|
-
const result = await execa(command, args);
|
|
6
|
-
return { stdout: result.stdout, stderr: result.stderr };
|
|
7
|
-
}
|
|
8
|
-
catch (error) {
|
|
9
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
10
|
-
throw new Error(`Command failed: ${command} ${args.join(' ')}\n${errorMessage}`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
2
|
export function fileExists(path) {
|
|
14
3
|
try {
|
|
15
4
|
return fs.existsSync(path);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
/**
|
|
3
|
+
* Print helpful next steps after successful ARK installation
|
|
4
|
+
*/
|
|
5
|
+
export function printNextSteps() {
|
|
6
|
+
console.log();
|
|
7
|
+
console.log(chalk.green.bold('✓ ARK installed successfully!'));
|
|
8
|
+
console.log();
|
|
9
|
+
console.log(chalk.gray('Next steps:'));
|
|
10
|
+
console.log();
|
|
11
|
+
console.log(` ${chalk.gray('docs:')} ${chalk.blue('https://mckinsey.github.io/agents-at-scale-ark/')}`);
|
|
12
|
+
console.log(` ${chalk.gray('create model:')} ${chalk.white.bold('ark models create default')}`);
|
|
13
|
+
console.log(` ${chalk.gray('open dashboard:')} ${chalk.white.bold('ark dashboard')}`);
|
|
14
|
+
console.log(` ${chalk.gray('show agents:')} ${chalk.white.bold('kubectl get agents')}`);
|
|
15
|
+
console.log(` ${chalk.gray('run a query:')} ${chalk.white.bold('ark query model/default "What are large language models?"')}`);
|
|
16
|
+
console.log(` ${chalk.gray('interactive chat:')} ${chalk.white.bold('ark')} ${chalk.gray('# then choose \'Chat\'')}`);
|
|
17
|
+
console.log(` ${chalk.gray('new project:')} ${chalk.white.bold('ark generate project my-agents')}`);
|
|
18
|
+
console.log(` ${chalk.gray('install fark:')} ${chalk.blue('https://mckinsey.github.io/agents-at-scale-ark/developer-guide/cli-tools/')}`);
|
|
19
|
+
console.log();
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { printNextSteps } from './nextSteps.js';
|
|
3
|
+
describe('printNextSteps', () => {
|
|
4
|
+
let consoleLogSpy;
|
|
5
|
+
let output = [];
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
output = [];
|
|
8
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation((...args) => {
|
|
9
|
+
output.push(args.join(' '));
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
consoleLogSpy.mockRestore();
|
|
14
|
+
});
|
|
15
|
+
it('prints successful installation message', () => {
|
|
16
|
+
printNextSteps();
|
|
17
|
+
const fullOutput = output.join('\n');
|
|
18
|
+
expect(fullOutput).toContain('ARK installed successfully!');
|
|
19
|
+
});
|
|
20
|
+
it('includes all required commands', () => {
|
|
21
|
+
printNextSteps();
|
|
22
|
+
const fullOutput = output.join('\n');
|
|
23
|
+
// Check for each command
|
|
24
|
+
expect(fullOutput).toContain('ark models create default');
|
|
25
|
+
expect(fullOutput).toContain('ark dashboard');
|
|
26
|
+
expect(fullOutput).toContain('kubectl get agents');
|
|
27
|
+
expect(fullOutput).toContain('ark query model/default "What are large language models?"');
|
|
28
|
+
expect(fullOutput).toContain('ark');
|
|
29
|
+
expect(fullOutput).toContain("# then choose 'Chat'");
|
|
30
|
+
expect(fullOutput).toContain('ark generate project my-agents');
|
|
31
|
+
});
|
|
32
|
+
it('includes all required links', () => {
|
|
33
|
+
printNextSteps();
|
|
34
|
+
const fullOutput = output.join('\n');
|
|
35
|
+
// Check for documentation links
|
|
36
|
+
expect(fullOutput).toContain('https://mckinsey.github.io/agents-at-scale-ark/');
|
|
37
|
+
expect(fullOutput).toContain('https://mckinsey.github.io/agents-at-scale-ark/developer-guide/cli-tools/');
|
|
38
|
+
});
|
|
39
|
+
it('includes all section labels', () => {
|
|
40
|
+
printNextSteps();
|
|
41
|
+
const fullOutput = output.join('\n');
|
|
42
|
+
// Check for labels
|
|
43
|
+
expect(fullOutput).toContain('Next steps:');
|
|
44
|
+
expect(fullOutput).toContain('docs:');
|
|
45
|
+
expect(fullOutput).toContain('create model:');
|
|
46
|
+
expect(fullOutput).toContain('open dashboard:');
|
|
47
|
+
expect(fullOutput).toContain('show agents:');
|
|
48
|
+
expect(fullOutput).toContain('run a query:');
|
|
49
|
+
expect(fullOutput).toContain('interactive chat:');
|
|
50
|
+
expect(fullOutput).toContain('new project:');
|
|
51
|
+
expect(fullOutput).toContain('install fark:');
|
|
52
|
+
});
|
|
53
|
+
it('has correct structure with empty lines', () => {
|
|
54
|
+
printNextSteps();
|
|
55
|
+
// Should have empty lines for formatting
|
|
56
|
+
expect(output[0]).toBe('');
|
|
57
|
+
expect(output[output.length - 1]).toBe('');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import output from './output.js';
|
|
4
|
+
describe('output', () => {
|
|
5
|
+
let consoleErrorSpy;
|
|
6
|
+
let consoleLogSpy;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
consoleErrorSpy = jest
|
|
9
|
+
.spyOn(console, 'error')
|
|
10
|
+
.mockImplementation(() => undefined);
|
|
11
|
+
consoleLogSpy = jest
|
|
12
|
+
.spyOn(console, 'log')
|
|
13
|
+
.mockImplementation(() => undefined);
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
describe('error', () => {
|
|
19
|
+
it('should output error message with red cross and prefix', () => {
|
|
20
|
+
output.error('Something went wrong');
|
|
21
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(chalk.red('✗'), chalk.red('error:'), 'Something went wrong');
|
|
22
|
+
});
|
|
23
|
+
it('should handle additional arguments', () => {
|
|
24
|
+
const error = new Error('Test error');
|
|
25
|
+
output.error('Failed to connect', error, 123);
|
|
26
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(chalk.red('✗'), chalk.red('error:'), 'Failed to connect', error, 123);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('success', () => {
|
|
30
|
+
it('should output success message with green checkmark', () => {
|
|
31
|
+
output.success('Operation completed');
|
|
32
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.green('✓'), 'Operation completed');
|
|
33
|
+
});
|
|
34
|
+
it('should handle additional arguments', () => {
|
|
35
|
+
output.success('Deployed', 'v1.0.0', { status: 'ok' });
|
|
36
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.green('✓'), 'Deployed', 'v1.0.0', { status: 'ok' });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('info', () => {
|
|
40
|
+
it('should output info message in gray', () => {
|
|
41
|
+
output.info('Processing request...');
|
|
42
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.gray('Processing request...'));
|
|
43
|
+
});
|
|
44
|
+
it('should handle additional arguments', () => {
|
|
45
|
+
output.info('Status:', 'running', 42);
|
|
46
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.gray('Status:'), 'running', 42);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('warning', () => {
|
|
50
|
+
it('should output warning message with yellow exclamation and prefix', () => {
|
|
51
|
+
output.warning('Resource limit approaching');
|
|
52
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.yellow.bold('!'), chalk.yellow('warning:'), 'Resource limit approaching');
|
|
53
|
+
});
|
|
54
|
+
it('should handle additional arguments', () => {
|
|
55
|
+
const details = { cpu: '85%', memory: '92%' };
|
|
56
|
+
output.warning('High resource usage', details);
|
|
57
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.yellow.bold('!'), chalk.yellow('warning:'), 'High resource usage', details);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('statusCheck', () => {
|
|
61
|
+
it('should display found status with value and details', () => {
|
|
62
|
+
output.statusCheck('found', 'platform', 'python3', 'v3.10');
|
|
63
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(` ${chalk.green('✓')} ${chalk.green('platform')} ${chalk.bold.white('python3')}${chalk.gray(' v3.10')}`);
|
|
64
|
+
});
|
|
65
|
+
it('should display found status with value only', () => {
|
|
66
|
+
output.statusCheck('found', 'fastmcp', 'v0.1.0');
|
|
67
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(` ${chalk.green('✓')} ${chalk.green('fastmcp')} ${chalk.bold.white('v0.1.0')}`);
|
|
68
|
+
});
|
|
69
|
+
it('should display found status with label only', () => {
|
|
70
|
+
output.statusCheck('found', '3 tools');
|
|
71
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(` ${chalk.green('✓')} ${chalk.green('3 tools')}`);
|
|
72
|
+
});
|
|
73
|
+
it('should display missing status with details', () => {
|
|
74
|
+
output.statusCheck('missing', 'fastmcp', undefined, 'not in dependencies');
|
|
75
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(` ${chalk.yellow('?')} ${chalk.yellow('fastmcp')}${chalk.gray(' not in dependencies')}`);
|
|
76
|
+
});
|
|
77
|
+
it('should display warning status', () => {
|
|
78
|
+
output.statusCheck('warning', 'version', '0.0.1', 'pre-release');
|
|
79
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(` ${chalk.yellow('!')} ${chalk.yellow('version')} ${chalk.bold.white('0.0.1')}${chalk.gray(' pre-release')}`);
|
|
80
|
+
});
|
|
81
|
+
it('should display error status', () => {
|
|
82
|
+
output.statusCheck('error', 'tools', undefined, 'discovery failed');
|
|
83
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(` ${chalk.red('✗')} ${chalk.red('tools')}${chalk.gray(' discovery failed')}`);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('section', () => {
|
|
87
|
+
it('should display section header with colon', () => {
|
|
88
|
+
output.section('dev-tests');
|
|
89
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.cyan.bold('dev-tests:'));
|
|
90
|
+
});
|
|
91
|
+
it('should display section header with custom text', () => {
|
|
92
|
+
output.section('ark services');
|
|
93
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.cyan.bold('ark services:'));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('statusMessage', () => {
|
|
97
|
+
it('should output success status with title and message', () => {
|
|
98
|
+
output.statusMessage('success', 'fastmcp', 'installed (v0.1.0)');
|
|
99
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.green('✓'), chalk.green('fastmcp:'), 'installed (v0.1.0)');
|
|
100
|
+
});
|
|
101
|
+
it('should output warning status with title and message', () => {
|
|
102
|
+
output.statusMessage('warning', 'virtual environment', 'not found');
|
|
103
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.yellow.bold('!'), chalk.yellow('virtual environment:'), 'not found');
|
|
104
|
+
});
|
|
105
|
+
it('should output error status with title and message', () => {
|
|
106
|
+
output.statusMessage('error', 'fastmcp', 'not found in dependencies');
|
|
107
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(chalk.red('✗'), chalk.red('fastmcp:'), 'not found in dependencies');
|
|
108
|
+
});
|
|
109
|
+
it('should output info status with title and message', () => {
|
|
110
|
+
output.statusMessage('info', 'platform', 'python3');
|
|
111
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.blue('ℹ'), chalk.blue('platform:'), 'python3');
|
|
112
|
+
});
|
|
113
|
+
it('should handle title-only messages', () => {
|
|
114
|
+
output.statusMessage('success', 'Operation completed');
|
|
115
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.green('✓'), 'Operation completed');
|
|
116
|
+
});
|
|
117
|
+
it('should handle additional arguments', () => {
|
|
118
|
+
const extra = { version: '1.0' };
|
|
119
|
+
output.statusMessage('info', 'status', 'running', extra, 42);
|
|
120
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(chalk.blue('ℹ'), chalk.blue('status:'), 'running', extra, 42);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a port is available
|
|
3
|
+
*/
|
|
4
|
+
export declare function isPortAvailable(port: number): Promise<boolean>;
|
|
5
|
+
/**
|
|
6
|
+
* Find an available port, starting from the preferred port
|
|
7
|
+
*/
|
|
8
|
+
export declare function findAvailablePort(preferredPort: number, maxAttempts?: number): Promise<number>;
|