@elisoncampos/local-router 0.2.0 → 0.2.1

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.
@@ -0,0 +1 @@
1
+ /opt/hostedtoolcache/node/24.14.0/x64/bin/node
@@ -0,0 +1,104 @@
1
+ #!/bin/sh
2
+
3
+ set -eu
4
+
5
+ SOURCE="$0"
6
+
7
+ while [ -h "$SOURCE" ]; do
8
+ DIR=$(CDPATH= cd -- "$(dirname -- "$SOURCE")" && pwd)
9
+ TARGET=$(readlink "$SOURCE")
10
+ case "$TARGET" in
11
+ /*) SOURCE="$TARGET" ;;
12
+ *) SOURCE="$DIR/$TARGET" ;;
13
+ esac
14
+ done
15
+
16
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$SOURCE")" && pwd)
17
+ PACKAGE_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
18
+ INSTALLED_NODE_PATH_FILE="$PACKAGE_ROOT/bin/.installed-node-path"
19
+
20
+ PRIMARY_NODE="$(command -v node 2>/dev/null || true)"
21
+
22
+ can_run_node() {
23
+ CANDIDATE="$1"
24
+ [ -n "$CANDIDATE" ] || return 1
25
+ [ -x "$CANDIDATE" ] || return 1
26
+ "$CANDIDATE" -e "process.exit(0)" >/dev/null 2>&1
27
+ }
28
+
29
+ find_fallback_node() {
30
+ OLD_IFS=$IFS
31
+ IFS=:
32
+ for DIR in $PATH; do
33
+ [ -n "$DIR" ] || continue
34
+ CANDIDATE="$DIR/node"
35
+ [ -x "$CANDIDATE" ] || continue
36
+
37
+ if [ -n "$PRIMARY_NODE" ] && [ "$CANDIDATE" = "$PRIMARY_NODE" ]; then
38
+ continue
39
+ fi
40
+
41
+ case "$CANDIDATE" in
42
+ */.asdf/shims/node|*/asdf/shims/node|*/.mise/shims/node|*/mise/shims/node)
43
+ continue
44
+ ;;
45
+ esac
46
+
47
+ if can_run_node "$CANDIDATE"; then
48
+ printf '%s\n' "$CANDIDATE"
49
+ IFS=$OLD_IFS
50
+ return 0
51
+ fi
52
+ done
53
+ IFS=$OLD_IFS
54
+ return 1
55
+ }
56
+
57
+ SELECTED_NODE=""
58
+ USED_FALLBACK="0"
59
+
60
+ if [ -n "$PRIMARY_NODE" ] && can_run_node "$PRIMARY_NODE"; then
61
+ SELECTED_NODE="$PRIMARY_NODE"
62
+ else
63
+ if [ -f "$INSTALLED_NODE_PATH_FILE" ]; then
64
+ INSTALLED_NODE="$(tr -d '\r\n' < "$INSTALLED_NODE_PATH_FILE")"
65
+ if can_run_node "$INSTALLED_NODE"; then
66
+ SELECTED_NODE="$INSTALLED_NODE"
67
+ fi
68
+ fi
69
+
70
+ if [ -z "$SELECTED_NODE" ]; then
71
+ SELECTED_NODE="$(find_fallback_node || true)"
72
+ fi
73
+
74
+ if [ -n "$SELECTED_NODE" ]; then
75
+ USED_FALLBACK="1"
76
+ fi
77
+ fi
78
+
79
+ if [ -z "$SELECTED_NODE" ]; then
80
+ echo "local-router: could not find a working Node.js runtime." >&2
81
+ echo "local-router: install the project's Node version or make a global node available in PATH." >&2
82
+ exit 1
83
+ fi
84
+
85
+ CLI_JS="$PACKAGE_ROOT/dist/cli.js"
86
+ TSX_CLI="$PACKAGE_ROOT/node_modules/tsx/dist/cli.mjs"
87
+ CLI_TS="$PACKAGE_ROOT/src/cli.ts"
88
+
89
+ if [ -f "$CLI_JS" ]; then
90
+ exec env \
91
+ LOCAL_ROUTER_NODE_BIN="$SELECTED_NODE" \
92
+ LOCAL_ROUTER_NODE_FALLBACK="$USED_FALLBACK" \
93
+ "$SELECTED_NODE" "$CLI_JS" "$@"
94
+ fi
95
+
96
+ if [ -f "$TSX_CLI" ] && [ -f "$CLI_TS" ]; then
97
+ exec env \
98
+ LOCAL_ROUTER_NODE_BIN="$SELECTED_NODE" \
99
+ LOCAL_ROUTER_NODE_FALLBACK="$USED_FALLBACK" \
100
+ "$SELECTED_NODE" "$TSX_CLI" "$CLI_TS" "$@"
101
+ fi
102
+
103
+ echo "local-router: CLI entrypoint not found. Build the project first." >&2
104
+ exit 1
package/dist/cli.js CHANGED
@@ -45,8 +45,11 @@ function formatUrl(hostname, https2, port) {
45
45
  function removeProtocol(hostname) {
46
46
  return hostname.replace(/^https?:\/\//i, "").split("/")[0].trim();
47
47
  }
48
+ function trimTrailingDots(hostname) {
49
+ return hostname.replace(/\.+$/, "");
50
+ }
48
51
  function normalizeExplicitHostname(hostname) {
49
- const normalized = removeProtocol(hostname).toLowerCase();
52
+ const normalized = trimTrailingDots(removeProtocol(hostname).toLowerCase());
50
53
  if (!normalized) {
51
54
  throw new Error("Hostname cannot be empty.");
52
55
  }
@@ -68,6 +71,18 @@ function normalizeExplicitHostname(hostname) {
68
71
  }
69
72
  return normalized;
70
73
  }
74
+ function normalizeRequestHostname(hostname) {
75
+ const authority = removeProtocol(hostname);
76
+ if (!authority) return "";
77
+ if (authority.startsWith("[")) {
78
+ const closingBracketIndex = authority.indexOf("]");
79
+ const bracketedHost = closingBracketIndex === -1 ? authority.slice(1) : authority.slice(1, closingBracketIndex);
80
+ return trimTrailingDots(bracketedHost.toLowerCase());
81
+ }
82
+ const lastColonIndex = authority.lastIndexOf(":");
83
+ const withoutPort = lastColonIndex === -1 || authority.includes(":", lastColonIndex + 1) ? authority : authority.slice(0, lastColonIndex);
84
+ return trimTrailingDots(withoutPort.toLowerCase());
85
+ }
71
86
 
72
87
  // src/certs.ts
73
88
  var CA_VALIDITY_DAYS = 3650;
@@ -299,22 +314,23 @@ function createSNICallback(stateDir, defaultCert, defaultKey) {
299
314
  const defaultContext = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
300
315
  return async (servername, callback) => {
301
316
  try {
302
- if (!servername || servername === "localhost") {
317
+ const normalizedServername = normalizeRequestHostname(servername);
318
+ if (!normalizedServername || normalizedServername === "localhost") {
303
319
  callback(null, defaultContext);
304
320
  return;
305
321
  }
306
- const cached = cache.get(servername);
322
+ const cached = cache.get(normalizedServername);
307
323
  if (cached) {
308
324
  callback(null, cached);
309
325
  return;
310
326
  }
311
- const safeName = sanitizeHostForFilename(servername);
327
+ const safeName = sanitizeHostForFilename(normalizedServername);
312
328
  const certPath = path.join(stateDir, HOST_CERTS_DIR, `${safeName}.pem`);
313
329
  const keyPath = path.join(stateDir, HOST_CERTS_DIR, `${safeName}-key.pem`);
314
330
  let resolvedCertPath = certPath;
315
331
  let resolvedKeyPath = keyPath;
316
332
  if (!fileExists(certPath) || !fileExists(keyPath) || !isCertValid(certPath) || !isCertSignatureStrong(certPath)) {
317
- const generated = await generateHostCertAsync(stateDir, servername);
333
+ const generated = await generateHostCertAsync(stateDir, normalizedServername);
318
334
  resolvedCertPath = generated.certPath;
319
335
  resolvedKeyPath = generated.keyPath;
320
336
  }
@@ -322,7 +338,7 @@ function createSNICallback(stateDir, defaultCert, defaultKey) {
322
338
  cert: fs2.readFileSync(resolvedCertPath),
323
339
  key: fs2.readFileSync(resolvedKeyPath)
324
340
  });
325
- cache.set(servername, context);
341
+ cache.set(normalizedServername, context);
326
342
  callback(null, context);
327
343
  } catch (error) {
328
344
  callback(error instanceof Error ? error : new Error(String(error)), defaultContext);
@@ -505,12 +521,19 @@ function inferProjectName(cwd = process.cwd()) {
505
521
 
506
522
  // src/config.ts
507
523
  var CONFIG_FILENAMES = [".local-router", ".local-router.json", "local-router.config.json"];
524
+ function isReadableFile(candidate) {
525
+ try {
526
+ return fs4.statSync(candidate).isFile();
527
+ } catch {
528
+ return false;
529
+ }
530
+ }
508
531
  function findLocalRouterConfig(cwd = process.cwd()) {
509
532
  let dir = cwd;
510
533
  for (; ; ) {
511
534
  for (const filename of CONFIG_FILENAMES) {
512
535
  const candidate = path3.join(dir, filename);
513
- if (fs4.existsSync(candidate)) {
536
+ if (isReadableFile(candidate)) {
514
537
  return candidate;
515
538
  }
516
539
  }
@@ -667,7 +690,7 @@ var MIN_APP_PORT = 4e3;
667
690
  var MAX_APP_PORT = 4999;
668
691
  var RANDOM_PORT_ATTEMPTS = 50;
669
692
  var SOCKET_TIMEOUT_MS = 750;
670
- var WAIT_FOR_PROXY_MAX_ATTEMPTS = 24;
693
+ var WAIT_FOR_PROXY_MAX_ATTEMPTS = 60;
671
694
  var WAIT_FOR_PROXY_INTERVAL_MS = 250;
672
695
  var HEALTH_HEADER = "x-local-router";
673
696
  var SIGNAL_CODES = {
@@ -972,19 +995,34 @@ function getHealthHeader() {
972
995
  }
973
996
 
974
997
  // src/proxy.ts
975
- function getRequestHost(req) {
998
+ function getRequestAuthority(req) {
976
999
  const authority = req.headers[":authority"];
977
1000
  if (typeof authority === "string" && authority) return authority;
1001
+ if (Array.isArray(req.headers.host)) {
1002
+ return req.headers.host[0] ?? "";
1003
+ }
978
1004
  return req.headers.host ?? "";
979
1005
  }
980
- function buildForwardedHeaders(req, tls2) {
1006
+ function getAuthorityPort(authority) {
1007
+ if (!authority) return void 0;
1008
+ if (authority.startsWith("[")) {
1009
+ const closingBracketIndex = authority.indexOf("]");
1010
+ if (closingBracketIndex !== -1 && authority[closingBracketIndex + 1] === ":") {
1011
+ return authority.slice(closingBracketIndex + 2);
1012
+ }
1013
+ return void 0;
1014
+ }
1015
+ const lastColonIndex = authority.lastIndexOf(":");
1016
+ if (lastColonIndex === -1) return void 0;
1017
+ return authority.slice(lastColonIndex + 1);
1018
+ }
1019
+ function buildForwardedHeaders(req, authority, tls2) {
981
1020
  const remoteAddress = req.socket.remoteAddress || "127.0.0.1";
982
- const hostHeader = getRequestHost(req);
983
1021
  return {
984
1022
  "x-forwarded-for": req.headers["x-forwarded-for"] ? `${req.headers["x-forwarded-for"]}, ${remoteAddress}` : remoteAddress,
985
1023
  "x-forwarded-proto": req.headers["x-forwarded-proto"] || (tls2 ? "https" : "http"),
986
- "x-forwarded-host": req.headers["x-forwarded-host"] || hostHeader,
987
- "x-forwarded-port": req.headers["x-forwarded-port"] || hostHeader.split(":")[1] || (tls2 ? "443" : "80")
1024
+ "x-forwarded-host": req.headers["x-forwarded-host"] || authority,
1025
+ "x-forwarded-port": req.headers["x-forwarded-port"] || getAuthorityPort(authority) || (tls2 ? "443" : "80")
988
1026
  };
989
1027
  }
990
1028
  function findRoute(routes, host) {
@@ -1021,7 +1059,8 @@ function createProxyServers(options) {
1021
1059
  res.end();
1022
1060
  return;
1023
1061
  }
1024
- const host = getRequestHost(req).split(":")[0];
1062
+ const authority = getRequestAuthority(req);
1063
+ const host = normalizeRequestHostname(authority);
1025
1064
  if (!host) {
1026
1065
  res.writeHead(400, { "content-type": "text/plain" });
1027
1066
  res.end("Missing Host header.");
@@ -1040,8 +1079,8 @@ function createProxyServers(options) {
1040
1079
  );
1041
1080
  return;
1042
1081
  }
1043
- const headers = { ...req.headers, ...buildForwardedHeaders(req, httpsRequest) };
1044
- headers.host = getRequestHost(req);
1082
+ const headers = { ...req.headers, ...buildForwardedHeaders(req, authority, httpsRequest) };
1083
+ headers.host = authority;
1045
1084
  for (const key of Object.keys(headers)) {
1046
1085
  if (key.startsWith(":")) {
1047
1086
  delete headers[key];
@@ -1090,14 +1129,15 @@ function createProxyServers(options) {
1090
1129
  };
1091
1130
  const handleUpgrade = (httpsRequest) => (req, socket, head) => {
1092
1131
  socket.on("error", () => socket.destroy());
1093
- const host = getRequestHost(req).split(":")[0];
1132
+ const authority = getRequestAuthority(req);
1133
+ const host = normalizeRequestHostname(authority);
1094
1134
  const route = findRoute(options.getRoutes(), host);
1095
1135
  if (!route) {
1096
1136
  socket.destroy();
1097
1137
  return;
1098
1138
  }
1099
- const headers = { ...req.headers, ...buildForwardedHeaders(req, httpsRequest) };
1100
- headers.host = getRequestHost(req);
1139
+ const headers = { ...req.headers, ...buildForwardedHeaders(req, authority, httpsRequest) };
1140
+ headers.host = authority;
1101
1141
  for (const key of Object.keys(headers)) {
1102
1142
  if (key.startsWith(":")) {
1103
1143
  delete headers[key];
@@ -1195,6 +1235,16 @@ var RouteConflictError = class extends Error {
1195
1235
  function isValidRoute(value) {
1196
1236
  return typeof value === "object" && value !== null && typeof value.hostname === "string" && typeof value.port === "number" && typeof value.pid === "number";
1197
1237
  }
1238
+ function normalizeRoute(route) {
1239
+ try {
1240
+ return {
1241
+ ...route,
1242
+ hostname: normalizeExplicitHostname(route.hostname)
1243
+ };
1244
+ } catch {
1245
+ return null;
1246
+ }
1247
+ }
1198
1248
  var RouteStore = class _RouteStore {
1199
1249
  dir;
1200
1250
  routesPath;
@@ -1279,7 +1329,10 @@ var RouteStore = class _RouteStore {
1279
1329
  this.onWarning?.(`Corrupted routes file: expected an array at ${this.routesPath}.`);
1280
1330
  return [];
1281
1331
  }
1282
- const routes = parsed.filter(isValidRoute);
1332
+ const routes = parsed.filter(isValidRoute).flatMap((route) => {
1333
+ const normalized = normalizeRoute(route);
1334
+ return normalized ? [normalized] : [];
1335
+ });
1283
1336
  const aliveRoutes = routes.filter((route) => this.isProcessAlive(route.pid));
1284
1337
  if (persistCleanup && aliveRoutes.length !== routes.length) {
1285
1338
  this.saveRoutes(aliveRoutes);
@@ -1295,30 +1348,32 @@ var RouteStore = class _RouteStore {
1295
1348
  fixOwnership(this.routesPath);
1296
1349
  }
1297
1350
  addRoute(hostname, port, pid, force = false) {
1351
+ const normalizedHostname = normalizeExplicitHostname(hostname);
1298
1352
  this.ensureDir();
1299
1353
  if (!this.acquireLock()) {
1300
1354
  throw new Error("Failed to acquire route lock.");
1301
1355
  }
1302
1356
  try {
1303
1357
  const routes = this.loadRoutes(true);
1304
- const existing = routes.find((route) => route.hostname === hostname);
1358
+ const existing = routes.find((route) => route.hostname === normalizedHostname);
1305
1359
  if (existing && existing.pid !== pid && this.isProcessAlive(existing.pid) && !force) {
1306
- throw new RouteConflictError(hostname, existing.pid);
1360
+ throw new RouteConflictError(normalizedHostname, existing.pid);
1307
1361
  }
1308
- const nextRoutes = routes.filter((route) => route.hostname !== hostname);
1309
- nextRoutes.push({ hostname, port, pid });
1362
+ const nextRoutes = routes.filter((route) => route.hostname !== normalizedHostname);
1363
+ nextRoutes.push({ hostname: normalizedHostname, port, pid });
1310
1364
  this.saveRoutes(nextRoutes);
1311
1365
  } finally {
1312
1366
  this.releaseLock();
1313
1367
  }
1314
1368
  }
1315
1369
  removeRoute(hostname) {
1370
+ const normalizedHostname = normalizeExplicitHostname(hostname);
1316
1371
  this.ensureDir();
1317
1372
  if (!this.acquireLock()) {
1318
1373
  throw new Error("Failed to acquire route lock.");
1319
1374
  }
1320
1375
  try {
1321
- const nextRoutes = this.loadRoutes(true).filter((route) => route.hostname !== hostname);
1376
+ const nextRoutes = this.loadRoutes(true).filter((route) => route.hostname !== normalizedHostname);
1322
1377
  this.saveRoutes(nextRoutes);
1323
1378
  } finally {
1324
1379
  this.releaseLock();
@@ -1334,7 +1389,31 @@ var POLL_INTERVAL_MS = 3e3;
1334
1389
  var EXIT_TIMEOUT_MS = 2e3;
1335
1390
  var START_TIMEOUT_MS = 3e4;
1336
1391
  var AUTO_STOP_IDLE_MS = 3e3;
1392
+ var AUTO_STOP_STARTUP_GRACE_MS = 15e3;
1337
1393
  var CLI_ENTRY_PATH = fileURLToPath(import.meta.url);
1394
+ var PACKAGE_VERSION = getPackageVersion();
1395
+ function getPackageVersion() {
1396
+ try {
1397
+ const packageJsonPath = path7.join(path7.dirname(path7.dirname(CLI_ENTRY_PATH)), "package.json");
1398
+ const packageJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
1399
+ return packageJson.version ?? "0.0.0";
1400
+ } catch {
1401
+ return "0.0.0";
1402
+ }
1403
+ }
1404
+ function maybeWarnAboutNodeFallback() {
1405
+ if (process.env.LOCAL_ROUTER_NODE_FALLBACK !== "1") return;
1406
+ const fallbackNode = process.env.LOCAL_ROUTER_NODE_BIN;
1407
+ if (!fallbackNode) return;
1408
+ console.warn(
1409
+ chalk.yellow(
1410
+ `Warning: the project-local Node.js runtime was not available. Falling back to ${fallbackNode}.`
1411
+ )
1412
+ );
1413
+ console.warn(
1414
+ chalk.gray("Install the project's Node version in asdf, mise, nvm, or your preferred manager.\n")
1415
+ );
1416
+ }
1338
1417
  function getSelfInvocation(args) {
1339
1418
  if (CLI_ENTRY_PATH.endsWith(".ts")) {
1340
1419
  const projectRoot = path7.dirname(path7.dirname(CLI_ENTRY_PATH));
@@ -1639,6 +1718,7 @@ function startProxyServer(store, runtime, stateDir) {
1639
1718
  let poller = null;
1640
1719
  let idleTimer = null;
1641
1720
  let cleaningUp = false;
1721
+ let hasSeenRoutes = cachedRoutes.length > 0;
1642
1722
  const clearIdleTimer = () => {
1643
1723
  if (!idleTimer) return;
1644
1724
  clearTimeout(idleTimer);
@@ -1646,13 +1726,14 @@ function startProxyServer(store, runtime, stateDir) {
1646
1726
  };
1647
1727
  const scheduleIdleShutdown = () => {
1648
1728
  if (!runtime.autoStopWhenIdle || idleTimer || cachedRoutes.length > 0) return;
1729
+ const delay = hasSeenRoutes ? AUTO_STOP_IDLE_MS : AUTO_STOP_STARTUP_GRACE_MS;
1649
1730
  idleTimer = setTimeout(() => {
1650
1731
  idleTimer = null;
1651
1732
  cachedRoutes = store.loadRoutes(true);
1652
1733
  if (cachedRoutes.length === 0) {
1653
1734
  cleanup();
1654
1735
  }
1655
- }, AUTO_STOP_IDLE_MS);
1736
+ }, delay);
1656
1737
  idleTimer.unref();
1657
1738
  };
1658
1739
  const reloadRoutes = () => {
@@ -1660,6 +1741,7 @@ function startProxyServer(store, runtime, stateDir) {
1660
1741
  cachedRoutes = store.loadRoutes(true);
1661
1742
  syncHostsFromStore(store);
1662
1743
  if (cachedRoutes.length > 0) {
1744
+ hasSeenRoutes = true;
1663
1745
  clearIdleTimer();
1664
1746
  } else {
1665
1747
  scheduleIdleShutdown();
@@ -1677,7 +1759,6 @@ function startProxyServer(store, runtime, stateDir) {
1677
1759
  poller = setInterval(reloadRoutes, POLL_INTERVAL_MS);
1678
1760
  poller.unref();
1679
1761
  syncHostsFromStore(store);
1680
- scheduleIdleShutdown();
1681
1762
  let tlsOptions;
1682
1763
  if (runtime.httpsEnabled) {
1683
1764
  const certs = ensureCerts(stateDir);
@@ -1726,6 +1807,7 @@ function startProxyServer(store, runtime, stateDir) {
1726
1807
  if (runtime.httpsEnabled) {
1727
1808
  console.log(chalk.gray(`HTTPS -> ${runtime.httpsPort}`));
1728
1809
  }
1810
+ scheduleIdleShutdown();
1729
1811
  };
1730
1812
  const registerServerError = (label, port) => (error) => {
1731
1813
  if (error.code === "EADDRINUSE") {
@@ -2023,6 +2105,7 @@ async function handleTrust() {
2023
2105
  console.log(chalk.green("Local CA added to the trust store."));
2024
2106
  }
2025
2107
  async function main() {
2108
+ maybeWarnAboutNodeFallback();
2026
2109
  const args = process.argv.slice(2);
2027
2110
  const command = args[0];
2028
2111
  if (!command || command === "--help" || command === "-h") {
@@ -2030,7 +2113,7 @@ async function main() {
2030
2113
  return;
2031
2114
  }
2032
2115
  if (command === "--version" || command === "-v") {
2033
- console.log("0.1.0");
2116
+ console.log(PACKAGE_VERSION);
2034
2117
  return;
2035
2118
  }
2036
2119
  if (command === "run") {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@elisoncampos/local-router",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Named local domains with automatic HTTP/HTTPS routing for development.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
- "local-router": "dist/cli.js"
8
+ "local-router": "bin/local-router"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
@@ -19,7 +19,9 @@
19
19
  "access": "public"
20
20
  },
21
21
  "files": [
22
- "dist"
22
+ "bin",
23
+ "dist",
24
+ "scripts"
23
25
  ],
24
26
  "engines": {
25
27
  "node": ">=20.0.0"
@@ -28,6 +30,7 @@
28
30
  "build": "tsup src/cli.ts --format esm --target node20 --out-dir dist --clean",
29
31
  "changeset": "changeset",
30
32
  "dev": "tsx src/cli.ts",
33
+ "postinstall": "node scripts/write-node-path.js",
31
34
  "prepack": "npm run build",
32
35
  "prepublishOnly": "npm run typecheck && npm test",
33
36
  "release": "npm run typecheck && npm test && npm run build && changeset publish",
@@ -0,0 +1,10 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
6
+ const projectRoot = path.dirname(scriptDir);
7
+ const outputPath = path.join(projectRoot, "bin", ".installed-node-path");
8
+
9
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
10
+ fs.writeFileSync(outputPath, `${process.execPath}\n`, "utf8");