ticketlens 0.37.0 → 0.38.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
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": {
@@ -6,15 +6,31 @@ import { findLinkedCommits } from './commit-linker.mjs';
6
6
  import { analyzeDiff } from './diff-analyzer.mjs';
7
7
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
8
8
  import { appendLedger } from './ledger.mjs';
9
+ import { createStyler } from './ansi.mjs';
9
10
 
10
11
  export const STATUS_ICON = { FOUND: '✔', PARTIAL: '~', NOT_FOUND: '✖' };
11
12
 
12
- function formatReport({ ticketKey, requirements, analysis, usage, isPro }) {
13
+ export function statusColor(status, s) {
14
+ if (status === 'FOUND') return s.green;
15
+ if (status === 'PARTIAL') return s.yellow;
16
+ return s.dim;
17
+ }
18
+
19
+ // Shared with matchColor in ticket-command.mjs (duplicates' match-confidence
20
+ // tiers) — same 70/50 thresholds, same green/yellow/dim vocabulary, applied
21
+ // here to overall requirement coverage instead of a single match score.
22
+ function coverageColor(pct, s) {
23
+ if (pct >= 70) return s.green;
24
+ if (pct >= 50) return s.yellow;
25
+ return s.dim;
26
+ }
27
+
28
+ function formatReport({ ticketKey, requirements, analysis, usage, isPro, s }) {
13
29
  const { results, coveragePercent } = analysis;
14
30
  const lines = [
15
31
  '',
16
- ` Compliance Check — ${ticketKey}`,
17
- ` ${'─'.repeat(50)}`,
32
+ ` Compliance Check — ${s.brand(s.bold(ticketKey))}`,
33
+ ` ${s.dim('─'.repeat(50))}`,
18
34
  '',
19
35
  ];
20
36
 
@@ -26,14 +42,15 @@ function formatReport({ ticketKey, requirements, analysis, usage, isPro }) {
26
42
  }
27
43
 
28
44
  for (const { requirement, status, evidence } of results) {
29
- const icon = STATUS_ICON[status] ?? '?';
45
+ const icon = statusColor(status, s)(STATUS_ICON[status] ?? '?');
30
46
  lines.push(` ${icon} ${requirement}`);
31
- if (evidence) lines.push(` └─ ${evidence}`);
47
+ if (evidence) lines.push(` └─ ${s.dim(evidence)}`);
32
48
  lines.push('');
33
49
  }
34
50
 
35
51
  lines.push('');
36
- lines.push(` Coverage: ${coveragePercent}% (${results.filter(r => r.status === 'FOUND').length}/${results.length} requirements found)`);
52
+ const found = results.filter(r => r.status === 'FOUND').length;
53
+ lines.push(` Coverage: ${coverageColor(coveragePercent, s)(`${coveragePercent}%`)} (${found}/${results.length} requirements found)`);
37
54
  lines.push('');
38
55
 
39
56
  if (!isPro) {
@@ -52,6 +69,12 @@ export async function runComplianceCheck({
52
69
  ticketKey,
53
70
  configDir = DEFAULT_CONFIG_DIR,
54
71
  stream = process.stderr,
72
+ // Separate from `stream` (stderr, only used for the Pro-upgrade nudge
73
+ // above) — the report string itself is printed to stdout by the caller
74
+ // (fetch-ticket.mjs), so styling must gate on stdout's TTY-ness, not
75
+ // stderr's, or ANSI codes would leak into `tl compliance X > out.txt`
76
+ // while stderr stays attached to a real terminal.
77
+ outStream = process.stdout,
55
78
  isLicensedFn = isLicensed,
56
79
  showUpgradeFn = showUpgradePrompt,
57
80
  checkUsageFn = checkUsage,
@@ -78,7 +101,8 @@ export async function runComplianceCheck({
78
101
  const { diff } = findLinkedCommitsFn(ticketKey, { cwd: process.cwd() });
79
102
  const analysis = analyzeDiffFn(requirements, diff);
80
103
 
81
- const report = formatReport({ ticketKey, requirements, analysis, usage, isPro });
104
+ const s = createStyler({ isTTY: outStream.isTTY });
105
+ const report = formatReport({ ticketKey, requirements, analysis, usage, isPro, s });
82
106
  const coveragePercent = analysis.coveragePercent;
83
107
  const missing = analysis.results
84
108
  .filter(r => r.status === 'NOT_FOUND')
@@ -8,8 +8,9 @@ import { spawnSync } from 'node:child_process';
8
8
  import { fetchTicket } from './jira-client.mjs';
9
9
  import { extractRequirements } from './requirement-extractor.mjs';
10
10
  import { findLinkedCommits } from './commit-linker.mjs';
11
- import { runComplianceCheck, STATUS_ICON } from './compliance-checker.mjs';
11
+ import { runComplianceCheck, STATUS_ICON, statusColor } from './compliance-checker.mjs';
12
12
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
13
+ import { createStyler } from './ansi.mjs';
13
14
 
14
15
  /**
15
16
  * Detect the git remote URL using execFn.
@@ -34,15 +35,16 @@ function detectRemoteUrl(execFn) {
34
35
  * Build the "### Linked tickets" section from a ticket's linkedIssues.
35
36
  *
36
37
  * @param {Array} linkedIssues - array of { key, summary, linkType, direction }
38
+ * @param {object} s - styler from createStyler()
37
39
  * @returns {string} section markdown or empty string
38
40
  */
39
- function buildLinkedTicketsSection(linkedIssues) {
41
+ function buildLinkedTicketsSection(linkedIssues, s) {
40
42
  if (!linkedIssues || linkedIssues.length === 0) return '';
41
43
 
42
44
  const lines = ['', '### Linked tickets'];
43
45
  for (const issue of linkedIssues) {
44
46
  const rel = issue.linkType ?? issue.direction ?? 'Linked';
45
- lines.push(`- ${issue.key}: ${rel} — ${issue.summary}`);
47
+ lines.push(`- ${s.brand(s.bold(issue.key))}: ${rel} — ${issue.summary}`);
46
48
  }
47
49
  return lines.join('\n');
48
50
  }
@@ -52,9 +54,10 @@ function buildLinkedTicketsSection(linkedIssues) {
52
54
  *
53
55
  * @param {object|null} complianceResult - result from runComplianceCheckFn or null
54
56
  * @param {string[]} requirements - raw requirements list
57
+ * @param {object} s - styler from createStyler()
55
58
  * @returns {string} section markdown
56
59
  */
57
- function buildCoverageSection(complianceResult, requirements) {
60
+ function buildCoverageSection(complianceResult, requirements, s) {
58
61
  if (complianceResult === null) {
59
62
  const lines = ['', '### Requirements coverage (coverage unavailable — Pro required)'];
60
63
  for (const req of requirements) {
@@ -67,7 +70,7 @@ function buildCoverageSection(complianceResult, requirements) {
67
70
  const lines = [``, `### Requirements coverage (${coveragePercent}%)`];
68
71
 
69
72
  for (const { requirement, status, evidence } of results) {
70
- const icon = STATUS_ICON[status] ?? '?';
73
+ const icon = statusColor(status, s)(STATUS_ICON[status] ?? '?');
71
74
  const suffix = evidence ? ` (${evidence})` : '';
72
75
  lines.push(`- ${icon} ${requirement}${suffix}`);
73
76
  }
@@ -86,7 +89,8 @@ function buildCoverageSection(complianceResult, requirements) {
86
89
  * @param {Function} [opts.findLinkedCommitsFn] - async fn(ticketKey, opts) → commit[]
87
90
  * @param {Function} [opts.runComplianceCheckFn] - async fn({ brief, ticketKey, configDir }) → result|null
88
91
  * @param {Function} [opts.execFn] - spawnSync-compatible for git remote detection
89
- * @param {object} [opts.stream] - stderr for progress output (optional)
92
+ * @param {object} [opts.outStream] - stdout — where this markdown is ultimately printed
93
+ * (`tl pr KEY | pbcopy` pipes stdout), so styling gates on its TTY-ness, not stderr's
90
94
  * @returns {Promise<string>} markdown PR description
91
95
  */
92
96
  export async function assemblePr(ticketKey, {
@@ -97,8 +101,9 @@ export async function assemblePr(ticketKey, {
97
101
  findLinkedCommitsFn = findLinkedCommits,
98
102
  runComplianceCheckFn = runComplianceCheck,
99
103
  execFn = spawnSync,
100
- stream,
104
+ outStream = process.stdout,
101
105
  } = {}) {
106
+ const s = createStyler({ isTTY: outStream.isTTY });
102
107
  // Fetch ticket data
103
108
  const ticket = await fetchTicketFn(ticketKey, { configDir });
104
109
 
@@ -130,7 +135,7 @@ export async function assemblePr(ticketKey, {
130
135
  );
131
136
 
132
137
  // Build output
133
- const lines = [`## ${ticketKey}: ${summary}`, ''];
138
+ const lines = [`## ${s.brand(s.bold(ticketKey))}: ${summary}`, ''];
134
139
 
135
140
  // What changed — commits are raw git --oneline strings: "<sha> <message>"
136
141
  lines.push('### What changed');
@@ -143,7 +148,7 @@ export async function assemblePr(ticketKey, {
143
148
  }
144
149
 
145
150
  // Requirements coverage
146
- lines.push(buildCoverageSection(complianceResult, requirements));
151
+ lines.push(buildCoverageSection(complianceResult, requirements, s));
147
152
 
148
153
  // Acceptance criteria
149
154
  lines.push('');
@@ -157,7 +162,7 @@ export async function assemblePr(ticketKey, {
157
162
  }
158
163
 
159
164
  // Linked tickets (optional section)
160
- const linkedSection = buildLinkedTicketsSection(linkedIssues);
165
+ const linkedSection = buildLinkedTicketsSection(linkedIssues, s);
161
166
  if (linkedSection) {
162
167
  lines.push(linkedSection);
163
168
  }