@build-astron-co/nimbus 0.2.0

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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Config Manager
3
+ *
4
+ * Manages Nimbus configuration stored at ~/.nimbus/config.yaml
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import { CONFIG_KEYS, type NimbusConfig, type ConfigKey } from './types';
11
+ import { NimbusConfigSchema } from './schema';
12
+
13
+ const CONFIG_VERSION = 1;
14
+
15
+ /**
16
+ * Forbidden keys that could lead to prototype pollution
17
+ */
18
+ const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype'];
19
+
20
+ /**
21
+ * Check if a key is safe from prototype pollution
22
+ */
23
+ function isSafeKey(key: string): boolean {
24
+ return !FORBIDDEN_KEYS.includes(key);
25
+ }
26
+
27
+ /**
28
+ * Validate all parts of a dot-notation key path
29
+ */
30
+ function validateKeyPath(key: string): void {
31
+ const parts = key.split('.');
32
+ for (const part of parts) {
33
+ if (!isSafeKey(part)) {
34
+ throw new Error(`Invalid config key: "${part}" is not allowed`);
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Resolve environment variables in config values.
41
+ * Supports ${VAR} and ${VAR:-default} syntax.
42
+ * Recursively walks objects and arrays.
43
+ */
44
+ function resolveEnvVars(value: any): any {
45
+ if (typeof value === 'string') {
46
+ return value.replace(/\$\{([^}]+)\}/g, (_match, expr: string) => {
47
+ const defaultSep = expr.indexOf(':-');
48
+ if (defaultSep !== -1) {
49
+ const varName = expr.slice(0, defaultSep);
50
+ const defaultValue = expr.slice(defaultSep + 2);
51
+ return process.env[varName] ?? defaultValue;
52
+ }
53
+ return process.env[expr] ?? '';
54
+ });
55
+ }
56
+ if (Array.isArray(value)) {
57
+ return value.map(resolveEnvVars);
58
+ }
59
+ if (value !== null && typeof value === 'object') {
60
+ const result: Record<string, any> = {};
61
+ for (const [k, v] of Object.entries(value)) {
62
+ result[k] = resolveEnvVars(v);
63
+ }
64
+ return result;
65
+ }
66
+ return value;
67
+ }
68
+
69
+ /**
70
+ * Create default configuration
71
+ */
72
+ function createDefaultConfig(): NimbusConfig {
73
+ return {
74
+ version: CONFIG_VERSION,
75
+ workspace: {
76
+ defaultProvider: 'aws',
77
+ outputDirectory: './infrastructure',
78
+ },
79
+ llm: {
80
+ temperature: 0.7,
81
+ maxTokens: 4096,
82
+ },
83
+ history: {
84
+ maxEntries: 100,
85
+ enabled: true,
86
+ },
87
+ safety: {
88
+ requireConfirmation: true,
89
+ dryRunByDefault: false,
90
+ },
91
+ ui: {
92
+ theme: 'auto',
93
+ colors: true,
94
+ spinner: 'dots',
95
+ },
96
+ persona: {
97
+ mode: 'standard',
98
+ verbosity: 'normal',
99
+ custom: '',
100
+ },
101
+ cloud: {
102
+ default_provider: 'aws',
103
+ aws: { default_region: 'us-east-1', default_profile: 'default' },
104
+ gcp: { default_region: 'us-central1' },
105
+ azure: { default_region: 'eastus' },
106
+ },
107
+ terraform: { default_backend: 's3' },
108
+ kubernetes: { default_namespace: 'default' },
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Parse YAML-like config (simple key: value format)
114
+ * Note: This is a simple parser, not full YAML
115
+ */
116
+ function parseSimpleYaml(content: string): Record<string, any> {
117
+ const result: Record<string, any> = {};
118
+ const lines = content.split('\n');
119
+ const stack: Array<{ indent: number; obj: Record<string, any>; key: string }> = [];
120
+ let currentObj = result;
121
+ let currentIndent = 0;
122
+
123
+ for (const line of lines) {
124
+ // Skip comments and empty lines
125
+ if (line.trim().startsWith('#') || !line.trim()) {
126
+ continue;
127
+ }
128
+
129
+ const indent = line.search(/\S/);
130
+ const trimmed = line.trim();
131
+
132
+ // Handle nested objects
133
+ if (trimmed.endsWith(':')) {
134
+ const key = trimmed.slice(0, -1);
135
+
136
+ if (indent > currentIndent) {
137
+ // Going deeper
138
+ stack.push({ indent: currentIndent, obj: currentObj, key: '' });
139
+ } else if (indent < currentIndent) {
140
+ // Going back up
141
+ while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
142
+ const item = stack.pop()!;
143
+ currentObj = item.obj;
144
+ }
145
+ }
146
+
147
+ currentObj[key] = {};
148
+ stack.push({ indent, obj: currentObj, key });
149
+ currentObj = currentObj[key];
150
+ currentIndent = indent;
151
+ } else if (trimmed.includes(':')) {
152
+ // Key-value pair
153
+ const colonIndex = trimmed.indexOf(':');
154
+ const key = trimmed.slice(0, colonIndex).trim();
155
+ let value: any = trimmed.slice(colonIndex + 1).trim();
156
+
157
+ // Handle indentation changes
158
+ if (indent < currentIndent && stack.length > 0) {
159
+ while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
160
+ const item = stack.pop()!;
161
+ currentObj = item.obj;
162
+ }
163
+ currentIndent = indent;
164
+ }
165
+
166
+ // Parse value type
167
+ if (value === 'true') {
168
+ value = true;
169
+ } else if (value === 'false') {
170
+ value = false;
171
+ } else if (value === 'null' || value === '~') {
172
+ value = null;
173
+ } else if (!isNaN(Number(value)) && value !== '') {
174
+ value = Number(value);
175
+ } else if (
176
+ (value.startsWith('"') && value.endsWith('"')) ||
177
+ (value.startsWith("'") && value.endsWith("'"))
178
+ ) {
179
+ value = value.slice(1, -1);
180
+ }
181
+
182
+ currentObj[key] = value;
183
+ }
184
+ }
185
+
186
+ return result;
187
+ }
188
+
189
+ /**
190
+ * Serialize config to YAML-like format
191
+ */
192
+ function serializeToYaml(config: Record<string, any>, indent = 0): string {
193
+ const lines: string[] = [];
194
+ const prefix = ' '.repeat(indent);
195
+
196
+ for (const [key, value] of Object.entries(config)) {
197
+ if (value === null || value === undefined) {
198
+ continue;
199
+ }
200
+
201
+ if (typeof value === 'object' && !Array.isArray(value)) {
202
+ lines.push(`${prefix}${key}:`);
203
+ lines.push(serializeToYaml(value, indent + 1));
204
+ } else {
205
+ let serializedValue: string;
206
+
207
+ if (typeof value === 'string') {
208
+ // Quote strings that need it - escape backslashes and quotes
209
+ if (
210
+ value.includes(':') ||
211
+ value.includes('#') ||
212
+ value.includes("'") ||
213
+ value.includes('"') ||
214
+ value.includes('\\')
215
+ ) {
216
+ serializedValue = `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
217
+ } else {
218
+ serializedValue = value;
219
+ }
220
+ } else if (typeof value === 'boolean') {
221
+ serializedValue = value ? 'true' : 'false';
222
+ } else {
223
+ serializedValue = String(value);
224
+ }
225
+
226
+ lines.push(`${prefix}${key}: ${serializedValue}`);
227
+ }
228
+ }
229
+
230
+ return lines.join('\n');
231
+ }
232
+
233
+ /**
234
+ * ConfigManager class for configuration persistence
235
+ */
236
+ export class ConfigManager {
237
+ private configPath: string;
238
+ private config: NimbusConfig | null = null;
239
+
240
+ constructor(configPath?: string) {
241
+ this.configPath = configPath || path.join(os.homedir(), '.nimbus', 'config.yaml');
242
+ }
243
+
244
+ /**
245
+ * Deep merge two objects, with source values taking precedence
246
+ */
247
+ private deepMerge(target: any, source: any): any {
248
+ const result = { ...target };
249
+ for (const key of Object.keys(source)) {
250
+ if (
251
+ source[key] &&
252
+ typeof source[key] === 'object' &&
253
+ !Array.isArray(source[key]) &&
254
+ target[key] &&
255
+ typeof target[key] === 'object' &&
256
+ !Array.isArray(target[key])
257
+ ) {
258
+ result[key] = this.deepMerge(target[key], source[key]);
259
+ } else if (source[key] !== undefined) {
260
+ result[key] = source[key];
261
+ }
262
+ }
263
+ return result;
264
+ }
265
+
266
+ /**
267
+ * Get the path to the config file
268
+ */
269
+ getConfigPath(): string {
270
+ return this.configPath;
271
+ }
272
+
273
+ /**
274
+ * Ensure the config directory exists
275
+ */
276
+ private ensureDirectory(): void {
277
+ const dir = path.dirname(this.configPath);
278
+ if (!fs.existsSync(dir)) {
279
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Load configuration from disk
285
+ */
286
+ load(): NimbusConfig {
287
+ if (this.config) {
288
+ return this.config;
289
+ }
290
+
291
+ this.ensureDirectory();
292
+
293
+ if (!fs.existsSync(this.configPath)) {
294
+ this.config = createDefaultConfig();
295
+ return this.config;
296
+ }
297
+
298
+ try {
299
+ const content = fs.readFileSync(this.configPath, 'utf-8');
300
+ const parsed = resolveEnvVars(parseSimpleYaml(content));
301
+
302
+ // Validate with Zod schema
303
+ const parseResult = NimbusConfigSchema.safeParse(parsed);
304
+ const defaults = createDefaultConfig();
305
+
306
+ if (!parseResult.success) {
307
+ // Use defaults for invalid fields — don't crash
308
+ const merged = this.deepMerge(defaults, parsed);
309
+ this.config = merged as NimbusConfig;
310
+ } else {
311
+ this.config = this.deepMerge(defaults, parseResult.data) as NimbusConfig;
312
+ }
313
+
314
+ this.config.version = CONFIG_VERSION;
315
+ return this.config;
316
+ } catch {
317
+ // If file is corrupted, start fresh
318
+ this.config = createDefaultConfig();
319
+ return this.config;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Save configuration to disk
325
+ */
326
+ save(config?: NimbusConfig): void {
327
+ this.ensureDirectory();
328
+
329
+ const configToSave = config || this.config;
330
+ if (!configToSave) {
331
+ throw new Error('No config to save');
332
+ }
333
+
334
+ this.config = configToSave;
335
+
336
+ const header = `# Nimbus CLI Configuration
337
+ # Version: ${CONFIG_VERSION}
338
+ # Documentation: https://github.com/the-ai-project-co/nimbus
339
+ #
340
+ # Edit this file to customize Nimbus behavior.
341
+ # Run 'nimbus config list' to see all available options.
342
+
343
+ `;
344
+
345
+ const content = header + serializeToYaml(configToSave);
346
+ fs.writeFileSync(this.configPath, content, { mode: 0o600 });
347
+ }
348
+
349
+ /**
350
+ * Check if config file exists
351
+ */
352
+ exists(): boolean {
353
+ return fs.existsSync(this.configPath);
354
+ }
355
+
356
+ /**
357
+ * Get a configuration value by dot-notation key
358
+ */
359
+ get(key: string): any {
360
+ const config = this.load();
361
+ const parts = key.split('.');
362
+ let value: any = config;
363
+
364
+ for (const part of parts) {
365
+ if (value === undefined || value === null) {
366
+ return undefined;
367
+ }
368
+ value = value[part];
369
+ }
370
+
371
+ return value;
372
+ }
373
+
374
+ /**
375
+ * Set a configuration value by dot-notation key
376
+ */
377
+ set(key: string, value: any): void {
378
+ // Guard against prototype pollution
379
+ validateKeyPath(key);
380
+
381
+ const config = this.load();
382
+ const configCopy = JSON.parse(JSON.stringify(config));
383
+ const parts = key.split('.');
384
+ let obj: any = configCopy;
385
+
386
+ // Navigate to parent object
387
+ for (let i = 0; i < parts.length - 1; i++) {
388
+ const part = parts[i];
389
+ if (obj[part] === undefined) {
390
+ // Use Object.create(null) to avoid prototype chain
391
+ obj[part] = Object.create(null);
392
+ }
393
+ obj = obj[part];
394
+ }
395
+
396
+ // Set the value
397
+ const lastPart = parts[parts.length - 1];
398
+ obj[lastPart] = value;
399
+
400
+ // Validate the entire config after the change
401
+ const result = NimbusConfigSchema.safeParse(configCopy);
402
+ if (!result.success) {
403
+ const issue = result.error.issues[0];
404
+ throw new Error(`Invalid value for '${key}': ${issue?.message || 'validation failed'}`);
405
+ }
406
+
407
+ // Apply to the real config and save
408
+ let realObj: any = config;
409
+ for (let i = 0; i < parts.length - 1; i++) {
410
+ const part = parts[i];
411
+ if (realObj[part] === undefined) {
412
+ realObj[part] = Object.create(null);
413
+ }
414
+ realObj = realObj[part];
415
+ }
416
+ realObj[lastPart] = value;
417
+
418
+ this.save(config);
419
+ }
420
+
421
+ /**
422
+ * Delete a configuration value by dot-notation key
423
+ */
424
+ delete(key: string): void {
425
+ // Guard against prototype pollution
426
+ validateKeyPath(key);
427
+
428
+ const config = this.load();
429
+ const parts = key.split('.');
430
+ let obj: any = config;
431
+
432
+ // Navigate to parent object
433
+ for (let i = 0; i < parts.length - 1; i++) {
434
+ const part = parts[i];
435
+ if (!Object.prototype.hasOwnProperty.call(obj, part) || obj[part] === undefined) {
436
+ return; // Key doesn't exist
437
+ }
438
+ obj = obj[part];
439
+ }
440
+
441
+ // Delete the value
442
+ const lastPart = parts[parts.length - 1];
443
+ delete obj[lastPart];
444
+
445
+ this.save(config);
446
+ }
447
+
448
+ /**
449
+ * Get all configuration as flat key-value pairs
450
+ */
451
+ getAllFlat(): Record<string, any> {
452
+ const config = this.load();
453
+ const result: Record<string, any> = {};
454
+
455
+ function flatten(obj: any, prefix = '') {
456
+ for (const [key, value] of Object.entries(obj)) {
457
+ const fullKey = prefix ? `${prefix}.${key}` : key;
458
+
459
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
460
+ flatten(value, fullKey);
461
+ } else {
462
+ result[fullKey] = value;
463
+ }
464
+ }
465
+ }
466
+
467
+ flatten(config);
468
+ return result;
469
+ }
470
+
471
+ /**
472
+ * Reset configuration to defaults
473
+ */
474
+ reset(): void {
475
+ this.config = createDefaultConfig();
476
+ this.save();
477
+ }
478
+
479
+ /**
480
+ * Reload configuration from disk
481
+ */
482
+ reload(): NimbusConfig {
483
+ this.config = null;
484
+ return this.load();
485
+ }
486
+
487
+ /**
488
+ * Get config key info
489
+ */
490
+ getKeyInfo(key: string): (typeof CONFIG_KEYS)[number] | undefined {
491
+ return CONFIG_KEYS.find(k => k.key === key);
492
+ }
493
+
494
+ /**
495
+ * Validate a key exists in the schema
496
+ */
497
+ isValidKey(key: string): boolean {
498
+ return CONFIG_KEYS.some(k => k.key === key);
499
+ }
500
+
501
+ /**
502
+ * Parse a value according to the key's type
503
+ */
504
+ parseValue(key: string, value: string): any {
505
+ const keyInfo = this.getKeyInfo(key);
506
+ if (!keyInfo) {
507
+ // Unknown key, return as string
508
+ return value;
509
+ }
510
+
511
+ switch (keyInfo.type) {
512
+ case 'boolean':
513
+ return value.toLowerCase() === 'true';
514
+ case 'number':
515
+ return Number(value);
516
+ case 'string':
517
+ default:
518
+ return value;
519
+ }
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Export singleton instance
525
+ */
526
+ export const configManager = new ConfigManager();
527
+
528
+ // Re-export types
529
+ export { CONFIG_KEYS };
530
+ export type { ConfigKey };