@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.
@@ -0,0 +1,667 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/logger.ts
4
+ import chalk from "chalk";
5
+ var logger = {
6
+ info: (message) => console.log(chalk.blue("\u2139"), message),
7
+ success: (message) => console.log(chalk.green("\u2713"), message),
8
+ warning: (message) => console.log(chalk.yellow("\u26A0"), message),
9
+ error: (message) => console.log(chalk.red("\u2717"), message),
10
+ debug: (message) => {
11
+ if (process.env.DEBUG) {
12
+ console.log(chalk.gray("\u22EF"), message);
13
+ }
14
+ },
15
+ dim: (message) => console.log(chalk.dim(message)),
16
+ bold: (message) => console.log(chalk.bold(message)),
17
+ highlight: (message) => console.log(chalk.cyan(message)),
18
+ box: (title, content) => {
19
+ const lines = content.split("\n");
20
+ const maxLen = Math.max(title.length, ...lines.map((l) => l.length)) + 4;
21
+ const border = "\u2500".repeat(maxLen);
22
+ console.log(chalk.dim(`\u250C${border}\u2510`));
23
+ console.log(chalk.dim("\u2502"), chalk.bold(title.padEnd(maxLen - 2)), chalk.dim("\u2502"));
24
+ console.log(chalk.dim(`\u251C${border}\u2524`));
25
+ for (const line of lines) {
26
+ console.log(chalk.dim("\u2502"), line.padEnd(maxLen - 2), chalk.dim("\u2502"));
27
+ }
28
+ console.log(chalk.dim(`\u2514${border}\u2518`));
29
+ },
30
+ list: (items, bullet = "\u2022") => {
31
+ for (const item of items) {
32
+ console.log(chalk.dim(bullet), item);
33
+ }
34
+ },
35
+ newline: () => console.log()
36
+ };
37
+ var colors = {
38
+ issue: chalk.cyan,
39
+ branch: chalk.magenta,
40
+ label: chalk.yellow,
41
+ file: chalk.green,
42
+ command: chalk.blue,
43
+ url: chalk.underline.blue
44
+ };
45
+
46
+ // src/utils/spinner.ts
47
+ import ora from "ora";
48
+ function createSpinner(text) {
49
+ return ora({
50
+ text,
51
+ spinner: "dots"
52
+ });
53
+ }
54
+ async function withSpinner(text, fn) {
55
+ const spinner = createSpinner(text);
56
+ spinner.start();
57
+ try {
58
+ const result = await fn();
59
+ spinner.succeed();
60
+ return result;
61
+ } catch (error) {
62
+ spinner.fail();
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ // src/lib/config.ts
68
+ import { existsSync, readFileSync } from "fs";
69
+ import { join } from "path";
70
+ import { parse as parseYaml } from "yaml";
71
+ var DEFAULT_CONFIG = {
72
+ version: 1,
73
+ github: {
74
+ labels: {
75
+ workflow: {
76
+ ready: "ai-ready",
77
+ in_progress: "ai-in-progress",
78
+ completed: "ai-completed",
79
+ blocked: "ai-blocked"
80
+ },
81
+ types: ["feature", "fix", "refactor", "chore", "docs", "test"],
82
+ priorities: ["critical", "high", "medium", "low"],
83
+ risks: ["low", "medium", "high"],
84
+ areas: ["ui", "api", "database", "workers", "shared", "testing", "infra"]
85
+ }
86
+ },
87
+ branch: {
88
+ pattern: "{author}/{type}-{issue}-{slug}",
89
+ author_source: "git",
90
+ author_env_var: "GENT_AUTHOR"
91
+ },
92
+ progress: {
93
+ file: "progress.txt",
94
+ archive_threshold: 500,
95
+ archive_dir: ".gent/archive"
96
+ },
97
+ claude: {
98
+ permission_mode: "acceptEdits",
99
+ agent_file: "AGENT.md"
100
+ },
101
+ validation: ["npm run typecheck", "npm run lint", "npm run test"]
102
+ };
103
+ function getConfigPath(cwd = process.cwd()) {
104
+ return join(cwd, ".gent.yml");
105
+ }
106
+ function getAgentPath(cwd = process.cwd()) {
107
+ const config = loadConfig(cwd);
108
+ const agentPath = join(cwd, config.claude.agent_file);
109
+ return existsSync(agentPath) ? agentPath : null;
110
+ }
111
+ function loadConfig(cwd = process.cwd()) {
112
+ const configPath = getConfigPath(cwd);
113
+ if (!existsSync(configPath)) {
114
+ return DEFAULT_CONFIG;
115
+ }
116
+ try {
117
+ const content = readFileSync(configPath, "utf-8");
118
+ const userConfig = parseYaml(content);
119
+ return mergeConfig(DEFAULT_CONFIG, userConfig);
120
+ } catch {
121
+ return DEFAULT_CONFIG;
122
+ }
123
+ }
124
+ function loadAgentInstructions(cwd = process.cwd()) {
125
+ const agentPath = getAgentPath(cwd);
126
+ if (!agentPath) {
127
+ return null;
128
+ }
129
+ try {
130
+ return readFileSync(agentPath, "utf-8");
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ function configExists(cwd = process.cwd()) {
136
+ return existsSync(getConfigPath(cwd));
137
+ }
138
+ function mergeConfig(defaults, user) {
139
+ return {
140
+ version: user.version ?? defaults.version,
141
+ github: {
142
+ labels: {
143
+ workflow: {
144
+ ...defaults.github.labels.workflow,
145
+ ...user.github?.labels?.workflow
146
+ },
147
+ types: user.github?.labels?.types ?? defaults.github.labels.types,
148
+ priorities: user.github?.labels?.priorities ?? defaults.github.labels.priorities,
149
+ risks: user.github?.labels?.risks ?? defaults.github.labels.risks,
150
+ areas: user.github?.labels?.areas ?? defaults.github.labels.areas
151
+ }
152
+ },
153
+ branch: {
154
+ ...defaults.branch,
155
+ ...user.branch
156
+ },
157
+ progress: {
158
+ ...defaults.progress,
159
+ ...user.progress
160
+ },
161
+ claude: {
162
+ ...defaults.claude,
163
+ ...user.claude
164
+ },
165
+ validation: user.validation ?? defaults.validation
166
+ };
167
+ }
168
+ function generateDefaultConfig() {
169
+ return `# Gent Configuration
170
+ # See https://github.com/rotorsoft/gent for documentation
171
+ version: 1
172
+
173
+ # GitHub settings
174
+ github:
175
+ labels:
176
+ workflow:
177
+ ready: "ai-ready"
178
+ in_progress: "ai-in-progress"
179
+ completed: "ai-completed"
180
+ blocked: "ai-blocked"
181
+ types:
182
+ - feature
183
+ - fix
184
+ - refactor
185
+ - chore
186
+ - docs
187
+ - test
188
+ priorities:
189
+ - critical
190
+ - high
191
+ - medium
192
+ - low
193
+ risks:
194
+ - low
195
+ - medium
196
+ - high
197
+ areas:
198
+ - ui
199
+ - api
200
+ - database
201
+ - workers
202
+ - shared
203
+ - testing
204
+ - infra
205
+
206
+ # Branch naming convention
207
+ branch:
208
+ pattern: "{author}/{type}-{issue}-{slug}"
209
+ author_source: "git" # git | env | prompt
210
+ author_env_var: "GENT_AUTHOR"
211
+
212
+ # Progress tracking
213
+ progress:
214
+ file: "progress.txt"
215
+ archive_threshold: 500
216
+ archive_dir: ".gent/archive"
217
+
218
+ # Claude settings
219
+ claude:
220
+ permission_mode: "acceptEdits"
221
+ agent_file: "AGENT.md"
222
+
223
+ # Validation commands (run before commit)
224
+ validation:
225
+ - "npm run typecheck"
226
+ - "npm run lint"
227
+ - "npm run test"
228
+ `;
229
+ }
230
+
231
+ // src/types/index.ts
232
+ var DEFAULT_LABELS = {
233
+ workflow: [
234
+ {
235
+ name: "ai-ready",
236
+ color: "0E8A16",
237
+ description: "Issue ready for AI implementation"
238
+ },
239
+ {
240
+ name: "ai-in-progress",
241
+ color: "FFA500",
242
+ description: "AI currently working on this"
243
+ },
244
+ {
245
+ name: "ai-completed",
246
+ color: "1D76DB",
247
+ description: "AI done, needs human review"
248
+ },
249
+ {
250
+ name: "ai-blocked",
251
+ color: "D93F0B",
252
+ description: "AI couldn't complete, needs help"
253
+ }
254
+ ],
255
+ priority: [
256
+ {
257
+ name: "priority:critical",
258
+ color: "B60205",
259
+ description: "Blocking production"
260
+ },
261
+ {
262
+ name: "priority:high",
263
+ color: "D93F0B",
264
+ description: "Important features/bugs"
265
+ },
266
+ {
267
+ name: "priority:medium",
268
+ color: "FBCA04",
269
+ description: "Nice-to-have improvements"
270
+ },
271
+ { name: "priority:low", color: "0E8A16", description: "Minor tweaks" }
272
+ ],
273
+ risk: [
274
+ {
275
+ name: "risk:low",
276
+ color: "C2E0C6",
277
+ description: "UI changes, tests, non-critical"
278
+ },
279
+ {
280
+ name: "risk:medium",
281
+ color: "FEF2C0",
282
+ description: "API changes, new features"
283
+ },
284
+ {
285
+ name: "risk:high",
286
+ color: "F9D0C4",
287
+ description: "Migrations, auth, security"
288
+ }
289
+ ],
290
+ type: [
291
+ { name: "type:feature", color: "1D76DB", description: "New feature" },
292
+ { name: "type:fix", color: "D73A4A", description: "Bug fix" },
293
+ {
294
+ name: "type:refactor",
295
+ color: "5319E7",
296
+ description: "Code improvement"
297
+ },
298
+ { name: "type:chore", color: "FEF2C0", description: "Maintenance" },
299
+ { name: "type:docs", color: "0075CA", description: "Documentation" },
300
+ { name: "type:test", color: "D4C5F9", description: "Testing" }
301
+ ],
302
+ area: [
303
+ { name: "area:ui", color: "C5DEF5", description: "User interface" },
304
+ { name: "area:api", color: "D4C5F9", description: "API/Backend" },
305
+ { name: "area:database", color: "FEF2C0", description: "Database/Models" },
306
+ {
307
+ name: "area:workers",
308
+ color: "F9D0C4",
309
+ description: "Background workers"
310
+ },
311
+ { name: "area:shared", color: "C2E0C6", description: "Shared libraries" },
312
+ { name: "area:testing", color: "E99695", description: "Test infrastructure" },
313
+ { name: "area:infra", color: "BFD4F2", description: "Infrastructure/DevOps" }
314
+ ]
315
+ };
316
+
317
+ // src/lib/labels.ts
318
+ function getAllLabels(config) {
319
+ const labels = [];
320
+ labels.push(...DEFAULT_LABELS.workflow);
321
+ for (const priority of config.github.labels.priorities) {
322
+ const defaultLabel = DEFAULT_LABELS.priority.find(
323
+ (l) => l.name === `priority:${priority}`
324
+ );
325
+ if (defaultLabel) {
326
+ labels.push(defaultLabel);
327
+ } else {
328
+ labels.push({
329
+ name: `priority:${priority}`,
330
+ color: "FBCA04",
331
+ description: `Priority: ${priority}`
332
+ });
333
+ }
334
+ }
335
+ for (const risk of config.github.labels.risks) {
336
+ const defaultLabel = DEFAULT_LABELS.risk.find(
337
+ (l) => l.name === `risk:${risk}`
338
+ );
339
+ if (defaultLabel) {
340
+ labels.push(defaultLabel);
341
+ } else {
342
+ labels.push({
343
+ name: `risk:${risk}`,
344
+ color: "FEF2C0",
345
+ description: `Risk: ${risk}`
346
+ });
347
+ }
348
+ }
349
+ for (const type of config.github.labels.types) {
350
+ const defaultLabel = DEFAULT_LABELS.type.find(
351
+ (l) => l.name === `type:${type}`
352
+ );
353
+ if (defaultLabel) {
354
+ labels.push(defaultLabel);
355
+ } else {
356
+ labels.push({
357
+ name: `type:${type}`,
358
+ color: "1D76DB",
359
+ description: `Type: ${type}`
360
+ });
361
+ }
362
+ }
363
+ for (const area of config.github.labels.areas) {
364
+ const defaultLabel = DEFAULT_LABELS.area.find(
365
+ (l) => l.name === `area:${area}`
366
+ );
367
+ if (defaultLabel) {
368
+ labels.push(defaultLabel);
369
+ } else {
370
+ labels.push({
371
+ name: `area:${area}`,
372
+ color: "C5DEF5",
373
+ description: `Area: ${area}`
374
+ });
375
+ }
376
+ }
377
+ return labels;
378
+ }
379
+ function getWorkflowLabels(config) {
380
+ return {
381
+ ready: config.github.labels.workflow.ready,
382
+ inProgress: config.github.labels.workflow.in_progress,
383
+ completed: config.github.labels.workflow.completed,
384
+ blocked: config.github.labels.workflow.blocked
385
+ };
386
+ }
387
+ function buildIssueLabels(meta) {
388
+ return [
389
+ "ai-ready",
390
+ `type:${meta.type}`,
391
+ `priority:${meta.priority}`,
392
+ `risk:${meta.risk}`,
393
+ `area:${meta.area}`
394
+ ];
395
+ }
396
+ function extractTypeFromLabels(labels) {
397
+ for (const label of labels) {
398
+ if (label.startsWith("type:")) {
399
+ return label.replace("type:", "");
400
+ }
401
+ }
402
+ return "feature";
403
+ }
404
+ function extractPriorityFromLabels(labels) {
405
+ for (const label of labels) {
406
+ if (label.startsWith("priority:")) {
407
+ return label.replace("priority:", "");
408
+ }
409
+ }
410
+ return "medium";
411
+ }
412
+ function sortByPriority(issues) {
413
+ const priorityOrder = ["critical", "high", "medium", "low"];
414
+ issues.sort((a, b) => {
415
+ const aPriority = extractPriorityFromLabels(a.labels);
416
+ const bPriority = extractPriorityFromLabels(b.labels);
417
+ return priorityOrder.indexOf(aPriority) - priorityOrder.indexOf(bPriority);
418
+ });
419
+ }
420
+
421
+ // src/lib/github.ts
422
+ import { execa } from "execa";
423
+ async function getIssue(issueNumber) {
424
+ const { stdout } = await execa("gh", [
425
+ "issue",
426
+ "view",
427
+ String(issueNumber),
428
+ "--json",
429
+ "number,title,body,labels,state,assignees,url"
430
+ ]);
431
+ const data = JSON.parse(stdout);
432
+ return {
433
+ number: data.number,
434
+ title: data.title,
435
+ body: data.body || "",
436
+ labels: data.labels.map((l) => l.name),
437
+ state: data.state.toLowerCase(),
438
+ assignee: data.assignees?.[0]?.login,
439
+ url: data.url
440
+ };
441
+ }
442
+ async function listIssues(options) {
443
+ const args = ["issue", "list", "--json", "number,title,body,labels,state,url"];
444
+ if (options.labels?.length) {
445
+ args.push("--label", options.labels.join(","));
446
+ }
447
+ if (options.state) {
448
+ args.push("--state", options.state);
449
+ }
450
+ args.push("--limit", String(options.limit || 50));
451
+ const { stdout } = await execa("gh", args);
452
+ const data = JSON.parse(stdout);
453
+ return data.map(
454
+ (d) => ({
455
+ number: d.number,
456
+ title: d.title,
457
+ body: d.body || "",
458
+ labels: d.labels.map((l) => l.name),
459
+ state: d.state.toLowerCase(),
460
+ url: d.url
461
+ })
462
+ );
463
+ }
464
+ async function createIssue(options) {
465
+ const args = ["issue", "create", "--title", options.title, "--body", options.body];
466
+ if (options.labels?.length) {
467
+ args.push("--label", options.labels.join(","));
468
+ }
469
+ const { stdout } = await execa("gh", args);
470
+ const match = stdout.match(/\/issues\/(\d+)/);
471
+ if (!match) {
472
+ throw new Error("Failed to extract issue number from gh output");
473
+ }
474
+ return parseInt(match[1], 10);
475
+ }
476
+ async function updateIssueLabels(issueNumber, options) {
477
+ const promises = [];
478
+ if (options.add?.length) {
479
+ promises.push(
480
+ execa("gh", [
481
+ "issue",
482
+ "edit",
483
+ String(issueNumber),
484
+ "--add-label",
485
+ options.add.join(",")
486
+ ])
487
+ );
488
+ }
489
+ if (options.remove?.length) {
490
+ promises.push(
491
+ execa("gh", [
492
+ "issue",
493
+ "edit",
494
+ String(issueNumber),
495
+ "--remove-label",
496
+ options.remove.join(",")
497
+ ])
498
+ );
499
+ }
500
+ await Promise.all(promises);
501
+ }
502
+ async function addIssueComment(issueNumber, body) {
503
+ await execa("gh", ["issue", "comment", String(issueNumber), "--body", body]);
504
+ }
505
+ async function assignIssue(issueNumber, assignee) {
506
+ await execa("gh", [
507
+ "issue",
508
+ "edit",
509
+ String(issueNumber),
510
+ "--add-assignee",
511
+ assignee
512
+ ]);
513
+ }
514
+ async function createLabel(label) {
515
+ try {
516
+ await execa("gh", [
517
+ "label",
518
+ "create",
519
+ label.name,
520
+ "--color",
521
+ label.color,
522
+ "--description",
523
+ label.description || "",
524
+ "--force"
525
+ ]);
526
+ } catch {
527
+ }
528
+ }
529
+ async function createPullRequest(options) {
530
+ const args = [
531
+ "pr",
532
+ "create",
533
+ "--title",
534
+ options.title,
535
+ "--body",
536
+ options.body,
537
+ "--assignee",
538
+ "@me"
539
+ ];
540
+ if (options.base) {
541
+ args.push("--base", options.base);
542
+ }
543
+ if (options.draft) {
544
+ args.push("--draft");
545
+ }
546
+ const { stdout } = await execa("gh", args);
547
+ return stdout.trim();
548
+ }
549
+ async function getPrForBranch() {
550
+ try {
551
+ const { stdout } = await execa("gh", [
552
+ "pr",
553
+ "view",
554
+ "--json",
555
+ "number,url"
556
+ ]);
557
+ const data = JSON.parse(stdout);
558
+ return { number: data.number, url: data.url };
559
+ } catch {
560
+ return null;
561
+ }
562
+ }
563
+ async function getCurrentUser() {
564
+ const { stdout } = await execa("gh", ["api", "user", "--jq", ".login"]);
565
+ return stdout.trim();
566
+ }
567
+
568
+ // src/utils/validators.ts
569
+ import { execa as execa2 } from "execa";
570
+ async function checkGhAuth() {
571
+ try {
572
+ await execa2("gh", ["auth", "status"]);
573
+ return true;
574
+ } catch {
575
+ return false;
576
+ }
577
+ }
578
+ async function checkClaudeCli() {
579
+ try {
580
+ await execa2("claude", ["--version"]);
581
+ return true;
582
+ } catch {
583
+ return false;
584
+ }
585
+ }
586
+ async function checkGitRepo() {
587
+ try {
588
+ await execa2("git", ["rev-parse", "--git-dir"]);
589
+ return true;
590
+ } catch {
591
+ return false;
592
+ }
593
+ }
594
+ function isValidIssueNumber(value) {
595
+ const num = parseInt(value, 10);
596
+ return !isNaN(num) && num > 0;
597
+ }
598
+ function sanitizeSlug(title, maxLength = 40) {
599
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength);
600
+ }
601
+
602
+ // src/commands/setup-labels.ts
603
+ async function setupLabelsCommand() {
604
+ logger.bold("Setting up GitHub labels...");
605
+ logger.newline();
606
+ const isAuthed = await checkGhAuth();
607
+ if (!isAuthed) {
608
+ logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
609
+ process.exit(1);
610
+ }
611
+ const config = loadConfig();
612
+ const labels = getAllLabels(config);
613
+ logger.info(`Creating ${labels.length} labels...`);
614
+ logger.newline();
615
+ let created = 0;
616
+ let failed = 0;
617
+ for (const label of labels) {
618
+ try {
619
+ await withSpinner(`Creating ${colors.label(label.name)}`, async () => {
620
+ await createLabel(label);
621
+ });
622
+ created++;
623
+ } catch (error) {
624
+ logger.error(`Failed to create ${label.name}: ${error}`);
625
+ failed++;
626
+ }
627
+ }
628
+ logger.newline();
629
+ logger.success(`Created ${created} labels`);
630
+ if (failed > 0) {
631
+ logger.warning(`Failed to create ${failed} labels`);
632
+ }
633
+ logger.newline();
634
+ logger.info("Labels are ready. You can now create AI-ready issues.");
635
+ }
636
+
637
+ export {
638
+ logger,
639
+ colors,
640
+ checkGhAuth,
641
+ checkClaudeCli,
642
+ checkGitRepo,
643
+ isValidIssueNumber,
644
+ sanitizeSlug,
645
+ getConfigPath,
646
+ loadConfig,
647
+ loadAgentInstructions,
648
+ configExists,
649
+ generateDefaultConfig,
650
+ withSpinner,
651
+ getWorkflowLabels,
652
+ buildIssueLabels,
653
+ extractTypeFromLabels,
654
+ extractPriorityFromLabels,
655
+ sortByPriority,
656
+ getIssue,
657
+ listIssues,
658
+ createIssue,
659
+ updateIssueLabels,
660
+ addIssueComment,
661
+ assignIssue,
662
+ createPullRequest,
663
+ getPrForBranch,
664
+ getCurrentUser,
665
+ setupLabelsCommand
666
+ };
667
+ //# sourceMappingURL=chunk-NRTQPDZB.js.map