gencode-ai 0.1.1 → 0.1.3

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 (287) hide show
  1. package/.gencode/settings.local.json +7 -0
  2. package/CLAUDE.md +86 -0
  3. package/README.md +13 -16
  4. package/dist/agent/agent.d.ts +50 -1
  5. package/dist/agent/agent.d.ts.map +1 -1
  6. package/dist/agent/agent.js +96 -16
  7. package/dist/agent/agent.js.map +1 -1
  8. package/dist/agent/index.d.ts +1 -0
  9. package/dist/agent/index.d.ts.map +1 -1
  10. package/dist/agent/types.d.ts +14 -1
  11. package/dist/agent/types.d.ts.map +1 -1
  12. package/dist/cli/components/App.d.ts +8 -1
  13. package/dist/cli/components/App.d.ts.map +1 -1
  14. package/dist/cli/components/App.js +266 -29
  15. package/dist/cli/components/App.js.map +1 -1
  16. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  17. package/dist/cli/components/CommandSuggestions.js +2 -0
  18. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  19. package/dist/cli/components/Header.d.ts +1 -1
  20. package/dist/cli/components/Header.d.ts.map +1 -1
  21. package/dist/cli/components/Header.js +4 -6
  22. package/dist/cli/components/Header.js.map +1 -1
  23. package/dist/cli/components/Logo.d.ts +1 -0
  24. package/dist/cli/components/Logo.d.ts.map +1 -1
  25. package/dist/cli/components/Logo.js +16 -3
  26. package/dist/cli/components/Logo.js.map +1 -1
  27. package/dist/cli/components/Messages.d.ts +4 -4
  28. package/dist/cli/components/Messages.d.ts.map +1 -1
  29. package/dist/cli/components/Messages.js +66 -23
  30. package/dist/cli/components/Messages.js.map +1 -1
  31. package/dist/cli/components/PermissionPrompt.d.ts +60 -0
  32. package/dist/cli/components/PermissionPrompt.d.ts.map +1 -0
  33. package/dist/cli/components/PermissionPrompt.js +192 -0
  34. package/dist/cli/components/PermissionPrompt.js.map +1 -0
  35. package/dist/cli/components/ProviderManager.js +3 -3
  36. package/dist/cli/components/ProviderManager.js.map +1 -1
  37. package/dist/cli/components/QuestionPrompt.d.ts +23 -0
  38. package/dist/cli/components/QuestionPrompt.d.ts.map +1 -0
  39. package/dist/cli/components/QuestionPrompt.js +231 -0
  40. package/dist/cli/components/QuestionPrompt.js.map +1 -0
  41. package/dist/cli/components/Spinner.d.ts +7 -2
  42. package/dist/cli/components/Spinner.d.ts.map +1 -1
  43. package/dist/cli/components/Spinner.js +116 -25
  44. package/dist/cli/components/Spinner.js.map +1 -1
  45. package/dist/cli/components/TodoList.d.ts +7 -0
  46. package/dist/cli/components/TodoList.d.ts.map +1 -0
  47. package/dist/cli/components/TodoList.js +34 -0
  48. package/dist/cli/components/TodoList.js.map +1 -0
  49. package/dist/cli/components/index.d.ts +2 -0
  50. package/dist/cli/components/index.d.ts.map +1 -1
  51. package/dist/cli/components/index.js +2 -0
  52. package/dist/cli/components/index.js.map +1 -1
  53. package/dist/cli/components/theme.d.ts +7 -0
  54. package/dist/cli/components/theme.d.ts.map +1 -1
  55. package/dist/cli/components/theme.js +11 -1
  56. package/dist/cli/components/theme.js.map +1 -1
  57. package/dist/cli/index.js +47 -7
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/config/index.d.ts +13 -4
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +18 -3
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/config/levels.d.ts +49 -0
  64. package/dist/config/levels.d.ts.map +1 -0
  65. package/dist/config/levels.js +222 -0
  66. package/dist/config/levels.js.map +1 -0
  67. package/dist/config/loader.d.ts +46 -0
  68. package/dist/config/loader.d.ts.map +1 -0
  69. package/dist/config/loader.js +153 -0
  70. package/dist/config/loader.js.map +1 -0
  71. package/dist/config/manager.d.ts +115 -15
  72. package/dist/config/manager.d.ts.map +1 -1
  73. package/dist/config/manager.js +260 -34
  74. package/dist/config/manager.js.map +1 -1
  75. package/dist/config/manager.test.d.ts +5 -0
  76. package/dist/config/manager.test.d.ts.map +1 -0
  77. package/dist/config/manager.test.js +192 -0
  78. package/dist/config/manager.test.js.map +1 -0
  79. package/dist/config/merger.d.ts +56 -0
  80. package/dist/config/merger.d.ts.map +1 -0
  81. package/dist/config/merger.js +177 -0
  82. package/dist/config/merger.js.map +1 -0
  83. package/dist/config/test-utils.d.ts +24 -0
  84. package/dist/config/test-utils.d.ts.map +1 -0
  85. package/dist/config/test-utils.js +55 -0
  86. package/dist/config/test-utils.js.map +1 -0
  87. package/dist/config/types.d.ts +78 -9
  88. package/dist/config/types.d.ts.map +1 -1
  89. package/dist/config/types.js +52 -2
  90. package/dist/config/types.js.map +1 -1
  91. package/dist/memory/import-resolver.d.ts +46 -0
  92. package/dist/memory/import-resolver.d.ts.map +1 -0
  93. package/dist/memory/import-resolver.js +117 -0
  94. package/dist/memory/import-resolver.js.map +1 -0
  95. package/dist/memory/index.d.ts +7 -6
  96. package/dist/memory/index.d.ts.map +1 -1
  97. package/dist/memory/index.js +7 -5
  98. package/dist/memory/index.js.map +1 -1
  99. package/dist/memory/init-prompt.d.ts +22 -0
  100. package/dist/memory/init-prompt.d.ts.map +1 -0
  101. package/dist/memory/init-prompt.js +103 -0
  102. package/dist/memory/init-prompt.js.map +1 -0
  103. package/dist/memory/memory-manager.d.ts +119 -0
  104. package/dist/memory/memory-manager.d.ts.map +1 -0
  105. package/dist/memory/memory-manager.js +587 -0
  106. package/dist/memory/memory-manager.js.map +1 -0
  107. package/dist/memory/rules-parser.d.ts +38 -0
  108. package/dist/memory/rules-parser.d.ts.map +1 -0
  109. package/dist/memory/rules-parser.js +69 -0
  110. package/dist/memory/rules-parser.js.map +1 -0
  111. package/dist/memory/test-utils.d.ts +20 -0
  112. package/dist/memory/test-utils.d.ts.map +1 -0
  113. package/dist/memory/test-utils.js +44 -0
  114. package/dist/memory/test-utils.js.map +1 -0
  115. package/dist/memory/types.d.ts +70 -63
  116. package/dist/memory/types.d.ts.map +1 -1
  117. package/dist/memory/types.js +42 -2
  118. package/dist/memory/types.js.map +1 -1
  119. package/dist/permissions/audit.d.ts +82 -0
  120. package/dist/permissions/audit.d.ts.map +1 -0
  121. package/dist/permissions/audit.js +229 -0
  122. package/dist/permissions/audit.js.map +1 -0
  123. package/dist/permissions/index.d.ts +11 -1
  124. package/dist/permissions/index.d.ts.map +1 -1
  125. package/dist/permissions/index.js +15 -0
  126. package/dist/permissions/index.js.map +1 -1
  127. package/dist/permissions/manager.d.ts +149 -13
  128. package/dist/permissions/manager.d.ts.map +1 -1
  129. package/dist/permissions/manager.js +480 -35
  130. package/dist/permissions/manager.js.map +1 -1
  131. package/dist/permissions/manager.test.d.ts +5 -0
  132. package/dist/permissions/manager.test.d.ts.map +1 -0
  133. package/dist/permissions/manager.test.js +213 -0
  134. package/dist/permissions/manager.test.js.map +1 -0
  135. package/dist/permissions/persistence.d.ts +74 -0
  136. package/dist/permissions/persistence.d.ts.map +1 -0
  137. package/dist/permissions/persistence.js +248 -0
  138. package/dist/permissions/persistence.js.map +1 -0
  139. package/dist/permissions/persistence.test.d.ts +5 -0
  140. package/dist/permissions/persistence.test.d.ts.map +1 -0
  141. package/dist/permissions/persistence.test.js +171 -0
  142. package/dist/permissions/persistence.test.js.map +1 -0
  143. package/dist/permissions/prompt-matcher.d.ts +64 -0
  144. package/dist/permissions/prompt-matcher.d.ts.map +1 -0
  145. package/dist/permissions/prompt-matcher.js +415 -0
  146. package/dist/permissions/prompt-matcher.js.map +1 -0
  147. package/dist/permissions/prompt-matcher.test.d.ts +5 -0
  148. package/dist/permissions/prompt-matcher.test.d.ts.map +1 -0
  149. package/dist/permissions/prompt-matcher.test.js +107 -0
  150. package/dist/permissions/prompt-matcher.test.js.map +1 -0
  151. package/dist/permissions/types.d.ts +157 -0
  152. package/dist/permissions/types.d.ts.map +1 -1
  153. package/dist/permissions/types.js +45 -8
  154. package/dist/permissions/types.js.map +1 -1
  155. package/dist/prompts/index.d.ts +92 -0
  156. package/dist/prompts/index.d.ts.map +1 -0
  157. package/dist/prompts/index.js +241 -0
  158. package/dist/prompts/index.js.map +1 -0
  159. package/dist/tools/builtin/ask-user.d.ts +64 -0
  160. package/dist/tools/builtin/ask-user.d.ts.map +1 -0
  161. package/dist/tools/builtin/ask-user.js +148 -0
  162. package/dist/tools/builtin/ask-user.js.map +1 -0
  163. package/dist/tools/builtin/bash.d.ts.map +1 -1
  164. package/dist/tools/builtin/bash.js +2 -1
  165. package/dist/tools/builtin/bash.js.map +1 -1
  166. package/dist/tools/builtin/edit.d.ts.map +1 -1
  167. package/dist/tools/builtin/edit.js +2 -1
  168. package/dist/tools/builtin/edit.js.map +1 -1
  169. package/dist/tools/builtin/glob.d.ts.map +1 -1
  170. package/dist/tools/builtin/glob.js +2 -1
  171. package/dist/tools/builtin/glob.js.map +1 -1
  172. package/dist/tools/builtin/grep.d.ts.map +1 -1
  173. package/dist/tools/builtin/grep.js +2 -1
  174. package/dist/tools/builtin/grep.js.map +1 -1
  175. package/dist/tools/builtin/read.d.ts.map +1 -1
  176. package/dist/tools/builtin/read.js +2 -1
  177. package/dist/tools/builtin/read.js.map +1 -1
  178. package/dist/tools/builtin/todowrite.d.ts +15 -0
  179. package/dist/tools/builtin/todowrite.d.ts.map +1 -0
  180. package/dist/tools/builtin/todowrite.js +88 -0
  181. package/dist/tools/builtin/todowrite.js.map +1 -0
  182. package/dist/tools/builtin/webfetch.d.ts.map +1 -1
  183. package/dist/tools/builtin/webfetch.js +2 -5
  184. package/dist/tools/builtin/webfetch.js.map +1 -1
  185. package/dist/tools/builtin/websearch.d.ts.map +1 -1
  186. package/dist/tools/builtin/websearch.js +2 -16
  187. package/dist/tools/builtin/websearch.js.map +1 -1
  188. package/dist/tools/builtin/write.d.ts.map +1 -1
  189. package/dist/tools/builtin/write.js +2 -1
  190. package/dist/tools/builtin/write.js.map +1 -1
  191. package/dist/tools/index.d.ts +19 -0
  192. package/dist/tools/index.d.ts.map +1 -1
  193. package/dist/tools/index.js +8 -0
  194. package/dist/tools/index.js.map +1 -1
  195. package/dist/tools/types.d.ts +39 -0
  196. package/dist/tools/types.d.ts.map +1 -1
  197. package/dist/tools/types.js +8 -0
  198. package/dist/tools/types.js.map +1 -1
  199. package/docs/config-system-comparison.md +707 -0
  200. package/docs/memory-system.md +238 -0
  201. package/docs/permissions.md +368 -0
  202. package/docs/proposals/0005-todo-system.md +350 -85
  203. package/docs/proposals/0006-memory-system.md +11 -10
  204. package/docs/proposals/0012-ask-user-question.md +1007 -207
  205. package/docs/proposals/0023-permission-enhancements.md +61 -2
  206. package/docs/proposals/0041-configuration-system.md +33 -2
  207. package/docs/proposals/0042-prompt-optimization.md +866 -0
  208. package/docs/proposals/README.md +7 -6
  209. package/examples/test-ask-user.ts +167 -0
  210. package/jest.config.js +26 -0
  211. package/package.json +8 -2
  212. package/src/agent/agent.ts +130 -16
  213. package/src/agent/index.ts +1 -0
  214. package/src/agent/types.ts +13 -1
  215. package/src/cli/components/App.tsx +362 -37
  216. package/src/cli/components/CommandSuggestions.tsx +2 -0
  217. package/src/cli/components/Header.tsx +11 -17
  218. package/src/cli/components/Logo.tsx +76 -9
  219. package/src/cli/components/Messages.tsx +104 -41
  220. package/src/cli/components/PermissionPrompt.tsx +388 -0
  221. package/src/cli/components/ProviderManager.tsx +5 -5
  222. package/src/cli/components/QuestionPrompt.tsx +462 -0
  223. package/src/cli/components/Spinner.tsx +138 -25
  224. package/src/cli/components/TodoList.tsx +54 -0
  225. package/src/cli/components/index.ts +7 -0
  226. package/src/cli/components/theme.ts +11 -1
  227. package/src/cli/index.tsx +54 -6
  228. package/src/config/index.ts +78 -4
  229. package/src/config/levels.test.ts +163 -0
  230. package/src/config/levels.ts +285 -0
  231. package/src/config/loader.test.ts +120 -0
  232. package/src/config/loader.ts +178 -0
  233. package/src/config/manager.test.ts +215 -0
  234. package/src/config/manager.ts +328 -40
  235. package/src/config/merger.test.ts +360 -0
  236. package/src/config/merger.ts +221 -0
  237. package/src/config/test-utils.ts +79 -0
  238. package/src/config/types.ts +152 -9
  239. package/src/memory/import-resolver.test.ts +117 -0
  240. package/src/memory/import-resolver.ts +149 -0
  241. package/src/memory/index.ts +11 -0
  242. package/src/memory/init-prompt.ts +113 -0
  243. package/src/memory/memory-manager.test.ts +198 -0
  244. package/src/memory/memory-manager.ts +716 -0
  245. package/src/memory/rules-parser.test.ts +182 -0
  246. package/src/memory/rules-parser.ts +82 -0
  247. package/src/memory/test-utils.ts +60 -0
  248. package/src/memory/types.ts +119 -0
  249. package/src/permissions/audit.ts +284 -0
  250. package/src/permissions/index.ts +20 -1
  251. package/src/permissions/manager.test.ts +260 -0
  252. package/src/permissions/manager.ts +592 -40
  253. package/src/permissions/persistence.test.ts +220 -0
  254. package/src/permissions/persistence.ts +301 -0
  255. package/src/permissions/prompt-matcher.test.ts +213 -0
  256. package/src/permissions/prompt-matcher.ts +472 -0
  257. package/src/permissions/types.ts +238 -8
  258. package/src/prompts/index.test.ts +279 -0
  259. package/src/prompts/index.ts +306 -0
  260. package/src/prompts/system/anthropic.txt +29 -0
  261. package/src/prompts/system/base.txt +166 -0
  262. package/src/prompts/system/gemini.txt +35 -0
  263. package/src/prompts/system/generic.txt +128 -0
  264. package/src/prompts/system/openai.txt +29 -0
  265. package/src/prompts/tools/ask-user.txt +110 -0
  266. package/src/prompts/tools/bash.txt +60 -0
  267. package/src/prompts/tools/edit.txt +29 -0
  268. package/src/prompts/tools/glob.txt +35 -0
  269. package/src/prompts/tools/grep.txt +43 -0
  270. package/src/prompts/tools/read.txt +22 -0
  271. package/src/prompts/tools/todowrite.txt +71 -0
  272. package/src/prompts/tools/webfetch.txt +34 -0
  273. package/src/prompts/tools/websearch.txt +41 -0
  274. package/src/prompts/tools/write.txt +23 -0
  275. package/src/tools/builtin/ask-user.ts +185 -0
  276. package/src/tools/builtin/bash.ts +2 -1
  277. package/src/tools/builtin/edit.ts +2 -1
  278. package/src/tools/builtin/glob.ts +2 -1
  279. package/src/tools/builtin/grep.ts +2 -1
  280. package/src/tools/builtin/read.ts +2 -1
  281. package/src/tools/builtin/todowrite.ts +102 -0
  282. package/src/tools/builtin/webfetch.ts +2 -5
  283. package/src/tools/builtin/websearch.ts +2 -16
  284. package/src/tools/builtin/write.ts +2 -1
  285. package/src/tools/index.ts +19 -0
  286. package/src/tools/types.ts +30 -0
  287. package/tsconfig.json +1 -1
