src/profile/paths.ts (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
/**
* Filesystem locations for browser profile discovery and pin persistence.
*
* Two independent path families live here:
*
* 1. Chromium user-data-dir candidates, per platform and channel. A
* user-data-dir holds `Local State` (the profile registry) and
* `DevToolsActivePort`; individual profiles are subdirectories of it
* (`Default`, `Profile 1`, …).
* Reference: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/user_data_dir.md
*
* 2. pi's agent config dir, where the profile pin is stored.
*
* On the agent dir: pi exports `getAgentDir()` from
* `@mariozechner/pi-coding-agent`, but importing it would be a RUNTIME import
* of the host package. Extensions installed via `pi install npm:...` do not
* get pi resolvable from their own node_modules (verified: the installed
* tree's `@mariozechner/` directory is empty), which is why every other import
* in this codebase is `import type` and erased at build time. So the three
* lines of `getAgentDir()` are replicated here instead — env var first, then
* `~/.pi/agent` (pi's dist/config.js: `PI_CODING_AGENT_DIR`, CONFIG_DIR_NAME
* `.pi`).
*/
import { homedir } from "node:os";
import { join } from "node:path";
// ── pi agent config dir ────────────────────────────────────────────────────
/** pi's env override for the agent dir (APP_NAME.toUpperCase() + "_CODING_AGENT_DIR"). */
const ENV_AGENT_DIR = "PI_CODING_AGENT_DIR";
/**
* Expand a leading `~` exactly as pi's expandTildePath does — `~` and `~/…`
* only, never `~\…`, on every platform. This deliberately mirrors pi rather
* than improving on it: the pin has to land in the directory pi itself resolves
* from the same variable, so being more lenient here would put the file
* somewhere pi does not look.
*/
const expandTildePiCompatible = (path: string): string => {
if (path === "~") return homedir();
if (path.startsWith("~/")) return homedir() + path.slice(1);
return path;
};
/**
* Tilde expansion for values this package interprets on its own. Windows users
* write `~\dir` as naturally as `~/dir`, and no compatibility constraint
* applies, so both are accepted.
*/
const expandTildeLenient = (path: string): string => {
if (path === "~") return homedir();
if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
return path;
};
/** pi's agent config directory — `$PI_CODING_AGENT_DIR` or `~/.pi/agent`. */
export const agentDir = (): string => {
const fromEnv = process.env[ENV_AGENT_DIR];
if (fromEnv) return expandTildePiCompatible(fromEnv);
return join(homedir(), ".pi", "agent");
};
/** Where the selected-profile pin is persisted. */
export const pinFilePath = (): string => join(agentDir(), "browser-harness.json");
// ── Chromium user-data-dir candidates ──────────────────────────────────────
/**
* Windows resolves its per-user data root from %LOCALAPPDATA%, which can be
* redirected on roaming / managed accounts. Fall back to the conventional
* layout only when the variable is absent.
*/
const localAppData = (): string => process.env["LOCALAPPDATA"] ?? join(homedir(), "AppData", "Local");
/**
* Every known default user-data-dir for Chromium-family browsers on the
* current platform. Order is stable but carries no priority — callers probe
* all of them.
*
* Includes paths the harness previously missed: snap Chromium (the default
* Chromium on Ubuntu) and the Linux-only `$CHROME_USER_DATA_DIR` override
* documented in user_data_dir.md.
*/
export const userDataDirCandidates = (): ReadonlyArray<string> => {
const home = homedir();
const dirs: string[] = [];
// A user-set override wins a spot at the front regardless of platform.
const envOverride = process.env["CHROME_USER_DATA_DIR"];
if (envOverride) dirs.push(expandTildeLenient(envOverride));
if (process.platform === "darwin") {
const support = join(home, "Library", "Application Support");
dirs.push(
join(support, "Google/Chrome"),
join(support, "Google/Chrome Beta"),
join(support, "Google/Chrome Dev"),
join(support, "Google/Chrome Canary"),
join(support, "Chromium"),
join(support, "BraveSoftware/Brave-Browser"),
join(support, "BraveSoftware/Brave-Browser-Beta"),
join(support, "BraveSoftware/Brave-Browser-Nightly"),
join(support, "BraveSoftware/Brave-Browser-Dev"),
join(support, "Microsoft Edge"),
join(support, "Microsoft Edge Beta"),
join(support, "Microsoft Edge Dev"),
join(support, "Microsoft Edge Canary"),
);
} else if (process.platform === "win32") {
const lad = localAppData();
dirs.push(
join(lad, "Google/Chrome/User Data"),
join(lad, "Google/Chrome Beta/User Data"),
join(lad, "Google/Chrome Dev/User Data"),
join(lad, "Google/Chrome SxS/User Data"),
join(lad, "Chromium/User Data"),
join(lad, "BraveSoftware/Brave-Browser/User Data"),
join(lad, "BraveSoftware/Brave-Browser-Beta/User Data"),
join(lad, "Microsoft/Edge/User Data"),
join(lad, "Microsoft/Edge Beta/User Data"),
join(lad, "Microsoft/Edge Dev/User Data"),
join(lad, "Microsoft/Edge SxS/User Data"),
);
} else {
dirs.push(
join(home, ".config/google-chrome"),
join(home, ".config/google-chrome-beta"),
join(home, ".config/google-chrome-unstable"),
join(home, ".config/chromium"),
join(home, ".config/chromium-browser"),
join(home, ".config/BraveSoftware/Brave-Browser"),
join(home, ".config/BraveSoftware/Brave-Browser-Beta"),
join(home, ".config/BraveSoftware/Brave-Browser-Nightly"),
join(home, ".config/microsoft-edge"),
join(home, ".config/microsoft-edge-beta"),
join(home, ".config/microsoft-edge-dev"),
// snap (Ubuntu's default Chromium) — confined to ~/snap/<pkg>/common
join(home, "snap/chromium/common/chromium"),
// flatpak
join(home, ".var/app/org.chromium.Chromium/config/chromium"),
join(home, ".var/app/com.google.Chrome/config/google-chrome"),
join(home, ".var/app/com.brave.Browser/config/BraveSoftware/Brave-Browser"),
join(home, ".var/app/com.microsoft.Edge/config/microsoft-edge"),
);
}
// De-duplicate while preserving order — an env override may repeat a default.
return Array.from(new Set(dirs));
};
/**
* Human-readable browser name for a user-data-dir, used to disambiguate the
* picker when more than one browser is installed. Falls back to the directory
* name, which is already descriptive for every path we generate.
*/
export const browserNameForUserDataDir = (userDataDir: string): string => {
const p = userDataDir.replace(/\\/g, "/");
const has = (needle: string): boolean => p.toLowerCase().includes(needle);
if (has("brave")) return "Brave";
if (has("edge")) return "Edge";
if (has("chromium")) return "Chromium";
if (has("chrome")) return "Chrome";
const last = p.split("/").filter(Boolean).pop();
return last ?? userDataDir;
};
|