@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.
- package/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- 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
|
+
}
|