ticketlens 0.1.16 → 0.1.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Jira CLI for developers — fetch ticket context, triage your queue, and stop tab-switching. Zero dependencies, all local.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -146,18 +146,29 @@ async function applySummarize(brief, args, opts, configDir, conn, licensedFn, up
146
146
  }
147
147
 
148
148
  function makeSpinner(s) {
149
- if (!process.stderr.isTTY) return { update: () => {}, done: () => {} };
149
+ // setInterval won't fire while spawnSync blocks the event loop, so we draw
150
+ // synchronously on update() and only use setInterval during async fetch phases.
151
+ if (!process.stderr.isTTY) return { update: () => {}, startAnim: () => {}, done: () => {} };
150
152
  const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
151
- let i = 0, msg = 'Working…', finished = false;
152
- const id = setInterval(() => {
153
- process.stderr.write(`\r ${s.brand(frames[i++ % frames.length])} ${s.dim(msg)}`);
154
- }, 80);
153
+ let fi = 0, msg = '', lastW = 0, timerId = null, finished = false;
154
+
155
+ const draw = () => {
156
+ const raw = ` ${s.brand(frames[fi % frames.length])} ${s.dim(msg)}`;
157
+ const vis = raw.replace(/\x1b\[[0-9;]*m/g, '');
158
+ process.stderr.write(`\r${raw}${' '.repeat(Math.max(0, lastW - vis.length))}`);
159
+ lastW = vis.length;
160
+ };
161
+
155
162
  return {
156
- update(m) { if (!finished) msg = m; },
163
+ update(m) { if (!finished) { fi++; msg = m; draw(); } },
164
+ startAnim() {
165
+ if (finished || timerId) return;
166
+ timerId = setInterval(() => { if (!finished) { fi++; draw(); } }, 80);
167
+ },
157
168
  done() {
158
169
  if (finished) return;
159
170
  finished = true;
160
- clearInterval(id);
171
+ if (timerId) { clearInterval(timerId); timerId = null; }
161
172
  process.stderr.write('\r\x1b[2K');
162
173
  },
163
174
  };
@@ -470,15 +481,56 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
470
481
  const cwd = process.cwd();
471
482
 
472
483
  const sErr = createStyler({ isTTY: process.stderr.isTTY });
484
+
485
+ // Validate flags before any git work
486
+ const reviewFlags = args.slice(1);
487
+ const VALID_REVIEW_FLAG = /^(--base=.+|--branch=.+|--profile=.+)$/;
488
+ for (const flag of reviewFlags) {
489
+ if (!flag.startsWith('-')) continue;
490
+ if (VALID_REVIEW_FLAG.test(flag)) continue;
491
+ // Detect --profile-NAME typo (dash instead of =)
492
+ const profileDashM = flag.match(/^--profile-(.+)$/);
493
+ if (profileDashM) {
494
+ process.stderr.write(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--profile=${profileDashM[1]}`)}?\n`);
495
+ process.exitCode = 1;
496
+ return;
497
+ }
498
+ const baseDashM = flag.match(/^--base-(.+)$/);
499
+ if (baseDashM) {
500
+ process.stderr.write(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--base=${baseDashM[1]}`)}?\n`);
501
+ process.exitCode = 1;
502
+ return;
503
+ }
504
+ const branchDashM = flag.match(/^--branch-(.+)$/);
505
+ if (branchDashM) {
506
+ process.stderr.write(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Did you mean ${sErr.cyan(`--branch=${branchDashM[1]}`)}?\n`);
507
+ process.exitCode = 1;
508
+ return;
509
+ }
510
+ process.stderr.write(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Usage: ${sErr.cyan('ticketlens review [--base=BRANCH] [--branch=BRANCH] [--profile=NAME]')}\n`);
511
+ process.exitCode = 1;
512
+ return;
513
+ }
514
+
473
515
  const spinner = makeSpinner(sErr);
516
+ const stderrNotes = [];
474
517
 
475
518
  // Resolve base branch: --base=BRANCH or auto-detect main/master/develop
476
- const baseArg = args.find(a => a.startsWith('--base='));
477
- let baseBranch = baseArg ? baseArg.split('=')[1] : null;
519
+ const baseArg = reviewFlags.find(a => a.startsWith('--base=') || a.startsWith('--branch='));
520
+ let baseBranch = baseArg ? baseArg.replace(/^--(base|branch)=/, '') : null;
478
521
 
479
522
  spinner.update('Scanning branch…');
480
523
 
481
- if (!baseBranch) {
524
+ if (baseBranch) {
525
+ // Explicit --base: verify the branch exists
526
+ const verifyR = execFn('git', ['rev-parse', '--verify', baseBranch], { encoding: 'utf8', cwd, timeout: 5_000 });
527
+ if (verifyR.status !== 0) {
528
+ spinner.done();
529
+ process.stderr.write(`${sErr.red('✖')} Branch "${baseBranch}" not found in this repository.\n`);
530
+ process.exitCode = 1;
531
+ return;
532
+ }
533
+ } else {
482
534
  for (const candidate of ['main', 'master', 'develop']) {
483
535
  const r = execFn('git', ['rev-parse', '--verify', candidate], { encoding: 'utf8', cwd, timeout: 5_000 });
484
536
  if (r.status === 0) { baseBranch = candidate; break; }
@@ -489,6 +541,12 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
489
541
  const headResult = execFn('git', ['branch', '--show-current'], { encoding: 'utf8', cwd, timeout: 5_000 });
490
542
  const headBranch = headResult.status === 0 ? (headResult.stdout.trim() || null) : null;
491
543
 
544
+ if (headBranch && headBranch === baseBranch) {
545
+ stderrNotes.push(
546
+ ` ${sErr.yellow('⚠')} You are on ${sErr.cyan(headBranch)} — switch to a feature branch for meaningful output.`
547
+ );
548
+ }
549
+
492
550
  const logResult = execFn('git', ['log', '--oneline', `${baseBranch}..HEAD`], { encoding: 'utf8', cwd, timeout: 10_000 });
493
551
  const commitMessages = logResult.status === 0 ? (logResult.stdout ?? '') : '';
494
552
 
@@ -498,7 +556,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
498
556
  const allText = [headBranch ?? '', commitMessages].join('\n');
499
557
  const ticketKeys = extractTicketKeys(allText);
500
558
 
501
- const profileArgR = args.find(a => a.startsWith('--profile='));
559
+ const profileArgR = reviewFlags.find(a => a.startsWith('--profile='));
502
560
  const profileNameR = profileArgR ? profileArgR.split('=')[1] : undefined;
503
561
 
504
562
  // Validate explicit --profile exists before attempting any fetch
@@ -515,12 +573,12 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
515
573
  }
516
574
  }
517
575
 
518
- // Fetch tickets (best-effort — collect notes/warnings, write after spinner stops)
576
+ // Fetch tickets (best-effort) — start animation here since fetch is async
519
577
  const tickets = [];
520
- const stderrNotes = [];
521
578
 
522
579
  if (ticketKeys.length > 0) {
523
580
  spinner.update(`Fetching ticket context (${ticketKeys.length} key${ticketKeys.length > 1 ? 's' : ''})…`);
581
+ spinner.startAnim();
524
582
  const connR = resolveConnection(ticketKeys[0], {
525
583
  env,
526
584
  configDir: resolvedConfigDir,
@@ -552,6 +610,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
552
610
 
553
611
  spinner.done();
554
612
  for (const note of stderrNotes) process.stderr.write(note + '\n');
613
+ if (stderrNotes.length > 0) process.stderr.write('\n');
555
614
 
556
615
  const isLic = opts.isLicensedFn ?? ((tier) => isLicensed(tier, resolvedConfigDir));
557
616
  const assembleFn = opts.assemblePrReviewFn ?? assemblePrReview;
@@ -38,7 +38,7 @@ export function printHelp({ stream = process.stdout } = {}) {
38
38
  ` ${s.brand('ticketlens')} ${s.dim('<TICKET-KEY>')} ${s.dim('[options]')} Fetch a ticket brief`,
39
39
  ` ${s.brand('ticketlens')} get ${s.dim('<TICKET-KEY>')} Same as above ${s.dim('(explicit alias)')}`,
40
40
  ` ${s.brand('ticketlens')} triage ${s.dim('[options]')} Scan your assigned tickets`,
41
- ` ${s.brand('ticketlens')} review ${s.dim('[--base=BRANCH]')} Code-review context brief from current branch`,
41
+ ` ${s.brand('ticketlens')} review ${s.dim('[--branch=BRANCH]')} Code-review context brief from current branch`,
42
42
  ` ${s.brand('ticketlens')} compliance ${s.dim('<TICKET-KEY>')} Check requirements coverage ${s.dim('[Pro/Free 3/mo]')}`,
43
43
  '',
44
44
  ` ${s.brand('ticketlens')} delete ${s.dim('<PROFILE-NAME>')} Remove a profile`,
@@ -545,7 +545,7 @@ export function printReviewHelp({ stream = process.stdout } = {}) {
545
545
  const s = createStyler({ isTTY: stream.isTTY });
546
546
  const lines = [
547
547
  '',
548
- ` ${s.bold(s.brand('ticketlens'))} ${s.bold('review')} ${s.dim('[--base=BRANCH] [--profile=NAME]')}`,
548
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('review')} ${s.dim('[--branch=BRANCH] [--profile=NAME]')}`,
549
549
  '',
550
550
  ` Assemble a code-review context brief from your current branch.`,
551
551
  ` Extracts linked ticket keys from the branch name and commit messages,`,
@@ -555,16 +555,17 @@ export function printReviewHelp({ stream = process.stdout } = {}) {
555
555
  '',
556
556
  ` ${s.bold('OPTIONS')}`,
557
557
  '',
558
- ` ${s.brand('--base')}=${s.dim('BRANCH')} Compare against this branch ${s.dim('(default: auto-detect main/master/develop)')}`,
558
+ ` ${s.brand('--branch')}=${s.dim('BRANCH')} Compare against this branch ${s.dim('(default: auto-detect main/master/develop)')}`,
559
+ ` ${s.brand('--base')}=${s.dim('BRANCH')} Alias for ${s.dim('--branch')}`,
559
560
  ` ${s.brand('--profile')}=${s.dim('NAME')} Use a specific tracker profile for ticket fetching`,
560
561
  ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
561
562
  '',
562
563
  ` ${s.bold('EXAMPLES')}`,
563
564
  '',
564
565
  ` ${s.dim('$')} ticketlens review`,
565
- ` ${s.dim('$')} ticketlens review --base=main`,
566
- ` ${s.dim('$')} ticketlens review --base=main --profile=myteam`,
567
- ` ${s.dim('$')} ticketlens review --base=main | pbcopy ${s.dim('# copy brief to clipboard')}`,
566
+ ` ${s.dim('$')} ticketlens review --branch=main`,
567
+ ` ${s.dim('$')} ticketlens review --branch=main --profile=myteam`,
568
+ ` ${s.dim('$')} ticketlens review --branch=main | pbcopy ${s.dim('# copy brief to clipboard')}`,
568
569
  '',
569
570
  ` ${s.bold('OUTPUT SECTIONS')}`,
570
571
  '',