@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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the nimbus rollback command (G20).
|
|
3
|
+
*
|
|
4
|
+
* The rollbackCommand provides guided infrastructure rollback for Helm and K8s.
|
|
5
|
+
* We test the module exports and option parsing rather than making real CLI calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, it, expect } from 'vitest';
|
|
9
|
+
import { rollbackCommand, type RollbackOptions } from '../commands/rollback';
|
|
10
|
+
|
|
11
|
+
describe('rollbackCommand (G20)', () => {
|
|
12
|
+
test('exports rollbackCommand function', () => {
|
|
13
|
+
expect(typeof rollbackCommand).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('RollbackOptions accepts helm flag', () => {
|
|
17
|
+
const opts: RollbackOptions = { helm: 'my-release', namespace: 'production' };
|
|
18
|
+
expect(opts.helm).toBe('my-release');
|
|
19
|
+
expect(opts.namespace).toBe('production');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('RollbackOptions accepts k8s flag', () => {
|
|
23
|
+
const opts: RollbackOptions = { k8s: 'my-deployment', namespace: 'staging' };
|
|
24
|
+
expect(opts.k8s).toBe('my-deployment');
|
|
25
|
+
expect(opts.namespace).toBe('staging');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('RollbackOptions accepts tf flag', () => {
|
|
29
|
+
const opts: RollbackOptions = { tf: true };
|
|
30
|
+
expect(opts.tf).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('RollbackOptions all fields are optional', () => {
|
|
34
|
+
const opts: RollbackOptions = {};
|
|
35
|
+
expect(opts.helm).toBeUndefined();
|
|
36
|
+
expect(opts.k8s).toBeUndefined();
|
|
37
|
+
expect(opts.namespace).toBeUndefined();
|
|
38
|
+
expect(opts.tf).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('rollbackCommand with no options outputs usage error', async () => {
|
|
42
|
+
// When no target specified, should print an error without throwing
|
|
43
|
+
const originalError = process.stderr.write.bind(process.stderr);
|
|
44
|
+
// Just ensure it doesn't throw
|
|
45
|
+
await expect(rollbackCommand({})).resolves.toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// L1: nimbus rollout command
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
describe('parseTimeoutToMs (L1)', () => {
|
|
54
|
+
it('exists and is importable', async () => {
|
|
55
|
+
const { parseTimeoutToMs } = await import('../commands/rollout');
|
|
56
|
+
expect(typeof parseTimeoutToMs).toBe('function');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('converts seconds to milliseconds', async () => {
|
|
60
|
+
const { parseTimeoutToMs } = await import('../commands/rollout');
|
|
61
|
+
expect(parseTimeoutToMs('30s')).toBe(30_000);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('converts minutes to milliseconds', async () => {
|
|
65
|
+
const { parseTimeoutToMs } = await import('../commands/rollout');
|
|
66
|
+
expect(parseTimeoutToMs('5m')).toBe(300_000);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('converts hours to milliseconds', async () => {
|
|
70
|
+
const { parseTimeoutToMs } = await import('../commands/rollout');
|
|
71
|
+
expect(parseTimeoutToMs('2h')).toBe(7_200_000);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns 300000 for unrecognized format', async () => {
|
|
75
|
+
const { parseTimeoutToMs } = await import('../commands/rollout');
|
|
76
|
+
expect(parseTimeoutToMs('invalid')).toBe(300_000);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rolloutCommand is exported from rollout.ts', async () => {
|
|
80
|
+
const { rolloutCommand } = await import('../commands/rollout');
|
|
81
|
+
expect(typeof rolloutCommand).toBe('function');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runbook Command Tests — G15
|
|
3
|
+
*
|
|
4
|
+
* Tests the YAML parser, runbookCommand('list'), and step prompt building.
|
|
5
|
+
* runbookCreate involves interactive readline so we test source-level
|
|
6
|
+
* assertions for that path.
|
|
7
|
+
*
|
|
8
|
+
* Note: process.chdir is not supported in vitest workers. The runbook list
|
|
9
|
+
* test isolates the homedir via vi.mock('node:os') and mocks fs operations
|
|
10
|
+
* to control which directories are seen.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Inline reproduction of parseRunbookYaml (matches source implementation)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
interface RunbookDef {
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
context?: string;
|
|
26
|
+
steps: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseRunbookYaml(content: string): RunbookDef {
|
|
30
|
+
const lines = content.split('\n');
|
|
31
|
+
const def: RunbookDef = { name: '', steps: [] };
|
|
32
|
+
let inSteps = false;
|
|
33
|
+
|
|
34
|
+
for (const raw of lines) {
|
|
35
|
+
const line = raw.trimEnd();
|
|
36
|
+
if (line.startsWith('#') || !line.trim()) continue;
|
|
37
|
+
|
|
38
|
+
if (line.startsWith('name:')) {
|
|
39
|
+
def.name = line.slice(5).trim().replace(/^['"]|['"]$/g, '');
|
|
40
|
+
inSteps = false;
|
|
41
|
+
} else if (line.startsWith('description:')) {
|
|
42
|
+
def.description = line.slice(12).trim().replace(/^['"]|['"]$/g, '');
|
|
43
|
+
inSteps = false;
|
|
44
|
+
} else if (line.startsWith('context:')) {
|
|
45
|
+
def.context = line.slice(8).trim().replace(/^['"]|['"]$/g, '');
|
|
46
|
+
inSteps = false;
|
|
47
|
+
} else if (line.trim() === 'steps:') {
|
|
48
|
+
inSteps = true;
|
|
49
|
+
} else if (inSteps && /^\s*-\s/.test(line)) {
|
|
50
|
+
def.steps.push(line.replace(/^\s*-\s*/, '').trim().replace(/^['"]|['"]$/g, ''));
|
|
51
|
+
} else {
|
|
52
|
+
inSteps = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return def;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Inline reproduction of buildRunbookPrompt
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function buildRunbookPrompt(def: RunbookDef): string {
|
|
64
|
+
const parts = [`# Runbook: ${def.name}`];
|
|
65
|
+
if (def.description) parts.push(`\n${def.description}`);
|
|
66
|
+
if (def.context) parts.push(`\nContext/profile: ${def.context}`);
|
|
67
|
+
parts.push('\n## Steps to execute in order:');
|
|
68
|
+
def.steps.forEach((step, i) => parts.push(`${i + 1}. ${step}`));
|
|
69
|
+
parts.push('\nExecute each step in sequence. Check for errors after each step before proceeding. Report progress clearly.');
|
|
70
|
+
return parts.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// YAML parser tests
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
describe('parseRunbookYaml (G15)', () => {
|
|
78
|
+
const SAMPLE = [
|
|
79
|
+
'name: rotate-certs',
|
|
80
|
+
'description: Rotate TLS certs in prod namespace',
|
|
81
|
+
'context: prod',
|
|
82
|
+
'steps:',
|
|
83
|
+
' - Check for expiring certs in all namespaces',
|
|
84
|
+
' - Rotate each cert using cert-manager annotate',
|
|
85
|
+
' - Verify new certs are valid and pods restarted',
|
|
86
|
+
].join('\n');
|
|
87
|
+
|
|
88
|
+
it('parses name field correctly', () => {
|
|
89
|
+
const def = parseRunbookYaml(SAMPLE);
|
|
90
|
+
expect(def.name).toBe('rotate-certs');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('parses description field', () => {
|
|
94
|
+
const def = parseRunbookYaml(SAMPLE);
|
|
95
|
+
expect(def.description).toBe('Rotate TLS certs in prod namespace');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('parses context field', () => {
|
|
99
|
+
const def = parseRunbookYaml(SAMPLE);
|
|
100
|
+
expect(def.context).toBe('prod');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('parses steps array with correct count', () => {
|
|
104
|
+
const def = parseRunbookYaml(SAMPLE);
|
|
105
|
+
expect(def.steps).toHaveLength(3);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('parses step content correctly', () => {
|
|
109
|
+
const def = parseRunbookYaml(SAMPLE);
|
|
110
|
+
expect(def.steps[0]).toBe('Check for expiring certs in all namespaces');
|
|
111
|
+
expect(def.steps[2]).toBe('Verify new certs are valid and pods restarted');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('ignores comment lines', () => {
|
|
115
|
+
const withComments = [
|
|
116
|
+
'name: test-runbook',
|
|
117
|
+
'# this is a comment',
|
|
118
|
+
'description: desc',
|
|
119
|
+
'steps:',
|
|
120
|
+
' - step one',
|
|
121
|
+
].join('\n');
|
|
122
|
+
const def = parseRunbookYaml(withComments);
|
|
123
|
+
expect(def.name).toBe('test-runbook');
|
|
124
|
+
expect(def.steps).toHaveLength(1);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// buildRunbookPrompt tests
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
describe('buildRunbookPrompt step composition (G15)', () => {
|
|
133
|
+
it('builds a multi-step numbered prompt', () => {
|
|
134
|
+
const def: RunbookDef = {
|
|
135
|
+
name: 'deploy-rollback',
|
|
136
|
+
description: 'Rollback a failed deployment',
|
|
137
|
+
steps: ['Check rollout status', 'Run helm rollback', 'Verify pods are healthy'],
|
|
138
|
+
};
|
|
139
|
+
const prompt = buildRunbookPrompt(def);
|
|
140
|
+
expect(prompt).toContain('# Runbook: deploy-rollback');
|
|
141
|
+
expect(prompt).toContain('1. Check rollout status');
|
|
142
|
+
expect(prompt).toContain('2. Run helm rollback');
|
|
143
|
+
expect(prompt).toContain('3. Verify pods are healthy');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('includes description in prompt when set', () => {
|
|
147
|
+
const def: RunbookDef = { name: 'my-rb', description: 'My description', steps: ['step 1'] };
|
|
148
|
+
const prompt = buildRunbookPrompt(def);
|
|
149
|
+
expect(prompt).toContain('My description');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('includes context/profile when set', () => {
|
|
153
|
+
const def: RunbookDef = { name: 'rb', context: 'staging', steps: ['step 1'] };
|
|
154
|
+
const prompt = buildRunbookPrompt(def);
|
|
155
|
+
expect(prompt).toContain('Context/profile: staging');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('includes execution instructions', () => {
|
|
159
|
+
const def: RunbookDef = { name: 'rb', steps: ['step 1'] };
|
|
160
|
+
const prompt = buildRunbookPrompt(def);
|
|
161
|
+
expect(prompt).toContain('Execute each step in sequence');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// runbookCommand list — isolate via mocking node:os and node:fs
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
describe('runbookCommand list with no runbooks (G15)', () => {
|
|
170
|
+
let tmpDir: string;
|
|
171
|
+
|
|
172
|
+
beforeEach(() => {
|
|
173
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-runbook-test-'));
|
|
174
|
+
vi.resetModules();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
afterEach(() => {
|
|
178
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
179
|
+
vi.restoreAllMocks();
|
|
180
|
+
vi.resetModules();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('prints "No runbooks found" when no runbooks directory exists', async () => {
|
|
184
|
+
// Mock node:os so homedir() returns our tmp dir (which has no runbooks/ subdir)
|
|
185
|
+
vi.doMock('node:os', async () => {
|
|
186
|
+
const actual = await vi.importActual<typeof os>('node:os');
|
|
187
|
+
return { ...actual, homedir: () => tmpDir };
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const logs: string[] = [];
|
|
191
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
192
|
+
logs.push(args.join(' '));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const { runbookCommand } = await import('../commands/runbook');
|
|
196
|
+
await runbookCommand('list', []);
|
|
197
|
+
|
|
198
|
+
const allOutput = logs.join('\n');
|
|
199
|
+
expect(allOutput).toContain('No runbooks found');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('shows usage for unknown subcommand', async () => {
|
|
203
|
+
vi.doMock('node:os', async () => {
|
|
204
|
+
const actual = await vi.importActual<typeof os>('node:os');
|
|
205
|
+
return { ...actual, homedir: () => tmpDir };
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const logs: string[] = [];
|
|
209
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
210
|
+
logs.push(args.join(' '));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const { runbookCommand } = await import('../commands/runbook');
|
|
214
|
+
await runbookCommand('unknown-subcmd', []);
|
|
215
|
+
|
|
216
|
+
const allOutput = logs.join('\n');
|
|
217
|
+
expect(allOutput).toContain('Usage: nimbus runbook');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedule Command Tests — G13
|
|
3
|
+
*
|
|
4
|
+
* Tests schedule list, add, remove, invalid cron rejection, and
|
|
5
|
+
* crontab activation hint output.
|
|
6
|
+
*
|
|
7
|
+
* File I/O is isolated by mocking node:os.homedir to point to a temp dir.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import * as os from 'node:os';
|
|
14
|
+
|
|
15
|
+
// We need to control where schedules.json is written.
|
|
16
|
+
// Approach: mock node:os homedir to our temp dir, then resetModules
|
|
17
|
+
// so the schedule module re-evaluates SCHEDULE_FILE with the new homedir.
|
|
18
|
+
|
|
19
|
+
let tmpDir: string;
|
|
20
|
+
|
|
21
|
+
vi.mock('node:os', async () => {
|
|
22
|
+
const actual = await vi.importActual<typeof os>('node:os');
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
homedir: () => tmpDir ?? actual.homedir(),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function getScheduleModule() {
|
|
30
|
+
vi.resetModules();
|
|
31
|
+
return await import('../commands/schedule');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('scheduleCommand list (G13)', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
vi.resetModules();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('prints "No schedules configured" when schedules.json does not exist', async () => {
|
|
46
|
+
const logs: string[] = [];
|
|
47
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
48
|
+
logs.push(args.join(' '));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const { scheduleCommand } = await getScheduleModule();
|
|
52
|
+
await scheduleCommand('list', []);
|
|
53
|
+
|
|
54
|
+
const output = logs.join('\n');
|
|
55
|
+
expect(output).toContain('No schedules configured');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('scheduleCommand add (G13)', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
66
|
+
vi.restoreAllMocks();
|
|
67
|
+
vi.resetModules();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('saves a schedule entry when given a valid cron and prompt', async () => {
|
|
71
|
+
const logs: string[] = [];
|
|
72
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
73
|
+
logs.push(args.join(' '));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const { scheduleCommand } = await getScheduleModule();
|
|
77
|
+
await scheduleCommand('add', ['0 8 * * *', 'check drift']);
|
|
78
|
+
|
|
79
|
+
// Verify the schedule file was written
|
|
80
|
+
const scheduleFile = path.join(tmpDir, '.nimbus', 'schedules.json');
|
|
81
|
+
expect(fs.existsSync(scheduleFile)).toBe(true);
|
|
82
|
+
|
|
83
|
+
const data = JSON.parse(fs.readFileSync(scheduleFile, 'utf-8')) as Array<{
|
|
84
|
+
id: string; name: string; cron: string; prompt: string;
|
|
85
|
+
}>;
|
|
86
|
+
expect(data).toHaveLength(1);
|
|
87
|
+
expect(data[0].cron).toBe('0 8 * * *');
|
|
88
|
+
expect(data[0].prompt).toBe('check drift');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('prints crontab activation hint after adding a schedule', async () => {
|
|
92
|
+
const logs: string[] = [];
|
|
93
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
94
|
+
logs.push(args.join(' '));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const { scheduleCommand } = await getScheduleModule();
|
|
98
|
+
await scheduleCommand('add', ['0 9 * * 1', 'weekly cost report']);
|
|
99
|
+
|
|
100
|
+
const output = logs.join('\n');
|
|
101
|
+
expect(output).toContain('crontab');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('scheduleCommand invalid cron rejection (G13)', () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
112
|
+
vi.restoreAllMocks();
|
|
113
|
+
vi.resetModules();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects a cron with fewer than 5 fields', async () => {
|
|
117
|
+
const errors: string[] = [];
|
|
118
|
+
vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
|
|
119
|
+
errors.push(args.join(' '));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
|
|
123
|
+
throw new Error('process.exit called');
|
|
124
|
+
}) as never);
|
|
125
|
+
|
|
126
|
+
const { scheduleCommand } = await getScheduleModule();
|
|
127
|
+
|
|
128
|
+
await expect(scheduleCommand('add', ['* * *', 'bad cron'])).rejects.toThrow('process.exit');
|
|
129
|
+
|
|
130
|
+
expect(errors.join('\n')).toContain('Invalid cron expression');
|
|
131
|
+
exitSpy.mockRestore();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('scheduleCommand remove (G13)', () => {
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
142
|
+
vi.restoreAllMocks();
|
|
143
|
+
vi.resetModules();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('removes an existing schedule by id', async () => {
|
|
147
|
+
// Seed the schedule file directly
|
|
148
|
+
const nimbusDir = path.join(tmpDir, '.nimbus');
|
|
149
|
+
fs.mkdirSync(nimbusDir, { recursive: true });
|
|
150
|
+
const entry = { id: 'abc123', name: 'my-schedule', cron: '0 8 * * *', prompt: 'drift check', createdAt: new Date().toISOString() };
|
|
151
|
+
fs.writeFileSync(path.join(nimbusDir, 'schedules.json'), JSON.stringify([entry]), 'utf-8');
|
|
152
|
+
|
|
153
|
+
const logs: string[] = [];
|
|
154
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
155
|
+
logs.push(args.join(' '));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const { scheduleCommand } = await getScheduleModule();
|
|
159
|
+
await scheduleCommand('remove', ['abc123']);
|
|
160
|
+
|
|
161
|
+
const data = JSON.parse(fs.readFileSync(path.join(nimbusDir, 'schedules.json'), 'utf-8')) as unknown[];
|
|
162
|
+
expect(data).toHaveLength(0);
|
|
163
|
+
expect(logs.join('\n')).toContain('Removed schedule');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('shows usage when no id is provided to remove', async () => {
|
|
167
|
+
const errors: string[] = [];
|
|
168
|
+
vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
|
|
169
|
+
errors.push(args.join(' '));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
|
|
173
|
+
throw new Error('process.exit called');
|
|
174
|
+
}) as never);
|
|
175
|
+
|
|
176
|
+
const { scheduleCommand } = await getScheduleModule();
|
|
177
|
+
await expect(scheduleCommand('remove', [])).rejects.toThrow('process.exit');
|
|
178
|
+
|
|
179
|
+
expect(errors.join('\n')).toContain('Usage');
|
|
180
|
+
exitSpy.mockRestore();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('scheduleCommand default/unknown subcommand (G13)', () => {
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-schedule-test-'));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
191
|
+
vi.restoreAllMocks();
|
|
192
|
+
vi.resetModules();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('prints usage for unknown subcommand', async () => {
|
|
196
|
+
const logs: string[] = [];
|
|
197
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
198
|
+
logs.push(args.join(' '));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const { scheduleCommand } = await getScheduleModule();
|
|
202
|
+
await scheduleCommand('unknown', []);
|
|
203
|
+
|
|
204
|
+
expect(logs.join('\n')).toContain('Usage: nimbus schedule');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for Multi-Session Manager
|
|
3
3
|
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach } from '
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
5
|
import { Database } from '../compat/sqlite';
|
|
6
6
|
import { SessionManager } from '../sessions/manager';
|
|
7
7
|
import type { SessionEvent } from '../sessions/types';
|
|
@@ -224,4 +224,99 @@ describe('SessionManager', () => {
|
|
|
224
224
|
expect(events).toHaveLength(1); // No new events after unsubscribe
|
|
225
225
|
});
|
|
226
226
|
});
|
|
227
|
+
|
|
228
|
+
// -------------------------------------------------------------------------
|
|
229
|
+
// PERF-2a: Debounced SQLite write batching
|
|
230
|
+
// -------------------------------------------------------------------------
|
|
231
|
+
describe('PERF-2a: debounced SQLite flush (saveConversationAndStats)', () => {
|
|
232
|
+
let fastManager: SessionManager;
|
|
233
|
+
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
// flushDebounceMs=0 triggers immediate flush for deterministic tests
|
|
236
|
+
fastManager = new SessionManager(db, { flushDebounceMs: 0 });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('saveConversationAndStats flushes immediately when flushDebounceMs=0', () => {
|
|
240
|
+
const session = fastManager.create({ name: 'Flush Test' });
|
|
241
|
+
const msgs = [{ role: 'user' as const, content: 'hello' }];
|
|
242
|
+
fastManager.saveConversationAndStats(session.id, msgs, { tokenCount: 10 });
|
|
243
|
+
|
|
244
|
+
const loaded = fastManager.loadConversation(session.id);
|
|
245
|
+
expect(loaded).toHaveLength(1);
|
|
246
|
+
expect(loaded[0].content).toBe('hello');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('multiple saveConversationAndStats calls merge stats before flush', () => {
|
|
250
|
+
const session = fastManager.create({ name: 'Merge Test' });
|
|
251
|
+
const msgs = [{ role: 'user' as const, content: 'first' }];
|
|
252
|
+
// First call (with debounce=0, flushes immediately per-call)
|
|
253
|
+
fastManager.saveConversationAndStats(session.id, msgs, { tokenCount: 5 });
|
|
254
|
+
const loaded = fastManager.loadConversation(session.id);
|
|
255
|
+
expect(loaded[0].content).toBe('first');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('flushAll() persists pending writes when called explicitly', () => {
|
|
259
|
+
// Use a high debounce so writes are pending
|
|
260
|
+
const deferredManager = new SessionManager(db, { flushDebounceMs: 60_000 });
|
|
261
|
+
const session = deferredManager.create({ name: 'Deferred' });
|
|
262
|
+
const msgs = [{ role: 'assistant' as const, content: 'deferred write' }];
|
|
263
|
+
deferredManager.saveConversationAndStats(session.id, msgs, {});
|
|
264
|
+
|
|
265
|
+
// Without flushAll, nothing is written yet
|
|
266
|
+
// (flushDebounceMs=0 manager can't verify this without timing, but we can
|
|
267
|
+
// verify flushAll itself writes correctly)
|
|
268
|
+
deferredManager.flushAll();
|
|
269
|
+
|
|
270
|
+
const loaded = deferredManager.loadConversation(session.id);
|
|
271
|
+
expect(loaded[0].content).toBe('deferred write');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('flushAll() is a no-op when nothing is pending', () => {
|
|
275
|
+
// Should not throw
|
|
276
|
+
expect(() => fastManager.flushAll()).not.toThrow();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('constructor accepts flushDebounceMs option', () => {
|
|
280
|
+
const m = new SessionManager(db, { flushDebounceMs: 1000 });
|
|
281
|
+
expect(m).toBeInstanceOf(SessionManager);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ===========================================================================
|
|
287
|
+
// M2: Session rename
|
|
288
|
+
// ===========================================================================
|
|
289
|
+
|
|
290
|
+
describe('SessionManager.rename (M2)', () => {
|
|
291
|
+
let renameDb: Database;
|
|
292
|
+
let renameManager: SessionManager;
|
|
293
|
+
|
|
294
|
+
beforeEach(() => {
|
|
295
|
+
renameDb = new Database(':memory:');
|
|
296
|
+
renameDb.exec('PRAGMA journal_mode=WAL');
|
|
297
|
+
renameDb.exec('PRAGMA foreign_keys=ON');
|
|
298
|
+
SessionManager.resetInstance();
|
|
299
|
+
renameManager = new SessionManager(renameDb);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
afterEach(() => {
|
|
303
|
+
renameDb.close();
|
|
304
|
+
SessionManager.resetInstance();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('rename updates the session name', () => {
|
|
308
|
+
const session = renameManager.create({ name: 'chat-2026-01-01T10:00' });
|
|
309
|
+
renameManager.rename(session.id, 'deploy-staging-app');
|
|
310
|
+
const updated = renameManager.get(session.id);
|
|
311
|
+
expect(updated?.name).toBe('deploy-staging-app');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('rename is a no-op for non-existent session (no throw)', () => {
|
|
315
|
+
expect(() => renameManager.rename('non-existent-id', 'new-name')).not.toThrow();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('rename accepts empty string', () => {
|
|
319
|
+
const session = renameManager.create({ name: 'original' });
|
|
320
|
+
expect(() => renameManager.rename(session.id, '')).not.toThrow();
|
|
321
|
+
});
|
|
227
322
|
});
|