portless 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DEFAULT_TLD,
3
4
  FILE_MODE,
4
5
  PRIVILEGED_PORT_THRESHOLD,
6
+ RISKY_TLDS,
5
7
  RouteConflictError,
6
8
  RouteStore,
7
9
  cleanHostsFile,
@@ -12,19 +14,24 @@ import {
12
14
  fixOwnership,
13
15
  formatUrl,
14
16
  getDefaultPort,
17
+ getDefaultTld,
15
18
  injectFrameworkFlags,
16
19
  isErrnoException,
17
20
  isHttpsEnvEnabled,
18
21
  isProxyRunning,
22
+ isWindows,
19
23
  parseHostname,
20
24
  prompt,
25
+ readTldFromDir,
21
26
  readTlsMarker,
22
27
  resolveStateDir,
23
28
  spawnCommand,
24
29
  syncHostsFile,
30
+ validateTld,
25
31
  waitForProxy,
32
+ writeTldFile,
26
33
  writeTlsMarker
27
- } from "./chunk-P3DHZHEZ.js";
34
+ } from "./chunk-Y6FWHU6F.js";
28
35
 
29
36
  // src/cli.ts
30
37
  import chalk from "chalk";
@@ -213,24 +220,44 @@ function isCATrusted(stateDir) {
213
220
  return isCATrustedMacOS(caCertPath);
214
221
  } else if (process.platform === "linux") {
215
222
  return isCATrustedLinux(stateDir);
223
+ } else if (process.platform === "win32") {
224
+ return isCATrustedWindows(caCertPath);
216
225
  }
217
226
  return false;
218
227
  }
