@rotorsoft/gent 1.1.2 → 1.3.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 +246 -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,10 @@ async function createCommand(description, options) {
|
|
|
420
519
|
risk: "low",
|
|
421
520
|
area: "shared"
|
|
422
521
|
};
|
|
423
|
-
const issueBody = extractIssueBody(
|
|
522
|
+
const issueBody = extractIssueBody(aiOutput) + `
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
*Created with ${providerName} by [gent](https://github.com/Rotorsoft/gent)*`;
|
|
424
526
|
const title = description.length > 60 ? description.slice(0, 57) + "..." : description;
|
|
425
527
|
const labels = buildIssueLabels(finalMeta);
|
|
426
528
|
displayTicketPreview(title, finalMeta, issueBody);
|
|
@@ -621,9 +723,9 @@ function getStatusColor(status) {
|
|
|
621
723
|
import inquirer3 from "inquirer";
|
|
622
724
|
|
|
623
725
|
// src/lib/git.ts
|
|
624
|
-
import { execa as
|
|
726
|
+
import { execa as execa3 } from "execa";
|
|
625
727
|
async function getCurrentBranch() {
|
|
626
|
-
const { stdout } = await
|
|
728
|
+
const { stdout } = await execa3("git", ["branch", "--show-current"]);
|
|
627
729
|
return stdout.trim();
|
|
628
730
|
}
|
|
629
731
|
async function isOnMainBranch() {
|
|
@@ -632,14 +734,14 @@ async function isOnMainBranch() {
|
|
|
632
734
|
}
|
|
633
735
|
async function getDefaultBranch() {
|
|
634
736
|
try {
|
|
635
|
-
const { stdout } = await
|
|
737
|
+
const { stdout } = await execa3("git", [
|
|
636
738
|
"symbolic-ref",
|
|
637
739
|
"refs/remotes/origin/HEAD"
|
|
638
740
|
]);
|
|
639
741
|
return stdout.trim().replace("refs/remotes/origin/", "");
|
|
640
742
|
} catch {
|
|
641
743
|
try {
|
|
642
|
-
await
|
|
744
|
+
await execa3("git", ["rev-parse", "--verify", "main"]);
|
|
643
745
|
return "main";
|
|
644
746
|
} catch {
|
|
645
747
|
return "master";
|
|
@@ -648,7 +750,7 @@ async function getDefaultBranch() {
|
|
|
648
750
|
}
|
|
649
751
|
async function branchExists(name) {
|
|
650
752
|
try {
|
|
651
|
-
await
|
|
753
|
+
await execa3("git", ["rev-parse", "--verify", name]);
|
|
652
754
|
return true;
|
|
653
755
|
} catch {
|
|
654
756
|
return false;
|
|
@@ -656,21 +758,21 @@ async function branchExists(name) {
|
|
|
656
758
|
}
|
|
657
759
|
async function createBranch(name, from) {
|
|
658
760
|
if (from) {
|
|
659
|
-
await
|
|
761
|
+
await execa3("git", ["checkout", "-b", name, from]);
|
|
660
762
|
} else {
|
|
661
|
-
await
|
|
763
|
+
await execa3("git", ["checkout", "-b", name]);
|
|
662
764
|
}
|
|
663
765
|
}
|
|
664
766
|
async function checkoutBranch(name) {
|
|
665
|
-
await
|
|
767
|
+
await execa3("git", ["checkout", name]);
|
|
666
768
|
}
|
|
667
769
|
async function hasUncommittedChanges() {
|
|
668
|
-
const { stdout } = await
|
|
770
|
+
const { stdout } = await execa3("git", ["status", "--porcelain"]);
|
|
669
771
|
return stdout.trim().length > 0;
|
|
670
772
|
}
|
|
671
773
|
async function getUnpushedCommits() {
|
|
672
774
|
try {
|
|
673
|
-
const { stdout } = await
|
|
775
|
+
const { stdout } = await execa3("git", [
|
|
674
776
|
"log",
|
|
675
777
|
"@{u}..HEAD",
|
|
676
778
|
"--oneline"
|
|
@@ -682,18 +784,18 @@ async function getUnpushedCommits() {
|
|
|
682
784
|
}
|
|
683
785
|
async function pushBranch(branch) {
|
|
684
786
|
const branchName = branch || await getCurrentBranch();
|
|
685
|
-
await
|
|
787
|
+
await execa3("git", ["push", "-u", "origin", branchName]);
|
|
686
788
|
}
|
|
687
789
|
async function getAuthorInitials() {
|
|
688
790
|
try {
|
|
689
|
-
const { stdout } = await
|
|
791
|
+
const { stdout } = await execa3("git", ["config", "user.initials"]);
|
|
690
792
|
if (stdout.trim()) {
|
|
691
793
|
return stdout.trim();
|
|
692
794
|
}
|
|
693
795
|
} catch {
|
|
694
796
|
}
|
|
695
797
|
try {
|
|
696
|
-
const { stdout } = await
|
|
798
|
+
const { stdout } = await execa3("git", ["config", "user.name"]);
|
|
697
799
|
const name = stdout.trim();
|
|
698
800
|
if (name) {
|
|
699
801
|
const parts = name.split(/\s+/);
|
|
@@ -705,7 +807,7 @@ async function getAuthorInitials() {
|
|
|
705
807
|
}
|
|
706
808
|
async function getCommitsSinceBase(base = "main") {
|
|
707
809
|
try {
|
|
708
|
-
const { stdout } = await
|
|
810
|
+
const { stdout } = await execa3("git", [
|
|
709
811
|
"log",
|
|
710
812
|
`${base}..HEAD`,
|
|
711
813
|
"--pretty=format:%s"
|
|
@@ -717,7 +819,7 @@ async function getCommitsSinceBase(base = "main") {
|
|
|
717
819
|
}
|
|
718
820
|
async function getDiffSummary(base = "main") {
|
|
719
821
|
try {
|
|
720
|
-
const { stdout } = await
|
|
822
|
+
const { stdout } = await execa3("git", [
|
|
721
823
|
"diff",
|
|
722
824
|
`${base}...HEAD`,
|
|
723
825
|
"--stat"
|
|
@@ -728,7 +830,7 @@ async function getDiffSummary(base = "main") {
|
|
|
728
830
|
}
|
|
729
831
|
}
|
|
730
832
|
async function getCurrentCommitSha() {
|
|
731
|
-
const { stdout } = await
|
|
833
|
+
const { stdout } = await execa3("git", ["rev-parse", "HEAD"]);
|
|
732
834
|
return stdout.trim();
|
|
733
835
|
}
|
|
734
836
|
async function hasNewCommits(beforeSha) {
|
|
@@ -811,13 +913,19 @@ function extractIssueNumber(branchName) {
|
|
|
811
913
|
async function runCommand(issueNumberArg, options) {
|
|
812
914
|
logger.bold("Running AI implementation workflow...");
|
|
813
915
|
logger.newline();
|
|
814
|
-
const
|
|
916
|
+
const config = loadConfig();
|
|
917
|
+
const provider = options.provider ?? config.ai.provider;
|
|
918
|
+
const providerName = getProviderDisplayName(provider);
|
|
919
|
+
const [ghAuth, aiOk] = await Promise.all([
|
|
920
|
+
checkGhAuth(),
|
|
921
|
+
checkAIProvider(provider)
|
|
922
|
+
]);
|
|
815
923
|
if (!ghAuth) {
|
|
816
924
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
817
925
|
process.exit(1);
|
|
818
926
|
}
|
|
819
|
-
if (!
|
|
820
|
-
logger.error(
|
|
927
|
+
if (!aiOk) {
|
|
928
|
+
logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
|
|
821
929
|
process.exit(1);
|
|
822
930
|
}
|
|
823
931
|
const hasChanges = await hasUncommittedChanges();
|
|
@@ -836,7 +944,6 @@ async function runCommand(issueNumberArg, options) {
|
|
|
836
944
|
process.exit(0);
|
|
837
945
|
}
|
|
838
946
|
}
|
|
839
|
-
const config = loadConfig();
|
|
840
947
|
const workflowLabels = getWorkflowLabels(config);
|
|
841
948
|
let issueNumber;
|
|
842
949
|
if (options.auto) {
|
|
@@ -943,8 +1050,8 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
943
1050
|
const progressContent = readProgress(config);
|
|
944
1051
|
const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config);
|
|
945
1052
|
logger.newline();
|
|
946
|
-
logger.info(
|
|
947
|
-
logger.dim(
|
|
1053
|
+
logger.info(`Starting ${colors.provider(providerName)} implementation session...`);
|
|
1054
|
+
logger.dim(`${providerName} will implement the feature and create a commit.`);
|
|
948
1055
|
logger.dim("Review the changes before pushing.");
|
|
949
1056
|
logger.newline();
|
|
950
1057
|
const beforeSha = await getCurrentCommitSha();
|
|
@@ -954,15 +1061,17 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
954
1061
|
};
|
|
955
1062
|
process.on("SIGINT", handleSignal);
|
|
956
1063
|
process.on("SIGTERM", handleSignal);
|
|
957
|
-
let
|
|
1064
|
+
let aiExitCode;
|
|
1065
|
+
let usedProvider = provider;
|
|
958
1066
|
try {
|
|
959
|
-
const result = await
|
|
960
|
-
|
|
1067
|
+
const { result, provider: actualProvider } = await invokeAIInteractive(prompt, config, options.provider);
|
|
1068
|
+
usedProvider = actualProvider;
|
|
1069
|
+
aiExitCode = result.exitCode ?? void 0;
|
|
961
1070
|
} catch (error) {
|
|
962
1071
|
if (error && typeof error === "object" && "exitCode" in error) {
|
|
963
|
-
|
|
1072
|
+
aiExitCode = error.exitCode;
|
|
964
1073
|
}
|
|
965
|
-
logger.error(
|
|
1074
|
+
logger.error(`${getProviderDisplayName(usedProvider)} session failed: ${error}`);
|
|
966
1075
|
} finally {
|
|
967
1076
|
process.off("SIGINT", handleSignal);
|
|
968
1077
|
process.off("SIGTERM", handleSignal);
|
|
@@ -973,8 +1082,9 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
973
1082
|
logger.warning("Operation was cancelled. Labels unchanged.");
|
|
974
1083
|
return;
|
|
975
1084
|
}
|
|
1085
|
+
const usedProviderName = getProviderDisplayName(usedProvider);
|
|
976
1086
|
if (commitsCreated) {
|
|
977
|
-
logger.success(
|
|
1087
|
+
logger.success(`${usedProviderName} session completed with new commits.`);
|
|
978
1088
|
try {
|
|
979
1089
|
await updateIssueLabels(issueNumber, {
|
|
980
1090
|
add: [workflowLabels.completed],
|
|
@@ -987,7 +1097,7 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
987
1097
|
try {
|
|
988
1098
|
await addIssueComment(
|
|
989
1099
|
issueNumber,
|
|
990
|
-
`AI implementation completed on branch \`${branchName}
|
|
1100
|
+
`AI implementation completed on branch \`${branchName}\` using ${usedProviderName}.
|
|
991
1101
|
|
|
992
1102
|
Please review the changes and create a PR when ready.`
|
|
993
1103
|
);
|
|
@@ -996,9 +1106,9 @@ Please review the changes and create a PR when ready.`
|
|
|
996
1106
|
logger.warning(`Failed to post comment: ${error}`);
|
|
997
1107
|
}
|
|
998
1108
|
} else {
|
|
999
|
-
const isRateLimited =
|
|
1109
|
+
const isRateLimited = aiExitCode === 2;
|
|
1000
1110
|
if (isRateLimited) {
|
|
1001
|
-
logger.warning(
|
|
1111
|
+
logger.warning(`${usedProviderName} session ended due to rate limits. No commits were created.`);
|
|
1002
1112
|
try {
|
|
1003
1113
|
await updateIssueLabels(issueNumber, {
|
|
1004
1114
|
add: [workflowLabels.blocked],
|
|
@@ -1011,7 +1121,7 @@ Please review the changes and create a PR when ready.`
|
|
|
1011
1121
|
try {
|
|
1012
1122
|
await addIssueComment(
|
|
1013
1123
|
issueNumber,
|
|
1014
|
-
`AI implementation was blocked due to API rate limits on branch \`${branchName}
|
|
1124
|
+
`AI implementation was blocked due to API rate limits on branch \`${branchName}\` (${usedProviderName}).
|
|
1015
1125
|
|
|
1016
1126
|
No commits were created. Please retry later.`
|
|
1017
1127
|
);
|
|
@@ -1020,7 +1130,7 @@ No commits were created. Please retry later.`
|
|
|
1020
1130
|
logger.warning(`Failed to post comment: ${error}`);
|
|
1021
1131
|
}
|
|
1022
1132
|
} else {
|
|
1023
|
-
logger.warning(
|
|
1133
|
+
logger.warning(`${usedProviderName} session completed but no commits were created. Labels unchanged.`);
|
|
1024
1134
|
}
|
|
1025
1135
|
return;
|
|
1026
1136
|
}
|
|
@@ -1058,13 +1168,19 @@ import inquirer4 from "inquirer";
|
|
|
1058
1168
|
async function prCommand(options) {
|
|
1059
1169
|
logger.bold("Creating AI-enhanced pull request...");
|
|
1060
1170
|
logger.newline();
|
|
1061
|
-
const
|
|
1171
|
+
const config = loadConfig();
|
|
1172
|
+
const provider = options.provider ?? config.ai.provider;
|
|
1173
|
+
const providerName = getProviderDisplayName(provider);
|
|
1174
|
+
const [ghAuth, aiOk] = await Promise.all([
|
|
1175
|
+
checkGhAuth(),
|
|
1176
|
+
checkAIProvider(provider)
|
|
1177
|
+
]);
|
|
1062
1178
|
if (!ghAuth) {
|
|
1063
1179
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
1064
1180
|
process.exit(1);
|
|
1065
1181
|
}
|
|
1066
|
-
if (!
|
|
1067
|
-
logger.error(
|
|
1182
|
+
if (!aiOk) {
|
|
1183
|
+
logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
|
|
1068
1184
|
process.exit(1);
|
|
1069
1185
|
}
|
|
1070
1186
|
if (await isOnMainBranch()) {
|
|
@@ -1121,14 +1237,19 @@ async function prCommand(options) {
|
|
|
1121
1237
|
const prompt = buildPrPrompt(issue, commits, diffSummary);
|
|
1122
1238
|
let prBody;
|
|
1123
1239
|
try {
|
|
1124
|
-
logger.info(
|
|
1240
|
+
logger.info(`Generating PR description with ${colors.provider(providerName)}...`);
|
|
1125
1241
|
logger.newline();
|
|
1126
|
-
|
|
1242
|
+
const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
|
|
1243
|
+
prBody = result.output;
|
|
1127
1244
|
logger.newline();
|
|
1128
1245
|
} catch (error) {
|
|
1129
|
-
logger.warning(
|
|
1246
|
+
logger.warning(`${providerName} invocation failed: ${error}`);
|
|
1130
1247
|
prBody = generateFallbackBody(issue, commits);
|
|
1131
1248
|
}
|
|
1249
|
+
prBody += `
|
|
1250
|
+
|
|
1251
|
+
---
|
|
1252
|
+
*Created with ${providerName} by [gent](https://github.com/Rotorsoft/gent)*`;
|
|
1132
1253
|
const prTitle = issue?.title || commits[0] || currentBranch;
|
|
1133
1254
|
let prUrl;
|
|
1134
1255
|
try {
|
|
@@ -1206,6 +1327,14 @@ async function statusCommand() {
|
|
|
1206
1327
|
logger.warning(` ${config.progress.file} not found`);
|
|
1207
1328
|
}
|
|
1208
1329
|
logger.newline();
|
|
1330
|
+
logger.bold("AI Provider:");
|
|
1331
|
+
const providerName = getProviderDisplayName(config.ai.provider);
|
|
1332
|
+
logger.info(` Active: ${colors.provider(providerName)}`);
|
|
1333
|
+
if (config.ai.fallback_provider) {
|
|
1334
|
+
const fallbackName = getProviderDisplayName(config.ai.fallback_provider);
|
|
1335
|
+
logger.info(` Fallback: ${fallbackName} (auto: ${config.ai.auto_fallback ? "enabled" : "disabled"})`);
|
|
1336
|
+
}
|
|
1337
|
+
logger.newline();
|
|
1209
1338
|
logger.bold("Prerequisites:");
|
|
1210
1339
|
const ghAuth = await checkGhAuth();
|
|
1211
1340
|
if (ghAuth) {
|
|
@@ -1214,10 +1343,22 @@ async function statusCommand() {
|
|
|
1214
1343
|
logger.error(" GitHub CLI not authenticated");
|
|
1215
1344
|
}
|
|
1216
1345
|
const claudeOk = await checkClaudeCli();
|
|
1346
|
+
const geminiOk = await checkGeminiCli();
|
|
1347
|
+
const getProviderStatus = (provider) => {
|
|
1348
|
+
const isActive = config.ai.provider === provider;
|
|
1349
|
+
const isFallback = config.ai.fallback_provider === provider;
|
|
1350
|
+
const suffix = isActive ? " (active)" : isFallback ? " (fallback)" : "";
|
|
1351
|
+
return suffix;
|
|
1352
|
+
};
|
|
1217
1353
|
if (claudeOk) {
|
|
1218
|
-
logger.success(
|
|
1354
|
+
logger.success(` Claude CLI available${getProviderStatus("claude")}`);
|
|
1355
|
+
} else {
|
|
1356
|
+
logger.error(` Claude CLI not found${getProviderStatus("claude")}`);
|
|
1357
|
+
}
|
|
1358
|
+
if (geminiOk) {
|
|
1359
|
+
logger.success(` Gemini CLI available${getProviderStatus("gemini")}`);
|
|
1219
1360
|
} else {
|
|
1220
|
-
logger.error(
|
|
1361
|
+
logger.error(` Gemini CLI not found${getProviderStatus("gemini")}`);
|
|
1221
1362
|
}
|
|
1222
1363
|
logger.newline();
|
|
1223
1364
|
logger.bold("Git Status:");
|
|
@@ -1313,8 +1454,8 @@ program.command("init").description("Initialize gent workflow in current reposit
|
|
|
1313
1454
|
program.command("setup-labels").description("Setup GitHub labels for AI workflow").action(async () => {
|
|
1314
1455
|
await setupLabelsCommand();
|
|
1315
1456
|
});
|
|
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 });
|
|
1457
|
+
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) => {
|
|
1458
|
+
await createCommand(description, { yes: options.yes, provider: options.provider });
|
|
1318
1459
|
});
|
|
1319
1460
|
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
1461
|
await listCommand({
|
|
@@ -1323,11 +1464,11 @@ program.command("list").description("List GitHub issues by label/status").option
|
|
|
1323
1464
|
limit: parseInt(options.limit, 10)
|
|
1324
1465
|
});
|
|
1325
1466
|
});
|
|
1326
|
-
program.command("run [issue-number]").description("Run
|
|
1327
|
-
await runCommand(issueNumber, { auto: options.auto });
|
|
1467
|
+
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) => {
|
|
1468
|
+
await runCommand(issueNumber, { auto: options.auto, provider: options.provider });
|
|
1328
1469
|
});
|
|
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 });
|
|
1470
|
+
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) => {
|
|
1471
|
+
await prCommand({ draft: options.draft, provider: options.provider });
|
|
1331
1472
|
});
|
|
1332
1473
|
program.command("status").description("Show current workflow status").action(async () => {
|
|
1333
1474
|
await statusCommand();
|