@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,436 @@
1
+ /**
2
+ * Tests for the Nimbus project initialization module (src/cli/init.ts).
3
+ *
4
+ * Each test creates a temporary directory with the appropriate marker files,
5
+ * exercises the detection and init functions, and cleans up afterward.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import * as os from 'node:os';
12
+
13
+ import {
14
+ detectProjectType,
15
+ detectInfrastructure,
16
+ detectCloudProviders,
17
+ detectPackageManager,
18
+ detectProject,
19
+ generateNimbusMd,
20
+ runInit,
21
+ } from '../cli/init';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Shared helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function makeTmpDir(): string {
28
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-init-test-'));
29
+ }
30
+
31
+ function removeTmpDir(dir: string): void {
32
+ fs.rmSync(dir, { recursive: true, force: true });
33
+ }
34
+
35
+ // ============================================================================
36
+ // detectProjectType()
37
+ // ============================================================================
38
+
39
+ describe('detectProjectType()', () => {
40
+ let tmpDir: string;
41
+
42
+ beforeEach(() => {
43
+ tmpDir = makeTmpDir();
44
+ });
45
+
46
+ afterEach(() => {
47
+ removeTmpDir(tmpDir);
48
+ });
49
+
50
+ test('detects TypeScript (tsconfig.json)', () => {
51
+ fs.writeFileSync(path.join(tmpDir, 'tsconfig.json'), '{}');
52
+ expect(detectProjectType(tmpDir)).toBe('typescript');
53
+ });
54
+
55
+ test('detects Go (go.mod)', () => {
56
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/myapp\n\ngo 1.21\n');
57
+ expect(detectProjectType(tmpDir)).toBe('go');
58
+ });
59
+
60
+ test('detects Python (pyproject.toml)', () => {
61
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "myapp"\n');
62
+ expect(detectProjectType(tmpDir)).toBe('python');
63
+ });
64
+
65
+ test('detects Rust (Cargo.toml)', () => {
66
+ fs.writeFileSync(
67
+ path.join(tmpDir, 'Cargo.toml'),
68
+ '[package]\nname = "myapp"\nversion = "0.1.0"\n'
69
+ );
70
+ expect(detectProjectType(tmpDir)).toBe('rust');
71
+ });
72
+
73
+ test('detects Java (pom.xml)', () => {
74
+ fs.writeFileSync(
75
+ path.join(tmpDir, 'pom.xml'),
76
+ '<project><modelVersion>4.0.0</modelVersion></project>'
77
+ );
78
+ expect(detectProjectType(tmpDir)).toBe('java');
79
+ });
80
+
81
+ test('returns "unknown" for empty dir', () => {
82
+ expect(detectProjectType(tmpDir)).toBe('unknown');
83
+ });
84
+ });
85
+
86
+ // ============================================================================
87
+ // detectInfrastructure()
88
+ // ============================================================================
89
+
90
+ describe('detectInfrastructure()', () => {
91
+ let tmpDir: string;
92
+
93
+ beforeEach(() => {
94
+ tmpDir = makeTmpDir();
95
+ });
96
+
97
+ afterEach(() => {
98
+ removeTmpDir(tmpDir);
99
+ });
100
+
101
+ test('detects Terraform (.tf files)', () => {
102
+ fs.writeFileSync(path.join(tmpDir, 'main.tf'), 'provider "aws" {\n region = "us-east-1"\n}\n');
103
+
104
+ const infra = detectInfrastructure(tmpDir);
105
+ expect(infra).toContain('terraform');
106
+ });
107
+
108
+ test('detects Kubernetes (manifest with kind: Deployment)', () => {
109
+ fs.writeFileSync(
110
+ path.join(tmpDir, 'deployment.yaml'),
111
+ ['apiVersion: apps/v1', 'kind: Deployment', 'metadata:', ' name: web'].join('\n')
112
+ );
113
+
114
+ const infra = detectInfrastructure(tmpDir);
115
+ expect(infra).toContain('kubernetes');
116
+ });
117
+
118
+ test('detects Helm (Chart.yaml)', () => {
119
+ fs.writeFileSync(
120
+ path.join(tmpDir, 'Chart.yaml'),
121
+ 'apiVersion: v2\nname: my-chart\nversion: 0.1.0\n'
122
+ );
123
+
124
+ const infra = detectInfrastructure(tmpDir);
125
+ expect(infra).toContain('helm');
126
+ });
127
+
128
+ test('detects Docker (Dockerfile)', () => {
129
+ fs.writeFileSync(path.join(tmpDir, 'Dockerfile'), 'FROM node:20-alpine\nWORKDIR /app\n');
130
+
131
+ const infra = detectInfrastructure(tmpDir);
132
+ expect(infra).toContain('docker');
133
+ });
134
+
135
+ test('detects CI/CD (.github/workflows/)', () => {
136
+ const workflowsDir = path.join(tmpDir, '.github', 'workflows');
137
+ fs.mkdirSync(workflowsDir, { recursive: true });
138
+ fs.writeFileSync(path.join(workflowsDir, 'ci.yml'), 'name: CI\non: push\n');
139
+
140
+ const infra = detectInfrastructure(tmpDir);
141
+ expect(infra).toContain('cicd');
142
+ });
143
+
144
+ test('returns empty array for bare directory', () => {
145
+ const infra = detectInfrastructure(tmpDir);
146
+ expect(infra).toEqual([]);
147
+ });
148
+ });
149
+
150
+ // ============================================================================
151
+ // detectCloudProviders()
152
+ // ============================================================================
153
+
154
+ describe('detectCloudProviders()', () => {
155
+ let tmpDir: string;
156
+
157
+ beforeEach(() => {
158
+ tmpDir = makeTmpDir();
159
+ });
160
+
161
+ afterEach(() => {
162
+ removeTmpDir(tmpDir);
163
+ });
164
+
165
+ test('detects AWS (provider "aws" in .tf)', () => {
166
+ fs.writeFileSync(
167
+ path.join(tmpDir, 'providers.tf'),
168
+ 'provider "aws" {\n region = "us-east-1"\n}\n'
169
+ );
170
+
171
+ const providers = detectCloudProviders(tmpDir);
172
+ expect(providers).toContain('aws');
173
+ });
174
+
175
+ test('detects GCP (provider "google" in .tf)', () => {
176
+ fs.writeFileSync(
177
+ path.join(tmpDir, 'providers.tf'),
178
+ 'provider "google" {\n project = "my-project"\n}\n'
179
+ );
180
+
181
+ const providers = detectCloudProviders(tmpDir);
182
+ expect(providers).toContain('gcp');
183
+ });
184
+
185
+ test('detects Azure (provider "azurerm" in .tf)', () => {
186
+ fs.writeFileSync(path.join(tmpDir, 'providers.tf'), 'provider "azurerm" {\n features {}\n}\n');
187
+
188
+ const providers = detectCloudProviders(tmpDir);
189
+ expect(providers).toContain('azure');
190
+ });
191
+ });
192
+
193
+ // ============================================================================
194
+ // detectPackageManager()
195
+ // ============================================================================
196
+
197
+ describe('detectPackageManager()', () => {
198
+ let tmpDir: string;
199
+
200
+ beforeEach(() => {
201
+ tmpDir = makeTmpDir();
202
+ });
203
+
204
+ afterEach(() => {
205
+ removeTmpDir(tmpDir);
206
+ });
207
+
208
+ test('detects bun (bun.lock)', () => {
209
+ fs.writeFileSync(path.join(tmpDir, 'bun.lock'), '');
210
+ expect(detectPackageManager(tmpDir)).toBe('bun');
211
+ });
212
+
213
+ test('detects npm (package-lock.json)', () => {
214
+ fs.writeFileSync(path.join(tmpDir, 'package-lock.json'), '{}');
215
+ expect(detectPackageManager(tmpDir)).toBe('npm');
216
+ });
217
+
218
+ test('returns undefined when no lock file is present', () => {
219
+ expect(detectPackageManager(tmpDir)).toBeUndefined();
220
+ });
221
+ });
222
+
223
+ // ============================================================================
224
+ // detectProject()
225
+ // ============================================================================
226
+
227
+ describe('detectProject()', () => {
228
+ let tmpDir: string;
229
+
230
+ beforeEach(() => {
231
+ tmpDir = makeTmpDir();
232
+ });
233
+
234
+ afterEach(() => {
235
+ removeTmpDir(tmpDir);
236
+ });
237
+
238
+ test('aggregates all detection results', () => {
239
+ // TypeScript project
240
+ fs.writeFileSync(path.join(tmpDir, 'tsconfig.json'), '{}');
241
+ // Bun lock file
242
+ fs.writeFileSync(path.join(tmpDir, 'bun.lock'), '');
243
+ // Terraform
244
+ fs.writeFileSync(path.join(tmpDir, 'main.tf'), 'provider "aws" {\n region = "us-east-1"\n}\n');
245
+ // Docker
246
+ fs.writeFileSync(path.join(tmpDir, 'Dockerfile'), 'FROM node:20\n');
247
+ // Git
248
+ fs.mkdirSync(path.join(tmpDir, '.git'));
249
+
250
+ const detection = detectProject(tmpDir);
251
+
252
+ expect(detection.projectName).toBe(path.basename(tmpDir));
253
+ expect(detection.projectType).toBe('typescript');
254
+ expect(detection.packageManager).toBe('bun');
255
+ expect(detection.infraTypes).toContain('terraform');
256
+ expect(detection.infraTypes).toContain('docker');
257
+ expect(detection.cloudProviders).toContain('aws');
258
+ expect(detection.hasGit).toBe(true);
259
+ });
260
+ });
261
+
262
+ // ============================================================================
263
+ // generateNimbusMd()
264
+ // ============================================================================
265
+
266
+ describe('generateNimbusMd()', () => {
267
+ test('includes project name', () => {
268
+ const md = generateNimbusMd(
269
+ {
270
+ projectName: 'my-cool-app',
271
+ projectType: 'typescript',
272
+ infraTypes: [],
273
+ cloudProviders: [],
274
+ hasGit: true,
275
+ packageManager: 'bun',
276
+ },
277
+ '/tmp/my-cool-app'
278
+ );
279
+
280
+ expect(md).toContain('# my-cool-app');
281
+ });
282
+
283
+ test('includes detected infrastructure', () => {
284
+ const md = generateNimbusMd(
285
+ {
286
+ projectName: 'infra-project',
287
+ projectType: 'typescript',
288
+ infraTypes: ['terraform', 'kubernetes', 'docker'],
289
+ cloudProviders: ['aws', 'gcp'],
290
+ hasGit: true,
291
+ packageManager: 'npm',
292
+ },
293
+ '/tmp/infra-project'
294
+ );
295
+
296
+ expect(md).toContain('## Infrastructure');
297
+ expect(md).toContain('terraform');
298
+ expect(md).toContain('kubernetes');
299
+ expect(md).toContain('docker');
300
+ expect(md).toContain('aws');
301
+ expect(md).toContain('gcp');
302
+ });
303
+
304
+ test('includes safety rules section', () => {
305
+ const md = generateNimbusMd(
306
+ {
307
+ projectName: 'test-project',
308
+ projectType: 'go',
309
+ infraTypes: [],
310
+ cloudProviders: [],
311
+ hasGit: false,
312
+ },
313
+ '/tmp/test-project'
314
+ );
315
+
316
+ expect(md).toContain('## Safety Rules');
317
+ expect(md).toContain('Protected branches');
318
+ expect(md).toContain('Never store secrets');
319
+ });
320
+
321
+ test('includes package manager when present', () => {
322
+ const md = generateNimbusMd(
323
+ {
324
+ projectName: 'bun-app',
325
+ projectType: 'typescript',
326
+ infraTypes: [],
327
+ cloudProviders: [],
328
+ hasGit: true,
329
+ packageManager: 'bun',
330
+ },
331
+ '/tmp/bun-app'
332
+ );
333
+
334
+ expect(md).toContain('**Package Manager:** bun');
335
+ });
336
+ });
337
+
338
+ // ============================================================================
339
+ // runInit()
340
+ // ============================================================================
341
+
342
+ describe('runInit()', () => {
343
+ let tmpDir: string;
344
+
345
+ beforeEach(() => {
346
+ tmpDir = makeTmpDir();
347
+ // Create a minimal project marker so detectProjectType returns something
348
+ fs.writeFileSync(path.join(tmpDir, 'tsconfig.json'), '{}');
349
+ });
350
+
351
+ afterEach(() => {
352
+ removeTmpDir(tmpDir);
353
+ });
354
+
355
+ test('creates NIMBUS.md file', async () => {
356
+ const result = await runInit({ cwd: tmpDir, quiet: true });
357
+
358
+ const nimbusmdPath = path.join(tmpDir, 'NIMBUS.md');
359
+ expect(fs.existsSync(nimbusmdPath)).toBe(true);
360
+ expect(result.nimbusmdPath).toBe(nimbusmdPath);
361
+
362
+ const content = fs.readFileSync(nimbusmdPath, 'utf-8');
363
+ expect(content).toContain(path.basename(tmpDir));
364
+ expect(content).toContain('typescript');
365
+ });
366
+
367
+ test('creates .nimbus/ directory', async () => {
368
+ await runInit({ cwd: tmpDir, quiet: true });
369
+
370
+ const nimbusDirPath = path.join(tmpDir, '.nimbus');
371
+ expect(fs.existsSync(nimbusDirPath)).toBe(true);
372
+ expect(fs.statSync(nimbusDirPath).isDirectory()).toBe(true);
373
+ });
374
+
375
+ test('creates .nimbus/config.yaml', async () => {
376
+ const result = await runInit({ cwd: tmpDir, quiet: true });
377
+
378
+ const configPath = path.join(tmpDir, '.nimbus', 'config.yaml');
379
+ expect(fs.existsSync(configPath)).toBe(true);
380
+ expect(result.filesCreated).toContain(configPath);
381
+
382
+ const content = fs.readFileSync(configPath, 'utf-8');
383
+ expect(content).toContain('default_model:');
384
+ expect(content).toContain('permissions:');
385
+ expect(content).toContain('safety:');
386
+ expect(content).toContain('type: typescript');
387
+ });
388
+
389
+ test('throws if NIMBUS.md exists without --force', async () => {
390
+ // First init
391
+ await runInit({ cwd: tmpDir, quiet: true });
392
+ expect(fs.existsSync(path.join(tmpDir, 'NIMBUS.md'))).toBe(true);
393
+
394
+ // Second init should throw
395
+ await expect(runInit({ cwd: tmpDir, quiet: true })).rejects.toThrow('NIMBUS.md already exists');
396
+ });
397
+
398
+ test('overwrites with --force', async () => {
399
+ // First init
400
+ await runInit({ cwd: tmpDir, quiet: true });
401
+
402
+ // Modify NIMBUS.md so we can tell if it's overwritten
403
+ const nimbusmdPath = path.join(tmpDir, 'NIMBUS.md');
404
+ fs.writeFileSync(nimbusmdPath, 'CUSTOM CONTENT THAT SHOULD BE REPLACED');
405
+
406
+ // Second init with force
407
+ await runInit({ cwd: tmpDir, force: true, quiet: true });
408
+
409
+ const content = fs.readFileSync(nimbusmdPath, 'utf-8');
410
+ expect(content).not.toContain('CUSTOM CONTENT THAT SHOULD BE REPLACED');
411
+ expect(content).toContain('Auto-generated by `nimbus init`');
412
+ });
413
+
414
+ test('creates hooks and agents subdirectories', async () => {
415
+ await runInit({ cwd: tmpDir, quiet: true });
416
+
417
+ expect(fs.existsSync(path.join(tmpDir, '.nimbus', 'hooks'))).toBe(true);
418
+ expect(fs.existsSync(path.join(tmpDir, '.nimbus', 'agents'))).toBe(true);
419
+ expect(fs.existsSync(path.join(tmpDir, '.nimbus', 'hooks', 'pre-commit.ts'))).toBe(true);
420
+ expect(fs.existsSync(path.join(tmpDir, '.nimbus', 'agents', 'default.yaml'))).toBe(true);
421
+ });
422
+
423
+ test('returns correct detection results', async () => {
424
+ // Add some extra markers
425
+ fs.writeFileSync(path.join(tmpDir, 'bun.lock'), '');
426
+ fs.writeFileSync(path.join(tmpDir, 'main.tf'), 'provider "aws" {\n region = "us-east-1"\n}\n');
427
+
428
+ const result = await runInit({ cwd: tmpDir, quiet: true });
429
+
430
+ expect(result.detection.projectType).toBe('typescript');
431
+ expect(result.detection.packageManager).toBe('bun');
432
+ expect(result.detection.infraTypes).toContain('terraform');
433
+ expect(result.detection.cloudProviders).toContain('aws');
434
+ expect(result.filesCreated.length).toBeGreaterThan(0);
435
+ });
436
+ });
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Tests for src/generator/intent-parser.ts – IntentParser
3
+ *
4
+ * The IntentParser is instantiated WITHOUT an LLM router so every test
5
+ * exercises the deterministic heuristic (regex + keyword) path only.
6
+ * LLM-based classification requires live API keys and is therefore
7
+ * intentionally excluded from unit tests.
8
+ */
9
+
10
+ import { describe, it, expect } from 'bun:test';
11
+ import { IntentParser, type ConversationalIntent } from '../generator/intent-parser';
12
+
13
+ // Helper: create a parser in pure-heuristic mode (no router)
14
+ function makeParser(): IntentParser {
15
+ return new IntentParser();
16
+ }
17
+
18
+ // Helper: extract entities of a given type from a parsed result
19
+ function entitiesOfType(intent: ConversationalIntent, type: string): string[] {
20
+ return intent.entities.filter(e => e.type === type).map(e => e.value);
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Intent type detection
25
+ // ---------------------------------------------------------------------------
26
+
27
+ describe('IntentParser – generate intents', () => {
28
+ it('parse("create a vpc on aws") returns type: generate', async () => {
29
+ const parser = makeParser();
30
+ const result = await parser.parse('create a vpc on aws');
31
+ expect(result.type).toBe('generate');
32
+ });
33
+
34
+ it('parse("generate a helm chart") returns type: generate', async () => {
35
+ const parser = makeParser();
36
+ const result = await parser.parse('generate a helm chart');
37
+ expect(result.type).toBe('generate');
38
+ });
39
+
40
+ it('parse("build an eks cluster on aws") returns type: generate', async () => {
41
+ const parser = makeParser();
42
+ const result = await parser.parse('build an eks cluster on aws');
43
+ expect(result.type).toBe('generate');
44
+ });
45
+
46
+ it('parse("create a deployment") returns type: generate', async () => {
47
+ const parser = makeParser();
48
+ const result = await parser.parse('create a deployment');
49
+ expect(result.type).toBe('generate');
50
+ });
51
+
52
+ it('parse("setup an s3 bucket") returns type: generate', async () => {
53
+ const parser = makeParser();
54
+ const result = await parser.parse('setup an s3 bucket');
55
+ expect(result.type).toBe('generate');
56
+ });
57
+ });
58
+
59
+ describe('IntentParser – explain intents', () => {
60
+ it('parse("explain kubernetes") returns type: explain', async () => {
61
+ const parser = makeParser();
62
+ const result = await parser.parse('explain kubernetes');
63
+ expect(result.type).toBe('explain');
64
+ });
65
+
66
+ it('parse("what is terraform") returns type: explain', async () => {
67
+ const parser = makeParser();
68
+ const result = await parser.parse('what is terraform');
69
+ expect(result.type).toBe('explain');
70
+ });
71
+
72
+ it('parse("describe a vpc") returns type: explain', async () => {
73
+ const parser = makeParser();
74
+ const result = await parser.parse('describe a vpc');
75
+ expect(result.type).toBe('explain');
76
+ });
77
+
78
+ it('parse("why should I use helm") returns type: explain', async () => {
79
+ const parser = makeParser();
80
+ const result = await parser.parse('why should I use helm');
81
+ expect(result.type).toBe('explain');
82
+ });
83
+ });
84
+
85
+ describe('IntentParser – help intents', () => {
86
+ it('parse("help") returns type: help', async () => {
87
+ const parser = makeParser();
88
+ const result = await parser.parse('help');
89
+ expect(result.type).toBe('help');
90
+ });
91
+
92
+ it('parse("what can you do") returns type: help', async () => {
93
+ const parser = makeParser();
94
+ const result = await parser.parse('what can you do');
95
+ expect(result.type).toBe('help');
96
+ });
97
+
98
+ it('parse("guide me through the process") returns type: help', async () => {
99
+ const parser = makeParser();
100
+ const result = await parser.parse('guide me through the process');
101
+ expect(result.type).toBe('help');
102
+ });
103
+ });
104
+
105
+ describe('IntentParser – unknown intents', () => {
106
+ it('parse("random gibberish qwerty xyz") returns type: unknown', async () => {
107
+ const parser = makeParser();
108
+ const result = await parser.parse('random gibberish qwerty xyz');
109
+ expect(result.type).toBe('unknown');
110
+ });
111
+
112
+ it('parse("xyzzy plugh foo bar baz") returns type: unknown', async () => {
113
+ const parser = makeParser();
114
+ const result = await parser.parse('xyzzy plugh foo bar baz');
115
+ expect(result.type).toBe('unknown');
116
+ });
117
+ });
118
+
119
+ describe('IntentParser – modify intents', () => {
120
+ it('parse("change the vpc") returns type: modify', async () => {
121
+ const parser = makeParser();
122
+ const result = await parser.parse('change the vpc');
123
+ expect(result.type).toBe('modify');
124
+ });
125
+
126
+ it('parse("update the rds instance") returns type: modify', async () => {
127
+ const parser = makeParser();
128
+ const result = await parser.parse('update the rds instance');
129
+ expect(result.type).toBe('modify');
130
+ });
131
+ });
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Entity extraction
135
+ // ---------------------------------------------------------------------------
136
+
137
+ describe('IntentParser – entity extraction', () => {
138
+ it('parse("create a vpc on aws") extracts provider: aws', async () => {
139
+ const parser = makeParser();
140
+ const result = await parser.parse('create a vpc on aws');
141
+ const providers = entitiesOfType(result, 'provider');
142
+ expect(providers).toContain('aws');
143
+ });
144
+
145
+ it('parse("create a vpc on aws") extracts component: vpc', async () => {
146
+ const parser = makeParser();
147
+ const result = await parser.parse('create a vpc on aws');
148
+ const components = entitiesOfType(result, 'component');
149
+ expect(components).toContain('vpc');
150
+ });
151
+
152
+ it('parse("generate a helm chart") extracts generation_type: helm', async () => {
153
+ const parser = makeParser();
154
+ const result = await parser.parse('generate a helm chart');
155
+ const genTypes = entitiesOfType(result, 'generation_type');
156
+ expect(genTypes).toContain('helm');
157
+ });
158
+
159
+ it('parse("create a deployment") extracts generation_type: kubernetes', async () => {
160
+ const parser = makeParser();
161
+ const result = await parser.parse('create a deployment');
162
+ const genTypes = entitiesOfType(result, 'generation_type');
163
+ expect(genTypes).toContain('kubernetes');
164
+ });
165
+
166
+ it('parse("deploy something on gcp") extracts provider: gcp', async () => {
167
+ const parser = makeParser();
168
+ const result = await parser.parse('deploy something on gcp');
169
+ const providers = entitiesOfType(result, 'provider');
170
+ expect(providers).toContain('gcp');
171
+ });
172
+
173
+ it('parse("provision infrastructure in us-east-1") extracts region entity', async () => {
174
+ const parser = makeParser();
175
+ const result = await parser.parse('provision infrastructure in us-east-1');
176
+ const regions = entitiesOfType(result, 'region');
177
+ expect(regions).toContain('us-east-1');
178
+ });
179
+
180
+ it('parse("provision eks for the production environment") extracts environment entity', async () => {
181
+ const parser = makeParser();
182
+ const result = await parser.parse('provision eks for the production environment');
183
+ const envs = entitiesOfType(result, 'environment');
184
+ // The parser normalises "production" to "production"
185
+ expect(envs.some(e => e.includes('production'))).toBe(true);
186
+ });
187
+ });
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Confidence
191
+ // ---------------------------------------------------------------------------
192
+
193
+ describe('IntentParser – confidence', () => {
194
+ it('returns confidence between 0 and 1 for any input', async () => {
195
+ const parser = makeParser();
196
+ const inputs = ['create a vpc on aws', 'explain kubernetes', 'help', 'random gibberish xyz'];
197
+
198
+ for (const input of inputs) {
199
+ const result = await parser.parse(input);
200
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
201
+ expect(result.confidence).toBeLessThanOrEqual(1);
202
+ }
203
+ });
204
+
205
+ it('unknown intent has confidence 0', async () => {
206
+ const parser = makeParser();
207
+ const result = await parser.parse('xyzzy plugh aaabbbccc');
208
+ expect(result.confidence).toBe(0);
209
+ });
210
+
211
+ it('matched intents have confidence greater than 0', async () => {
212
+ const parser = makeParser();
213
+ const result = await parser.parse('create a vpc on aws');
214
+ expect(result.confidence).toBeGreaterThan(0);
215
+ });
216
+ });
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // setRouter
220
+ // ---------------------------------------------------------------------------
221
+
222
+ describe('IntentParser – setRouter', () => {
223
+ it('setRouter() accepts a router without throwing', () => {
224
+ const parser = makeParser();
225
+ // Pass a minimal mock object; we only test that the setter accepts it
226
+ const mockRouter = {} as any;
227
+ expect(() => parser.setRouter(mockRouter)).not.toThrow();
228
+ });
229
+ });