@agents-at-scale/ark 0.1.35 → 0.1.36

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 (167) hide show
  1. package/dist/arkServices.d.ts +42 -0
  2. package/dist/arkServices.js +138 -0
  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 +3 -0
  6. package/dist/commands/agents/index.js +65 -0
  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 +3 -0
  10. package/dist/commands/chat/index.js +29 -0
  11. package/dist/commands/cluster/get.d.ts +2 -0
  12. package/dist/commands/cluster/get.js +39 -0
  13. package/dist/commands/cluster/get.spec.d.ts +1 -0
  14. package/dist/commands/cluster/get.spec.js +92 -0
  15. package/dist/commands/cluster/index.d.ts +2 -1
  16. package/dist/commands/cluster/index.js +3 -5
  17. package/dist/commands/cluster/index.spec.d.ts +1 -0
  18. package/dist/commands/cluster/index.spec.js +24 -0
  19. package/dist/commands/completion/index.d.ts +3 -0
  20. package/dist/commands/completion/index.js +230 -0
  21. package/dist/commands/completion/index.spec.d.ts +1 -0
  22. package/dist/commands/completion/index.spec.js +34 -0
  23. package/dist/commands/config/index.d.ts +3 -0
  24. package/dist/commands/config/index.js +42 -0
  25. package/dist/commands/config/index.spec.d.ts +1 -0
  26. package/dist/commands/config/index.spec.js +78 -0
  27. package/dist/commands/dashboard/index.d.ts +4 -0
  28. package/dist/commands/dashboard/index.js +39 -0
  29. package/dist/commands/docs/index.d.ts +4 -0
  30. package/dist/commands/docs/index.js +18 -0
  31. package/dist/commands/generate/config.js +5 -24
  32. package/dist/commands/generate/generators/mcpserver.d.ts +2 -1
  33. package/dist/commands/generate/generators/mcpserver.js +26 -5
  34. package/dist/commands/generate/generators/project.js +22 -41
  35. package/dist/commands/generate/index.d.ts +2 -1
  36. package/dist/commands/generate/index.js +1 -1
  37. package/dist/commands/install/index.d.ts +8 -0
  38. package/dist/commands/install/index.js +295 -0
  39. package/dist/commands/install/index.spec.d.ts +1 -0
  40. package/dist/commands/install/index.spec.js +143 -0
  41. package/dist/commands/models/create.d.ts +1 -0
  42. package/dist/commands/models/create.js +213 -0
  43. package/dist/commands/models/create.spec.d.ts +1 -0
  44. package/dist/commands/models/create.spec.js +125 -0
  45. package/dist/commands/models/index.d.ts +3 -0
  46. package/dist/commands/models/index.js +75 -0
  47. package/dist/commands/models/index.spec.d.ts +1 -0
  48. package/dist/commands/models/index.spec.js +96 -0
  49. package/dist/commands/query/index.d.ts +3 -0
  50. package/dist/commands/query/index.js +24 -0
  51. package/dist/commands/query/index.spec.d.ts +1 -0
  52. package/dist/commands/query/index.spec.js +53 -0
  53. package/dist/commands/routes/index.d.ts +3 -0
  54. package/dist/commands/routes/index.js +93 -0
  55. package/dist/commands/status/index.d.ts +3 -0
  56. package/dist/commands/status/index.js +281 -0
  57. package/dist/commands/targets/index.d.ts +3 -0
  58. package/dist/commands/targets/index.js +72 -0
  59. package/dist/commands/targets/index.spec.d.ts +1 -0
  60. package/dist/commands/targets/index.spec.js +154 -0
  61. package/dist/commands/teams/index.d.ts +3 -0
  62. package/dist/commands/teams/index.js +64 -0
  63. package/dist/commands/teams/index.spec.d.ts +1 -0
  64. package/dist/commands/teams/index.spec.js +70 -0
  65. package/dist/commands/tools/index.d.ts +3 -0
  66. package/dist/commands/tools/index.js +49 -0
  67. package/dist/commands/tools/index.spec.d.ts +1 -0
  68. package/dist/commands/tools/index.spec.js +70 -0
  69. package/dist/commands/uninstall/index.d.ts +3 -0
  70. package/dist/commands/uninstall/index.js +101 -0
  71. package/dist/commands/uninstall/index.spec.d.ts +1 -0
  72. package/dist/commands/uninstall/index.spec.js +125 -0
  73. package/dist/components/ChatUI.d.ts +16 -0
  74. package/dist/components/ChatUI.js +801 -0
  75. package/dist/components/statusChecker.d.ts +14 -24
  76. package/dist/components/statusChecker.js +295 -129
  77. package/dist/index.d.ts +1 -1
  78. package/dist/index.js +42 -42
  79. package/dist/lib/arkApiClient.d.ts +53 -0
  80. package/dist/lib/arkApiClient.js +102 -0
  81. package/dist/lib/arkApiProxy.d.ts +9 -0
  82. package/dist/lib/arkApiProxy.js +22 -0
  83. package/dist/lib/arkServiceProxy.d.ts +14 -0
  84. package/dist/lib/arkServiceProxy.js +95 -0
  85. package/dist/lib/arkStatus.d.ts +10 -0
  86. package/dist/lib/arkStatus.js +79 -0
  87. package/dist/lib/arkStatus.spec.d.ts +1 -0
  88. package/dist/lib/arkStatus.spec.js +49 -0
  89. package/dist/lib/chatClient.d.ts +33 -0
  90. package/dist/lib/chatClient.js +93 -0
  91. package/dist/lib/cluster.d.ts +2 -1
  92. package/dist/lib/cluster.js +37 -16
  93. package/dist/lib/cluster.spec.d.ts +1 -0
  94. package/dist/lib/cluster.spec.js +338 -0
  95. package/dist/lib/commands.d.ts +16 -0
  96. package/dist/lib/commands.js +29 -0
  97. package/dist/lib/commands.spec.d.ts +1 -0
  98. package/dist/lib/commands.spec.js +146 -0
  99. package/dist/lib/config.d.ts +26 -80
  100. package/dist/lib/config.js +70 -205
  101. package/dist/lib/config.spec.d.ts +1 -0
  102. package/dist/lib/config.spec.js +99 -0
  103. package/dist/lib/errors.js +1 -1
  104. package/dist/lib/errors.spec.d.ts +1 -0
  105. package/dist/lib/errors.spec.js +221 -0
  106. package/dist/lib/executeQuery.d.ts +20 -0
  107. package/dist/lib/executeQuery.js +135 -0
  108. package/dist/lib/executeQuery.spec.d.ts +1 -0
  109. package/dist/lib/executeQuery.spec.js +170 -0
  110. package/dist/lib/nextSteps.d.ts +4 -0
  111. package/dist/lib/nextSteps.js +20 -0
  112. package/dist/lib/nextSteps.spec.d.ts +1 -0
  113. package/dist/lib/nextSteps.spec.js +59 -0
  114. package/dist/lib/output.d.ts +36 -0
  115. package/dist/lib/output.js +89 -0
  116. package/dist/lib/output.spec.d.ts +1 -0
  117. package/dist/lib/output.spec.js +123 -0
  118. package/dist/lib/startup.d.ts +9 -0
  119. package/dist/lib/startup.js +87 -0
  120. package/dist/lib/startup.spec.d.ts +1 -0
  121. package/dist/lib/startup.spec.js +152 -0
  122. package/dist/lib/types.d.ts +87 -3
  123. package/dist/lib/versions.d.ts +23 -0
  124. package/dist/lib/versions.js +51 -0
  125. package/dist/types/types.d.ts +40 -0
  126. package/dist/types/types.js +1 -0
  127. package/dist/ui/AgentSelector.d.ts +8 -0
  128. package/dist/ui/AgentSelector.js +53 -0
  129. package/dist/ui/MainMenu.d.ts +5 -1
  130. package/dist/ui/MainMenu.js +226 -91
  131. package/dist/ui/ModelSelector.d.ts +8 -0
  132. package/dist/ui/ModelSelector.js +53 -0
  133. package/dist/ui/TeamSelector.d.ts +8 -0
  134. package/dist/ui/TeamSelector.js +55 -0
  135. package/dist/ui/ToolSelector.d.ts +8 -0
  136. package/dist/ui/ToolSelector.js +53 -0
  137. package/dist/ui/statusFormatter.d.ts +22 -7
  138. package/dist/ui/statusFormatter.js +39 -39
  139. package/dist/ui/statusFormatter.spec.d.ts +1 -0
  140. package/dist/ui/statusFormatter.spec.js +58 -0
  141. package/package.json +16 -5
  142. package/dist/commands/cluster/get-ip.d.ts +0 -2
  143. package/dist/commands/cluster/get-ip.js +0 -32
  144. package/dist/commands/cluster/get-type.d.ts +0 -2
  145. package/dist/commands/cluster/get-type.js +0 -26
  146. package/dist/commands/completion.d.ts +0 -2
  147. package/dist/commands/completion.js +0 -108
  148. package/dist/commands/config.d.ts +0 -5
  149. package/dist/commands/config.js +0 -327
  150. package/dist/components/DashboardCLI.d.ts +0 -3
  151. package/dist/components/DashboardCLI.js +0 -149
  152. package/dist/config.d.ts +0 -42
  153. package/dist/config.js +0 -243
  154. package/dist/lib/arkClient.d.ts +0 -32
  155. package/dist/lib/arkClient.js +0 -43
  156. package/dist/lib/consts.d.ts +0 -10
  157. package/dist/lib/consts.js +0 -15
  158. package/dist/lib/exec.d.ts +0 -5
  159. package/dist/lib/exec.js +0 -20
  160. package/dist/lib/gatewayManager.d.ts +0 -24
  161. package/dist/lib/gatewayManager.js +0 -85
  162. package/dist/lib/kubernetes.d.ts +0 -28
  163. package/dist/lib/kubernetes.js +0 -122
  164. package/dist/lib/progress.d.ts +0 -128
  165. package/dist/lib/progress.js +0 -273
  166. package/dist/lib/wrappers/git.d.ts +0 -2
  167. package/dist/lib/wrappers/git.js +0 -43
