@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,854 @@
1
+ /**
2
+ * Project Initialization & Auto-Detection
3
+ *
4
+ * Scaffolds a new Nimbus project by detecting the existing project type,
5
+ * infrastructure tooling, cloud providers, and development conventions.
6
+ * Generates a NIMBUS.md file and .nimbus/ configuration directory.
7
+ *
8
+ * Usage:
9
+ * nimbus init
10
+ * nimbus init --force # overwrite existing NIMBUS.md
11
+ * nimbus init --quiet # suppress console output
12
+ */
13
+
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { exec } from 'node:child_process';
17
+ import { promisify } from 'node:util';
18
+
19
+ const _execAsync = promisify(exec);
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Detected language / runtime of the project */
26
+ export type ProjectType =
27
+ | 'typescript'
28
+ | 'javascript'
29
+ | 'go'
30
+ | 'python'
31
+ | 'rust'
32
+ | 'java'
33
+ | 'unknown';
34
+
35
+ /** Infrastructure tool category */
36
+ export type InfraType = 'terraform' | 'kubernetes' | 'helm' | 'docker' | 'cicd';
37
+
38
+ /** Cloud provider identifier */
39
+ export type CloudProvider = 'aws' | 'gcp' | 'azure';
40
+
41
+ /** Complete detection result for a project directory */
42
+ export interface ProjectDetection {
43
+ /** Inferred project name (directory basename) */
44
+ projectName: string;
45
+ /** Primary language / runtime */
46
+ projectType: ProjectType;
47
+ /** Infrastructure tools found */
48
+ infraTypes: InfraType[];
49
+ /** Cloud providers detected from config or Terraform */
50
+ cloudProviders: CloudProvider[];
51
+ /** Whether the directory is a git repository */
52
+ hasGit: boolean;
53
+ /** Node.js package manager, if applicable */
54
+ packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun';
55
+ /** Test framework name, if detected */
56
+ testFramework?: string;
57
+ /** Linter name, if detected */
58
+ linter?: string;
59
+ /** Formatter name, if detected */
60
+ formatter?: string;
61
+ }
62
+
63
+ /** Options accepted by {@link runInit} */
64
+ export interface InitOptions {
65
+ /** Working directory (defaults to `process.cwd()`) */
66
+ cwd?: string;
67
+ /** Overwrite an existing NIMBUS.md without prompting */
68
+ force?: boolean;
69
+ /** Suppress all console output */
70
+ quiet?: boolean;
71
+ }
72
+
73
+ /** Value returned by {@link runInit} on success */
74
+ export interface InitResult {
75
+ /** Full detection results */
76
+ detection: ProjectDetection;
77
+ /** Absolute paths of files created during init */
78
+ filesCreated: string[];
79
+ /** Absolute path to the generated NIMBUS.md */
80
+ nimbusmdPath: string;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Internal helpers
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * Check whether a file or directory exists at `filePath`.
89
+ * Swallows all errors and returns `false` on failure.
90
+ */
91
+ function exists(filePath: string): boolean {
92
+ try {
93
+ return fs.existsSync(filePath);
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * List immediate children of `dir`, returning an empty array when the
101
+ * directory does not exist or is unreadable.
102
+ */
103
+ function listDir(dir: string): string[] {
104
+ try {
105
+ return fs.readdirSync(dir);
106
+ } catch {
107
+ return [];
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Read a file as UTF-8 text. Returns an empty string on failure.
113
+ */
114
+ function readText(filePath: string): string {
115
+ try {
116
+ return fs.readFileSync(filePath, 'utf-8');
117
+ } catch {
118
+ return '';
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Recursively collect file names matching a predicate.
124
+ * Searches at most `maxDepth` levels deep and stops after `limit` matches.
125
+ */
126
+ function findFiles(
127
+ dir: string,
128
+ predicate: (name: string) => boolean,
129
+ maxDepth = 3,
130
+ limit = 50
131
+ ): string[] {
132
+ const results: string[] = [];
133
+
134
+ function walk(current: string, depth: number): void {
135
+ if (depth > maxDepth || results.length >= limit) {
136
+ return;
137
+ }
138
+
139
+ for (const entry of listDir(current)) {
140
+ if (results.length >= limit) {
141
+ return;
142
+ }
143
+
144
+ // Skip heavy directories that would slow detection
145
+ if (entry === 'node_modules' || entry === '.git' || entry === 'dist' || entry === 'vendor') {
146
+ continue;
147
+ }
148
+
149
+ const full = path.join(current, entry);
150
+
151
+ try {
152
+ const stat = fs.statSync(full);
153
+ if (stat.isDirectory()) {
154
+ walk(full, depth + 1);
155
+ } else if (predicate(entry)) {
156
+ results.push(full);
157
+ }
158
+ } catch {
159
+ // Permission or broken symlink -- skip
160
+ }
161
+ }
162
+ }
163
+
164
+ walk(dir, 0);
165
+ return results;
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Detection functions
170
+ // ---------------------------------------------------------------------------
171
+
172
+ /**
173
+ * Detect the primary project type from marker files in `dir`.
174
+ *
175
+ * Priority order: TypeScript > JavaScript > Go > Python > Rust > Java > unknown
176
+ */
177
+ export function detectProjectType(dir: string): ProjectType {
178
+ try {
179
+ if (exists(path.join(dir, 'tsconfig.json'))) {
180
+ return 'typescript';
181
+ }
182
+ if (exists(path.join(dir, 'package.json'))) {
183
+ return 'javascript';
184
+ }
185
+ if (exists(path.join(dir, 'go.mod'))) {
186
+ return 'go';
187
+ }
188
+ if (
189
+ exists(path.join(dir, 'pyproject.toml')) ||
190
+ exists(path.join(dir, 'setup.py')) ||
191
+ exists(path.join(dir, 'requirements.txt'))
192
+ ) {
193
+ return 'python';
194
+ }
195
+ if (exists(path.join(dir, 'Cargo.toml'))) {
196
+ return 'rust';
197
+ }
198
+ if (
199
+ exists(path.join(dir, 'pom.xml')) ||
200
+ exists(path.join(dir, 'build.gradle')) ||
201
+ exists(path.join(dir, 'build.gradle.kts'))
202
+ ) {
203
+ return 'java';
204
+ }
205
+ } catch {
206
+ // Fall through to unknown
207
+ }
208
+
209
+ return 'unknown';
210
+ }
211
+
212
+ /**
213
+ * Detect which infrastructure tools are present in `dir`.
214
+ *
215
+ * Scans for Terraform files, Kubernetes manifests, Helm charts,
216
+ * Docker files, and CI/CD configuration.
217
+ */
218
+ export function detectInfrastructure(dir: string): InfraType[] {
219
+ const found: Set<InfraType> = new Set();
220
+
221
+ try {
222
+ // Terraform -- look for any .tf files
223
+ const tfFiles = findFiles(dir, name => name.endsWith('.tf'), 3, 5);
224
+ if (tfFiles.length > 0) {
225
+ found.add('terraform');
226
+ }
227
+
228
+ // Kubernetes -- look for YAML files containing common K8s markers
229
+ const yamlFiles = findFiles(
230
+ dir,
231
+ name => name.endsWith('.yaml') || name.endsWith('.yml'),
232
+ 3,
233
+ 30
234
+ );
235
+ for (const yamlFile of yamlFiles) {
236
+ const content = readText(yamlFile);
237
+ if (
238
+ content.includes('kind: Deployment') ||
239
+ content.includes('kind: Service') ||
240
+ content.includes('apiVersion:')
241
+ ) {
242
+ found.add('kubernetes');
243
+ break;
244
+ }
245
+ }
246
+
247
+ // Helm
248
+ if (findFiles(dir, name => name === 'Chart.yaml', 3, 1).length > 0) {
249
+ found.add('helm');
250
+ }
251
+
252
+ // Docker
253
+ const entries = listDir(dir);
254
+ if (
255
+ entries.some(
256
+ e => e === 'Dockerfile' || e === 'docker-compose.yml' || e === 'docker-compose.yaml'
257
+ )
258
+ ) {
259
+ found.add('docker');
260
+ }
261
+ // Also check for a docker/ directory with Dockerfiles
262
+ if (exists(path.join(dir, 'docker'))) {
263
+ const dockerDir = listDir(path.join(dir, 'docker'));
264
+ if (
265
+ dockerDir.some(e => e.startsWith('Dockerfile') || e.endsWith('.yml') || e.endsWith('.yaml'))
266
+ ) {
267
+ found.add('docker');
268
+ }
269
+ }
270
+
271
+ // CI/CD
272
+ if (
273
+ exists(path.join(dir, '.github', 'workflows')) ||
274
+ exists(path.join(dir, '.gitlab-ci.yml')) ||
275
+ exists(path.join(dir, 'Jenkinsfile')) ||
276
+ exists(path.join(dir, '.circleci'))
277
+ ) {
278
+ found.add('cicd');
279
+ }
280
+ } catch {
281
+ // Return whatever we collected so far
282
+ }
283
+
284
+ return Array.from(found);
285
+ }
286
+
287
+ /**
288
+ * Detect cloud providers referenced in Terraform files or local credentials.
289
+ *
290
+ * Checks both `.tf` file contents and well-known credential locations
291
+ * or environment variables.
292
+ */
293
+ export function detectCloudProviders(dir: string): CloudProvider[] {
294
+ const found: Set<CloudProvider> = new Set();
295
+
296
+ try {
297
+ // --- Scan Terraform files for provider blocks ---
298
+ const tfFiles = findFiles(dir, name => name.endsWith('.tf'), 3, 20);
299
+
300
+ for (const tfFile of tfFiles) {
301
+ const content = readText(tfFile);
302
+
303
+ if (content.includes('provider "aws"') || content.includes("provider 'aws'")) {
304
+ found.add('aws');
305
+ }
306
+ if (content.includes('provider "google"') || content.includes("provider 'google'")) {
307
+ found.add('gcp');
308
+ }
309
+ if (content.includes('provider "azurerm"') || content.includes("provider 'azurerm'")) {
310
+ found.add('azure');
311
+ }
312
+ }
313
+
314
+ // --- Check environment variables ---
315
+ if (process.env['AWS_ACCESS_KEY_ID'] || process.env['AWS_PROFILE']) {
316
+ found.add('aws');
317
+ }
318
+ if (process.env['GOOGLE_APPLICATION_CREDENTIALS'] || process.env['GCLOUD_PROJECT']) {
319
+ found.add('gcp');
320
+ }
321
+ if (process.env['AZURE_SUBSCRIPTION_ID'] || process.env['ARM_SUBSCRIPTION_ID']) {
322
+ found.add('azure');
323
+ }
324
+
325
+ // --- Check local credential files ---
326
+ const home = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
327
+ if (home) {
328
+ if (
329
+ exists(path.join(home, '.aws', 'credentials')) ||
330
+ exists(path.join(home, '.aws', 'config'))
331
+ ) {
332
+ found.add('aws');
333
+ }
334
+ if (exists(path.join(home, '.config', 'gcloud'))) {
335
+ found.add('gcp');
336
+ }
337
+ if (exists(path.join(home, '.azure'))) {
338
+ found.add('azure');
339
+ }
340
+ }
341
+ } catch {
342
+ // Return whatever we collected
343
+ }
344
+
345
+ return Array.from(found);
346
+ }
347
+
348
+ /**
349
+ * Detect which Node.js package manager is used in `dir`.
350
+ *
351
+ * Lock-file priority: bun > yarn > pnpm > npm.
352
+ * Returns `undefined` when no lock file is found.
353
+ */
354
+ export function detectPackageManager(dir: string): 'npm' | 'yarn' | 'pnpm' | 'bun' | undefined {
355
+ try {
356
+ if (exists(path.join(dir, 'bun.lock')) || exists(path.join(dir, 'bun.lockb'))) {
357
+ return 'bun';
358
+ }
359
+ if (exists(path.join(dir, 'yarn.lock'))) {
360
+ return 'yarn';
361
+ }
362
+ if (exists(path.join(dir, 'pnpm-lock.yaml'))) {
363
+ return 'pnpm';
364
+ }
365
+ if (exists(path.join(dir, 'package-lock.json'))) {
366
+ return 'npm';
367
+ }
368
+ } catch {
369
+ // fall through
370
+ }
371
+ return undefined;
372
+ }
373
+
374
+ /**
375
+ * Detect the test framework from `package.json` dependencies or
376
+ * lock-file presence.
377
+ *
378
+ * Returns the framework name as a human-readable string, or `undefined`
379
+ * if none is detected.
380
+ */
381
+ export function detectTestFramework(dir: string): string | undefined {
382
+ try {
383
+ const pkgPath = path.join(dir, 'package.json');
384
+ if (!exists(pkgPath)) {
385
+ return undefined;
386
+ }
387
+
388
+ const pkg = JSON.parse(readText(pkgPath)) as Record<string, unknown>;
389
+ const allDeps = {
390
+ ...(pkg['dependencies'] as Record<string, string> | undefined),
391
+ ...(pkg['devDependencies'] as Record<string, string> | undefined),
392
+ };
393
+
394
+ if ('vitest' in allDeps) {
395
+ return 'vitest';
396
+ }
397
+ if ('jest' in allDeps) {
398
+ return 'jest';
399
+ }
400
+ if ('mocha' in allDeps) {
401
+ return 'mocha';
402
+ }
403
+ if ('@playwright/test' in allDeps) {
404
+ return 'playwright';
405
+ }
406
+
407
+ // Bun ships its own test runner -- detect via lock file
408
+ if (exists(path.join(dir, 'bun.lock')) || exists(path.join(dir, 'bun.lockb'))) {
409
+ return 'bun:test';
410
+ }
411
+ } catch {
412
+ // fall through
413
+ }
414
+
415
+ // Non-JS projects
416
+ if (exists(path.join(dir, 'go.mod'))) {
417
+ return 'go test';
418
+ }
419
+ if (exists(path.join(dir, 'Cargo.toml'))) {
420
+ return 'cargo test';
421
+ }
422
+ if (exists(path.join(dir, 'pyproject.toml')) || exists(path.join(dir, 'setup.py'))) {
423
+ const pyproject = readText(path.join(dir, 'pyproject.toml'));
424
+ if (pyproject.includes('pytest')) {
425
+ return 'pytest';
426
+ }
427
+ if (exists(path.join(dir, 'pytest.ini')) || exists(path.join(dir, 'setup.cfg'))) {
428
+ return 'pytest';
429
+ }
430
+ return 'unittest';
431
+ }
432
+
433
+ return undefined;
434
+ }
435
+
436
+ /**
437
+ * Detect the linter used in the project.
438
+ *
439
+ * Checks for ESLint, Biome, golangci-lint, Ruff, and Clippy configuration.
440
+ */
441
+ export function detectLinter(dir: string): string | undefined {
442
+ try {
443
+ const entries = listDir(dir);
444
+
445
+ // ESLint (various config formats)
446
+ if (entries.some(e => e.startsWith('.eslintrc') || e.startsWith('eslint.config'))) {
447
+ return 'eslint';
448
+ }
449
+
450
+ // Biome
451
+ if (entries.includes('biome.json') || entries.includes('biome.jsonc')) {
452
+ return 'biome';
453
+ }
454
+
455
+ // Go -- golangci-lint
456
+ if (entries.includes('.golangci.yml') || entries.includes('.golangci.yaml')) {
457
+ return 'golangci-lint';
458
+ }
459
+
460
+ // Python -- ruff
461
+ if (entries.includes('ruff.toml') || entries.includes('.ruff.toml')) {
462
+ return 'ruff';
463
+ }
464
+
465
+ // Check pyproject.toml for ruff or flake8
466
+ if (exists(path.join(dir, 'pyproject.toml'))) {
467
+ const pyproject = readText(path.join(dir, 'pyproject.toml'));
468
+ if (pyproject.includes('[tool.ruff]')) {
469
+ return 'ruff';
470
+ }
471
+ if (pyproject.includes('[tool.flake8]')) {
472
+ return 'flake8';
473
+ }
474
+ }
475
+
476
+ // Rust -- clippy is part of the toolchain, detect via Cargo.toml
477
+ if (exists(path.join(dir, 'Cargo.toml'))) {
478
+ return 'clippy';
479
+ }
480
+ } catch {
481
+ // fall through
482
+ }
483
+ return undefined;
484
+ }
485
+
486
+ /**
487
+ * Detect the code formatter used in the project.
488
+ *
489
+ * Checks for Prettier, Biome, gofmt, rustfmt, and Black configuration.
490
+ */
491
+ export function detectFormatter(dir: string): string | undefined {
492
+ try {
493
+ const entries = listDir(dir);
494
+
495
+ // Prettier
496
+ if (entries.some(e => e.startsWith('.prettierrc') || e.startsWith('prettier.config'))) {
497
+ return 'prettier';
498
+ }
499
+
500
+ // Biome doubles as formatter
501
+ if (entries.includes('biome.json') || entries.includes('biome.jsonc')) {
502
+ return 'biome';
503
+ }
504
+
505
+ // Go -- gofmt is built-in
506
+ if (exists(path.join(dir, 'go.mod'))) {
507
+ return 'gofmt';
508
+ }
509
+
510
+ // Rust -- rustfmt
511
+ if (exists(path.join(dir, 'rustfmt.toml')) || exists(path.join(dir, '.rustfmt.toml'))) {
512
+ return 'rustfmt';
513
+ }
514
+ if (exists(path.join(dir, 'Cargo.toml'))) {
515
+ return 'rustfmt';
516
+ }
517
+
518
+ // Python -- black / ruff format
519
+ if (exists(path.join(dir, 'pyproject.toml'))) {
520
+ const pyproject = readText(path.join(dir, 'pyproject.toml'));
521
+ if (pyproject.includes('[tool.black]')) {
522
+ return 'black';
523
+ }
524
+ if (pyproject.includes('[tool.ruff]')) {
525
+ return 'ruff';
526
+ }
527
+ }
528
+ } catch {
529
+ // fall through
530
+ }
531
+ return undefined;
532
+ }
533
+
534
+ /**
535
+ * Run the full project detection pipeline on `dir`.
536
+ *
537
+ * Aggregates results from all individual detection functions into a
538
+ * single {@link ProjectDetection} object.
539
+ */
540
+ export function detectProject(dir: string): ProjectDetection {
541
+ const resolvedDir = path.resolve(dir);
542
+
543
+ return {
544
+ projectName: path.basename(resolvedDir),
545
+ projectType: detectProjectType(resolvedDir),
546
+ infraTypes: detectInfrastructure(resolvedDir),
547
+ cloudProviders: detectCloudProviders(resolvedDir),
548
+ hasGit: exists(path.join(resolvedDir, '.git')),
549
+ packageManager: detectPackageManager(resolvedDir),
550
+ testFramework: detectTestFramework(resolvedDir),
551
+ linter: detectLinter(resolvedDir),
552
+ formatter: detectFormatter(resolvedDir),
553
+ };
554
+ }
555
+
556
+ // ---------------------------------------------------------------------------
557
+ // Generation
558
+ // ---------------------------------------------------------------------------
559
+
560
+ /**
561
+ * Produce the contents of a `NIMBUS.md` file from detection results.
562
+ *
563
+ * The generated markdown serves as both human-readable documentation
564
+ * and machine-readable project metadata for the Nimbus agent.
565
+ */
566
+ export function generateNimbusMd(detection: ProjectDetection, _dir: string): string {
567
+ const lines: string[] = [];
568
+
569
+ // --- Header ---
570
+ lines.push(`# ${detection.projectName}`);
571
+ lines.push('');
572
+ lines.push('> Auto-generated by `nimbus init`. Edit freely to refine agent behaviour.');
573
+ lines.push('');
574
+
575
+ // --- Project Overview ---
576
+ lines.push('## Project Overview');
577
+ lines.push('');
578
+ lines.push(`- **Type:** ${detection.projectType}`);
579
+ if (detection.packageManager) {
580
+ lines.push(`- **Package Manager:** ${detection.packageManager}`);
581
+ }
582
+ if (detection.testFramework) {
583
+ lines.push(`- **Test Framework:** ${detection.testFramework}`);
584
+ }
585
+ if (detection.hasGit) {
586
+ lines.push('- **Version Control:** git');
587
+ }
588
+ lines.push('');
589
+
590
+ // --- Infrastructure ---
591
+ if (detection.infraTypes.length > 0 || detection.cloudProviders.length > 0) {
592
+ lines.push('## Infrastructure');
593
+ lines.push('');
594
+ if (detection.infraTypes.length > 0) {
595
+ lines.push(`- **Tools:** ${detection.infraTypes.join(', ')}`);
596
+ }
597
+ if (detection.cloudProviders.length > 0) {
598
+ lines.push(`- **Cloud Providers:** ${detection.cloudProviders.join(', ')}`);
599
+ }
600
+ lines.push('');
601
+ }
602
+
603
+ // --- Conventions ---
604
+ if (detection.linter || detection.formatter) {
605
+ lines.push('## Conventions');
606
+ lines.push('');
607
+ if (detection.linter) {
608
+ lines.push(`- **Linter:** ${detection.linter}`);
609
+ }
610
+ if (detection.formatter) {
611
+ lines.push(`- **Formatter:** ${detection.formatter}`);
612
+ }
613
+ lines.push('');
614
+ }
615
+
616
+ // --- Safety Rules ---
617
+ lines.push('## Safety Rules');
618
+ lines.push('');
619
+ lines.push('- Protected branches: `main`, `master`');
620
+ lines.push('- Protected Kubernetes namespaces: `production`, `kube-system`');
621
+ lines.push('- Always preview before `terraform apply`');
622
+ lines.push('- Run tests before committing');
623
+ lines.push('- Never store secrets in source control');
624
+ lines.push('');
625
+
626
+ // --- Custom Instructions ---
627
+ lines.push('## Custom Instructions');
628
+ lines.push('');
629
+ lines.push('<!-- Add project-specific instructions for the Nimbus agent here -->');
630
+ lines.push('');
631
+
632
+ return lines.join('\n');
633
+ }
634
+
635
+ /**
636
+ * Generate the contents of `.nimbus/config.yaml`.
637
+ *
638
+ * Produces a valid YAML string without requiring an external YAML library.
639
+ */
640
+ function generateConfigYaml(detection: ProjectDetection): string {
641
+ const lines: string[] = [];
642
+
643
+ lines.push('# Nimbus project configuration');
644
+ lines.push('# See https://nimbus.dev/docs/config for all options');
645
+ lines.push('');
646
+ lines.push('# Default LLM model for agent interactions');
647
+ lines.push('default_model: anthropic/claude-sonnet-4');
648
+ lines.push('');
649
+ lines.push('# Default agent mode: build | plan | debug | review');
650
+ lines.push('default_mode: build');
651
+ lines.push('');
652
+ lines.push('# Project metadata');
653
+ lines.push('project:');
654
+ lines.push(` name: ${detection.projectName}`);
655
+ lines.push(` type: ${detection.projectType}`);
656
+ if (detection.packageManager) {
657
+ lines.push(` package_manager: ${detection.packageManager}`);
658
+ }
659
+ lines.push('');
660
+
661
+ // Permissions
662
+ lines.push('# Permission rules control what the agent can do without asking');
663
+ lines.push('permissions:');
664
+ lines.push(' # File operations');
665
+ lines.push(' file_read: allow');
666
+ lines.push(' file_write: ask');
667
+ lines.push(' file_delete: deny');
668
+ lines.push('');
669
+ lines.push(' # Shell commands');
670
+ lines.push(' shell_read: allow # non-destructive commands (ls, cat, git status)');
671
+ lines.push(' shell_write: ask # potentially destructive commands');
672
+ lines.push('');
673
+ lines.push(' # Git operations');
674
+ lines.push(' git_read: allow');
675
+ lines.push(' git_write: ask');
676
+ lines.push('');
677
+ lines.push(' # Infrastructure operations');
678
+ lines.push(' terraform_plan: allow');
679
+ lines.push(' terraform_apply: deny');
680
+ lines.push(' kubectl_read: allow');
681
+ lines.push(' kubectl_write: deny');
682
+ lines.push('');
683
+
684
+ // Safety
685
+ lines.push('# Safety settings');
686
+ lines.push('safety:');
687
+ lines.push(' protected_branches:');
688
+ lines.push(' - main');
689
+ lines.push(' - master');
690
+ lines.push(' protected_k8s_namespaces:');
691
+ lines.push(' - production');
692
+ lines.push(' - kube-system');
693
+ lines.push(' require_plan_before_apply: true');
694
+ lines.push(' require_tests_before_commit: true');
695
+ lines.push('');
696
+
697
+ return lines.join('\n');
698
+ }
699
+
700
+ // ---------------------------------------------------------------------------
701
+ // Main init
702
+ // ---------------------------------------------------------------------------
703
+
704
+ /**
705
+ * Initialize a Nimbus project in the given directory.
706
+ *
707
+ * Creates the `.nimbus/` directory structure and a `NIMBUS.md` file
708
+ * populated with auto-detected project metadata.
709
+ *
710
+ * @param options - Configuration for the init process
711
+ * @returns The detection results and list of created files
712
+ *
713
+ * @example
714
+ * ```ts
715
+ * const result = await runInit({ cwd: '/path/to/project' });
716
+ * console.log(result.detection.projectType); // 'typescript'
717
+ * console.log(result.filesCreated); // ['.nimbus/config.yaml', ...]
718
+ * ```
719
+ */
720
+ export async function runInit(options?: InitOptions): Promise<InitResult> {
721
+ const dir = path.resolve(options?.cwd ?? process.cwd());
722
+ const force = options?.force ?? false;
723
+ const quiet = options?.quiet ?? false;
724
+
725
+ const log = (msg: string): void => {
726
+ if (!quiet) {
727
+ console.log(msg);
728
+ }
729
+ };
730
+
731
+ // ---- Step 1: Detect project characteristics ----
732
+ log('Detecting project...');
733
+ const detection = detectProject(dir);
734
+
735
+ log(` Project type: ${detection.projectType}`);
736
+ if (detection.packageManager) {
737
+ log(` Package manager: ${detection.packageManager}`);
738
+ }
739
+ if (detection.infraTypes.length > 0) {
740
+ log(` Infrastructure: ${detection.infraTypes.join(', ')}`);
741
+ }
742
+ if (detection.cloudProviders.length > 0) {
743
+ log(` Cloud providers: ${detection.cloudProviders.join(', ')}`);
744
+ }
745
+ if (detection.testFramework) {
746
+ log(` Test framework: ${detection.testFramework}`);
747
+ }
748
+
749
+ // ---- Step 2: Check for existing NIMBUS.md ----
750
+ const nimbusmdPath = path.join(dir, 'NIMBUS.md');
751
+ if (exists(nimbusmdPath) && !force) {
752
+ throw new Error('NIMBUS.md already exists. Use --force to overwrite.');
753
+ }
754
+
755
+ // ---- Step 3: Create .nimbus/ directory structure ----
756
+ const filesCreated: string[] = [];
757
+ const nimbusDirPath = path.join(dir, '.nimbus');
758
+ const hooksDirPath = path.join(nimbusDirPath, 'hooks');
759
+ const agentsDirPath = path.join(nimbusDirPath, 'agents');
760
+
761
+ if (!exists(nimbusDirPath)) {
762
+ fs.mkdirSync(nimbusDirPath, { recursive: true });
763
+ }
764
+ if (!exists(hooksDirPath)) {
765
+ fs.mkdirSync(hooksDirPath, { recursive: true });
766
+ }
767
+ if (!exists(agentsDirPath)) {
768
+ fs.mkdirSync(agentsDirPath, { recursive: true });
769
+ }
770
+
771
+ // ---- Step 4: Create .nimbus/config.yaml ----
772
+ const configPath = path.join(nimbusDirPath, 'config.yaml');
773
+ if (!exists(configPath) || force) {
774
+ const configContent = generateConfigYaml(detection);
775
+ fs.writeFileSync(configPath, configContent, 'utf-8');
776
+ filesCreated.push(configPath);
777
+ log(' Created .nimbus/config.yaml');
778
+ }
779
+
780
+ // ---- Step 5: Create placeholder hook files ----
781
+ const preCommitHookPath = path.join(hooksDirPath, 'pre-commit.ts');
782
+ if (!exists(preCommitHookPath) || force) {
783
+ const preCommitContent = [
784
+ '/**',
785
+ ' * Nimbus pre-commit hook',
786
+ ' *',
787
+ ' * Runs automatically before each commit when enabled.',
788
+ ' * Add custom validation logic here.',
789
+ ' */',
790
+ '',
791
+ 'export default async function preCommit(): Promise<void> {',
792
+ ' // Example: ensure tests pass before committing',
793
+ ' // await $`bun test`;',
794
+ '}',
795
+ '',
796
+ ].join('\n');
797
+ fs.writeFileSync(preCommitHookPath, preCommitContent, 'utf-8');
798
+ filesCreated.push(preCommitHookPath);
799
+ log(' Created .nimbus/hooks/pre-commit.ts');
800
+ }
801
+
802
+ // ---- Step 6: Create placeholder agent config ----
803
+ const defaultAgentPath = path.join(agentsDirPath, 'default.yaml');
804
+ if (!exists(defaultAgentPath) || force) {
805
+ const agentContent = [
806
+ '# Default agent profile',
807
+ '# Customize the system prompt and tool access for this agent',
808
+ '',
809
+ 'name: default',
810
+ 'description: General-purpose Nimbus agent',
811
+ '',
812
+ 'tools:',
813
+ ' - file_read',
814
+ ' - file_write',
815
+ ' - shell',
816
+ ' - git',
817
+ '',
818
+ 'system_prompt: |',
819
+ ` You are working on the ${detection.projectName} project.`,
820
+ ` It is a ${detection.projectType} project.`,
821
+ ' Follow the safety rules in NIMBUS.md.',
822
+ '',
823
+ ].join('\n');
824
+ fs.writeFileSync(defaultAgentPath, agentContent, 'utf-8');
825
+ filesCreated.push(defaultAgentPath);
826
+ log(' Created .nimbus/agents/default.yaml');
827
+ }
828
+
829
+ // ---- Step 7: Generate and write NIMBUS.md ----
830
+ const nimbusmdContent = generateNimbusMd(detection, dir);
831
+ fs.writeFileSync(nimbusmdPath, nimbusmdContent, 'utf-8');
832
+ filesCreated.push(nimbusmdPath);
833
+ log(' Created NIMBUS.md');
834
+
835
+ // ---- Step 8: Append .nimbus/ to .gitignore if not already present ----
836
+ const gitignorePath = path.join(dir, '.gitignore');
837
+ if (exists(gitignorePath)) {
838
+ const gitignore = readText(gitignorePath);
839
+ if (!gitignore.includes('.nimbus/')) {
840
+ fs.appendFileSync(gitignorePath, '\n# Nimbus local config\n.nimbus/\n', 'utf-8');
841
+ log(' Updated .gitignore');
842
+ }
843
+ }
844
+
845
+ log('');
846
+ log('Nimbus project initialized successfully.');
847
+ log('Edit NIMBUS.md to customise agent behaviour.');
848
+
849
+ return {
850
+ detection,
851
+ filesCreated,
852
+ nimbusmdPath,
853
+ };
854
+ }