ticketlens 0.1.4 → 0.1.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.
@@ -17,9 +17,16 @@ import { run as runConfig } from '../skills/jtb/scripts/lib/config-wizard.mjs';
17
17
  import { activateLicense, checkLicense, revalidateIfStale, isLicensed, showUpgradePrompt, readLicense } from '../skills/jtb/scripts/lib/license.mjs';
18
18
  import { deleteProfile, loadProfiles } from '../skills/jtb/scripts/lib/profile-resolver.mjs';
19
19
  import { run as runCache } from '../skills/jtb/scripts/lib/cache-manager.mjs';
20
- import { printHelp, printProfiles } from '../skills/jtb/scripts/lib/help.mjs';
20
+ import {
21
+ printHelp, printProfiles,
22
+ printLoginHelp, printLogoutHelp, printSyncHelp,
23
+ printActivateHelp, printLicenseHelp, printDeleteHelp,
24
+ printProfilesHelp, printScheduleHelp,
25
+ printInitHelp, printSwitchHelp, printConfigHelp,
26
+ } from '../skills/jtb/scripts/lib/help.mjs';
21
27
  import { createStyler } from '../skills/jtb/scripts/lib/ansi.mjs';
22
28
  import { readCliToken, saveCliToken, deleteCliToken } from '../skills/jtb/scripts/lib/cli-auth.mjs';
29
+ import { browserLogin } from '../skills/jtb/scripts/lib/browser-login.mjs';
23
30
  import { syncProfiles, getApiBase, getConsoleBase } from '../skills/jtb/scripts/lib/sync.mjs';
24
31
  import { promptSecret, promptText } from '../skills/jtb/scripts/lib/prompt-helpers.mjs';
25
32
 
