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.
Files changed (263) hide show
  1. package/.gencode/settings.local.json +7 -0
  2. package/README.md +11 -11
  3. package/dist/agent/agent.d.ts +42 -1
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +82 -15
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/cli/components/App.d.ts +8 -1
  8. package/dist/cli/components/App.d.ts.map +1 -1
  9. package/dist/cli/components/App.js +231 -29
  10. package/dist/cli/components/App.js.map +1 -1
  11. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  12. package/dist/cli/components/CommandSuggestions.js +2 -0
  13. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  14. package/dist/cli/components/Header.d.ts +1 -1
  15. package/dist/cli/components/Header.d.ts.map +1 -1
  16. package/dist/cli/components/Header.js +4 -6
  17. package/dist/cli/components/Header.js.map +1 -1
  18. package/dist/cli/components/Logo.d.ts +1 -0
  19. package/dist/cli/components/Logo.d.ts.map +1 -1
  20. package/dist/cli/components/Logo.js +16 -3
  21. package/dist/cli/components/Logo.js.map +1 -1
  22. package/dist/cli/components/Messages.d.ts +4 -4
  23. package/dist/cli/components/Messages.d.ts.map +1 -1
  24. package/dist/cli/components/Messages.js +51 -25
  25. package/dist/cli/components/Messages.js.map +1 -1
  26. package/dist/cli/components/PermissionPrompt.d.ts +60 -0
  27. package/dist/cli/components/PermissionPrompt.d.ts.map +1 -0
  28. package/dist/cli/components/PermissionPrompt.js +192 -0
  29. package/dist/cli/components/PermissionPrompt.js.map +1 -0
  30. package/dist/cli/components/ProviderManager.js +3 -3
  31. package/dist/cli/components/ProviderManager.js.map +1 -1
  32. package/dist/cli/components/Spinner.d.ts +7 -2
  33. package/dist/cli/components/Spinner.d.ts.map +1 -1
  34. package/dist/cli/components/Spinner.js +116 -25
  35. package/dist/cli/components/Spinner.js.map +1 -1
  36. package/dist/cli/components/TodoList.d.ts +7 -0
  37. package/dist/cli/components/TodoList.d.ts.map +1 -0
  38. package/dist/cli/components/TodoList.js +34 -0
  39. package/dist/cli/components/TodoList.js.map +1 -0
  40. package/dist/cli/components/index.d.ts +1 -0
  41. package/dist/cli/components/index.d.ts.map +1 -1
  42. package/dist/cli/components/index.js +1 -0
  43. package/dist/cli/components/index.js.map +1 -1
  44. package/dist/cli/index.js +47 -7
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/config/index.d.ts +13 -4
  47. package/dist/config/index.d.ts.map +1 -1
  48. package/dist/config/index.js +18 -3
  49. package/dist/config/index.js.map +1 -1
  50. package/dist/config/levels.d.ts +49 -0
  51. package/dist/config/levels.d.ts.map +1 -0
  52. package/dist/config/levels.js +222 -0
  53. package/dist/config/levels.js.map +1 -0
  54. package/dist/config/loader.d.ts +46 -0
  55. package/dist/config/loader.d.ts.map +1 -0
  56. package/dist/config/loader.js +153 -0
  57. package/dist/config/loader.js.map +1 -0
  58. package/dist/config/manager.d.ts +115 -15
  59. package/dist/config/manager.d.ts.map +1 -1
  60. package/dist/config/manager.js +260 -34
  61. package/dist/config/manager.js.map +1 -1
  62. package/dist/config/manager.test.d.ts +5 -0
  63. package/dist/config/manager.test.d.ts.map +1 -0
  64. package/dist/config/manager.test.js +192 -0
  65. package/dist/config/manager.test.js.map +1 -0
  66. package/dist/config/merger.d.ts +56 -0
  67. package/dist/config/merger.d.ts.map +1 -0
  68. package/dist/config/merger.js +177 -0
  69. package/dist/config/merger.js.map +1 -0
  70. package/dist/config/test-utils.d.ts +24 -0
  71. package/dist/config/test-utils.d.ts.map +1 -0
  72. package/dist/config/test-utils.js +55 -0
  73. package/dist/config/test-utils.js.map +1 -0
  74. package/dist/config/types.d.ts +78 -9
  75. package/dist/config/types.d.ts.map +1 -1
  76. package/dist/config/types.js +52 -2
  77. package/dist/config/types.js.map +1 -1
  78. package/dist/memory/import-resolver.d.ts +46 -0
  79. package/dist/memory/import-resolver.d.ts.map +1 -0
  80. package/dist/memory/import-resolver.js +117 -0
  81. package/dist/memory/import-resolver.js.map +1 -0
  82. package/dist/memory/index.d.ts +7 -6
  83. package/dist/memory/index.d.ts.map +1 -1
  84. package/dist/memory/index.js +7 -5
  85. package/dist/memory/index.js.map +1 -1
  86. package/dist/memory/init-prompt.d.ts +22 -0
  87. package/dist/memory/init-prompt.d.ts.map +1 -0
  88. package/dist/memory/init-prompt.js +103 -0
  89. package/dist/memory/init-prompt.js.map +1 -0
  90. package/dist/memory/memory-manager.d.ts +119 -0
  91. package/dist/memory/memory-manager.d.ts.map +1 -0
  92. package/dist/memory/memory-manager.js +587 -0
  93. package/dist/memory/memory-manager.js.map +1 -0
  94. package/dist/memory/rules-parser.d.ts +38 -0
  95. package/dist/memory/rules-parser.d.ts.map +1 -0
  96. package/dist/memory/rules-parser.js +69 -0
  97. package/dist/memory/rules-parser.js.map +1 -0
  98. package/dist/memory/test-utils.d.ts +20 -0
  99. package/dist/memory/test-utils.d.ts.map +1 -0
  100. package/dist/memory/test-utils.js +44 -0
  101. package/dist/memory/test-utils.js.map +1 -0
  102. package/dist/memory/types.d.ts +70 -63
  103. package/dist/memory/types.d.ts.map +1 -1
  104. package/dist/memory/types.js +42 -2
  105. package/dist/memory/types.js.map +1 -1
  106. package/dist/permissions/audit.d.ts +82 -0
  107. package/dist/permissions/audit.d.ts.map +1 -0
  108. package/dist/permissions/audit.js +229 -0
  109. package/dist/permissions/audit.js.map +1 -0
  110. package/dist/permissions/index.d.ts +11 -1
  111. package/dist/permissions/index.d.ts.map +1 -1
  112. package/dist/permissions/index.js +15 -0
  113. package/dist/permissions/index.js.map +1 -1
  114. package/dist/permissions/manager.d.ts +149 -13
  115. package/dist/permissions/manager.d.ts.map +1 -1
  116. package/dist/permissions/manager.js +480 -35
  117. package/dist/permissions/manager.js.map +1 -1
  118. package/dist/permissions/manager.test.d.ts +5 -0
  119. package/dist/permissions/manager.test.d.ts.map +1 -0
  120. package/dist/permissions/manager.test.js +213 -0
  121. package/dist/permissions/manager.test.js.map +1 -0
  122. package/dist/permissions/persistence.d.ts +74 -0
  123. package/dist/permissions/persistence.d.ts.map +1 -0
  124. package/dist/permissions/persistence.js +248 -0
  125. package/dist/permissions/persistence.js.map +1 -0
  126. package/dist/permissions/persistence.test.d.ts +5 -0
  127. package/dist/permissions/persistence.test.d.ts.map +1 -0
  128. package/dist/permissions/persistence.test.js +171 -0
  129. package/dist/permissions/persistence.test.js.map +1 -0
  130. package/dist/permissions/prompt-matcher.d.ts +64 -0
  131. package/dist/permissions/prompt-matcher.d.ts.map +1 -0
  132. package/dist/permissions/prompt-matcher.js +415 -0
  133. package/dist/permissions/prompt-matcher.js.map +1 -0
  134. package/dist/permissions/prompt-matcher.test.d.ts +5 -0
  135. package/dist/permissions/prompt-matcher.test.d.ts.map +1 -0
  136. package/dist/permissions/prompt-matcher.test.js +107 -0
  137. package/dist/permissions/prompt-matcher.test.js.map +1 -0
  138. package/dist/permissions/types.d.ts +157 -0
  139. package/dist/permissions/types.d.ts.map +1 -1
  140. package/dist/permissions/types.js +43 -8
  141. package/dist/permissions/types.js.map +1 -1
  142. package/dist/prompts/index.d.ts +92 -0
  143. package/dist/prompts/index.d.ts.map +1 -0
  144. package/dist/prompts/index.js +241 -0
  145. package/dist/prompts/index.js.map +1 -0
  146. package/dist/tools/builtin/bash.d.ts.map +1 -1
  147. package/dist/tools/builtin/bash.js +2 -1
  148. package/dist/tools/builtin/bash.js.map +1 -1
  149. package/dist/tools/builtin/edit.d.ts.map +1 -1
  150. package/dist/tools/builtin/edit.js +2 -1
  151. package/dist/tools/builtin/edit.js.map +1 -1
  152. package/dist/tools/builtin/glob.d.ts.map +1 -1
  153. package/dist/tools/builtin/glob.js +2 -1
  154. package/dist/tools/builtin/glob.js.map +1 -1
  155. package/dist/tools/builtin/grep.d.ts.map +1 -1
  156. package/dist/tools/builtin/grep.js +2 -1
  157. package/dist/tools/builtin/grep.js.map +1 -1
  158. package/dist/tools/builtin/read.d.ts.map +1 -1
  159. package/dist/tools/builtin/read.js +2 -1
  160. package/dist/tools/builtin/read.js.map +1 -1
  161. package/dist/tools/builtin/todowrite.d.ts +15 -0
  162. package/dist/tools/builtin/todowrite.d.ts.map +1 -0
  163. package/dist/tools/builtin/todowrite.js +88 -0
  164. package/dist/tools/builtin/todowrite.js.map +1 -0
  165. package/dist/tools/builtin/webfetch.d.ts.map +1 -1
  166. package/dist/tools/builtin/webfetch.js +2 -5
  167. package/dist/tools/builtin/webfetch.js.map +1 -1
  168. package/dist/tools/builtin/websearch.d.ts.map +1 -1
  169. package/dist/tools/builtin/websearch.js +2 -16
  170. package/dist/tools/builtin/websearch.js.map +1 -1
  171. package/dist/tools/builtin/write.d.ts.map +1 -1
  172. package/dist/tools/builtin/write.js +2 -1
  173. package/dist/tools/builtin/write.js.map +1 -1
  174. package/dist/tools/index.d.ts +7 -0
  175. package/dist/tools/index.d.ts.map +1 -1
  176. package/dist/tools/index.js +4 -0
  177. package/dist/tools/index.js.map +1 -1
  178. package/dist/tools/types.d.ts +22 -0
  179. package/dist/tools/types.d.ts.map +1 -1
  180. package/dist/tools/types.js +8 -0
  181. package/dist/tools/types.js.map +1 -1
  182. package/docs/config-system-comparison.md +707 -0
  183. package/docs/memory-system.md +238 -0
  184. package/docs/permissions.md +368 -0
  185. package/docs/proposals/0005-todo-system.md +350 -85
  186. package/docs/proposals/0006-memory-system.md +11 -10
  187. package/docs/proposals/0012-ask-user-question.md +941 -206
  188. package/docs/proposals/0023-permission-enhancements.md +61 -2
  189. package/docs/proposals/0041-configuration-system.md +33 -2
  190. package/docs/proposals/0042-prompt-optimization.md +866 -0
  191. package/docs/proposals/README.md +6 -5
  192. package/jest.config.js +26 -0
  193. package/package.json +8 -2
  194. package/src/agent/agent.ts +111 -16
  195. package/src/cli/components/App.tsx +309 -36
  196. package/src/cli/components/CommandSuggestions.tsx +2 -0
  197. package/src/cli/components/Header.tsx +11 -17
  198. package/src/cli/components/Logo.tsx +76 -9
  199. package/src/cli/components/Messages.tsx +73 -53
  200. package/src/cli/components/PermissionPrompt.tsx +388 -0
  201. package/src/cli/components/ProviderManager.tsx +5 -5
  202. package/src/cli/components/Spinner.tsx +138 -25
  203. package/src/cli/components/TodoList.tsx +54 -0
  204. package/src/cli/components/index.ts +6 -0
  205. package/src/cli/index.tsx +54 -6
  206. package/src/config/index.ts +78 -4
  207. package/src/config/levels.test.ts +163 -0
  208. package/src/config/levels.ts +285 -0
  209. package/src/config/loader.test.ts +120 -0
  210. package/src/config/loader.ts +178 -0
  211. package/src/config/manager.test.ts +215 -0
  212. package/src/config/manager.ts +328 -40
  213. package/src/config/merger.test.ts +360 -0
  214. package/src/config/merger.ts +221 -0
  215. package/src/config/test-utils.ts +79 -0
  216. package/src/config/types.ts +152 -9
  217. package/src/memory/import-resolver.test.ts +117 -0
  218. package/src/memory/import-resolver.ts +149 -0
  219. package/src/memory/index.ts +11 -0
  220. package/src/memory/init-prompt.ts +113 -0
  221. package/src/memory/memory-manager.test.ts +198 -0
  222. package/src/memory/memory-manager.ts +716 -0
  223. package/src/memory/rules-parser.test.ts +182 -0
  224. package/src/memory/rules-parser.ts +82 -0
  225. package/src/memory/test-utils.ts +60 -0
  226. package/src/memory/types.ts +119 -0
  227. package/src/permissions/audit.ts +284 -0
  228. package/src/permissions/index.ts +20 -1
  229. package/src/permissions/manager.test.ts +260 -0
  230. package/src/permissions/manager.ts +592 -40
  231. package/src/permissions/persistence.test.ts +220 -0
  232. package/src/permissions/persistence.ts +301 -0
  233. package/src/permissions/prompt-matcher.test.ts +213 -0
  234. package/src/permissions/prompt-matcher.ts +472 -0
  235. package/src/permissions/types.ts +236 -8
  236. package/src/prompts/index.test.ts +279 -0
  237. package/src/prompts/index.ts +306 -0
  238. package/src/prompts/system/anthropic.txt +29 -0
  239. package/src/prompts/system/base.txt +124 -0
  240. package/src/prompts/system/gemini.txt +35 -0
  241. package/src/prompts/system/generic.txt +128 -0
  242. package/src/prompts/system/openai.txt +29 -0
  243. package/src/prompts/tools/bash.txt +60 -0
  244. package/src/prompts/tools/edit.txt +29 -0
  245. package/src/prompts/tools/glob.txt +35 -0
  246. package/src/prompts/tools/grep.txt +43 -0
  247. package/src/prompts/tools/read.txt +22 -0
  248. package/src/prompts/tools/todowrite.txt +71 -0
  249. package/src/prompts/tools/webfetch.txt +34 -0
  250. package/src/prompts/tools/websearch.txt +41 -0
  251. package/src/prompts/tools/write.txt +23 -0
  252. package/src/tools/builtin/bash.ts +2 -1
  253. package/src/tools/builtin/edit.ts +2 -1
  254. package/src/tools/builtin/glob.ts +2 -1
  255. package/src/tools/builtin/grep.ts +2 -1
  256. package/src/tools/builtin/read.ts +2 -1
  257. package/src/tools/builtin/todowrite.ts +102 -0
  258. package/src/tools/builtin/webfetch.ts +2 -5
  259. package/src/tools/builtin/websearch.ts +2 -16
  260. package/src/tools/builtin/write.ts +2 -1
  261. package/src/tools/index.ts +4 -0
  262. package/src/tools/types.ts +12 -0
  263. 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
