ticketlens 0.1.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/LICENSE +21 -0
- package/README.md +457 -0
- package/bin/ticketlens.mjs +376 -0
- package/package.json +37 -0
- package/skills/jtb/scripts/fetch-my-tickets.mjs +377 -0
- package/skills/jtb/scripts/fetch-ticket.mjs +682 -0
- package/skills/jtb/scripts/lib/adapters/github-adapter.mjs +112 -0
- package/skills/jtb/scripts/lib/adapters/jira-adapter.mjs +19 -0
- package/skills/jtb/scripts/lib/adf-converter.mjs +67 -0
- package/skills/jtb/scripts/lib/ansi.mjs +87 -0
- package/skills/jtb/scripts/lib/arg-validator.mjs +178 -0
- package/skills/jtb/scripts/lib/attachment-downloader.mjs +123 -0
- package/skills/jtb/scripts/lib/attention-scorer.mjs +152 -0
- package/skills/jtb/scripts/lib/banner.mjs +201 -0
- package/skills/jtb/scripts/lib/brief-assembler.mjs +137 -0
- package/skills/jtb/scripts/lib/brief-cache.mjs +137 -0
- package/skills/jtb/scripts/lib/budget-pruner.mjs +242 -0
- package/skills/jtb/scripts/lib/cache-manager.mjs +499 -0
- package/skills/jtb/scripts/lib/cli-auth.mjs +40 -0
- package/skills/jtb/scripts/lib/cli.mjs +87 -0
- package/skills/jtb/scripts/lib/code-ref-parser.mjs +113 -0
- package/skills/jtb/scripts/lib/commit-linker.mjs +42 -0
- package/skills/jtb/scripts/lib/compliance-checker.mjs +92 -0
- package/skills/jtb/scripts/lib/config-wizard.mjs +392 -0
- package/skills/jtb/scripts/lib/config.mjs +63 -0
- package/skills/jtb/scripts/lib/diff-analyzer.mjs +66 -0
- package/skills/jtb/scripts/lib/drift-tracker.mjs +120 -0
- package/skills/jtb/scripts/lib/error-classifier.mjs +119 -0
- package/skills/jtb/scripts/lib/help.mjs +253 -0
- package/skills/jtb/scripts/lib/hook-installer.mjs +81 -0
- package/skills/jtb/scripts/lib/init-wizard.mjs +508 -0
- package/skills/jtb/scripts/lib/interactive-list.mjs +257 -0
- package/skills/jtb/scripts/lib/jira-client.mjs +169 -0
- package/skills/jtb/scripts/lib/ledger.mjs +96 -0
- package/skills/jtb/scripts/lib/license.mjs +195 -0
- package/skills/jtb/scripts/lib/pr-assembler.mjs +186 -0
- package/skills/jtb/scripts/lib/profile-picker.mjs +216 -0
- package/skills/jtb/scripts/lib/profile-resolver.mjs +236 -0
- package/skills/jtb/scripts/lib/profile-switcher.mjs +147 -0
- package/skills/jtb/scripts/lib/prompt-helpers.mjs +122 -0
- package/skills/jtb/scripts/lib/requirement-extractor.mjs +52 -0
- package/skills/jtb/scripts/lib/resolve-adapter.mjs +28 -0
- package/skills/jtb/scripts/lib/schedule-wizard.mjs +153 -0
- package/skills/jtb/scripts/lib/select-prompt.mjs +106 -0
- package/skills/jtb/scripts/lib/spinner.mjs +44 -0
- package/skills/jtb/scripts/lib/styled-assembler.mjs +183 -0
- package/skills/jtb/scripts/lib/summarizer.mjs +109 -0
- package/skills/jtb/scripts/lib/sync.mjs +119 -0
- package/skills/jtb/scripts/lib/table-formatter.mjs +48 -0
- package/skills/jtb/scripts/lib/triage-exporter.mjs +93 -0
- package/skills/jtb/scripts/lib/triage-history.mjs +166 -0
- package/skills/jtb/scripts/lib/triage-push.mjs +98 -0
- package/skills/jtb/scripts/lib/usage-tracker.mjs +54 -0
- package/skills/jtb/scripts/lib/vcs-detector.mjs +12 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point: scans assigned tickets and surfaces what needs attention.
|
|
5
|
+
* Usage: node fetch-my-tickets.mjs [--stale=N] [--status=X,Y] [--profile=NAME]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { scoreAttention, sortByUrgency } from './lib/attention-scorer.mjs';
|
|
9
|
+
import { assembleTriageSummary } from './lib/brief-assembler.mjs';
|
|
10
|
+
import { styleTriageSummary } from './lib/styled-assembler.mjs';
|
|
11
|
+
import { resolveConnection, loadProfiles, saveProfile } from './lib/profile-resolver.mjs';
|
|
12
|
+
import { resolveAdapter } from './lib/resolve-adapter.mjs';
|
|
13
|
+
import { createSpinner } from './lib/spinner.mjs';
|
|
14
|
+
import { createSession } from './lib/banner.mjs';
|
|
15
|
+
import { classifyError } from './lib/error-classifier.mjs';
|
|
16
|
+
import { runInteractiveList } from './lib/interactive-list.mjs';
|
|
17
|
+
import { promptProfileSelect } from './lib/profile-picker.mjs';
|
|
18
|
+
import { printTriageHelp } from './lib/help.mjs';
|
|
19
|
+
import { handleUnknownFlags } from './lib/arg-validator.mjs';
|
|
20
|
+
import { isLicensed, showUpgradePrompt, revalidateIfStale, readLicense } from './lib/license.mjs';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_STATUSES = ['In Progress', 'Code Review', 'QA'];
|
|
23
|
+
|
|
24
|
+
async function defaultDigestDeliverer(payload) {
|
|
25
|
+
const { readLicense } = await import('./lib/license.mjs');
|
|
26
|
+
const licenseKey = readLicense()?.key;
|
|
27
|
+
const res = await fetch('https://api.ticketlens.dev/v1/digest/deliver', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
signal: AbortSignal.timeout(10_000),
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'Authorization': `Bearer ${licenseKey}`,
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(payload),
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) throw new Error(`Digest delivery failed: ${res.status}`);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function escapeJql(s) {
|
|
41
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function run(args, envOrOpts = process.env, fetcher = globalThis.fetch, configDir = undefined) {
|
|
45
|
+
// Support both legacy positional form run(args, env, fetcher, configDir)
|
|
46
|
+
// and new opts-object form run(args, { env, fetcher, configDir, exporter, isLicensed, showUpgradePrompt, print })
|
|
47
|
+
let env, opts;
|
|
48
|
+
if (envOrOpts && typeof envOrOpts === 'object' && 'env' in envOrOpts) {
|
|
49
|
+
opts = envOrOpts;
|
|
50
|
+
env = opts.env ?? process.env;
|
|
51
|
+
fetcher = opts.fetcher ?? globalThis.fetch;
|
|
52
|
+
configDir = opts.configDir ?? undefined;
|
|
53
|
+
} else {
|
|
54
|
+
opts = {};
|
|
55
|
+
env = envOrOpts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Strip leading 'triage' subcommand if present (when called via CLI router)
|
|
59
|
+
if (args[0] === 'triage') args = args.slice(1);
|
|
60
|
+
|
|
61
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
62
|
+
printTriageHelp();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Normalize --project= alias once at entry so all recursive calls only see --profile=
|
|
66
|
+
const projectArg = args.find(a => a.startsWith('--project='));
|
|
67
|
+
if (projectArg) {
|
|
68
|
+
process.stderr.write(`Hint: --project recognized as alias for --profile=${projectArg.split('=')[1]}\n\n`);
|
|
69
|
+
args = args.map(a => a.startsWith('--project=') ? `--profile=${a.split('=')[1]}` : a);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const profileArg = args.find(a => a.startsWith('--profile='));
|
|
73
|
+
const profileName = profileArg ? profileArg.split('=')[1] : undefined;
|
|
74
|
+
|
|
75
|
+
const validatedArgs = await handleUnknownFlags(
|
|
76
|
+
args,
|
|
77
|
+
['--help', '-h', '--static', '--plain', '--styled', '--profile=', '--stale=', '--status=', '--assignee=', '--sprint=', '--export=', '--digest', '--push'],
|
|
78
|
+
{ hints: ['--depth=', '--no-attachments', '--no-cache'] } // fetch-only flags — shown as hints, not applied
|
|
79
|
+
);
|
|
80
|
+
if (validatedArgs === null) { process.exitCode = 1; return; }
|
|
81
|
+
args = validatedArgs;
|
|
82
|
+
|
|
83
|
+
// Fire-and-forget: silently refresh license.json if >7 days since last validation
|
|
84
|
+
revalidateIfStale({ configDir, fetcher });
|
|
85
|
+
|
|
86
|
+
const staleArg = args.find(a => a.startsWith('--stale='));
|
|
87
|
+
const staleDays = staleArg ? parseInt(staleArg.split('=')[1], 10) : 5;
|
|
88
|
+
|
|
89
|
+
const statusArg = args.find(a => a.startsWith('--status='));
|
|
90
|
+
const assigneeArg = args.find(a => a.startsWith('--assignee='));
|
|
91
|
+
const sprintArg = args.find(a => a.startsWith('--sprint='));
|
|
92
|
+
const exportArg = args.find(a => a.startsWith('--export='))?.split('=')[1] ?? null;
|
|
93
|
+
const digestFlag = args.includes('--digest');
|
|
94
|
+
const pushFlag = args.includes('--push');
|
|
95
|
+
|
|
96
|
+
if (exportArg && exportArg !== 'csv' && exportArg !== 'json') {
|
|
97
|
+
process.stderr.write(`Error: --export must be csv or json, got: ${exportArg}\n`);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const licensedFn = opts.isLicensed ?? isLicensed;
|
|
103
|
+
const upgradeFn = opts.showUpgradePrompt ?? showUpgradePrompt;
|
|
104
|
+
|
|
105
|
+
// Team-tier gate: --assignee and --sprint require a Team license
|
|
106
|
+
if ((assigneeArg || sprintArg) && !licensedFn('team', configDir)) {
|
|
107
|
+
upgradeFn('team', assigneeArg ? '--assignee' : '--sprint');
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Team-tier gate: --export requires a Team license
|
|
113
|
+
if (exportArg && !licensedFn('team', configDir)) {
|
|
114
|
+
upgradeFn('team', '--export');
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const assigneeName = assigneeArg ? assigneeArg.split('=').slice(1).join('=') : null;
|
|
120
|
+
const sprintName = sprintArg ? sprintArg.split('=').slice(1).join('=') : null;
|
|
121
|
+
|
|
122
|
+
const cwd = process.cwd();
|
|
123
|
+
let profileError = null;
|
|
124
|
+
const conn = resolveConnection(null, {
|
|
125
|
+
env,
|
|
126
|
+
configDir,
|
|
127
|
+
profileName,
|
|
128
|
+
cwd,
|
|
129
|
+
onWarning: (w) => process.stderr.write(w + '\n'),
|
|
130
|
+
onProfileNotFound: (info) => { profileError = info; },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const hasAuth = conn.pat || (conn.email && conn.apiToken);
|
|
134
|
+
if (!conn.baseUrl || !hasAuth) {
|
|
135
|
+
if (profileError) {
|
|
136
|
+
const picked = await promptProfileSelect(profileError);
|
|
137
|
+
if (picked) {
|
|
138
|
+
// Re-run with the selected profile
|
|
139
|
+
const newArgs = args.filter(a => !a.startsWith('--profile='));
|
|
140
|
+
newArgs.push(`--profile=${picked}`);
|
|
141
|
+
return run(newArgs, { ...opts, env, fetcher, configDir });
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
const noProfiles = !loadProfiles(configDir)?.profiles;
|
|
145
|
+
const msg = noProfiles
|
|
146
|
+
? 'Error: Could not determine Jira profile.\nRun `ticketlens init` to set up your connection.'
|
|
147
|
+
: 'Error: Could not determine Jira profile. Use --profile=NAME or add projectPaths to ~/.ticketlens/profiles.json';
|
|
148
|
+
process.stderr.write(msg + '\n');
|
|
149
|
+
}
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const adapter = resolveAdapter(conn, { fetcher });
|
|
155
|
+
|
|
156
|
+
// Status resolution: --status flag > profile triageStatuses > defaults
|
|
157
|
+
const statuses = statusArg
|
|
158
|
+
? statusArg.split('=')[1].split(',').map(s => s.trim())
|
|
159
|
+
: conn.triageStatuses || DEFAULT_STATUSES;
|
|
160
|
+
|
|
161
|
+
// Build JQL before any I/O — pure computation, no dependency on currentUser
|
|
162
|
+
const statusList = statuses.map(s => `"${escapeJql(s)}"`).join(',');
|
|
163
|
+
const assigneeClause = assigneeName ? `assignee = "${escapeJql(assigneeName)}"` : `assignee = currentUser()`;
|
|
164
|
+
const sprintClause = sprintName ? ` AND sprint = "${escapeJql(sprintName)}"` : '';
|
|
165
|
+
const jql = `${assigneeClause} AND status IN (${statusList})${sprintClause} ORDER BY updated DESC`;
|
|
166
|
+
|
|
167
|
+
const session = createSession(conn);
|
|
168
|
+
session.spin(`Connecting to ${session.label}…`);
|
|
169
|
+
|
|
170
|
+
// Fire both requests concurrently — they are independent of each other
|
|
171
|
+
const userPromise = adapter.fetchCurrentUser();
|
|
172
|
+
const ticketsPromise = adapter.searchTickets(jql);
|
|
173
|
+
|
|
174
|
+
let currentUser;
|
|
175
|
+
try {
|
|
176
|
+
currentUser = await userPromise;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
ticketsPromise.catch(() => {}); // prevent unhandled rejection — we're bailing on the user request
|
|
179
|
+
const classified = classifyError(err, conn);
|
|
180
|
+
session.failed();
|
|
181
|
+
session.footer(classified.message, 'error', classified.hint);
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
session.connected();
|
|
187
|
+
process.stderr.write('\n');
|
|
188
|
+
|
|
189
|
+
const scanSpinner = createSpinner('Scanning tickets…');
|
|
190
|
+
scanSpinner.start();
|
|
191
|
+
|
|
192
|
+
let tickets;
|
|
193
|
+
try {
|
|
194
|
+
tickets = await ticketsPromise;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
scanSpinner.stop();
|
|
197
|
+
if (err.status === 400 && err.detail && /does not exist for the field 'status'/.test(err.detail)) {
|
|
198
|
+
const s = session.styler;
|
|
199
|
+
const out = process.stderr;
|
|
200
|
+
out.write(`\n ${s.yellow('○')} Status mismatch — checking Jira...\n`);
|
|
201
|
+
try {
|
|
202
|
+
const available = await adapter.fetchStatuses();
|
|
203
|
+
const lowerMap = new Map(available.map(n => [n.toLowerCase(), n]));
|
|
204
|
+
|
|
205
|
+
// Map each configured status to its best match (exact → case-insensitive → partial)
|
|
206
|
+
const mappings = statuses.map(name => {
|
|
207
|
+
if (available.includes(name)) return { input: name, fix: name, ok: true };
|
|
208
|
+
const caseMatch = lowerMap.get(name.toLowerCase());
|
|
209
|
+
if (caseMatch) return { input: name, fix: caseMatch, ok: false };
|
|
210
|
+
const partial = available.find(a =>
|
|
211
|
+
a.toLowerCase().includes(name.toLowerCase()) ||
|
|
212
|
+
name.toLowerCase().startsWith(a.toLowerCase().split(' ')[0])
|
|
213
|
+
);
|
|
214
|
+
return { input: name, fix: partial || null, ok: false };
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
out.write('\n');
|
|
218
|
+
for (const m of mappings) {
|
|
219
|
+
if (m.ok) out.write(` ${s.green('✔')} ${m.input}\n`);
|
|
220
|
+
else if (m.fix) out.write(` ${s.yellow('~')} ${s.dim(m.input)} → ${s.cyan(m.fix)}\n`);
|
|
221
|
+
else out.write(` ${s.red('✖')} ${m.input} ${s.dim('(not found in this Jira instance)')}\n`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const suggested = mappings.filter(m => m.fix).map(m => m.fix);
|
|
225
|
+
|
|
226
|
+
// On TTY: offer to auto-fix the profile and re-run
|
|
227
|
+
if (suggested.length > 0 && conn.profileName && out.isTTY && process.stdin.setRawMode) {
|
|
228
|
+
out.write(`\n Update ${s.cyan(`"${conn.profileName}"`)} with corrected statuses? ${s.dim('y/N')} `);
|
|
229
|
+
const answer = await new Promise(res => {
|
|
230
|
+
process.stdin.setRawMode(true);
|
|
231
|
+
process.stdin.resume();
|
|
232
|
+
process.stdin.setEncoding('utf8');
|
|
233
|
+
process.stdin.once('data', char => {
|
|
234
|
+
process.stdin.setRawMode(false);
|
|
235
|
+
process.stdin.pause();
|
|
236
|
+
out.write('\n');
|
|
237
|
+
if (char === '\x03') process.exit(0);
|
|
238
|
+
res(char === 'y' || char === 'Y');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
if (answer) {
|
|
242
|
+
const config = loadProfiles(configDir);
|
|
243
|
+
if (config?.profiles[conn.profileName]) {
|
|
244
|
+
const existing = config.profiles[conn.profileName].triageStatuses || [];
|
|
245
|
+
const merged = [...new Set([...existing, ...suggested])];
|
|
246
|
+
const updated = { ...config.profiles[conn.profileName], triageStatuses: merged };
|
|
247
|
+
saveProfile(conn.profileName, updated, {}, configDir);
|
|
248
|
+
out.write(` ${s.green('✔')} Profile updated. Rerunning...\n\n`);
|
|
249
|
+
// Strip --status flag so the re-run uses the corrected profile statuses
|
|
250
|
+
const rerunArgs = args.filter(a => !a.startsWith('--status='));
|
|
251
|
+
return run(rerunArgs, { ...opts, env, fetcher, configDir });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Fallback: show compact fix hint
|
|
257
|
+
if (suggested.length > 0) {
|
|
258
|
+
out.write(`\n ${s.dim('Suggested fix for')} ~/.ticketlens/profiles.json ${s.dim(`→ "${conn.profileName || 'your-profile'}"`)}\n`);
|
|
259
|
+
out.write(` ${s.cyan('"triageStatuses"')}: ${JSON.stringify(suggested)}\n\n`);
|
|
260
|
+
out.write(` ${s.dim('Or:')} ticketlens triage --status=${suggested.join(',')}\n`);
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
out.write(`\n ${s.dim('Could not fetch available statuses.')}\n`);
|
|
264
|
+
}
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const classified = classifyError(err, conn);
|
|
269
|
+
session.footer(classified.message, 'error', classified.hint);
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
scanSpinner.stop();
|
|
275
|
+
|
|
276
|
+
// When viewing another dev's tickets, score from their perspective (they need to respond)
|
|
277
|
+
const effectiveUser = assigneeName
|
|
278
|
+
? { displayName: assigneeName, name: null, accountId: null, emailAddress: null }
|
|
279
|
+
: currentUser;
|
|
280
|
+
|
|
281
|
+
if (assigneeName) {
|
|
282
|
+
process.stderr.write(`Viewing ${assigneeName}'s tickets\n\n`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const scored = tickets.map(t => scoreAttention(t, effectiveUser, { staleDays }));
|
|
286
|
+
const actionable = scored.filter(s => s.urgency !== 'clear');
|
|
287
|
+
const sorted = sortByUrgency(actionable);
|
|
288
|
+
const rawTicketMap = new Map(tickets.map(t => [t.key, t]));
|
|
289
|
+
|
|
290
|
+
// --digest: POST scored results to the digest backend endpoint
|
|
291
|
+
if (digestFlag) {
|
|
292
|
+
if (!licensedFn('pro', configDir)) {
|
|
293
|
+
upgradeFn('pro', '--digest');
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const deliverer = opts.digestDeliverer ?? defaultDigestDeliverer;
|
|
298
|
+
|
|
299
|
+
// Triage history delta (non-fatal — wrapped in try/catch)
|
|
300
|
+
let delta = null;
|
|
301
|
+
try {
|
|
302
|
+
const { saveTriageSnapshot, loadYesterdaySnapshot, diffSnapshots, buildDeltaSection } =
|
|
303
|
+
await import('./lib/triage-history.mjs');
|
|
304
|
+
saveTriageSnapshot(sorted, { profile: profileName ?? 'default', configDir });
|
|
305
|
+
const yesterday = loadYesterdaySnapshot({ profile: profileName ?? 'default', configDir });
|
|
306
|
+
if (yesterday) {
|
|
307
|
+
const deltas = diffSnapshots(sorted, yesterday);
|
|
308
|
+
delta = buildDeltaSection(deltas) || null;
|
|
309
|
+
}
|
|
310
|
+
} catch { /* non-fatal — digest still sends */ }
|
|
311
|
+
|
|
312
|
+
await deliverer({
|
|
313
|
+
profile: profileName ?? 'default',
|
|
314
|
+
staleDays,
|
|
315
|
+
summary: {
|
|
316
|
+
total: sorted.length,
|
|
317
|
+
needsResponse: sorted.filter(t => t.urgency === 'needs-response').length,
|
|
318
|
+
aging: sorted.filter(t => t.urgency === 'aging').length,
|
|
319
|
+
},
|
|
320
|
+
tickets: sorted,
|
|
321
|
+
delta,
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --export: write results to file instead of (or in addition to) printing
|
|
327
|
+
if (exportArg) {
|
|
328
|
+
const { exportTriage } = await import('./lib/triage-exporter.mjs');
|
|
329
|
+
const exporterFn = opts.exporter ?? exportTriage;
|
|
330
|
+
const outputPath = await Promise.resolve(exporterFn({ tickets: sorted, format: exportArg, profile: profileName ?? 'default', configDir }));
|
|
331
|
+
const printFn = opts.print ?? ((msg) => process.stdout.write(msg + '\n'));
|
|
332
|
+
printFn(`Export written to ${outputPath}`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Interactive mode: TTY + not --plain + not --static
|
|
337
|
+
const wantInteractive = process.stdout.isTTY && !args.includes('--plain') && !args.includes('--static');
|
|
338
|
+
if (wantInteractive && process.stdin.setRawMode) {
|
|
339
|
+
const result = await runInteractiveList(sorted, { baseUrl: conn.baseUrl, staleDays, styled: true });
|
|
340
|
+
if (result === 'switch') {
|
|
341
|
+
const cleanArgs = args.filter(a => !a.startsWith('--profile=') && !a.startsWith('--project='));
|
|
342
|
+
return run(cleanArgs, { ...opts, env, fetcher, configDir });
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const useStyled = args.includes('--styled') || (!args.includes('--plain') && process.stdout.isTTY);
|
|
348
|
+
const summary = useStyled
|
|
349
|
+
? styleTriageSummary(sorted, { styled: true, staleDays, baseUrl: conn.baseUrl })
|
|
350
|
+
: assembleTriageSummary(sorted, { staleDays, baseUrl: conn.baseUrl });
|
|
351
|
+
process.stdout.write(summary + '\n');
|
|
352
|
+
|
|
353
|
+
if (pushFlag) {
|
|
354
|
+
const { pushTriageSnapshot } = await import('./lib/triage-push.mjs');
|
|
355
|
+
const pushFn = opts.pushFn ?? pushTriageSnapshot;
|
|
356
|
+
const licenseKey = readLicense(configDir)?.key ?? null;
|
|
357
|
+
const printFn = opts.print ?? ((s) => process.stdout.write(s));
|
|
358
|
+
await pushFn({
|
|
359
|
+
sorted,
|
|
360
|
+
rawTicketMap,
|
|
361
|
+
profile: profileName ?? 'default',
|
|
362
|
+
baseUrl: conn.baseUrl,
|
|
363
|
+
licenseKey,
|
|
364
|
+
fetcher,
|
|
365
|
+
print: printFn,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Run if invoked directly
|
|
371
|
+
const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/.*\//, ''));
|
|
372
|
+
if (isMain) {
|
|
373
|
+
run(process.argv.slice(2)).catch(err => {
|
|
374
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
375
|
+
process.exitCode = 1;
|
|
376
|
+
});
|
|
377
|
+
}
|