@@ -45,6 +52,7 @@ switch (command) {
45
52
  break;
46
53
 
47
54
  case 'init':
55
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printInitHelp(); break; }
48
56
  runInit().catch(err => {
49
57
  process.stderr.write(`Error: ${err.message}\n`);
50
58
  process.exitCode = 1;
@@ -52,6 +60,7 @@ switch (command) {
52
60
  break;
53
61
 
54
62
  case 'switch':
63
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printSwitchHelp(); break; }
55
64
  runSwitch().catch(err => {
56
65
  process.stderr.write(`Error: ${err.message}\n`);
57
66
  process.exitCode = 1;
@@ -59,6 +68,7 @@ switch (command) {
59
68
  break;
60
69
 
61
70
  case 'config': {
71
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printConfigHelp(); break; }
62
72
  const profileArg = cmdArgs.find(a => a.startsWith('--profile='));
63
73
  const profileName = profileArg ? profileArg.split('=')[1] : undefined;
64
74
  runConfig({ profileName }).catch(err => {
@@ -69,6 +79,7 @@ switch (command) {
69
79
  }
70
80
 
71
81
  case 'activate': {
82
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printActivateHelp(); break; }
72
83
  const s = createStyler({ isTTY: process.stdout.isTTY });
73
84
  const key = cmdArgs.find(a => !a.startsWith('--'));
74
85
  if (!key) {
@@ -90,6 +101,7 @@ switch (command) {
90
101
  }
91
102
 
92
103
  case 'license': {
104
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printLicenseHelp(); break; }
93
105
  const s = createStyler({ isTTY: process.stdout.isTTY });
94
106
  const status = checkLicense();
95
107
  const daysSinceVal = status.validatedAt
@@ -131,6 +143,7 @@ switch (command) {
131
143
  }
132
144
 
133
145
  case 'delete': {
146
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printDeleteHelp(); break; }
134
147
  const s = createStyler({ isTTY: process.stderr.isTTY });
135
148
  const profileName = cmdArgs.find(a => !a.startsWith('--'));
136
149
  if (!profileName) {
@@ -182,6 +195,7 @@ switch (command) {
182
195
  }
183
196
 
184
197
  case 'profiles': {
198
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printProfilesHelp(); break; }
185
199
  const plain = cmdArgs.includes('--plain');
186
200
  const config = loadProfiles();
187
201
  printProfiles({ config, plain });
@@ -203,6 +217,7 @@ switch (command) {
203
217
  }
204
218
 
205
219
  case 'schedule': {
220
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printScheduleHelp(); break; }
206
221
  const subCmd = cmdArgs[0];
207
222
 
208
223
  if (subCmd === '--stop') {
@@ -262,21 +277,46 @@ switch (command) {
262
277
  break;
263
278
 
264
279
  case 'login': {
280
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printLoginHelp(); break; }
281
+
282
+ const useManual = cmdArgs.includes('--manual');
283
+
265
284
  (async () => {
266
285
  const s = createStyler({ isTTY: process.stderr.isTTY });
267
- process.stderr.write(`\n ${s.bold('TicketLens Login')}\n`);
268
- process.stderr.write(` ${s.dim('─'.repeat(44))}\n`);
269
- process.stderr.write(` ${s.dim(`Generate a CLI token at ${s.cyan(`${getConsoleBase()}/console/account`)}`)}\n`);
270
- process.stderr.write(` ${s.dim('then paste it below.')}\n\n`);
271
-
272
- const token = await promptSecret(`CLI Token ${s.dim('(tl_…)')}:`, { stream: process.stderr });
273
- if (!token.startsWith('tl_')) {
274
- process.stderr.write(` ${s.red('✖')} Token must start with ${s.dim('tl_')}\n`);
275
- process.exitCode = 1;
276
- return;
286
+
287
+ let token;
288
+
289
+ if (useManual) {
290
+ // ── manual paste flow (CI / headless environments) ──────────────────
291
+ process.stderr.write(`\n ${s.bold('TicketLens Login')}\n`);
292
+ process.stderr.write(` ${s.dim(''.repeat(44))}\n`);
293
+ process.stderr.write(` ${s.dim(`Generate a CLI token at ${s.cyan(`${getConsoleBase()}/console/account`)}`)}\n`);
294
+ process.stderr.write(` ${s.dim('then paste it below.')}\n\n`);
295
+
296
+ token = await promptSecret(`CLI Token ${s.dim('(tl_…)')}:`, { stream: process.stderr });
297
+ if (!token.startsWith('tl_')) {
298
+ process.stderr.write(` ${s.red('✖')} Token must start with ${s.dim('tl_')}\n`);
299
+ process.exitCode = 1;
300
+ return;
301
+ }
302
+ } else {
303
+ // ── browser flow (default) ────────────────────────────────────────
304
+ process.stderr.write(`\n ${s.bold('TicketLens Login')}\n`);
305
+ process.stderr.write(` ${s.dim('─'.repeat(44))}\n`);
306
+ process.stderr.write(` Opening browser to authorize…\n\n`);
307
+ process.stderr.write(` ${s.dim('○ Waiting for authorization (120s)…')}\n`);
308
+
309
+ try {
310
+ token = await browserLogin();
311
+ } catch (err) {
312
+ process.stderr.write(`\x1b[A\r\x1b[2K ${s.red('✖')} ${err.message}\n`);
313
+ process.stderr.write(`\n ${s.dim(`Try ${s.cyan('ticketlens login --manual')} to paste a token instead.`)}\n\n`);
314
+ process.exitCode = 1;
315
+ return;
316
+ }
277
317
  }
278
318
 
279
- // Validate against the API before saving
319
+ // ── verify token against API (both flows) ─────────────────────────
280
320
  process.stderr.write(`\n ${s.dim('○ Verifying token…')}\n`);
281
321
  let res;
282
322
  try {
@@ -284,7 +324,7 @@ switch (command) {
284
324
  headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
285
325
  signal: AbortSignal.timeout(15000),
286
326
  });
287
- } catch (err) {
327
+ } catch {
288
328
  process.stderr.write(`\x1b[A\r\x1b[2K ${s.red('✖')} Could not reach ${getApiBase()} — check your connection.\n`);
289
329
  process.exitCode = 1;
290
330
  return;
@@ -312,6 +352,7 @@ switch (command) {
312
352
  }
313
353
 
314
354
  case 'logout': {
355
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printLogoutHelp(); break; }
315
356
  const s = createStyler({ isTTY: process.stderr.isTTY });
316
357
  deleteCliToken();
317
358
  process.stderr.write(` ${s.green('✔')} CLI token removed.\n`);
@@ -319,6 +360,7 @@ switch (command) {
319
360
  }
320
361
 
321
362
  case 'sync': {
363
+ if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printSyncHelp(); break; }
322
364
  (async () => {
323
365
  const s = createStyler({ isTTY: process.stderr.isTTY });
324
366
  process.stderr.write(`\n ${s.dim('Syncing from TicketLens console…')}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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": {
@@ -0,0 +1,108 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { spawn } from 'node:child_process';
4
+ import { hostname as osHostname } from 'node:os';
5
+ import { getConsoleBase } from './sync.mjs';
6
+
7
+ const PORT_MIN = 49152;
8
+ const PORT_MAX = 65535;
9
+ const TIMEOUT_MS = 120_000;
10
+
11
+ export const generateState = () => randomBytes(16).toString('hex');
12
+
13
+ export const pickPort = () =>
14
+ Math.floor(Math.random() * (PORT_MAX - PORT_MIN + 1)) + PORT_MIN;
15
+
16
+ export function openBrowser(url) {
17
+ const cmd = process.platform === 'win32' ? 'cmd'
18
+ : process.platform === 'darwin' ? 'open'
19
+ : 'xdg-open';
20
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
21
+ spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
22
+ }
23
+
24
+ /**
25
+ * Start a one-shot local HTTP server that waits for the CLI auth callback.
26
+ * Resolves with the token string on success; rejects on state mismatch,
27
+ * missing/invalid token, or timeout.
28
+ */
29
+ export function startLocalServer(port, expectedState, timeoutMs = TIMEOUT_MS) {
30
+ return new Promise((resolve, reject) => {
31
+ let settled = false;
32
+
33
+ const settle = (fn, value) => {
34
+ if (settled) return;
35
+ settled = true;
36
+ clearTimeout(timer);
37
+ server.close();
38
+ fn(value);
39
+ };
40
+
41
+ const server = createServer((req, res) => {
42
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
43
+
44
+ if (url.pathname !== '/callback') {
45
+ res.writeHead(404);
46
+ res.end();
47
+ return;
48
+ }
49
+
50
+ const token = url.searchParams.get('token') ?? '';
51
+ const state = url.searchParams.get('state') ?? '';
52
+
53
+ if (state !== expectedState) {
54
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
55
+ res.end('State mismatch — authorization rejected.');
56
+ settle(reject, new Error('State mismatch'));
57
+ return;
58
+ }
59
+
60
+ if (!token.startsWith('tl_')) {
61
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
62
+ res.end('Invalid token received.');
63
+ settle(reject, new Error('Invalid token'));
64
+ return;
65
+ }
66
+
67
+ res.writeHead(200, { 'Content-Type': 'text/html' });
68
+ res.end(
69
+ '<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0d1117;color:#cdd9e5">'
70
+ + '<h2 style="color:#3fb950">&#10003; Authorized</h2>'
71
+ + '<p style="color:#8b949e">You can close this tab and return to the terminal.</p>'
72
+ + '</body></html>',
73
+ );
74
+
75
+ settle(resolve, token);
76
+ });
77
+
78
+ server.on('error', (err) => settle(reject, err));
79
+
80
+ const timer = setTimeout(
81
+ () => settle(reject, new Error('Authorization timed out after 120 seconds')),
82
+ timeoutMs,
83
+ );
84
+
85
+ server.listen(port, '127.0.0.1');
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Full browser login flow. Opens the authorize page in the default browser,
91
+ * waits for the callback, and returns the plaintext CLI token.
92
+ */
93
+ export async function browserLogin() {
94
+ const state = generateState();
95
+ const port = pickPort();
96
+ const hostname = osHostname();
97
+
98
+ const consoleBase = getConsoleBase();
99
+ const url = `${consoleBase}/console/auth/cli`
100
+ + `?port=${port}`
101
+ + `&state=${encodeURIComponent(state)}`
102
+ + `&hostname=${encodeURIComponent(hostname)}`;
103
+
104
+ const tokenPromise = startLocalServer(port, state);
105
+ openBrowser(url);
106
+
107
+ return tokenPromise;
108
+ }
@@ -215,6 +215,288 @@ export function printProfiles({ stream = process.stdout, config, plain = false }
215
215
  stream.write(lines.join('\n') + '\n');
216
216
  }
217
217
 
218
+ export function printLoginHelp({ stream = process.stdout } = {}) {
219
+ const s = createStyler({ isTTY: stream.isTTY });
220
+ const lines = [
221
+ '',
222
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('login')} ${s.dim('[--manual]')}`,
223
+ '',
224
+ ` Connect the CLI to your TicketLens account.`,
225
+ ` Opens a browser window to authorize — no copy-pasting required.`,
226
+ '',
227
+ ` ${s.bold('HOW IT WORKS')}`,
228
+ '',
229
+ ` 1. Run ${s.cyan('ticketlens login')} — your browser opens the authorize page`,
230
+ ` 2. Click ${s.bold('Authorize TicketLens CLI')} while logged in to the Console`,
231
+ ` 3. The terminal confirms login automatically`,
232
+ ` 4. Run ${s.cyan('ticketlens sync')} to pull your tracker connections`,
233
+ '',
234
+ ` ${s.bold('OPTIONS')}`,
235
+ '',
236
+ ` ${s.brand('--manual')} Paste a token instead of using the browser`,
237
+ ` ${s.dim('Useful for CI, SSH sessions, or headless environments.')}`,
238
+ ` ${s.dim(`Generate a token at ${s.cyan('<console-url>/console/account')}`)}`,
239
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
240
+ '',
241
+ ` ${s.bold('EXAMPLES')}`,
242
+ '',
243
+ ` ${s.dim('$')} ticketlens login ${s.dim('# opens browser (default)')}`,
244
+ ` ${s.dim('$')} ticketlens login --manual ${s.dim('# paste token (CI / headless)')}`,
245
+ ` ${s.dim('$')} ticketlens sync ${s.dim('# after login, pull connections')}`,
246
+ '',
247
+ ` ${s.bold('FILES')}`,
248
+ '',
249
+ ` ${s.dim('Token saved to:')} ~/.ticketlens/cli-token ${s.dim('(written by ticketlens login)')}`,
250
+ '',
251
+ ];
252
+ stream.write(lines.join('\n') + '\n');
253
+ }
254
+
255
+ export function printLogoutHelp({ stream = process.stdout } = {}) {
256
+ const s = createStyler({ isTTY: stream.isTTY });
257
+ const lines = [
258
+ '',
259
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('logout')}`,
260
+ '',
261
+ ` Remove the stored CLI token, disconnecting this machine from your`,
262
+ ` TicketLens account. Local profiles and credentials are kept intact.`,
263
+ '',
264
+ ` ${s.bold('OPTIONS')}`,
265
+ '',
266
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
267
+ '',
268
+ ` ${s.bold('EXAMPLES')}`,
269
+ '',
270
+ ` ${s.dim('$')} ticketlens logout`,
271
+ ` ${s.dim('$')} ticketlens login ${s.dim('# re-authenticate')}`,
272
+ '',
273
+ ];
274
+ stream.write(lines.join('\n') + '\n');
275
+ }
276
+
277
+ export function printSyncHelp({ stream = process.stdout } = {}) {
278
+ const s = createStyler({ isTTY: stream.isTTY });
279
+ const lines = [
280
+ '',
281
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('sync')}`,
282
+ '',
283
+ ` Pull tracker connections from the TicketLens console and write them`,
284
+ ` to ${s.dim('~/.ticketlens/profiles.json')}. Requires ${s.cyan('ticketlens login')} first.`,
285
+ '',
286
+ ` Profiles that need credentials will be listed with a reminder to`,
287
+ ` run ${s.cyan('ticketlens config --profile=NAME')} to add them.`,
288
+ '',
289
+ ` ${s.bold('OPTIONS')}`,
290
+ '',
291
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
292
+ '',
293
+ ` ${s.bold('EXAMPLES')}`,
294
+ '',
295
+ ` ${s.dim('$')} ticketlens login`,
296
+ ` ${s.dim('$')} ticketlens sync`,
297
+ ` ${s.dim('$')} ticketlens profiles ${s.dim('# verify pulled connections')}`,
298
+ '',
299
+ ];
300
+ stream.write(lines.join('\n') + '\n');
301
+ }
302
+
303
+ export function printActivateHelp({ stream = process.stdout } = {}) {
304
+ const s = createStyler({ isTTY: stream.isTTY });
305
+ const lines = [
306
+ '',
307
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('activate')} ${s.dim('<LICENSE-KEY>')}`,
308
+ '',
309
+ ` Activate a Pro or Team license key to unlock paid features.`,
310
+ ` Validates the key online and writes the result to ${s.dim('~/.ticketlens/license.json')}.`,
311
+ '',
312
+ ` ${s.bold('ARGUMENTS')}`,
313
+ '',
314
+ ` ${s.brand('<LICENSE-KEY>')} Your LemonSqueezy license key`,
315
+ '',
316
+ ` ${s.bold('OPTIONS')}`,
317
+ '',
318
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
319
+ '',
320
+ ` ${s.bold('EXAMPLES')}`,
321
+ '',
322
+ ` ${s.dim('$')} ticketlens activate tl_abc123xxxx`,
323
+ ` ${s.dim('$')} ticketlens license ${s.dim('# verify activation')}`,
324
+ '',
325
+ ];
326
+ stream.write(lines.join('\n') + '\n');
327
+ }
328
+
329
+ export function printLicenseHelp({ stream = process.stdout } = {}) {
330
+ const s = createStyler({ isTTY: stream.isTTY });
331
+ const lines = [
332
+ '',
333
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('license')}`,
334
+ '',
335
+ ` Show current license status: tier, email, and last validation date.`,
336
+ ` License is re-validated automatically in the background every 7 days.`,
337
+ '',
338
+ ` ${s.bold('OPTIONS')}`,
339
+ '',
340
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
341
+ '',
342
+ ` ${s.bold('EXAMPLES')}`,
343
+ '',
344
+ ` ${s.dim('$')} ticketlens license`,
345
+ ` ${s.dim('$')} ticketlens activate ${s.dim('<KEY>')} ${s.dim('# activate or renew')}`,
346
+ '',
347
+ ];
348
+ stream.write(lines.join('\n') + '\n');
349
+ }
350
+
351
+ export function printDeleteHelp({ stream = process.stdout } = {}) {
352
+ const s = createStyler({ isTTY: stream.isTTY });
353
+ const lines = [
354
+ '',
355
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('delete')} ${s.dim('<PROFILE-NAME>')}`,
356
+ '',
357
+ ` Permanently remove a locally configured profile. In TTY mode, prompts`,
358
+ ` for confirmation before deleting. Pass ${s.cyan('--yes')} to skip the prompt.`,
359
+ '',
360
+ ` ${s.bold('ARGUMENTS')}`,
361
+ '',
362
+ ` ${s.brand('<PROFILE-NAME>')} Name of the profile to remove`,
363
+ '',
364
+ ` ${s.bold('OPTIONS')}`,
365
+ '',
366
+ ` ${s.brand('--yes')}, ${s.brand('-y')} Skip confirmation prompt`,
367
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
368
+ '',
369
+ ` ${s.bold('EXAMPLES')}`,
370
+ '',
371
+ ` ${s.dim('$')} ticketlens delete myprofile`,
372
+ ` ${s.dim('$')} ticketlens delete myprofile --yes`,
373
+ ` ${s.dim('$')} ticketlens profiles ${s.dim('# list remaining profiles')}`,
374
+ '',
375
+ ];
376
+ stream.write(lines.join('\n') + '\n');
377
+ }
378
+
379
+ export function printProfilesHelp({ stream = process.stdout } = {}) {
380
+ const s = createStyler({ isTTY: stream.isTTY });
381
+ const lines = [
382
+ '',
383
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('profiles')} ${s.dim('[--plain]')}`,
384
+ '',
385
+ ` List all locally configured Jira profiles and their active status.`,
386
+ ` Also available as ${s.cyan('ticketlens ls')}.`,
387
+ '',
388
+ ` ${s.bold('OPTIONS')}`,
389
+ '',
390
+ ` ${s.brand('--plain')} Tab-separated output ${s.dim('(for scripting)')}`,
391
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
392
+ '',
393
+ ` ${s.bold('EXAMPLES')}`,
394
+ '',
395
+ ` ${s.dim('$')} ticketlens profiles`,
396
+ ` ${s.dim('$')} ticketlens ls`,
397
+ ` ${s.dim('$')} ticketlens profiles --plain`,
398
+ '',
399
+ ];
400
+ stream.write(lines.join('\n') + '\n');
401
+ }
402
+
403
+ export function printScheduleHelp({ stream = process.stdout } = {}) {
404
+ const s = createStyler({ isTTY: stream.isTTY });
405
+ const lines = [
406
+ '',
407
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('schedule')} ${s.dim('[--stop|--status]')} ${s.dim('[Pro]')}`,
408
+ '',
409
+ ` Set up a recurring digest email with your triage results. ${s.dim('[Pro]')}`,
410
+ ` Runs an interactive wizard to configure day, time, and timezone.`,
411
+ '',
412
+ ` ${s.bold('OPTIONS')}`,
413
+ '',
414
+ ` ${s.brand('--stop')} Cancel the active digest schedule`,
415
+ ` ${s.brand('--status')} Show current schedule configuration`,
416
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
417
+ '',
418
+ ` ${s.bold('EXAMPLES')}`,
419
+ '',
420
+ ` ${s.dim('$')} ticketlens schedule`,
421
+ ` ${s.dim('$')} ticketlens schedule --status`,
422
+ ` ${s.dim('$')} ticketlens schedule --stop`,
423
+ '',
424
+ ];
425
+ stream.write(lines.join('\n') + '\n');
426
+ }
427
+
428
+ export function printInitHelp({ stream = process.stdout } = {}) {
429
+ const s = createStyler({ isTTY: stream.isTTY });
430
+ const lines = [
431
+ '',
432
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('init')}`,
433
+ '',
434
+ ` Configure a new Jira connection locally using an interactive wizard.`,
435
+ ` Supports Jira Cloud ${s.dim('(Basic auth)')} and Jira Server/DC ${s.dim('(Bearer PAT or Basic)')}`,
436
+ '',
437
+ ` After setup, run ${s.cyan('ticketlens PROJ-123')} to fetch your first ticket.`,
438
+ '',
439
+ ` ${s.bold('OPTIONS')}`,
440
+ '',
441
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
442
+ '',
443
+ ` ${s.bold('EXAMPLES')}`,
444
+ '',
445
+ ` ${s.dim('$')} ticketlens init`,
446
+ ` ${s.dim('$')} ticketlens profiles ${s.dim('# verify the new profile')}`,
447
+ '',
448
+ ` ${s.dim('Tip: use')} ${s.cyan('ticketlens sync')} ${s.dim('instead to pull connections from the console.')}`,
449
+ '',
450
+ ];
451
+ stream.write(lines.join('\n') + '\n');
452
+ }
453
+
454
+ export function printSwitchHelp({ stream = process.stdout } = {}) {
455
+ const s = createStyler({ isTTY: stream.isTTY });
456
+ const lines = [
457
+ '',
458
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('switch')}`,
459
+ '',
460
+ ` Interactively select which profile is active by default.`,
461
+ ` The chosen profile is used when no ${s.cyan('--profile')} flag is given.`,
462
+ '',
463
+ ` ${s.bold('OPTIONS')}`,
464
+ '',
465
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
466
+ '',
467
+ ` ${s.bold('EXAMPLES')}`,
468
+ '',
469
+ ` ${s.dim('$')} ticketlens switch`,
470
+ ` ${s.dim('$')} ticketlens profiles ${s.dim('# confirm new active profile')}`,
471
+ '',
472
+ ];
473
+ stream.write(lines.join('\n') + '\n');
474
+ }
475
+
476
+ export function printConfigHelp({ stream = process.stdout } = {}) {
477
+ const s = createStyler({ isTTY: stream.isTTY });
478
+ const lines = [
479
+ '',
480
+ ` ${s.bold(s.brand('ticketlens'))} ${s.bold('config')} ${s.dim('[--profile=NAME]')}`,
481
+ '',
482
+ ` Edit settings for an existing profile using an interactive wizard.`,
483
+ ` Without ${s.cyan('--profile')}, edits the currently active profile.`,
484
+ '',
485
+ ` ${s.bold('OPTIONS')}`,
486
+ '',
487
+ ` ${s.brand('--profile')}=${s.dim('NAME')} Profile to configure`,
488
+ ` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
489
+ '',
490
+ ` ${s.bold('EXAMPLES')}`,
491
+ '',
492
+ ` ${s.dim('$')} ticketlens config`,
493
+ ` ${s.dim('$')} ticketlens config --profile=work`,
494
+ ` ${s.dim('$')} ticketlens config --profile=acme`,
495
+ '',
496
+ ];
497
+ stream.write(lines.join('\n') + '\n');
498
+ }
499
+
218
500
  export function printTriageHelp({ stream = process.stdout } = {}) {
219
501
  const s = createStyler({ isTTY: stream.isTTY });
220
502
  const lines = [