@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/README.md +397 -0
- package/dist/chunk-NRTQPDZB.js +667 -0
- package/dist/chunk-NRTQPDZB.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1184 -0
- package/dist/index.js.map +1 -0
- package/dist/setup-labels-2YOKZ2AK.js +8 -0
- package/dist/setup-labels-2YOKZ2AK.js.map +1 -0
- package/package.json +87 -0
- package/templates/.gent.yml +61 -0
- package/templates/AGENT.md +108 -0
- package/templates/issue-template.md +44 -0
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
|