@@ -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
+ });
@@ -1,82 +1,28 @@
1
- /**
2
- * Configuration management for ARK CLI
3
- */
4
- export interface ArkConfig {
5
- defaultProjectType: 'empty' | 'with-samples';
6
- defaultDestination: string;
7
- skipGitByDefault: boolean;
8
- skipModelsbyDefault: boolean;
9
- preferredEditor: string;
10
- colorOutput: boolean;
11
- verboseOutput: boolean;
12
- defaultModelProvider: 'azure' | 'openai' | 'claude' | 'gemini' | 'custom';
13
- templateDirectory?: string;
14
- customTemplates: Record<string, string>;
15
- parallelOperations: boolean;
16
- maxConcurrentFiles: number;
17
- fileWatchingEnabled: boolean;
18
- telemetryEnabled: boolean;
19
- errorReporting: boolean;
1
+ import type { ClusterInfo } from './cluster.js';
2
+ export interface ChatConfig {
3
+ streaming?: boolean;
4
+ outputFormat?: 'text' | 'markdown';
20
5
  }
21
- export declare const DEFAULT_CONFIG: ArkConfig;
22
- export declare class ConfigManager {
23
- private configPath;
24
- private config;
25
- constructor();
26
- /**
27
- * Get the path to the configuration file
28
- */
29
- private getConfigPath;
30
- /**
31
- * Load configuration from file or create with defaults
32
- */
33
- private loadConfig;
34
- /**
35
- * Save configuration to file
36
- */
37
- private saveConfig;
38
- /**
39
- * Get the current configuration
40
- */
41
- getConfig(): ArkConfig;
42
- /**
43
- * Update configuration
44
- */
45
- updateConfig(updates: Partial<ArkConfig>): void;
46
- /**
47
- * Reset configuration to defaults
48
- */
49
- resetConfig(): void;
50
- /**
51
- * Get a specific configuration value
52
- */
53
- get<K extends keyof ArkConfig>(key: K): ArkConfig[K];
54
- /**
55
- * Set a specific configuration value
56
- */
57
- set<K extends keyof ArkConfig>(key: K, value: ArkConfig[K]): void;
58
- /**
59
- * Validate configuration values
60
- */
61
- validateConfig(): void;
62
- /**
63
- * Get environment variable overrides
64
- */
65
- getEnvironmentOverrides(): Partial<ArkConfig>;
66
- /**
67
- * Get merged configuration with environment overrides
68
- */
69
- getMergedConfig(): ArkConfig;
70
- /**
71
- * Export configuration for backup
72
- */
73
- exportConfig(): string;
74
- /**
75
- * Import configuration from backup
76
- */
77
- importConfig(configJson: string): void;
78
- /**
79
- * Get configuration file path for CLI display
80
- */
81
- getConfigFilePath(): string;
6
+ export interface ArkConfig {
7
+ chat?: ChatConfig;
8
+ clusterInfo?: ClusterInfo;
82
9
  }
10
+ /**
11
+ * Load configuration from multiple sources with proper precedence:
12
+ * 1. Defaults
13
+ * 2. ~/.arkrc.yaml (user config)
14
+ * 3. .arkrc.yaml (project config)
15
+ * 4. Environment variables (override all)
16
+ */
17
+ export declare function loadConfig(): ArkConfig;
18
+ /**
19
+ * Get the paths checked for config files
20
+ */
21
+ export declare function getConfigPaths(): {
22
+ user: string;
23
+ project: string;
24
+ };
25
+ /**
26
+ * Format config as YAML for display
27
+ */
28
+ export declare function formatConfig(config: ArkConfig): string;
@@ -1,223 +1,88 @@
1
- /**
2
- * Configuration management for ARK CLI
3
- */
4
1
  import fs from 'fs';
5
2
  import path from 'path';
6
3
  import os from 'os';
7
- import { SecurityUtils } from './security.js';
8
- import { ValidationError } from './errors.js';
9
- export const DEFAULT_CONFIG = {
10
- defaultProjectType: 'with-samples',
11
- defaultDestination: process.cwd(),
12
- skipGitByDefault: false,
13
- skipModelsbyDefault: false,
14
- preferredEditor: process.env.EDITOR || 'code',
15
- colorOutput: true,
16
- verboseOutput: false,
17
- defaultModelProvider: 'azure',
18
- customTemplates: {},
19
- parallelOperations: true,
20
- maxConcurrentFiles: 10,
21
- fileWatchingEnabled: false,
22
- telemetryEnabled: false,
23
- errorReporting: false,
24
- };
25
- export class ConfigManager {
26
- constructor() {
27
- this.configPath = this.getConfigPath();
28
- this.config = this.loadConfig();
29
- }
30
- /**
31
- * Get the path to the configuration file
32
- */
33
- getConfigPath() {
34
- const configDir = process.env.ARK_CONFIG_DIR || path.join(os.homedir(), '.config', 'ark');
35
- // Ensure config directory exists
36
- if (!fs.existsSync(configDir)) {
37
- fs.mkdirSync(configDir, { recursive: true, mode: 0o755 });
38
- }
39
- return path.join(configDir, 'config.json');
40
- }
41
- /**
42
- * Load configuration from file or create with defaults
43
- */
44
- loadConfig() {
4
+ import yaml from 'yaml';
5
+ /**
6
+ * Load configuration from multiple sources with proper precedence:
7
+ * 1. Defaults
8
+ * 2. ~/.arkrc.yaml (user config)
9
+ * 3. .arkrc.yaml (project config)
10
+ * 4. Environment variables (override all)
11
+ */
12
+ export function loadConfig() {
13
+ // Start with defaults
14
+ const config = {
15
+ chat: {
16
+ streaming: true,
17
+ outputFormat: 'text',
18
+ },
19
+ };
20
+ // Load user config from home directory
21
+ const userConfigPath = path.join(os.homedir(), '.arkrc.yaml');
22
+ if (fs.existsSync(userConfigPath)) {
45
23
  try {
46
- if (fs.existsSync(this.configPath)) {
47
- const configContent = fs.readFileSync(this.configPath, 'utf-8');
48
- const userConfig = JSON.parse(configContent);
49
- // Merge with defaults to ensure all properties exist
50
- return { ...DEFAULT_CONFIG, ...userConfig };
51
- }
24
+ const userConfig = yaml.parse(fs.readFileSync(userConfigPath, 'utf-8'));
25
+ mergeConfig(config, userConfig);
52
26
  }
53
- catch (error) {
54
- console.warn(`Warning: Failed to load config from ${this.configPath}: ${error}`);
27
+ catch (e) {
28
+ const message = e instanceof Error ? e.message : 'Unknown error';
29
+ throw new Error(`Invalid YAML in ${userConfigPath}: ${message}`);
55
30
  }
56
- // Return defaults and save them
57
- this.saveConfig(DEFAULT_CONFIG);
58
- return { ...DEFAULT_CONFIG };
59
31
  }
60
- /**
61
- * Save configuration to file
62
- */
63
- saveConfig(config) {
32
+ // Load project config from current directory
33
+ const projectConfigPath = path.join(process.cwd(), '.arkrc.yaml');
34
+ if (fs.existsSync(projectConfigPath)) {
64
35
  try {
65
- const configContent = JSON.stringify(config, null, 2);
66
- SecurityUtils.validatePath(this.configPath, 'config file');
67
- fs.writeFileSync(this.configPath, configContent, {
68
- mode: 0o600, // Owner read/write only
69
- flag: 'w',
70
- });
36
+ const projectConfig = yaml.parse(fs.readFileSync(projectConfigPath, 'utf-8'));
37
+ mergeConfig(config, projectConfig);
71
38
  }
72
- catch (error) {
73
- throw new ValidationError(`Failed to save configuration: ${error}`, 'config', [
74
- 'Check file permissions',
75
- 'Ensure config directory exists',
76
- 'Verify disk space',
77
- ]);
39
+ catch (e) {
40
+ const message = e instanceof Error ? e.message : 'Unknown error';
41
+ throw new Error(`Invalid YAML in ${projectConfigPath}: ${message}`);
78
42
  }
79
43
  }
80
- /**
81
- * Get the current configuration
82
- */
83
- getConfig() {
84
- return { ...this.config };
85
- }
86
- /**
87
- * Update configuration
88
- */
89
- updateConfig(updates) {
90
- this.config = { ...this.config, ...updates };
91
- this.saveConfig(this.config);
92
- }
93
- /**
94
- * Reset configuration to defaults
95
- */
96
- resetConfig() {
97
- this.config = { ...DEFAULT_CONFIG };
98
- this.saveConfig(this.config);
44
+ // Apply environment variable overrides
45
+ if (process.env.ARK_CHAT_STREAMING !== undefined) {
46
+ config.chat = config.chat || {};
47
+ config.chat.streaming =
48
+ process.env.ARK_CHAT_STREAMING === '1' ||
49
+ process.env.ARK_CHAT_STREAMING === 'true';
99
50
  }
100
- /**
101
- * Get a specific configuration value
102
- */
103
- get(key) {
104
- return this.config[key];
105
- }
106
- /**
107
- * Set a specific configuration value
108
- */
109
- set(key, value) {
110
- this.config[key] = value;
111
- this.saveConfig(this.config);
112
- }
113
- /**
114
- * Validate configuration values
115
- */
116
- validateConfig() {
117
- const config = this.config;
118
- // Validate project type
119
- if (!['empty', 'with-samples'].includes(config.defaultProjectType)) {
120
- throw new ValidationError(`Invalid defaultProjectType: ${config.defaultProjectType}`, 'defaultProjectType', ['Must be "empty" or "with-samples"']);
121
- }
122
- // Validate model provider
123
- const validProviders = ['azure', 'openai', 'claude', 'gemini', 'custom'];
124
- if (!validProviders.includes(config.defaultModelProvider)) {
125
- throw new ValidationError(`Invalid defaultModelProvider: ${config.defaultModelProvider}`, 'defaultModelProvider', [`Must be one of: ${validProviders.join(', ')}`]);
126
- }
127
- // Validate numeric values
128
- if (config.maxConcurrentFiles < 1 || config.maxConcurrentFiles > 100) {
129
- throw new ValidationError(`Invalid maxConcurrentFiles: ${config.maxConcurrentFiles}`, 'maxConcurrentFiles', ['Must be between 1 and 100']);
130
- }
131
- // Validate paths
132
- if (config.defaultDestination) {
133
- SecurityUtils.validatePath(config.defaultDestination, 'default destination');
134
- }
135
- if (config.templateDirectory) {
136
- SecurityUtils.validatePath(config.templateDirectory, 'template directory');
137
- }
138
- }
139
- /**
140
- * Get environment variable overrides
141
- */
142
- getEnvironmentOverrides() {
143
- const overrides = {};
144
- // Check for environment variable overrides
145
- if (process.env.ARK_DEFAULT_PROJECT_TYPE) {
146
- const projectType = process.env.ARK_DEFAULT_PROJECT_TYPE;
147
- if (['empty', 'with-samples'].includes(projectType)) {
148
- overrides.defaultProjectType = projectType;
149
- }
150
- }
151
- if (process.env.ARK_DEFAULT_DESTINATION) {
152
- overrides.defaultDestination = process.env.ARK_DEFAULT_DESTINATION;
51
+ if (process.env.ARK_CHAT_OUTPUT_FORMAT !== undefined) {
52
+ config.chat = config.chat || {};
53
+ const format = process.env.ARK_CHAT_OUTPUT_FORMAT.toLowerCase();
54
+ if (format === 'markdown' || format === 'text') {
55
+ config.chat.outputFormat = format;
153
56
  }
154
- if (process.env.ARK_SKIP_GIT) {
155
- overrides.skipGitByDefault = process.env.ARK_SKIP_GIT === 'true';
156
- }
157
- if (process.env.ARK_SKIP_MODELS) {
158
- overrides.skipModelsbyDefault = process.env.ARK_SKIP_MODELS === 'true';
159
- }
160
- if (process.env.ARK_COLOR_OUTPUT) {
161
- overrides.colorOutput = process.env.ARK_COLOR_OUTPUT !== 'false';
162
- }
163
- if (process.env.ARK_VERBOSE) {
164
- overrides.verboseOutput = process.env.ARK_VERBOSE === 'true';
165
- }
166
- if (process.env.ARK_DEFAULT_MODEL_PROVIDER) {
167
- const provider = process.env.ARK_DEFAULT_MODEL_PROVIDER;
168
- const validProviders = [
169
- 'azure',
170
- 'openai',
171
- 'claude',
172
- 'gemini',
173
- 'custom',
174
- ];
175
- if (validProviders.includes(provider)) {
176
- overrides.defaultModelProvider =
177
- provider;
178
- }
179
- }
180
- return overrides;
181
57
  }
182
- /**
183
- * Get merged configuration with environment overrides
184
- */
185
- getMergedConfig() {
186
- const envOverrides = this.getEnvironmentOverrides();
187
- return { ...this.config, ...envOverrides };
188
- }
189
- /**
190
- * Export configuration for backup
191
- */
192
- exportConfig() {
193
- return JSON.stringify(this.config, null, 2);
194
- }
195
- /**
196
- * Import configuration from backup
197
- */
198
- importConfig(configJson) {
199
- try {
200
- const importedConfig = JSON.parse(configJson);
201
- // Validate the imported config
202
- const tempManager = new ConfigManager();
203
- tempManager.config = { ...DEFAULT_CONFIG, ...importedConfig };
204
- tempManager.validateConfig();
205
- // If validation passes, update our config
206
- this.config = tempManager.config;
207
- this.saveConfig(this.config);
58
+ return config;
59
+ }
60
+ /**
61
+ * Merge source config into target config (mutates target)
62
+ */
63
+ function mergeConfig(target, source) {
64
+ if (source.chat) {
65
+ target.chat = target.chat || {};
66
+ if (source.chat.streaming !== undefined) {
67
+ target.chat.streaming = source.chat.streaming;
208
68
  }
209
- catch (error) {
210
- throw new ValidationError(`Failed to import configuration: ${error}`, 'config', [
211
- 'Check JSON syntax',
212
- 'Ensure all required fields are present',
213
- 'Verify configuration values are valid',
214
- ]);
69
+ if (source.chat.outputFormat !== undefined) {
70
+ target.chat.outputFormat = source.chat.outputFormat;
215
71
  }
216
72
  }
217
- /**
218
- * Get configuration file path for CLI display
219
- */
220
- getConfigFilePath() {
221
- return this.configPath;
222
- }
73
+ }
74
+ /**
75
+ * Get the paths checked for config files
76
+ */
77
+ export function getConfigPaths() {
78
+ return {
79
+ user: path.join(os.homedir(), '.arkrc.yaml'),
80
+ project: path.join(process.cwd(), '.arkrc.yaml'),
81
+ };
82
+ }
83
+ /**
84
+ * Format config as YAML for display
85
+ */
86
+ export function formatConfig(config) {
87
+ return yaml.stringify(config);
223
88
  }
@@ -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
+ });
@@ -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 {};