@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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeployPreview Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a resource-change table before a deploy action is applied. Each
|
|
5
|
+
* change is prefixed with a symbol indicating the action:
|
|
6
|
+
*
|
|
7
|
+
* + create (green)
|
|
8
|
+
* ~ modify (yellow)
|
|
9
|
+
* - destroy (red)
|
|
10
|
+
* -/+ replace (magenta)
|
|
11
|
+
*
|
|
12
|
+
* Below the table the component shows optional cost impact, blast radius, and
|
|
13
|
+
* affected services. Keyboard shortcuts let the user approve, reject, or
|
|
14
|
+
* request the full plan output.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { Box, Text, useInput } from 'ink';
|
|
19
|
+
import type { DeployPreviewData, DeployChange } from './types';
|
|
20
|
+
|
|
21
|
+
/** Possible decisions from the deploy preview prompt. */
|
|
22
|
+
export type DeployDecision = 'approve' | 'reject' | 'show_plan';
|
|
23
|
+
|
|
24
|
+
/** Props accepted by the DeployPreview component. */
|
|
25
|
+
export interface DeployPreviewProps {
|
|
26
|
+
preview: DeployPreviewData;
|
|
27
|
+
onDecide: (decision: DeployDecision) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Map change action to a prefix character and colour. */
|
|
31
|
+
const ACTION_DISPLAY: Record<DeployChange['action'], { prefix: string; color: string }> = {
|
|
32
|
+
create: { prefix: '+', color: 'green' },
|
|
33
|
+
modify: { prefix: '~', color: 'yellow' },
|
|
34
|
+
destroy: { prefix: '-', color: 'red' },
|
|
35
|
+
replace: { prefix: '-/+', color: 'magenta' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute summary counts for the banner line.
|
|
40
|
+
*/
|
|
41
|
+
function summaryCounts(changes: DeployChange[]): { add: number; change: number; destroy: number } {
|
|
42
|
+
let add = 0;
|
|
43
|
+
let change = 0;
|
|
44
|
+
let destroy = 0;
|
|
45
|
+
for (const c of changes) {
|
|
46
|
+
switch (c.action) {
|
|
47
|
+
case 'create':
|
|
48
|
+
add++;
|
|
49
|
+
break;
|
|
50
|
+
case 'modify':
|
|
51
|
+
case 'replace':
|
|
52
|
+
change++;
|
|
53
|
+
break;
|
|
54
|
+
case 'destroy':
|
|
55
|
+
destroy++;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { add, change, destroy };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A single row in the change table.
|
|
64
|
+
*/
|
|
65
|
+
function ChangeRow({ change }: { change: DeployChange }) {
|
|
66
|
+
const display = ACTION_DISPLAY[change.action];
|
|
67
|
+
return (
|
|
68
|
+
<Box>
|
|
69
|
+
<Text color={display.color} bold>
|
|
70
|
+
{display.prefix.padEnd(4)}
|
|
71
|
+
</Text>
|
|
72
|
+
<Text>{change.resourceType}</Text>
|
|
73
|
+
<Text dimColor>.</Text>
|
|
74
|
+
<Text bold>{change.resourceName}</Text>
|
|
75
|
+
{change.details && <Text dimColor> ({change.details})</Text>}
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* DeployPreview renders the full preview modal with a change table and
|
|
82
|
+
* action key legend.
|
|
83
|
+
*/
|
|
84
|
+
export function DeployPreview({ preview, onDecide }: DeployPreviewProps) {
|
|
85
|
+
useInput(input => {
|
|
86
|
+
switch (input) {
|
|
87
|
+
case 'a':
|
|
88
|
+
onDecide('approve');
|
|
89
|
+
break;
|
|
90
|
+
case 'r':
|
|
91
|
+
onDecide('reject');
|
|
92
|
+
break;
|
|
93
|
+
case 'p':
|
|
94
|
+
onDecide('show_plan');
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const counts = summaryCounts(preview.changes);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Box flexDirection="column" borderStyle="double" borderColor="yellow" paddingX={1} paddingY={1}>
|
|
103
|
+
{/* Title */}
|
|
104
|
+
<Box marginBottom={1}>
|
|
105
|
+
<Text bold color="yellow">
|
|
106
|
+
Deploy Preview
|
|
107
|
+
</Text>
|
|
108
|
+
<Text dimColor> ({preview.tool})</Text>
|
|
109
|
+
</Box>
|
|
110
|
+
|
|
111
|
+
{/* Summary counts */}
|
|
112
|
+
<Box marginBottom={1}>
|
|
113
|
+
<Text color="green">+{counts.add} to add</Text>
|
|
114
|
+
<Text> </Text>
|
|
115
|
+
<Text color="yellow">~{counts.change} to change</Text>
|
|
116
|
+
<Text> </Text>
|
|
117
|
+
<Text color="red">-{counts.destroy} to destroy</Text>
|
|
118
|
+
</Box>
|
|
119
|
+
|
|
120
|
+
{/* Change table */}
|
|
121
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
122
|
+
{preview.changes.map((change, idx) => (
|
|
123
|
+
<ChangeRow key={idx} change={change} />
|
|
124
|
+
))}
|
|
125
|
+
{preview.changes.length === 0 && <Text dimColor>No resource changes detected.</Text>}
|
|
126
|
+
</Box>
|
|
127
|
+
|
|
128
|
+
{/* Cost impact */}
|
|
129
|
+
{preview.costImpact && (
|
|
130
|
+
<Box>
|
|
131
|
+
<Text dimColor>Cost impact: </Text>
|
|
132
|
+
<Text>{preview.costImpact}</Text>
|
|
133
|
+
</Box>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{/* Blast radius */}
|
|
137
|
+
{preview.blastRadius && (
|
|
138
|
+
<Box>
|
|
139
|
+
<Text dimColor>Blast radius: </Text>
|
|
140
|
+
<Text color="yellow">{preview.blastRadius}</Text>
|
|
141
|
+
</Box>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{/* Affected services */}
|
|
145
|
+
{preview.affectedServices && preview.affectedServices.length > 0 && (
|
|
146
|
+
<Box>
|
|
147
|
+
<Text dimColor>Affected services: </Text>
|
|
148
|
+
<Text>{preview.affectedServices.join(', ')}</Text>
|
|
149
|
+
</Box>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{/* Action keys */}
|
|
153
|
+
<Box marginTop={1}>
|
|
154
|
+
<Text color="green" bold>
|
|
155
|
+
[a]
|
|
156
|
+
</Text>
|
|
157
|
+
<Text> Approve </Text>
|
|
158
|
+
<Text color="red" bold>
|
|
159
|
+
[r]
|
|
160
|
+
</Text>
|
|
161
|
+
<Text> Reject </Text>
|
|
162
|
+
<Text color="cyan" bold>
|
|
163
|
+
[p]
|
|
164
|
+
</Text>
|
|
165
|
+
<Text> Show full plan</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
</Box>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header Component
|
|
3
|
+
*
|
|
4
|
+
* Displays the Nimbus banner at the top of the TUI: version string, active
|
|
5
|
+
* model, session ID, and a color-coded mode indicator (plan / build / deploy).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { Box, Text } from 'ink';
|
|
10
|
+
import type { SessionInfo, AgentMode } from './types';
|
|
11
|
+
import { VERSION } from '../version';
|
|
12
|
+
|
|
13
|
+
/** Props accepted by the Header component. */
|
|
14
|
+
export interface HeaderProps {
|
|
15
|
+
session: SessionInfo;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Map each mode to its display colour. */
|
|
19
|
+
const MODE_COLORS: Record<AgentMode, string> = {
|
|
20
|
+
plan: 'blue',
|
|
21
|
+
build: 'yellow',
|
|
22
|
+
deploy: 'red',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Truncate a session ID to a short prefix for display purposes.
|
|
27
|
+
*/
|
|
28
|
+
function shortId(id: string): string {
|
|
29
|
+
return id.length > 8 ? id.slice(0, 8) : id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Header renders a single-line banner containing the CLI version, the model
|
|
34
|
+
* name, the abbreviated session ID, and a colour-coded mode badge.
|
|
35
|
+
*/
|
|
36
|
+
export function Header({ session }: HeaderProps) {
|
|
37
|
+
const modeColor = MODE_COLORS[session.mode];
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Box
|
|
41
|
+
borderStyle="round"
|
|
42
|
+
borderColor="cyan"
|
|
43
|
+
paddingX={1}
|
|
44
|
+
justifyContent="space-between"
|
|
45
|
+
width="100%"
|
|
46
|
+
>
|
|
47
|
+
{/* Left: branding + version */}
|
|
48
|
+
<Box>
|
|
49
|
+
<Text bold color="cyan">
|
|
50
|
+
nimbus
|
|
51
|
+
</Text>
|
|
52
|
+
<Text dimColor> v{VERSION}</Text>
|
|
53
|
+
<Text dimColor> {' \u2014 '}</Text>
|
|
54
|
+
<Text>{session.model}</Text>
|
|
55
|
+
<Text dimColor> {' \u2014 '}</Text>
|
|
56
|
+
<Text dimColor>session: {shortId(session.id)}</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
|
|
59
|
+
{/* Right: mode badge */}
|
|
60
|
+
<Box>
|
|
61
|
+
<Text color={modeColor} bold inverse>
|
|
62
|
+
{' '}
|
|
63
|
+
{session.mode.toUpperCase()}{' '}
|
|
64
|
+
</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputBox Component
|
|
3
|
+
*
|
|
4
|
+
* A text input area with a "> " prompt character. Uses ink-text-input for
|
|
5
|
+
* editing and submits on Enter. The parent component receives the submitted
|
|
6
|
+
* text via the `onSubmit` callback.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Input history (Up/Down arrows)
|
|
10
|
+
* - Multi-line paste detection with line count indicator
|
|
11
|
+
* - Slash command autocomplete (Tab to cycle)
|
|
12
|
+
* - @file mention with Tab completion (type @ then Tab to cycle files)
|
|
13
|
+
* - Reverse search (Ctrl+R) through history
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
17
|
+
import { Box, Text, useInput } from 'ink';
|
|
18
|
+
import TextInput from 'ink-text-input';
|
|
19
|
+
|
|
20
|
+
/** Maximum number of history entries to keep. */
|
|
21
|
+
const MAX_HISTORY = 100;
|
|
22
|
+
|
|
23
|
+
/** All recognized slash commands for autocomplete. */
|
|
24
|
+
const SLASH_COMMANDS = [
|
|
25
|
+
'/clear',
|
|
26
|
+
'/compact',
|
|
27
|
+
'/context',
|
|
28
|
+
'/help',
|
|
29
|
+
'/model',
|
|
30
|
+
'/models',
|
|
31
|
+
'/mode',
|
|
32
|
+
'/new',
|
|
33
|
+
'/redo',
|
|
34
|
+
'/sessions',
|
|
35
|
+
'/switch',
|
|
36
|
+
'/undo',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Props accepted by the InputBox component. */
|
|
40
|
+
export interface InputBoxProps {
|
|
41
|
+
/** Called when the user presses Enter with non-empty input. */
|
|
42
|
+
onSubmit: (text: string) => void;
|
|
43
|
+
/** Called when the user presses Escape to abort the current operation. */
|
|
44
|
+
onAbort?: () => void;
|
|
45
|
+
/** Placeholder text shown when the input is empty. */
|
|
46
|
+
placeholder?: string;
|
|
47
|
+
/** Whether the input is disabled (e.g. while the agent is processing). */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* InputBox renders a ">" prompt followed by an editable text field.
|
|
53
|
+
* Pressing Enter submits the value and clears the field. Pressing Escape
|
|
54
|
+
* fires the optional onAbort callback. Up/Down arrows navigate history.
|
|
55
|
+
* Tab autocompletes slash commands. Ctrl+R opens reverse search.
|
|
56
|
+
*/
|
|
57
|
+
export function InputBox({ onSubmit, onAbort, placeholder, disabled = false }: InputBoxProps) {
|
|
58
|
+
const [value, setValue] = useState('') as [string, React.Dispatch<React.SetStateAction<string>>];
|
|
59
|
+
|
|
60
|
+
// History: most recent entry is at the end
|
|
61
|
+
const history = useRef<string[]>([]);
|
|
62
|
+
// -1 means "not browsing history" (showing current draft)
|
|
63
|
+
const historyIndex = useRef(-1);
|
|
64
|
+
// Stores the in-progress text before the user started browsing history
|
|
65
|
+
const draft = useRef('');
|
|
66
|
+
|
|
67
|
+
// Slash command autocomplete state
|
|
68
|
+
const [slashHint, setSlashHint] = useState('');
|
|
69
|
+
const suggestionIndex = useRef(0);
|
|
70
|
+
const lastSuggestions = useRef<string[]>([]);
|
|
71
|
+
|
|
72
|
+
// @file completion state
|
|
73
|
+
const [fileSuggestions, setFileSuggestions] = useState<string[]>([]);
|
|
74
|
+
const [fileHint, setFileHint] = useState('');
|
|
75
|
+
const fileSuggestionIndex = useRef(0);
|
|
76
|
+
|
|
77
|
+
// Ctrl+R search mode
|
|
78
|
+
const [searchMode, setSearchMode] = useState(false);
|
|
79
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
80
|
+
const [searchResults, setSearchResults] = useState<string[]>([]);
|
|
81
|
+
|
|
82
|
+
const handleSubmit = useCallback(
|
|
83
|
+
(submitted: string) => {
|
|
84
|
+
const trimmed = submitted.trim();
|
|
85
|
+
if (trimmed.length === 0) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
onSubmit(trimmed);
|
|
89
|
+
|
|
90
|
+
// Add to history (avoid consecutive duplicates)
|
|
91
|
+
const h = history.current;
|
|
92
|
+
if (h.length === 0 || h[h.length - 1] !== trimmed) {
|
|
93
|
+
h.push(trimmed);
|
|
94
|
+
if (h.length > MAX_HISTORY) {
|
|
95
|
+
h.shift();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Reset history navigation
|
|
100
|
+
historyIndex.current = -1;
|
|
101
|
+
draft.current = '';
|
|
102
|
+
setValue('');
|
|
103
|
+
setSlashHint('');
|
|
104
|
+
},
|
|
105
|
+
[onSubmit]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Handle Escape, Up/Down arrows, Tab autocomplete, Ctrl+R
|
|
109
|
+
useInput(
|
|
110
|
+
(input, key) => {
|
|
111
|
+
// --- Ctrl+R: toggle search mode ---
|
|
112
|
+
if (input === 'r' && key.ctrl) {
|
|
113
|
+
if (!searchMode) {
|
|
114
|
+
setSearchMode(true);
|
|
115
|
+
setSearchQuery('');
|
|
116
|
+
setSearchResults([]);
|
|
117
|
+
} else {
|
|
118
|
+
setSearchMode(false);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Search mode key handling ---
|
|
124
|
+
if (searchMode) {
|
|
125
|
+
if (key.escape) {
|
|
126
|
+
setSearchMode(false);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (key.return) {
|
|
130
|
+
// Select top result
|
|
131
|
+
if (searchResults.length > 0) {
|
|
132
|
+
setValue(searchResults[0]);
|
|
133
|
+
}
|
|
134
|
+
setSearchMode(false);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Let the search TextInput handle other keys
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (key.escape && onAbort) {
|
|
142
|
+
onAbort();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Tab: autocomplete ---
|
|
147
|
+
if (key.tab) {
|
|
148
|
+
// @file completion
|
|
149
|
+
const atMatch = value.match(/@(\S*)$/);
|
|
150
|
+
if (atMatch && fileSuggestions.length > 0) {
|
|
151
|
+
const idx = fileSuggestionIndex.current % fileSuggestions.length;
|
|
152
|
+
const replacement = `${value.slice(0, value.length - atMatch[0].length)}@${fileSuggestions[idx]}`;
|
|
153
|
+
setValue(replacement);
|
|
154
|
+
fileSuggestionIndex.current = idx + 1;
|
|
155
|
+
setFileHint(`[${fileSuggestions.length} files, Tab to cycle]`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Slash command completion
|
|
160
|
+
if (value.startsWith('/')) {
|
|
161
|
+
const prefix = value.toLowerCase();
|
|
162
|
+
const matches = SLASH_COMMANDS.filter(cmd => cmd.startsWith(prefix));
|
|
163
|
+
if (matches.length === 0) {
|
|
164
|
+
setSlashHint('');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (matches.length === 1) {
|
|
168
|
+
setValue(`${matches[0]} `);
|
|
169
|
+
setSlashHint('');
|
|
170
|
+
lastSuggestions.current = [];
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Multiple matches: cycle through them
|
|
174
|
+
if (
|
|
175
|
+
lastSuggestions.current.length === matches.length &&
|
|
176
|
+
lastSuggestions.current.every((s, i) => s === matches[i])
|
|
177
|
+
) {
|
|
178
|
+
suggestionIndex.current = (suggestionIndex.current + 1) % matches.length;
|
|
179
|
+
} else {
|
|
180
|
+
lastSuggestions.current = matches;
|
|
181
|
+
suggestionIndex.current = 0;
|
|
182
|
+
}
|
|
183
|
+
setValue(matches[suggestionIndex.current]);
|
|
184
|
+
setSlashHint(`[${matches.length} matches, Tab to cycle]`);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const h = history.current;
|
|
190
|
+
if (h.length === 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (key.upArrow) {
|
|
195
|
+
if (historyIndex.current === -1) {
|
|
196
|
+
// Starting to browse: save current draft
|
|
197
|
+
draft.current = value;
|
|
198
|
+
historyIndex.current = h.length - 1;
|
|
199
|
+
} else if (historyIndex.current > 0) {
|
|
200
|
+
historyIndex.current--;
|
|
201
|
+
}
|
|
202
|
+
setValue(h[historyIndex.current]);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (key.downArrow) {
|
|
207
|
+
if (historyIndex.current === -1) {
|
|
208
|
+
return;
|
|
209
|
+
} // not browsing
|
|
210
|
+
|
|
211
|
+
if (historyIndex.current < h.length - 1) {
|
|
212
|
+
historyIndex.current++;
|
|
213
|
+
setValue(h[historyIndex.current]);
|
|
214
|
+
} else {
|
|
215
|
+
// Past the end of history: restore draft
|
|
216
|
+
historyIndex.current = -1;
|
|
217
|
+
setValue(draft.current);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{ isActive: !disabled }
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Count lines for multi-line paste indicator
|
|
226
|
+
const lineCount = value.split('\n').length;
|
|
227
|
+
const isMultiLine = lineCount > 1;
|
|
228
|
+
|
|
229
|
+
if (disabled) {
|
|
230
|
+
return (
|
|
231
|
+
<Box paddingX={1}>
|
|
232
|
+
<Text dimColor>{'> '}</Text>
|
|
233
|
+
<Text dimColor italic>
|
|
234
|
+
{placeholder ?? 'waiting...'}
|
|
235
|
+
</Text>
|
|
236
|
+
</Box>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- Search mode UI ---
|
|
241
|
+
if (searchMode) {
|
|
242
|
+
return (
|
|
243
|
+
<Box flexDirection="column" paddingX={1}>
|
|
244
|
+
<Box>
|
|
245
|
+
<Text color="yellow">{'(reverse-search): '}</Text>
|
|
246
|
+
<TextInput
|
|
247
|
+
value={searchQuery}
|
|
248
|
+
onChange={q => {
|
|
249
|
+
setSearchQuery(q);
|
|
250
|
+
if (q.length > 0) {
|
|
251
|
+
const results = history.current
|
|
252
|
+
.filter(entry => entry.toLowerCase().includes(q.toLowerCase()))
|
|
253
|
+
.reverse()
|
|
254
|
+
.slice(0, 10);
|
|
255
|
+
setSearchResults(results);
|
|
256
|
+
} else {
|
|
257
|
+
setSearchResults([]);
|
|
258
|
+
}
|
|
259
|
+
}}
|
|
260
|
+
onSubmit={() => {
|
|
261
|
+
if (searchResults.length > 0) {
|
|
262
|
+
setValue(searchResults[0]);
|
|
263
|
+
}
|
|
264
|
+
setSearchMode(false);
|
|
265
|
+
}}
|
|
266
|
+
placeholder="type to search history..."
|
|
267
|
+
/>
|
|
268
|
+
</Box>
|
|
269
|
+
{searchResults.length > 0 && (
|
|
270
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
271
|
+
{searchResults.slice(0, 5).map((result, i) => (
|
|
272
|
+
<Text key={i} dimColor={i > 0}>
|
|
273
|
+
{i === 0 ? '> ' : ' '}
|
|
274
|
+
{result.length > 80 ? `${result.slice(0, 77)}...` : result}
|
|
275
|
+
</Text>
|
|
276
|
+
))}
|
|
277
|
+
{searchResults.length > 5 && (
|
|
278
|
+
<Text dimColor italic>
|
|
279
|
+
{' '}
|
|
280
|
+
... {searchResults.length - 5} more
|
|
281
|
+
</Text>
|
|
282
|
+
)}
|
|
283
|
+
</Box>
|
|
284
|
+
)}
|
|
285
|
+
</Box>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// --- Normal input UI ---
|
|
290
|
+
return (
|
|
291
|
+
<Box paddingX={1}>
|
|
292
|
+
<Text bold color="green">
|
|
293
|
+
{'> '}
|
|
294
|
+
</Text>
|
|
295
|
+
<TextInput
|
|
296
|
+
value={value}
|
|
297
|
+
onChange={v => {
|
|
298
|
+
setValue(v);
|
|
299
|
+
// If user types while browsing history, exit history mode
|
|
300
|
+
if (historyIndex.current !== -1) {
|
|
301
|
+
historyIndex.current = -1;
|
|
302
|
+
}
|
|
303
|
+
// Reset slash autocomplete on any change
|
|
304
|
+
if (!v.startsWith('/')) {
|
|
305
|
+
setSlashHint('');
|
|
306
|
+
lastSuggestions.current = [];
|
|
307
|
+
}
|
|
308
|
+
// @file mention detection
|
|
309
|
+
const atMatch = v.match(/@(\S*)$/);
|
|
310
|
+
if (atMatch) {
|
|
311
|
+
const partial = atMatch[1];
|
|
312
|
+
try {
|
|
313
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
314
|
+
const fs = require('node:fs');
|
|
315
|
+
const cwd = process.cwd();
|
|
316
|
+
const entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
317
|
+
const matches = (entries as Array<{ name: string; isDirectory(): boolean }>)
|
|
318
|
+
.filter(
|
|
319
|
+
e =>
|
|
320
|
+
!e.name.startsWith('.') && e.name.toLowerCase().includes(partial.toLowerCase())
|
|
321
|
+
)
|
|
322
|
+
.map(e => (e.isDirectory() ? `${e.name}/` : e.name))
|
|
323
|
+
.slice(0, 10);
|
|
324
|
+
setFileSuggestions(matches);
|
|
325
|
+
fileSuggestionIndex.current = 0;
|
|
326
|
+
if (matches.length > 0) {
|
|
327
|
+
setFileHint(`[${matches.length} files, Tab to complete]`);
|
|
328
|
+
} else {
|
|
329
|
+
setFileHint('');
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
setFileSuggestions([]);
|
|
333
|
+
setFileHint('');
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
if (fileSuggestions.length > 0) {
|
|
337
|
+
setFileSuggestions([]);
|
|
338
|
+
setFileHint('');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}}
|
|
342
|
+
onSubmit={handleSubmit}
|
|
343
|
+
placeholder={placeholder ?? 'Type a message... (paste multi-line supported)'}
|
|
344
|
+
/>
|
|
345
|
+
{isMultiLine && <Text color="cyan">{` [${lineCount} lines]`}</Text>}
|
|
346
|
+
{slashHint && <Text dimColor>{` ${slashHint}`}</Text>}
|
|
347
|
+
{fileHint && <Text dimColor>{` ${fileHint}`}</Text>}
|
|
348
|
+
</Box>
|
|
349
|
+
);
|
|
350
|
+
}
|