@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,56 @@
1
+ /**
2
+ * Auth Module - Barrel Export
3
+ * Exports all authentication-related functionality
4
+ */
5
+
6
+ // Types
7
+ export type {
8
+ LLMProviderName,
9
+ GitHubIdentity,
10
+ LLMProviderCredential,
11
+ AuthFile,
12
+ AuthIdentity,
13
+ AuthProviders,
14
+ AuthStatus,
15
+ LoginWizardContext,
16
+ ProviderInfo,
17
+ GitHubDeviceCodeResponse,
18
+ GitHubAccessTokenResponse,
19
+ GitHubUserResponse,
20
+ GitHubEmailResponse,
21
+ ProviderValidationResult,
22
+ } from './types';
23
+
24
+ // Store
25
+ import { AuthStore, authStore } from './store';
26
+ export { AuthStore, authStore };
27
+
28
+ // Providers
29
+ export {
30
+ PROVIDER_REGISTRY,
31
+ getProviderInfo,
32
+ getProviderNames,
33
+ getDefaultModel,
34
+ validateProviderApiKey,
35
+ } from './providers';
36
+
37
+ // OAuth
38
+ export {
39
+ GitHubDeviceFlow,
40
+ BrowserOAuthServer,
41
+ fetchGitHubUser,
42
+ fetchGitHubEmail,
43
+ completeGitHubAuth,
44
+ exchangeCodeForToken,
45
+ } from './oauth';
46
+
47
+ // Guard
48
+ export { requiresAuth, isAuthenticated, getAuthMessage } from './guard';
49
+
50
+ // SSO Device Flow
51
+ export { SSODeviceFlow, validateSSOToken } from './sso';
52
+
53
+ // Helper to get auth store instance
54
+ export function getAuthStore(): AuthStore {
55
+ return authStore;
56
+ }
@@ -0,0 +1,455 @@
1
+ /**
2
+ * GitHub OAuth Implementation
3
+ * Primary: Device Flow (works in SSH/headless)
4
+ * Fallback: Browser-based OAuth with local callback server
5
+ */
6
+
7
+ import type {
8
+ GitHubDeviceCodeResponse,
9
+ GitHubAccessTokenResponse,
10
+ GitHubUserResponse,
11
+ GitHubEmailResponse,
12
+ GitHubIdentity,
13
+ } from './types';
14
+
15
+ /**
16
+ * GitHub OAuth App Client ID
17
+ * This is safe to commit - it's a public identifier for the OAuth app
18
+ * The OAuth app must be registered at github.com/settings/developers
19
+ *
20
+ * Note: Until the OAuth App is registered, the GitHub identity step
21
+ * will be deferred/skipped. LLM provider setup works independently.
22
+ */
23
+ const GITHUB_CLIENT_ID = 'Ov23liPzN7sAjwDsqUcx';
24
+
25
+ /**
26
+ * Callback server port for browser-based OAuth fallback
27
+ */
28
+ const CALLBACK_PORT = 19284;
29
+
30
+ /**
31
+ * GitHub Device Flow OAuth
32
+ * Works in SSH sessions and headless environments
33
+ */
34
+ export class GitHubDeviceFlow {
35
+ private clientId: string;
36
+ private deviceCode: string | null = null;
37
+ private interval: number = 5;
38
+ private expiresAt: number = 0;
39
+
40
+ constructor(clientId: string = GITHUB_CLIENT_ID) {
41
+ this.clientId = clientId;
42
+ }
43
+
44
+ /**
45
+ * Step 1: Request device code from GitHub
46
+ * Returns the user code that must be entered at github.com/login/device
47
+ */
48
+ async requestDeviceCode(): Promise<GitHubDeviceCodeResponse> {
49
+ const response = await fetch('https://github.com/login/device/code', {
50
+ method: 'POST',
51
+ headers: {
52
+ Accept: 'application/json',
53
+ 'Content-Type': 'application/x-www-form-urlencoded',
54
+ },
55
+ body: new URLSearchParams({
56
+ client_id: this.clientId,
57
+ scope: 'read:user user:email',
58
+ }),
59
+ });
60
+
61
+ if (!response.ok) {
62
+ const text = await response.text();
63
+ throw new Error(`Failed to request device code: ${response.status} ${text}`);
64
+ }
65
+
66
+ const data = (await response.json()) as GitHubDeviceCodeResponse;
67
+
68
+ // Store for polling
69
+ this.deviceCode = data.device_code;
70
+ this.interval = data.interval || 5;
71
+ this.expiresAt = Date.now() + data.expires_in * 1000;
72
+
73
+ return data;
74
+ }
75
+
76
+ /**
77
+ * Step 2: Poll for access token
78
+ * Call this repeatedly until it returns a token or throws an error
79
+ */
80
+ async pollForToken(): Promise<GitHubAccessTokenResponse> {
81
+ if (!this.deviceCode) {
82
+ throw new Error('Device code not requested. Call requestDeviceCode first.');
83
+ }
84
+
85
+ if (Date.now() > this.expiresAt) {
86
+ throw new Error('Device code expired. Please start the login process again.');
87
+ }
88
+
89
+ const response = await fetch('https://github.com/login/oauth/access_token', {
90
+ method: 'POST',
91
+ headers: {
92
+ Accept: 'application/json',
93
+ 'Content-Type': 'application/x-www-form-urlencoded',
94
+ },
95
+ body: new URLSearchParams({
96
+ client_id: this.clientId,
97
+ device_code: this.deviceCode,
98
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
99
+ }),
100
+ });
101
+
102
+ if (!response.ok) {
103
+ const text = await response.text();
104
+ throw new Error(`Failed to poll for token: ${response.status} ${text}`);
105
+ }
106
+
107
+ return (await response.json()) as GitHubAccessTokenResponse;
108
+ }
109
+
110
+ /**
111
+ * Get the polling interval in milliseconds
112
+ */
113
+ getPollingInterval(): number {
114
+ return this.interval * 1000;
115
+ }
116
+
117
+ /**
118
+ * Wait for authorization by polling
119
+ * Returns the access token when the user completes authorization
120
+ */
121
+ async waitForAuthorization(onPoll?: () => void, abortSignal?: AbortSignal): Promise<string> {
122
+ for (;;) {
123
+ if (abortSignal?.aborted) {
124
+ throw new Error('Authorization cancelled');
125
+ }
126
+
127
+ const result = await this.pollForToken();
128
+
129
+ if (result.access_token) {
130
+ return result.access_token;
131
+ }
132
+
133
+ if (result.error === 'authorization_pending') {
134
+ // User hasn't authorized yet, keep polling
135
+ onPoll?.();
136
+ await this.sleep(this.getPollingInterval());
137
+ continue;
138
+ }
139
+
140
+ if (result.error === 'slow_down') {
141
+ // GitHub is asking us to slow down
142
+ this.interval += 5;
143
+ await this.sleep(this.getPollingInterval());
144
+ continue;
145
+ }
146
+
147
+ if (result.error === 'expired_token') {
148
+ throw new Error('Device code expired. Please start the login process again.');
149
+ }
150
+
151
+ if (result.error === 'access_denied') {
152
+ throw new Error('Authorization was denied by the user.');
153
+ }
154
+
155
+ // Unknown error
156
+ throw new Error(result.error_description || result.error || 'Unknown authorization error');
157
+ }
158
+ }
159
+
160
+ private sleep(ms: number): Promise<void> {
161
+ return new Promise(resolve => setTimeout(resolve, ms));
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Fetch GitHub user profile using access token
167
+ */
168
+ export async function fetchGitHubUser(accessToken: string): Promise<GitHubUserResponse> {
169
+ const response = await fetch('https://api.github.com/user', {
170
+ headers: {
171
+ Authorization: `Bearer ${accessToken}`,
172
+ Accept: 'application/vnd.github.v3+json',
173
+ 'User-Agent': 'Nimbus-CLI',
174
+ },
175
+ });
176
+
177
+ if (!response.ok) {
178
+ const text = await response.text();
179
+ throw new Error(`Failed to fetch user profile: ${response.status} ${text}`);
180
+ }
181
+
182
+ return (await response.json()) as GitHubUserResponse;
183
+ }
184
+
185
+ /**
186
+ * Fetch GitHub user's primary email
187
+ */
188
+ export async function fetchGitHubEmail(accessToken: string): Promise<string | null> {
189
+ const response = await fetch('https://api.github.com/user/emails', {
190
+ headers: {
191
+ Authorization: `Bearer ${accessToken}`,
192
+ Accept: 'application/vnd.github.v3+json',
193
+ 'User-Agent': 'Nimbus-CLI',
194
+ },
195
+ });
196
+
197
+ if (!response.ok) {
198
+ // Email access might be restricted, return null
199
+ return null;
200
+ }
201
+
202
+ const emails = (await response.json()) as GitHubEmailResponse[];
203
+ const primaryEmail = emails.find(e => e.primary && e.verified);
204
+ return primaryEmail?.email || emails[0]?.email || null;
205
+ }
206
+
207
+ /**
208
+ * Complete GitHub authentication flow
209
+ * Returns a GitHubIdentity object ready to store
210
+ */
211
+ export async function completeGitHubAuth(accessToken: string): Promise<GitHubIdentity> {
212
+ const [user, email] = await Promise.all([
213
+ fetchGitHubUser(accessToken),
214
+ fetchGitHubEmail(accessToken),
215
+ ]);
216
+
217
+ return {
218
+ username: user.login,
219
+ name: user.name,
220
+ email: email || user.email,
221
+ avatarUrl: user.avatar_url,
222
+ accessToken,
223
+ authenticatedAt: new Date().toISOString(),
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Browser-based OAuth callback server (fallback)
229
+ * Creates a temporary local server to receive the OAuth callback
230
+ */
231
+ export class BrowserOAuthServer {
232
+ private server: ReturnType<typeof Bun.serve> | null = null;
233
+ private clientId: string;
234
+ private codePromise: Promise<string> | null = null;
235
+ private codeResolve: ((code: string) => void) | null = null;
236
+ private codeReject: ((error: Error) => void) | null = null;
237
+
238
+ constructor(clientId: string = GITHUB_CLIENT_ID) {
239
+ this.clientId = clientId;
240
+ }
241
+
242
+ /**
243
+ * Start the callback server and return the authorization URL
244
+ */
245
+ async start(): Promise<string> {
246
+ // Create promise for receiving the code
247
+ this.codePromise = new Promise((resolve, reject) => {
248
+ this.codeResolve = resolve;
249
+ this.codeReject = reject;
250
+ });
251
+
252
+ // Start the server
253
+ this.server = Bun.serve({
254
+ port: CALLBACK_PORT,
255
+ fetch: request => this.handleRequest(request),
256
+ });
257
+
258
+ // Build authorization URL
259
+ const params = new URLSearchParams({
260
+ client_id: this.clientId,
261
+ redirect_uri: `http://localhost:${CALLBACK_PORT}/callback`,
262
+ scope: 'read:user user:email',
263
+ state: this.generateState(),
264
+ });
265
+
266
+ return `https://github.com/login/oauth/authorize?${params}`;
267
+ }
268
+
269
+ /**
270
+ * Wait for the authorization code
271
+ */
272
+ async waitForCode(timeout: number = 300000): Promise<string> {
273
+ if (!this.codePromise) {
274
+ throw new Error('Server not started. Call start() first.');
275
+ }
276
+
277
+ const timeoutPromise = new Promise<never>((_, reject) => {
278
+ setTimeout(() => {
279
+ reject(new Error('Authorization timed out'));
280
+ }, timeout);
281
+ });
282
+
283
+ try {
284
+ return await Promise.race([this.codePromise, timeoutPromise]);
285
+ } finally {
286
+ this.stop();
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Stop the callback server
292
+ */
293
+ stop(): void {
294
+ if (this.server) {
295
+ this.server.stop();
296
+ this.server = null;
297
+ }
298
+ }
299
+
300
+ private handleRequest(request: Request): Response {
301
+ const url = new URL(request.url);
302
+
303
+ if (url.pathname === '/callback') {
304
+ const code = url.searchParams.get('code');
305
+ const error = url.searchParams.get('error');
306
+ const errorDescription = url.searchParams.get('error_description');
307
+
308
+ if (error) {
309
+ this.codeReject?.(new Error(errorDescription || error));
310
+ return new Response(this.getErrorPage(errorDescription || error), {
311
+ headers: { 'Content-Type': 'text/html' },
312
+ });
313
+ }
314
+
315
+ if (code) {
316
+ this.codeResolve?.(code);
317
+ return new Response(this.getSuccessPage(), {
318
+ headers: { 'Content-Type': 'text/html' },
319
+ });
320
+ }
321
+
322
+ return new Response('Bad request', { status: 400 });
323
+ }
324
+
325
+ return new Response('Not found', { status: 404 });
326
+ }
327
+
328
+ private generateState(): string {
329
+ const array = new Uint8Array(16);
330
+ crypto.getRandomValues(array);
331
+ return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
332
+ }
333
+
334
+ private getSuccessPage(): string {
335
+ return `
336
+ <!DOCTYPE html>
337
+ <html>
338
+ <head>
339
+ <title>Nimbus CLI - Authorization Successful</title>
340
+ <style>
341
+ body {
342
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
343
+ display: flex;
344
+ justify-content: center;
345
+ align-items: center;
346
+ height: 100vh;
347
+ margin: 0;
348
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
349
+ color: white;
350
+ }
351
+ .container {
352
+ text-align: center;
353
+ padding: 40px;
354
+ background: rgba(255,255,255,0.1);
355
+ border-radius: 16px;
356
+ backdrop-filter: blur(10px);
357
+ }
358
+ h1 { margin-bottom: 10px; }
359
+ p { opacity: 0.9; }
360
+ </style>
361
+ </head>
362
+ <body>
363
+ <div class="container">
364
+ <h1>✓ Authorization Successful</h1>
365
+ <p>You can close this window and return to the terminal.</p>
366
+ </div>
367
+ </body>
368
+ </html>`;
369
+ }
370
+
371
+ private getErrorPage(error: string): string {
372
+ return `
373
+ <!DOCTYPE html>
374
+ <html>
375
+ <head>
376
+ <title>Nimbus CLI - Authorization Failed</title>
377
+ <style>
378
+ body {
379
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
380
+ display: flex;
381
+ justify-content: center;
382
+ align-items: center;
383
+ height: 100vh;
384
+ margin: 0;
385
+ background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%);
386
+ color: white;
387
+ }
388
+ .container {
389
+ text-align: center;
390
+ padding: 40px;
391
+ background: rgba(255,255,255,0.1);
392
+ border-radius: 16px;
393
+ backdrop-filter: blur(10px);
394
+ }
395
+ h1 { margin-bottom: 10px; }
396
+ p { opacity: 0.9; }
397
+ </style>
398
+ </head>
399
+ <body>
400
+ <div class="container">
401
+ <h1>✗ Authorization Failed</h1>
402
+ <p>${error}</p>
403
+ <p>Please close this window and try again.</p>
404
+ </div>
405
+ </body>
406
+ </html>`;
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Exchange authorization code for access token (browser flow)
412
+ * Note: This requires a client secret which should not be in CLI code
413
+ * For true CLI-only auth, use the Device Flow instead
414
+ */
415
+ export async function exchangeCodeForToken(
416
+ code: string,
417
+ clientId: string = GITHUB_CLIENT_ID,
418
+ clientSecret?: string
419
+ ): Promise<string> {
420
+ if (!clientSecret) {
421
+ throw new Error(
422
+ 'Browser-based OAuth requires a client secret. Use Device Flow for CLI-only auth.'
423
+ );
424
+ }
425
+
426
+ const response = await fetch('https://github.com/login/oauth/access_token', {
427
+ method: 'POST',
428
+ headers: {
429
+ Accept: 'application/json',
430
+ 'Content-Type': 'application/x-www-form-urlencoded',
431
+ },
432
+ body: new URLSearchParams({
433
+ client_id: clientId,
434
+ client_secret: clientSecret,
435
+ code,
436
+ }),
437
+ });
438
+
439
+ if (!response.ok) {
440
+ const text = await response.text();
441
+ throw new Error(`Failed to exchange code for token: ${response.status} ${text}`);
442
+ }
443
+
444
+ const data = (await response.json()) as GitHubAccessTokenResponse;
445
+
446
+ if (data.error) {
447
+ throw new Error(data.error_description || data.error);
448
+ }
449
+
450
+ if (!data.access_token) {
451
+ throw new Error('No access token in response');
452
+ }
453
+
454
+ return data.access_token;
455
+ }