ticketlens 0.1.16 → 0.1.17

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.17",
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,50 @@ 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=.+|--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
+ process.stderr.write(`${sErr.red('✖')} Unknown flag: ${sErr.bold(flag)}\n Usage: ${sErr.cyan('ticketlens review [--base=BRANCH] [--profile=NAME]')}\n`);
505
+ process.exitCode = 1;
506
+ return;
507
+ }
508
+
473
509
  const spinner = makeSpinner(sErr);
510
+ const stderrNotes = [];
474
511
 
475
512
  // Resolve base branch: --base=BRANCH or auto-detect main/master/develop
476
- const baseArg = args.find(a => a.startsWith('--base='));
513
+ const baseArg = reviewFlags.find(a => a.startsWith('--base='));
477
514
  let baseBranch = baseArg ? baseArg.split('=')[1] : null;
478
515
 
479
516
  spinner.update('Scanning branch…');
480
517
 
481
- if (!baseBranch) {
518
+ if (baseBranch) {
519
+ // Explicit --base: verify the branch exists
520
+ const verifyR = execFn('git', ['rev-parse', '--verify', baseBranch], { encoding: 'utf8', cwd, timeout: 5_000 });
521
+ if (verifyR.status !== 0) {
522
+ spinner.done();
523
+ process.stderr.write(`${sErr.red('✖')} Branch "${baseBranch}" not found in this repository.\n`);
524
+ process.exitCode = 1;
525
+ return;
526
+ }
527
+ } else {
482
528
  for (const candidate of ['main', 'master', 'develop']) {
483
529
  const r = execFn('git', ['rev-parse', '--verify', candidate], { encoding: 'utf8', cwd, timeout: 5_000 });
484
530
  if (r.status === 0) { baseBranch = candidate; break; }
@@ -489,6 +535,12 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
489
535
  const headResult = execFn('git', ['branch', '--show-current'], { encoding: 'utf8', cwd, timeout: 5_000 });
490
536
  const headBranch = headResult.status === 0 ? (headResult.stdout.trim() || null) : null;
491
537
 
538
+ if (headBranch && headBranch === baseBranch) {
539
+ stderrNotes.push(
540
+ ` ${sErr.yellow('⚠')} You are on ${sErr.cyan(headBranch)} — switch to a feature branch for meaningful output.`
541
+ );
542
+ }
543
+
492
544
  const logResult = execFn('git', ['log', '--oneline', `${baseBranch}..HEAD`], { encoding: 'utf8', cwd, timeout: 10_000 });
493
545
  const commitMessages = logResult.status === 0 ? (logResult.stdout ?? '') : '';
494
546
 
@@ -498,7 +550,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
498
550
  const allText = [headBranch ?? '', commitMessages].join('\n');
499
551
  const ticketKeys = extractTicketKeys(allText);
500
552
 
501
- const profileArgR = args.find(a => a.startsWith('--profile='));
553
+ const profileArgR = reviewFlags.find(a => a.startsWith('--profile='));
502
554
  const profileNameR = profileArgR ? profileArgR.split('=')[1] : undefined;
503
555
 
504
556
  // Validate explicit --profile exists before attempting any fetch
@@ -515,12 +567,12 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
515
567
  }
516
568
  }
517
569
 
518
- // Fetch tickets (best-effort — collect notes/warnings, write after spinner stops)
570
+ // Fetch tickets (best-effort) — start animation here since fetch is async
519
571
  const tickets = [];
520
- const stderrNotes = [];
521
572
 
522
573
  if (ticketKeys.length > 0) {
523
574
  spinner.update(`Fetching ticket context (${ticketKeys.length} key${ticketKeys.length > 1 ? 's' : ''})…`);
575
+ spinner.startAnim();
524
576
  const connR = resolveConnection(ticketKeys[0], {
525
577
  env,
526
578
  configDir: resolvedConfigDir,