@build-astron-co/nimbus 0.2.0 → 0.4.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/bin/nimbus +26 -10
- package/bin/nimbus.cmd +41 -0
- package/bin/nimbus.mjs +70 -0
- package/completions/nimbus.bash +38 -0
- package/completions/nimbus.fish +48 -0
- package/completions/nimbus.zsh +81 -0
- package/dist/src/agent/compaction-agent.js +215 -0
- package/dist/src/agent/context-manager.js +385 -0
- package/dist/src/agent/context.js +322 -0
- package/dist/src/agent/deploy-preview.js +395 -0
- package/dist/src/agent/expand-files.js +95 -0
- package/dist/src/agent/index.js +18 -0
- package/dist/src/agent/loop.js +1535 -0
- package/dist/src/agent/modes.js +347 -0
- package/dist/src/agent/permissions.js +396 -0
- package/dist/src/agent/subagents/base.js +67 -0
- package/dist/src/agent/subagents/cost.js +45 -0
- package/dist/src/agent/subagents/explore.js +36 -0
- package/dist/src/agent/subagents/general.js +41 -0
- package/dist/src/agent/subagents/index.js +88 -0
- package/dist/src/agent/subagents/infra.js +52 -0
- package/dist/src/agent/subagents/security.js +60 -0
- package/dist/src/agent/system-prompt.js +860 -0
- package/dist/src/app.js +152 -0
- package/dist/src/audit/activity-log.js +209 -0
- package/dist/src/audit/compliance-checker.js +419 -0
- package/dist/src/audit/cost-tracker.js +231 -0
- package/dist/src/audit/index.js +10 -0
- package/dist/src/audit/security-scanner.js +490 -0
- package/dist/src/auth/guard.js +64 -0
- package/dist/src/auth/index.js +19 -0
- package/dist/src/auth/keychain.js +79 -0
- package/dist/src/auth/oauth.js +389 -0
- package/dist/src/auth/providers.js +415 -0
- package/dist/src/auth/sso.js +87 -0
- package/dist/src/auth/store.js +424 -0
- package/dist/src/auth/types.js +5 -0
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/init.js +1048 -0
- package/dist/src/cli/openapi-spec.js +346 -0
- package/dist/src/cli/run.js +505 -0
- package/dist/src/cli/serve-auth.js +56 -0
- package/dist/src/cli/serve.js +432 -0
- package/dist/src/cli/web.js +50 -0
- package/dist/src/cli.js +1574 -0
- package/dist/src/clients/core-engine-client.js +156 -0
- package/dist/src/clients/enterprise-client.js +246 -0
- package/dist/src/clients/generator-client.js +219 -0
- package/dist/src/clients/git-client.js +367 -0
- package/dist/src/clients/github-client.js +229 -0
- package/dist/src/clients/helm-client.js +299 -0
- package/dist/src/clients/index.js +18 -0
- package/dist/src/clients/k8s-client.js +270 -0
- package/dist/src/clients/llm-client.js +119 -0
- package/dist/src/clients/rest-client.js +104 -0
- package/dist/src/clients/service-discovery.js +35 -0
- package/dist/src/clients/terraform-client.js +302 -0
- package/dist/src/clients/tools-client.js +1227 -0
- package/dist/src/clients/ws-client.js +93 -0
- package/dist/src/commands/alias.js +91 -0
- package/dist/src/commands/analyze/index.js +313 -0
- package/dist/src/commands/apply/helm.js +375 -0
- package/dist/src/commands/apply/index.js +176 -0
- package/dist/src/commands/apply/k8s.js +350 -0
- package/dist/src/commands/apply/terraform.js +465 -0
- package/dist/src/commands/ask.js +137 -0
- package/dist/src/commands/audit/index.js +322 -0
- package/dist/src/commands/auth-cloud.js +345 -0
- package/dist/src/commands/auth-list.js +112 -0
- package/dist/src/commands/auth-profile.js +104 -0
- package/dist/src/commands/auth-refresh.js +161 -0
- package/dist/src/commands/auth-status.js +122 -0
- package/dist/src/commands/aws/ec2.js +402 -0
- package/dist/src/commands/aws/iam.js +304 -0
- package/dist/src/commands/aws/index.js +108 -0
- package/dist/src/commands/aws/lambda.js +317 -0
- package/dist/src/commands/aws/rds.js +345 -0
- package/dist/src/commands/aws/s3.js +346 -0
- package/dist/src/commands/aws/vpc.js +302 -0
- package/dist/src/commands/aws-discover.js +413 -0
- package/dist/src/commands/aws-terraform.js +618 -0
- package/dist/src/commands/azure/aks.js +305 -0
- package/dist/src/commands/azure/functions.js +200 -0
- package/dist/src/commands/azure/index.js +93 -0
- package/dist/src/commands/azure/storage.js +378 -0
- package/dist/src/commands/azure/vm.js +291 -0
- package/dist/src/commands/billing/index.js +224 -0
- package/dist/src/commands/chat.js +259 -0
- package/dist/src/commands/completions.js +255 -0
- package/dist/src/commands/config.js +291 -0
- package/dist/src/commands/cost/cloud-cost-estimator.js +211 -0
- package/dist/src/commands/cost/estimator.js +73 -0
- package/dist/src/commands/cost/index.js +625 -0
- package/dist/src/commands/cost/parsers/terraform.js +234 -0
- package/dist/src/commands/cost/parsers/types.js +4 -0
- package/dist/src/commands/cost/pricing/aws.js +501 -0
- package/dist/src/commands/cost/pricing/azure.js +462 -0
- package/dist/src/commands/cost/pricing/gcp.js +359 -0
- package/dist/src/commands/cost/pricing/index.js +24 -0
- package/dist/src/commands/demo.js +196 -0
- package/dist/src/commands/deploy.js +215 -0
- package/dist/src/commands/doctor.js +1291 -0
- package/dist/src/commands/drift/index.js +674 -0
- package/dist/src/commands/explain.js +235 -0
- package/dist/src/commands/export.js +120 -0
- package/dist/src/commands/feedback.js +319 -0
- package/dist/src/commands/fix.js +263 -0
- package/dist/src/commands/fs/index.js +338 -0
- package/dist/src/commands/gcp/compute.js +266 -0
- package/dist/src/commands/gcp/functions.js +221 -0
- package/dist/src/commands/gcp/gke.js +357 -0
- package/dist/src/commands/gcp/iam.js +295 -0
- package/dist/src/commands/gcp/index.js +105 -0
- package/dist/src/commands/gcp/storage.js +232 -0
- package/dist/src/commands/generate-helm.js +1026 -0
- package/dist/src/commands/generate-k8s.js +1263 -0
- package/dist/src/commands/generate-terraform.js +1058 -0
- package/dist/src/commands/gh/index.js +663 -0
- package/dist/src/commands/git/index.js +1208 -0
- package/dist/src/commands/helm/index.js +985 -0
- package/dist/src/commands/help.js +639 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/src/commands/import.js +782 -0
- package/dist/src/commands/incident.js +144 -0
- package/dist/src/commands/index.js +109 -0
- package/dist/src/commands/init.js +955 -0
- package/dist/src/commands/k8s/index.js +979 -0
- package/dist/src/commands/login.js +588 -0
- package/dist/src/commands/logout.js +61 -0
- package/dist/src/commands/logs.js +160 -0
- package/dist/src/commands/onboarding.js +382 -0
- package/dist/src/commands/pipeline.js +153 -0
- package/dist/src/commands/plan/display.js +216 -0
- package/dist/src/commands/plan/index.js +525 -0
- package/dist/src/commands/plugin.js +325 -0
- package/dist/src/commands/preview.js +356 -0
- package/dist/src/commands/profile.js +297 -0
- package/dist/src/commands/questionnaire.js +1021 -0
- package/dist/src/commands/resume.js +35 -0
- package/dist/src/commands/rollback.js +259 -0
- package/dist/src/commands/rollout.js +74 -0
- package/dist/src/commands/runbook.js +307 -0
- package/dist/src/commands/schedule.js +202 -0
- package/dist/src/commands/status.js +213 -0
- package/dist/src/commands/team/index.js +309 -0
- package/dist/src/commands/team-context.js +200 -0
- package/dist/src/commands/template.js +204 -0
- package/dist/src/commands/tf/index.js +989 -0
- package/dist/src/commands/upgrade.js +515 -0
- package/dist/src/commands/usage/index.js +118 -0
- package/dist/src/commands/version.js +145 -0
- package/dist/src/commands/watch.js +127 -0
- package/dist/src/compat/index.js +2 -0
- package/dist/src/compat/runtime.js +10 -0
- package/dist/src/compat/sqlite.js +144 -0
- package/dist/src/config/index.js +6 -0
- package/dist/src/config/manager.js +469 -0
- package/dist/src/config/mode-store.js +57 -0
- package/dist/src/config/profiles.js +66 -0
- package/dist/src/config/safety-policy.js +251 -0
- package/dist/src/config/schema.js +107 -0
- package/dist/src/config/types.js +311 -0
- package/dist/src/config/workspace-state.js +38 -0
- package/dist/src/context/context-db.js +138 -0
- package/dist/src/demo/index.js +295 -0
- package/dist/src/demo/scenarios/full-journey.js +226 -0
- package/dist/src/demo/scenarios/getting-started.js +124 -0
- package/dist/src/demo/scenarios/helm-release.js +334 -0
- package/dist/src/demo/scenarios/k8s-deployment.js +190 -0
- package/dist/src/demo/scenarios/terraform-vpc.js +167 -0
- package/dist/src/demo/types.js +6 -0
- package/dist/src/engine/cost-estimator.js +334 -0
- package/dist/src/engine/diagram-generator.js +192 -0
- package/dist/src/engine/drift-detector.js +688 -0
- package/dist/src/engine/executor.js +832 -0
- package/dist/src/engine/index.js +39 -0
- package/dist/src/engine/orchestrator.js +436 -0
- package/dist/src/engine/planner.js +616 -0
- package/dist/src/engine/safety.js +609 -0
- package/dist/src/engine/verifier.js +664 -0
- package/dist/src/enterprise/audit.js +241 -0
- package/dist/src/enterprise/auth.js +189 -0
- package/dist/src/enterprise/billing.js +512 -0
- package/dist/src/enterprise/index.js +16 -0
- package/dist/src/enterprise/teams.js +315 -0
- package/dist/src/generator/best-practices.js +1375 -0
- package/dist/src/generator/helm.js +495 -0
- package/dist/src/generator/index.js +11 -0
- package/dist/src/generator/intent-parser.js +420 -0
- package/dist/src/generator/kubernetes.js +773 -0
- package/dist/src/generator/terraform.js +1472 -0
- package/dist/src/history/index.js +6 -0
- package/dist/src/history/manager.js +199 -0
- package/dist/src/history/types.js +6 -0
- package/dist/src/hooks/config.js +318 -0
- package/dist/src/hooks/engine.js +317 -0
- package/dist/src/hooks/index.js +2 -0
- package/dist/src/llm/auth-bridge.js +157 -0
- package/dist/src/llm/circuit-breaker.js +116 -0
- package/dist/src/llm/config-loader.js +172 -0
- package/dist/src/llm/cost-calculator.js +137 -0
- package/dist/src/llm/index.js +7 -0
- package/dist/src/llm/model-aliases.js +99 -0
- package/dist/src/llm/provider-registry.js +57 -0
- package/dist/src/llm/providers/anthropic.js +430 -0
- package/dist/src/llm/providers/bedrock.js +409 -0
- package/dist/src/llm/providers/google.js +344 -0
- package/dist/src/llm/providers/ollama.js +661 -0
- package/dist/src/llm/providers/openai-compatible.js +289 -0
- package/dist/src/llm/providers/openai.js +284 -0
- package/dist/src/llm/providers/openrouter.js +293 -0
- package/dist/src/llm/router.js +844 -0
- package/dist/src/llm/types.js +69 -0
- package/dist/src/lsp/client.js +239 -0
- package/dist/src/lsp/languages.js +95 -0
- package/dist/src/lsp/manager.js +243 -0
- package/dist/src/mcp/client.js +289 -0
- package/dist/src/mcp/index.js +5 -0
- package/dist/src/mcp/manager.js +113 -0
- package/dist/src/nimbus.js +212 -0
- package/dist/src/plugins/index.js +13 -0
- package/dist/src/plugins/loader.js +280 -0
- package/dist/src/plugins/manager.js +282 -0
- package/dist/src/plugins/types.js +23 -0
- package/dist/src/scanners/cicd-scanner.js +230 -0
- package/dist/src/scanners/cloud-scanner.js +415 -0
- package/dist/src/scanners/framework-scanner.js +430 -0
- package/dist/src/scanners/iac-scanner.js +350 -0
- package/dist/src/scanners/index.js +454 -0
- package/dist/src/scanners/language-scanner.js +258 -0
- package/dist/src/scanners/package-manager-scanner.js +252 -0
- package/dist/src/scanners/types.js +6 -0
- package/dist/src/sessions/manager.js +395 -0
- package/dist/src/sessions/types.js +4 -0
- package/dist/src/sharing/sync.js +238 -0
- package/dist/src/sharing/viewer.js +131 -0
- package/dist/src/snapshots/index.js +1 -0
- package/dist/src/snapshots/manager.js +432 -0
- package/dist/src/state/artifacts.js +94 -0
- package/dist/src/state/audit.js +73 -0
- package/dist/src/state/billing.js +126 -0
- package/dist/src/state/checkpoints.js +81 -0
- package/dist/src/state/config.js +58 -0
- package/dist/src/state/conversations.js +7 -0
- package/dist/src/state/credentials.js +96 -0
- package/dist/src/state/db.js +53 -0
- package/dist/src/state/index.js +23 -0
- package/dist/src/state/messages.js +76 -0
- package/dist/src/state/projects.js +92 -0
- package/dist/src/state/schema.js +233 -0
- package/dist/src/state/sessions.js +79 -0
- package/dist/src/state/teams.js +131 -0
- package/dist/src/telemetry.js +91 -0
- package/dist/src/tools/aws-ops.js +747 -0
- package/dist/src/tools/azure-ops.js +491 -0
- package/dist/src/tools/file-ops.js +451 -0
- package/dist/src/tools/gcp-ops.js +559 -0
- package/dist/src/tools/git-ops.js +557 -0
- package/dist/src/tools/github-ops.js +460 -0
- package/dist/src/tools/helm-ops.js +634 -0
- package/dist/src/tools/index.js +16 -0
- package/dist/src/tools/k8s-ops.js +579 -0
- package/dist/src/tools/schemas/converter.js +129 -0
- package/dist/src/tools/schemas/devops.js +3319 -0
- package/dist/src/tools/schemas/index.js +19 -0
- package/dist/src/tools/schemas/standard.js +966 -0
- package/dist/src/tools/schemas/types.js +409 -0
- package/dist/src/tools/spawn-exec.js +109 -0
- package/dist/src/tools/terraform-ops.js +627 -0
- package/dist/src/types/config.js +1 -0
- package/dist/src/types/drift.js +4 -0
- package/dist/src/types/enterprise.js +5 -0
- package/dist/src/types/index.js +14 -0
- package/dist/src/types/plan.js +1 -0
- package/dist/src/types/request.js +1 -0
- package/dist/src/types/response.js +1 -0
- package/dist/src/types/service.js +1 -0
- package/dist/src/ui/App.js +1672 -0
- package/dist/src/ui/DeployPreview.js +60 -0
- package/dist/src/ui/FileDiffModal.js +108 -0
- package/dist/src/ui/Header.js +46 -0
- package/dist/src/ui/HelpModal.js +9 -0
- package/dist/src/ui/InputBox.js +408 -0
- package/dist/src/ui/MessageList.js +795 -0
- package/dist/src/ui/PermissionPrompt.js +72 -0
- package/dist/src/ui/StatusBar.js +109 -0
- package/dist/src/ui/TerminalPane.js +31 -0
- package/dist/src/ui/ToolCallDisplay.js +303 -0
- package/dist/src/ui/TreePane.js +83 -0
- package/dist/src/ui/chat-ui.js +721 -0
- package/dist/src/ui/index.js +11 -0
- package/dist/src/ui/ink/index.js +1325 -0
- package/dist/src/ui/streaming.js +137 -0
- package/dist/src/ui/theme.js +78 -0
- package/dist/src/ui/types.js +7 -0
- package/dist/src/utils/analytics.js +61 -0
- package/dist/src/utils/cost-warning.js +25 -0
- package/dist/src/utils/env.js +42 -0
- package/dist/src/utils/errors.js +54 -0
- package/dist/src/utils/event-bus.js +22 -0
- package/dist/src/utils/index.js +16 -0
- package/dist/src/utils/logger.js +150 -0
- package/dist/src/utils/rate-limiter.js +90 -0
- package/dist/src/utils/service-auth.js +36 -0
- package/dist/src/utils/validation.js +39 -0
- package/dist/src/version.js +3 -0
- package/dist/src/watcher/index.js +192 -0
- package/dist/src/wizard/approval.js +275 -0
- package/dist/src/wizard/index.js +13 -0
- package/dist/src/wizard/prompts.js +273 -0
- package/dist/src/wizard/types.js +4 -0
- package/dist/src/wizard/ui.js +453 -0
- package/dist/src/wizard/wizard.js +227 -0
- package/package.json +31 -23
- package/src/__tests__/alias.test.ts +133 -0
- package/src/__tests__/app.test.ts +1 -1
- package/src/__tests__/audit.test.ts +1 -1
- package/src/__tests__/circuit-breaker.test.ts +1 -1
- package/src/__tests__/cli-run.test.ts +237 -1
- package/src/__tests__/compat-sqlite.test.ts +68 -0
- package/src/__tests__/context-manager.test.ts +131 -1
- package/src/__tests__/context.test.ts +1 -1
- package/src/__tests__/devops-terminal-gaps.test.ts +718 -0
- package/src/__tests__/doctor.test.ts +48 -0
- package/src/__tests__/enterprise.test.ts +1 -1
- package/src/__tests__/export.test.ts +236 -0
- package/src/__tests__/gap-11-18-20.test.ts +958 -0
- package/src/__tests__/generator.test.ts +1 -1
- package/src/__tests__/helm-streaming.test.ts +127 -0
- package/src/__tests__/hooks.test.ts +1 -1
- package/src/__tests__/incident.test.ts +179 -0
- package/src/__tests__/init.test.ts +55 -4
- package/src/__tests__/intent-parser.test.ts +1 -1
- package/src/__tests__/llm-router.test.ts +1 -1
- package/src/__tests__/logs.test.ts +107 -0
- package/src/__tests__/loop-errors.test.ts +244 -0
- package/src/__tests__/lsp.test.ts +1 -1
- package/src/__tests__/modes.test.ts +1 -1
- package/src/__tests__/perf-optimizations.test.ts +847 -0
- package/src/__tests__/permissions.test.ts +1 -1
- package/src/__tests__/pipeline.test.ts +50 -0
- package/src/__tests__/polish-phase3.test.ts +340 -0
- package/src/__tests__/profile.test.ts +237 -0
- package/src/__tests__/rollback.test.ts +83 -0
- package/src/__tests__/runbook.test.ts +219 -0
- package/src/__tests__/schedule.test.ts +206 -0
- package/src/__tests__/serve.test.ts +1 -1
- package/src/__tests__/sessions.test.ts +96 -1
- package/src/__tests__/sharing.test.ts +53 -1
- package/src/__tests__/snapshots.test.ts +1 -1
- package/src/__tests__/standalone-migration.test.ts +199 -0
- package/src/__tests__/state-db.test.ts +1 -1
- package/src/__tests__/status.test.ts +158 -0
- package/src/__tests__/stream-with-tools.test.ts +71 -25
- package/src/__tests__/subagents.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +82 -3
- package/src/__tests__/terminal-gap-v2.test.ts +395 -0
- package/src/__tests__/terminal-parity.test.ts +393 -0
- package/src/__tests__/tf-apply.test.ts +187 -0
- package/src/__tests__/tool-converter.test.ts +1 -1
- package/src/__tests__/tool-schemas.test.ts +209 -4
- package/src/__tests__/tools.test.ts +4 -3
- package/src/__tests__/version-json.test.ts +184 -0
- package/src/__tests__/version.test.ts +1 -1
- package/src/__tests__/watch.test.ts +129 -0
- package/src/agent/compaction-agent.ts +40 -1
- package/src/agent/context-manager.ts +67 -3
- package/src/agent/deploy-preview.ts +62 -1
- package/src/agent/expand-files.ts +108 -0
- package/src/agent/loop.ts +1312 -31
- package/src/agent/permissions.ts +51 -4
- package/src/agent/system-prompt.ts +573 -19
- package/src/app.ts +58 -0
- package/src/audit/security-scanner.ts +45 -0
- package/src/auth/keychain.ts +82 -0
- package/src/auth/oauth.ts +15 -5
- package/src/cli/init.ts +378 -5
- package/src/cli/run.ts +407 -16
- package/src/cli/serve.ts +78 -1
- package/src/cli/web.ts +10 -6
- package/src/cli.ts +312 -1
- package/src/clients/service-discovery.ts +30 -25
- package/src/commands/alias.ts +100 -0
- package/src/commands/audit/index.ts +121 -2
- package/src/commands/auth-cloud.ts +113 -0
- package/src/commands/auth-refresh.ts +187 -0
- package/src/commands/aws-discover.ts +144 -251
- package/src/commands/aws-terraform.ts +68 -118
- package/src/commands/chat.ts +9 -3
- package/src/commands/completions.ts +268 -0
- package/src/commands/config.ts +26 -0
- package/src/commands/cost/index.ts +218 -2
- package/src/commands/deploy.ts +260 -0
- package/src/commands/doctor.ts +744 -152
- package/src/commands/drift/index.ts +371 -23
- package/src/commands/export.ts +146 -0
- package/src/commands/generate-k8s.ts +9 -61
- package/src/commands/generate-terraform.ts +191 -449
- package/src/commands/help.ts +212 -36
- package/src/commands/history.ts +8 -1
- package/src/commands/incident.ts +166 -0
- package/src/commands/init.ts +5 -0
- package/src/commands/login.ts +86 -1
- package/src/commands/logs.ts +167 -0
- package/src/commands/onboarding.ts +211 -34
- package/src/commands/pipeline.ts +186 -0
- package/src/commands/plugin.ts +398 -0
- package/src/commands/profile.ts +342 -0
- package/src/commands/questionnaire.ts +0 -98
- package/src/commands/resume.ts +26 -34
- package/src/commands/rollback.ts +315 -0
- package/src/commands/rollout.ts +88 -0
- package/src/commands/runbook.ts +346 -0
- package/src/commands/schedule.ts +236 -0
- package/src/commands/status.ts +252 -0
- package/src/commands/team-context.ts +220 -0
- package/src/commands/template.ts +58 -57
- package/src/commands/tf/index.ts +70 -11
- package/src/commands/upgrade.ts +57 -0
- package/src/commands/version.ts +54 -50
- package/src/commands/watch.ts +153 -0
- package/src/compat/runtime.ts +1 -1
- package/src/compat/sqlite.ts +75 -5
- package/src/config/mode-store.ts +62 -0
- package/src/config/profiles.ts +84 -0
- package/src/config/types.ts +83 -1
- package/src/config/workspace-state.ts +53 -0
- package/src/engine/cost-estimator.ts +52 -10
- package/src/engine/executor.ts +33 -2
- package/src/engine/planner.ts +68 -1
- package/src/generator/terraform.ts +8 -0
- package/src/history/manager.ts +2 -74
- package/src/hooks/engine.ts +5 -4
- package/src/llm/cost-calculator.ts +2 -2
- package/src/llm/providers/anthropic.ts +50 -21
- package/src/llm/router.ts +76 -7
- package/src/lsp/languages.ts +3 -0
- package/src/lsp/manager.ts +21 -5
- package/src/nimbus.ts +37 -18
- package/src/sessions/manager.ts +108 -1
- package/src/sharing/sync.ts +4 -0
- package/src/sharing/viewer.ts +66 -0
- package/src/tools/file-ops.ts +22 -0
- package/src/tools/schemas/devops.ts +3007 -117
- package/src/tools/schemas/standard.ts +5 -1
- package/src/tools/schemas/types.ts +31 -1
- package/src/tools/spawn-exec.ts +148 -0
- package/src/ui/App.tsx +1183 -66
- package/src/ui/DeployPreview.tsx +62 -57
- package/src/ui/FileDiffModal.tsx +162 -0
- package/src/ui/Header.tsx +87 -24
- package/src/ui/HelpModal.tsx +57 -0
- package/src/ui/InputBox.tsx +163 -10
- package/src/ui/MessageList.tsx +487 -40
- package/src/ui/PermissionPrompt.tsx +17 -5
- package/src/ui/StatusBar.tsx +122 -3
- package/src/ui/TerminalPane.tsx +84 -0
- package/src/ui/ToolCallDisplay.tsx +252 -18
- package/src/ui/TreePane.tsx +132 -0
- package/src/ui/chat-ui.ts +41 -44
- package/src/ui/ink/index.ts +771 -38
- package/src/ui/streaming.ts +1 -1
- package/src/ui/theme.ts +104 -0
- package/src/ui/types.ts +18 -0
- package/src/version.ts +1 -1
- package/src/watcher/index.ts +66 -15
- package/src/wizard/types.ts +1 -0
- package/src/wizard/ui.ts +1 -1
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Initialization & Auto-Detection
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds a new Nimbus project by detecting the existing project type,
|
|
5
|
+
* infrastructure tooling, cloud providers, and development conventions.
|
|
6
|
+
* Generates a NIMBUS.md file and .nimbus/ configuration directory.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* nimbus init
|
|
10
|
+
* nimbus init --force # overwrite existing NIMBUS.md
|
|
11
|
+
* nimbus init --quiet # suppress console output
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import { exec } from 'node:child_process';
|
|
16
|
+
import { promisify } from 'node:util';
|
|
17
|
+
const _execAsync = promisify(exec);
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Internal helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Check whether a file or directory exists at `filePath`.
|
|
23
|
+
* Swallows all errors and returns `false` on failure.
|
|
24
|
+
*/
|
|
25
|
+
function exists(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.existsSync(filePath);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* List immediate children of `dir`, returning an empty array when the
|
|
35
|
+
* directory does not exist or is unreadable.
|
|
36
|
+
*/
|
|
37
|
+
function listDir(dir) {
|
|
38
|
+
try {
|
|
39
|
+
return fs.readdirSync(dir);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read a file as UTF-8 text. Returns an empty string on failure.
|
|
47
|
+
*/
|
|
48
|
+
function readText(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Recursively collect file names matching a predicate.
|
|
58
|
+
* Searches at most `maxDepth` levels deep and stops after `limit` matches.
|
|
59
|
+
*/
|
|
60
|
+
function findFiles(dir, predicate, maxDepth = 3, limit = 50) {
|
|
61
|
+
const results = [];
|
|
62
|
+
function walk(current, depth) {
|
|
63
|
+
if (depth > maxDepth || results.length >= limit) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const entry of listDir(current)) {
|
|
67
|
+
if (results.length >= limit) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Skip heavy directories that would slow detection
|
|
71
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist' || entry === 'vendor') {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const full = path.join(current, entry);
|
|
75
|
+
try {
|
|
76
|
+
const stat = fs.statSync(full);
|
|
77
|
+
if (stat.isDirectory()) {
|
|
78
|
+
walk(full, depth + 1);
|
|
79
|
+
}
|
|
80
|
+
else if (predicate(entry)) {
|
|
81
|
+
results.push(full);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Permission or broken symlink -- skip
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
walk(dir, 0);
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Detection functions
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
/**
|
|
96
|
+
* Detect the primary project type from marker files in `dir`.
|
|
97
|
+
*
|
|
98
|
+
* Priority order: TypeScript > JavaScript > Go > Python > Rust > Java > unknown
|
|
99
|
+
*/
|
|
100
|
+
export function detectProjectType(dir) {
|
|
101
|
+
try {
|
|
102
|
+
if (exists(path.join(dir, 'tsconfig.json'))) {
|
|
103
|
+
return 'typescript';
|
|
104
|
+
}
|
|
105
|
+
if (exists(path.join(dir, 'package.json'))) {
|
|
106
|
+
return 'javascript';
|
|
107
|
+
}
|
|
108
|
+
if (exists(path.join(dir, 'go.mod'))) {
|
|
109
|
+
return 'go';
|
|
110
|
+
}
|
|
111
|
+
if (exists(path.join(dir, 'pyproject.toml')) ||
|
|
112
|
+
exists(path.join(dir, 'setup.py')) ||
|
|
113
|
+
exists(path.join(dir, 'requirements.txt'))) {
|
|
114
|
+
return 'python';
|
|
115
|
+
}
|
|
116
|
+
if (exists(path.join(dir, 'Cargo.toml'))) {
|
|
117
|
+
return 'rust';
|
|
118
|
+
}
|
|
119
|
+
if (exists(path.join(dir, 'pom.xml')) ||
|
|
120
|
+
exists(path.join(dir, 'build.gradle')) ||
|
|
121
|
+
exists(path.join(dir, 'build.gradle.kts'))) {
|
|
122
|
+
return 'java';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Fall through to unknown
|
|
127
|
+
}
|
|
128
|
+
return 'unknown';
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Detect which infrastructure tools are present in `dir`.
|
|
132
|
+
*
|
|
133
|
+
* Scans for Terraform files, Kubernetes manifests, Helm charts,
|
|
134
|
+
* Docker files, and CI/CD configuration.
|
|
135
|
+
*/
|
|
136
|
+
export function detectInfrastructure(dir) {
|
|
137
|
+
const found = new Set();
|
|
138
|
+
try {
|
|
139
|
+
// Terraform -- look for any .tf files
|
|
140
|
+
const tfFiles = findFiles(dir, name => name.endsWith('.tf'), 3, 5);
|
|
141
|
+
if (tfFiles.length > 0) {
|
|
142
|
+
found.add('terraform');
|
|
143
|
+
}
|
|
144
|
+
// Kubernetes -- look for YAML files containing common K8s markers
|
|
145
|
+
const yamlFiles = findFiles(dir, name => name.endsWith('.yaml') || name.endsWith('.yml'), 3, 30);
|
|
146
|
+
for (const yamlFile of yamlFiles) {
|
|
147
|
+
const content = readText(yamlFile);
|
|
148
|
+
if (content.includes('kind: Deployment') ||
|
|
149
|
+
content.includes('kind: Service') ||
|
|
150
|
+
content.includes('apiVersion:')) {
|
|
151
|
+
found.add('kubernetes');
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Helm
|
|
156
|
+
if (findFiles(dir, name => name === 'Chart.yaml', 3, 1).length > 0) {
|
|
157
|
+
found.add('helm');
|
|
158
|
+
}
|
|
159
|
+
// Docker
|
|
160
|
+
const entries = listDir(dir);
|
|
161
|
+
if (entries.some(e => e === 'Dockerfile' || e === 'docker-compose.yml' || e === 'docker-compose.yaml')) {
|
|
162
|
+
found.add('docker');
|
|
163
|
+
}
|
|
164
|
+
// Also check for a docker/ directory with Dockerfiles
|
|
165
|
+
if (exists(path.join(dir, 'docker'))) {
|
|
166
|
+
const dockerDir = listDir(path.join(dir, 'docker'));
|
|
167
|
+
if (dockerDir.some(e => e.startsWith('Dockerfile') || e.endsWith('.yml') || e.endsWith('.yaml'))) {
|
|
168
|
+
found.add('docker');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// CI/CD
|
|
172
|
+
if (exists(path.join(dir, '.github', 'workflows')) ||
|
|
173
|
+
exists(path.join(dir, '.gitlab-ci.yml')) ||
|
|
174
|
+
exists(path.join(dir, 'Jenkinsfile')) ||
|
|
175
|
+
exists(path.join(dir, '.circleci'))) {
|
|
176
|
+
found.add('cicd');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Return whatever we collected so far
|
|
181
|
+
}
|
|
182
|
+
return Array.from(found);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Detect cloud providers referenced in Terraform files or local credentials.
|
|
186
|
+
*
|
|
187
|
+
* Checks both `.tf` file contents and well-known credential locations
|
|
188
|
+
* or environment variables.
|
|
189
|
+
*/
|
|
190
|
+
export function detectCloudProviders(dir) {
|
|
191
|
+
const found = new Set();
|
|
192
|
+
try {
|
|
193
|
+
// --- Scan Terraform files for provider blocks ---
|
|
194
|
+
const tfFiles = findFiles(dir, name => name.endsWith('.tf'), 3, 20);
|
|
195
|
+
for (const tfFile of tfFiles) {
|
|
196
|
+
const content = readText(tfFile);
|
|
197
|
+
if (content.includes('provider "aws"') || content.includes("provider 'aws'")) {
|
|
198
|
+
found.add('aws');
|
|
199
|
+
}
|
|
200
|
+
if (content.includes('provider "google"') || content.includes("provider 'google'")) {
|
|
201
|
+
found.add('gcp');
|
|
202
|
+
}
|
|
203
|
+
if (content.includes('provider "azurerm"') || content.includes("provider 'azurerm'")) {
|
|
204
|
+
found.add('azure');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// --- Check environment variables ---
|
|
208
|
+
if (process.env['AWS_ACCESS_KEY_ID'] || process.env['AWS_PROFILE']) {
|
|
209
|
+
found.add('aws');
|
|
210
|
+
}
|
|
211
|
+
if (process.env['GOOGLE_APPLICATION_CREDENTIALS'] || process.env['GCLOUD_PROJECT']) {
|
|
212
|
+
found.add('gcp');
|
|
213
|
+
}
|
|
214
|
+
if (process.env['AZURE_SUBSCRIPTION_ID'] || process.env['ARM_SUBSCRIPTION_ID']) {
|
|
215
|
+
found.add('azure');
|
|
216
|
+
}
|
|
217
|
+
// --- Check local credential files ---
|
|
218
|
+
const home = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
|
|
219
|
+
if (home) {
|
|
220
|
+
if (exists(path.join(home, '.aws', 'credentials')) ||
|
|
221
|
+
exists(path.join(home, '.aws', 'config'))) {
|
|
222
|
+
found.add('aws');
|
|
223
|
+
}
|
|
224
|
+
if (exists(path.join(home, '.config', 'gcloud'))) {
|
|
225
|
+
found.add('gcp');
|
|
226
|
+
}
|
|
227
|
+
if (exists(path.join(home, '.azure'))) {
|
|
228
|
+
found.add('azure');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Return whatever we collected
|
|
234
|
+
}
|
|
235
|
+
return Array.from(found);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Discover live infrastructure context by querying CLI tools.
|
|
239
|
+
* All queries are best-effort with short timeouts.
|
|
240
|
+
*/
|
|
241
|
+
export async function discoverInfraContext(dir) {
|
|
242
|
+
const { execFileSync } = await import('node:child_process');
|
|
243
|
+
const ctx = {};
|
|
244
|
+
// Terraform workspace (only if .terraform exists)
|
|
245
|
+
if (exists(path.join(dir, '.terraform'))) {
|
|
246
|
+
try {
|
|
247
|
+
ctx.terraformWorkspace = execFileSync('terraform', ['workspace', 'show'], {
|
|
248
|
+
cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
249
|
+
}).trim();
|
|
250
|
+
}
|
|
251
|
+
catch { /* ignore */ }
|
|
252
|
+
}
|
|
253
|
+
// kubectl context
|
|
254
|
+
try {
|
|
255
|
+
ctx.kubectlContext = execFileSync('kubectl', ['config', 'current-context'], {
|
|
256
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
257
|
+
}).trim();
|
|
258
|
+
const clustersOut = execFileSync('kubectl', ['config', 'get-clusters'], {
|
|
259
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
260
|
+
});
|
|
261
|
+
ctx.kubectlClusters = clustersOut.split('\n').slice(1).map(s => s.trim()).filter(Boolean);
|
|
262
|
+
}
|
|
263
|
+
catch { /* ignore */ }
|
|
264
|
+
// Helm releases
|
|
265
|
+
try {
|
|
266
|
+
const helmOut = execFileSync('helm', ['list', '-A', '--output=json'], {
|
|
267
|
+
encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
268
|
+
});
|
|
269
|
+
const releases = JSON.parse(helmOut || '[]');
|
|
270
|
+
ctx.helmReleases = releases.map(r => `${r.name} (${r.namespace})`);
|
|
271
|
+
}
|
|
272
|
+
catch { /* ignore */ }
|
|
273
|
+
// AWS account + region
|
|
274
|
+
try {
|
|
275
|
+
const awsIdOut = execFileSync('aws', ['sts', 'get-caller-identity', '--output=json'], {
|
|
276
|
+
encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
277
|
+
});
|
|
278
|
+
const awsId = JSON.parse(awsIdOut);
|
|
279
|
+
ctx.awsAccount = awsId.Account;
|
|
280
|
+
}
|
|
281
|
+
catch { /* ignore */ }
|
|
282
|
+
try {
|
|
283
|
+
ctx.awsRegion = execFileSync('aws', ['configure', 'get', 'region'], {
|
|
284
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
285
|
+
}).trim() || process.env.AWS_DEFAULT_REGION;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
ctx.awsRegion = process.env.AWS_DEFAULT_REGION;
|
|
289
|
+
}
|
|
290
|
+
// G13: Discover all AWS profiles from ~/.aws/config
|
|
291
|
+
try {
|
|
292
|
+
const { readFileSync } = await import('node:fs');
|
|
293
|
+
const { homedir } = await import('node:os');
|
|
294
|
+
const awsConfigPath = path.join(homedir(), '.aws', 'config');
|
|
295
|
+
if (exists(awsConfigPath)) {
|
|
296
|
+
const awsConfig = readFileSync(awsConfigPath, 'utf-8');
|
|
297
|
+
const profiles = [];
|
|
298
|
+
const profileRegex = /^\[(?:profile\s+)?(\S+)\]/gm;
|
|
299
|
+
let match;
|
|
300
|
+
while ((match = profileRegex.exec(awsConfig)) !== null) {
|
|
301
|
+
const profileName = match[1];
|
|
302
|
+
if (profileName === 'default')
|
|
303
|
+
continue; // handled separately
|
|
304
|
+
// Extract region from the profile block
|
|
305
|
+
const blockStart = match.index + match[0].length;
|
|
306
|
+
const nextBlock = awsConfig.indexOf('\n[', blockStart);
|
|
307
|
+
const block = awsConfig.slice(blockStart, nextBlock === -1 ? undefined : nextBlock);
|
|
308
|
+
const regionMatch = block.match(/^\s*region\s*=\s*(.+)$/m);
|
|
309
|
+
profiles.push({ profile: profileName, region: regionMatch?.[1]?.trim() });
|
|
310
|
+
}
|
|
311
|
+
if (profiles.length > 0)
|
|
312
|
+
ctx.awsProfiles = profiles.slice(0, 10); // max 10 profiles
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch { /* ignore */ }
|
|
316
|
+
// GCP project
|
|
317
|
+
try {
|
|
318
|
+
const proj = execFileSync('gcloud', ['config', 'get-value', 'project'], {
|
|
319
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
320
|
+
}).trim();
|
|
321
|
+
if (proj && proj !== '(unset)')
|
|
322
|
+
ctx.gcpProject = proj;
|
|
323
|
+
}
|
|
324
|
+
catch { /* ignore */ }
|
|
325
|
+
// G16: Count K8s namespaces and deployments
|
|
326
|
+
if (ctx.kubectlContext) {
|
|
327
|
+
try {
|
|
328
|
+
const nsOut = execFileSync('kubectl', ['get', 'namespaces', '--no-headers'], {
|
|
329
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
330
|
+
});
|
|
331
|
+
const nsCount = nsOut.trim().split('\n').filter(Boolean).length;
|
|
332
|
+
ctx.k8sNamespaceCount = nsCount;
|
|
333
|
+
}
|
|
334
|
+
catch { /* ignore */ }
|
|
335
|
+
try {
|
|
336
|
+
const deplOut = execFileSync('kubectl', ['get', 'deployments', '-A', '--no-headers'], {
|
|
337
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
338
|
+
});
|
|
339
|
+
const deplCount = deplOut.trim().split('\n').filter(Boolean).length;
|
|
340
|
+
ctx.k8sDeploymentCount = deplCount;
|
|
341
|
+
}
|
|
342
|
+
catch { /* ignore */ }
|
|
343
|
+
}
|
|
344
|
+
// CI/CD pipeline detection (G9)
|
|
345
|
+
const cicdFiles = [
|
|
346
|
+
['.github/workflows', 'GitHub Actions'],
|
|
347
|
+
['.gitlab-ci.yml', 'GitLab CI'],
|
|
348
|
+
['.circleci', 'CircleCI'],
|
|
349
|
+
['Jenkinsfile', 'Jenkins'],
|
|
350
|
+
['.buildkite', 'Buildkite'],
|
|
351
|
+
['azure-pipelines.yml', 'Azure Pipelines'],
|
|
352
|
+
];
|
|
353
|
+
for (const [file, name] of cicdFiles) {
|
|
354
|
+
if (exists(path.join(dir, file))) {
|
|
355
|
+
ctx.cicdPipeline = name;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return ctx;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Format an InfraContext into a compact single-line summary for injection
|
|
363
|
+
* into agent messages (Gaps 7 & 10).
|
|
364
|
+
*/
|
|
365
|
+
export function formatInfraContext(ctx) {
|
|
366
|
+
const parts = [];
|
|
367
|
+
if (ctx.terraformWorkspace)
|
|
368
|
+
parts.push(`tf-workspace: ${ctx.terraformWorkspace}`);
|
|
369
|
+
if (ctx.kubectlContext)
|
|
370
|
+
parts.push(`k8s-context: ${ctx.kubectlContext}`);
|
|
371
|
+
if (ctx.helmReleases && ctx.helmReleases.length > 0) {
|
|
372
|
+
parts.push(`helm-releases: ${ctx.helmReleases.slice(0, 3).join(', ')}${ctx.helmReleases.length > 3 ? ` +${ctx.helmReleases.length - 3} more` : ''}`);
|
|
373
|
+
}
|
|
374
|
+
if (ctx.awsAccount)
|
|
375
|
+
parts.push(`aws-account: ${ctx.awsAccount}`);
|
|
376
|
+
if (ctx.awsRegion)
|
|
377
|
+
parts.push(`aws-region: ${ctx.awsRegion}`);
|
|
378
|
+
if (ctx.gcpProject)
|
|
379
|
+
parts.push(`gcp-project: ${ctx.gcpProject}`);
|
|
380
|
+
if (ctx.cicdPipeline)
|
|
381
|
+
parts.push(`cicd: ${ctx.cicdPipeline}`);
|
|
382
|
+
return parts.length > 0 ? parts.join(' | ') : 'no infra context detected';
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Detect which Node.js package manager is used in `dir`.
|
|
386
|
+
*
|
|
387
|
+
* Lock-file priority: bun > yarn > pnpm > npm.
|
|
388
|
+
* Returns `undefined` when no lock file is found.
|
|
389
|
+
*/
|
|
390
|
+
export function detectPackageManager(dir) {
|
|
391
|
+
try {
|
|
392
|
+
if (exists(path.join(dir, 'bun.lock')) || exists(path.join(dir, 'bun.lockb'))) {
|
|
393
|
+
return 'bun';
|
|
394
|
+
}
|
|
395
|
+
if (exists(path.join(dir, 'yarn.lock'))) {
|
|
396
|
+
return 'yarn';
|
|
397
|
+
}
|
|
398
|
+
if (exists(path.join(dir, 'pnpm-lock.yaml'))) {
|
|
399
|
+
return 'pnpm';
|
|
400
|
+
}
|
|
401
|
+
if (exists(path.join(dir, 'package-lock.json'))) {
|
|
402
|
+
return 'npm';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// fall through
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Detect the test framework from `package.json` dependencies or
|
|
412
|
+
* lock-file presence.
|
|
413
|
+
*
|
|
414
|
+
* Returns the framework name as a human-readable string, or `undefined`
|
|
415
|
+
* if none is detected.
|
|
416
|
+
*/
|
|
417
|
+
export function detectTestFramework(dir) {
|
|
418
|
+
try {
|
|
419
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
420
|
+
if (!exists(pkgPath)) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
const pkg = JSON.parse(readText(pkgPath));
|
|
424
|
+
const allDeps = {
|
|
425
|
+
...pkg['dependencies'],
|
|
426
|
+
...pkg['devDependencies'],
|
|
427
|
+
};
|
|
428
|
+
if ('vitest' in allDeps) {
|
|
429
|
+
return 'vitest';
|
|
430
|
+
}
|
|
431
|
+
if ('jest' in allDeps) {
|
|
432
|
+
return 'jest';
|
|
433
|
+
}
|
|
434
|
+
if ('mocha' in allDeps) {
|
|
435
|
+
return 'mocha';
|
|
436
|
+
}
|
|
437
|
+
if ('@playwright/test' in allDeps) {
|
|
438
|
+
return 'playwright';
|
|
439
|
+
}
|
|
440
|
+
// Bun ships its own test runner -- detect via lock file
|
|
441
|
+
if (exists(path.join(dir, 'bun.lock')) || exists(path.join(dir, 'bun.lockb'))) {
|
|
442
|
+
return 'bun:test';
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// fall through
|
|
447
|
+
}
|
|
448
|
+
// Non-JS projects
|
|
449
|
+
if (exists(path.join(dir, 'go.mod'))) {
|
|
450
|
+
return 'go test';
|
|
451
|
+
}
|
|
452
|
+
if (exists(path.join(dir, 'Cargo.toml'))) {
|
|
453
|
+
return 'cargo test';
|
|
454
|
+
}
|
|
455
|
+
if (exists(path.join(dir, 'pyproject.toml')) || exists(path.join(dir, 'setup.py'))) {
|
|
456
|
+
const pyproject = readText(path.join(dir, 'pyproject.toml'));
|
|
457
|
+
if (pyproject.includes('pytest')) {
|
|
458
|
+
return 'pytest';
|
|
459
|
+
}
|
|
460
|
+
if (exists(path.join(dir, 'pytest.ini')) || exists(path.join(dir, 'setup.cfg'))) {
|
|
461
|
+
return 'pytest';
|
|
462
|
+
}
|
|
463
|
+
return 'unittest';
|
|
464
|
+
}
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Detect the linter used in the project.
|
|
469
|
+
*
|
|
470
|
+
* Checks for ESLint, Biome, golangci-lint, Ruff, and Clippy configuration.
|
|
471
|
+
*/
|
|
472
|
+
export function detectLinter(dir) {
|
|
473
|
+
try {
|
|
474
|
+
const entries = listDir(dir);
|
|
475
|
+
// ESLint (various config formats)
|
|
476
|
+
if (entries.some(e => e.startsWith('.eslintrc') || e.startsWith('eslint.config'))) {
|
|
477
|
+
return 'eslint';
|
|
478
|
+
}
|
|
479
|
+
// Biome
|
|
480
|
+
if (entries.includes('biome.json') || entries.includes('biome.jsonc')) {
|
|
481
|
+
return 'biome';
|
|
482
|
+
}
|
|
483
|
+
// Go -- golangci-lint
|
|
484
|
+
if (entries.includes('.golangci.yml') || entries.includes('.golangci.yaml')) {
|
|
485
|
+
return 'golangci-lint';
|
|
486
|
+
}
|
|
487
|
+
// Python -- ruff
|
|
488
|
+
if (entries.includes('ruff.toml') || entries.includes('.ruff.toml')) {
|
|
489
|
+
return 'ruff';
|
|
490
|
+
}
|
|
491
|
+
// Check pyproject.toml for ruff or flake8
|
|
492
|
+
if (exists(path.join(dir, 'pyproject.toml'))) {
|
|
493
|
+
const pyproject = readText(path.join(dir, 'pyproject.toml'));
|
|
494
|
+
if (pyproject.includes('[tool.ruff]')) {
|
|
495
|
+
return 'ruff';
|
|
496
|
+
}
|
|
497
|
+
if (pyproject.includes('[tool.flake8]')) {
|
|
498
|
+
return 'flake8';
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// Rust -- clippy is part of the toolchain, detect via Cargo.toml
|
|
502
|
+
if (exists(path.join(dir, 'Cargo.toml'))) {
|
|
503
|
+
return 'clippy';
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// fall through
|
|
508
|
+
}
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Detect the code formatter used in the project.
|
|
513
|
+
*
|
|
514
|
+
* Checks for Prettier, Biome, gofmt, rustfmt, and Black configuration.
|
|
515
|
+
*/
|
|
516
|
+
export function detectFormatter(dir) {
|
|
517
|
+
try {
|
|
518
|
+
const entries = listDir(dir);
|
|
519
|
+
// Prettier
|
|
520
|
+
if (entries.some(e => e.startsWith('.prettierrc') || e.startsWith('prettier.config'))) {
|
|
521
|
+
return 'prettier';
|
|
522
|
+
}
|
|
523
|
+
// Biome doubles as formatter
|
|
524
|
+
if (entries.includes('biome.json') || entries.includes('biome.jsonc')) {
|
|
525
|
+
return 'biome';
|
|
526
|
+
}
|
|
527
|
+
// Go -- gofmt is built-in
|
|
528
|
+
if (exists(path.join(dir, 'go.mod'))) {
|
|
529
|
+
return 'gofmt';
|
|
530
|
+
}
|
|
531
|
+
// Rust -- rustfmt
|
|
532
|
+
if (exists(path.join(dir, 'rustfmt.toml')) || exists(path.join(dir, '.rustfmt.toml'))) {
|
|
533
|
+
return 'rustfmt';
|
|
534
|
+
}
|
|
535
|
+
if (exists(path.join(dir, 'Cargo.toml'))) {
|
|
536
|
+
return 'rustfmt';
|
|
537
|
+
}
|
|
538
|
+
// Python -- black / ruff format
|
|
539
|
+
if (exists(path.join(dir, 'pyproject.toml'))) {
|
|
540
|
+
const pyproject = readText(path.join(dir, 'pyproject.toml'));
|
|
541
|
+
if (pyproject.includes('[tool.black]')) {
|
|
542
|
+
return 'black';
|
|
543
|
+
}
|
|
544
|
+
if (pyproject.includes('[tool.ruff]')) {
|
|
545
|
+
return 'ruff';
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// fall through
|
|
551
|
+
}
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Run the full project detection pipeline on `dir`.
|
|
556
|
+
*
|
|
557
|
+
* Aggregates results from all individual detection functions into a
|
|
558
|
+
* single {@link ProjectDetection} object.
|
|
559
|
+
*/
|
|
560
|
+
export function detectProject(dir) {
|
|
561
|
+
const resolvedDir = path.resolve(dir);
|
|
562
|
+
return {
|
|
563
|
+
projectName: path.basename(resolvedDir),
|
|
564
|
+
projectType: detectProjectType(resolvedDir),
|
|
565
|
+
infraTypes: detectInfrastructure(resolvedDir),
|
|
566
|
+
cloudProviders: detectCloudProviders(resolvedDir),
|
|
567
|
+
hasGit: exists(path.join(resolvedDir, '.git')),
|
|
568
|
+
packageManager: detectPackageManager(resolvedDir),
|
|
569
|
+
testFramework: detectTestFramework(resolvedDir),
|
|
570
|
+
linter: detectLinter(resolvedDir),
|
|
571
|
+
formatter: detectFormatter(resolvedDir),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
// Generation
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
/**
|
|
578
|
+
* Produce the contents of a `NIMBUS.md` file from detection results.
|
|
579
|
+
*
|
|
580
|
+
* The generated markdown serves as both human-readable documentation
|
|
581
|
+
* and machine-readable project metadata for the Nimbus agent.
|
|
582
|
+
*/
|
|
583
|
+
export function generateNimbusMd(detection, _dir, infraCtx) {
|
|
584
|
+
const lines = [];
|
|
585
|
+
// --- Header ---
|
|
586
|
+
lines.push(`# ${detection.projectName}`);
|
|
587
|
+
lines.push('');
|
|
588
|
+
lines.push('> Auto-generated by `nimbus init`. Edit freely to refine agent behaviour.');
|
|
589
|
+
lines.push('');
|
|
590
|
+
// --- Project Overview ---
|
|
591
|
+
lines.push('## Project Overview');
|
|
592
|
+
lines.push('');
|
|
593
|
+
lines.push(`- **Type:** ${detection.projectType}`);
|
|
594
|
+
if (detection.packageManager) {
|
|
595
|
+
lines.push(`- **Package Manager:** ${detection.packageManager}`);
|
|
596
|
+
}
|
|
597
|
+
if (detection.testFramework) {
|
|
598
|
+
lines.push(`- **Test Framework:** ${detection.testFramework}`);
|
|
599
|
+
}
|
|
600
|
+
if (detection.hasGit) {
|
|
601
|
+
lines.push('- **Version Control:** git');
|
|
602
|
+
}
|
|
603
|
+
lines.push('');
|
|
604
|
+
// --- Infrastructure ---
|
|
605
|
+
if (detection.infraTypes.length > 0 || detection.cloudProviders.length > 0) {
|
|
606
|
+
lines.push('## Infrastructure');
|
|
607
|
+
lines.push('');
|
|
608
|
+
if (detection.infraTypes.length > 0) {
|
|
609
|
+
lines.push(`- **Tools:** ${detection.infraTypes.join(', ')}`);
|
|
610
|
+
}
|
|
611
|
+
if (detection.cloudProviders.length > 0) {
|
|
612
|
+
lines.push(`- **Cloud Providers:** ${detection.cloudProviders.join(', ')}`);
|
|
613
|
+
}
|
|
614
|
+
if (infraCtx) {
|
|
615
|
+
if (infraCtx.terraformWorkspace) {
|
|
616
|
+
lines.push(`- **Terraform Workspace:** ${infraCtx.terraformWorkspace}`);
|
|
617
|
+
}
|
|
618
|
+
if (infraCtx.kubectlContext) {
|
|
619
|
+
lines.push(`- **Kubernetes Context:** ${infraCtx.kubectlContext}`);
|
|
620
|
+
}
|
|
621
|
+
if (infraCtx.kubectlClusters && infraCtx.kubectlClusters.length > 0) {
|
|
622
|
+
lines.push(`- **Kubernetes Clusters:** ${infraCtx.kubectlClusters.join(', ')}`);
|
|
623
|
+
}
|
|
624
|
+
if (infraCtx.helmReleases && infraCtx.helmReleases.length > 0) {
|
|
625
|
+
lines.push(`- **Helm Releases:** ${infraCtx.helmReleases.slice(0, 5).join(', ')}${infraCtx.helmReleases.length > 5 ? ` (+${infraCtx.helmReleases.length - 5} more)` : ''}`);
|
|
626
|
+
}
|
|
627
|
+
if (infraCtx.awsAccount) {
|
|
628
|
+
lines.push(`- **AWS Account:** ${infraCtx.awsAccount}${infraCtx.awsRegion ? ` (${infraCtx.awsRegion})` : ''}`);
|
|
629
|
+
}
|
|
630
|
+
// G13: List named AWS profiles
|
|
631
|
+
if (infraCtx.awsProfiles && infraCtx.awsProfiles.length > 0) {
|
|
632
|
+
lines.push(`- **AWS Profiles:** ${infraCtx.awsProfiles.map(p => p.profile).join(', ')}`);
|
|
633
|
+
}
|
|
634
|
+
if (infraCtx.gcpProject) {
|
|
635
|
+
lines.push(`- **GCP Project:** ${infraCtx.gcpProject}`);
|
|
636
|
+
}
|
|
637
|
+
// G16: K8s namespace and deployment counts
|
|
638
|
+
if (infraCtx.k8sNamespaceCount !== undefined) {
|
|
639
|
+
lines.push(`- **K8s Namespaces:** ${infraCtx.k8sNamespaceCount}`);
|
|
640
|
+
}
|
|
641
|
+
if (infraCtx.k8sDeploymentCount !== undefined) {
|
|
642
|
+
lines.push(`- **K8s Deployments:** ${infraCtx.k8sDeploymentCount}`);
|
|
643
|
+
}
|
|
644
|
+
// G9: CI/CD pipeline
|
|
645
|
+
if (infraCtx.cicdPipeline) {
|
|
646
|
+
lines.push(`- **CI/CD:** ${infraCtx.cicdPipeline}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
lines.push('');
|
|
650
|
+
}
|
|
651
|
+
// --- CI/CD (M4) ---
|
|
652
|
+
if (infraCtx?.cicdPipeline) {
|
|
653
|
+
lines.push('## CI/CD');
|
|
654
|
+
lines.push('');
|
|
655
|
+
const pipelineDir = {
|
|
656
|
+
'GitHub Actions': '.github/workflows/',
|
|
657
|
+
'GitLab CI': '.gitlab-ci.yml',
|
|
658
|
+
'CircleCI': '.circleci/',
|
|
659
|
+
'Jenkins': 'Jenkinsfile',
|
|
660
|
+
};
|
|
661
|
+
const pipelinePath = pipelineDir[infraCtx.cicdPipeline] ?? '';
|
|
662
|
+
lines.push(`Pipeline: ${infraCtx.cicdPipeline}${pipelinePath ? ` (${pipelinePath})` : ''}`);
|
|
663
|
+
lines.push('Convention: Always run `terraform plan` in CI before apply. Apply only on main branch merge.');
|
|
664
|
+
lines.push('');
|
|
665
|
+
}
|
|
666
|
+
// --- Conventions ---
|
|
667
|
+
if (detection.linter || detection.formatter) {
|
|
668
|
+
lines.push('## Conventions');
|
|
669
|
+
lines.push('');
|
|
670
|
+
if (detection.linter) {
|
|
671
|
+
lines.push(`- **Linter:** ${detection.linter}`);
|
|
672
|
+
}
|
|
673
|
+
if (detection.formatter) {
|
|
674
|
+
lines.push(`- **Formatter:** ${detection.formatter}`);
|
|
675
|
+
}
|
|
676
|
+
lines.push('');
|
|
677
|
+
}
|
|
678
|
+
// --- Environments (GAP-22) ---
|
|
679
|
+
lines.push('## Environments');
|
|
680
|
+
lines.push('');
|
|
681
|
+
lines.push('| Name | Terraform Workspace | Kubernetes Context | Protected |');
|
|
682
|
+
lines.push('|------|--------------------|--------------------|-----------|');
|
|
683
|
+
lines.push('| dev | dev | | false |');
|
|
684
|
+
lines.push('| staging | staging | | false |');
|
|
685
|
+
lines.push('| prod | prod | | true |');
|
|
686
|
+
lines.push('');
|
|
687
|
+
// --- Safety Rules ---
|
|
688
|
+
lines.push('## Safety Rules');
|
|
689
|
+
lines.push('');
|
|
690
|
+
lines.push('- Protected branches: `main`, `master`');
|
|
691
|
+
lines.push('- Protected Kubernetes namespaces: `production`, `kube-system`');
|
|
692
|
+
lines.push('- Always preview before `terraform apply`');
|
|
693
|
+
lines.push('- Run tests before committing');
|
|
694
|
+
lines.push('- Never store secrets in source control');
|
|
695
|
+
lines.push('');
|
|
696
|
+
// --- Guardrails (G5): DevOps-specific rules when infra is detected ---
|
|
697
|
+
const hasInfra = detection.infraTypes.length > 0 || detection.cloudProviders.length > 0;
|
|
698
|
+
if (hasInfra) {
|
|
699
|
+
lines.push('## Guardrails');
|
|
700
|
+
lines.push('');
|
|
701
|
+
lines.push('- Never run `terraform destroy` without explicit confirmation');
|
|
702
|
+
lines.push('- Protected Kubernetes namespaces: `production`, `kube-system`, `monitoring`');
|
|
703
|
+
lines.push('- Always show terraform plan before apply');
|
|
704
|
+
lines.push('- Confirm target cloud account and region before resource creation');
|
|
705
|
+
lines.push('- Protected environments: any workspace/namespace containing `prod`, `prd`, `production`');
|
|
706
|
+
lines.push('');
|
|
707
|
+
}
|
|
708
|
+
// --- Forbidden placeholder (G5) ---
|
|
709
|
+
lines.push('## Forbidden');
|
|
710
|
+
lines.push('');
|
|
711
|
+
lines.push('<!-- List operations Nimbus must never perform in this project -->');
|
|
712
|
+
lines.push('<!-- Example: - Never destroy the production database -->');
|
|
713
|
+
lines.push('');
|
|
714
|
+
// --- Custom Instructions ---
|
|
715
|
+
lines.push('## Custom Instructions');
|
|
716
|
+
lines.push('');
|
|
717
|
+
lines.push('<!-- Add project-specific instructions for the Nimbus agent here -->');
|
|
718
|
+
lines.push('');
|
|
719
|
+
// --- G18: Runbooks ---
|
|
720
|
+
const runbookDirs = ['docs/runbooks', 'runbooks', '.github/runbooks', '.nimbus/runbooks'];
|
|
721
|
+
const foundRunbooks = [];
|
|
722
|
+
for (const dir of runbookDirs) {
|
|
723
|
+
const fullPath = path.join(_dir, dir);
|
|
724
|
+
if (fs.existsSync(fullPath)) {
|
|
725
|
+
try {
|
|
726
|
+
const files = fs.readdirSync(fullPath).filter(f => /\.(md|yaml|yml)$/.test(f));
|
|
727
|
+
foundRunbooks.push(...files.map(f => `- ${dir}/${f}`));
|
|
728
|
+
}
|
|
729
|
+
catch { /* non-critical */ }
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const runbookSection = foundRunbooks.length > 0
|
|
733
|
+
? foundRunbooks.join('\n') + '\n\nRefer to these runbooks for incident response and operational procedures.'
|
|
734
|
+
: '<!-- Add runbook references, e.g.:\n- docs/runbooks/cert-rotation.md -->';
|
|
735
|
+
lines.push('## Runbooks');
|
|
736
|
+
lines.push('');
|
|
737
|
+
lines.push(runbookSection);
|
|
738
|
+
lines.push('');
|
|
739
|
+
return lines.join('\n');
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Generate the contents of `.nimbus/config.yaml`.
|
|
743
|
+
*
|
|
744
|
+
* Produces a valid YAML string without requiring an external YAML library.
|
|
745
|
+
*/
|
|
746
|
+
function generateConfigYaml(detection) {
|
|
747
|
+
const lines = [];
|
|
748
|
+
lines.push('# Nimbus project configuration');
|
|
749
|
+
lines.push('# See https://nimbus.dev/docs/config for all options');
|
|
750
|
+
lines.push('');
|
|
751
|
+
lines.push('# Default LLM model for agent interactions');
|
|
752
|
+
lines.push('default_model: anthropic/claude-sonnet-4');
|
|
753
|
+
lines.push('');
|
|
754
|
+
lines.push('# Default agent mode: build | plan | debug | review');
|
|
755
|
+
lines.push('default_mode: build');
|
|
756
|
+
lines.push('');
|
|
757
|
+
lines.push('# Project metadata');
|
|
758
|
+
lines.push('project:');
|
|
759
|
+
lines.push(` name: ${detection.projectName}`);
|
|
760
|
+
lines.push(` type: ${detection.projectType}`);
|
|
761
|
+
if (detection.packageManager) {
|
|
762
|
+
lines.push(` package_manager: ${detection.packageManager}`);
|
|
763
|
+
}
|
|
764
|
+
lines.push('');
|
|
765
|
+
// Permissions
|
|
766
|
+
lines.push('# Permission rules control what the agent can do without asking');
|
|
767
|
+
lines.push('permissions:');
|
|
768
|
+
lines.push(' # File operations');
|
|
769
|
+
lines.push(' file_read: allow');
|
|
770
|
+
lines.push(' file_write: ask');
|
|
771
|
+
lines.push(' file_delete: deny');
|
|
772
|
+
lines.push('');
|
|
773
|
+
lines.push(' # Shell commands');
|
|
774
|
+
lines.push(' shell_read: allow # non-destructive commands (ls, cat, git status)');
|
|
775
|
+
lines.push(' shell_write: ask # potentially destructive commands');
|
|
776
|
+
lines.push('');
|
|
777
|
+
lines.push(' # Git operations');
|
|
778
|
+
lines.push(' git_read: allow');
|
|
779
|
+
lines.push(' git_write: ask');
|
|
780
|
+
lines.push('');
|
|
781
|
+
lines.push(' # Infrastructure operations');
|
|
782
|
+
lines.push(' terraform_plan: allow');
|
|
783
|
+
lines.push(' terraform_apply: deny');
|
|
784
|
+
lines.push(' kubectl_read: allow');
|
|
785
|
+
lines.push(' kubectl_write: deny');
|
|
786
|
+
lines.push('');
|
|
787
|
+
// Safety
|
|
788
|
+
lines.push('# Safety settings');
|
|
789
|
+
lines.push('safety:');
|
|
790
|
+
lines.push(' protected_branches:');
|
|
791
|
+
lines.push(' - main');
|
|
792
|
+
lines.push(' - master');
|
|
793
|
+
lines.push(' protected_k8s_namespaces:');
|
|
794
|
+
lines.push(' - production');
|
|
795
|
+
lines.push(' - kube-system');
|
|
796
|
+
lines.push(' require_plan_before_apply: true');
|
|
797
|
+
lines.push(' require_tests_before_commit: true');
|
|
798
|
+
lines.push('');
|
|
799
|
+
return lines.join('\n');
|
|
800
|
+
}
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// Main init
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
/**
|
|
805
|
+
* Initialize a Nimbus project in the given directory.
|
|
806
|
+
*
|
|
807
|
+
* Creates the `.nimbus/` directory structure and a `NIMBUS.md` file
|
|
808
|
+
* populated with auto-detected project metadata.
|
|
809
|
+
*
|
|
810
|
+
* @param options - Configuration for the init process
|
|
811
|
+
* @returns The detection results and list of created files
|
|
812
|
+
*
|
|
813
|
+
* @example
|
|
814
|
+
* ```ts
|
|
815
|
+
* const result = await runInit({ cwd: '/path/to/project' });
|
|
816
|
+
* console.log(result.detection.projectType); // 'typescript'
|
|
817
|
+
* console.log(result.filesCreated); // ['.nimbus/config.yaml', ...]
|
|
818
|
+
* ```
|
|
819
|
+
*/
|
|
820
|
+
export async function runInit(options) {
|
|
821
|
+
const dir = path.resolve(options?.cwd ?? process.cwd());
|
|
822
|
+
const force = options?.force ?? false;
|
|
823
|
+
const quiet = options?.quiet ?? false;
|
|
824
|
+
const merge = options?.merge ?? false;
|
|
825
|
+
const log = (msg) => {
|
|
826
|
+
if (!quiet) {
|
|
827
|
+
console.log(msg);
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
// ---- M2: --merge mode — append Local Overrides section, don't overwrite ----
|
|
831
|
+
const nimbusmdPathEarly = path.join(dir, 'NIMBUS.md');
|
|
832
|
+
if (merge && exists(nimbusmdPathEarly)) {
|
|
833
|
+
const content = fs.readFileSync(nimbusmdPathEarly, 'utf-8');
|
|
834
|
+
const filesCreated = [];
|
|
835
|
+
if (!content.includes('## Local Overrides')) {
|
|
836
|
+
fs.appendFileSync(nimbusmdPathEarly, '\n\n## Local Overrides\n\n<!-- Personal additions -->\n', 'utf-8');
|
|
837
|
+
log(' Appended ## Local Overrides section to NIMBUS.md');
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
log(' NIMBUS.md already has a ## Local Overrides section');
|
|
841
|
+
}
|
|
842
|
+
const localMdPath = path.join(dir, '.nimbus', 'local.md');
|
|
843
|
+
if (!exists(localMdPath)) {
|
|
844
|
+
fs.mkdirSync(path.join(dir, '.nimbus'), { recursive: true });
|
|
845
|
+
fs.writeFileSync(localMdPath, '# Local Overrides\n\n<!-- Personal notes and overrides that are not committed -->\n', 'utf-8');
|
|
846
|
+
log(' Created .nimbus/local.md for personal overrides');
|
|
847
|
+
filesCreated.push(localMdPath);
|
|
848
|
+
}
|
|
849
|
+
const detection = detectProject(dir);
|
|
850
|
+
return { detection, filesCreated, nimbusmdPath: nimbusmdPathEarly };
|
|
851
|
+
}
|
|
852
|
+
// ---- Step 1: Detect project characteristics ----
|
|
853
|
+
log('Detecting project...');
|
|
854
|
+
const detection = detectProject(dir);
|
|
855
|
+
log(` Project type: ${detection.projectType}`);
|
|
856
|
+
if (detection.packageManager) {
|
|
857
|
+
log(` Package manager: ${detection.packageManager}`);
|
|
858
|
+
}
|
|
859
|
+
if (detection.infraTypes.length > 0) {
|
|
860
|
+
log(` Infrastructure: ${detection.infraTypes.join(', ')}`);
|
|
861
|
+
}
|
|
862
|
+
if (detection.cloudProviders.length > 0) {
|
|
863
|
+
log(` Cloud providers: ${detection.cloudProviders.join(', ')}`);
|
|
864
|
+
}
|
|
865
|
+
if (detection.testFramework) {
|
|
866
|
+
log(` Test framework: ${detection.testFramework}`);
|
|
867
|
+
}
|
|
868
|
+
// ---- Step 2: Check for existing NIMBUS.md and show diff (L8) ----
|
|
869
|
+
const nimbusmdPath = path.join(dir, 'NIMBUS.md');
|
|
870
|
+
if (exists(nimbusmdPath) && !force && !quiet) {
|
|
871
|
+
// Generate the new content first for diffing
|
|
872
|
+
const infraCtxForDiff = await discoverInfraContext(dir).catch(() => undefined);
|
|
873
|
+
const newContent = generateNimbusMd(detection, dir, infraCtxForDiff);
|
|
874
|
+
const existingContent = readText(nimbusmdPath);
|
|
875
|
+
if (newContent === existingContent) {
|
|
876
|
+
log('NIMBUS.md is already up to date.');
|
|
877
|
+
return { detection, filesCreated: [], nimbusmdPath };
|
|
878
|
+
}
|
|
879
|
+
// Show line-level diff
|
|
880
|
+
const oldLines = existingContent.split('\n');
|
|
881
|
+
const newLines = newContent.split('\n');
|
|
882
|
+
const diffLines = [];
|
|
883
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
884
|
+
for (let idx = 0; idx < maxLen; idx++) {
|
|
885
|
+
const o = oldLines[idx];
|
|
886
|
+
const n = newLines[idx];
|
|
887
|
+
if (o === undefined)
|
|
888
|
+
diffLines.push(`+ ${n}`);
|
|
889
|
+
else if (n === undefined)
|
|
890
|
+
diffLines.push(`- ${o}`);
|
|
891
|
+
else if (o !== n) {
|
|
892
|
+
diffLines.push(`- ${o}`);
|
|
893
|
+
diffLines.push(`+ ${n}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (diffLines.length > 0) {
|
|
897
|
+
log('\nNIMBUS.md changes:');
|
|
898
|
+
for (const dl of diffLines.slice(0, 50)) {
|
|
899
|
+
if (dl.startsWith('+'))
|
|
900
|
+
log(` \x1b[32m${dl}\x1b[0m`);
|
|
901
|
+
else if (dl.startsWith('-'))
|
|
902
|
+
log(` \x1b[31m${dl}\x1b[0m`);
|
|
903
|
+
else
|
|
904
|
+
log(` ${dl}`);
|
|
905
|
+
}
|
|
906
|
+
if (diffLines.length > 50)
|
|
907
|
+
log(` ... and ${diffLines.length - 50} more changes`);
|
|
908
|
+
log('');
|
|
909
|
+
log('Apply these changes? [y/N]');
|
|
910
|
+
// Synchronous readline for non-quiet mode
|
|
911
|
+
const answer = await new Promise(resolve => {
|
|
912
|
+
const { createInterface } = require('readline');
|
|
913
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
914
|
+
rl.question('', (ans) => { rl.close(); resolve(ans.trim()); });
|
|
915
|
+
});
|
|
916
|
+
if (answer.toLowerCase() !== 'y') {
|
|
917
|
+
log('Aborted — NIMBUS.md not changed.');
|
|
918
|
+
return { detection, filesCreated: [], nimbusmdPath };
|
|
919
|
+
}
|
|
920
|
+
fs.writeFileSync(nimbusmdPath, newContent, 'utf-8');
|
|
921
|
+
log(' Updated NIMBUS.md');
|
|
922
|
+
return { detection, filesCreated: [nimbusmdPath], nimbusmdPath };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
else if (exists(nimbusmdPath) && !force) {
|
|
926
|
+
// quiet mode — skip without prompting
|
|
927
|
+
return { detection, filesCreated: [], nimbusmdPath };
|
|
928
|
+
}
|
|
929
|
+
// ---- Step 3: Create .nimbus/ directory structure ----
|
|
930
|
+
const filesCreated = [];
|
|
931
|
+
const nimbusDirPath = path.join(dir, '.nimbus');
|
|
932
|
+
const hooksDirPath = path.join(nimbusDirPath, 'hooks');
|
|
933
|
+
const agentsDirPath = path.join(nimbusDirPath, 'agents');
|
|
934
|
+
if (!exists(nimbusDirPath)) {
|
|
935
|
+
fs.mkdirSync(nimbusDirPath, { recursive: true });
|
|
936
|
+
}
|
|
937
|
+
if (!exists(hooksDirPath)) {
|
|
938
|
+
fs.mkdirSync(hooksDirPath, { recursive: true });
|
|
939
|
+
}
|
|
940
|
+
if (!exists(agentsDirPath)) {
|
|
941
|
+
fs.mkdirSync(agentsDirPath, { recursive: true });
|
|
942
|
+
}
|
|
943
|
+
// ---- Step 4: Create .nimbus/config.yaml ----
|
|
944
|
+
const configPath = path.join(nimbusDirPath, 'config.yaml');
|
|
945
|
+
if (!exists(configPath) || force) {
|
|
946
|
+
const configContent = generateConfigYaml(detection);
|
|
947
|
+
fs.writeFileSync(configPath, configContent, 'utf-8');
|
|
948
|
+
filesCreated.push(configPath);
|
|
949
|
+
log(' Created .nimbus/config.yaml');
|
|
950
|
+
}
|
|
951
|
+
// ---- Step 5: Create placeholder hook files ----
|
|
952
|
+
const preCommitHookPath = path.join(hooksDirPath, 'pre-commit.ts');
|
|
953
|
+
if (!exists(preCommitHookPath) || force) {
|
|
954
|
+
const preCommitContent = [
|
|
955
|
+
'/**',
|
|
956
|
+
' * Nimbus pre-commit hook',
|
|
957
|
+
' *',
|
|
958
|
+
' * Runs automatically before each commit when enabled.',
|
|
959
|
+
' * Add custom validation logic here.',
|
|
960
|
+
' */',
|
|
961
|
+
'',
|
|
962
|
+
'export default async function preCommit(): Promise<void> {',
|
|
963
|
+
' // Example: ensure tests pass before committing',
|
|
964
|
+
' // await $`bun test`;',
|
|
965
|
+
'}',
|
|
966
|
+
'',
|
|
967
|
+
].join('\n');
|
|
968
|
+
fs.writeFileSync(preCommitHookPath, preCommitContent, 'utf-8');
|
|
969
|
+
filesCreated.push(preCommitHookPath);
|
|
970
|
+
log(' Created .nimbus/hooks/pre-commit.ts');
|
|
971
|
+
}
|
|
972
|
+
// ---- Step 6: Create placeholder agent config ----
|
|
973
|
+
const defaultAgentPath = path.join(agentsDirPath, 'default.yaml');
|
|
974
|
+
if (!exists(defaultAgentPath) || force) {
|
|
975
|
+
const agentContent = [
|
|
976
|
+
'# Default agent profile',
|
|
977
|
+
'# Customize the system prompt and tool access for this agent',
|
|
978
|
+
'',
|
|
979
|
+
'name: default',
|
|
980
|
+
'description: General-purpose Nimbus agent',
|
|
981
|
+
'',
|
|
982
|
+
'tools:',
|
|
983
|
+
' - file_read',
|
|
984
|
+
' - file_write',
|
|
985
|
+
' - shell',
|
|
986
|
+
' - git',
|
|
987
|
+
'',
|
|
988
|
+
'system_prompt: |',
|
|
989
|
+
` You are working on the ${detection.projectName} project.`,
|
|
990
|
+
` It is a ${detection.projectType} project.`,
|
|
991
|
+
' Follow the safety rules in NIMBUS.md.',
|
|
992
|
+
'',
|
|
993
|
+
].join('\n');
|
|
994
|
+
fs.writeFileSync(defaultAgentPath, agentContent, 'utf-8');
|
|
995
|
+
filesCreated.push(defaultAgentPath);
|
|
996
|
+
log(' Created .nimbus/agents/default.yaml');
|
|
997
|
+
}
|
|
998
|
+
// ---- Step 7: Generate and write NIMBUS.md ----
|
|
999
|
+
// L8: Monorepo detection — scan immediate subdirs for terraform roots
|
|
1000
|
+
let monorepoSection = '';
|
|
1001
|
+
try {
|
|
1002
|
+
const subdirs = fs.readdirSync(dir, { withFileTypes: true })
|
|
1003
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.') && !['node_modules', '.git'].includes(e.name))
|
|
1004
|
+
.map(e => e.name);
|
|
1005
|
+
const tfRoots = [];
|
|
1006
|
+
for (const sub of subdirs.slice(0, 20)) {
|
|
1007
|
+
const subPath = path.join(dir, sub);
|
|
1008
|
+
const hasTf = fs.readdirSync(subPath).some(f => f.endsWith('.tf'));
|
|
1009
|
+
if (hasTf)
|
|
1010
|
+
tfRoots.push(sub);
|
|
1011
|
+
}
|
|
1012
|
+
if (tfRoots.length > 1) {
|
|
1013
|
+
monorepoSection = `\n## Terraform Modules (Monorepo)\n\nThis is a monorepo with multiple Terraform roots:\n${tfRoots.map(r => `- \`./${r}/\``).join('\n')}\n\nTo target a specific root, \`cd\` into the directory or specify the path.\n`;
|
|
1014
|
+
log(` Detected ${tfRoots.length} Terraform roots (monorepo)`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
catch { /* non-critical */ }
|
|
1018
|
+
const nimbusmdContent = generateNimbusMd(detection, dir) + monorepoSection;
|
|
1019
|
+
fs.writeFileSync(nimbusmdPath, nimbusmdContent, 'utf-8');
|
|
1020
|
+
filesCreated.push(nimbusmdPath);
|
|
1021
|
+
log(' Created NIMBUS.md');
|
|
1022
|
+
// ---- Step 8: Append .nimbus/ to .gitignore if not already present ----
|
|
1023
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
1024
|
+
if (exists(gitignorePath)) {
|
|
1025
|
+
const gitignore = readText(gitignorePath);
|
|
1026
|
+
if (!gitignore.includes('.nimbus/')) {
|
|
1027
|
+
fs.appendFileSync(gitignorePath, '\n# Nimbus local config\n.nimbus/\n', 'utf-8');
|
|
1028
|
+
log(' Updated .gitignore');
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
log('');
|
|
1032
|
+
log('Nimbus project initialized successfully.');
|
|
1033
|
+
log('Edit NIMBUS.md to customise agent behaviour.');
|
|
1034
|
+
// M5: Print next steps after generating NIMBUS.md
|
|
1035
|
+
if (!quiet) {
|
|
1036
|
+
log('');
|
|
1037
|
+
log('\x1b[36mNext steps:\x1b[0m');
|
|
1038
|
+
log(' nimbus plan Preview infrastructure changes');
|
|
1039
|
+
log(' nimbus doctor Check your DevOps toolchain');
|
|
1040
|
+
log(' nimbus status Live infrastructure health dashboard');
|
|
1041
|
+
log(' nimbus Open the interactive DevOps agent');
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
detection,
|
|
1045
|
+
filesCreated,
|
|
1046
|
+
nimbusmdPath,
|
|
1047
|
+
};
|
|
1048
|
+
}
|