@g-abhishek/gitx 0.1.2 → 0.1.5

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 (165) hide show
  1. package/README.md +386 -3
  2. package/dist/ai/claudeAi.d.ts +35 -0
  3. package/dist/ai/claudeAi.d.ts.map +1 -0
  4. package/dist/ai/claudeAi.js +396 -0
  5. package/dist/ai/claudeAi.js.map +1 -0
  6. package/dist/ai/claudeCliAi.d.ts +27 -0
  7. package/dist/ai/claudeCliAi.d.ts.map +1 -0
  8. package/dist/ai/claudeCliAi.js +312 -0
  9. package/dist/ai/claudeCliAi.js.map +1 -0
  10. package/dist/ai/localClaudeAi.d.ts +2 -0
  11. package/dist/ai/localClaudeAi.d.ts.map +1 -0
  12. package/dist/ai/localClaudeAi.js +4 -0
  13. package/dist/ai/localClaudeAi.js.map +1 -0
  14. package/dist/ai/mockAi.d.ts +8 -1
  15. package/dist/ai/mockAi.d.ts.map +1 -1
  16. package/dist/ai/mockAi.js +57 -0
  17. package/dist/ai/mockAi.js.map +1 -1
  18. package/dist/ai/openAiAi.d.ts +33 -0
  19. package/dist/ai/openAiAi.d.ts.map +1 -0
  20. package/dist/ai/openAiAi.js +388 -0
  21. package/dist/ai/openAiAi.js.map +1 -0
  22. package/dist/ai/reviewHelpers.d.ts +66 -0
  23. package/dist/ai/reviewHelpers.d.ts.map +1 -0
  24. package/dist/ai/reviewHelpers.js +574 -0
  25. package/dist/ai/reviewHelpers.js.map +1 -0
  26. package/dist/ai/types.d.ts +247 -0
  27. package/dist/ai/types.d.ts.map +1 -1
  28. package/dist/ai/types.js.map +1 -1
  29. package/dist/cli/commands/ask.d.ts +27 -0
  30. package/dist/cli/commands/ask.d.ts.map +1 -0
  31. package/dist/cli/commands/ask.js +230 -0
  32. package/dist/cli/commands/ask.js.map +1 -0
  33. package/dist/cli/commands/commit.d.ts +16 -0
  34. package/dist/cli/commands/commit.d.ts.map +1 -0
  35. package/dist/cli/commands/commit.js +163 -0
  36. package/dist/cli/commands/commit.js.map +1 -0
  37. package/dist/cli/commands/config.d.ts +4 -0
  38. package/dist/cli/commands/config.d.ts.map +1 -0
  39. package/dist/cli/commands/config.js +666 -0
  40. package/dist/cli/commands/config.js.map +1 -0
  41. package/dist/cli/commands/implement.d.ts.map +1 -1
  42. package/dist/cli/commands/implement.js +149 -31
  43. package/dist/cli/commands/implement.js.map +1 -1
  44. package/dist/cli/commands/init.d.ts +4 -0
  45. package/dist/cli/commands/init.d.ts.map +1 -1
  46. package/dist/cli/commands/init.js +7 -69
  47. package/dist/cli/commands/init.js.map +1 -1
  48. package/dist/cli/commands/port.d.ts +32 -0
  49. package/dist/cli/commands/port.d.ts.map +1 -0
  50. package/dist/cli/commands/port.js +554 -0
  51. package/dist/cli/commands/port.js.map +1 -0
  52. package/dist/cli/commands/pr/close.d.ts +15 -0
  53. package/dist/cli/commands/pr/close.d.ts.map +1 -0
  54. package/dist/cli/commands/pr/close.js +71 -0
  55. package/dist/cli/commands/pr/close.js.map +1 -0
  56. package/dist/cli/commands/pr/create.d.ts +17 -0
  57. package/dist/cli/commands/pr/create.d.ts.map +1 -1
  58. package/dist/cli/commands/pr/create.js +208 -7
  59. package/dist/cli/commands/pr/create.js.map +1 -1
  60. package/dist/cli/commands/pr/fixComments.d.ts +5 -2
  61. package/dist/cli/commands/pr/fixComments.d.ts.map +1 -1
  62. package/dist/cli/commands/pr/fixComments.js +5 -13
  63. package/dist/cli/commands/pr/fixComments.js.map +1 -1
  64. package/dist/cli/commands/pr/index.d.ts.map +1 -1
  65. package/dist/cli/commands/pr/index.js +6 -2
  66. package/dist/cli/commands/pr/index.js.map +1 -1
  67. package/dist/cli/commands/pr/list.d.ts.map +1 -1
  68. package/dist/cli/commands/pr/list.js +24 -4
  69. package/dist/cli/commands/pr/list.js.map +1 -1
  70. package/dist/cli/commands/pr/merge.d.ts +23 -0
  71. package/dist/cli/commands/pr/merge.d.ts.map +1 -0
  72. package/dist/cli/commands/pr/merge.js +191 -0
  73. package/dist/cli/commands/pr/merge.js.map +1 -0
  74. package/dist/cli/commands/pr/resolve.d.ts +3 -0
  75. package/dist/cli/commands/pr/resolve.d.ts.map +1 -0
  76. package/dist/cli/commands/pr/resolve.js +92 -0
  77. package/dist/cli/commands/pr/resolve.js.map +1 -0
  78. package/dist/cli/commands/pr/review.d.ts.map +1 -1
  79. package/dist/cli/commands/pr/review.js +121 -6
  80. package/dist/cli/commands/pr/review.js.map +1 -1
  81. package/dist/cli/commands/push.d.ts +16 -0
  82. package/dist/cli/commands/push.d.ts.map +1 -0
  83. package/dist/cli/commands/push.js +166 -0
  84. package/dist/cli/commands/push.js.map +1 -0
  85. package/dist/cli/commands/sync.d.ts +24 -0
  86. package/dist/cli/commands/sync.d.ts.map +1 -0
  87. package/dist/cli/commands/sync.js +414 -0
  88. package/dist/cli/commands/sync.js.map +1 -0
  89. package/dist/cli/index.d.ts.map +1 -1
  90. package/dist/cli/index.js +34 -6
  91. package/dist/cli/index.js.map +1 -1
  92. package/dist/config/config.d.ts +20 -3
  93. package/dist/config/config.d.ts.map +1 -1
  94. package/dist/config/config.js +98 -45
  95. package/dist/config/config.js.map +1 -1
  96. package/dist/config/schema.d.ts.map +1 -1
  97. package/dist/config/schema.js +61 -6
  98. package/dist/config/schema.js.map +1 -1
  99. package/dist/core/context.d.ts +6 -0
  100. package/dist/core/context.d.ts.map +1 -1
  101. package/dist/core/context.js.map +1 -1
  102. package/dist/core/gitx.d.ts +43 -0
  103. package/dist/core/gitx.d.ts.map +1 -1
  104. package/dist/core/gitx.js +187 -20
  105. package/dist/core/gitx.js.map +1 -1
  106. package/dist/index.d.ts +1 -5
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +4 -1
  109. package/dist/index.js.map +1 -1
  110. package/dist/providers/azure.d.ts +26 -0
  111. package/dist/providers/azure.d.ts.map +1 -0
  112. package/dist/providers/azure.js +256 -0
  113. package/dist/providers/azure.js.map +1 -0
  114. package/dist/providers/base.d.ts +104 -0
  115. package/dist/providers/base.d.ts.map +1 -0
  116. package/dist/providers/base.js +5 -0
  117. package/dist/providers/base.js.map +1 -0
  118. package/dist/providers/factory.d.ts +8 -0
  119. package/dist/providers/factory.d.ts.map +1 -0
  120. package/dist/providers/factory.js +25 -0
  121. package/dist/providers/factory.js.map +1 -0
  122. package/dist/providers/github.d.ts +19 -0
  123. package/dist/providers/github.d.ts.map +1 -0
  124. package/dist/providers/github.js +291 -0
  125. package/dist/providers/github.js.map +1 -0
  126. package/dist/providers/gitlab.d.ts +19 -0
  127. package/dist/providers/gitlab.d.ts.map +1 -0
  128. package/dist/providers/gitlab.js +186 -0
  129. package/dist/providers/gitlab.js.map +1 -0
  130. package/dist/types/config.d.ts +50 -7
  131. package/dist/types/config.d.ts.map +1 -1
  132. package/dist/types/config.js.map +1 -1
  133. package/dist/utils/azureAuth.d.ts +51 -0
  134. package/dist/utils/azureAuth.d.ts.map +1 -0
  135. package/dist/utils/azureAuth.js +172 -0
  136. package/dist/utils/azureAuth.js.map +1 -0
  137. package/dist/utils/git.d.ts +19 -0
  138. package/dist/utils/git.d.ts.map +1 -1
  139. package/dist/utils/git.js +45 -8
  140. package/dist/utils/git.js.map +1 -1
  141. package/dist/utils/gitOps.d.ts +125 -0
  142. package/dist/utils/gitOps.d.ts.map +1 -0
  143. package/dist/utils/gitOps.js +396 -0
  144. package/dist/utils/gitOps.js.map +1 -0
  145. package/dist/utils/lockFile.d.ts +13 -0
  146. package/dist/utils/lockFile.d.ts.map +1 -0
  147. package/dist/utils/lockFile.js +54 -0
  148. package/dist/utils/lockFile.js.map +1 -0
  149. package/dist/utils/retry.d.ts +10 -0
  150. package/dist/utils/retry.d.ts.map +1 -0
  151. package/dist/utils/retry.js +31 -0
  152. package/dist/utils/retry.js.map +1 -0
  153. package/dist/workflows/implement.d.ts +41 -0
  154. package/dist/workflows/implement.d.ts.map +1 -0
  155. package/dist/workflows/implement.js +219 -0
  156. package/dist/workflows/implement.js.map +1 -0
  157. package/dist/workflows/pr.d.ts +41 -0
  158. package/dist/workflows/pr.d.ts.map +1 -0
  159. package/dist/workflows/pr.js +291 -0
  160. package/dist/workflows/pr.js.map +1 -0
  161. package/dist/workflows/prAddress.d.ts +55 -0
  162. package/dist/workflows/prAddress.d.ts.map +1 -0
  163. package/dist/workflows/prAddress.js +349 -0
  164. package/dist/workflows/prAddress.js.map +1 -0
  165. package/package.json +1 -1
