ticketlens 0.4.4 → 0.4.6
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 +4 -0
- package/bin/ticketlens.mjs +7 -5
- package/package.json +1 -1
- package/skills/jtb/scripts/fetch-my-tickets.mjs +23 -22
- package/skills/jtb/scripts/lib/run-collisions.mjs +20 -17
- package/skills/jtb/scripts/lib/schedule-wizard.mjs +33 -10
- package/skills/jtb/scripts/lib/ticket-payload.mjs +19 -0
- package/skills/jtb/scripts/lib/triage-push.mjs +18 -29
- package/skills/jtb/scripts/lib/triage-share.mjs +15 -9
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
|
package/bin/ticketlens.mjs
CHANGED
|
@@ -262,13 +262,13 @@ switch (command) {
|
|
|
262
262
|
|
|
263
263
|
if (subCmd === '--stop') {
|
|
264
264
|
const { runScheduleStop } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
|
|
265
|
-
await runScheduleStop({
|
|
265
|
+
await runScheduleStop({ cliToken: readCliToken() });
|
|
266
266
|
break;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
if (subCmd === '--status') {
|
|
270
270
|
const { runScheduleStatus } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
|
|
271
|
-
await runScheduleStatus({
|
|
271
|
+
await runScheduleStatus({ cliToken: readCliToken() });
|
|
272
272
|
break;
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -281,10 +281,12 @@ switch (command) {
|
|
|
281
281
|
const { runScheduleWizard } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
|
|
282
282
|
const { promptScheduleAnswers } = await import('../skills/jtb/scripts/lib/prompt-helpers.mjs');
|
|
283
283
|
const answers = await promptScheduleAnswers(cmdArgs);
|
|
284
|
-
const result = await runScheduleWizard({ answers,
|
|
284
|
+
const result = await runScheduleWizard({ answers, cliToken: readCliToken() });
|
|
285
|
+
if (!result.ok) { process.exitCode = 1; break; }
|
|
285
286
|
|
|
286
|
-
|
|
287
|
-
process.stdout.write(`
|
|
287
|
+
const s = createStyler({ isTTY: process.stdout.isTTY });
|
|
288
|
+
process.stdout.write(` ${s.green('✔')} Digest scheduled for ${s.cyan(answers.time)} ${answers.timezone}\n`);
|
|
289
|
+
process.stdout.write(` Next delivery: ${s.cyan(result.nextDelivery)}\n`);
|
|
288
290
|
break;
|
|
289
291
|
}
|
|
290
292
|
|
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
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readCliToken } from './cli-auth.mjs';
|
|
2
2
|
import { formatCollisions } from './collision-reporter.mjs';
|
|
3
|
-
|
|
3
|
+
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
4
4
|
import { apiBase, warnIfInsecure } from './api-utils.mjs';
|
|
5
|
+
import { red, yellow, bold, cyan } from './ansi.mjs';
|
|
5
6
|
|
|
6
7
|
const COLLISIONS_PATH = '/v1/triage/collisions';
|
|
7
8
|
|
|
@@ -10,42 +11,44 @@ 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(` ${red('✗')} ${bold('collisions')} requires Console access. Run ${cyan('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(` ${red('✗')} Session expired. Run ${cyan('ticketlens login')} to reconnect.\n`);
|
|
42
45
|
return { ok: false, status: 401 };
|
|
43
46
|
}
|
|
44
47
|
if (res.status === 403) {
|
|
45
|
-
print('✗ collisions requires a Team license\n
|
|
48
|
+
print(` ${red('✗')} ${bold('collisions')} requires a Team license\n`);
|
|
46
49
|
return { ok: false, status: 403 };
|
|
47
50
|
}
|
|
48
|
-
print(
|
|
51
|
+
print(` ${yellow('⚠')} Failed to fetch collisions (${res.status})\n`);
|
|
49
52
|
return { ok: false, status: res.status };
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -64,7 +67,7 @@ export async function runCollisions(args = [], opts = {}) {
|
|
|
64
67
|
|
|
65
68
|
return { ok: true };
|
|
66
69
|
} catch {
|
|
67
|
-
print('⚠ Failed to fetch collisions (network error)\n
|
|
70
|
+
print(` ${yellow('⚠')} Failed to fetch collisions (network error)\n`);
|
|
68
71
|
return { ok: false };
|
|
69
72
|
}
|
|
70
73
|
}
|
|
@@ -2,6 +2,7 @@ import { writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir, platform as osPlatform } from 'node:os';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { red, green, cyan } from './ansi.mjs';
|
|
5
6
|
|
|
6
7
|
const SCHEDULE_URL = 'https://api.ticketlens.dev/v1/schedule';
|
|
7
8
|
|
|
@@ -52,12 +53,17 @@ export function buildCronLine({ hour, minute, ticketlensBin }) {
|
|
|
52
53
|
export async function runScheduleWizard({
|
|
53
54
|
answers,
|
|
54
55
|
fetcher = globalThis.fetch,
|
|
55
|
-
|
|
56
|
+
cliToken,
|
|
56
57
|
configDir,
|
|
57
58
|
platform = osPlatform(),
|
|
58
59
|
writeLocalJob = defaultWriteLocalJob,
|
|
59
60
|
timeoutMs = 10_000,
|
|
61
|
+
print = s => process.stdout.write(s),
|
|
60
62
|
}) {
|
|
63
|
+
if (!cliToken) {
|
|
64
|
+
print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
|
|
65
|
+
return { ok: false };
|
|
66
|
+
}
|
|
61
67
|
const { time, email, timezone } = answers;
|
|
62
68
|
const [hourStr, minuteStr] = time.split(':');
|
|
63
69
|
const hour = parseInt(hourStr, 10);
|
|
@@ -68,7 +74,7 @@ export async function runScheduleWizard({
|
|
|
68
74
|
signal: AbortSignal.timeout(timeoutMs),
|
|
69
75
|
headers: {
|
|
70
76
|
'Content-Type': 'application/json',
|
|
71
|
-
'Authorization': `Bearer ${
|
|
77
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
72
78
|
},
|
|
73
79
|
body: JSON.stringify({ email, timezone, deliverAt: time }),
|
|
74
80
|
});
|
|
@@ -91,11 +97,20 @@ export async function runScheduleWizard({
|
|
|
91
97
|
return data;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
export async function runScheduleStop({
|
|
100
|
+
export async function runScheduleStop({
|
|
101
|
+
fetcher = globalThis.fetch,
|
|
102
|
+
cliToken,
|
|
103
|
+
platform = osPlatform(),
|
|
104
|
+
print = s => process.stdout.write(s),
|
|
105
|
+
}) {
|
|
106
|
+
if (!cliToken) {
|
|
107
|
+
print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
95
110
|
const res = await fetcher(SCHEDULE_URL, {
|
|
96
111
|
method: 'DELETE',
|
|
97
112
|
signal: AbortSignal.timeout(10_000),
|
|
98
|
-
headers: { 'Authorization': `Bearer ${
|
|
113
|
+
headers: { 'Authorization': `Bearer ${cliToken}` },
|
|
99
114
|
});
|
|
100
115
|
if (!res.ok) throw new Error(`Schedule API error ${res.status}`);
|
|
101
116
|
|
|
@@ -111,23 +126,31 @@ export async function runScheduleStop({ fetcher = globalThis.fetch, licenseKey,
|
|
|
111
126
|
spawnSync('crontab', [tmp], { encoding: 'utf8' });
|
|
112
127
|
}
|
|
113
128
|
|
|
114
|
-
|
|
129
|
+
print(` ${green('✔')} Digest schedule removed.\n`);
|
|
115
130
|
}
|
|
116
131
|
|
|
117
|
-
export async function runScheduleStatus({
|
|
132
|
+
export async function runScheduleStatus({
|
|
133
|
+
fetcher = globalThis.fetch,
|
|
134
|
+
cliToken,
|
|
135
|
+
print = s => process.stdout.write(s),
|
|
136
|
+
}) {
|
|
137
|
+
if (!cliToken) {
|
|
138
|
+
print(` ${red('✗')} schedule requires Console access. Run ${cyan('ticketlens login')} first.\n`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
118
141
|
const res = await fetcher(SCHEDULE_URL, {
|
|
119
142
|
method: 'GET',
|
|
120
143
|
signal: AbortSignal.timeout(10_000),
|
|
121
|
-
headers: { 'Authorization': `Bearer ${
|
|
144
|
+
headers: { 'Authorization': `Bearer ${cliToken}` },
|
|
122
145
|
});
|
|
123
146
|
if (!res.ok) {
|
|
124
147
|
process.stdout.write('No active digest schedule found.\n');
|
|
125
148
|
return;
|
|
126
149
|
}
|
|
127
150
|
const data = await res.json();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
print(` ${green('✔')} Digest schedule: ${cyan(data.deliverAt)} ${data.timezone}\n`);
|
|
152
|
+
print(` Last delivered: ${data.lastDeliveredAt ?? 'never'}\n`);
|
|
153
|
+
print(` Next delivery: ${cyan(data.nextDelivery)}\n`);
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
function resolveTicketlensBin() {
|
|
@@ -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,9 @@
|
|
|
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
|
+
import { red, green, yellow, bold, cyan } from './ansi.mjs';
|
|
11
12
|
|
|
12
13
|
const PUSH_PATH = '/v1/triage/push';
|
|
13
14
|
|
|
@@ -16,27 +17,10 @@ const PUSH_PATH = '/v1/triage/push';
|
|
|
16
17
|
// https://api.ticketlens.com → https://app.ticketlens.com/console/queue
|
|
17
18
|
export function queueUrl(base) {
|
|
18
19
|
const noApi = base.replace(/^(https?:\/\/)api\./, '$1');
|
|
19
|
-
|
|
20
|
-
const console = /ticketlens\.com/.test(noApi)
|
|
20
|
+
const consoleBase = /ticketlens\.com/.test(noApi)
|
|
21
21
|
? noApi.replace('://', '://app.')
|
|
22
22
|
: 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
|
-
};
|
|
23
|
+
return consoleBase + '/console/queue';
|
|
40
24
|
}
|
|
41
25
|
|
|
42
26
|
/**
|
|
@@ -47,7 +31,7 @@ function buildTicketPayload(scored, rawMap, baseUrl) {
|
|
|
47
31
|
* @param {Map} [opts.rawTicketMap] - Map<key, normalizedTicket>
|
|
48
32
|
* @param {string} [opts.profile] - Resolved profile name (max 100 chars)
|
|
49
33
|
* @param {string} [opts.baseUrl] - Jira base URL for URL construction
|
|
50
|
-
* @param {string} [opts.
|
|
34
|
+
* @param {string} [opts.cliToken] - CLI session token for the API
|
|
51
35
|
* @param {string} [opts.capturedAt] - ISO 8601 timestamp (defaults to now)
|
|
52
36
|
* @param {Array} [opts.gitBranches] - Branch metadata from scanCurrentBranch (null = not in git repo)
|
|
53
37
|
* @param {Function} [opts.fetcher] - Injectable fetch (default: globalThis.fetch)
|
|
@@ -59,7 +43,7 @@ export async function pushTriageSnapshot({
|
|
|
59
43
|
rawTicketMap = new Map(),
|
|
60
44
|
profile,
|
|
61
45
|
baseUrl,
|
|
62
|
-
|
|
46
|
+
cliToken,
|
|
63
47
|
capturedAt,
|
|
64
48
|
gitBranches,
|
|
65
49
|
fetcher = globalThis.fetch,
|
|
@@ -70,8 +54,8 @@ export async function pushTriageSnapshot({
|
|
|
70
54
|
configDir = DEFAULT_CONFIG_DIR,
|
|
71
55
|
} = {}) {
|
|
72
56
|
warnIfInsecure(apiBase(), warn);
|
|
73
|
-
if (!
|
|
74
|
-
print('✗ --push requires
|
|
57
|
+
if (!cliToken) {
|
|
58
|
+
print(` ${red('✗')} ${bold('--push')} requires Console access. Run ${cyan('ticketlens login')} first.\n`);
|
|
75
59
|
return { ok: false };
|
|
76
60
|
}
|
|
77
61
|
|
|
@@ -110,26 +94,31 @@ export async function pushTriageSnapshot({
|
|
|
110
94
|
method: 'POST',
|
|
111
95
|
headers: {
|
|
112
96
|
'Content-Type': 'application/json',
|
|
113
|
-
'Authorization': `Bearer ${
|
|
97
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
114
98
|
},
|
|
115
99
|
body: JSON.stringify(payload),
|
|
116
100
|
signal: AbortSignal.timeout(15_000),
|
|
117
101
|
});
|
|
118
102
|
|
|
119
103
|
if (res.ok) {
|
|
120
|
-
print(
|
|
104
|
+
print(` ${green('✓')} Queue updated — view at ${queueUrl(apiBase())}\n`);
|
|
121
105
|
return { ok: true, status: res.status };
|
|
122
106
|
}
|
|
123
107
|
|
|
108
|
+
if (res.status === 401) {
|
|
109
|
+
print(` ${red('✗')} Session expired. Run ${cyan('ticketlens login')} to reconnect.\n`);
|
|
110
|
+
return { ok: false, status: res.status };
|
|
111
|
+
}
|
|
112
|
+
|
|
124
113
|
if (res.status === 403) {
|
|
125
|
-
print('✗ --push requires a Team license\n
|
|
114
|
+
print(` ${red('✗')} ${bold('--push')} requires a Team license\n`);
|
|
126
115
|
return { ok: false, status: res.status };
|
|
127
116
|
}
|
|
128
117
|
|
|
129
|
-
print(
|
|
118
|
+
print(` ${yellow('⚠')} Push failed (${res.status}) — triage output unaffected\n`);
|
|
130
119
|
return { ok: false, status: res.status };
|
|
131
120
|
} catch {
|
|
132
|
-
print('⚠ Push failed (network error) — triage output unaffected\n
|
|
121
|
+
print(` ${yellow('⚠')} Push failed (network error) — triage output unaffected\n`);
|
|
133
122
|
return { ok: false };
|
|
134
123
|
}
|
|
135
124
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { apiBase, warnIfInsecure } from './api-utils.mjs';
|
|
7
|
+
import { red, green, yellow, bold, cyan } from './ansi.mjs';
|
|
7
8
|
|
|
8
9
|
const SHARE_PATH = '/v1/triage/share';
|
|
9
10
|
|
|
@@ -29,7 +30,7 @@ function buildTicketPayload(scored, rawMap, baseUrl) {
|
|
|
29
30
|
* @param {Map} [opts.rawTicketMap] - Map<key, normalizedTicket>
|
|
30
31
|
* @param {string} [opts.profile] - Resolved profile name (max 100 chars)
|
|
31
32
|
* @param {string} [opts.baseUrl] - Jira base URL
|
|
32
|
-
* @param {string} [opts.
|
|
33
|
+
* @param {string} [opts.cliToken] - CLI session token
|
|
33
34
|
* @param {string} [opts.capturedAt] - ISO 8601 timestamp
|
|
34
35
|
* @param {Function} [opts.fetcher] - Injectable fetch
|
|
35
36
|
* @param {Function} [opts.print] - Output fn
|
|
@@ -40,15 +41,15 @@ export async function shareTriageSnapshot({
|
|
|
40
41
|
rawTicketMap = new Map(),
|
|
41
42
|
profile,
|
|
42
43
|
baseUrl,
|
|
43
|
-
|
|
44
|
+
cliToken,
|
|
44
45
|
capturedAt,
|
|
45
46
|
fetcher = globalThis.fetch,
|
|
46
47
|
print = (s) => process.stdout.write(s),
|
|
47
48
|
warn = (s) => process.stderr.write(s),
|
|
48
49
|
} = {}) {
|
|
49
50
|
warnIfInsecure(apiBase(), warn);
|
|
50
|
-
if (!
|
|
51
|
-
print('✗ --share requires
|
|
51
|
+
if (!cliToken) {
|
|
52
|
+
print(` ${red('✗')} ${bold('--share')} requires Console access. Run ${cyan('ticketlens login')} first.\n`);
|
|
52
53
|
return { ok: false };
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -63,7 +64,7 @@ export async function shareTriageSnapshot({
|
|
|
63
64
|
method: 'POST',
|
|
64
65
|
headers: {
|
|
65
66
|
'Content-Type': 'application/json',
|
|
66
|
-
'Authorization': `Bearer ${
|
|
67
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
67
68
|
},
|
|
68
69
|
body: JSON.stringify(payload),
|
|
69
70
|
signal: AbortSignal.timeout(15_000),
|
|
@@ -71,19 +72,24 @@ export async function shareTriageSnapshot({
|
|
|
71
72
|
|
|
72
73
|
if (res.ok) {
|
|
73
74
|
const data = await res.json();
|
|
74
|
-
print(
|
|
75
|
+
print(` ${green('✓')} Share link (expires in 24h):\n ${data.url}\n`);
|
|
75
76
|
return { ok: true, status: res.status };
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
if (res.status === 401) {
|
|
80
|
+
print(` ${red('✗')} Session expired. Run ${cyan('ticketlens login')} to reconnect.\n`);
|
|
81
|
+
return { ok: false, status: res.status };
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
if (res.status === 403) {
|
|
79
|
-
print('✗ --share requires a Team license\n
|
|
85
|
+
print(` ${red('✗')} ${bold('--share')} requires a Team license\n`);
|
|
80
86
|
return { ok: false, status: res.status };
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
print(
|
|
89
|
+
print(` ${yellow('⚠')} Share failed (${res.status}) — triage output unaffected\n`);
|
|
84
90
|
return { ok: false, status: res.status };
|
|
85
91
|
} catch {
|
|
86
|
-
print('⚠ Share failed (network error) — triage output unaffected\n
|
|
92
|
+
print(` ${yellow('⚠')} Share failed (network error) — triage output unaffected\n`);
|
|
87
93
|
return { ok: false };
|
|
88
94
|
}
|
|
89
95
|
}
|