@agents-at-scale/ark 0.1.35-rc.1 → 0.1.35-rc1

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 (122) hide show
  1. package/dist/arkServices.d.ts +4 -12
  2. package/dist/arkServices.js +19 -34
  3. package/dist/arkServices.spec.d.ts +1 -0
  4. package/dist/arkServices.spec.js +24 -0
  5. package/dist/commands/agents/index.d.ts +2 -1
  6. package/dist/commands/agents/index.js +2 -7
  7. package/dist/commands/agents/index.spec.d.ts +1 -0
  8. package/dist/commands/agents/index.spec.js +67 -0
  9. package/dist/commands/chat/index.d.ts +2 -1
  10. package/dist/commands/chat/index.js +5 -21
  11. package/dist/commands/cluster/get.spec.d.ts +1 -0
  12. package/dist/commands/cluster/get.spec.js +92 -0
  13. package/dist/commands/cluster/index.d.ts +2 -1
  14. package/dist/commands/cluster/index.js +1 -1
  15. package/dist/commands/cluster/index.spec.d.ts +1 -0
  16. package/dist/commands/cluster/index.spec.js +24 -0
  17. package/dist/commands/completion/index.d.ts +2 -1
  18. package/dist/commands/completion/index.js +1 -1
  19. package/dist/commands/completion/index.spec.d.ts +1 -0
  20. package/dist/commands/completion/index.spec.js +34 -0
  21. package/dist/commands/config/index.d.ts +2 -1
  22. package/dist/commands/config/index.js +2 -2
  23. package/dist/commands/config/index.spec.d.ts +1 -0
  24. package/dist/commands/config/index.spec.js +78 -0
  25. package/dist/commands/dashboard/index.d.ts +2 -1
  26. package/dist/commands/dashboard/index.js +1 -1
  27. package/dist/commands/dev/index.d.ts +2 -1
  28. package/dist/commands/dev/index.js +1 -1
  29. package/dist/commands/dev/tool-generate.spec.d.ts +1 -0
  30. package/dist/commands/dev/tool-generate.spec.js +163 -0
  31. package/dist/commands/dev/tool.spec.d.ts +1 -0
  32. package/dist/commands/dev/tool.spec.js +48 -0
  33. package/dist/commands/generate/generators/project.js +22 -41
  34. package/dist/commands/generate/index.d.ts +2 -1
  35. package/dist/commands/generate/index.js +1 -1
  36. package/dist/commands/install/index.d.ts +4 -2
  37. package/dist/commands/install/index.js +215 -78
  38. package/dist/commands/install/index.spec.d.ts +1 -0
  39. package/dist/commands/install/index.spec.js +135 -0
  40. package/dist/commands/models/create.spec.d.ts +1 -0
  41. package/dist/commands/models/create.spec.js +125 -0
  42. package/dist/commands/models/index.d.ts +2 -1
  43. package/dist/commands/models/index.js +2 -7
  44. package/dist/commands/models/index.spec.d.ts +1 -0
  45. package/dist/commands/models/index.spec.js +76 -0
  46. package/dist/commands/routes/index.d.ts +2 -1
  47. package/dist/commands/routes/index.js +1 -9
  48. package/dist/commands/status/index.d.ts +3 -2
  49. package/dist/commands/status/index.js +210 -11
  50. package/dist/commands/targets/index.d.ts +2 -1
  51. package/dist/commands/targets/index.js +1 -1
  52. package/dist/commands/targets/index.spec.d.ts +1 -0
  53. package/dist/commands/targets/index.spec.js +105 -0
  54. package/dist/commands/teams/index.d.ts +2 -1
  55. package/dist/commands/teams/index.js +2 -7
  56. package/dist/commands/teams/index.spec.d.ts +1 -0
  57. package/dist/commands/teams/index.spec.js +70 -0
  58. package/dist/commands/tools/index.d.ts +2 -1
  59. package/dist/commands/tools/index.js +2 -7
  60. package/dist/commands/tools/index.spec.d.ts +1 -0
  61. package/dist/commands/tools/index.spec.js +70 -0
  62. package/dist/commands/uninstall/index.d.ts +2 -1
  63. package/dist/commands/uninstall/index.js +61 -38
  64. package/dist/commands/uninstall/index.spec.d.ts +1 -0
  65. package/dist/commands/uninstall/index.spec.js +117 -0
  66. package/dist/components/ChatUI.js +4 -4
  67. package/dist/components/statusChecker.d.ts +5 -12
  68. package/dist/components/statusChecker.js +172 -89
  69. package/dist/config.d.ts +3 -22
  70. package/dist/config.js +7 -151
  71. package/dist/index.js +22 -19
  72. package/dist/lib/arkServiceProxy.js +4 -2
  73. package/dist/lib/arkStatus.d.ts +5 -0
  74. package/dist/lib/arkStatus.js +61 -2
  75. package/dist/lib/arkStatus.spec.d.ts +1 -0
  76. package/dist/lib/arkStatus.spec.js +49 -0
  77. package/dist/lib/chatClient.js +1 -3
  78. package/dist/lib/cluster.js +11 -14
  79. package/dist/lib/cluster.spec.d.ts +1 -0
  80. package/dist/lib/cluster.spec.js +338 -0
  81. package/dist/lib/commandUtils.js +7 -7
  82. package/dist/lib/commands.d.ts +16 -0
  83. package/dist/lib/commands.js +29 -0
  84. package/dist/lib/commands.spec.d.ts +1 -0
  85. package/dist/lib/commands.spec.js +146 -0
  86. package/dist/lib/config.d.ts +2 -0
  87. package/dist/lib/config.js +6 -4
  88. package/dist/lib/config.spec.d.ts +1 -0
  89. package/dist/lib/config.spec.js +99 -0
  90. package/dist/lib/consts.d.ts +0 -1
  91. package/dist/lib/consts.js +0 -2
  92. package/dist/lib/consts.spec.d.ts +1 -0
  93. package/dist/lib/consts.spec.js +15 -0
  94. package/dist/lib/errors.js +1 -1
  95. package/dist/lib/errors.spec.d.ts +1 -0
  96. package/dist/lib/errors.spec.js +221 -0
  97. package/dist/lib/exec.d.ts +0 -4
  98. package/dist/lib/exec.js +0 -11
  99. package/dist/lib/output.spec.d.ts +1 -0
  100. package/dist/lib/output.spec.js +123 -0
  101. package/dist/lib/portUtils.d.ts +8 -0
  102. package/dist/lib/portUtils.js +39 -0
  103. package/dist/lib/startup.d.ts +5 -0
  104. package/dist/lib/startup.js +73 -0
  105. package/dist/lib/startup.spec.d.ts +1 -0
  106. package/dist/lib/startup.spec.js +168 -0
  107. package/dist/lib/types.d.ts +2 -0
  108. package/dist/ui/AgentSelector.d.ts +8 -0
  109. package/dist/ui/AgentSelector.js +53 -0
  110. package/dist/ui/MainMenu.d.ts +5 -1
  111. package/dist/ui/MainMenu.js +117 -54
  112. package/dist/ui/ModelSelector.d.ts +8 -0
  113. package/dist/ui/ModelSelector.js +53 -0
  114. package/dist/ui/TeamSelector.d.ts +8 -0
  115. package/dist/ui/TeamSelector.js +55 -0
  116. package/dist/ui/ToolSelector.d.ts +8 -0
  117. package/dist/ui/ToolSelector.js +53 -0
  118. package/dist/ui/statusFormatter.d.ts +22 -10
  119. package/dist/ui/statusFormatter.js +37 -109
  120. package/dist/ui/statusFormatter.spec.d.ts +1 -0
  121. package/dist/ui/statusFormatter.spec.js +58 -0
  122. package/package.json +3 -3
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ // Mock chalk to avoid ANSI codes in tests
3
+ jest.unstable_mockModule('chalk', () => ({
4
+ default: {
5
+ gray: (str) => str,
6
+ },
7
+ }));
8
+ // Mock execa using unstable_mockModule
9
+ jest.unstable_mockModule('execa', () => ({
10
+ execa: jest.fn(),
11
+ }));
12
+ // Dynamic imports after mock
13
+ const { execa } = await import('execa');
14
+ const { checkCommandExists, execute } = await import('./commands.js');
15
+ // Type the mock properly
16
+ const mockExeca = execa;
17
+ describe('commands', () => {
18
+ describe('checkCommandExists', () => {
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+ it('returns true when command executes successfully', async () => {
23
+ mockExeca.mockResolvedValue({
24
+ stdout: 'v1.0.0',
25
+ stderr: '',
26
+ exitCode: 0,
27
+ });
28
+ const result = await checkCommandExists('helm', ['version']);
29
+ expect(result).toBe(true);
30
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['version']);
31
+ });
32
+ it('returns false when command fails', async () => {
33
+ mockExeca.mockRejectedValue(new Error('Command not found'));
34
+ const result = await checkCommandExists('nonexistent', ['--version']);
35
+ expect(result).toBe(false);
36
+ expect(mockExeca).toHaveBeenCalledWith('nonexistent', ['--version']);
37
+ });
38
+ it('uses default --version arg when no args provided', async () => {
39
+ mockExeca.mockResolvedValue({
40
+ stdout: '1.0.0',
41
+ stderr: '',
42
+ exitCode: 0,
43
+ });
44
+ const result = await checkCommandExists('node');
45
+ expect(result).toBe(true);
46
+ expect(mockExeca).toHaveBeenCalledWith('node', ['--version']);
47
+ });
48
+ it('uses custom args when provided', async () => {
49
+ mockExeca.mockResolvedValue({
50
+ stdout: 'Client Version: v1.28.0',
51
+ stderr: '',
52
+ exitCode: 0,
53
+ });
54
+ const result = await checkCommandExists('kubectl', [
55
+ 'version',
56
+ '--client',
57
+ ]);
58
+ expect(result).toBe(true);
59
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', [
60
+ 'version',
61
+ '--client',
62
+ ]);
63
+ });
64
+ it('handles empty args array', async () => {
65
+ mockExeca.mockResolvedValue({
66
+ stdout: '',
67
+ stderr: '',
68
+ exitCode: 0,
69
+ });
70
+ const result = await checkCommandExists('echo', []);
71
+ expect(result).toBe(true);
72
+ expect(mockExeca).toHaveBeenCalledWith('echo', []);
73
+ });
74
+ });
75
+ describe('execute', () => {
76
+ let mockConsoleLog;
77
+ beforeEach(() => {
78
+ jest.clearAllMocks();
79
+ mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => { });
80
+ });
81
+ afterEach(() => {
82
+ mockConsoleLog.mockRestore();
83
+ });
84
+ it('executes command without verbose output by default', async () => {
85
+ mockExeca.mockResolvedValue({
86
+ stdout: 'success',
87
+ stderr: '',
88
+ exitCode: 0,
89
+ });
90
+ await execute('helm', ['install', 'test'], { stdio: 'inherit' });
91
+ expect(mockConsoleLog).not.toHaveBeenCalled();
92
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['install', 'test'], {
93
+ stdio: 'inherit',
94
+ });
95
+ });
96
+ it('prints command when verbose is true', async () => {
97
+ mockExeca.mockResolvedValue({
98
+ stdout: 'success',
99
+ stderr: '',
100
+ exitCode: 0,
101
+ });
102
+ await execute('helm', ['install', 'test'], { stdio: 'inherit' }, { verbose: true });
103
+ expect(mockConsoleLog).toHaveBeenCalledWith('$ helm install test');
104
+ expect(mockExeca).toHaveBeenCalledWith('helm', ['install', 'test'], {
105
+ stdio: 'inherit',
106
+ });
107
+ });
108
+ it('works with empty args array', async () => {
109
+ mockExeca.mockResolvedValue({
110
+ stdout: '',
111
+ stderr: '',
112
+ exitCode: 0,
113
+ });
114
+ await execute('ls', [], {}, { verbose: true });
115
+ expect(mockConsoleLog).toHaveBeenCalledWith('$ ls ');
116
+ expect(mockExeca).toHaveBeenCalledWith('ls', [], {});
117
+ });
118
+ it('passes through execa options correctly', async () => {
119
+ mockExeca.mockResolvedValue({
120
+ stdout: '',
121
+ stderr: '',
122
+ exitCode: 0,
123
+ });
124
+ const execaOpts = { stdio: 'pipe', timeout: 5000, cwd: '/tmp' };
125
+ await execute('kubectl', ['get', 'pods'], execaOpts);
126
+ expect(mockConsoleLog).not.toHaveBeenCalled();
127
+ expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'pods'], execaOpts);
128
+ });
129
+ it('handles command failure', async () => {
130
+ const error = new Error('Command failed');
131
+ mockExeca.mockRejectedValue(error);
132
+ await expect(execute('fail', ['now'])).rejects.toThrow('Command failed');
133
+ expect(mockExeca).toHaveBeenCalledWith('fail', ['now'], {});
134
+ });
135
+ it('defaults to no verbose when additionalOptions not provided', async () => {
136
+ mockExeca.mockResolvedValue({
137
+ stdout: 'ok',
138
+ stderr: '',
139
+ exitCode: 0,
140
+ });
141
+ await execute('echo', ['test']);
142
+ expect(mockConsoleLog).not.toHaveBeenCalled();
143
+ expect(mockExeca).toHaveBeenCalledWith('echo', ['test'], {});
144
+ });
145
+ });
146
+ });
@@ -4,6 +4,8 @@ export interface ChatConfig {
4
4
  }
