@fractary/faber-cli 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,748 @@
1
+ /**
2
+ * Work subcommand - Work tracking operations
3
+ *
4
+ * Provides issue, comment, label, and milestone operations via @fractary/faber WorkManager.
5
+ */
6
+ import { Command } from 'commander';
7
+ import chalk from 'chalk';
8
+ import { promises as fs } from 'fs';
9
+ import path from 'path';
10
+ import { WorkManager } from '@fractary/faber';
11
+ import { parseValidInteger, parsePositiveInteger } from '../../utils/validation.js';
12
+ /**
13
+ * Create the work command tree
14
+ */
15
+ export function createWorkCommand() {
16
+ const work = new Command('work')
17
+ .description('Work item tracking operations');
18
+ // Issue operations
19
+ const issue = new Command('issue')
20
+ .description('Issue operations');
21
+ issue.addCommand(createIssueFetchCommand());
22
+ issue.addCommand(createIssueCreateCommand());
23
+ issue.addCommand(createIssueUpdateCommand());
24
+ issue.addCommand(createIssueCloseCommand());
25
+ issue.addCommand(createIssueReopenCommand());
26
+ issue.addCommand(createIssueAssignCommand());
27
+ issue.addCommand(createIssueClassifyCommand());
28
+ issue.addCommand(createIssueSearchCommand());
29
+ // Comment operations
30
+ const comment = new Command('comment')
31
+ .description('Comment operations');
32
+ comment.addCommand(createCommentCreateCommand());
33
+ comment.addCommand(createCommentListCommand());
34
+ // Label operations
35
+ const label = new Command('label')
36
+ .description('Label operations');
37
+ label.addCommand(createLabelAddCommand());
38
+ label.addCommand(createLabelRemoveCommand());
39
+ label.addCommand(createLabelListCommand());
40
+ // Milestone operations
41
+ const milestone = new Command('milestone')
42
+ .description('Milestone operations');
43
+ milestone.addCommand(createMilestoneCreateCommand());
44
+ milestone.addCommand(createMilestoneListCommand());
45
+ milestone.addCommand(createMilestoneSetCommand());
46
+ work.addCommand(issue);
47
+ work.addCommand(comment);
48
+ work.addCommand(label);
49
+ work.addCommand(milestone);
50
+ work.addCommand(createInitCommand());
51
+ return work;
52
+ }
53
+ // Issue Commands
54
+ function createIssueFetchCommand() {
55
+ return new Command('fetch')
56
+ .description('Fetch a work item by ID')
57
+ .argument('<number>', 'Issue number')
58
+ .option('--json', 'Output as JSON')
59
+ .option('--verbose', 'Show additional details')
60
+ .action(async (number, options) => {
61
+ try {
62
+ const workManager = new WorkManager();
63
+ const issue = await workManager.fetchIssue(parseValidInteger(number, 'issue number'));
64
+ if (options.json) {
65
+ console.log(JSON.stringify({ status: 'success', data: issue }, null, 2));
66
+ }
67
+ else {
68
+ console.log(chalk.bold(`#${issue.number}: ${issue.title}`));
69
+ console.log(chalk.gray(`State: ${issue.state}`));
70
+ if (issue.body) {
71
+ console.log('\n' + issue.body);
72
+ }
73
+ }
74
+ }
75
+ catch (error) {
76
+ handleWorkError(error, options);
77
+ }
78
+ });
79
+ }
80
+ function createIssueCreateCommand() {
81
+ return new Command('create')
82
+ .description('Create a new work item')
83
+ .requiredOption('--title <title>', 'Issue title')
84
+ .option('--body <body>', 'Issue body')
85
+ .option('--labels <labels>', 'Comma-separated labels')
86
+ .option('--assignees <assignees>', 'Comma-separated assignees')
87
+ .option('--json', 'Output as JSON')
88
+ .action(async (options) => {
89
+ try {
90
+ const workManager = new WorkManager();
91
+ const issue = await workManager.createIssue({
92
+ title: options.title,
93
+ body: options.body,
94
+ labels: options.labels?.split(',').map((l) => l.trim()),
95
+ assignees: options.assignees?.split(',').map((a) => a.trim()),
96
+ });
97
+ if (options.json) {
98
+ console.log(JSON.stringify({ status: 'success', data: issue }, null, 2));
99
+ }
100
+ else {
101
+ console.log(chalk.green(`✓ Created issue #${issue.number}: ${issue.title}`));
102
+ }
103
+ }
104
+ catch (error) {
105
+ handleWorkError(error, options);
106
+ }
107
+ });
108
+ }
109
+ function createIssueUpdateCommand() {
110
+ return new Command('update')
111
+ .description('Update a work item')
112
+ .argument('<number>', 'Issue number')
113
+ .option('--title <title>', 'New title')
114
+ .option('--body <body>', 'New body')
115
+ .option('--state <state>', 'New state (open, closed)')
116
+ .option('--json', 'Output as JSON')
117
+ .action(async (number, options) => {
118
+ try {
119
+ const workManager = new WorkManager();
120
+ const issue = await workManager.updateIssue(parseValidInteger(number, 'issue number'), {
121
+ title: options.title,
122
+ body: options.body,
123
+ state: options.state,
124
+ });
125
+ if (options.json) {
126
+ console.log(JSON.stringify({ status: 'success', data: issue }, null, 2));
127
+ }
128
+ else {
129
+ console.log(chalk.green(`✓ Updated issue #${issue.number}`));
130
+ }
131
+ }
132
+ catch (error) {
133
+ handleWorkError(error, options);
134
+ }
135
+ });
136
+ }
137
+ function createIssueCloseCommand() {
138
+ return new Command('close')
139
+ .description('Close a work item')
140
+ .argument('<number>', 'Issue number')
141
+ .option('--comment <text>', 'Add closing comment')
142
+ .option('--json', 'Output as JSON')
143
+ .action(async (number, options) => {
144
+ try {
145
+ const workManager = new WorkManager();
146
+ // Add comment if provided
147
+ if (options.comment) {
148
+ await workManager.createComment(parseValidInteger(number, 'issue number'), options.comment);
149
+ }
150
+ const issue = await workManager.closeIssue(parseValidInteger(number, 'issue number'));
151
+ if (options.json) {
152
+ console.log(JSON.stringify({ status: 'success', data: issue }, null, 2));
153
+ }
154
+ else {
155
+ console.log(chalk.green(`✓ Closed issue #${number}`));
156
+ }
157
+ }
158
+ catch (error) {
159
+ handleWorkError(error, options);
160
+ }
161
+ });
162
+ }
163
+ function createIssueSearchCommand() {
164
+ return new Command('search')
165
+ .description('Search work items')
166
+ .requiredOption('--query <query>', 'Search query')
167
+ .option('--state <state>', 'Filter by state (open, closed, all)', 'open')
168
+ .option('--labels <labels>', 'Filter by labels (comma-separated)')
169
+ .option('--limit <n>', 'Max results', '10')
170
+ .option('--json', 'Output as JSON')
171
+ .action(async (options) => {
172
+ try {
173
+ const workManager = new WorkManager();
174
+ const issues = await workManager.searchIssues(options.query, {
175
+ state: options.state,
176
+ labels: options.labels?.split(',').map((l) => l.trim()),
177
+ });
178
+ const limitedIssues = options.limit ? issues.slice(0, parsePositiveInteger(options.limit, 'limit')) : issues;
179
+ if (options.json) {
180
+ console.log(JSON.stringify({ status: 'success', data: limitedIssues }, null, 2));
181
+ }
182
+ else {
183
+ if (limitedIssues.length === 0) {
184
+ console.log(chalk.yellow('No issues found'));
185
+ }
186
+ else {
187
+ limitedIssues.forEach((issue) => {
188
+ console.log(`#${issue.number} ${issue.title} [${issue.state}]`);
189
+ });
190
+ }
191
+ }
192
+ }
193
+ catch (error) {
194
+ handleWorkError(error, options);
195
+ }
196
+ });
197
+ }
198
+ function createIssueReopenCommand() {
199
+ return new Command('reopen')
200
+ .description('Reopen a closed work item')
201
+ .argument('<number>', 'Issue number')
202
+ .option('--comment <text>', 'Add comment when reopening')
203
+ .option('--json', 'Output as JSON')
204
+ .action(async (number, options) => {
205
+ try {
206
+ const workManager = new WorkManager();
207
+ // Add comment if provided
208
+ if (options.comment) {
209
+ await workManager.createComment(parseValidInteger(number, 'issue number'), options.comment);
210
+ }
211
+ const issue = await workManager.reopenIssue(parseValidInteger(number, 'issue number'));
212
+ if (options.json) {
213
+ console.log(JSON.stringify({
214
+ status: 'success',
215
+ data: {
216
+ number: issue.number,
217
+ state: issue.state,
218
+ url: issue.url,
219
+ },
220
+ }, null, 2));
221
+ }
222
+ else {
223
+ console.log(chalk.green(`✓ Reopened issue #${number}`));
224
+ }
225
+ }
226
+ catch (error) {
227
+ handleWorkError(error, options);
228
+ }
229
+ });
230
+ }
231
+ function createIssueAssignCommand() {
232
+ return new Command('assign')
233
+ .description('Assign or unassign a work item')
234
+ .argument('<number>', 'Issue number')
235
+ .option('--user <username>', 'User to assign (use @me for self, omit to unassign)')
236
+ .option('--json', 'Output as JSON')
237
+ .action(async (number, options) => {
238
+ try {
239
+ const workManager = new WorkManager();
240
+ let issue;
241
+ if (options.user) {
242
+ issue = await workManager.assignIssue(parseValidInteger(number, 'issue number'), options.user);
243
+ }
244
+ else {
245
+ issue = await workManager.unassignIssue(parseValidInteger(number, 'issue number'));
246
+ }
247
+ if (options.json) {
248
+ console.log(JSON.stringify({
249
+ status: 'success',
250
+ data: {
251
+ number: issue.number,
252
+ assignees: issue.assignees || [],
253
+ url: issue.url,
254
+ },
255
+ }, null, 2));
256
+ }
257
+ else {
258
+ if (options.user) {
259
+ console.log(chalk.green(`✓ Assigned issue #${number} to ${options.user}`));
260
+ }
261
+ else {
262
+ console.log(chalk.green(`✓ Unassigned issue #${number}`));
263
+ }
264
+ }
265
+ }
266
+ catch (error) {
267
+ handleWorkError(error, options);
268
+ }
269
+ });
270
+ }
271
+ function createIssueClassifyCommand() {
272
+ return new Command('classify')
273
+ .description('Classify work item type (feature, bug, chore, patch)')
274
+ .argument('<number>', 'Issue number')
275
+ .option('--json', 'Output as JSON')
276
+ .action(async (number, options) => {
277
+ try {
278
+ const workManager = new WorkManager();
279
+ const issue = await workManager.fetchIssue(parseValidInteger(number, 'issue number'));
280
+ const result = classifyWorkType(issue);
281
+ if (options.json) {
282
+ console.log(JSON.stringify({
283
+ status: 'success',
284
+ data: {
285
+ number: parseValidInteger(number, 'issue number'),
286
+ work_type: result.work_type,
287
+ confidence: result.confidence,
288
+ signals: result.signals,
289
+ },
290
+ }, null, 2));
291
+ }
292
+ else {
293
+ console.log(result.work_type);
294
+ if (result.confidence < 0.5) {
295
+ console.log(chalk.red(`⚠ LOW CONFIDENCE: ${Math.round(result.confidence * 100)}% - review manually`));
296
+ }
297
+ else if (result.confidence < 0.8) {
298
+ console.log(chalk.yellow(`(confidence: ${Math.round(result.confidence * 100)}%)`));
299
+ }
300
+ }
301
+ }
302
+ catch (error) {
303
+ handleWorkError(error, options);
304
+ }
305
+ });
306
+ }
307
+ // Comment Commands
308
+ function createCommentCreateCommand() {
309
+ return new Command('create')
310
+ .description('Add a comment to an issue')
311
+ .argument('<issue_number>', 'Issue number')
312
+ .requiredOption('--body <text>', 'Comment body')
313
+ .option('--json', 'Output as JSON')
314
+ .action(async (issueNumber, options) => {
315
+ try {
316
+ const workManager = new WorkManager();
317
+ const comment = await workManager.createComment(parseValidInteger(issueNumber, 'issue number'), options.body);
318
+ if (options.json) {
319
+ console.log(JSON.stringify({ status: 'success', data: comment }, null, 2));
320
+ }
321
+ else {
322
+ console.log(chalk.green(`✓ Added comment to issue #${issueNumber}`));
323
+ }
324
+ }
325
+ catch (error) {
326
+ handleWorkError(error, options);
327
+ }
328
+ });
329
+ }
330
+ function createCommentListCommand() {
331
+ return new Command('list')
332
+ .description('List comments on an issue')
333
+ .argument('<issue_number>', 'Issue number')
334
+ .option('--limit <n>', 'Max results', '20')
335
+ .option('--json', 'Output as JSON')
336
+ .action(async (issueNumber, options) => {
337
+ try {
338
+ const workManager = new WorkManager();
339
+ const comments = await workManager.listComments(parseValidInteger(issueNumber, 'issue number'), {
340
+ limit: parsePositiveInteger(options.limit, 'limit'),
341
+ });
342
+ if (options.json) {
343
+ console.log(JSON.stringify({ status: 'success', data: comments }, null, 2));
344
+ }
345
+ else {
346
+ if (comments.length === 0) {
347
+ console.log(chalk.yellow('No comments found'));
348
+ }
349
+ else {
350
+ comments.forEach((comment) => {
351
+ console.log(chalk.gray(`[${comment.author}] ${comment.created_at}`));
352
+ console.log(comment.body);
353
+ console.log('');
354
+ });
355
+ }
356
+ }
357
+ }
358
+ catch (error) {
359
+ handleWorkError(error, options);
360
+ }
361
+ });
362
+ }
363
+ // Label Commands
364
+ function createLabelAddCommand() {
365
+ return new Command('add')
366
+ .description('Add labels to an issue')
367
+ .argument('<issue_number>', 'Issue number')
368
+ .requiredOption('--label <names>', 'Label name(s), comma-separated')
369
+ .option('--json', 'Output as JSON')
370
+ .action(async (issueNumber, options) => {
371
+ try {
372
+ const workManager = new WorkManager();
373
+ const labels = options.label.split(',').map((l) => l.trim());
374
+ const result = await workManager.addLabels(parseValidInteger(issueNumber, 'issue number'), labels);
375
+ if (options.json) {
376
+ console.log(JSON.stringify({ status: 'success', data: result }, null, 2));
377
+ }
378
+ else {
379
+ console.log(chalk.green(`✓ Added label(s) to issue #${issueNumber}`));
380
+ }
381
+ }
382
+ catch (error) {
383
+ handleWorkError(error, options);
384
+ }
385
+ });
386
+ }
387
+ function createLabelRemoveCommand() {
388
+ return new Command('remove')
389
+ .description('Remove labels from an issue')
390
+ .argument('<issue_number>', 'Issue number')
391
+ .requiredOption('--label <names>', 'Label name(s), comma-separated')
392
+ .option('--json', 'Output as JSON')
393
+ .action(async (issueNumber, options) => {
394
+ try {
395
+ const workManager = new WorkManager();
396
+ const labels = options.label.split(',').map((l) => l.trim());
397
+ await workManager.removeLabels(parseValidInteger(issueNumber, 'issue number'), labels);
398
+ if (options.json) {
399
+ console.log(JSON.stringify({ status: 'success', data: { removed: labels } }, null, 2));
400
+ }
401
+ else {
402
+ console.log(chalk.green(`✓ Removed label(s) from issue #${issueNumber}`));
403
+ }
404
+ }
405
+ catch (error) {
406
+ handleWorkError(error, options);
407
+ }
408
+ });
409
+ }
410
+ function createLabelListCommand() {
411
+ return new Command('list')
412
+ .description('List labels')
413
+ .option('--issue <number>', 'List labels for specific issue')
414
+ .option('--json', 'Output as JSON')
415
+ .action(async (options) => {
416
+ try {
417
+ const workManager = new WorkManager();
418
+ const labels = options.issue
419
+ ? await workManager.listLabels(parseValidInteger(options.issue, 'issue number'))
420
+ : await workManager.listLabels();
421
+ if (options.json) {
422
+ console.log(JSON.stringify({ status: 'success', data: labels }, null, 2));
423
+ }
424
+ else {
425
+ if (labels.length === 0) {
426
+ console.log(chalk.yellow('No labels found'));
427
+ }
428
+ else {
429
+ labels.forEach((label) => {
430
+ console.log(`- ${label.name}`);
431
+ });
432
+ }
433
+ }
434
+ }
435
+ catch (error) {
436
+ handleWorkError(error, options);
437
+ }
438
+ });
439
+ }
440
+ // Milestone Commands
441
+ function createMilestoneCreateCommand() {
442
+ return new Command('create')
443
+ .description('Create a milestone')
444
+ .requiredOption('--title <title>', 'Milestone title')
445
+ .option('--description <text>', 'Milestone description')
446
+ .option('--due-on <date>', 'Due date (ISO format)')
447
+ .option('--json', 'Output as JSON')
448
+ .action(async (options) => {
449
+ try {
450
+ const workManager = new WorkManager();
451
+ const milestone = await workManager.createMilestone({
452
+ title: options.title,
453
+ description: options.description,
454
+ due_on: options.dueOn,
455
+ });
456
+ if (options.json) {
457
+ console.log(JSON.stringify({ status: 'success', data: milestone }, null, 2));
458
+ }
459
+ else {
460
+ console.log(chalk.green(`✓ Created milestone: ${milestone.title}`));
461
+ }
462
+ }
463
+ catch (error) {
464
+ handleWorkError(error, options);
465
+ }
466
+ });
467
+ }
468
+ function createMilestoneListCommand() {
469
+ return new Command('list')
470
+ .description('List milestones')
471
+ .option('--state <state>', 'Filter by state (open, closed, all)', 'open')
472
+ .option('--json', 'Output as JSON')
473
+ .action(async (options) => {
474
+ try {
475
+ const workManager = new WorkManager();
476
+ const milestones = await workManager.listMilestones(options.state);
477
+ if (options.json) {
478
+ console.log(JSON.stringify({ status: 'success', data: milestones }, null, 2));
479
+ }
480
+ else {
481
+ if (milestones.length === 0) {
482
+ console.log(chalk.yellow('No milestones found'));
483
+ }
484
+ else {
485
+ milestones.forEach((ms) => {
486
+ console.log(`${ms.title} [${ms.state}]`);
487
+ });
488
+ }
489
+ }
490
+ }
491
+ catch (error) {
492
+ handleWorkError(error, options);
493
+ }
494
+ });
495
+ }
496
+ function createMilestoneSetCommand() {
497
+ return new Command('set')
498
+ .description('Set milestone on an issue')
499
+ .argument('<issue_number>', 'Issue number')
500
+ .requiredOption('--milestone <title>', 'Milestone title')
501
+ .option('--json', 'Output as JSON')
502
+ .action(async (issueNumber, options) => {
503
+ try {
504
+ const workManager = new WorkManager();
505
+ const issue = await workManager.setMilestone(parseValidInteger(issueNumber, 'issue number'), options.milestone);
506
+ if (options.json) {
507
+ console.log(JSON.stringify({ status: 'success', data: issue }, null, 2));
508
+ }
509
+ else {
510
+ console.log(chalk.green(`✓ Set milestone '${options.milestone}' on issue #${issueNumber}`));
511
+ }
512
+ }
513
+ catch (error) {
514
+ handleWorkError(error, options);
515
+ }
516
+ });
517
+ }
518
+ // Init Command
519
+ function createInitCommand() {
520
+ return new Command('init')
521
+ .description('Initialize work tracking configuration')
522
+ .option('--platform <name>', 'Platform: github, jira, linear (auto-detect if not specified)')
523
+ .option('--token <value>', 'API token (or use env var)')
524
+ .option('--project <key>', 'Project key for Jira/Linear')
525
+ .option('--yes', 'Accept defaults without prompting')
526
+ .option('--json', 'Output as JSON')
527
+ .action(async (options) => {
528
+ try {
529
+ const platform = options.platform || await detectPlatformFromGit();
530
+ const config = await buildWorkConfig(platform, options);
531
+ const configPath = await writeWorkConfig(config);
532
+ if (options.json) {
533
+ console.log(JSON.stringify({
534
+ status: 'success',
535
+ data: {
536
+ platform: config.work.platform,
537
+ config_path: configPath,
538
+ repository: config.work.repository
539
+ ? `${config.work.repository.owner}/${config.work.repository.name}`
540
+ : config.work.project,
541
+ },
542
+ }, null, 2));
543
+ }
544
+ else {
545
+ console.log(chalk.green(`✓ Work tracking initialized`));
546
+ console.log(chalk.gray(`Platform: ${config.work.platform}`));
547
+ console.log(chalk.gray(`Config: ${configPath}`));
548
+ }
549
+ }
550
+ catch (error) {
551
+ const message = error instanceof Error ? error.message : String(error);
552
+ if (options.json) {
553
+ console.error(JSON.stringify({
554
+ status: 'error',
555
+ error: { code: 'INIT_ERROR', message },
556
+ }));
557
+ }
558
+ else {
559
+ console.error(chalk.red('Error:'), message);
560
+ }
561
+ process.exit(1);
562
+ }
563
+ });
564
+ }
565
+ // Helper functions
566
+ async function detectPlatformFromGit() {
567
+ try {
568
+ const gitConfigPath = path.join(process.cwd(), '.git', 'config');
569
+ const gitConfig = await fs.readFile(gitConfigPath, 'utf-8');
570
+ const remoteMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(.+)/);
571
+ if (!remoteMatch) {
572
+ throw new Error('No origin remote found');
573
+ }
574
+ const remoteUrl = remoteMatch[1].trim();
575
+ if (remoteUrl.includes('github.com')) {
576
+ return 'github';
577
+ }
578
+ else if (remoteUrl.includes('gitlab.com')) {
579
+ return 'gitlab';
580
+ }
581
+ else if (remoteUrl.includes('bitbucket.org')) {
582
+ return 'bitbucket';
583
+ }
584
+ else if (remoteUrl.includes('atlassian.net')) {
585
+ return 'jira';
586
+ }
587
+ return 'github';
588
+ }
589
+ catch {
590
+ return 'github';
591
+ }
592
+ }
593
+ function parseGitRemote(url) {
594
+ const sshMatch = url.match(/@[^:]+:([^/]+)\/([^.]+)/);
595
+ if (sshMatch) {
596
+ return { owner: sshMatch[1], name: sshMatch[2] };
597
+ }
598
+ const httpsMatch = url.match(/https?:\/\/[^/]+\/([^/]+)\/([^/.]+)/);
599
+ if (httpsMatch) {
600
+ return { owner: httpsMatch[1], name: httpsMatch[2] };
601
+ }
602
+ return null;
603
+ }
604
+ async function buildWorkConfig(platform, options) {
605
+ const config = {
606
+ work: {
607
+ platform,
608
+ },
609
+ };
610
+ if (platform === 'github' || platform === 'gitlab' || platform === 'bitbucket') {
611
+ try {
612
+ const gitConfigPath = path.join(process.cwd(), '.git', 'config');
613
+ const gitConfig = await fs.readFile(gitConfigPath, 'utf-8');
614
+ const remoteMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(.+)/);
615
+ if (remoteMatch) {
616
+ const repoInfo = parseGitRemote(remoteMatch[1].trim());
617
+ if (repoInfo) {
618
+ config.work.repository = repoInfo;
619
+ }
620
+ }
621
+ }
622
+ catch {
623
+ // Ignore errors
624
+ }
625
+ }
626
+ else if (platform === 'jira' || platform === 'linear') {
627
+ if (options.project) {
628
+ config.work.project = options.project;
629
+ }
630
+ }
631
+ return config;
632
+ }
633
+ async function writeWorkConfig(config) {
634
+ const configDir = path.join(process.cwd(), '.fractary', 'faber');
635
+ const configPath = path.join(configDir, 'config.json');
636
+ await fs.mkdir(configDir, { recursive: true });
637
+ let existingConfig = {};
638
+ try {
639
+ const existing = await fs.readFile(configPath, 'utf-8');
640
+ existingConfig = JSON.parse(existing);
641
+ }
642
+ catch {
643
+ // No existing config
644
+ }
645
+ const mergedConfig = {
646
+ ...existingConfig,
647
+ ...config,
648
+ };
649
+ await fs.writeFile(configPath, JSON.stringify(mergedConfig, null, 2) + '\n');
650
+ return configPath;
651
+ }
652
+ function classifyWorkType(issue) {
653
+ const title = (issue.title || '').toLowerCase();
654
+ const labels = (issue.labels || []).map((l) => typeof l === 'string' ? l.toLowerCase() : l.name.toLowerCase());
655
+ const signals = {
656
+ labels: labels,
657
+ title_keywords: [],
658
+ has_bug_markers: false,
659
+ };
660
+ const labelScores = {
661
+ 'bug': { type: 'bug', score: 0.95 },
662
+ 'defect': { type: 'bug', score: 0.95 },
663
+ 'regression': { type: 'bug', score: 0.9 },
664
+ 'enhancement': { type: 'feature', score: 0.9 },
665
+ 'feature': { type: 'feature', score: 0.95 },
666
+ 'new feature': { type: 'feature', score: 0.95 },
667
+ 'chore': { type: 'chore', score: 0.9 },
668
+ 'maintenance': { type: 'chore', score: 0.85 },
669
+ 'dependencies': { type: 'chore', score: 0.8 },
670
+ 'hotfix': { type: 'patch', score: 0.95 },
671
+ 'urgent': { type: 'patch', score: 0.7 },
672
+ 'security': { type: 'patch', score: 0.85 },
673
+ 'critical': { type: 'patch', score: 0.8 },
674
+ };
675
+ for (const label of labels) {
676
+ if (labelScores[label]) {
677
+ return {
678
+ work_type: labelScores[label].type,
679
+ confidence: labelScores[label].score,
680
+ signals,
681
+ };
682
+ }
683
+ }
684
+ const bugKeywords = ['fix', 'bug', 'error', 'crash', 'broken', 'issue', 'problem'];
685
+ const featureKeywords = ['add', 'implement', 'new', 'create', 'feature', 'support'];
686
+ const choreKeywords = ['update', 'upgrade', 'refactor', 'clean', 'remove', 'deprecate', 'migrate'];
687
+ const patchKeywords = ['hotfix', 'urgent', 'critical', 'security'];
688
+ let workType = 'feature';
689
+ let confidence = 0.5;
690
+ for (const keyword of bugKeywords) {
691
+ if (title.includes(keyword)) {
692
+ signals.title_keywords.push(keyword);
693
+ signals.has_bug_markers = true;
694
+ workType = 'bug';
695
+ confidence = 0.7;
696
+ break;
697
+ }
698
+ }
699
+ for (const keyword of patchKeywords) {
700
+ if (title.includes(keyword)) {
701
+ signals.title_keywords.push(keyword);
702
+ workType = 'patch';
703
+ confidence = 0.75;
704
+ break;
705
+ }
706
+ }
707
+ if (workType !== 'patch') {
708
+ for (const keyword of featureKeywords) {
709
+ if (title.includes(keyword)) {
710
+ signals.title_keywords.push(keyword);
711
+ if (!signals.has_bug_markers) {
712
+ workType = 'feature';
713
+ confidence = 0.7;
714
+ }
715
+ break;
716
+ }
717
+ }
718
+ }
719
+ for (const keyword of choreKeywords) {
720
+ if (title.includes(keyword)) {
721
+ signals.title_keywords.push(keyword);
722
+ if (!signals.has_bug_markers && workType !== 'patch') {
723
+ workType = 'chore';
724
+ confidence = 0.65;
725
+ }
726
+ break;
727
+ }
728
+ }
729
+ return {
730
+ work_type: workType,
731
+ confidence,
732
+ signals,
733
+ };
734
+ }
735
+ // Error handling
736
+ function handleWorkError(error, options) {
737
+ const message = error instanceof Error ? error.message : String(error);
738
+ if (options.json) {
739
+ console.error(JSON.stringify({
740
+ status: 'error',
741
+ error: { code: 'WORK_ERROR', message },
742
+ }));
743
+ }
744
+ else {
745
+ console.error(chalk.red('Error:'), message);
746
+ }
747
+ process.exit(1);
748
+ }