@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,593 @@
1
+ /**
2
+ * FileSystem Operations — Embedded tool (stripped HTTP wrappers)
3
+ *
4
+ * Copied from services/fs-tools-service/src/fs/operations.ts
5
+ * Provides direct filesystem operations for the embedded CLI binary.
6
+ */
7
+
8
+ import * as fs from 'fs/promises';
9
+ import * as fsSync from 'fs';
10
+ import * as path from 'path';
11
+ import { glob } from 'fast-glob';
12
+ import { exec } from 'child_process';
13
+ import { promisify } from 'util';
14
+ import { logger } from '../utils';
15
+
16
+ const execAsync = promisify(exec);
17
+
18
+ /**
19
+ * Sensitive file patterns that should be blocked from access
20
+ */
21
+ const SENSITIVE_PATTERNS: RegExp[] = [
22
+ /\.env(\.|$)/i, // .env, .env.local, .env.production
23
+ /credentials/i, // AWS credentials, any credentials file
24
+ /\.pem$/i, // PEM certificates
25
+ /\.key$/i, // Private keys
26
+ /id_rsa/i, // SSH keys
27
+ /id_ed25519/i, // SSH keys (Ed25519)
28
+ /id_ecdsa/i, // SSH keys (ECDSA)
29
+ /\.ssh[/\\]/i, // Anything inside .ssh directory
30
+ /\/etc\/shadow$/, // Unix shadow passwords
31
+ /\/etc\/passwd$/, // Unix passwords
32
+ /\.aws[/\\]credentials/i, // AWS credentials file specifically
33
+ /\.kube[/\\]config/i, // Kubeconfig with cluster secrets
34
+ ];
35
+
36
+ export interface FileStats {
37
+ size: number;
38
+ isFile: boolean;
39
+ isDirectory: boolean;
40
+ isSymbolicLink: boolean;
41
+ createdAt: Date;
42
+ modifiedAt: Date;
43
+ accessedAt: Date;
44
+ permissions: string;
45
+ }
46
+
47
+ export interface TreeNode {
48
+ name: string;
49
+ path: string;
50
+ type: 'file' | 'directory';
51
+ size?: number;
52
+ children?: TreeNode[];
53
+ }
54
+
55
+ export interface SearchResult {
56
+ file: string;
57
+ line: number;
58
+ column: number;
59
+ match: string;
60
+ context?: string;
61
+ }
62
+
63
+ export interface ListOptions {
64
+ pattern?: string;
65
+ recursive?: boolean;
66
+ includeHidden?: boolean;
67
+ onlyFiles?: boolean;
68
+ onlyDirectories?: boolean;
69
+ }
70
+
71
+ export interface TreeOptions {
72
+ maxDepth?: number;
73
+ includeHidden?: boolean;
74
+ includeFiles?: boolean;
75
+ }
76
+
77
+ export interface SearchOptions {
78
+ pattern: string;
79
+ caseSensitive?: boolean;
80
+ wholeWord?: boolean;
81
+ maxResults?: number;
82
+ includeContext?: boolean;
83
+ filePattern?: string;
84
+ }
85
+
86
+ export class FileSystemOperations {
87
+ private basePath: string;
88
+
89
+ constructor(basePath: string = process.cwd()) {
90
+ this.basePath = basePath;
91
+ }
92
+
93
+ /**
94
+ * Check if a resolved path points to a sensitive file
95
+ */
96
+ private assertNotSensitive(resolvedPath: string): void {
97
+ if (process.env.ALLOW_SENSITIVE_FILES === 'true') {
98
+ return;
99
+ }
100
+
101
+ const normalized = path.resolve(resolvedPath);
102
+ const basename = path.basename(normalized);
103
+
104
+ for (const pattern of SENSITIVE_PATTERNS) {
105
+ if (pattern.test(basename) || pattern.test(normalized)) {
106
+ throw new Error(
107
+ `Access denied: reading sensitive file '${basename}' is blocked for security`
108
+ );
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Resolve path relative to base path
115
+ */
116
+ private resolvePath(filePath: string): string {
117
+ const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(this.basePath, filePath);
118
+
119
+ this.assertNotSensitive(resolved);
120
+ return resolved;
121
+ }
122
+
123
+ /**
124
+ * Read file content
125
+ */
126
+ async readFile(filePath: string, encoding: BufferEncoding = 'utf-8'): Promise<string> {
127
+ const resolvedPath = this.resolvePath(filePath);
128
+ logger.info(`Reading file: ${resolvedPath}`);
129
+
130
+ const content = await fs.readFile(resolvedPath, encoding);
131
+ return content;
132
+ }
133
+
134
+ /**
135
+ * Read file as binary
136
+ */
137
+ async readFileBuffer(filePath: string): Promise<Buffer> {
138
+ const resolvedPath = this.resolvePath(filePath);
139
+ logger.info(`Reading file as buffer: ${resolvedPath}`);
140
+
141
+ return await fs.readFile(resolvedPath);
142
+ }
143
+
144
+ /**
145
+ * Write content to file
146
+ */
147
+ async writeFile(
148
+ filePath: string,
149
+ content: string | Buffer,
150
+ options?: { createDirs?: boolean }
151
+ ): Promise<{ success: boolean; path: string }> {
152
+ const resolvedPath = this.resolvePath(filePath);
153
+ logger.info(`Writing file: ${resolvedPath}`);
154
+
155
+ if (options?.createDirs) {
156
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
157
+ }
158
+
159
+ await fs.writeFile(resolvedPath, content, 'utf-8');
160
+
161
+ return { success: true, path: resolvedPath };
162
+ }
163
+
164
+ /**
165
+ * Append content to file
166
+ */
167
+ async appendFile(filePath: string, content: string): Promise<{ success: boolean; path: string }> {
168
+ const resolvedPath = this.resolvePath(filePath);
169
+ logger.info(`Appending to file: ${resolvedPath}`);
170
+
171
+ await fs.appendFile(resolvedPath, content, 'utf-8');
172
+
173
+ return { success: true, path: resolvedPath };
174
+ }
175
+
176
+ /**
177
+ * List files and directories
178
+ */
179
+ async list(directory: string, options: ListOptions = {}): Promise<string[]> {
180
+ const resolvedPath = this.resolvePath(directory);
181
+ logger.info(`Listing directory: ${resolvedPath}`);
182
+
183
+ const pattern = options.pattern || '*';
184
+ const fullPattern = options.recursive
185
+ ? path.join(resolvedPath, '**', pattern)
186
+ : path.join(resolvedPath, pattern);
187
+
188
+ const entries = await glob(fullPattern, {
189
+ dot: options.includeHidden,
190
+ onlyFiles: options.onlyFiles,
191
+ onlyDirectories: options.onlyDirectories,
192
+ absolute: true,
193
+ });
194
+
195
+ return entries;
196
+ }
197
+
198
+ /**
199
+ * Search for content in files using ripgrep (if available) or built-in search
200
+ */
201
+ async search(directory: string, options: SearchOptions): Promise<SearchResult[]> {
202
+ const resolvedPath = this.resolvePath(directory);
203
+ logger.info(`Searching in ${resolvedPath} for pattern: ${options.pattern}`);
204
+
205
+ // Try ripgrep first for performance
206
+ try {
207
+ return await this.searchWithRipgrep(resolvedPath, options);
208
+ } catch {
209
+ // Fall back to built-in search
210
+ return await this.searchBuiltin(resolvedPath, options);
211
+ }
212
+ }
213
+
214
+ private async searchWithRipgrep(
215
+ directory: string,
216
+ options: SearchOptions
217
+ ): Promise<SearchResult[]> {
218
+ const args: string[] = ['--json', '--line-number', '--column'];
219
+
220
+ if (!options.caseSensitive) {
221
+ args.push('-i');
222
+ }
223
+
224
+ if (options.wholeWord) {
225
+ args.push('-w');
226
+ }
227
+
228
+ if (options.maxResults) {
229
+ args.push('-m', options.maxResults.toString());
230
+ }
231
+
232
+ if (options.filePattern) {
233
+ args.push('-g', options.filePattern);
234
+ }
235
+
236
+ const command = `rg ${args.join(' ')} "${options.pattern}" "${directory}"`;
237
+
238
+ const { stdout } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 });
239
+
240
+ const results: SearchResult[] = [];
241
+ const lines = stdout
242
+ .trim()
243
+ .split('\n')
244
+ .filter(line => line);
245
+
246
+ for (const line of lines) {
247
+ try {
248
+ const parsed = JSON.parse(line);
249
+ if (parsed.type === 'match') {
250
+ results.push({
251
+ file: parsed.data.path.text,
252
+ line: parsed.data.line_number,
253
+ column: parsed.data.submatches[0]?.start || 0,
254
+ match: parsed.data.lines.text.trim(),
255
+ context: options.includeContext ? parsed.data.lines.text : undefined,
256
+ });
257
+ }
258
+ } catch {
259
+ // Skip malformed lines
260
+ }
261
+ }
262
+
263
+ return results;
264
+ }
265
+
266
+ private async searchBuiltin(directory: string, options: SearchOptions): Promise<SearchResult[]> {
267
+ const results: SearchResult[] = [];
268
+ const pattern = options.caseSensitive
269
+ ? new RegExp(options.pattern, 'g')
270
+ : new RegExp(options.pattern, 'gi');
271
+
272
+ const files = await this.list(directory, {
273
+ recursive: true,
274
+ onlyFiles: true,
275
+ pattern: options.filePattern || '*',
276
+ });
277
+
278
+ for (const file of files) {
279
+ try {
280
+ const content = await this.readFile(file);
281
+ const lines = content.split('\n');
282
+
283
+ for (let i = 0; i < lines.length; i++) {
284
+ const line = lines[i];
285
+ let match;
286
+
287
+ while ((match = pattern.exec(line)) !== null) {
288
+ results.push({
289
+ file,
290
+ line: i + 1,
291
+ column: match.index,
292
+ match: line.trim(),
293
+ context: options.includeContext ? line : undefined,
294
+ });
295
+
296
+ if (options.maxResults && results.length >= options.maxResults) {
297
+ return results;
298
+ }
299
+ }
300
+ }
301
+ } catch {
302
+ // Skip files that can't be read
303
+ }
304
+ }
305
+
306
+ return results;
307
+ }
308
+
309
+ /**
310
+ * Generate directory tree
311
+ */
312
+ async tree(directory: string, options: TreeOptions = {}): Promise<TreeNode> {
313
+ const resolvedPath = this.resolvePath(directory);
314
+ logger.info(`Generating tree for: ${resolvedPath}`);
315
+
316
+ const maxDepth = options.maxDepth ?? 5;
317
+
318
+ const buildTree = async (dir: string, depth: number): Promise<TreeNode> => {
319
+ const stats = await fs.stat(dir);
320
+ const name = path.basename(dir);
321
+
322
+ const node: TreeNode = {
323
+ name,
324
+ path: dir,
325
+ type: stats.isDirectory() ? 'directory' : 'file',
326
+ size: stats.isFile() ? stats.size : undefined,
327
+ };
328
+
329
+ if (stats.isDirectory() && depth < maxDepth) {
330
+ const entries = await fs.readdir(dir, { withFileTypes: true });
331
+ node.children = [];
332
+
333
+ for (const entry of entries) {
334
+ // Skip hidden files if not requested
335
+ if (!options.includeHidden && entry.name.startsWith('.')) {
336
+ continue;
337
+ }
338
+
339
+ // Skip files if not requested
340
+ if (!options.includeFiles && entry.isFile()) {
341
+ continue;
342
+ }
343
+
344
+ const entryPath = path.join(dir, entry.name);
345
+ const childNode = await buildTree(entryPath, depth + 1);
346
+ node.children.push(childNode);
347
+ }
348
+
349
+ // Sort: directories first, then alphabetically
350
+ node.children.sort((a, b) => {
351
+ if (a.type !== b.type) {
352
+ return a.type === 'directory' ? -1 : 1;
353
+ }
354
+ return a.name.localeCompare(b.name);
355
+ });
356
+ }
357
+
358
+ return node;
359
+ };
360
+
361
+ return await buildTree(resolvedPath, 0);
362
+ }
363
+
364
+ /**
365
+ * Get file diff using system diff command
366
+ */
367
+ async diff(
368
+ file1: string,
369
+ file2: string,
370
+ options?: { unified?: number; ignoreWhitespace?: boolean }
371
+ ): Promise<string> {
372
+ const path1 = this.resolvePath(file1);
373
+ const path2 = this.resolvePath(file2);
374
+ logger.info(`Diffing ${path1} and ${path2}`);
375
+
376
+ const args: string[] = [];
377
+
378
+ if (options?.unified !== undefined) {
379
+ args.push(`-U${options.unified}`);
380
+ } else {
381
+ args.push('-u'); // Default unified format
382
+ }
383
+
384
+ if (options?.ignoreWhitespace) {
385
+ args.push('-w');
386
+ }
387
+
388
+ try {
389
+ const { stdout } = await execAsync(`diff ${args.join(' ')} "${path1}" "${path2}"`);
390
+ return stdout;
391
+ } catch (error: any) {
392
+ // diff returns exit code 1 when files are different
393
+ if (error.stdout) {
394
+ return error.stdout;
395
+ }
396
+ throw error;
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Copy file or directory
402
+ */
403
+ async copy(
404
+ source: string,
405
+ destination: string,
406
+ options?: { recursive?: boolean; overwrite?: boolean }
407
+ ): Promise<{ success: boolean; source: string; destination: string }> {
408
+ const srcPath = this.resolvePath(source);
409
+ const destPath = this.resolvePath(destination);
410
+ logger.info(`Copying ${srcPath} to ${destPath}`);
411
+
412
+ const srcStats = await fs.stat(srcPath);
413
+
414
+ if (srcStats.isDirectory()) {
415
+ if (!options?.recursive) {
416
+ throw new Error('Cannot copy directory without recursive option');
417
+ }
418
+ await this.copyDir(srcPath, destPath, options?.overwrite);
419
+ } else {
420
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
421
+
422
+ if (!options?.overwrite) {
423
+ try {
424
+ await fs.access(destPath);
425
+ throw new Error(`Destination file already exists: ${destPath}`);
426
+ } catch (e: any) {
427
+ if (e.code !== 'ENOENT') {
428
+ throw e;
429
+ }
430
+ }
431
+ }
432
+
433
+ await fs.copyFile(srcPath, destPath);
434
+ }
435
+
436
+ return { success: true, source: srcPath, destination: destPath };
437
+ }
438
+
439
+ private async copyDir(src: string, dest: string, overwrite?: boolean): Promise<void> {
440
+ await fs.mkdir(dest, { recursive: true });
441
+
442
+ const entries = await fs.readdir(src, { withFileTypes: true });
443
+
444
+ for (const entry of entries) {
445
+ const srcPath = path.join(src, entry.name);
446
+ const destPath = path.join(dest, entry.name);
447
+
448
+ if (entry.isDirectory()) {
449
+ await this.copyDir(srcPath, destPath, overwrite);
450
+ } else {
451
+ if (!overwrite) {
452
+ try {
453
+ await fs.access(destPath);
454
+ continue; // Skip existing files
455
+ } catch {
456
+ // File doesn't exist, proceed with copy
457
+ }
458
+ }
459
+ await fs.copyFile(srcPath, destPath);
460
+ }
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Move/rename file or directory
466
+ */
467
+ async move(
468
+ source: string,
469
+ destination: string
470
+ ): Promise<{ success: boolean; source: string; destination: string }> {
471
+ const srcPath = this.resolvePath(source);
472
+ const destPath = this.resolvePath(destination);
473
+ logger.info(`Moving ${srcPath} to ${destPath}`);
474
+
475
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
476
+ await fs.rename(srcPath, destPath);
477
+
478
+ return { success: true, source: srcPath, destination: destPath };
479
+ }
480
+
481
+ /**
482
+ * Delete file or directory
483
+ */
484
+ async delete(
485
+ filePath: string,
486
+ options?: { recursive?: boolean; force?: boolean }
487
+ ): Promise<{ success: boolean; path: string }> {
488
+ const resolvedPath = this.resolvePath(filePath);
489
+ logger.info(`Deleting: ${resolvedPath}`);
490
+
491
+ const stats = await fs.stat(resolvedPath);
492
+
493
+ if (stats.isDirectory()) {
494
+ if (!options?.recursive) {
495
+ throw new Error('Cannot delete directory without recursive option');
496
+ }
497
+ await fs.rm(resolvedPath, { recursive: true, force: options?.force });
498
+ } else {
499
+ await fs.unlink(resolvedPath);
500
+ }
501
+
502
+ return { success: true, path: resolvedPath };
503
+ }
504
+
505
+ /**
506
+ * Create directory
507
+ */
508
+ async mkdir(
509
+ dirPath: string,
510
+ options?: { recursive?: boolean }
511
+ ): Promise<{ success: boolean; path: string }> {
512
+ const resolvedPath = this.resolvePath(dirPath);
513
+ logger.info(`Creating directory: ${resolvedPath}`);
514
+
515
+ await fs.mkdir(resolvedPath, { recursive: options?.recursive ?? true });
516
+
517
+ return { success: true, path: resolvedPath };
518
+ }
519
+
520
+ /**
521
+ * Check if file or directory exists
522
+ */
523
+ async exists(filePath: string): Promise<boolean> {
524
+ const resolvedPath = this.resolvePath(filePath);
525
+
526
+ try {
527
+ await fs.access(resolvedPath);
528
+ return true;
529
+ } catch {
530
+ return false;
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Get file stats
536
+ */
537
+ async stat(filePath: string): Promise<FileStats> {
538
+ const resolvedPath = this.resolvePath(filePath);
539
+ logger.info(`Getting stats for: ${resolvedPath}`);
540
+
541
+ const stats = await fs.stat(resolvedPath);
542
+ const lstat = await fs.lstat(resolvedPath);
543
+
544
+ return {
545
+ size: stats.size,
546
+ isFile: stats.isFile(),
547
+ isDirectory: stats.isDirectory(),
548
+ isSymbolicLink: lstat.isSymbolicLink(),
549
+ createdAt: stats.birthtime,
550
+ modifiedAt: stats.mtime,
551
+ accessedAt: stats.atime,
552
+ permissions: (stats.mode & 0o777).toString(8),
553
+ };
554
+ }
555
+
556
+ /**
557
+ * Read directory entries
558
+ */
559
+ async readDir(
560
+ dirPath: string
561
+ ): Promise<Array<{ name: string; type: 'file' | 'directory' | 'symlink' }>> {
562
+ const resolvedPath = this.resolvePath(dirPath);
563
+ logger.info(`Reading directory: ${resolvedPath}`);
564
+
565
+ const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
566
+
567
+ return entries.map(entry => ({
568
+ name: entry.name,
569
+ type: entry.isDirectory() ? 'directory' : entry.isSymbolicLink() ? 'symlink' : 'file',
570
+ }));
571
+ }
572
+
573
+ /**
574
+ * Watch for file changes
575
+ */
576
+ watch(filePath: string, callback: (event: string, filename: string | null) => void): () => void {
577
+ const resolvedPath = this.resolvePath(filePath);
578
+ logger.info(`Watching: ${resolvedPath}`);
579
+
580
+ const watcher = fsSync.watch(
581
+ resolvedPath,
582
+ { recursive: true },
583
+ (event: string, filename: string | null) => {
584
+ callback(event, filename);
585
+ }
586
+ );
587
+
588
+ // Return a cleanup function
589
+ return () => {
590
+ watcher.close();
591
+ };
592
+ }
593
+ }