5
5
  export interface ArkConfig {
6
6
  chat?: ChatConfig;
7
+ latestVersion?: string;
8
+ currentVersion?: string;
7
9
  }
8
10
  /**
9
11
  * Load configuration from multiple sources with proper precedence:
@@ -24,8 +24,9 @@ export function loadConfig() {
24
24
  const userConfig = yaml.parse(fs.readFileSync(userConfigPath, 'utf-8'));
25
25
  mergeConfig(config, userConfig);
26
26
  }
27
- catch (_e) {
28
- // Silently ignore invalid config files
27
+ catch (e) {
28
+ const message = e instanceof Error ? e.message : 'Unknown error';
29
+ throw new Error(`Invalid YAML in ${userConfigPath}: ${message}`);
29
30
  }
30
31
  }
31
32
  // Load project config from current directory
@@ -35,8 +36,9 @@ export function loadConfig() {
35
36
  const projectConfig = yaml.parse(fs.readFileSync(projectConfigPath, 'utf-8'));
36
37
  mergeConfig(config, projectConfig);
37
38
  }
38
- catch (_e) {
39
- // Silently ignore invalid config files
39
+ catch (e) {
40
+ const message = e instanceof Error ? e.message : 'Unknown error';
41
+ throw new Error(`Invalid YAML in ${projectConfigPath}: ${message}`);
40
42
  }
41
43
  }
42
44
  // Apply environment variable overrides
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ });
@@ -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.";
@@ -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
+ });
@@ -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
+ });
@@ -1,5 +1 @@
1
- export declare function executeCommand(command: string, args?: string[]): Promise<{
2
- stdout: string;
3
- stderr: string;
4
- }>;
5
1
  export declare function fileExists(path: string): boolean;
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 @@
1
+ export {};