@@ -19,3 +19,10 @@ export { PromptInput, ConfirmPrompt } from './Input.js';
19
19
  export { colors, icons } from './theme.js';
20
20
  export { ModelSelector } from './ModelSelector.js';
21
21
  export { CommandSuggestions, COMMANDS, getFilteredCommands } from './CommandSuggestions.js';
22
+ export {
23
+ PermissionPrompt,
24
+ SimpleConfirmPrompt,
25
+ PermissionRulesDisplay,
26
+ PermissionAuditDisplay,
27
+ } from './PermissionPrompt.js';
28
+ export { QuestionPrompt, AnswerDisplay } from './QuestionPrompt.js';
@@ -35,9 +35,19 @@ export const icons = {
35
35
  // UI
36
36
  thinking: '✱', // Star for thinking state
37
37
  cursor: '▋',
38
- // Selection
38
+ // Selection (single-select)
39
39
  radio: '●', // Filled radio for selected
40
40
  radioEmpty: '○', // Empty radio for unselected
41
+ // Selection (multi-select)
42
+ checkbox: '☑', // Checked checkbox
43
+ checkboxEmpty: '☐', // Empty checkbox
44
+ // Chip/tag borders (Claude Code style headers)
45
+ chipLeft: '╭─',
46
+ chipRight: '─╮',
47
+ // Box drawing
48
+ boxTop: '╭',
49
+ boxBottom: '╰',
50
+ boxVertical: '│',
41
51
  // Tree connectors
42
52
  treeEnd: '└', // Tree end connector for tool results
43
53
  };
