@rotorsoft/gent 1.11.0 → 1.12.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 +45 -0
- package/dist/{chunk-3N6U3NTN.js → chunk-TLGYE7J2.js} +19 -3
- package/dist/chunk-TLGYE7J2.js.map +1 -0
- package/dist/index.js +783 -3
- package/dist/index.js.map +1 -1
- package/dist/{setup-labels-HKXXO65J.js → setup-labels-FLCGVPFY.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-3N6U3NTN.js.map +0 -1
- /package/dist/{setup-labels-HKXXO65J.js.map → setup-labels-FLCGVPFY.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
configExists,
|
|
14
14
|
createIssue,
|
|
15
15
|
createPullRequest,
|
|
16
|
+
createSpinner,
|
|
16
17
|
extractPriorityFromLabels,
|
|
17
18
|
extractTypeFromLabels,
|
|
18
19
|
generateDefaultConfig,
|
|
@@ -32,9 +33,10 @@ import {
|
|
|
32
33
|
sanitizeSlug,
|
|
33
34
|
setupLabelsCommand,
|
|
34
35
|
sortByPriority,
|
|
36
|
+
updateConfigProvider,
|
|
35
37
|
updateIssueLabels,
|
|
36
38
|
withSpinner
|
|
37
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-TLGYE7J2.js";
|
|
38
40
|
|
|
39
41
|
// src/index.ts
|
|
40
42
|
import { Command } from "commander";
|
|
@@ -195,7 +197,7 @@ async function initCommand(options) {
|
|
|
195
197
|
}
|
|
196
198
|
]);
|
|
197
199
|
if (setupLabels) {
|
|
198
|
-
const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-
|
|
200
|
+
const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-FLCGVPFY.js");
|
|
199
201
|
await setupLabelsCommand2();
|
|
200
202
|
}
|
|
201
203
|
}
|
|
@@ -540,6 +542,22 @@ ${issue ? `Closes #${issue.number}` : ""}
|
|
|
540
542
|
|
|
541
543
|
Only output the PR description, nothing else.`;
|
|
542
544
|
}
|
|
545
|
+
function buildCommitMessagePrompt(diff, issueNumber, issueTitle) {
|
|
546
|
+
const issueContext = issueNumber ? `
|
|
547
|
+
Related Issue: #${issueNumber}${issueTitle ? ` - ${issueTitle}` : ""}
|
|
548
|
+
` : "";
|
|
549
|
+
return `Generate a concise git commit message for the following changes.
|
|
550
|
+
${issueContext}
|
|
551
|
+
## Diff
|
|
552
|
+
${diff}
|
|
553
|
+
|
|
554
|
+
Rules:
|
|
555
|
+
- Use conventional commit format: <type>: <short description>
|
|
556
|
+
- Types: feat, fix, refactor, chore, docs, test, style, perf
|
|
557
|
+
- Keep the first line under 72 characters
|
|
558
|
+
- Do NOT include a body or footer
|
|
559
|
+
- Output ONLY the commit message, nothing else`;
|
|
560
|
+
}
|
|
543
561
|
function parseTicketMeta(output) {
|
|
544
562
|
const metaMatch = output.match(
|
|
545
563
|
/META:type=(\w+),priority=(\w+),risk=(\w+),area=(\w+)/
|
|
@@ -592,6 +610,46 @@ function generateFallbackTitle(description) {
|
|
|
592
610
|
}
|
|
593
611
|
return truncated;
|
|
594
612
|
}
|
|
613
|
+
function buildVideoPrompt(issueNumber, issueTitle, videoConfig, agentInstructions) {
|
|
614
|
+
return `You are helping capture a Playwright video demonstration of UI changes for GitHub Issue #${issueNumber}: ${issueTitle}
|
|
615
|
+
|
|
616
|
+
${agentInstructions ? `## Project-Specific Instructions
|
|
617
|
+
${agentInstructions}
|
|
618
|
+
|
|
619
|
+
` : ""}
|
|
620
|
+
|
|
621
|
+
## Task: Record UI Demo Video
|
|
622
|
+
|
|
623
|
+
Create a short video (max ${videoConfig.max_duration}s) demonstrating the UI changes made for this issue.
|
|
624
|
+
|
|
625
|
+
### Video Requirements
|
|
626
|
+
- Resolution: ${videoConfig.width}x${videoConfig.height}
|
|
627
|
+
- Format: WebM or MP4
|
|
628
|
+
- Duration: Under ${videoConfig.max_duration} seconds
|
|
629
|
+
- Show the key UI interactions and visual changes
|
|
630
|
+
|
|
631
|
+
### Steps
|
|
632
|
+
|
|
633
|
+
1. **Start the development server** if not already running
|
|
634
|
+
2. **Use Playwright to record video** of the relevant UI interactions:
|
|
635
|
+
- Navigate to the affected pages/components
|
|
636
|
+
- Demonstrate the new or changed functionality
|
|
637
|
+
- Show before/after if applicable
|
|
638
|
+
|
|
639
|
+
3. **Upload video to GitHub** as a release asset or use GitHub's drag-drop upload:
|
|
640
|
+
- Create a GitHub release or upload to issue comments
|
|
641
|
+
- Get the permanent URL for the video
|
|
642
|
+
- Do NOT commit video files to the repository
|
|
643
|
+
|
|
644
|
+
4. **Add video to PR** by commenting with the video URL or embedding it
|
|
645
|
+
|
|
646
|
+
### Important
|
|
647
|
+
- Upload video to GitHub assets, NOT to the repository
|
|
648
|
+
- Keep the video concise - focus on demonstrating the changes
|
|
649
|
+
- Ensure the video clearly shows the UI improvements
|
|
650
|
+
|
|
651
|
+
Output the GitHub URL where the video was uploaded when complete.`;
|
|
652
|
+
}
|
|
595
653
|
|
|
596
654
|
// src/commands/create.ts
|
|
597
655
|
async function createCommand(description, options) {
|
|
@@ -1836,7 +1894,7 @@ import { homedir } from "os";
|
|
|
1836
1894
|
// package.json
|
|
1837
1895
|
var package_default = {
|
|
1838
1896
|
name: "@rotorsoft/gent",
|
|
1839
|
-
version: "1.
|
|
1897
|
+
version: "1.12.0",
|
|
1840
1898
|
description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
|
|
1841
1899
|
keywords: [
|
|
1842
1900
|
"cli",
|
|
@@ -2245,6 +2303,722 @@ async function statusCommand() {
|
|
|
2245
2303
|
}
|
|
2246
2304
|
}
|
|
2247
2305
|
|
|
2306
|
+
// src/commands/tui.ts
|
|
2307
|
+
import inquirer6 from "inquirer";
|
|
2308
|
+
import { execa as execa4 } from "execa";
|
|
2309
|
+
|
|
2310
|
+
// src/tui/state.ts
|
|
2311
|
+
async function aggregateState() {
|
|
2312
|
+
const isGitRepo = await checkGitRepo();
|
|
2313
|
+
if (!isGitRepo) {
|
|
2314
|
+
const config2 = loadConfig();
|
|
2315
|
+
return {
|
|
2316
|
+
isGitRepo: false,
|
|
2317
|
+
isGhAuthenticated: false,
|
|
2318
|
+
isAIProviderAvailable: false,
|
|
2319
|
+
config: config2,
|
|
2320
|
+
hasConfig: false,
|
|
2321
|
+
hasProgress: false,
|
|
2322
|
+
branch: "",
|
|
2323
|
+
branchInfo: null,
|
|
2324
|
+
isOnMain: false,
|
|
2325
|
+
hasUncommittedChanges: false,
|
|
2326
|
+
hasUnpushedCommits: false,
|
|
2327
|
+
commits: [],
|
|
2328
|
+
baseBranch: "main",
|
|
2329
|
+
issue: null,
|
|
2330
|
+
workflowStatus: "none",
|
|
2331
|
+
pr: null,
|
|
2332
|
+
reviewFeedback: [],
|
|
2333
|
+
hasActionableFeedback: false,
|
|
2334
|
+
hasUIChanges: false,
|
|
2335
|
+
isPlaywrightAvailable: false
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
const config = loadConfig();
|
|
2339
|
+
const workflowLabels = getWorkflowLabels(config);
|
|
2340
|
+
const [
|
|
2341
|
+
isGhAuthenticated,
|
|
2342
|
+
isAIProviderAvailable,
|
|
2343
|
+
branch,
|
|
2344
|
+
isOnMain,
|
|
2345
|
+
uncommitted,
|
|
2346
|
+
baseBranch
|
|
2347
|
+
] = await Promise.all([
|
|
2348
|
+
checkGhAuth(),
|
|
2349
|
+
checkAIProvider(config.ai.provider),
|
|
2350
|
+
getCurrentBranch(),
|
|
2351
|
+
isOnMainBranch(),
|
|
2352
|
+
hasUncommittedChanges(),
|
|
2353
|
+
getDefaultBranch()
|
|
2354
|
+
]);
|
|
2355
|
+
const hasConfig = configExists();
|
|
2356
|
+
const hasProgress = progressExists(config);
|
|
2357
|
+
const branchInfo = parseBranchName(branch);
|
|
2358
|
+
const [commits, unpushed] = await Promise.all([
|
|
2359
|
+
getCommitsSinceBase(baseBranch),
|
|
2360
|
+
getUnpushedCommits()
|
|
2361
|
+
]);
|
|
2362
|
+
let issue = null;
|
|
2363
|
+
let workflowStatus = "none";
|
|
2364
|
+
let pr = null;
|
|
2365
|
+
let reviewFeedback = [];
|
|
2366
|
+
let hasActionableFeedback = false;
|
|
2367
|
+
let uiChanges = false;
|
|
2368
|
+
let playwrightAvailable = false;
|
|
2369
|
+
if (!isOnMain) {
|
|
2370
|
+
const issueNumber = extractIssueNumber(branch);
|
|
2371
|
+
const [issueResult, prResult, changedFiles, playwrightResult] = await Promise.all([
|
|
2372
|
+
issueNumber ? getIssue(issueNumber).catch(() => null) : Promise.resolve(null),
|
|
2373
|
+
getPrStatus().catch(() => null),
|
|
2374
|
+
getChangedFiles(baseBranch),
|
|
2375
|
+
isPlaywrightAvailable()
|
|
2376
|
+
]);
|
|
2377
|
+
issue = issueResult;
|
|
2378
|
+
pr = prResult;
|
|
2379
|
+
uiChanges = hasUIChanges(changedFiles);
|
|
2380
|
+
playwrightAvailable = playwrightResult;
|
|
2381
|
+
if (issue) {
|
|
2382
|
+
if (issue.labels.includes(workflowLabels.ready)) {
|
|
2383
|
+
workflowStatus = "ready";
|
|
2384
|
+
} else if (issue.labels.includes(workflowLabels.inProgress)) {
|
|
2385
|
+
workflowStatus = "in-progress";
|
|
2386
|
+
} else if (issue.labels.includes(workflowLabels.completed)) {
|
|
2387
|
+
workflowStatus = "completed";
|
|
2388
|
+
} else if (issue.labels.includes(workflowLabels.blocked)) {
|
|
2389
|
+
workflowStatus = "blocked";
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
if (pr && pr.state === "open") {
|
|
2393
|
+
try {
|
|
2394
|
+
const lastCommitTimestamp = await getLastCommitTimestamp();
|
|
2395
|
+
const reviewData = await getPrReviewData(pr.number);
|
|
2396
|
+
const { items } = summarizeReviewFeedback(reviewData, { afterTimestamp: lastCommitTimestamp });
|
|
2397
|
+
reviewFeedback = items;
|
|
2398
|
+
hasActionableFeedback = items.length > 0;
|
|
2399
|
+
} catch {
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
return {
|
|
2404
|
+
isGitRepo,
|
|
2405
|
+
isGhAuthenticated,
|
|
2406
|
+
isAIProviderAvailable,
|
|
2407
|
+
config,
|
|
2408
|
+
hasConfig,
|
|
2409
|
+
hasProgress,
|
|
2410
|
+
branch,
|
|
2411
|
+
branchInfo,
|
|
2412
|
+
isOnMain,
|
|
2413
|
+
hasUncommittedChanges: uncommitted,
|
|
2414
|
+
hasUnpushedCommits: unpushed,
|
|
2415
|
+
commits,
|
|
2416
|
+
baseBranch,
|
|
2417
|
+
issue,
|
|
2418
|
+
workflowStatus,
|
|
2419
|
+
pr,
|
|
2420
|
+
reviewFeedback,
|
|
2421
|
+
hasActionableFeedback,
|
|
2422
|
+
hasUIChanges: uiChanges,
|
|
2423
|
+
isPlaywrightAvailable: playwrightAvailable
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/tui/actions.ts
|
|
2428
|
+
function getAvailableActions(state) {
|
|
2429
|
+
const actions = [];
|
|
2430
|
+
if (!state.isGitRepo || !state.isGhAuthenticated) {
|
|
2431
|
+
actions.push({ id: "quit", label: "quit", shortcut: "q" });
|
|
2432
|
+
return actions;
|
|
2433
|
+
}
|
|
2434
|
+
if (state.isOnMain) {
|
|
2435
|
+
actions.push({ id: "create", label: "new", shortcut: "n" });
|
|
2436
|
+
actions.push({ id: "run-auto", label: "run next", shortcut: "r" });
|
|
2437
|
+
actions.push({ id: "list", label: "list", shortcut: "l" });
|
|
2438
|
+
actions.push({ id: "switch-provider", label: "switch", shortcut: "s" });
|
|
2439
|
+
actions.push({ id: "quit", label: "quit", shortcut: "q" });
|
|
2440
|
+
return actions;
|
|
2441
|
+
}
|
|
2442
|
+
if (state.hasUncommittedChanges) {
|
|
2443
|
+
actions.push({ id: "commit", label: "commit", shortcut: "c" });
|
|
2444
|
+
}
|
|
2445
|
+
if (state.hasUnpushedCommits) {
|
|
2446
|
+
actions.push({ id: "push", label: "Push", shortcut: "P" });
|
|
2447
|
+
}
|
|
2448
|
+
if (!state.pr && state.commits.length > 0) {
|
|
2449
|
+
actions.push({ id: "pr", label: "Create pr", shortcut: "C" });
|
|
2450
|
+
}
|
|
2451
|
+
if (state.pr && state.pr.state === "open") {
|
|
2452
|
+
if (state.hasActionableFeedback) {
|
|
2453
|
+
actions.push({ id: "fix", label: "fix feedback", shortcut: "f" });
|
|
2454
|
+
}
|
|
2455
|
+
if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled) {
|
|
2456
|
+
actions.push({ id: "video", label: "video", shortcut: "v" });
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (state.pr && (state.pr.state === "merged" || state.pr.state === "closed")) {
|
|
2460
|
+
actions.push({ id: "checkout-main", label: "main", shortcut: "m" });
|
|
2461
|
+
}
|
|
2462
|
+
actions.push({ id: "switch-provider", label: "switch", shortcut: "s" });
|
|
2463
|
+
actions.push({ id: "quit", label: "quit", shortcut: "q" });
|
|
2464
|
+
return actions;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// src/tui/display.ts
|
|
2468
|
+
import chalk3 from "chalk";
|
|
2469
|
+
var stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2470
|
+
var visibleLen = (str) => stripAnsi(str).length;
|
|
2471
|
+
function termWidth() {
|
|
2472
|
+
return Math.min(process.stdout.columns || 80, 90);
|
|
2473
|
+
}
|
|
2474
|
+
function termHeight() {
|
|
2475
|
+
return process.stdout.rows || 24;
|
|
2476
|
+
}
|
|
2477
|
+
function truncate(text, max) {
|
|
2478
|
+
if (text.length <= max) return text;
|
|
2479
|
+
return text.slice(0, max - 1) + "\u2026";
|
|
2480
|
+
}
|
|
2481
|
+
function extractDescription(body, maxLen) {
|
|
2482
|
+
const lines = body.split("\n");
|
|
2483
|
+
for (const line of lines) {
|
|
2484
|
+
const trimmed = line.trim();
|
|
2485
|
+
if (!trimmed) continue;
|
|
2486
|
+
if (trimmed.startsWith("#")) continue;
|
|
2487
|
+
if (trimmed.startsWith("---")) continue;
|
|
2488
|
+
if (trimmed.startsWith("META:")) continue;
|
|
2489
|
+
if (trimmed.startsWith("**Type:**")) continue;
|
|
2490
|
+
const clean = trimmed.replace(/\*\*/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
2491
|
+
return truncate(clean, maxLen);
|
|
2492
|
+
}
|
|
2493
|
+
return "";
|
|
2494
|
+
}
|
|
2495
|
+
function topRow(title, w) {
|
|
2496
|
+
const label = ` ${title} `;
|
|
2497
|
+
const fill = w - 2 - label.length;
|
|
2498
|
+
return chalk3.dim("\u250C") + chalk3.bold.cyan(label) + chalk3.dim("\u2500".repeat(Math.max(0, fill)) + "\u2510");
|
|
2499
|
+
}
|
|
2500
|
+
function midRow(title, w) {
|
|
2501
|
+
const label = ` ${title} `;
|
|
2502
|
+
const fill = w - 2 - label.length;
|
|
2503
|
+
return chalk3.dim("\u251C") + chalk3.bold.cyan(label) + chalk3.dim("\u2500".repeat(Math.max(0, fill)) + "\u2524");
|
|
2504
|
+
}
|
|
2505
|
+
function divRow(w) {
|
|
2506
|
+
return chalk3.dim("\u251C" + "\u2500".repeat(w - 2) + "\u2524");
|
|
2507
|
+
}
|
|
2508
|
+
function botRow(w) {
|
|
2509
|
+
return chalk3.dim("\u2514" + "\u2500".repeat(w - 2) + "\u2518");
|
|
2510
|
+
}
|
|
2511
|
+
function row(text, w) {
|
|
2512
|
+
const inner = w - 4;
|
|
2513
|
+
const pad = Math.max(0, inner - visibleLen(text));
|
|
2514
|
+
return chalk3.dim("\u2502") + " " + text + " ".repeat(pad) + " " + chalk3.dim("\u2502");
|
|
2515
|
+
}
|
|
2516
|
+
function workflowBadge(status) {
|
|
2517
|
+
switch (status) {
|
|
2518
|
+
case "ready":
|
|
2519
|
+
return chalk3.bgGreen.black(" READY ");
|
|
2520
|
+
case "in-progress":
|
|
2521
|
+
return chalk3.bgYellow.black(" IN PROGRESS ");
|
|
2522
|
+
case "completed":
|
|
2523
|
+
return chalk3.bgBlue.white(" COMPLETED ");
|
|
2524
|
+
case "blocked":
|
|
2525
|
+
return chalk3.bgRed.white(" BLOCKED ");
|
|
2526
|
+
default:
|
|
2527
|
+
return "";
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
function prBadge(state, draft) {
|
|
2531
|
+
if (state === "merged") return chalk3.bgMagenta.white(" MERGED ");
|
|
2532
|
+
if (state === "closed") return chalk3.bgRed.white(" CLOSED ");
|
|
2533
|
+
return draft ? chalk3.bgYellow.black(" DRAFT ") : chalk3.bgGreen.black(" OPEN ");
|
|
2534
|
+
}
|
|
2535
|
+
function reviewBadge(decision) {
|
|
2536
|
+
if (!decision) return "";
|
|
2537
|
+
switch (decision) {
|
|
2538
|
+
case "APPROVED":
|
|
2539
|
+
return " " + chalk3.green("Approved");
|
|
2540
|
+
case "CHANGES_REQUESTED":
|
|
2541
|
+
return " " + chalk3.red("Changes requested");
|
|
2542
|
+
case "REVIEW_REQUIRED":
|
|
2543
|
+
return " " + chalk3.yellow("Review pending");
|
|
2544
|
+
default:
|
|
2545
|
+
return "";
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
var shortcutColors = [
|
|
2549
|
+
chalk3.cyan.bold,
|
|
2550
|
+
chalk3.green.bold,
|
|
2551
|
+
chalk3.yellow.bold,
|
|
2552
|
+
chalk3.magenta.bold,
|
|
2553
|
+
chalk3.blue.bold,
|
|
2554
|
+
chalk3.red.bold
|
|
2555
|
+
];
|
|
2556
|
+
function formatAction(a, color) {
|
|
2557
|
+
const key = a.shortcut;
|
|
2558
|
+
const idx = a.label.indexOf(key);
|
|
2559
|
+
if (idx >= 0) {
|
|
2560
|
+
const before = a.label.slice(0, idx);
|
|
2561
|
+
const after = a.label.slice(idx + key.length);
|
|
2562
|
+
return chalk3.dim(before) + color(key) + chalk3.dim(after);
|
|
2563
|
+
}
|
|
2564
|
+
return color(key) + " " + chalk3.dim(a.label);
|
|
2565
|
+
}
|
|
2566
|
+
function formatCommandBar(actions, w) {
|
|
2567
|
+
const parts = actions.map((a, i) => {
|
|
2568
|
+
const color = shortcutColors[i % shortcutColors.length];
|
|
2569
|
+
return formatAction(a, color);
|
|
2570
|
+
});
|
|
2571
|
+
const inner = w - 4;
|
|
2572
|
+
const lines = [];
|
|
2573
|
+
let cur = "";
|
|
2574
|
+
for (const part of parts) {
|
|
2575
|
+
const next = cur + (cur.length > 0 ? " " : "") + part;
|
|
2576
|
+
if (visibleLen(next) > inner) {
|
|
2577
|
+
lines.push(cur);
|
|
2578
|
+
cur = part;
|
|
2579
|
+
} else {
|
|
2580
|
+
cur = next;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
if (cur.length > 0) lines.push(cur);
|
|
2584
|
+
return lines;
|
|
2585
|
+
}
|
|
2586
|
+
function renderModal(message) {
|
|
2587
|
+
const w = termWidth();
|
|
2588
|
+
const h = termHeight();
|
|
2589
|
+
const modalW = Math.min(w - 4, Math.max(30, visibleLen(message) + 8));
|
|
2590
|
+
const padX = Math.max(0, Math.floor((w - modalW) / 2));
|
|
2591
|
+
const padY = Math.max(0, Math.floor((h - 5) / 2));
|
|
2592
|
+
const indent = " ".repeat(padX);
|
|
2593
|
+
const inner = modalW - 4;
|
|
2594
|
+
const textPad = Math.max(0, inner - visibleLen(message));
|
|
2595
|
+
const lines = [
|
|
2596
|
+
indent + chalk3.dim("\u250C" + "\u2500".repeat(modalW - 2) + "\u2510"),
|
|
2597
|
+
indent + chalk3.dim("\u2502") + " ".repeat(modalW - 2) + chalk3.dim("\u2502"),
|
|
2598
|
+
indent + chalk3.dim("\u2502") + " " + chalk3.bold(message) + " ".repeat(textPad) + " " + chalk3.dim("\u2502"),
|
|
2599
|
+
indent + chalk3.dim("\u2502") + " ".repeat(modalW - 2) + chalk3.dim("\u2502"),
|
|
2600
|
+
indent + chalk3.dim("\u2514" + "\u2500".repeat(modalW - 2) + "\u2518")
|
|
2601
|
+
];
|
|
2602
|
+
for (let i = 0; i < padY; i++) console.log();
|
|
2603
|
+
for (const line of lines) console.log(line);
|
|
2604
|
+
}
|
|
2605
|
+
function renderSettings(state, w) {
|
|
2606
|
+
const provider = getProviderDisplayName(state.config.ai.provider);
|
|
2607
|
+
const provTag = state.isAIProviderAvailable ? chalk3.green(provider) : chalk3.red(provider);
|
|
2608
|
+
const ghTag = state.isGhAuthenticated ? chalk3.green("authenticated") : chalk3.red("not authenticated");
|
|
2609
|
+
const videoTag = state.config.video.enabled ? chalk3.green("on") : chalk3.dim("off");
|
|
2610
|
+
console.log(row(chalk3.dim("Provider: ") + provTag, w));
|
|
2611
|
+
console.log(row(chalk3.dim("GitHub: ") + ghTag, w));
|
|
2612
|
+
console.log(row(chalk3.dim("Video: ") + videoTag, w));
|
|
2613
|
+
}
|
|
2614
|
+
function renderDashboard(state, actions, hint) {
|
|
2615
|
+
const w = termWidth();
|
|
2616
|
+
const descMax = w - 8;
|
|
2617
|
+
const version2 = getVersion();
|
|
2618
|
+
const titleLabel = `gent v${version2}`;
|
|
2619
|
+
console.log(topRow(titleLabel, w));
|
|
2620
|
+
if (!state.isGitRepo) {
|
|
2621
|
+
console.log(row(chalk3.red("Not a git repository"), w));
|
|
2622
|
+
console.log(row(chalk3.dim("Run gent init in a git repo to get started"), w));
|
|
2623
|
+
console.log(botRow(w));
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
if (!state.isGhAuthenticated) {
|
|
2627
|
+
console.log(row(chalk3.red("GitHub CLI not authenticated"), w));
|
|
2628
|
+
console.log(row(chalk3.dim("Run: gh auth login"), w));
|
|
2629
|
+
console.log(botRow(w));
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
if (state.isOnMain) {
|
|
2633
|
+
console.log(row(chalk3.magenta(state.branch) + chalk3.dim(" \xB7 ready to start new work"), w));
|
|
2634
|
+
if (state.hasUncommittedChanges) {
|
|
2635
|
+
console.log(row(chalk3.yellow("\u25CF uncommitted changes"), w));
|
|
2636
|
+
}
|
|
2637
|
+
console.log(midRow("Settings", w));
|
|
2638
|
+
renderSettings(state, w);
|
|
2639
|
+
if (hint) {
|
|
2640
|
+
console.log(midRow("Hint", w));
|
|
2641
|
+
console.log(row(chalk3.yellow(hint), w));
|
|
2642
|
+
}
|
|
2643
|
+
console.log(divRow(w));
|
|
2644
|
+
for (const line of formatCommandBar(actions, w)) {
|
|
2645
|
+
console.log(row(line, w));
|
|
2646
|
+
}
|
|
2647
|
+
console.log(botRow(w));
|
|
2648
|
+
console.log();
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
const section = (title) => {
|
|
2652
|
+
console.log(midRow(title, w));
|
|
2653
|
+
};
|
|
2654
|
+
section("Ticket");
|
|
2655
|
+
if (state.issue) {
|
|
2656
|
+
console.log(row(
|
|
2657
|
+
chalk3.cyan(`#${state.issue.number}`) + " " + chalk3.bold(truncate(state.issue.title, descMax - 6)),
|
|
2658
|
+
w
|
|
2659
|
+
));
|
|
2660
|
+
const desc = extractDescription(state.issue.body, descMax);
|
|
2661
|
+
if (desc) console.log(row(chalk3.dim(desc), w));
|
|
2662
|
+
const tags = [];
|
|
2663
|
+
if (state.workflowStatus !== "none") tags.push(workflowBadge(state.workflowStatus));
|
|
2664
|
+
for (const prefix of ["type:", "priority:", "risk:", "area:"]) {
|
|
2665
|
+
const l = state.issue.labels.find((x) => x.startsWith(prefix));
|
|
2666
|
+
if (l) tags.push(chalk3.dim(l));
|
|
2667
|
+
}
|
|
2668
|
+
if (tags.length) console.log(row(tags.join(" "), w));
|
|
2669
|
+
} else {
|
|
2670
|
+
console.log(row(chalk3.dim("No linked issue"), w));
|
|
2671
|
+
}
|
|
2672
|
+
section("Branch");
|
|
2673
|
+
console.log(row(chalk3.magenta(state.branch), w));
|
|
2674
|
+
const bits = [];
|
|
2675
|
+
if (state.commits.length > 0) bits.push(chalk3.dim(`${state.commits.length} ahead`));
|
|
2676
|
+
if (state.hasUncommittedChanges) bits.push(chalk3.yellow("\u25CF uncommitted"));
|
|
2677
|
+
if (state.hasUnpushedCommits) bits.push(chalk3.yellow("\u25CF unpushed"));
|
|
2678
|
+
if (!state.hasUncommittedChanges && !state.hasUnpushedCommits && state.commits.length > 0) {
|
|
2679
|
+
bits.push(chalk3.green("\u25CF synced"));
|
|
2680
|
+
}
|
|
2681
|
+
if (bits.length) console.log(row(bits.join(chalk3.dim(" \xB7 ")), w));
|
|
2682
|
+
section("Pull Request");
|
|
2683
|
+
if (state.pr) {
|
|
2684
|
+
const titleText = state.pr.title ? " " + truncate(state.pr.title, descMax - 12) : "";
|
|
2685
|
+
console.log(row(
|
|
2686
|
+
chalk3.cyan(`#${state.pr.number}`) + titleText,
|
|
2687
|
+
w
|
|
2688
|
+
));
|
|
2689
|
+
console.log(row(
|
|
2690
|
+
prBadge(state.pr.state, state.pr.isDraft) + reviewBadge(state.pr.reviewDecision),
|
|
2691
|
+
w
|
|
2692
|
+
));
|
|
2693
|
+
if (state.hasActionableFeedback) {
|
|
2694
|
+
const n = state.reviewFeedback.length;
|
|
2695
|
+
console.log(row(chalk3.yellow(`${n} actionable comment${n !== 1 ? "s" : ""} pending`), w));
|
|
2696
|
+
}
|
|
2697
|
+
if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled && state.pr.state === "open") {
|
|
2698
|
+
console.log(row(chalk3.cyan("UI changes detected") + chalk3.dim(" \xB7 video capture available"), w));
|
|
2699
|
+
}
|
|
2700
|
+
console.log(row(chalk3.dim(state.pr.url), w));
|
|
2701
|
+
} else {
|
|
2702
|
+
console.log(row(chalk3.dim("No PR created"), w));
|
|
2703
|
+
}
|
|
2704
|
+
section("Commits");
|
|
2705
|
+
if (state.commits.length > 0) {
|
|
2706
|
+
const max = 6;
|
|
2707
|
+
for (const c of state.commits.slice(0, max)) {
|
|
2708
|
+
console.log(row(c, w));
|
|
2709
|
+
}
|
|
2710
|
+
if (state.commits.length > max) {
|
|
2711
|
+
console.log(row(chalk3.dim(`\u2026 and ${state.commits.length - max} more`), w));
|
|
2712
|
+
}
|
|
2713
|
+
} else {
|
|
2714
|
+
console.log(row(chalk3.dim("No commits"), w));
|
|
2715
|
+
}
|
|
2716
|
+
section("Settings");
|
|
2717
|
+
renderSettings(state, w);
|
|
2718
|
+
if (hint) {
|
|
2719
|
+
section("Hint");
|
|
2720
|
+
console.log(row(chalk3.yellow(hint), w));
|
|
2721
|
+
}
|
|
2722
|
+
console.log(divRow(w));
|
|
2723
|
+
for (const line of formatCommandBar(actions, w)) {
|
|
2724
|
+
console.log(row(line, w));
|
|
2725
|
+
}
|
|
2726
|
+
console.log(botRow(w));
|
|
2727
|
+
}
|
|
2728
|
+
function clearScreen() {
|
|
2729
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// src/commands/tui.ts
|
|
2733
|
+
var CANCEL = /* @__PURE__ */ Symbol("cancel");
|
|
2734
|
+
async function confirm(message) {
|
|
2735
|
+
const { ok } = await inquirer6.prompt([
|
|
2736
|
+
{
|
|
2737
|
+
type: "confirm",
|
|
2738
|
+
name: "ok",
|
|
2739
|
+
message,
|
|
2740
|
+
default: true
|
|
2741
|
+
}
|
|
2742
|
+
]);
|
|
2743
|
+
return ok;
|
|
2744
|
+
}
|
|
2745
|
+
async function loadState() {
|
|
2746
|
+
clearScreen();
|
|
2747
|
+
renderModal("Loading workflow state...");
|
|
2748
|
+
const state = await aggregateState();
|
|
2749
|
+
return state;
|
|
2750
|
+
}
|
|
2751
|
+
async function waitForKey(validKeys) {
|
|
2752
|
+
return new Promise((resolve) => {
|
|
2753
|
+
const { stdin } = process;
|
|
2754
|
+
const wasRaw = stdin.isRaw;
|
|
2755
|
+
stdin.setRawMode(true);
|
|
2756
|
+
stdin.resume();
|
|
2757
|
+
stdin.setEncoding("utf8");
|
|
2758
|
+
const onData = (key) => {
|
|
2759
|
+
if (key === "") {
|
|
2760
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
2761
|
+
stdin.pause();
|
|
2762
|
+
stdin.removeListener("data", onData);
|
|
2763
|
+
resolve("q");
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
if (validKeys.includes(key)) {
|
|
2767
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
2768
|
+
stdin.pause();
|
|
2769
|
+
stdin.removeListener("data", onData);
|
|
2770
|
+
resolve(key);
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
stdin.on("data", onData);
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
async function executeAction(actionId, state) {
|
|
2777
|
+
switch (actionId) {
|
|
2778
|
+
case "quit":
|
|
2779
|
+
return false;
|
|
2780
|
+
case "list":
|
|
2781
|
+
clearScreen();
|
|
2782
|
+
await listCommand({ status: "ready", limit: 20 });
|
|
2783
|
+
await promptContinue();
|
|
2784
|
+
return true;
|
|
2785
|
+
case "run-auto": {
|
|
2786
|
+
clearScreen();
|
|
2787
|
+
if (!await confirm("Start AI agent to implement next ticket?")) return true;
|
|
2788
|
+
await runCommand(void 0, { auto: true });
|
|
2789
|
+
return false;
|
|
2790
|
+
}
|
|
2791
|
+
case "create": {
|
|
2792
|
+
clearScreen();
|
|
2793
|
+
const { description } = await inquirer6.prompt([
|
|
2794
|
+
{
|
|
2795
|
+
type: "input",
|
|
2796
|
+
name: "description",
|
|
2797
|
+
message: "Describe the ticket (empty to cancel):"
|
|
2798
|
+
}
|
|
2799
|
+
]);
|
|
2800
|
+
if (!description.trim()) {
|
|
2801
|
+
logger.info("Cancelled");
|
|
2802
|
+
return true;
|
|
2803
|
+
}
|
|
2804
|
+
await createCommand(description, {});
|
|
2805
|
+
await promptContinue();
|
|
2806
|
+
return true;
|
|
2807
|
+
}
|
|
2808
|
+
case "commit":
|
|
2809
|
+
clearScreen();
|
|
2810
|
+
await handleCommit(state);
|
|
2811
|
+
await promptContinue();
|
|
2812
|
+
return true;
|
|
2813
|
+
case "push":
|
|
2814
|
+
clearScreen();
|
|
2815
|
+
await handlePush();
|
|
2816
|
+
await promptContinue();
|
|
2817
|
+
return true;
|
|
2818
|
+
case "pr": {
|
|
2819
|
+
clearScreen();
|
|
2820
|
+
if (!await confirm("Create a pull request?")) return true;
|
|
2821
|
+
await prCommand({});
|
|
2822
|
+
await promptContinue();
|
|
2823
|
+
return true;
|
|
2824
|
+
}
|
|
2825
|
+
case "fix": {
|
|
2826
|
+
clearScreen();
|
|
2827
|
+
if (!await confirm("Start AI agent to fix review feedback?")) return true;
|
|
2828
|
+
await fixCommand({});
|
|
2829
|
+
return false;
|
|
2830
|
+
}
|
|
2831
|
+
case "video": {
|
|
2832
|
+
clearScreen();
|
|
2833
|
+
if (!await confirm("Record video of UI changes?")) return true;
|
|
2834
|
+
await handleVideoCapture(state);
|
|
2835
|
+
await promptContinue();
|
|
2836
|
+
return true;
|
|
2837
|
+
}
|
|
2838
|
+
case "switch-provider":
|
|
2839
|
+
clearScreen();
|
|
2840
|
+
await handleSwitchProvider(state);
|
|
2841
|
+
return true;
|
|
2842
|
+
case "checkout-main": {
|
|
2843
|
+
clearScreen();
|
|
2844
|
+
if (!await confirm("Switch to main branch?")) return true;
|
|
2845
|
+
await handleCheckoutMain();
|
|
2846
|
+
return true;
|
|
2847
|
+
}
|
|
2848
|
+
default:
|
|
2849
|
+
return true;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
async function handleCommit(state) {
|
|
2853
|
+
try {
|
|
2854
|
+
const { stdout: status } = await execa4("git", ["status", "--short"]);
|
|
2855
|
+
if (!status.trim()) {
|
|
2856
|
+
logger.info("No changes to commit");
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
logger.info("Changes:");
|
|
2860
|
+
console.log(status);
|
|
2861
|
+
console.log();
|
|
2862
|
+
await execa4("git", ["add", "-A"]);
|
|
2863
|
+
const { stdout: diffStat } = await execa4("git", ["diff", "--cached", "--stat"]);
|
|
2864
|
+
const { stdout: diffPatch } = await execa4("git", ["diff", "--cached"]);
|
|
2865
|
+
const diffContent = (diffStat + "\n\n" + diffPatch).slice(0, 4e3);
|
|
2866
|
+
const issueNumber = state.issue?.number ?? null;
|
|
2867
|
+
const issueTitle = state.issue?.title ?? null;
|
|
2868
|
+
const provider = state.config.ai.provider;
|
|
2869
|
+
const providerName = getProviderDisplayName(provider);
|
|
2870
|
+
logger.info(`Generating commit message with ${providerName}...`);
|
|
2871
|
+
const message = await generateCommitMessage(diffContent, issueNumber, issueTitle, state);
|
|
2872
|
+
if (message === CANCEL) {
|
|
2873
|
+
await execa4("git", ["reset", "HEAD"]);
|
|
2874
|
+
logger.info("Cancelled");
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
console.log();
|
|
2878
|
+
logger.info(`Message: ${message}`);
|
|
2879
|
+
console.log();
|
|
2880
|
+
if (!await confirm("Commit with this message?")) {
|
|
2881
|
+
await execa4("git", ["reset", "HEAD"]);
|
|
2882
|
+
logger.info("Commit cancelled");
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const providerEmail = getProviderEmail(provider);
|
|
2886
|
+
const fullMessage = `${message}
|
|
2887
|
+
|
|
2888
|
+
Co-Authored-By: ${providerName} <${providerEmail}>`;
|
|
2889
|
+
const spinner = createSpinner("Committing...");
|
|
2890
|
+
spinner.start();
|
|
2891
|
+
await execa4("git", ["commit", "-m", fullMessage]);
|
|
2892
|
+
spinner.succeed("Changes committed");
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
logger.error(`Commit failed: ${error}`);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
async function generateCommitMessage(diffContent, issueNumber, issueTitle, state) {
|
|
2898
|
+
try {
|
|
2899
|
+
const prompt = buildCommitMessagePrompt(diffContent, issueNumber, issueTitle);
|
|
2900
|
+
const result = await invokeAI({ prompt, streamOutput: true }, state.config);
|
|
2901
|
+
let message = result.output.trim().split("\n")[0].trim();
|
|
2902
|
+
for (const q of ['"', "'", "`"]) {
|
|
2903
|
+
if (message.startsWith(q) && message.endsWith(q)) {
|
|
2904
|
+
message = message.slice(1, -1);
|
|
2905
|
+
break;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
message = message.replace(/^```\w*\s*/, "").replace(/\s*```$/, "");
|
|
2909
|
+
return message;
|
|
2910
|
+
} catch {
|
|
2911
|
+
logger.warning("AI commit message generation failed");
|
|
2912
|
+
console.log();
|
|
2913
|
+
const { message } = await inquirer6.prompt([
|
|
2914
|
+
{
|
|
2915
|
+
type: "input",
|
|
2916
|
+
name: "message",
|
|
2917
|
+
message: "Commit message (empty to cancel):"
|
|
2918
|
+
}
|
|
2919
|
+
]);
|
|
2920
|
+
return message.trim() || CANCEL;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
async function handlePush() {
|
|
2924
|
+
try {
|
|
2925
|
+
const { stdout: branch } = await execa4("git", ["branch", "--show-current"]);
|
|
2926
|
+
if (!await confirm(`Push ${branch.trim()} to remote?`)) return;
|
|
2927
|
+
const spinner = createSpinner("Pushing...");
|
|
2928
|
+
spinner.start();
|
|
2929
|
+
await execa4("git", ["push", "-u", "origin", branch.trim()]);
|
|
2930
|
+
spinner.succeed("Pushed to remote");
|
|
2931
|
+
} catch (error) {
|
|
2932
|
+
logger.error(`Push failed: ${error}`);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
var PROVIDERS = ["claude", "gemini", "codex"];
|
|
2936
|
+
async function handleSwitchProvider(state) {
|
|
2937
|
+
const current = state.config.ai.provider;
|
|
2938
|
+
const { provider } = await inquirer6.prompt([
|
|
2939
|
+
{
|
|
2940
|
+
type: "list",
|
|
2941
|
+
name: "provider",
|
|
2942
|
+
message: "Select AI provider:",
|
|
2943
|
+
choices: PROVIDERS.map((p) => ({
|
|
2944
|
+
name: p.charAt(0).toUpperCase() + p.slice(1) + (p === current ? " (current)" : ""),
|
|
2945
|
+
value: p
|
|
2946
|
+
})),
|
|
2947
|
+
default: current
|
|
2948
|
+
}
|
|
2949
|
+
]);
|
|
2950
|
+
if (provider === current) return;
|
|
2951
|
+
try {
|
|
2952
|
+
updateConfigProvider(provider);
|
|
2953
|
+
logger.success(`Provider switched to ${provider}`);
|
|
2954
|
+
} catch (error) {
|
|
2955
|
+
logger.error(`Failed to switch provider: ${error}`);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
async function handleCheckoutMain() {
|
|
2959
|
+
try {
|
|
2960
|
+
const spinner = createSpinner("Switching to main...");
|
|
2961
|
+
spinner.start();
|
|
2962
|
+
await execa4("git", ["checkout", "main"]);
|
|
2963
|
+
await execa4("git", ["pull"]);
|
|
2964
|
+
spinner.succeed("Switched to main");
|
|
2965
|
+
} catch (error) {
|
|
2966
|
+
logger.error(`Checkout failed: ${error}`);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
async function handleVideoCapture(state) {
|
|
2970
|
+
if (!state.issue) {
|
|
2971
|
+
logger.error("No linked issue found");
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
try {
|
|
2975
|
+
const agentInstructions = loadAgentInstructions();
|
|
2976
|
+
const videoPrompt = buildVideoPrompt(
|
|
2977
|
+
state.issue.number,
|
|
2978
|
+
state.issue.title,
|
|
2979
|
+
state.config.video,
|
|
2980
|
+
agentInstructions
|
|
2981
|
+
);
|
|
2982
|
+
await invokeAIInteractive(videoPrompt, state.config);
|
|
2983
|
+
logger.success("Video capture completed");
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
logger.error(`Video capture failed: ${error}`);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
async function promptContinue() {
|
|
2989
|
+
console.log();
|
|
2990
|
+
await inquirer6.prompt([
|
|
2991
|
+
{
|
|
2992
|
+
type: "input",
|
|
2993
|
+
name: "continue",
|
|
2994
|
+
message: "Press Enter to continue..."
|
|
2995
|
+
}
|
|
2996
|
+
]);
|
|
2997
|
+
}
|
|
2998
|
+
async function tuiCommand() {
|
|
2999
|
+
let running = true;
|
|
3000
|
+
while (running) {
|
|
3001
|
+
const state = await loadState();
|
|
3002
|
+
clearScreen();
|
|
3003
|
+
const actions = getAvailableActions(state);
|
|
3004
|
+
let hint;
|
|
3005
|
+
if (state.isOnMain) {
|
|
3006
|
+
hint = "Select an action to get started";
|
|
3007
|
+
} else if (state.hasUncommittedChanges && !state.pr) {
|
|
3008
|
+
hint = "Commit your changes before creating a PR";
|
|
3009
|
+
} else if (state.hasActionableFeedback) {
|
|
3010
|
+
hint = "Review feedback needs attention";
|
|
3011
|
+
}
|
|
3012
|
+
renderDashboard(state, actions, hint);
|
|
3013
|
+
const validKeys = actions.map((a) => a.shortcut);
|
|
3014
|
+
const key = await waitForKey(validKeys);
|
|
3015
|
+
const action = actions.find((a) => a.shortcut === key);
|
|
3016
|
+
if (action) {
|
|
3017
|
+
running = await executeAction(action.id, state);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
|
|
2248
3022
|
// src/index.ts
|
|
2249
3023
|
var version = getVersion();
|
|
2250
3024
|
function startVersionCheck() {
|
|
@@ -2295,5 +3069,11 @@ program.command("fix").description("Apply PR review feedback using AI").option("
|
|
|
2295
3069
|
program.command("status").description("Show current workflow status").action(async () => {
|
|
2296
3070
|
await statusCommand();
|
|
2297
3071
|
});
|
|
3072
|
+
program.command("ui").description("Launch interactive dashboard").action(async () => {
|
|
3073
|
+
await tuiCommand();
|
|
3074
|
+
});
|
|
3075
|
+
program.action(async () => {
|
|
3076
|
+
await tuiCommand();
|
|
3077
|
+
});
|
|
2298
3078
|
program.parse();
|
|
2299
3079
|
//# sourceMappingURL=index.js.map
|