chrome-tools 1.1.6 → 1.1.7

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.
Files changed (44) hide show
  1. package/dist/bin/cli.js +556 -1
  2. package/dist/src/core/browsers.d.ts +7 -3
  3. package/dist/src/core/browsers.js +186 -1
  4. package/dist/src/core/chrome.d.ts +6 -4
  5. package/dist/src/core/chrome.js +240 -1
  6. package/dist/src/core/crypto.d.ts +1 -1
  7. package/dist/src/core/crypto.js +113 -1
  8. package/dist/src/core/db.d.ts +4 -2
  9. package/dist/src/core/db.js +90 -1
  10. package/dist/src/core/keychain.js +191 -1
  11. package/dist/src/core/limit.d.ts +4 -0
  12. package/dist/src/core/limit.js +12 -0
  13. package/dist/src/core/platform/darwin.js +31 -1
  14. package/dist/src/core/platform/index.d.ts +4 -3
  15. package/dist/src/core/platform/index.js +69 -1
  16. package/dist/src/core/platform/linux.js +64 -1
  17. package/dist/src/core/platform/types.d.ts +3 -2
  18. package/dist/src/core/platform/types.js +1 -1
  19. package/dist/src/core/platform/win32.js +114 -1
  20. package/dist/src/core/sql.d.ts +2 -0
  21. package/dist/src/core/sql.js +4 -0
  22. package/dist/src/index.d.ts +1 -1
  23. package/dist/src/index.js +34 -1
  24. package/dist/src/modules/autofill.d.ts +7 -15
  25. package/dist/src/modules/autofill.js +170 -1
  26. package/dist/src/modules/bookmarks.d.ts +3 -5
  27. package/dist/src/modules/bookmarks.js +83 -1
  28. package/dist/src/modules/cookies.d.ts +3 -5
  29. package/dist/src/modules/cookies.js +157 -1
  30. package/dist/src/modules/downloads.d.ts +3 -5
  31. package/dist/src/modules/downloads.js +80 -1
  32. package/dist/src/modules/extensions.d.ts +3 -5
  33. package/dist/src/modules/extensions.js +71 -1
  34. package/dist/src/modules/history.d.ts +5 -10
  35. package/dist/src/modules/history.js +83 -1
  36. package/dist/src/modules/local-storage.d.ts +3 -5
  37. package/dist/src/modules/local-storage.js +455 -1
  38. package/dist/src/modules/passwords.d.ts +3 -5
  39. package/dist/src/modules/passwords.js +64 -1
  40. package/dist/src/modules/search-engines.d.ts +3 -5
  41. package/dist/src/modules/search-engines.js +38 -1
  42. package/dist/src/modules/top-sites.d.ts +3 -5
  43. package/dist/src/modules/top-sites.js +33 -1
  44. package/package.json +14 -12
