@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.
@@ -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;AAKpC,wBAAgB,iBAAiB,IAAI,OAAO,CAsE3C"}
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"}
@@ -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;AA+CpC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAkB3C"}
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: `🤖 Workflow plan created: ${planId}`,
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: `🤖 Workflow plan created: ${planId}`,
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 (10 minutes max for GitHub)
272
- GitHubAppAuth.JWT_EXPIRY_SECONDS = 600;
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
  /**
@@ -12,6 +12,8 @@ interface CLIIssue {
12
12
  labels: string[];
13
13
  url: string;
14
14
  state: string;
15
+ createdAt?: string;
16
+ updatedAt?: string;
15
17
  }
16
18
  interface WorktreeResult {
17
19
  path: string;
@@ -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;CACf;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,CAU/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"}
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"}
@@ -18,6 +18,8 @@ export function sdkIssueToCLIIssue(sdkIssue) {
18
18
  labels: sdkIssue.labels.map(label => label.name), // Extract label names
19
19
  url: sdkIssue.url,
20
20
  state: sdkIssue.state,
21
+ createdAt: sdkIssue.created_at,
22
+ updatedAt: sdkIssue.updated_at,
21
23
  };
22
24
  }
23
25
  /**
@@ -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;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE;QACT,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH"}
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fractary/faber-cli",
3
- "version": "1.3.14",
3
+ "version": "1.4.0",
4
4
  "description": "FABER CLI - Command-line interface for FABER development toolkit",
5
5
  "main": "dist/index.js",
6
6
  "bin": {