@fractary/faber-cli 1.3.14 → 1.4.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/__mocks__/chalk.d.ts +16 -0
- package/dist/__mocks__/chalk.d.ts.map +1 -0
- package/dist/__mocks__/chalk.js +15 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +29 -2
- package/dist/commands/plan/index.d.ts.map +1 -1
- package/dist/commands/plan/index.js +117 -7
- package/dist/lib/github-app-auth.js +2 -2
- package/dist/lib/sdk-type-adapter.d.ts +2 -0
- package/dist/lib/sdk-type-adapter.d.ts.map +1 -1
- package/dist/lib/sdk-type-adapter.js +2 -0
- package/dist/types/config.d.ts +8 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/labels.d.ts +50 -0
- package/dist/utils/labels.d.ts.map +1 -0
- package/dist/utils/labels.js +182 -0
- package/dist/utils/sorting.d.ts +46 -0
- package/dist/utils/sorting.d.ts.map +1 -0
- package/dist/utils/sorting.js +89 -0
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock implementation of chalk for testing
|
|
3
|
+
*/
|
|
4
|
+
declare const mockChalk: {
|
|
5
|
+
green: (str: string) => string;
|
|
6
|
+
red: (str: string) => string;
|
|
7
|
+
yellow: (str: string) => string;
|
|
8
|
+
blue: (str: string) => string;
|
|
9
|
+
cyan: (str: string) => string;
|
|
10
|
+
gray: (str: string) => string;
|
|
11
|
+
grey: (str: string) => string;
|
|
12
|
+
bold: (str: string) => string;
|
|
13
|
+
dim: (str: string) => string;
|
|
14
|
+
};
|
|
15
|
+
export default mockChalk;
|
|
16
|
+
//# sourceMappingURL=chalk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chalk.d.ts","sourceRoot":"","sources":["../../src/__mocks__/chalk.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,QAAA,MAAM,SAAS;iBACA,MAAM;eACR,MAAM;kBACH,MAAM;gBACR,MAAM;gBACN,MAAM;gBACN,MAAM;gBACN,MAAM;gBACN,MAAM;eACP,MAAM;CAClB,CAAC;AAEF,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock implementation of chalk for testing
|
|
3
|
+
*/
|
|
4
|
+
const mockChalk = {
|
|
5
|
+
green: (str) => str,
|
|
6
|
+
red: (str) => str,
|
|
7
|
+
yellow: (str) => str,
|
|
8
|
+
blue: (str) => str,
|
|
9
|
+
cyan: (str) => str,
|
|
10
|
+
gray: (str) => str,
|
|
11
|
+
grey: (str) => str,
|
|
12
|
+
bold: (str) => str,
|
|
13
|
+
dim: (str) => str,
|
|
14
|
+
};
|
|
15
|
+
export default mockChalk;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,iBAAiB,IAAI,OAAO,CAkG3C"}
|
package/dist/commands/init.js
CHANGED
|
@@ -5,6 +5,8 @@ import { Command } from 'commander';
|
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
+
import { prompt } from '../utils/prompt.js';
|
|
9
|
+
import { createPriorityLabels, isGitHubCLIAvailable } from '../utils/labels.js';
|
|
8
10
|
export function createInitCommand() {
|
|
9
11
|
return new Command('workflow-init')
|
|
10
12
|
.description('Initialize a new FABER project')
|
|
@@ -44,20 +46,45 @@ logs/session-*.md
|
|
|
44
46
|
*.tmp
|
|
45
47
|
`;
|
|
46
48
|
await fs.writeFile(path.join(configDir, '.gitignore'), gitignore);
|
|
49
|
+
// Offer to create priority labels (if not in JSON mode)
|
|
50
|
+
let labelsCreated = false;
|
|
51
|
+
if (!options.json) {
|
|
52
|
+
const ghAvailable = await isGitHubCLIAvailable();
|
|
53
|
+
if (ghAvailable) {
|
|
54
|
+
console.log('');
|
|
55
|
+
const createLabels = await prompt('Create priority labels (priority-1 through priority-4) for backlog management? [Y/n]: ');
|
|
56
|
+
if (!createLabels || createLabels.toLowerCase() === 'y' || createLabels.toLowerCase() === 'yes') {
|
|
57
|
+
console.log(chalk.cyan('\n→ Creating priority labels...'));
|
|
58
|
+
const result = await createPriorityLabels('priority', false);
|
|
59
|
+
if (result.created.length > 0) {
|
|
60
|
+
labelsCreated = true;
|
|
61
|
+
}
|
|
62
|
+
if (result.errors.length > 0) {
|
|
63
|
+
console.log(chalk.yellow('\n⚠️ Some labels could not be created. You can create them manually later.'));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
47
68
|
if (options.json) {
|
|
48
69
|
console.log(JSON.stringify({
|
|
49
70
|
status: 'success',
|
|
50
|
-
data: { configPath, preset: options.preset },
|
|
71
|
+
data: { configPath, preset: options.preset, labelsCreated },
|
|
51
72
|
}, null, 2));
|
|
52
73
|
}
|
|
53
74
|
else {
|
|
54
|
-
console.log(chalk.green('✓ FABER initialized successfully'));
|
|
75
|
+
console.log(chalk.green('\n✓ FABER initialized successfully'));
|
|
55
76
|
console.log(chalk.gray(` Config: ${configPath}`));
|
|
56
77
|
console.log(chalk.gray(` Preset: ${options.preset}`));
|
|
78
|
+
if (labelsCreated) {
|
|
79
|
+
console.log(chalk.gray(' Priority labels: Created'));
|
|
80
|
+
}
|
|
57
81
|
console.log('\nNext steps:');
|
|
58
82
|
console.log(' 1. Configure work tracking: Edit .fractary/faber/config.json');
|
|
59
83
|
console.log(' 2. Start a workflow: fractary-faber run --work-id <issue-number>');
|
|
60
84
|
console.log(' 3. Check status: fractary-faber status');
|
|
85
|
+
if (labelsCreated) {
|
|
86
|
+
console.log(' 4. Use priority labels: gh issue edit <number> --add-label "priority-1"');
|
|
87
|
+
}
|
|
61
88
|
}
|
|
62
89
|
}
|
|
63
90
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/plan/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/plan/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqDpC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAqB3C"}
|
|
@@ -26,6 +26,9 @@ export function createPlanCommand() {
|
|
|
26
26
|
.option('--skip-confirm', 'Skip confirmation prompt (use with caution)')
|
|
27
27
|
.option('--output <format>', 'Output format: text|json|yaml', 'text')
|
|
28
28
|
.option('--json', 'Output as JSON (shorthand for --output json)')
|
|
29
|
+
.option('--limit <n>', 'Maximum number of issues to plan', parseInt)
|
|
30
|
+
.option('--order-by <strategy>', 'Order issues by: priority|created|updated (default: none)', 'none')
|
|
31
|
+
.option('--order-direction <dir>', 'Order direction: asc|desc (default: desc)', 'desc')
|
|
29
32
|
.action(async (options) => {
|
|
30
33
|
try {
|
|
31
34
|
await executePlanCommand(options);
|
|
@@ -39,7 +42,6 @@ export function createPlanCommand() {
|
|
|
39
42
|
* Main execution logic for plan command
|
|
40
43
|
*/
|
|
41
44
|
async function executePlanCommand(options) {
|
|
42
|
-
console.error('[DEBUG] Starting executePlanCommand');
|
|
43
45
|
const outputFormat = options.json ? 'json' : options.output || 'text';
|
|
44
46
|
// Validate arguments
|
|
45
47
|
if (!options.workId && !options.workLabel) {
|
|
@@ -48,14 +50,31 @@ async function executePlanCommand(options) {
|
|
|
48
50
|
if (options.workId && options.workLabel) {
|
|
49
51
|
throw new Error('Cannot use both --work-id and --work-label at the same time');
|
|
50
52
|
}
|
|
53
|
+
// Validate backlog management options
|
|
54
|
+
if (options.limit !== undefined) {
|
|
55
|
+
if (!Number.isInteger(options.limit) || options.limit <= 0) {
|
|
56
|
+
throw new Error('--limit must be a positive integer');
|
|
57
|
+
}
|
|
58
|
+
if (options.limit > 100) {
|
|
59
|
+
throw new Error('--limit cannot exceed 100');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (options.orderBy && options.orderBy !== 'none') {
|
|
63
|
+
const validOrderBy = ['priority', 'created', 'updated'];
|
|
64
|
+
if (!validOrderBy.includes(options.orderBy)) {
|
|
65
|
+
throw new Error(`--order-by must be one of: ${validOrderBy.join(', ')}, or 'none'`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (options.orderDirection) {
|
|
69
|
+
const validDirections = ['asc', 'desc'];
|
|
70
|
+
if (!validDirections.includes(options.orderDirection)) {
|
|
71
|
+
throw new Error(`--order-direction must be one of: ${validDirections.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
51
74
|
// Initialize clients
|
|
52
|
-
console.error('[DEBUG] Loading config...');
|
|
53
75
|
const config = await ConfigManager.load();
|
|
54
|
-
console.error('[DEBUG] Creating RepoClient...');
|
|
55
76
|
const repoClient = await RepoClient.create(config);
|
|
56
|
-
console.error('[DEBUG] Creating AnthropicClient...');
|
|
57
77
|
const anthropicClient = new AnthropicClient(config);
|
|
58
|
-
console.error('[DEBUG] Clients initialized');
|
|
59
78
|
if (outputFormat === 'text') {
|
|
60
79
|
console.log(chalk.blue('FABER CLI - Workflow Planning'));
|
|
61
80
|
console.log(chalk.gray('═'.repeat(50)));
|
|
@@ -116,6 +135,41 @@ async function executePlanCommand(options) {
|
|
|
116
135
|
}
|
|
117
136
|
return;
|
|
118
137
|
}
|
|
138
|
+
// Auto-create priority labels if using priority ordering and labels don't exist
|
|
139
|
+
if (options.orderBy === 'priority') {
|
|
140
|
+
const { ensurePriorityLabels } = await import('../../utils/labels.js');
|
|
141
|
+
const priorityLabelPrefix = config.backlog_management?.priority_config?.label_prefix || 'priority';
|
|
142
|
+
// Try to ensure labels exist (quiet mode - won't spam console)
|
|
143
|
+
const labelsEnsured = await ensurePriorityLabels(priorityLabelPrefix, true);
|
|
144
|
+
if (outputFormat === 'text' && labelsEnsured) {
|
|
145
|
+
console.log(chalk.gray(`\n→ Priority labels are ready (using prefix: ${priorityLabelPrefix})`));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Apply ordering if requested
|
|
149
|
+
if (options.orderBy && options.orderBy !== 'none') {
|
|
150
|
+
const { sortIssues } = await import('../../utils/sorting.js');
|
|
151
|
+
// Load priority label prefix from config (with fallback)
|
|
152
|
+
const priorityLabelPrefix = config.backlog_management?.priority_config?.label_prefix || 'priority';
|
|
153
|
+
const originalCount = issues.length;
|
|
154
|
+
issues = sortIssues(issues, {
|
|
155
|
+
orderBy: options.orderBy,
|
|
156
|
+
direction: (options.orderDirection || 'desc'),
|
|
157
|
+
priorityConfig: {
|
|
158
|
+
labelPrefix: priorityLabelPrefix,
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
if (outputFormat === 'text') {
|
|
162
|
+
console.log(chalk.blue(`\n→ Sorted ${originalCount} issue(s) by ${options.orderBy} (${options.orderDirection || 'desc'})`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Apply limit if specified
|
|
166
|
+
if (options.limit && issues.length > options.limit) {
|
|
167
|
+
const totalFound = issues.length;
|
|
168
|
+
issues = issues.slice(0, options.limit);
|
|
169
|
+
if (outputFormat === 'text') {
|
|
170
|
+
console.log(chalk.yellow(`→ Limiting to top ${options.limit} issue(s) (found ${totalFound})`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
119
173
|
// Step 2: Extract workflows from labels or prompt user
|
|
120
174
|
if (outputFormat === 'text') {
|
|
121
175
|
console.log(chalk.cyan('\n→ Identifying workflows...'));
|
|
@@ -346,6 +400,8 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
|
|
|
346
400
|
console.log(chalk.gray(` → Plan written to ${planPath}`));
|
|
347
401
|
}
|
|
348
402
|
}
|
|
403
|
+
// Generate detailed comment for GitHub issue
|
|
404
|
+
const planSummary = generatePlanComment(plan, issue.workflow, worktreePath, planId);
|
|
349
405
|
// Update GitHub issue with plan_id
|
|
350
406
|
if (outputFormat === 'text') {
|
|
351
407
|
console.log(chalk.gray(` → Updating GitHub issue...`));
|
|
@@ -353,7 +409,7 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
|
|
|
353
409
|
try {
|
|
354
410
|
await repoClient.updateIssue({
|
|
355
411
|
id: issue.number.toString(),
|
|
356
|
-
comment:
|
|
412
|
+
comment: planSummary,
|
|
357
413
|
addLabel: 'faber:planned',
|
|
358
414
|
});
|
|
359
415
|
}
|
|
@@ -368,7 +424,7 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
|
|
|
368
424
|
}
|
|
369
425
|
await repoClient.updateIssue({
|
|
370
426
|
id: issue.number.toString(),
|
|
371
|
-
comment:
|
|
427
|
+
comment: planSummary,
|
|
372
428
|
});
|
|
373
429
|
}
|
|
374
430
|
else {
|
|
@@ -385,6 +441,60 @@ async function planSingleIssue(issue, config, repoClient, anthropicClient, optio
|
|
|
385
441
|
worktree: worktreePath,
|
|
386
442
|
};
|
|
387
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Generate a detailed plan comment for GitHub issue
|
|
446
|
+
*/
|
|
447
|
+
function generatePlanComment(plan, workflow, worktreePath, planId) {
|
|
448
|
+
let comment = `🤖 **Workflow Plan Created**\n\n`;
|
|
449
|
+
comment += `**Plan ID:** \`${planId}\`\n`;
|
|
450
|
+
comment += `**Workflow:** \`${workflow}\`\n`;
|
|
451
|
+
// Add workflow inheritance info if available
|
|
452
|
+
if (plan.workflow_config?.inherits_from) {
|
|
453
|
+
comment += `**Inherits from:** \`${plan.workflow_config.inherits_from}\`\n`;
|
|
454
|
+
}
|
|
455
|
+
comment += `\n---\n\n`;
|
|
456
|
+
// Add plan summary by phase
|
|
457
|
+
if (plan.phases && Array.isArray(plan.phases)) {
|
|
458
|
+
comment += `### Workflow Phases\n\n`;
|
|
459
|
+
plan.phases.forEach((phase, index) => {
|
|
460
|
+
comment += `**${index + 1}. ${phase.name || phase.phase}**\n\n`;
|
|
461
|
+
// Show phase description if available
|
|
462
|
+
if (phase.description) {
|
|
463
|
+
comment += `*${phase.description}*\n\n`;
|
|
464
|
+
}
|
|
465
|
+
// Show steps/tasks
|
|
466
|
+
if (phase.steps && Array.isArray(phase.steps)) {
|
|
467
|
+
phase.steps.forEach((step) => {
|
|
468
|
+
const action = step.action || step.name || step.description || step;
|
|
469
|
+
comment += ` - **${action}**`;
|
|
470
|
+
if (step.details) {
|
|
471
|
+
comment += `: ${step.details}`;
|
|
472
|
+
}
|
|
473
|
+
comment += `\n`;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
else if (phase.tasks && Array.isArray(phase.tasks)) {
|
|
477
|
+
phase.tasks.forEach((task) => {
|
|
478
|
+
const taskDesc = task.description || task.name || task;
|
|
479
|
+
comment += ` - ${taskDesc}\n`;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
comment += `\n`;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
comment += `---\n\n`;
|
|
486
|
+
comment += `### Plan Location\n\n`;
|
|
487
|
+
comment += `\`\`\`\n${worktreePath}/.fractary/plans/${planId}.json\n\`\`\`\n\n`;
|
|
488
|
+
comment += `### Next Steps\n\n`;
|
|
489
|
+
comment += `Execute the workflow plan:\n\n`;
|
|
490
|
+
comment += `\`\`\`bash\n`;
|
|
491
|
+
comment += `cd ${worktreePath}\n`;
|
|
492
|
+
comment += `claude\n`;
|
|
493
|
+
comment += `# Then in Claude Code:\n`;
|
|
494
|
+
comment += `/fractary-faber:workflow-run ${plan.issue_number || ''}\n`;
|
|
495
|
+
comment += `\`\`\`\n`;
|
|
496
|
+
return comment;
|
|
497
|
+
}
|
|
388
498
|
/**
|
|
389
499
|
* Get repository info from config
|
|
390
500
|
*/
|
|
@@ -268,8 +268,8 @@ export class GitHubAppAuth {
|
|
|
268
268
|
}
|
|
269
269
|
// Token refresh threshold (5 minutes before expiration)
|
|
270
270
|
GitHubAppAuth.REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
|
|
271
|
-
// JWT validity period (
|
|
272
|
-
GitHubAppAuth.JWT_EXPIRY_SECONDS =
|
|
271
|
+
// JWT validity period (reduced to 5 minutes to handle clock skew)
|
|
272
|
+
GitHubAppAuth.JWT_EXPIRY_SECONDS = 300;
|
|
273
273
|
// GitHub API base URL
|
|
274
274
|
GitHubAppAuth.GITHUB_API_URL = 'https://api.github.com';
|
|
275
275
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk-type-adapter.d.ts","sourceRoot":"","sources":["../../src/lib/sdk-type-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,IAAI,QAAQ,EAAE,QAAQ,IAAI,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGjF,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"sdk-type-adapter.d.ts","sourceRoot":"","sources":["../../src/lib/sdk-type-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,IAAI,QAAQ,EAAE,QAAQ,IAAI,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGjF,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAY/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,CAC5C,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,cAAc,CAUhB"}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -32,11 +32,19 @@ export interface WorkflowConfig {
|
|
|
32
32
|
default?: string;
|
|
33
33
|
config_path?: string;
|
|
34
34
|
}
|
|
35
|
+
export interface BacklogManagementConfig {
|
|
36
|
+
default_limit?: number;
|
|
37
|
+
default_order_by?: 'priority' | 'created' | 'updated' | 'none';
|
|
38
|
+
priority_config?: {
|
|
39
|
+
label_prefix?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
35
42
|
export interface FaberConfig {
|
|
36
43
|
anthropic?: AnthropicConfig;
|
|
37
44
|
github?: GitHubConfig;
|
|
38
45
|
worktree?: WorktreeConfig;
|
|
39
46
|
workflow?: WorkflowConfig;
|
|
47
|
+
backlog_management?: BacklogManagementConfig;
|
|
40
48
|
}
|
|
41
49
|
export interface ClaudeConfig {
|
|
42
50
|
worktree?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,eAAe,GAAG,QAAQ,CAAC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,eAAe,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,eAAe,GAAG,QAAQ,CAAC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,eAAe,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IAC/D,eAAe,CAAC,EAAE;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;CAC9C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE;QACT,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Label Management Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles creation and management of priority labels for backlog management
|
|
5
|
+
*/
|
|
6
|
+
export interface PriorityLabel {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
color: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Default priority labels (priority-1 through priority-4)
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_PRIORITY_LABELS: PriorityLabel[];
|
|
15
|
+
/**
|
|
16
|
+
* Generate priority labels based on a custom prefix
|
|
17
|
+
*/
|
|
18
|
+
export declare function generatePriorityLabels(prefix: string): PriorityLabel[];
|
|
19
|
+
/**
|
|
20
|
+
* Check if a label exists in the repository
|
|
21
|
+
* @param labelName - Name of the label to check
|
|
22
|
+
* @returns Promise resolving to true if label exists, false otherwise
|
|
23
|
+
*/
|
|
24
|
+
export declare function labelExists(labelName: string): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* Create a single label in the repository
|
|
27
|
+
* @param label - Label to create with name, description, and color
|
|
28
|
+
*/
|
|
29
|
+
export declare function createLabel(label: PriorityLabel): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Create multiple priority labels
|
|
32
|
+
* @param prefix - Label prefix (e.g., "priority" for priority-1, priority-2, etc.)
|
|
33
|
+
* @param quiet - If true, suppress console output
|
|
34
|
+
* @returns Object with arrays of created, skipped, and error labels
|
|
35
|
+
*/
|
|
36
|
+
export declare function createPriorityLabels(prefix?: string, quiet?: boolean): Promise<{
|
|
37
|
+
created: string[];
|
|
38
|
+
skipped: string[];
|
|
39
|
+
errors: string[];
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Check if GitHub CLI is available
|
|
43
|
+
*/
|
|
44
|
+
export declare function isGitHubCLIAvailable(): Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* Ensure priority labels exist, creating them if necessary
|
|
47
|
+
* This is a convenience function for automatic label creation
|
|
48
|
+
*/
|
|
49
|
+
export declare function ensurePriorityLabels(prefix?: string, quiet?: boolean): Promise<boolean>;
|
|
50
|
+
//# sourceMappingURL=labels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../../src/utils/labels.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,EAqBlD,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAKtE;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASrE;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAarE;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,GAAE,MAAmB,EAC3B,KAAK,GAAE,OAAe,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAsDrE;AAED;;GAEG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO7D;AAaD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,GAAE,MAAmB,EAC3B,KAAK,GAAE,OAAc,GACpB,OAAO,CAAC,OAAO,CAAC,CAqBlB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Label Management Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles creation and management of priority labels for backlog management
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
/**
|
|
11
|
+
* Default priority labels (priority-1 through priority-4)
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_PRIORITY_LABELS = [
|
|
14
|
+
{
|
|
15
|
+
name: 'priority-1',
|
|
16
|
+
description: 'Highest priority - Critical issues that need immediate attention',
|
|
17
|
+
color: 'd73a4a', // Red
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'priority-2',
|
|
21
|
+
description: 'High priority - Important issues that should be addressed soon',
|
|
22
|
+
color: 'e99695', // Light red
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'priority-3',
|
|
26
|
+
description: 'Medium priority - Standard priority issues',
|
|
27
|
+
color: 'fbca04', // Yellow
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'priority-4',
|
|
31
|
+
description: 'Low priority - Nice to have, can be deferred',
|
|
32
|
+
color: 'd4c5f9', // Light purple
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Generate priority labels based on a custom prefix
|
|
37
|
+
*/
|
|
38
|
+
export function generatePriorityLabels(prefix) {
|
|
39
|
+
return DEFAULT_PRIORITY_LABELS.map((label, index) => ({
|
|
40
|
+
...label,
|
|
41
|
+
name: `${prefix}-${index + 1}`,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if a label exists in the repository
|
|
46
|
+
* @param labelName - Name of the label to check
|
|
47
|
+
* @returns Promise resolving to true if label exists, false otherwise
|
|
48
|
+
*/
|
|
49
|
+
export async function labelExists(labelName) {
|
|
50
|
+
try {
|
|
51
|
+
const { stdout } = await execFileAsync('gh', ['label', 'list', '--json', 'name', '--jq', '.[].name']);
|
|
52
|
+
const labels = stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
53
|
+
return labels.includes(labelName);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// If gh command fails, assume label doesn't exist
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create a single label in the repository
|
|
62
|
+
* @param label - Label to create with name, description, and color
|
|
63
|
+
*/
|
|
64
|
+
export async function createLabel(label) {
|
|
65
|
+
const { name, description, color } = label;
|
|
66
|
+
// Use execFileAsync with array arguments to prevent command injection
|
|
67
|
+
await execFileAsync('gh', [
|
|
68
|
+
'label',
|
|
69
|
+
'create',
|
|
70
|
+
name,
|
|
71
|
+
'--description',
|
|
72
|
+
description,
|
|
73
|
+
'--color',
|
|
74
|
+
color,
|
|
75
|
+
'--force'
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create multiple priority labels
|
|
80
|
+
* @param prefix - Label prefix (e.g., "priority" for priority-1, priority-2, etc.)
|
|
81
|
+
* @param quiet - If true, suppress console output
|
|
82
|
+
* @returns Object with arrays of created, skipped, and error labels
|
|
83
|
+
*/
|
|
84
|
+
export async function createPriorityLabels(prefix = 'priority', quiet = false) {
|
|
85
|
+
// Validate prefix format
|
|
86
|
+
if (!isValidLabelPrefix(prefix)) {
|
|
87
|
+
return {
|
|
88
|
+
created: [],
|
|
89
|
+
skipped: [],
|
|
90
|
+
errors: [`Invalid label prefix: ${prefix}. Must contain only letters, numbers, and hyphens.`]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const labels = generatePriorityLabels(prefix);
|
|
94
|
+
const created = [];
|
|
95
|
+
const skipped = [];
|
|
96
|
+
const errors = [];
|
|
97
|
+
// Fetch all existing labels once to optimize API calls
|
|
98
|
+
let existingLabels = [];
|
|
99
|
+
try {
|
|
100
|
+
const { stdout } = await execFileAsync('gh', ['label', 'list', '--json', 'name', '--jq', '.[].name']);
|
|
101
|
+
existingLabels = stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// If we can't fetch labels, proceed cautiously
|
|
105
|
+
if (!quiet) {
|
|
106
|
+
console.log(chalk.yellow(' ⚠️ Could not fetch existing labels, will attempt creation'));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const label of labels) {
|
|
110
|
+
try {
|
|
111
|
+
// Check if label already exists (using cached list)
|
|
112
|
+
if (existingLabels.includes(label.name)) {
|
|
113
|
+
skipped.push(label.name);
|
|
114
|
+
if (!quiet) {
|
|
115
|
+
console.log(chalk.gray(` ⊳ Label already exists: ${label.name}`));
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Create the label
|
|
120
|
+
await createLabel(label);
|
|
121
|
+
created.push(label.name);
|
|
122
|
+
if (!quiet) {
|
|
123
|
+
console.log(chalk.green(` ✓ Created label: ${label.name}`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
+
errors.push(`${label.name}: ${message}`);
|
|
129
|
+
if (!quiet) {
|
|
130
|
+
console.log(chalk.red(` ✗ Failed to create ${label.name}: ${message}`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { created, skipped, errors };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Check if GitHub CLI is available
|
|
138
|
+
*/
|
|
139
|
+
export async function isGitHubCLIAvailable() {
|
|
140
|
+
try {
|
|
141
|
+
await execFileAsync('gh', ['--version']);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Validate label prefix format
|
|
150
|
+
* @param prefix - Label prefix to validate
|
|
151
|
+
* @returns True if valid, false otherwise
|
|
152
|
+
*/
|
|
153
|
+
function isValidLabelPrefix(prefix) {
|
|
154
|
+
// Label prefix should only contain lowercase letters, numbers, and hyphens
|
|
155
|
+
// and should not be empty
|
|
156
|
+
return /^[a-z0-9-]+$/i.test(prefix) && prefix.length > 0 && prefix.length <= 50;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Ensure priority labels exist, creating them if necessary
|
|
160
|
+
* This is a convenience function for automatic label creation
|
|
161
|
+
*/
|
|
162
|
+
export async function ensurePriorityLabels(prefix = 'priority', quiet = true) {
|
|
163
|
+
// Check if gh CLI is available
|
|
164
|
+
const ghAvailable = await isGitHubCLIAvailable();
|
|
165
|
+
if (!ghAvailable) {
|
|
166
|
+
if (!quiet) {
|
|
167
|
+
console.log(chalk.yellow(' ⚠️ GitHub CLI (gh) not available, skipping label creation'));
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const result = await createPriorityLabels(prefix, quiet);
|
|
173
|
+
// Return true if we created any labels or all were already present
|
|
174
|
+
return result.created.length > 0 || result.skipped.length > 0;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (!quiet) {
|
|
178
|
+
console.log(chalk.yellow(` ⚠️ Could not create priority labels: ${error instanceof Error ? error.message : String(error)}`));
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Sorting Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides sorting and priority extraction for backlog management
|
|
5
|
+
*/
|
|
6
|
+
export interface SortOptions {
|
|
7
|
+
orderBy: 'priority' | 'created' | 'updated';
|
|
8
|
+
direction: 'asc' | 'desc';
|
|
9
|
+
priorityConfig: {
|
|
10
|
+
labelPrefix: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface Issue {
|
|
14
|
+
id: string;
|
|
15
|
+
number: number;
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
labels: string[];
|
|
19
|
+
url: string;
|
|
20
|
+
state: string;
|
|
21
|
+
createdAt?: string;
|
|
22
|
+
updatedAt?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sort issues according to strategy
|
|
26
|
+
*
|
|
27
|
+
* @param issues - Array of issues to sort
|
|
28
|
+
* @param options - Sort options including orderBy, direction, and priority config
|
|
29
|
+
* @returns Sorted array of issues
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Priority sorting has inverted direction semantics compared to date sorting:
|
|
33
|
+
*
|
|
34
|
+
* **Priority Sorting:**
|
|
35
|
+
* - `direction: 'desc'` (default) = Highest priority first = priority-1, priority-2, priority-3, ...
|
|
36
|
+
* - `direction: 'asc'` = Lowest priority first = priority-4, priority-3, priority-2, priority-1
|
|
37
|
+
*
|
|
38
|
+
* This is because "descending priority" means "descending importance" (highest first),
|
|
39
|
+
* which corresponds to ascending numeric values (1 < 2 < 3).
|
|
40
|
+
*
|
|
41
|
+
* **Date Sorting:**
|
|
42
|
+
* - `direction: 'desc'` = Newest first (standard descending chronological order)
|
|
43
|
+
* - `direction: 'asc'` = Oldest first (standard ascending chronological order)
|
|
44
|
+
*/
|
|
45
|
+
export declare function sortIssues(issues: Issue[], options: SortOptions): Issue[];
|
|
46
|
+
//# sourceMappingURL=sorting.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sorting.d.ts","sourceRoot":"","sources":["../../src/utils/sorting.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAC5C,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B,cAAc,EAAE;QACd,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA8CD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,WAAW,GAAG,KAAK,EAAE,CA2BzE"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Sorting Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides sorting and priority extraction for backlog management
|
|
5
|
+
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
/**
|
|
8
|
+
* Extract numeric priority from issue labels
|
|
9
|
+
* Examples: priority-1 → 1, priority-2 → 2, p-1 → 1
|
|
10
|
+
* Returns 999 if no priority found (sorts last)
|
|
11
|
+
*
|
|
12
|
+
* @param labels - Array of label strings
|
|
13
|
+
* @param prefix - Label prefix to match (e.g., "priority")
|
|
14
|
+
* @param issueNumber - Optional issue number for warning messages
|
|
15
|
+
* @returns Priority number (1-4 for valid priorities, 999 for no/invalid priority)
|
|
16
|
+
*/
|
|
17
|
+
function extractPriority(labels, prefix, issueNumber) {
|
|
18
|
+
// Use stricter pattern matching: prefix must be followed by hyphen and digits
|
|
19
|
+
const pattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-\\d+$`, 'i');
|
|
20
|
+
const priorityLabels = labels.filter(l => pattern.test(l));
|
|
21
|
+
if (priorityLabels.length === 0) {
|
|
22
|
+
return 999; // No priority label found
|
|
23
|
+
}
|
|
24
|
+
// Warn if multiple priority labels found
|
|
25
|
+
if (priorityLabels.length > 1) {
|
|
26
|
+
const issueRef = issueNumber ? `Issue #${issueNumber}` : 'An issue';
|
|
27
|
+
console.log(chalk.yellow(`⚠️ ${issueRef} has multiple priority labels: ${priorityLabels.join(', ')}. Using first match: ${priorityLabels[0]}`));
|
|
28
|
+
}
|
|
29
|
+
// Extract numeric part: "priority-1" → "1"
|
|
30
|
+
const match = priorityLabels[0].match(/-(\d+)$/);
|
|
31
|
+
if (!match) {
|
|
32
|
+
return 999; // Shouldn't happen with regex, but be safe
|
|
33
|
+
}
|
|
34
|
+
const numeric = parseInt(match[1], 10);
|
|
35
|
+
// Validate priority is in reasonable range (1-10)
|
|
36
|
+
// We allow up to 10 to be flexible, but typical range is 1-4
|
|
37
|
+
if (isNaN(numeric) || numeric < 1 || numeric > 10) {
|
|
38
|
+
const issueRef = issueNumber ? `Issue #${issueNumber}` : 'An issue';
|
|
39
|
+
console.log(chalk.yellow(`⚠️ ${issueRef} has invalid priority value: ${priorityLabels[0]}. Priority should be between 1-10.`));
|
|
40
|
+
return 999;
|
|
41
|
+
}
|
|
42
|
+
return numeric;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sort issues according to strategy
|
|
46
|
+
*
|
|
47
|
+
* @param issues - Array of issues to sort
|
|
48
|
+
* @param options - Sort options including orderBy, direction, and priority config
|
|
49
|
+
* @returns Sorted array of issues
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* Priority sorting has inverted direction semantics compared to date sorting:
|
|
53
|
+
*
|
|
54
|
+
* **Priority Sorting:**
|
|
55
|
+
* - `direction: 'desc'` (default) = Highest priority first = priority-1, priority-2, priority-3, ...
|
|
56
|
+
* - `direction: 'asc'` = Lowest priority first = priority-4, priority-3, priority-2, priority-1
|
|
57
|
+
*
|
|
58
|
+
* This is because "descending priority" means "descending importance" (highest first),
|
|
59
|
+
* which corresponds to ascending numeric values (1 < 2 < 3).
|
|
60
|
+
*
|
|
61
|
+
* **Date Sorting:**
|
|
62
|
+
* - `direction: 'desc'` = Newest first (standard descending chronological order)
|
|
63
|
+
* - `direction: 'asc'` = Oldest first (standard ascending chronological order)
|
|
64
|
+
*/
|
|
65
|
+
export function sortIssues(issues, options) {
|
|
66
|
+
const sorted = [...issues];
|
|
67
|
+
sorted.sort((a, b) => {
|
|
68
|
+
let comparison = 0;
|
|
69
|
+
if (options.orderBy === 'priority') {
|
|
70
|
+
// Pass issue numbers for better warning messages
|
|
71
|
+
const aPriority = extractPriority(a.labels, options.priorityConfig.labelPrefix, a.number);
|
|
72
|
+
const bPriority = extractPriority(b.labels, options.priorityConfig.labelPrefix, b.number);
|
|
73
|
+
comparison = aPriority - bPriority; // Lower number = higher priority
|
|
74
|
+
// For priority, direction is inverted:
|
|
75
|
+
// - desc (default) = highest priority first = lowest number first = use comparison as-is
|
|
76
|
+
// - asc = lowest priority first = highest number first = negate comparison
|
|
77
|
+
return options.direction === 'asc' ? -comparison : comparison;
|
|
78
|
+
}
|
|
79
|
+
else if (options.orderBy === 'created' && a.createdAt && b.createdAt) {
|
|
80
|
+
comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
81
|
+
}
|
|
82
|
+
else if (options.orderBy === 'updated' && a.updatedAt && b.updatedAt) {
|
|
83
|
+
comparison = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
|
84
|
+
}
|
|
85
|
+
// Apply direction for dates (standard: asc = oldest first, desc = newest first)
|
|
86
|
+
return options.direction === 'asc' ? comparison : -comparison;
|
|
87
|
+
});
|
|
88
|
+
return sorted;
|
|
89
|
+
}
|