ticketlens 0.4.2 → 0.4.5
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/README.md +9 -3
- package/bin/ticketlens.mjs +12 -7
- package/package.json +1 -1
- package/skills/jtb/scripts/fetch-my-tickets.mjs +23 -22
- package/skills/jtb/scripts/lib/brief-cache.mjs +9 -0
- package/skills/jtb/scripts/lib/ledger.mjs +21 -2
- package/skills/jtb/scripts/lib/run-collisions.mjs +16 -13
- package/skills/jtb/scripts/lib/schedule-wizard.mjs +28 -6
- package/skills/jtb/scripts/lib/ticket-payload.mjs +19 -0
- package/skills/jtb/scripts/lib/triage-push.mjs +13 -25
- package/skills/jtb/scripts/lib/triage-share.mjs +10 -5
package/README.md
CHANGED
|
@@ -350,8 +350,12 @@ ticketlens sync # Pull your latest tracker profiles from the Console
|
|
|
350
350
|
|
|
351
351
|
Use `--manual` when there is no GUI (CI runners, SSH sessions, containers).
|
|
352
352
|
|
|
353
|
+
`ticketlens logout` removes the stored Console auth token and disconnects this machine from your TicketLens account. Local Jira profiles and credentials are kept intact — re-run `ticketlens login` to reconnect.
|
|
354
|
+
|
|
353
355
|
`ticketlens sync` pulls any tracker profiles you have configured in the Console and writes them locally, keeping your CLI in sync with your team settings without re-running `init`.
|
|
354
356
|
|
|
357
|
+
> **Console features** — `ticketlens triage --push`, `--share`, `ticketlens collisions`, and `ticketlens schedule` all require an active Console session. Run `ticketlens login` once and the token is stored automatically.
|
|
358
|
+
|
|
355
359
|
---
|
|
356
360
|
|
|
357
361
|
### License
|
|
@@ -587,7 +591,7 @@ ticketlens CNV1-2 --summarize # AI summary via your own API key (BYOK
|
|
|
587
591
|
ticketlens CNV1-2 --summarize --cloud # AI summary via TicketLens API (no local key needed)
|
|
588
592
|
ticketlens CNV1-2 --handoff # AI handoff brief from the ticket's comment thread (BYOK)
|
|
589
593
|
ticketlens CNV1-2 --handoff --cloud # AI handoff brief via TicketLens API
|
|
590
|
-
ticketlens CNV1-2 --compliance # Check ticket requirements against local diff [Free 3/mo]
|
|
594
|
+
ticketlens CNV1-2 --compliance # Check ticket requirements against local diff [Pro/Free 3/mo]
|
|
591
595
|
ticketlens triage --stale=3 # Custom stale threshold (default is 5)
|
|
592
596
|
ticketlens triage --digest # POST scored triage results to digest endpoint
|
|
593
597
|
ticketlens schedule # Set up a scheduled daily digest
|
|
@@ -735,11 +739,13 @@ npm test
|
|
|
735
739
|
See [ROADMAP.md](ROADMAP.md) for the full plan.
|
|
736
740
|
|
|
737
741
|
Recently shipped:
|
|
738
|
-
- **
|
|
742
|
+
- **Collision detection** (`ticketlens collisions`) — shows which files your branch shares with teammates' in-flight branches; `--push` auto-sends git branch data to the team snapshot. Team tier
|
|
743
|
+
- **Shareable triage snapshot** (`ticketlens triage --share`) — generates a 24h signed URL; recipients open it in any browser, no account needed. Team tier
|
|
744
|
+
- **Compliance push** (`ticketlens triage --push`) — enriches the team snapshot with per-ticket compliance status and coverage from the local ledger. Pro tier
|
|
745
|
+
- **"TicketLens for PRs"** (`ticketlens review`) — assembles a code-review context brief from your current branch: linked tickets, changed files, and ticket context in one brief
|
|
739
746
|
- **Confluence pages** — linked Confluence pages fetched automatically and included in the brief; origin-validated, non-fatal, capped at 10 pages
|
|
740
747
|
- **Linear support** — `ticketlens init` → Linear; connects via GraphQL API key, fetches tickets, triage, and statuses
|
|
741
748
|
- **GitHub Issues support** — `ticketlens init` → GitHub Issues; PAT-based, same normalized ticket shape
|
|
742
|
-
- **Tracker-aware config** — `ticketlens config` shows the right labels and skips irrelevant prompts per tracker type; always re-validates the connection
|
|
743
749
|
|
|
744
750
|
---
|
|
745
751
|
|
package/bin/ticketlens.mjs
CHANGED
|
@@ -203,13 +203,17 @@ switch (command) {
|
|
|
203
203
|
process.stdin.setRawMode(true);
|
|
204
204
|
process.stdin.resume();
|
|
205
205
|
process.stdin.setEncoding('utf8');
|
|
206
|
-
process.stdin.
|
|
207
|
-
|
|
208
|
-
process.stdin.
|
|
206
|
+
const cleanup = () => { process.stdin.setRawMode(false); process.stdin.pause(); };
|
|
207
|
+
const onData = char => {
|
|
208
|
+
process.stdin.removeListener('end', onEnd);
|
|
209
|
+
cleanup();
|
|
209
210
|
process.stderr.write('\n');
|
|
210
211
|
if (char === '\x03') process.exit(0);
|
|
211
212
|
res(char === 'y' || char === 'Y');
|
|
212
|
-
}
|
|
213
|
+
};
|
|
214
|
+
const onEnd = () => { process.stdin.removeListener('data', onData); cleanup(); res(false); };
|
|
215
|
+
process.stdin.once('data', onData);
|
|
216
|
+
process.stdin.once('end', onEnd);
|
|
213
217
|
});
|
|
214
218
|
if (!answer) {
|
|
215
219
|
process.stderr.write(` ${s.dim('Cancelled.')}\n`);
|
|
@@ -258,13 +262,13 @@ switch (command) {
|
|
|
258
262
|
|
|
259
263
|
if (subCmd === '--stop') {
|
|
260
264
|
const { runScheduleStop } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
|
|
261
|
-
await runScheduleStop({
|
|
265
|
+
await runScheduleStop({ cliToken: readCliToken() });
|
|
262
266
|
break;
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
if (subCmd === '--status') {
|
|
266
270
|
const { runScheduleStatus } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
|
|
267
|
-
await runScheduleStatus({
|
|
271
|
+
await runScheduleStatus({ cliToken: readCliToken() });
|
|
268
272
|
break;
|
|
269
273
|
}
|
|
270
274
|
|
|
@@ -277,7 +281,8 @@ switch (command) {
|
|
|
277
281
|
const { runScheduleWizard } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
|
|
278
282
|
const { promptScheduleAnswers } = await import('../skills/jtb/scripts/lib/prompt-helpers.mjs');
|
|
279
283
|
const answers = await promptScheduleAnswers(cmdArgs);
|
|
280
|
-
const result = await runScheduleWizard({ answers,
|
|
284
|
+
const result = await runScheduleWizard({ answers, cliToken: readCliToken() });
|
|
285
|
+
if (!result.ok) { process.exitCode = 1; break; }
|
|
281
286
|
|
|
282
287
|
process.stdout.write(`✔ Digest scheduled for ${answers.time} ${answers.timezone}\n`);
|
|
283
288
|
process.stdout.write(` Next delivery: ${result.nextDelivery}\n`);
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import { promptProfileSelect } from './lib/profile-picker.mjs';
|
|
|
18
18
|
import { printTriageHelp } from './lib/help.mjs';
|
|
19
19
|
import { handleUnknownFlags } from './lib/arg-validator.mjs';
|
|
20
20
|
import { isLicensed, showUpgradePrompt, revalidateIfStale, readLicense } from './lib/license.mjs';
|
|
21
|
+
import { readCliToken } from './lib/cli-auth.mjs';
|
|
21
22
|
|
|
22
23
|
const DEFAULT_STATUSES = ['In Progress', 'Code Review', 'QA'];
|
|
23
24
|
|
|
@@ -334,37 +335,20 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
334
335
|
return;
|
|
335
336
|
}
|
|
336
337
|
|
|
337
|
-
//
|
|
338
|
-
const wantInteractive = process.stdout.isTTY && !args.includes('--plain') && !args.includes('--static');
|
|
339
|
-
if (wantInteractive && process.stdin.setRawMode) {
|
|
340
|
-
const result = await runInteractiveList(sorted, { baseUrl: conn.baseUrl, staleDays, styled: true });
|
|
341
|
-
if (result === 'switch') {
|
|
342
|
-
const cleanArgs = args.filter(a => !a.startsWith('--profile=') && !a.startsWith('--project='));
|
|
343
|
-
return run(cleanArgs, { ...opts, env, fetcher, configDir });
|
|
344
|
-
}
|
|
345
|
-
// Fall through to push/share if those flags were passed
|
|
346
|
-
if (!pushFlag && !shareFlag) return;
|
|
347
|
-
} else {
|
|
348
|
-
const useStyled = args.includes('--styled') || (!args.includes('--plain') && process.stdout.isTTY);
|
|
349
|
-
const summary = useStyled
|
|
350
|
-
? styleTriageSummary(sorted, { styled: true, staleDays, baseUrl: conn.baseUrl })
|
|
351
|
-
: assembleTriageSummary(sorted, { staleDays, baseUrl: conn.baseUrl });
|
|
352
|
-
process.stdout.write(summary + '\n');
|
|
353
|
-
}
|
|
354
|
-
|
|
338
|
+
// push/share run first so their result is visible before (or instead of) the TUI
|
|
355
339
|
if (pushFlag) {
|
|
356
340
|
const { pushTriageSnapshot } = await import('./lib/triage-push.mjs');
|
|
357
341
|
const { scanCurrentBranch } = await import('./lib/branch-scanner.mjs');
|
|
358
342
|
const pushFn = opts.pushFn ?? pushTriageSnapshot;
|
|
359
343
|
const scanFn = opts.scanFn ?? scanCurrentBranch;
|
|
360
|
-
const
|
|
344
|
+
const cliToken = opts.cliToken ?? readCliToken(configDir) ?? null;
|
|
361
345
|
const printFn = opts.print ?? ((s) => process.stdout.write(s));
|
|
362
346
|
await pushFn({
|
|
363
347
|
sorted,
|
|
364
348
|
rawTicketMap,
|
|
365
349
|
profile: profileName ?? 'default',
|
|
366
350
|
baseUrl: conn.baseUrl,
|
|
367
|
-
|
|
351
|
+
cliToken,
|
|
368
352
|
gitBranches: scanFn(),
|
|
369
353
|
fetcher,
|
|
370
354
|
print: printFn,
|
|
@@ -374,18 +358,35 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
374
358
|
if (shareFlag) {
|
|
375
359
|
const { shareTriageSnapshot } = await import('./lib/triage-share.mjs');
|
|
376
360
|
const shareFn = opts.shareFn ?? shareTriageSnapshot;
|
|
377
|
-
const
|
|
361
|
+
const cliToken = opts.cliToken ?? readCliToken(configDir) ?? null;
|
|
378
362
|
const printFn = opts.print ?? ((s) => process.stdout.write(s));
|
|
379
363
|
await shareFn({
|
|
380
364
|
sorted,
|
|
381
365
|
rawTicketMap,
|
|
382
366
|
profile: profileName ?? 'default',
|
|
383
367
|
baseUrl: conn.baseUrl,
|
|
384
|
-
|
|
368
|
+
cliToken,
|
|
385
369
|
fetcher,
|
|
386
370
|
print: printFn,
|
|
387
371
|
});
|
|
388
372
|
}
|
|
373
|
+
|
|
374
|
+
// Interactive mode: TTY + not --plain + not --static
|
|
375
|
+
const wantInteractive = process.stdout.isTTY && !args.includes('--plain') && !args.includes('--static');
|
|
376
|
+
if (wantInteractive && process.stdin.setRawMode) {
|
|
377
|
+
const result = await runInteractiveList(sorted, { baseUrl: conn.baseUrl, staleDays, styled: true });
|
|
378
|
+
if (result === 'switch') {
|
|
379
|
+
const cleanArgs = args.filter(a => !a.startsWith('--profile=') && !a.startsWith('--project='));
|
|
380
|
+
return run(cleanArgs, { ...opts, env, fetcher, configDir });
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const useStyled = args.includes('--styled') || (!args.includes('--plain') && process.stdout.isTTY);
|
|
386
|
+
const summary = useStyled
|
|
387
|
+
? styleTriageSummary(sorted, { styled: true, staleDays, baseUrl: conn.baseUrl })
|
|
388
|
+
: assembleTriageSummary(sorted, { staleDays, baseUrl: conn.baseUrl });
|
|
389
|
+
process.stdout.write(summary + '\n');
|
|
389
390
|
}
|
|
390
391
|
|
|
391
392
|
// Run if invoked directly
|
|
@@ -82,11 +82,19 @@ export function clearBriefCache(ticketKey, profileName, configDir = DEFAULT_CONF
|
|
|
82
82
|
} catch { /* non-fatal */ }
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Memoize getBriefCacheEntries per configDir within a single process invocation.
|
|
86
|
+
// 5s TTL is well beyond the lifetime of any CLI run; prevents repeated stat+read+parse
|
|
87
|
+
// when multiple callers (cache list, cache prune, cache status) hit the same directory.
|
|
88
|
+
const _entriesCache = new Map(); // configDir → { entries, time }
|
|
89
|
+
const _ENTRIES_TTL_MS = 5_000;
|
|
90
|
+
|
|
85
91
|
/**
|
|
86
92
|
* Returns all brief cache entries across all profiles.
|
|
87
93
|
* Each entry: { profileName, ticketKey, filePath, size, mtimeMs, fetchedAt, depth }
|
|
88
94
|
*/
|
|
89
95
|
export function getBriefCacheEntries(configDir = DEFAULT_CONFIG_DIR) {
|
|
96
|
+
const cached = _entriesCache.get(configDir);
|
|
97
|
+
if (cached && Date.now() - cached.time < _ENTRIES_TTL_MS) return cached.entries;
|
|
90
98
|
const cacheDir = path.join(configDir, 'cache');
|
|
91
99
|
if (!fs.existsSync(cacheDir)) return [];
|
|
92
100
|
|
|
@@ -120,6 +128,7 @@ export function getBriefCacheEntries(configDir = DEFAULT_CONFIG_DIR) {
|
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
|
|
131
|
+
_entriesCache.set(configDir, { entries, time: Date.now() });
|
|
123
132
|
return entries;
|
|
124
133
|
}
|
|
125
134
|
|
|
@@ -8,8 +8,9 @@ import { join } from 'node:path';
|
|
|
8
8
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
9
9
|
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
10
10
|
|
|
11
|
-
const LEDGER_FILE
|
|
12
|
-
const KEY_FILE
|
|
11
|
+
const LEDGER_FILE = 'ledger.jsonl';
|
|
12
|
+
const KEY_FILE = 'ledger-key';
|
|
13
|
+
const MAX_LEDGER_RECORDS = 5_000;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Append one compliance record to ledger.jsonl.
|
|
@@ -25,6 +26,7 @@ export function appendLedger(record, { configDir = DEFAULT_CONFIG_DIR, fsModule
|
|
|
25
26
|
|
|
26
27
|
const line = JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n';
|
|
27
28
|
fsModule.appendFileSync(join(configDir, LEDGER_FILE), line, 'utf8');
|
|
29
|
+
_rotateIfNeeded(configDir, fsModule);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -84,6 +86,23 @@ export function exportLedger(format, { configDir = DEFAULT_CONFIG_DIR, fsModule
|
|
|
84
86
|
|
|
85
87
|
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
86
88
|
|
|
89
|
+
function _rotateIfNeeded(configDir, fsModule) {
|
|
90
|
+
const ledgerPath = join(configDir, LEDGER_FILE);
|
|
91
|
+
// Cheap size check first — each JSONL record averages ~180 bytes, so 5000 records ≈ 900 KB.
|
|
92
|
+
// Skip the full read when the file is well under the threshold.
|
|
93
|
+
try {
|
|
94
|
+
if (fsModule.statSync(ledgerPath).size < 900_000) return;
|
|
95
|
+
} catch { return; }
|
|
96
|
+
|
|
97
|
+
let raw;
|
|
98
|
+
try { raw = fsModule.readFileSync(ledgerPath, 'utf8'); } catch { return; }
|
|
99
|
+
|
|
100
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
101
|
+
if (lines.length > MAX_LEDGER_RECORDS) {
|
|
102
|
+
fsModule.writeFileSync(ledgerPath, lines.slice(-MAX_LEDGER_RECORDS).join('\n') + '\n', 'utf8');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
function _getOrCreateKey(configDir, fsModule) {
|
|
88
107
|
const keyPath = join(configDir, KEY_FILE);
|
|
89
108
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readCliToken } from './cli-auth.mjs';
|
|
2
2
|
import { formatCollisions } from './collision-reporter.mjs';
|
|
3
|
+
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
3
4
|
|
|
4
5
|
import { apiBase, warnIfInsecure } from './api-utils.mjs';
|
|
5
6
|
|
|
@@ -10,35 +11,37 @@ const COLLISIONS_PATH = '/v1/triage/collisions';
|
|
|
10
11
|
*
|
|
11
12
|
* @param {string[]} args CLI argument array (e.g. ['--json', '--plain'])
|
|
12
13
|
* @param {object} [opts]
|
|
13
|
-
* @param {Function} [opts.fetcher]
|
|
14
|
-
* @param {Function} [opts.print]
|
|
15
|
-
* @param {Function} [opts.
|
|
14
|
+
* @param {Function} [opts.fetcher] Injectable fetch
|
|
15
|
+
* @param {Function} [opts.print] Output function
|
|
16
|
+
* @param {Function} [opts.readCliTokenFn] Injectable CLI token reader
|
|
17
|
+
* @param {string} [opts.configDir] Config directory override
|
|
16
18
|
* @returns {Promise<{ ok: boolean, status?: number }>}
|
|
17
19
|
*/
|
|
18
20
|
export async function runCollisions(args = [], opts = {}) {
|
|
19
21
|
const jsonFlag = args.includes('--json');
|
|
20
22
|
const plainFlag = args.includes('--plain');
|
|
21
|
-
const fetcher = opts.fetcher
|
|
22
|
-
const print = opts.print
|
|
23
|
-
const warn = opts.warn
|
|
24
|
-
const
|
|
23
|
+
const fetcher = opts.fetcher ?? globalThis.fetch;
|
|
24
|
+
const print = opts.print ?? ((s) => process.stdout.write(s));
|
|
25
|
+
const warn = opts.warn ?? ((s) => process.stderr.write(s));
|
|
26
|
+
const configDir = opts.configDir ?? DEFAULT_CONFIG_DIR;
|
|
27
|
+
const readCliTokenFn = opts.readCliTokenFn ?? ((dir) => readCliToken(dir));
|
|
25
28
|
warnIfInsecure(apiBase(), warn);
|
|
26
29
|
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
print('✗ collisions requires
|
|
30
|
+
const cliToken = readCliTokenFn(configDir) ?? null;
|
|
31
|
+
if (!cliToken) {
|
|
32
|
+
print('✗ collisions requires Console access. Run ticketlens login first.\n');
|
|
30
33
|
return { ok: false };
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
try {
|
|
34
37
|
const res = await fetcher(`${apiBase()}${COLLISIONS_PATH}`, {
|
|
35
|
-
headers: { Authorization: `Bearer ${
|
|
38
|
+
headers: { Authorization: `Bearer ${cliToken}` },
|
|
36
39
|
signal: AbortSignal.timeout(15_000),
|
|
37
40
|
});
|
|
38
41
|
|
|
39
42
|
if (!res.ok) {
|
|
40
43
|
if (res.status === 401) {
|
|
41
|
-
print('✗
|
|
44
|
+
print('✗ Session expired. Run ticketlens login to reconnect.\n');
|
|
42
45
|
return { ok: false, status: 401 };
|
|
43
46
|
}
|
|
44
47
|
if (res.status === 403) {
|
|
@@ -52,12 +52,17 @@ export function buildCronLine({ hour, minute, ticketlensBin }) {
|
|
|
52
52
|
export async function runScheduleWizard({
|
|
53
53
|
answers,
|
|
54
54
|
fetcher = globalThis.fetch,
|
|
55
|
-
|
|
55
|
+
cliToken,
|
|
56
56
|
configDir,
|
|
57
57
|
platform = osPlatform(),
|
|
58
58
|
writeLocalJob = defaultWriteLocalJob,
|
|
59
59
|
timeoutMs = 10_000,
|
|
60
|
+
print = s => process.stdout.write(s),
|
|
60
61
|
}) {
|
|
62
|
+
if (!cliToken) {
|
|
63
|
+
print('✗ schedule requires Console access. Run ticketlens login first.\n');
|
|
64
|
+
return { ok: false };
|
|
65
|
+
}
|
|
61
66
|
const { time, email, timezone } = answers;
|
|
62
67
|
const [hourStr, minuteStr] = time.split(':');
|
|
63
68
|
const hour = parseInt(hourStr, 10);
|
|
@@ -68,7 +73,7 @@ export async function runScheduleWizard({
|
|
|
68
73
|
signal: AbortSignal.timeout(timeoutMs),
|
|
69
74
|
headers: {
|
|
70
75
|
'Content-Type': 'application/json',
|
|
71
|
-
'Authorization': `Bearer ${
|
|
76
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
72
77
|
},
|
|
73
78
|
body: JSON.stringify({ email, timezone, deliverAt: time }),
|
|
74
79
|
});
|
|
@@ -91,11 +96,20 @@ export async function runScheduleWizard({
|
|
|
91
96
|
return data;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
export async function runScheduleStop({
|
|
99
|
+
export async function runScheduleStop({
|
|
100
|
+
fetcher = globalThis.fetch,
|
|
101
|
+
cliToken,
|
|
102
|
+
platform = osPlatform(),
|
|
103
|
+
print = s => process.stdout.write(s),
|
|
104
|
+
}) {
|
|
105
|
+
if (!cliToken) {
|
|
106
|
+
print('✗ schedule requires Console access. Run ticketlens login first.\n');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
95
109
|
const res = await fetcher(SCHEDULE_URL, {
|
|
96
110
|
method: 'DELETE',
|
|
97
111
|
signal: AbortSignal.timeout(10_000),
|
|
98
|
-
headers: { 'Authorization': `Bearer ${
|
|
112
|
+
headers: { 'Authorization': `Bearer ${cliToken}` },
|
|
99
113
|
});
|
|
100
114
|
if (!res.ok) throw new Error(`Schedule API error ${res.status}`);
|
|
101
115
|
|
|
@@ -114,11 +128,19 @@ export async function runScheduleStop({ fetcher = globalThis.fetch, licenseKey,
|
|
|
114
128
|
process.stdout.write('✔ Digest schedule removed.\n');
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
export async function runScheduleStatus({
|
|
131
|
+
export async function runScheduleStatus({
|
|
132
|
+
fetcher = globalThis.fetch,
|
|
133
|
+
cliToken,
|
|
134
|
+
print = s => process.stdout.write(s),
|
|
135
|
+
}) {
|
|
136
|
+
if (!cliToken) {
|
|
137
|
+
print('✗ schedule requires Console access. Run ticketlens login first.\n');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
118
140
|
const res = await fetcher(SCHEDULE_URL, {
|
|
119
141
|
method: 'GET',
|
|
120
142
|
signal: AbortSignal.timeout(10_000),
|
|
121
|
-
headers: { 'Authorization': `Bearer ${
|
|
143
|
+
headers: { 'Authorization': `Bearer ${cliToken}` },
|
|
122
144
|
});
|
|
123
145
|
if (!res.ok) {
|
|
124
146
|
process.stdout.write('No active digest schedule found.\n');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ticket payload builder used by triage-push and triage-share.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function buildTicketPayload(scored, rawMap, baseUrl) {
|
|
6
|
+
const raw = rawMap?.get(scored.ticketKey);
|
|
7
|
+
return {
|
|
8
|
+
key: scored.ticketKey,
|
|
9
|
+
summary: scored.summary ?? null,
|
|
10
|
+
status: scored.status ?? null,
|
|
11
|
+
assignee: raw?.assignee ?? null,
|
|
12
|
+
attention_score: null,
|
|
13
|
+
flags: scored.urgency === 'clear' ? [] : [scored.urgency],
|
|
14
|
+
compliance_coverage: null,
|
|
15
|
+
compliance_status: 'unknown',
|
|
16
|
+
url: baseUrl ? `${baseUrl}/browse/${scored.ticketKey}` : null,
|
|
17
|
+
last_updated: raw?.updated ?? null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import { readLedger } from './ledger.mjs';
|
|
7
7
|
import { isLicensed } from './license.mjs';
|
|
8
8
|
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
9
|
-
|
|
10
9
|
import { apiBase, warnIfInsecure } from './api-utils.mjs';
|
|
10
|
+
import { buildTicketPayload } from './ticket-payload.mjs';
|
|
11
11
|
|
|
12
12
|
const PUSH_PATH = '/v1/triage/push';
|
|
13
13
|
|
|
@@ -16,27 +16,10 @@ const PUSH_PATH = '/v1/triage/push';
|
|
|
16
16
|
// https://api.ticketlens.com → https://app.ticketlens.com/console/queue
|
|
17
17
|
export function queueUrl(base) {
|
|
18
18
|
const noApi = base.replace(/^(https?:\/\/)api\./, '$1');
|
|
19
|
-
|
|
20
|
-
const console = /ticketlens\.com/.test(noApi)
|
|
19
|
+
const consoleBase = /ticketlens\.com/.test(noApi)
|
|
21
20
|
? noApi.replace('://', '://app.')
|
|
22
21
|
: noApi;
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function buildTicketPayload(scored, rawMap, baseUrl) {
|
|
27
|
-
const raw = rawMap?.get(scored.ticketKey);
|
|
28
|
-
return {
|
|
29
|
-
key: scored.ticketKey,
|
|
30
|
-
summary: scored.summary ?? null,
|
|
31
|
-
status: scored.status ?? null,
|
|
32
|
-
assignee: raw?.assignee ?? null,
|
|
33
|
-
attention_score: null,
|
|
34
|
-
flags: scored.urgency === 'clear' ? [] : [scored.urgency],
|
|
35
|
-
compliance_coverage: null,
|
|
36
|
-
compliance_status: 'unknown',
|
|
37
|
-
url: baseUrl ? `${baseUrl}/browse/${scored.ticketKey}` : null,
|
|
38
|
-
last_updated: raw?.updated ?? null,
|
|
39
|
-
};
|
|
22
|
+
return consoleBase + '/console/queue';
|
|
40
23
|
}
|
|
41
24
|
|
|
42
25
|
/**
|
|
@@ -47,7 +30,7 @@ function buildTicketPayload(scored, rawMap, baseUrl) {
|
|
|
47
30
|
* @param {Map} [opts.rawTicketMap] - Map<key, normalizedTicket>
|
|
48
31
|
* @param {string} [opts.profile] - Resolved profile name (max 100 chars)
|
|
49
32
|
* @param {string} [opts.baseUrl] - Jira base URL for URL construction
|
|
50
|
-
* @param {string} [opts.
|
|
33
|
+
* @param {string} [opts.cliToken] - CLI session token for the API
|
|
51
34
|
* @param {string} [opts.capturedAt] - ISO 8601 timestamp (defaults to now)
|
|
52
35
|
* @param {Array} [opts.gitBranches] - Branch metadata from scanCurrentBranch (null = not in git repo)
|
|
53
36
|
* @param {Function} [opts.fetcher] - Injectable fetch (default: globalThis.fetch)
|
|
@@ -59,7 +42,7 @@ export async function pushTriageSnapshot({
|
|
|
59
42
|
rawTicketMap = new Map(),
|
|
60
43
|
profile,
|
|
61
44
|
baseUrl,
|
|
62
|
-
|
|
45
|
+
cliToken,
|
|
63
46
|
capturedAt,
|
|
64
47
|
gitBranches,
|
|
65
48
|
fetcher = globalThis.fetch,
|
|
@@ -70,8 +53,8 @@ export async function pushTriageSnapshot({
|
|
|
70
53
|
configDir = DEFAULT_CONFIG_DIR,
|
|
71
54
|
} = {}) {
|
|
72
55
|
warnIfInsecure(apiBase(), warn);
|
|
73
|
-
if (!
|
|
74
|
-
print('✗ --push requires
|
|
56
|
+
if (!cliToken) {
|
|
57
|
+
print('✗ --push requires Console access. Run ticketlens login first.\n');
|
|
75
58
|
return { ok: false };
|
|
76
59
|
}
|
|
77
60
|
|
|
@@ -110,7 +93,7 @@ export async function pushTriageSnapshot({
|
|
|
110
93
|
method: 'POST',
|
|
111
94
|
headers: {
|
|
112
95
|
'Content-Type': 'application/json',
|
|
113
|
-
'Authorization': `Bearer ${
|
|
96
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
114
97
|
},
|
|
115
98
|
body: JSON.stringify(payload),
|
|
116
99
|
signal: AbortSignal.timeout(15_000),
|
|
@@ -121,6 +104,11 @@ export async function pushTriageSnapshot({
|
|
|
121
104
|
return { ok: true, status: res.status };
|
|
122
105
|
}
|
|
123
106
|
|
|
107
|
+
if (res.status === 401) {
|
|
108
|
+
print('✗ Session expired. Run ticketlens login to reconnect.\n');
|
|
109
|
+
return { ok: false, status: res.status };
|
|
110
|
+
}
|
|
111
|
+
|
|
124
112
|
if (res.status === 403) {
|
|
125
113
|
print('✗ --push requires a Team license\n');
|
|
126
114
|
return { ok: false, status: res.status };
|
|
@@ -29,7 +29,7 @@ function buildTicketPayload(scored, rawMap, baseUrl) {
|
|
|
29
29
|
* @param {Map} [opts.rawTicketMap] - Map<key, normalizedTicket>
|
|
30
30
|
* @param {string} [opts.profile] - Resolved profile name (max 100 chars)
|
|
31
31
|
* @param {string} [opts.baseUrl] - Jira base URL
|
|
32
|
-
* @param {string} [opts.
|
|
32
|
+
* @param {string} [opts.cliToken] - CLI session token
|
|
33
33
|
* @param {string} [opts.capturedAt] - ISO 8601 timestamp
|
|
34
34
|
* @param {Function} [opts.fetcher] - Injectable fetch
|
|
35
35
|
* @param {Function} [opts.print] - Output fn
|
|
@@ -40,15 +40,15 @@ export async function shareTriageSnapshot({
|
|
|
40
40
|
rawTicketMap = new Map(),
|
|
41
41
|
profile,
|
|
42
42
|
baseUrl,
|
|
43
|
-
|
|
43
|
+
cliToken,
|
|
44
44
|
capturedAt,
|
|
45
45
|
fetcher = globalThis.fetch,
|
|
46
46
|
print = (s) => process.stdout.write(s),
|
|
47
47
|
warn = (s) => process.stderr.write(s),
|
|
48
48
|
} = {}) {
|
|
49
49
|
warnIfInsecure(apiBase(), warn);
|
|
50
|
-
if (!
|
|
51
|
-
print('✗ --share requires
|
|
50
|
+
if (!cliToken) {
|
|
51
|
+
print('✗ --share requires Console access. Run ticketlens login first.\n');
|
|
52
52
|
return { ok: false };
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -63,7 +63,7 @@ export async function shareTriageSnapshot({
|
|
|
63
63
|
method: 'POST',
|
|
64
64
|
headers: {
|
|
65
65
|
'Content-Type': 'application/json',
|
|
66
|
-
'Authorization': `Bearer ${
|
|
66
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
67
67
|
},
|
|
68
68
|
body: JSON.stringify(payload),
|
|
69
69
|
signal: AbortSignal.timeout(15_000),
|
|
@@ -75,6 +75,11 @@ export async function shareTriageSnapshot({
|
|
|
75
75
|
return { ok: true, status: res.status };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
if (res.status === 401) {
|
|
79
|
+
print('✗ Session expired. Run ticketlens login to reconnect.\n');
|
|
80
|
+
return { ok: false, status: res.status };
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
if (res.status === 403) {
|
|
79
84
|
print('✗ --share requires a Team license\n');
|
|
80
85
|
return { ok: false, status: res.status };
|