@@ -0,0 +1,666 @@
1
+ import inquirer from "inquirer";
2
+ import ora from "ora";
3
+ import { logger } from "../../logger/logger.js";
4
+ import { loadConfig, saveConfig, getConfigPath } from "../../config/config.js";
5
+ import { GitxError } from "../../utils/errors.js";
6
+ import { validateNonEmpty } from "../../utils/validators.js";
7
+ import { Gitx } from "../../core/gitx.js";
8
+ import { ClaudeCliAi } from "../../ai/claudeCliAi.js";
9
+ import { verifyGcmSetup } from "../../utils/azureAuth.js";
10
+ // ─── Constants ────────────────────────────────────────────────────────────────
11
+ const GIT_PROVIDERS = ["github", "gitlab", "azure"];
12
+ const AI_PROVIDERS = ["claude", "openai", "claude-cli"];
13
+ function isGitProvider(key) {
14
+ return GIT_PROVIDERS.includes(key);
15
+ }
16
+ function isAiProvider(key) {
17
+ return AI_PROVIDERS.includes(key);
18
+ }
19
+ async function loadOrEmpty() {
20
+ try {
21
+ return await loadConfig();
22
+ }
23
+ catch {
24
+ return { providers: {} };
25
+ }
26
+ }
27
+ function redactConfig(config) {
28
+ const providers = {};
29
+ for (const [k, v] of Object.entries(config.providers)) {
30
+ if (v?.authMethod === "gcm") {
31
+ providers[k] = { authMethod: "gcm" };
32
+ }
33
+ else {
34
+ providers[k] = v?.token ? { token: v.token.slice(0, 6) + "***", authMethod: v.authMethod ?? "pat" } : {};
35
+ }
36
+ }
37
+ const aiProviders = {};
38
+ for (const [k, v] of Object.entries(config.aiProviders ?? {})) {
39
+ aiProviders[k] = k === "claude-cli"
40
+ ? { type: "local CLI" }
41
+ : { apiKey: v?.apiKey ? v.apiKey.slice(0, 6) + "***" : "(none)", ...(v?.model ? { model: v.model } : {}) };
42
+ }
43
+ return {
44
+ ...config,
45
+ providers,
46
+ ...(Object.keys(aiProviders).length ? { aiProviders } : {}),
47
+ ai: undefined,
48
+ };
49
+ }
50
+ // ─── Register command ─────────────────────────────────────────────────────────
51
+ export function registerConfigCommand(program) {
52
+ const config = program
53
+ .command("config")
54
+ .description("⚙️ Configure gitx (runs setup wizard when called with no subcommand)")
55
+ .action(async () => {
56
+ // `gitx config` with no subcommand → run the setup wizard
57
+ await runSetup();
58
+ });
59
+ // ── gitx config show ───────────────────────────────────────────────────────
60
+ config
61
+ .command("show")
62
+ .description("🔍 Show current gitx config")
63
+ .action(async () => {
64
+ let cfg;
65
+ try {
66
+ cfg = await loadConfig();
67
+ }
68
+ catch {
69
+ logger.warn("No config found. Run `gitx config` to get started.");
70
+ return;
71
+ }
72
+ // Auto-detect current repo provider
73
+ const gitx = await Gitx.fromCwd().catch(() => null);
74
+ const detected = gitx ? await gitx.detectProvider().catch(() => null) : null;
75
+ if (detected) {
76
+ logger.info(`🔎 Current repo: ${detected.repoSlug} (${detected.provider})`);
77
+ const provCfg = cfg.providers[detected.provider];
78
+ const authMethod = provCfg?.authMethod ?? "pat";
79
+ if (authMethod === "gcm") {
80
+ logger.success(` ${detected.provider}: configured via GCM (OAuth) ✓`);
81
+ }
82
+ else if (provCfg?.token) {
83
+ logger.success(` ${detected.provider} token: configured (PAT) ✓`);
84
+ }
85
+ else {
86
+ logger.warn(` ${detected.provider} token: NOT configured — run: gitx config set ${detected.provider}`);
87
+ }
88
+ }
89
+ // Show all AI providers
90
+ logger.info("\n🤖 AI Providers:");
91
+ const aiEntries = Object.entries(cfg.aiProviders ?? {});
92
+ if (aiEntries.length === 0) {
93
+ logger.warn(" none configured — run: gitx config set claude|openai|claude-cli");
94
+ }
95
+ else {
96
+ for (const [kind] of aiEntries) {
97
+ const isDefault = cfg.defaultAiProvider === kind;
98
+ const marker = isDefault ? " (default)" : "";
99
+ if (kind === "claude-cli") {
100
+ const available = await ClaudeCliAi.isAvailable();
101
+ logger.info(` ${isDefault ? "✓" : "○"} claude-cli${marker} — ${available ? "installed ✓" : "not detected ✗"}`);
102
+ }
103
+ else {
104
+ logger.info(` ${isDefault ? "✓" : "○"} ${kind}${marker}`);
105
+ }
106
+ }
107
+ }
108
+ logger.info(`\n📍 Config file: ${getConfigPath()}`);
109
+ logger.info(JSON.stringify(redactConfig(cfg), null, 2));
110
+ });
111
+ // ── gitx config set <KEY> [value] ─────────────────────────────────────────
112
+ // KEY = github | gitlab | azure | claude | openai | claude-cli
113
+ config
114
+ .command("set")
115
+ .description("🔑 Set a provider token or AI key (also sets it as the default AI)\n" +
116
+ " Git: gitx config set github|gitlab|azure [token]\n" +
117
+ " AI: gitx config set claude|openai [apiKey]\n" +
118
+ " gitx config set claude-cli")
119
+ .argument("<key>", "Provider: github | gitlab | azure | claude | openai | claude-cli")
120
+ .argument("[value]", "Token or API key (prompted if omitted; not needed for claude-cli)")
121
+ .action(async (key, valueArg) => {
122
+ if (isGitProvider(key)) {
123
+ await setGitProvider(key, valueArg);
124
+ }
125
+ else if (isAiProvider(key)) {
126
+ await setAiProvider(key, valueArg);
127
+ }
128
+ else {
129
+ throw new GitxError(`Unknown key: "${key}". Use one of: github, gitlab, azure, claude, openai, claude-cli`, { exitCode: 2 });
130
+ }
131
+ });
132
+ // ── gitx config set-default-ai [provider] ────────────────────────────────
133
+ config
134
+ .command("set-default-ai")
135
+ .description("⭐ Switch which AI provider gitx uses by default")
136
+ .argument("[provider]", "AI provider to set as default (prompted if omitted)")
137
+ .action(async (providerArg) => {
138
+ await setDefaultAi(providerArg);
139
+ });
140
+ // ── gitx config set-default-branch <branch> ──────────────────────────────
141
+ config
142
+ .command("set-default-branch")
143
+ .description("🌿 Set default base branch")
144
+ .argument("<branch>", "Branch name (e.g. main)")
145
+ .action(async (branch) => {
146
+ const ok = validateNonEmpty("Default branch")(branch);
147
+ if (ok !== true)
148
+ throw new GitxError(String(ok), { exitCode: 2 });
149
+ const existing = await loadOrEmpty();
150
+ const updated = { ...existing, defaultBranch: branch.trim() };
151
+ const spinner = ora("Saving…").start();
152
+ const path = await saveConfig(updated);
153
+ spinner.succeed(`Saved to ${path}`);
154
+ logger.success("✅ Default branch updated.");
155
+ });
156
+ }
157
+ // ─── Set git provider ─────────────────────────────────────────────────────────
158
+ async function setGitProvider(provider, tokenArg) {
159
+ const existing = await loadOrEmpty();
160
+ // Azure DevOps: offer GCM (OAuth) or PAT
161
+ if (provider === "azure") {
162
+ await setAzureProvider(existing, tokenArg);
163
+ return;
164
+ }
165
+ const hints = {
166
+ github: "github.com/settings/tokens → New token → scope: repo",
167
+ gitlab: "gitlab.com/-/profile/personal_access_tokens → scope: api",
168
+ };
169
+ logger.info(` ℹ️ Get a token at: ${hints[provider]}\n`);
170
+ const token = tokenArg?.trim().length
171
+ ? tokenArg.trim()
172
+ : (await inquirer.prompt([
173
+ {
174
+ type: "password",
175
+ name: "token",
176
+ message: `Token for ${provider}:`,
177
+ mask: "*",
178
+ validate: validateNonEmpty("Token"),
179
+ },
180
+ ])).token;
181
+ const updated = {
182
+ ...existing,
183
+ providers: { ...existing.providers, [provider]: { token } },
184
+ };
185
+ const spinner = ora("Saving…").start();
186
+ const path = await saveConfig(updated);
187
+ spinner.succeed(`Saved to ${path}`);
188
+ logger.success(`✅ ${provider} token updated.`);
189
+ }
190
+ // ─── Azure DevOps: GCM or PAT ─────────────────────────────────────────────────
191
+ async function setAzureProvider(existing, tokenArg) {
192
+ const currentMethod = existing.providers.azure?.authMethod ?? "pat";
193
+ logger.info("\n🔐 Azure DevOps authentication\n");
194
+ logger.info(" Your company may restrict PAT tokens. GCM (OAuth) is the recommended method.\n");
195
+ const { authMethod } = await inquirer.prompt([
196
+ {
197
+ type: "list",
198
+ name: "authMethod",
199
+ message: "Authentication method:",
200
+ choices: [
201
+ {
202
+ name: `GCM — Git Credential Manager (OAuth, no token to manage)${currentMethod === "gcm" ? " ✓ current" : " ← recommended"}`,
203
+ value: "gcm",
204
+ },
205
+ {
206
+ name: `PAT — Personal Access Token${currentMethod === "pat" ? " ✓ current" : ""}`,
207
+ value: "pat",
208
+ },
209
+ ],
210
+ default: currentMethod,
211
+ },
212
+ ]);
213
+ if (authMethod === "gcm") {
214
+ await setupAzureGcm(existing);
215
+ }
216
+ else {
217
+ await setupAzurePat(existing, tokenArg);
218
+ }
219
+ }
220
+ async function setupAzureGcm(existing) {
221
+ logger.info("\n── GCM setup\n");
222
+ logger.info(" GCM uses `git credential fill` to obtain a short-lived OAuth token.");
223
+ logger.info(" No token is stored in the gitx config — GCM is the secure credential store.\n");
224
+ // Try to detect the org from the current repo remote
225
+ let detectedOrg;
226
+ try {
227
+ const gitx = await Gitx.fromCwd();
228
+ const det = await gitx.detectProvider();
229
+ if (det?.provider === "azure") {
230
+ detectedOrg = det.repoSlug.split("/")[0];
231
+ }
232
+ }
233
+ catch { /* not in a git repo */ }
234
+ const { org } = await inquirer.prompt([
235
+ {
236
+ type: "input",
237
+ name: "org",
238
+ message: "Azure DevOps org name (e.g. MyCompany):",
239
+ default: detectedOrg,
240
+ validate: validateNonEmpty("Org name"),
241
+ },
242
+ ]);
243
+ const verifySpinner = ora("Verifying GCM setup…").start();
244
+ const result = await verifyGcmSetup(org);
245
+ if (result.ok) {
246
+ verifySpinner.succeed("GCM is correctly configured and a token was fetched successfully ✓");
247
+ }
248
+ else {
249
+ verifySpinner.warn("GCM setup has issues:");
250
+ result.issues.forEach((issue) => logger.warn(` ✗ ${issue}`));
251
+ if (result.fixes.length > 0) {
252
+ logger.info("\n Run these commands to fix the issues:");
253
+ result.fixes.forEach((fix) => logger.info(` $ ${fix}`));
254
+ }
255
+ logger.info("");
256
+ const { saveAnyway } = await inquirer.prompt([
257
+ {
258
+ type: "confirm",
259
+ name: "saveAnyway",
260
+ message: "Save GCM config anyway? (you can fix the issues and it will work next time)",
261
+ default: false,
262
+ },
263
+ ]);
264
+ if (!saveAnyway) {
265
+ logger.info(" Cancelled — no changes saved.");
266
+ return;
267
+ }
268
+ }
269
+ const updated = {
270
+ ...existing,
271
+ providers: {
272
+ ...existing.providers,
273
+ azure: { authMethod: "gcm" },
274
+ },
275
+ };
276
+ const spinner = ora("Saving…").start();
277
+ const path = await saveConfig(updated);
278
+ spinner.succeed(`Saved to ${path}`);
279
+ logger.success("✅ Azure DevOps configured to use GCM (OAuth).");
280
+ logger.info(" gitx will call `git credential fill` automatically when needed.");
281
+ }
282
+ async function setupAzurePat(existing, tokenArg) {
283
+ logger.info("\n── PAT setup\n");
284
+ logger.info(" ℹ️ Get a PAT at: dev.azure.com → User settings → Personal access tokens\n");
285
+ logger.info(" Scope required: Code (Read & write)\n");
286
+ const token = tokenArg?.trim().length
287
+ ? tokenArg.trim()
288
+ : (await inquirer.prompt([
289
+ {
290
+ type: "password",
291
+ name: "token",
292
+ message: "Azure DevOps PAT token:",
293
+ mask: "*",
294
+ validate: validateNonEmpty("Token"),
295
+ },
296
+ ])).token;
297
+ const updated = {
298
+ ...existing,
299
+ providers: {
300
+ ...existing.providers,
301
+ azure: { token, authMethod: "pat" },
302
+ },
303
+ };
304
+ const spinner = ora("Saving…").start();
305
+ const path = await saveConfig(updated);
306
+ spinner.succeed(`Saved to ${path}`);
307
+ logger.success("✅ Azure DevOps PAT token saved.");
308
+ }
309
+ // ─── Set AI provider ──────────────────────────────────────────────────────────
310
+ async function setAiProvider(aiProvider, keyArg) {
311
+ const existing = await loadOrEmpty();
312
+ if (aiProvider === "claude-cli") {
313
+ // No key needed — just detect and register
314
+ const spinner = ora("Checking for local Claude CLI…").start();
315
+ const available = await ClaudeCliAi.isAvailable();
316
+ if (!available) {
317
+ spinner.fail("claude-cli not found on PATH.");
318
+ logger.warn("Install Claude Code from https://claude.ai/download and try again.");
319
+ return;
320
+ }
321
+ spinner.succeed("claude-cli detected ✓");
322
+ const updated = {
323
+ ...existing,
324
+ aiProviders: { ...(existing.aiProviders ?? {}), "claude-cli": {} },
325
+ defaultAiProvider: "claude-cli",
326
+ };
327
+ const savePath = await saveConfig(updated);
328
+ logger.success(`✅ claude-cli set as default AI. (saved to ${savePath})`);
329
+ return;
330
+ }
331
+ // claude / openai — need an API key
332
+ const hints = {
333
+ claude: "console.anthropic.com → API Keys",
334
+ openai: "platform.openai.com → API keys",
335
+ };
336
+ logger.info(` ℹ️ Get a key at: ${hints[aiProvider]}\n`);
337
+ const apiKey = keyArg?.trim().length
338
+ ? keyArg.trim()
339
+ : (await inquirer.prompt([
340
+ {
341
+ type: "password",
342
+ name: "apiKey",
343
+ message: `API key for ${aiProvider}:`,
344
+ mask: "*",
345
+ validate: validateNonEmpty("API key"),
346
+ },
347
+ ])).apiKey;
348
+ const updated = {
349
+ ...existing,
350
+ aiProviders: {
351
+ ...(existing.aiProviders ?? {}),
352
+ [aiProvider]: {
353
+ ...(existing.aiProviders?.[aiProvider] ?? {}),
354
+ apiKey,
355
+ },
356
+ },
357
+ defaultAiProvider: aiProvider,
358
+ };
359
+ const spinner = ora("Saving…").start();
360
+ const path = await saveConfig(updated);
361
+ spinner.succeed(`Saved to ${path}`);
362
+ logger.success(`✅ ${aiProvider} configured and set as default AI provider.`);
363
+ logger.info(" Note: ANTHROPIC_API_KEY env var always overrides stored keys.");
364
+ }
365
+ // ─── Switch default AI ────────────────────────────────────────────────────────
366
+ async function setDefaultAi(providerArg) {
367
+ const existing = await loadOrEmpty();
368
+ const configured = Object.keys(existing.aiProviders ?? {});
369
+ if (configured.length === 0) {
370
+ logger.warn("No AI providers configured yet.");
371
+ logger.info("Run `gitx config set claude|openai|claude-cli` to add one.");
372
+ return;
373
+ }
374
+ let chosen;
375
+ if (providerArg && isAiProvider(providerArg)) {
376
+ if (!configured.includes(providerArg)) {
377
+ logger.warn(`"${providerArg}" is not yet configured. Run: gitx config set ${providerArg}`);
378
+ return;
379
+ }
380
+ chosen = providerArg;
381
+ }
382
+ else if (configured.length === 1) {
383
+ // Only one provider — set it silently, no need to ask
384
+ chosen = configured[0];
385
+ logger.info(`Only one AI provider configured — setting "${chosen}" as default.`);
386
+ }
387
+ else {
388
+ // Multiple providers — show a picker
389
+ const choices = await Promise.all(configured.map(async (k) => {
390
+ let suffix = "";
391
+ if (k === "claude-cli") {
392
+ const avail = await ClaudeCliAi.isAvailable();
393
+ suffix = avail ? " (installed ✓)" : " (not detected ✗)";
394
+ }
395
+ const isDefault = existing.defaultAiProvider === k;
396
+ return {
397
+ name: `${k}${suffix}${isDefault ? " ← current default" : ""}`,
398
+ value: k,
399
+ };
400
+ }));
401
+ const result = await inquirer.prompt([
402
+ {
403
+ type: "list",
404
+ name: "provider",
405
+ message: "Which AI provider should be the default?",
406
+ choices,
407
+ default: existing.defaultAiProvider,
408
+ },
409
+ ]);
410
+ chosen = result.provider;
411
+ }
412
+ const updated = { ...existing, defaultAiProvider: chosen };
413
+ const spinner = ora("Saving…").start();
414
+ const path = await saveConfig(updated);
415
+ spinner.succeed(`Saved to ${path}`);
416
+ logger.success(`✅ Default AI provider set to: ${chosen}`);
417
+ }
418
+ // ─── Setup wizard ─────────────────────────────────────────────────────────────
419
+ export async function runSetup() {
420
+ logger.info("🚀 gitx setup\n");
421
+ // Detect current repo git provider
422
+ let detectedProvider;
423
+ try {
424
+ const gitx = await Gitx.fromCwd();
425
+ const det = await gitx.detectProvider();
426
+ if (det) {
427
+ detectedProvider = det.provider;
428
+ logger.info(`🔎 Detected repo provider: ${det.provider} (${det.repoSlug})\n`);
429
+ }
430
+ }
431
+ catch { /* not in a git repo */ }
432
+ const existing = await loadOrEmpty();
433
+ const cliAvail = await ClaudeCliAi.isAvailable();
434
+ // ── Step 1: Git provider ─────────────────────────────────────────────────
435
+ logger.info("── Step 1 of 2: Git provider\n");
436
+ const hasAnyGitProvider = Object.values(existing.providers).some((v) => v?.token);
437
+ const gitChoices = [
438
+ { name: "GitHub", value: "github" },
439
+ { name: "GitLab", value: "gitlab" },
440
+ { name: "Azure DevOps", value: "azure" },
441
+ ];
442
+ // Annotate already-configured providers
443
+ const annotatedGitChoices = gitChoices.map((c) => {
444
+ const hasToken = Boolean(existing.providers[c.value]?.token);
445
+ return hasToken ? { ...c, name: `${c.name} ✓ already configured` } : c;
446
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
447
+ });
448
+ // Only show Skip if at least one provider is already configured
449
+ if (hasAnyGitProvider) {
450
+ annotatedGitChoices.push(new inquirer.Separator());
451
+ annotatedGitChoices.push({ name: "Skip — keep existing git config", value: "skip" });
452
+ }
453
+ const { providerOrSkip } = await inquirer.prompt([
454
+ {
455
+ type: "list",
456
+ name: "providerOrSkip",
457
+ message: hasAnyGitProvider
458
+ ? "Which git provider? (select Skip to leave unchanged)"
459
+ : "Which git provider is this repo on?",
460
+ choices: annotatedGitChoices,
461
+ default: detectedProvider ?? "github",
462
+ },
463
+ ]);
464
+ const providerHints = {
465
+ github: "github.com/settings/tokens → New token → scope: repo",
466
+ gitlab: "gitlab.com/-/profile/personal_access_tokens → scope: api",
467
+ azure: "dev.azure.com → User settings → Personal access tokens → scope: Code (Read & write)",
468
+ };
469
+ // Track what to save for git providers
470
+ let updatedProviders = existing.providers;
471
+ if (providerOrSkip === "skip") {
472
+ logger.info(" Skipping git provider setup — existing config unchanged.\n");
473
+ }
474
+ else if (providerOrSkip === "azure") {
475
+ // Azure DevOps — delegate to the full GCM/PAT wizard which saves its own config
476
+ await setAzureProvider(existing);
477
+ // Reload providers so the merged save below reflects any changes
478
+ const reloaded = await loadOrEmpty();
479
+ updatedProviders = reloaded.providers;
480
+ }
481
+ else {
482
+ const provider = providerOrSkip;
483
+ const existingToken = existing.providers[provider]?.token;
484
+ if (existingToken) {
485
+ // Already configured — show masked value and ask to keep or replace
486
+ const masked = existingToken.slice(0, 6) + "•".repeat(Math.min(existingToken.length - 6, 20));
487
+ logger.info(` ✓ ${provider} token already configured: ${masked}`);
488
+ const { replaceToken } = await inquirer.prompt([
489
+ {
490
+ type: "confirm",
491
+ name: "replaceToken",
492
+ message: `Replace the existing ${provider} token?`,
493
+ default: false,
494
+ },
495
+ ]);
496
+ if (replaceToken) {
497
+ logger.info(` ℹ️ Get a token at: ${providerHints[provider]}\n`);
498
+ const answer = await inquirer.prompt([
499
+ {
500
+ type: "password",
501
+ name: "token",
502
+ message: `New ${provider} access token:`,
503
+ mask: "*",
504
+ validate: validateNonEmpty("Token"),
505
+ },
506
+ ]);
507
+ updatedProviders = { ...existing.providers, [provider]: { token: answer.token } };
508
+ }
509
+ else {
510
+ logger.info(" Keeping existing token.\n");
511
+ // updatedProviders already has existing.providers, no change needed
512
+ }
513
+ }
514
+ else {
515
+ logger.info(` ℹ️ Get a token at: ${providerHints[provider]}\n`);
516
+ const answer = await inquirer.prompt([
517
+ {
518
+ type: "password",
519
+ name: "token",
520
+ message: `${provider} access token:`,
521
+ mask: "*",
522
+ validate: validateNonEmpty("Token"),
523
+ },
524
+ ]);
525
+ updatedProviders = { ...existing.providers, [provider]: { token: answer.token } };
526
+ }
527
+ }
528
+ // ── Step 2: AI provider ──────────────────────────────────────────────────
529
+ logger.info("\n── Step 2 of 2: AI provider\n");
530
+ if (cliAvail)
531
+ logger.info(" ✓ Claude CLI detected on your system.\n");
532
+ const { setupAi } = await inquirer.prompt([
533
+ {
534
+ type: "confirm",
535
+ name: "setupAi",
536
+ message: "Configure an AI provider? (needed for implement, review, fix-comments)",
537
+ default: true,
538
+ },
539
+ ]);
540
+ let newAiProviders = existing.aiProviders ?? {};
541
+ let newDefaultAi = existing.defaultAiProvider;
542
+ if (setupAi) {
543
+ // Annotate already-configured AI providers
544
+ const aiChoices = [
545
+ ...(cliAvail
546
+ ? [{ name: `Claude CLI (free, uses your Claude login)${existing.aiProviders?.["claude-cli"] ? " ✓ already configured" : " ← detected!"}`, value: "claude-cli" }]
547
+ : [{ name: "Claude CLI (not detected — install Claude Code first)", value: "claude-cli", disabled: true }]),
548
+ {
549
+ name: `Claude API (Anthropic)${existing.aiProviders?.["claude"] ? " ✓ already configured" : " — recommended for CI/CD"}`,
550
+ value: "claude",
551
+ },
552
+ {
553
+ name: `OpenAI (GPT-4o)${existing.aiProviders?.["openai"] ? " ✓ already configured" : ""}`,
554
+ value: "openai",
555
+ },
556
+ new inquirer.Separator(),
557
+ { name: "Skip — keep existing AI config", value: "skip" },
558
+ ];
559
+ const { aiProvider } = await inquirer.prompt([
560
+ {
561
+ type: "list",
562
+ name: "aiProvider",
563
+ message: "AI provider: (select Skip to leave unchanged)",
564
+ choices: aiChoices,
565
+ default: cliAvail ? "claude-cli" : (existing.defaultAiProvider ?? "claude"),
566
+ },
567
+ ]);
568
+ if (aiProvider === "skip") {
569
+ logger.info(" Skipping AI provider setup — existing AI config unchanged.\n");
570
+ // newAiProviders and newDefaultAi stay as their current values, fall through to save
571
+ }
572
+ else if (aiProvider === "claude-cli") {
573
+ // Claude CLI — no key needed
574
+ if (existing.aiProviders?.["claude-cli"]) {
575
+ logger.info(" ✓ Claude CLI already configured — keeping as is.\n");
576
+ }
577
+ else {
578
+ logger.success(" Using Claude CLI — no API key needed.\n");
579
+ }
580
+ newAiProviders = { ...newAiProviders, "claude-cli": {} };
581
+ newDefaultAi = "claude-cli";
582
+ }
583
+ else if (aiProvider === "claude" || aiProvider === "openai") {
584
+ const hints = {
585
+ claude: "console.anthropic.com → API Keys",
586
+ openai: "platform.openai.com → API keys",
587
+ };
588
+ const existingKey = existing.aiProviders?.[aiProvider]?.apiKey;
589
+ if (existingKey) {
590
+ // Already configured — show masked and ask to keep or replace
591
+ const maskedKey = existingKey.slice(0, 10) + "•".repeat(Math.min(existingKey.length - 10, 20));
592
+ logger.info(` ✓ ${aiProvider} API key already configured: ${maskedKey}`);
593
+ const { replaceKey } = await inquirer.prompt([
594
+ {
595
+ type: "confirm",
596
+ name: "replaceKey",
597
+ message: `Replace the existing ${aiProvider} API key?`,
598
+ default: false,
599
+ },
600
+ ]);
601
+ if (replaceKey) {
602
+ logger.info(` ℹ️ Get a key at: ${hints[aiProvider]}\n`);
603
+ const answer = await inquirer.prompt([
604
+ {
605
+ type: "password",
606
+ name: "apiKey",
607
+ message: `New ${aiProvider} API key (leave blank to skip):`,
608
+ mask: "*",
609
+ },
610
+ ]);
611
+ if (!answer.apiKey.trim()) {
612
+ logger.info(" Skipping — keeping existing key.\n");
613
+ newAiProviders = { ...newAiProviders, [aiProvider]: { ...(newAiProviders?.[aiProvider] ?? {}), apiKey: existingKey } };
614
+ }
615
+ else {
616
+ newAiProviders = { ...newAiProviders, [aiProvider]: { ...(newAiProviders?.[aiProvider] ?? {}), apiKey: answer.apiKey } };
617
+ newDefaultAi = aiProvider;
618
+ }
619
+ }
620
+ else {
621
+ logger.info(" Keeping existing key.\n");
622
+ newAiProviders = { ...newAiProviders, [aiProvider]: { ...(newAiProviders?.[aiProvider] ?? {}), apiKey: existingKey } };
623
+ newDefaultAi = aiProvider;
624
+ }
625
+ }
626
+ else {
627
+ logger.info(` ℹ️ Get a key at: ${hints[aiProvider]}\n`);
628
+ const answer = await inquirer.prompt([
629
+ {
630
+ type: "password",
631
+ name: "apiKey",
632
+ message: `${aiProvider} API key (leave blank to skip):`,
633
+ mask: "*",
634
+ },
635
+ ]);
636
+ if (!answer.apiKey.trim()) {
637
+ logger.info(" Skipping — no key entered, AI provider not saved.\n");
638
+ }
639
+ else {
640
+ newAiProviders = { ...newAiProviders, [aiProvider]: { ...(newAiProviders?.[aiProvider] ?? {}), apiKey: answer.apiKey } };
641
+ newDefaultAi = aiProvider;
642
+ }
643
+ }
644
+ }
645
+ }
646
+ // ── Save ─────────────────────────────────────────────────────────────────
647
+ const merged = {
648
+ providers: updatedProviders,
649
+ ...(Object.keys(newAiProviders).length ? { aiProviders: newAiProviders } : {}),
650
+ ...(newDefaultAi ? { defaultAiProvider: newDefaultAi } : {}),
651
+ ...(existing.defaultBranch ? { defaultBranch: existing.defaultBranch } : {}),
652
+ };
653
+ const spinner = ora("\nSaving config…").start();
654
+ const savedPath = await saveConfig(merged);
655
+ spinner.succeed(`Config saved to ${savedPath}`);
656
+ const allProviders = Object.keys(merged.providers).join(", ");
657
+ logger.success(`\n✅ gitx is ready!`);
658
+ logger.info(` Git providers: ${allProviders}`);
659
+ logger.info(` Default AI: ${merged.defaultAiProvider ?? "not configured"}`);
660
+ if (merged.aiProviders && Object.keys(merged.aiProviders).length > 1) {
661
+ logger.info(` All AI: ${Object.keys(merged.aiProviders).join(", ")}`);
662
+ logger.info(` Switch AI: gitx config set-default-ai`);
663
+ }
664
+ logger.info(`\nRun \`gitx pr list\` or \`gitx implement "<task>"\` in any git repo.`);
665
+ }
666
+ //# sourceMappingURL=config.js.map