ticketlens 0.1.15 → 0.1.16
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
|
@@ -145,6 +145,124 @@ async function applySummarize(brief, args, opts, configDir, conn, licensedFn, up
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function makeSpinner(s) {
|
|
149
|
+
if (!process.stderr.isTTY) return { update: () => {}, done: () => {} };
|
|
150
|
+
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);
|
|
155
|
+
return {
|
|
156
|
+
update(m) { if (!finished) msg = m; },
|
|
157
|
+
done() {
|
|
158
|
+
if (finished) return;
|
|
159
|
+
finished = true;
|
|
160
|
+
clearInterval(id);
|
|
161
|
+
process.stderr.write('\r\x1b[2K');
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function styleReviewMd(md, s) {
|
|
167
|
+
if (!s.enabled) return md;
|
|
168
|
+
const lines = md.split('\n');
|
|
169
|
+
const out = [];
|
|
170
|
+
let section = '';
|
|
171
|
+
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
if (line === '## PR Review Context') {
|
|
174
|
+
out.push('');
|
|
175
|
+
out.push(` ${s.brand(s.bold('◆ PR Review Context'))}`);
|
|
176
|
+
out.push(` ${s.dim('─'.repeat(50))}`);
|
|
177
|
+
section = 'header';
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (line.startsWith('### ')) {
|
|
182
|
+
const title = line.slice(4);
|
|
183
|
+
const fm = title.match(/^Changed files \((\d+)\)$/);
|
|
184
|
+
if (fm) {
|
|
185
|
+
out.push('');
|
|
186
|
+
out.push(` ${s.bold('Changed files')} ${s.dim('(')}${s.yellow(fm[1])}${s.dim(')')}`);
|
|
187
|
+
section = 'changed-files';
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const icons = { Branch: '⎇ ', 'Ticket context': '◉ ', 'Requirements coverage': '✦ ', 'Review focus': '▲ ' };
|
|
191
|
+
const icon = icons[title] ?? '';
|
|
192
|
+
out.push('');
|
|
193
|
+
out.push(` ${s.bold(icon + title)}`);
|
|
194
|
+
section = title.toLowerCase().replace(/\s+/g, '-');
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (line.startsWith('#### ')) {
|
|
199
|
+
const inner = line.slice(5);
|
|
200
|
+
const covM = inner.match(/^([A-Z][A-Z0-9]+-\d+) \((\d+)%\)$/);
|
|
201
|
+
if (covM) {
|
|
202
|
+
const pct = parseInt(covM[2], 10);
|
|
203
|
+
const pctFmt = pct >= 75 ? s.green(`${pct}%`) : pct >= 50 ? s.yellow(`${pct}%`) : s.red(`${pct}%`);
|
|
204
|
+
out.push(` ${s.cyan(s.bold(covM[1]))} ${s.dim('coverage:')} ${pctFmt}`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const tkM = inner.match(/^([A-Z][A-Z0-9]+-\d+)[: —–-]+(.*)$/);
|
|
208
|
+
if (tkM) {
|
|
209
|
+
out.push('');
|
|
210
|
+
out.push(` ${s.cyan(s.bold(tkM[1]))} ${s.dim(tkM[2].trim())}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
out.push(line);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (line.match(/^`[^`]+` → `[^`]+`$/)) {
|
|
218
|
+
const [h, b] = line.split(' → ');
|
|
219
|
+
out.push(` ${s.cyan(h.replace(/`/g, ''))} ${s.dim('→')} ${s.dim(b.replace(/`/g, ''))}`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (line.match(/^→ `[^`]+`$/)) {
|
|
223
|
+
out.push(` ${s.dim('(detached HEAD)')} ${s.dim('→')} ${s.dim(line.slice(4).replace(/`/g, ''))}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (line.startsWith('- ✔ ')) {
|
|
228
|
+
const text = line.slice(4);
|
|
229
|
+
const di = text.lastIndexOf(' — ');
|
|
230
|
+
if (di > 0) out.push(` ${s.green('✔')} ${text.slice(0, di)}${s.dim(' — ' + text.slice(di + 3))}`);
|
|
231
|
+
else out.push(` ${s.green('✔')} ${text}`);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (line.startsWith('- ✖ ')) { out.push(` ${s.red('✖')} ${line.slice(4)}`); continue; }
|
|
235
|
+
if (line.startsWith('- ~ ')) { out.push(` ${s.yellow('~')} ${s.dim(line.slice(4))}`); continue; }
|
|
236
|
+
|
|
237
|
+
if (line.startsWith('- ')) {
|
|
238
|
+
out.push(section === 'changed-files'
|
|
239
|
+
? ` ${s.dim('·')} ${s.dim(line.slice(2))}`
|
|
240
|
+
: ` ${s.dim('·')} ${line.slice(2)}`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (line.startsWith('_') && line.endsWith('_') && line.length > 2) {
|
|
245
|
+
const inner = line.slice(1, -1);
|
|
246
|
+
if (inner.includes('No linked tickets found')) {
|
|
247
|
+
out.push(` ${s.yellow('○')} ${s.dim(inner)}`);
|
|
248
|
+
} else if (inner.includes('Requirements coverage analysis requires')) {
|
|
249
|
+
out.push(` ${s.yellow('⚡')} ${s.dim(inner)}`);
|
|
250
|
+
} else if (inner.includes('Generated by TicketLens')) {
|
|
251
|
+
out.push(s.dim(inner));
|
|
252
|
+
} else {
|
|
253
|
+
out.push(` ${s.dim(inner)}`);
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (line === '---') { out.push(` ${s.dim('─'.repeat(50))}`); continue; }
|
|
259
|
+
|
|
260
|
+
out.push(line);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return out.join('\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
148
266
|
const RETRY_OPTIONS = [
|
|
149
267
|
{ label: 'Retry', sublabel: 'Try again — e.g. VPN just connected', value: 'retry' },
|
|
150
268
|
{ label: 'Switch profile', sublabel: 'Use a different Jira profile', value: 'switch' },
|
|
@@ -351,9 +469,15 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
351
469
|
const execFn = opts.execFn ?? spawnSync;
|
|
352
470
|
const cwd = process.cwd();
|
|
353
471
|
|
|
472
|
+
const sErr = createStyler({ isTTY: process.stderr.isTTY });
|
|
473
|
+
const spinner = makeSpinner(sErr);
|
|
474
|
+
|
|
354
475
|
// Resolve base branch: --base=BRANCH or auto-detect main/master/develop
|
|
355
476
|
const baseArg = args.find(a => a.startsWith('--base='));
|
|
356
477
|
let baseBranch = baseArg ? baseArg.split('=')[1] : null;
|
|
478
|
+
|
|
479
|
+
spinner.update('Scanning branch…');
|
|
480
|
+
|
|
357
481
|
if (!baseBranch) {
|
|
358
482
|
for (const candidate of ['main', 'master', 'develop']) {
|
|
359
483
|
const r = execFn('git', ['rev-parse', '--verify', candidate], { encoding: 'utf8', cwd, timeout: 5_000 });
|
|
@@ -362,28 +486,41 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
362
486
|
baseBranch = baseBranch ?? 'main';
|
|
363
487
|
}
|
|
364
488
|
|
|
365
|
-
// Current branch name
|
|
366
489
|
const headResult = execFn('git', ['branch', '--show-current'], { encoding: 'utf8', cwd, timeout: 5_000 });
|
|
367
490
|
const headBranch = headResult.status === 0 ? (headResult.stdout.trim() || null) : null;
|
|
368
491
|
|
|
369
|
-
// Commits in range
|
|
370
492
|
const logResult = execFn('git', ['log', '--oneline', `${baseBranch}..HEAD`], { encoding: 'utf8', cwd, timeout: 10_000 });
|
|
371
493
|
const commitMessages = logResult.status === 0 ? (logResult.stdout ?? '') : '';
|
|
372
494
|
|
|
373
|
-
// Diff between base and HEAD
|
|
374
495
|
const diffResult = execFn('git', ['diff', `${baseBranch}..HEAD`], { encoding: 'utf8', cwd, timeout: 30_000 });
|
|
375
496
|
const diff = diffResult.status === 0 ? (diffResult.stdout || null) : null;
|
|
376
497
|
|
|
377
|
-
// Extract ticket keys from branch name + commit messages
|
|
378
498
|
const allText = [headBranch ?? '', commitMessages].join('\n');
|
|
379
499
|
const ticketKeys = extractTicketKeys(allText);
|
|
380
500
|
|
|
381
|
-
// Fetch tickets (best-effort — gracefully degrade if no credentials)
|
|
382
501
|
const profileArgR = args.find(a => a.startsWith('--profile='));
|
|
383
502
|
const profileNameR = profileArgR ? profileArgR.split('=')[1] : undefined;
|
|
503
|
+
|
|
504
|
+
// Validate explicit --profile exists before attempting any fetch
|
|
505
|
+
if (profileNameR) {
|
|
506
|
+
const profiles = loadProfiles(resolvedConfigDir);
|
|
507
|
+
if (!profiles?.profiles?.[profileNameR]) {
|
|
508
|
+
spinner.done();
|
|
509
|
+
process.stderr.write(`${sErr.red('✖')} Profile "${profileNameR}" not found.\n`);
|
|
510
|
+
const names = Object.keys(profiles?.profiles ?? {});
|
|
511
|
+
if (names.length > 0) process.stderr.write(` ${sErr.dim('Available:')} ${names.join(', ')}\n`);
|
|
512
|
+
else process.stderr.write(` Run ${sErr.cyan('ticketlens init')} to configure a profile.\n`);
|
|
513
|
+
process.exitCode = 1;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Fetch tickets (best-effort — collect notes/warnings, write after spinner stops)
|
|
384
519
|
const tickets = [];
|
|
520
|
+
const stderrNotes = [];
|
|
385
521
|
|
|
386
522
|
if (ticketKeys.length > 0) {
|
|
523
|
+
spinner.update(`Fetching ticket context (${ticketKeys.length} key${ticketKeys.length > 1 ? 's' : ''})…`);
|
|
387
524
|
const connR = resolveConnection(ticketKeys[0], {
|
|
388
525
|
env,
|
|
389
526
|
configDir: resolvedConfigDir,
|
|
@@ -398,15 +535,24 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
398
535
|
for (const key of ticketKeys) {
|
|
399
536
|
try {
|
|
400
537
|
tickets.push(await adapterR.fetchTicket(key, { depth: 0 }));
|
|
401
|
-
} catch {
|
|
538
|
+
} catch (err) {
|
|
539
|
+
stderrNotes.push(` ${sErr.yellow('⚠')} ${key}: ${err.message ?? 'fetch failed'}`);
|
|
540
|
+
}
|
|
402
541
|
}
|
|
403
542
|
} else {
|
|
404
|
-
|
|
405
|
-
`
|
|
543
|
+
stderrNotes.push(
|
|
544
|
+
` ${sErr.yellow('○')} Found tickets [${ticketKeys.join(', ')}] but no profile configured. Run 'ticketlens init' to set up.`
|
|
406
545
|
);
|
|
407
546
|
}
|
|
547
|
+
} else if (profileNameR) {
|
|
548
|
+
stderrNotes.push(
|
|
549
|
+
` ${sErr.dim('Note:')} --profile=${profileNameR} specified but no ticket keys found in branch or commits.`
|
|
550
|
+
);
|
|
408
551
|
}
|
|
409
552
|
|
|
553
|
+
spinner.done();
|
|
554
|
+
for (const note of stderrNotes) process.stderr.write(note + '\n');
|
|
555
|
+
|
|
410
556
|
const isLic = opts.isLicensedFn ?? ((tier) => isLicensed(tier, resolvedConfigDir));
|
|
411
557
|
const assembleFn = opts.assemblePrReviewFn ?? assemblePrReview;
|
|
412
558
|
|
|
@@ -419,7 +565,9 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
419
565
|
analyzeDiffFn: analyzeDiff,
|
|
420
566
|
extractRequirementsFn: extractRequirements,
|
|
421
567
|
});
|
|
422
|
-
|
|
568
|
+
|
|
569
|
+
const sOut = createStyler({ isTTY: process.stdout.isTTY });
|
|
570
|
+
printFn((sOut.enabled ? styleReviewMd(md, sOut) : md) + '\n');
|
|
423
571
|
return;
|
|
424
572
|
}
|
|
425
573
|
|
|
@@ -82,6 +82,10 @@ export function parseCommand(args) {
|
|
|
82
82
|
return { command: 'compliance', args: args.slice(1) };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (first === 'review') {
|
|
86
|
+
return { command: 'review', args: args.slice(1) };
|
|
87
|
+
}
|
|
88
|
+
|
|
85
89
|
if (first === 'login') {
|
|
86
90
|
return { command: 'login', args: args.slice(1) };
|
|
87
91
|
}
|