@tacone/prosey 0.2.3 → 0.2.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/bin/prosey CHANGED
@@ -2114,7 +2114,7 @@ var require_symbol_define_to_primitive = __commonJS((exports, module) => {
2114
2114
  var valueOf = SymbolPrototype && SymbolPrototype.valueOf;
2115
2115
  var TO_PRIMITIVE = wellKnownSymbol("toPrimitive");
2116
2116
  if (SymbolPrototype && !SymbolPrototype[TO_PRIMITIVE]) {
2117
- defineBuiltIn(SymbolPrototype, TO_PRIMITIVE, function(hint) {
2117
+ defineBuiltIn(SymbolPrototype, TO_PRIMITIVE, function(hint2) {
2118
2118
  return call(valueOf, this);
2119
2119
  }, { arity: 1 });
2120
2120
  }
@@ -4397,9 +4397,9 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
4397
4397
  return global2 ? result || [] : result && result[0];
4398
4398
  };
4399
4399
  XRegExp.matchChain = function(str, chain) {
4400
- return function recurseChain(values, level) {
4401
- var item = chain[level].regex ? chain[level] : {
4402
- regex: chain[level]
4400
+ return function recurseChain(values, level2) {
4401
+ var item = chain[level2].regex ? chain[level2] : {
4402
+ regex: chain[level2]
4403
4403
  };
4404
4404
  var matches = [];
4405
4405
  function addMatch(match) {
@@ -4430,7 +4430,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
4430
4430
  } finally {
4431
4431
  _iterator3.f();
4432
4432
  }
4433
- return level === chain.length - 1 || !matches.length ? matches : recurseChain(matches, level + 1);
4433
+ return level2 === chain.length - 1 || !matches.length ? matches : recurseChain(matches, level2 + 1);
4434
4434
  }([str], 0);
4435
4435
  };
4436
4436
  XRegExp.replace = function(str, search, replacement, scope) {
@@ -94145,9 +94145,81 @@ ${Me13.join(`
94145
94145
  Y42 = k0(G42);
94146
94146
  });
94147
94147
 
94148
+ // src/debug.ts
94149
+ var GRAY = "\x1B[90m";
94150
+ var RESET = "\x1B[0m";
94151
+ var level = "normal";
94152
+ function setLevel(l) {
94153
+ level = l;
94154
+ }
94155
+ var lastTime = performance.now();
94156
+ var resumed = false;
94157
+ function stamp() {
94158
+ const now = performance.now();
94159
+ if (resumed) {
94160
+ lastTime = now;
94161
+ resumed = false;
94162
+ }
94163
+ const elapsed = now - lastTime;
94164
+ lastTime = now;
94165
+ const text = elapsed < 1000 ? `${elapsed.toFixed(0)}ms` : `${(elapsed / 1000).toFixed(1)}s`;
94166
+ return text.padStart(5);
94167
+ }
94168
+ var INFO_BEFORE = "\x1B[0m\x1B[2m\x1B[1m";
94169
+ var INFO_AFTER = "\x1B[0m";
94170
+ function info(...args) {
94171
+ if (level === "quiet")
94172
+ return;
94173
+ console.error(INFO_BEFORE, `[${stamp()}]`, ...args, INFO_AFTER);
94174
+ }
94175
+ function debug(...args) {
94176
+ if (level !== "verbose")
94177
+ return;
94178
+ console.error(GRAY, `[${stamp()}]`, ...args, RESET);
94179
+ }
94180
+ function startTimer() {
94181
+ resumed = true;
94182
+ }
94183
+ function resetTimer() {
94184
+ lastTime = performance.now();
94185
+ resumed = false;
94186
+ }
94187
+ var YELLOW = "\x1B[33m";
94188
+ function hint(message) {
94189
+ console.error(YELLOW + message + RESET + " " + GRAY + "[use prosey config to disable hints]" + RESET);
94190
+ }
94191
+
94148
94192
  // src/index.ts
94193
+ import { spawn as spawn2 } from "node:child_process";
94149
94194
  import { writeFile as writeFile3 } from "node:fs/promises";
94150
94195
 
94196
+ // src/pager.ts
94197
+ import { execSync } from "node:child_process";
94198
+ function hasCommand(cmd) {
94199
+ try {
94200
+ execSync(`command -v ${cmd} 2>/dev/null`, { stdio: "ignore" });
94201
+ return true;
94202
+ } catch {
94203
+ return false;
94204
+ }
94205
+ }
94206
+ function detectPager(cfgPager) {
94207
+ const env = process.env.PROSEY_PAGER;
94208
+ if (env !== undefined && env !== "" && env !== "auto")
94209
+ return env;
94210
+ if (cfgPager !== undefined && cfgPager !== "" && cfgPager !== "auto")
94211
+ return cfgPager;
94212
+ if (hasCommand("bat"))
94213
+ return "bat -lmd --style plain";
94214
+ if (hasCommand("glow"))
94215
+ return "glow -p";
94216
+ if (hasCommand("mdcat"))
94217
+ return "mdcat -l -p";
94218
+ if (hasCommand("less"))
94219
+ return "less";
94220
+ return null;
94221
+ }
94222
+
94151
94223
  // node_modules/youtube-transcript-plus/dist/youtube-transcript-plus.mjs
94152
94224
  function __awaiter(thisArg, _arguments, P, generator) {
94153
94225
  function adopt(value) {
@@ -101530,6 +101602,16 @@ var lt = (r) => {
101530
101602
  var FALLBACK_CONFIG_TOML = `# Default prosey configuration
101531
101603
  # Created automatically on first run. Edit as needed.
101532
101604
 
101605
+ # Pager command for transcript and summary output.
101606
+ # Defaults to "auto": bat -lmd --style plain → glow -p → mdcat -l -p → less
101607
+ # Set to a custom command (e.g. "less -R") to override.
101608
+ # Can also be set via the PROSEY_PAGER env var (takes precedence).
101609
+ pager = "auto"
101610
+
101611
+ # Show hints for missing tools (e.g. markdown highlighter).
101612
+ # Can also be set via PROSEY_HINTS env var (yes, no, 1, 0, true, false).
101613
+ hints = true
101614
+
101533
101615
  [summarize]
101534
101616
  # Prompt sent to the command via stdin.
101535
101617
  # Customize this to change how transcripts are summarized.
@@ -101674,23 +101756,10 @@ async function writeCache(dir, filename, data) {
101674
101756
  await mkdir2(dir, { recursive: true });
101675
101757
  await writeFile2(join2(dir, filename), data, "utf8");
101676
101758
  }
101677
-
101678
- // src/debug.ts
101679
- var GRAY = "\x1B[90m";
101680
- var RESET = "\x1B[0m";
101681
- var enabled = false;
101682
- function enableDebug() {
101683
- enabled = true;
101684
- }
101685
- function debug(...args) {
101686
- if (!enabled)
101687
- return;
101688
- console.error(GRAY, ...args, RESET);
101689
- }
101690
101759
  // package.json
101691
101760
  var package_default = {
101692
101761
  name: "@tacone/prosey",
101693
- version: "0.2.3",
101762
+ version: "0.2.6",
101694
101763
  description: "Download YouTube video transcripts from the CLI",
101695
101764
  module: "src/index.ts",
101696
101765
  type: "module",
@@ -101753,6 +101822,31 @@ var package_default = {
101753
101822
  }
101754
101823
  };
101755
101824
 
101825
+ // src/version-check.ts
101826
+ var TIMEOUT_MS = 3000;
101827
+ var registryUrl = `https://registry.npmjs.org/${package_default.name}/latest`;
101828
+ async function checkVersion() {
101829
+ try {
101830
+ const controller = new AbortController;
101831
+ const timer2 = setTimeout(() => controller.abort(), TIMEOUT_MS);
101832
+ const res = await fetch(registryUrl, { signal: controller.signal });
101833
+ clearTimeout(timer2);
101834
+ if (!res.ok) {
101835
+ debug("Version check failed: HTTP", res.status);
101836
+ return null;
101837
+ }
101838
+ const data = await res.json();
101839
+ if (!data.version) {
101840
+ debug("Version check: no version field in response");
101841
+ return null;
101842
+ }
101843
+ return data.version;
101844
+ } catch (err) {
101845
+ debug("Version check error:", err instanceof Error ? err.message : String(err));
101846
+ return null;
101847
+ }
101848
+ }
101849
+
101756
101850
  // node_modules/prettier/index.mjs
101757
101851
  import { createRequire as __prettierCreateRequire } from "module";
101758
101852
  import { fileURLToPath as __prettierFileUrlToPath } from "url";
@@ -102266,7 +102360,7 @@ function addAlignmentToDoc(doc, size, tabWidth) {
102266
102360
  assertDoc(doc);
102267
102361
  let aligned = doc;
102268
102362
  if (size > 0) {
102269
- for (let level = 0;level < Math.floor(size / tabWidth); ++level) {
102363
+ for (let level2 = 0;level2 < Math.floor(size / tabWidth); ++level2) {
102270
102364
  aligned = indent(aligned);
102271
102365
  }
102272
102366
  aligned = align(size % tabWidth, aligned);
@@ -107697,7 +107791,7 @@ var require_partial = __commonJS2({
107697
107791
  match(filepath) {
107698
107792
  const parts = filepath.split("/");
107699
107793
  const levels = parts.length;
107700
- const patterns = this._storage.filter((info) => !info.complete || info.segments.length > levels);
107794
+ const patterns = this._storage.filter((info2) => !info2.complete || info2.segments.length > levels);
107701
107795
  for (const pattern of patterns) {
107702
107796
  const section = pattern.sections[0];
107703
107797
  if (!pattern.complete && levels > section.length) {
@@ -108269,10 +108363,10 @@ var require_picocolors = __commonJS2({
108269
108363
  } while (~index);
108270
108364
  return result + string.substring(cursor2);
108271
108365
  };
108272
- var createColors2 = (enabled2 = isColorSupported2) => {
108273
- let f7 = enabled2 ? formatter : () => String;
108366
+ var createColors2 = (enabled = isColorSupported2) => {
108367
+ let f7 = enabled ? formatter : () => String;
108274
108368
  return {
108275
- isColorSupported: enabled2,
108369
+ isColorSupported: enabled,
108276
108370
  reset: f7("\x1B[0m", "\x1B[0m"),
108277
108371
  bold: f7("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
108278
108372
  dim: f7("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
@@ -113965,8 +114059,8 @@ function buildDefs(colors) {
113965
114059
  }
113966
114060
  var defsOn = buildDefs((0, import_picocolors4.createColors)(true));
113967
114061
  var defsOff = buildDefs((0, import_picocolors4.createColors)(false));
113968
- function getDefs(enabled2) {
113969
- return enabled2 ? defsOn : defsOff;
114062
+ function getDefs(enabled) {
114063
+ return enabled ? defsOn : defsOff;
113970
114064
  }
113971
114065
  var sometimesKeywords = /* @__PURE__ */ new Set(["as", "async", "from", "get", "of", "set"]);
113972
114066
  var NEWLINE$1 = /\r\n|[\n\r\u2028\u2029]/;
@@ -120293,18 +120387,30 @@ var debugApis = {
120293
120387
  // src/index.ts
120294
120388
  var NAME2 = "prosey";
120295
120389
  var VERSION2 = package_default.version;
120390
+ var latestVersion = null;
120391
+ var versionCheck = checkVersion().then((v11) => {
120392
+ latestVersion = v11;
120393
+ });
120394
+ function exitProcess(code) {
120395
+ if (useHints && code === 0 && latestVersion && latestVersion !== VERSION2) {
120396
+ hint(`\uD83D\uDCE6 New version available: ${latestVersion} — use npm/pnpm/bun -g i ${package_default.name} to upgrade`);
120397
+ }
120398
+ process.exit(code);
120399
+ }
120296
120400
  function help() {
120297
120401
  return `${NAME2} v${VERSION2}
120298
120402
 
120299
120403
  Usage: ${NAME2} [options] <video-url-or-id>
120300
120404
  ${NAME2} info [options] <video-url-or-id>
120301
120405
  ${NAME2} summarize [options] <video-url-or-id>
120406
+ ${NAME2} config
120302
120407
 
120303
120408
  Download a YouTube video transcript or show video details.
120304
120409
 
120305
120410
  Commands:
120306
120411
  info Show video metadata (title, channel, duration, etc.)
120307
120412
  summarize Pipe transcript to the command configured in [summarize]
120413
+ config Open config file in $EDITOR
120308
120414
 
120309
120415
  Arguments:
120310
120416
  video-url-or-id YouTube URL (full or short) or bare video ID
@@ -120322,7 +120428,12 @@ Options:
120322
120428
  --reset-config Reset config file to defaults and exit.
120323
120429
  --no-cache Skip cache and overwrite cache files.
120324
120430
  --no-format Skip prettier formatting.
120325
- --debug Print debug information to stderr.
120431
+ --no-pager Disable pager for stdout output.
120432
+ --pager Use pager for stdout output (default).
120433
+ --no-hints Disable hints.
120434
+ --hints Show hints (default).
120435
+ -q, --quiet Suppress all stderr logging.
120436
+ -v, --verbose Print debug information to stderr.
120326
120437
  --help Show this help message.
120327
120438
  --version Show version.
120328
120439
 
@@ -120392,23 +120503,56 @@ async function formatMd(text) {
120392
120503
  return text;
120393
120504
  }
120394
120505
  }
120506
+ async function outputText(text) {
120507
+ if (outputPath) {
120508
+ await writeFile3(outputPath, text, "utf8");
120509
+ return;
120510
+ }
120511
+ if (!pagerCmd || !process.stdout.isTTY) {
120512
+ process.stdout.write(text);
120513
+ return;
120514
+ }
120515
+ const parts = pagerCmd.split(/\s+/);
120516
+ const proc = spawn2(parts[0], parts.slice(1), {
120517
+ stdio: ["pipe", "inherit", "inherit"]
120518
+ });
120519
+ await new Promise((resolve3) => {
120520
+ let done = false;
120521
+ proc.on("error", () => {
120522
+ if (done)
120523
+ return;
120524
+ done = true;
120525
+ process.stdout.write(text);
120526
+ resolve3();
120527
+ });
120528
+ proc.on("exit", () => {
120529
+ if (done)
120530
+ return;
120531
+ done = true;
120532
+ resolve3();
120533
+ });
120534
+ proc.stdin.write(text);
120535
+ proc.stdin.end();
120536
+ });
120537
+ }
120538
+ var pagerCmd = null;
120395
120539
  var args = process.argv.slice(2);
120396
120540
  if (args.length === 0 || args.includes("--help")) {
120397
120541
  console.log(help());
120398
- process.exit(0);
120542
+ exitProcess(0);
120399
120543
  }
120400
120544
  if (args.includes("--version")) {
120401
120545
  console.log(VERSION2);
120402
- process.exit(0);
120546
+ exitProcess(0);
120403
120547
  }
120404
120548
  if (args.includes("--reset-config")) {
120405
120549
  const path15 = await resetConfig();
120406
120550
  console.log(`Config reset to defaults: ${path15}`);
120407
- process.exit(0);
120551
+ exitProcess(0);
120408
120552
  }
120409
120553
  var config = await loadConfig().catch(() => ({}));
120410
120554
  var mode = "transcript";
120411
- var subcmdIndex = args.findIndex((a5) => a5 === "info" || a5 === "summarize");
120555
+ var subcmdIndex = args.findIndex((a5) => a5 === "info" || a5 === "summarize" || a5 === "config");
120412
120556
  if (subcmdIndex !== -1) {
120413
120557
  mode = args[subcmdIndex];
120414
120558
  args.splice(subcmdIndex, 1);
@@ -120423,7 +120567,9 @@ var noDecode = false;
120423
120567
  var showDetails = true;
120424
120568
  var noCache = false;
120425
120569
  var noFormat = false;
120426
- var debugMode = false;
120570
+ var usePager = true;
120571
+ var useHints = true;
120572
+ var logLevel = "normal";
120427
120573
  for (let i = 0;i < args.length; i++) {
120428
120574
  const arg = args[i];
120429
120575
  if (!arg)
@@ -120432,7 +120578,7 @@ for (let i = 0;i < args.length; i++) {
120432
120578
  lang = args[++i] ?? undefined;
120433
120579
  if (!lang) {
120434
120580
  console.error("Error: --lang requires a language code");
120435
- process.exit(1);
120581
+ exitProcess(1);
120436
120582
  }
120437
120583
  } else if (arg === "--timestamps" || arg === "-t") {
120438
120584
  timestamps = true;
@@ -120442,7 +120588,7 @@ for (let i = 0;i < args.length; i++) {
120442
120588
  outputPath = args[++i] ?? undefined;
120443
120589
  if (!outputPath) {
120444
120590
  console.error("Error: -o/--output requires a file path");
120445
- process.exit(1);
120591
+ exitProcess(1);
120446
120592
  }
120447
120593
  } else if (arg === "--json") {
120448
120594
  outputJson = true;
@@ -120456,35 +120602,76 @@ for (let i = 0;i < args.length; i++) {
120456
120602
  noCache = true;
120457
120603
  } else if (arg === "--no-format") {
120458
120604
  noFormat = true;
120459
- } else if (arg === "--debug") {
120460
- debugMode = true;
120605
+ } else if (arg === "--no-pager") {
120606
+ usePager = false;
120607
+ } else if (arg === "--pager") {
120608
+ usePager = true;
120609
+ } else if (arg === "--no-hints") {
120610
+ useHints = false;
120611
+ } else if (arg === "--hints") {
120612
+ useHints = true;
120613
+ } else if (arg === "--quiet" || arg === "-q") {
120614
+ logLevel = "quiet";
120615
+ } else if (arg === "--verbose" || arg === "-v") {
120616
+ logLevel = "verbose";
120461
120617
  } else if (arg === "--no-decode-entities") {
120462
120618
  noDecode = true;
120463
120619
  } else if (arg.startsWith("-")) {
120464
120620
  console.error(`Unknown option: ${arg}`);
120465
- process.exit(1);
120621
+ exitProcess(1);
120466
120622
  } else {
120467
120623
  videoId = arg;
120468
120624
  }
120469
120625
  }
120626
+ if (mode === "config") {
120627
+ const path15 = configPath();
120628
+ const editor = process.env.EDITOR;
120629
+ if (editor) {
120630
+ await new Promise((resolve3) => {
120631
+ const proc = spawn2(editor, [path15], { stdio: "inherit" });
120632
+ proc.on("exit", () => resolve3());
120633
+ proc.on("error", () => resolve3());
120634
+ });
120635
+ } else {
120636
+ console.log(`Config file: ${path15}`);
120637
+ }
120638
+ exitProcess(0);
120639
+ }
120470
120640
  if (!videoId) {
120471
120641
  console.error("Error: missing video URL or ID");
120472
120642
  console.log(help());
120473
- process.exit(1);
120643
+ exitProcess(1);
120474
120644
  }
120475
120645
  var extracted = extractVideoId(videoId);
120476
120646
  if (!extracted) {
120477
120647
  console.error("Error: invalid YouTube video URL or ID");
120478
- process.exit(65);
120648
+ exitProcess(65);
120479
120649
  }
120480
120650
  videoId = extracted;
120481
- if (debugMode)
120482
- enableDebug();
120651
+ setLevel(logLevel);
120652
+ resetTimer();
120653
+ pagerCmd = usePager ? detectPager(config.pager) : null;
120654
+ debug("Pager:", pagerCmd ?? "none");
120655
+ {
120656
+ const envHints = process.env.PROSEY_HINTS;
120657
+ if (envHints !== undefined) {
120658
+ useHints = envHints === "yes" || envHints === "1" || envHints === "true";
120659
+ } else if (config.hints !== undefined) {
120660
+ useHints = config.hints;
120661
+ }
120662
+ }
120663
+ if (useHints) {
120664
+ const hasMarkdownPager = pagerCmd === "bat -lmd --style plain" || pagerCmd === "glow -p" || pagerCmd === "mdcat -l -p";
120665
+ if (!hasMarkdownPager) {
120666
+ hint("Tip: install a markdown highlighter for better output (e.g. bat, glow, mdcat)");
120667
+ }
120668
+ }
120483
120669
  debug("Config file:", configPath());
120484
120670
  debug("Video ID:", videoId);
120485
120671
  debug("Mode:", mode);
120486
120672
  if (lang)
120487
120673
  debug("Language:", lang);
120674
+ await Promise.race([versionCheck, new Promise((r5) => setTimeout(r5, 1000))]);
120488
120675
  try {
120489
120676
  if (mode === "info") {
120490
120677
  const result = await fetchTranscript(videoId, { videoDetails: true, lang });
@@ -120493,21 +120680,23 @@ try {
120493
120680
  } else {
120494
120681
  printVideoInfo(result.videoDetails);
120495
120682
  }
120496
- process.exit(0);
120683
+ exitProcess(0);
120497
120684
  }
120498
120685
  if (mode === "summarize") {
120499
120686
  if (!config.summarize?.command) {
120500
120687
  console.error("Error: [summarize] section with a command is required in config");
120501
- process.exit(1);
120688
+ exitProcess(1);
120502
120689
  }
120503
120690
  const cacheOpts2 = { lang, mode: "summarize", noDecode };
120504
120691
  const dir2 = cacheDir(videoId, cacheOpts2);
120505
120692
  let segments2 = null;
120506
120693
  let summary = null;
120694
+ startTimer();
120507
120695
  if (!noCache) {
120508
120696
  const cachedSegments = await readCache(dir2, "transcript.json");
120509
120697
  const cachedSummary = await readCache(dir2, "summary.md");
120510
120698
  if (cachedSegments && cachedSummary) {
120699
+ info("Transcript cached");
120511
120700
  debug("Cache hit:", dir2);
120512
120701
  segments2 = JSON.parse(cachedSegments);
120513
120702
  summary = cachedSummary;
@@ -120518,46 +120707,45 @@ try {
120518
120707
  debug("Cache skipped (--no-cache)");
120519
120708
  }
120520
120709
  if (!segments2) {
120521
- debug("Fetching transcript...");
120710
+ info("Fetching transcript...");
120522
120711
  segments2 = lang ? await fetchTranscript(videoId, { lang }) : await fetchTranscript(videoId);
120523
- debug(`Transcript fetched: ${segments2.length} segments`);
120712
+ info(`Transcript: ${segments2.length} segments`);
120524
120713
  await writeCache(dir2, "transcript.json", JSON.stringify(segments2));
120525
120714
  debug("Cache written: transcript.json");
120526
120715
  }
120527
120716
  const prompt = config.summarize.prompt ?? "";
120528
120717
  const transcriptText = toText(segments2, !noDecode);
120529
120718
  if (!summary) {
120530
- debug("Running command:", config.summarize.command);
120719
+ info(`Summarizing...`);
120531
120720
  summary = await summarize({
120532
120721
  prompt,
120533
120722
  command: config.summarize.command,
120534
120723
  transcript: transcriptText,
120535
120724
  cwd: dir2
120536
120725
  });
120537
- debug("Command exit: 0");
120726
+ info("Summary ready");
120538
120727
  await writeCache(dir2, "summary.md", summary);
120539
120728
  debug("Cache written: summary.md");
120540
120729
  }
120541
120730
  const formatted = noFormat ? summary : await formatMd(summary);
120542
- if (outputPath) {
120543
- await writeFile3(outputPath, formatted, "utf8");
120544
- } else {
120545
- console.log(formatted);
120546
- }
120547
- process.exit(0);
120731
+ await outputText(formatted + `
120732
+ `);
120733
+ exitProcess(0);
120548
120734
  } else if (listOnly) {
120549
120735
  const languages2 = await listLanguages(videoId);
120550
120736
  printLanguages(languages2);
120551
- process.exit(0);
120737
+ exitProcess(0);
120552
120738
  }
120553
120739
  const decode = !noDecode;
120554
120740
  const cacheOpts = { lang, timestamps, json: outputJson, noDecode };
120555
120741
  const dir = cacheDir(videoId, cacheOpts);
120556
120742
  let segments = null;
120557
120743
  let videoDetailsCache = null;
120744
+ startTimer();
120558
120745
  if (!noCache) {
120559
120746
  const cached = await readCache(dir, "transcript.json");
120560
120747
  if (cached) {
120748
+ info("Transcript cached");
120561
120749
  debug("Cache hit:", dir);
120562
120750
  segments = JSON.parse(cached);
120563
120751
  } else {
@@ -120567,7 +120755,7 @@ try {
120567
120755
  debug("Cache skipped (--no-cache)");
120568
120756
  }
120569
120757
  if (!segments) {
120570
- debug("Fetching transcript...");
120758
+ info("Fetching transcript...");
120571
120759
  if (showDetails && !outputJson) {
120572
120760
  const opts = lang ? { lang, videoDetails: true } : { videoDetails: true };
120573
120761
  const result = await fetchTranscript(videoId, opts);
@@ -120576,6 +120764,7 @@ try {
120576
120764
  } else {
120577
120765
  segments = lang ? await fetchTranscript(videoId, { lang }) : await fetchTranscript(videoId);
120578
120766
  }
120767
+ info(`Transcript: ${segments.length} segments`);
120579
120768
  await writeCache(dir, "transcript.json", JSON.stringify(segments));
120580
120769
  }
120581
120770
  if (showDetails && !outputJson) {
@@ -120591,24 +120780,17 @@ try {
120591
120780
 
120592
120781
  ` + transcriptText + `
120593
120782
  `;
120594
- if (outputPath) {
120595
- await writeFile3(outputPath, output, "utf8");
120596
- } else {
120597
- console.log(output);
120598
- }
120783
+ await outputText(output);
120599
120784
  } else {
120600
120785
  const output = outputJson ? toJSON(segments, decode) + `
120601
120786
  ` : timestamps ? formatWithTimestamps(segments, decode) + `
120602
120787
  ` : toText(segments, decode) + `
120603
120788
  `;
120604
- if (outputPath) {
120605
- await writeFile3(outputPath, output, "utf8");
120606
- } else {
120607
- console.log(output);
120608
- }
120789
+ await outputText(output);
120609
120790
  }
120791
+ exitProcess(0);
120610
120792
  } catch (err) {
120611
120793
  const message = err instanceof Error ? err.message : String(err);
120612
120794
  console.error(`Error: ${message}`);
120613
- process.exit(1);
120795
+ exitProcess(1);
120614
120796
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacone/prosey",
3
- "version": "0.2.3",
3
+ "version": "0.2.6",
4
4
  "description": "Download YouTube video transcripts from the CLI",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -0,0 +1,50 @@
1
+ import { describe, expect, test, afterEach } from "bun:test";
2
+ import { $ } from "bun";
3
+ import { rm } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+
6
+ const testConfigPath = "/tmp/prosey-test-config.toml";
7
+
8
+ afterEach(async () => {
9
+ try {
10
+ await rm(testConfigPath);
11
+ } catch {}
12
+ delete process.env.PROSEY_CONFIG_PATH;
13
+ });
14
+
15
+ describe("config subcommand", () => {
16
+ test("prints config path when EDITOR is not set", async () => {
17
+ delete process.env.EDITOR;
18
+ process.env.PROSEY_CONFIG_PATH = testConfigPath;
19
+
20
+ const { stdout, exitCode } = await $`bin/prosey config`.quiet();
21
+
22
+ expect(exitCode).toBe(0);
23
+ expect(stdout.toString().trim()).toBe(`Config file: ${testConfigPath}`);
24
+ });
25
+
26
+ test("creates config file when missing", async () => {
27
+ delete process.env.EDITOR;
28
+ process.env.PROSEY_CONFIG_PATH = testConfigPath;
29
+
30
+ expect(existsSync(testConfigPath)).toBe(false);
31
+ await $`bin/prosey config`.quiet();
32
+ expect(existsSync(testConfigPath)).toBe(true);
33
+ });
34
+
35
+ test("does not require a video ID", async () => {
36
+ delete process.env.EDITOR;
37
+ process.env.PROSEY_CONFIG_PATH = testConfigPath;
38
+
39
+ const { exitCode } = await $`bin/prosey config`.quiet();
40
+ expect(exitCode).toBe(0);
41
+ });
42
+
43
+ test("exits with code 0 after spawning editor", async () => {
44
+ process.env.EDITOR = "cat";
45
+ process.env.PROSEY_CONFIG_PATH = testConfigPath;
46
+
47
+ const { exitCode } = await $`bin/prosey config`.quiet();
48
+ expect(exitCode).toBe(0);
49
+ });
50
+ });
package/src/config.ts CHANGED
@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
6
6
  import { load } from "js-toml";
7
7
 
8
8
  export interface ProseyConfig {
9
+ pager?: string;
10
+ hints?: boolean;
9
11
  summarize?: {
10
12
  prompt?: string;
11
13
  command?: string;
@@ -15,6 +17,16 @@ export interface ProseyConfig {
15
17
  const FALLBACK_CONFIG_TOML = `# Default prosey configuration
16
18
  # Created automatically on first run. Edit as needed.
17
19
 
20
+ # Pager command for transcript and summary output.
21
+ # Defaults to "auto": bat -lmd --style plain → glow -p → mdcat -l -p → less
22
+ # Set to a custom command (e.g. "less -R") to override.
23
+ # Can also be set via the PROSEY_PAGER env var (takes precedence).
24
+ pager = "auto"
25
+
26
+ # Show hints for missing tools (e.g. markdown highlighter).
27
+ # Can also be set via PROSEY_HINTS env var (yes, no, 1, 0, true, false).
28
+ hints = true
29
+
18
30
  [summarize]
19
31
  # Prompt sent to the command via stdin.
20
32
  # Customize this to change how transcripts are summarized.