+ });
@@ -1,77 +1,365 @@
1
1
  /**
2
- * Settings Manager - Persists user settings (Claude Code style)
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 * as os from 'os';
8
- import type { Settings, SettingsManagerOptions } from './types.js';
9
- import { DEFAULT_SETTINGS_DIR, SETTINGS_FILE_NAME } from './types.js';
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
- export class SettingsManager {
12
- private settingsDir: string;
13
- private settingsPath: string;
14
- private settings: Settings = {};
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: SettingsManagerOptions = {}) {
17
- const dir = options.settingsDir ?? DEFAULT_SETTINGS_DIR;
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
- * Ensure settings directory exists
41
+ * Load and merge all configuration sources
24
42
  */
25
- private async ensureDir(): Promise<void> {
26
- try {
27
- await fs.mkdir(this.settingsDir, { recursive: true });
28
- } catch {
29
- // Directory may already exist
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
- * Load settings from disk
63
+ * Set CLI argument overrides
35
64
  */
36
- async load(): Promise<Settings> {
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(this.settingsPath, 'utf-8');
39
- this.settings = JSON.parse(content);
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 or is invalid
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 settings to disk (merges with existing)
138
+ * Save to global user settings (default)
50
139
  */
51
140
  async save(updates: Partial<Settings>): Promise<void> {
52
- await this.ensureDir();
141
+ await this.saveToLevel(updates, 'user');
142
+ }
53
143
 
54
- // Merge updates with existing settings
55
- this.settings = { ...this.settings, ...updates };
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
- await fs.writeFile(
58
- this.settingsPath,
59
- JSON.stringify(this.settings, null, 2),
60
- 'utf-8'
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 current settings
168
+ * Get debug summary of configuration
66
169
  */
67
- get(): Settings {
68
- return { ...this.settings };
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 settings file path
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.settingsPath;
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
  }