219
- function isCATrustedMacOS(caCertPath) {
228
+ function isCATrustedWindows(caCertPath) {
220
229
  try {
221
230
  const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
222
- for (const keychain of [loginKeychainPath(), "/Library/Keychains/System.keychain"]) {
223
- try {
224
- const result = execFileSync("security", ["find-certificate", "-a", "-Z", keychain], {
225
- encoding: "utf-8",
226
- timeout: 5e3,
227
- stdio: ["pipe", "pipe", "pipe"]
228
- });
229
- if (result.toLowerCase().includes(fingerprint)) return true;
230
- } catch {
231
- }
232
- }
231
+ const result = execFileSync("certutil", ["-store", "-user", "Root"], {
232
+ encoding: "utf-8",
233
+ timeout: 1e4,
234
+ stdio: ["pipe", "pipe", "pipe"]
235
+ });
236
+ return result.replace(/\s/g, "").toLowerCase().includes(fingerprint);
237
+ } catch {
233
238
  return false;
239
+ }
240
+ }
241
+ function isCATrustedMacOS(caCertPath) {
242
+ try {
243
+ const isRoot = (process.getuid?.() ?? -1) === 0;
244
+ const sudoUser = process.env.SUDO_USER;
245
+ if (isRoot && sudoUser) {
246
+ execFileSync(
247
+ "sudo",
248
+ ["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
249
+ {
250
+ stdio: "pipe",
251
+ timeout: 5e3
252
+ }
253
+ );
254
+ } else {
255
+ execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
256
+ stdio: "pipe",
257
+ timeout: 5e3
258
+ });
259
+ }
260
+ return true;
234
261
  } catch {
235
262
  return false;
236
263
  }
@@ -364,12 +391,12 @@ async function generateHostCertAsync(stateDir, hostname) {
364
391
  fixOwnership(keyPath, certPath);
365
392
  return { certPath, keyPath };
366
393
  }
367
- function createSNICallback(stateDir, defaultCert, defaultKey) {
394
+ function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost") {
368
395
  const cache = /* @__PURE__ */ new Map();
369
396
  const pending = /* @__PURE__ */ new Map();
370
397
  const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
371
398
  return (servername, cb) => {
372
- if (servername === "localhost") {
399
+ if (servername === tld) {
373
400
  cb(null, defaultCtx);
374
401
  return;
375
402
  }
@@ -422,12 +449,29 @@ function trustCA(stateDir) {
422
449
  }
423
450
  try {
424
451
  if (process.platform === "darwin") {
425
- const keychain = loginKeychainPath();
426
- execFileSync(
427
- "security",
428
- ["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
429
- { stdio: "pipe", timeout: 3e4 }
430
- );
452
+ const isRoot = (process.getuid?.() ?? -1) === 0;
453
+ if (isRoot) {
454
+ execFileSync(
455
+ "security",
456
+ [
457
+ "add-trusted-cert",
458
+ "-d",
459
+ "-r",
460
+ "trustRoot",
461
+ "-k",
462
+ "/Library/Keychains/System.keychain",
463
+ caCertPath
464
+ ],
465
+ { stdio: "pipe", timeout: 3e4 }
466
+ );
467
+ } else {
468
+ const keychain = loginKeychainPath();
469
+ execFileSync(
470
+ "security",
471
+ ["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
472
+ { stdio: "pipe", timeout: 3e4 }
473
+ );
474
+ }
431
475
  return { trusted: true };
432
476
  } else if (process.platform === "linux") {
433
477
  const config = getLinuxCATrustConfig();
@@ -438,6 +482,12 @@ function trustCA(stateDir) {
438
482
  fs.copyFileSync(caCertPath, dest);
439
483
  execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
440
484
  return { trusted: true };
485
+ } else if (process.platform === "win32") {
486
+ execFileSync("certutil", ["-addstore", "-user", "Root", caCertPath], {
487
+ stdio: "pipe",
488
+ timeout: 3e4
489
+ });
490
+ return { trusted: true };
441
491
  }
442
492
  return { trusted: false, error: `Unsupported platform: ${process.platform}` };
443
493
  } catch (err) {
@@ -453,11 +503,21 @@ function trustCA(stateDir) {
453
503
  }
454
504
 
455
505
  // src/auto.ts
506
+ import { createHash } from "crypto";
456
507
  import { execFileSync as execFileSync2 } from "child_process";
457
508
  import * as fs2 from "fs";
458
509
  import * as path2 from "path";
510
+ var MAX_DNS_LABEL_LENGTH = 63;
511
+ function truncateLabel(label) {
512
+ if (label.length <= MAX_DNS_LABEL_LENGTH) return label;
513
+ const hash = createHash("sha256").update(label).digest("hex").slice(0, 6);
514
+ const maxPrefixLength = MAX_DNS_LABEL_LENGTH - 7;
515
+ const prefix = label.slice(0, maxPrefixLength).replace(/-+$/, "");
516
+ return `${prefix}-${hash}`;
517
+ }
459
518
  function sanitizeForHostname(name) {
460
- return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
519
+ const sanitized = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
520
+ return truncateLabel(sanitized);
461
521
  }
462
522
  function inferProjectName(cwd = process.cwd()) {
463
523
  const pkgResult = findPackageJsonName(cwd);
@@ -546,6 +606,25 @@ function detectWorktreeViaCli(cwd) {
546
606
  });
547
607
  const worktreeCount = listOutput.split("\n").filter((l) => l.startsWith("worktree ")).length;
548
608
  if (worktreeCount <= 1) return null;
609
+ const gitDir = path2.resolve(
610
+ cwd,
611
+ execFileSync2("git", ["rev-parse", "--git-dir"], {
612
+ cwd,
613
+ encoding: "utf-8",
614
+ timeout: 5e3,
615
+ stdio: ["ignore", "pipe", "ignore"]
616
+ }).trim()
617
+ );
618
+ const gitCommonDir = path2.resolve(
619
+ cwd,
620
+ execFileSync2("git", ["rev-parse", "--git-common-dir"], {
621
+ cwd,
622
+ encoding: "utf-8",
623
+ timeout: 5e3,
624
+ stdio: ["ignore", "pipe", "ignore"]
625
+ }).trim()
626
+ );
627
+ if (gitDir === gitCommonDir) return null;
549
628
  const branch = execFileSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
550
629
  cwd,
551
630
  encoding: "utf-8",
@@ -573,7 +652,7 @@ function detectWorktreeViaFilesystem(startDir) {
573
652
  const match = content.match(/^gitdir:\s*(.+)$/);
574
653
  if (!match) return null;
575
654
  const gitdir = match[1];
576
- if (!gitdir.match(/\/worktrees\/[^/]+$/)) return null;
655
+ if (!gitdir.match(/[/\\]worktrees[/\\][^/\\]+$/)) return null;
577
656
  const branch = readBranchFromHead(path2.resolve(dir, gitdir));
578
657
  const prefix = branchToPrefix(branch ?? "");
579
658
  if (!prefix) return null;
@@ -598,11 +677,13 @@ function readBranchFromHead(gitdir) {
598
677
  }
599
678
 
600
679
  // src/cli.ts
680
+ var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
681
+ var SUDO_PREFIX = isWindows ? "" : "sudo ";
601
682
  var DEBOUNCE_MS = 100;
602
683
  var POLL_INTERVAL_MS = 3e3;
603
684
  var EXIT_TIMEOUT_MS = 2e3;
604
685
  var SUDO_SPAWN_TIMEOUT_MS = 3e4;
605
- function startProxyServer(store, proxyPort, tlsOptions) {
686
+ function startProxyServer(store, proxyPort, tld, tlsOptions) {
606
687
  store.ensureDir();
607
688
  const isTls = !!tlsOptions;
608
689
  const routesPath = store.getRoutesPath();
@@ -618,7 +699,8 @@ function startProxyServer(store, proxyPort, tlsOptions) {
618
699
  let debounceTimer = null;
619
700
  let watcher = null;
620
701
  let pollingInterval = null;
621
- const autoSyncHosts = process.env.PORTLESS_SYNC_HOSTS === "1";
702
+ const syncVal = process.env.PORTLESS_SYNC_HOSTS;
703
+ const autoSyncHosts = syncVal === "1" || syncVal === "true" || tld !== DEFAULT_TLD && syncVal !== "0" && syncVal !== "false";
622
704
  const reloadRoutes = () => {
623
705
  try {
624
706
  cachedRoutes = store.loadRoutes();
@@ -643,6 +725,7 @@ function startProxyServer(store, proxyPort, tlsOptions) {
643
725
  const server = createProxyServer({
644
726
  getRoutes: () => cachedRoutes,
645
727
  proxyPort,
728
+ tld,
646
729
  onError: (msg) => console.error(chalk.red(msg)),
647
730
  tls: tlsOptions
648
731
  });
@@ -652,7 +735,11 @@ function startProxyServer(store, proxyPort, tlsOptions) {
652
735
  console.error(chalk.blue("Stop the existing proxy first:"));
653
736
  console.error(chalk.cyan(" portless proxy stop"));
654
737
  console.error(chalk.blue("Or check what is using the port:"));
655
- console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
738
+ console.error(
739
+ chalk.cyan(
740
+ isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
741
+ )
742
+ );
656
743
  } else if (err.code === "EACCES") {
657
744
  console.error(chalk.red(`Permission denied for port ${proxyPort}.`));
658
745
  console.error(chalk.blue("Either run with sudo:"));
@@ -668,9 +755,11 @@ function startProxyServer(store, proxyPort, tlsOptions) {
668
755
  fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
669
756
  fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
670
757
  writeTlsMarker(store.dir, isTls);
758
+ writeTldFile(store.dir, tld);
671
759
  fixOwnership(store.dir, store.pidPath, store.portFilePath);
672
760
  const proto = isTls ? "HTTPS/2" : "HTTP";
673
- console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}`));
761
+ const tldLabel = tld !== DEFAULT_TLD ? ` (TLD: .${tld})` : "";
762
+ console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}`));
674
763
  });
675
764
  let exiting = false;
676
765
  const cleanup = () => {
@@ -690,6 +779,7 @@ function startProxyServer(store, proxyPort, tlsOptions) {
690
779
  } catch {
691
780
  }
692
781
  writeTlsMarker(store.dir, false);
782
+ writeTldFile(store.dir, DEFAULT_TLD);
693
783
  if (autoSyncHosts) cleanHostsFile();
694
784
  server.close(() => process.exit(0));
695
785
  setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
@@ -699,12 +789,12 @@ function startProxyServer(store, proxyPort, tlsOptions) {
699
789
  console.log(chalk.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
700
790
  console.log(chalk.gray(`Routes file: ${store.getRoutesPath()}`));
701
791
  }
702
- async function stopProxy(store, proxyPort, tls2) {
792
+ async function stopProxy(store, proxyPort, _tls) {
703
793
  const pidPath = store.pidPath;
704
- const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
794
+ const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
705
795
  const sudoHint = needsSudo ? "sudo " : "";
706
796
  if (!fs3.existsSync(pidPath)) {
707
- if (await isProxyRunning(proxyPort, tls2)) {
797
+ if (await isProxyRunning(proxyPort)) {
708
798
  console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
709
799
  const pid = findPidOnPort(proxyPort);
710
800
  if (pid !== null) {
@@ -717,24 +807,38 @@ async function stopProxy(store, proxyPort, tls2) {
717
807
  console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
718
808
  } catch (err) {
719
809
  if (isErrnoException(err) && err.code === "EPERM") {
720
- console.error(chalk.red("Permission denied. The proxy was started with sudo."));
810
+ console.error(
811
+ chalk.red("Permission denied. The proxy was started with elevated privileges.")
812
+ );
721
813
  console.error(chalk.blue("Stop it with:"));
722
- console.error(chalk.cyan(" sudo portless proxy stop"));
814
+ console.error(
815
+ chalk.cyan(
816
+ isWindows ? " Run portless proxy stop as Administrator" : " sudo portless proxy stop"
817
+ )
818
+ );
723
819
  } else {
724
820
  const message = err instanceof Error ? err.message : String(err);
725
821
  console.error(chalk.red(`Failed to stop proxy: ${message}`));
726
822
  console.error(chalk.blue("Check if the process is still running:"));
727
- console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
823
+ console.error(
824
+ chalk.cyan(
825
+ isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
826
+ )
827
+ );
728
828
  }
729
829
  }
730
- } else if (process.getuid?.() !== 0) {
830
+ } else if (!isWindows && process.getuid?.() !== 0) {
731
831
  console.error(chalk.red("Cannot identify the process. It may be running as root."));
732
832
  console.error(chalk.blue("Try stopping with sudo:"));
733
833
  console.error(chalk.cyan(" sudo portless proxy stop"));
734
834
  } else {
735
835
  console.error(chalk.red(`Could not identify the process on port ${proxyPort}.`));
736
836
  console.error(chalk.blue("Try manually:"));
737
- console.error(chalk.cyan(` sudo kill "$(lsof -ti tcp:${proxyPort})"`));
837
+ console.error(
838
+ chalk.cyan(
839
+ isWindows ? " taskkill /F /PID <pid>" : ` sudo kill "$(lsof -ti tcp:${proxyPort})"`
840
+ )
841
+ );
738
842
  }
739
843
  } else {
740
844
  console.log(chalk.yellow("Proxy is not running."));
@@ -759,7 +863,7 @@ async function stopProxy(store, proxyPort, tls2) {
759
863
  }
760
864
  return;
761
865
  }
762
- if (!await isProxyRunning(proxyPort, tls2)) {
866
+ if (!await isProxyRunning(proxyPort)) {
763
867
  console.log(
764
868
  chalk.yellow(
765
869
  `PID file exists but port ${proxyPort} is not listening. The PID may have been recycled.`
@@ -778,14 +882,20 @@ async function stopProxy(store, proxyPort, tls2) {
778
882
  console.log(chalk.green("Proxy stopped."));
779
883
  } catch (err) {
780
884
  if (isErrnoException(err) && err.code === "EPERM") {
781
- console.error(chalk.red("Permission denied. The proxy was started with sudo."));
885
+ console.error(
886
+ chalk.red("Permission denied. The proxy was started with elevated privileges.")
887
+ );
782
888
  console.error(chalk.blue("Stop it with:"));
783
889
  console.error(chalk.cyan(` ${sudoHint}portless proxy stop`));
784
890
  } else {
785
891
  const message = err instanceof Error ? err.message : String(err);
786
892
  console.error(chalk.red(`Failed to stop proxy: ${message}`));
787
893
  console.error(chalk.blue("Check if the process is still running:"));
788
- console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
894
+ console.error(
895
+ chalk.cyan(
896
+ isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
897
+ )
898
+ );
789
899
  }
790
900
  }
791
901
  }
@@ -806,8 +916,22 @@ function listRoutes(store, proxyPort, tls2) {
806
916
  }
807
917
  console.log();
808
918
  }
809
- async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force, autoInfo, desiredPort) {
810
- const hostname = parseHostname(name);
919
+ async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
920
+ const hostname = parseHostname(name, tld);
921
+ let envTld;
922
+ try {
923
+ envTld = getDefaultTld();
924
+ } catch (err) {
925
+ console.error(chalk.red(`Error: ${err.message}`));
926
+ process.exit(1);
927
+ }
928
+ if (envTld !== DEFAULT_TLD && envTld !== tld) {
929
+ console.warn(
930
+ chalk.yellow(
931
+ `Warning: PORTLESS_TLD=${envTld} but the running proxy uses .${tld}. Using .${tld}.`
932
+ )
933
+ );
934
+ }
811
935
  console.log(chalk.blue.bold(`
812
936
  portless
813
937
  `));
@@ -821,7 +945,7 @@ portless
821
945
  }
822
946
  if (!await isProxyRunning(proxyPort, tls2)) {
823
947
  const defaultPort = getDefaultPort();
824
- const needsSudo = defaultPort < PRIVILEGED_PORT_THRESHOLD;
948
+ const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
825
949
  const wantHttps = isHttpsEnvEnabled();
826
950
  if (needsSudo) {
827
951
  if (!process.stdin.isTTY) {
@@ -845,6 +969,7 @@ portless
845
969
  console.log(chalk.yellow("Starting proxy (requires sudo)..."));
846
970
  const startArgs = [process.execPath, process.argv[1], "proxy", "start"];
847
971
  if (wantHttps) startArgs.push("--https");
972
+ if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
848
973
  const result = spawnSync("sudo", startArgs, {
849
974
  stdio: "inherit",
850
975
  timeout: SUDO_SPAWN_TIMEOUT_MS
@@ -859,6 +984,7 @@ portless
859
984
  console.log(chalk.yellow("Starting proxy..."));
860
985
  const startArgs = [process.argv[1], "proxy", "start"];
861
986
  if (wantHttps) startArgs.push("--https");
987
+ if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
862
988
  const result = spawnSync(process.execPath, startArgs, {
863
989
  stdio: "inherit",
864
990
  timeout: SUDO_SPAWN_TIMEOUT_MS
@@ -871,6 +997,7 @@ portless
871
997
  }
872
998
  }
873
999
  const autoTls = readTlsMarker(stateDir);
1000
+ tld = readTldFromDir(stateDir);
874
1001
  if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
875
1002
  console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
876
1003
  const logPath = path3.join(stateDir, "proxy.log");
@@ -918,7 +1045,7 @@ portless
918
1045
  PORT: port.toString(),
919
1046
  HOST: "127.0.0.1",
920
1047
  PORTLESS_URL: finalUrl,
921
- __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: ".localhost"
1048
+ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}`
922
1049
  },
923
1050
  onCleanup: () => {
924
1051
  try {
@@ -953,6 +1080,7 @@ function appPortFromEnv() {
953
1080
  function parseRunArgs(args) {
954
1081
  let force = false;
955
1082
  let appPort;
1083
+ let name;
956
1084
  let i = 0;
957
1085
  while (i < args.length && args[i].startsWith("-")) {
958
1086
  if (args[i] === "--") {
@@ -966,6 +1094,7 @@ ${chalk.bold("Usage:")}
966
1094
  ${chalk.cyan("portless run [options] <command...>")}
967
1095
 
968
1096
  ${chalk.bold("Options:")}
1097
+ --name <name> Override the inferred base name (worktree prefix still applies)
969
1098
  --force Override an existing route registered by another process
970
1099
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
971
1100
  --help, -h Show this help
@@ -975,11 +1104,13 @@ ${chalk.bold("Name inference (in order):")}
975
1104
  2. Git repo root directory name
976
1105
  3. Current directory basename
977
1106
 
1107
+ Use --name to override the inferred name while keeping worktree prefixes.
978
1108
  In git worktrees, the branch name is prepended as a subdomain prefix
979
1109
  (e.g. feature-auth.myapp.localhost).
980
1110
 
981
1111
  ${chalk.bold("Examples:")}
982
1112
  portless run next dev # -> http://<project>.localhost:1355
1113
+ portless run --name myapp next dev # -> http://myapp.localhost:1355
983
1114
  portless run vite dev # -> http://<project>.localhost:1355
984
1115
  portless run --app-port 3000 pnpm start
985
1116
  `);
@@ -989,15 +1120,23 @@ ${chalk.bold("Examples:")}
989
1120
  } else if (args[i] === "--app-port") {
990
1121
  i++;
991
1122
  appPort = parseAppPort(args[i]);
1123
+ } else if (args[i] === "--name") {
1124
+ i++;
1125
+ if (!args[i] || args[i].startsWith("-")) {
1126
+ console.error(chalk.red("Error: --name requires a name value."));
1127
+ console.error(chalk.cyan(" portless run --name <name> <command...>"));
1128
+ process.exit(1);
1129
+ }
1130
+ name = args[i];
992
1131
  } else {
993
1132
  console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
994
- console.error(chalk.blue("Known flags: --force, --app-port, --help"));
1133
+ console.error(chalk.blue("Known flags: --name, --force, --app-port, --help"));
995
1134
  process.exit(1);
996
1135
  }
997
1136
  i++;
998
1137
  }
999
1138
  if (!appPort) appPort = appPortFromEnv();
1000
- return { force, appPort, commandArgs: args.slice(i) };
1139
+ return { force, appPort, name, commandArgs: args.slice(i) };
1001
1140
  }
1002
1141
  function parseAppArgs(args) {
1003
1142
  let force = false;
@@ -1058,12 +1197,13 @@ ${chalk.bold("Usage:")}
1058
1197
  ${chalk.cyan("portless proxy stop")} Stop the proxy
1059
1198
  ${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
1060
1199
  ${chalk.cyan("portless run <cmd>")} Infer name from project, run through proxy
1200
+ ${chalk.cyan("portless get <name>")} Print URL for a service (for cross-service refs)
1061
1201
  ${chalk.cyan("portless alias <name> <port>")} Register a static route (e.g. for Docker)
1062
1202
  ${chalk.cyan("portless alias --remove <name>")} Remove a static route
1063
1203
  ${chalk.cyan("portless list")} Show active routes
1064
1204
  ${chalk.cyan("portless trust")} Add local CA to system trust store
1065
- ${chalk.cyan("portless hosts sync")} Add routes to /etc/hosts (fixes Safari)
1066
- ${chalk.cyan("portless hosts clean")} Remove portless entries from /etc/hosts
1205
+ ${chalk.cyan("portless hosts sync")} Add routes to ${HOSTS_DISPLAY} (fixes Safari)
1206
+ ${chalk.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
1067
1207
 
1068
1208
  ${chalk.bold("Examples:")}
1069
1209
  portless proxy start # Start proxy on port 1355
@@ -1073,6 +1213,7 @@ ${chalk.bold("Examples:")}
1073
1213
  portless api.myapp pnpm start # -> http://api.myapp.localhost:1355
1074
1214
  portless run next dev # -> http://<project>.localhost:1355
1075
1215
  portless run next dev # in worktree -> http://<worktree>.<project>.localhost:1355
1216
+ portless get backend # -> http://backend.localhost:1355 (for cross-service refs)
1076
1217
  # Wildcard subdomains: tenant.myapp.localhost also routes to myapp
1077
1218
 
1078
1219
  ${chalk.bold("In package.json:")}
@@ -1097,7 +1238,7 @@ ${chalk.bold("HTTP/2 + HTTPS:")}
1097
1238
  system trust store. No browser warnings. No sudo required on macOS.
1098
1239
 
1099
1240
  ${chalk.bold("Options:")}
1100
- run <cmd> Infer project name from package.json / git / cwd
1241
+ run [--name <name>] <cmd> Infer project name (or override with --name)
1101
1242
  Adds worktree prefix in git worktrees
1102
1243
  -p, --port <number> Port for the proxy to listen on (default: 1355)
1103
1244
  Ports < 1024 require sudo
@@ -1106,6 +1247,7 @@ ${chalk.bold("Options:")}
1106
1247
  --key <path> Use a custom TLS private key (implies --https)
1107
1248
  --no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
1108
1249
  --foreground Run proxy in foreground (for debugging)
1250
+ --tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
1109
1251
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
1110
1252
  --force Override an existing route registered by another process
1111
1253
  --name <name> Use <name> as the app name (bypasses subcommand dispatch)
@@ -1114,10 +1256,11 @@ ${chalk.bold("Options:")}
1114
1256
  ${chalk.bold("Environment variables:")}
1115
1257
  PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
1116
1258
  PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
1117
- PORTLESS_HTTPS=1|true Always enable HTTPS (set in .bashrc / .zshrc)
1118
- PORTLESS_SYNC_HOSTS=1 Auto-sync /etc/hosts (requires sudo proxy start)
1259
+ PORTLESS_HTTPS=1 Always enable HTTPS (set in .bashrc / .zshrc)
1260
+ PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
1261
+ PORTLESS_SYNC_HOSTS=1 Auto-sync ${HOSTS_DISPLAY} (auto-enabled for custom TLDs)
1119
1262
  PORTLESS_STATE_DIR=<path> Override the state directory
1120
- PORTLESS=0 | PORTLESS=skip Run command directly without proxy
1263
+ PORTLESS=0 Run command directly without proxy
1121
1264
 
1122
1265
  ${chalk.bold("Child process environment:")}
1123
1266
  PORT Ephemeral port the child should listen on
@@ -1127,26 +1270,24 @@ ${chalk.bold("Child process environment:")}
1127
1270
  ${chalk.bold("Safari / DNS:")}
1128
1271
  .localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
1129
1272
  Safari relies on the system DNS resolver, which may not handle them.
1130
- If Safari can't find your .localhost URL, run:
1131
- ${chalk.cyan("sudo portless hosts sync")}
1132
- This adds entries to /etc/hosts. Clean up later with:
1133
- ${chalk.cyan("sudo portless hosts clean")}
1134
- To auto-sync whenever routes change, set PORTLESS_SYNC_HOSTS=1 and
1135
- start the proxy with sudo.
1273
+ Auto-syncs ${HOSTS_DISPLAY} for custom TLDs (e.g. --tld test). For .localhost,
1274
+ set PORTLESS_SYNC_HOSTS=1 to enable. To manually sync:
1275
+ ${chalk.cyan(`${SUDO_PREFIX}portless hosts sync`)}
1276
+ Clean up later with:
1277
+ ${chalk.cyan(`${SUDO_PREFIX}portless hosts clean`)}
1136
1278
 
1137
1279
  ${chalk.bold("Skip portless:")}
1138
1280
  PORTLESS=0 pnpm dev # Runs command directly without proxy
1139
- PORTLESS=skip pnpm dev # Same as above
1140
1281
 
1141
1282
  ${chalk.bold("Reserved names:")}
1142
- run, alias, hosts, list, trust, proxy are subcommands and cannot be
1143
- used as app names directly. Use "portless run" to infer the name, or
1144
- "portless --name <name>" to force any name including reserved ones.
1283
+ run, get, alias, hosts, list, trust, proxy are subcommands and cannot
1284
+ be used as app names directly. Use "portless run" to infer the name,
1285
+ or "portless --name <name>" to force any name including reserved ones.
1145
1286
  `);
1146
1287
  process.exit(0);
1147
1288
  }
1148
1289
  function printVersion() {
1149
- console.log("0.5.2");
1290
+ console.log("0.7.0");
1150
1291
  process.exit(0);
1151
1292
  }
1152
1293
  async function handleTrust() {
@@ -1171,6 +1312,60 @@ async function handleList() {
1171
1312
  });
1172
1313
  listRoutes(store, port, tls2);
1173
1314
  }
1315
+ async function handleGet(args) {
1316
+ if (args[1] === "--help" || args[1] === "-h") {
1317
+ console.log(`
1318
+ ${chalk.bold("portless get")} - Print the URL for a service.
1319
+
1320
+ ${chalk.bold("Usage:")}
1321
+ ${chalk.cyan("portless get <name>")}
1322
+
1323
+ Constructs the URL using the same hostname and worktree logic as
1324
+ "portless run", then prints it to stdout. Useful for wiring services
1325
+ together:
1326
+
1327
+ BACKEND_URL=$(portless get backend)
1328
+
1329
+ ${chalk.bold("Options:")}
1330
+ --no-worktree Skip worktree prefix detection
1331
+ --help, -h Show this help
1332
+
1333
+ ${chalk.bold("Examples:")}
1334
+ portless get backend # -> http://backend.localhost:1355
1335
+ portless get backend # in worktree -> http://auth.backend.localhost:1355
1336
+ portless get backend --no-worktree # -> http://backend.localhost:1355 (skip worktree)
1337
+ `);
1338
+ process.exit(0);
1339
+ }
1340
+ let skipWorktree = false;
1341
+ const positional = [];
1342
+ for (let i = 1; i < args.length; i++) {
1343
+ if (args[i] === "--no-worktree") {
1344
+ skipWorktree = true;
1345
+ } else if (args[i].startsWith("-")) {
1346
+ console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
1347
+ console.error(chalk.blue("Known flags: --no-worktree, --help"));
1348
+ process.exit(1);
1349
+ } else {
1350
+ positional.push(args[i]);
1351
+ }
1352
+ }
1353
+ if (positional.length === 0) {
1354
+ console.error(chalk.red("Error: Missing service name."));
1355
+ console.error(chalk.blue("Usage:"));
1356
+ console.error(chalk.cyan(" portless get <name>"));
1357
+ console.error(chalk.blue("Example:"));
1358
+ console.error(chalk.cyan(" portless get backend"));
1359
+ process.exit(1);
1360
+ }
1361
+ const name = positional[0];
1362
+ const worktree = skipWorktree ? null : detectWorktreePrefix();
1363
+ const effectiveName = worktree ? `${worktree.prefix}.${name}` : name;
1364
+ const { port, tls: tls2, tld } = await discoverState();
1365
+ const hostname = parseHostname(effectiveName, tld);
1366
+ const url = formatUrl(hostname, port, tls2);
1367
+ process.stdout.write(url + "\n");
1368
+ }
1174
1369
  async function handleAlias(args) {
1175
1370
  if (args[1] === "--help" || args[1] === "-h") {
1176
1371
  console.log(`
@@ -1188,7 +1383,7 @@ ${chalk.bold("Examples:")}
1188
1383
  `);
1189
1384
  process.exit(0);
1190
1385
  }
1191
- const { dir } = await discoverState();
1386
+ const { dir, tld } = await discoverState();
1192
1387
  const store = new RouteStore(dir, {
1193
1388
  onWarning: (msg) => console.warn(chalk.yellow(msg))
1194
1389
  });
@@ -1199,7 +1394,7 @@ ${chalk.bold("Examples:")}
1199
1394
  console.error(chalk.cyan(" portless alias --remove <name>"));
1200
1395
  process.exit(1);
1201
1396
  }
1202
- const hostname2 = parseHostname(aliasName2);
1397
+ const hostname2 = parseHostname(aliasName2, tld);
1203
1398
  const routes = store.loadRoutes();
1204
1399
  const existing = routes.find((r) => r.hostname === hostname2 && r.pid === 0);
1205
1400
  if (!existing) {
@@ -1221,7 +1416,7 @@ ${chalk.bold("Examples:")}
1221
1416
  console.error(chalk.cyan(" portless alias my-postgres 5432"));
1222
1417
  process.exit(1);
1223
1418
  }
1224
- const hostname = parseHostname(aliasName);
1419
+ const hostname = parseHostname(aliasName, tld);
1225
1420
  const port = parseInt(aliasPort, 10);
1226
1421
  if (isNaN(port) || port < 1 || port > 65535) {
1227
1422
  console.error(chalk.red(`Error: Invalid port "${aliasPort}". Must be 1-65535.`));
@@ -1229,32 +1424,36 @@ ${chalk.bold("Examples:")}
1229
1424
  }
1230
1425
  const force = args.includes("--force");
1231
1426
  store.addRoute(hostname, port, 0, force);
1232
- console.log(chalk.green(`Alias registered: ${hostname} -> localhost:${port}`));
1427
+ console.log(chalk.green(`Alias registered: ${hostname} -> 127.0.0.1:${port}`));
1233
1428
  }
1234
1429
  async function handleHosts(args) {
1235
1430
  if (args[1] === "--help" || args[1] === "-h") {
1236
1431
  console.log(`
1237
- ${chalk.bold("portless hosts")} - Manage /etc/hosts entries for .localhost subdomains.
1432
+ ${chalk.bold("portless hosts")} - Manage ${HOSTS_DISPLAY} entries for .localhost subdomains.
1238
1433
 
1239
1434
  Safari relies on the system DNS resolver, which may not handle .localhost
1240
- subdomains. This command adds entries to /etc/hosts as a workaround.
1435
+ subdomains. This command adds entries to ${HOSTS_DISPLAY} as a workaround.
1241
1436
 
1242
1437
  ${chalk.bold("Usage:")}
1243
- ${chalk.cyan("sudo portless hosts sync")} Add current routes to /etc/hosts
1244
- ${chalk.cyan("sudo portless hosts clean")} Remove portless entries from /etc/hosts
1438
+ ${chalk.cyan(`${SUDO_PREFIX}portless hosts sync`)} Add current routes to ${HOSTS_DISPLAY}
1439
+ ${chalk.cyan(`${SUDO_PREFIX}portless hosts clean`)} Remove portless entries from ${HOSTS_DISPLAY}
1245
1440
 
1246
1441
  ${chalk.bold("Auto-sync:")}
1247
- Set PORTLESS_SYNC_HOSTS=1 and start the proxy with sudo to auto-sync
1248
- /etc/hosts whenever routes change.
1442
+ Auto-enabled for custom TLDs (e.g. --tld test). For .localhost, set
1443
+ PORTLESS_SYNC_HOSTS=1 to enable. Disable with PORTLESS_SYNC_HOSTS=0.
1249
1444
  `);
1250
1445
  process.exit(0);
1251
1446
  }
1252
1447
  if (args[1] === "clean") {
1253
1448
  if (cleanHostsFile()) {
1254
- console.log(chalk.green("Removed portless entries from /etc/hosts."));
1449
+ console.log(chalk.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
1255
1450
  } else {
1256
- console.error(chalk.red("Failed to update /etc/hosts (requires sudo)."));
1257
- console.error(chalk.cyan(" sudo portless hosts clean"));
1451
+ console.error(
1452
+ chalk.red(
1453
+ `Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : " (requires sudo)."}`
1454
+ )
1455
+ );
1456
+ console.error(chalk.cyan(` ${SUDO_PREFIX}portless hosts clean`));
1258
1457
  process.exit(1);
1259
1458
  }
1260
1459
  return;
@@ -1263,16 +1462,18 @@ ${chalk.bold("Auto-sync:")}
1263
1462
  console.log(`
1264
1463
  ${chalk.bold("Usage: portless hosts <command>")}
1265
1464
 
1266
- ${chalk.cyan("sudo portless hosts sync")} Add current routes to /etc/hosts
1267
- ${chalk.cyan("sudo portless hosts clean")} Remove portless entries from /etc/hosts
1465
+ ${chalk.cyan(`${SUDO_PREFIX}portless hosts sync`)} Add current routes to ${HOSTS_DISPLAY}
1466
+ ${chalk.cyan(`${SUDO_PREFIX}portless hosts clean`)} Remove portless entries from ${HOSTS_DISPLAY}
1268
1467
  `);
1269
1468
  process.exit(0);
1270
1469
  }
1271
1470
  if (args[1] !== "sync") {
1272
1471
  console.error(chalk.red(`Error: Unknown hosts subcommand "${args[1]}".`));
1273
1472
  console.error(chalk.blue("Usage:"));
1274
- console.error(chalk.cyan(" portless hosts sync # Add routes to /etc/hosts"));
1275
- console.error(chalk.cyan(" portless hosts clean # Remove portless entries"));
1473
+ console.error(
1474
+ chalk.cyan(` ${SUDO_PREFIX}portless hosts sync # Add routes to ${HOSTS_DISPLAY}`)
1475
+ );
1476
+ console.error(chalk.cyan(` ${SUDO_PREFIX}portless hosts clean # Remove portless entries`));
1276
1477
  process.exit(1);
1277
1478
  }
1278
1479
  const { dir } = await discoverState();
@@ -1286,13 +1487,17 @@ ${chalk.bold("Usage: portless hosts <command>")}
1286
1487
  }
1287
1488
  const hostnames = routes.map((r) => r.hostname);
1288
1489
  if (syncHostsFile(hostnames)) {
1289
- console.log(chalk.green(`Synced ${hostnames.length} hostname(s) to /etc/hosts:`));
1490
+ console.log(chalk.green(`Synced ${hostnames.length} hostname(s) to ${HOSTS_DISPLAY}:`));
1290
1491
  for (const h of hostnames) {
1291
1492
  console.log(chalk.cyan(` 127.0.0.1 ${h}`));
1292
1493
  }
1293
1494
  } else {
1294
- console.error(chalk.red("Failed to update /etc/hosts (requires sudo)."));
1295
- console.error(chalk.cyan(" sudo portless hosts sync"));
1495
+ console.error(
1496
+ chalk.red(
1497
+ `Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : " (requires sudo)."}`
1498
+ )
1499
+ );
1500
+ console.error(chalk.cyan(` ${SUDO_PREFIX}portless hosts sync`));
1296
1501
  process.exit(1);
1297
1502
  }
1298
1503
  }
@@ -1315,6 +1520,7 @@ ${chalk.bold("Usage:")}
1315
1520
  ${chalk.cyan("portless proxy start --https")} Start with HTTP/2 + TLS
1316
1521
  ${chalk.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
1317
1522
  ${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
1523
+ ${chalk.cyan("portless proxy start --tld test")} Use .test instead of .localhost
1318
1524
  ${chalk.cyan("portless proxy stop")} Stop the proxy
1319
1525
  `);
1320
1526
  process.exit(isProxyHelp || !args[1] ? 0 : 1);
@@ -1363,6 +1569,41 @@ ${chalk.bold("Usage:")}
1363
1569
  console.error(chalk.red("Error: --cert and --key must be used together."));
1364
1570
  process.exit(1);
1365
1571
  }
1572
+ let tld;
1573
+ try {
1574
+ tld = getDefaultTld();
1575
+ } catch (err) {
1576
+ console.error(chalk.red(`Error: ${err.message}`));
1577
+ process.exit(1);
1578
+ }
1579
+ const tldIdx = args.indexOf("--tld");
1580
+ if (tldIdx !== -1) {
1581
+ const tldValue = args[tldIdx + 1];
1582
+ if (!tldValue || tldValue.startsWith("-")) {
1583
+ console.error(chalk.red("Error: --tld requires a TLD value (e.g. test, localhost)."));
1584
+ process.exit(1);
1585
+ }
1586
+ tld = tldValue.trim().toLowerCase();
1587
+ const tldErr = validateTld(tld);
1588
+ if (tldErr) {
1589
+ console.error(chalk.red(`Error: ${tldErr}`));
1590
+ process.exit(1);
1591
+ }
1592
+ }
1593
+ const riskyReason = RISKY_TLDS.get(tld);
1594
+ if (riskyReason) {
1595
+ console.warn(chalk.yellow(`Warning: .${tld} -- ${riskyReason}`));
1596
+ }
1597
+ const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
1598
+ if (tld !== DEFAULT_TLD && syncDisabled) {
1599
+ console.warn(
1600
+ chalk.yellow(
1601
+ `Warning: .${tld} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
1602
+ )
1603
+ );
1604
+ console.warn(chalk.yellow("Hosts sync is disabled. To add entries manually, run:"));
1605
+ console.warn(chalk.cyan(` ${SUDO_PREFIX}portless hosts sync`));
1606
+ }
1366
1607
  const useHttps = wantHttps || !!(customCertPath && customKeyPath);
1367
1608
  const stateDir = resolveStateDir(proxyPort);
1368
1609
  const store = new RouteStore(stateDir, {
@@ -1372,13 +1613,18 @@ ${chalk.bold("Usage:")}
1372
1613
  if (isForeground) {
1373
1614
  return;
1374
1615
  }
1375
- const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
1616
+ const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
1376
1617
  const sudoPrefix = needsSudo ? "sudo " : "";
1618
+ const portFlag = proxyPort !== getDefaultPort() ? ` -p ${proxyPort}` : "";
1377
1619
  console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
1378
- console.log(chalk.blue(`To restart: portless proxy stop && ${sudoPrefix}portless proxy start`));
1620
+ console.log(
1621
+ chalk.blue(
1622
+ `To restart: ${sudoPrefix}portless proxy stop${portFlag} && ${sudoPrefix}portless proxy start${portFlag}`
1623
+ )
1624
+ );
1379
1625
  return;
1380
1626
  }
1381
- if (proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
1627
+ if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
1382
1628
  console.error(chalk.red(`Error: Port ${proxyPort} requires sudo.`));
1383
1629
  console.error(chalk.blue("Either run with sudo:"));
1384
1630
  console.error(chalk.cyan(" sudo portless proxy start -p 80"));
@@ -1440,13 +1686,13 @@ ${chalk.bold("Usage:")}
1440
1686
  tlsOptions = {
1441
1687
  cert,
1442
1688
  key,
1443
- SNICallback: createSNICallback(stateDir, cert, key)
1689
+ SNICallback: createSNICallback(stateDir, cert, key, tld)
1444
1690
  };
1445
1691
  }
1446
1692
  }
1447
1693
  if (isForeground) {
1448
1694
  console.log(chalk.blue.bold("\nportless proxy\n"));
1449
- startProxyServer(store, proxyPort, tlsOptions);
1695
+ startProxyServer(store, proxyPort, tld, tlsOptions);
1450
1696
  return;
1451
1697
  }
1452
1698
  store.ensureDir();
@@ -1469,10 +1715,14 @@ ${chalk.bold("Usage:")}
1469
1715
  daemonArgs.push("--https");
1470
1716
  }
1471
1717
  }
1718
+ if (tld !== DEFAULT_TLD) {
1719
+ daemonArgs.push("--tld", tld);
1720
+ }
1472
1721
  const child = spawn(process.execPath, daemonArgs, {
1473
1722
  detached: true,
1474
1723
  stdio: ["ignore", logFd, logFd],
1475
- env: process.env
1724
+ env: process.env,
1725
+ windowsHide: true
1476
1726
  });
1477
1727
  child.unref();
1478
1728
  } finally {
@@ -1481,7 +1731,7 @@ ${chalk.bold("Usage:")}
1481
1731
  if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
1482
1732
  console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
1483
1733
  console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
1484
- const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
1734
+ const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
1485
1735
  console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
1486
1736
  if (fs3.existsSync(logPath)) {
1487
1737
  console.error(chalk.gray(`Logs: ${logPath}`));
@@ -1501,10 +1751,19 @@ async function handleRunMode(args) {
1501
1751
  console.error(chalk.cyan(" portless run next dev"));
1502
1752
  process.exit(1);
1503
1753
  }
1504
- const inferred = inferProjectName();
1754
+ let baseName;
1755
+ let nameSource;
1756
+ if (parsed.name) {
1757
+ baseName = parsed.name;
1758
+ nameSource = "--name flag";
1759
+ } else {
1760
+ const inferred = inferProjectName();
1761
+ baseName = inferred.name;
1762
+ nameSource = inferred.source;
1763
+ }
1505
1764
  const worktree = detectWorktreePrefix();
1506
- const effectiveName = worktree ? `${worktree.prefix}.${inferred.name}` : inferred.name;
1507
- const { dir, port, tls: tls2 } = await discoverState();
1765
+ const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
1766
+ const { dir, port, tls: tls2, tld } = await discoverState();
1508
1767
  const store = new RouteStore(dir, {
1509
1768
  onWarning: (msg) => console.warn(chalk.yellow(msg))
1510
1769
  });
@@ -1515,8 +1774,9 @@ async function handleRunMode(args) {
1515
1774
  effectiveName,
1516
1775
  parsed.commandArgs,
1517
1776
  tls2,
1777
+ tld,
1518
1778
  parsed.force,
1519
- { nameSource: inferred.source, prefix: worktree?.prefix, prefixSource: worktree?.source },
1779
+ { nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
1520
1780
  parsed.appPort
1521
1781
  );
1522
1782
  }
@@ -1530,7 +1790,7 @@ async function handleNamedMode(args) {
1530
1790
  console.error(chalk.cyan(" portless myapp next dev"));
1531
1791
  process.exit(1);
1532
1792
  }
1533
- const { dir, port, tls: tls2 } = await discoverState();
1793
+ const { dir, port, tls: tls2, tld } = await discoverState();
1534
1794
  const store = new RouteStore(dir, {
1535
1795
  onWarning: (msg) => console.warn(chalk.yellow(msg))
1536
1796
  });
@@ -1541,6 +1801,7 @@ async function handleNamedMode(args) {
1541
1801
  parsed.name,
1542
1802
  parsed.commandArgs,
1543
1803
  tls2,
1804
+ tld,
1544
1805
  parsed.force,
1545
1806
  void 0,
1546
1807
  parsed.appPort
@@ -1571,7 +1832,7 @@ async function main() {
1571
1832
  console.error(chalk.cyan(" portless --name <name> <command...>"));
1572
1833
  process.exit(1);
1573
1834
  }
1574
- const skipPortless2 = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
1835
+ const skipPortless2 = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
1575
1836
  if (skipPortless2) {
1576
1837
  const { commandArgs } = parseAppArgs(args);
1577
1838
  if (commandArgs.length === 0) {
@@ -1588,7 +1849,7 @@ async function main() {
1588
1849
  if (isRunCommand) {
1589
1850
  args.shift();
1590
1851
  }
1591
- const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
1852
+ const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
1592
1853
  if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy")) {
1593
1854
  const { commandArgs } = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
1594
1855
  if (commandArgs.length === 0) {
@@ -1615,6 +1876,10 @@ async function main() {
1615
1876
  await handleList();
1616
1877
  return;
1617
1878
  }
1879
+ if (args[0] === "get") {
1880
+ await handleGet(args);
1881
+ return;
1882
+ }
1618
1883
  if (args[0] === "alias") {
1619
1884
  await handleAlias(args);
1620
1885
  return;