@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.
- package/dist/arkServices.d.ts +42 -0
- package/dist/arkServices.js +138 -0
- package/dist/arkServices.spec.d.ts +1 -0
- package/dist/arkServices.spec.js +24 -0
- package/dist/commands/agents/index.d.ts +3 -0
- package/dist/commands/agents/index.js +65 -0
- 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 +3 -0
- package/dist/commands/chat/index.js +29 -0
- package/dist/commands/cluster/get.d.ts +2 -0
- package/dist/commands/cluster/get.js +39 -0
- 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 +3 -5
- 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 +3 -0
- package/dist/commands/completion/index.js +230 -0
- 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 +3 -0
- package/dist/commands/config/index.js +42 -0
- 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 +4 -0
- package/dist/commands/dashboard/index.js +39 -0
- package/dist/commands/docs/index.d.ts +4 -0
- package/dist/commands/docs/index.js +18 -0
- package/dist/commands/generate/config.js +5 -24
- package/dist/commands/generate/generators/mcpserver.d.ts +2 -1
- package/dist/commands/generate/generators/mcpserver.js +26 -5
- 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 +8 -0
- package/dist/commands/install/index.js +295 -0
- package/dist/commands/install/index.spec.d.ts +1 -0
- package/dist/commands/install/index.spec.js +143 -0
- package/dist/commands/models/create.d.ts +1 -0
- package/dist/commands/models/create.js +213 -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 +3 -0
- package/dist/commands/models/index.js +75 -0
- package/dist/commands/models/index.spec.d.ts +1 -0
- package/dist/commands/models/index.spec.js +96 -0
- package/dist/commands/query/index.d.ts +3 -0
- package/dist/commands/query/index.js +24 -0
- package/dist/commands/query/index.spec.d.ts +1 -0
- package/dist/commands/query/index.spec.js +53 -0
- package/dist/commands/routes/index.d.ts +3 -0
- package/dist/commands/routes/index.js +93 -0
- package/dist/commands/status/index.d.ts +3 -0
- package/dist/commands/status/index.js +281 -0
- package/dist/commands/targets/index.d.ts +3 -0
- package/dist/commands/targets/index.js +72 -0
- package/dist/commands/targets/index.spec.d.ts +1 -0
- package/dist/commands/targets/index.spec.js +154 -0
- package/dist/commands/teams/index.d.ts +3 -0
- package/dist/commands/teams/index.js +64 -0
- 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 +3 -0
- package/dist/commands/tools/index.js +49 -0
- 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 +3 -0
- package/dist/commands/uninstall/index.js +101 -0
- package/dist/commands/uninstall/index.spec.d.ts +1 -0
- package/dist/commands/uninstall/index.spec.js +125 -0
- package/dist/components/ChatUI.d.ts +16 -0
- package/dist/components/ChatUI.js +801 -0
- package/dist/components/statusChecker.d.ts +14 -24
- package/dist/components/statusChecker.js +295 -129
- package/dist/index.d.ts +1 -1
- package/dist/index.js +42 -42
- package/dist/lib/arkApiClient.d.ts +53 -0
- package/dist/lib/arkApiClient.js +102 -0
- package/dist/lib/arkApiProxy.d.ts +9 -0
- package/dist/lib/arkApiProxy.js +22 -0
- package/dist/lib/arkServiceProxy.d.ts +14 -0
- package/dist/lib/arkServiceProxy.js +95 -0
- package/dist/lib/arkStatus.d.ts +10 -0
- package/dist/lib/arkStatus.js +79 -0
- package/dist/lib/arkStatus.spec.d.ts +1 -0
- package/dist/lib/arkStatus.spec.js +49 -0
- package/dist/lib/chatClient.d.ts +33 -0
- package/dist/lib/chatClient.js +93 -0
- package/dist/lib/cluster.d.ts +2 -1
- package/dist/lib/cluster.js +37 -16
- package/dist/lib/cluster.spec.d.ts +1 -0
- package/dist/lib/cluster.spec.js +338 -0
- 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 +26 -80
- package/dist/lib/config.js +70 -205
- package/dist/lib/config.spec.d.ts +1 -0
- package/dist/lib/config.spec.js +99 -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/executeQuery.d.ts +20 -0
- package/dist/lib/executeQuery.js +135 -0
- package/dist/lib/executeQuery.spec.d.ts +1 -0
- package/dist/lib/executeQuery.spec.js +170 -0
- 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.d.ts +36 -0
- package/dist/lib/output.js +89 -0
- package/dist/lib/output.spec.d.ts +1 -0
- package/dist/lib/output.spec.js +123 -0
- package/dist/lib/startup.d.ts +9 -0
- package/dist/lib/startup.js +87 -0
- package/dist/lib/startup.spec.d.ts +1 -0
- package/dist/lib/startup.spec.js +152 -0
- package/dist/lib/types.d.ts +87 -3
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +51 -0
- package/dist/types/types.d.ts +40 -0
- package/dist/types/types.js +1 -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 +226 -91
- 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 -7
- package/dist/ui/statusFormatter.js +39 -39
- package/dist/ui/statusFormatter.spec.d.ts +1 -0
- package/dist/ui/statusFormatter.spec.js +58 -0
- package/package.json +16 -5
- package/dist/commands/cluster/get-ip.d.ts +0 -2
- package/dist/commands/cluster/get-ip.js +0 -32
- package/dist/commands/cluster/get-type.d.ts +0 -2
- package/dist/commands/cluster/get-type.js +0 -26
- package/dist/commands/completion.d.ts +0 -2
- package/dist/commands/completion.js +0 -108
- package/dist/commands/config.d.ts +0 -5
- package/dist/commands/config.js +0 -327
- package/dist/components/DashboardCLI.d.ts +0 -3
- package/dist/components/DashboardCLI.js +0 -149
- package/dist/config.d.ts +0 -42
- package/dist/config.js +0 -243
- package/dist/lib/arkClient.d.ts +0 -32
- package/dist/lib/arkClient.js +0 -43
- package/dist/lib/consts.d.ts +0 -10
- package/dist/lib/consts.js +0 -15
- package/dist/lib/exec.d.ts +0 -5
- package/dist/lib/exec.js +0 -20
- package/dist/lib/gatewayManager.d.ts +0 -24
- package/dist/lib/gatewayManager.js +0 -85
- package/dist/lib/kubernetes.d.ts +0 -28
- package/dist/lib/kubernetes.js +0 -122
- package/dist/lib/progress.d.ts +0 -128
- package/dist/lib/progress.js +0 -273
- package/dist/lib/wrappers/git.d.ts +0 -2
- 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
|
+
});
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,82 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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;
|
package/dist/lib/config.js
CHANGED
|
@@ -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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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 (
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
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 (
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
});
|
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 {};
|