@rotorsoft/gent 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1184 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addIssueComment,
4
+ assignIssue,
5
+ buildIssueLabels,
6
+ checkClaudeCli,
7
+ checkGhAuth,
8
+ checkGitRepo,
9
+ colors,
10
+ configExists,
11
+ createIssue,
12
+ createPullRequest,
13
+ extractPriorityFromLabels,
14
+ extractTypeFromLabels,
15
+ generateDefaultConfig,
16
+ getConfigPath,
17
+ getCurrentUser,
18
+ getIssue,
19
+ getPrForBranch,
20
+ getWorkflowLabels,
21
+ isValidIssueNumber,
22
+ listIssues,
23
+ loadAgentInstructions,
24
+ loadConfig,
25
+ logger,
26
+ sanitizeSlug,
27
+ setupLabelsCommand,
28
+ sortByPriority,
29
+ updateIssueLabels,
30
+ withSpinner
31
+ } from "./chunk-NRTQPDZB.js";
32
+
33
+ // src/index.ts
34
+ import { Command } from "commander";
35
+
36
+ // src/commands/init.ts
37
+ import { writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
38
+ import { join as join2 } from "path";
39
+ import inquirer from "inquirer";
40
+
41
+ // src/lib/progress.ts
42
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
43
+ import { join, dirname } from "path";
44
+ function getProgressPath(config, cwd = process.cwd()) {
45
+ return join(cwd, config.progress.file);
46
+ }
47
+ function progressExists(config, cwd = process.cwd()) {
48
+ return existsSync(getProgressPath(config, cwd));
49
+ }
50
+ function readProgress(config, cwd = process.cwd()) {
51
+ const path = getProgressPath(config, cwd);
52
+ if (!existsSync(path)) {
53
+ return "";
54
+ }
55
+ return readFileSync(path, "utf-8");
56
+ }
57
+ function initializeProgress(config, cwd = process.cwd()) {
58
+ const path = getProgressPath(config, cwd);
59
+ if (existsSync(path)) {
60
+ return;
61
+ }
62
+ const initialContent = `# Progress Log
63
+
64
+ This file tracks AI-assisted development progress.
65
+ Each entry documents: date, feature, decisions, files changed, tests, and concerns.
66
+
67
+ ---
68
+ `;
69
+ const dir = dirname(path);
70
+ if (!existsSync(dir)) {
71
+ mkdirSync(dir, { recursive: true });
72
+ }
73
+ writeFileSync(path, initialContent, "utf-8");
74
+ }
75
+
76
+ // src/commands/init.ts
77
+ var DEFAULT_AGENT_MD = `# AI Agent Instructions
78
+
79
+ This file contains instructions for Claude when working on this repository.
80
+
81
+ ## Project Overview
82
+
83
+ [Describe your project, its purpose, and key technologies used]
84
+
85
+ ## Code Patterns
86
+
87
+ ### Architecture
88
+ [Document your architecture - e.g., MVC, Clean Architecture, etc.]
89
+
90
+ ### Naming Conventions
91
+ [Document naming conventions for files, functions, variables, etc.]
92
+
93
+ ### Component Structure
94
+ [If applicable, describe component/module structure]
95
+
96
+ ## Testing Requirements
97
+
98
+ ### Unit Tests
99
+ - All new functions should have corresponding unit tests
100
+ - Use [your testing framework] for unit tests
101
+ - Aim for [X]% coverage on new code
102
+
103
+ ### Integration Tests
104
+ [Document when and how to write integration tests]
105
+
106
+ ## Commit Conventions
107
+
108
+ Follow conventional commits format:
109
+ - \`feat:\` New feature
110
+ - \`fix:\` Bug fix
111
+ - \`refactor:\` Code improvement without behavior change
112
+ - \`test:\` Testing additions
113
+ - \`chore:\` Maintenance/dependencies
114
+ - \`docs:\` Documentation
115
+
116
+ All AI commits should include:
117
+ \`\`\`
118
+ Co-Authored-By: Claude <noreply@anthropic.com>
119
+ \`\`\`
120
+
121
+ ## Important Files
122
+
123
+ [List key files the AI should understand before making changes]
124
+
125
+ - \`src/index.ts\` - Main entry point
126
+ - \`src/config/\` - Configuration files
127
+ - [Add more key files]
128
+
129
+ ## Constraints
130
+
131
+ - [List any constraints or limitations]
132
+ - [E.g., "Do not modify files in /vendor"]
133
+ - [E.g., "Always use async/await over callbacks"]
134
+ `;
135
+ async function initCommand(options) {
136
+ logger.bold("Initializing gent workflow...");
137
+ logger.newline();
138
+ const isGitRepo = await checkGitRepo();
139
+ if (!isGitRepo) {
140
+ logger.error("Not a git repository. Please run 'git init' first.");
141
+ process.exit(1);
142
+ }
143
+ const cwd = process.cwd();
144
+ if (configExists(cwd) && !options.force) {
145
+ const { overwrite } = await inquirer.prompt([
146
+ {
147
+ type: "confirm",
148
+ name: "overwrite",
149
+ message: "gent is already initialized. Overwrite existing config?",
150
+ default: false
151
+ }
152
+ ]);
153
+ if (!overwrite) {
154
+ logger.info("Initialization cancelled.");
155
+ return;
156
+ }
157
+ }
158
+ const configPath = getConfigPath(cwd);
159
+ writeFileSync2(configPath, generateDefaultConfig(), "utf-8");
160
+ logger.success(`Created ${colors.file(".gent.yml")}`);
161
+ const agentPath = join2(cwd, "AGENT.md");
162
+ if (!existsSync2(agentPath) || options.force) {
163
+ writeFileSync2(agentPath, DEFAULT_AGENT_MD, "utf-8");
164
+ logger.success(`Created ${colors.file("AGENT.md")}`);
165
+ } else {
166
+ logger.info(`${colors.file("AGENT.md")} already exists, skipping`);
167
+ }
168
+ const config = loadConfig(cwd);
169
+ initializeProgress(config, cwd);
170
+ logger.success(`Created ${colors.file(config.progress.file)}`);
171
+ logger.newline();
172
+ logger.box("Setup Complete", `Next steps:
173
+ 1. Edit ${colors.file("AGENT.md")} with your project-specific instructions
174
+ 2. Edit ${colors.file(".gent.yml")} to customize settings
175
+ 3. Run ${colors.command("gent setup-labels")} to create GitHub labels
176
+ 4. Run ${colors.command("gent create <description>")} to create your first ticket`);
177
+ const { setupLabels } = await inquirer.prompt([
178
+ {
179
+ type: "confirm",
180
+ name: "setupLabels",
181
+ message: "Would you like to setup GitHub labels now?",
182
+ default: true
183
+ }
184
+ ]);
185
+ if (setupLabels) {
186
+ const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-2YOKZ2AK.js");
187
+ await setupLabelsCommand2();
188
+ }
189
+ }
190
+
191
+ // src/lib/claude.ts
192
+ import { spawn } from "child_process";
193
+ import { execa } from "execa";
194
+ async function invokeClaude(options) {
195
+ const args = ["--print"];
196
+ if (options.permissionMode) {
197
+ args.push("--permission-mode", options.permissionMode);
198
+ }
199
+ args.push(options.prompt);
200
+ if (options.printOutput) {
201
+ const subprocess = execa("claude", args, {
202
+ stdio: "inherit"
203
+ });
204
+ await subprocess;
205
+ return "";
206
+ } else if (options.streamOutput) {
207
+ return new Promise((resolve, reject) => {
208
+ const child = spawn("claude", args, {
209
+ stdio: ["inherit", "pipe", "pipe"]
210
+ });
211
+ let output = "";
212
+ child.stdout.on("data", (chunk) => {
213
+ const text = chunk.toString();
214
+ output += text;
215
+ process.stdout.write(text);
216
+ });
217
+ child.stderr.on("data", (chunk) => {
218
+ process.stderr.write(chunk);
219
+ });
220
+ child.on("close", (code) => {
221
+ if (code === 0) {
222
+ resolve(output);
223
+ } else {
224
+ reject(new Error(`Claude exited with code ${code}`));
225
+ }
226
+ });
227
+ child.on("error", reject);
228
+ });
229
+ } else {
230
+ const { stdout } = await execa("claude", args);
231
+ return stdout;
232
+ }
233
+ }
234
+ async function invokeClaudeInteractive(prompt, config) {
235
+ const args = ["--permission-mode", config.claude.permission_mode, prompt];
236
+ return execa("claude", args, {
237
+ stdio: "inherit"
238
+ });
239
+ }
240
+ function buildTicketPrompt(description, agentInstructions) {
241
+ const basePrompt = `You are creating a GitHub issue for a software project following an AI-assisted development workflow.
242
+
243
+ User Request: ${description}
244
+
245
+ ${agentInstructions ? `Project-Specific Instructions:
246
+ ${agentInstructions}
247
+
248
+ ` : ""}
249
+
250
+ Create a detailed GitHub issue following this exact template:
251
+
252
+ ## Description
253
+ [Clear user-facing description of what needs to be done]
254
+
255
+ ## Technical Context
256
+ **Type:** feature | fix | refactor | chore | docs | test
257
+ **Category:** ui | api | database | workers | shared | testing | infra
258
+ **Priority:** critical | high | medium | low
259
+ **Risk:** low | medium | high
260
+
261
+ ### Architecture Notes
262
+ - [Relevant patterns to follow]
263
+ - [Related systems affected]
264
+ - [Constraints or invariants]
265
+
266
+ ## Implementation Steps
267
+ - [ ] Step 1: Specific technical task
268
+ - [ ] Step 2: Specific technical task
269
+ - [ ] Step 3: Specific technical task
270
+
271
+ ## Testing Requirements
272
+ - **Unit tests:** [What to test]
273
+ - **Integration tests:** [What to test if applicable]
274
+ - **Manual verification:** [What to check]
275
+
276
+ ## Acceptance Criteria
277
+ - [ ] Criterion 1
278
+ - [ ] Criterion 2
279
+
280
+ ---
281
+ IMPORTANT: After the issue content, on a new line, output ONLY the following metadata in this exact format:
282
+ META:type=<type>,priority=<priority>,risk=<risk>,area=<area>
283
+
284
+ Example: META:type=feature,priority=high,risk=low,area=ui`;
285
+ return basePrompt;
286
+ }
287
+ function buildImplementationPrompt(issue, agentInstructions, progressContent, config) {
288
+ return `GitHub Issue #${issue.number}: ${issue.title}
289
+
290
+ ${issue.body}
291
+
292
+ ${agentInstructions ? `## Project-Specific Instructions
293
+ ${agentInstructions}
294
+
295
+ ` : ""}
296
+ ${progressContent ? `## Previous Progress
297
+ ${progressContent}
298
+
299
+ ` : ""}
300
+
301
+ ## Your Task
302
+
303
+ 1. **Implement the feature/fix** following patterns from the project's AGENT.md or codebase conventions
304
+ 2. **Add unit tests** for any new functionality
305
+ 3. **Run validation** before committing:
306
+ ${config.validation.map((cmd) => ` - ${cmd}`).join("\n")}
307
+ 4. **Make an atomic commit** with a clear message following conventional commits format:
308
+ - Use format: <type>: <description>
309
+ - Include "Completed GitHub issue #${issue.number}" in body
310
+ - End with: Co-Authored-By: Claude <noreply@anthropic.com>
311
+ 5. **Do NOT push** - the user will review and push manually
312
+
313
+ Focus on clean, minimal implementation. Don't over-engineer.`;
314
+ }
315
+ function buildPrPrompt(issue, commits, diffSummary) {
316
+ return `Generate a pull request description for the following changes.
317
+
318
+ ${issue ? `## Related Issue
319
+ #${issue.number}: ${issue.title}
320
+
321
+ ${issue.body}
322
+
323
+ ` : ""}
324
+
325
+ ## Commits
326
+ ${commits.map((c) => `- ${c}`).join("\n")}
327
+
328
+ ## Changed Files
329
+ ${diffSummary}
330
+
331
+ Generate a PR description in this format:
332
+
333
+ ## Summary
334
+ - [1-3 bullet points summarizing the changes]
335
+
336
+ ## Test Plan
337
+ - [ ] [Testing steps]
338
+
339
+ ${issue ? `Closes #${issue.number}` : ""}
340
+
341
+ Only output the PR description, nothing else.`;
342
+ }
343
+ function parseTicketMeta(output) {
344
+ const metaMatch = output.match(
345
+ /META:type=(\w+),priority=(\w+),risk=(\w+),area=(\w+)/
346
+ );
347
+ if (!metaMatch) {
348
+ return null;
349
+ }
350
+ return {
351
+ type: metaMatch[1],
352
+ priority: metaMatch[2],
353
+ risk: metaMatch[3],
354
+ area: metaMatch[4]
355
+ };
356
+ }
357
+ function extractIssueBody(output) {
358
+ return output.replace(/\n?META:type=\w+,priority=\w+,risk=\w+,area=\w+\s*$/, "").trim();
359
+ }
360
+
361
+ // src/commands/create.ts
362
+ async function createCommand(description) {
363
+ logger.bold("Creating AI-enhanced ticket...");
364
+ logger.newline();
365
+ const [ghAuth, claudeOk] = await Promise.all([checkGhAuth(), checkClaudeCli()]);
366
+ if (!ghAuth) {
367
+ logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
368
+ process.exit(1);
369
+ }
370
+ if (!claudeOk) {
371
+ logger.error("Claude CLI not found. Please install claude CLI first.");
372
+ process.exit(1);
373
+ }
374
+ const agentInstructions = loadAgentInstructions();
375
+ const prompt = buildTicketPrompt(description, agentInstructions);
376
+ let claudeOutput;
377
+ try {
378
+ logger.info("Generating ticket with Claude...");
379
+ logger.newline();
380
+ claudeOutput = await invokeClaude({ prompt, streamOutput: true });
381
+ logger.newline();
382
+ } catch (error) {
383
+ logger.error(`Claude invocation failed: ${error}`);
384
+ return;
385
+ }
386
+ const meta = parseTicketMeta(claudeOutput);
387
+ if (!meta) {
388
+ logger.warning("Could not parse metadata from Claude output. Using defaults.");
389
+ }
390
+ const finalMeta = meta || {
391
+ type: "feature",
392
+ priority: "medium",
393
+ risk: "low",
394
+ area: "shared"
395
+ };
396
+ const issueBody = extractIssueBody(claudeOutput);
397
+ const title = description.length > 60 ? description.slice(0, 57) + "..." : description;
398
+ const labels = buildIssueLabels(finalMeta);
399
+ let issueNumber;
400
+ try {
401
+ issueNumber = await withSpinner("Creating GitHub issue...", async () => {
402
+ return createIssue({
403
+ title,
404
+ body: issueBody,
405
+ labels
406
+ });
407
+ });
408
+ } catch (error) {
409
+ logger.error(`Failed to create issue: ${error}`);
410
+ return;
411
+ }
412
+ logger.newline();
413
+ logger.success(`Created issue ${colors.issue(`#${issueNumber}`)}`);
414
+ logger.newline();
415
+ logger.box("Issue Created", `Issue: ${colors.issue(`#${issueNumber}`)}
416
+ Type: ${colors.label(`type:${finalMeta.type}`)}
417
+ Priority: ${colors.label(`priority:${finalMeta.priority}`)}
418
+ Risk: ${colors.label(`risk:${finalMeta.risk}`)}
419
+ Area: ${colors.label(`area:${finalMeta.area}`)}
420
+
421
+ Next steps:
422
+ 1. Review the issue on GitHub
423
+ 2. Run ${colors.command(`gent run ${issueNumber}`)} to implement`);
424
+ }
425
+
426
+ // src/commands/list.ts
427
+ import chalk from "chalk";
428
+ async function listCommand(options) {
429
+ const isAuthed = await checkGhAuth();
430
+ if (!isAuthed) {
431
+ logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
432
+ process.exit(1);
433
+ }
434
+ const config = loadConfig();
435
+ const workflowLabels = getWorkflowLabels(config);
436
+ const labels = [];
437
+ if (options.label) {
438
+ labels.push(options.label);
439
+ }
440
+ if (options.status && options.status !== "all") {
441
+ switch (options.status) {
442
+ case "ready":
443
+ labels.push(workflowLabels.ready);
444
+ break;
445
+ case "in-progress":
446
+ labels.push(workflowLabels.inProgress);
447
+ break;
448
+ case "completed":
449
+ labels.push(workflowLabels.completed);
450
+ break;
451
+ case "blocked":
452
+ labels.push(workflowLabels.blocked);
453
+ break;
454
+ }
455
+ } else if (!options.status) {
456
+ labels.push(workflowLabels.ready);
457
+ }
458
+ let issues;
459
+ try {
460
+ issues = await listIssues({
461
+ labels: labels.length > 0 ? labels : void 0,
462
+ state: "open",
463
+ limit: options.limit || 20
464
+ });
465
+ } catch (error) {
466
+ logger.error(`Failed to fetch issues: ${error}`);
467
+ return;
468
+ }
469
+ if (issues.length === 0) {
470
+ logger.info("No issues found matching the criteria.");
471
+ return;
472
+ }
473
+ sortByPriority(issues);
474
+ logger.bold(`Found ${issues.length} issue(s):`);
475
+ logger.newline();
476
+ for (const issue of issues) {
477
+ const type = extractTypeFromLabels(issue.labels);
478
+ const priority = extractPriorityFromLabels(issue.labels);
479
+ const status = getIssueStatus(issue.labels, workflowLabels);
480
+ const priorityColor = getPriorityColor(priority);
481
+ const statusColor = getStatusColor(status);
482
+ console.log(
483
+ ` ${colors.issue(`#${issue.number.toString().padStart(4)}`)} ${priorityColor(`[${priority}]`.padEnd(10))} ${statusColor(`[${status}]`.padEnd(14))} ${colors.label(`[${type}]`.padEnd(10))} ` + issue.title.slice(0, 50) + (issue.title.length > 50 ? "..." : "")
484
+ );
485
+ }
486
+ logger.newline();
487
+ logger.dim(`Run ${colors.command("gent run <issue-number>")} to implement an issue`);
488
+ logger.dim(`Run ${colors.command("gent run --auto")} to auto-select highest priority`);
489
+ }
490
+ function getIssueStatus(labels, workflowLabels) {
491
+ if (labels.includes(workflowLabels.ready)) return "ready";
492
+ if (labels.includes(workflowLabels.inProgress)) return "in-progress";
493
+ if (labels.includes(workflowLabels.completed)) return "completed";
494
+ if (labels.includes(workflowLabels.blocked)) return "blocked";
495
+ return "unknown";
496
+ }
497
+ function getPriorityColor(priority) {
498
+ switch (priority) {
499
+ case "critical":
500
+ return chalk.red;
501
+ case "high":
502
+ return chalk.yellow;
503
+ case "medium":
504
+ return chalk.blue;
505
+ case "low":
506
+ return chalk.green;
507
+ default:
508
+ return chalk.gray;
509
+ }
510
+ }
511
+ function getStatusColor(status) {
512
+ switch (status) {
513
+ case "ready":
514
+ return chalk.green;
515
+ case "in-progress":
516
+ return chalk.yellow;
517
+ case "completed":
518
+ return chalk.blue;
519
+ case "blocked":
520
+ return chalk.red;
521
+ default:
522
+ return chalk.gray;
523
+ }
524
+ }
525
+
526
+ // src/commands/run.ts
527
+ import inquirer2 from "inquirer";
528
+
529
+ // src/lib/git.ts
530
+ import { execa as execa2 } from "execa";
531
+ async function getCurrentBranch() {
532
+ const { stdout } = await execa2("git", ["branch", "--show-current"]);
533
+ return stdout.trim();
534
+ }
535
+ async function isOnMainBranch() {
536
+ const branch = await getCurrentBranch();
537
+ return branch === "main" || branch === "master";
538
+ }
539
+ async function getDefaultBranch() {
540
+ try {
541
+ const { stdout } = await execa2("git", [
542
+ "symbolic-ref",
543
+ "refs/remotes/origin/HEAD"
544
+ ]);
545
+ return stdout.trim().replace("refs/remotes/origin/", "");
546
+ } catch {
547
+ try {
548
+ await execa2("git", ["rev-parse", "--verify", "main"]);
549
+ return "main";
550
+ } catch {
551
+ return "master";
552
+ }
553
+ }
554
+ }
555
+ async function branchExists(name) {
556
+ try {
557
+ await execa2("git", ["rev-parse", "--verify", name]);
558
+ return true;
559
+ } catch {
560
+ return false;
561
+ }
562
+ }
563
+ async function createBranch(name, from) {
564
+ if (from) {
565
+ await execa2("git", ["checkout", "-b", name, from]);
566
+ } else {
567
+ await execa2("git", ["checkout", "-b", name]);
568
+ }
569
+ }
570
+ async function checkoutBranch(name) {
571
+ await execa2("git", ["checkout", name]);
572
+ }
573
+ async function hasUncommittedChanges() {
574
+ const { stdout } = await execa2("git", ["status", "--porcelain"]);
575
+ return stdout.trim().length > 0;
576
+ }
577
+ async function getUnpushedCommits() {
578
+ try {
579
+ const { stdout } = await execa2("git", [
580
+ "log",
581
+ "@{u}..HEAD",
582
+ "--oneline"
583
+ ]);
584
+ return stdout.trim().length > 0;
585
+ } catch {
586
+ return true;
587
+ }
588
+ }
589
+ async function pushBranch(branch) {
590
+ const branchName = branch || await getCurrentBranch();
591
+ await execa2("git", ["push", "-u", "origin", branchName]);
592
+ }
593
+ async function getAuthorInitials() {
594
+ try {
595
+ const { stdout } = await execa2("git", ["config", "user.initials"]);
596
+ if (stdout.trim()) {
597
+ return stdout.trim();
598
+ }
599
+ } catch {
600
+ }
601
+ try {
602
+ const { stdout } = await execa2("git", ["config", "user.name"]);
603
+ const name = stdout.trim();
604
+ if (name) {
605
+ const parts = name.split(/\s+/);
606
+ return parts.map((p) => p[0]?.toLowerCase() || "").join("");
607
+ }
608
+ } catch {
609
+ }
610
+ return "dev";
611
+ }
612
+ async function getCommitsSinceBase(base = "main") {
613
+ try {
614
+ const { stdout } = await execa2("git", [
615
+ "log",
616
+ `${base}..HEAD`,
617
+ "--pretty=format:%s"
618
+ ]);
619
+ return stdout.trim().split("\n").filter(Boolean);
620
+ } catch {
621
+ return [];
622
+ }
623
+ }
624
+ async function getDiffSummary(base = "main") {
625
+ try {
626
+ const { stdout } = await execa2("git", [
627
+ "diff",
628
+ `${base}...HEAD`,
629
+ "--stat"
630
+ ]);
631
+ return stdout.trim();
632
+ } catch {
633
+ return "";
634
+ }
635
+ }
636
+
637
+ // src/lib/branch.ts
638
+ async function generateBranchName(config, issueNumber, issueTitle, type) {
639
+ const author = await resolveAuthor(config);
640
+ const slug = sanitizeSlug(issueTitle);
641
+ return config.branch.pattern.replace("{author}", author).replace("{type}", type).replace("{issue}", String(issueNumber)).replace("{slug}", slug);
642
+ }
643
+ async function resolveAuthor(config) {
644
+ switch (config.branch.author_source) {
645
+ case "env": {
646
+ const envValue = process.env[config.branch.author_env_var];
647
+ if (envValue) {
648
+ return envValue;
649
+ }
650
+ return getAuthorInitials();
651
+ }
652
+ case "git":
653
+ default:
654
+ return getAuthorInitials();
655
+ }
656
+ }
657
+ function parseBranchName(branchName) {
658
+ const pattern1 = /^([^/]+)\/(feature|fix|refactor|chore|docs|test)-(\d+)-(.+)$/;
659
+ const match1 = branchName.match(pattern1);
660
+ if (match1) {
661
+ return {
662
+ name: branchName,
663
+ author: match1[1],
664
+ type: match1[2],
665
+ issueNumber: parseInt(match1[3], 10),
666
+ slug: match1[4]
667
+ };
668
+ }
669
+ const pattern2 = /^(feature|fix|refactor|chore|docs|test)\/(\d+)-(.+)$/;
670
+ const match2 = branchName.match(pattern2);
671
+ if (match2) {
672
+ return {
673
+ name: branchName,
674
+ author: "",
675
+ type: match2[1],
676
+ issueNumber: parseInt(match2[2], 10),
677
+ slug: match2[3]
678
+ };
679
+ }
680
+ const pattern3 = /^(\d+)-(.+)$/;
681
+ const match3 = branchName.match(pattern3);
682
+ if (match3) {
683
+ return {
684
+ name: branchName,
685
+ author: "",
686
+ type: "feature",
687
+ issueNumber: parseInt(match3[1], 10),
688
+ slug: match3[2]
689
+ };
690
+ }
691
+ const issueMatch = branchName.match(/(\d+)/);
692
+ if (issueMatch) {
693
+ return {
694
+ name: branchName,
695
+ author: "",
696
+ type: "feature",
697
+ issueNumber: parseInt(issueMatch[1], 10),
698
+ slug: branchName
699
+ };
700
+ }
701
+ return null;
702
+ }
703
+ function extractIssueNumber(branchName) {
704
+ const info = parseBranchName(branchName);
705
+ return info?.issueNumber ?? null;
706
+ }
707
+
708
+ // src/commands/run.ts
709
+ async function runCommand(issueNumberArg, options) {
710
+ logger.bold("Running AI implementation workflow...");
711
+ logger.newline();
712
+ const [ghAuth, claudeOk] = await Promise.all([checkGhAuth(), checkClaudeCli()]);
713
+ if (!ghAuth) {
714
+ logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
715
+ process.exit(1);
716
+ }
717
+ if (!claudeOk) {
718
+ logger.error("Claude CLI not found. Please install claude CLI first.");
719
+ process.exit(1);
720
+ }
721
+ const hasChanges = await hasUncommittedChanges();
722
+ if (hasChanges) {
723
+ logger.warning("You have uncommitted changes.");
724
+ const { proceed } = await inquirer2.prompt([
725
+ {
726
+ type: "confirm",
727
+ name: "proceed",
728
+ message: "Continue anyway?",
729
+ default: false
730
+ }
731
+ ]);
732
+ if (!proceed) {
733
+ logger.info("Aborting. Please commit or stash your changes first.");
734
+ process.exit(0);
735
+ }
736
+ }
737
+ const config = loadConfig();
738
+ const workflowLabels = getWorkflowLabels(config);
739
+ let issueNumber;
740
+ if (options.auto) {
741
+ const autoIssue = await autoSelectIssue(workflowLabels.ready);
742
+ if (!autoIssue) {
743
+ logger.error("No ai-ready issues found.");
744
+ process.exit(1);
745
+ }
746
+ issueNumber = autoIssue.number;
747
+ logger.info(`Auto-selected: ${colors.issue(`#${issueNumber}`)} - ${autoIssue.title}`);
748
+ } else if (issueNumberArg) {
749
+ if (!isValidIssueNumber(issueNumberArg)) {
750
+ logger.error("Invalid issue number.");
751
+ process.exit(1);
752
+ }
753
+ issueNumber = parseInt(issueNumberArg, 10);
754
+ } else {
755
+ logger.error("Please provide an issue number or use --auto");
756
+ process.exit(1);
757
+ }
758
+ let issue;
759
+ try {
760
+ issue = await withSpinner("Fetching issue...", async () => {
761
+ return getIssue(issueNumber);
762
+ });
763
+ } catch (error) {
764
+ logger.error(`Failed to fetch issue #${issueNumber}: ${error}`);
765
+ return;
766
+ }
767
+ if (!issue.labels.includes(workflowLabels.ready)) {
768
+ logger.warning(`Issue #${issueNumber} does not have the '${workflowLabels.ready}' label.`);
769
+ const { proceed } = await inquirer2.prompt([
770
+ {
771
+ type: "confirm",
772
+ name: "proceed",
773
+ message: "Continue anyway?",
774
+ default: false
775
+ }
776
+ ]);
777
+ if (!proceed) {
778
+ return;
779
+ }
780
+ }
781
+ logger.newline();
782
+ logger.box("Issue Details", `#${issue.number}: ${issue.title}
783
+ Labels: ${issue.labels.join(", ")}`);
784
+ logger.newline();
785
+ const type = extractTypeFromLabels(issue.labels);
786
+ const branchName = await generateBranchName(config, issueNumber, issue.title, type);
787
+ const currentBranch = await getCurrentBranch();
788
+ const onMain = await isOnMainBranch();
789
+ if (await branchExists(branchName)) {
790
+ logger.info(`Branch ${colors.branch(branchName)} already exists.`);
791
+ const { action } = await inquirer2.prompt([
792
+ {
793
+ type: "list",
794
+ name: "action",
795
+ message: "What would you like to do?",
796
+ choices: [
797
+ { name: "Continue on existing branch", value: "continue" },
798
+ { name: "Delete and recreate branch", value: "recreate" },
799
+ { name: "Cancel", value: "cancel" }
800
+ ]
801
+ }
802
+ ]);
803
+ if (action === "cancel") {
804
+ return;
805
+ } else if (action === "continue") {
806
+ await checkoutBranch(branchName);
807
+ } else {
808
+ await checkoutBranch(branchName);
809
+ }
810
+ } else {
811
+ if (!onMain) {
812
+ logger.warning(`Not on main branch (currently on ${colors.branch(currentBranch)}).`);
813
+ const { fromMain } = await inquirer2.prompt([
814
+ {
815
+ type: "confirm",
816
+ name: "fromMain",
817
+ message: "Create branch from main instead?",
818
+ default: true
819
+ }
820
+ ]);
821
+ if (fromMain) {
822
+ await createBranch(branchName, "main");
823
+ } else {
824
+ await createBranch(branchName);
825
+ }
826
+ } else {
827
+ await createBranch(branchName);
828
+ }
829
+ logger.success(`Created branch ${colors.branch(branchName)}`);
830
+ }
831
+ try {
832
+ await updateIssueLabels(issueNumber, {
833
+ add: [workflowLabels.inProgress],
834
+ remove: [workflowLabels.ready]
835
+ });
836
+ logger.success(`Updated issue labels: ${colors.label(workflowLabels.ready)} \u2192 ${colors.label(workflowLabels.inProgress)}`);
837
+ } catch (error) {
838
+ logger.warning(`Failed to update labels: ${error}`);
839
+ }
840
+ const agentInstructions = loadAgentInstructions();
841
+ const progressContent = readProgress(config);
842
+ const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config);
843
+ logger.newline();
844
+ logger.info("Starting Claude implementation session...");
845
+ logger.dim("Claude will implement the feature and create a commit.");
846
+ logger.dim("Review the changes before pushing.");
847
+ logger.newline();
848
+ try {
849
+ await invokeClaudeInteractive(prompt, config);
850
+ } catch (error) {
851
+ logger.error(`Claude session failed: ${error}`);
852
+ }
853
+ logger.newline();
854
+ logger.success("Claude session completed.");
855
+ try {
856
+ await updateIssueLabels(issueNumber, {
857
+ add: [workflowLabels.completed],
858
+ remove: [workflowLabels.inProgress]
859
+ });
860
+ logger.success(`Updated labels: ${colors.label(workflowLabels.inProgress)} \u2192 ${colors.label(workflowLabels.completed)}`);
861
+ } catch (error) {
862
+ logger.warning(`Failed to update labels: ${error}`);
863
+ }
864
+ try {
865
+ await addIssueComment(
866
+ issueNumber,
867
+ `AI implementation completed on branch \`${branchName}\`.
868
+
869
+ Please review the changes and create a PR when ready.`
870
+ );
871
+ logger.success("Posted completion comment to issue");
872
+ } catch (error) {
873
+ logger.warning(`Failed to post comment: ${error}`);
874
+ }
875
+ logger.newline();
876
+ logger.box("Next Steps", `1. Review changes: ${colors.command("git diff HEAD~1")}
877
+ 2. Run tests: ${colors.command("npm test")}
878
+ 3. Push branch: ${colors.command("git push -u origin " + branchName)}
879
+ 4. Create PR: ${colors.command("gent pr")}`);
880
+ }
881
+ async function autoSelectIssue(readyLabel) {
882
+ let issues = await listIssues({
883
+ labels: [readyLabel, "priority:critical"],
884
+ state: "open",
885
+ limit: 1
886
+ });
887
+ if (issues.length > 0) return issues[0];
888
+ issues = await listIssues({
889
+ labels: [readyLabel, "priority:high"],
890
+ state: "open",
891
+ limit: 1
892
+ });
893
+ if (issues.length > 0) return issues[0];
894
+ issues = await listIssues({
895
+ labels: [readyLabel],
896
+ state: "open",
897
+ limit: 10
898
+ });
899
+ if (issues.length === 0) return null;
900
+ sortByPriority(issues);
901
+ return issues[0];
902
+ }
903
+
904
+ // src/commands/pr.ts
905
+ import inquirer3 from "inquirer";
906
+ async function prCommand(options) {
907
+ logger.bold("Creating AI-enhanced pull request...");
908
+ logger.newline();
909
+ const [ghAuth, claudeOk] = await Promise.all([checkGhAuth(), checkClaudeCli()]);
910
+ if (!ghAuth) {
911
+ logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
912
+ process.exit(1);
913
+ }
914
+ if (!claudeOk) {
915
+ logger.error("Claude CLI not found. Please install claude CLI first.");
916
+ process.exit(1);
917
+ }
918
+ if (await isOnMainBranch()) {
919
+ logger.error("Cannot create PR from main/master branch.");
920
+ process.exit(1);
921
+ }
922
+ const existingPr = await getPrForBranch();
923
+ if (existingPr) {
924
+ logger.warning(`A PR already exists for this branch: ${colors.url(existingPr.url)}`);
925
+ return;
926
+ }
927
+ const currentBranch = await getCurrentBranch();
928
+ const baseBranch = await getDefaultBranch();
929
+ logger.info(`Branch: ${colors.branch(currentBranch)}`);
930
+ logger.info(`Base: ${colors.branch(baseBranch)}`);
931
+ const hasUnpushed = await getUnpushedCommits();
932
+ if (hasUnpushed) {
933
+ logger.warning("Branch has unpushed commits.");
934
+ const { push } = await inquirer3.prompt([
935
+ {
936
+ type: "confirm",
937
+ name: "push",
938
+ message: "Push to remote before creating PR?",
939
+ default: true
940
+ }
941
+ ]);
942
+ if (push) {
943
+ await withSpinner("Pushing branch...", async () => {
944
+ await pushBranch();
945
+ });
946
+ logger.success("Branch pushed");
947
+ }
948
+ }
949
+ const issueNumber = extractIssueNumber(currentBranch);
950
+ let issue = null;
951
+ if (issueNumber) {
952
+ try {
953
+ issue = await getIssue(issueNumber);
954
+ logger.info(`Linked issue: ${colors.issue(`#${issueNumber}`)} - ${issue.title}`);
955
+ } catch {
956
+ logger.warning(`Could not fetch issue #${issueNumber}`);
957
+ }
958
+ } else {
959
+ logger.warning("Could not extract issue number from branch name.");
960
+ }
961
+ const commits = await getCommitsSinceBase(baseBranch);
962
+ const diffSummary = await getDiffSummary(baseBranch);
963
+ if (commits.length === 0) {
964
+ logger.error("No commits found since base branch.");
965
+ return;
966
+ }
967
+ logger.info(`Commits: ${commits.length}`);
968
+ logger.newline();
969
+ const prompt = buildPrPrompt(issue, commits, diffSummary);
970
+ let prBody;
971
+ try {
972
+ logger.info("Generating PR description with Claude...");
973
+ logger.newline();
974
+ prBody = await invokeClaude({ prompt, streamOutput: true });
975
+ logger.newline();
976
+ } catch (error) {
977
+ logger.warning(`Claude invocation failed: ${error}`);
978
+ prBody = generateFallbackBody(issue, commits);
979
+ }
980
+ const prTitle = issue?.title || commits[0] || currentBranch;
981
+ let prUrl;
982
+ try {
983
+ prUrl = await withSpinner("Creating pull request...", async () => {
984
+ return createPullRequest({
985
+ title: prTitle,
986
+ body: prBody,
987
+ base: baseBranch,
988
+ draft: options.draft
989
+ });
990
+ });
991
+ } catch (error) {
992
+ logger.error(`Failed to create PR: ${error}`);
993
+ return;
994
+ }
995
+ if (issueNumber) {
996
+ try {
997
+ const user = await getCurrentUser();
998
+ await assignIssue(issueNumber, user);
999
+ logger.success(`Assigned issue #${issueNumber} to ${user}`);
1000
+ } catch {
1001
+ }
1002
+ }
1003
+ logger.newline();
1004
+ logger.success(`Pull request created!`);
1005
+ logger.newline();
1006
+ logger.highlight(prUrl);
1007
+ logger.newline();
1008
+ if (options.draft) {
1009
+ logger.dim("Created as draft. Mark as ready for review when done.");
1010
+ }
1011
+ }
1012
+ function generateFallbackBody(issue, commits) {
1013
+ let body = "## Summary\n\n";
1014
+ if (issue) {
1015
+ body += `Implements #${issue.number}: ${issue.title}
1016
+
1017
+ `;
1018
+ }
1019
+ body += "## Changes\n\n";
1020
+ for (const commit of commits.slice(0, 10)) {
1021
+ body += `- ${commit}
1022
+ `;
1023
+ }
1024
+ body += "\n## Test Plan\n\n- [ ] Tests pass\n- [ ] Manual verification\n\n";
1025
+ if (issue) {
1026
+ body += `Closes #${issue.number}
1027
+ `;
1028
+ }
1029
+ return body;
1030
+ }
1031
+
1032
+ // src/commands/status.ts
1033
+ async function statusCommand() {
1034
+ logger.bold("Gent Workflow Status");
1035
+ logger.newline();
1036
+ const gitRepo = await checkGitRepo();
1037
+ if (!gitRepo) {
1038
+ logger.error("Not a git repository.");
1039
+ process.exit(1);
1040
+ }
1041
+ const config = loadConfig();
1042
+ const workflowLabels = getWorkflowLabels(config);
1043
+ logger.bold("Configuration:");
1044
+ if (configExists()) {
1045
+ logger.success(" .gent.yml found");
1046
+ } else {
1047
+ logger.warning(" .gent.yml not found - using defaults");
1048
+ }
1049
+ if (progressExists(config)) {
1050
+ const progress = readProgress(config);
1051
+ const lines = progress.split("\n").length;
1052
+ logger.success(` ${config.progress.file} found (${lines} lines)`);
1053
+ } else {
1054
+ logger.warning(` ${config.progress.file} not found`);
1055
+ }
1056
+ logger.newline();
1057
+ logger.bold("Prerequisites:");
1058
+ const ghAuth = await checkGhAuth();
1059
+ if (ghAuth) {
1060
+ logger.success(" GitHub CLI authenticated");
1061
+ } else {
1062
+ logger.error(" GitHub CLI not authenticated");
1063
+ }
1064
+ const claudeOk = await checkClaudeCli();
1065
+ if (claudeOk) {
1066
+ logger.success(" Claude CLI available");
1067
+ } else {
1068
+ logger.error(" Claude CLI not found");
1069
+ }
1070
+ logger.newline();
1071
+ logger.bold("Git Status:");
1072
+ const currentBranch = await getCurrentBranch();
1073
+ const onMain = await isOnMainBranch();
1074
+ const uncommitted = await hasUncommittedChanges();
1075
+ const baseBranch = await getDefaultBranch();
1076
+ logger.info(` Branch: ${colors.branch(currentBranch)}`);
1077
+ if (onMain) {
1078
+ logger.info(" On main branch - ready to start new work");
1079
+ } else {
1080
+ const branchInfo = parseBranchName(currentBranch);
1081
+ if (branchInfo) {
1082
+ logger.info(` Issue: ${colors.issue(`#${branchInfo.issueNumber}`)}`);
1083
+ logger.info(` Type: ${branchInfo.type}`);
1084
+ }
1085
+ const commits = await getCommitsSinceBase(baseBranch);
1086
+ logger.info(` Commits ahead of ${baseBranch}: ${commits.length}`);
1087
+ const unpushed = await getUnpushedCommits();
1088
+ if (unpushed) {
1089
+ logger.warning(" Has unpushed commits");
1090
+ } else {
1091
+ logger.success(" Up to date with remote");
1092
+ }
1093
+ }
1094
+ if (uncommitted) {
1095
+ logger.warning(" Has uncommitted changes");
1096
+ }
1097
+ logger.newline();
1098
+ if (!onMain) {
1099
+ const issueNumber = extractIssueNumber(currentBranch);
1100
+ if (issueNumber) {
1101
+ logger.bold("Linked Issue:");
1102
+ try {
1103
+ const issue = await getIssue(issueNumber);
1104
+ logger.info(` #${issue.number}: ${issue.title}`);
1105
+ logger.info(` State: ${issue.state}`);
1106
+ logger.info(` Labels: ${issue.labels.join(", ")}`);
1107
+ if (issue.labels.includes(workflowLabels.ready)) {
1108
+ logger.info(` Workflow: ${colors.label("ai-ready")}`);
1109
+ } else if (issue.labels.includes(workflowLabels.inProgress)) {
1110
+ logger.info(` Workflow: ${colors.label("ai-in-progress")}`);
1111
+ } else if (issue.labels.includes(workflowLabels.completed)) {
1112
+ logger.info(` Workflow: ${colors.label("ai-completed")}`);
1113
+ } else if (issue.labels.includes(workflowLabels.blocked)) {
1114
+ logger.info(` Workflow: ${colors.label("ai-blocked")}`);
1115
+ }
1116
+ } catch {
1117
+ logger.warning(` Could not fetch issue #${issueNumber}`);
1118
+ }
1119
+ logger.newline();
1120
+ }
1121
+ logger.bold("Pull Request:");
1122
+ const pr = await getPrForBranch();
1123
+ if (pr) {
1124
+ logger.success(` PR #${pr.number} exists`);
1125
+ logger.info(` ${colors.url(pr.url)}`);
1126
+ } else {
1127
+ logger.info(" No PR created yet");
1128
+ logger.dim(` Run ${colors.command("gent pr")} to create one`);
1129
+ }
1130
+ logger.newline();
1131
+ }
1132
+ logger.bold("Suggested Actions:");
1133
+ if (onMain) {
1134
+ logger.list([
1135
+ `${colors.command("gent list")} - View ai-ready issues`,
1136
+ `${colors.command("gent run --auto")} - Start working on highest priority issue`,
1137
+ `${colors.command("gent create <description>")} - Create a new ticket`
1138
+ ]);
1139
+ } else {
1140
+ const pr = await getPrForBranch();
1141
+ if (!pr) {
1142
+ logger.list([
1143
+ `${colors.command("gent pr")} - Create a pull request`,
1144
+ `${colors.command("git push")} - Push your changes`
1145
+ ]);
1146
+ } else {
1147
+ logger.list([
1148
+ `Review and merge your PR`,
1149
+ `${colors.command("git checkout main")} - Return to main branch`
1150
+ ]);
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ // src/index.ts
1156
+ var program = new Command();
1157
+ program.name("gent").description("AI-powered GitHub workflow CLI - leverage Claude AI to create tickets, implement features, and manage PRs").version("0.1.0");
1158
+ program.command("init").description("Initialize gent workflow in current repository").option("-f, --force", "Overwrite existing configuration").action(async (options) => {
1159
+ await initCommand(options);
1160
+ });
1161
+ program.command("setup-labels").description("Setup GitHub labels for AI workflow").action(async () => {
1162
+ await setupLabelsCommand();
1163
+ });
1164
+ program.command("create <description>").description("Create an AI-enhanced GitHub issue").action(async (description) => {
1165
+ await createCommand(description);
1166
+ });
1167
+ 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) => {
1168
+ await listCommand({
1169
+ label: options.label,
1170
+ status: options.status,
1171
+ limit: parseInt(options.limit, 10)
1172
+ });
1173
+ });
1174
+ program.command("run [issue-number]").description("Run Claude to implement a GitHub issue").option("-a, --auto", "Auto-select highest priority ai-ready issue").action(async (issueNumber, options) => {
1175
+ await runCommand(issueNumber, { auto: options.auto });
1176
+ });
1177
+ program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").action(async (options) => {
1178
+ await prCommand({ draft: options.draft });
1179
+ });
1180
+ program.command("status").description("Show current workflow status").action(async () => {
1181
+ await statusCommand();
1182
+ });
1183
+ program.parse();
1184
+ //# sourceMappingURL=index.js.map