@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,1343 @@
1
+ /**
2
+ * Git Commands
3
+ *
4
+ * CLI commands for Git operations
5
+ */
6
+
7
+ import { gitClient } from '../../clients';
8
+ import { ui } from '../../wizard/ui';
9
+ import { historyManager } from '../../history';
10
+
11
+ export interface GitCommandOptions {
12
+ directory?: string;
13
+ all?: boolean;
14
+ amend?: boolean;
15
+ remote?: string;
16
+ branch?: string;
17
+ force?: boolean;
18
+ setUpstream?: boolean;
19
+ rebase?: boolean;
20
+ prune?: boolean;
21
+ limit?: number;
22
+ staged?: boolean;
23
+ file?: string;
24
+ create?: boolean;
25
+ // tag options
26
+ message?: string;
27
+ annotated?: boolean;
28
+ tagName?: string;
29
+ // reset options
30
+ soft?: boolean;
31
+ mixed?: boolean;
32
+ hard?: boolean;
33
+ // revert options
34
+ noCommit?: boolean;
35
+ noEdit?: boolean;
36
+ // cherry-pick options
37
+ // blame options
38
+ lineRange?: string;
39
+ // init options
40
+ bare?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Show git status
45
+ */
46
+ export async function gitStatusCommand(options: GitCommandOptions = {}): Promise<void> {
47
+ ui.header('Git Status');
48
+
49
+ ui.startSpinner({ message: 'Getting git status...' });
50
+
51
+ try {
52
+ const available = await gitClient.isAvailable();
53
+ if (!available) {
54
+ ui.stopSpinnerFail('Git Tools Service not available');
55
+ ui.error('Please ensure the Git Tools Service is running.');
56
+ return;
57
+ }
58
+
59
+ const result = await gitClient.status(options.directory);
60
+
61
+ if (result.success) {
62
+ ui.stopSpinnerSuccess(`On branch ${result.status.branch}`);
63
+
64
+ if (result.status.ahead > 0 || result.status.behind > 0) {
65
+ ui.info(
66
+ `Your branch is ${result.status.ahead} commit(s) ahead, ${result.status.behind} commit(s) behind`
67
+ );
68
+ }
69
+
70
+ if (result.status.staged.length > 0) {
71
+ ui.success('Changes to be committed:');
72
+ result.status.staged.forEach(f => ui.info(` ${f}`));
73
+ }
74
+
75
+ if (result.status.modified.length > 0) {
76
+ ui.warning('Changes not staged for commit:');
77
+ result.status.modified.forEach(f => ui.info(` modified: ${f}`));
78
+ }
79
+
80
+ if (result.status.untracked.length > 0) {
81
+ ui.info('Untracked files:');
82
+ result.status.untracked.forEach(f => ui.info(` ${f}`));
83
+ }
84
+
85
+ if (result.status.deleted.length > 0) {
86
+ ui.error('Deleted files:');
87
+ result.status.deleted.forEach(f => ui.info(` deleted: ${f}`));
88
+ }
89
+
90
+ if (
91
+ result.status.staged.length === 0 &&
92
+ result.status.modified.length === 0 &&
93
+ result.status.untracked.length === 0 &&
94
+ result.status.deleted.length === 0
95
+ ) {
96
+ ui.success('Working tree clean');
97
+ }
98
+ } else {
99
+ ui.stopSpinnerFail('Failed to get status');
100
+ if (result.error) {
101
+ ui.error(result.error);
102
+ }
103
+ }
104
+ } catch (error: any) {
105
+ ui.stopSpinnerFail('Error getting git status');
106
+ ui.error(error.message);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Stage files
112
+ */
113
+ export async function gitAddCommand(
114
+ files: string[],
115
+ options: GitCommandOptions = {}
116
+ ): Promise<void> {
117
+ ui.header('Git Add');
118
+
119
+ if (options.all) {
120
+ ui.info('Staging all changes');
121
+ } else {
122
+ ui.info(`Staging: ${files.join(', ')}`);
123
+ }
124
+
125
+ ui.startSpinner({ message: 'Staging files...' });
126
+
127
+ try {
128
+ const available = await gitClient.isAvailable();
129
+ if (!available) {
130
+ ui.stopSpinnerFail('Git Tools Service not available');
131
+ ui.error('Please ensure the Git Tools Service is running.');
132
+ return;
133
+ }
134
+
135
+ const result = await gitClient.add(files, {
136
+ directory: options.directory,
137
+ all: options.all,
138
+ });
139
+
140
+ if (result.success) {
141
+ ui.stopSpinnerSuccess('Files staged');
142
+ if (result.output) {
143
+ ui.info(result.output);
144
+ }
145
+ } else {
146
+ ui.stopSpinnerFail('Failed to stage files');
147
+ if (result.error) {
148
+ ui.error(result.error);
149
+ }
150
+ }
151
+ } catch (error: any) {
152
+ ui.stopSpinnerFail('Error staging files');
153
+ ui.error(error.message);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Create a commit
159
+ */
160
+ export async function gitCommitCommand(
161
+ message: string,
162
+ options: GitCommandOptions = {}
163
+ ): Promise<void> {
164
+ ui.header('Git Commit');
165
+
166
+ if (options.amend) {
167
+ ui.info('Amending previous commit');
168
+ }
169
+ if (options.all) {
170
+ ui.info('Committing all changes');
171
+ }
172
+
173
+ ui.startSpinner({ message: 'Creating commit...' });
174
+
175
+ try {
176
+ const available = await gitClient.isAvailable();
177
+ if (!available) {
178
+ ui.stopSpinnerFail('Git Tools Service not available');
179
+ ui.error('Please ensure the Git Tools Service is running.');
180
+ return;
181
+ }
182
+
183
+ const result = await gitClient.commit(message, {
184
+ directory: options.directory,
185
+ all: options.all,
186
+ amend: options.amend,
187
+ });
188
+
189
+ if (result.success) {
190
+ ui.stopSpinnerSuccess(`Committed ${result.commit.shortHash}`);
191
+ ui.info(`Author: ${result.commit.author}`);
192
+ ui.info(`Message: ${result.commit.message}`);
193
+ } else {
194
+ ui.stopSpinnerFail('Failed to create commit');
195
+ if (result.error) {
196
+ ui.error(result.error);
197
+ }
198
+ }
199
+ } catch (error: any) {
200
+ ui.stopSpinnerFail('Error creating commit');
201
+ ui.error(error.message);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Push to remote
207
+ */
208
+ export async function gitPushCommand(options: GitCommandOptions = {}): Promise<void> {
209
+ ui.header('Git Push');
210
+
211
+ const remote = options.remote || 'origin';
212
+ ui.info(`Remote: ${remote}`);
213
+ if (options.branch) {
214
+ ui.info(`Branch: ${options.branch}`);
215
+ }
216
+ if (options.force) {
217
+ ui.warning('Force push enabled');
218
+ }
219
+ if (options.setUpstream) {
220
+ ui.info('Setting upstream');
221
+ }
222
+
223
+ ui.startSpinner({ message: 'Pushing to remote...' });
224
+
225
+ try {
226
+ const available = await gitClient.isAvailable();
227
+ if (!available) {
228
+ ui.stopSpinnerFail('Git Tools Service not available');
229
+ ui.error('Please ensure the Git Tools Service is running.');
230
+ return;
231
+ }
232
+
233
+ const result = await gitClient.push({
234
+ directory: options.directory,
235
+ remote: options.remote,
236
+ branch: options.branch,
237
+ force: options.force,
238
+ setUpstream: options.setUpstream,
239
+ });
240
+
241
+ if (result.success) {
242
+ ui.stopSpinnerSuccess('Pushed to remote');
243
+ if (result.output) {
244
+ ui.info(result.output);
245
+ }
246
+ } else {
247
+ ui.stopSpinnerFail('Failed to push');
248
+ if (result.error) {
249
+ ui.error(result.error);
250
+ }
251
+ }
252
+ } catch (error: any) {
253
+ ui.stopSpinnerFail('Error pushing to remote');
254
+ ui.error(error.message);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Pull from remote
260
+ */
261
+ export async function gitPullCommand(options: GitCommandOptions = {}): Promise<void> {
262
+ ui.header('Git Pull');
263
+
264
+ const remote = options.remote || 'origin';
265
+ ui.info(`Remote: ${remote}`);
266
+ if (options.branch) {
267
+ ui.info(`Branch: ${options.branch}`);
268
+ }
269
+ if (options.rebase) {
270
+ ui.info('Rebasing enabled');
271
+ }
272
+
273
+ ui.startSpinner({ message: 'Pulling from remote...' });
274
+
275
+ try {
276
+ const available = await gitClient.isAvailable();
277
+ if (!available) {
278
+ ui.stopSpinnerFail('Git Tools Service not available');
279
+ ui.error('Please ensure the Git Tools Service is running.');
280
+ return;
281
+ }
282
+
283
+ const result = await gitClient.pull({
284
+ directory: options.directory,
285
+ remote: options.remote,
286
+ branch: options.branch,
287
+ rebase: options.rebase,
288
+ });
289
+
290
+ if (result.success) {
291
+ ui.stopSpinnerSuccess('Pulled from remote');
292
+ if (result.output) {
293
+ ui.info(result.output);
294
+ }
295
+ } else {
296
+ ui.stopSpinnerFail('Failed to pull');
297
+ if (result.error) {
298
+ ui.error(result.error);
299
+ }
300
+ }
301
+ } catch (error: any) {
302
+ ui.stopSpinnerFail('Error pulling from remote');
303
+ ui.error(error.message);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Fetch from remote
309
+ */
310
+ export async function gitFetchCommand(options: GitCommandOptions = {}): Promise<void> {
311
+ ui.header('Git Fetch');
312
+
313
+ if (options.all) {
314
+ ui.info('Fetching from all remotes');
315
+ } else {
316
+ const remote = options.remote || 'origin';
317
+ ui.info(`Remote: ${remote}`);
318
+ }
319
+ if (options.prune) {
320
+ ui.info('Pruning remote-tracking branches');
321
+ }
322
+
323
+ ui.startSpinner({ message: 'Fetching from remote...' });
324
+
325
+ try {
326
+ const available = await gitClient.isAvailable();
327
+ if (!available) {
328
+ ui.stopSpinnerFail('Git Tools Service not available');
329
+ ui.error('Please ensure the Git Tools Service is running.');
330
+ return;
331
+ }
332
+
333
+ const result = await gitClient.fetch({
334
+ directory: options.directory,
335
+ remote: options.remote,
336
+ all: options.all,
337
+ prune: options.prune,
338
+ });
339
+
340
+ if (result.success) {
341
+ ui.stopSpinnerSuccess('Fetched from remote');
342
+ if (result.output) {
343
+ ui.info(result.output);
344
+ }
345
+ } else {
346
+ ui.stopSpinnerFail('Failed to fetch');
347
+ if (result.error) {
348
+ ui.error(result.error);
349
+ }
350
+ }
351
+ } catch (error: any) {
352
+ ui.stopSpinnerFail('Error fetching from remote');
353
+ ui.error(error.message);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Show commit log
359
+ */
360
+ export async function gitLogCommand(options: GitCommandOptions = {}): Promise<void> {
361
+ ui.header('Git Log');
362
+
363
+ const limit = options.limit || 10;
364
+
365
+ ui.startSpinner({ message: 'Fetching commit log...' });
366
+
367
+ try {
368
+ const available = await gitClient.isAvailable();
369
+ if (!available) {
370
+ ui.stopSpinnerFail('Git Tools Service not available');
371
+ ui.error('Please ensure the Git Tools Service is running.');
372
+ return;
373
+ }
374
+
375
+ const result = await gitClient.log({
376
+ directory: options.directory,
377
+ limit,
378
+ branch: options.branch,
379
+ });
380
+
381
+ if (result.success) {
382
+ ui.stopSpinnerSuccess(`Found ${result.commits.length} commits`);
383
+
384
+ if (result.commits.length > 0) {
385
+ ui.table({
386
+ columns: [
387
+ { key: 'hash', header: 'Hash' },
388
+ { key: 'author', header: 'Author' },
389
+ { key: 'date', header: 'Date' },
390
+ { key: 'message', header: 'Message' },
391
+ ],
392
+ data: result.commits.map(commit => ({
393
+ hash: commit.shortHash,
394
+ author: commit.author,
395
+ date: commit.date,
396
+ message: commit.message.substring(0, 50) + (commit.message.length > 50 ? '...' : ''),
397
+ })),
398
+ });
399
+ }
400
+ } else {
401
+ ui.stopSpinnerFail('Failed to get log');
402
+ if (result.error) {
403
+ ui.error(result.error);
404
+ }
405
+ }
406
+ } catch (error: any) {
407
+ ui.stopSpinnerFail('Error getting git log');
408
+ ui.error(error.message);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * List branches
414
+ */
415
+ export async function gitBranchCommand(options: GitCommandOptions = {}): Promise<void> {
416
+ ui.header('Git Branches');
417
+
418
+ ui.startSpinner({ message: 'Fetching branches...' });
419
+
420
+ try {
421
+ const available = await gitClient.isAvailable();
422
+ if (!available) {
423
+ ui.stopSpinnerFail('Git Tools Service not available');
424
+ ui.error('Please ensure the Git Tools Service is running.');
425
+ return;
426
+ }
427
+
428
+ const result = await gitClient.branches({
429
+ directory: options.directory,
430
+ all: options.all,
431
+ });
432
+
433
+ if (result.success) {
434
+ ui.stopSpinnerSuccess(`Found ${result.branches.length} branches`);
435
+
436
+ if (result.branches.length > 0) {
437
+ result.branches.forEach(branch => {
438
+ const prefix = branch.current ? '* ' : ' ';
439
+ const tracking = branch.tracking ? ` -> ${branch.tracking}` : '';
440
+ ui.info(`${prefix}${branch.name}${tracking}`);
441
+ });
442
+ }
443
+ } else {
444
+ ui.stopSpinnerFail('Failed to list branches');
445
+ if (result.error) {
446
+ ui.error(result.error);
447
+ }
448
+ }
449
+ } catch (error: any) {
450
+ ui.stopSpinnerFail('Error listing branches');
451
+ ui.error(error.message);
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Checkout branch or file
457
+ */
458
+ export async function gitCheckoutCommand(
459
+ target: string,
460
+ options: GitCommandOptions = {}
461
+ ): Promise<void> {
462
+ ui.header('Git Checkout');
463
+
464
+ ui.info(`Target: ${target}`);
465
+ if (options.create) {
466
+ ui.info('Creating new branch');
467
+ }
468
+
469
+ ui.startSpinner({ message: `Checking out ${target}...` });
470
+
471
+ try {
472
+ const available = await gitClient.isAvailable();
473
+ if (!available) {
474
+ ui.stopSpinnerFail('Git Tools Service not available');
475
+ ui.error('Please ensure the Git Tools Service is running.');
476
+ return;
477
+ }
478
+
479
+ const result = await gitClient.checkout(target, {
480
+ directory: options.directory,
481
+ create: options.create,
482
+ });
483
+
484
+ if (result.success) {
485
+ ui.stopSpinnerSuccess(`Checked out ${target}`);
486
+ if (result.output) {
487
+ ui.info(result.output);
488
+ }
489
+ } else {
490
+ ui.stopSpinnerFail(`Failed to checkout ${target}`);
491
+ if (result.error) {
492
+ ui.error(result.error);
493
+ }
494
+ }
495
+ } catch (error: any) {
496
+ ui.stopSpinnerFail(`Error checking out ${target}`);
497
+ ui.error(error.message);
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Show diff
503
+ */
504
+ export async function gitDiffCommand(options: GitCommandOptions = {}): Promise<void> {
505
+ ui.header('Git Diff');
506
+
507
+ if (options.staged) {
508
+ ui.info('Showing staged changes');
509
+ }
510
+ if (options.file) {
511
+ ui.info(`File: ${options.file}`);
512
+ }
513
+
514
+ ui.startSpinner({ message: 'Getting diff...' });
515
+
516
+ try {
517
+ const available = await gitClient.isAvailable();
518
+ if (!available) {
519
+ ui.stopSpinnerFail('Git Tools Service not available');
520
+ ui.error('Please ensure the Git Tools Service is running.');
521
+ return;
522
+ }
523
+
524
+ const result = await gitClient.diff({
525
+ directory: options.directory,
526
+ staged: options.staged,
527
+ file: options.file,
528
+ });
529
+
530
+ if (result.success) {
531
+ ui.stopSpinnerSuccess('Diff retrieved');
532
+ if (result.diff) {
533
+ // Display diff as raw text - sideBySideDiff requires original/modified strings
534
+ console.log(result.diff);
535
+ } else {
536
+ ui.info('No changes');
537
+ }
538
+ } else {
539
+ ui.stopSpinnerFail('Failed to get diff');
540
+ if (result.error) {
541
+ ui.error(result.error);
542
+ }
543
+ }
544
+ } catch (error: any) {
545
+ ui.stopSpinnerFail('Error getting diff');
546
+ ui.error(error.message);
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Merge a branch
552
+ */
553
+ export async function gitMergeCommand(
554
+ branch: string,
555
+ options: GitCommandOptions = {}
556
+ ): Promise<void> {
557
+ ui.header('Git Merge');
558
+
559
+ ui.info(`Merging branch: ${branch}`);
560
+
561
+ ui.startSpinner({ message: `Merging ${branch}...` });
562
+
563
+ try {
564
+ const available = await gitClient.isAvailable();
565
+ if (!available) {
566
+ ui.stopSpinnerFail('Git Tools Service not available');
567
+ ui.error('Please ensure the Git Tools Service is running.');
568
+ return;
569
+ }
570
+
571
+ const result = await gitClient.merge(branch, {
572
+ directory: options.directory,
573
+ });
574
+
575
+ if (result.success) {
576
+ ui.stopSpinnerSuccess(`Merged ${branch}`);
577
+ if (result.output) {
578
+ ui.info(result.output);
579
+ }
580
+ } else {
581
+ ui.stopSpinnerFail(`Failed to merge ${branch}`);
582
+ if (result.error) {
583
+ ui.error(result.error);
584
+ }
585
+ }
586
+ } catch (error: any) {
587
+ ui.stopSpinnerFail(`Error merging ${branch}`);
588
+ ui.error(error.message);
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Stash operations
594
+ */
595
+ export async function gitStashCommand(
596
+ stashAction: 'push' | 'pop' | 'list' | 'drop' | 'apply' | 'clear',
597
+ options: GitCommandOptions & { message?: string; index?: number } = {}
598
+ ): Promise<void> {
599
+ ui.header(`Git Stash ${stashAction}`);
600
+
601
+ if (options.message) {
602
+ ui.info(`Message: ${options.message}`);
603
+ }
604
+
605
+ ui.startSpinner({ message: `Running stash ${stashAction}...` });
606
+
607
+ try {
608
+ const available = await gitClient.isAvailable();
609
+ if (!available) {
610
+ ui.stopSpinnerFail('Git Tools Service not available');
611
+ ui.error('Please ensure the Git Tools Service is running.');
612
+ return;
613
+ }
614
+
615
+ const result = await gitClient.stash(stashAction, {
616
+ directory: options.directory,
617
+ message: options.message,
618
+ index: options.index,
619
+ });
620
+
621
+ if (result.success) {
622
+ ui.stopSpinnerSuccess(`Stash ${stashAction} complete`);
623
+ if (result.output) {
624
+ console.log(result.output);
625
+ }
626
+ } else {
627
+ ui.stopSpinnerFail(`Stash ${stashAction} failed`);
628
+ if (result.error) {
629
+ ui.error(result.error);
630
+ }
631
+ }
632
+ } catch (error: any) {
633
+ ui.stopSpinnerFail(`Error during stash ${stashAction}`);
634
+ ui.error(error.message);
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Clone a repository
640
+ */
641
+ export async function gitCloneCommand(
642
+ url: string,
643
+ targetPath?: string,
644
+ options: GitCommandOptions = {}
645
+ ): Promise<void> {
646
+ ui.header('Git Clone');
647
+
648
+ ui.info(`URL: ${url}`);
649
+ if (targetPath) {
650
+ ui.info(`Path: ${targetPath}`);
651
+ }
652
+ if (options.branch) {
653
+ ui.info(`Branch: ${options.branch}`);
654
+ }
655
+ if (options.limit) {
656
+ ui.info(`Depth: ${options.limit}`);
657
+ }
658
+
659
+ ui.startSpinner({ message: `Cloning ${url}...` });
660
+
661
+ try {
662
+ const available = await gitClient.isAvailable();
663
+ if (!available) {
664
+ ui.stopSpinnerFail('Git Tools Service not available');
665
+ ui.error('Please ensure the Git Tools Service is running.');
666
+ return;
667
+ }
668
+
669
+ const result = await gitClient.clone(url, targetPath, {
670
+ branch: options.branch,
671
+ depth: options.limit,
672
+ });
673
+
674
+ if (result.success) {
675
+ ui.stopSpinnerSuccess('Repository cloned');
676
+ if (result.output) {
677
+ ui.info(result.output);
678
+ }
679
+ } else {
680
+ ui.stopSpinnerFail('Failed to clone repository');
681
+ if (result.error) {
682
+ ui.error(result.error);
683
+ }
684
+ }
685
+ } catch (error: any) {
686
+ ui.stopSpinnerFail('Error cloning repository');
687
+ ui.error(error.message);
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Tag operations
693
+ */
694
+ export async function gitTagCommand(
695
+ tagAction: 'list' | 'create' | 'delete' | 'push' | 'show',
696
+ options: GitCommandOptions & { tagArg?: string } = {}
697
+ ): Promise<void> {
698
+ ui.header(`Git Tag ${tagAction}`);
699
+
700
+ ui.startSpinner({ message: `Running tag ${tagAction}...` });
701
+
702
+ try {
703
+ const available = await gitClient.isAvailable();
704
+ if (!available) {
705
+ ui.stopSpinnerFail('Git Tools Service not available');
706
+ ui.error('Please ensure the Git Tools Service is running.');
707
+ return;
708
+ }
709
+
710
+ if (tagAction === 'list') {
711
+ const result = await gitClient.tagList({ directory: options.directory });
712
+ if (result.success) {
713
+ ui.stopSpinnerSuccess(`Found ${result.tags.length} tag(s)`);
714
+ if (result.tags.length > 0) {
715
+ result.tags.forEach(tag => ui.info(` ${tag}`));
716
+ } else {
717
+ ui.info('No tags found');
718
+ }
719
+ } else {
720
+ ui.stopSpinnerFail('Failed to list tags');
721
+ if (result.error) {
722
+ ui.error(result.error);
723
+ }
724
+ }
725
+ } else if (tagAction === 'create') {
726
+ if (!options.tagArg) {
727
+ ui.stopSpinnerFail('Tag name required');
728
+ ui.error('Usage: nimbus git tag create <name> [--message <msg>]');
729
+ return;
730
+ }
731
+ const result = await gitClient.tagCreate(options.tagArg, {
732
+ directory: options.directory,
733
+ message: options.message,
734
+ annotated: options.annotated || !!options.message,
735
+ force: options.force,
736
+ });
737
+ if (result.success) {
738
+ ui.stopSpinnerSuccess(`Tag '${options.tagArg}' created`);
739
+ if (result.output) {
740
+ ui.info(result.output);
741
+ }
742
+ } else {
743
+ ui.stopSpinnerFail(`Failed to create tag '${options.tagArg}'`);
744
+ if (result.error) {
745
+ ui.error(result.error);
746
+ }
747
+ }
748
+ } else if (tagAction === 'delete') {
749
+ if (!options.tagArg) {
750
+ ui.stopSpinnerFail('Tag name required');
751
+ ui.error('Usage: nimbus git tag delete <name>');
752
+ return;
753
+ }
754
+ const result = await gitClient.tagDelete(options.tagArg, { directory: options.directory });
755
+ if (result.success) {
756
+ ui.stopSpinnerSuccess(`Tag '${options.tagArg}' deleted`);
757
+ } else {
758
+ ui.stopSpinnerFail(`Failed to delete tag '${options.tagArg}'`);
759
+ if (result.error) {
760
+ ui.error(result.error);
761
+ }
762
+ }
763
+ } else if (tagAction === 'push') {
764
+ const result = await gitClient.tagPush({
765
+ directory: options.directory,
766
+ remote: options.remote,
767
+ tagName: options.tagArg,
768
+ });
769
+ if (result.success) {
770
+ ui.stopSpinnerSuccess('Tags pushed');
771
+ if (result.output) {
772
+ ui.info(result.output);
773
+ }
774
+ } else {
775
+ ui.stopSpinnerFail('Failed to push tags');
776
+ if (result.error) {
777
+ ui.error(result.error);
778
+ }
779
+ }
780
+ } else if (tagAction === 'show') {
781
+ if (!options.tagArg) {
782
+ ui.stopSpinnerFail('Tag name required');
783
+ ui.error('Usage: nimbus git tag show <name>');
784
+ return;
785
+ }
786
+ const result = await gitClient.tagShow(options.tagArg, { directory: options.directory });
787
+ if (result.success) {
788
+ ui.stopSpinnerSuccess(`Tag info for '${options.tagArg}'`);
789
+ if (result.output) {
790
+ console.log(result.output);
791
+ }
792
+ } else {
793
+ ui.stopSpinnerFail(`Failed to show tag '${options.tagArg}'`);
794
+ if (result.error) {
795
+ ui.error(result.error);
796
+ }
797
+ }
798
+ }
799
+ } catch (error: any) {
800
+ ui.stopSpinnerFail(`Error during tag ${tagAction}`);
801
+ ui.error(error.message);
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Show remote URL
807
+ */
808
+ export async function gitRemoteCommand(
809
+ remoteName?: string,
810
+ options: GitCommandOptions = {}
811
+ ): Promise<void> {
812
+ ui.header('Git Remote');
813
+
814
+ const name = remoteName || 'origin';
815
+ ui.info(`Remote: ${name}`);
816
+
817
+ ui.startSpinner({ message: 'Fetching remote URL...' });
818
+
819
+ try {
820
+ const available = await gitClient.isAvailable();
821
+ if (!available) {
822
+ ui.stopSpinnerFail('Git Tools Service not available');
823
+ ui.error('Please ensure the Git Tools Service is running.');
824
+ return;
825
+ }
826
+
827
+ const result = await gitClient.remote(name, { directory: options.directory });
828
+
829
+ if (result.success) {
830
+ ui.stopSpinnerSuccess(`Remote '${result.remote}'`);
831
+ if (result.url) {
832
+ ui.info(`URL: ${result.url}`);
833
+ } else {
834
+ ui.warning('No URL found for this remote');
835
+ }
836
+ } else {
837
+ ui.stopSpinnerFail('Failed to get remote URL');
838
+ if (result.error) {
839
+ ui.error(result.error);
840
+ }
841
+ }
842
+ } catch (error: any) {
843
+ ui.stopSpinnerFail('Error fetching remote URL');
844
+ ui.error(error.message);
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Reset to a commit
850
+ */
851
+ export async function gitResetCommand(
852
+ target: string,
853
+ options: GitCommandOptions = {}
854
+ ): Promise<void> {
855
+ ui.header('Git Reset');
856
+
857
+ let mode: 'soft' | 'mixed' | 'hard' = 'mixed';
858
+ if (options.soft) {
859
+ mode = 'soft';
860
+ } else if (options.hard) {
861
+ mode = 'hard';
862
+ }
863
+
864
+ ui.info(`Target: ${target}`);
865
+ ui.info(`Mode: ${mode}`);
866
+
867
+ if (mode === 'hard') {
868
+ ui.warning(
869
+ 'WARNING: --hard reset will discard all uncommitted changes. This cannot be undone.'
870
+ );
871
+ }
872
+
873
+ ui.startSpinner({ message: `Resetting to ${target} (${mode})...` });
874
+
875
+ try {
876
+ const available = await gitClient.isAvailable();
877
+ if (!available) {
878
+ ui.stopSpinnerFail('Git Tools Service not available');
879
+ ui.error('Please ensure the Git Tools Service is running.');
880
+ return;
881
+ }
882
+
883
+ const result = await gitClient.reset(target, { directory: options.directory, mode });
884
+
885
+ if (result.success) {
886
+ ui.stopSpinnerSuccess(`Reset to ${target} (${mode}) complete`);
887
+ if (result.output) {
888
+ ui.info(result.output);
889
+ }
890
+ } else {
891
+ ui.stopSpinnerFail(`Failed to reset to ${target}`);
892
+ if (result.error) {
893
+ ui.error(result.error);
894
+ }
895
+ }
896
+ } catch (error: any) {
897
+ ui.stopSpinnerFail(`Error resetting to ${target}`);
898
+ ui.error(error.message);
899
+ }
900
+ }
901
+
902
+ /**
903
+ * Revert a commit
904
+ */
905
+ export async function gitRevertCommand(
906
+ commit: string,
907
+ options: GitCommandOptions = {}
908
+ ): Promise<void> {
909
+ ui.header('Git Revert');
910
+
911
+ ui.info(`Reverting commit: ${commit}`);
912
+ if (options.noCommit) {
913
+ ui.info('--no-commit: staging revert without creating a commit');
914
+ }
915
+ if (options.noEdit) {
916
+ ui.info('--no-edit: using default revert commit message');
917
+ }
918
+
919
+ ui.startSpinner({ message: `Reverting ${commit}...` });
920
+
921
+ try {
922
+ const available = await gitClient.isAvailable();
923
+ if (!available) {
924
+ ui.stopSpinnerFail('Git Tools Service not available');
925
+ ui.error('Please ensure the Git Tools Service is running.');
926
+ return;
927
+ }
928
+
929
+ const result = await gitClient.revert(commit, {
930
+ directory: options.directory,
931
+ noCommit: options.noCommit,
932
+ noEdit: options.noEdit,
933
+ });
934
+
935
+ if (result.success) {
936
+ ui.stopSpinnerSuccess(`Reverted commit ${commit}`);
937
+ if (result.output) {
938
+ ui.info(result.output);
939
+ }
940
+ } else {
941
+ ui.stopSpinnerFail(`Failed to revert ${commit}`);
942
+ if (result.error) {
943
+ ui.error(result.error);
944
+ }
945
+ }
946
+ } catch (error: any) {
947
+ ui.stopSpinnerFail(`Error reverting ${commit}`);
948
+ ui.error(error.message);
949
+ }
950
+ }
951
+
952
+ /**
953
+ * Cherry-pick operations
954
+ */
955
+ export async function gitCherryPickCommand(
956
+ cherryPickAction: 'pick' | 'abort' | 'continue',
957
+ options: GitCommandOptions & { commit?: string } = {}
958
+ ): Promise<void> {
959
+ ui.header(`Git Cherry-Pick ${cherryPickAction}`);
960
+
961
+ ui.startSpinner({ message: `Running cherry-pick ${cherryPickAction}...` });
962
+
963
+ try {
964
+ const available = await gitClient.isAvailable();
965
+ if (!available) {
966
+ ui.stopSpinnerFail('Git Tools Service not available');
967
+ ui.error('Please ensure the Git Tools Service is running.');
968
+ return;
969
+ }
970
+
971
+ if (cherryPickAction === 'pick') {
972
+ if (!options.commit) {
973
+ ui.stopSpinnerFail('Commit hash required');
974
+ ui.error('Usage: nimbus git cherry-pick <commit-hash>');
975
+ return;
976
+ }
977
+ const result = await gitClient.cherryPick(options.commit, {
978
+ directory: options.directory,
979
+ noCommit: options.noCommit,
980
+ });
981
+ if (result.success) {
982
+ ui.stopSpinnerSuccess(`Cherry-picked ${options.commit}`);
983
+ if (result.output) {
984
+ ui.info(result.output);
985
+ }
986
+ } else {
987
+ ui.stopSpinnerFail(`Failed to cherry-pick ${options.commit}`);
988
+ if (result.error) {
989
+ ui.error(result.error);
990
+ }
991
+ }
992
+ } else if (cherryPickAction === 'abort') {
993
+ const result = await gitClient.cherryPickAbort({ directory: options.directory });
994
+ if (result.success) {
995
+ ui.stopSpinnerSuccess('Cherry-pick aborted');
996
+ } else {
997
+ ui.stopSpinnerFail('Failed to abort cherry-pick');
998
+ if (result.error) {
999
+ ui.error(result.error);
1000
+ }
1001
+ }
1002
+ } else if (cherryPickAction === 'continue') {
1003
+ const result = await gitClient.cherryPickContinue({ directory: options.directory });
1004
+ if (result.success) {
1005
+ ui.stopSpinnerSuccess('Cherry-pick continued');
1006
+ if (result.output) {
1007
+ ui.info(result.output);
1008
+ }
1009
+ } else {
1010
+ ui.stopSpinnerFail('Failed to continue cherry-pick');
1011
+ if (result.error) {
1012
+ ui.error(result.error);
1013
+ }
1014
+ }
1015
+ }
1016
+ } catch (error: any) {
1017
+ ui.stopSpinnerFail(`Error during cherry-pick ${cherryPickAction}`);
1018
+ ui.error(error.message);
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Show blame for a file
1024
+ */
1025
+ export async function gitBlameCommand(
1026
+ file: string,
1027
+ options: GitCommandOptions = {}
1028
+ ): Promise<void> {
1029
+ ui.header('Git Blame');
1030
+
1031
+ ui.info(`File: ${file}`);
1032
+ if (options.lineRange) {
1033
+ ui.info(`Line range: ${options.lineRange}`);
1034
+ }
1035
+
1036
+ ui.startSpinner({ message: `Getting blame for ${file}...` });
1037
+
1038
+ try {
1039
+ const available = await gitClient.isAvailable();
1040
+ if (!available) {
1041
+ ui.stopSpinnerFail('Git Tools Service not available');
1042
+ ui.error('Please ensure the Git Tools Service is running.');
1043
+ return;
1044
+ }
1045
+
1046
+ const result = await gitClient.blame(file, {
1047
+ directory: options.directory,
1048
+ lineRange: options.lineRange,
1049
+ });
1050
+
1051
+ if (result.success) {
1052
+ ui.stopSpinnerSuccess(`Blame retrieved for ${file}`);
1053
+ if (result.blame.length > 0) {
1054
+ result.blame.forEach(line => console.log(line));
1055
+ } else {
1056
+ ui.info('No blame data returned');
1057
+ }
1058
+ } else {
1059
+ ui.stopSpinnerFail(`Failed to get blame for ${file}`);
1060
+ if (result.error) {
1061
+ ui.error(result.error);
1062
+ }
1063
+ }
1064
+ } catch (error: any) {
1065
+ ui.stopSpinnerFail(`Error getting blame for ${file}`);
1066
+ ui.error(error.message);
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Initialize a repository
1072
+ */
1073
+ export async function gitInitCommand(options: GitCommandOptions = {}): Promise<void> {
1074
+ ui.header('Git Init');
1075
+
1076
+ if (options.directory) {
1077
+ ui.info(`Directory: ${options.directory}`);
1078
+ }
1079
+ if (options.bare) {
1080
+ ui.info('Creating bare repository');
1081
+ }
1082
+
1083
+ ui.startSpinner({ message: 'Initializing repository...' });
1084
+
1085
+ try {
1086
+ const available = await gitClient.isAvailable();
1087
+ if (!available) {
1088
+ ui.stopSpinnerFail('Git Tools Service not available');
1089
+ ui.error('Please ensure the Git Tools Service is running.');
1090
+ return;
1091
+ }
1092
+
1093
+ const result = await gitClient.init({ directory: options.directory, bare: options.bare });
1094
+
1095
+ if (result.success) {
1096
+ ui.stopSpinnerSuccess('Repository initialized');
1097
+ if (result.output) {
1098
+ ui.info(result.output);
1099
+ }
1100
+ } else {
1101
+ ui.stopSpinnerFail('Failed to initialize repository');
1102
+ if (result.error) {
1103
+ ui.error(result.error);
1104
+ }
1105
+ }
1106
+ } catch (error: any) {
1107
+ ui.stopSpinnerFail('Error initializing repository');
1108
+ ui.error(error.message);
1109
+ }
1110
+ }
1111
+
1112
+ /**
1113
+ * Main git command router
1114
+ */
1115
+ export async function gitCommand(subcommand: string, args: string[]): Promise<void> {
1116
+ const options: GitCommandOptions = {};
1117
+
1118
+ // Extract positional args and options
1119
+ const positionalArgs: string[] = [];
1120
+
1121
+ for (let i = 0; i < args.length; i++) {
1122
+ const arg = args[i];
1123
+ if (arg === '-d' || arg === '--directory') {
1124
+ options.directory = args[++i];
1125
+ } else if (arg === '-a' || arg === '--all') {
1126
+ options.all = true;
1127
+ } else if (arg === '--amend') {
1128
+ options.amend = true;
1129
+ } else if (arg === '-r' || arg === '--remote') {
1130
+ options.remote = args[++i];
1131
+ } else if (arg === '-b' || arg === '--branch') {
1132
+ options.branch = args[++i];
1133
+ } else if (arg === '-f' || arg === '--force') {
1134
+ options.force = true;
1135
+ } else if (arg === '-u' || arg === '--set-upstream') {
1136
+ options.setUpstream = true;
1137
+ } else if (arg === '--rebase') {
1138
+ options.rebase = true;
1139
+ } else if (arg === '-p' || arg === '--prune') {
1140
+ options.prune = true;
1141
+ } else if (arg === '-n' || arg === '--limit') {
1142
+ options.limit = parseInt(args[++i], 10);
1143
+ } else if (arg === '-s' || arg === '--staged') {
1144
+ options.staged = true;
1145
+ } else if (arg === '--file') {
1146
+ options.file = args[++i];
1147
+ } else if (arg === '-c' || arg === '--create') {
1148
+ options.create = true;
1149
+ } else if (arg === '-m' || arg === '--message') {
1150
+ options.message = args[++i];
1151
+ positionalArgs.push(options.message);
1152
+ } else if (arg === '--soft') {
1153
+ options.soft = true;
1154
+ } else if (arg === '--mixed') {
1155
+ options.mixed = true;
1156
+ } else if (arg === '--hard') {
1157
+ options.hard = true;
1158
+ } else if (arg === '--no-commit') {
1159
+ options.noCommit = true;
1160
+ } else if (arg === '--no-edit') {
1161
+ options.noEdit = true;
1162
+ } else if (arg === '--annotated') {
1163
+ options.annotated = true;
1164
+ } else if (arg === '--bare') {
1165
+ options.bare = true;
1166
+ } else if (arg === '--line-range') {
1167
+ options.lineRange = args[++i];
1168
+ } else if (!arg.startsWith('-')) {
1169
+ positionalArgs.push(arg);
1170
+ }
1171
+ }
1172
+
1173
+ const startTime = Date.now();
1174
+ const entry = historyManager.addEntry('git', [subcommand, ...args]);
1175
+
1176
+ try {
1177
+ switch (subcommand) {
1178
+ case 'status':
1179
+ await gitStatusCommand(options);
1180
+ break;
1181
+ case 'add':
1182
+ if (positionalArgs.length < 1 && !options.all) {
1183
+ ui.error('Usage: nimbus git add <files...> or nimbus git add -a');
1184
+ return;
1185
+ }
1186
+ await gitAddCommand(positionalArgs, options);
1187
+ break;
1188
+ case 'commit':
1189
+ if (positionalArgs.length < 1) {
1190
+ ui.error('Usage: nimbus git commit -m "message"');
1191
+ return;
1192
+ }
1193
+ await gitCommitCommand(positionalArgs[0], options);
1194
+ break;
1195
+ case 'push':
1196
+ await gitPushCommand(options);
1197
+ break;
1198
+ case 'pull':
1199
+ await gitPullCommand(options);
1200
+ break;
1201
+ case 'fetch':
1202
+ await gitFetchCommand(options);
1203
+ break;
1204
+ case 'log':
1205
+ await gitLogCommand(options);
1206
+ break;
1207
+ case 'branch':
1208
+ await gitBranchCommand(options);
1209
+ break;
1210
+ case 'checkout':
1211
+ if (positionalArgs.length < 1) {
1212
+ ui.error('Usage: nimbus git checkout <branch-or-file>');
1213
+ return;
1214
+ }
1215
+ await gitCheckoutCommand(positionalArgs[0], options);
1216
+ break;
1217
+ case 'diff':
1218
+ await gitDiffCommand(options);
1219
+ break;
1220
+ case 'merge':
1221
+ if (positionalArgs.length < 1) {
1222
+ ui.error('Usage: nimbus git merge <branch>');
1223
+ return;
1224
+ }
1225
+ await gitMergeCommand(positionalArgs[0], options);
1226
+ break;
1227
+ case 'clone':
1228
+ if (positionalArgs.length < 1) {
1229
+ ui.error('Usage: nimbus git clone <url> [path] [--branch <branch>] [--limit <depth>]');
1230
+ return;
1231
+ }
1232
+ await gitCloneCommand(positionalArgs[0], positionalArgs[1], options);
1233
+ break;
1234
+ case 'stash': {
1235
+ const validStashActions = ['push', 'pop', 'list', 'drop', 'apply', 'clear'];
1236
+ const stashAction = (positionalArgs[0] || 'push') as
1237
+ | 'push'
1238
+ | 'pop'
1239
+ | 'list'
1240
+ | 'drop'
1241
+ | 'apply'
1242
+ | 'clear';
1243
+ if (!validStashActions.includes(stashAction)) {
1244
+ ui.error(`Unknown stash action: ${stashAction}`);
1245
+ ui.info('Actions: push, pop, list, drop, apply, clear');
1246
+ return;
1247
+ }
1248
+ await gitStashCommand(stashAction, {
1249
+ ...options,
1250
+ message: options.message,
1251
+ });
1252
+ break;
1253
+ }
1254
+ case 'tag': {
1255
+ const validTagActions = ['list', 'create', 'delete', 'push', 'show'];
1256
+ const tagAction = (positionalArgs[0] || 'list') as
1257
+ | 'list'
1258
+ | 'create'
1259
+ | 'delete'
1260
+ | 'push'
1261
+ | 'show';
1262
+ if (!validTagActions.includes(tagAction)) {
1263
+ ui.error(`Unknown tag action: ${tagAction}`);
1264
+ ui.info('Actions: list (default), create, delete, push, show');
1265
+ return;
1266
+ }
1267
+ // The second positional arg is the tag name (for create/delete/push/show)
1268
+ const tagArg = positionalArgs[1];
1269
+ await gitTagCommand(tagAction, { ...options, tagArg });
1270
+ break;
1271
+ }
1272
+ case 'remote': {
1273
+ const remoteName = positionalArgs[0];
1274
+ await gitRemoteCommand(remoteName, options);
1275
+ break;
1276
+ }
1277
+ case 'reset': {
1278
+ if (positionalArgs.length < 1) {
1279
+ ui.error('Usage: nimbus git reset <commit-ref> [--soft | --mixed | --hard]');
1280
+ return;
1281
+ }
1282
+ await gitResetCommand(positionalArgs[0], options);
1283
+ break;
1284
+ }
1285
+ case 'revert': {
1286
+ if (positionalArgs.length < 1) {
1287
+ ui.error('Usage: nimbus git revert <commit-hash> [--no-commit] [--no-edit]');
1288
+ return;
1289
+ }
1290
+ await gitRevertCommand(positionalArgs[0], options);
1291
+ break;
1292
+ }
1293
+ case 'cherry-pick': {
1294
+ // Determine action: if first positional arg is 'abort' or 'continue', use it as action
1295
+ let cherryPickAction: 'pick' | 'abort' | 'continue' = 'pick';
1296
+ let cherryPickCommit: string | undefined;
1297
+ if (positionalArgs[0] === 'abort') {
1298
+ cherryPickAction = 'abort';
1299
+ } else if (positionalArgs[0] === 'continue') {
1300
+ cherryPickAction = 'continue';
1301
+ } else {
1302
+ cherryPickAction = 'pick';
1303
+ cherryPickCommit = positionalArgs[0];
1304
+ if (!cherryPickCommit) {
1305
+ ui.error('Usage: nimbus git cherry-pick <commit-hash> [--no-commit]');
1306
+ ui.info('Also: nimbus git cherry-pick abort | continue');
1307
+ return;
1308
+ }
1309
+ }
1310
+ await gitCherryPickCommand(cherryPickAction, { ...options, commit: cherryPickCommit });
1311
+ break;
1312
+ }
1313
+ case 'blame': {
1314
+ if (positionalArgs.length < 1) {
1315
+ ui.error('Usage: nimbus git blame <file> [--line-range <start,end>]');
1316
+ return;
1317
+ }
1318
+ await gitBlameCommand(positionalArgs[0], options);
1319
+ break;
1320
+ }
1321
+ case 'init': {
1322
+ // optional directory as positional arg
1323
+ if (positionalArgs[0]) {
1324
+ options.directory = positionalArgs[0];
1325
+ }
1326
+ await gitInitCommand(options);
1327
+ break;
1328
+ }
1329
+ default:
1330
+ ui.error(`Unknown git subcommand: ${subcommand}`);
1331
+ ui.info(
1332
+ 'Available commands: status, add, commit, push, pull, fetch, log, branch, checkout, diff, merge, clone, stash, tag, remote, reset, revert, cherry-pick, blame, init'
1333
+ );
1334
+ }
1335
+
1336
+ historyManager.completeEntry(entry.id, 'success', Date.now() - startTime);
1337
+ } catch (error: any) {
1338
+ historyManager.completeEntry(entry.id, 'failure', Date.now() - startTime, {
1339
+ error: error.message,
1340
+ });
1341
+ throw error;
1342
+ }
1343
+ }