rechrome 1.31.4 → 1.31.5

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 (3) hide show
  1. package/package.json +1 -1
  2. package/rechrome.js +132 -9
  3. package/rechrome.ts +132 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.31.4",
3
+ "version": "1.31.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/rechrome.js CHANGED
@@ -942,16 +942,135 @@ export function buildProfileRows(cache: Record<string, ChromeProfileInfo> | null
942
942
  return rows;
943
943
  }
944
944
 
945
- /** PIDs of browser processes (not helpers) running on a Chrome user-data dir. POSIX only. */
946
- function browsersUsing(userDataDir: string): number[] {
947
- if (process.platform === "win32") return [];
945
+ /**
946
+ * Split a Windows command line into arguments the way the C runtime does (CommandLineToArgvW
947
+ * rules): whitespace separates outside quotes; quotes toggle and may sit mid-argument
948
+ * (`--x="C:\a"b` is `--x=C:\ab`); 2n backslashes before a quote are n backslashes and the quote
949
+ * toggles, 2n+1 are n and a literal quote; `""` inside quotes is a literal quote.
950
+ */
951
+ export function splitWindowsCommandLine(commandLine: string): string[] {
952
+ const args: string[] = [];
953
+ let current = "", quoted = false, started = false;
954
+ for (let i = 0; i < commandLine.length; i++) {
955
+ const c = commandLine[i];
956
+ if (c === "\\") {
957
+ let n = 0;
958
+ while (commandLine[i] === "\\") { n++; i++; }
959
+ if (commandLine[i] === '"') {
960
+ current += "\\".repeat(n >> 1);
961
+ if (n % 2) current += '"'; else quoted = !quoted;
962
+ } else { current += "\\".repeat(n); i--; }
963
+ started = true;
964
+ } else if (c === '"') {
965
+ if (quoted && commandLine[i + 1] === '"') { current += '"'; i++; } else quoted = !quoted;
966
+ started = true;
967
+ } else if (!quoted && (c === " " || c === "\t")) {
968
+ if (started) { args.push(current); current = ""; started = false; }
969
+ } else { current += c; started = true; }
970
+ }
971
+ if (started) args.push(current);
972
+ return args;
973
+ }
974
+
975
+ /** The `--user-data-dir` value among exact arguments, or null. */
976
+ export function userDataDirArg(args: string[]): string | null {
977
+ const arg = args.find(a => a.startsWith("--user-data-dir="));
978
+ return arg === undefined ? null : arg.slice("--user-data-dir=".length);
979
+ }
980
+
981
+ /** Same folder: exact path, ignoring trailing separators (and case on Windows). */
982
+ export function sameDataDir(a: string, b: string, windows = process.platform === "win32"): boolean {
983
+ const norm = (p: string) => { const t = p.replace(/[\\/]+$/, ""); return windows ? t.replaceAll("/", "\\").toLowerCase() : t; };
984
+ return norm(a) === norm(b);
985
+ }
986
+
987
+ /**
988
+ * Match a flattened POSIX `ps` command line (no quotes, argument boundaries lost) against a
989
+ * user-data dir: "exact" only when the path is the last thing on the line; "ambiguous" when
990
+ * more text follows (another flag, or a sibling folder like `qa -backup`); else "none".
991
+ */
992
+ export function flatUserDataDirMatch(command: string, userDataDir: string): "exact" | "ambiguous" | "none" {
993
+ const flag = `--user-data-dir=${userDataDir.replace(/\/+$/, "")}`;
994
+ const at = command.indexOf(flag);
995
+ if (at < 0 || (at > 0 && !/\s/.test(command[at - 1]))) return "none";
996
+ const rest = command.slice(at + flag.length);
997
+ if (/^\/*\s*$/.test(rest)) return "exact";
998
+ return /^\/*\s/.test(rest) ? "ambiguous" : "none"; // `qa2` / `qa-x` are other folders
999
+ }
1000
+
1001
+ /**
1002
+ * Browser processes (not helpers) on this Chrome user-data dir. `exact`: certainly this folder
1003
+ * (Windows command lines are split with the C runtime's rules; Linux reads argv from /proc).
1004
+ * `ambiguous`: flattened `ps` output that may name a sibling folder — never killed.
1005
+ */
1006
+ function browsersUsing(userDataDir: string): { exact: number[]; ambiguous: number[] } {
1007
+ const exact: number[] = [], ambiguous: number[] = [];
1008
+ const isHelper = (args: string[]) => args.some(a => a.startsWith("--type="));
1009
+ if (process.platform === "win32") {
1010
+ const out = Bun.spawnSync(["powershell", "-NoProfile", "-NonInteractive", "-Command",
1011
+ "Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId)`t$($_.CommandLine)\" }"], { windowsHide: true }).stdout.toString();
1012
+ for (const line of out.split(/\r?\n/)) {
1013
+ const tab = line.indexOf("\t"), pid = Number(line.slice(0, tab));
1014
+ if (!pid || pid === process.pid) continue;
1015
+ const args = splitWindowsCommandLine(line.slice(tab + 1));
1016
+ const dir = userDataDirArg(args);
1017
+ if (dir !== null && !isHelper(args) && sameDataDir(dir, userDataDir)) exact.push(pid);
1018
+ }
1019
+ return { exact, ambiguous };
1020
+ }
948
1021
  const ps = Bun.spawnSync(["ps", "ax", "-o", "pid=,command="]).stdout.toString();
949
- return ps.split("\n").filter(line => line.includes(`--user-data-dir=${userDataDir}`) && !line.includes("--type="))
950
- .map(line => Number(line.trim().split(/\s+/)[0])).filter(Boolean);
1022
+ for (const line of ps.split("\n")) {
1023
+ const m = /^\s*(\d+)\s+(.*)$/.exec(line);
1024
+ if (!m || Number(m[1]) === process.pid || !m[2].includes("--user-data-dir=")) continue;
1025
+ const pid = Number(m[1]);
1026
+ let argv: string[] | null = null;
1027
+ try { argv = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean); } catch { /* not Linux, or gone */ }
1028
+ if (argv) {
1029
+ const dir = userDataDirArg(argv);
1030
+ if (dir !== null && !isHelper(argv) && sameDataDir(dir, userDataDir, false)) exact.push(pid);
1031
+ continue;
1032
+ }
1033
+ if (/(?:^|\s)--type=/.test(m[2])) continue;
1034
+ const match = flatUserDataDirMatch(m[2], userDataDir);
1035
+ if (match === "exact") exact.push(pid);
1036
+ else if (match === "ambiguous") ambiguous.push(pid);
1037
+ }
1038
+ return { exact, ambiguous };
951
1039
  }
952
1040
 
953
1041
  /** Move a folder to the user's Trash (recoverable). Returns where it went, or null if unsupported here. */
954
1042
  function moveToTrash(dir: string): string | null {
1043
+ // An explicit trash folder (tests use it to keep the real Trash / Recycle Bin clean).
1044
+ const override = process.env.RECH_TRASH_DIR;
1045
+ if (override) {
1046
+ mkdirSync(override, { recursive: true });
1047
+ let dest = join(override, basename(dir));
1048
+ if (existsSync(dest)) dest = `${dest} ${new Date().toISOString().replace(/[:.]/g, "-")}`;
1049
+ renameSync(dir, dest);
1050
+ return dest;
1051
+ }
1052
+ if (process.platform === "win32") {
1053
+ // SHFileOperation with ALLOWUNDO sends it to the Recycle Bin, with no prompts or progress UI.
1054
+ // WANTNUKEWARNING makes Windows ask rather than silently delete for good when the item
1055
+ // cannot be recycled. (.NET's FileSystem.DeleteDirectory(..., SendToRecycleBin) deleted
1056
+ // permanently here.) The path goes through the environment so quoting can't break it.
1057
+ const source = [
1058
+ "using System; using System.Runtime.InteropServices;",
1059
+ "public static class RechRecycle {",
1060
+ " [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]",
1061
+ " struct Op { public IntPtr hwnd; public uint wFunc; public string pFrom; public string pTo; public ushort fFlags; public bool aborted; public IntPtr mappings; public string title; }",
1062
+ " [DllImport(\"shell32.dll\", CharSet = CharSet.Unicode)] static extern int SHFileOperation(ref Op op);",
1063
+ " public static int Recycle(string path) {",
1064
+ " var op = new Op { wFunc = 3, pFrom = path + \"\\0\\0\", fFlags = 0x0040 | 0x0010 | 0x0004 | 0x0400 | 0x4000 };",
1065
+ " int r = SHFileOperation(ref op); return op.aborted ? -1 : r;",
1066
+ " }",
1067
+ "}",
1068
+ ].join("\n");
1069
+ const r = Bun.spawnSync(["powershell", "-NoProfile", "-NonInteractive", "-Command",
1070
+ "Add-Type -TypeDefinition $env:RECH_RECYCLE_SOURCE; exit [RechRecycle]::Recycle($env:RECH_RECYCLE_PATH)"],
1071
+ { env: { ...process.env, RECH_RECYCLE_SOURCE: source, RECH_RECYCLE_PATH: dir.replaceAll("/", "\\").replace(/\\+$/, "") }, windowsHide: true }); // SHFileOperation rejects `/` and a trailing `\`
1072
+ return r.exitCode === 0 && !existsSync(dir) ? "the Recycle Bin" : null;
1073
+ }
955
1074
  const trash = process.platform === "darwin" ? join(HOME, ".Trash") : process.platform === "linux" ? join(HOME, ".local", "share", "Trash", "files") : null;
956
1075
  if (!trash) return null;
957
1076
  mkdirSync(trash, { recursive: true });
@@ -972,13 +1091,17 @@ async function removeProfile(selector: string, opts: { yes?: boolean; close?: bo
972
1091
  const key = (await resolveGlobalProfile(registry, cache, selector)).email;
973
1092
  const config = await readListeners();
974
1093
  const plan = planProfileRemoval(key, registry, config?.listeners ?? [], join(RECH_DIR, "profiles"));
975
- const running = plan.dataDir ? browsersUsing(plan.dataDir) : [];
1094
+ const using = plan.dataDir ? browsersUsing(plan.dataDir) : { exact: [], ambiguous: [] };
1095
+ const running = using.exact;
976
1096
  console.log(`Remove profile "${key}"${plan.keys.length > 1 ? ` (registered as ${plan.keys.join(", ")})` : ""}:`);
977
1097
  for (const edit of plan.listeners) console.log(edit.drop ? ` - remove listener "${edit.name}" (it serves only this profile)` : ` - stop sharing it on listener "${edit.name}"`);
978
1098
  console.log(` - unregister it from rech (~/.rechrome/profiles.yaml)`);
979
1099
  if (plan.dataDir) console.log(` - move its data folder to the Trash: ${plan.dataDir}`);
980
1100
  else console.log(` - its Chrome data is left alone (this only unregisters it from rech)`);
981
1101
  if (running.length) console.log(` - close its browser window, which is running now (pid ${running.join(", ")})`);
1102
+ // A process that may be on this folder or on a sibling one: never killed, and nothing changes.
1103
+ if (using.ambiguous.length)
1104
+ throw new Error(`A process that may be using ${plan.dataDir} is running (pid ${using.ambiguous.join(", ")}), and rech can't tell it from one on a similarly named folder. Close it yourself, then run this again. Nothing changed.`);
982
1105
  const interactive = isInteractive();
983
1106
  if (!opts.yes) {
984
1107
  if (!interactive) throw new Error("Not removed. Re-run with --yes to confirm" + (running.length ? " and --close to close its running window." : "."));
@@ -989,8 +1112,8 @@ async function removeProfile(selector: string, opts: { yes?: boolean; close?: bo
989
1112
  // In a terminal, confirming the plan above (which lists closing it) is the consent; otherwise --close.
990
1113
  if (!interactive && !opts.close) throw new Error("Its window is running; re-run with --close to close it, or close it yourself first.");
991
1114
  for (const pid of running) process.kill(pid, "SIGTERM");
992
- for (let i = 0; i < 40 && browsersUsing(plan.dataDir!).length; i++) await Bun.sleep(250);
993
- if (browsersUsing(plan.dataDir!).length) throw new Error("Its window did not close; close it and run this again. Nothing else changed.");
1115
+ for (let i = 0; i < 40 && browsersUsing(plan.dataDir!).exact.length; i++) await Bun.sleep(250);
1116
+ if (browsersUsing(plan.dataDir!).exact.length) throw new Error("Its window did not close; close it and run this again. Nothing else changed.");
994
1117
  }
995
1118
  if (config && plan.listeners.length) {
996
1119
  for (const edit of plan.listeners) {
@@ -1011,7 +1134,7 @@ async function removeProfile(selector: string, opts: { yes?: boolean; close?: bo
1011
1134
  }
1012
1135
  let moved: string | null = null;
1013
1136
  if (plan.dataDir && existsSync(plan.dataDir)) moved = moveToTrash(plan.dataDir);
1014
- console.log(`Removed "${key}".${moved ? ` Its data folder is in the Trash: ${moved}` : plan.dataDir ? ` Its data folder was left at ${plan.dataDir}; delete it yourself if you like.` : ""}`);
1137
+ console.log(`Removed "${key}".${moved ? ` Its data folder was moved to ${moved}.` : plan.dataDir ? ` Its data folder was left at ${plan.dataDir}; delete it yourself if you like.` : ""}`);
1015
1138
  }
1016
1139
 
1017
1140
  /** On a client of a remote host, `rech profile` lists what that host's link shares. */
package/rechrome.ts CHANGED
@@ -942,16 +942,135 @@ export function buildProfileRows(cache: Record<string, ChromeProfileInfo> | null
942
942
  return rows;
943
943
  }
944
944
 
945
- /** PIDs of browser processes (not helpers) running on a Chrome user-data dir. POSIX only. */
946
- function browsersUsing(userDataDir: string): number[] {
947
- if (process.platform === "win32") return [];
945
+ /**
946
+ * Split a Windows command line into arguments the way the C runtime does (CommandLineToArgvW
947
+ * rules): whitespace separates outside quotes; quotes toggle and may sit mid-argument
948
+ * (`--x="C:\a"b` is `--x=C:\ab`); 2n backslashes before a quote are n backslashes and the quote
949
+ * toggles, 2n+1 are n and a literal quote; `""` inside quotes is a literal quote.
950
+ */
951
+ export function splitWindowsCommandLine(commandLine: string): string[] {
952
+ const args: string[] = [];
953
+ let current = "", quoted = false, started = false;
954
+ for (let i = 0; i < commandLine.length; i++) {
955
+ const c = commandLine[i];
956
+ if (c === "\\") {
957
+ let n = 0;
958
+ while (commandLine[i] === "\\") { n++; i++; }
959
+ if (commandLine[i] === '"') {
960
+ current += "\\".repeat(n >> 1);
961
+ if (n % 2) current += '"'; else quoted = !quoted;
962
+ } else { current += "\\".repeat(n); i--; }
963
+ started = true;
964
+ } else if (c === '"') {
965
+ if (quoted && commandLine[i + 1] === '"') { current += '"'; i++; } else quoted = !quoted;
966
+ started = true;
967
+ } else if (!quoted && (c === " " || c === "\t")) {
968
+ if (started) { args.push(current); current = ""; started = false; }
969
+ } else { current += c; started = true; }
970
+ }
971
+ if (started) args.push(current);
972
+ return args;
973
+ }
974
+
975
+ /** The `--user-data-dir` value among exact arguments, or null. */
976
+ export function userDataDirArg(args: string[]): string | null {
977
+ const arg = args.find(a => a.startsWith("--user-data-dir="));
978
+ return arg === undefined ? null : arg.slice("--user-data-dir=".length);
979
+ }
980
+
981
+ /** Same folder: exact path, ignoring trailing separators (and case on Windows). */
982
+ export function sameDataDir(a: string, b: string, windows = process.platform === "win32"): boolean {
983
+ const norm = (p: string) => { const t = p.replace(/[\\/]+$/, ""); return windows ? t.replaceAll("/", "\\").toLowerCase() : t; };
984
+ return norm(a) === norm(b);
985
+ }
986
+
987
+ /**
988
+ * Match a flattened POSIX `ps` command line (no quotes, argument boundaries lost) against a
989
+ * user-data dir: "exact" only when the path is the last thing on the line; "ambiguous" when
990
+ * more text follows (another flag, or a sibling folder like `qa -backup`); else "none".
991
+ */
992
+ export function flatUserDataDirMatch(command: string, userDataDir: string): "exact" | "ambiguous" | "none" {
993
+ const flag = `--user-data-dir=${userDataDir.replace(/\/+$/, "")}`;
994
+ const at = command.indexOf(flag);
995
+ if (at < 0 || (at > 0 && !/\s/.test(command[at - 1]))) return "none";
996
+ const rest = command.slice(at + flag.length);
997
+ if (/^\/*\s*$/.test(rest)) return "exact";
998
+ return /^\/*\s/.test(rest) ? "ambiguous" : "none"; // `qa2` / `qa-x` are other folders
999
+ }
1000
+
1001
+ /**
1002
+ * Browser processes (not helpers) on this Chrome user-data dir. `exact`: certainly this folder
1003
+ * (Windows command lines are split with the C runtime's rules; Linux reads argv from /proc).
1004
+ * `ambiguous`: flattened `ps` output that may name a sibling folder — never killed.
1005
+ */
1006
+ function browsersUsing(userDataDir: string): { exact: number[]; ambiguous: number[] } {
1007
+ const exact: number[] = [], ambiguous: number[] = [];
1008
+ const isHelper = (args: string[]) => args.some(a => a.startsWith("--type="));
1009
+ if (process.platform === "win32") {
1010
+ const out = Bun.spawnSync(["powershell", "-NoProfile", "-NonInteractive", "-Command",
1011
+ "Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId)`t$($_.CommandLine)\" }"], { windowsHide: true }).stdout.toString();
1012
+ for (const line of out.split(/\r?\n/)) {
1013
+ const tab = line.indexOf("\t"), pid = Number(line.slice(0, tab));
1014
+ if (!pid || pid === process.pid) continue;
1015
+ const args = splitWindowsCommandLine(line.slice(tab + 1));
1016
+ const dir = userDataDirArg(args);
1017
+ if (dir !== null && !isHelper(args) && sameDataDir(dir, userDataDir)) exact.push(pid);
1018
+ }
1019
+ return { exact, ambiguous };
1020
+ }
948
1021
  const ps = Bun.spawnSync(["ps", "ax", "-o", "pid=,command="]).stdout.toString();
949
- return ps.split("\n").filter(line => line.includes(`--user-data-dir=${userDataDir}`) && !line.includes("--type="))
950
- .map(line => Number(line.trim().split(/\s+/)[0])).filter(Boolean);
1022
+ for (const line of ps.split("\n")) {
1023
+ const m = /^\s*(\d+)\s+(.*)$/.exec(line);
1024
+ if (!m || Number(m[1]) === process.pid || !m[2].includes("--user-data-dir=")) continue;
1025
+ const pid = Number(m[1]);
1026
+ let argv: string[] | null = null;
1027
+ try { argv = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean); } catch { /* not Linux, or gone */ }
1028
+ if (argv) {
1029
+ const dir = userDataDirArg(argv);
1030
+ if (dir !== null && !isHelper(argv) && sameDataDir(dir, userDataDir, false)) exact.push(pid);
1031
+ continue;
1032
+ }
1033
+ if (/(?:^|\s)--type=/.test(m[2])) continue;
1034
+ const match = flatUserDataDirMatch(m[2], userDataDir);
1035
+ if (match === "exact") exact.push(pid);
1036
+ else if (match === "ambiguous") ambiguous.push(pid);
1037
+ }
1038
+ return { exact, ambiguous };
951
1039
  }
952
1040
 
953
1041
  /** Move a folder to the user's Trash (recoverable). Returns where it went, or null if unsupported here. */
954
1042
  function moveToTrash(dir: string): string | null {
1043
+ // An explicit trash folder (tests use it to keep the real Trash / Recycle Bin clean).
1044
+ const override = process.env.RECH_TRASH_DIR;
1045
+ if (override) {
1046
+ mkdirSync(override, { recursive: true });
1047
+ let dest = join(override, basename(dir));
1048
+ if (existsSync(dest)) dest = `${dest} ${new Date().toISOString().replace(/[:.]/g, "-")}`;
1049
+ renameSync(dir, dest);
1050
+ return dest;
1051
+ }
1052
+ if (process.platform === "win32") {
1053
+ // SHFileOperation with ALLOWUNDO sends it to the Recycle Bin, with no prompts or progress UI.
1054
+ // WANTNUKEWARNING makes Windows ask rather than silently delete for good when the item
1055
+ // cannot be recycled. (.NET's FileSystem.DeleteDirectory(..., SendToRecycleBin) deleted
1056
+ // permanently here.) The path goes through the environment so quoting can't break it.
1057
+ const source = [
1058
+ "using System; using System.Runtime.InteropServices;",
1059
+ "public static class RechRecycle {",
1060
+ " [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]",
1061
+ " struct Op { public IntPtr hwnd; public uint wFunc; public string pFrom; public string pTo; public ushort fFlags; public bool aborted; public IntPtr mappings; public string title; }",
1062
+ " [DllImport(\"shell32.dll\", CharSet = CharSet.Unicode)] static extern int SHFileOperation(ref Op op);",
1063
+ " public static int Recycle(string path) {",
1064
+ " var op = new Op { wFunc = 3, pFrom = path + \"\\0\\0\", fFlags = 0x0040 | 0x0010 | 0x0004 | 0x0400 | 0x4000 };",
1065
+ " int r = SHFileOperation(ref op); return op.aborted ? -1 : r;",
1066
+ " }",
1067
+ "}",
1068
+ ].join("\n");
1069
+ const r = Bun.spawnSync(["powershell", "-NoProfile", "-NonInteractive", "-Command",
1070
+ "Add-Type -TypeDefinition $env:RECH_RECYCLE_SOURCE; exit [RechRecycle]::Recycle($env:RECH_RECYCLE_PATH)"],
1071
+ { env: { ...process.env, RECH_RECYCLE_SOURCE: source, RECH_RECYCLE_PATH: dir.replaceAll("/", "\\").replace(/\\+$/, "") }, windowsHide: true }); // SHFileOperation rejects `/` and a trailing `\`
1072
+ return r.exitCode === 0 && !existsSync(dir) ? "the Recycle Bin" : null;
1073
+ }
955
1074
  const trash = process.platform === "darwin" ? join(HOME, ".Trash") : process.platform === "linux" ? join(HOME, ".local", "share", "Trash", "files") : null;
956
1075
  if (!trash) return null;
957
1076
  mkdirSync(trash, { recursive: true });
@@ -972,13 +1091,17 @@ async function removeProfile(selector: string, opts: { yes?: boolean; close?: bo
972
1091
  const key = (await resolveGlobalProfile(registry, cache, selector)).email;
973
1092
  const config = await readListeners();
974
1093
  const plan = planProfileRemoval(key, registry, config?.listeners ?? [], join(RECH_DIR, "profiles"));
975
- const running = plan.dataDir ? browsersUsing(plan.dataDir) : [];
1094
+ const using = plan.dataDir ? browsersUsing(plan.dataDir) : { exact: [], ambiguous: [] };
1095
+ const running = using.exact;
976
1096
  console.log(`Remove profile "${key}"${plan.keys.length > 1 ? ` (registered as ${plan.keys.join(", ")})` : ""}:`);
977
1097
  for (const edit of plan.listeners) console.log(edit.drop ? ` - remove listener "${edit.name}" (it serves only this profile)` : ` - stop sharing it on listener "${edit.name}"`);
978
1098
  console.log(` - unregister it from rech (~/.rechrome/profiles.yaml)`);
979
1099
  if (plan.dataDir) console.log(` - move its data folder to the Trash: ${plan.dataDir}`);
980
1100
  else console.log(` - its Chrome data is left alone (this only unregisters it from rech)`);
981
1101
  if (running.length) console.log(` - close its browser window, which is running now (pid ${running.join(", ")})`);
1102
+ // A process that may be on this folder or on a sibling one: never killed, and nothing changes.
1103
+ if (using.ambiguous.length)
1104
+ throw new Error(`A process that may be using ${plan.dataDir} is running (pid ${using.ambiguous.join(", ")}), and rech can't tell it from one on a similarly named folder. Close it yourself, then run this again. Nothing changed.`);
982
1105
  const interactive = isInteractive();
983
1106
  if (!opts.yes) {
984
1107
  if (!interactive) throw new Error("Not removed. Re-run with --yes to confirm" + (running.length ? " and --close to close its running window." : "."));
@@ -989,8 +1112,8 @@ async function removeProfile(selector: string, opts: { yes?: boolean; close?: bo
989
1112
  // In a terminal, confirming the plan above (which lists closing it) is the consent; otherwise --close.
990
1113
  if (!interactive && !opts.close) throw new Error("Its window is running; re-run with --close to close it, or close it yourself first.");
991
1114
  for (const pid of running) process.kill(pid, "SIGTERM");
992
- for (let i = 0; i < 40 && browsersUsing(plan.dataDir!).length; i++) await Bun.sleep(250);
993
- if (browsersUsing(plan.dataDir!).length) throw new Error("Its window did not close; close it and run this again. Nothing else changed.");
1115
+ for (let i = 0; i < 40 && browsersUsing(plan.dataDir!).exact.length; i++) await Bun.sleep(250);
1116
+ if (browsersUsing(plan.dataDir!).exact.length) throw new Error("Its window did not close; close it and run this again. Nothing else changed.");
994
1117
  }
995
1118
  if (config && plan.listeners.length) {
996
1119
  for (const edit of plan.listeners) {
@@ -1011,7 +1134,7 @@ async function removeProfile(selector: string, opts: { yes?: boolean; close?: bo
1011
1134
  }
1012
1135
  let moved: string | null = null;
1013
1136
  if (plan.dataDir && existsSync(plan.dataDir)) moved = moveToTrash(plan.dataDir);
1014
- console.log(`Removed "${key}".${moved ? ` Its data folder is in the Trash: ${moved}` : plan.dataDir ? ` Its data folder was left at ${plan.dataDir}; delete it yourself if you like.` : ""}`);
1137
+ console.log(`Removed "${key}".${moved ? ` Its data folder was moved to ${moved}.` : plan.dataDir ? ` Its data folder was left at ${plan.dataDir}; delete it yourself if you like.` : ""}`);
1015
1138
  }
1016
1139
 
1017
1140
  /** On a client of a remote host, `rech profile` lists what that host's link shares. */