@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,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift Commands
|
|
3
|
+
*
|
|
4
|
+
* Commands for detecting and fixing infrastructure drift
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ui } from '../../wizard/ui';
|
|
8
|
+
import { select, confirm } from '../../wizard/prompts';
|
|
9
|
+
import { CoreEngineClient } from '../../clients/core-engine-client';
|
|
10
|
+
import type { DriftReport, DriftRemediationResult, DriftProvider } from '../../types';
|
|
11
|
+
|
|
12
|
+
// ==========================================
|
|
13
|
+
// Types
|
|
14
|
+
// ==========================================
|
|
15
|
+
|
|
16
|
+
export interface DriftDetectOptions {
|
|
17
|
+
/** Provider to check: terraform, kubernetes, helm */
|
|
18
|
+
provider?: DriftProvider;
|
|
19
|
+
/** Directory containing infrastructure code */
|
|
20
|
+
directory?: string;
|
|
21
|
+
/** Output format */
|
|
22
|
+
json?: boolean;
|
|
23
|
+
/** Show verbose output */
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DriftFixOptions {
|
|
28
|
+
/** Provider to fix: terraform, kubernetes, helm */
|
|
29
|
+
provider?: DriftProvider;
|
|
30
|
+
/** Directory containing infrastructure code */
|
|
31
|
+
directory?: string;
|
|
32
|
+
/** Auto-approve all changes */
|
|
33
|
+
autoApprove?: boolean;
|
|
34
|
+
/** Dry run - show what would be fixed */
|
|
35
|
+
dryRun?: boolean;
|
|
36
|
+
/** Output format */
|
|
37
|
+
json?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ==========================================
|
|
41
|
+
// Parsers
|
|
42
|
+
// ==========================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse drift detect options
|
|
46
|
+
*/
|
|
47
|
+
export function parseDriftDetectOptions(args: string[]): DriftDetectOptions {
|
|
48
|
+
const options: DriftDetectOptions = {};
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
const arg = args[i];
|
|
52
|
+
if (arg === '--provider' && args[i + 1]) {
|
|
53
|
+
options.provider = args[++i] as DriftProvider;
|
|
54
|
+
} else if (arg === '--directory' && args[i + 1]) {
|
|
55
|
+
options.directory = args[++i];
|
|
56
|
+
} else if (arg === '-d' && args[i + 1]) {
|
|
57
|
+
options.directory = args[++i];
|
|
58
|
+
} else if (arg === '--json') {
|
|
59
|
+
options.json = true;
|
|
60
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
61
|
+
options.verbose = true;
|
|
62
|
+
} else if (!arg.startsWith('-') && !options.provider) {
|
|
63
|
+
options.provider = arg as DriftProvider;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return options;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse drift fix options
|
|
72
|
+
*/
|
|
73
|
+
export function parseDriftFixOptions(args: string[]): DriftFixOptions {
|
|
74
|
+
const options: DriftFixOptions = {};
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < args.length; i++) {
|
|
77
|
+
const arg = args[i];
|
|
78
|
+
if (arg === '--provider' && args[i + 1]) {
|
|
79
|
+
options.provider = args[++i] as DriftProvider;
|
|
80
|
+
} else if (arg === '--directory' && args[i + 1]) {
|
|
81
|
+
options.directory = args[++i];
|
|
82
|
+
} else if (arg === '-d' && args[i + 1]) {
|
|
83
|
+
options.directory = args[++i];
|
|
84
|
+
} else if (arg === '--auto-approve' || arg === '-y') {
|
|
85
|
+
options.autoApprove = true;
|
|
86
|
+
} else if (arg === '--dry-run') {
|
|
87
|
+
options.dryRun = true;
|
|
88
|
+
} else if (arg === '--json') {
|
|
89
|
+
options.json = true;
|
|
90
|
+
} else if (!arg.startsWith('-') && !options.provider) {
|
|
91
|
+
options.provider = arg as DriftProvider;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return options;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ==========================================
|
|
99
|
+
// Display Functions
|
|
100
|
+
// ==========================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format drift severity with color
|
|
104
|
+
*/
|
|
105
|
+
function _formatSeverity(severity: 'critical' | 'high' | 'medium' | 'low'): string {
|
|
106
|
+
switch (severity) {
|
|
107
|
+
case 'critical':
|
|
108
|
+
return ui.color('CRITICAL', 'red');
|
|
109
|
+
case 'high':
|
|
110
|
+
return ui.color('HIGH', 'red');
|
|
111
|
+
case 'medium':
|
|
112
|
+
return ui.color('MEDIUM', 'yellow');
|
|
113
|
+
case 'low':
|
|
114
|
+
default:
|
|
115
|
+
return ui.color('LOW', 'blue');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Format drift type with color
|
|
121
|
+
*/
|
|
122
|
+
function formatDriftType(type: 'added' | 'removed' | 'modified'): string {
|
|
123
|
+
switch (type) {
|
|
124
|
+
case 'added':
|
|
125
|
+
return ui.color('+', 'green');
|
|
126
|
+
case 'removed':
|
|
127
|
+
return ui.color('-', 'red');
|
|
128
|
+
case 'modified':
|
|
129
|
+
return ui.color('~', 'yellow');
|
|
130
|
+
default:
|
|
131
|
+
return '?';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Display drift report
|
|
137
|
+
*/
|
|
138
|
+
function displayDriftReport(report: DriftReport): void {
|
|
139
|
+
ui.newLine();
|
|
140
|
+
ui.section(`Drift Report - ${report.provider.toUpperCase()}`);
|
|
141
|
+
|
|
142
|
+
ui.print(` ${ui.dim('Detected at:')} ${new Date(report.detectedAt).toLocaleString()}`);
|
|
143
|
+
ui.print(` ${ui.dim('Total items:')} ${report.summary.total}`);
|
|
144
|
+
ui.print(
|
|
145
|
+
` ${ui.dim('Has drift:')} ${report.hasDrift ? ui.color('Yes', 'yellow') : ui.color('No', 'green')}`
|
|
146
|
+
);
|
|
147
|
+
ui.newLine();
|
|
148
|
+
|
|
149
|
+
if (!report.hasDrift) {
|
|
150
|
+
ui.success('No drift detected. Infrastructure is in sync.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Summary
|
|
155
|
+
ui.print(' Changes:');
|
|
156
|
+
if (report.summary.added > 0) {
|
|
157
|
+
ui.print(` ${ui.color('+', 'green')} Added: ${report.summary.added}`);
|
|
158
|
+
}
|
|
159
|
+
if (report.summary.removed > 0) {
|
|
160
|
+
ui.print(` ${ui.color('-', 'red')} Removed: ${report.summary.removed}`);
|
|
161
|
+
}
|
|
162
|
+
if (report.summary.modified > 0) {
|
|
163
|
+
ui.print(` ${ui.color('~', 'yellow')} Modified: ${report.summary.modified}`);
|
|
164
|
+
}
|
|
165
|
+
ui.newLine();
|
|
166
|
+
|
|
167
|
+
// Resource Details
|
|
168
|
+
ui.section('Resources with Drift');
|
|
169
|
+
|
|
170
|
+
for (const resource of report.resources) {
|
|
171
|
+
ui.newLine();
|
|
172
|
+
ui.print(` ${formatDriftType(resource.driftType)} ${ui.bold(resource.resourceId)}`);
|
|
173
|
+
ui.print(` ${ui.dim('Type:')} ${resource.resourceType}`);
|
|
174
|
+
if (resource.name) {
|
|
175
|
+
ui.print(` ${ui.dim('Name:')} ${resource.name}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (resource.changes.length > 0) {
|
|
179
|
+
ui.print(` ${ui.dim('Changes:')}`);
|
|
180
|
+
for (const change of resource.changes.slice(0, 5)) {
|
|
181
|
+
const expected = change.expected !== undefined ? JSON.stringify(change.expected) : 'null';
|
|
182
|
+
const actual = change.actual !== undefined ? JSON.stringify(change.actual) : 'null';
|
|
183
|
+
ui.print(
|
|
184
|
+
` ${ui.dim(change.attribute)}: ${ui.color(expected, 'red')} -> ${ui.color(actual, 'green')}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (resource.changes.length > 5) {
|
|
188
|
+
ui.print(ui.dim(` ... and ${resource.changes.length - 5} more changes`));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Display remediation result
|
|
196
|
+
*/
|
|
197
|
+
function displayRemediationResult(result: DriftRemediationResult): void {
|
|
198
|
+
ui.newLine();
|
|
199
|
+
ui.section('Remediation Result');
|
|
200
|
+
|
|
201
|
+
const statusColor = result.success ? 'green' : 'red';
|
|
202
|
+
ui.print(
|
|
203
|
+
` ${ui.dim('Status:')} ${ui.color(result.success ? 'Success' : 'Failed', statusColor)}`
|
|
204
|
+
);
|
|
205
|
+
ui.print(` ${ui.dim('Applied:')} ${result.appliedCount}`);
|
|
206
|
+
ui.print(` ${ui.dim('Failed:')} ${result.failedCount}`);
|
|
207
|
+
ui.print(` ${ui.dim('Skipped:')} ${result.skippedCount}`);
|
|
208
|
+
ui.newLine();
|
|
209
|
+
|
|
210
|
+
if (result.actions.length > 0) {
|
|
211
|
+
ui.section('Actions Taken');
|
|
212
|
+
|
|
213
|
+
for (const action of result.actions) {
|
|
214
|
+
const icon =
|
|
215
|
+
action.status === 'applied'
|
|
216
|
+
? ui.color('✓', 'green')
|
|
217
|
+
: action.status === 'failed'
|
|
218
|
+
? ui.color('✗', 'red')
|
|
219
|
+
: ui.color('○', 'dim');
|
|
220
|
+
|
|
221
|
+
ui.print(` ${icon} ${action.description}`);
|
|
222
|
+
if (action.error) {
|
|
223
|
+
ui.print(` ${ui.color('Error:', 'red')} ${action.error}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (result.report) {
|
|
229
|
+
ui.newLine();
|
|
230
|
+
ui.print(ui.dim('Full report:'));
|
|
231
|
+
ui.print(result.report);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ==========================================
|
|
236
|
+
// Commands
|
|
237
|
+
// ==========================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Drift parent command
|
|
241
|
+
*/
|
|
242
|
+
export async function driftCommand(args: string[]): Promise<void> {
|
|
243
|
+
if (args.length === 0) {
|
|
244
|
+
ui.header('Nimbus Drift', 'Infrastructure drift detection and remediation');
|
|
245
|
+
ui.newLine();
|
|
246
|
+
ui.print('Usage: nimbus drift <command> [options]');
|
|
247
|
+
ui.newLine();
|
|
248
|
+
ui.print('Commands:');
|
|
249
|
+
ui.print(` ${ui.bold('detect')} Detect infrastructure drift`);
|
|
250
|
+
ui.print(` ${ui.bold('fix')} Fix detected drift`);
|
|
251
|
+
ui.newLine();
|
|
252
|
+
ui.print('Examples:');
|
|
253
|
+
ui.print(' nimbus drift detect --provider terraform');
|
|
254
|
+
ui.print(' nimbus drift detect kubernetes -d ./manifests');
|
|
255
|
+
ui.print(' nimbus drift fix terraform --auto-approve');
|
|
256
|
+
ui.print(' nimbus drift fix --dry-run');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const subcommand = args[0];
|
|
261
|
+
const subArgs = args.slice(1);
|
|
262
|
+
|
|
263
|
+
switch (subcommand) {
|
|
264
|
+
case 'detect':
|
|
265
|
+
await driftDetectCommand(parseDriftDetectOptions(subArgs));
|
|
266
|
+
break;
|
|
267
|
+
case 'fix':
|
|
268
|
+
await driftFixCommand(parseDriftFixOptions(subArgs));
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
ui.error(`Unknown drift command: ${subcommand}`);
|
|
272
|
+
ui.info('Run "nimbus drift" for usage');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Detect drift command
|
|
278
|
+
*/
|
|
279
|
+
export async function driftDetectCommand(options: DriftDetectOptions): Promise<void> {
|
|
280
|
+
const directory = options.directory || process.cwd();
|
|
281
|
+
let provider = options.provider;
|
|
282
|
+
|
|
283
|
+
ui.header('Nimbus Drift Detect', directory);
|
|
284
|
+
|
|
285
|
+
// If no provider specified, try to detect or ask
|
|
286
|
+
if (!provider) {
|
|
287
|
+
const providerChoice = await select({
|
|
288
|
+
message: 'Select infrastructure provider to check:',
|
|
289
|
+
options: [
|
|
290
|
+
{ label: 'Terraform', value: 'terraform', description: 'Check Terraform state drift' },
|
|
291
|
+
{
|
|
292
|
+
label: 'Kubernetes',
|
|
293
|
+
value: 'kubernetes',
|
|
294
|
+
description: 'Check Kubernetes manifest drift',
|
|
295
|
+
},
|
|
296
|
+
{ label: 'Helm', value: 'helm', description: 'Check Helm release drift' },
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
provider = providerChoice as DriftProvider;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
ui.startSpinner({ message: `Detecting ${provider} drift...` });
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const client = new CoreEngineClient();
|
|
306
|
+
const report = await client.detectDrift({
|
|
307
|
+
provider,
|
|
308
|
+
directory,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
ui.stopSpinnerSuccess('Drift detection complete');
|
|
312
|
+
|
|
313
|
+
if (options.json) {
|
|
314
|
+
console.log(JSON.stringify(report, null, 2));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
displayDriftReport(report);
|
|
319
|
+
|
|
320
|
+
if (report.hasDrift) {
|
|
321
|
+
ui.newLine();
|
|
322
|
+
ui.info('Run "nimbus drift fix" to remediate detected drift');
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
ui.stopSpinnerFail('Drift detection failed');
|
|
326
|
+
ui.error((error as Error).message);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Fix drift command
|
|
332
|
+
*/
|
|
333
|
+
export async function driftFixCommand(options: DriftFixOptions): Promise<void> {
|
|
334
|
+
const directory = options.directory || process.cwd();
|
|
335
|
+
let provider = options.provider;
|
|
336
|
+
|
|
337
|
+
ui.header('Nimbus Drift Fix', directory);
|
|
338
|
+
|
|
339
|
+
// If no provider specified, ask
|
|
340
|
+
if (!provider) {
|
|
341
|
+
const providerChoice = await select({
|
|
342
|
+
message: 'Select infrastructure provider to fix:',
|
|
343
|
+
options: [
|
|
344
|
+
{ label: 'Terraform', value: 'terraform', description: 'Fix Terraform state drift' },
|
|
345
|
+
{ label: 'Kubernetes', value: 'kubernetes', description: 'Fix Kubernetes manifest drift' },
|
|
346
|
+
{ label: 'Helm', value: 'helm', description: 'Fix Helm release drift' },
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
provider = providerChoice as DriftProvider;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// First detect drift
|
|
353
|
+
ui.startSpinner({ message: `Detecting ${provider} drift...` });
|
|
354
|
+
|
|
355
|
+
let report: DriftReport;
|
|
356
|
+
try {
|
|
357
|
+
const client = new CoreEngineClient();
|
|
358
|
+
report = await client.detectDrift({
|
|
359
|
+
provider,
|
|
360
|
+
directory,
|
|
361
|
+
});
|
|
362
|
+
ui.stopSpinnerSuccess('Drift detection complete');
|
|
363
|
+
} catch (error) {
|
|
364
|
+
ui.stopSpinnerFail('Drift detection failed');
|
|
365
|
+
ui.error((error as Error).message);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!report.hasDrift) {
|
|
370
|
+
ui.newLine();
|
|
371
|
+
ui.success('No drift detected. Nothing to fix.');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Show what will be fixed
|
|
376
|
+
displayDriftReport(report);
|
|
377
|
+
|
|
378
|
+
// Confirm before fixing (unless auto-approve or dry-run)
|
|
379
|
+
if (!options.autoApprove && !options.dryRun) {
|
|
380
|
+
ui.newLine();
|
|
381
|
+
const proceed = await confirm({
|
|
382
|
+
message: `Apply ${report.summary.total} remediation actions?`,
|
|
383
|
+
defaultValue: false,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (!proceed) {
|
|
387
|
+
ui.info('Fix cancelled.');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (options.dryRun) {
|
|
393
|
+
ui.newLine();
|
|
394
|
+
ui.info('Dry run mode - no changes will be applied');
|
|
395
|
+
ui.newLine();
|
|
396
|
+
|
|
397
|
+
// Show what would be done
|
|
398
|
+
ui.section('Actions that would be taken:');
|
|
399
|
+
for (const resource of report.resources) {
|
|
400
|
+
ui.print(` ${formatDriftType(resource.driftType)} ${resource.resourceId}`);
|
|
401
|
+
if (resource.driftType === 'added') {
|
|
402
|
+
ui.print(` ${ui.dim('Would be removed from actual state')}`);
|
|
403
|
+
} else if (resource.driftType === 'removed') {
|
|
404
|
+
ui.print(` ${ui.dim('Would be recreated')}`);
|
|
405
|
+
} else {
|
|
406
|
+
ui.print(` ${ui.dim('Would be updated to match desired state')}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Apply fixes
|
|
413
|
+
ui.startSpinner({ message: 'Applying remediation...' });
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const client = new CoreEngineClient();
|
|
417
|
+
const result = await client.fixDrift({
|
|
418
|
+
provider,
|
|
419
|
+
directory,
|
|
420
|
+
dryRun: false,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
if (result.success) {
|
|
424
|
+
ui.stopSpinnerSuccess('Remediation complete');
|
|
425
|
+
} else {
|
|
426
|
+
ui.stopSpinnerFail('Remediation partially failed');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (options.json) {
|
|
430
|
+
console.log(JSON.stringify(result, null, 2));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
displayRemediationResult(result);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
ui.stopSpinnerFail('Remediation failed');
|
|
437
|
+
ui.error((error as Error).message);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explain Command
|
|
3
|
+
*
|
|
4
|
+
* Get AI explanations for code, infrastructure, or errors
|
|
5
|
+
*
|
|
6
|
+
* Usage: nimbus explain <target> [options]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '../utils';
|
|
10
|
+
import { ui } from '../wizard';
|
|
11
|
+
import { llmClient } from '../clients';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Content type for explanation
|
|
15
|
+
*/
|
|
16
|
+
export type ExplainType = 'code' | 'infra' | 'error' | 'auto';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Command options
|
|
20
|
+
*/
|
|
21
|
+
export interface ExplainOptions {
|
|
22
|
+
type?: ExplainType;
|
|
23
|
+
file?: string;
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detect content type from target
|
|
30
|
+
*/
|
|
31
|
+
function detectExplainType(target: string, content?: string): ExplainType {
|
|
32
|
+
// Check for error patterns
|
|
33
|
+
const errorPatterns = [
|
|
34
|
+
/^error:/i,
|
|
35
|
+
/^exception:/i,
|
|
36
|
+
/failed/i,
|
|
37
|
+
/traceback/i,
|
|
38
|
+
/stack trace/i,
|
|
39
|
+
/undefined variable/i,
|
|
40
|
+
/cannot find/i,
|
|
41
|
+
/not found/i,
|
|
42
|
+
/invalid/i,
|
|
43
|
+
/permission denied/i,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const targetLower = target.toLowerCase();
|
|
47
|
+
|
|
48
|
+
if (errorPatterns.some(p => p.test(target) || (content && p.test(content)))) {
|
|
49
|
+
return 'error';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check for infrastructure file extensions
|
|
53
|
+
const infraExtensions = ['.tf', '.yaml', '.yml', '.json', '.hcl', '.toml'];
|
|
54
|
+
if (infraExtensions.some(ext => targetLower.endsWith(ext))) {
|
|
55
|
+
return 'infra';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check content for infrastructure patterns
|
|
59
|
+
if (content) {
|
|
60
|
+
if (content.includes('apiVersion:') && content.includes('kind:')) {
|
|
61
|
+
return 'infra'; // Kubernetes
|
|
62
|
+
}
|
|
63
|
+
if (content.includes('resource ') && content.includes('provider ')) {
|
|
64
|
+
return 'infra'; // Terraform
|
|
65
|
+
}
|
|
66
|
+
if (content.includes('AWSTemplateFormatVersion')) {
|
|
67
|
+
return 'infra'; // CloudFormation
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for code file extensions
|
|
72
|
+
const codeExtensions = [
|
|
73
|
+
'.ts',
|
|
74
|
+
'.js',
|
|
75
|
+
'.py',
|
|
76
|
+
'.go',
|
|
77
|
+
'.java',
|
|
78
|
+
'.rs',
|
|
79
|
+
'.rb',
|
|
80
|
+
'.php',
|
|
81
|
+
'.c',
|
|
82
|
+
'.cpp',
|
|
83
|
+
'.cs',
|
|
84
|
+
];
|
|
85
|
+
if (codeExtensions.some(ext => targetLower.endsWith(ext))) {
|
|
86
|
+
return 'code';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Default to code for unknown content
|
|
90
|
+
return 'code';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build the explanation prompt based on type
|
|
95
|
+
*/
|
|
96
|
+
function buildPrompt(type: ExplainType, content: string, verbose: boolean): string {
|
|
97
|
+
const detailLevel = verbose ? 'detailed' : 'concise';
|
|
98
|
+
|
|
99
|
+
switch (type) {
|
|
100
|
+
case 'error':
|
|
101
|
+
return `Please explain this error and suggest how to fix it. Provide a ${detailLevel} explanation.
|
|
102
|
+
|
|
103
|
+
Error:
|
|
104
|
+
\`\`\`
|
|
105
|
+
${content}
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
Include:
|
|
109
|
+
1. What the error means
|
|
110
|
+
2. Why it might have occurred
|
|
111
|
+
3. How to fix it
|
|
112
|
+
4. How to prevent it in the future`;
|
|
113
|
+
|
|
114
|
+
case 'infra':
|
|
115
|
+
return `Please explain this infrastructure configuration. Provide a ${detailLevel} explanation.
|
|
116
|
+
|
|
117
|
+
Configuration:
|
|
118
|
+
\`\`\`
|
|
119
|
+
${content}
|
|
120
|
+
\`\`\`
|
|
121
|
+
|
|
122
|
+
Include:
|
|
123
|
+
1. What this configuration does
|
|
124
|
+
2. Key components and their purpose
|
|
125
|
+
3. Any potential issues or improvements
|
|
126
|
+
${verbose ? '4. Security considerations\n5. Best practices recommendations' : ''}`;
|
|
127
|
+
|
|
128
|
+
case 'code':
|
|
129
|
+
default:
|
|
130
|
+
return `Please explain this code. Provide a ${detailLevel} explanation.
|
|
131
|
+
|
|
132
|
+
Code:
|
|
133
|
+
\`\`\`
|
|
134
|
+
${content}
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
Include:
|
|
138
|
+
1. What the code does
|
|
139
|
+
2. How it works
|
|
140
|
+
3. Any potential issues
|
|
141
|
+
${verbose ? '4. Suggestions for improvement\n5. Related best practices' : ''}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run the explain command
|
|
147
|
+
*/
|
|
148
|
+
export async function explainCommand(target: string, options: ExplainOptions = {}): Promise<void> {
|
|
149
|
+
logger.info('Running explain command', { target, options });
|
|
150
|
+
|
|
151
|
+
let content: string;
|
|
152
|
+
let filePath: string | undefined;
|
|
153
|
+
|
|
154
|
+
// Get content to explain
|
|
155
|
+
if (options.file) {
|
|
156
|
+
// Read from specified file
|
|
157
|
+
filePath = options.file;
|
|
158
|
+
try {
|
|
159
|
+
const fs = await import('fs/promises');
|
|
160
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
161
|
+
} catch (error: any) {
|
|
162
|
+
ui.error(`Could not read file: ${error.message}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
} else if (target) {
|
|
166
|
+
// Check if target is a file path
|
|
167
|
+
try {
|
|
168
|
+
const fs = await import('fs/promises');
|
|
169
|
+
const stat = await fs.stat(target);
|
|
170
|
+
if (stat.isFile()) {
|
|
171
|
+
filePath = target;
|
|
172
|
+
content = await fs.readFile(target, 'utf-8');
|
|
173
|
+
} else if (stat.isDirectory()) {
|
|
174
|
+
ui.error('Please specify a file, not a directory');
|
|
175
|
+
ui.info('Usage: nimbus explain <file> or nimbus explain "error message"');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
} else {
|
|
178
|
+
content = target;
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// Not a file, use as content directly
|
|
182
|
+
content = target;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
ui.error('Please provide something to explain');
|
|
186
|
+
ui.newLine();
|
|
187
|
+
ui.print('Usage: nimbus explain <target> [options]');
|
|
188
|
+
ui.newLine();
|
|
189
|
+
ui.print('Examples:');
|
|
190
|
+
ui.print(' nimbus explain ./main.tf');
|
|
191
|
+
ui.print(' nimbus explain "Error: resource not found" --type error');
|
|
192
|
+
ui.print(' nimbus explain --file ./deployment.yaml');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Limit content size
|
|
197
|
+
const maxLength = 10000;
|
|
198
|
+
if (content.length > maxLength) {
|
|
199
|
+
ui.warning(`Content truncated to ${maxLength} characters`);
|
|
200
|
+
content = `${content.slice(0, maxLength)}\n... (content truncated)`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Detect or use specified type
|
|
204
|
+
const type =
|
|
205
|
+
options.type === 'auto' || !options.type ? detectExplainType(target, content) : options.type;
|
|
206
|
+
|
|
207
|
+
// Display header
|
|
208
|
+
ui.header('Nimbus Explain');
|
|
209
|
+
if (filePath) {
|
|
210
|
+
ui.info(`File: ${filePath}`);
|
|
211
|
+
}
|
|
212
|
+
ui.info(`Type: ${type}`);
|
|
213
|
+
ui.newLine();
|
|
214
|
+
|
|
215
|
+
// Check if LLM is available
|
|
216
|
+
const llmAvailable = await llmClient.isAvailable();
|
|
217
|
+
|
|
218
|
+
if (!llmAvailable) {
|
|
219
|
+
ui.error('LLM service is not available');
|
|
220
|
+
ui.info('Make sure you have configured an LLM provider with "nimbus login"');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Build prompt
|
|
225
|
+
const prompt = buildPrompt(type, content, options.verbose ?? false);
|
|
226
|
+
|
|
227
|
+
ui.startSpinner({ message: 'Analyzing...' });
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
let response = '';
|
|
231
|
+
let firstChunk = true;
|
|
232
|
+
|
|
233
|
+
for await (const chunk of llmClient.chat(prompt, [])) {
|
|
234
|
+
if (chunk.type === 'content' && chunk.content) {
|
|
235
|
+
if (firstChunk) {
|
|
236
|
+
ui.stopSpinnerSuccess('');
|
|
237
|
+
ui.newLine();
|
|
238
|
+
firstChunk = false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
response += chunk.content;
|
|
242
|
+
process.stdout.write(chunk.content);
|
|
243
|
+
} else if (chunk.type === 'error') {
|
|
244
|
+
ui.stopSpinnerFail('Error');
|
|
245
|
+
ui.error(chunk.message || chunk.error || 'Unknown error');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Ensure newline at end
|
|
251
|
+
if (!response.endsWith('\n')) {
|
|
252
|
+
ui.newLine();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// JSON output mode
|
|
256
|
+
if (options.json) {
|
|
257
|
+
console.log(
|
|
258
|
+
JSON.stringify(
|
|
259
|
+
{
|
|
260
|
+
type,
|
|
261
|
+
file: filePath,
|
|
262
|
+
explanation: response,
|
|
263
|
+
},
|
|
264
|
+
null,
|
|
265
|
+
2
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
} catch (error: any) {
|
|
270
|
+
ui.stopSpinnerFail('Failed');
|
|
271
|
+
ui.error(error.message);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Export as default
|
|
277
|
+
export default explainCommand;
|