gencode-ai 0.1.1 → 0.1.2
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/.gencode/settings.local.json +7 -0
- package/README.md +11 -11
- package/dist/agent/agent.d.ts +42 -1
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +82 -15
- package/dist/agent/agent.js.map +1 -1
- package/dist/cli/components/App.d.ts +8 -1
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +231 -29
- package/dist/cli/components/App.js.map +1 -1
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
- package/dist/cli/components/CommandSuggestions.js +2 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Header.d.ts +1 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/Header.js +4 -6
- package/dist/cli/components/Header.js.map +1 -1
- package/dist/cli/components/Logo.d.ts +1 -0
- package/dist/cli/components/Logo.d.ts.map +1 -1
- package/dist/cli/components/Logo.js +16 -3
- package/dist/cli/components/Logo.js.map +1 -1
- package/dist/cli/components/Messages.d.ts +4 -4
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +51 -25
- package/dist/cli/components/Messages.js.map +1 -1
- package/dist/cli/components/PermissionPrompt.d.ts +60 -0
- package/dist/cli/components/PermissionPrompt.d.ts.map +1 -0
- package/dist/cli/components/PermissionPrompt.js +192 -0
- package/dist/cli/components/PermissionPrompt.js.map +1 -0
- package/dist/cli/components/ProviderManager.js +3 -3
- package/dist/cli/components/ProviderManager.js.map +1 -1
- package/dist/cli/components/Spinner.d.ts +7 -2
- package/dist/cli/components/Spinner.d.ts.map +1 -1
- package/dist/cli/components/Spinner.js +116 -25
- package/dist/cli/components/Spinner.js.map +1 -1
- package/dist/cli/components/TodoList.d.ts +7 -0
- package/dist/cli/components/TodoList.d.ts.map +1 -0
- package/dist/cli/components/TodoList.js +34 -0
- package/dist/cli/components/TodoList.js.map +1 -0
- package/dist/cli/components/index.d.ts +1 -0
- package/dist/cli/components/index.d.ts.map +1 -1
- package/dist/cli/components/index.js +1 -0
- package/dist/cli/components/index.js.map +1 -1
- package/dist/cli/index.js +47 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +13 -4
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +18 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/levels.d.ts +49 -0
- package/dist/config/levels.d.ts.map +1 -0
- package/dist/config/levels.js +222 -0
- package/dist/config/levels.js.map +1 -0
- package/dist/config/loader.d.ts +46 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +153 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/manager.d.ts +115 -15
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +260 -34
- package/dist/config/manager.js.map +1 -1
- package/dist/config/manager.test.d.ts +5 -0
- package/dist/config/manager.test.d.ts.map +1 -0
- package/dist/config/manager.test.js +192 -0
- package/dist/config/manager.test.js.map +1 -0
- package/dist/config/merger.d.ts +56 -0
- package/dist/config/merger.d.ts.map +1 -0
- package/dist/config/merger.js +177 -0
- package/dist/config/merger.js.map +1 -0
- package/dist/config/test-utils.d.ts +24 -0
- package/dist/config/test-utils.d.ts.map +1 -0
- package/dist/config/test-utils.js +55 -0
- package/dist/config/test-utils.js.map +1 -0
- package/dist/config/types.d.ts +78 -9
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +52 -2
- package/dist/config/types.js.map +1 -1
- package/dist/memory/import-resolver.d.ts +46 -0
- package/dist/memory/import-resolver.d.ts.map +1 -0
- package/dist/memory/import-resolver.js +117 -0
- package/dist/memory/import-resolver.js.map +1 -0
- package/dist/memory/index.d.ts +7 -6
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +7 -5
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/init-prompt.d.ts +22 -0
- package/dist/memory/init-prompt.d.ts.map +1 -0
- package/dist/memory/init-prompt.js +103 -0
- package/dist/memory/init-prompt.js.map +1 -0
- package/dist/memory/memory-manager.d.ts +119 -0
- package/dist/memory/memory-manager.d.ts.map +1 -0
- package/dist/memory/memory-manager.js +587 -0
- package/dist/memory/memory-manager.js.map +1 -0
- package/dist/memory/rules-parser.d.ts +38 -0
- package/dist/memory/rules-parser.d.ts.map +1 -0
- package/dist/memory/rules-parser.js +69 -0
- package/dist/memory/rules-parser.js.map +1 -0
- package/dist/memory/test-utils.d.ts +20 -0
- package/dist/memory/test-utils.d.ts.map +1 -0
- package/dist/memory/test-utils.js +44 -0
- package/dist/memory/test-utils.js.map +1 -0
- package/dist/memory/types.d.ts +70 -63
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/memory/types.js +42 -2
- package/dist/memory/types.js.map +1 -1
- package/dist/permissions/audit.d.ts +82 -0
- package/dist/permissions/audit.d.ts.map +1 -0
- package/dist/permissions/audit.js +229 -0
- package/dist/permissions/audit.js.map +1 -0
- package/dist/permissions/index.d.ts +11 -1
- package/dist/permissions/index.d.ts.map +1 -1
- package/dist/permissions/index.js +15 -0
- package/dist/permissions/index.js.map +1 -1
- package/dist/permissions/manager.d.ts +149 -13
- package/dist/permissions/manager.d.ts.map +1 -1
- package/dist/permissions/manager.js +480 -35
- package/dist/permissions/manager.js.map +1 -1
- package/dist/permissions/manager.test.d.ts +5 -0
- package/dist/permissions/manager.test.d.ts.map +1 -0
- package/dist/permissions/manager.test.js +213 -0
- package/dist/permissions/manager.test.js.map +1 -0
- package/dist/permissions/persistence.d.ts +74 -0
- package/dist/permissions/persistence.d.ts.map +1 -0
- package/dist/permissions/persistence.js +248 -0
- package/dist/permissions/persistence.js.map +1 -0
- package/dist/permissions/persistence.test.d.ts +5 -0
- package/dist/permissions/persistence.test.d.ts.map +1 -0
- package/dist/permissions/persistence.test.js +171 -0
- package/dist/permissions/persistence.test.js.map +1 -0
- package/dist/permissions/prompt-matcher.d.ts +64 -0
- package/dist/permissions/prompt-matcher.d.ts.map +1 -0
- package/dist/permissions/prompt-matcher.js +415 -0
- package/dist/permissions/prompt-matcher.js.map +1 -0
- package/dist/permissions/prompt-matcher.test.d.ts +5 -0
- package/dist/permissions/prompt-matcher.test.d.ts.map +1 -0
- package/dist/permissions/prompt-matcher.test.js +107 -0
- package/dist/permissions/prompt-matcher.test.js.map +1 -0
- package/dist/permissions/types.d.ts +157 -0
- package/dist/permissions/types.d.ts.map +1 -1
- package/dist/permissions/types.js +43 -8
- package/dist/permissions/types.js.map +1 -1
- package/dist/prompts/index.d.ts +92 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +241 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts.map +1 -1
- package/dist/tools/builtin/bash.js +2 -1
- package/dist/tools/builtin/bash.js.map +1 -1
- package/dist/tools/builtin/edit.d.ts.map +1 -1
- package/dist/tools/builtin/edit.js +2 -1
- package/dist/tools/builtin/edit.js.map +1 -1
- package/dist/tools/builtin/glob.d.ts.map +1 -1
- package/dist/tools/builtin/glob.js +2 -1
- package/dist/tools/builtin/glob.js.map +1 -1
- package/dist/tools/builtin/grep.d.ts.map +1 -1
- package/dist/tools/builtin/grep.js +2 -1
- package/dist/tools/builtin/grep.js.map +1 -1
- package/dist/tools/builtin/read.d.ts.map +1 -1
- package/dist/tools/builtin/read.js +2 -1
- package/dist/tools/builtin/read.js.map +1 -1
- package/dist/tools/builtin/todowrite.d.ts +15 -0
- package/dist/tools/builtin/todowrite.d.ts.map +1 -0
- package/dist/tools/builtin/todowrite.js +88 -0
- package/dist/tools/builtin/todowrite.js.map +1 -0
- package/dist/tools/builtin/webfetch.d.ts.map +1 -1
- package/dist/tools/builtin/webfetch.js +2 -5
- package/dist/tools/builtin/webfetch.js.map +1 -1
- package/dist/tools/builtin/websearch.d.ts.map +1 -1
- package/dist/tools/builtin/websearch.js +2 -16
- package/dist/tools/builtin/websearch.js.map +1 -1
- package/dist/tools/builtin/write.d.ts.map +1 -1
- package/dist/tools/builtin/write.js +2 -1
- package/dist/tools/builtin/write.js.map +1 -1
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +22 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +8 -0
- package/dist/tools/types.js.map +1 -1
- package/docs/config-system-comparison.md +707 -0
- package/docs/memory-system.md +238 -0
- package/docs/permissions.md +368 -0
- package/docs/proposals/0005-todo-system.md +350 -85
- package/docs/proposals/0006-memory-system.md +11 -10
- package/docs/proposals/0012-ask-user-question.md +941 -206
- package/docs/proposals/0023-permission-enhancements.md +61 -2
- package/docs/proposals/0041-configuration-system.md +33 -2
- package/docs/proposals/0042-prompt-optimization.md +866 -0
- package/docs/proposals/README.md +6 -5
- package/jest.config.js +26 -0
- package/package.json +8 -2
- package/src/agent/agent.ts +111 -16
- package/src/cli/components/App.tsx +309 -36
- package/src/cli/components/CommandSuggestions.tsx +2 -0
- package/src/cli/components/Header.tsx +11 -17
- package/src/cli/components/Logo.tsx +76 -9
- package/src/cli/components/Messages.tsx +73 -53
- package/src/cli/components/PermissionPrompt.tsx +388 -0
- package/src/cli/components/ProviderManager.tsx +5 -5
- package/src/cli/components/Spinner.tsx +138 -25
- package/src/cli/components/TodoList.tsx +54 -0
- package/src/cli/components/index.ts +6 -0
- package/src/cli/index.tsx +54 -6
- package/src/config/index.ts +78 -4
- package/src/config/levels.test.ts +163 -0
- package/src/config/levels.ts +285 -0
- package/src/config/loader.test.ts +120 -0
- package/src/config/loader.ts +178 -0
- package/src/config/manager.test.ts +215 -0
- package/src/config/manager.ts +328 -40
- package/src/config/merger.test.ts +360 -0
- package/src/config/merger.ts +221 -0
- package/src/config/test-utils.ts +79 -0
- package/src/config/types.ts +152 -9
- package/src/memory/import-resolver.test.ts +117 -0
- package/src/memory/import-resolver.ts +149 -0
- package/src/memory/index.ts +11 -0
- package/src/memory/init-prompt.ts +113 -0
- package/src/memory/memory-manager.test.ts +198 -0
- package/src/memory/memory-manager.ts +716 -0
- package/src/memory/rules-parser.test.ts +182 -0
- package/src/memory/rules-parser.ts +82 -0
- package/src/memory/test-utils.ts +60 -0
- package/src/memory/types.ts +119 -0
- package/src/permissions/audit.ts +284 -0
- package/src/permissions/index.ts +20 -1
- package/src/permissions/manager.test.ts +260 -0
- package/src/permissions/manager.ts +592 -40
- package/src/permissions/persistence.test.ts +220 -0
- package/src/permissions/persistence.ts +301 -0
- package/src/permissions/prompt-matcher.test.ts +213 -0
- package/src/permissions/prompt-matcher.ts +472 -0
- package/src/permissions/types.ts +236 -8
- package/src/prompts/index.test.ts +279 -0
- package/src/prompts/index.ts +306 -0
- package/src/prompts/system/anthropic.txt +29 -0
- package/src/prompts/system/base.txt +124 -0
- package/src/prompts/system/gemini.txt +35 -0
- package/src/prompts/system/generic.txt +128 -0
- package/src/prompts/system/openai.txt +29 -0
- package/src/prompts/tools/bash.txt +60 -0
- package/src/prompts/tools/edit.txt +29 -0
- package/src/prompts/tools/glob.txt +35 -0
- package/src/prompts/tools/grep.txt +43 -0
- package/src/prompts/tools/read.txt +22 -0
- package/src/prompts/tools/todowrite.txt +71 -0
- package/src/prompts/tools/webfetch.txt +34 -0
- package/src/prompts/tools/websearch.txt +41 -0
- package/src/prompts/tools/write.txt +23 -0
- package/src/tools/builtin/bash.ts +2 -1
- package/src/tools/builtin/edit.ts +2 -1
- package/src/tools/builtin/glob.ts +2 -1
- package/src/tools/builtin/grep.ts +2 -1
- package/src/tools/builtin/read.ts +2 -1
- package/src/tools/builtin/todowrite.ts +102 -0
- package/src/tools/builtin/webfetch.ts +2 -5
- package/src/tools/builtin/websearch.ts +2 -16
- package/src/tools/builtin/write.ts +2 -1
- package/src/tools/index.ts +4 -0
- package/src/tools/types.ts +12 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigManager Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
8
|
+
import { ConfigManager } from './manager.js';
|
|
9
|
+
import { createTestProject, writeSettings, type TestProject } from './test-utils.js';
|
|
10
|
+
|
|
11
|
+
describe('ConfigManager', () => {
|
|
12
|
+
let test: TestProject;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
test = await createTestProject('gencode-config-');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => test.cleanup());
|
|
19
|
+
|
|
20
|
+
describe('load', () => {
|
|
21
|
+
it('should load settings from .gencode directory', async () => {
|
|
22
|
+
await writeSettings(test.projectDir, 'gencode', { provider: 'anthropic', model: 'claude-sonnet' });
|
|
23
|
+
|
|
24
|
+
const config = await new ConfigManager({ cwd: test.projectDir }).load();
|
|
25
|
+
|
|
26
|
+
expect(config.settings.provider).toBe('anthropic');
|
|
27
|
+
expect(config.settings.model).toBe('claude-sonnet');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should load settings from .claude directory', async () => {
|
|
31
|
+
await writeSettings(test.projectDir, 'claude', { provider: 'openai', model: 'gpt-4' });
|
|
32
|
+
|
|
33
|
+
const config = await new ConfigManager({ cwd: test.projectDir }).load();
|
|
34
|
+
|
|
35
|
+
expect(config.settings.provider).toBe('openai');
|
|
36
|
+
expect(config.settings.model).toBe('gpt-4');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should merge both .claude and .gencode with gencode winning', async () => {
|
|
40
|
+
await writeSettings(test.projectDir, 'claude', { provider: 'openai', model: 'gpt-4', theme: 'dark' });
|
|
41
|
+
await writeSettings(test.projectDir, 'gencode', { provider: 'anthropic' });
|
|
42
|
+
|
|
43
|
+
const config = await new ConfigManager({ cwd: test.projectDir }).load();
|
|
44
|
+
|
|
45
|
+
expect(config.settings.provider).toBe('anthropic'); // gencode wins
|
|
46
|
+
expect(config.settings.model).toBe('gpt-4'); // preserved from claude
|
|
47
|
+
expect(config.settings.theme).toBe('dark'); // preserved from claude
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should concatenate permission arrays from both namespaces', async () => {
|
|
51
|
+
await writeSettings(test.projectDir, 'claude', {
|
|
52
|
+
permissions: { allow: ['Bash(git:*)'], deny: ['WebFetch'] },
|
|
53
|
+
});
|
|
54
|
+
await writeSettings(test.projectDir, 'gencode', {
|
|
55
|
+
permissions: { allow: ['Bash(npm:*)'], deny: ['Bash(rm -rf:*)'] },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const { settings } = await new ConfigManager({ cwd: test.projectDir }).load();
|
|
59
|
+
|
|
60
|
+
expect(settings.permissions?.allow).toContain('Bash(git:*)');
|
|
61
|
+
expect(settings.permissions?.allow).toContain('Bash(npm:*)');
|
|
62
|
+
expect(settings.permissions?.deny).toContain('WebFetch');
|
|
63
|
+
expect(settings.permissions?.deny).toContain('Bash(rm -rf:*)');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should load local settings with higher priority', async () => {
|
|
67
|
+
await writeSettings(test.projectDir, 'gencode', { model: 'claude-sonnet', theme: 'light' });
|
|
68
|
+
await writeSettings(test.projectDir, 'gencode', { model: 'claude-opus' }, true);
|
|
69
|
+
|
|
70
|
+
const { settings } = await new ConfigManager({ cwd: test.projectDir }).load();
|
|
71
|
+
|
|
72
|
+
expect(settings.model).toBe('claude-opus'); // local wins
|
|
73
|
+
expect(settings.theme).toBe('light'); // preserved
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should load from extra config dirs', async () => {
|
|
77
|
+
const extraDir = path.join(test.tempDir, 'team-config');
|
|
78
|
+
await fs.mkdir(extraDir, { recursive: true });
|
|
79
|
+
await fs.writeFile(path.join(extraDir, 'settings.json'), JSON.stringify({ teamSetting: 'enabled' }));
|
|
80
|
+
process.env.GENCODE_CONFIG_DIRS = extraDir;
|
|
81
|
+
|
|
82
|
+
const { settings } = await new ConfigManager({ cwd: test.projectDir }).load();
|
|
83
|
+
|
|
84
|
+
expect(settings.teamSetting).toBe('enabled');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('setCliArgs', () => {
|
|
89
|
+
it('should apply CLI args with highest priority', async () => {
|
|
90
|
+
await writeSettings(test.projectDir, 'gencode', { model: 'claude-sonnet', provider: 'anthropic' });
|
|
91
|
+
|
|
92
|
+
const manager = new ConfigManager({ cwd: test.projectDir });
|
|
93
|
+
manager.setCliArgs({ model: 'gpt-4o' });
|
|
94
|
+
const { settings } = await manager.load();
|
|
95
|
+
|
|
96
|
+
expect(settings.model).toBe('gpt-4o'); // CLI wins
|
|
97
|
+
expect(settings.provider).toBe('anthropic'); // unchanged
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('saveToLevel', () => {
|
|
102
|
+
it('should save to project and local levels', async () => {
|
|
103
|
+
const manager = new ConfigManager({ cwd: test.projectDir });
|
|
104
|
+
await manager.load();
|
|
105
|
+
|
|
106
|
+
await manager.saveToLevel({ model: 'project-model' }, 'project');
|
|
107
|
+
await manager.saveToLevel({ debug: true }, 'local');
|
|
108
|
+
|
|
109
|
+
const projectContent = JSON.parse(await fs.readFile(
|
|
110
|
+
path.join(test.projectDir, '.gencode', 'settings.json'), 'utf-8'
|
|
111
|
+
));
|
|
112
|
+
const localContent = JSON.parse(await fs.readFile(
|
|
113
|
+
path.join(test.projectDir, '.gencode', 'settings.local.json'), 'utf-8'
|
|
114
|
+
));
|
|
115
|
+
|
|
116
|
+
expect(projectContent.model).toBe('project-model');
|
|
117
|
+
expect(localContent.debug).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should merge with existing settings', async () => {
|
|
121
|
+
await writeSettings(test.projectDir, 'gencode', { model: 'old', theme: 'dark' });
|
|
122
|
+
|
|
123
|
+
const manager = new ConfigManager({ cwd: test.projectDir });
|
|
124
|
+
await manager.load();
|
|
125
|
+
await manager.saveToLevel({ model: 'new' }, 'project');
|
|
126
|
+
|
|
127
|
+
const saved = JSON.parse(await fs.readFile(
|
|
128
|
+
path.join(test.projectDir, '.gencode', 'settings.json'), 'utf-8'
|
|
129
|
+
));
|
|
130
|
+
|
|
131
|
+
expect(saved.model).toBe('new');
|
|
132
|
+
expect(saved.theme).toBe('dark'); // preserved
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('addPermissionRule', () => {
|
|
137
|
+
it('should add allow and deny rules', async () => {
|
|
138
|
+
const manager = new ConfigManager({ cwd: test.projectDir });
|
|
139
|
+
await manager.load();
|
|
140
|
+
|
|
141
|
+
await manager.addPermissionRule('Bash(npm:*)', 'allow', 'project');
|
|
142
|
+
await manager.addPermissionRule('Bash(rm:*)', 'deny', 'project');
|
|
143
|
+
|
|
144
|
+
const saved = JSON.parse(await fs.readFile(
|
|
145
|
+
path.join(test.projectDir, '.gencode', 'settings.json'), 'utf-8'
|
|
146
|
+
));
|
|
147
|
+
|
|
148
|
+
expect(saved.permissions?.allow).toContain('Bash(npm:*)');
|
|
149
|
+
expect(saved.permissions?.deny).toContain('Bash(rm:*)');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('getEffectivePermissions', () => {
|
|
154
|
+
it('should return all permission lists', async () => {
|
|
155
|
+
await writeSettings(test.projectDir, 'gencode', {
|
|
156
|
+
permissions: { allow: ['A'], ask: ['B'], deny: ['C'] },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const manager = new ConfigManager({ cwd: test.projectDir });
|
|
160
|
+
await manager.load();
|
|
161
|
+
const perms = manager.getEffectivePermissions();
|
|
162
|
+
|
|
163
|
+
expect(perms.allow).toContain('A');
|
|
164
|
+
expect(perms.ask).toContain('B');
|
|
165
|
+
expect(perms.deny).toContain('C');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('isAllowed and shouldAsk', () => {
|
|
170
|
+
it('should check permissions correctly', async () => {
|
|
171
|
+
await writeSettings(test.projectDir, 'gencode', {
|
|
172
|
+
permissions: { allow: ['Bash(git:*)'], deny: ['Bash(rm:*)'], ask: ['WebFetch'] },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const manager = new ConfigManager({ cwd: test.projectDir });
|
|
176
|
+
await manager.load();
|
|
177
|
+
|
|
178
|
+
expect(manager.isAllowed('Bash(git:status)')).toBe(true);
|
|
179
|
+
expect(manager.isAllowed('Bash(rm:file)')).toBe(false);
|
|
180
|
+
expect(manager.isAllowed('Unknown')).toBe(false);
|
|
181
|
+
|
|
182
|
+
expect(manager.shouldAsk('Bash(git:status)')).toBe(false); // allowed
|
|
183
|
+
expect(manager.shouldAsk('Bash(rm:file)')).toBe(false); // denied
|
|
184
|
+
expect(manager.shouldAsk('WebFetch')).toBe(true);
|
|
185
|
+
expect(manager.shouldAsk('Unknown')).toBe(true); // default ask
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('getSources', () => {
|
|
190
|
+
it('should return all loaded sources', async () => {
|
|
191
|
+
await writeSettings(test.projectDir, 'claude', { model: 'gpt-4' });
|
|
192
|
+
await writeSettings(test.projectDir, 'gencode', { provider: 'anthropic' });
|
|
193
|
+
|
|
194
|
+
const manager = new ConfigManager({ cwd: test.projectDir });
|
|
195
|
+
await manager.load();
|
|
196
|
+
const sources = manager.getSources();
|
|
197
|
+
|
|
198
|
+
expect(sources.find((s) => s.namespace === 'claude')).toBeDefined();
|
|
199
|
+
expect(sources.find((s) => s.namespace === 'gencode')).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('getDebugSummary', () => {
|
|
204
|
+
it('should return summary or indicate not loaded', async () => {
|
|
205
|
+
const manager = new ConfigManager({ cwd: test.projectDir });
|
|
206
|
+
|
|
207
|
+
expect(manager.getDebugSummary()).toBe('Configuration not loaded');
|
|
208
|
+
|
|
209
|
+
await writeSettings(test.projectDir, 'gencode', { model: 'test' });
|
|
210
|
+
await manager.load();
|
|
211
|
+
|
|
212
|
+
expect(manager.getDebugSummary()).toContain('Configuration Sources');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
package/src/config/manager.ts
CHANGED
|
@@ -1,77 +1,365 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Configuration Manager - Unified multi-level configuration (Claude Code compatible)
|
|
3
|
+
*
|
|
4
|
+
* Configuration hierarchy (merged in order, later overrides earlier):
|
|
5
|
+
* 1. User: ~/.claude/ + ~/.gencode/ (gencode wins within level)
|
|
6
|
+
* 2. Extra: GENCODE_CONFIG_DIRS directories
|
|
7
|
+
* 3. Project: .claude/ + .gencode/ (gencode wins within level)
|
|
8
|
+
* 4. Local: *.local.* files (gencode wins within level)
|
|
9
|
+
* 5. CLI: Command line arguments
|
|
10
|
+
* 6. Managed: System-wide enforced settings (cannot be overridden)
|
|
11
|
+
*
|
|
12
|
+
* Within each level, both .claude and .gencode directories are loaded and merged,
|
|
13
|
+
* with .gencode taking higher priority.
|
|
3
14
|
*/
|
|
4
15
|
|
|
5
16
|
import * as fs from 'fs/promises';
|
|
6
17
|
import * as path from 'path';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
18
|
+
import type { Settings, MergedConfig, ConfigSource, SettingsManagerOptions } from './types.js';
|
|
19
|
+
import { SETTINGS_FILE_NAME, SETTINGS_LOCAL_FILE_NAME } from './types.js';
|
|
20
|
+
import { loadAllSources, getExistingConfigFiles } from './loader.js';
|
|
21
|
+
import { mergeAllSources, mergeWithCliArgs, deepMerge, createMergeSummary } from './merger.js';
|
|
22
|
+
import { findProjectRoot, getPrimarySettingsDir, getSettingsFilePath } from './levels.js';
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Configuration Manager
|
|
26
|
+
*
|
|
27
|
+
* Manages multi-level configuration loading, merging, and persistence.
|
|
28
|
+
* Compatible with both GenCode (.gencode/) and Claude Code (.claude/) directories.
|
|
29
|
+
*/
|
|
30
|
+
export class ConfigManager {
|
|
31
|
+
private cwd: string;
|
|
32
|
+
private projectRoot: string | null = null;
|
|
33
|
+
private mergedConfig: MergedConfig | null = null;
|
|
34
|
+
private cliArgs: Partial<Settings> = {};
|
|
15
35
|
|
|
16
|
-
constructor(options:
|
|
17
|
-
|
|
18
|
-
this.settingsDir = dir.replace('~', os.homedir());
|
|
19
|
-
this.settingsPath = path.join(this.settingsDir, SETTINGS_FILE_NAME);
|
|
36
|
+
constructor(options: { cwd?: string } = {}) {
|
|
37
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
20
38
|
}
|
|
21
39
|
|
|
22
40
|
/**
|
|
23
|
-
*
|
|
41
|
+
* Load and merge all configuration sources
|
|
24
42
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
async load(): Promise<MergedConfig> {
|
|
44
|
+
// Find project root
|
|
45
|
+
this.projectRoot = await findProjectRoot(this.cwd);
|
|
46
|
+
|
|
47
|
+
// Load all sources
|
|
48
|
+
const sources = await loadAllSources(this.cwd);
|
|
49
|
+
|
|
50
|
+
// Merge all sources
|
|
51
|
+
let merged = mergeAllSources(sources);
|
|
52
|
+
|
|
53
|
+
// Apply CLI arguments if any
|
|
54
|
+
if (Object.keys(this.cliArgs).length > 0) {
|
|
55
|
+
merged = mergeWithCliArgs(merged, this.cliArgs);
|
|
30
56
|
}
|
|
57
|
+
|
|
58
|
+
this.mergedConfig = merged;
|
|
59
|
+
return merged;
|
|
31
60
|
}
|
|
32
61
|
|
|
33
62
|
/**
|
|
34
|
-
*
|
|
63
|
+
* Set CLI argument overrides
|
|
35
64
|
*/
|
|
36
|
-
|
|
65
|
+
setCliArgs(args: Partial<Settings>): void {
|
|
66
|
+
this.cliArgs = args;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the current merged settings
|
|
71
|
+
*/
|
|
72
|
+
get(): Settings {
|
|
73
|
+
if (!this.mergedConfig) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
return { ...this.mergedConfig.settings };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the full merged config with sources info
|
|
81
|
+
*/
|
|
82
|
+
getMergedConfig(): MergedConfig | null {
|
|
83
|
+
return this.mergedConfig;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all loaded sources
|
|
88
|
+
*/
|
|
89
|
+
getSources(): ConfigSource[] {
|
|
90
|
+
return this.mergedConfig?.sources ?? [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get managed deny rules
|
|
95
|
+
*/
|
|
96
|
+
getManagedDeny(): string[] {
|
|
97
|
+
return this.mergedConfig?.managedDeny ?? [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Save settings to a specific level
|
|
102
|
+
*/
|
|
103
|
+
async saveToLevel(
|
|
104
|
+
updates: Partial<Settings>,
|
|
105
|
+
level: 'user' | 'project' | 'local'
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
if (!this.projectRoot) {
|
|
108
|
+
this.projectRoot = await findProjectRoot(this.cwd);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const dir = getPrimarySettingsDir(level, this.projectRoot);
|
|
112
|
+
const fileName = level === 'local' ? SETTINGS_LOCAL_FILE_NAME : SETTINGS_FILE_NAME;
|
|
113
|
+
const filePath = path.join(dir, fileName);
|
|
114
|
+
|
|
115
|
+
// Ensure directory exists
|
|
116
|
+
await fs.mkdir(dir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
// Load existing settings for this level
|
|
119
|
+
let existing: Settings = {};
|
|
37
120
|
try {
|
|
38
|
-
const content = await fs.readFile(
|
|
39
|
-
|
|
40
|
-
return this.settings;
|
|
121
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
122
|
+
existing = JSON.parse(content);
|
|
41
123
|
} catch {
|
|
42
|
-
// File doesn't exist
|
|
43
|
-
this.settings = {};
|
|
44
|
-
return this.settings;
|
|
124
|
+
// File doesn't exist, start fresh
|
|
45
125
|
}
|
|
126
|
+
|
|
127
|
+
// Merge updates into existing
|
|
128
|
+
const merged = deepMerge(existing, updates);
|
|
129
|
+
|
|
130
|
+
// Write back
|
|
131
|
+
await fs.writeFile(filePath, JSON.stringify(merged, null, 2), 'utf-8');
|
|
132
|
+
|
|
133
|
+
// Reload configuration
|
|
134
|
+
await this.load();
|
|
46
135
|
}
|
|
47
136
|
|
|
48
137
|
/**
|
|
49
|
-
* Save
|
|
138
|
+
* Save to global user settings (default)
|
|
50
139
|
*/
|
|
51
140
|
async save(updates: Partial<Settings>): Promise<void> {
|
|
52
|
-
await this.
|
|
141
|
+
await this.saveToLevel(updates, 'user');
|
|
142
|
+
}
|
|
53
143
|
|
|
54
|
-
|
|
55
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Add a permission rule
|
|
146
|
+
*/
|
|
147
|
+
async addPermissionRule(
|
|
148
|
+
pattern: string,
|
|
149
|
+
type: 'allow' | 'deny' | 'ask',
|
|
150
|
+
level: 'user' | 'project' | 'local' = 'project'
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
const updates: Settings = {
|
|
153
|
+
permissions: {
|
|
154
|
+
[type]: [pattern],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
await this.saveToLevel(updates, level);
|
|
158
|
+
}
|
|
56
159
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
160
|
+
/**
|
|
161
|
+
* Get existing config files
|
|
162
|
+
*/
|
|
163
|
+
async getExistingFiles(): Promise<string[]> {
|
|
164
|
+
return getExistingConfigFiles(this.cwd);
|
|
62
165
|
}
|
|
63
166
|
|
|
64
167
|
/**
|
|
65
|
-
* Get
|
|
168
|
+
* Get debug summary of configuration
|
|
66
169
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
170
|
+
getDebugSummary(): string {
|
|
171
|
+
if (!this.mergedConfig) {
|
|
172
|
+
return 'Configuration not loaded';
|
|
173
|
+
}
|
|
174
|
+
return createMergeSummary(this.mergedConfig);
|
|
69
175
|
}
|
|
70
176
|
|
|
71
177
|
/**
|
|
72
|
-
* Get
|
|
178
|
+
* Get the project root directory
|
|
73
179
|
*/
|
|
180
|
+
getProjectRoot(): string {
|
|
181
|
+
return this.projectRoot ?? this.cwd;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the current working directory
|
|
186
|
+
*/
|
|
187
|
+
getCwd(): string {
|
|
188
|
+
return this.cwd;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the primary settings file path for a level
|
|
193
|
+
*/
|
|
194
|
+
getSettingsPath(level: 'user' | 'project' | 'local' = 'user'): string {
|
|
195
|
+
const projectRoot = this.projectRoot ?? this.cwd;
|
|
196
|
+
return getSettingsFilePath(level, projectRoot);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get effective permissions (merged from all sources)
|
|
201
|
+
*/
|
|
202
|
+
getEffectivePermissions(): {
|
|
203
|
+
allow: string[];
|
|
204
|
+
ask: string[];
|
|
205
|
+
deny: string[];
|
|
206
|
+
managedDeny: string[];
|
|
207
|
+
} {
|
|
208
|
+
const settings = this.get();
|
|
209
|
+
const permissions = settings.permissions ?? {};
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
allow: permissions.allow ?? [],
|
|
213
|
+
ask: permissions.ask ?? [],
|
|
214
|
+
deny: permissions.deny ?? [],
|
|
215
|
+
managedDeny: this.getManagedDeny(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if a permission pattern is allowed
|
|
221
|
+
*/
|
|
222
|
+
isAllowed(pattern: string): boolean {
|
|
223
|
+
const { allow, deny, managedDeny } = this.getEffectivePermissions();
|
|
224
|
+
|
|
225
|
+
// Managed deny always wins
|
|
226
|
+
if (managedDeny.some((p) => this.matchPattern(pattern, p))) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check deny list
|
|
231
|
+
if (deny.some((p) => this.matchPattern(pattern, p))) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check allow list
|
|
236
|
+
return allow.some((p) => this.matchPattern(pattern, p));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if a permission pattern requires asking
|
|
241
|
+
*/
|
|
242
|
+
shouldAsk(pattern: string): boolean {
|
|
243
|
+
const { ask, allow, deny, managedDeny } = this.getEffectivePermissions();
|
|
244
|
+
|
|
245
|
+
// Managed deny always wins
|
|
246
|
+
if (managedDeny.some((p) => this.matchPattern(pattern, p))) {
|
|
247
|
+
return false; // Don't ask, just deny
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If explicitly allowed, don't ask
|
|
251
|
+
if (allow.some((p) => this.matchPattern(pattern, p))) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// If explicitly denied, don't ask
|
|
256
|
+
if (deny.some((p) => this.matchPattern(pattern, p))) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check ask list
|
|
261
|
+
if (ask.some((p) => this.matchPattern(pattern, p))) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Default: ask for unknown patterns
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Simple pattern matching (supports * and :* wildcards)
|
|
271
|
+
*/
|
|
272
|
+
private matchPattern(value: string, pattern: string): boolean {
|
|
273
|
+
// Exact match
|
|
274
|
+
if (value === pattern) return true;
|
|
275
|
+
|
|
276
|
+
// Handle :* suffix (e.g., "Bash(git:*)" matches "Bash(git:status)")
|
|
277
|
+
if (pattern.endsWith(':*)')) {
|
|
278
|
+
const prefix = pattern.slice(0, -3); // Remove ":*)"
|
|
279
|
+
const valuePrefix = value.slice(0, value.lastIndexOf(':'));
|
|
280
|
+
return valuePrefix.startsWith(prefix);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Handle * wildcards
|
|
284
|
+
if (pattern.includes('*')) {
|
|
285
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
286
|
+
return regex.test(value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// Legacy SettingsManager (backward compatibility)
|
|
295
|
+
// =============================================================================
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Legacy SettingsManager for backward compatibility
|
|
299
|
+
*
|
|
300
|
+
* @deprecated Use ConfigManager instead
|
|
301
|
+
*/
|
|
302
|
+
export class SettingsManager {
|
|
303
|
+
private configManager: ConfigManager;
|
|
304
|
+
private globalDir: string;
|
|
305
|
+
private projectDir: string;
|
|
306
|
+
|
|
307
|
+
constructor(options: SettingsManagerOptions & { cwd?: string } = {}) {
|
|
308
|
+
this.configManager = new ConfigManager({ cwd: options.cwd });
|
|
309
|
+
|
|
310
|
+
// For legacy compatibility
|
|
311
|
+
const cwd = options.cwd ?? process.cwd();
|
|
312
|
+
this.globalDir = options.settingsDir ?? getPrimarySettingsDir('user', cwd);
|
|
313
|
+
this.projectDir = getPrimarySettingsDir('project', cwd);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async load(): Promise<Settings> {
|
|
317
|
+
const merged = await this.configManager.load();
|
|
318
|
+
return merged.settings;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async save(updates: Partial<Settings>): Promise<void> {
|
|
322
|
+
await this.configManager.save(updates);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async saveToLevel(
|
|
326
|
+
updates: Partial<Settings>,
|
|
327
|
+
level: 'global' | 'project' | 'local'
|
|
328
|
+
): Promise<void> {
|
|
329
|
+
const mappedLevel = level === 'global' ? 'user' : level;
|
|
330
|
+
await this.configManager.saveToLevel(updates, mappedLevel);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async addPermissionRule(
|
|
334
|
+
pattern: string,
|
|
335
|
+
type: 'allow' | 'deny',
|
|
336
|
+
level: 'global' | 'project' | 'local' = 'project'
|
|
337
|
+
): Promise<void> {
|
|
338
|
+
const mappedLevel = level === 'global' ? 'user' : level;
|
|
339
|
+
await this.configManager.addPermissionRule(pattern, type, mappedLevel);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
get(): Settings {
|
|
343
|
+
return this.configManager.get();
|
|
344
|
+
}
|
|
345
|
+
|
|
74
346
|
getPath(): string {
|
|
75
|
-
return this.
|
|
347
|
+
return this.configManager.getSettingsPath('user');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
getProjectPath(): string {
|
|
351
|
+
return this.configManager.getSettingsPath('project');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
getLocalPath(): string {
|
|
355
|
+
return this.configManager.getSettingsPath('local');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getCwd(): string {
|
|
359
|
+
return this.configManager.getCwd();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getProjectDir(): string {
|
|
363
|
+
return this.projectDir;
|
|
76
364
|
}
|
|
77
365
|
}
|