package/dist/bin/cli.js CHANGED
@@ -1,2 +1,557 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as o,existsSync as e}from"node:fs";import{join as r}from"node:path";import{Command as t}from"commander";import s from"chalk";import i from"cli-table3";import{discoverProfiles as n,getProfileMetadata as l,getProfileStates as a,getDirectorySize as c,detectInstalledBrowsers as d}from"../src/core/chrome.js";import{setActiveBrowser as m,getActiveBrowser as p,isKeychainAccessDenied as f}from"../src/core/platform/index.js";import{getPasswords as h}from"../src/modules/passwords.js";import{getBookmarks as u}from"../src/modules/bookmarks.js";import{getHistory as g,getSearchTerms as w}from"../src/modules/history.js";import{getCookies as b}from"../src/modules/cookies.js";import{getLocalStorage as y}from"../src/modules/local-storage.js";import{getAutofillEntries as S,getCreditCards as v,getAddresses as k}from"../src/modules/autofill.js";import{getDownloads as L}from"../src/modules/downloads.js";import{getExtensions as P}from"../src/modules/extensions.js";import{getSearchEngines as D}from"../src/modules/search-engines.js";import{getTopSites as j}from"../src/modules/top-sites.js";const N=function(){const t={...process.env},s=r(process.cwd(),".env");if(e(s))try{const e=o(s,"utf8");for(const o of e.split(/\r?\n/)){const e=o.trim();if(!e||e.startsWith("#"))continue;const r=e.indexOf("=");if(-1!==r){const o=e.substring(0,r).trim();let s=e.substring(r+1).trim();(s.startsWith('"')&&s.endsWith('"')||s.startsWith("'")&&s.endsWith("'"))&&(s=s.slice(1,-1)),t[o]=s}}}catch{}return t}(),B=N.PROFILE_PATH||void 0,C=N.PROFILE_NAME||void 0,$=N.BROWSER||N.CHROME_BROWSER||void 0;function O(o){console.log(JSON.stringify(o,null,2))}function R(o,e){return new i({head:o.map(o=>s.bold.white(o)),colWidths:e,wordWrap:!0,style:{border:["grey"],head:[]}})}function E(o,e){0===e?console.log(s.yellow(`\n⚠ No ${o} found.\n`)):console.log(s.green(`\n✔ Found ${e} ${o}.\n`))}let U=null;function x(o,e){return U?(U.push({browser:p().name,results:e}),!0):!!o.json&&(O(e),!0)}function F(o){const e=o.optsWithGlobals(),r=Boolean(e.browser)||Boolean(e.allBrowsers)||Boolean($);return e.browser=e.browser||$||void 0,e.profilePath||(e.profilePath=r?void 0:B),e.profile||(e.profile=r?void 0:C),e}function W(o,e){e.allBrowsers?function(o,e){const r=d();if(0===r.length)return e.json?O([]):void console.log(s.yellow("\n⚠ No supported Chromium browsers found on this machine.\n"));const t={...e,profilePath:void 0};if(e.json){U=[];try{for(const e of r){m(e.browser.id);try{o(t)}catch(o){U.push({browser:e.browser.name,results:{error:o?.message??String(o)}})}}O(U)}finally{U=null}}else for(const e of r){m(e.browser.id),console.log(s.bold.magenta(`\n══════════ ${e.browser.name} ══════════`));try{o(t)}catch(o){f(o)?(console.log(s.red(` 🔑 No keychain access — skipped (${o.service}).`)),console.log(s.dim(` ${o.message.split("\n").join("\n ")}`))):console.log(s.red(` ⚠ Skipped: ${o?.message??String(o)}`))}}}(o,e):(m(e.browser),o(e))}function I(o){const e=d({includeMissing:!0});if(o.json)return O(e.map(o=>({id:o.browser.id,name:o.browser.name,installed:o.installed,profileCount:o.profileCount,path:o.chromeDir,safeStorageLabel:o.browser.safeStorageLabel})));const r=e.filter(o=>o.installed);console.log(s.bold(`\n🌐 Supported browsers (${r.length} installed):\n`));const t=R(["Browser","Id","Installed","Profiles","Path"],[20,14,11,9,50]);for(const o of e)t.push([s.white(o.browser.name),s.cyan(o.browser.id),o.installed?s.green("✓"):s.dim("✗"),o.installed?s.yellow(String(o.profileCount)):s.dim("—"),s.dim(o.chromeDir)]);console.log(t.toString())}function M(o){return o.option("--profile-path <path>","Custom browser User Data Path (overrides --browser)").option("--profile <name>","Target a specific profile").option("--browser <name>","Browser to read (chrome, edge, brave, comet, vivaldi, opera, chromium…)").option("--all-browsers","Read from every installed Chromium browser").option("--json","Output as JSON")}function T(o){return o.option("--search <term>","Filter results (case-insensitive)")}function A(o,e=100){return o.option("--limit <n>","Limit number of results",String(e))}const z=new t,V=JSON.parse(o(new URL("../../package.json",import.meta.url),"utf8"));z.name("chrome-tools").description(s.bold.cyan("Chrome Data Toolkit")+s.dim(" — Extract passwords, bookmarks, history, cookies, and more from Chrome on macOS, Windows, and Linux")).version(V.version).option("--passwords","Shortcut: list saved passwords").option("--bookmarks","Shortcut: list bookmarks").option("--local-storage","Shortcut: list localStorage entries").option("--profiles","Shortcut: list profiles").option("--browsers","Shortcut: list supported / installed browsers").option("--size","Show each profile's size on disk when listing profiles (slower)").option("--profile-path <path>","Custom browser User Data Path (overrides --browser)").option("--profile <name>","Target a specific profile").option("--browser <name>","Browser to read (chrome, edge, brave, comet, vivaldi, opera, chromium…)").option("--all-browsers","Read from every installed Chromium browser").option("--json","Output as JSON").option("--search <term>","Filter results (for shortcut flags)").option("--limit <n>","Limit results (for shortcut flags)").action((o,e)=>{const r=F(e);r.browsers?I(r):r.passwords?W(G,r):r.bookmarks?W(_,r):r.localStorage?W(Y,r):W(H,r)});const J=z.command("profiles").description("List Chrome profiles");M(J).option("--size","Compute and show each profile's size on disk (slower)").action((o,e)=>{W(H,F(e))});M(J.command("states").description("Show detailed profile states with available data files")).action((o,e)=>W(K,F(e)));function H(o){const e=n(o.profilePath),r=l(o.profilePath),t=Boolean(o.size);if(o.json){const s=new Map(r.map(o=>[o.directory,o]));return void x(o,e.map(o=>({...o,...s.get(o.name)||{},...t?{sizeBytes:c(o.path)}:{}})))}if(0===e.length)return void console.log(s.yellow("\n⚠ No Chrome profiles found.\n"));const i=new Map(r.map(o=>[o.directory,o]));console.log(s.bold(`\n📁 Discovered ${e.length} profile(s):\n`));const a=R(t?["Directory","Display Name","Email","Size","Path"]:["Directory","Display Name","Email","Path"],t?[16,22,30,10,42]:[16,22,30,50]);for(const o of e){const e=i.get(o.name)||{},r=[s.cyan(o.name),s.white(e.name||o.name),s.dim(e.email||"—")];t&&r.push(s.yellow(Z(c(o.path)))),r.push(s.dim(o.path)),a.push(r)}console.log(a.toString()),E("profile(s)",e.length)}function K(o){const e=a(o.profilePath);if(o.json)x(o,e);else if(0!==e.length){console.log(s.bold(`\n📊 Profile States (${e.length} profile(s)):\n`));for(const o of e){console.log(s.bold.cyan(` ${o.displayName}`)+s.dim(` (${o.directory})`)),o.email&&console.log(s.dim(` Email: ${o.email}`)),console.log(s.dim(` Path: ${o.path}`));const e=o.dataFiles,r=Object.entries(e).filter(([,o])=>o).map(([o])=>o),t=Object.entries(e).filter(([,o])=>!o).map(([o])=>o);console.log(` Available: ${r.map(o=>s.green(o)).join(", ")||s.dim("none")}`),t.length>0&&console.log(` Missing: ${t.map(o=>s.dim(o)).join(", ")}`),console.log()}}else console.log(s.yellow("\n⚠ No Chrome profiles found.\n"))}z.command("browsers").description("List supported browsers and detect which are installed").option("--json","Output as JSON").action((o,e)=>I(F(e)));function G(o){const e=h({chromeDir:o.profilePath,profile:o.profile,search:o.search});if(x(o,e))return;if(0===e.length)return E("passwords",0);console.log(s.bold("\n🔑 Saved Passwords:\n"));const r=new Set(e.map(o=>o.profile)).size>1,t=["Source","URL","Username","Password","Last Used"],i=[9,42,24,22,20];r&&(t.unshift("Profile"),i.unshift(12));const n=R(t,i);for(const o of e){const e=[s.blue(o.source??"?"),s.cyan(o.url),s.green(o.username||"<empty>"),s.yellow(o.password||"<empty>"),s.dim(o.lastUsed)];r&&e.unshift(s.magenta(o.profile)),n.push(e)}console.log(n.toString()),E("password(s)",e.length)}T(M(z.command("passwords").description("List saved passwords"))).action((o,e)=>W(G,F(e)));function _(o){const e=u({chromeDir:o.profilePath,profile:o.profile,folder:o.folder,search:o.search});if(x(o,e))return;if(0===e.length)return E("bookmarks",0);console.log(s.bold("\n📚 Bookmarks:\n"));const r=R(["Title","URL","Folder","Date Added"],[30,50,25,20]);for(const o of e)r.push([s.white(o.title),s.cyan(o.url),s.dim(o.folder||"/"),s.dim(o.dateAdded)]);console.log(r.toString()),E("bookmark(s)",e.length)}T(M(z.command("bookmarks").description("List bookmarks"))).option("--folder <name>","Filter by folder name").action((o,e)=>W(_,F(e)));T(A(M(z.command("history").description("List browsing history")))).action((o,e)=>W(o=>{const e=g({chromeDir:o.profilePath,profile:o.profile,search:o.search,limit:parseInt(o.limit,10)||100});if(x(o,e))return;if(0===e.length)return E("history entries",0);console.log(s.bold("\n🕐 Browsing History:\n"));const r=R(["URL","Title","Visits","Last Visit"],[50,30,8,22]);for(const o of e)r.push([s.cyan(o.url),s.white(o.title||"—"),s.yellow(String(o.visitCount)),s.dim(o.lastVisit)]);console.log(r.toString()),E("history entry(ies)",e.length)},F(e)));A(M(z.command("search-terms").description("List search terms from history"))).action((o,e)=>W(o=>{const e=w({chromeDir:o.profilePath,profile:o.profile,limit:parseInt(o.limit,10)||100});if(x(o,e))return;if(0===e.length)return E("search terms",0);console.log(s.bold("\n🔍 Search Terms:\n"));const r=R(["Term","URL"],[40,80]);for(const o of e)r.push([s.white(o.term),s.dim(o.url)]);console.log(r.toString()),E("search term(s)",e.length)},F(e)));A(M(z.command("cookies").description("List cookies (encrypted values decrypted)"))).option("--domain <domain>","Filter by domain").option("--url <url>","Filter by URL with path").option("--no-decrypt","Skip decryption of encrypted values").action((o,e)=>W(o=>{const e=b({chromeDir:o.profilePath,profile:o.profile,domain:o.domain,url:o.url,decrypt:!1!==o.decrypt,limit:parseInt(o.limit,10)||100});if(x(o,e))return;if(0===e.length)return E("cookies",0);console.log(s.bold("\n🍪 Cookies:\n"));const r=R(["Domain","Name","Value","Secure","Expires"],[30,25,30,8,22]);for(const o of e)r.push([s.cyan(o.domain),s.white(o.name),s.dim(o.value?o.value.substring(0,50)+(o.value.length>50?"…":""):"—"),o.secure?s.green("✓"):s.dim("✗"),s.dim(o.expires)]);console.log(r.toString()),E("cookie(s)",e.length)},F(e)));function Y(o){const e=y({chromeDir:o.profilePath,profile:o.profile,origin:o.origin,url:o.url,key:o.key,search:o.search,limit:parseInt(o.limit,10)||100});if(x(o,e))return;if(0===e.length)return E("localStorage entries",0);console.log(s.bold("\n💾 localStorage:\n"));const r=R(["Origin","Key","Value"],[36,28,46]);for(const o of e)r.push([s.cyan(o.origin),s.white(o.key),s.dim(o.value?o.value.substring(0,80)+(o.value.length>80?"…":""):"—")]);console.log(r.toString()),E("localStorage entries",e.length)}T(A(M(z.command("local-storage").description("List localStorage entries")))).option("--origin <origin>","Filter by origin").option("--url <url>","Filter by URL origin").option("--key <key>","Filter by localStorage key").action((o,e)=>W(Y,F(e)));T(A(M(z.command("autofill").description("List autofill entries")))).action((o,e)=>W(o=>{const e=S({chromeDir:o.profilePath,profile:o.profile,search:o.search,limit:parseInt(o.limit,10)||100});if(x(o,e))return;if(0===e.length)return E("autofill entries",0);console.log(s.bold("\n📝 Autofill Entries:\n"));const r=R(["Field Name","Value","Count","Last Used"],[25,40,8,22]);for(const o of e)r.push([s.cyan(o.name),s.white(o.value),s.yellow(String(o.count)),s.dim(o.dateLastUsed)]);console.log(r.toString()),E("autofill entry(ies)",e.length)},F(e)));M(z.command("credit-cards").description("List saved credit cards")).action((o,e)=>W(o=>{const e=v({chromeDir:o.profilePath,profile:o.profile});if(x(o,e))return;if(0===e.length)return E("credit cards",0);console.log(s.bold("\n💳 Saved Credit Cards:\n"));const r=R(["Name on Card","Card Number","Expires","Nickname"],[25,25,14,20]);for(const o of e)r.push([s.white(o.nameOnCard||"—"),s.yellow(o.cardNumber||"—"),s.dim(`${o.expirationMonth}/${o.expirationYear}`),s.dim(o.nickname||"—")]);console.log(r.toString()),E("credit card(s)",e.length)},F(e)));M(z.command("addresses").description("List saved addresses")).action((o,e)=>W(o=>{const e=k({chromeDir:o.profilePath,profile:o.profile});if(x(o,e))return;if(0===e.length)return E("addresses",0);console.log(s.bold("\n🏠 Saved Addresses:\n"));const r=R(["Name","Email","Phone","Address","City/State/Zip"],[20,25,16,30,25]);for(const o of e)r.push([s.white(o.fullName||"—"),s.cyan(o.email||"—"),s.dim(o.phone||"—"),s.white(o.streetAddress||"—"),s.dim([o.city,o.state,o.zipcode].filter(Boolean).join(", ")||"—")]);console.log(r.toString()),E("address(es)",e.length)},F(e)));T(A(M(z.command("downloads").description("List download history")))).action((o,e)=>W(o=>{const e=L({chromeDir:o.profilePath,profile:o.profile,search:o.search,limit:parseInt(o.limit,10)||100});if(x(o,e))return;if(0===e.length)return E("downloads",0);console.log(s.bold("\n⬇️ Download History:\n"));const r=R(["File","URL","Size","State","Date"],[35,40,12,12,20]);for(const o of e){const e=o.targetPath?o.targetPath.split("/").pop():"—",t=o.totalBytes>0?Z(o.totalBytes):"—";r.push([s.white(e||""),s.cyan(o.url?o.url.substring(0,60):"—"),s.dim(t),"complete"===o.state?s.green(o.state):s.yellow(o.state),s.dim(o.startTime)])}console.log(r.toString()),E("download(s)",e.length)},F(e)));T(M(z.command("extensions").description("List installed extensions"))).action((o,e)=>W(o=>{const e=P({chromeDir:o.profilePath,profile:o.profile,search:o.search});if(x(o,e))return;if(0===e.length)return E("extensions",0);console.log(s.bold("\n🧩 Installed Extensions:\n"));const r=R(["Name","Version","ID","Enabled","Webstore"],[30,12,36,9,9]);for(const o of e)r.push([s.white(o.name),s.dim(o.version),s.dim(o.id),o.enabled?s.green("✓"):s.red("✗"),o.fromWebstore?s.green("✓"):s.dim("✗")]);console.log(r.toString()),E("extension(s)",e.length)},F(e)));M(z.command("search-engines").description("List configured search engines")).action((o,e)=>W(o=>{const e=D({chromeDir:o.profilePath,profile:o.profile});if(x(o,e))return;if(0===e.length)return E("search engines",0);console.log(s.bold("\n🔎 Search Engines:\n"));const r=R(["Name","Keyword","URL","Active"],[20,16,60,8]);for(const o of e)r.push([s.white(o.name),s.cyan(o.keyword),s.dim(o.url?o.url.substring(0,80):"—"),o.isActive?s.green("✓"):s.dim("✗")]);console.log(r.toString()),E("search engine(s)",e.length)},F(e)));function Z(o){if(0===o)return"0 B";const e=Math.floor(Math.log(o)/Math.log(1024));return`${(o/Math.pow(1024,e)).toFixed(1)} ${["B","KB","MB","GB","TB"][e]}`}M(z.command("top-sites").description("List top sites")).action((o,e)=>W(o=>{const e=j({chromeDir:o.profilePath,profile:o.profile});if(x(o,e))return;if(0===e.length)return E("top sites",0);console.log(s.bold("\n⭐ Top Sites:\n"));const r=R(["Rank","Title","URL"],[8,30,80]);for(const o of e)r.push([s.yellow(String(o.rank+1)),s.white(o.title||"—"),s.cyan(o.url)]);console.log(r.toString()),E("top site(s)",e.length)},F(e)));try{z.parse()}catch(o){(function(o){return!!f(o)&&(console.error(s.red.bold("\n🔑 No keychain access\n")),console.error(s.yellow("chrome-tools couldn't read the browser's encryption key from your macOS\nKeychain, so passwords and other encrypted data can't be decrypted.")),console.error(s.dim("\n"+o.message+"\n")),!0)})(o)||console.error(s.red(`\n✖ ${o?.message??String(o)}\n`)),process.env.DEBUG&&console.error(o),process.exit(1)}
2
+ /**
3
+ * chrome-tools CLI
4
+ *
5
+ * A comprehensive command-line interface for extracting Chrome browser data on
6
+ * macOS, Windows, and Linux.
7
+ * Supports subcommands and shortcut flags for quick access to all data types.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { Command, InvalidArgumentError } from "commander";
11
+ import chalk from "chalk";
12
+ import Table from "cli-table3";
13
+ import { discoverProfiles, getProfileMetadata, getProfileStates, getDirectorySize, detectInstalledBrowsers } from "../src/core/chrome.js";
14
+ import { setActiveBrowser, getActiveBrowser, isKeychainAccessDenied } from "../src/core/platform/index.js";
15
+ import { getPasswords } from "../src/modules/passwords.js";
16
+ import { getBookmarks } from "../src/modules/bookmarks.js";
17
+ import { getHistory, getSearchTerms } from "../src/modules/history.js";
18
+ import { getCookies } from "../src/modules/cookies.js";
19
+ import { getLocalStorage } from "../src/modules/local-storage.js";
20
+ import { getAutofillEntries, getCreditCards, getAddresses } from "../src/modules/autofill.js";
21
+ import { getDownloads } from "../src/modules/downloads.js";
22
+ import { getExtensions } from "../src/modules/extensions.js";
23
+ import { getSearchEngines } from "../src/modules/search-engines.js";
24
+ import { getTopSites } from "../src/modules/top-sites.js";
25
+ // Resolved lazily per-invocation (inside handlers, after the active browser is
26
+ // chosen) so `--browser` can drive the default profile path.
27
+ const defaultProfilePath = process.env.PROFILE_PATH || undefined;
28
+ const defaultProfileName = process.env.PROFILE_NAME || undefined;
29
+ const defaultBrowser = process.env.BROWSER || process.env.CHROME_BROWSER || undefined;
30
+ // ─── Output Helpers ──────────────────────────────────────────────────────────
31
+ function outputJSON(data) {
32
+ console.log(JSON.stringify(data, null, 2));
33
+ }
34
+ function makeTable(headers, colWidths) {
35
+ return new Table({
36
+ head: headers.map((h) => chalk.bold.white(h)),
37
+ colWidths,
38
+ wordWrap: true,
39
+ style: { border: ["grey"], head: [] },
40
+ });
41
+ }
42
+ /**
43
+ * Prints a prominent, friendly "No keychain access" message for a denied/locked
44
+ * Keychain authorization. macOS itself shows the access prompt when we read the
45
+ * key — this explains what happened when that prompt is denied or unavailable.
46
+ * Returns true if the error was a keychain-access denial (and was handled here).
47
+ */
48
+ function reportKeychainDenied(err) {
49
+ if (!isKeychainAccessDenied(err))
50
+ return false;
51
+ console.error(chalk.red.bold("\n🔑 No keychain access\n"));
52
+ console.error(chalk.yellow("chrome-tools couldn't read the browser's encryption key from your macOS\n" +
53
+ "Keychain, so passwords and other encrypted data can't be decrypted."));
54
+ // The typed error already carries actionable, cause-specific guidance.
55
+ console.error(chalk.dim("\n" + err.message + "\n"));
56
+ return true;
57
+ }
58
+ function printCount(label, count) {
59
+ if (count === 0) {
60
+ console.log(chalk.yellow(`\n⚠ No ${label} found.\n`));
61
+ }
62
+ else {
63
+ console.log(chalk.green(`\n✔ Found ${count} ${label}.\n`));
64
+ }
65
+ }
66
+ function renderResults(opts, results, title, label, headers, widths, toRow) {
67
+ if (emit(opts, results))
68
+ return;
69
+ if (results.length === 0)
70
+ return printCount(label, 0);
71
+ console.log(chalk.bold(`\n${title}:\n`));
72
+ const table = makeTable(headers, widths);
73
+ for (const result of results)
74
+ table.push(toRow(result));
75
+ console.log(table.toString());
76
+ printCount(label, results.length);
77
+ }
78
+ function errorMessage(err) {
79
+ return err instanceof Error ? err.message : String(err);
80
+ }
81
+ // When set (during `--all-browsers --json`), handlers route their JSON payload
82
+ // here instead of printing, so we can emit a single combined document.
83
+ let _jsonCollector = null;
84
+ /**
85
+ * Emits a handler's results. Returns true when output was handled here (JSON or
86
+ * collected), letting the caller skip its human-readable table rendering.
87
+ */
88
+ function emit(opts, results) {
89
+ if (_jsonCollector) {
90
+ _jsonCollector.push({ browser: getActiveBrowser().name, results });
91
+ return true;
92
+ }
93
+ if (opts.json) {
94
+ outputJSON(results);
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+ /**
100
+ * Runs a single data handler for every installed Chromium browser, reusing the
101
+ * handler's own rendering. Human output gets a per-browser section header; JSON
102
+ * output is collected into one combined array.
103
+ */
104
+ function runAll(handler, opts) {
105
+ const installed = detectInstalledBrowsers();
106
+ if (installed.length === 0) {
107
+ if (opts.json)
108
+ return outputJSON([]);
109
+ console.log(chalk.yellow("\n⚠ No supported Chromium browsers found on this machine.\n"));
110
+ return;
111
+ }
112
+ // --all-browsers spans browsers with different dirs, so a fixed --profile-path
113
+ // makes no sense; let each browser resolve its own default.
114
+ const perBrowserOpts = {
115
+ ...opts,
116
+ browser: undefined,
117
+ profilePath: undefined,
118
+ };
119
+ const previousBrowser = getActiveBrowser();
120
+ _jsonCollector = opts.json ? [] : null;
121
+ try {
122
+ for (const b of installed) {
123
+ setActiveBrowser(b.browser.id);
124
+ if (!opts.json) {
125
+ console.log(chalk.bold.magenta(`\n══════════ ${b.browser.name} ══════════`));
126
+ }
127
+ try {
128
+ handler(perBrowserOpts);
129
+ }
130
+ catch (err) {
131
+ if (_jsonCollector) {
132
+ _jsonCollector.push({ browser: b.browser.name, results: { error: errorMessage(err) } });
133
+ }
134
+ else if (isKeychainAccessDenied(err)) {
135
+ console.log(chalk.red(` 🔑 No keychain access — skipped (${err.service}).`));
136
+ console.log(chalk.dim(` ${err.message.split("\n").join("\n ")}`));
137
+ }
138
+ else {
139
+ console.log(chalk.red(` ⚠ Skipped: ${errorMessage(err)}`));
140
+ }
141
+ }
142
+ }
143
+ if (_jsonCollector)
144
+ outputJSON(_jsonCollector);
145
+ }
146
+ finally {
147
+ _jsonCollector = null;
148
+ setActiveBrowser(previousBrowser.id);
149
+ }
150
+ }
151
+ /**
152
+ * Resolves the effective scope (browser + path + profile) from a command's
153
+ * options, applying environment fallbacks with the right precedence:
154
+ *
155
+ * - A typed `--profile-path` / `--profile` always wins.
156
+ * - When a browser is explicitly scoped (`--browser`, `--all-browsers`, or a
157
+ * `BROWSER`/`CHROME_BROWSER` env var), the legacy `PROFILE_PATH` /
158
+ * `PROFILE_NAME` env defaults are ignored — they point at one specific
159
+ * Chrome dir and would otherwise mask the chosen browser's own location.
160
+ * - Otherwise (the classic single-browser setup), `PROFILE_PATH` /
161
+ * `PROFILE_NAME` apply as before.
162
+ */
163
+ function scopeFromCli(cmd) {
164
+ const opts = cmd.optsWithGlobals();
165
+ const browserScoped = Boolean(opts.browser) || Boolean(opts.allBrowsers) || Boolean(defaultBrowser);
166
+ opts.browser = opts.browser || defaultBrowser || undefined;
167
+ if (!opts.profilePath) {
168
+ opts.profilePath = browserScoped ? undefined : defaultProfilePath;
169
+ }
170
+ if (!opts.profile) {
171
+ opts.profile = browserScoped ? undefined : defaultProfileName;
172
+ }
173
+ return opts;
174
+ }
175
+ /**
176
+ * Central entry point for data handlers: applies `--browser` (single) or fans
177
+ * out across `--all-browsers`.
178
+ */
179
+ function dispatch(handler, opts) {
180
+ if (opts.allBrowsers) {
181
+ runAll(handler, opts);
182
+ return;
183
+ }
184
+ setActiveBrowser(opts.browser);
185
+ handler(opts);
186
+ }
187
+ // ─── Browsers Command ─────────────────────────────────────────────────────────
188
+ function handleBrowsers(opts) {
189
+ const detected = detectInstalledBrowsers({ includeMissing: true });
190
+ if (opts.json) {
191
+ return outputJSON(detected.map((d) => ({
192
+ id: d.browser.id,
193
+ name: d.browser.name,
194
+ installed: d.installed,
195
+ profileCount: d.profileCount,
196
+ path: d.chromeDir,
197
+ safeStorageLabel: d.browser.safeStorageLabel,
198
+ })));
199
+ }
200
+ const installed = detected.filter((d) => d.installed);
201
+ console.log(chalk.bold(`\n🌐 Supported browsers (${installed.length} installed):\n`));
202
+ const table = makeTable(["Browser", "Id", "Installed", "Profiles", "Path"], [20, 14, 11, 9, 50]);
203
+ for (const d of detected) {
204
+ table.push([
205
+ chalk.white(d.browser.name),
206
+ chalk.cyan(d.browser.id),
207
+ d.installed ? chalk.green("✓") : chalk.dim("✗"),
208
+ d.installed ? chalk.yellow(String(d.profileCount)) : chalk.dim("—"),
209
+ chalk.dim(d.chromeDir),
210
+ ]);
211
+ }
212
+ console.log(table.toString());
213
+ }
214
+ // ─── Shared Options ──────────────────────────────────────────────────────────
215
+ function addCommonOptions(cmd) {
216
+ return cmd
217
+ .option("--profile-path <path>", "Custom browser User Data Path (overrides --browser)")
218
+ .option("--profile <name>", "Target a specific profile")
219
+ .option("--browser <name>", "Browser to read (chrome, edge, brave, comet, vivaldi, opera, chromium…)")
220
+ .option("--all-browsers", "Read from every installed Chromium browser")
221
+ .option("--json", "Output as JSON");
222
+ }
223
+ function addSearchOption(cmd) {
224
+ return cmd.option("--search <term>", "Filter results (case-insensitive)");
225
+ }
226
+ function addLimitOption(cmd, defaultLimit = 100) {
227
+ return cmd.option("--limit <n>", "Limit number of results", parseLimit, defaultLimit);
228
+ }
229
+ function parseLimit(value) {
230
+ const limit = Number(value);
231
+ if (!Number.isSafeInteger(limit) || limit < 0) {
232
+ throw new InvalidArgumentError("Limit must be a non-negative integer.");
233
+ }
234
+ return limit;
235
+ }
236
+ // ─── Program Setup ───────────────────────────────────────────────────────────
237
+ const program = new Command();
238
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
239
+ program
240
+ .name("chrome-tools")
241
+ .description(chalk.bold.cyan("Chrome Data Toolkit") +
242
+ chalk.dim(" — Extract passwords, bookmarks, history, cookies, and more from Chrome on macOS, Windows, and Linux"))
243
+ .version(pkg.version)
244
+ .option("--passwords", "Shortcut: list saved passwords")
245
+ .option("--bookmarks", "Shortcut: list bookmarks")
246
+ .option("--local-storage", "Shortcut: list localStorage entries")
247
+ .option("--profiles", "Shortcut: list profiles")
248
+ .option("--browsers", "Shortcut: list supported / installed browsers")
249
+ .option("--size", "Show each profile's size on disk when listing profiles (slower)");
250
+ addCommonOptions(program)
251
+ .option("--search <term>", "Filter results (for shortcut flags)")
252
+ .option("--limit <n>", "Limit results (for shortcut flags)", parseLimit)
253
+ .action((opts, cmd) => {
254
+ // Default action when no subcommand is given.
255
+ const o = scopeFromCli(cmd);
256
+ if (o.browsers) {
257
+ handleBrowsers(o);
258
+ }
259
+ else if (o.passwords) {
260
+ dispatch(handlePasswords, o);
261
+ }
262
+ else if (o.bookmarks) {
263
+ dispatch(handleBookmarks, o);
264
+ }
265
+ else if (o.localStorage) {
266
+ dispatch(handleLocalStorage, o);
267
+ }
268
+ else {
269
+ // Default (including --profiles shortcut): list profiles.
270
+ dispatch(handleProfiles, o);
271
+ }
272
+ });
273
+ // ─── Profiles Command ───────────────────────────────────────────────────────
274
+ const profilesCmd = program
275
+ .command("profiles")
276
+ .description("List Chrome profiles");
277
+ addCommonOptions(profilesCmd)
278
+ .option("--size", "Compute and show each profile's size on disk (slower)")
279
+ .action((opts, cmd) => {
280
+ dispatch(handleProfiles, scopeFromCli(cmd));
281
+ });
282
+ const statesCmd = profilesCmd
283
+ .command("states")
284
+ .description("Show detailed profile states with available data files");
285
+ addCommonOptions(statesCmd).action((opts, cmd) => dispatch(handleProfileStates, scopeFromCli(cmd)));
286
+ // ─── Browsers Command ─────────────────────────────────────────────────────────
287
+ const browsersCmd = program
288
+ .command("browsers")
289
+ .description("List supported browsers and detect which are installed");
290
+ browsersCmd
291
+ .option("--json", "Output as JSON")
292
+ .action((opts, cmd) => handleBrowsers(scopeFromCli(cmd)));
293
+ function handleProfiles(opts) {
294
+ const profiles = discoverProfiles(opts.profilePath);
295
+ const metadata = getProfileMetadata(opts.profilePath);
296
+ const withSize = Boolean(opts.size);
297
+ if (opts.json) {
298
+ const metaMap = new Map(metadata.map((m) => [m.directory, m]));
299
+ const enriched = profiles.map((p) => ({
300
+ ...p,
301
+ ...metaMap.get(p.name),
302
+ ...(withSize ? { sizeBytes: getDirectorySize(p.path) } : {}),
303
+ }));
304
+ emit(opts, enriched);
305
+ return;
306
+ }
307
+ if (profiles.length === 0) {
308
+ console.log(chalk.yellow("\n⚠ No Chrome profiles found.\n"));
309
+ return;
310
+ }
311
+ const metaMap = new Map(metadata.map((m) => [m.directory, m]));
312
+ console.log(chalk.bold(`\n📁 Discovered ${profiles.length} profile(s):\n`));
313
+ const headers = withSize
314
+ ? ["Directory", "Display Name", "Email", "Size", "Path"]
315
+ : ["Directory", "Display Name", "Email", "Path"];
316
+ const widths = withSize ? [16, 22, 30, 10, 42] : [16, 22, 30, 50];
317
+ const table = makeTable(headers, widths);
318
+ for (const p of profiles) {
319
+ const meta = metaMap.get(p.name);
320
+ const row = [
321
+ chalk.cyan(p.name),
322
+ chalk.white(meta?.name || p.name),
323
+ chalk.dim(meta?.email || "—"),
324
+ ];
325
+ if (withSize) {
326
+ row.push(chalk.yellow(formatBytes(getDirectorySize(p.path))));
327
+ }
328
+ row.push(chalk.dim(p.path));
329
+ table.push(row);
330
+ }
331
+ console.log(table.toString());
332
+ printCount("profile(s)", profiles.length);
333
+ }
334
+ function handleProfileStates(opts) {
335
+ const states = getProfileStates(opts.profilePath);
336
+ if (opts.json) {
337
+ emit(opts, states);
338
+ return;
339
+ }
340
+ if (states.length === 0) {
341
+ console.log(chalk.yellow("\n⚠ No Chrome profiles found.\n"));
342
+ return;
343
+ }
344
+ console.log(chalk.bold(`\n📊 Profile States (${states.length} profile(s)):\n`));
345
+ for (const state of states) {
346
+ console.log(chalk.bold.cyan(` ${state.displayName}`) + chalk.dim(` (${state.directory})`));
347
+ if (state.email) {
348
+ console.log(chalk.dim(` Email: ${state.email}`));
349
+ }
350
+ console.log(chalk.dim(` Path: ${state.path}`));
351
+ const files = state.dataFiles;
352
+ const available = Object.entries(files)
353
+ .filter(([, exists]) => exists)
354
+ .map(([name]) => name);
355
+ const missing = Object.entries(files)
356
+ .filter(([, exists]) => !exists)
357
+ .map(([name]) => name);
358
+ console.log(` Available: ${available.map((f) => chalk.green(f)).join(", ") || chalk.dim("none")}`);
359
+ if (missing.length > 0) {
360
+ console.log(` Missing: ${missing.map((f) => chalk.dim(f)).join(", ")}`);
361
+ }
362
+ console.log();
363
+ }
364
+ }
365
+ // ─── Passwords Command ──────────────────────────────────────────────────────
366
+ const passwordsCmd = program.command("passwords").description("List saved passwords");
367
+ addSearchOption(addCommonOptions(passwordsCmd)).action((opts, cmd) => dispatch(handlePasswords, scopeFromCli(cmd)));
368
+ function handlePasswords(opts) {
369
+ const results = getPasswords(opts);
370
+ if (emit(opts, results))
371
+ return;
372
+ if (results.length === 0) {
373
+ return printCount("passwords", 0);
374
+ }
375
+ console.log(chalk.bold("\n🔑 Saved Passwords:\n"));
376
+ const showProfile = new Set(results.map((r) => r.profile)).size > 1;
377
+ // Always show the source (device vs account) — it explains where each entry
378
+ // lives and why a device-only listing can look empty when sync is on.
379
+ const head = ["Source", "URL", "Username", "Password", "Last Used"];
380
+ const widths = [9, 42, 24, 22, 20];
381
+ if (showProfile) {
382
+ head.unshift("Profile");
383
+ widths.unshift(12);
384
+ }
385
+ const table = makeTable(head, widths);
386
+ for (const r of results) {
387
+ const row = [
388
+ chalk.blue(r.source ?? "?"),
389
+ chalk.cyan(r.url),
390
+ chalk.green(r.username || "<empty>"),
391
+ chalk.yellow(r.password || "<empty>"),
392
+ chalk.dim(r.lastUsed),
393
+ ];
394
+ if (showProfile)
395
+ row.unshift(chalk.magenta(r.profile));
396
+ table.push(row);
397
+ }
398
+ console.log(table.toString());
399
+ printCount("password(s)", results.length);
400
+ }
401
+ // ─── Bookmarks Command ──────────────────────────────────────────────────────
402
+ const bookmarksCmd = program.command("bookmarks").description("List bookmarks");
403
+ addSearchOption(addCommonOptions(bookmarksCmd))
404
+ .option("--folder <name>", "Filter by folder name")
405
+ .action((opts, cmd) => dispatch(handleBookmarks, scopeFromCli(cmd)));
406
+ function handleBookmarks(opts) {
407
+ renderResults(opts, getBookmarks(opts), "📚 Bookmarks", "bookmark(s)", ["Title", "URL", "Folder", "Date Added"], [30, 50, 25, 20], (r) => [
408
+ chalk.white(r.title),
409
+ chalk.cyan(r.url),
410
+ chalk.dim(r.folder || "/"),
411
+ chalk.dim(r.dateAdded),
412
+ ]);
413
+ }
414
+ // ─── History Command ─────────────────────────────────────────────────────────
415
+ const historyCmd = program.command("history").description("List browsing history");
416
+ addSearchOption(addLimitOption(addCommonOptions(historyCmd))).action((opts, cmd) => dispatch((mergedOpts) => {
417
+ renderResults(mergedOpts, getHistory(mergedOpts), "🕐 Browsing History", "history entry(ies)", ["URL", "Title", "Visits", "Last Visit"], [50, 30, 8, 22], (r) => [
418
+ chalk.cyan(r.url),
419
+ chalk.white(r.title || "—"),
420
+ chalk.yellow(String(r.visitCount)),
421
+ chalk.dim(r.lastVisit),
422
+ ]);
423
+ }, scopeFromCli(cmd)));
424
+ // ─── Search Terms Command ────────────────────────────────────────────────────
425
+ const searchTermsCmd = program.command("search-terms").description("List search terms from history");
426
+ addLimitOption(addCommonOptions(searchTermsCmd)).action((opts, cmd) => dispatch((mergedOpts) => {
427
+ renderResults(mergedOpts, getSearchTerms(mergedOpts), "🔍 Search Terms", "search term(s)", ["Term", "URL"], [40, 80], (r) => [chalk.white(r.term), chalk.dim(r.url)]);
428
+ }, scopeFromCli(cmd)));
429
+ // ─── Cookies Command ─────────────────────────────────────────────────────────
430
+ const cookiesCmd = program.command("cookies").description("List cookies (encrypted values decrypted)");
431
+ addLimitOption(addCommonOptions(cookiesCmd))
432
+ .option("--domain <domain>", "Filter by domain")
433
+ .option("--url <url>", "Filter by URL with path")
434
+ .option("--no-decrypt", "Skip decryption of encrypted values")
435
+ .action((opts, cmd) => dispatch((mergedOpts) => {
436
+ renderResults(mergedOpts, getCookies(mergedOpts), "🍪 Cookies", "cookie(s)", ["Domain", "Name", "Value", "Secure", "Expires"], [30, 25, 30, 8, 22], (r) => [
437
+ chalk.cyan(r.domain),
438
+ chalk.white(r.name),
439
+ chalk.dim(truncate(r.value, 50)),
440
+ r.secure ? chalk.green("✓") : chalk.dim("✗"),
441
+ chalk.dim(r.expires),
442
+ ]);
443
+ }, scopeFromCli(cmd)));
444
+ // ─── Local Storage Command ──────────────────────────────────────────────────
445
+ const localStorageCmd = program.command("local-storage").description("List localStorage entries");
446
+ addSearchOption(addLimitOption(addCommonOptions(localStorageCmd)))
447
+ .option("--origin <origin>", "Filter by origin")
448
+ .option("--url <url>", "Filter by URL origin")
449
+ .option("--key <key>", "Filter by localStorage key")
450
+ .action((opts, cmd) => dispatch(handleLocalStorage, scopeFromCli(cmd)));
451
+ function handleLocalStorage(opts) {
452
+ renderResults(opts, getLocalStorage(opts), "💾 localStorage", "localStorage entries", ["Origin", "Key", "Value"], [36, 28, 46], (r) => [
453
+ chalk.cyan(r.origin),
454
+ chalk.white(r.key),
455
+ chalk.dim(truncate(r.value, 80)),
456
+ ]);
457
+ }
458
+ // ─── Autofill Command ────────────────────────────────────────────────────────
459
+ const autofillCmd = program.command("autofill").description("List autofill entries");
460
+ addSearchOption(addLimitOption(addCommonOptions(autofillCmd))).action((opts, cmd) => dispatch((mergedOpts) => {
461
+ renderResults(mergedOpts, getAutofillEntries(mergedOpts), "📝 Autofill Entries", "autofill entry(ies)", ["Field Name", "Value", "Count", "Last Used"], [25, 40, 8, 22], (r) => [
462
+ chalk.cyan(r.name),
463
+ chalk.white(r.value),
464
+ chalk.yellow(String(r.count)),
465
+ chalk.dim(r.dateLastUsed),
466
+ ]);
467
+ }, scopeFromCli(cmd)));
468
+ // ─── Credit Cards Command ────────────────────────────────────────────────────
469
+ const creditCardsCmd = program.command("credit-cards").description("List saved credit cards");
470
+ addCommonOptions(creditCardsCmd).action((opts, cmd) => dispatch((mergedOpts) => {
471
+ renderResults(mergedOpts, getCreditCards(mergedOpts), "💳 Saved Credit Cards", "credit card(s)", ["Name on Card", "Card Number", "Expires", "Nickname"], [25, 25, 14, 20], (r) => [
472
+ chalk.white(r.nameOnCard || "—"),
473
+ chalk.yellow(r.cardNumber || "—"),
474
+ chalk.dim(`${r.expirationMonth}/${r.expirationYear}`),
475
+ chalk.dim(r.nickname || "—"),
476
+ ]);
477
+ }, scopeFromCli(cmd)));
478
+ // ─── Addresses Command ───────────────────────────────────────────────────────
479
+ const addressesCmd = program.command("addresses").description("List saved addresses");
480
+ addCommonOptions(addressesCmd).action((opts, cmd) => dispatch((mergedOpts) => {
481
+ renderResults(mergedOpts, getAddresses(mergedOpts), "🏠 Saved Addresses", "address(es)", ["Name", "Email", "Phone", "Address", "City/State/Zip"], [20, 25, 16, 30, 25], (r) => [
482
+ chalk.white(r.fullName || "—"),
483
+ chalk.cyan(r.email || "—"),
484
+ chalk.dim(r.phone || "—"),
485
+ chalk.white(r.streetAddress || "—"),
486
+ chalk.dim([r.city, r.state, r.zipcode].filter(Boolean).join(", ") || "—"),
487
+ ]);
488
+ }, scopeFromCli(cmd)));
489
+ // ─── Downloads Command ───────────────────────────────────────────────────────
490
+ const downloadsCmd = program.command("downloads").description("List download history");
491
+ addSearchOption(addLimitOption(addCommonOptions(downloadsCmd))).action((opts, cmd) => dispatch((mergedOpts) => {
492
+ renderResults(mergedOpts, getDownloads(mergedOpts), "⬇️ Download History", "download(s)", ["File", "URL", "Size", "State", "Date"], [35, 40, 12, 12, 20], (r) => [
493
+ chalk.white(r.targetPath ? r.targetPath.split(/[\\/]/).pop() || "" : "—"),
494
+ chalk.cyan(truncate(r.url, 60)),
495
+ chalk.dim(r.totalBytes > 0 ? formatBytes(r.totalBytes) : "—"),
496
+ r.state === "complete" ? chalk.green(r.state) : chalk.yellow(r.state),
497
+ chalk.dim(r.startTime),
498
+ ]);
499
+ }, scopeFromCli(cmd)));
500
+ // ─── Extensions Command ─────────────────────────────────────────────────────
501
+ const extensionsCmd = program.command("extensions").description("List installed extensions");
502
+ addSearchOption(addCommonOptions(extensionsCmd)).action((opts, cmd) => dispatch((mergedOpts) => {
503
+ renderResults(mergedOpts, getExtensions(mergedOpts), "🧩 Installed Extensions", "extension(s)", ["Name", "Version", "ID", "Enabled", "Webstore"], [30, 12, 36, 9, 9], (r) => [
504
+ chalk.white(r.name),
505
+ chalk.dim(r.version),
506
+ chalk.dim(r.id),
507
+ r.enabled ? chalk.green("✓") : chalk.red("✗"),
508
+ r.fromWebstore ? chalk.green("✓") : chalk.dim("✗"),
509
+ ]);
510
+ }, scopeFromCli(cmd)));
511
+ // ─── Search Engines Command ─────────────────────────────────────────────────
512
+ const searchEnginesCmd = program.command("search-engines").description("List configured search engines");
513
+ addCommonOptions(searchEnginesCmd).action((opts, cmd) => dispatch((mergedOpts) => {
514
+ renderResults(mergedOpts, getSearchEngines(mergedOpts), "🔎 Search Engines", "search engine(s)", ["Name", "Keyword", "URL", "Active"], [20, 16, 60, 8], (r) => [
515
+ chalk.white(r.name),
516
+ chalk.cyan(r.keyword),
517
+ chalk.dim(truncate(r.url, 80)),
518
+ r.isActive ? chalk.green("✓") : chalk.dim("✗"),
519
+ ]);
520
+ }, scopeFromCli(cmd)));
521
+ // ─── Top Sites Command ──────────────────────────────────────────────────────
522
+ const topSitesCmd = program.command("top-sites").description("List top sites");
523
+ addCommonOptions(topSitesCmd).action((opts, cmd) => dispatch((mergedOpts) => {
524
+ renderResults(mergedOpts, getTopSites(mergedOpts), "⭐ Top Sites", "top site(s)", ["Rank", "Title", "URL"], [8, 30, 80], (r) => [
525
+ chalk.yellow(String(r.rank + 1)),
526
+ chalk.white(r.title || "—"),
527
+ chalk.cyan(r.url),
528
+ ]);
529
+ }, scopeFromCli(cmd)));
530
+ // ─── Utilities ───────────────────────────────────────────────────────────────
531
+ function formatBytes(bytes) {
532
+ if (bytes === 0)
533
+ return "0 B";
534
+ const units = ["B", "KB", "MB", "GB", "TB"];
535
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
536
+ return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
537
+ }
538
+ function truncate(value, maxLength) {
539
+ if (!value)
540
+ return "—";
541
+ return value.length > maxLength ? value.slice(0, maxLength) + "…" : value;
542
+ }
543
+ // ─── Run ─────────────────────────────────────────────────────────────────────
544
+ try {
545
+ program.parse();
546
+ }
547
+ catch (err) {
548
+ // Print a clean, actionable message instead of dumping the minified source and
549
+ // a Node stack trace for expected failures (bad --browser, keychain denial, …).
550
+ // Set DEBUG=1 to see the full stack.
551
+ if (!reportKeychainDenied(err)) {
552
+ console.error(chalk.red(`\n✖ ${errorMessage(err)}\n`));
553
+ }
554
+ if (process.env.DEBUG)
555
+ console.error(err);
556
+ process.exit(1);
557
+ }
@@ -10,16 +10,18 @@
10
10
  * Linux Secret Service application).
11
11
  *
12
12
  * "Browser" is therefore a small dimension orthogonal to "platform": the
13
- * platform backends (darwin/win32/linux) read the active browser's dir + label
14
- * from here and stay otherwise unchanged.
13
+ * platform backends (darwin/win32/linux) receive a browser descriptor and stay
14
+ * otherwise unchanged.
15
15
  */
16
16
  export type BrowserId = "chrome" | "chrome-beta" | "chrome-canary" | "chromium" | "edge" | "brave" | "comet" | "vivaldi" | "opera";
17
17
  /** Subpaths to a browser's User Data dir, relative to each OS's app-data base. */
18
18
  export interface BrowserDirs {
19
19
  /** Under `~/Library/Application Support`. */
20
20
  darwin: string;
21
- /** Under `%LOCALAPPDATA%`. Includes the trailing `User Data` segment. */
21
+ /** Under the selected Windows app-data root. Includes any `User Data` segment. */
22
22
  win32: string;
23
+ /** Windows application-data root. Chromium browsers normally use Local. */
24
+ win32Base?: "local" | "roaming";
23
25
  /** Under `$XDG_CONFIG_HOME` (or `~/.config`). */
24
26
  linux: string;
25
27
  }
@@ -53,6 +55,8 @@ export declare function listBrowsers(): BrowserDescriptor[];
53
55
  * @returns The descriptor, or `null` if unrecognized.
54
56
  */
55
57
  export declare function resolveBrowser(idOrName: string): BrowserDescriptor | null;
58
+ /** Resolves a browser or throws an actionable error without changing global state. */
59
+ export declare function requireBrowser(idOrName: string): BrowserDescriptor;
56
60
  /** The browser currently targeted by dir/key lookups. Defaults to Chrome. */
57
61
  export declare function getActiveBrowser(): BrowserDescriptor;
58
62
  /**