@rotorsoft/gent 1.1.2 → 1.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.
- package/README.md +28 -9
- package/dist/{chunk-32TIYLFY.js → chunk-2LGYNV6S.js} +46 -2
- package/dist/chunk-2LGYNV6S.js.map +1 -0
- package/dist/index.js +239 -105
- package/dist/index.js.map +1 -1
- package/dist/{setup-labels-3VZA2QD6.js → setup-labels-3IMSEEKN.js} +2 -2
- package/package.json +1 -1
- package/templates/.gent.yml +11 -0
- package/dist/chunk-32TIYLFY.js.map +0 -1
- /package/dist/{setup-labels-3VZA2QD6.js.map → setup-labels-3IMSEEKN.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
addIssueComment,
|
|
4
4
|
assignIssue,
|
|
5
5
|
buildIssueLabels,
|
|
6
|
+
checkAIProvider,
|
|
6
7
|
checkClaudeCli,
|
|
8
|
+
checkGeminiCli,
|
|
7
9
|
checkGhAuth,
|
|
8
10
|
checkGitRepo,
|
|
9
11
|
colors,
|
|
@@ -28,7 +30,7 @@ import {
|
|
|
28
30
|
sortByPriority,
|
|
29
31
|
updateIssueLabels,
|
|
30
32
|
withSpinner
|
|
31
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-2LGYNV6S.js";
|
|
32
34
|
|
|
33
35
|
// src/index.ts
|
|
34
36
|
import { Command } from "commander";
|
|
@@ -183,7 +185,7 @@ async function initCommand(options) {
|
|
|
183
185
|
}
|
|
184
186
|
]);
|
|
185
187
|
if (setupLabels) {
|
|
186
|
-
const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-
|
|
188
|
+
const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-3IMSEEKN.js");
|
|
187
189
|
await setupLabelsCommand2();
|
|
188
190
|
}
|
|
189
191
|
}
|
|
@@ -193,54 +195,7 @@ import inquirer2 from "inquirer";
|
|
|
193
195
|
import chalk from "chalk";
|
|
194
196
|
|
|
195
197
|
// src/lib/claude.ts
|
|
196
|
-
import { spawn } from "child_process";
|
|
197
198
|
import { execa } from "execa";
|
|
198
|
-
async function invokeClaude(options) {
|
|
199
|
-
const args = ["--print"];
|
|
200
|
-
if (options.permissionMode) {
|
|
201
|
-
args.push("--permission-mode", options.permissionMode);
|
|
202
|
-
}
|
|
203
|
-
args.push(options.prompt);
|
|
204
|
-
if (options.printOutput) {
|
|
205
|
-
const subprocess = execa("claude", args, {
|
|
206
|
-
stdio: "inherit"
|
|
207
|
-
});
|
|
208
|
-
await subprocess;
|
|
209
|
-
return "";
|
|
210
|
-
} else if (options.streamOutput) {
|
|
211
|
-
return new Promise((resolve, reject) => {
|
|
212
|
-
const child = spawn("claude", args, {
|
|
213
|
-
stdio: ["inherit", "pipe", "pipe"]
|
|
214
|
-
});
|
|
215
|
-
let output = "";
|
|
216
|
-
child.stdout.on("data", (chunk) => {
|
|
217
|
-
const text = chunk.toString();
|
|
218
|
-
output += text;
|
|
219
|
-
process.stdout.write(text);
|
|
220
|
-
});
|
|
221
|
-
child.stderr.on("data", (chunk) => {
|
|
222
|
-
process.stderr.write(chunk);
|
|
223
|
-
});
|
|
224
|
-
child.on("close", (code) => {
|
|
225
|
-
if (code === 0) {
|
|
226
|
-
resolve(output);
|
|
227
|
-
} else {
|
|
228
|
-
reject(new Error(`Claude exited with code ${code}`));
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
child.on("error", reject);
|
|
232
|
-
});
|
|
233
|
-
} else {
|
|
234
|
-
const { stdout } = await execa("claude", args);
|
|
235
|
-
return stdout;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
async function invokeClaudeInteractive(prompt, config) {
|
|
239
|
-
const args = ["--permission-mode", config.claude.permission_mode, prompt];
|
|
240
|
-
return execa("claude", args, {
|
|
241
|
-
stdio: "inherit"
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
199
|
function buildTicketPrompt(description, agentInstructions, additionalHints = null) {
|
|
245
200
|
const basePrompt = `You are creating a GitHub issue for a software project following an AI-assisted development workflow.
|
|
246
201
|
|
|
@@ -381,36 +336,180 @@ function extractIssueBody(output) {
|
|
|
381
336
|
return body;
|
|
382
337
|
}
|
|
383
338
|
|
|
339
|
+
// src/lib/ai-provider.ts
|
|
340
|
+
import { spawn } from "child_process";
|
|
341
|
+
import { execa as execa2 } from "execa";
|
|
342
|
+
async function invokeAI(options, config, providerOverride) {
|
|
343
|
+
const provider = providerOverride ?? config.ai.provider;
|
|
344
|
+
try {
|
|
345
|
+
const output = provider === "claude" ? await invokeClaudeInternal(options) : await invokeGeminiInternal(options);
|
|
346
|
+
return { output, provider };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (isRateLimitError(error, provider)) {
|
|
349
|
+
if (config.ai.auto_fallback && config.ai.fallback_provider && !providerOverride) {
|
|
350
|
+
const fallback = config.ai.fallback_provider;
|
|
351
|
+
logger.warning(`Rate limit reached on ${getProviderDisplayName(provider)}, switching to ${getProviderDisplayName(fallback)}...`);
|
|
352
|
+
const output = fallback === "claude" ? await invokeClaudeInternal(options) : await invokeGeminiInternal(options);
|
|
353
|
+
return { output, provider: fallback };
|
|
354
|
+
}
|
|
355
|
+
const err = error;
|
|
356
|
+
err.message = `Rate limited on ${getProviderDisplayName(provider)}`;
|
|
357
|
+
err.rateLimited = true;
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function invokeAIInteractive(prompt, config, providerOverride) {
|
|
364
|
+
const provider = providerOverride ?? config.ai.provider;
|
|
365
|
+
if (provider === "claude") {
|
|
366
|
+
const args = ["--permission-mode", config.claude.permission_mode, prompt];
|
|
367
|
+
return {
|
|
368
|
+
result: execa2("claude", args, { stdio: "inherit" }),
|
|
369
|
+
provider
|
|
370
|
+
};
|
|
371
|
+
} else {
|
|
372
|
+
return {
|
|
373
|
+
result: execa2("gemini", [prompt], { stdio: "inherit" }),
|
|
374
|
+
provider
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function getProviderDisplayName(provider) {
|
|
379
|
+
return provider === "claude" ? "Claude" : "Gemini";
|
|
380
|
+
}
|
|
381
|
+
function isRateLimitError(error, provider) {
|
|
382
|
+
if (!error || typeof error !== "object") return false;
|
|
383
|
+
if (provider === "claude" && "exitCode" in error && error.exitCode === 2) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
if ("message" in error && typeof error.message === "string") {
|
|
387
|
+
const msg = error.message.toLowerCase();
|
|
388
|
+
if (msg.includes("rate limit") || msg.includes("quota exceeded") || msg.includes("too many requests")) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
async function invokeClaudeInternal(options) {
|
|
395
|
+
const args = ["--print"];
|
|
396
|
+
if (options.permissionMode) {
|
|
397
|
+
args.push("--permission-mode", options.permissionMode);
|
|
398
|
+
}
|
|
399
|
+
args.push(options.prompt);
|
|
400
|
+
if (options.printOutput) {
|
|
401
|
+
const subprocess = execa2("claude", args, {
|
|
402
|
+
stdio: "inherit"
|
|
403
|
+
});
|
|
404
|
+
await subprocess;
|
|
405
|
+
return "";
|
|
406
|
+
} else if (options.streamOutput) {
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
const child = spawn("claude", args, {
|
|
409
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
410
|
+
});
|
|
411
|
+
let output = "";
|
|
412
|
+
child.stdout.on("data", (chunk) => {
|
|
413
|
+
const text = chunk.toString();
|
|
414
|
+
output += text;
|
|
415
|
+
process.stdout.write(text);
|
|
416
|
+
});
|
|
417
|
+
child.stderr.on("data", (chunk) => {
|
|
418
|
+
process.stderr.write(chunk);
|
|
419
|
+
});
|
|
420
|
+
child.on("close", (code) => {
|
|
421
|
+
if (code === 0) {
|
|
422
|
+
resolve(output);
|
|
423
|
+
} else {
|
|
424
|
+
const error = new Error(`Claude exited with code ${code}`);
|
|
425
|
+
error.exitCode = code ?? 1;
|
|
426
|
+
reject(error);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
child.on("error", reject);
|
|
430
|
+
});
|
|
431
|
+
} else {
|
|
432
|
+
const { stdout } = await execa2("claude", args);
|
|
433
|
+
return stdout;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function invokeGeminiInternal(options) {
|
|
437
|
+
const args = [];
|
|
438
|
+
args.push(options.prompt);
|
|
439
|
+
if (options.printOutput) {
|
|
440
|
+
const subprocess = execa2("gemini", args, {
|
|
441
|
+
stdio: "inherit"
|
|
442
|
+
});
|
|
443
|
+
await subprocess;
|
|
444
|
+
return "";
|
|
445
|
+
} else if (options.streamOutput) {
|
|
446
|
+
return new Promise((resolve, reject) => {
|
|
447
|
+
const child = spawn("gemini", args, {
|
|
448
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
449
|
+
});
|
|
450
|
+
let output = "";
|
|
451
|
+
child.stdout.on("data", (chunk) => {
|
|
452
|
+
const text = chunk.toString();
|
|
453
|
+
output += text;
|
|
454
|
+
process.stdout.write(text);
|
|
455
|
+
});
|
|
456
|
+
child.stderr.on("data", (chunk) => {
|
|
457
|
+
process.stderr.write(chunk);
|
|
458
|
+
});
|
|
459
|
+
child.on("close", (code) => {
|
|
460
|
+
if (code === 0) {
|
|
461
|
+
resolve(output);
|
|
462
|
+
} else {
|
|
463
|
+
const error = new Error(`Gemini exited with code ${code}`);
|
|
464
|
+
error.exitCode = code ?? 1;
|
|
465
|
+
reject(error);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
child.on("error", reject);
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
const { stdout } = await execa2("gemini", args);
|
|
472
|
+
return stdout;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
384
476
|
// src/commands/create.ts
|
|
385
477
|
async function createCommand(description, options) {
|
|
386
478
|
logger.bold("Creating AI-enhanced ticket...");
|
|
387
479
|
logger.newline();
|
|
388
|
-
const
|
|
480
|
+
const config = loadConfig();
|
|
481
|
+
const provider = options.provider ?? config.ai.provider;
|
|
482
|
+
const providerName = getProviderDisplayName(provider);
|
|
483
|
+
const [ghAuth, aiOk] = await Promise.all([
|
|
484
|
+
checkGhAuth(),
|
|
485
|
+
checkAIProvider(provider)
|
|
486
|
+
]);
|
|
389
487
|
if (!ghAuth) {
|
|
390
488
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
391
489
|
process.exit(1);
|
|
392
490
|
}
|
|
393
|
-
if (!
|
|
394
|
-
logger.error(
|
|
491
|
+
if (!aiOk) {
|
|
492
|
+
logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
|
|
395
493
|
process.exit(1);
|
|
396
494
|
}
|
|
397
495
|
const agentInstructions = loadAgentInstructions();
|
|
398
|
-
let
|
|
496
|
+
let aiOutput;
|
|
399
497
|
let additionalHints = null;
|
|
400
498
|
while (true) {
|
|
401
499
|
const prompt = buildTicketPrompt(description, agentInstructions, additionalHints);
|
|
402
500
|
try {
|
|
403
|
-
console.log(chalk.dim(
|
|
501
|
+
console.log(chalk.dim(`\u250C\u2500 Generating ticket with ${providerName}... \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
|
|
404
502
|
logger.newline();
|
|
405
|
-
|
|
503
|
+
const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
|
|
504
|
+
aiOutput = result.output;
|
|
406
505
|
logger.newline();
|
|
407
506
|
console.log(chalk.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
408
507
|
logger.newline();
|
|
409
508
|
} catch (error) {
|
|
410
|
-
logger.error(
|
|
509
|
+
logger.error(`${providerName} invocation failed: ${error}`);
|
|
411
510
|
return;
|
|
412
511
|
}
|
|
413
|
-
const meta = parseTicketMeta(
|
|
512
|
+
const meta = parseTicketMeta(aiOutput);
|
|
414
513
|
if (!meta) {
|
|
415
514
|
logger.warning("Could not parse metadata from Claude output. Using defaults.");
|
|
416
515
|
}
|
|
@@ -420,7 +519,7 @@ async function createCommand(description, options) {
|
|
|
420
519
|
risk: "low",
|
|
421
520
|
area: "shared"
|
|
422
521
|
};
|
|
423
|
-
const issueBody = extractIssueBody(
|
|
522
|
+
const issueBody = extractIssueBody(aiOutput);
|
|
424
523
|
const title = description.length > 60 ? description.slice(0, 57) + "..." : description;
|
|
425
524
|
const labels = buildIssueLabels(finalMeta);
|
|
426
525
|
displayTicketPreview(title, finalMeta, issueBody);
|
|
@@ -621,9 +720,9 @@ function getStatusColor(status) {
|
|
|
621
720
|
import inquirer3 from "inquirer";
|
|
622
721
|
|
|
623
722
|
// src/lib/git.ts
|
|
624
|
-
import { execa as
|
|
723
|
+
import { execa as execa3 } from "execa";
|
|
625
724
|
async function getCurrentBranch() {
|
|
626
|
-
const { stdout } = await
|
|
725
|
+
const { stdout } = await execa3("git", ["branch", "--show-current"]);
|
|
627
726
|
return stdout.trim();
|
|
628
727
|
}
|
|
629
728
|
async function isOnMainBranch() {
|
|
@@ -632,14 +731,14 @@ async function isOnMainBranch() {
|
|
|
632
731
|
}
|
|
633
732
|
async function getDefaultBranch() {
|
|
634
733
|
try {
|
|
635
|
-
const { stdout } = await
|
|
734
|
+
const { stdout } = await execa3("git", [
|
|
636
735
|
"symbolic-ref",
|
|
637
736
|
"refs/remotes/origin/HEAD"
|
|
638
737
|
]);
|
|
639
738
|
return stdout.trim().replace("refs/remotes/origin/", "");
|
|
640
739
|
} catch {
|
|
641
740
|
try {
|
|
642
|
-
await
|
|
741
|
+
await execa3("git", ["rev-parse", "--verify", "main"]);
|
|
643
742
|
return "main";
|
|
644
743
|
} catch {
|
|
645
744
|
return "master";
|
|
@@ -648,7 +747,7 @@ async function getDefaultBranch() {
|
|
|
648
747
|
}
|
|
649
748
|
async function branchExists(name) {
|
|
650
749
|
try {
|
|
651
|
-
await
|
|
750
|
+
await execa3("git", ["rev-parse", "--verify", name]);
|
|
652
751
|
return true;
|
|
653
752
|
} catch {
|
|
654
753
|
return false;
|
|
@@ -656,21 +755,21 @@ async function branchExists(name) {
|
|
|
656
755
|
}
|
|
657
756
|
async function createBranch(name, from) {
|
|
658
757
|
if (from) {
|
|
659
|
-
await
|
|
758
|
+
await execa3("git", ["checkout", "-b", name, from]);
|
|
660
759
|
} else {
|
|
661
|
-
await
|
|
760
|
+
await execa3("git", ["checkout", "-b", name]);
|
|
662
761
|
}
|
|
663
762
|
}
|
|
664
763
|
async function checkoutBranch(name) {
|
|
665
|
-
await
|
|
764
|
+
await execa3("git", ["checkout", name]);
|
|
666
765
|
}
|
|
667
766
|
async function hasUncommittedChanges() {
|
|
668
|
-
const { stdout } = await
|
|
767
|
+
const { stdout } = await execa3("git", ["status", "--porcelain"]);
|
|
669
768
|
return stdout.trim().length > 0;
|
|
670
769
|
}
|
|
671
770
|
async function getUnpushedCommits() {
|
|
672
771
|
try {
|
|
673
|
-
const { stdout } = await
|
|
772
|
+
const { stdout } = await execa3("git", [
|
|
674
773
|
"log",
|
|
675
774
|
"@{u}..HEAD",
|
|
676
775
|
"--oneline"
|
|
@@ -682,18 +781,18 @@ async function getUnpushedCommits() {
|
|
|
682
781
|
}
|
|
683
782
|
async function pushBranch(branch) {
|
|
684
783
|
const branchName = branch || await getCurrentBranch();
|
|
685
|
-
await
|
|
784
|
+
await execa3("git", ["push", "-u", "origin", branchName]);
|
|
686
785
|
}
|
|
687
786
|
async function getAuthorInitials() {
|
|
688
787
|
try {
|
|
689
|
-
const { stdout } = await
|
|
788
|
+
const { stdout } = await execa3("git", ["config", "user.initials"]);
|
|
690
789
|
if (stdout.trim()) {
|
|
691
790
|
return stdout.trim();
|
|
692
791
|
}
|
|
693
792
|
} catch {
|
|
694
793
|
}
|
|
695
794
|
try {
|
|
696
|
-
const { stdout } = await
|
|
795
|
+
const { stdout } = await execa3("git", ["config", "user.name"]);
|
|
697
796
|
const name = stdout.trim();
|
|
698
797
|
if (name) {
|
|
699
798
|
const parts = name.split(/\s+/);
|
|
@@ -705,7 +804,7 @@ async function getAuthorInitials() {
|
|
|
705
804
|
}
|
|
706
805
|
async function getCommitsSinceBase(base = "main") {
|
|
707
806
|
try {
|
|
708
|
-
const { stdout } = await
|
|
807
|
+
const { stdout } = await execa3("git", [
|
|
709
808
|
"log",
|
|
710
809
|
`${base}..HEAD`,
|
|
711
810
|
"--pretty=format:%s"
|
|
@@ -717,7 +816,7 @@ async function getCommitsSinceBase(base = "main") {
|
|
|
717
816
|
}
|
|
718
817
|
async function getDiffSummary(base = "main") {
|
|
719
818
|
try {
|
|
720
|
-
const { stdout } = await
|
|
819
|
+
const { stdout } = await execa3("git", [
|
|
721
820
|
"diff",
|
|
722
821
|
`${base}...HEAD`,
|
|
723
822
|
"--stat"
|
|
@@ -728,7 +827,7 @@ async function getDiffSummary(base = "main") {
|
|
|
728
827
|
}
|
|
729
828
|
}
|
|
730
829
|
async function getCurrentCommitSha() {
|
|
731
|
-
const { stdout } = await
|
|
830
|
+
const { stdout } = await execa3("git", ["rev-parse", "HEAD"]);
|
|
732
831
|
return stdout.trim();
|
|
733
832
|
}
|
|
734
833
|
async function hasNewCommits(beforeSha) {
|
|
@@ -811,13 +910,19 @@ function extractIssueNumber(branchName) {
|
|
|
811
910
|
async function runCommand(issueNumberArg, options) {
|
|
812
911
|
logger.bold("Running AI implementation workflow...");
|
|
813
912
|
logger.newline();
|
|
814
|
-
const
|
|
913
|
+
const config = loadConfig();
|
|
914
|
+
const provider = options.provider ?? config.ai.provider;
|
|
915
|
+
const providerName = getProviderDisplayName(provider);
|
|
916
|
+
const [ghAuth, aiOk] = await Promise.all([
|
|
917
|
+
checkGhAuth(),
|
|
918
|
+
checkAIProvider(provider)
|
|
919
|
+
]);
|
|
815
920
|
if (!ghAuth) {
|
|
816
921
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
817
922
|
process.exit(1);
|
|
818
923
|
}
|
|
819
|
-
if (!
|
|
820
|
-
logger.error(
|
|
924
|
+
if (!aiOk) {
|
|
925
|
+
logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
|
|
821
926
|
process.exit(1);
|
|
822
927
|
}
|
|
823
928
|
const hasChanges = await hasUncommittedChanges();
|
|
@@ -836,7 +941,6 @@ async function runCommand(issueNumberArg, options) {
|
|
|
836
941
|
process.exit(0);
|
|
837
942
|
}
|
|
838
943
|
}
|
|
839
|
-
const config = loadConfig();
|
|
840
944
|
const workflowLabels = getWorkflowLabels(config);
|
|
841
945
|
let issueNumber;
|
|
842
946
|
if (options.auto) {
|
|
@@ -943,8 +1047,8 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
943
1047
|
const progressContent = readProgress(config);
|
|
944
1048
|
const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config);
|
|
945
1049
|
logger.newline();
|
|
946
|
-
logger.info(
|
|
947
|
-
logger.dim(
|
|
1050
|
+
logger.info(`Starting ${colors.provider(providerName)} implementation session...`);
|
|
1051
|
+
logger.dim(`${providerName} will implement the feature and create a commit.`);
|
|
948
1052
|
logger.dim("Review the changes before pushing.");
|
|
949
1053
|
logger.newline();
|
|
950
1054
|
const beforeSha = await getCurrentCommitSha();
|
|
@@ -954,15 +1058,17 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
954
1058
|
};
|
|
955
1059
|
process.on("SIGINT", handleSignal);
|
|
956
1060
|
process.on("SIGTERM", handleSignal);
|
|
957
|
-
let
|
|
1061
|
+
let aiExitCode;
|
|
1062
|
+
let usedProvider = provider;
|
|
958
1063
|
try {
|
|
959
|
-
const result = await
|
|
960
|
-
|
|
1064
|
+
const { result, provider: actualProvider } = await invokeAIInteractive(prompt, config, options.provider);
|
|
1065
|
+
usedProvider = actualProvider;
|
|
1066
|
+
aiExitCode = result.exitCode ?? void 0;
|
|
961
1067
|
} catch (error) {
|
|
962
1068
|
if (error && typeof error === "object" && "exitCode" in error) {
|
|
963
|
-
|
|
1069
|
+
aiExitCode = error.exitCode;
|
|
964
1070
|
}
|
|
965
|
-
logger.error(
|
|
1071
|
+
logger.error(`${getProviderDisplayName(usedProvider)} session failed: ${error}`);
|
|
966
1072
|
} finally {
|
|
967
1073
|
process.off("SIGINT", handleSignal);
|
|
968
1074
|
process.off("SIGTERM", handleSignal);
|
|
@@ -973,8 +1079,9 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
973
1079
|
logger.warning("Operation was cancelled. Labels unchanged.");
|
|
974
1080
|
return;
|
|
975
1081
|
}
|
|
1082
|
+
const usedProviderName = getProviderDisplayName(usedProvider);
|
|
976
1083
|
if (commitsCreated) {
|
|
977
|
-
logger.success(
|
|
1084
|
+
logger.success(`${usedProviderName} session completed with new commits.`);
|
|
978
1085
|
try {
|
|
979
1086
|
await updateIssueLabels(issueNumber, {
|
|
980
1087
|
add: [workflowLabels.completed],
|
|
@@ -987,7 +1094,7 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
987
1094
|
try {
|
|
988
1095
|
await addIssueComment(
|
|
989
1096
|
issueNumber,
|
|
990
|
-
`AI implementation completed on branch \`${branchName}
|
|
1097
|
+
`AI implementation completed on branch \`${branchName}\` using ${usedProviderName}.
|
|
991
1098
|
|
|
992
1099
|
Please review the changes and create a PR when ready.`
|
|
993
1100
|
);
|
|
@@ -996,9 +1103,9 @@ Please review the changes and create a PR when ready.`
|
|
|
996
1103
|
logger.warning(`Failed to post comment: ${error}`);
|
|
997
1104
|
}
|
|
998
1105
|
} else {
|
|
999
|
-
const isRateLimited =
|
|
1106
|
+
const isRateLimited = aiExitCode === 2;
|
|
1000
1107
|
if (isRateLimited) {
|
|
1001
|
-
logger.warning(
|
|
1108
|
+
logger.warning(`${usedProviderName} session ended due to rate limits. No commits were created.`);
|
|
1002
1109
|
try {
|
|
1003
1110
|
await updateIssueLabels(issueNumber, {
|
|
1004
1111
|
add: [workflowLabels.blocked],
|
|
@@ -1011,7 +1118,7 @@ Please review the changes and create a PR when ready.`
|
|
|
1011
1118
|
try {
|
|
1012
1119
|
await addIssueComment(
|
|
1013
1120
|
issueNumber,
|
|
1014
|
-
`AI implementation was blocked due to API rate limits on branch \`${branchName}
|
|
1121
|
+
`AI implementation was blocked due to API rate limits on branch \`${branchName}\` (${usedProviderName}).
|
|
1015
1122
|
|
|
1016
1123
|
No commits were created. Please retry later.`
|
|
1017
1124
|
);
|
|
@@ -1020,7 +1127,7 @@ No commits were created. Please retry later.`
|
|
|
1020
1127
|
logger.warning(`Failed to post comment: ${error}`);
|
|
1021
1128
|
}
|
|
1022
1129
|
} else {
|
|
1023
|
-
logger.warning(
|
|
1130
|
+
logger.warning(`${usedProviderName} session completed but no commits were created. Labels unchanged.`);
|
|
1024
1131
|
}
|
|
1025
1132
|
return;
|
|
1026
1133
|
}
|
|
@@ -1058,13 +1165,19 @@ import inquirer4 from "inquirer";
|
|
|
1058
1165
|
async function prCommand(options) {
|
|
1059
1166
|
logger.bold("Creating AI-enhanced pull request...");
|
|
1060
1167
|
logger.newline();
|
|
1061
|
-
const
|
|
1168
|
+
const config = loadConfig();
|
|
1169
|
+
const provider = options.provider ?? config.ai.provider;
|
|
1170
|
+
const providerName = getProviderDisplayName(provider);
|
|
1171
|
+
const [ghAuth, aiOk] = await Promise.all([
|
|
1172
|
+
checkGhAuth(),
|
|
1173
|
+
checkAIProvider(provider)
|
|
1174
|
+
]);
|
|
1062
1175
|
if (!ghAuth) {
|
|
1063
1176
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
1064
1177
|
process.exit(1);
|
|
1065
1178
|
}
|
|
1066
|
-
if (!
|
|
1067
|
-
logger.error(
|
|
1179
|
+
if (!aiOk) {
|
|
1180
|
+
logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
|
|
1068
1181
|
process.exit(1);
|
|
1069
1182
|
}
|
|
1070
1183
|
if (await isOnMainBranch()) {
|
|
@@ -1121,12 +1234,13 @@ async function prCommand(options) {
|
|
|
1121
1234
|
const prompt = buildPrPrompt(issue, commits, diffSummary);
|
|
1122
1235
|
let prBody;
|
|
1123
1236
|
try {
|
|
1124
|
-
logger.info(
|
|
1237
|
+
logger.info(`Generating PR description with ${colors.provider(providerName)}...`);
|
|
1125
1238
|
logger.newline();
|
|
1126
|
-
|
|
1239
|
+
const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
|
|
1240
|
+
prBody = result.output;
|
|
1127
1241
|
logger.newline();
|
|
1128
1242
|
} catch (error) {
|
|
1129
|
-
logger.warning(
|
|
1243
|
+
logger.warning(`${providerName} invocation failed: ${error}`);
|
|
1130
1244
|
prBody = generateFallbackBody(issue, commits);
|
|
1131
1245
|
}
|
|
1132
1246
|
const prTitle = issue?.title || commits[0] || currentBranch;
|
|
@@ -1206,6 +1320,14 @@ async function statusCommand() {
|
|
|
1206
1320
|
logger.warning(` ${config.progress.file} not found`);
|
|
1207
1321
|
}
|
|
1208
1322
|
logger.newline();
|
|
1323
|
+
logger.bold("AI Provider:");
|
|
1324
|
+
const providerName = getProviderDisplayName(config.ai.provider);
|
|
1325
|
+
logger.info(` Active: ${colors.provider(providerName)}`);
|
|
1326
|
+
if (config.ai.fallback_provider) {
|
|
1327
|
+
const fallbackName = getProviderDisplayName(config.ai.fallback_provider);
|
|
1328
|
+
logger.info(` Fallback: ${fallbackName} (auto: ${config.ai.auto_fallback ? "enabled" : "disabled"})`);
|
|
1329
|
+
}
|
|
1330
|
+
logger.newline();
|
|
1209
1331
|
logger.bold("Prerequisites:");
|
|
1210
1332
|
const ghAuth = await checkGhAuth();
|
|
1211
1333
|
if (ghAuth) {
|
|
@@ -1214,10 +1336,22 @@ async function statusCommand() {
|
|
|
1214
1336
|
logger.error(" GitHub CLI not authenticated");
|
|
1215
1337
|
}
|
|
1216
1338
|
const claudeOk = await checkClaudeCli();
|
|
1339
|
+
const geminiOk = await checkGeminiCli();
|
|
1340
|
+
const getProviderStatus = (provider) => {
|
|
1341
|
+
const isActive = config.ai.provider === provider;
|
|
1342
|
+
const isFallback = config.ai.fallback_provider === provider;
|
|
1343
|
+
const suffix = isActive ? " (active)" : isFallback ? " (fallback)" : "";
|
|
1344
|
+
return suffix;
|
|
1345
|
+
};
|
|
1217
1346
|
if (claudeOk) {
|
|
1218
|
-
logger.success(
|
|
1347
|
+
logger.success(` Claude CLI available${getProviderStatus("claude")}`);
|
|
1348
|
+
} else {
|
|
1349
|
+
logger.error(` Claude CLI not found${getProviderStatus("claude")}`);
|
|
1350
|
+
}
|
|
1351
|
+
if (geminiOk) {
|
|
1352
|
+
logger.success(` Gemini CLI available${getProviderStatus("gemini")}`);
|
|
1219
1353
|
} else {
|
|
1220
|
-
logger.error(
|
|
1354
|
+
logger.error(` Gemini CLI not found${getProviderStatus("gemini")}`);
|
|
1221
1355
|
}
|
|
1222
1356
|
logger.newline();
|
|
1223
1357
|
logger.bold("Git Status:");
|
|
@@ -1313,8 +1447,8 @@ program.command("init").description("Initialize gent workflow in current reposit
|
|
|
1313
1447
|
program.command("setup-labels").description("Setup GitHub labels for AI workflow").action(async () => {
|
|
1314
1448
|
await setupLabelsCommand();
|
|
1315
1449
|
});
|
|
1316
|
-
program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").action(async (description, options) => {
|
|
1317
|
-
await createCommand(description, { yes: options.yes });
|
|
1450
|
+
program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").action(async (description, options) => {
|
|
1451
|
+
await createCommand(description, { yes: options.yes, provider: options.provider });
|
|
1318
1452
|
});
|
|
1319
1453
|
program.command("list").description("List GitHub issues by label/status").option("-l, --label <label>", "Filter by label").option("-s, --status <status>", "Filter by workflow status (ready, in-progress, completed, blocked, all)").option("-n, --limit <number>", "Maximum number of issues to show", "20").action(async (options) => {
|
|
1320
1454
|
await listCommand({
|
|
@@ -1323,11 +1457,11 @@ program.command("list").description("List GitHub issues by label/status").option
|
|
|
1323
1457
|
limit: parseInt(options.limit, 10)
|
|
1324
1458
|
});
|
|
1325
1459
|
});
|
|
1326
|
-
program.command("run [issue-number]").description("Run
|
|
1327
|
-
await runCommand(issueNumber, { auto: options.auto });
|
|
1460
|
+
program.command("run [issue-number]").description("Run AI to implement a GitHub issue").option("-a, --auto", "Auto-select highest priority ai-ready issue").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").action(async (issueNumber, options) => {
|
|
1461
|
+
await runCommand(issueNumber, { auto: options.auto, provider: options.provider });
|
|
1328
1462
|
});
|
|
1329
|
-
program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").action(async (options) => {
|
|
1330
|
-
await prCommand({ draft: options.draft });
|
|
1463
|
+
program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").action(async (options) => {
|
|
1464
|
+
await prCommand({ draft: options.draft, provider: options.provider });
|
|
1331
1465
|
});
|
|
1332
1466
|
program.command("status").description("Show current workflow status").action(async () => {
|
|
1333
1467
|
await statusCommand();
|