ticketlens 0.9.10 → 0.9.11

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.9.10",
3
+ "version": "0.9.11",
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": {
@@ -83,8 +83,19 @@ export async function downloadAttachments(ticket, opts = {}) {
83
83
  onProgress?.(` download ${a.filename}`);
84
84
  try {
85
85
  const headers = buildAuthHeader(env);
86
- // redirect:'error' prevents a Jira redirect from leaking auth headers to a different origin.
87
- const response = await fetcher(a.content, { headers, redirect: 'error' });
86
+ let response;
87
+ try {
88
+ // Prefer no-redirect so auth headers stay on the Jira origin.
89
+ response = await fetcher(a.content, { headers, redirect: 'error' });
90
+ } catch (err) {
91
+ // Jira Cloud attachment URLs sometimes redirect to CDN/S3 presigned URLs.
92
+ // On redirect, retry without auth — the presigned URL provides its own authentication.
93
+ if (err?.cause?.message?.includes('redirect mode is set to error')) {
94
+ response = await fetcher(a.content, { redirect: 'follow' });
95
+ } else {
96
+ throw err;
97
+ }
98
+ }
88
99
  if (!response.ok) throw new Error(`HTTP ${response.status} (${response.statusText})`);
89
100
  const buffer = await response.arrayBuffer();
90
101
  fs.writeFileSync(localPath, Buffer.from(buffer));
@@ -8,6 +8,7 @@ const RE_GWT = /^\s*(given|when|then)\s+(.+)/i;
8
8
  const RE_MUST_ITEM = /^\s*[-*•]\s+((?:must|should|shall|ensure|verify)\b.+|.+(?:must|should|shall|ensure|verify).+)/i;
9
9
  const RE_NUM_MUST = /^\s*\d+\.\s+((?:must|should|shall|ensure|verify)\b.+|.+(?:must|should|shall|ensure|verify).+)/i;
10
10
  const RE_AC_HEADER = /^\s*(?:#+\s*|h[1-6]\.\s*)?acceptance criteria\s*:?\s*$/i;
11
+ const RE_HEADING = /^\s*(?:#+|h[1-6]\.)\s+/i;
11
12
  const RE_BULLET = /^\s*[-*•]\s+(.+)/;
12
13
  const RE_NUM_ITEM = /^\s*\d+\.\s+(.+)/;
13
14
 
@@ -19,32 +20,30 @@ export function extractRequirements(text) {
19
20
  let inAcSection = false;
20
21
 
21
22
  for (const line of lines) {
22
- // Given/When/Then
23
23
  const gwt = RE_GWT.exec(line);
24
24
  if (gwt) { results.push(line.trim()); continue; }
25
25
 
26
- // Acceptance Criteria section header
27
26
  if (RE_AC_HEADER.test(line)) { inAcSection = true; continue; }
28
27
 
29
- // Exit AC section on next heading
30
- if (inAcSection && /^\s*#+\s/.test(line) && !RE_AC_HEADER.test(line)) {
28
+ // Exit on any explicit heading (markdown ## or wiki h2.) that is not the AC header
29
+ if (inAcSection && RE_HEADING.test(line) && !RE_AC_HEADER.test(line)) {
31
30
  inAcSection = false;
32
31
  }
33
32
 
34
- // must/should/shall in bullet
35
33
  const mustItem = RE_MUST_ITEM.exec(line);
36
34
  if (mustItem) { results.push(mustItem[1].trim()); continue; }
37
35
 
38
- // must/should/shall in numbered item
39
36
  const numMust = RE_NUM_MUST.exec(line);
40
37
  if (numMust) { results.push(numMust[1].trim()); continue; }
41
38
 
42
- // Inside AC section: capture all bullet and numbered items
43
39
  if (inAcSection) {
44
40
  const bullet = RE_BULLET.exec(line);
45
41
  if (bullet) { results.push(bullet[1].trim()); continue; }
46
42
  const numItem = RE_NUM_ITEM.exec(line);
47
43
  if (numItem) { results.push(numItem[1].trim()); continue; }
44
+ // Capture plain sentences — ADF paragraph nodes output as bare text (no list marker)
45
+ const plain = line.trim();
46
+ if (plain) { results.push(plain); continue; }
48
47
  }
49
48
  }
50
49