@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,55 @@
1
+ /**
2
+ * Resume Command
3
+ * Resume a task from its last checkpoint
4
+ */
5
+
6
+ import { ui } from '../wizard/ui';
7
+ import { CoreEngineClient } from '../clients/core-engine-client';
8
+
9
+ export interface ResumeOptions {
10
+ taskId?: string;
11
+ }
12
+
13
+ export async function resumeCommand(taskIdOrOptions: string | ResumeOptions = {}): Promise<void> {
14
+ const taskId = typeof taskIdOrOptions === 'string' ? taskIdOrOptions : taskIdOrOptions.taskId;
15
+
16
+ if (!taskId) {
17
+ ui.error('Task ID is required. Usage: nimbus resume <task-id>');
18
+ process.exit(1);
19
+ }
20
+
21
+ ui.header('Resume Task');
22
+ ui.info(`Task ID: ${taskId}`);
23
+ ui.newLine();
24
+
25
+ const client = new CoreEngineClient();
26
+
27
+ const available = await client.isAvailable();
28
+ if (!available) {
29
+ ui.error('Core Engine service is not available.');
30
+ process.exit(1);
31
+ }
32
+
33
+ ui.startSpinner({ message: 'Resuming task from checkpoint...' });
34
+
35
+ try {
36
+ const result = await client.resumeTask(taskId);
37
+
38
+ if (result.success) {
39
+ ui.stopSpinnerSuccess('Task resumed and completed successfully');
40
+ ui.newLine();
41
+ if (result.data) {
42
+ const executionResults = result.data.executionResults || [];
43
+ ui.print(`Steps completed: ${executionResults.length}`);
44
+ }
45
+ } else {
46
+ ui.stopSpinnerFail('Resume failed');
47
+ ui.error(result.error || 'Unknown error');
48
+ process.exit(1);
49
+ }
50
+ } catch (error: any) {
51
+ ui.stopSpinnerFail('Resume failed');
52
+ ui.error(error.message || 'Failed to resume task');
53
+ process.exit(1);
54
+ }
55
+ }
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Team Commands
3
+ * Team collaboration CLI commands
4
+ */
5
+
6
+ import { ui } from '../../wizard/ui';
7
+ import { teamClient } from '../../clients/enterprise-client';
8
+ import { getAuthStore } from '../../auth';
9
+ import type {
10
+ TeamCreateOptions,
11
+ TeamInviteOptions,
12
+ TeamMembersOptions,
13
+ TeamRemoveOptions,
14
+ TeamSwitchOptions,
15
+ TeamRole,
16
+ } from '../../types';
17
+
18
+ /**
19
+ * Get current user ID from auth store
20
+ */
21
+ function getCurrentUserId(): string {
22
+ const authStore = getAuthStore();
23
+ const auth = authStore.load();
24
+ const userId = auth?.identity?.github?.username;
25
+ if (!userId) {
26
+ throw new Error('Not authenticated. Run `nimbus login` first.');
27
+ }
28
+ return userId;
29
+ }
30
+
31
+ /**
32
+ * Get current team ID from config or environment
33
+ */
34
+ function getCurrentTeamId(): string | null {
35
+ return process.env.NIMBUS_TEAM_ID || null;
36
+ }
37
+
38
+ /**
39
+ * Parse team create options
40
+ */
41
+ export function parseTeamCreateOptions(args: string[]): TeamCreateOptions {
42
+ const options: TeamCreateOptions = {};
43
+
44
+ for (let i = 0; i < args.length; i++) {
45
+ const arg = args[i];
46
+ if (arg === '--non-interactive') {
47
+ options.nonInteractive = true;
48
+ } else if (!arg.startsWith('-') && !options.name) {
49
+ options.name = arg;
50
+ }
51
+ }
52
+
53
+ return options;
54
+ }
55
+
56
+ /**
57
+ * Parse team invite options
58
+ */
59
+ export function parseTeamInviteOptions(args: string[]): TeamInviteOptions {
60
+ const options: TeamInviteOptions = {};
61
+
62
+ for (let i = 0; i < args.length; i++) {
63
+ const arg = args[i];
64
+ if (arg === '--role' && args[i + 1]) {
65
+ options.role = args[++i] as TeamRole;
66
+ } else if (arg === '--non-interactive') {
67
+ options.nonInteractive = true;
68
+ } else if (!arg.startsWith('-') && !options.email) {
69
+ options.email = arg;
70
+ }
71
+ }
72
+
73
+ return options;
74
+ }
75
+
76
+ /**
77
+ * Parse team members options
78
+ */
79
+ export function parseTeamMembersOptions(args: string[]): TeamMembersOptions {
80
+ const options: TeamMembersOptions = {};
81
+
82
+ for (let i = 0; i < args.length; i++) {
83
+ const arg = args[i];
84
+ if (arg === '--json') {
85
+ options.json = true;
86
+ } else if (arg === '--non-interactive') {
87
+ options.nonInteractive = true;
88
+ }
89
+ }
90
+
91
+ return options;
92
+ }
93
+
94
+ /**
95
+ * Parse team remove options
96
+ */
97
+ export function parseTeamRemoveOptions(args: string[]): TeamRemoveOptions {
98
+ const options: TeamRemoveOptions = {};
99
+
100
+ for (let i = 0; i < args.length; i++) {
101
+ const arg = args[i];
102
+ if (arg === '--force' || arg === '-f') {
103
+ options.force = true;
104
+ } else if (arg === '--non-interactive') {
105
+ options.nonInteractive = true;
106
+ } else if (!arg.startsWith('-') && !options.email) {
107
+ options.email = arg;
108
+ }
109
+ }
110
+
111
+ return options;
112
+ }
113
+
114
+ /**
115
+ * Parse team switch options
116
+ */
117
+ export function parseTeamSwitchOptions(args: string[]): TeamSwitchOptions {
118
+ const options: TeamSwitchOptions = {};
119
+
120
+ for (let i = 0; i < args.length; i++) {
121
+ const arg = args[i];
122
+ if (arg === '--non-interactive') {
123
+ options.nonInteractive = true;
124
+ } else if (!arg.startsWith('-') && !options.teamId) {
125
+ options.teamId = arg;
126
+ }
127
+ }
128
+
129
+ return options;
130
+ }
131
+
132
+ /**
133
+ * Team create command
134
+ */
135
+ export async function teamCreateCommand(options: TeamCreateOptions): Promise<void> {
136
+ try {
137
+ const name = options.name;
138
+ if (!name) {
139
+ ui.error('Team name is required');
140
+ ui.info('Usage: nimbus team create <name>');
141
+ return;
142
+ }
143
+
144
+ const userId = getCurrentUserId();
145
+
146
+ ui.startSpinner({ message: 'Creating team...' });
147
+ const team = await teamClient.createTeam({ name, ownerId: userId });
148
+ ui.stopSpinnerSuccess(`Team "${team.name}" created`);
149
+
150
+ ui.newLine();
151
+ ui.info(`Team ID: ${team.id}`);
152
+ ui.info(`To use this team, run: nimbus team switch ${team.id}`);
153
+ ui.info(`Or set environment variable: export NIMBUS_TEAM_ID=${team.id}`);
154
+ } catch (error: any) {
155
+ ui.stopSpinnerFail('Failed to create team');
156
+ ui.error(error.message);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Team invite command
162
+ */
163
+ export async function teamInviteCommand(options: TeamInviteOptions): Promise<void> {
164
+ try {
165
+ const email = options.email;
166
+ if (!email) {
167
+ ui.error('Email is required');
168
+ ui.info('Usage: nimbus team invite <email> [--role member|admin|viewer]');
169
+ return;
170
+ }
171
+
172
+ const teamId = getCurrentTeamId();
173
+ if (!teamId) {
174
+ ui.error('No team selected. Run `nimbus team switch <team-id>` first.');
175
+ return;
176
+ }
177
+
178
+ ui.startSpinner({ message: `Inviting ${email}...` });
179
+ const member = await teamClient.inviteMember(teamId, {
180
+ email,
181
+ role: options.role || 'member',
182
+ });
183
+ ui.stopSpinnerSuccess(`Invited ${email} as ${member.role}`);
184
+ } catch (error: any) {
185
+ ui.stopSpinnerFail('Failed to invite member');
186
+ ui.error(error.message);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Team members command
192
+ */
193
+ export async function teamMembersCommand(options: TeamMembersOptions): Promise<void> {
194
+ try {
195
+ const teamId = getCurrentTeamId();
196
+ if (!teamId) {
197
+ ui.error('No team selected. Run `nimbus team switch <team-id>` first.');
198
+ return;
199
+ }
200
+
201
+ ui.startSpinner({ message: 'Fetching members...' });
202
+ const members = await teamClient.listMembers(teamId);
203
+ ui.stopSpinnerSuccess(`Found ${members.length} members`);
204
+
205
+ if (options.json) {
206
+ console.log(JSON.stringify(members, null, 2));
207
+ return;
208
+ }
209
+
210
+ ui.newLine();
211
+ ui.table({
212
+ columns: [
213
+ { key: 'email', header: 'Email' },
214
+ { key: 'role', header: 'Role' },
215
+ { key: 'joinedAt', header: 'Joined' },
216
+ ],
217
+ data: members.map(m => ({
218
+ email: m.user?.email || m.userId,
219
+ role: m.role,
220
+ joinedAt: new Date(m.joinedAt).toLocaleDateString(),
221
+ })),
222
+ });
223
+ } catch (error: any) {
224
+ ui.stopSpinnerFail('Failed to list members');
225
+ ui.error(error.message);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Team remove command
231
+ */
232
+ export async function teamRemoveCommand(options: TeamRemoveOptions): Promise<void> {
233
+ try {
234
+ const email = options.email;
235
+ if (!email) {
236
+ ui.error('Email is required');
237
+ ui.info('Usage: nimbus team remove <email>');
238
+ return;
239
+ }
240
+
241
+ const teamId = getCurrentTeamId();
242
+ if (!teamId) {
243
+ ui.error('No team selected. Run `nimbus team switch <team-id>` first.');
244
+ return;
245
+ }
246
+
247
+ // First, find the user ID by email from members list
248
+ const members = await teamClient.listMembers(teamId);
249
+ const member = members.find(m => m.user?.email === email);
250
+
251
+ if (!member) {
252
+ ui.error(`Member with email ${email} not found`);
253
+ return;
254
+ }
255
+
256
+ ui.startSpinner({ message: `Removing ${email}...` });
257
+ await teamClient.removeMember(teamId, member.userId);
258
+ ui.stopSpinnerSuccess(`Removed ${email} from team`);
259
+ } catch (error: any) {
260
+ ui.stopSpinnerFail('Failed to remove member');
261
+ ui.error(error.message);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Team switch command
267
+ */
268
+ export async function teamSwitchCommand(options: TeamSwitchOptions): Promise<void> {
269
+ try {
270
+ const userId = getCurrentUserId();
271
+
272
+ // If no team ID provided, list teams for selection
273
+ if (!options.teamId) {
274
+ ui.startSpinner({ message: 'Fetching teams...' });
275
+ const teams = await teamClient.listTeams(userId);
276
+ ui.stopSpinnerSuccess(`Found ${teams.length} teams`);
277
+
278
+ if (teams.length === 0) {
279
+ ui.info('No teams found. Create one with `nimbus team create <name>`');
280
+ return;
281
+ }
282
+
283
+ ui.newLine();
284
+ ui.info('Available teams:');
285
+ for (const team of teams) {
286
+ ui.print(` ${team.id} - ${team.name} (${team.plan})`);
287
+ }
288
+ ui.newLine();
289
+ ui.info('To switch: nimbus team switch <team-id>');
290
+ ui.info('Or set: export NIMBUS_TEAM_ID=<team-id>');
291
+ return;
292
+ }
293
+
294
+ // Verify team exists and user has access
295
+ ui.startSpinner({ message: 'Switching team...' });
296
+ const team = await teamClient.getTeam(options.teamId);
297
+ if (!team) {
298
+ ui.stopSpinnerFail('Team not found');
299
+ return;
300
+ }
301
+
302
+ ui.stopSpinnerSuccess(`Switched to team "${team.name}"`);
303
+ ui.newLine();
304
+ ui.info(`Set this environment variable to persist:`);
305
+ ui.print(` export NIMBUS_TEAM_ID=${team.id}`);
306
+ } catch (error: any) {
307
+ ui.stopSpinnerFail('Failed to switch team');
308
+ ui.error(error.message);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Main team command dispatcher
314
+ */
315
+ export async function teamCommand(subcommand: string, args: string[]): Promise<void> {
316
+ switch (subcommand) {
317
+ case 'create':
318
+ await teamCreateCommand(parseTeamCreateOptions(args));
319
+ break;
320
+ case 'invite':
321
+ await teamInviteCommand(parseTeamInviteOptions(args));
322
+ break;
323
+ case 'members':
324
+ await teamMembersCommand(parseTeamMembersOptions(args));
325
+ break;
326
+ case 'remove':
327
+ await teamRemoveCommand(parseTeamRemoveOptions(args));
328
+ break;
329
+ case 'switch':
330
+ await teamSwitchCommand(parseTeamSwitchOptions(args));
331
+ break;
332
+ case 'list':
333
+ await teamSwitchCommand(parseTeamSwitchOptions([])); // List mode
334
+ break;
335
+ default:
336
+ ui.error(`Unknown team command: ${subcommand}`);
337
+ ui.newLine();
338
+ ui.info('Available team commands:');
339
+ ui.print(' nimbus team create <name> - Create a new team');
340
+ ui.print(' nimbus team invite <email> - Invite a member');
341
+ ui.print(' nimbus team members - List team members');
342
+ ui.print(' nimbus team remove <email> - Remove a member');
343
+ ui.print(' nimbus team switch [team-id] - Switch to a team');
344
+ ui.print(' nimbus team list - List your teams');
345
+ }
346
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Template Commands
3
+ *
4
+ * CLI commands for managing infrastructure templates
5
+ */
6
+
7
+ import { RestClient } from '../clients';
8
+ import { ui } from '../wizard/ui';
9
+
10
+ const STATE_SERVICE_URL = process.env.STATE_SERVICE_URL || 'http://localhost:3004';
11
+
12
+ export interface TemplateCommandOptions {
13
+ type?: string;
14
+ name?: string;
15
+ file?: string;
16
+ json?: boolean;
17
+ }
18
+
19
+ /**
20
+ * List all templates
21
+ */
22
+ async function templateListCommand(options: TemplateCommandOptions = {}): Promise<void> {
23
+ ui.header('Templates');
24
+ ui.startSpinner({ message: 'Fetching templates...' });
25
+
26
+ try {
27
+ const client = new RestClient(STATE_SERVICE_URL);
28
+ const params = options.type ? `?type=${encodeURIComponent(options.type)}` : '';
29
+ const result = await client.get<any>(`/api/state/templates${params}`);
30
+
31
+ if (result.success && result.data) {
32
+ const templates = Array.isArray(result.data) ? result.data : [];
33
+ ui.stopSpinnerSuccess(`Found ${templates.length} template(s)`);
34
+
35
+ if (templates.length > 0) {
36
+ ui.table({
37
+ columns: [
38
+ { key: 'id', header: 'ID' },
39
+ { key: 'name', header: 'Name' },
40
+ { key: 'type', header: 'Type' },
41
+ { key: 'createdAt', header: 'Created' },
42
+ ],
43
+ data: templates.map((t: any) => ({
44
+ id: t.id?.substring(0, 8) || '-',
45
+ name: t.name || '-',
46
+ type: t.type || '-',
47
+ createdAt: t.createdAt ? new Date(t.createdAt).toLocaleDateString() : '-',
48
+ })),
49
+ });
50
+ } else {
51
+ ui.info('No templates found. Use "nimbus template save" to create one.');
52
+ }
53
+ } else {
54
+ ui.stopSpinnerFail('Failed to fetch templates');
55
+ }
56
+ } catch (error: any) {
57
+ ui.stopSpinnerFail('Error fetching templates');
58
+ ui.error(error.message);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get a specific template by ID
64
+ */
65
+ async function templateGetCommand(
66
+ id: string,
67
+ _options: TemplateCommandOptions = {}
68
+ ): Promise<void> {
69
+ ui.header(`Template: ${id}`);
70
+ ui.startSpinner({ message: 'Fetching template...' });
71
+
72
+ try {
73
+ const client = new RestClient(STATE_SERVICE_URL);
74
+ const result = await client.get<any>(`/api/state/templates/${encodeURIComponent(id)}`);
75
+
76
+ if (result.success && result.data) {
77
+ ui.stopSpinnerSuccess('Template retrieved');
78
+ const template = result.data;
79
+
80
+ ui.print(` ${ui.color('Name:', 'cyan')} ${template.name || '-'}`);
81
+ ui.print(` ${ui.color('Type:', 'cyan')} ${template.type || '-'}`);
82
+ ui.print(` ${ui.color('ID:', 'cyan')} ${template.id || '-'}`);
83
+ ui.print(` ${ui.color('Created:', 'cyan')} ${template.createdAt || '-'}`);
84
+
85
+ if (template.variables && Object.keys(template.variables).length > 0) {
86
+ ui.newLine();
87
+ ui.print(` ${ui.color('Variables:', 'cyan')}`);
88
+ for (const [key, val] of Object.entries(template.variables)) {
89
+ ui.print(` ${key}: ${JSON.stringify(val)}`);
90
+ }
91
+ }
92
+
93
+ if (template.content) {
94
+ ui.newLine();
95
+ ui.box({
96
+ title: 'Content',
97
+ content:
98
+ typeof template.content === 'string'
99
+ ? template.content
100
+ : JSON.stringify(template.content, null, 2),
101
+ });
102
+ }
103
+ } else {
104
+ ui.stopSpinnerFail('Template not found');
105
+ }
106
+ } catch (error: any) {
107
+ ui.stopSpinnerFail('Error fetching template');
108
+ ui.error(error.message);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Save a new template
114
+ */
115
+ async function templateSaveCommand(options: TemplateCommandOptions = {}): Promise<void> {
116
+ ui.header('Save Template');
117
+
118
+ if (!options.name) {
119
+ ui.error('Template name is required. Use --name <name>');
120
+ return;
121
+ }
122
+
123
+ let content = '';
124
+ if (options.file) {
125
+ try {
126
+ const fs = await import('fs');
127
+ content = fs.readFileSync(options.file, 'utf-8');
128
+ } catch (err: any) {
129
+ ui.error(`Failed to read file: ${err.message}`);
130
+ return;
131
+ }
132
+ }
133
+
134
+ ui.startSpinner({ message: 'Saving template...' });
135
+
136
+ try {
137
+ const client = new RestClient(STATE_SERVICE_URL);
138
+ const result = await client.post<any>('/api/state/templates', {
139
+ name: options.name,
140
+ type: options.type || 'terraform',
141
+ content,
142
+ variables: {},
143
+ });
144
+
145
+ if (result.success) {
146
+ ui.stopSpinnerSuccess(`Template "${options.name}" saved`);
147
+ if (result.data?.id) {
148
+ ui.info(`ID: ${result.data.id}`);
149
+ }
150
+ } else {
151
+ ui.stopSpinnerFail('Failed to save template');
152
+ }
153
+ } catch (error: any) {
154
+ ui.stopSpinnerFail('Error saving template');
155
+ ui.error(error.message);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Delete a template by ID
161
+ */
162
+ async function templateDeleteCommand(id: string): Promise<void> {
163
+ ui.header(`Delete Template: ${id}`);
164
+ ui.startSpinner({ message: 'Deleting template...' });
165
+
166
+ try {
167
+ const client = new RestClient(STATE_SERVICE_URL);
168
+ const result = await client.delete<any>(`/api/state/templates/${encodeURIComponent(id)}`);
169
+
170
+ if (result.success) {
171
+ ui.stopSpinnerSuccess('Template deleted');
172
+ } else {
173
+ ui.stopSpinnerFail('Failed to delete template');
174
+ }
175
+ } catch (error: any) {
176
+ ui.stopSpinnerFail('Error deleting template');
177
+ ui.error(error.message);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Main template command router
183
+ */
184
+ export async function templateCommand(subcommand: string, args: string[]): Promise<void> {
185
+ const options: TemplateCommandOptions = {};
186
+ const positionalArgs: string[] = [];
187
+
188
+ for (let i = 0; i < args.length; i++) {
189
+ const arg = args[i];
190
+ if (arg === '--type' || arg === '-t') {
191
+ options.type = args[++i];
192
+ } else if (arg === '--name' || arg === '-n') {
193
+ options.name = args[++i];
194
+ } else if (arg === '--file' || arg === '-f') {
195
+ options.file = args[++i];
196
+ } else if (arg === '--json') {
197
+ options.json = true;
198
+ } else if (!arg.startsWith('-')) {
199
+ positionalArgs.push(arg);
200
+ }
201
+ }
202
+
203
+ switch (subcommand) {
204
+ case 'list':
205
+ case 'ls':
206
+ await templateListCommand(options);
207
+ break;
208
+ case 'get':
209
+ if (positionalArgs.length < 1) {
210
+ ui.error('Usage: nimbus template get <id>');
211
+ return;
212
+ }
213
+ await templateGetCommand(positionalArgs[0], options);
214
+ break;
215
+ case 'save':
216
+ await templateSaveCommand(options);
217
+ break;
218
+ case 'delete':
219
+ case 'rm':
220
+ if (positionalArgs.length < 1) {
221
+ ui.error('Usage: nimbus template delete <id>');
222
+ return;
223
+ }
224
+ await templateDeleteCommand(positionalArgs[0]);
225
+ break;
226
+ default:
227
+ ui.error(`Unknown template subcommand: ${subcommand || '(none)'}`);
228
+ ui.info(
229
+ 'Available commands: list, get <id>, save --name <name> [--file <path>], delete <id>'
230
+ );
231
+ }
232
+ }