@build-astron-co/nimbus 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,550 @@
1
+ /**
2
+ * Upgrade Command
3
+ *
4
+ * Checks for the latest Nimbus version and offers to upgrade.
5
+ * Supports npm registry, GitHub Releases binary download, and Homebrew.
6
+ *
7
+ * Detects the installation method and executes the appropriate upgrade
8
+ * command automatically (with user confirmation unless --force is passed).
9
+ */
10
+
11
+ import { VERSION } from '../version';
12
+
13
+ /** GitHub repository used for release downloads. */
14
+ const GITHUB_REPO = 'the-ai-project-co/nimbus';
15
+
16
+ /** npm package name (may not be published yet). */
17
+ const NPM_PACKAGE = '@build-astron-co/nimbus';
18
+
19
+ /** Homebrew tap name for the formula. */
20
+ const HOMEBREW_TAP = 'the-ai-project-co/tap/nimbus';
21
+
22
+ export interface UpgradeOptions {
23
+ /** Skip confirmation prompt */
24
+ force?: boolean;
25
+ /** Check only, don't actually upgrade */
26
+ check?: boolean;
27
+ }
28
+
29
+ /** Detected installation method for nimbus. */
30
+ type InstallMethod = 'homebrew' | 'npm' | 'bun' | 'binary' | 'unknown';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // ANSI helpers (avoid importing the full wizard/ui for this standalone command)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const c = {
37
+ reset: '\x1b[0m',
38
+ bold: '\x1b[1m',
39
+ dim: '\x1b[2m',
40
+ green: '\x1b[32m',
41
+ yellow: '\x1b[33m',
42
+ red: '\x1b[31m',
43
+ cyan: '\x1b[36m',
44
+ };
45
+
46
+ function bold(s: string): string {
47
+ return `${c.bold}${s}${c.reset}`;
48
+ }
49
+ function green(s: string): string {
50
+ return `${c.green}${s}${c.reset}`;
51
+ }
52
+ function yellow(s: string): string {
53
+ return `${c.yellow}${s}${c.reset}`;
54
+ }
55
+ function red(s: string): string {
56
+ return `${c.red}${s}${c.reset}`;
57
+ }
58
+ function dim(s: string): string {
59
+ return `${c.dim}${s}${c.reset}`;
60
+ }
61
+ function cyan(s: string): string {
62
+ return `${c.cyan}${s}${c.reset}`;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Spinner
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const SPINNER_FRAMES = [
70
+ '\u28CB',
71
+ '\u2819',
72
+ '\u2839',
73
+ '\u2838',
74
+ '\u283C',
75
+ '\u2834',
76
+ '\u2826',
77
+ '\u2827',
78
+ '\u2807',
79
+ '\u280F',
80
+ ];
81
+
82
+ interface Spinner {
83
+ update(msg: string): void;
84
+ success(msg: string): void;
85
+ fail(msg: string): void;
86
+ }
87
+
88
+ function startSpinner(message: string): Spinner {
89
+ let frame = 0;
90
+ let currentMsg = message;
91
+
92
+ const interval = setInterval(() => {
93
+ const f = cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
94
+ process.stderr.write(`\r\x1b[K ${f} ${currentMsg}`);
95
+ frame++;
96
+ }, 80);
97
+
98
+ return {
99
+ update(msg: string) {
100
+ currentMsg = msg;
101
+ },
102
+ success(msg: string) {
103
+ clearInterval(interval);
104
+ process.stderr.write(`\r\x1b[K ${green('\u2714')} ${msg}\n`);
105
+ },
106
+ fail(msg: string) {
107
+ clearInterval(interval);
108
+ process.stderr.write(`\r\x1b[K ${red('\u2716')} ${msg}\n`);
109
+ },
110
+ };
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Confirmation prompt
115
+ // ---------------------------------------------------------------------------
116
+
117
+ async function confirmPrompt(message: string, defaultYes = true): Promise<boolean> {
118
+ const { createInterface } = await import('node:readline');
119
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
120
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
121
+
122
+ return new Promise(resolve => {
123
+ rl.question(` ${message} ${hint} `, answer => {
124
+ rl.close();
125
+ const trimmed = answer.trim().toLowerCase();
126
+ if (!trimmed) {
127
+ resolve(defaultYes);
128
+ } else {
129
+ resolve(trimmed === 'y' || trimmed === 'yes');
130
+ }
131
+ });
132
+ });
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Version fetching
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /**
140
+ * Try fetching the latest version from the npm registry.
141
+ * Returns the version string on success, or `null` when the package is not
142
+ * published or the registry is unreachable.
143
+ */
144
+ async function fetchNpmVersion(): Promise<string | null> {
145
+ try {
146
+ const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`, {
147
+ signal: AbortSignal.timeout(5000),
148
+ });
149
+ if (!response.ok) {
150
+ return null;
151
+ }
152
+ const data = (await response.json()) as { version?: string };
153
+ return data.version ?? null;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Try fetching the latest release tag from GitHub Releases.
161
+ * Returns the tag name (e.g. `'v0.3.0'`) on success, or `null` when the
162
+ * API is unreachable or there are no releases.
163
+ */
164
+ async function fetchGitHubReleaseVersion(): Promise<string | null> {
165
+ try {
166
+ const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, {
167
+ headers: { Accept: 'application/vnd.github+json' },
168
+ signal: AbortSignal.timeout(5000),
169
+ });
170
+ if (!response.ok) {
171
+ return null;
172
+ }
173
+ const data = (await response.json()) as { tag_name?: string };
174
+ // Strip leading 'v' from tag name (e.g. 'v0.3.0' -> '0.3.0')
175
+ const tag = data.tag_name ?? null;
176
+ return tag ? tag.replace(/^v/, '') : null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Installation method detection
184
+ // ---------------------------------------------------------------------------
185
+
186
+ interface DetectionResult {
187
+ method: InstallMethod;
188
+ /** Human-readable description of what was detected. */
189
+ detail: string;
190
+ }
191
+
192
+ /**
193
+ * Detect how nimbus was installed by probing Homebrew, npm, bun, and the
194
+ * binary path. Detection commands are given a short timeout so they never
195
+ * block the CLI for too long.
196
+ */
197
+ async function detectInstallMethod(): Promise<DetectionResult> {
198
+ const { execSync } = await import('node:child_process');
199
+ const execOpts = {
200
+ encoding: 'utf-8' as const,
201
+ stdio: ['pipe', 'pipe', 'pipe'] as ['pipe', 'pipe', 'pipe'],
202
+ timeout: 10_000,
203
+ };
204
+
205
+ // 1. Homebrew
206
+ try {
207
+ const brewList = execSync('brew list --formula 2>/dev/null', execOpts);
208
+ if (brewList.includes('nimbus')) {
209
+ return { method: 'homebrew', detail: `Homebrew formula (${HOMEBREW_TAP})` };
210
+ }
211
+ } catch {
212
+ /* not homebrew */
213
+ }
214
+
215
+ // 2. npm global
216
+ try {
217
+ const npmList = execSync(`npm list -g ${NPM_PACKAGE} 2>/dev/null`, execOpts);
218
+ if (npmList.includes(NPM_PACKAGE)) {
219
+ return { method: 'npm', detail: `npm global package (${NPM_PACKAGE})` };
220
+ }
221
+ } catch {
222
+ /* not npm */
223
+ }
224
+
225
+ // 3. bun global
226
+ try {
227
+ const bunList = execSync('bun pm ls -g 2>/dev/null', execOpts);
228
+ if (bunList.includes(NPM_PACKAGE)) {
229
+ return { method: 'bun', detail: `bun global package (${NPM_PACKAGE})` };
230
+ }
231
+ } catch {
232
+ /* not bun */
233
+ }
234
+
235
+ // 4. Compiled binary (not running through bun/node)
236
+ const argv0 = process.argv[0] ?? '';
237
+ const isBinary = !argv0.includes('bun') && !argv0.includes('node');
238
+ if (isBinary) {
239
+ return { method: 'binary', detail: `standalone binary (${argv0 || '/usr/local/bin/nimbus'})` };
240
+ }
241
+
242
+ return { method: 'unknown', detail: 'could not determine installation method' };
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Upgrade execution helpers
247
+ // ---------------------------------------------------------------------------
248
+
249
+ /**
250
+ * Run a shell command, streaming output to the terminal.
251
+ * Throws on non-zero exit code.
252
+ */
253
+ async function runShellCommand(cmd: string, timeoutMs = 120_000): Promise<void> {
254
+ const { execSync } = await import('node:child_process');
255
+ execSync(cmd, { stdio: 'inherit', timeout: timeoutMs });
256
+ }
257
+
258
+ /**
259
+ * Verify the upgrade by checking the installed version.
260
+ * Returns the new version string, or null if verification failed.
261
+ */
262
+ async function verifyUpgrade(): Promise<string | null> {
263
+ const { execSync } = await import('node:child_process');
264
+ try {
265
+ const output = execSync('nimbus --version 2>/dev/null', {
266
+ encoding: 'utf-8' as const,
267
+ stdio: ['pipe', 'pipe', 'pipe'] as ['pipe', 'pipe', 'pipe'],
268
+ timeout: 10_000,
269
+ });
270
+ // Output looks like "nimbus 0.3.0" or just "0.3.0"
271
+ const match = output.trim().match(/(\d+\.\d+\.\d+)/);
272
+ return match ? match[1] : null;
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Execute the upgrade via Homebrew.
280
+ */
281
+ async function upgradeViaHomebrew(spinner: Spinner): Promise<void> {
282
+ spinner.update('Updating Homebrew tap...');
283
+ try {
284
+ await runShellCommand(`brew upgrade ${HOMEBREW_TAP} 2>&1`);
285
+ } catch {
286
+ // If the tap formula isn't found, try the short name
287
+ spinner.update('Trying brew upgrade nimbus...');
288
+ await runShellCommand('brew upgrade nimbus 2>&1');
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Execute the upgrade via npm.
294
+ */
295
+ async function upgradeViaNpm(spinner: Spinner): Promise<void> {
296
+ spinner.update(`Installing ${NPM_PACKAGE}@latest via npm...`);
297
+ await runShellCommand(`npm install -g ${NPM_PACKAGE}@latest 2>&1`);
298
+ }
299
+
300
+ /**
301
+ * Execute the upgrade via bun.
302
+ */
303
+ async function upgradeViaBun(spinner: Spinner): Promise<void> {
304
+ spinner.update(`Installing ${NPM_PACKAGE}@latest via bun...`);
305
+ await runShellCommand(`bun install -g ${NPM_PACKAGE}@latest 2>&1`);
306
+ }
307
+
308
+ /**
309
+ * Execute the upgrade by downloading a compiled binary from GitHub Releases.
310
+ */
311
+ async function upgradeViaBinaryDownload(latestVersion: string, spinner: Spinner): Promise<void> {
312
+ const platform =
313
+ process.platform === 'darwin' ? 'darwin' : process.platform === 'linux' ? 'linux' : null;
314
+ const arch = process.arch === 'arm64' ? 'arm64' : process.arch === 'x64' ? 'x64' : null;
315
+
316
+ if (!platform || !arch) {
317
+ throw new Error(
318
+ `Unsupported platform/architecture: ${process.platform}/${process.arch}. ` +
319
+ `Please download the binary manually from https://github.com/${GITHUB_REPO}/releases/latest`
320
+ );
321
+ }
322
+
323
+ const assetName = `nimbus-${platform}-${arch}`;
324
+ const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latestVersion}/${assetName}`;
325
+ const binaryPath = process.argv[0] || '/usr/local/bin/nimbus';
326
+ const tmpPath = `${binaryPath}.upgrade-tmp`;
327
+
328
+ spinner.update(`Downloading ${assetName} v${latestVersion}...`);
329
+
330
+ // Download to a temp file, make executable, then atomically replace
331
+ const cmd = [
332
+ `curl -fsSL "${downloadUrl}" -o "${tmpPath}"`,
333
+ `chmod +x "${tmpPath}"`,
334
+ `mv "${tmpPath}" "${binaryPath}"`,
335
+ ].join(' && ');
336
+
337
+ try {
338
+ await runShellCommand(cmd, 120_000);
339
+ } catch (err) {
340
+ // Clean up temp file on failure
341
+ try {
342
+ const fs = await import('node:fs');
343
+ if (fs.existsSync(tmpPath)) {
344
+ fs.unlinkSync(tmpPath);
345
+ }
346
+ } catch {
347
+ /* best effort */
348
+ }
349
+ throw err;
350
+ }
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // Main upgrade command
355
+ // ---------------------------------------------------------------------------
356
+
357
+ /**
358
+ * Check for and install the latest version of Nimbus.
359
+ */
360
+ export async function upgradeCommand(options: UpgradeOptions = {}): Promise<void> {
361
+ console.log(`Current version: ${bold(VERSION)}`);
362
+ console.log('Checking for updates...\n');
363
+
364
+ // Try npm registry first, then fall back to GitHub Releases
365
+ let latestVersion = await fetchNpmVersion();
366
+ let source: 'npm' | 'github' | 'none' = latestVersion ? 'npm' : 'none';
367
+
368
+ if (!latestVersion) {
369
+ latestVersion = await fetchGitHubReleaseVersion();
370
+ if (latestVersion) {
371
+ source = 'github';
372
+ }
373
+ }
374
+
375
+ if (!latestVersion) {
376
+ console.log(
377
+ 'No updates available (package not yet published to npm, and no GitHub releases found).'
378
+ );
379
+ console.log(`You are on version ${VERSION}.`);
380
+ return;
381
+ }
382
+
383
+ if (latestVersion === VERSION) {
384
+ console.log(green(`You're already on the latest version (${VERSION}).`));
385
+ return;
386
+ }
387
+
388
+ // Simple semver comparison to check if remote is actually newer
389
+ const currentParts = VERSION.split('.').map(Number);
390
+ const remoteParts = latestVersion.split('.').map(Number);
391
+ let isNewer = false;
392
+ for (let i = 0; i < 3; i++) {
393
+ if ((remoteParts[i] ?? 0) > (currentParts[i] ?? 0)) {
394
+ isNewer = true;
395
+ break;
396
+ }
397
+ if ((remoteParts[i] ?? 0) < (currentParts[i] ?? 0)) {
398
+ break;
399
+ }
400
+ }
401
+
402
+ if (!isNewer) {
403
+ console.log(green(`You're already on the latest version (${VERSION}).`));
404
+ return;
405
+ }
406
+
407
+ console.log(
408
+ `${yellow('New version available:')} ${dim(VERSION)} ${dim('->')} ${bold(green(latestVersion))}`
409
+ );
410
+ if (source === 'github') {
411
+ console.log(dim(` Source: GitHub Releases (https://github.com/${GITHUB_REPO}/releases)`));
412
+ }
413
+ console.log('');
414
+
415
+ if (options.check) {
416
+ return;
417
+ }
418
+
419
+ // Detect how nimbus was installed
420
+ const detection = await detectInstallMethod();
421
+ console.log(`Detected installation: ${bold(detection.detail)}`);
422
+ console.log('');
423
+
424
+ // For unknown install methods, just print manual instructions
425
+ if (detection.method === 'unknown') {
426
+ printManualInstructions(source);
427
+ return;
428
+ }
429
+
430
+ // Ask for confirmation unless --force
431
+ if (!options.force) {
432
+ const proceed = await confirmPrompt(`Upgrade nimbus ${VERSION} -> ${latestVersion}?`, true);
433
+ if (!proceed) {
434
+ console.log('\nUpgrade cancelled.');
435
+ return;
436
+ }
437
+ console.log('');
438
+ }
439
+
440
+ // Execute the upgrade
441
+ const spinner = startSpinner('Upgrading nimbus...');
442
+
443
+ try {
444
+ switch (detection.method) {
445
+ case 'homebrew':
446
+ await upgradeViaHomebrew(spinner);
447
+ break;
448
+ case 'npm':
449
+ await upgradeViaNpm(spinner);
450
+ break;
451
+ case 'bun':
452
+ await upgradeViaBun(spinner);
453
+ break;
454
+ case 'binary':
455
+ if (source !== 'github') {
456
+ spinner.fail('Binary upgrade requires GitHub Releases, but no release was found.');
457
+ printManualInstructions(source);
458
+ return;
459
+ }
460
+ await upgradeViaBinaryDownload(latestVersion, spinner);
461
+ break;
462
+ }
463
+
464
+ spinner.success('Upgrade command completed');
465
+ } catch (error: any) {
466
+ const msg = error.message || String(error);
467
+ spinner.fail(`Upgrade failed: ${msg}`);
468
+ console.log('');
469
+
470
+ // Provide recovery instructions
471
+ console.log(yellow('You can try upgrading manually:'));
472
+ printUpgradeInstructionForMethod(detection.method, latestVersion, source);
473
+ return;
474
+ }
475
+
476
+ // Verify the upgrade succeeded
477
+ console.log('');
478
+ const verifySpinner = startSpinner('Verifying upgrade...');
479
+ const newVersion = await verifyUpgrade();
480
+
481
+ if (newVersion && newVersion === latestVersion) {
482
+ verifySpinner.success(`Successfully upgraded to ${bold(green(newVersion))}`);
483
+ } else if (newVersion && newVersion !== VERSION) {
484
+ verifySpinner.success(`Upgraded to ${bold(green(newVersion))}`);
485
+ } else if (detection.method === 'binary') {
486
+ // For binary upgrades, the current process is the old binary so
487
+ // `nimbus --version` will report the new version only on next launch.
488
+ verifySpinner.success(`Binary replaced. Restart nimbus to use ${bold(green(latestVersion))}.`);
489
+ } else {
490
+ verifySpinner.fail('Could not verify the new version. Please run `nimbus --version` to check.');
491
+ }
492
+ }
493
+
494
+ // ---------------------------------------------------------------------------
495
+ // Helper: print manual instructions (fallback for unknown install method)
496
+ // ---------------------------------------------------------------------------
497
+
498
+ function printManualInstructions(source: 'npm' | 'github' | 'none'): void {
499
+ console.log('Could not detect installation method. Upgrade manually:');
500
+ console.log('');
501
+ console.log(` ${dim('# npm')}`);
502
+ console.log(` npm install -g ${NPM_PACKAGE}@latest`);
503
+ console.log('');
504
+ console.log(` ${dim('# bun')}`);
505
+ console.log(` bun install -g ${NPM_PACKAGE}@latest`);
506
+ console.log('');
507
+ console.log(` ${dim('# Homebrew')}`);
508
+ console.log(` brew upgrade ${HOMEBREW_TAP}`);
509
+
510
+ if (source === 'github') {
511
+ console.log('');
512
+ console.log(` ${dim('# Binary download')}`);
513
+ console.log(` https://github.com/${GITHUB_REPO}/releases/latest`);
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Print the specific upgrade instruction for a detected method (used after
519
+ * an automatic upgrade fails).
520
+ */
521
+ function printUpgradeInstructionForMethod(
522
+ method: InstallMethod,
523
+ latestVersion: string,
524
+ source: 'npm' | 'github' | 'none'
525
+ ): void {
526
+ switch (method) {
527
+ case 'homebrew':
528
+ console.log(` brew upgrade ${HOMEBREW_TAP}`);
529
+ break;
530
+ case 'npm':
531
+ console.log(` npm install -g ${NPM_PACKAGE}@latest`);
532
+ break;
533
+ case 'bun':
534
+ console.log(` bun install -g ${NPM_PACKAGE}@latest`);
535
+ break;
536
+ case 'binary': {
537
+ const platform = process.platform === 'darwin' ? 'darwin' : 'linux';
538
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
539
+ const assetName = `nimbus-${platform}-${arch}`;
540
+ const url = `https://github.com/${GITHUB_REPO}/releases/download/v${latestVersion}/${assetName}`;
541
+ console.log(
542
+ ` curl -fsSL "${url}" -o /usr/local/bin/nimbus && chmod +x /usr/local/bin/nimbus`
543
+ );
544
+ break;
545
+ }
546
+ default:
547
+ printManualInstructions(source);
548
+ break;
549
+ }
550
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Usage Command
3
+ * Usage dashboard CLI command
4
+ */
5
+
6
+ import { ui } from '../../wizard/ui';
7
+ import { billingClient } from '../../clients/enterprise-client';
8
+ import type { UsageOptions } from '../../types';
9
+
10
+ /**
11
+ * Get current team ID from config or environment
12
+ */
13
+ function getCurrentTeamId(): string | null {
14
+ return process.env.NIMBUS_TEAM_ID || null;
15
+ }
16
+
17
+ /**
18
+ * Parse usage options
19
+ */
20
+ export function parseUsageOptions(args: string[]): UsageOptions {
21
+ const options: UsageOptions = {};
22
+
23
+ for (let i = 0; i < args.length; i++) {
24
+ const arg = args[i];
25
+ if (arg === '--period' && args[i + 1]) {
26
+ options.period = args[++i] as 'day' | 'week' | 'month';
27
+ } else if (arg === '--team' && args[i + 1]) {
28
+ options.teamId = args[++i];
29
+ } else if (arg === '--json') {
30
+ options.json = true;
31
+ } else if (arg === '--non-interactive') {
32
+ options.nonInteractive = true;
33
+ }
34
+ }
35
+
36
+ return options;
37
+ }
38
+
39
+ /**
40
+ * Format number with commas
41
+ */
42
+ function formatNumber(n: number): string {
43
+ return n.toLocaleString();
44
+ }
45
+
46
+ /**
47
+ * Format cost in USD
48
+ */
49
+ function formatCost(n: number): string {
50
+ return `$${n.toFixed(2)}`;
51
+ }
52
+
53
+ /**
54
+ * Usage command
55
+ */
56
+ export async function usageCommand(options: UsageOptions): Promise<void> {
57
+ try {
58
+ const teamId = options.teamId || getCurrentTeamId();
59
+ if (!teamId) {
60
+ ui.error('No team selected. Run `nimbus team switch <team-id>` first.');
61
+ ui.info('Or use: nimbus usage --team <team-id>');
62
+ return;
63
+ }
64
+
65
+ const period = options.period || 'month';
66
+
67
+ ui.startSpinner({ message: `Fetching ${period} usage...` });
68
+ const usage = await billingClient.getUsage(teamId, period);
69
+ ui.stopSpinnerSuccess('Usage retrieved');
70
+
71
+ if (options.json) {
72
+ console.log(JSON.stringify(usage, null, 2));
73
+ return;
74
+ }
75
+
76
+ ui.newLine();
77
+ ui.header(
78
+ 'Usage Dashboard',
79
+ `${new Date(usage.period.start).toLocaleDateString()} - ${new Date(usage.period.end).toLocaleDateString()}`
80
+ );
81
+
82
+ // Summary
83
+ ui.section('Summary');
84
+ ui.print(` Total Operations: ${formatNumber(usage.totals.operations)}`);
85
+ ui.print(` Total Tokens: ${formatNumber(usage.totals.tokensUsed)}`);
86
+ ui.print(` Total Cost: ${formatCost(usage.totals.costUsd)}`);
87
+
88
+ // By operation type
89
+ const operationTypes = Object.entries(usage.byOperationType);
90
+ if (operationTypes.length > 0) {
91
+ ui.section('By Operation Type');
92
+ ui.table({
93
+ columns: [
94
+ { key: 'type', header: 'Operation' },
95
+ { key: 'count', header: 'Count' },
96
+ { key: 'tokens', header: 'Tokens' },
97
+ { key: 'cost', header: 'Cost' },
98
+ ],
99
+ data: operationTypes.map(([type, data]) => ({
100
+ type,
101
+ count: formatNumber(data.count),
102
+ tokens: formatNumber(data.tokensUsed),
103
+ cost: formatCost(data.costUsd),
104
+ })),
105
+ });
106
+ }
107
+
108
+ // By user (if available)
109
+ if (usage.byUser && Object.keys(usage.byUser).length > 0) {
110
+ const userUsage = Object.entries(usage.byUser);
111
+ ui.section('By User');
112
+ ui.table({
113
+ columns: [
114
+ { key: 'user', header: 'User' },
115
+ { key: 'count', header: 'Operations' },
116
+ { key: 'tokens', header: 'Tokens' },
117
+ { key: 'cost', header: 'Cost' },
118
+ ],
119
+ data: userUsage.map(([user, data]) => ({
120
+ user,
121
+ count: formatNumber(data.count),
122
+ tokens: formatNumber(data.tokensUsed),
123
+ cost: formatCost(data.costUsd),
124
+ })),
125
+ });
126
+ }
127
+
128
+ ui.newLine();
129
+ ui.dim(`Period: ${period} | Use --period day|week|month to change`);
130
+ } catch (error: any) {
131
+ ui.stopSpinnerFail('Failed to get usage');
132
+ ui.error(error.message);
133
+ }
134
+ }