package/src/cli/index.tsx CHANGED
@@ -88,10 +88,21 @@ function detectConfig(settings: Settings, providersConfig: ProvidersConfigManage
88
88
  // ============================================================================
89
89
  function parseArgs() {
90
90
  const args = process.argv.slice(2);
91
+
92
+ // Extract prompt value from -p "message" or --prompt "message"
93
+ let prompt: string | undefined;
94
+ for (let i = 0; i < args.length; i++) {
95
+ if ((args[i] === '-p' || args[i] === '--prompt') && args[i + 1]) {
96
+ prompt = args[i + 1];
97
+ break;
98
+ }
99
+ }
100
+
91
101
  return {
92
102
  continue: args.includes('-c') || args.includes('--continue'),
93
103
  resume: args.includes('-r') || args.includes('--resume'),
94
104
  help: args.includes('-h') || args.includes('--help'),
105
+ prompt,
95
106
  };
96
107
  }
97
108
 
@@ -102,17 +113,47 @@ function printUsage(): void {
102
113
  console.log(' Usage: gencode [options]');
103
114
  console.log();
104
115
  console.log(' Options:');
105
- console.log(' -c, --continue Resume the most recent session');
106
- console.log(' -r, --resume Select a session interactively');
107
- console.log(' -h, --help Show this help');
116
+ console.log(' -c, --continue Resume the most recent session');
117
+ console.log(' -r, --resume Select a session interactively');
118
+ console.log(' -p, --prompt <msg> Run a single prompt (non-interactive)');
119
+ console.log(' -h, --help Show this help');
108
120
  console.log();
109
121
  console.log(' Examples:');
110
- console.log(' gencode Start new session');
111
- console.log(' gencode -c Continue last session');
112
- console.log(' gencode -r Pick a session');
122
+ console.log(' gencode Start new session');
123
+ console.log(' gencode -c Continue last session');
124
+ console.log(' gencode -r Pick a session');
125
+ console.log(' gencode -p "2+2" Run single prompt');
113
126
  console.log();
114
127
  }
115
128
 
129
+ // ============================================================================
130
+ // Non-interactive mode
131
+ // ============================================================================
132
+ async function runNonInteractive(prompt: string, config: AgentConfig): Promise<void> {
133
+ const { Agent } = await import('../agent/agent.js');
134
+
135
+ const agent = new Agent(config);
136
+
137
+ let response = '';
138
+ for await (const event of agent.run(prompt)) {
139
+ switch (event.type) {
140
+ case 'text':
141
+ response += event.text;
142
+ break;
143
+ case 'tool_start':
144
+ console.error(`[tool] ${event.name}`);
145
+ break;
146
+ case 'error':
147
+ console.error(`[error] ${event.error.message}`);
148
+ break;
149
+ case 'done':
150
+ break;
151
+ }
152
+ }
153
+
154
+ console.log(response);
155
+ }
156
+
116
157
  // ============================================================================
117
158
  // Main
118
159
  // ============================================================================
@@ -135,12 +176,19 @@ async function main() {
135
176
 
136
177
  const config = detectConfig(settings, providersConfig);
137
178
 
179
+ // Non-interactive mode with -p flag
180
+ if (args.prompt) {
181
+ await runNonInteractive(args.prompt, config);
182
+ return;
183
+ }
184
+
138
185
  // Render the Ink app
139
186
  render(
140
187
  <App
141
188
  config={config}
142
189
  settingsManager={settingsManager}
143
190
  resumeLatest={args.continue}
191
+ permissionSettings={settings.permissions}
144
192
  />
145
193
  );
146
194
  }
@@ -1,8 +1,82 @@
1
1
  /**
2
- * Config Module Exports
2
+ * Configuration Module - Multi-level configuration system (Claude Code compatible)
3
+ *
4
+ * This module provides a hierarchical configuration system that:
5
+ * - Supports multiple levels: user, project, local, managed
6
+ * - Merges .gencode and .claude directories at each level (gencode wins)
7
+ * - Supports GENCODE_CONFIG_DIRS environment variable for extra config dirs
8
+ * - Enforces managed settings that cannot be overridden
3
9
  */
4
10
 
5
- export { SettingsManager } from './manager.js';
11
+ // Core types
12
+ export type {
13
+ Settings,
14
+ SettingsManagerOptions,
15
+ ProviderName,
16
+ PermissionRules,
17
+ ConfigLevelType,
18
+ ConfigLevel,
19
+ ConfigSource,
20
+ MergedConfig,
21
+ ProvidersConfig,
22
+ ProviderConnection,
23
+ CachedModel,
24
+ ProviderModels,
25
+ } from './types.js';
26
+
27
+ // Constants
28
+ export {
29
+ DEFAULT_SETTINGS_DIR,
30
+ PROJECT_SETTINGS_DIR,
31
+ FALLBACK_SETTINGS_DIR,
32
+ FALLBACK_PROJECT_DIR,
33
+ SETTINGS_FILE_NAME,
34
+ SETTINGS_LOCAL_FILE_NAME,
35
+ MANAGED_SETTINGS_FILE_NAME,
36
+ PROVIDERS_FILE_NAME,
37
+ GENCODE_DIR,
38
+ CLAUDE_DIR,
39
+ USER_GENCODE_DIR,
40
+ USER_CLAUDE_DIR,
41
+ GENCODE_CONFIG_DIRS_ENV,
42
+ getManagedPaths,
43
+ } from './types.js';
44
+
45
+ // Configuration levels
46
+ export {
47
+ findProjectRoot,
48
+ parseExtraConfigDirs,
49
+ getConfigLevels,
50
+ getPrimarySettingsDir,
51
+ getSettingsFilePath,
52
+ type ConfigPathInfo,
53
+ type ResolvedLevel,
54
+ } from './levels.js';
55
+
56
+ // Configuration loader
57
+ export {
58
+ loadAllSources,
59
+ loadSourcesByLevel,
60
+ loadUserSettings,
61
+ loadProjectSettings,
62
+ loadManagedSettings,
63
+ getConfigInfo,
64
+ getExistingConfigFiles,
65
+ } from './loader.js';
66
+
67
+ // Configuration merger
68
+ export {
69
+ deepMerge,
70
+ mergeSettings,
71
+ extractManagedDeny,
72
+ applyManagedRestrictions,
73
+ mergeAllSources,
74
+ mergeWithCliArgs,
75
+ createMergeSummary,
76
+ } from './merger.js';
77
+
78
+ // Configuration managers
79
+ export { ConfigManager, SettingsManager } from './manager.js';
80
+
81
+ // Providers configuration
6
82
  export { ProvidersConfigManager } from './providers-config.js';
7
- export type { Settings, SettingsManagerOptions, ProviderName, ProvidersConfig } from './types.js';
8
- export { DEFAULT_SETTINGS_DIR, SETTINGS_FILE_NAME, PROVIDERS_FILE_NAME } from './types.js';
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Config Levels Tests
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
9
+ import {
10
+ findProjectRoot,
11
+ parseExtraConfigDirs,
12
+ getConfigLevels,
13
+ getPrimarySettingsDir,
14
+ getSettingsFilePath,
15
+ } from './levels.js';
16
+ import { createTestProject, type TestProject } from './test-utils.js';
17
+
18
+ describe('findProjectRoot', () => {
19
+ let test: TestProject;
20
+
21
+ beforeEach(async () => {
22
+ test = await createTestProject('gencode-levels-');
23
+ });
24
+
25
+ afterEach(() => test.cleanup());
26
+
27
+ it('should find git root', async () => {
28
+ const subDir = path.join(test.projectDir, 'src', 'components');
29
+ await fs.mkdir(subDir, { recursive: true });
30
+
31
+ expect(await findProjectRoot(subDir)).toBe(test.projectDir);
32
+ });
33
+
34
+ it('should find .gencode directory as project root', async () => {
35
+ // Remove .git, add .gencode
36
+ await fs.rm(path.join(test.projectDir, '.git'), { recursive: true });
37
+ await fs.mkdir(path.join(test.projectDir, '.gencode'));
38
+ const subDir = path.join(test.projectDir, 'src');
39
+ await fs.mkdir(subDir, { recursive: true });
40
+
41
+ expect(await findProjectRoot(subDir)).toBe(test.projectDir);
42
+ });
43
+
44
+ it('should find .claude directory as project root', async () => {
45
+ await fs.rm(path.join(test.projectDir, '.git'), { recursive: true });
46
+ await fs.mkdir(path.join(test.projectDir, '.claude'));
47
+ const subDir = path.join(test.projectDir, 'lib');
48
+ await fs.mkdir(subDir, { recursive: true });
49
+
50
+ expect(await findProjectRoot(subDir)).toBe(test.projectDir);
51
+ });
52
+
53
+ it('should return cwd if no project markers found', async () => {
54
+ const subDir = path.join(test.tempDir, 'random', 'path');
55
+ await fs.mkdir(subDir, { recursive: true });
56
+
57
+ expect(await findProjectRoot(subDir)).toBe(subDir);
58
+ });
59
+ });
60
+
61
+ describe('parseExtraConfigDirs', () => {
62
+ const originalEnv = process.env.GENCODE_CONFIG_DIRS;
63
+
64
+ afterEach(() => {
65
+ if (originalEnv === undefined) {
66
+ delete process.env.GENCODE_CONFIG_DIRS;
67
+ } else {
68
+ process.env.GENCODE_CONFIG_DIRS = originalEnv;
69
+ }
70
+ });
71
+
72
+ it('should return empty array when env var not set', () => {
73
+ delete process.env.GENCODE_CONFIG_DIRS;
74
+ expect(parseExtraConfigDirs()).toEqual([]);
75
+ });
76
+
77
+ it('should parse single directory', () => {
78
+ process.env.GENCODE_CONFIG_DIRS = '/team/config';
79
+ expect(parseExtraConfigDirs()).toEqual(['/team/config']);
80
+ });
81
+
82
+ it('should parse multiple directories', () => {
83
+ process.env.GENCODE_CONFIG_DIRS = '/team/config:/shared/rules';
84
+ expect(parseExtraConfigDirs()).toEqual(['/team/config', '/shared/rules']);
85
+ });
86
+
87
+ it('should expand tilde to home directory', () => {
88
+ process.env.GENCODE_CONFIG_DIRS = '~/my-config';
89
+ expect(parseExtraConfigDirs()[0]).toBe(path.join(os.homedir(), 'my-config'));
90
+ });
91
+
92
+ it('should trim whitespace and filter empty strings', () => {
93
+ process.env.GENCODE_CONFIG_DIRS = ' /path/one : : /path/two ';
94
+ expect(parseExtraConfigDirs()).toEqual(['/path/one', '/path/two']);
95
+ });
96
+ });
97
+
98
+ describe('getConfigLevels', () => {
99
+ let test: TestProject;
100
+
101
+ beforeEach(async () => {
102
+ test = await createTestProject('gencode-levels-');
103
+ });
104
+
105
+ afterEach(() => test.cleanup());
106
+
107
+ it('should return levels in priority order', async () => {
108
+ const types = (await getConfigLevels(test.projectDir)).map((l) => l.type);
109
+
110
+ expect(types.indexOf('user')).toBeLessThan(types.indexOf('project'));
111
+ expect(types.indexOf('project')).toBeLessThan(types.indexOf('local'));
112
+ expect(types.indexOf('local')).toBeLessThan(types.indexOf('managed'));
113
+ });
114
+
115
+ it('should include both claude and gencode paths at each level', async () => {
116
+ const userLevel = (await getConfigLevels(test.projectDir)).find((l) => l.type === 'user');
117
+
118
+ expect(userLevel?.paths.length).toBe(2);
119
+ expect(userLevel?.paths.some((p) => p.namespace === 'claude')).toBe(true);
120
+ expect(userLevel?.paths.some((p) => p.namespace === 'gencode')).toBe(true);
121
+ });
122
+
123
+ it('should include extra dirs when env var is set', async () => {
124
+ process.env.GENCODE_CONFIG_DIRS = '/team/config';
125
+ const extraLevels = (await getConfigLevels(test.projectDir)).filter((l) => l.type === 'extra');
126
+
127
+ expect(extraLevels.length).toBeGreaterThan(0);
128
+ });
129
+
130
+ it('should have claude before gencode in each level (for merge order)', async () => {
131
+ for (const level of await getConfigLevels(test.projectDir)) {
132
+ if (level.paths.length >= 2) {
133
+ const claudeIdx = level.paths.findIndex((p) => p.namespace === 'claude');
134
+ const gencodeIdx = level.paths.findIndex((p) => p.namespace === 'gencode');
135
+ if (claudeIdx !== -1 && gencodeIdx !== -1) {
136
+ expect(claudeIdx).toBeLessThan(gencodeIdx);
137
+ }
138
+ }
139
+ }
140
+ });
141
+ });
142
+
143
+ describe('getPrimarySettingsDir', () => {
144
+ it('should return ~/.gencode for user level', () => {
145
+ expect(getPrimarySettingsDir('user', '/project')).toBe(path.join(os.homedir(), '.gencode'));
146
+ });
147
+
148
+ it('should return project/.gencode for project and local levels', () => {
149
+ expect(getPrimarySettingsDir('project', '/my/project')).toBe('/my/project/.gencode');
150
+ expect(getPrimarySettingsDir('local', '/my/project')).toBe('/my/project/.gencode');
151
+ });
152
+ });
153
+
154
+ describe('getSettingsFilePath', () => {
155
+ it('should return correct paths for each level', () => {
156
+ expect(getSettingsFilePath('user', '/project'))
157
+ .toBe(path.join(os.homedir(), '.gencode', 'settings.json'));
158
+ expect(getSettingsFilePath('project', '/my/project'))
159
+ .toBe('/my/project/.gencode/settings.json');
160
+ expect(getSettingsFilePath('local', '/my/project'))
161
+ .toBe('/my/project/.gencode/settings.local.json');
162
+ });
163
+ });
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Configuration Levels - Path resolution for multi-level config
3
+ *
4
+ * Defines the configuration hierarchy and resolves paths for each level.
5
+ * At each level, both .gencode and .claude directories are loaded and merged,
6
+ * with .gencode taking higher priority.
7
+ */
8
+
9
+ import * as path from 'path';
10
+ import * as os from 'os';
11
+ import * as fs from 'fs/promises';
12
+ import {
13
+ ConfigLevelType,
14
+ GENCODE_DIR,
15
+ CLAUDE_DIR,
16
+ SETTINGS_FILE_NAME,
17
+ SETTINGS_LOCAL_FILE_NAME,
18
+ MANAGED_SETTINGS_FILE_NAME,
19
+ GENCODE_CONFIG_DIRS_ENV,
20
+ getManagedPaths,
21
+ } from './types.js';
22
+
23
+ /**
24
+ * Configuration path info for a specific namespace
25
+ */
26
+ export interface ConfigPathInfo {
27
+ settingsPath: string;
28
+ localSettingsPath?: string;
29
+ dir: string;
30
+ namespace: 'gencode' | 'claude' | 'extra';
31
+ exists: boolean;
32
+ }
33
+
34
+ /**
35
+ * Configuration level with resolved paths
36
+ */
37
+ export interface ResolvedLevel {
38
+ type: ConfigLevelType;
39
+ priority: number;
40
+ paths: ConfigPathInfo[];
41
+ description: string;
42
+ }
43
+
44
+ /**
45
+ * Find the project root by walking up the directory tree
46
+ */
47
+ export async function findProjectRoot(cwd: string): Promise<string> {
48
+ let current = path.resolve(cwd);
49
+ const root = path.parse(current).root;
50
+
51
+ while (current !== root) {
52
+ // Check for git directory
53
+ try {
54
+ await fs.access(path.join(current, '.git'));
55
+ return current;
56
+ } catch {
57
+ // Not a git root, continue
58
+ }
59
+
60
+ // Check for .gencode or .claude directories
61
+ try {
62
+ await fs.access(path.join(current, GENCODE_DIR));
63
+ return current;
64
+ } catch {
65
+ // Continue
66
+ }
67
+
68
+ try {
69
+ await fs.access(path.join(current, CLAUDE_DIR));
70
+ return current;
71
+ } catch {
72
+ // Continue
73
+ }
74
+
75
+ const parent = path.dirname(current);
76
+ if (parent === current) break;
77
+ current = parent;
78
+ }
79
+
80
+ return cwd;
81
+ }
82
+
83
+ /**
84
+ * Check if a path exists
85
+ */
86
+ async function pathExists(filePath: string): Promise<boolean> {
87
+ try {
88
+ await fs.access(filePath);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Parse GENCODE_CONFIG_DIRS environment variable
97
+ */
98
+ export function parseExtraConfigDirs(): string[] {
99
+ const value = process.env[GENCODE_CONFIG_DIRS_ENV];
100
+ if (!value) return [];
101
+
102
+ return value
103
+ .split(':')
104
+ .map((dir) => dir.trim())
105
+ .filter((dir) => dir.length > 0)
106
+ .map((dir) => dir.replace(/^~/, os.homedir()));
107
+ }
108
+
109
+ /**
110
+ * Get all configuration levels with resolved paths
111
+ */
112
+ export async function getConfigLevels(cwd: string): Promise<ResolvedLevel[]> {
113
+ const projectRoot = await findProjectRoot(cwd);
114
+ const home = os.homedir();
115
+ const managedPaths = getManagedPaths();
116
+ const extraDirs = parseExtraConfigDirs();
117
+
118
+ const levels: ResolvedLevel[] = [];
119
+
120
+ // Level 1: User (lowest priority for settings, loaded first)
121
+ const userPaths: ConfigPathInfo[] = [];
122
+
123
+ // Claude first (lower priority within level)
124
+ const userClaudeDir = path.join(home, CLAUDE_DIR);
125
+ userPaths.push({
126
+ settingsPath: path.join(userClaudeDir, SETTINGS_FILE_NAME),
127
+ dir: userClaudeDir,
128
+ namespace: 'claude',
129
+ exists: await pathExists(path.join(userClaudeDir, SETTINGS_FILE_NAME)),
130
+ });
131
+
132
+ // GenCode second (higher priority within level)
133
+ const userGencodeDir = path.join(home, GENCODE_DIR);
134
+ userPaths.push({
135
+ settingsPath: path.join(userGencodeDir, SETTINGS_FILE_NAME),
136
+ dir: userGencodeDir,
137
+ namespace: 'gencode',
138
+ exists: await pathExists(path.join(userGencodeDir, SETTINGS_FILE_NAME)),
139
+ });
140
+
141
+ levels.push({
142
+ type: 'user',
143
+ priority: 10,
144
+ paths: userPaths,
145
+ description: 'User global settings',
146
+ });
147
+
148
+ // Level 2: Extra config dirs from environment variable
149
+ if (extraDirs.length > 0) {
150
+ for (let i = 0; i < extraDirs.length; i++) {
151
+ const dir = extraDirs[i];
152
+ const extraPaths: ConfigPathInfo[] = [
153
+ {
154
+ settingsPath: path.join(dir, SETTINGS_FILE_NAME),
155
+ dir,
156
+ namespace: 'extra',
157
+ exists: await pathExists(path.join(dir, SETTINGS_FILE_NAME)),
158
+ },
159
+ ];
160
+
161
+ levels.push({
162
+ type: 'extra',
163
+ priority: 20 + i, // Each extra dir has slightly higher priority
164
+ paths: extraPaths,
165
+ description: `Extra config from ${dir}`,
166
+ });
167
+ }
168
+ }
169
+
170
+ // Level 3: Project (shared, committed to git)
171
+ const projectPaths: ConfigPathInfo[] = [];
172
+
173
+ // Claude first (lower priority within level)
174
+ const projectClaudeDir = path.join(projectRoot, CLAUDE_DIR);
175
+ projectPaths.push({
176
+ settingsPath: path.join(projectClaudeDir, SETTINGS_FILE_NAME),
177
+ dir: projectClaudeDir,
178
+ namespace: 'claude',
179
+ exists: await pathExists(path.join(projectClaudeDir, SETTINGS_FILE_NAME)),
180
+ });
181
+
182
+ // GenCode second (higher priority within level)
183
+ const projectGencodeDir = path.join(projectRoot, GENCODE_DIR);
184
+ projectPaths.push({
185
+ settingsPath: path.join(projectGencodeDir, SETTINGS_FILE_NAME),
186
+ dir: projectGencodeDir,
187
+ namespace: 'gencode',
188
+ exists: await pathExists(path.join(projectGencodeDir, SETTINGS_FILE_NAME)),
189
+ });
190
+
191
+ levels.push({
192
+ type: 'project',
193
+ priority: 30,
194
+ paths: projectPaths,
195
+ description: 'Project shared settings',
196
+ });
197
+
198
+ // Level 4: Local (personal, gitignored)
199
+ const localPaths: ConfigPathInfo[] = [];
200
+
201
+ // Claude first (lower priority within level)
202
+ localPaths.push({
203
+ settingsPath: path.join(projectClaudeDir, SETTINGS_LOCAL_FILE_NAME),
204
+ dir: projectClaudeDir,
205
+ namespace: 'claude',
206
+ exists: await pathExists(path.join(projectClaudeDir, SETTINGS_LOCAL_FILE_NAME)),
207
+ });
208
+
209
+ // GenCode second (higher priority within level)
210
+ localPaths.push({
211
+ settingsPath: path.join(projectGencodeDir, SETTINGS_LOCAL_FILE_NAME),
212
+ dir: projectGencodeDir,
213
+ namespace: 'gencode',
214
+ exists: await pathExists(path.join(projectGencodeDir, SETTINGS_LOCAL_FILE_NAME)),
215
+ });
216
+
217
+ levels.push({
218
+ type: 'local',
219
+ priority: 40,
220
+ paths: localPaths,
221
+ description: 'Local personal settings (gitignored)',
222
+ });
223
+
224
+ // Level 5: Managed (highest priority, enforced)
225
+ const managedPathsList: ConfigPathInfo[] = [];
226
+
227
+ // Claude first (lower priority within level)
228
+ managedPathsList.push({
229
+ settingsPath: path.join(managedPaths.claude, MANAGED_SETTINGS_FILE_NAME),
230
+ dir: managedPaths.claude,
231
+ namespace: 'claude',
232
+ exists: await pathExists(path.join(managedPaths.claude, MANAGED_SETTINGS_FILE_NAME)),
233
+ });
234
+
235
+ // GenCode second (higher priority within level)
236
+ managedPathsList.push({
237
+ settingsPath: path.join(managedPaths.gencode, MANAGED_SETTINGS_FILE_NAME),
238
+ dir: managedPaths.gencode,
239
+ namespace: 'gencode',
240
+ exists: await pathExists(path.join(managedPaths.gencode, MANAGED_SETTINGS_FILE_NAME)),
241
+ });
242
+
243
+ levels.push({
244
+ type: 'managed',
245
+ priority: 100, // Highest priority
246
+ paths: managedPathsList,
247
+ description: 'System-wide managed settings (enforced)',
248
+ });
249
+
250
+ // Sort by priority (ascending, so we merge from low to high)
251
+ return levels.sort((a, b) => a.priority - b.priority);
252
+ }
253
+
254
+ /**
255
+ * Get the primary settings directory for saving
256
+ * Prefers .gencode if it exists, otherwise creates it
257
+ */
258
+ export function getPrimarySettingsDir(
259
+ level: 'user' | 'project' | 'local',
260
+ projectRoot: string
261
+ ): string {
262
+ const home = os.homedir();
263
+
264
+ switch (level) {
265
+ case 'user':
266
+ return path.join(home, GENCODE_DIR);
267
+ case 'project':
268
+ case 'local':
269
+ return path.join(projectRoot, GENCODE_DIR);
270
+ default:
271
+ return path.join(home, GENCODE_DIR);
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Get the settings file path for a specific level
277
+ */
278
+ export function getSettingsFilePath(
279
+ level: 'user' | 'project' | 'local',
280
+ projectRoot: string
281
+ ): string {
282
+ const dir = getPrimarySettingsDir(level, projectRoot);
283
+ const fileName = level === 'local' ? SETTINGS_LOCAL_FILE_NAME : SETTINGS_FILE_NAME;
284
+ return path.join(dir, fileName);
285
+ }