ticketlens 0.21.9 → 0.21.10
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 +7 -4
- package/bin/ticketlens.mjs +25 -2
- package/package.json +1 -1
- package/skills/jtb/SKILL.md +5 -1
- package/skills/jtb/scripts/fetch-ticket.mjs +16 -0
- package/skills/jtb/scripts/lib/help.mjs +122 -2
- package/skills/jtb/scripts/lib/hook-installer.mjs +48 -0
- package/skills/jtb/scripts/lib/schedule-wizard.mjs +27 -0
package/README.md
CHANGED
|
@@ -260,8 +260,8 @@ Runs the same compliance check as `ticketlens CNV1-2 --compliance` but as a dedi
|
|
|
260
260
|
### Compliance Ledger
|
|
261
261
|
|
|
262
262
|
```bash
|
|
263
|
-
ticketlens ledger # View the local compliance audit ledger [Pro]
|
|
264
|
-
ticketlens ledger --
|
|
263
|
+
ticketlens ledger # View the local compliance audit ledger, JSON + signature [Pro]
|
|
264
|
+
ticketlens ledger --format=csv # Flat CSV export, no signature [Pro]
|
|
265
265
|
```
|
|
266
266
|
|
|
267
267
|
Displays the append-only local ledger of all compliance checks run on this machine. Useful for SOC 2 / HIPAA audit trails. Requires a Pro license.
|
|
@@ -365,9 +365,11 @@ Cache locations:
|
|
|
365
365
|
ticketlens schedule # Interactive wizard — set digest time, timezone, profile [Pro]
|
|
366
366
|
ticketlens schedule --stop # Cancel the scheduled digest
|
|
367
367
|
ticketlens schedule --status # Show current schedule
|
|
368
|
-
ticketlens schedule --local
|
|
368
|
+
ticketlens schedule --local --time=07:00 --save=./triage.txt # Local-only cron/LaunchAgent, no Console auth [Pro]
|
|
369
369
|
```
|
|
370
370
|
|
|
371
|
+
Not logged in? `ticketlens schedule` falls back to local-only mode automatically — same as passing `--local` explicitly.
|
|
372
|
+
|
|
371
373
|
Stores the schedule as a cron entry. Delivers your triage digest at the configured time without an open terminal. Requires a Pro license.
|
|
372
374
|
|
|
373
375
|
---
|
|
@@ -595,6 +597,7 @@ ticketlens CNV1-2 --template=quick # Apply quick template (meta + 2
|
|
|
595
597
|
ticketlens CNV1-2 --template=code-review # Apply code-review template (meta + desc + linked + code refs)
|
|
596
598
|
ticketlens CNV1-2 --template=full # Apply full template (all sections, default)
|
|
597
599
|
ticketlens CNV1-2 --template=my-team-template # Apply a custom team template [Team]
|
|
600
|
+
ticketlens CNV1-2 --budget=8000 # Trim brief to fit a token budget [Pro]
|
|
598
601
|
ticketlens CNV1-2 --depth=2 --profile=acme --plain # Combine flags freely
|
|
599
602
|
|
|
600
603
|
# Pipe plain output to clipboard, LLM, or file
|
|
@@ -675,7 +678,7 @@ ticketlens cache clear --older-than=30d --yes # Skip confirmation (CI
|
|
|
675
678
|
ticketlens schedule # Interactive wizard — set time, timezone, profile [Pro]
|
|
676
679
|
ticketlens schedule --stop # Cancel the scheduled digest [Pro]
|
|
677
680
|
ticketlens schedule --status # Show current schedule [Pro]
|
|
678
|
-
ticketlens schedule --local
|
|
681
|
+
ticketlens schedule --local --time=07:00 --save=./triage.txt # Local-only cron/LaunchAgent, no Console auth [Pro]
|
|
679
682
|
|
|
680
683
|
# ── History ───────────────────────────────────────────────────────────────────
|
|
681
684
|
ticketlens history <TICKET-KEY> # Show urgency timeline for a ticket [Pro]
|
package/bin/ticketlens.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
printProfilesHelp, printScheduleHelp,
|
|
25
25
|
printInitHelp, printSwitchHelp, printConfigHelp,
|
|
26
26
|
printReviewHelp, printStandupHelp, printUpdateSkillHelp,
|
|
27
|
+
printComplianceHelp, printLedgerHelp, printPrHelp, printInstallHooksHelp,
|
|
27
28
|
printCollisionsHelp, printStatsHelp,
|
|
28
29
|
printCloudKeysHelp,
|
|
29
30
|
printNoteHelp, printRecallHelp,
|
|
@@ -394,10 +395,28 @@ switch (command) {
|
|
|
394
395
|
break;
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
const
|
|
398
|
+
const cliTokenForSchedule = readCliToken();
|
|
399
|
+
const { shouldUseLocalSchedule, parseLocalScheduleFlags, runScheduleLocal, runScheduleWizard } = await import('../skills/jtb/scripts/lib/schedule-wizard.mjs');
|
|
400
|
+
|
|
401
|
+
if (shouldUseLocalSchedule({ cmdArgs, cliToken: cliTokenForSchedule })) {
|
|
402
|
+
const { time, outputFile } = parseLocalScheduleFlags(cmdArgs);
|
|
403
|
+
if (!time || !outputFile) {
|
|
404
|
+
process.stderr.write(` Local scheduling requires --time and --save.\n`);
|
|
405
|
+
process.stderr.write(` Usage: ticketlens schedule --local --time=HH:MM --save=FILE\n`);
|
|
406
|
+
if (!cliTokenForSchedule) {
|
|
407
|
+
process.stderr.write(` Or run \`ticketlens login\` first to use the Console-backed wizard.\n`);
|
|
408
|
+
}
|
|
409
|
+
process.exitCode = 1;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
const localResult = await runScheduleLocal({ answers: { time, outputFile } });
|
|
413
|
+
if (!localResult.ok) { process.exitCode = 1; }
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
|
|
398
417
|
const { promptScheduleAnswers } = await import('../skills/jtb/scripts/lib/prompt-helpers.mjs');
|
|
399
418
|
const answers = await promptScheduleAnswers(cmdArgs);
|
|
400
|
-
const result = await runScheduleWizard({ answers, cliToken:
|
|
419
|
+
const result = await runScheduleWizard({ answers, cliToken: cliTokenForSchedule });
|
|
401
420
|
if (!result.ok) { process.exitCode = 1; break; }
|
|
402
421
|
|
|
403
422
|
const s = createStyler({ isTTY: process.stdout.isTTY });
|
|
@@ -410,6 +429,7 @@ switch (command) {
|
|
|
410
429
|
}
|
|
411
430
|
|
|
412
431
|
case 'install-hooks':
|
|
432
|
+
if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printInstallHooksHelp(); break; }
|
|
413
433
|
runFetch(['install-hooks', ...cmdArgs]).catch(err => {
|
|
414
434
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
415
435
|
process.exitCode = 1;
|
|
@@ -417,6 +437,7 @@ switch (command) {
|
|
|
417
437
|
break;
|
|
418
438
|
|
|
419
439
|
case 'pr':
|
|
440
|
+
if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printPrHelp(); break; }
|
|
420
441
|
runFetch(['pr', ...cmdArgs]).catch(err => {
|
|
421
442
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
422
443
|
process.exitCode = 1;
|
|
@@ -440,6 +461,7 @@ switch (command) {
|
|
|
440
461
|
break;
|
|
441
462
|
|
|
442
463
|
case 'ledger':
|
|
464
|
+
if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printLedgerHelp(); break; }
|
|
443
465
|
runFetch(['ledger', ...cmdArgs]).catch(err => {
|
|
444
466
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
445
467
|
process.exitCode = 1;
|
|
@@ -447,6 +469,7 @@ switch (command) {
|
|
|
447
469
|
break;
|
|
448
470
|
|
|
449
471
|
case 'compliance':
|
|
472
|
+
if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printComplianceHelp(); break; }
|
|
450
473
|
runFetch(['compliance', ...cmdArgs]).catch(err => {
|
|
451
474
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
452
475
|
process.exitCode = 1;
|
package/package.json
CHANGED
package/skills/jtb/SKILL.md
CHANGED
|
@@ -44,6 +44,10 @@ Fetches a Jira ticket and produces a structured brief with code references, then
|
|
|
44
44
|
/jtb stats --format=json # JSON output for scripting
|
|
45
45
|
/jtb collisions # show branch collisions with teammates (Team)
|
|
46
46
|
/jtb collisions --json # machine-readable output
|
|
47
|
+
/jtb review # code-review context brief from current branch
|
|
48
|
+
/jtb review --branch=main # compare against a specific branch
|
|
49
|
+
/jtb standup # standup summary from git log (last 24h)
|
|
50
|
+
/jtb standup --format=pr # PR body instead of standup bullets
|
|
47
51
|
/jtb cloud-keys list # list configured AI provider keys (Pro)
|
|
48
52
|
/jtb cloud-keys add groq gsk_xxxx # add Groq key (free tier — console.groq.com)
|
|
49
53
|
/jtb cloud-keys add anthropic sk-ant-x # add Anthropic key
|
|
@@ -52,7 +56,7 @@ Fetches a Jira ticket and produces a structured brief with code references, then
|
|
|
52
56
|
/jtb cloud-keys remove groq # remove a provider
|
|
53
57
|
/jtb cloud-keys priority groq 1 # set provider priority (lower = tried first)
|
|
54
58
|
/jtb cloud-keys timeout anthropic 15 # set per-request timeout in seconds
|
|
55
|
-
/jtb note "gotcha text" --ticket=PROD-1234 # save a Recall note (Pro)
|
|
59
|
+
/jtb note add --title="gotcha text" --ticket=PROD-1234 # save a Recall note (Pro, body from stdin)
|
|
56
60
|
/jtb recall PROD-1234 # search saved Recall notes (Pro)
|
|
57
61
|
/jtb recall sync # retry any notes stuck in the local queue (Pro)
|
|
58
62
|
/jtb recall settings # show effective retry-queue settings, fetched live (Pro)
|
|
@@ -493,6 +493,22 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
|
|
|
493
493
|
|
|
494
494
|
// Early dispatch for non-ticket subcommands
|
|
495
495
|
if (args[0] === 'install-hooks') {
|
|
496
|
+
if (args.includes('--uninstall')) {
|
|
497
|
+
const { uninstallHook } = await import('./lib/hook-installer.mjs');
|
|
498
|
+
try {
|
|
499
|
+
const result = await uninstallHook({ cwd: process.cwd() });
|
|
500
|
+
if (result.skipped) {
|
|
501
|
+
process.stderr.write(` Hook uninstall skipped: ${result.reason}\n`);
|
|
502
|
+
} else {
|
|
503
|
+
process.stdout.write(` Hook removed: ${result.path}\n`);
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
process.stderr.write(` Error removing hook: ${err.message}\n`);
|
|
507
|
+
process.exitCode = 1;
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
496
512
|
const { installHook } = await import('./lib/hook-installer.mjs');
|
|
497
513
|
try {
|
|
498
514
|
const result = await installHook({ cwd: process.cwd() });
|
|
@@ -43,23 +43,30 @@ export function printHelp({ stream = process.stdout } = {}) {
|
|
|
43
43
|
` ${s.brand('ticketlens')} review ${s.dim('[--branch=BRANCH]')} Code-review context brief from current branch`,
|
|
44
44
|
` ${s.brand('ticketlens')} standup ${s.dim('[--since=N]')} Standup summary from git log ${s.dim('(last 24h by default)')}`,
|
|
45
45
|
` ${s.brand('ticketlens')} compliance ${s.dim('<TICKET-KEY>')} Check requirements coverage ${s.dim('[Pro/Free 3/mo]')}`,
|
|
46
|
+
` ${s.brand('ticketlens')} install-hooks ${s.dim('[--uninstall]')} Git pre-push compliance gate`,
|
|
47
|
+
` ${s.brand('ticketlens')} pr ${s.dim('<TICKET-KEY>')} Assemble a PR description from ticket context`,
|
|
48
|
+
` ${s.brand('ticketlens')} ledger ${s.dim('[--format=json|csv]')} Export your signed usage ledger ${s.dim('[Pro]')}`,
|
|
46
49
|
` ${s.brand('ticketlens')} history ${s.dim('<TICKET-KEY>')} Urgency timeline for a ticket ${s.dim('[Pro]')}`,
|
|
47
50
|
` ${s.brand('ticketlens')} stats ${s.dim('[options]')} Personal response-time metrics from local history`,
|
|
48
51
|
` ${s.brand('ticketlens')} note add ${s.dim('--title=... [--ticket=KEY]')} Save a Recall note ${s.dim('[Pro]')}`,
|
|
49
52
|
` ${s.brand('ticketlens')} note delete ${s.dim('--id=... [--ticket=KEY]')} Remove a note from your local vault ${s.dim('[Pro]')}`,
|
|
50
53
|
` ${s.brand('ticketlens')} recall ${s.dim('<query|TICKET-KEY>')} Search your saved Recall notes ${s.dim('[Pro]')}`,
|
|
54
|
+
` ${s.brand('ticketlens')} recall sync Retry any notes stuck in the local queue ${s.dim('[Pro]')}`,
|
|
55
|
+
` ${s.brand('ticketlens')} recall settings Show effective retry-queue settings, fetched live ${s.dim('[Pro]')}`,
|
|
51
56
|
'',
|
|
52
57
|
` ${s.brand('ticketlens')} delete ${s.dim('<PROFILE-NAME>')} Remove a profile`,
|
|
53
58
|
` ${s.brand('ticketlens')} activate ${s.dim('<KEY>')} Activate a license key`,
|
|
54
59
|
` ${s.brand('ticketlens')} license Show license status`,
|
|
55
60
|
` ${s.brand('ticketlens')} cache ${s.dim('[size|clear]')} Manage attachment cache ${s.dim('(try cache --help)')}`,
|
|
56
|
-
` ${s.brand('ticketlens')} schedule ${s.dim('[--stop|--status]')} Manage digest schedule ${s.dim('[Pro]')}`,
|
|
61
|
+
` ${s.brand('ticketlens')} schedule ${s.dim('[--stop|--status|--local]')} Manage digest schedule ${s.dim('[Pro]')}`,
|
|
57
62
|
` ${s.brand('ticketlens')} cloud-keys ${s.dim('[add|remove|list|test]')} Manage your encrypted AI provider keys ${s.dim('[Pro]')}`,
|
|
58
63
|
` ${s.brand('ticketlens')} update-skill ${s.dim('[--dry-run]')} Update /jtb skill in Claude Code and other AI assistants`,
|
|
59
64
|
'',
|
|
60
65
|
` ${s.bold('GLOBAL OPTIONS')}`,
|
|
61
66
|
'',
|
|
62
67
|
` ${s.brand('--no-input')} Force non-interactive behavior even in a terminal`,
|
|
68
|
+
` ${s.brand('-v')}, ${s.brand('--version')} Show version and exit ${s.dim('(same as ticketlens version)')}`,
|
|
69
|
+
` ${s.brand('clear')} Alias for ${s.brand('cache clear')}`,
|
|
63
70
|
'',
|
|
64
71
|
` ${s.bold('FETCH OPTIONS')}`,
|
|
65
72
|
'',
|
|
@@ -78,6 +85,7 @@ export function printHelp({ stream = process.stdout } = {}) {
|
|
|
78
85
|
` ${s.brand('--cloud')} Route AI request through TicketLens API ${s.dim('[Pro]')}`,
|
|
79
86
|
` ${s.brand('--provider')}=${s.dim('NAME')} Force AI provider ${s.dim('(anthropic|openai|groq)')}`,
|
|
80
87
|
` ${s.brand('--template')}=${s.dim('SLUG')} Apply a brief template ${s.dim('(full|quick|code-review, or custom [Team])')}`,
|
|
88
|
+
` ${s.brand('--budget')}=${s.dim('N')} Trim brief to fit a token budget ${s.dim('[Pro]')}`,
|
|
81
89
|
'',
|
|
82
90
|
` ${s.bold('TRIAGE OPTIONS')}`,
|
|
83
91
|
'',
|
|
@@ -100,6 +108,7 @@ export function printHelp({ stream = process.stdout } = {}) {
|
|
|
100
108
|
` ${s.brand('--digest')} POST scored results to digest endpoint ${s.dim('[Pro]')}`,
|
|
101
109
|
` ${s.brand('--static')} Static table output ${s.dim('(skip interactive mode)')}`,
|
|
102
110
|
` ${s.brand('--plain')} Plain markdown output ${s.dim('(for piping / LLM)')}`,
|
|
111
|
+
` ${s.brand('--styled')} Force ANSI-styled table output`,
|
|
103
112
|
'',
|
|
104
113
|
` ${s.bold('EXAMPLES')}`,
|
|
105
114
|
'',
|
|
@@ -444,15 +453,23 @@ export function printScheduleHelp({ stream = process.stdout } = {}) {
|
|
|
444
453
|
const s = createStyler({ isTTY: stream.isTTY });
|
|
445
454
|
const lines = [
|
|
446
455
|
'',
|
|
447
|
-
` ${s.bold(s.brand('ticketlens'))} ${s.bold('schedule')} ${s.dim('[--stop|--status]')} ${s.dim('[Pro]')}`,
|
|
456
|
+
` ${s.bold(s.brand('ticketlens'))} ${s.bold('schedule')} ${s.dim('[--stop|--status|--local]')} ${s.dim('[Pro]')}`,
|
|
448
457
|
'',
|
|
449
458
|
` Set up a recurring digest email with your triage results. ${s.dim('[Pro]')}`,
|
|
450
459
|
` Runs an interactive wizard to configure day, time, and timezone.`,
|
|
460
|
+
` Without Console login, falls back to local-only scheduling automatically —`,
|
|
461
|
+
` no interactive wizard, no digest email, just a cron/LaunchAgent entry that`,
|
|
462
|
+
` writes triage output to a file.`,
|
|
451
463
|
'',
|
|
452
464
|
` ${s.bold('OPTIONS')}`,
|
|
453
465
|
'',
|
|
454
466
|
` ${s.brand('--stop')} Cancel the active digest schedule`,
|
|
455
467
|
` ${s.brand('--status')} Show current schedule configuration`,
|
|
468
|
+
` ${s.brand('--time')}=${s.dim('HH:MM')} Pre-fill delivery/run time ${s.dim('(skips the interactive prompt)')}`,
|
|
469
|
+
` ${s.brand('--email')}=${s.dim('ADDR')} Pre-fill delivery email ${s.dim('(Console-backed wizard only)')}`,
|
|
470
|
+
` ${s.brand('--timezone')}=${s.dim('TZ')} Pre-fill timezone ${s.dim('(Console-backed wizard only)')}`,
|
|
471
|
+
` ${s.brand('--local')} Local-only cron/LaunchAgent, no Console auth ${s.dim('(auto-detected when logged out)')}`,
|
|
472
|
+
` ${s.brand('--save')}=${s.dim('FILE')} Output file for local mode ${s.dim('(required with --local)')}`,
|
|
456
473
|
` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
|
|
457
474
|
'',
|
|
458
475
|
` ${s.bold('EXAMPLES')}`,
|
|
@@ -460,6 +477,7 @@ export function printScheduleHelp({ stream = process.stdout } = {}) {
|
|
|
460
477
|
` ${s.dim('$')} ticketlens schedule`,
|
|
461
478
|
` ${s.dim('$')} ticketlens schedule --status`,
|
|
462
479
|
` ${s.dim('$')} ticketlens schedule --stop`,
|
|
480
|
+
` ${s.dim('$')} ticketlens schedule --local --time=07:00 --save=./triage.txt`,
|
|
463
481
|
'',
|
|
464
482
|
];
|
|
465
483
|
stream.write(lines.join('\n') + '\n');
|
|
@@ -710,6 +728,108 @@ export function printReviewHelp({ stream = process.stdout } = {}) {
|
|
|
710
728
|
stream.write(lines.join('\n') + '\n');
|
|
711
729
|
}
|
|
712
730
|
|
|
731
|
+
export function printComplianceHelp({ stream = process.stdout } = {}) {
|
|
732
|
+
const s = createStyler({ isTTY: stream.isTTY });
|
|
733
|
+
const lines = [
|
|
734
|
+
'',
|
|
735
|
+
` ${s.bold(s.brand('ticketlens'))} ${s.bold('compliance')} ${s.dim('<TICKET-KEY> [--profile=NAME]')} ${s.dim('[Pro/Free 3/mo]')}`,
|
|
736
|
+
'',
|
|
737
|
+
` Check your current branch's diff against the ticket's requirements.`,
|
|
738
|
+
` Extracts candidate requirements from the ticket description and diffs`,
|
|
739
|
+
` them against what the local git diff actually covers, reporting a`,
|
|
740
|
+
` coverage percentage and a list of uncovered items.`,
|
|
741
|
+
'',
|
|
742
|
+
` Used internally by the pre-push hook installed via ${s.brand('ticketlens install-hooks')}`,
|
|
743
|
+
` (reads the threshold from ${s.dim('.ticketlens-hooks.json')}, default 80%).`,
|
|
744
|
+
'',
|
|
745
|
+
` ${s.bold('OPTIONS')}`,
|
|
746
|
+
'',
|
|
747
|
+
` ${s.brand('--profile')}=${s.dim('NAME')} Use a specific Jira profile`,
|
|
748
|
+
` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
|
|
749
|
+
'',
|
|
750
|
+
` ${s.bold('EXAMPLES')}`,
|
|
751
|
+
'',
|
|
752
|
+
` ${s.dim('$')} ticketlens compliance PROJ-123`,
|
|
753
|
+
` ${s.dim('$')} ticketlens compliance PROJ-123 --profile=myteam`,
|
|
754
|
+
'',
|
|
755
|
+
];
|
|
756
|
+
stream.write(lines.join('\n') + '\n');
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export function printLedgerHelp({ stream = process.stdout } = {}) {
|
|
760
|
+
const s = createStyler({ isTTY: stream.isTTY });
|
|
761
|
+
const lines = [
|
|
762
|
+
'',
|
|
763
|
+
` ${s.bold(s.brand('ticketlens'))} ${s.bold('ledger')} ${s.dim('[--format=json|csv]')} ${s.dim('[Pro]')}`,
|
|
764
|
+
'',
|
|
765
|
+
` Export your local usage ledger — a signed, tamper-evident record of`,
|
|
766
|
+
` billable actions (AI calls, exports, etc). Verifiable offline via an`,
|
|
767
|
+
` HMAC-SHA256 signature over {records, exportedAt}, keyed at ledger-key.`,
|
|
768
|
+
'',
|
|
769
|
+
` ${s.bold('OPTIONS')}`,
|
|
770
|
+
'',
|
|
771
|
+
` ${s.brand('--format')}=${s.dim('json')} Full record export with signature ${s.dim('(default)')}`,
|
|
772
|
+
` ${s.brand('--format')}=${s.dim('csv')} Flat CSV, no signature`,
|
|
773
|
+
` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
|
|
774
|
+
'',
|
|
775
|
+
` ${s.bold('EXAMPLES')}`,
|
|
776
|
+
'',
|
|
777
|
+
` ${s.dim('$')} ticketlens ledger`,
|
|
778
|
+
` ${s.dim('$')} ticketlens ledger --format=csv`,
|
|
779
|
+
'',
|
|
780
|
+
];
|
|
781
|
+
stream.write(lines.join('\n') + '\n');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export function printPrHelp({ stream = process.stdout } = {}) {
|
|
785
|
+
const s = createStyler({ isTTY: stream.isTTY });
|
|
786
|
+
const lines = [
|
|
787
|
+
'',
|
|
788
|
+
` ${s.bold(s.brand('ticketlens'))} ${s.bold('pr')} ${s.dim('<TICKET-KEY> [--profile=NAME]')}`,
|
|
789
|
+
'',
|
|
790
|
+
` Assemble a pull-request description from a ticket's context —`,
|
|
791
|
+
` summary, acceptance criteria, and linked issues, formatted as a`,
|
|
792
|
+
` ready-to-paste PR body.`,
|
|
793
|
+
'',
|
|
794
|
+
` ${s.bold('OPTIONS')}`,
|
|
795
|
+
'',
|
|
796
|
+
` ${s.brand('--profile')}=${s.dim('NAME')} Use a specific Jira profile`,
|
|
797
|
+
` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
|
|
798
|
+
'',
|
|
799
|
+
` ${s.bold('EXAMPLES')}`,
|
|
800
|
+
'',
|
|
801
|
+
` ${s.dim('$')} ticketlens pr PROJ-123`,
|
|
802
|
+
` ${s.dim('$')} ticketlens pr PROJ-123 --profile=myteam`,
|
|
803
|
+
'',
|
|
804
|
+
];
|
|
805
|
+
stream.write(lines.join('\n') + '\n');
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function printInstallHooksHelp({ stream = process.stdout } = {}) {
|
|
809
|
+
const s = createStyler({ isTTY: stream.isTTY });
|
|
810
|
+
const lines = [
|
|
811
|
+
'',
|
|
812
|
+
` ${s.bold(s.brand('ticketlens'))} ${s.bold('install-hooks')} ${s.dim('[--uninstall]')}`,
|
|
813
|
+
'',
|
|
814
|
+
` Installs a git pre-push hook that blocks a push when this branch's`,
|
|
815
|
+
` compliance coverage (see ${s.brand('ticketlens compliance')}) is below 80%.`,
|
|
816
|
+
` Appends to an existing pre-push hook rather than overwriting it;`,
|
|
817
|
+
` calling it again is a no-op if already installed.`,
|
|
818
|
+
'',
|
|
819
|
+
` ${s.bold('OPTIONS')}`,
|
|
820
|
+
'',
|
|
821
|
+
` ${s.brand('--uninstall')} Remove the hook, restoring any pre-existing pre-push content`,
|
|
822
|
+
` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
|
|
823
|
+
'',
|
|
824
|
+
` ${s.bold('EXAMPLES')}`,
|
|
825
|
+
'',
|
|
826
|
+
` ${s.dim('$')} ticketlens install-hooks`,
|
|
827
|
+
` ${s.dim('$')} ticketlens install-hooks --uninstall`,
|
|
828
|
+
'',
|
|
829
|
+
];
|
|
830
|
+
stream.write(lines.join('\n') + '\n');
|
|
831
|
+
}
|
|
832
|
+
|
|
713
833
|
export function printUpdateSkillHelp({ stream = process.stdout } = {}) {
|
|
714
834
|
const s = createStyler({ isTTY: stream.isTTY });
|
|
715
835
|
const lines = [
|
|
@@ -79,3 +79,51 @@ export function installHook({ cwd = process.cwd(), threshold = 80, fsModule = fs
|
|
|
79
79
|
|
|
80
80
|
return { installed: true, path: hookPath };
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Removes a previously-installed compliance gate, leaving any pre-existing
|
|
85
|
+
* (non-ticketlens) pre-push hook content untouched. installHook() always
|
|
86
|
+
* appends its block last, prefixed with its own `#!/bin/sh` line — this
|
|
87
|
+
* strips from that shebang line (and one preceding blank line from the
|
|
88
|
+
* append's leading '\n', if present) through end of file. If nothing
|
|
89
|
+
* precedes that point, the file is deleted rather than left empty.
|
|
90
|
+
*
|
|
91
|
+
* @param {{ cwd?: string, fsModule?: typeof import('node:fs') }} [opts]
|
|
92
|
+
* @returns {{ uninstalled: true, path: string } | { skipped: true, reason: string }}
|
|
93
|
+
*/
|
|
94
|
+
export function uninstallHook({ cwd = process.cwd(), fsModule = fs } = {}) {
|
|
95
|
+
const hookPath = join(cwd, '.git', 'hooks', 'pre-push');
|
|
96
|
+
|
|
97
|
+
let existing;
|
|
98
|
+
try {
|
|
99
|
+
existing = fsModule.readFileSync(hookPath, 'utf8');
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err.code === 'ENOENT') return { skipped: true, reason: 'No pre-push hook installed.' };
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!existing.includes(GUARD)) {
|
|
106
|
+
return { skipped: true, reason: 'No ticketlens compliance gate found in the pre-push hook.' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const guardIdx = existing.indexOf(GUARD);
|
|
110
|
+
const shebangIdx = existing.lastIndexOf('#!/bin/sh\n', guardIdx);
|
|
111
|
+
let cutStart = shebangIdx;
|
|
112
|
+
if (cutStart > 0 && existing[cutStart - 1] === '\n') cutStart -= 1;
|
|
113
|
+
|
|
114
|
+
const remaining = existing.slice(0, cutStart);
|
|
115
|
+
if (remaining.trim() === '') {
|
|
116
|
+
fsModule.unlinkSync(hookPath);
|
|
117
|
+
} else {
|
|
118
|
+
fsModule.writeFileSync(hookPath, remaining, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const configPath = join(cwd, '.ticketlens-hooks.json');
|
|
122
|
+
try {
|
|
123
|
+
fsModule.unlinkSync(configPath);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err.code !== 'ENOENT') throw err;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { uninstalled: true, path: hookPath };
|
|
129
|
+
}
|
|
@@ -252,6 +252,33 @@ export function buildLocalCronLine({ hour, minute, ticketlensBin, outputFile })
|
|
|
252
252
|
return `${minute} ${hour} * * * ${ticketlensBin} triage '--save=${outputFile}' >> '${outputFile}.err' 2>&1`;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Decides whether `ticketlens schedule` should use the local-only path
|
|
257
|
+
* (runScheduleLocal) instead of the Console-backed wizard (runScheduleWizard)
|
|
258
|
+
* — explicit `--local`, or auto-detected whenever there's no cliToken at all,
|
|
259
|
+
* so a logged-out user gets a working local schedule instead of a hard
|
|
260
|
+
* "log in first" error.
|
|
261
|
+
*
|
|
262
|
+
* @param {{ cmdArgs?: string[], cliToken?: string|null }} opts
|
|
263
|
+
* @returns {boolean}
|
|
264
|
+
*/
|
|
265
|
+
export function shouldUseLocalSchedule({ cmdArgs = [], cliToken } = {}) {
|
|
266
|
+
return cmdArgs.includes('--local') || !cliToken;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Extracts the --time= and --save= flags local scheduling needs. Both are
|
|
271
|
+
* required by runScheduleLocal itself; this just does the string parsing.
|
|
272
|
+
*
|
|
273
|
+
* @param {string[]} cmdArgs
|
|
274
|
+
* @returns {{ time?: string, outputFile?: string }}
|
|
275
|
+
*/
|
|
276
|
+
export function parseLocalScheduleFlags(cmdArgs = []) {
|
|
277
|
+
const time = cmdArgs.find(a => a.startsWith('--time='))?.split('=')[1];
|
|
278
|
+
const outputFile = cmdArgs.find(a => a.startsWith('--save='))?.split('=')[1];
|
|
279
|
+
return { time, outputFile };
|
|
280
|
+
}
|
|
281
|
+
|
|
255
282
|
/**
|
|
256
283
|
* Set up a local-only scheduled triage (no Console auth, no cloud push).
|
|
257
284
|
* Writes a cron/LaunchAgent entry that runs `ticketlens triage --save=FILE`.
|