portless 0.15.1 → 0.15.3

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.
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  checkHostResolution,
8
8
  cleanHostsFile,
9
9
  createHttpRedirectServer,
10
+ createLoopbackConnection,
10
11
  createProxyServer,
11
12
  fixOwnership,
12
13
  formatUrl,
@@ -15,9 +16,10 @@ import {
15
16
  isProcessAlive,
16
17
  parseHostname,
17
18
  parseHostnames,
19
+ resolveUserHome,
18
20
  shouldAutoSyncHosts,
19
21
  syncHostsFile
20
- } from "./chunk-SD2PIWJU.js";
22
+ } from "./chunk-JSJUKQRJ.js";
21
23
 
22
24
  // src/colors.ts
23
25
  function supportsColor() {
@@ -43,18 +45,96 @@ var gray = dim;
43
45
  var colors_default = { bold, dim, red, green, yellow, blue, cyan, white, gray };
44
46
 
45
47
  // src/cli.ts
46
- import * as fs9 from "fs";
48
+ import * as fs10 from "fs";
47
49
  import * as path9 from "path";
48
50
  import { spawn as spawn4, spawnSync as spawnSync5 } from "child_process";
49
51
  import { StringDecoder } from "string_decoder";
50
52
 
51
53
  // src/certs.ts
52
- import * as fs from "fs";
54
+ import * as fs2 from "fs";
53
55
  import * as path from "path";
54
- import * as crypto from "crypto";
56
+ import * as crypto2 from "crypto";
55
57
  import * as tls from "tls";
56
- import { execFile as execFileCb, execFileSync } from "child_process";
58
+ import { execFile as execFileCb, execFileSync as execFileSync2 } from "child_process";
57
59
  import { promisify } from "util";
60
+
61
+ // src/windows-ca.ts
62
+ import * as crypto from "crypto";
63
+ import * as fs from "fs";
64
+ import * as os from "os";
65
+ import { execFileSync } from "child_process";
66
+ var WINDOWS_COMMAND_TIMEOUT_MS = 3e4;
67
+ var commandOptions = {
68
+ encoding: "utf-8",
69
+ timeout: WINDOWS_COMMAND_TIMEOUT_MS,
70
+ stdio: ["pipe", "pipe", "pipe"]
71
+ };
72
+ var defaultRunner = (command, args, options) => execFileSync(command, args, options);
73
+ function isWSL(options = {}) {
74
+ const platform = options.platform ?? process.platform;
75
+ if (platform !== "linux") return false;
76
+ const env = options.env ?? process.env;
77
+ if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
78
+ const release2 = options.release ?? os.release();
79
+ return release2.toLowerCase().includes("microsoft");
80
+ }
81
+ function wslWindowsCAStoreOptions(run = defaultRunner) {
82
+ const command = run(
83
+ "wslpath",
84
+ ["-u", String.raw`C:\Windows\System32\certutil.exe`],
85
+ commandOptions
86
+ ).trim();
87
+ return {
88
+ command,
89
+ certificatePath: (certificatePath) => run("wslpath", ["-w", certificatePath], commandOptions).trim(),
90
+ run
91
+ };
92
+ }
93
+ function certificateFingerprint(certificatePath) {
94
+ const certificate = new crypto.X509Certificate(fs.readFileSync(certificatePath));
95
+ return certificate.fingerprint.replace(/:/g, "").toLowerCase();
96
+ }
97
+ function storeOptions(options) {
98
+ return {
99
+ command: options.command ?? "certutil",
100
+ certificatePath: options.certificatePath ?? ((certificatePath) => certificatePath),
101
+ run: options.run ?? defaultRunner
102
+ };
103
+ }
104
+ function storeContainsFingerprint(fingerprint, resolved) {
105
+ const listing = resolved.run(resolved.command, ["-store", "-user", "Root"], commandOptions);
106
+ return listing.replace(/\s/g, "").toLowerCase().includes(fingerprint);
107
+ }
108
+ function isWindowsCATrusted(caCertPath, options = {}) {
109
+ try {
110
+ const resolved = storeOptions(options);
111
+ const fingerprint = certificateFingerprint(caCertPath);
112
+ return storeContainsFingerprint(fingerprint, resolved);
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ function trustWindowsCA(caCertPath, options = {}) {
118
+ const resolved = storeOptions(options);
119
+ resolved.run(
120
+ resolved.command,
121
+ ["-addstore", "-user", "Root", resolved.certificatePath(caCertPath)],
122
+ commandOptions
123
+ );
124
+ }
125
+ function untrustWindowsCA(caCertPath, options = {}) {
126
+ try {
127
+ const resolved = storeOptions(options);
128
+ const fingerprint = certificateFingerprint(caCertPath);
129
+ if (!storeContainsFingerprint(fingerprint, resolved)) return { removed: true };
130
+ resolved.run(resolved.command, ["-delstore", "-user", "Root", fingerprint], commandOptions);
131
+ return storeContainsFingerprint(fingerprint, resolved) ? { removed: false, error: "certutil could not remove the portless CA from Root" } : { removed: true };
132
+ } catch (error) {
133
+ return { removed: false, error: error instanceof Error ? error.message : String(error) };
134
+ }
135
+ }
136
+
137
+ // src/certs.ts
58
138
  var CA_VALIDITY_DAYS = 3650;
59
139
  var SERVER_VALIDITY_DAYS = 365;
60
140
  var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -68,9 +148,10 @@ var CA_CERT_FILE = "ca.pem";
68
148
  var SERVER_KEY_FILE = "server-key.pem";
69
149
  var SERVER_CERT_FILE = "server.pem";
70
150
  var CA_TRUST_MARKER = "ca.trusted";
151
+ var CA_TRUST_REFRESH_PENDING = "ca.trust-refresh-pending";
71
152
  function fileExists(filePath) {
72
153
  try {
73
- fs.accessSync(filePath, fs.constants.R_OK);
154
+ fs2.accessSync(filePath, fs2.constants.R_OK);
74
155
  return true;
75
156
  } catch {
76
157
  return false;
@@ -79,30 +160,38 @@ function fileExists(filePath) {
79
160
  function caFingerprint(stateDir) {
80
161
  const caCertPath = path.join(stateDir, CA_CERT_FILE);
81
162
  try {
82
- const pem = fs.readFileSync(caCertPath);
83
- return crypto.createHash("sha256").update(pem).digest("hex");
163
+ const pem = fs2.readFileSync(caCertPath);
164
+ return crypto2.createHash("sha256").update(pem).digest("hex");
84
165
  } catch {
85
166
  return null;
86
167
  }
87
168
  }
88
169
  function readTrustMarker(stateDir) {
89
170
  try {
90
- const value = fs.readFileSync(path.join(stateDir, CA_TRUST_MARKER), "utf-8").trim();
171
+ const value = fs2.readFileSync(path.join(stateDir, CA_TRUST_MARKER), "utf-8").trim();
91
172
  return value || null;
92
173
  } catch {
93
174
  return null;
94
175
  }
95
176
  }
177
+ function clearTrustRefreshPending(stateDir) {
178
+ try {
179
+ fs2.unlinkSync(path.join(stateDir, CA_TRUST_REFRESH_PENDING));
180
+ } catch {
181
+ }
182
+ }
96
183
  function writeTrustMarker(stateDir) {
97
184
  const fp = caFingerprint(stateDir);
98
185
  if (fp) {
99
- fs.writeFileSync(path.join(stateDir, CA_TRUST_MARKER), fp + "\n");
186
+ clearTrustRefreshPending(stateDir);
187
+ const marker = isWSL() ? `wsl:${fp}` : fp;
188
+ fs2.writeFileSync(path.join(stateDir, CA_TRUST_MARKER), marker + "\n");
100
189
  fixOwnership(path.join(stateDir, CA_TRUST_MARKER));
101
190
  }
102
191
  }
103
192
  function clearTrustMarker(stateDir) {
104
193
  try {
105
- fs.unlinkSync(path.join(stateDir, CA_TRUST_MARKER));
194
+ fs2.unlinkSync(path.join(stateDir, CA_TRUST_MARKER));
106
195
  } catch {
107
196
  }
108
197
  }
@@ -142,8 +231,8 @@ function opensslErrorMessage() {
142
231
  }
143
232
  function isCertValid(certPath) {
144
233
  try {
145
- const pem = fs.readFileSync(certPath, "utf-8");
146
- const cert = new crypto.X509Certificate(pem);
234
+ const pem = fs2.readFileSync(certPath, "utf-8");
235
+ const cert = new crypto2.X509Certificate(pem);
147
236
  const expiry = new Date(cert.validTo).getTime();
148
237
  return Date.now() + EXPIRY_BUFFER_MS < expiry;
149
238
  } catch {
@@ -172,7 +261,7 @@ function isCertSignatureStrong(certPath) {
172
261
  function openssl(args, options) {
173
262
  try {
174
263
  const extraEnv = getOpensslEnv();
175
- return execFileSync("openssl", args, {
264
+ return execFileSync2("openssl", args, {
176
265
  encoding: "utf-8",
177
266
  timeout: OPENSSL_TIMEOUT_MS,
178
267
  input: options?.input,
@@ -225,8 +314,8 @@ function generateCA(stateDir) {
225
314
  "-addext",
226
315
  "keyUsage=critical,keyCertSign,cRLSign"
227
316
  ]);
228
- fs.chmodSync(keyPath, 384);
229
- fs.chmodSync(certPath, 420);
317
+ fs2.chmodSync(keyPath, 384);
318
+ fs2.chmodSync(certPath, 420);
230
319
  fixOwnership(keyPath, certPath);
231
320
  clearTrustMarker(stateDir);
232
321
  return { certPath, keyPath };
@@ -241,7 +330,7 @@ function generateServerCert(stateDir) {
241
330
  openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
242
331
  openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
243
332
  const sans = ["DNS:localhost", "DNS:*.localhost", "DNS:*.local"];
244
- fs.writeFileSync(
333
+ fs2.writeFileSync(
245
334
  extPath,
246
335
  [
247
336
  "authorityKeyIdentifier=keyid,issuer",
@@ -253,9 +342,9 @@ function generateServerCert(stateDir) {
253
342
  );
254
343
  const srlPath = path.join(stateDir, "ca.srl");
255
344
  if (!fileExists(srlPath)) {
256
- fs.writeFileSync(
345
+ fs2.writeFileSync(
257
346
  srlPath,
258
- crypto.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
347
+ crypto2.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
259
348
  );
260
349
  }
261
350
  openssl([
@@ -279,12 +368,12 @@ function generateServerCert(stateDir) {
279
368
  ]);
280
369
  for (const tmp of [csrPath, extPath]) {
281
370
  try {
282
- fs.unlinkSync(tmp);
371
+ fs2.unlinkSync(tmp);
283
372
  } catch {
284
373
  }
285
374
  }
286
- fs.chmodSync(serverKeyPath, 384);
287
- fs.chmodSync(serverCertPath, 420);
375
+ fs2.chmodSync(serverKeyPath, 384);
376
+ fs2.chmodSync(serverCertPath, 420);
288
377
  fixOwnership(serverKeyPath, serverCertPath);
289
378
  return { certPath: serverCertPath, keyPath: serverKeyPath };
290
379
  }
@@ -315,36 +404,30 @@ function isCATrusted(stateDir) {
315
404
  const marker = readTrustMarker(stateDir);
316
405
  if (marker) {
317
406
  const fp = caFingerprint(stateDir);
318
- if (fp && marker === fp) return true;
407
+ const expected = fp && isWSL() ? `wsl:${fp}` : fp;
408
+ if (expected && marker === expected) return true;
319
409
  }
320
410
  if (process.platform === "darwin") {
321
411
  return isCATrustedMacOS(caCertPath);
322
412
  } else if (process.platform === "linux") {
323
- return isCATrustedLinux(stateDir);
413
+ if (!isCATrustedLinux(stateDir)) return false;
414
+ if (!isWSL()) return true;
415
+ try {
416
+ return isWindowsCATrusted(caCertPath, wslWindowsCAStoreOptions());
417
+ } catch {
418
+ return false;
419
+ }
324
420
  } else if (process.platform === "win32") {
325
- return isCATrustedWindows(caCertPath);
421
+ return isWindowsCATrusted(caCertPath);
326
422
  }
327
423
  return false;
328
424
  }
329
- function isCATrustedWindows(caCertPath) {
330
- try {
331
- const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
332
- const result = execFileSync("certutil", ["-store", "-user", "Root"], {
333
- encoding: "utf-8",
334
- timeout: 1e4,
335
- stdio: ["pipe", "pipe", "pipe"]
336
- });
337
- return result.replace(/\s/g, "").toLowerCase().includes(fingerprint);
338
- } catch {
339
- return false;
340
- }
341
- }
342
425
  function isCATrustedMacOS(caCertPath) {
343
426
  try {
344
427
  const isRoot = (process.getuid?.() ?? -1) === 0;
345
428
  const sudoUser = process.env.SUDO_USER;
346
429
  if (isRoot && sudoUser) {
347
- execFileSync(
430
+ execFileSync2(
348
431
  "sudo",
349
432
  ["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
350
433
  {
@@ -353,7 +436,7 @@ function isCATrustedMacOS(caCertPath) {
353
436
  }
354
437
  );
355
438
  } else {
356
- execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
439
+ execFileSync2("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
357
440
  stdio: "pipe",
358
441
  timeout: MACOS_SECURITY_TIMEOUT_MS
359
442
  });
@@ -365,7 +448,7 @@ function isCATrustedMacOS(caCertPath) {
365
448
  }
366
449
  function loginKeychainPath() {
367
450
  try {
368
- const result = execFileSync("security", ["default-keychain"], {
451
+ const result = execFileSync2("security", ["default-keychain"], {
369
452
  encoding: "utf-8",
370
453
  timeout: MACOS_SECURITY_TIMEOUT_MS
371
454
  }).trim();
@@ -396,7 +479,7 @@ var LINUX_CA_TRUST_CONFIGS = {
396
479
  };
397
480
  function detectLinuxDistro() {
398
481
  try {
399
- const osRelease = fs.readFileSync("/etc/os-release", "utf-8").toLowerCase();
482
+ const osRelease = fs2.readFileSync("/etc/os-release", "utf-8").toLowerCase();
400
483
  if (osRelease.includes("arch")) return "arch";
401
484
  if (osRelease.includes("fedora") || osRelease.includes("rhel") || osRelease.includes("centos"))
402
485
  return "fedora";
@@ -406,8 +489,8 @@ function detectLinuxDistro() {
406
489
  }
407
490
  for (const [distro, config] of Object.entries(LINUX_CA_TRUST_CONFIGS)) {
408
491
  try {
409
- execFileSync("which", [config.updateCommand], { stdio: "pipe", timeout: 5e3 });
410
- if (fs.existsSync(path.dirname(config.certDir))) return distro;
492
+ execFileSync2("which", [config.updateCommand], { stdio: "pipe", timeout: 5e3 });
493
+ if (fs2.existsSync(path.dirname(config.certDir))) return distro;
411
494
  } catch {
412
495
  }
413
496
  }
@@ -417,13 +500,12 @@ function getLinuxCATrustConfig() {
417
500
  const distro = detectLinuxDistro();
418
501
  return LINUX_CA_TRUST_CONFIGS[distro ?? "debian"];
419
502
  }
420
- function isCATrustedLinux(stateDir) {
421
- const config = getLinuxCATrustConfig();
503
+ function isCATrustedLinux(stateDir, config = getLinuxCATrustConfig()) {
422
504
  const systemCertPath = path.join(config.certDir, "portless-ca.crt");
423
505
  if (!fileExists(systemCertPath)) return false;
424
506
  try {
425
- const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
426
- const installed = fs.readFileSync(systemCertPath, "utf-8").trim();
507
+ const ours = fs2.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
508
+ const installed = fs2.readFileSync(systemCertPath, "utf-8").trim();
427
509
  return ours === installed;
428
510
  } catch {
429
511
  return false;
@@ -438,8 +520,8 @@ async function generateHostCertAsync(stateDir, hostname) {
438
520
  const caKeyPath = path.join(stateDir, CA_KEY_FILE);
439
521
  const caCertPath = path.join(stateDir, CA_CERT_FILE);
440
522
  const hostDir = path.join(stateDir, HOST_CERTS_DIR);
441
- if (!fs.existsSync(hostDir)) {
442
- await fs.promises.mkdir(hostDir, { recursive: true, mode: 493 });
523
+ if (!fs2.existsSync(hostDir)) {
524
+ await fs2.promises.mkdir(hostDir, { recursive: true, mode: 493 });
443
525
  fixOwnership(hostDir);
444
526
  }
445
527
  const safeName = sanitizeHostForFilename(hostname);
@@ -455,7 +537,7 @@ async function generateHostCertAsync(stateDir, hostname) {
455
537
  if (parts.length >= 2) {
456
538
  sans.push(`DNS:*.${parts.slice(1).join(".")}`);
457
539
  }
458
- await fs.promises.writeFile(
540
+ await fs2.promises.writeFile(
459
541
  extPath,
460
542
  [
461
543
  "authorityKeyIdentifier=keyid,issuer",
@@ -466,10 +548,10 @@ async function generateHostCertAsync(stateDir, hostname) {
466
548
  ].join("\n") + "\n"
467
549
  );
468
550
  const srlPath = path.join(stateDir, "ca.srl");
469
- if (!fs.existsSync(srlPath)) {
470
- await fs.promises.writeFile(
551
+ if (!fs2.existsSync(srlPath)) {
552
+ await fs2.promises.writeFile(
471
553
  srlPath,
472
- crypto.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
554
+ crypto2.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
473
555
  );
474
556
  }
475
557
  await opensslAsync([
@@ -493,12 +575,12 @@ async function generateHostCertAsync(stateDir, hostname) {
493
575
  ]);
494
576
  for (const tmp of [csrPath, extPath]) {
495
577
  try {
496
- await fs.promises.unlink(tmp);
578
+ await fs2.promises.unlink(tmp);
497
579
  } catch {
498
580
  }
499
581
  }
500
- await fs.promises.chmod(keyPath, 384);
501
- await fs.promises.chmod(certPath, 420);
582
+ await fs2.promises.chmod(keyPath, 384);
583
+ await fs2.promises.chmod(certPath, 420);
502
584
  fixOwnership(keyPath, certPath);
503
585
  return { certPath, keyPath };
504
586
  }
@@ -525,10 +607,10 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost"
525
607
  const keyPath = path.join(hostDir, `${safeName}-key.pem`);
526
608
  if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath) && isCertSignatureStrong(certPath)) {
527
609
  try {
528
- const hostCert = fs.readFileSync(certPath);
610
+ const hostCert = fs2.readFileSync(certPath);
529
611
  const ctx = tls.createSecureContext({
530
612
  cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
531
- key: fs.readFileSync(keyPath)
613
+ key: fs2.readFileSync(keyPath)
532
614
  });
533
615
  cache.set(servername, ctx);
534
616
  cb(null, ctx);
@@ -542,8 +624,8 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost"
542
624
  }
543
625
  const promise = generateHostCertAsync(stateDir, servername).then(async (generated) => {
544
626
  const [hostCert, key] = await Promise.all([
545
- fs.promises.readFile(generated.certPath),
546
- fs.promises.readFile(generated.keyPath)
627
+ fs2.promises.readFile(generated.certPath),
628
+ fs2.promises.readFile(generated.keyPath)
547
629
  ]);
548
630
  return tls.createSecureContext({
549
631
  cert: caCert ? Buffer.concat([hostCert, caCert]) : hostCert,
@@ -573,7 +655,7 @@ function trustCA(stateDir) {
573
655
  if (process.platform === "darwin") {
574
656
  const isRoot = (process.getuid?.() ?? -1) === 0;
575
657
  if (isRoot) {
576
- execFileSync(
658
+ execFileSync2(
577
659
  "security",
578
660
  [
579
661
  "add-trusted-cert",
@@ -588,7 +670,7 @@ function trustCA(stateDir) {
588
670
  );
589
671
  } else {
590
672
  const keychain = loginKeychainPath();
591
- execFileSync(
673
+ execFileSync2(
592
674
  "security",
593
675
  ["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
594
676
  { stdio: "pipe", timeout: MACOS_SECURITY_AUTH_TIMEOUT_MS }
@@ -598,19 +680,19 @@ function trustCA(stateDir) {
598
680
  return { trusted: true };
599
681
  } else if (process.platform === "linux") {
600
682
  const config = getLinuxCATrustConfig();
601
- if (!fs.existsSync(config.certDir)) {
602
- fs.mkdirSync(config.certDir, { recursive: true });
683
+ if (!fs2.existsSync(config.certDir)) {
684
+ fs2.mkdirSync(config.certDir, { recursive: true });
603
685
  }
604
686
  const dest = path.join(config.certDir, "portless-ca.crt");
605
- fs.copyFileSync(caCertPath, dest);
606
- execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
687
+ fs2.copyFileSync(caCertPath, dest);
688
+ execFileSync2(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
689
+ if (isWSL()) {
690
+ trustWindowsCA(caCertPath, wslWindowsCAStoreOptions());
691
+ }
607
692
  writeTrustMarker(stateDir);
608
693
  return { trusted: true };
609
694
  } else if (process.platform === "win32") {
610
- execFileSync("certutil", ["-addstore", "-user", "Root", caCertPath], {
611
- stdio: "pipe",
612
- timeout: 3e4
613
- });
695
+ trustWindowsCA(caCertPath);
614
696
  writeTrustMarker(stateDir);
615
697
  return { trusted: true };
616
698
  }
@@ -636,18 +718,15 @@ function untrustCA(stateDir) {
636
718
  clearTrustMarker(stateDir);
637
719
  return { removed: true };
638
720
  }
639
- if (!isCATrusted(stateDir)) {
640
- clearTrustMarker(stateDir);
641
- return { removed: true };
642
- }
721
+ const runningInWSL = isWSL();
643
722
  try {
644
723
  let result;
645
724
  if (process.platform === "darwin") {
646
725
  result = untrustCAMacOS(caCertPath);
647
726
  } else if (process.platform === "linux") {
648
- result = untrustCALinux(stateDir);
727
+ result = runningInWSL ? untrustCAWSL(stateDir, caCertPath) : untrustCALinux(stateDir);
649
728
  } else if (process.platform === "win32") {
650
- result = untrustCAWindows(caCertPath);
729
+ result = untrustWindowsCA(caCertPath);
651
730
  } else {
652
731
  result = { removed: false, error: `Unsupported platform: ${process.platform}` };
653
732
  }
@@ -662,7 +741,7 @@ function untrustCAMacOS(caCertPath) {
662
741
  const errors = [];
663
742
  const tryExec = (args) => {
664
743
  try {
665
- execFileSync("security", args, { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS });
744
+ execFileSync2("security", args, { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS });
666
745
  return true;
667
746
  } catch (err) {
668
747
  const message = err instanceof Error ? err.message : String(err);
@@ -684,13 +763,13 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
684
763
  const isRoot = (process.getuid?.() ?? -1) === 0;
685
764
  const sudoUser = process.env.SUDO_USER;
686
765
  if (isRoot && sudoUser) {
687
- execFileSync(
766
+ execFileSync2(
688
767
  "sudo",
689
768
  ["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
690
769
  { stdio: "pipe", timeout: MACOS_SECURITY_TIMEOUT_MS }
691
770
  );
692
771
  } else {
693
- execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
772
+ execFileSync2("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
694
773
  stdio: "pipe",
695
774
  timeout: MACOS_SECURITY_TIMEOUT_MS
696
775
  });
@@ -700,63 +779,67 @@ function isCATrustedMacOSAfterAttempt(caCertPath) {
700
779
  return false;
701
780
  }
702
781
  }
703
- function untrustCALinux(stateDir) {
782
+ function untrustCALinux(stateDir, options = {}) {
704
783
  const errors = [];
705
- let deletedAny = false;
706
- for (const config of Object.values(LINUX_CA_TRUST_CONFIGS)) {
784
+ const pendingRefreshPath = path.join(stateDir, CA_TRUST_REFRESH_PENDING);
785
+ let refreshNeeded = fileExists(pendingRefreshPath);
786
+ const configs = options.configs ?? Object.values(LINUX_CA_TRUST_CONFIGS);
787
+ const activeConfig = options.activeConfig ?? getLinuxCATrustConfig();
788
+ const runUpdate = options.runUpdate ?? ((command) => {
789
+ execFileSync2(command, [], { stdio: "pipe", timeout: 3e4 });
790
+ });
791
+ for (const config of configs) {
707
792
  const dest = path.join(config.certDir, "portless-ca.crt");
708
793
  try {
709
794
  if (fileExists(dest)) {
710
- const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
711
- const installed = fs.readFileSync(dest, "utf-8").trim();
795
+ const ours = fs2.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
796
+ const installed = fs2.readFileSync(dest, "utf-8").trim();
712
797
  if (ours === installed) {
713
- fs.unlinkSync(dest);
714
- deletedAny = true;
798
+ if (!refreshNeeded) {
799
+ fs2.writeFileSync(pendingRefreshPath, "1\n");
800
+ fixOwnership(pendingRefreshPath);
801
+ refreshNeeded = true;
802
+ }
803
+ fs2.unlinkSync(dest);
715
804
  }
716
805
  }
717
806
  } catch (err) {
718
807
  errors.push(err instanceof Error ? err.message : String(err));
719
808
  }
720
809
  }
721
- if (deletedAny) {
810
+ if (refreshNeeded) {
722
811
  try {
723
- const config = getLinuxCATrustConfig();
724
- execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
812
+ runUpdate(activeConfig.updateCommand);
725
813
  } catch (err) {
726
814
  errors.push(err instanceof Error ? err.message : String(err));
727
815
  }
728
816
  }
729
- if (isCATrusted(stateDir)) {
817
+ if (errors.length > 0 || isCATrustedLinux(stateDir, activeConfig)) {
730
818
  return {
731
819
  removed: false,
732
820
  error: errors.join("; ") || "CA still trusted (remove portless-ca.crt and run the distro CA update command, often with sudo)"
733
821
  };
734
822
  }
823
+ clearTrustRefreshPending(stateDir);
735
824
  return { removed: true };
736
825
  }
737
- function untrustCAWindows(caCertPath) {
826
+ function untrustCAWSL(stateDir, caCertPath) {
827
+ const linuxResult = untrustCALinux(stateDir);
828
+ let windowsResult;
738
829
  try {
739
- const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
740
- const storeListing = execFileSync("certutil", ["-store", "-user", "Root"], {
741
- encoding: "utf-8",
742
- timeout: 1e4,
743
- stdio: ["pipe", "pipe", "pipe"]
744
- });
745
- const normalized = storeListing.replace(/\s/g, "").toLowerCase();
746
- if (!normalized.includes(fingerprint)) {
747
- return { removed: true };
748
- }
749
- execFileSync("certutil", ["-delstore", "-user", "Root", "portless Local CA"], {
750
- stdio: "pipe",
751
- timeout: 3e4
752
- });
753
- if (isCATrustedWindows(caCertPath)) {
754
- return { removed: false, error: "certutil could not remove the portless CA from Root" };
755
- }
756
- return { removed: true };
830
+ windowsResult = untrustWindowsCA(caCertPath, wslWindowsCAStoreOptions());
757
831
  } catch (err) {
758
- return { removed: false, error: err instanceof Error ? err.message : String(err) };
832
+ windowsResult = {
833
+ removed: false,
834
+ error: err instanceof Error ? err.message : String(err)
835
+ };
759
836
  }
837
+ if (linuxResult.removed && windowsResult.removed) return { removed: true };
838
+ const errors = [linuxResult.error, windowsResult.error].filter(Boolean);
839
+ return {
840
+ removed: false,
841
+ error: errors.join("; ") || "Could not remove the portless CA from every WSL trust store"
842
+ };
760
843
  }
761
844
 
762
845
  // src/tailscale.ts
@@ -765,7 +848,7 @@ var TAILSCALE_BINARY = "tailscale";
765
848
  var TAILSCALE_COMMAND_TIMEOUT_MS = 3e4;
766
849
  var PREFERRED_SERVE_PORTS = [443, 8443, 8444, 8445, 8446, 8447, 8448, 8449, 8450];
767
850
  var FUNNEL_PORTS = [443, 8443, 1e4];
768
- function defaultRunner(args) {
851
+ function defaultRunner2(args) {
769
852
  const result = spawnSync(TAILSCALE_BINARY, args, {
770
853
  encoding: "utf-8",
771
854
  killSignal: "SIGKILL",
@@ -857,7 +940,7 @@ function throwFunnelNotEnabled(status) {
857
940
  );
858
941
  }
859
942
  function ensureTailscaleReady(options = {}) {
860
- const runner = options.runner ?? defaultRunner;
943
+ const runner = options.runner ?? defaultRunner2;
861
944
  runOrThrow(["version"], "check tailscale version", runner);
862
945
  const statusResult = runOrThrow(["status", "--json"], "read tailscale status", runner);
863
946
  const status = parseStatusJson(statusResult.stdout);
@@ -873,7 +956,7 @@ function ensureTailscaleReady(options = {}) {
873
956
  baseUrl: `https://${dnsName}`
874
957
  };
875
958
  }
876
- function getUsedServePorts(runner = defaultRunner) {
959
+ function getUsedServePorts(runner = defaultRunner2) {
877
960
  const result = runner(["serve", "status", "--json"]);
878
961
  if (result.error || result.status !== 0) {
879
962
  return /* @__PURE__ */ new Set();
@@ -969,7 +1052,7 @@ function register(mode, localPort, httpsPort, runner) {
969
1052
  }
970
1053
  }
971
1054
  function unregister(mode, httpsPort, options) {
972
- const runner = options?.runner ?? defaultRunner;
1055
+ const runner = options?.runner ?? defaultRunner2;
973
1056
  const result = runner([mode, "--yes", `--https=${httpsPort}`, "off"]);
974
1057
  if (result.error) {
975
1058
  const errno = result.error;
@@ -988,10 +1071,10 @@ ${result.stdout}`.toLowerCase();
988
1071
  }
989
1072
  }
990
1073
  function registerServe(localPort, httpsPort, options) {
991
- register("serve", localPort, httpsPort, options?.runner ?? defaultRunner);
1074
+ register("serve", localPort, httpsPort, options?.runner ?? defaultRunner2);
992
1075
  }
993
1076
  function registerFunnel(localPort, httpsPort, options) {
994
- register("funnel", localPort, httpsPort, options?.runner ?? defaultRunner);
1077
+ register("funnel", localPort, httpsPort, options?.runner ?? defaultRunner2);
995
1078
  }
996
1079
  function unregisterTailscale(route) {
997
1080
  if (!route.tailscaleHttpsPort) return;
@@ -1016,7 +1099,7 @@ function defaultSpawner(args) {
1016
1099
  windowsHide: true
1017
1100
  });
1018
1101
  }
1019
- function defaultRunner2(args) {
1102
+ function defaultRunner3(args) {
1020
1103
  const result = spawnSync2(NGROK_BINARY, args, {
1021
1104
  encoding: "utf-8",
1022
1105
  killSignal: "SIGKILL",
@@ -1053,7 +1136,7 @@ function formatOutputError(output) {
1053
1136
  `Failed to start ngrok tunnel: ${details || "ngrok exited before printing a public URL"}`
1054
1137
  );
1055
1138
  }
1056
- function ensureNgrokAvailable(runner = defaultRunner2) {
1139
+ function ensureNgrokAvailable(runner = defaultRunner3) {
1057
1140
  const result = runner(["version"]);
1058
1141
  if (result.error) {
1059
1142
  throw formatSpawnError(result.error);
@@ -1178,8 +1261,8 @@ function stopNgrok(route) {
1178
1261
 
1179
1262
  // src/auto.ts
1180
1263
  import { createHash as createHash2 } from "crypto";
1181
- import { execFileSync as execFileSync2 } from "child_process";
1182
- import * as fs2 from "fs";
1264
+ import { execFileSync as execFileSync3 } from "child_process";
1265
+ import * as fs3 from "fs";
1183
1266
  import * as path2 from "path";
1184
1267
  var MAX_DNS_LABEL_LENGTH = 63;
1185
1268
  function truncateLabel(label) {
@@ -1219,7 +1302,7 @@ function findPackageJsonName(startDir) {
1219
1302
  for (; ; ) {
1220
1303
  const pkgPath = path2.join(dir, "package.json");
1221
1304
  try {
1222
- const raw = fs2.readFileSync(pkgPath, "utf-8");
1305
+ const raw = fs3.readFileSync(pkgPath, "utf-8");
1223
1306
  const pkg = JSON.parse(raw);
1224
1307
  if (typeof pkg.name === "string" && pkg.name) {
1225
1308
  return pkg.name.replace(/^@[^/]+\//, "");
@@ -1234,7 +1317,7 @@ function findPackageJsonName(startDir) {
1234
1317
  }
1235
1318
  function findGitRoot(startDir) {
1236
1319
  try {
1237
- const toplevel = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
1320
+ const toplevel = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
1238
1321
  cwd: startDir,
1239
1322
  encoding: "utf-8",
1240
1323
  timeout: 5e3,
@@ -1247,7 +1330,7 @@ function findGitRoot(startDir) {
1247
1330
  for (; ; ) {
1248
1331
  const gitPath = path2.join(dir, ".git");
1249
1332
  try {
1250
- const stat = fs2.statSync(gitPath);
1333
+ const stat = fs3.statSync(gitPath);
1251
1334
  if (stat.isDirectory()) return dir;
1252
1335
  if (stat.isFile()) return dir;
1253
1336
  } catch {
@@ -1258,6 +1341,9 @@ function findGitRoot(startDir) {
1258
1341
  }
1259
1342
  return null;
1260
1343
  }
1344
+ function applyWorktreePrefix(baseName, worktree) {
1345
+ return worktree ? `${worktree.prefix}.${baseName}` : baseName;
1346
+ }
1261
1347
  var DEFAULT_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
1262
1348
  function branchToPrefix(branch) {
1263
1349
  if (!branch || branch === "HEAD" || DEFAULT_BRANCHES.has(branch)) return null;
@@ -1272,7 +1358,7 @@ function detectWorktreePrefix(cwd = process.cwd()) {
1272
1358
  }
1273
1359
  function detectWorktreeViaCli(cwd) {
1274
1360
  try {
1275
- const listOutput = execFileSync2("git", ["worktree", "list", "--porcelain"], {
1361
+ const listOutput = execFileSync3("git", ["worktree", "list", "--porcelain"], {
1276
1362
  cwd,
1277
1363
  encoding: "utf-8",
1278
1364
  timeout: 5e3,
@@ -1282,7 +1368,7 @@ function detectWorktreeViaCli(cwd) {
1282
1368
  if (worktreeCount <= 1) return null;
1283
1369
  const gitDir = path2.resolve(
1284
1370
  cwd,
1285
- execFileSync2("git", ["rev-parse", "--git-dir"], {
1371
+ execFileSync3("git", ["rev-parse", "--git-dir"], {
1286
1372
  cwd,
1287
1373
  encoding: "utf-8",
1288
1374
  timeout: 5e3,
@@ -1291,7 +1377,7 @@ function detectWorktreeViaCli(cwd) {
1291
1377
  );
1292
1378
  const gitCommonDir = path2.resolve(
1293
1379
  cwd,
1294
- execFileSync2("git", ["rev-parse", "--git-common-dir"], {
1380
+ execFileSync3("git", ["rev-parse", "--git-common-dir"], {
1295
1381
  cwd,
1296
1382
  encoding: "utf-8",
1297
1383
  timeout: 5e3,
@@ -1299,7 +1385,7 @@ function detectWorktreeViaCli(cwd) {
1299
1385
  }).trim()
1300
1386
  );
1301
1387
  if (gitDir === gitCommonDir) return null;
1302
- const branch = execFileSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1388
+ const branch = execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1303
1389
  cwd,
1304
1390
  encoding: "utf-8",
1305
1391
  timeout: 5e3,
@@ -1317,12 +1403,12 @@ function detectWorktreeViaFilesystem(startDir) {
1317
1403
  for (; ; ) {
1318
1404
  const gitPath = path2.join(dir, ".git");
1319
1405
  try {
1320
- const stat = fs2.statSync(gitPath);
1406
+ const stat = fs3.statSync(gitPath);
1321
1407
  if (stat.isDirectory()) {
1322
1408
  return null;
1323
1409
  }
1324
1410
  if (stat.isFile()) {
1325
- const content = fs2.readFileSync(gitPath, "utf-8").trim();
1411
+ const content = fs3.readFileSync(gitPath, "utf-8").trim();
1326
1412
  const match = content.match(/^gitdir:\s*(.+)$/);
1327
1413
  if (!match) return null;
1328
1414
  const gitdir = match[1];
@@ -1342,7 +1428,7 @@ function detectWorktreeViaFilesystem(startDir) {
1342
1428
  }
1343
1429
  function readBranchFromHead(gitdir) {
1344
1430
  try {
1345
- const head = fs2.readFileSync(path2.join(gitdir, "HEAD"), "utf-8").trim();
1431
+ const head = fs3.readFileSync(path2.join(gitdir, "HEAD"), "utf-8").trim();
1346
1432
  const refMatch = head.match(/^ref: refs\/heads\/(.+)$/);
1347
1433
  return refMatch ? refMatch[1] : null;
1348
1434
  } catch {
@@ -1351,11 +1437,11 @@ function readBranchFromHead(gitdir) {
1351
1437
  }
1352
1438
 
1353
1439
  // src/cli-utils.ts
1354
- import * as fs3 from "fs";
1440
+ import * as fs4 from "fs";
1355
1441
  import * as http from "http";
1356
1442
  import * as https from "https";
1357
1443
  import * as net from "net";
1358
- import * as os from "os";
1444
+ import * as os2 from "os";
1359
1445
  import * as path3 from "path";
1360
1446
  import * as readline from "readline";
1361
1447
  import { execSync, spawn as spawn2 } from "child_process";
@@ -1364,8 +1450,8 @@ var FALLBACK_PROXY_PORT = 1355;
1364
1450
  var PRIVILEGED_PORT_THRESHOLD = 1024;
1365
1451
  var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
1366
1452
  var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
1367
- var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(os.tmpdir(), "portless") : "/tmp/portless";
1368
- var USER_STATE_DIR = path3.join(os.homedir(), ".portless");
1453
+ var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(os2.tmpdir(), "portless") : "/tmp/portless";
1454
+ var USER_STATE_DIR = path3.join(resolveUserHome(), ".portless");
1369
1455
  var MIN_APP_PORT = 4e3;
1370
1456
  var MAX_APP_PORT = 4999;
1371
1457
  var RANDOM_PORT_ATTEMPTS = 50;
@@ -1500,7 +1586,7 @@ function resolveStateDir(_port) {
1500
1586
  }
1501
1587
  function readPortFromDir(dir) {
1502
1588
  try {
1503
- const raw = fs3.readFileSync(path3.join(dir, "proxy.port"), "utf-8").trim();
1589
+ const raw = fs4.readFileSync(path3.join(dir, "proxy.port"), "utf-8").trim();
1504
1590
  const port = parseInt(raw, 10);
1505
1591
  return isNaN(port) ? null : port;
1506
1592
  } catch {
@@ -1511,7 +1597,7 @@ var TLS_MARKER_FILE = "proxy.tls";
1511
1597
  var CUSTOM_CERT_MARKER_FILE = "proxy.custom-cert";
1512
1598
  function readTlsMarker(dir) {
1513
1599
  try {
1514
- return fs3.existsSync(path3.join(dir, TLS_MARKER_FILE));
1600
+ return fs4.existsSync(path3.join(dir, TLS_MARKER_FILE));
1515
1601
  } catch {
1516
1602
  return false;
1517
1603
  }
@@ -1519,17 +1605,17 @@ function readTlsMarker(dir) {
1519
1605
  function writeTlsMarker(dir, enabled2) {
1520
1606
  const markerPath = path3.join(dir, TLS_MARKER_FILE);
1521
1607
  if (enabled2) {
1522
- fs3.writeFileSync(markerPath, "1", { mode: 420 });
1608
+ fs4.writeFileSync(markerPath, "1", { mode: 420 });
1523
1609
  } else {
1524
1610
  try {
1525
- fs3.unlinkSync(markerPath);
1611
+ fs4.unlinkSync(markerPath);
1526
1612
  } catch {
1527
1613
  }
1528
1614
  }
1529
1615
  }
1530
1616
  function readCustomCertMarker(dir) {
1531
1617
  try {
1532
- return fs3.existsSync(path3.join(dir, CUSTOM_CERT_MARKER_FILE));
1618
+ return fs4.existsSync(path3.join(dir, CUSTOM_CERT_MARKER_FILE));
1533
1619
  } catch {
1534
1620
  return false;
1535
1621
  }
@@ -1537,10 +1623,10 @@ function readCustomCertMarker(dir) {
1537
1623
  function writeCustomCertMarker(dir, enabled2) {
1538
1624
  const markerPath = path3.join(dir, CUSTOM_CERT_MARKER_FILE);
1539
1625
  if (enabled2) {
1540
- fs3.writeFileSync(markerPath, "1", { mode: 420 });
1626
+ fs4.writeFileSync(markerPath, "1", { mode: 420 });
1541
1627
  } else {
1542
1628
  try {
1543
- fs3.unlinkSync(markerPath);
1629
+ fs4.unlinkSync(markerPath);
1544
1630
  } catch {
1545
1631
  }
1546
1632
  }
@@ -1548,7 +1634,7 @@ function writeCustomCertMarker(dir, enabled2) {
1548
1634
  var LAN_MARKER_FILE = "proxy.lan";
1549
1635
  function readLanMarker(dir) {
1550
1636
  try {
1551
- const raw = fs3.readFileSync(path3.join(dir, LAN_MARKER_FILE), "utf-8").trim();
1637
+ const raw = fs4.readFileSync(path3.join(dir, LAN_MARKER_FILE), "utf-8").trim();
1552
1638
  return raw || null;
1553
1639
  } catch {
1554
1640
  return null;
@@ -1558,11 +1644,11 @@ function writeLanMarker(dir, ip) {
1558
1644
  const markerPath = path3.join(dir, LAN_MARKER_FILE);
1559
1645
  if (!ip) {
1560
1646
  try {
1561
- fs3.unlinkSync(markerPath);
1647
+ fs4.unlinkSync(markerPath);
1562
1648
  } catch {
1563
1649
  }
1564
1650
  } else {
1565
- fs3.writeFileSync(markerPath, ip, { mode: 420 });
1651
+ fs4.writeFileSync(markerPath, ip, { mode: 420 });
1566
1652
  }
1567
1653
  }
1568
1654
  var DEFAULT_TLD = "localhost";
@@ -1606,7 +1692,7 @@ var TLD_FILE = "proxy.tld";
1606
1692
  var TLDS_FILE = "proxy.tlds";
1607
1693
  function readLegacyTldFromDir(dir) {
1608
1694
  try {
1609
- const raw = fs3.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
1695
+ const raw = fs4.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
1610
1696
  return raw || DEFAULT_TLD;
1611
1697
  } catch {
1612
1698
  return DEFAULT_TLD;
@@ -1614,7 +1700,7 @@ function readLegacyTldFromDir(dir) {
1614
1700
  }
1615
1701
  function readTldsFromDir(dir) {
1616
1702
  try {
1617
- const raw = fs3.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
1703
+ const raw = fs4.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
1618
1704
  const parsed = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\r?\n/).flatMap((line) => line.split(",")).map((line) => line.trim()).filter(Boolean);
1619
1705
  if (!Array.isArray(parsed)) return [readLegacyTldFromDir(dir)];
1620
1706
  const tlds = parsed.flatMap((value) => typeof value === "string" ? parseTldList(value) : []);
@@ -1629,16 +1715,16 @@ function writeTldsFile(dir, tlds) {
1629
1715
  const tldPath = path3.join(dir, TLD_FILE);
1630
1716
  if (uniqueTlds.length === 1 && uniqueTlds[0] === DEFAULT_TLD) {
1631
1717
  try {
1632
- fs3.unlinkSync(tldsPath);
1718
+ fs4.unlinkSync(tldsPath);
1633
1719
  } catch {
1634
1720
  }
1635
1721
  try {
1636
- fs3.unlinkSync(tldPath);
1722
+ fs4.unlinkSync(tldPath);
1637
1723
  } catch {
1638
1724
  }
1639
1725
  } else {
1640
- fs3.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
1641
- fs3.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
1726
+ fs4.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
1727
+ fs4.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
1642
1728
  }
1643
1729
  }
1644
1730
  function writeTldFile(dir, tld) {
@@ -1868,7 +1954,7 @@ function isProxyRunning(port, tls2 = false) {
1868
1954
  }
1869
1955
  function isPortListening(port) {
1870
1956
  return new Promise((resolve4) => {
1871
- const socket = net.createConnection({ host: "127.0.0.1", port });
1957
+ const socket = createLoopbackConnection(port);
1872
1958
  let settled = false;
1873
1959
  const finish = (result) => {
1874
1960
  if (settled) return;
@@ -1953,7 +2039,7 @@ function collectBinPaths(cwd) {
1953
2039
  let dir = cwd;
1954
2040
  for (; ; ) {
1955
2041
  const bin = path3.join(dir, "node_modules", ".bin");
1956
- if (fs3.existsSync(bin)) {
2042
+ if (fs4.existsSync(bin)) {
1957
2043
  dirs.push(bin);
1958
2044
  }
1959
2045
  const parent = path3.dirname(dir);
@@ -2084,7 +2170,7 @@ function injectFrameworkFlags(commandArgs, port) {
2084
2170
  }
2085
2171
 
2086
2172
  // src/clean-utils.ts
2087
- import * as fs4 from "fs";
2173
+ import * as fs5 from "fs";
2088
2174
  import * as path4 from "path";
2089
2175
  var PORTLESS_STATE_FILES = [
2090
2176
  "routes.json",
@@ -2097,6 +2183,8 @@ var PORTLESS_STATE_FILES = [
2097
2183
  "proxy.tld",
2098
2184
  "proxy.tlds",
2099
2185
  "proxy.lan",
2186
+ "ca.trusted",
2187
+ "ca.trust-refresh-pending",
2100
2188
  "ca-key.pem",
2101
2189
  "ca.pem",
2102
2190
  "server-key.pem",
@@ -2106,28 +2194,38 @@ var PORTLESS_STATE_FILES = [
2106
2194
  "ca.srl"
2107
2195
  ];
2108
2196
  var HOST_CERTS_DIR2 = "host-certs";
2197
+ var CA_IDENTITY_FILES = /* @__PURE__ */ new Set(["ca-key.pem", "ca.pem", "ca.trust-refresh-pending"]);
2109
2198
  function collectStateDirsForCleanup() {
2110
2199
  const dirs = /* @__PURE__ */ new Set();
2111
2200
  const add = (d) => {
2112
2201
  const trimmed = d?.trim();
2113
2202
  if (!trimmed) return;
2114
2203
  const resolved = path4.resolve(trimmed);
2115
- if (fs4.existsSync(resolved)) dirs.add(resolved);
2204
+ if (fs5.existsSync(resolved)) dirs.add(resolved);
2116
2205
  };
2117
2206
  add(USER_STATE_DIR);
2118
2207
  add(LEGACY_SYSTEM_STATE_DIR);
2119
2208
  add(process.env.PORTLESS_STATE_DIR);
2120
2209
  return [...dirs];
2121
2210
  }
2122
- function removePortlessStateFiles(dir) {
2211
+ function attemptCATrustRemovalForCleanup(stateDirs, untrust) {
2212
+ const results = /* @__PURE__ */ new Map();
2213
+ for (const stateDir of stateDirs) {
2214
+ if (!fs5.existsSync(path4.join(stateDir, "ca.pem"))) continue;
2215
+ results.set(stateDir, untrust(stateDir));
2216
+ }
2217
+ return results;
2218
+ }
2219
+ function removePortlessStateFiles(dir, options = {}) {
2123
2220
  for (const f of PORTLESS_STATE_FILES) {
2221
+ if (options.preserveCAIdentity && CA_IDENTITY_FILES.has(f)) continue;
2124
2222
  try {
2125
- fs4.unlinkSync(path4.join(dir, f));
2223
+ fs5.unlinkSync(path4.join(dir, f));
2126
2224
  } catch {
2127
2225
  }
2128
2226
  }
2129
2227
  try {
2130
- fs4.rmSync(path4.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
2228
+ fs5.rmSync(path4.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
2131
2229
  } catch {
2132
2230
  }
2133
2231
  }
@@ -2336,7 +2434,7 @@ function cleanupAll() {
2336
2434
  }
2337
2435
 
2338
2436
  // src/config.ts
2339
- import * as fs5 from "fs";
2437
+ import * as fs6 from "fs";
2340
2438
  import * as path5 from "path";
2341
2439
  var ConfigValidationError = class extends Error {
2342
2440
  constructor(message) {
@@ -2348,7 +2446,7 @@ var CONFIG_FILENAME = "portless.json";
2348
2446
  function loadConfig(cwd = process.cwd()) {
2349
2447
  const configPath = path5.join(cwd, CONFIG_FILENAME);
2350
2448
  try {
2351
- const raw = fs5.readFileSync(configPath, "utf-8");
2449
+ const raw = fs6.readFileSync(configPath, "utf-8");
2352
2450
  const parsed = JSON.parse(raw);
2353
2451
  validateConfig(parsed, configPath);
2354
2452
  return { config: parsed, configDir: cwd };
@@ -2371,7 +2469,7 @@ function normalizePortlessValue(value) {
2371
2469
  function loadConfigFromPackageJson(dir) {
2372
2470
  const pkgPath = path5.join(dir, "package.json");
2373
2471
  try {
2374
- const raw = fs5.readFileSync(pkgPath, "utf-8");
2472
+ const raw = fs6.readFileSync(pkgPath, "utf-8");
2375
2473
  const pkg = JSON.parse(raw);
2376
2474
  if (pkg && typeof pkg === "object" && "portless" in pkg) {
2377
2475
  const config = normalizePortlessValue(pkg.portless);
@@ -2389,7 +2487,7 @@ function loadConfigFromPackageJson(dir) {
2389
2487
  function loadPackagePortlessConfig(dir) {
2390
2488
  const pkgPath = path5.join(dir, "package.json");
2391
2489
  try {
2392
- const raw = fs5.readFileSync(pkgPath, "utf-8");
2490
+ const raw = fs6.readFileSync(pkgPath, "utf-8");
2393
2491
  const pkg = JSON.parse(raw);
2394
2492
  if (pkg && typeof pkg === "object" && "portless" in pkg) {
2395
2493
  const config = normalizePortlessValue(pkg.portless);
@@ -2425,7 +2523,7 @@ function resolveAppConfig(config, configDir, packageDir) {
2425
2523
  function hasScript(scriptName, dir) {
2426
2524
  const pkgPath = path5.join(dir, "package.json");
2427
2525
  try {
2428
- const raw = fs5.readFileSync(pkgPath, "utf-8");
2526
+ const raw = fs6.readFileSync(pkgPath, "utf-8");
2429
2527
  const pkg = JSON.parse(raw);
2430
2528
  return typeof pkg?.scripts?.[scriptName] === "string";
2431
2529
  } catch {
@@ -2444,7 +2542,7 @@ function detectPackageManager(cwd) {
2444
2542
  for (; ; ) {
2445
2543
  const pkgPath = path5.join(dir, "package.json");
2446
2544
  try {
2447
- const raw = fs5.readFileSync(pkgPath, "utf-8");
2545
+ const raw = fs6.readFileSync(pkgPath, "utf-8");
2448
2546
  const pkg = JSON.parse(raw);
2449
2547
  if (typeof pkg.packageManager === "string") {
2450
2548
  const name = pkg.packageManager.split("@")[0];
@@ -2456,7 +2554,7 @@ function detectPackageManager(cwd) {
2456
2554
  }
2457
2555
  for (const [file, pm] of LOCK_FILES) {
2458
2556
  try {
2459
- fs5.accessSync(path5.join(dir, file), fs5.constants.F_OK);
2557
+ fs6.accessSync(path5.join(dir, file), fs6.constants.F_OK);
2460
2558
  return pm;
2461
2559
  } catch {
2462
2560
  }
@@ -2615,13 +2713,13 @@ function warnUnknownKeys(obj, known, configPath, prefix) {
2615
2713
  }
2616
2714
 
2617
2715
  // src/workspace.ts
2618
- import * as fs6 from "fs";
2716
+ import * as fs7 from "fs";
2619
2717
  import * as path6 from "path";
2620
2718
  function findWorkspaceRoot(cwd = process.cwd()) {
2621
2719
  let dir = cwd;
2622
2720
  for (; ; ) {
2623
2721
  try {
2624
- fs6.accessSync(path6.join(dir, "pnpm-workspace.yaml"), fs6.constants.R_OK);
2722
+ fs7.accessSync(path6.join(dir, "pnpm-workspace.yaml"), fs7.constants.R_OK);
2625
2723
  return dir;
2626
2724
  } catch {
2627
2725
  }
@@ -2635,7 +2733,7 @@ function findWorkspaceRoot(cwd = process.cwd()) {
2635
2733
  }
2636
2734
  function detectWorkspaceSource(workspaceRoot) {
2637
2735
  try {
2638
- fs6.accessSync(path6.join(workspaceRoot, "pnpm-workspace.yaml"), fs6.constants.R_OK);
2736
+ fs7.accessSync(path6.join(workspaceRoot, "pnpm-workspace.yaml"), fs7.constants.R_OK);
2639
2737
  return "pnpm";
2640
2738
  } catch {
2641
2739
  }
@@ -2647,7 +2745,7 @@ function detectWorkspaceSource(workspaceRoot) {
2647
2745
  function readWorkspacesFromPackageJson(dir) {
2648
2746
  const pkgPath = path6.join(dir, "package.json");
2649
2747
  try {
2650
- const raw = fs6.readFileSync(pkgPath, "utf-8");
2748
+ const raw = fs7.readFileSync(pkgPath, "utf-8");
2651
2749
  const pkg = JSON.parse(raw);
2652
2750
  if (!pkg || typeof pkg !== "object") return null;
2653
2751
  const ws = pkg.workspaces;
@@ -2668,7 +2766,7 @@ function discoverWorkspacePackages(workspaceRoot) {
2668
2766
  const wsPath = path6.join(workspaceRoot, "pnpm-workspace.yaml");
2669
2767
  let content;
2670
2768
  try {
2671
- content = fs6.readFileSync(wsPath, "utf-8");
2769
+ content = fs7.readFileSync(wsPath, "utf-8");
2672
2770
  } catch {
2673
2771
  return [];
2674
2772
  }
@@ -2683,7 +2781,7 @@ function discoverWorkspacePackages(workspaceRoot) {
2683
2781
  for (const dir of dirs) {
2684
2782
  const pkgPath = path6.join(dir, "package.json");
2685
2783
  try {
2686
- const raw = fs6.readFileSync(pkgPath, "utf-8");
2784
+ const raw = fs7.readFileSync(pkgPath, "utf-8");
2687
2785
  const pkg = JSON.parse(raw);
2688
2786
  const rawName = typeof pkg.name === "string" ? pkg.name : null;
2689
2787
  const scopeMatch = rawName?.match(/^@([^/]+)\//);
@@ -2765,7 +2863,7 @@ function expandSingleGlob(root, glob) {
2765
2863
  function expandSegments(base, segments) {
2766
2864
  if (segments.length === 0) {
2767
2865
  try {
2768
- const stat = fs6.statSync(base);
2866
+ const stat = fs7.statSync(base);
2769
2867
  if (stat.isDirectory()) return [base];
2770
2868
  } catch {
2771
2869
  }
@@ -2774,7 +2872,7 @@ function expandSegments(base, segments) {
2774
2872
  const [current, ...rest] = segments;
2775
2873
  if (current.includes("*")) {
2776
2874
  try {
2777
- const entries = fs6.readdirSync(base, { withFileTypes: true });
2875
+ const entries = fs7.readdirSync(base, { withFileTypes: true });
2778
2876
  const matched = entries.filter((e) => e.isDirectory() && segmentMatches(current, e.name));
2779
2877
  if (rest.length === 0) {
2780
2878
  return matched.map((e) => path6.join(base, e.name));
@@ -2792,7 +2890,7 @@ function expandSegments(base, segments) {
2792
2890
  }
2793
2891
 
2794
2892
  // src/turbo.ts
2795
- import * as fs7 from "fs";
2893
+ import * as fs8 from "fs";
2796
2894
  import * as path7 from "path";
2797
2895
  var LOADER_FILENAME = "turbo-env-loader.cjs";
2798
2896
  var MANIFEST_FILENAME = "dev-manifest.json";
@@ -2822,23 +2920,23 @@ try {
2822
2920
  `;
2823
2921
  }
2824
2922
  function ensureEnvLoader(baseDir = USER_STATE_DIR) {
2825
- fs7.mkdirSync(baseDir, { recursive: true, mode: 493 });
2923
+ fs8.mkdirSync(baseDir, { recursive: true, mode: 493 });
2826
2924
  const target = loaderPath(baseDir);
2827
2925
  const source = loaderSource(baseDir);
2828
2926
  try {
2829
- const existing = fs7.readFileSync(target, "utf-8");
2927
+ const existing = fs8.readFileSync(target, "utf-8");
2830
2928
  if (existing === source) return;
2831
2929
  } catch {
2832
2930
  }
2833
- fs7.writeFileSync(target, source, { mode: 420 });
2931
+ fs8.writeFileSync(target, source, { mode: 420 });
2834
2932
  }
2835
2933
  function writeManifest(entries, baseDir = USER_STATE_DIR) {
2836
- fs7.mkdirSync(baseDir, { recursive: true, mode: 493 });
2837
- fs7.writeFileSync(manifestPath(baseDir), JSON.stringify(entries, null, 2) + "\n", { mode: 420 });
2934
+ fs8.mkdirSync(baseDir, { recursive: true, mode: 493 });
2935
+ fs8.writeFileSync(manifestPath(baseDir), JSON.stringify(entries, null, 2) + "\n", { mode: 420 });
2838
2936
  }
2839
2937
  function removeManifest(baseDir = USER_STATE_DIR) {
2840
2938
  try {
2841
- fs7.unlinkSync(manifestPath(baseDir));
2939
+ fs8.unlinkSync(manifestPath(baseDir));
2842
2940
  } catch {
2843
2941
  }
2844
2942
  }
@@ -2850,7 +2948,7 @@ function buildNodeOptions(baseDir = USER_STATE_DIR) {
2850
2948
  }
2851
2949
  function hasTurboConfig(wsRoot) {
2852
2950
  try {
2853
- fs7.accessSync(path7.join(wsRoot, "turbo.json"), fs7.constants.R_OK);
2951
+ fs8.accessSync(path7.join(wsRoot, "turbo.json"), fs8.constants.R_OK);
2854
2952
  return true;
2855
2953
  } catch {
2856
2954
  return false;
@@ -2858,8 +2956,8 @@ function hasTurboConfig(wsRoot) {
2858
2956
  }
2859
2957
 
2860
2958
  // src/service.ts
2861
- import * as fs8 from "fs";
2862
- import * as os2 from "os";
2959
+ import * as fs9 from "fs";
2960
+ import * as os3 from "os";
2863
2961
  import * as path8 from "path";
2864
2962
  import { spawnSync as spawnSync4 } from "child_process";
2865
2963
  var DEFAULT_SERVICE_PORT = getProtocolPort(true);
@@ -2890,7 +2988,7 @@ var DEFAULT_SERVICE_CONFIG = {
2890
2988
  useWildcard: false,
2891
2989
  extraEnv: {}
2892
2990
  };
2893
- function defaultRunner3(command, args, options) {
2991
+ function defaultRunner4(command, args, options) {
2894
2992
  return spawnSync4(command, args, {
2895
2993
  encoding: "utf-8",
2896
2994
  stdio: options?.stdio ?? "pipe"
@@ -2932,7 +3030,7 @@ function getFlagValue(args, index, flag) {
2932
3030
  return value;
2933
3031
  }
2934
3032
  function resolveServicePath(value) {
2935
- const expanded = value === "~" ? os2.homedir() : value.startsWith("~/") || value.startsWith("~\\") ? path8.join(os2.homedir(), value.slice(2)) : value;
3033
+ const expanded = value === "~" ? os3.homedir() : value.startsWith("~/") || value.startsWith("~\\") ? path8.join(os3.homedir(), value.slice(2)) : value;
2936
3034
  return path8.resolve(expanded);
2937
3035
  }
2938
3036
  function normalizeServiceInstallPaths(config) {
@@ -3060,34 +3158,21 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
3060
3158
  }
3061
3159
  return config;
3062
3160
  }
3063
- function readPasswdHome(username) {
3064
- try {
3065
- const passwd = fs8.readFileSync("/etc/passwd", "utf-8");
3066
- for (const line of passwd.split("\n")) {
3067
- const fields = line.split(":");
3068
- if (fields[0] === username && fields[5]) {
3069
- return fields[5];
3070
- }
3071
- }
3072
- } catch {
3073
- }
3074
- return null;
3075
- }
3076
3161
  function resolveUserContext(platform) {
3077
3162
  if (platform === "win32") {
3078
- const home = process.env.USERPROFILE || os2.homedir();
3163
+ const home = resolveUserHome({ platform });
3079
3164
  return { home, username: process.env.USERNAME };
3080
3165
  }
3081
3166
  const sudoUser = process.env.SUDO_USER;
3082
3167
  const sudoUid = process.env.SUDO_UID;
3083
3168
  const sudoGid = process.env.SUDO_GID;
3084
3169
  if (sudoUser && sudoUser !== "root") {
3085
- const home = process.env.HOME && process.env.HOME !== "/var/root" && process.env.HOME !== "/root" ? process.env.HOME : readPasswdHome(sudoUser) || (platform === "darwin" ? path8.posix.join("/Users", sudoUser) : path8.posix.join("/home", sudoUser));
3170
+ const home = resolveUserHome({ platform });
3086
3171
  return { home, uid: sudoUid, gid: sudoGid, username: sudoUser };
3087
3172
  }
3088
- const userInfo2 = os2.userInfo();
3173
+ const userInfo2 = os3.userInfo();
3089
3174
  return {
3090
- home: os2.homedir(),
3175
+ home: os3.homedir(),
3091
3176
  uid: process.getuid?.()?.toString(),
3092
3177
  gid: process.getgid?.()?.toString(),
3093
3178
  username: userInfo2.username
@@ -3365,8 +3450,8 @@ function parsePlistEnv(block) {
3365
3450
  function readInstalledServiceSnapshot(spec) {
3366
3451
  try {
3367
3452
  if (spec.platform === "darwin") {
3368
- if (!fs8.existsSync(spec.plistPath)) return null;
3369
- const plist = fs8.readFileSync(spec.plistPath, "utf-8");
3453
+ if (!fs9.existsSync(spec.plistPath)) return null;
3454
+ const plist = fs9.readFileSync(spec.plistPath, "utf-8");
3370
3455
  const argsBlock = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
3371
3456
  const envBlock = plist.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
3372
3457
  if (!argsBlock) return null;
@@ -3376,8 +3461,8 @@ function readInstalledServiceSnapshot(spec) {
3376
3461
  };
3377
3462
  }
3378
3463
  if (spec.platform === "linux") {
3379
- if (!fs8.existsSync(spec.unitPath)) return null;
3380
- const unit = fs8.readFileSync(spec.unitPath, "utf-8");
3464
+ if (!fs9.existsSync(spec.unitPath)) return null;
3465
+ const unit = fs9.readFileSync(spec.unitPath, "utf-8");
3381
3466
  const env2 = {};
3382
3467
  let command = null;
3383
3468
  for (const line of unit.split("\n")) {
@@ -3395,8 +3480,8 @@ function readInstalledServiceSnapshot(spec) {
3395
3480
  }
3396
3481
  return command ? { command, env: env2 } : null;
3397
3482
  }
3398
- if (!fs8.existsSync(spec.scriptPath)) return null;
3399
- const script = fs8.readFileSync(spec.scriptPath, "utf-8");
3483
+ if (!fs9.existsSync(spec.scriptPath)) return null;
3484
+ const script = fs9.readFileSync(spec.scriptPath, "utf-8");
3400
3485
  const env = {};
3401
3486
  let commandLine = null;
3402
3487
  for (const rawLine of script.split(/\r?\n/)) {
@@ -3458,7 +3543,7 @@ function buildElevatedEnvArgs(options) {
3458
3543
  }
3459
3544
  function buildServiceUninstallSudoArgs(entryScript, options = {}) {
3460
3545
  const env = options.env ?? process.env;
3461
- const home = options.home ?? os2.homedir();
3546
+ const home = options.home ?? os3.homedir();
3462
3547
  const stateDir = options.stateDir ?? env.PORTLESS_STATE_DIR ?? path8.join(home, ".portless");
3463
3548
  return [
3464
3549
  ...buildElevatedEnvArgs({ home, stateDir, env }),
@@ -3472,7 +3557,7 @@ function requireUnixElevation(args, runner) {
3472
3557
  if (process.platform !== "darwin" && process.platform !== "linux") return;
3473
3558
  if ((process.getuid?.() ?? -1) === 0) return;
3474
3559
  if (process.env[INTERNAL_ELEVATED_ENV] === "1") return;
3475
- const home = os2.homedir();
3560
+ const home = os3.homedir();
3476
3561
  const stateDir = process.env.PORTLESS_STATE_DIR || path8.join(home, ".portless");
3477
3562
  const result = runner(
3478
3563
  "sudo",
@@ -3529,7 +3614,7 @@ async function stopExistingProxy(entryScript, runner, proxyPort) {
3529
3614
  }
3530
3615
  }
3531
3616
  function prepareServiceState(stateDir) {
3532
- fs8.mkdirSync(stateDir, { recursive: true });
3617
+ fs9.mkdirSync(stateDir, { recursive: true });
3533
3618
  fixOwnership(stateDir);
3534
3619
  }
3535
3620
  function prepareTrust(stateDir) {
@@ -3577,8 +3662,8 @@ async function installService(entryScript, runner, args) {
3577
3662
  if (spec.platform === "darwin") {
3578
3663
  runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
3579
3664
  await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
3580
- fs8.writeFileSync(spec.plistPath, spec.plist);
3581
- fs8.chmodSync(spec.plistPath, 420);
3665
+ fs9.writeFileSync(spec.plistPath, spec.plist);
3666
+ fs9.chmodSync(spec.plistPath, 420);
3582
3667
  runRequired(runner, "chown", ["root:wheel", spec.plistPath]);
3583
3668
  runRequired(runner, "launchctl", ["bootstrap", "system", spec.plistPath]);
3584
3669
  runRequired(runner, "launchctl", ["enable", `system/${spec.label}`]);
@@ -3586,16 +3671,16 @@ async function installService(entryScript, runner, args) {
3586
3671
  } else if (spec.platform === "linux") {
3587
3672
  runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
3588
3673
  await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
3589
- fs8.writeFileSync(spec.unitPath, spec.unit);
3590
- fs8.chmodSync(spec.unitPath, 420);
3674
+ fs9.writeFileSync(spec.unitPath, spec.unit);
3675
+ fs9.chmodSync(spec.unitPath, 420);
3591
3676
  runRequired(runner, "systemctl", ["daemon-reload"]);
3592
3677
  runRequired(runner, "systemctl", ["enable", spec.serviceName]);
3593
3678
  runRequired(runner, "systemctl", ["restart", spec.serviceName]);
3594
3679
  } else {
3595
3680
  runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
3596
3681
  await stopExistingProxy(entryScript, runner, spec.config.proxyPort);
3597
- fs8.mkdirSync(spec.scriptDir, { recursive: true });
3598
- fs8.writeFileSync(spec.scriptPath, spec.script);
3682
+ fs9.mkdirSync(spec.scriptDir, { recursive: true });
3683
+ fs9.writeFileSync(spec.scriptPath, spec.script);
3599
3684
  runRequired(runner, "schtasks", spec.createArgs);
3600
3685
  runOptional(runner, "schtasks", spec.runArgs);
3601
3686
  }
@@ -3608,32 +3693,32 @@ async function uninstallService(entryScript, runner) {
3608
3693
  const spec = currentServiceSpec(entryScript);
3609
3694
  if (spec.platform === "darwin") {
3610
3695
  runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
3611
- fs8.rmSync(spec.plistPath, { force: true });
3696
+ fs9.rmSync(spec.plistPath, { force: true });
3612
3697
  } else if (spec.platform === "linux") {
3613
3698
  runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
3614
- fs8.rmSync(spec.unitPath, { force: true });
3699
+ fs9.rmSync(spec.unitPath, { force: true });
3615
3700
  runOptional(runner, "systemctl", ["daemon-reload"]);
3616
3701
  } else {
3617
3702
  runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
3618
3703
  runOptional(runner, "schtasks", spec.deleteArgs);
3619
- fs8.rmSync(spec.scriptDir, { recursive: true, force: true });
3704
+ fs9.rmSync(spec.scriptDir, { recursive: true, force: true });
3620
3705
  }
3621
3706
  console.log(colors_default.green("Portless service uninstalled."));
3622
3707
  }
3623
- function tryUninstallService(entryScript, runner = defaultRunner3) {
3708
+ function tryUninstallService(entryScript, runner = defaultRunner4) {
3624
3709
  let installed = false;
3625
3710
  try {
3626
3711
  const spec = currentServiceSpec(entryScript);
3627
3712
  if (spec.platform === "darwin") {
3628
- installed = fs8.existsSync(spec.plistPath);
3713
+ installed = fs9.existsSync(spec.plistPath);
3629
3714
  if (!installed) return { removed: false, installed: false };
3630
3715
  runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
3631
- fs8.rmSync(spec.plistPath, { force: true });
3716
+ fs9.rmSync(spec.plistPath, { force: true });
3632
3717
  } else if (spec.platform === "linux") {
3633
- installed = fs8.existsSync(spec.unitPath);
3718
+ installed = fs9.existsSync(spec.unitPath);
3634
3719
  if (!installed) return { removed: false, installed: false };
3635
3720
  runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
3636
- fs8.rmSync(spec.unitPath, { force: true });
3721
+ fs9.rmSync(spec.unitPath, { force: true });
3637
3722
  runOptional(runner, "systemctl", ["daemon-reload"]);
3638
3723
  } else {
3639
3724
  const query = runner("schtasks", ["/Query", "/TN", spec.taskName, "/FO", "LIST"]);
@@ -3641,7 +3726,7 @@ function tryUninstallService(entryScript, runner = defaultRunner3) {
3641
3726
  if (!installed) return { removed: false, installed: false };
3642
3727
  runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
3643
3728
  runRequired(runner, "schtasks", spec.deleteArgs);
3644
- fs8.rmSync(spec.scriptDir, { recursive: true, force: true });
3729
+ fs9.rmSync(spec.scriptDir, { recursive: true, force: true });
3645
3730
  }
3646
3731
  return { removed: true, installed: true };
3647
3732
  } catch (err) {
@@ -3658,7 +3743,7 @@ async function getServiceStatus(entryScript, runner) {
3658
3743
  const installedConfig = readInstalledServiceConfig(spec) ?? spec.config;
3659
3744
  const proxyRunning = await isProxyRunning(installedConfig.proxyPort, installedConfig.useHttps);
3660
3745
  if (spec.platform === "darwin") {
3661
- const installed2 = fs8.existsSync(spec.plistPath);
3746
+ const installed2 = fs9.existsSync(spec.plistPath);
3662
3747
  const result = runner("launchctl", ["print", `system/${spec.label}`]);
3663
3748
  const output2 = `${result.stdout || ""}${result.stderr || ""}`;
3664
3749
  const managerState = result.status === 0 && /state = running|pid = \d+/.test(output2) ? "running" : installed2 ? "installed" : "not installed";
@@ -3673,7 +3758,7 @@ async function getServiceStatus(entryScript, runner) {
3673
3758
  if (spec.platform === "linux") {
3674
3759
  const enabled2 = runner("systemctl", ["is-enabled", spec.serviceName]);
3675
3760
  const active = runner("systemctl", ["is-active", spec.serviceName]);
3676
- const installed2 = enabled2.status === 0 || active.status === 0 || fs8.existsSync(spec.unitPath);
3761
+ const installed2 = enabled2.status === 0 || active.status === 0 || fs9.existsSync(spec.unitPath);
3677
3762
  const activeText = (active.stdout || "").trim();
3678
3763
  return {
3679
3764
  installed: installed2,
@@ -3748,7 +3833,7 @@ ${colors_default.bold("Notes:")}
3748
3833
  }
3749
3834
  async function handleService(args, options) {
3750
3835
  const action = args[1];
3751
- const runner = options.runner || defaultRunner3;
3836
+ const runner = options.runner || defaultRunner4;
3752
3837
  if (!action || action === "--help" || action === "-h") {
3753
3838
  printServiceHelp();
3754
3839
  process.exit(0);
@@ -3945,7 +4030,7 @@ function getEntryScript() {
3945
4030
  function isLocallyInstalled() {
3946
4031
  let dir = process.cwd();
3947
4032
  for (; ; ) {
3948
- if (fs9.existsSync(path9.join(dir, "node_modules", "portless", "package.json"))) {
4033
+ if (fs10.existsSync(path9.join(dir, "node_modules", "portless", "package.json"))) {
3949
4034
  return true;
3950
4035
  }
3951
4036
  const parent = path9.dirname(dir);
@@ -4059,11 +4144,11 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4059
4144
  console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
4060
4145
  }
4061
4146
  const routesPath = store.getRoutesPath();
4062
- if (!fs9.existsSync(routesPath)) {
4063
- fs9.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
4147
+ if (!fs10.existsSync(routesPath)) {
4148
+ fs10.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
4064
4149
  }
4065
4150
  try {
4066
- fs9.chmodSync(routesPath, FILE_MODE);
4151
+ fs10.chmodSync(routesPath, FILE_MODE);
4067
4152
  } catch {
4068
4153
  }
4069
4154
  fixOwnership(routesPath);
@@ -4123,7 +4208,7 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4123
4208
  }
4124
4209
  };
4125
4210
  try {
4126
- watcher = fs9.watch(routesPath, () => {
4211
+ watcher = fs10.watch(routesPath, () => {
4127
4212
  if (debounceTimer) clearTimeout(debounceTimer);
4128
4213
  debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
4129
4214
  });
@@ -4174,8 +4259,8 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4174
4259
  redirectServer.listen(80);
4175
4260
  }
4176
4261
  server.listen(proxyPort, () => {
4177
- fs9.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
4178
- fs9.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
4262
+ fs10.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
4263
+ fs10.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
4179
4264
  writeTlsMarker(store.dir, isTls);
4180
4265
  writeCustomCertMarker(store.dir, isTls && customCert);
4181
4266
  writeTldsFile(store.dir, tlds);
@@ -4224,11 +4309,11 @@ function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict
4224
4309
  redirectServer.close();
4225
4310
  }
4226
4311
  try {
4227
- fs9.unlinkSync(store.pidPath);
4312
+ fs10.unlinkSync(store.pidPath);
4228
4313
  } catch {
4229
4314
  }
4230
4315
  try {
4231
- fs9.unlinkSync(store.portFilePath);
4316
+ fs10.unlinkSync(store.portFilePath);
4232
4317
  } catch {
4233
4318
  }
4234
4319
  writeTlsMarker(store.dir, false);
@@ -4259,7 +4344,7 @@ function sudoStopOrHint(port) {
4259
4344
  }
4260
4345
  async function stopProxy(store, proxyPort, _tls) {
4261
4346
  const pidPath = store.pidPath;
4262
- if (!fs9.existsSync(pidPath)) {
4347
+ if (!fs10.existsSync(pidPath)) {
4263
4348
  if (await isProxyRunning(proxyPort)) {
4264
4349
  console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
4265
4350
  const pid = findPidOnPort(proxyPort);
@@ -4267,7 +4352,7 @@ async function stopProxy(store, proxyPort, _tls) {
4267
4352
  try {
4268
4353
  process.kill(pid, "SIGTERM");
4269
4354
  try {
4270
- fs9.unlinkSync(store.portFilePath);
4355
+ fs10.unlinkSync(store.portFilePath);
4271
4356
  } catch {
4272
4357
  }
4273
4358
  writeTlsMarker(store.dir, false);
@@ -4306,10 +4391,10 @@ async function stopProxy(store, proxyPort, _tls) {
4306
4391
  return;
4307
4392
  }
4308
4393
  try {
4309
- const pid = parseInt(fs9.readFileSync(pidPath, "utf-8"), 10);
4394
+ const pid = parseInt(fs10.readFileSync(pidPath, "utf-8"), 10);
4310
4395
  if (isNaN(pid)) {
4311
4396
  console.error(colors_default.red("Corrupted PID file. Removing it."));
4312
- fs9.unlinkSync(pidPath);
4397
+ fs10.unlinkSync(pidPath);
4313
4398
  writeTlsMarker(store.dir, false);
4314
4399
  writeCustomCertMarker(store.dir, false);
4315
4400
  writeTldFile(store.dir, DEFAULT_TLD);
@@ -4324,9 +4409,9 @@ async function stopProxy(store, proxyPort, _tls) {
4324
4409
  return;
4325
4410
  }
4326
4411
  console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
4327
- fs9.unlinkSync(pidPath);
4412
+ fs10.unlinkSync(pidPath);
4328
4413
  try {
4329
- fs9.unlinkSync(store.portFilePath);
4414
+ fs10.unlinkSync(store.portFilePath);
4330
4415
  } catch {
4331
4416
  }
4332
4417
  writeTlsMarker(store.dir, false);
@@ -4342,7 +4427,7 @@ async function stopProxy(store, proxyPort, _tls) {
4342
4427
  )
4343
4428
  );
4344
4429
  console.log(colors_default.yellow("Removing stale PID file."));
4345
- fs9.unlinkSync(pidPath);
4430
+ fs10.unlinkSync(pidPath);
4346
4431
  writeTlsMarker(store.dir, false);
4347
4432
  writeCustomCertMarker(store.dir, false);
4348
4433
  writeTldFile(store.dir, DEFAULT_TLD);
@@ -4350,9 +4435,9 @@ async function stopProxy(store, proxyPort, _tls) {
4350
4435
  return;
4351
4436
  }
4352
4437
  process.kill(pid, "SIGTERM");
4353
- fs9.unlinkSync(pidPath);
4438
+ fs10.unlinkSync(pidPath);
4354
4439
  try {
4355
- fs9.unlinkSync(store.portFilePath);
4440
+ fs10.unlinkSync(store.portFilePath);
4356
4441
  } catch {
4357
4442
  }
4358
4443
  writeTlsMarker(store.dir, false);
@@ -4503,7 +4588,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
4503
4588
  const logPath = path9.join(fallbackDir, "proxy.log");
4504
4589
  console.error(colors_default.blue("Try starting it manually:"));
4505
4590
  console.error(colors_default.cyan(` ${manualStartCommand}`));
4506
- if (fs9.existsSync(logPath)) {
4591
+ if (fs10.existsSync(logPath)) {
4507
4592
  console.error(colors_default.gray(`Logs: ${logPath}`));
4508
4593
  }
4509
4594
  process.exit(1);
@@ -4768,7 +4853,7 @@ portless
4768
4853
  const caEnv = {};
4769
4854
  if (tls2 && !process.env.NODE_EXTRA_CA_CERTS) {
4770
4855
  const caPath = path9.join(stateDir, "ca.pem");
4771
- if (fs9.existsSync(caPath)) {
4856
+ if (fs10.existsSync(caPath)) {
4772
4857
  caEnv.NODE_EXTRA_CA_CERTS = caPath;
4773
4858
  }
4774
4859
  }
@@ -5047,11 +5132,13 @@ ${colors_default.bold("How it works:")}
5047
5132
  5. Frameworks that ignore PORT (Vite, VitePlus, Astro, React Router, Angular,
5048
5133
  Expo, React Native) get --port and, when needed, --host flags
5049
5134
  injected automatically
5135
+ Elevated proxy processes keep the invoking user's ~/.portless state directory.
5050
5136
 
5051
5137
  ${colors_default.bold("HTTP/2 + HTTPS (default):")}
5052
5138
  HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
5053
5139
  On first use, portless generates a local CA and adds it to your
5054
5140
  system trust store. No browser warnings. Disable with --no-tls.
5141
+ On WSL, portless also adds the CA to the Windows user certificate store.
5055
5142
 
5056
5143
  ${colors_default.bold("LAN mode:")}
5057
5144
  Use --lan to make services accessible from other devices (phones,
@@ -5153,13 +5240,13 @@ ${colors_default.bold("Reserved names:")}
5153
5240
  process.exit(0);
5154
5241
  }
5155
5242
  function printVersion() {
5156
- console.log("0.15.1");
5243
+ console.log("0.15.3");
5157
5244
  process.exit(0);
5158
5245
  }
5159
5246
  async function handleTrust() {
5160
5247
  const { dir } = await discoverState();
5161
- if (!fs9.existsSync(dir)) {
5162
- fs9.mkdirSync(dir, { recursive: true });
5248
+ if (!fs10.existsSync(dir)) {
5249
+ fs10.mkdirSync(dir, { recursive: true });
5163
5250
  }
5164
5251
  const { caGenerated } = ensureCerts(dir);
5165
5252
  if (caGenerated) {
@@ -5207,7 +5294,8 @@ directory, and PORTLESS_STATE_DIR when set), and removes the portless block
5207
5294
  from ${HOSTS_DISPLAY}.
5208
5295
 
5209
5296
  Only allowlisted filenames under each state directory are deleted. Custom
5210
- certificate paths from --cert and --key are never removed.
5297
+ certificate paths from --cert and --key are never removed. If trust removal
5298
+ fails, the CA certificate and key are retained so clean can safely retry.
5211
5299
 
5212
5300
  macOS/Linux may prompt for sudo when the proxy, trust store, or ${HOSTS_DISPLAY}
5213
5301
  require elevated privileges. On Windows, run as Administrator if needed.
@@ -5263,27 +5351,32 @@ ${colors_default.bold("Options:")}
5263
5351
  }
5264
5352
  }
5265
5353
  const stateDirs = collectStateDirsForCleanup();
5266
- for (const stateDir of stateDirs) {
5267
- const caPath = path9.join(stateDir, "ca.pem");
5268
- if (!fs9.existsSync(caPath)) continue;
5269
- const wasTrusted = isCATrusted(stateDir);
5270
- if (!wasTrusted) continue;
5271
- const untrustResult = untrustCA(stateDir);
5354
+ const failedCAStateDirs = /* @__PURE__ */ new Set();
5355
+ const caRemovalResults = attemptCATrustRemovalForCleanup(stateDirs, untrustCA);
5356
+ for (const [stateDir, untrustResult] of caRemovalResults) {
5272
5357
  if (untrustResult.removed) {
5273
5358
  console.log(colors_default.green("Removed local CA from the system trust store."));
5274
- } else if (untrustResult.error) {
5359
+ } else {
5360
+ failedCAStateDirs.add(stateDir);
5275
5361
  console.warn(
5276
5362
  colors_default.yellow(
5277
- `Could not remove CA from trust store: ${untrustResult.error}
5363
+ `Could not remove CA from trust store: ${untrustResult.error ?? "unknown error"}
5278
5364
  Try: sudo portless clean (Linux), or delete the certificate manually.`
5279
5365
  )
5280
5366
  );
5281
5367
  }
5282
5368
  }
5283
5369
  for (const stateDir of stateDirs) {
5284
- removePortlessStateFiles(stateDir);
5370
+ removePortlessStateFiles(stateDir, {
5371
+ preserveCAIdentity: failedCAStateDirs.has(stateDir)
5372
+ });
5285
5373
  }
5286
5374
  console.log(colors_default.green("Removed portless state files from known state directories."));
5375
+ if (failedCAStateDirs.size > 0) {
5376
+ console.warn(
5377
+ colors_default.yellow("Retained CA identity files so trust removal can be retried safely.")
5378
+ );
5379
+ }
5287
5380
  if (cleanHostsFile()) {
5288
5381
  console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
5289
5382
  } else if (!isWindows && process.getuid?.() !== 0) {
@@ -5609,7 +5702,7 @@ function isProcessAliveForDoctor(pid) {
5609
5702
  }
5610
5703
  function checkPathWritable(targetPath) {
5611
5704
  try {
5612
- fs9.accessSync(targetPath, fs9.constants.W_OK);
5705
+ fs10.accessSync(targetPath, fs10.constants.W_OK);
5613
5706
  return true;
5614
5707
  } catch {
5615
5708
  return false;
@@ -5618,7 +5711,7 @@ function checkPathWritable(targetPath) {
5618
5711
  function findExistingAncestor(targetPath) {
5619
5712
  let current = targetPath;
5620
5713
  for (; ; ) {
5621
- if (fs9.existsSync(current)) return current;
5714
+ if (fs10.existsSync(current)) return current;
5622
5715
  const parent = path9.dirname(current);
5623
5716
  if (parent === current) return null;
5624
5717
  current = parent;
@@ -5689,7 +5782,7 @@ ${colors_default.bold("Options:")}
5689
5782
  const store = new RouteStore(state.dir, {
5690
5783
  onWarning: (msg) => add("warn", msg)
5691
5784
  });
5692
- const hasPortFile = fs9.existsSync(store.portFilePath);
5785
+ const hasPortFile = fs10.existsSync(store.portFilePath);
5693
5786
  const configuredTls = hasPortFile ? state.tls : !isHttpsEnvDisabled();
5694
5787
  const configuredPort = hasPortFile ? state.port : getDefaultPort(configuredTls);
5695
5788
  const initialProxyRunning = await isProxyRunning(state.port, state.tls);
@@ -5701,9 +5794,9 @@ ${colors_default.bold("Options:")}
5701
5794
  const proxyTls = proxyRunning || portListening || hasPortFile ? probeTls : configuredTls;
5702
5795
  const currentProxyStateIsHttp = (proxyRunning || portListening || hasPortFile) && !proxyTls;
5703
5796
  const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
5704
- const stateExists = fs9.existsSync(state.dir);
5797
+ const stateExists = fs10.existsSync(state.dir);
5705
5798
  console.log(colors_default.blue.bold("\nportless doctor\n"));
5706
- console.log(`Version: ${"0.15.1"}`);
5799
+ console.log(`Version: ${"0.15.3"}`);
5707
5800
  console.log(`Node.js: ${process.versions.node}`);
5708
5801
  console.log(`Platform: ${process.platform} ${process.arch}`);
5709
5802
  console.log(`State dir: ${state.dir}`);
@@ -5720,7 +5813,7 @@ ${colors_default.bold("Options:")}
5720
5813
  }
5721
5814
  if (stateExists) {
5722
5815
  try {
5723
- const stat = fs9.statSync(state.dir);
5816
+ const stat = fs10.statSync(state.dir);
5724
5817
  if (!stat.isDirectory()) {
5725
5818
  add("fail", `State path exists but is not a directory: ${state.dir}`);
5726
5819
  } else if (checkPathWritable(state.dir)) {
@@ -5740,7 +5833,7 @@ ${colors_default.bold("Options:")}
5740
5833
  `State directory does not exist and no writable ancestor was found: ${state.dir}`
5741
5834
  );
5742
5835
  } else {
5743
- const ancestorStat = fs9.statSync(ancestor);
5836
+ const ancestorStat = fs10.statSync(ancestor);
5744
5837
  if (!ancestorStat.isDirectory()) {
5745
5838
  add("fail", `State directory does not exist and ancestor is not a directory: ${ancestor}`);
5746
5839
  } else if (checkPathWritable(ancestor)) {
@@ -5766,9 +5859,9 @@ ${colors_default.bold("Options:")}
5766
5859
  doctorProxyStartHint(proxyPort, proxyTls)
5767
5860
  );
5768
5861
  }
5769
- if (fs9.existsSync(store.pidPath)) {
5862
+ if (fs10.existsSync(store.pidPath)) {
5770
5863
  try {
5771
- const rawPid = fs9.readFileSync(store.pidPath, "utf-8").trim();
5864
+ const rawPid = fs10.readFileSync(store.pidPath, "utf-8").trim();
5772
5865
  const pid = parseInt(rawPid, 10);
5773
5866
  if (isNaN(pid) || pid <= 0) {
5774
5867
  add("fail", `Proxy PID file is invalid: ${store.pidPath}`);
@@ -5818,7 +5911,7 @@ ${colors_default.bold("Options:")}
5818
5911
  add("info", "Generated local CA is not required for custom TLS certificates.");
5819
5912
  } else if (proxyTls) {
5820
5913
  const caPath = path9.join(state.dir, "ca.pem");
5821
- if (fs9.existsSync(caPath)) {
5914
+ if (fs10.existsSync(caPath)) {
5822
5915
  if (isCATrusted(state.dir)) {
5823
5916
  add("ok", "Local CA is trusted by the OS trust store.");
5824
5917
  } else {
@@ -6243,7 +6336,7 @@ ${colors_default.bold("LAN mode (--lan):")}
6243
6336
  } else {
6244
6337
  console.error(colors_default.red("Proxy process started but is not responding."));
6245
6338
  const logPath2 = path9.join(resolveStateDir(proxyPort), "proxy.log");
6246
- if (fs9.existsSync(logPath2)) {
6339
+ if (fs10.existsSync(logPath2)) {
6247
6340
  console.error(colors_default.gray(`Logs: ${logPath2}`));
6248
6341
  }
6249
6342
  }
@@ -6282,8 +6375,8 @@ ${colors_default.bold("LAN mode (--lan):")}
6282
6375
  store.ensureDir();
6283
6376
  if (customCertPath && customKeyPath) {
6284
6377
  try {
6285
- const cert = fs9.readFileSync(customCertPath);
6286
- const key = fs9.readFileSync(customKeyPath);
6378
+ const cert = fs10.readFileSync(customCertPath);
6379
+ const key = fs10.readFileSync(customKeyPath);
6287
6380
  const certStr = cert.toString("utf-8");
6288
6381
  const keyStr = key.toString("utf-8");
6289
6382
  if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
@@ -6328,9 +6421,9 @@ ${colors_default.bold("LAN mode (--lan):")}
6328
6421
  console.warn(colors_default.cyan(" portless trust"));
6329
6422
  }
6330
6423
  }
6331
- const cert = fs9.readFileSync(certs.certPath);
6332
- const key = fs9.readFileSync(certs.keyPath);
6333
- const ca = fs9.readFileSync(certs.caPath);
6424
+ const cert = fs10.readFileSync(certs.certPath);
6425
+ const key = fs10.readFileSync(certs.keyPath);
6426
+ const ca = fs10.readFileSync(certs.caPath);
6334
6427
  tlsOptions = {
6335
6428
  cert,
6336
6429
  key,
@@ -6355,10 +6448,10 @@ ${colors_default.bold("LAN mode (--lan):")}
6355
6448
  }
6356
6449
  store.ensureDir();
6357
6450
  const logPath = path9.join(stateDir, "proxy.log");
6358
- const logFd = fs9.openSync(logPath, "a");
6451
+ const logFd = fs10.openSync(logPath, "a");
6359
6452
  try {
6360
6453
  try {
6361
- fs9.chmodSync(logPath, FILE_MODE);
6454
+ fs10.chmodSync(logPath, FILE_MODE);
6362
6455
  } catch {
6363
6456
  }
6364
6457
  fixOwnership(logPath);
@@ -6390,13 +6483,13 @@ ${colors_default.bold("LAN mode (--lan):")}
6390
6483
  });
6391
6484
  child.unref();
6392
6485
  } finally {
6393
- fs9.closeSync(logFd);
6486
+ fs10.closeSync(logFd);
6394
6487
  }
6395
6488
  if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
6396
6489
  console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
6397
6490
  console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
6398
6491
  console.error(colors_default.cyan(" portless proxy start --foreground"));
6399
- if (fs9.existsSync(logPath)) {
6492
+ if (fs10.existsSync(logPath)) {
6400
6493
  console.error(colors_default.gray(`Logs: ${logPath}`));
6401
6494
  }
6402
6495
  process.exit(1);
@@ -6467,7 +6560,7 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
6467
6560
  nameSource = inferred.source;
6468
6561
  }
6469
6562
  const worktree = detectWorktreePrefix(cwd);
6470
- const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
6563
+ const effectiveName = applyWorktreePrefix(baseName, worktree);
6471
6564
  const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
6472
6565
  const store = new RouteStore(dir, {
6473
6566
  onWarning: (msg) => console.warn(colors_default.yellow(msg))
@@ -6549,7 +6642,7 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tlds, exitCodes)
6549
6642
  };
6550
6643
  if (tls2) {
6551
6644
  const caPath = path9.join(stateDir, "ca.pem");
6552
- if (fs9.existsSync(caPath)) {
6645
+ if (fs10.existsSync(caPath)) {
6553
6646
  env.NODE_EXTRA_CA_CERTS = caPath;
6554
6647
  }
6555
6648
  }
@@ -6627,6 +6720,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
6627
6720
  }
6628
6721
  }
6629
6722
  const apps = [];
6723
+ const worktree = detectWorktreePrefix(wsRoot);
6630
6724
  for (const pkg of packages) {
6631
6725
  const rel = path9.relative(wsRoot, pkg.dir).replace(/\\/g, "/");
6632
6726
  const rootOverride = loaded ? resolveAppConfig(loaded.config, loaded.configDir, pkg.dir) : null;
@@ -6668,6 +6762,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
6668
6762
  name = pkgLabel === projectName ? projectName : `${pkgLabel}.${projectName}`;
6669
6763
  label = pkg.scope ? `@${pkg.scope}/${pkg.name}` : pkg.name ?? rel;
6670
6764
  }
6765
+ name = applyWorktreePrefix(name, worktree);
6671
6766
  apps.push({ pkg, name, label, commandArgs, appPort: appOverride.appPort, proxied });
6672
6767
  }
6673
6768
  if (apps.length === 0) {
@@ -6737,7 +6832,7 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tlds, scriptName,
6737
6832
  };
6738
6833
  if (tls2) {
6739
6834
  const caPath = path9.join(stateDir, "ca.pem");
6740
- if (fs9.existsSync(caPath)) {
6835
+ if (fs10.existsSync(caPath)) {
6741
6836
  entry.NODE_EXTRA_CA_CERTS = caPath;
6742
6837
  }
6743
6838
  }