open-nami 1.3.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1435 -275
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ var require_package = __commonJS({
9
9
  "package.json"(exports, module) {
10
10
  module.exports = {
11
11
  name: "open-nami",
12
- version: "1.3.1",
12
+ version: "1.6.0",
13
13
  description: "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
14
14
  type: "module",
15
15
  bin: {
@@ -73,11 +73,11 @@ var require_package = __commonJS({
73
73
  });
74
74
 
75
75
  // src/cli.tsx
76
- import { createRequire } from "module";
76
+ import { createRequire as createRequire2 } from "module";
77
77
  import { render } from "ink";
78
78
 
79
79
  // src/app.tsx
80
- import { useState as useState9, useCallback as useCallback4, useRef as useRef5, useEffect as useEffect8, useMemo as useMemo3 } from "react";
80
+ import { useState as useState10, useCallback as useCallback4, useRef as useRef5, useEffect as useEffect10, useMemo as useMemo3 } from "react";
81
81
  import { Box as Box15, Text as Text17, useApp, useInput as useInput5, useStdout as useStdout3 } from "ink";
82
82
  import Spinner4 from "ink-spinner";
83
83
 
@@ -730,16 +730,8 @@ async function clearCheckpoint(destDir) {
730
730
  }
731
731
 
732
732
  // src/lib/engines/dispatcher.ts
733
- import path10 from "path";
734
- import os5 from "os";
735
- import fs10 from "fs/promises";
736
- import fsSync from "fs";
737
-
738
- // src/lib/cookies.ts
739
- import fs2 from "fs";
740
- import fsPromises2 from "fs/promises";
741
- import path3 from "path";
742
- import os2 from "os";
733
+ import path13 from "path";
734
+ import fs13 from "fs/promises";
743
735
 
744
736
  // src/lib/config.ts
745
737
  import fs from "fs";
@@ -757,9 +749,25 @@ function loadConfig(configPath = getDefaultConfigPath()) {
757
749
  const raw = fs.readFileSync(configPath, "utf8");
758
750
  const parsed = JSON.parse(raw);
759
751
  if (parsed && typeof parsed === "object") {
752
+ let cookiesFromBrowser = "auto";
753
+ if (typeof parsed.cookiesFromBrowser === "string") {
754
+ const trimmed = parsed.cookiesFromBrowser.trim();
755
+ const lower = trimmed.toLowerCase();
756
+ if (lower === "none") {
757
+ cookiesFromBrowser = "none";
758
+ } else if (lower === "auto" || trimmed === "") {
759
+ cookiesFromBrowser = "auto";
760
+ } else {
761
+ const knownBrowsers = ["chrome", "edge", "brave", "firefox", "helium", "opera", "vivaldi"];
762
+ cookiesFromBrowser = knownBrowsers.includes(lower) ? lower : trimmed;
763
+ }
764
+ } else {
765
+ cookiesFromBrowser = "auto";
766
+ }
760
767
  return {
761
768
  theme: typeof parsed.theme === "string" ? parsed.theme : void 0,
762
769
  cookiesDir: typeof parsed.cookiesDir === "string" ? parsed.cookiesDir.trim() : void 0,
770
+ cookiesFromBrowser,
763
771
  downloadDir: typeof parsed.downloadDir === "string" ? parsed.downloadDir.trim() : void 0,
764
772
  writeMetadata: typeof parsed.writeMetadata === "boolean" ? parsed.writeMetadata : void 0,
765
773
  defaultLimit: typeof parsed.defaultLimit === "number" ? parsed.defaultLimit : void 0
@@ -781,22 +789,442 @@ function saveConfig(config, configPath = getDefaultConfigPath()) {
781
789
  }
782
790
  }
783
791
 
792
+ // src/lib/cookies.ts
793
+ import fs3 from "fs";
794
+ import fsPromises2 from "fs/promises";
795
+ import path4 from "path";
796
+ import os3 from "os";
797
+
798
+ // src/lib/cookies/browser-bridge.ts
799
+ import { execSync } from "child_process";
800
+ import fs2 from "fs";
801
+ import os2 from "os";
802
+ import path3 from "path";
803
+ function listDetectedBrowsers() {
804
+ const isWin = process.platform === "win32";
805
+ const isMac = process.platform === "darwin";
806
+ const home = os2.homedir();
807
+ const candidates = [
808
+ {
809
+ id: "chrome",
810
+ name: "Google Chrome",
811
+ winDir: path3.join(
812
+ process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"),
813
+ "Google",
814
+ "Chrome",
815
+ "User Data"
816
+ ),
817
+ macDir: path3.join(home, "Library", "Application Support", "Google", "Chrome"),
818
+ linuxDir: path3.join(home, ".config", "google-chrome"),
819
+ engine: "chrome"
820
+ },
821
+ {
822
+ id: "edge",
823
+ name: "Microsoft Edge",
824
+ winDir: path3.join(
825
+ process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"),
826
+ "Microsoft",
827
+ "Edge",
828
+ "User Data"
829
+ ),
830
+ macDir: path3.join(home, "Library", "Application Support", "Microsoft Edge"),
831
+ linuxDir: path3.join(home, ".config", "microsoft-edge"),
832
+ engine: "edge"
833
+ },
834
+ {
835
+ id: "brave",
836
+ name: "Brave Browser",
837
+ winDir: path3.join(
838
+ process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"),
839
+ "BraveSoftware",
840
+ "Brave-Browser",
841
+ "User Data"
842
+ ),
843
+ macDir: path3.join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
844
+ linuxDir: path3.join(home, ".config", "BraveSoftware", "Brave-Browser"),
845
+ engine: "brave"
846
+ },
847
+ {
848
+ id: "firefox",
849
+ name: "Mozilla Firefox",
850
+ winDir: path3.join(
851
+ process.env.APPDATA || path3.join(home, "AppData", "Roaming"),
852
+ "Mozilla",
853
+ "Firefox",
854
+ "Profiles"
855
+ ),
856
+ macDir: path3.join(home, "Library", "Application Support", "Firefox", "Profiles"),
857
+ linuxDir: path3.join(home, ".mozilla", "firefox"),
858
+ engine: "firefox"
859
+ },
860
+ {
861
+ id: "helium",
862
+ name: "Helium",
863
+ winDir: path3.join(
864
+ process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"),
865
+ "imput",
866
+ "Helium",
867
+ "User Data"
868
+ ),
869
+ macDir: path3.join(home, "Library", "Application Support", "net.imput.Helium"),
870
+ linuxDir: path3.join(home, ".config", "helium"),
871
+ engine: "chrome"
872
+ },
873
+ {
874
+ id: "opera",
875
+ name: "Opera",
876
+ winDir: path3.join(
877
+ process.env.APPDATA || path3.join(home, "AppData", "Roaming"),
878
+ "Opera Software",
879
+ "Opera Stable"
880
+ ),
881
+ macDir: path3.join(home, "Library", "Application Support", "com.operasoftware.Opera"),
882
+ linuxDir: path3.join(home, ".config", "opera"),
883
+ engine: "opera"
884
+ },
885
+ {
886
+ id: "vivaldi",
887
+ name: "Vivaldi",
888
+ winDir: path3.join(
889
+ process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"),
890
+ "Vivaldi",
891
+ "User Data"
892
+ ),
893
+ macDir: path3.join(home, "Library", "Application Support", "Vivaldi"),
894
+ linuxDir: path3.join(home, ".config", "vivaldi"),
895
+ engine: "vivaldi"
896
+ }
897
+ ];
898
+ return candidates.map((c) => {
899
+ const profileDir = isWin ? c.winDir : isMac ? c.macDir : c.linuxDir;
900
+ return {
901
+ id: c.id,
902
+ name: c.name,
903
+ profileDir,
904
+ available: fs2.existsSync(profileDir),
905
+ engine: c.engine
906
+ };
907
+ });
908
+ }
909
+ function parseWindowsDefaultBrowserProgId(rawOutput) {
910
+ if (!rawOutput) return null;
911
+ const lines = rawOutput.split(/\r?\n/);
912
+ const pattern = /^\s*ProgId\s+REG_\w+\s+(\S.*)$/i;
913
+ for (const line of lines) {
914
+ const match = line.match(pattern);
915
+ if (match && match[1]) {
916
+ const progId = match[1].trim();
917
+ if (progId) return progId;
918
+ }
919
+ }
920
+ return null;
921
+ }
922
+ function mapProgIdToBrowserId(progId, commandOutput, platformOverride = "win32") {
923
+ const p = (progId || "").trim();
924
+ const pLower = p.toLowerCase();
925
+ const cmd = (commandOutput || "").toLowerCase();
926
+ const isWin = platformOverride === "win32";
927
+ const isMac = platformOverride === "darwin";
928
+ const home = os2.homedir();
929
+ const heliumDir = isWin ? path3.join(
930
+ process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"),
931
+ "imput",
932
+ "Helium",
933
+ "User Data"
934
+ ) : isMac ? path3.join(home, "Library", "Application Support", "net.imput.Helium") : path3.join(home, ".config", "helium");
935
+ if (pLower.includes("helium") || cmd.includes("helium")) {
936
+ return {
937
+ id: "helium",
938
+ name: "Helium",
939
+ engine: "chrome",
940
+ profileDir: heliumDir
941
+ };
942
+ }
943
+ if (pLower.startsWith("chromehtml") || pLower.includes("chrome") || cmd.includes("chrome.exe")) {
944
+ return {
945
+ id: "chrome",
946
+ name: "Google Chrome",
947
+ engine: "chrome"
948
+ };
949
+ }
950
+ if (pLower.startsWith("msedge") || pLower.includes("edge") || pLower.startsWith("appxq0fevzme2pys62n3e0fbqsw5cj08ege9") || cmd.includes("msedge.exe")) {
951
+ return {
952
+ id: "edge",
953
+ name: "Microsoft Edge",
954
+ engine: "edge"
955
+ };
956
+ }
957
+ if (pLower.startsWith("brave") || pLower.includes("brave") || cmd.includes("brave.exe")) {
958
+ return {
959
+ id: "brave",
960
+ name: "Brave Browser",
961
+ engine: "brave"
962
+ };
963
+ }
964
+ if (pLower.startsWith("firefox") || pLower.includes("firefox") || cmd.includes("firefox.exe")) {
965
+ return {
966
+ id: "firefox",
967
+ name: "Mozilla Firefox",
968
+ engine: "firefox"
969
+ };
970
+ }
971
+ if (pLower.startsWith("opera") || pLower.includes("opera") || cmd.includes("opera.exe")) {
972
+ return {
973
+ id: "opera",
974
+ name: "Opera",
975
+ engine: "opera"
976
+ };
977
+ }
978
+ if (pLower.startsWith("vivaldi") || pLower.includes("vivaldi") || cmd.includes("vivaldi.exe")) {
979
+ return {
980
+ id: "vivaldi",
981
+ name: "Vivaldi",
982
+ engine: "vivaldi"
983
+ };
984
+ }
985
+ return {
986
+ id: pLower || "chromium",
987
+ name: p || "Chromium",
988
+ engine: "chromium"
989
+ };
990
+ }
991
+ function parseMacDefaultBrowserBundleId(rawOutput) {
992
+ if (!rawOutput) return null;
993
+ const blocks = rawOutput.split(/[{}]/);
994
+ let httpBundle = null;
995
+ for (const block of blocks) {
996
+ const isHttps = /LSHandlerURLScheme\s*=\s*https\b/i.test(block);
997
+ const isHttp = /LSHandlerURLScheme\s*=\s*http\b/i.test(block);
998
+ if (!isHttps && !isHttp) continue;
999
+ const roleMatch = block.match(/LSHandlerRole(?:All|Viewer)\s*=\s*["']?([^"';\s]+)["']?/i);
1000
+ if (roleMatch && roleMatch[1]) {
1001
+ const bundleId = roleMatch[1].trim();
1002
+ if (isHttps) {
1003
+ return bundleId;
1004
+ }
1005
+ if (!httpBundle) {
1006
+ httpBundle = bundleId;
1007
+ }
1008
+ }
1009
+ }
1010
+ return httpBundle;
1011
+ }
1012
+ function mapBundleIdToBrowserId(bundleId) {
1013
+ const lower = bundleId.toLowerCase();
1014
+ if (lower.includes("helium")) {
1015
+ const home = os2.homedir();
1016
+ return {
1017
+ id: "helium",
1018
+ name: "Helium",
1019
+ engine: "chrome",
1020
+ profileDir: path3.join(home, "Library", "Application Support", "net.imput.Helium")
1021
+ };
1022
+ }
1023
+ if (lower.includes("chrome")) {
1024
+ return { id: "chrome", name: "Google Chrome", engine: "chrome" };
1025
+ }
1026
+ if (lower.includes("edge")) {
1027
+ return { id: "edge", name: "Microsoft Edge", engine: "edge" };
1028
+ }
1029
+ if (lower.includes("brave")) {
1030
+ return { id: "brave", name: "Brave Browser", engine: "brave" };
1031
+ }
1032
+ if (lower.includes("firefox")) {
1033
+ return { id: "firefox", name: "Mozilla Firefox", engine: "firefox" };
1034
+ }
1035
+ if (lower.includes("opera")) {
1036
+ return { id: "opera", name: "Opera", engine: "opera" };
1037
+ }
1038
+ if (lower.includes("vivaldi")) {
1039
+ return { id: "vivaldi", name: "Vivaldi", engine: "vivaldi" };
1040
+ }
1041
+ if (lower.includes("safari")) {
1042
+ return { id: "safari", name: "Safari", engine: "safari" };
1043
+ }
1044
+ return { id: lower, name: bundleId, engine: "chromium" };
1045
+ }
1046
+ function parseLinuxDefaultBrowserDesktop(rawOutput) {
1047
+ if (!rawOutput) return null;
1048
+ const lines = rawOutput.split(/\r?\n/);
1049
+ for (const line of lines) {
1050
+ const trimmed = line.trim();
1051
+ if (trimmed && !trimmed.startsWith("ERROR:") && !trimmed.includes("not found")) {
1052
+ return trimmed;
1053
+ }
1054
+ }
1055
+ return null;
1056
+ }
1057
+ function mapLinuxDesktopToBrowserId(desktopFile) {
1058
+ const lower = desktopFile.toLowerCase();
1059
+ if (lower.includes("helium")) {
1060
+ const home = os2.homedir();
1061
+ return {
1062
+ id: "helium",
1063
+ name: "Helium",
1064
+ engine: "chrome",
1065
+ profileDir: path3.join(home, ".config", "helium")
1066
+ };
1067
+ }
1068
+ if (lower.includes("chrome")) {
1069
+ return { id: "chrome", name: "Google Chrome", engine: "chrome" };
1070
+ }
1071
+ if (lower.includes("edge")) {
1072
+ return { id: "edge", name: "Microsoft Edge", engine: "edge" };
1073
+ }
1074
+ if (lower.includes("brave")) {
1075
+ return { id: "brave", name: "Brave Browser", engine: "brave" };
1076
+ }
1077
+ if (lower.includes("firefox")) {
1078
+ return { id: "firefox", name: "Mozilla Firefox", engine: "firefox" };
1079
+ }
1080
+ if (lower.includes("opera")) {
1081
+ return { id: "opera", name: "Opera", engine: "opera" };
1082
+ }
1083
+ if (lower.includes("vivaldi")) {
1084
+ return { id: "vivaldi", name: "Vivaldi", engine: "vivaldi" };
1085
+ }
1086
+ return { id: lower.replace(/\.desktop$/, ""), name: desktopFile, engine: "chromium" };
1087
+ }
1088
+ function queryWindows(execute, platformOverride) {
1089
+ let out = execute(
1090
+ 'reg query "HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice" /v ProgId'
1091
+ );
1092
+ let progId = parseWindowsDefaultBrowserProgId(out);
1093
+ if (!progId) {
1094
+ out = execute(
1095
+ 'reg query "HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice" /v ProgId'
1096
+ );
1097
+ progId = parseWindowsDefaultBrowserProgId(out);
1098
+ }
1099
+ if (progId) {
1100
+ const cmdOut = execute(`reg query "HKCR\\${progId}\\shell\\open\\command"`);
1101
+ return mapProgIdToBrowserId(progId, cmdOut, platformOverride ?? "win32");
1102
+ }
1103
+ return null;
1104
+ }
1105
+ function queryMac(execute) {
1106
+ const out = execute(
1107
+ "defaults read com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers"
1108
+ );
1109
+ const bundleId = parseMacDefaultBrowserBundleId(out);
1110
+ if (bundleId) {
1111
+ return mapBundleIdToBrowserId(bundleId);
1112
+ }
1113
+ return null;
1114
+ }
1115
+ function queryLinux(execute) {
1116
+ let out = execute("xdg-settings get default-web-browser");
1117
+ let desktop = parseLinuxDefaultBrowserDesktop(out);
1118
+ if (!desktop) {
1119
+ out = execute("xdg-mime query default x-scheme-handler/https");
1120
+ desktop = parseLinuxDefaultBrowserDesktop(out);
1121
+ }
1122
+ if (!desktop) {
1123
+ out = execute("xdg-mime query default x-scheme-handler/http");
1124
+ desktop = parseLinuxDefaultBrowserDesktop(out);
1125
+ }
1126
+ if (desktop) {
1127
+ return mapLinuxDesktopToBrowserId(desktop);
1128
+ }
1129
+ return null;
1130
+ }
1131
+ function detectSystemDefaultBrowser(execFn, platformOverride) {
1132
+ const platform = platformOverride ?? process.platform;
1133
+ const execute = execFn ?? ((cmd) => {
1134
+ try {
1135
+ return execSync(cmd, {
1136
+ encoding: "utf8",
1137
+ stdio: ["pipe", "pipe", "ignore"],
1138
+ timeout: 2500
1139
+ });
1140
+ } catch {
1141
+ return "";
1142
+ }
1143
+ });
1144
+ let detectedInfo = null;
1145
+ if (platform === "win32") {
1146
+ detectedInfo = queryWindows(execute, platform);
1147
+ } else if (platform === "darwin") {
1148
+ detectedInfo = queryMac(execute);
1149
+ } else if (platform === "linux") {
1150
+ detectedInfo = queryLinux(execute);
1151
+ }
1152
+ if (!detectedInfo && execFn && !platformOverride) {
1153
+ if (platform !== "win32") detectedInfo = queryWindows(execute, "win32");
1154
+ if (!detectedInfo && platform !== "darwin") detectedInfo = queryMac(execute);
1155
+ if (!detectedInfo && platform !== "linux") detectedInfo = queryLinux(execute);
1156
+ }
1157
+ const allBrowsers = listDetectedBrowsers();
1158
+ if (detectedInfo) {
1159
+ const matched = allBrowsers.find((b) => b.id === detectedInfo.id);
1160
+ if (matched) {
1161
+ if (detectedInfo.profileDir) {
1162
+ return {
1163
+ ...matched,
1164
+ profileDir: detectedInfo.profileDir,
1165
+ available: fs2.existsSync(detectedInfo.profileDir)
1166
+ };
1167
+ }
1168
+ return matched;
1169
+ }
1170
+ const profDir = detectedInfo.profileDir || "";
1171
+ return {
1172
+ id: detectedInfo.id,
1173
+ name: detectedInfo.name || detectedInfo.id,
1174
+ profileDir: profDir,
1175
+ available: profDir ? fs2.existsSync(profDir) : false,
1176
+ engine: detectedInfo.engine
1177
+ };
1178
+ }
1179
+ const availableFallback = allBrowsers.find((b) => b.available);
1180
+ return availableFallback || null;
1181
+ }
1182
+ function resolveBrowserCookieArg(browser) {
1183
+ if (!browser) {
1184
+ return null;
1185
+ }
1186
+ if (typeof browser === "string") {
1187
+ const trimmed = browser.trim();
1188
+ if (!trimmed || trimmed.toLowerCase() === "none") {
1189
+ return null;
1190
+ }
1191
+ if (trimmed.toLowerCase() === "helium") {
1192
+ const detectedHelium = listDetectedBrowsers().find((b) => b.id === "helium");
1193
+ const profileDir = detectedHelium?.profileDir;
1194
+ return profileDir ? `chrome:${profileDir}` : "chrome";
1195
+ }
1196
+ if (trimmed.toLowerCase() === "auto") {
1197
+ const defaultBrowser = detectSystemDefaultBrowser();
1198
+ return resolveBrowserCookieArg(defaultBrowser);
1199
+ }
1200
+ return trimmed;
1201
+ }
1202
+ if (!browser.id || browser.id.toLowerCase() === "none") {
1203
+ return null;
1204
+ }
1205
+ if (browser.id.toLowerCase() === "helium") {
1206
+ const profileDir = browser.profileDir || listDetectedBrowsers().find((b) => b.id === "helium")?.profileDir;
1207
+ return profileDir ? `chrome:${profileDir}` : "chrome";
1208
+ }
1209
+ return browser.id;
1210
+ }
1211
+
784
1212
  // src/lib/cookies.ts
785
1213
  var CANDIDATE_DIRS = [
786
- path3.join(os2.homedir(), ".open-nami", "cookies"),
787
- path3.join(os2.homedir(), ".nami", "cookies"),
788
- path3.join(process.cwd(), "cookies")
1214
+ path4.join(os3.homedir(), ".open-nami", "cookies"),
1215
+ path4.join(os3.homedir(), ".nami", "cookies"),
1216
+ path4.join(process.cwd(), "cookies")
789
1217
  ];
790
1218
  function getCandidateDirs(customDir) {
791
1219
  const dirs = [];
792
1220
  const configured = loadConfig().cookiesDir;
793
1221
  const custom = customDir || process.env.OPEN_NAMI_COOKIES_DIR || process.env.NAMI_COOKIES_DIR || configured;
794
1222
  if (custom && custom.trim()) {
795
- dirs.push(path3.resolve(custom.trim()));
1223
+ dirs.push(path4.resolve(custom.trim()));
796
1224
  }
797
- const seen = new Set(dirs.map((d) => path3.normalize(d).toLowerCase()));
1225
+ const seen = new Set(dirs.map((d) => path4.normalize(d).toLowerCase()));
798
1226
  for (const dir of CANDIDATE_DIRS) {
799
- const norm = path3.normalize(dir).toLowerCase();
1227
+ const norm = path4.normalize(dir).toLowerCase();
800
1228
  if (!seen.has(norm)) {
801
1229
  seen.add(norm);
802
1230
  dirs.push(dir);
@@ -816,12 +1244,12 @@ function findCookieFile(platform, customDir) {
816
1244
  const filenames = domainNames[platform] || [];
817
1245
  const candidateDirs = getCandidateDirs(customDir);
818
1246
  for (const dir of candidateDirs) {
819
- if (!fs2.existsSync(dir)) continue;
1247
+ if (!fs3.existsSync(dir)) continue;
820
1248
  for (const file of filenames) {
821
- const fullPath = path3.join(dir, file);
822
- if (fs2.existsSync(fullPath)) {
1249
+ const fullPath = path4.join(dir, file);
1250
+ if (fs3.existsSync(fullPath)) {
823
1251
  try {
824
- const stats = fs2.statSync(fullPath);
1252
+ const stats = fs3.statSync(fullPath);
825
1253
  if (stats.size > 20) {
826
1254
  return fullPath;
827
1255
  }
@@ -837,14 +1265,14 @@ function detectAvailableCookies(customDir) {
837
1265
  return platforms.filter((p) => Boolean(findCookieFile(p, customDir)));
838
1266
  }
839
1267
  async function createSafeCookieCopy(masterPath) {
840
- const userTempDir = path3.join(os2.homedir(), ".open-nami", "temp");
1268
+ const userTempDir = path4.join(os3.homedir(), ".open-nami", "temp");
841
1269
  let targetDir = userTempDir;
842
1270
  try {
843
1271
  await fsPromises2.mkdir(userTempDir, { recursive: true, mode: 448 });
844
1272
  } catch {
845
- targetDir = os2.tmpdir();
1273
+ targetDir = os3.tmpdir();
846
1274
  }
847
- const tempPath = path3.join(targetDir, `nami_cookie_${Date.now()}_${Math.random().toString(36).slice(2)}.txt`);
1275
+ const tempPath = path4.join(targetDir, `nami_cookie_${Date.now()}_${Math.random().toString(36).slice(2)}.txt`);
848
1276
  await fsPromises2.copyFile(masterPath, tempPath);
849
1277
  await fsPromises2.chmod(tempPath, 384).catch(() => {
850
1278
  });
@@ -860,141 +1288,6 @@ async function createSafeCookieCopy(masterPath) {
860
1288
  };
861
1289
  }
862
1290
 
863
- // src/lib/known-folders.ts
864
- import { execFileSync } from "child_process";
865
- import fs3 from "fs";
866
- import os3 from "os";
867
- import path4 from "path";
868
- var KNOWN_FOLDER_DOWNLOADS_GUID = "{374DE290-123F-4565-9164-39C4925E467B}";
869
- var USER_SHELL_FOLDERS_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders";
870
- function expandWindowsEnv(str, env = process.env, homeDir) {
871
- return str.replace(/%([^%]+)%/g, (original, name) => {
872
- if (homeDir && name.toLowerCase() === "userprofile") {
873
- return homeDir;
874
- }
875
- const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
876
- if (key && env[key] !== void 0) {
877
- return env[key];
878
- }
879
- return original;
880
- });
881
- }
882
- function parseWindowsRegistryOutput(output, targetKey = KNOWN_FOLDER_DOWNLOADS_GUID) {
883
- const lines = output.split(/\r?\n/);
884
- const escaped = targetKey.replace(/[{}]/g, "\\$&");
885
- const pattern = new RegExp(`^\\s*${escaped}\\s+REG_\\w+\\s+(.*)$`, "i");
886
- for (const line of lines) {
887
- const match = line.match(pattern);
888
- if (match && match[1]) {
889
- const rawPath = match[1].trim();
890
- if (rawPath) return rawPath;
891
- }
892
- }
893
- return void 0;
894
- }
895
- function parseLinuxUserDirsOutput(content) {
896
- const lines = content.split(/\r?\n/);
897
- for (const line of lines) {
898
- const trimmed = line.trim();
899
- if (trimmed.startsWith("#")) continue;
900
- const match = trimmed.match(/^XDG_DOWNLOAD_DIR=["']?([^"']+)["']?$/);
901
- if (match && match[1]) {
902
- return match[1].trim();
903
- }
904
- }
905
- return void 0;
906
- }
907
- function isPathDriveAccessible(filePath, fsExists = fs3.existsSync, platform = process.platform) {
908
- try {
909
- if (fsExists(filePath)) {
910
- return true;
911
- }
912
- if (platform === "win32") {
913
- const match = filePath.match(/^([a-zA-Z]:[\\/])/i);
914
- const driveRoot = match ? match[1] : path4.win32.parse(path4.win32.resolve(filePath)).root;
915
- return fsExists(driveRoot);
916
- }
917
- const normalized = filePath.replace(/\\/g, "/");
918
- const mountMatch = normalized.match(/^(\/(?:media(?:\/[^/]+)?|mnt|run\/media(?:\/[^/]+)?)\/[^/]+)/);
919
- if (mountMatch) {
920
- return fsExists(mountMatch[1]);
921
- }
922
- let curr = path4.posix.dirname(normalized);
923
- while (curr && curr !== "/") {
924
- if (fsExists(curr)) return true;
925
- curr = path4.posix.dirname(curr);
926
- }
927
- return fsExists("/");
928
- } catch {
929
- return false;
930
- }
931
- }
932
- function resolveWindowsDownloadsDir(options = {}) {
933
- const homeDir = options.homeDir ?? os3.homedir();
934
- const env = options.env ?? process.env;
935
- const runQuery = options.regQueryFn ?? ((key, value) => {
936
- try {
937
- return execFileSync("reg", ["query", key, "/v", value], {
938
- encoding: "utf8",
939
- stdio: ["pipe", "pipe", "ignore"]
940
- });
941
- } catch {
942
- return "";
943
- }
944
- });
945
- const checkAccessible = options.driveAccessibleFn ?? ((p) => isPathDriveAccessible(p, fs3.existsSync, "win32"));
946
- let raw = parseWindowsRegistryOutput(
947
- runQuery(USER_SHELL_FOLDERS_REG_KEY, KNOWN_FOLDER_DOWNLOADS_GUID),
948
- KNOWN_FOLDER_DOWNLOADS_GUID
949
- );
950
- if (!raw) {
951
- raw = parseWindowsRegistryOutput(runQuery(USER_SHELL_FOLDERS_REG_KEY, "Downloads"), "Downloads");
952
- }
953
- if (raw) {
954
- const expanded = expandWindowsEnv(raw, env, homeDir);
955
- if (checkAccessible(expanded)) {
956
- return path4.win32.resolve(expanded);
957
- }
958
- }
959
- return void 0;
960
- }
961
- function resolveLinuxDownloadsDir(options = {}) {
962
- const homeDir = options.homeDir ?? os3.homedir();
963
- const env = options.env ?? process.env;
964
- const readFn = options.readFileFn ?? ((p) => {
965
- try {
966
- return fs3.readFileSync(p, "utf8");
967
- } catch {
968
- return "";
969
- }
970
- });
971
- const checkAccessible = options.driveAccessibleFn ?? ((p) => isPathDriveAccessible(p, fs3.existsSync, "linux"));
972
- const configHome = env.XDG_CONFIG_HOME || path4.join(homeDir, ".config");
973
- const userDirsFile = path4.join(configHome, "user-dirs.dirs");
974
- const content = readFn(userDirsFile);
975
- if (!content) return void 0;
976
- const raw = parseLinuxUserDirsOutput(content);
977
- if (!raw) return void 0;
978
- const expanded = raw.replace(/\$HOME/g, homeDir).replace(/\$\{HOME\}/g, homeDir);
979
- if (checkAccessible(expanded)) {
980
- return path4.posix.resolve(expanded);
981
- }
982
- return void 0;
983
- }
984
- function resolvePlatformDownloadsDir(options = {}) {
985
- const platform = options.platform ?? process.platform;
986
- const homeDir = options.homeDir ?? os3.homedir();
987
- const defaultFallback = path4.join(homeDir, "Downloads");
988
- if (platform === "win32") {
989
- const resolved = resolveWindowsDownloadsDir(options);
990
- if (resolved) return resolved;
991
- } else if (platform === "linux") {
992
- const resolved = resolveLinuxDownloadsDir(options);
993
- if (resolved) return resolved;
994
- }
995
- return defaultFallback;
996
- }
997
-
998
1291
  // src/lib/engines/extractors/instagram.ts
999
1292
  import crypto from "crypto";
1000
1293
 
@@ -1379,8 +1672,8 @@ var InstagramExtractor = class {
1379
1672
  }
1380
1673
  const user = res.data.user;
1381
1674
  if (user.is_private) {
1382
- const fs14 = user.friendship_status;
1383
- const isFollowing = fs14 ? Boolean(fs14.following) : Boolean(user.followed_by_viewer);
1675
+ const fs18 = user.friendship_status;
1676
+ const isFollowing = fs18 ? Boolean(fs18.following) : Boolean(user.followed_by_viewer);
1384
1677
  if (!isFollowing) {
1385
1678
  throw new Error(`Instagram profile @${username} is private. You must provide a session cookie jar of an account following this profile.`);
1386
1679
  }
@@ -2947,6 +3240,30 @@ var FacebookExtractor = class {
2947
3240
  }
2948
3241
  };
2949
3242
 
3243
+ // src/lib/engines/core/plugin.ts
3244
+ var ExtractorPluginRegistry = class {
3245
+ plugins = /* @__PURE__ */ new Map();
3246
+ register(plugin) {
3247
+ this.plugins.set(plugin.platform, plugin);
3248
+ }
3249
+ get(platform) {
3250
+ return this.plugins.get(platform);
3251
+ }
3252
+ has(platform) {
3253
+ return this.plugins.has(platform);
3254
+ }
3255
+ list() {
3256
+ return Array.from(this.plugins.values());
3257
+ }
3258
+ findByUrl(url) {
3259
+ return this.list().find((plugin) => plugin.canHandle(url));
3260
+ }
3261
+ clear() {
3262
+ this.plugins.clear();
3263
+ }
3264
+ };
3265
+ var defaultPluginRegistry = new ExtractorPluginRegistry();
3266
+
2950
3267
  // src/lib/engines/extractors/index.ts
2951
3268
  function getExtractor(platform) {
2952
3269
  switch (platform) {
@@ -3750,7 +4067,7 @@ var PLATFORM_SLEEP = {
3750
4067
  facebook: "1.0-1.8"
3751
4068
  };
3752
4069
  function buildGalleryDlArgs(options) {
3753
- const { destDir, subDir, platform, cleanUrl, username, cookiePath, writeMetadata } = options;
4070
+ const { destDir, subDir, platform, cleanUrl, username, cookiePath, cookiesFromBrowser, writeMetadata } = options;
3754
4071
  const args = [
3755
4072
  "-D",
3756
4073
  destDir,
@@ -3801,6 +4118,9 @@ function buildGalleryDlArgs(options) {
3801
4118
  if (cookiePath) {
3802
4119
  args.push("--cookies", cookiePath);
3803
4120
  }
4121
+ if (cookiesFromBrowser && cookiesFromBrowser !== "none") {
4122
+ args.push("--cookies-from-browser", cookiesFromBrowser);
4123
+ }
3804
4124
  args.push("--", targetUrl);
3805
4125
  return args;
3806
4126
  }
@@ -3815,6 +4135,7 @@ async function runGalleryDl(options) {
3815
4135
  cleanUrl: profile.cleanUrl,
3816
4136
  username: profile.username,
3817
4137
  cookiePath,
4138
+ cookiesFromBrowser: options.cookiesFromBrowser,
3818
4139
  writeMetadata: options.writeMetadata
3819
4140
  });
3820
4141
  let downloadedCount = 0;
@@ -3923,7 +4244,7 @@ import fs9 from "fs/promises";
3923
4244
  var YTDLP_ARCHIVE = "archive_yt-dlp.txt";
3924
4245
  var UA2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
3925
4246
  function buildYtDlpArgs(options) {
3926
- const { destDir, platform, cleanUrl, cookiePath, writeMetadata } = options;
4247
+ const { destDir, platform, cleanUrl, cookiePath, cookiesFromBrowser, writeMetadata } = options;
3927
4248
  const args = [
3928
4249
  "-o",
3929
4250
  path9.join(destDir, "%(title).150B [%(id)s].%(ext)s"),
@@ -3957,6 +4278,9 @@ function buildYtDlpArgs(options) {
3957
4278
  if (cookiePath) {
3958
4279
  args.push("--cookies", cookiePath);
3959
4280
  }
4281
+ if (cookiesFromBrowser && cookiesFromBrowser !== "none") {
4282
+ args.push("--cookies-from-browser", cookiesFromBrowser);
4283
+ }
3960
4284
  args.push("--", cleanUrl);
3961
4285
  return args;
3962
4286
  }
@@ -3969,6 +4293,7 @@ async function runYtDlp(options) {
3969
4293
  platform: profile.platform,
3970
4294
  cleanUrl: profile.cleanUrl,
3971
4295
  cookiePath,
4296
+ cookiesFromBrowser: options.cookiesFromBrowser,
3972
4297
  writeMetadata: options.writeMetadata
3973
4298
  });
3974
4299
  let downloadedCount = 0;
@@ -4071,78 +4396,489 @@ async function runYtDlp(options) {
4071
4396
  skipped: skippedCount,
4072
4397
  error: err.message
4073
4398
  });
4074
- });
4399
+ });
4400
+ });
4401
+ }
4402
+
4403
+ // src/lib/engines/core/task-manager.ts
4404
+ import path12 from "path";
4405
+ import os7 from "os";
4406
+ import fs12 from "fs/promises";
4407
+ import fsSync from "fs";
4408
+
4409
+ // src/lib/engines/core/event-bus.ts
4410
+ import { EventEmitter } from "events";
4411
+ var EngineEventBus = class {
4412
+ emitter = new EventEmitter();
4413
+ constructor() {
4414
+ this.emitter.setMaxListeners(50);
4415
+ }
4416
+ on(event, listener) {
4417
+ this.emitter.on(event, listener);
4418
+ return () => this.emitter.off(event, listener);
4419
+ }
4420
+ off(event, listener) {
4421
+ this.emitter.off(event, listener);
4422
+ }
4423
+ once(event, listener) {
4424
+ this.emitter.once(event, listener);
4425
+ }
4426
+ emit(event, payload) {
4427
+ return this.emitter.emit(event, payload);
4428
+ }
4429
+ removeAllListeners(event) {
4430
+ if (event !== void 0) {
4431
+ this.emitter.removeAllListeners(event);
4432
+ } else {
4433
+ this.emitter.removeAllListeners();
4434
+ }
4435
+ }
4436
+ };
4437
+
4438
+ // src/lib/engines/storage/ledger.ts
4439
+ import fs10 from "fs";
4440
+ import path10 from "path";
4441
+ import os5 from "os";
4442
+ import { createRequire } from "module";
4443
+ var require2 = createRequire(import.meta.url);
4444
+ var MediaLedger = class {
4445
+ db = null;
4446
+ inMemoryFallback = {
4447
+ media: /* @__PURE__ */ new Map(),
4448
+ checkpoints: /* @__PURE__ */ new Map()
4449
+ };
4450
+ isFallback = false;
4451
+ resolvedPath;
4452
+ constructor(dbPath, forceFallback = false) {
4453
+ const resolvedPath = dbPath ?? (forceFallback ? ":memory:" : path10.join(os5.homedir(), ".open-nami", "nami.db"));
4454
+ this.resolvedPath = resolvedPath;
4455
+ if (forceFallback) {
4456
+ this.isFallback = true;
4457
+ this.loadFallbackDisk();
4458
+ return;
4459
+ }
4460
+ try {
4461
+ if (resolvedPath !== ":memory:") {
4462
+ fs10.mkdirSync(path10.dirname(resolvedPath), { recursive: true });
4463
+ }
4464
+ const { DatabaseSync } = require2("node:sqlite");
4465
+ this.db = new DatabaseSync(resolvedPath);
4466
+ this.initTables();
4467
+ } catch {
4468
+ this.isFallback = true;
4469
+ this.loadFallbackDisk();
4470
+ }
4471
+ }
4472
+ loadFallbackDisk() {
4473
+ if (this.resolvedPath === ":memory:") return;
4474
+ try {
4475
+ if (fs10.existsSync(this.resolvedPath)) {
4476
+ const raw = fs10.readFileSync(this.resolvedPath, "utf8");
4477
+ const data = JSON.parse(raw);
4478
+ if (data && typeof data === "object") {
4479
+ if (data.media && typeof data.media === "object") {
4480
+ for (const [k, v] of Object.entries(data.media)) {
4481
+ this.inMemoryFallback.media.set(k, v);
4482
+ }
4483
+ }
4484
+ if (data.checkpoints && typeof data.checkpoints === "object") {
4485
+ for (const [k, v] of Object.entries(data.checkpoints)) {
4486
+ this.inMemoryFallback.checkpoints.set(k, v);
4487
+ }
4488
+ }
4489
+ }
4490
+ }
4491
+ } catch {
4492
+ }
4493
+ }
4494
+ flushFallbackDisk() {
4495
+ if (this.resolvedPath === ":memory:") return;
4496
+ try {
4497
+ fs10.mkdirSync(path10.dirname(this.resolvedPath), { recursive: true });
4498
+ const data = {
4499
+ media: Object.fromEntries(this.inMemoryFallback.media),
4500
+ checkpoints: Object.fromEntries(this.inMemoryFallback.checkpoints)
4501
+ };
4502
+ fs10.writeFileSync(this.resolvedPath, JSON.stringify(data, null, 2), "utf8");
4503
+ } catch {
4504
+ }
4505
+ }
4506
+ initTables() {
4507
+ if (!this.db) return;
4508
+ this.db.exec(`
4509
+ CREATE TABLE IF NOT EXISTS media_ledger (
4510
+ id TEXT PRIMARY KEY,
4511
+ platform TEXT,
4512
+ author TEXT,
4513
+ url TEXT,
4514
+ filename TEXT,
4515
+ file_path TEXT,
4516
+ downloaded_at TEXT
4517
+ );
4518
+ CREATE TABLE IF NOT EXISTS checkpoints (
4519
+ key TEXT PRIMARY KEY,
4520
+ platform TEXT,
4521
+ username TEXT,
4522
+ target_dir TEXT,
4523
+ cursors_json TEXT,
4524
+ updated_at TEXT
4525
+ );
4526
+ `);
4527
+ }
4528
+ hasMedia(id) {
4529
+ if (this.isFallback || !this.db) {
4530
+ return this.inMemoryFallback.media.has(id);
4531
+ }
4532
+ const stmt = this.db.prepare("SELECT 1 FROM media_ledger WHERE id = ?");
4533
+ const row = stmt.get(id);
4534
+ return Boolean(row);
4535
+ }
4536
+ recordMedia(media) {
4537
+ if (this.isFallback || !this.db) {
4538
+ this.inMemoryFallback.media.set(media.id, media);
4539
+ this.flushFallbackDisk();
4540
+ return;
4541
+ }
4542
+ const stmt = this.db.prepare(`
4543
+ INSERT OR REPLACE INTO media_ledger (id, platform, author, url, filename, file_path, downloaded_at)
4544
+ VALUES (?, ?, ?, ?, ?, ?, ?)
4545
+ `);
4546
+ stmt.run(
4547
+ media.id,
4548
+ media.platform,
4549
+ media.author,
4550
+ media.url,
4551
+ media.filename,
4552
+ media.filePath,
4553
+ media.downloadedAt
4554
+ );
4555
+ }
4556
+ saveCheckpoint(checkpoint) {
4557
+ const key = `${checkpoint.platform}:${checkpoint.username.toLowerCase()}`;
4558
+ if (this.isFallback || !this.db) {
4559
+ this.inMemoryFallback.checkpoints.set(key, checkpoint);
4560
+ this.flushFallbackDisk();
4561
+ return;
4562
+ }
4563
+ const stmt = this.db.prepare(`
4564
+ INSERT OR REPLACE INTO checkpoints (key, platform, username, target_dir, cursors_json, updated_at)
4565
+ VALUES (?, ?, ?, ?, ?, ?)
4566
+ `);
4567
+ stmt.run(
4568
+ key,
4569
+ checkpoint.platform,
4570
+ checkpoint.username,
4571
+ checkpoint.targetDir,
4572
+ JSON.stringify(checkpoint.cursors),
4573
+ checkpoint.updatedAt
4574
+ );
4575
+ }
4576
+ getCheckpoint(platform, username) {
4577
+ const key = `${platform}:${username.toLowerCase()}`;
4578
+ if (this.isFallback || !this.db) {
4579
+ return this.inMemoryFallback.checkpoints.get(key) || null;
4580
+ }
4581
+ const stmt = this.db.prepare(
4582
+ "SELECT platform, username, target_dir, cursors_json, updated_at FROM checkpoints WHERE key = ?"
4583
+ );
4584
+ const row = stmt.get(key);
4585
+ if (!row) return null;
4586
+ return {
4587
+ platform: row.platform,
4588
+ username: row.username,
4589
+ targetDir: row.target_dir,
4590
+ cursors: JSON.parse(row.cursors_json || "{}"),
4591
+ updatedAt: row.updated_at
4592
+ };
4593
+ }
4594
+ clearCheckpoint(platform, username) {
4595
+ const key = `${platform}:${username.toLowerCase()}`;
4596
+ if (this.isFallback || !this.db) {
4597
+ this.inMemoryFallback.checkpoints.delete(key);
4598
+ this.flushFallbackDisk();
4599
+ return;
4600
+ }
4601
+ const stmt = this.db.prepare("DELETE FROM checkpoints WHERE key = ?");
4602
+ stmt.run(key);
4603
+ }
4604
+ close() {
4605
+ if (this.db) {
4606
+ try {
4607
+ this.db.close();
4608
+ } catch {
4609
+ }
4610
+ this.db = null;
4611
+ }
4612
+ }
4613
+ };
4614
+
4615
+ // src/lib/known-folders.ts
4616
+ import { execFileSync } from "child_process";
4617
+ import fs11 from "fs";
4618
+ import os6 from "os";
4619
+ import path11 from "path";
4620
+ var KNOWN_FOLDER_DOWNLOADS_GUID = "{374DE290-123F-4565-9164-39C4925E467B}";
4621
+ var USER_SHELL_FOLDERS_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders";
4622
+ function expandWindowsEnv(str, env = process.env, homeDir) {
4623
+ return str.replace(/%([^%]+)%/g, (original, name) => {
4624
+ if (homeDir && name.toLowerCase() === "userprofile") {
4625
+ return homeDir;
4626
+ }
4627
+ const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
4628
+ if (key && env[key] !== void 0) {
4629
+ return env[key];
4630
+ }
4631
+ return original;
4632
+ });
4633
+ }
4634
+ function parseWindowsRegistryOutput(output, targetKey = KNOWN_FOLDER_DOWNLOADS_GUID) {
4635
+ const lines = output.split(/\r?\n/);
4636
+ const escaped = targetKey.replace(/[{}]/g, "\\$&");
4637
+ const pattern = new RegExp(`^\\s*${escaped}\\s+REG_\\w+\\s+(.*)$`, "i");
4638
+ for (const line of lines) {
4639
+ const match = line.match(pattern);
4640
+ if (match && match[1]) {
4641
+ const rawPath = match[1].trim();
4642
+ if (rawPath) return rawPath;
4643
+ }
4644
+ }
4645
+ return void 0;
4646
+ }
4647
+ function parseLinuxUserDirsOutput(content) {
4648
+ const lines = content.split(/\r?\n/);
4649
+ for (const line of lines) {
4650
+ const trimmed = line.trim();
4651
+ if (trimmed.startsWith("#")) continue;
4652
+ const match = trimmed.match(/^XDG_DOWNLOAD_DIR=["']?([^"']+)["']?$/);
4653
+ if (match && match[1]) {
4654
+ return match[1].trim();
4655
+ }
4656
+ }
4657
+ return void 0;
4658
+ }
4659
+ function isPathDriveAccessible(filePath, fsExists = fs11.existsSync, platform = process.platform) {
4660
+ try {
4661
+ if (fsExists(filePath)) {
4662
+ return true;
4663
+ }
4664
+ if (platform === "win32") {
4665
+ const match = filePath.match(/^([a-zA-Z]:[\\/])/i);
4666
+ const driveRoot = match ? match[1] : path11.win32.parse(path11.win32.resolve(filePath)).root;
4667
+ return fsExists(driveRoot);
4668
+ }
4669
+ const normalized = filePath.replace(/\\/g, "/");
4670
+ const mountMatch = normalized.match(/^(\/(?:media(?:\/[^/]+)?|mnt|run\/media(?:\/[^/]+)?)\/[^/]+)/);
4671
+ if (mountMatch) {
4672
+ return fsExists(mountMatch[1]);
4673
+ }
4674
+ let curr = path11.posix.dirname(normalized);
4675
+ while (curr && curr !== "/") {
4676
+ if (fsExists(curr)) return true;
4677
+ curr = path11.posix.dirname(curr);
4678
+ }
4679
+ return fsExists("/");
4680
+ } catch {
4681
+ return false;
4682
+ }
4683
+ }
4684
+ function resolveWindowsDownloadsDir(options = {}) {
4685
+ const homeDir = options.homeDir ?? os6.homedir();
4686
+ const env = options.env ?? process.env;
4687
+ const runQuery = options.regQueryFn ?? ((key, value) => {
4688
+ try {
4689
+ return execFileSync("reg", ["query", key, "/v", value], {
4690
+ encoding: "utf8",
4691
+ stdio: ["pipe", "pipe", "ignore"]
4692
+ });
4693
+ } catch {
4694
+ return "";
4695
+ }
4696
+ });
4697
+ const checkAccessible = options.driveAccessibleFn ?? ((p) => isPathDriveAccessible(p, fs11.existsSync, "win32"));
4698
+ let raw = parseWindowsRegistryOutput(
4699
+ runQuery(USER_SHELL_FOLDERS_REG_KEY, KNOWN_FOLDER_DOWNLOADS_GUID),
4700
+ KNOWN_FOLDER_DOWNLOADS_GUID
4701
+ );
4702
+ if (!raw) {
4703
+ raw = parseWindowsRegistryOutput(runQuery(USER_SHELL_FOLDERS_REG_KEY, "Downloads"), "Downloads");
4704
+ }
4705
+ if (raw) {
4706
+ const expanded = expandWindowsEnv(raw, env, homeDir);
4707
+ if (checkAccessible(expanded)) {
4708
+ return path11.win32.resolve(expanded);
4709
+ }
4710
+ }
4711
+ return void 0;
4712
+ }
4713
+ function resolveLinuxDownloadsDir(options = {}) {
4714
+ const homeDir = options.homeDir ?? os6.homedir();
4715
+ const env = options.env ?? process.env;
4716
+ const readFn = options.readFileFn ?? ((p) => {
4717
+ try {
4718
+ return fs11.readFileSync(p, "utf8");
4719
+ } catch {
4720
+ return "";
4721
+ }
4075
4722
  });
4723
+ const checkAccessible = options.driveAccessibleFn ?? ((p) => isPathDriveAccessible(p, fs11.existsSync, "linux"));
4724
+ const configHome = env.XDG_CONFIG_HOME || path11.join(homeDir, ".config");
4725
+ const userDirsFile = path11.join(configHome, "user-dirs.dirs");
4726
+ const content = readFn(userDirsFile);
4727
+ if (!content) return void 0;
4728
+ const raw = parseLinuxUserDirsOutput(content);
4729
+ if (!raw) return void 0;
4730
+ const expanded = raw.replace(/\$HOME/g, homeDir).replace(/\$\{HOME\}/g, homeDir);
4731
+ if (checkAccessible(expanded)) {
4732
+ return path11.posix.resolve(expanded);
4733
+ }
4734
+ return void 0;
4735
+ }
4736
+ function resolvePlatformDownloadsDir(options = {}) {
4737
+ const platform = options.platform ?? process.platform;
4738
+ const homeDir = options.homeDir ?? os6.homedir();
4739
+ const defaultFallback = path11.join(homeDir, "Downloads");
4740
+ if (platform === "win32") {
4741
+ const resolved = resolveWindowsDownloadsDir(options);
4742
+ if (resolved) return resolved;
4743
+ } else if (platform === "linux") {
4744
+ const resolved = resolveLinuxDownloadsDir(options);
4745
+ if (resolved) return resolved;
4746
+ }
4747
+ return defaultFallback;
4076
4748
  }
4077
4749
 
4078
- // src/lib/engines/dispatcher.ts
4079
- var REFERER_MAP = {
4080
- tiktok: "https://www.tiktok.com/",
4081
- instagram: "https://www.instagram.com/",
4082
- x: "https://x.com/",
4083
- facebook: "https://www.facebook.com/"
4084
- };
4750
+ // src/lib/engines/core/task-manager.ts
4085
4751
  function expandPath(dir) {
4086
4752
  if (dir.startsWith("~/") || dir.startsWith("~\\") || dir === "~") {
4087
- return path10.join(os5.homedir(), dir.slice(1));
4753
+ return path12.join(os7.homedir(), dir.slice(1));
4088
4754
  }
4089
4755
  return dir;
4090
4756
  }
4091
4757
  function resolveDownloadBaseDir(customBaseDir) {
4092
4758
  if (customBaseDir && customBaseDir.trim()) {
4093
- return path10.resolve(expandPath(customBaseDir.trim()));
4759
+ return path12.resolve(expandPath(customBaseDir.trim()));
4094
4760
  }
4095
4761
  const envDir = process.env.OPEN_NAMI_DIR || process.env.NAMI_DIR;
4096
4762
  if (envDir && envDir.trim()) {
4097
- return path10.resolve(expandPath(envDir.trim()));
4763
+ return path12.resolve(expandPath(envDir.trim()));
4098
4764
  }
4099
4765
  const configDir = loadConfig().downloadDir;
4100
4766
  if (configDir && configDir.trim()) {
4101
- return path10.resolve(expandPath(configDir.trim()));
4767
+ return path12.resolve(expandPath(configDir.trim()));
4102
4768
  }
4103
4769
  if (process.env.NAMI_BASE_DIR && process.env.NAMI_BASE_DIR.trim()) {
4104
4770
  const raw = process.env.NAMI_BASE_DIR.trim();
4105
- return path10.resolve(expandPath(raw.endsWith("downloads") ? raw : path10.join(raw, "downloads")));
4771
+ return path12.resolve(expandPath(raw.endsWith("downloads") ? raw : path12.join(raw, "downloads")));
4106
4772
  }
4107
- return path10.join(resolvePlatformDownloadsDir(), "Open Nami");
4773
+ return path12.join(resolvePlatformDownloadsDir(), "Open Nami");
4108
4774
  }
4109
4775
  function resolveDefaultDownloadDir(platform, username, baseDir) {
4110
4776
  const root = resolveDownloadBaseDir(baseDir);
4111
- return path10.join(root, platform, username);
4777
+ return path12.join(root, platform, username);
4112
4778
  }
4113
4779
  async function cleanupEmptyCategoryDirs(targetDir) {
4114
4780
  const candidateDirs = ["All", "Stories", "Highlights", "Photos", "Videos"];
4115
4781
  for (const dirName of candidateDirs) {
4116
- const fullPath = path10.join(targetDir, dirName);
4782
+ const fullPath = path12.join(targetDir, dirName);
4117
4783
  try {
4118
4784
  if (!fsSync.existsSync(fullPath)) continue;
4119
- const entries = await fs10.readdir(fullPath);
4785
+ const entries = await fs12.readdir(fullPath);
4120
4786
  const nonArchiveEntries = entries.filter(
4121
4787
  (e) => e !== ".open-nami-archive.json" && e !== "archive.txt" && e !== "archive_yt-dlp.txt" && e !== "archive_gallery-dl.sqlite3"
4122
4788
  );
4123
4789
  if (nonArchiveEntries.length === 0) {
4124
4790
  for (const entry of entries) {
4125
4791
  try {
4126
- await fs10.unlink(path10.join(fullPath, entry));
4792
+ await fs12.unlink(path12.join(fullPath, entry));
4127
4793
  } catch {
4128
4794
  }
4129
4795
  }
4130
- await fs10.rmdir(fullPath).catch(() => {
4796
+ await fs12.rmdir(fullPath).catch(() => {
4131
4797
  });
4132
4798
  }
4133
4799
  } catch {
4134
4800
  }
4135
4801
  }
4136
4802
  }
4803
+ var TaskManager = class {
4804
+ events;
4805
+ ledger;
4806
+ plugins;
4807
+ constructor(eventBus, ledger, plugins) {
4808
+ this.events = eventBus || new EngineEventBus();
4809
+ this.ledger = ledger || new MediaLedger();
4810
+ this.plugins = plugins || defaultPluginRegistry;
4811
+ }
4812
+ async execute(options) {
4813
+ const { profile, choice } = options;
4814
+ const targetDir = options.outputDir || resolveDefaultDownloadDir(profile.platform, profile.username);
4815
+ this.events.emit("job:start", { profile, choice, outputDir: targetDir });
4816
+ let downloadedCount = 0;
4817
+ let skippedCount = 0;
4818
+ const errors = [];
4819
+ if (options.runnerOverrides?.nativeRunner) {
4820
+ const subDir = choice === "all" ? "All" : choice.charAt(0).toUpperCase() + choice.slice(1);
4821
+ const runRes = await options.runnerOverrides.nativeRunner(subDir);
4822
+ downloadedCount += runRes.downloaded;
4823
+ skippedCount += runRes.skipped;
4824
+ if (!runRes.success) errors.push(runRes.error || "Execution failed");
4825
+ }
4826
+ await cleanupEmptyCategoryDirs(targetDir);
4827
+ const result = {
4828
+ success: errors.length === 0,
4829
+ downloadedCount,
4830
+ skippedCount,
4831
+ errors,
4832
+ outputDir: targetDir
4833
+ };
4834
+ this.events.emit("job:complete", { result });
4835
+ return result;
4836
+ }
4837
+ };
4838
+ var defaultTaskManager = new TaskManager();
4839
+
4840
+ // src/lib/engines/dispatcher.ts
4841
+ var REFERER_MAP = {
4842
+ tiktok: "https://www.tiktok.com/",
4843
+ instagram: "https://www.instagram.com/",
4844
+ x: "https://x.com/",
4845
+ facebook: "https://www.facebook.com/"
4846
+ };
4137
4847
  async function dispatchDownload(options) {
4138
4848
  const { profile, choice, onProgress } = options;
4139
4849
  const targetDir = options.outputDir || resolveDefaultDownloadDir(profile.platform, profile.username);
4140
- await fs10.mkdir(targetDir, { recursive: true });
4850
+ await fs13.mkdir(targetDir, { recursive: true });
4851
+ const rawBrowserPref = options.cookiesFromBrowser ?? loadConfig().cookiesFromBrowser ?? "auto";
4852
+ const resolvedBrowserArg = resolveBrowserCookieArg(rawBrowserPref);
4853
+ const cookiesFromBrowser = resolvedBrowserArg ?? void 0;
4854
+ defaultTaskManager.events.emit("job:start", { profile, choice, outputDir: targetDir });
4141
4855
  const masterCookie = findCookieFile(profile.platform);
4142
4856
  const cookieCopy = masterCookie ? await createSafeCookieCopy(masterCookie) : null;
4143
4857
  let totalDownloaded = 0;
4144
4858
  let totalSkipped = 0;
4145
4859
  const errors = [];
4860
+ const recordError = (err) => {
4861
+ errors.push(err);
4862
+ defaultTaskManager.events.emit("job:error", { error: err });
4863
+ };
4864
+ const handleProgress = (progress) => {
4865
+ defaultTaskManager.events.emit("job:progress", progress);
4866
+ if (progress.currentFile && progress.statusText?.startsWith("Saved:")) {
4867
+ try {
4868
+ defaultTaskManager.ledger.recordMedia({
4869
+ id: `${profile.platform}:${profile.username}:${progress.currentFile}`,
4870
+ platform: profile.platform,
4871
+ author: profile.username,
4872
+ url: profile.cleanUrl || "",
4873
+ filename: progress.currentFile,
4874
+ filePath: path13.join(targetDir, progress.currentFile),
4875
+ downloadedAt: (/* @__PURE__ */ new Date()).toISOString()
4876
+ });
4877
+ } catch {
4878
+ }
4879
+ }
4880
+ onProgress?.(progress);
4881
+ };
4146
4882
  try {
4147
4883
  const jobs = [];
4148
4884
  if (choice === "photos") {
@@ -4166,7 +4902,7 @@ async function dispatchDownload(options) {
4166
4902
  const savedCheckpoint = options.resume ? await loadCheckpoint(targetDir) : null;
4167
4903
  const currentCursors = savedCheckpoint?.cursors ? { ...savedCheckpoint.cursors } : {};
4168
4904
  const defaultRunNative = async (subDir) => {
4169
- const dest = subDir === "All" ? targetDir : path10.join(targetDir, subDir);
4905
+ const dest = subDir === "All" ? targetDir : path13.join(targetDir, subDir);
4170
4906
  const remainingLimit = globalLimit !== void 0 ? Math.max(0, globalLimit - totalDownloaded) : void 0;
4171
4907
  if (remainingLimit === 0) {
4172
4908
  return { success: true, downloaded: 0, skipped: 0 };
@@ -4208,8 +4944,8 @@ async function dispatchDownload(options) {
4208
4944
  jobName: `${profile.username} ${subDir}`,
4209
4945
  destDir: dest,
4210
4946
  destDirMap: subDir === "All" ? {
4211
- photo: path10.join(targetDir, "Photos"),
4212
- video: path10.join(targetDir, "Videos")
4947
+ photo: path13.join(targetDir, "Photos"),
4948
+ video: path13.join(targetDir, "Videos")
4213
4949
  } : void 0,
4214
4950
  cookiePath: cookieCopy?.filePath,
4215
4951
  referer: REFERER_MAP[profile.platform],
@@ -4218,7 +4954,7 @@ async function dispatchDownload(options) {
4218
4954
  writeMetadata: options.writeMetadata,
4219
4955
  defaultPlatform: profile.platform,
4220
4956
  defaultAuthor: profile.username,
4221
- onProgress
4957
+ onProgress: handleProgress
4222
4958
  });
4223
4959
  return {
4224
4960
  success: res.errors.length === 0,
@@ -4232,35 +4968,43 @@ async function dispatchDownload(options) {
4232
4968
  success: false,
4233
4969
  downloaded: 0,
4234
4970
  skipped: 0,
4235
- error: errMsg
4971
+ error: errMsg || "Execution failed"
4236
4972
  };
4237
4973
  }
4238
4974
  };
4239
4975
  const runNative = options.runnerOverrides?.nativeRunner ?? defaultRunNative;
4240
- const runGdl = options.runnerOverrides?.gdlRunner ?? ((subDir) => runGalleryDl({
4241
- profile,
4242
- targetDir,
4243
- subDir,
4244
- cookiePath: cookieCopy?.filePath,
4245
- signal: options.signal,
4246
- writeMetadata: options.writeMetadata,
4247
- onProgress
4248
- }));
4249
- const runYt = options.runnerOverrides?.ytRunner ?? (() => runYtDlp({
4250
- profile,
4251
- targetDir,
4252
- cookiePath: cookieCopy?.filePath,
4253
- signal: options.signal,
4254
- writeMetadata: options.writeMetadata,
4255
- onProgress
4256
- }));
4976
+ const runGdl = (subDir) => {
4977
+ const runnerOpts = {
4978
+ profile,
4979
+ targetDir,
4980
+ subDir,
4981
+ cookiePath: cookieCopy?.filePath,
4982
+ cookiesFromBrowser,
4983
+ signal: options.signal,
4984
+ writeMetadata: options.writeMetadata,
4985
+ onProgress: handleProgress
4986
+ };
4987
+ return options.runnerOverrides?.gdlRunner ? options.runnerOverrides.gdlRunner(subDir, runnerOpts) : runGalleryDl(runnerOpts);
4988
+ };
4989
+ const runYt = () => {
4990
+ const runnerOpts = {
4991
+ profile,
4992
+ targetDir,
4993
+ cookiePath: cookieCopy?.filePath,
4994
+ cookiesFromBrowser,
4995
+ signal: options.signal,
4996
+ writeMetadata: options.writeMetadata,
4997
+ onProgress: handleProgress
4998
+ };
4999
+ return options.runnerOverrides?.ytRunner ? options.runnerOverrides.ytRunner(runnerOpts) : runYtDlp(runnerOpts);
5000
+ };
4257
5001
  for (const job of jobs) {
4258
5002
  if (options.signal?.aborted) break;
4259
5003
  if (globalLimit !== void 0 && totalDownloaded >= globalLimit) break;
4260
5004
  if (job.type === "all") {
4261
5005
  let res = await runNative("All");
4262
5006
  if ((!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
4263
- onProgress?.({
5007
+ handleProgress({
4264
5008
  job: "Photos",
4265
5009
  downloadedCount: totalDownloaded,
4266
5010
  skippedCount: totalSkipped,
@@ -4269,8 +5013,8 @@ async function dispatchDownload(options) {
4269
5013
  const gdlPhotos = await runGdl("Photos");
4270
5014
  totalDownloaded += gdlPhotos.downloaded;
4271
5015
  totalSkipped += gdlPhotos.skipped;
4272
- if (!gdlPhotos.success && gdlPhotos.error) errors.push(`[Photos] ${gdlPhotos.error}`);
4273
- onProgress?.({
5016
+ if (!gdlPhotos.success) recordError(gdlPhotos.error ? `[Photos] ${gdlPhotos.error}` : "[Photos] Execution failed");
5017
+ handleProgress({
4274
5018
  job: "Videos",
4275
5019
  downloadedCount: totalDownloaded,
4276
5020
  skippedCount: totalSkipped,
@@ -4280,9 +5024,9 @@ async function dispatchDownload(options) {
4280
5024
  if (ytVideos.downloaded > 0 || ytVideos.skipped > 0 || ytVideos.success) {
4281
5025
  totalDownloaded += ytVideos.downloaded;
4282
5026
  totalSkipped += ytVideos.skipped;
4283
- if (!ytVideos.success && ytVideos.error) errors.push(`[Videos] ${ytVideos.error}`);
5027
+ if (!ytVideos.success) recordError(ytVideos.error ? `[Videos] ${ytVideos.error}` : "[Videos] Execution failed");
4284
5028
  } else if (!options.signal?.aborted) {
4285
- onProgress?.({
5029
+ handleProgress({
4286
5030
  job: "Videos",
4287
5031
  downloadedCount: totalDownloaded,
4288
5032
  skippedCount: totalSkipped,
@@ -4291,19 +5035,19 @@ async function dispatchDownload(options) {
4291
5035
  const gdlVideos = await runGdl("Videos");
4292
5036
  totalDownloaded += gdlVideos.downloaded;
4293
5037
  totalSkipped += gdlVideos.skipped;
4294
- if (!gdlVideos.success && gdlVideos.error) errors.push(`[Videos] ${gdlVideos.error}`);
5038
+ if (!gdlVideos.success) recordError(gdlVideos.error ? `[Videos] ${gdlVideos.error}` : "[Videos] Execution failed");
4295
5039
  }
4296
5040
  } else {
4297
5041
  totalDownloaded += res.downloaded;
4298
5042
  totalSkipped += res.skipped;
4299
- if (!res.success && res.error) errors.push(`[All] ${res.error}`);
5043
+ if (!res.success) recordError(res.error ? `[All] ${res.error}` : "[All] Execution failed");
4300
5044
  }
4301
5045
  } else if (job.type === "photos" || job.type === "stories" || job.type === "highlights") {
4302
5046
  const subDir = job.type === "photos" ? "Photos" : job.type === "stories" ? "Stories" : "Highlights";
4303
5047
  let res = await runNative(subDir);
4304
5048
  const isAuthError = Boolean(res.error && (res.error.includes("cookie") || res.error.includes("authenticated session") || res.error.includes("Settings (^s)")));
4305
5049
  if (!isAuthError && (!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
4306
- onProgress?.({
5050
+ handleProgress({
4307
5051
  job: subDir,
4308
5052
  downloadedCount: totalDownloaded,
4309
5053
  skippedCount: totalSkipped,
@@ -4316,14 +5060,14 @@ async function dispatchDownload(options) {
4316
5060
  }
4317
5061
  totalDownloaded += res.downloaded;
4318
5062
  totalSkipped += res.skipped;
4319
- if (!res.success && res.error) errors.push(`[${subDir}] ${res.error}`);
5063
+ if (!res.success) recordError(res.error ? `[${subDir}] ${res.error}` : `[${subDir}] Execution failed`);
4320
5064
  } else if (job.type === "videos") {
4321
5065
  let res = await runNative("Videos");
4322
5066
  if (res.downloaded > 0 || res.skipped > 0) {
4323
5067
  totalDownloaded += res.downloaded;
4324
5068
  totalSkipped += res.skipped;
4325
5069
  } else if (!options.signal?.aborted) {
4326
- onProgress?.({
5070
+ handleProgress({
4327
5071
  job: "Videos",
4328
5072
  downloadedCount: totalDownloaded,
4329
5073
  skippedCount: totalSkipped,
@@ -4333,9 +5077,9 @@ async function dispatchDownload(options) {
4333
5077
  if (fbRes.downloaded > 0 || fbRes.skipped > 0 || fbRes.success) {
4334
5078
  totalDownloaded += fbRes.downloaded;
4335
5079
  totalSkipped += fbRes.skipped;
4336
- if (!fbRes.success && fbRes.error) errors.push(`[Videos] ${fbRes.error}`);
5080
+ if (!fbRes.success) recordError(fbRes.error ? `[Videos] ${fbRes.error}` : "[Videos] Execution failed");
4337
5081
  } else if (!options.signal?.aborted) {
4338
- onProgress?.({
5082
+ handleProgress({
4339
5083
  job: "Videos",
4340
5084
  downloadedCount: totalDownloaded,
4341
5085
  skippedCount: totalSkipped,
@@ -4344,7 +5088,7 @@ async function dispatchDownload(options) {
4344
5088
  const gdlRes = await runGdl("Videos");
4345
5089
  totalDownloaded += gdlRes.downloaded;
4346
5090
  totalSkipped += gdlRes.skipped;
4347
- if (!gdlRes.success && gdlRes.error) errors.push(`[Videos] ${gdlRes.error}`);
5091
+ if (!gdlRes.success) recordError(gdlRes.error ? `[Videos] ${gdlRes.error}` : "[Videos] Execution failed");
4348
5092
  }
4349
5093
  }
4350
5094
  }
@@ -4353,13 +5097,19 @@ async function dispatchDownload(options) {
4353
5097
  if (errors.length === 0 && !options.signal?.aborted) {
4354
5098
  await clearCheckpoint(targetDir);
4355
5099
  }
4356
- return {
5100
+ const engineResult = {
4357
5101
  success: errors.length === 0 || totalDownloaded > 0,
4358
5102
  downloadedCount: totalDownloaded,
4359
5103
  skippedCount: totalSkipped,
4360
5104
  errors,
4361
5105
  outputDir: targetDir
4362
5106
  };
5107
+ defaultTaskManager.events.emit("job:complete", { result: engineResult });
5108
+ return engineResult;
5109
+ } catch (err) {
5110
+ const errMsg = err instanceof Error ? err.message : String(err);
5111
+ defaultTaskManager.events.emit("job:error", { error: errMsg || "Execution failed" });
5112
+ throw err;
4363
5113
  } finally {
4364
5114
  if (cookieCopy) {
4365
5115
  await cookieCopy.cleanup();
@@ -4559,7 +5309,7 @@ function ProbingView({
4559
5309
 
4560
5310
  // src/components/views/downloading-view.tsx
4561
5311
  import { useState as useState5, useEffect as useEffect5 } from "react";
4562
- import os6 from "os";
5312
+ import os8 from "os";
4563
5313
  import { Box as Box9, Text as Text11 } from "ink";
4564
5314
  import Spinner from "ink-spinner";
4565
5315
 
@@ -4695,35 +5445,51 @@ function DownloadingView({
4695
5445
  activeBadges.length > 0 ? /* @__PURE__ */ jsx12(Text11, { color: theme.gray, dimColor: theme.dimSecondary, children: activeBadges.join(" \xB7 ") }) : null,
4696
5446
  outputDir ? /* @__PURE__ */ jsxs9(Text11, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4697
5447
  "saving to ",
4698
- shortenPath(outputDir, os6.homedir(), 54)
5448
+ shortenPath(outputDir, os8.homedir(), 54)
4699
5449
  ] }) : null
4700
5450
  ] });
4701
5451
  }
4702
5452
 
4703
5453
  // src/components/views/completion-view.tsx
4704
- import os7 from "os";
5454
+ import os9 from "os";
4705
5455
  import { Box as Box10, Text as Text12 } from "ink";
4706
- import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
5456
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
4707
5457
  var DONE_LABEL = "\u21B5 download another";
5458
+ var LEDGER_INDEXED_LABEL = "\u2713 indexed in media ledger";
5459
+ function shouldShowLedgerBadge(result, showLedgerBadge) {
5460
+ return showLedgerBadge ?? result.downloadedCount > 0;
5461
+ }
5462
+ function formatCompletionSummary(result) {
5463
+ const isZero = result.downloadedCount === 0 && result.skippedCount === 0;
5464
+ const plural = result.downloadedCount === 1 ? "" : "s";
5465
+ const skipText = result.skippedCount > 0 ? ` (${result.skippedCount} archived)` : "";
5466
+ return {
5467
+ isZero,
5468
+ headerPrefix: isZero ? "\u2713 profile scanned " : "\u2713 profile downloaded! ",
5469
+ headerSuffix: isZero ? ": 0 media items found" : "saved to:",
5470
+ itemsText: isZero ? "no photos or videos available" : `${result.downloadedCount} media item${plural} downloaded${skipText}`
5471
+ };
5472
+ }
5473
+ function LedgerBadge({ theme }) {
5474
+ return /* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: LEDGER_INDEXED_LABEL });
5475
+ }
4708
5476
  function CompletionView({
4709
5477
  profile,
4710
5478
  result,
4711
5479
  theme,
4712
- gapComponent: Gap2
5480
+ gapComponent: Gap2,
5481
+ showLedgerBadge
4713
5482
  }) {
4714
- const isZero = result.downloadedCount === 0 && result.skippedCount === 0;
4715
- const plural = result.downloadedCount === 1 ? "" : "s";
4716
- const skipText = result.skippedCount > 0 ? ` (${result.skippedCount} archived)` : "";
5483
+ const { headerPrefix, headerSuffix, itemsText } = formatCompletionSummary(result);
5484
+ const displayLedger = shouldShowLedgerBadge(result, showLedgerBadge);
4717
5485
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", alignItems: "center", children: [
4718
- /* @__PURE__ */ jsx13(Text12, { children: isZero ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
4719
- /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "\u2713 profile scanned " }),
4720
- /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: "\u2014 0 media items found" })
4721
- ] }) : /* @__PURE__ */ jsxs10(Fragment2, { children: [
4722
- /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "\u2713 profile downloaded! " }),
4723
- /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: "saved to:" })
4724
- ] }) }),
4725
- /* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: shortenPath(result.outputDir, os7.homedir(), 60) }),
4726
- /* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: isZero ? "no photos or videos available" : `${result.downloadedCount} media item${plural} downloaded${skipText}` }),
5486
+ /* @__PURE__ */ jsxs10(Text12, { children: [
5487
+ /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: headerPrefix }),
5488
+ /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: headerSuffix })
5489
+ ] }),
5490
+ /* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: shortenPath(result.outputDir, os9.homedir(), 60) }),
5491
+ /* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: itemsText }),
5492
+ displayLedger && /* @__PURE__ */ jsx13(LedgerBadge, { theme }),
4727
5493
  /* @__PURE__ */ jsx13(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4728
5494
  /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "[o]" }),
4729
5495
  " reveal in folder"
@@ -4765,18 +5531,220 @@ function ErrorView({ message, hint, columns, theme }) {
4765
5531
  }
4766
5532
 
4767
5533
  // src/components/views/settings-view.tsx
4768
- import { useState as useState6, useMemo as useMemo2, useCallback } from "react";
5534
+ import { useState as useState6, useMemo as useMemo2, useCallback, useEffect as useEffect6 } from "react";
4769
5535
  import { Box as Box12, Text as Text14, useInput as useInput2 } from "ink";
4770
- import fs11 from "fs";
4771
- import path11 from "path";
4772
- import os8 from "os";
4773
- import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
5536
+ import fs15 from "fs";
5537
+ import path15 from "path";
5538
+ import os11 from "os";
5539
+
5540
+ // src/lib/diagnostics/doctor.ts
5541
+ import { execFile } from "child_process";
5542
+ import { promisify } from "util";
5543
+ import os10 from "os";
5544
+ import fs14 from "fs";
5545
+ import path14 from "path";
5546
+ var execFileAsync = promisify(execFile);
5547
+ async function defaultCheckBinary(bin) {
5548
+ const versionFlag = bin === "ffmpeg" ? "-version" : "--version";
5549
+ try {
5550
+ await execFileAsync(bin, [versionFlag], { timeout: 3e3 });
5551
+ return true;
5552
+ } catch {
5553
+ if (bin === "gallery-dl" || bin === "yt-dlp") {
5554
+ try {
5555
+ const resolved = resolveEngineCommand(bin);
5556
+ await execFileAsync(resolved.command, [...resolved.baseArgs, versionFlag], { timeout: 3e3 });
5557
+ return true;
5558
+ } catch {
5559
+ return false;
5560
+ }
5561
+ }
5562
+ return false;
5563
+ }
5564
+ }
5565
+ async function runDoctor(options = {}) {
5566
+ const check = options.binaryChecker ?? defaultCheckBinary;
5567
+ const [gdl, ytdlp, ffmpeg] = await Promise.all([
5568
+ check("gallery-dl"),
5569
+ check("yt-dlp"),
5570
+ check("ffmpeg")
5571
+ ]);
5572
+ const availableCookies = detectAvailableCookies(options.cookiesDir);
5573
+ const cookieStatus = {
5574
+ instagram: availableCookies.includes("instagram"),
5575
+ tiktok: availableCookies.includes("tiktok"),
5576
+ facebook: availableCookies.includes("facebook"),
5577
+ x: availableCookies.includes("x")
5578
+ };
5579
+ let storageOk = false;
5580
+ try {
5581
+ const testDir = options.storageDir || path14.join(os10.homedir(), ".open-nami");
5582
+ fs14.mkdirSync(testDir, { recursive: true });
5583
+ const testFile = path14.join(testDir, `.doctor_test_${Date.now()}_${Math.random().toString(36).slice(2)}`);
5584
+ fs14.writeFileSync(testFile, "ok");
5585
+ fs14.unlinkSync(testFile);
5586
+ storageOk = true;
5587
+ } catch {
5588
+ }
5589
+ return {
5590
+ nodeVersion: process.version,
5591
+ binaries: {
5592
+ "gallery-dl": gdl,
5593
+ "yt-dlp": ytdlp,
5594
+ ffmpeg
5595
+ },
5596
+ cookies: cookieStatus,
5597
+ storageOk
5598
+ };
5599
+ }
5600
+
5601
+ // src/components/views/settings-view.tsx
5602
+ import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
4774
5603
  function expandDir(dir) {
4775
5604
  if (dir.startsWith("~/") || dir.startsWith("~\\") || dir === "~") {
4776
- return path11.join(os8.homedir(), dir.slice(1));
5605
+ return path15.join(os11.homedir(), dir.slice(1));
4777
5606
  }
4778
5607
  return dir;
4779
5608
  }
5609
+ function resolveBrowserOptions(detected) {
5610
+ const available = detected.filter((b) => b.available !== false);
5611
+ return ["auto", "none", ...available.map((b) => b.id)];
5612
+ }
5613
+ function formatBrowserFeedback(cookiesFromBrowser, detectedBrowsers = [], defaultBrowser) {
5614
+ if (cookiesFromBrowser === "auto") {
5615
+ if (defaultBrowser) {
5616
+ return `system default: ${defaultBrowser.name} (auto-extract session cookies)`;
5617
+ }
5618
+ return "system default: none detected (falls back to cookie files)";
5619
+ }
5620
+ if (!cookiesFromBrowser || cookiesFromBrowser === "none") {
5621
+ return "manual cookie files only";
5622
+ }
5623
+ const found = detectedBrowsers.find((b) => b.id === cookiesFromBrowser);
5624
+ const browserName = found ? found.name : cookiesFromBrowser;
5625
+ return `live session cookies from ${browserName}`;
5626
+ }
5627
+ function cycleBrowserOption(current, options, forward = true) {
5628
+ if (options.length === 0) return "auto";
5629
+ const idx = options.indexOf(current);
5630
+ const currentIndex = idx === -1 ? 0 : idx;
5631
+ const nextIdx = forward ? (currentIndex + 1) % options.length : (currentIndex - 1 + options.length) % options.length;
5632
+ return options[nextIdx];
5633
+ }
5634
+ function formatDoctorStorage(storageOk) {
5635
+ return {
5636
+ ok: storageOk,
5637
+ text: storageOk ? "\u2713 SQLite ready" : "\u2717 SQLite unavailable"
5638
+ };
5639
+ }
5640
+ function formatDoctorCookies(cookies) {
5641
+ const platforms = Object.entries(cookies).filter(([_, ok]) => ok).map(([name]) => name);
5642
+ const count = platforms.length;
5643
+ const text = count > 0 ? `\u2713 ${count} (${platforms.join(", ")})` : "none detected";
5644
+ return { count, platforms, text };
5645
+ }
5646
+ function DoctorCard({ boxWidth, theme, report }) {
5647
+ const cookieSummary = report ? formatDoctorCookies(report.cookies) : { count: 0, platforms: [], text: "" };
5648
+ const storageSummary = report ? formatDoctorStorage(report.storageOk) : { ok: false, text: "" };
5649
+ return /* @__PURE__ */ jsxs12(
5650
+ Box12,
5651
+ {
5652
+ width: boxWidth,
5653
+ borderStyle: "round",
5654
+ borderColor: theme.gray,
5655
+ borderDimColor: theme.dimSecondary,
5656
+ paddingX: 2,
5657
+ flexDirection: "column",
5658
+ children: [
5659
+ /* @__PURE__ */ jsxs12(Box12, { justifyContent: "space-between", children: [
5660
+ /* @__PURE__ */ jsx15(Text14, { color: theme.primary, bold: true, children: "System Diagnostics" }),
5661
+ /* @__PURE__ */ jsx15(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: report ? report.nodeVersion : "checking\u2026" })
5662
+ ] }),
5663
+ report ? /* @__PURE__ */ jsxs12(Fragment2, { children: [
5664
+ /* @__PURE__ */ jsxs12(Box12, { marginTop: 0, children: [
5665
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5666
+ "Engines:",
5667
+ " "
5668
+ ] }),
5669
+ /* @__PURE__ */ jsxs12(
5670
+ Text14,
5671
+ {
5672
+ color: report.binaries["gallery-dl"] ? "green" : "red",
5673
+ dimColor: !report.binaries["gallery-dl"] && theme.dimSecondary,
5674
+ children: [
5675
+ report.binaries["gallery-dl"] ? "\u2713" : "\u2717",
5676
+ " gallery-dl"
5677
+ ]
5678
+ }
5679
+ ),
5680
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5681
+ " ",
5682
+ "\xB7",
5683
+ " "
5684
+ ] }),
5685
+ /* @__PURE__ */ jsxs12(
5686
+ Text14,
5687
+ {
5688
+ color: report.binaries["yt-dlp"] ? "green" : "red",
5689
+ dimColor: !report.binaries["yt-dlp"] && theme.dimSecondary,
5690
+ children: [
5691
+ report.binaries["yt-dlp"] ? "\u2713" : "\u2717",
5692
+ " yt-dlp"
5693
+ ]
5694
+ }
5695
+ ),
5696
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5697
+ " ",
5698
+ "\xB7",
5699
+ " "
5700
+ ] }),
5701
+ /* @__PURE__ */ jsxs12(
5702
+ Text14,
5703
+ {
5704
+ color: report.binaries.ffmpeg ? "green" : "red",
5705
+ dimColor: !report.binaries.ffmpeg && theme.dimSecondary,
5706
+ children: [
5707
+ report.binaries.ffmpeg ? "\u2713" : "\u2717",
5708
+ " ffmpeg"
5709
+ ]
5710
+ }
5711
+ )
5712
+ ] }),
5713
+ /* @__PURE__ */ jsxs12(Box12, { justifyContent: "space-between", marginTop: 0, children: [
5714
+ /* @__PURE__ */ jsxs12(Box12, { children: [
5715
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5716
+ "Storage:",
5717
+ " "
5718
+ ] }),
5719
+ /* @__PURE__ */ jsx15(
5720
+ Text14,
5721
+ {
5722
+ color: storageSummary.ok ? "green" : "red",
5723
+ dimColor: !storageSummary.ok && theme.dimSecondary,
5724
+ children: storageSummary.text
5725
+ }
5726
+ )
5727
+ ] }),
5728
+ /* @__PURE__ */ jsxs12(Box12, { children: [
5729
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5730
+ "Cookies:",
5731
+ " "
5732
+ ] }),
5733
+ /* @__PURE__ */ jsx15(
5734
+ Text14,
5735
+ {
5736
+ color: cookieSummary.count > 0 ? theme.primary : theme.gray,
5737
+ dimColor: cookieSummary.count === 0 && theme.dimSecondary,
5738
+ children: cookieSummary.text
5739
+ }
5740
+ )
5741
+ ] })
5742
+ ] })
5743
+ ] }) : /* @__PURE__ */ jsx15(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: "checking system engines and storage\u2026" })
5744
+ ]
5745
+ }
5746
+ );
5747
+ }
4780
5748
  var LIMIT_OPTIONS = [void 0, 10, 25, 50, 100];
4781
5749
  function SettingsView({
4782
5750
  boxWidth,
@@ -4785,9 +5753,14 @@ function SettingsView({
4785
5753
  focusedField: controlledFocus,
4786
5754
  onFocusChange,
4787
5755
  onSave,
4788
- onCancel
5756
+ onCancel,
5757
+ doctorChecker,
5758
+ initialReport,
5759
+ browsers: browsersProp,
5760
+ defaultBrowser: defaultBrowserProp,
5761
+ initialConfig: initialConfigProp
4789
5762
  }) {
4790
- const initialConfig = useMemo2(() => loadConfig(), []);
5763
+ const initialConfig = useMemo2(() => initialConfigProp ?? loadConfig(), [initialConfigProp]);
4791
5764
  const [internalFocus, setInternalFocus] = useState6("cookies");
4792
5765
  const focusedField = controlledFocus ?? internalFocus;
4793
5766
  const setField = useCallback(
@@ -4797,16 +5770,64 @@ function SettingsView({
4797
5770
  },
4798
5771
  [onFocusChange]
4799
5772
  );
5773
+ const defaultBrowser = useMemo2(() => {
5774
+ return defaultBrowserProp !== void 0 ? defaultBrowserProp : detectSystemDefaultBrowser();
5775
+ }, [defaultBrowserProp]);
5776
+ const detectedBrowsers = useMemo2(() => {
5777
+ return browsersProp ?? listDetectedBrowsers();
5778
+ }, [browsersProp]);
5779
+ const browserOptions = useMemo2(() => {
5780
+ return resolveBrowserOptions(detectedBrowsers);
5781
+ }, [detectedBrowsers]);
4800
5782
  const [cookiesDir, setCookiesDir] = useState6(initialConfig.cookiesDir ?? "");
5783
+ const [cookiesFromBrowser, setCookiesFromBrowser] = useState6(
5784
+ initialConfig.cookiesFromBrowser ?? "auto"
5785
+ );
4801
5786
  const [downloadDir, setDownloadDir] = useState6(initialConfig.downloadDir ?? "");
4802
5787
  const [writeMetadata, setWriteMetadata] = useState6(Boolean(initialConfig.writeMetadata));
4803
5788
  const [defaultLimit, setDefaultLimit] = useState6(initialConfig.defaultLimit);
4804
5789
  const [saved, setSaved] = useState6(false);
5790
+ const [doctorReport, setDoctorReport] = useState6(initialReport ?? null);
5791
+ useEffect6(() => {
5792
+ let cancelled = false;
5793
+ const fetchReport = async () => {
5794
+ try {
5795
+ const options = {
5796
+ cookiesDir: cookiesDir.trim() ? expandDir(cookiesDir.trim()) : void 0
5797
+ };
5798
+ let report;
5799
+ if (doctorChecker) {
5800
+ const res = await doctorChecker(options);
5801
+ if (res && typeof res === "object" && "binaries" in res) {
5802
+ report = res;
5803
+ } else {
5804
+ report = await runDoctor({
5805
+ ...options,
5806
+ binaryChecker: doctorChecker
5807
+ });
5808
+ }
5809
+ } else {
5810
+ report = await runDoctor(options);
5811
+ }
5812
+ if (!cancelled) {
5813
+ setDoctorReport(report);
5814
+ }
5815
+ } catch {
5816
+ }
5817
+ };
5818
+ if (initialReport === void 0 || doctorChecker !== void 0) {
5819
+ fetchReport();
5820
+ }
5821
+ return () => {
5822
+ cancelled = true;
5823
+ };
5824
+ }, [cookiesDir, doctorChecker, initialReport]);
4805
5825
  const handleSave = useCallback(() => {
4806
5826
  const existing = loadConfig();
4807
5827
  const nextConfig = {
4808
5828
  ...existing,
4809
5829
  cookiesDir: cookiesDir.trim() ? cookiesDir.trim() : void 0,
5830
+ cookiesFromBrowser,
4810
5831
  downloadDir: downloadDir.trim() ? downloadDir.trim() : void 0,
4811
5832
  writeMetadata,
4812
5833
  defaultLimit
@@ -4814,7 +5835,10 @@ function SettingsView({
4814
5835
  saveConfig(nextConfig);
4815
5836
  setSaved(true);
4816
5837
  onSave();
4817
- }, [cookiesDir, downloadDir, writeMetadata, defaultLimit, onSave]);
5838
+ }, [cookiesDir, cookiesFromBrowser, downloadDir, writeMetadata, defaultLimit, onSave]);
5839
+ const browserFeedback = useMemo2(() => {
5840
+ return formatBrowserFeedback(cookiesFromBrowser, detectedBrowsers, defaultBrowser);
5841
+ }, [cookiesFromBrowser, detectedBrowsers, defaultBrowser]);
4818
5842
  const cookieFeedback = useMemo2(() => {
4819
5843
  const dir = cookiesDir.trim();
4820
5844
  if (!dir) {
@@ -4825,7 +5849,7 @@ function SettingsView({
4825
5849
  };
4826
5850
  }
4827
5851
  const expanded = expandDir(dir);
4828
- if (!fs11.existsSync(expanded)) {
5852
+ if (!fs15.existsSync(expanded)) {
4829
5853
  return {
4830
5854
  ok: false,
4831
5855
  text: "directory does not exist yet"
@@ -4848,14 +5872,20 @@ function SettingsView({
4848
5872
  if (!dir) {
4849
5873
  return "default: platform Downloads / Open Nami";
4850
5874
  }
4851
- return `target: ${path11.resolve(expandDir(dir))}`;
5875
+ return `target: ${path15.resolve(expandDir(dir))}`;
4852
5876
  }, [downloadDir]);
4853
5877
  const cycleFocus = useCallback((forward = true) => {
4854
- const fields = ["cookies", "download", "metadata", "limit"];
5878
+ const fields = ["cookies", "browser", "download", "metadata", "limit"];
4855
5879
  const idx = fields.indexOf(focusedField);
4856
5880
  const nextIdx = forward ? (idx + 1) % fields.length : (idx - 1 + fields.length) % fields.length;
4857
5881
  setField(fields[nextIdx]);
4858
5882
  }, [focusedField, setField]);
5883
+ const cycleBrowser = useCallback((forward = true) => {
5884
+ setCookiesFromBrowser((prev) => {
5885
+ const next = cycleBrowserOption(prev ?? "auto", browserOptions, forward);
5886
+ return next;
5887
+ });
5888
+ }, [browserOptions]);
4859
5889
  const cycleLimit = useCallback(() => {
4860
5890
  setDefaultLimit((prev) => {
4861
5891
  const idx = LIMIT_OPTIONS.indexOf(prev);
@@ -4872,6 +5902,20 @@ function SettingsView({
4872
5902
  handleSave();
4873
5903
  return;
4874
5904
  }
5905
+ if (focusedField === "browser") {
5906
+ if (input === " " || key.return) {
5907
+ cycleBrowser(true);
5908
+ return;
5909
+ }
5910
+ if (key.tab || key.downArrow) {
5911
+ cycleFocus(true);
5912
+ return;
5913
+ }
5914
+ if (key.upArrow) {
5915
+ cycleFocus(false);
5916
+ return;
5917
+ }
5918
+ }
4875
5919
  if (focusedField === "metadata") {
4876
5920
  if (input === " " || key.return) {
4877
5921
  setWriteMetadata((prev) => !prev);
@@ -4912,7 +5956,7 @@ function SettingsView({
4912
5956
  {
4913
5957
  value: cookiesDir,
4914
5958
  onChange: setCookiesDir,
4915
- onSubmit: () => setField("download"),
5959
+ onSubmit: () => setField("browser"),
4916
5960
  focus: focusedField === "cookies",
4917
5961
  onTab: () => cycleFocus(true),
4918
5962
  onArrowDown: () => cycleFocus(true),
@@ -4925,6 +5969,26 @@ function SettingsView({
4925
5969
  ),
4926
5970
  /* @__PURE__ */ jsx15(Text14, { color: cookieFeedback.ok ? theme.primary : theme.gray, dimColor: !cookieFeedback.ok && theme.dimSecondary, children: cookieFeedback.text }),
4927
5971
  /* @__PURE__ */ jsx15(Gap2, {}),
5972
+ /* @__PURE__ */ jsxs12(
5973
+ Box12,
5974
+ {
5975
+ width: boxWidth,
5976
+ borderStyle: "round",
5977
+ borderColor: focusedField === "browser" ? theme.primary : theme.gray,
5978
+ borderDimColor: focusedField !== "browser",
5979
+ paddingX: 2,
5980
+ justifyContent: "space-between",
5981
+ children: [
5982
+ /* @__PURE__ */ jsxs12(Text14, { color: focusedField === "browser" ? theme.primary : theme.gray, bold: focusedField === "browser", children: [
5983
+ focusedField === "browser" ? "\u25CF " : "\u25CB ",
5984
+ "Browser Cookie Bridge"
5985
+ ] }),
5986
+ /* @__PURE__ */ jsx15(Text14, { color: cookiesFromBrowser && cookiesFromBrowser !== "none" ? theme.primary : theme.gray, bold: true, children: `[${(cookiesFromBrowser ?? "auto").toUpperCase()}]` })
5987
+ ]
5988
+ }
5989
+ ),
5990
+ /* @__PURE__ */ jsx15(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: browserFeedback }),
5991
+ /* @__PURE__ */ jsx15(Gap2, {}),
4928
5992
  /* @__PURE__ */ jsx15(
4929
5993
  FramedInput,
4930
5994
  {
@@ -4986,12 +6050,14 @@ function SettingsView({
4986
6050
  }
4987
6051
  ),
4988
6052
  /* @__PURE__ */ jsx15(Gap2, {}),
6053
+ /* @__PURE__ */ jsx15(DoctorCard, { boxWidth, theme, report: doctorReport }),
6054
+ /* @__PURE__ */ jsx15(Gap2, {}),
4989
6055
  /* @__PURE__ */ jsx15(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: saved ? "\u2713 preferences saved to ~/.open-nami/config.json" : "tab / \u2191\u2193 switch field \xB7 space toggle/cycle \xB7 ^s / \u21B5 save \xB7 esc back" })
4990
6056
  ] });
4991
6057
  }
4992
6058
 
4993
6059
  // src/components/views/watcher-view.tsx
4994
- import { useState as useState7, useEffect as useEffect6, useRef as useRef3, useCallback as useCallback2 } from "react";
6060
+ import { useState as useState7, useEffect as useEffect7, useRef as useRef3, useCallback as useCallback2 } from "react";
4995
6061
  import { Box as Box13, Text as Text15, useInput as useInput3 } from "ink";
4996
6062
  import Spinner2 from "ink-spinner";
4997
6063
 
@@ -5278,9 +6344,9 @@ function startClipboardWatcher(options = {}) {
5278
6344
  }
5279
6345
 
5280
6346
  // src/lib/reveal.ts
5281
- import fs12 from "fs";
6347
+ import fs16 from "fs";
5282
6348
  import { spawn as spawn4 } from "child_process";
5283
- import path12 from "path";
6349
+ import path16 from "path";
5284
6350
  function buildOpenBrowserCommand(url, platform = process.platform) {
5285
6351
  try {
5286
6352
  const parsed = new URL(url.trim());
@@ -5307,12 +6373,12 @@ function openBrowser(url) {
5307
6373
  }
5308
6374
  }
5309
6375
  function getRevealCommand(targetPath, platform = process.platform, isDirectory) {
5310
- const pathModule = platform === "win32" ? path12.win32 : platform === "darwin" ? path12.posix : path12;
6376
+ const pathModule = platform === "win32" ? path16.win32 : platform === "darwin" ? path16.posix : path16;
5311
6377
  const resolved = pathModule.resolve(targetPath);
5312
6378
  const isDir = isDirectory !== void 0 ? isDirectory : (() => {
5313
6379
  try {
5314
- if (fs12.existsSync(resolved)) {
5315
- return fs12.statSync(resolved).isDirectory();
6380
+ if (fs16.existsSync(resolved)) {
6381
+ return fs16.statSync(resolved).isDirectory();
5316
6382
  }
5317
6383
  } catch {
5318
6384
  }
@@ -5360,13 +6426,13 @@ function WatcherView({
5360
6426
  mediaChoiceRef.current = mediaChoice;
5361
6427
  const writeMetadataRef = useRef3(writeMetadata);
5362
6428
  writeMetadataRef.current = writeMetadata;
5363
- useEffect6(() => {
6429
+ useEffect7(() => {
5364
6430
  const pulseTimer = setInterval(() => {
5365
6431
  setPulseTick((t) => t + 1);
5366
6432
  }, 120);
5367
6433
  return () => clearInterval(pulseTimer);
5368
6434
  }, []);
5369
- useEffect6(() => {
6435
+ useEffect7(() => {
5370
6436
  const watcher = startClipboardWatcher({
5371
6437
  choice: mediaChoiceRef.current,
5372
6438
  getChoice: () => mediaChoiceRef.current,
@@ -5440,7 +6506,7 @@ function WatcherView({
5440
6506
  const target = outputDir || resolveDefaultDownloadDir("instagram", "watcher");
5441
6507
  revealInFileManager(target);
5442
6508
  }, [outputDir]);
5443
- useEffect6(() => {
6509
+ useEffect7(() => {
5444
6510
  if (!onRegisterClickTargets) return;
5445
6511
  const targets = [
5446
6512
  { match: "Target:", padX: 1, action: () => {
@@ -5606,10 +6672,10 @@ function WatcherView({
5606
6672
  }
5607
6673
 
5608
6674
  // src/components/views/batch-view.tsx
5609
- import { useState as useState8, useRef as useRef4, useCallback as useCallback3, useEffect as useEffect7 } from "react";
6675
+ import { useState as useState8, useRef as useRef4, useCallback as useCallback3, useEffect as useEffect8 } from "react";
5610
6676
  import { Box as Box14, Text as Text16, useInput as useInput4 } from "ink";
5611
6677
  import Spinner3 from "ink-spinner";
5612
- import fs13 from "fs";
6678
+ import fs17 from "fs";
5613
6679
 
5614
6680
  // src/lib/batch.ts
5615
6681
  import fsPromises5 from "fs/promises";
@@ -5816,7 +6882,7 @@ function BatchView({
5816
6882
  setErrorMessage("Please enter a valid batch file path");
5817
6883
  return;
5818
6884
  }
5819
- if (!fs13.existsSync(trimmed)) {
6885
+ if (!fs17.existsSync(trimmed)) {
5820
6886
  setErrorMessage(`File not found: ${trimmed}`);
5821
6887
  return;
5822
6888
  }
@@ -6148,7 +7214,7 @@ function BatchView({
6148
7214
  }
6149
7215
  }
6150
7216
  }, { isActive: Boolean(process.stdin.isTTY) });
6151
- useEffect7(() => {
7217
+ useEffect8(() => {
6152
7218
  if (!onRegisterClickTargets) return;
6153
7219
  const targets = [];
6154
7220
  if (phase === "input") {
@@ -6649,6 +7715,80 @@ function frameRowSpan(row) {
6649
7715
  return [first + 1, line.trimEnd().length];
6650
7716
  }
6651
7717
 
7718
+ // src/lib/engines/core/use-engine-events.ts
7719
+ import { useState as useState9, useEffect as useEffect9 } from "react";
7720
+ var INITIAL_ENGINE_EVENT_STATE = {
7721
+ isDownloading: false,
7722
+ activeJob: void 0,
7723
+ progress: void 0,
7724
+ lastError: void 0,
7725
+ lastResult: void 0
7726
+ };
7727
+ function subscribeToEngineEvents(bus, onChange) {
7728
+ let state = { ...INITIAL_ENGINE_EVENT_STATE };
7729
+ const onJobStart = (payload) => {
7730
+ state = {
7731
+ ...state,
7732
+ isDownloading: true,
7733
+ activeJob: payload,
7734
+ progress: void 0,
7735
+ lastError: void 0,
7736
+ lastResult: void 0
7737
+ };
7738
+ onChange(state);
7739
+ };
7740
+ const onJobProgress = (payload) => {
7741
+ state = {
7742
+ ...state,
7743
+ isDownloading: true,
7744
+ progress: payload
7745
+ };
7746
+ onChange(state);
7747
+ };
7748
+ const onJobComplete = (payload) => {
7749
+ state = {
7750
+ ...state,
7751
+ isDownloading: false,
7752
+ lastResult: payload.result
7753
+ };
7754
+ onChange(state);
7755
+ };
7756
+ const onJobError = (payload) => {
7757
+ state = {
7758
+ ...state,
7759
+ isDownloading: false,
7760
+ lastError: payload.error
7761
+ };
7762
+ onChange(state);
7763
+ };
7764
+ const unsubStart = bus.on("job:start", onJobStart);
7765
+ const unsubProgress = bus.on("job:progress", onJobProgress);
7766
+ const unsubComplete = bus.on("job:complete", onJobComplete);
7767
+ const unsubError = bus.on("job:error", onJobError);
7768
+ return () => {
7769
+ if (typeof bus.off === "function") {
7770
+ bus.off("job:start", onJobStart);
7771
+ bus.off("job:progress", onJobProgress);
7772
+ bus.off("job:complete", onJobComplete);
7773
+ bus.off("job:error", onJobError);
7774
+ }
7775
+ unsubStart?.();
7776
+ unsubProgress?.();
7777
+ unsubComplete?.();
7778
+ unsubError?.();
7779
+ };
7780
+ }
7781
+ function useEngineEvents(customBus) {
7782
+ const bus = customBus || defaultTaskManager.events;
7783
+ const [state, setState] = useState9(() => ({
7784
+ ...INITIAL_ENGINE_EVENT_STATE
7785
+ }));
7786
+ useEffect9(() => {
7787
+ return subscribeToEngineEvents(bus, setState);
7788
+ }, [bus]);
7789
+ return state;
7790
+ }
7791
+
6652
7792
  // src/app.tsx
6653
7793
  import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
6654
7794
  var TAGLINE = "grab any profile. paste. download. done.";
@@ -6728,7 +7868,7 @@ function App({
6728
7868
  writeMetadata,
6729
7869
  version = "1.0.0"
6730
7870
  }) {
6731
- const [themeMode, setThemeMode] = useState9(initialThemeMode);
7871
+ const [themeMode, setThemeMode] = useState10(initialThemeMode);
6732
7872
  const cycleTheme = useCallback4(() => {
6733
7873
  setThemeMode(nextThemeMode);
6734
7874
  }, []);
@@ -6763,18 +7903,19 @@ function InnerApp({
6763
7903
  const theme = useTheme();
6764
7904
  const { exit } = useApp();
6765
7905
  const { stdout } = useStdout3();
7906
+ const engineEvents = useEngineEvents();
6766
7907
  const config = useMemo3(() => loadConfig(), []);
6767
7908
  const effectiveLimit = limit ?? config.defaultLimit;
6768
7909
  const effectiveWriteMetadata = writeMetadata ?? Boolean(config.writeMetadata);
6769
- const [urlInput, setUrlInput] = useState9(initialInput ?? "");
6770
- const [clipboardUrl, setClipboardUrl] = useState9(void 0);
6771
- const [settingsFocus, setSettingsFocus] = useState9("cookies");
6772
- const [batchClickTargets, setBatchClickTargets] = useState9([]);
6773
- const [watcherClickTargets, setWatcherClickTargets] = useState9([]);
7910
+ const [urlInput, setUrlInput] = useState10(initialInput ?? "");
7911
+ const [clipboardUrl, setClipboardUrl] = useState10(void 0);
7912
+ const [settingsFocus, setSettingsFocus] = useState10("cookies");
7913
+ const [batchClickTargets, setBatchClickTargets] = useState10([]);
7914
+ const [watcherClickTargets, setWatcherClickTargets] = useState10([]);
6774
7915
  const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
6775
7916
  const boxWidth = Math.max(14, Math.min(64, columns - 6));
6776
7917
  const contentWidth = Math.max(10, Math.min(columns - 4, 78));
6777
- const [phase, setPhase] = useState9(() => {
7918
+ const [phase, setPhase] = useState10(() => {
6778
7919
  if (initialInput) {
6779
7920
  const parsed = parseProfileInput(initialInput);
6780
7921
  if (parsed.platform !== "unknown" && parsed.cleanUrl) {
@@ -6786,7 +7927,7 @@ function InnerApp({
6786
7927
  }
6787
7928
  return { name: "input" };
6788
7929
  });
6789
- useEffect8(() => {
7930
+ useEffect10(() => {
6790
7931
  try {
6791
7932
  const clip = readClipboard();
6792
7933
  if (clip && (clip.startsWith("http://") || clip.startsWith("https://") || clip.startsWith("@"))) {
@@ -6799,7 +7940,7 @@ function InnerApp({
6799
7940
  const clipboardAccepted = Boolean(clipboardUrl) && urlInput === clipboardUrl;
6800
7941
  const abortControllerRef = useRef5(null);
6801
7942
  const probingTimeoutRef = useRef5(null);
6802
- useEffect8(() => {
7943
+ useEffect10(() => {
6803
7944
  return () => {
6804
7945
  abortControllerRef.current?.abort();
6805
7946
  if (probingTimeoutRef.current) {
@@ -7159,7 +8300,7 @@ function InnerApp({
7159
8300
  {
7160
8301
  profile: phase.profile,
7161
8302
  choice: phase.choice,
7162
- progress: phase.progress,
8303
+ progress: engineEvents.progress ?? phase.progress,
7163
8304
  outputDir: resolveDefaultDownloadDir(phase.profile.platform, phase.profile.username, outputDir),
7164
8305
  writeMetadata: effectiveWriteMetadata,
7165
8306
  limit: effectiveLimit,
@@ -7439,7 +8580,7 @@ import { jsx as jsx19 } from "react/jsx-runtime";
7439
8580
  var resolveVersion = () => {
7440
8581
  try {
7441
8582
  if (typeof import.meta !== "undefined" && import.meta?.url) {
7442
- return createRequire(import.meta.url)("../package.json").version;
8583
+ return createRequire2(import.meta.url)("../package.json").version;
7443
8584
  }
7444
8585
  } catch {
7445
8586
  }
@@ -7463,6 +8604,7 @@ var HELP = `
7463
8604
  Options:
7464
8605
  -h, --help Show this help message
7465
8606
  -v, --version Show version
8607
+ --doctor Run environment & dependency diagnostics
7466
8608
  -o, --output Set custom output directory
7467
8609
  -w, --watch Watch clipboard for profile links and auto-download
7468
8610
  -b, --batch <file> Download multiple profiles listed in a text file (one URL/user per line)
@@ -7509,6 +8651,24 @@ async function main() {
7509
8651
  console.log(`open-nami v${VERSION}`);
7510
8652
  process.exit(0);
7511
8653
  }
8654
+ if (args.includes("--doctor")) {
8655
+ const report = await runDoctor();
8656
+ console.log(`
8657
+ \x1B[36mOpen Nami Doctor\x1B[0m (v${VERSION})
8658
+ `);
8659
+ console.log(` \x1B[1mRuntime:\x1B[0m Node.js ${report.nodeVersion}`);
8660
+ console.log(` \x1B[1mStorage:\x1B[0m ${report.storageOk ? "\x1B[32m\u2713 Ready\x1B[0m" : "\x1B[31m\u2717 Inaccessible\x1B[0m"}`);
8661
+ console.log(` \x1B[1mEngines:\x1B[0m`);
8662
+ for (const [bin, ok] of Object.entries(report.binaries)) {
8663
+ console.log(` ${ok ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m"} ${bin}`);
8664
+ }
8665
+ console.log(` \x1B[1mCookies:\x1B[0m`);
8666
+ for (const [platform, ok] of Object.entries(report.cookies)) {
8667
+ console.log(` ${ok ? "\x1B[32m\u2713\x1B[0m" : "\x1B[90m\u25CB\x1B[0m"} ${platform}`);
8668
+ }
8669
+ console.log();
8670
+ process.exit(0);
8671
+ }
7512
8672
  const completionIdx = args.findIndex((arg) => arg === "--completion" || arg.startsWith("--completion="));
7513
8673
  if (completionIdx !== -1) {
7514
8674
  const arg = args[completionIdx];