portless 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,26 +7,401 @@ import {
7
7
  formatUrl,
8
8
  isErrnoException,
9
9
  parseHostname
10
- } from "./chunk-Y5OVKUR4.js";
10
+ } from "./chunk-VRBD6YAY.js";
11
11
 
12
12
  // src/cli.ts
13
13
  import chalk from "chalk";
14
- import * as fs2 from "fs";
15
- import * as path2 from "path";
14
+ import * as fs3 from "fs";
15
+ import * as path3 from "path";
16
16
  import { spawn as spawn2, spawnSync } from "child_process";
17
17
 
18
- // src/cli-utils.ts
18
+ // src/certs.ts
19
19
  import * as fs from "fs";
20
+ import * as path from "path";
21
+ import * as crypto from "crypto";
22
+ import * as tls from "tls";
23
+ import { execFile as execFileCb, execFileSync } from "child_process";
24
+ import { promisify } from "util";
25
+ var CA_VALIDITY_DAYS = 3650;
26
+ function fixOwnership(...paths) {
27
+ const uid = process.env.SUDO_UID;
28
+ const gid = process.env.SUDO_GID;
29
+ if (!uid || process.getuid?.() !== 0) return;
30
+ for (const p of paths) {
31
+ try {
32
+ fs.chownSync(p, parseInt(uid, 10), parseInt(gid || uid, 10));
33
+ } catch {
34
+ }
35
+ }
36
+ }
37
+ var SERVER_VALIDITY_DAYS = 365;
38
+ var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
39
+ var CA_COMMON_NAME = "portless Local CA";
40
+ var OPENSSL_TIMEOUT_MS = 15e3;
41
+ var CA_KEY_FILE = "ca-key.pem";
42
+ var CA_CERT_FILE = "ca.pem";
43
+ var SERVER_KEY_FILE = "server-key.pem";
44
+ var SERVER_CERT_FILE = "server.pem";
45
+ function fileExists(filePath) {
46
+ try {
47
+ fs.accessSync(filePath, fs.constants.R_OK);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function isCertValid(certPath) {
54
+ try {
55
+ const pem = fs.readFileSync(certPath, "utf-8");
56
+ const cert = new crypto.X509Certificate(pem);
57
+ const expiry = new Date(cert.validTo).getTime();
58
+ return Date.now() + EXPIRY_BUFFER_MS < expiry;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ function openssl(args, options) {
64
+ try {
65
+ return execFileSync("openssl", args, {
66
+ encoding: "utf-8",
67
+ timeout: OPENSSL_TIMEOUT_MS,
68
+ input: options?.input,
69
+ stdio: ["pipe", "pipe", "pipe"]
70
+ });
71
+ } catch (err) {
72
+ const message = err instanceof Error ? err.message : String(err);
73
+ throw new Error(
74
+ `openssl failed: ${message}
75
+
76
+ Make sure openssl is installed (ships with macOS and most Linux distributions).`
77
+ );
78
+ }
79
+ }
80
+ var execFileAsync = promisify(execFileCb);
81
+ async function opensslAsync(args) {
82
+ try {
83
+ const { stdout } = await execFileAsync("openssl", args, {
84
+ encoding: "utf-8",
85
+ timeout: OPENSSL_TIMEOUT_MS
86
+ });
87
+ return stdout;
88
+ } catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ throw new Error(
91
+ `openssl failed: ${message}
92
+
93
+ Make sure openssl is installed (ships with macOS and most Linux distributions).`
94
+ );
95
+ }
96
+ }
97
+ function generateCA(stateDir) {
98
+ const keyPath = path.join(stateDir, CA_KEY_FILE);
99
+ const certPath = path.join(stateDir, CA_CERT_FILE);
100
+ openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", keyPath]);
101
+ openssl([
102
+ "req",
103
+ "-new",
104
+ "-x509",
105
+ "-key",
106
+ keyPath,
107
+ "-out",
108
+ certPath,
109
+ "-days",
110
+ CA_VALIDITY_DAYS.toString(),
111
+ "-subj",
112
+ `/CN=${CA_COMMON_NAME}`,
113
+ "-addext",
114
+ "basicConstraints=critical,CA:TRUE",
115
+ "-addext",
116
+ "keyUsage=critical,keyCertSign,cRLSign"
117
+ ]);
118
+ fs.chmodSync(keyPath, 384);
119
+ fs.chmodSync(certPath, 420);
120
+ fixOwnership(keyPath, certPath);
121
+ return { certPath, keyPath };
122
+ }
123
+ function generateServerCert(stateDir) {
124
+ const caKeyPath = path.join(stateDir, CA_KEY_FILE);
125
+ const caCertPath = path.join(stateDir, CA_CERT_FILE);
126
+ const serverKeyPath = path.join(stateDir, SERVER_KEY_FILE);
127
+ const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
128
+ const csrPath = path.join(stateDir, "server.csr");
129
+ const extPath = path.join(stateDir, "server-ext.cnf");
130
+ openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
131
+ openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
132
+ fs.writeFileSync(
133
+ extPath,
134
+ [
135
+ "authorityKeyIdentifier=keyid,issuer",
136
+ "basicConstraints=CA:FALSE",
137
+ "keyUsage=digitalSignature,keyEncipherment",
138
+ "extendedKeyUsage=serverAuth",
139
+ "subjectAltName=DNS:localhost,DNS:*.localhost"
140
+ ].join("\n") + "\n"
141
+ );
142
+ openssl([
143
+ "x509",
144
+ "-req",
145
+ "-in",
146
+ csrPath,
147
+ "-CA",
148
+ caCertPath,
149
+ "-CAkey",
150
+ caKeyPath,
151
+ "-CAcreateserial",
152
+ "-out",
153
+ serverCertPath,
154
+ "-days",
155
+ SERVER_VALIDITY_DAYS.toString(),
156
+ "-extfile",
157
+ extPath
158
+ ]);
159
+ for (const tmp of [csrPath, extPath]) {
160
+ try {
161
+ fs.unlinkSync(tmp);
162
+ } catch {
163
+ }
164
+ }
165
+ fs.chmodSync(serverKeyPath, 384);
166
+ fs.chmodSync(serverCertPath, 420);
167
+ fixOwnership(serverKeyPath, serverCertPath);
168
+ return { certPath: serverCertPath, keyPath: serverKeyPath };
169
+ }
170
+ function ensureCerts(stateDir) {
171
+ const caCertPath = path.join(stateDir, CA_CERT_FILE);
172
+ const caKeyPath = path.join(stateDir, CA_KEY_FILE);
173
+ const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
174
+ let caGenerated = false;
175
+ if (!fileExists(caCertPath) || !fileExists(caKeyPath) || !isCertValid(caCertPath)) {
176
+ generateCA(stateDir);
177
+ caGenerated = true;
178
+ }
179
+ if (caGenerated || !fileExists(serverCertPath) || !isCertValid(serverCertPath)) {
180
+ generateServerCert(stateDir);
181
+ }
182
+ return {
183
+ certPath: serverCertPath,
184
+ keyPath: path.join(stateDir, SERVER_KEY_FILE),
185
+ caPath: caCertPath,
186
+ caGenerated
187
+ };
188
+ }
189
+ function isCATrusted(stateDir) {
190
+ const caCertPath = path.join(stateDir, CA_CERT_FILE);
191
+ if (!fileExists(caCertPath)) return false;
192
+ if (process.platform === "darwin") {
193
+ return isCATrustedMacOS(caCertPath);
194
+ } else if (process.platform === "linux") {
195
+ return isCATrustedLinux(stateDir);
196
+ }
197
+ return false;
198
+ }
199
+ function isCATrustedMacOS(caCertPath) {
200
+ try {
201
+ const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
202
+ for (const keychain of [loginKeychainPath(), "/Library/Keychains/System.keychain"]) {
203
+ try {
204
+ const result = execFileSync("security", ["find-certificate", "-a", "-Z", keychain], {
205
+ encoding: "utf-8",
206
+ timeout: 5e3,
207
+ stdio: ["pipe", "pipe", "pipe"]
208
+ });
209
+ if (result.toLowerCase().includes(fingerprint)) return true;
210
+ } catch {
211
+ }
212
+ }
213
+ return false;
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+ function loginKeychainPath() {
219
+ try {
220
+ const result = execFileSync("security", ["default-keychain"], {
221
+ encoding: "utf-8",
222
+ timeout: 5e3
223
+ }).trim();
224
+ const match = result.match(/"(.+)"/);
225
+ if (match) return match[1];
226
+ } catch {
227
+ }
228
+ const home = process.env.HOME || `/Users/${process.env.USER || "unknown"}`;
229
+ return path.join(home, "Library", "Keychains", "login.keychain-db");
230
+ }
231
+ function isCATrustedLinux(stateDir) {
232
+ const systemCertPath = `/usr/local/share/ca-certificates/portless-ca.crt`;
233
+ if (!fileExists(systemCertPath)) return false;
234
+ try {
235
+ const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
236
+ const installed = fs.readFileSync(systemCertPath, "utf-8").trim();
237
+ return ours === installed;
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+ var HOST_CERTS_DIR = "host-certs";
243
+ function sanitizeHostForFilename(hostname) {
244
+ return hostname.replace(/\./g, "_").replace(/[^a-z0-9_-]/gi, "");
245
+ }
246
+ async function generateHostCertAsync(stateDir, hostname) {
247
+ const caKeyPath = path.join(stateDir, CA_KEY_FILE);
248
+ const caCertPath = path.join(stateDir, CA_CERT_FILE);
249
+ const hostDir = path.join(stateDir, HOST_CERTS_DIR);
250
+ if (!fs.existsSync(hostDir)) {
251
+ await fs.promises.mkdir(hostDir, { recursive: true, mode: 493 });
252
+ fixOwnership(hostDir);
253
+ }
254
+ const safeName = sanitizeHostForFilename(hostname);
255
+ const keyPath = path.join(hostDir, `${safeName}-key.pem`);
256
+ const certPath = path.join(hostDir, `${safeName}.pem`);
257
+ const csrPath = path.join(hostDir, `${safeName}.csr`);
258
+ const extPath = path.join(hostDir, `${safeName}-ext.cnf`);
259
+ await opensslAsync(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", keyPath]);
260
+ await opensslAsync(["req", "-new", "-key", keyPath, "-out", csrPath, "-subj", `/CN=${hostname}`]);
261
+ const sans = [`DNS:${hostname}`];
262
+ const parts = hostname.split(".");
263
+ if (parts.length >= 2) {
264
+ sans.push(`DNS:*.${parts.slice(1).join(".")}`);
265
+ }
266
+ await fs.promises.writeFile(
267
+ extPath,
268
+ [
269
+ "authorityKeyIdentifier=keyid,issuer",
270
+ "basicConstraints=CA:FALSE",
271
+ "keyUsage=digitalSignature,keyEncipherment",
272
+ "extendedKeyUsage=serverAuth",
273
+ `subjectAltName=${sans.join(",")}`
274
+ ].join("\n") + "\n"
275
+ );
276
+ await opensslAsync([
277
+ "x509",
278
+ "-req",
279
+ "-in",
280
+ csrPath,
281
+ "-CA",
282
+ caCertPath,
283
+ "-CAkey",
284
+ caKeyPath,
285
+ "-CAcreateserial",
286
+ "-out",
287
+ certPath,
288
+ "-days",
289
+ SERVER_VALIDITY_DAYS.toString(),
290
+ "-extfile",
291
+ extPath
292
+ ]);
293
+ for (const tmp of [csrPath, extPath]) {
294
+ try {
295
+ await fs.promises.unlink(tmp);
296
+ } catch {
297
+ }
298
+ }
299
+ await fs.promises.chmod(keyPath, 384);
300
+ await fs.promises.chmod(certPath, 420);
301
+ fixOwnership(keyPath, certPath);
302
+ return { certPath, keyPath };
303
+ }
304
+ function isSimpleLocalhostSubdomain(hostname) {
305
+ const parts = hostname.split(".");
306
+ return parts.length === 2 && parts[1] === "localhost";
307
+ }
308
+ function createSNICallback(stateDir, defaultCert, defaultKey) {
309
+ const cache = /* @__PURE__ */ new Map();
310
+ const pending = /* @__PURE__ */ new Map();
311
+ const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
312
+ return (servername, cb) => {
313
+ if (servername === "localhost" || isSimpleLocalhostSubdomain(servername)) {
314
+ cb(null, defaultCtx);
315
+ return;
316
+ }
317
+ if (cache.has(servername)) {
318
+ cb(null, cache.get(servername));
319
+ return;
320
+ }
321
+ const safeName = sanitizeHostForFilename(servername);
322
+ const hostDir = path.join(stateDir, HOST_CERTS_DIR);
323
+ const certPath = path.join(hostDir, `${safeName}.pem`);
324
+ const keyPath = path.join(hostDir, `${safeName}-key.pem`);
325
+ if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath)) {
326
+ try {
327
+ const ctx = tls.createSecureContext({
328
+ cert: fs.readFileSync(certPath),
329
+ key: fs.readFileSync(keyPath)
330
+ });
331
+ cache.set(servername, ctx);
332
+ cb(null, ctx);
333
+ return;
334
+ } catch {
335
+ }
336
+ }
337
+ if (pending.has(servername)) {
338
+ pending.get(servername).then((ctx) => cb(null, ctx)).catch((err) => cb(err instanceof Error ? err : new Error(String(err))));
339
+ return;
340
+ }
341
+ const promise = generateHostCertAsync(stateDir, servername).then(async (generated) => {
342
+ const [cert, key] = await Promise.all([
343
+ fs.promises.readFile(generated.certPath),
344
+ fs.promises.readFile(generated.keyPath)
345
+ ]);
346
+ return tls.createSecureContext({ cert, key });
347
+ });
348
+ pending.set(servername, promise);
349
+ promise.then((ctx) => {
350
+ cache.set(servername, ctx);
351
+ pending.delete(servername);
352
+ cb(null, ctx);
353
+ }).catch((err) => {
354
+ pending.delete(servername);
355
+ cb(err instanceof Error ? err : new Error(String(err)));
356
+ });
357
+ };
358
+ }
359
+ function trustCA(stateDir) {
360
+ const caCertPath = path.join(stateDir, CA_CERT_FILE);
361
+ if (!fileExists(caCertPath)) {
362
+ return { trusted: false, error: "CA certificate not found. Run with --https first." };
363
+ }
364
+ try {
365
+ if (process.platform === "darwin") {
366
+ const keychain = loginKeychainPath();
367
+ execFileSync(
368
+ "security",
369
+ ["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
370
+ { stdio: "pipe", timeout: 3e4 }
371
+ );
372
+ return { trusted: true };
373
+ } else if (process.platform === "linux") {
374
+ const dest = "/usr/local/share/ca-certificates/portless-ca.crt";
375
+ fs.copyFileSync(caCertPath, dest);
376
+ execFileSync("update-ca-certificates", [], { stdio: "pipe", timeout: 3e4 });
377
+ return { trusted: true };
378
+ }
379
+ return { trusted: false, error: `Unsupported platform: ${process.platform}` };
380
+ } catch (err) {
381
+ const message = err instanceof Error ? err.message : String(err);
382
+ if (message.includes("authorization") || message.includes("permission") || message.includes("EACCES")) {
383
+ return {
384
+ trusted: false,
385
+ error: "Permission denied. Try: sudo portless trust"
386
+ };
387
+ }
388
+ return { trusted: false, error: message };
389
+ }
390
+ }
391
+
392
+ // src/cli-utils.ts
393
+ import * as fs2 from "fs";
20
394
  import * as http from "http";
395
+ import * as https from "https";
21
396
  import * as net from "net";
22
397
  import * as os from "os";
23
- import * as path from "path";
398
+ import * as path2 from "path";
24
399
  import * as readline from "readline";
25
400
  import { execSync, spawn } from "child_process";
26
401
  var DEFAULT_PROXY_PORT = 1355;
27
402
  var PRIVILEGED_PORT_THRESHOLD = 1024;
28
403
  var SYSTEM_STATE_DIR = "/tmp/portless";
29
- var USER_STATE_DIR = path.join(os.homedir(), ".portless");
404
+ var USER_STATE_DIR = path2.join(os.homedir(), ".portless");
30
405
  var MIN_APP_PORT = 4e3;
31
406
  var MAX_APP_PORT = 4999;
32
407
  var RANDOM_PORT_ATTEMPTS = 50;
@@ -56,29 +431,59 @@ function resolveStateDir(port) {
56
431
  }
57
432
  function readPortFromDir(dir) {
58
433
  try {
59
- const raw = fs.readFileSync(path.join(dir, "proxy.port"), "utf-8").trim();
434
+ const raw = fs2.readFileSync(path2.join(dir, "proxy.port"), "utf-8").trim();
60
435
  const port = parseInt(raw, 10);
61
436
  return isNaN(port) ? null : port;
62
437
  } catch {
63
438
  return null;
64
439
  }
65
440
  }
441
+ var TLS_MARKER_FILE = "proxy.tls";
442
+ function readTlsMarker(dir) {
443
+ try {
444
+ return fs2.existsSync(path2.join(dir, TLS_MARKER_FILE));
445
+ } catch {
446
+ return false;
447
+ }
448
+ }
449
+ function writeTlsMarker(dir, enabled) {
450
+ const markerPath = path2.join(dir, TLS_MARKER_FILE);
451
+ if (enabled) {
452
+ fs2.writeFileSync(markerPath, "1", { mode: 420 });
453
+ } else {
454
+ try {
455
+ fs2.unlinkSync(markerPath);
456
+ } catch {
457
+ }
458
+ }
459
+ }
460
+ function isHttpsEnvEnabled() {
461
+ const val = process.env.PORTLESS_HTTPS;
462
+ return val === "1" || val === "true";
463
+ }
66
464
  async function discoverState() {
67
465
  if (process.env.PORTLESS_STATE_DIR) {
68
466
  const dir = process.env.PORTLESS_STATE_DIR;
69
467
  const port = readPortFromDir(dir) ?? getDefaultPort();
70
- return { dir, port };
468
+ const tls2 = readTlsMarker(dir);
469
+ return { dir, port, tls: tls2 };
71
470
  }
72
471
  const userPort = readPortFromDir(USER_STATE_DIR);
73
- if (userPort !== null && await isProxyRunning(userPort)) {
74
- return { dir: USER_STATE_DIR, port: userPort };
472
+ if (userPort !== null) {
473
+ const tls2 = readTlsMarker(USER_STATE_DIR);
474
+ if (await isProxyRunning(userPort, tls2)) {
475
+ return { dir: USER_STATE_DIR, port: userPort, tls: tls2 };
476
+ }
75
477
  }
76
478
  const systemPort = readPortFromDir(SYSTEM_STATE_DIR);
77
- if (systemPort !== null && await isProxyRunning(systemPort)) {
78
- return { dir: SYSTEM_STATE_DIR, port: systemPort };
479
+ if (systemPort !== null) {
480
+ const tls2 = readTlsMarker(SYSTEM_STATE_DIR);
481
+ if (await isProxyRunning(systemPort, tls2)) {
482
+ return { dir: SYSTEM_STATE_DIR, port: systemPort, tls: tls2 };
483
+ }
79
484
  }
80
485
  const defaultPort = getDefaultPort();
81
- return { dir: resolveStateDir(defaultPort), port: defaultPort };
486
+ return { dir: resolveStateDir(defaultPort), port: defaultPort, tls: false };
82
487
  }
83
488
  async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
84
489
  if (minPort > maxPort) {
@@ -106,15 +511,17 @@ async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
106
511
  }
107
512
  throw new Error(`No free port found in range ${minPort}-${maxPort}`);
108
513
  }
109
- function isProxyRunning(port) {
514
+ function isProxyRunning(port, tls2 = false) {
110
515
  return new Promise((resolve) => {
111
- const req = http.request(
516
+ const requestFn = tls2 ? https.request : http.request;
517
+ const req = requestFn(
112
518
  {
113
519
  hostname: "127.0.0.1",
114
520
  port,
115
521
  path: "/",
116
522
  method: "HEAD",
117
- timeout: SOCKET_TIMEOUT_MS
523
+ timeout: SOCKET_TIMEOUT_MS,
524
+ ...tls2 ? { rejectUnauthorized: false } : {}
118
525
  },
119
526
  (res) => {
120
527
  res.resume();
@@ -141,10 +548,10 @@ function findPidOnPort(port) {
141
548
  return null;
142
549
  }
143
550
  }
144
- async function waitForProxy(port, maxAttempts = WAIT_FOR_PROXY_MAX_ATTEMPTS, intervalMs = WAIT_FOR_PROXY_INTERVAL_MS) {
551
+ async function waitForProxy(port, maxAttempts = WAIT_FOR_PROXY_MAX_ATTEMPTS, intervalMs = WAIT_FOR_PROXY_INTERVAL_MS, tls2 = false) {
145
552
  for (let i = 0; i < maxAttempts; i++) {
146
553
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
147
- if (await isProxyRunning(port)) {
554
+ if (await isProxyRunning(port, tls2)) {
148
555
  return true;
149
556
  }
150
557
  }
@@ -192,6 +599,28 @@ function spawnCommand(commandArgs, options) {
192
599
  process.exit(code ?? 1);
193
600
  });
194
601
  }
602
+ var FRAMEWORKS_NEEDING_PORT = {
603
+ vite: { strictPort: true },
604
+ "react-router": { strictPort: true },
605
+ astro: { strictPort: false },
606
+ ng: { strictPort: false }
607
+ };
608
+ function injectFrameworkFlags(commandArgs, port) {
609
+ const cmd = commandArgs[0];
610
+ if (!cmd) return;
611
+ const basename2 = path2.basename(cmd);
612
+ const framework = FRAMEWORKS_NEEDING_PORT[basename2];
613
+ if (!framework) return;
614
+ if (!commandArgs.includes("--port")) {
615
+ commandArgs.push("--port", port.toString());
616
+ if (framework.strictPort) {
617
+ commandArgs.push("--strictPort");
618
+ }
619
+ }
620
+ if (!commandArgs.includes("--host")) {
621
+ commandArgs.push("--host", "127.0.0.1");
622
+ }
623
+ }
195
624
  function prompt(question) {
196
625
  const rl = readline.createInterface({
197
626
  input: process.stdin,
@@ -211,14 +640,15 @@ var DEBOUNCE_MS = 100;
211
640
  var POLL_INTERVAL_MS = 3e3;
212
641
  var EXIT_TIMEOUT_MS = 2e3;
213
642
  var SUDO_SPAWN_TIMEOUT_MS = 3e4;
214
- function startProxyServer(store, proxyPort) {
643
+ function startProxyServer(store, proxyPort, tlsOptions) {
215
644
  store.ensureDir();
645
+ const isTls = !!tlsOptions;
216
646
  const routesPath = store.getRoutesPath();
217
- if (!fs2.existsSync(routesPath)) {
218
- fs2.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
647
+ if (!fs3.existsSync(routesPath)) {
648
+ fs3.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
219
649
  }
220
650
  try {
221
- fs2.chmodSync(routesPath, FILE_MODE);
651
+ fs3.chmodSync(routesPath, FILE_MODE);
222
652
  } catch {
223
653
  }
224
654
  let cachedRoutes = store.loadRoutes();
@@ -232,7 +662,7 @@ function startProxyServer(store, proxyPort) {
232
662
  }
233
663
  };
234
664
  try {
235
- watcher = fs2.watch(routesPath, () => {
665
+ watcher = fs3.watch(routesPath, () => {
236
666
  if (debounceTimer) clearTimeout(debounceTimer);
237
667
  debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
238
668
  });
@@ -243,7 +673,8 @@ function startProxyServer(store, proxyPort) {
243
673
  const server = createProxyServer({
244
674
  getRoutes: () => cachedRoutes,
245
675
  proxyPort,
246
- onError: (msg) => console.error(chalk.red(msg))
676
+ onError: (msg) => console.error(chalk.red(msg)),
677
+ tls: tlsOptions
247
678
  });
248
679
  server.on("error", (err) => {
249
680
  if (err.code === "EADDRINUSE") {
@@ -264,9 +695,11 @@ function startProxyServer(store, proxyPort) {
264
695
  process.exit(1);
265
696
  });
266
697
  server.listen(proxyPort, () => {
267
- fs2.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
268
- fs2.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
269
- console.log(chalk.green(`HTTP proxy listening on port ${proxyPort}`));
698
+ fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
699
+ fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
700
+ writeTlsMarker(store.dir, isTls);
701
+ const proto = isTls ? "HTTPS/2" : "HTTP";
702
+ console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}`));
270
703
  });
271
704
  let exiting = false;
272
705
  const cleanup = () => {
@@ -278,13 +711,14 @@ function startProxyServer(store, proxyPort) {
278
711
  watcher.close();
279
712
  }
280
713
  try {
281
- fs2.unlinkSync(store.pidPath);
714
+ fs3.unlinkSync(store.pidPath);
282
715
  } catch {
283
716
  }
284
717
  try {
285
- fs2.unlinkSync(store.portFilePath);
718
+ fs3.unlinkSync(store.portFilePath);
286
719
  } catch {
287
720
  }
721
+ writeTlsMarker(store.dir, false);
288
722
  server.close(() => process.exit(0));
289
723
  setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
290
724
  };
@@ -293,19 +727,19 @@ function startProxyServer(store, proxyPort) {
293
727
  console.log(chalk.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
294
728
  console.log(chalk.gray(`Routes file: ${store.getRoutesPath()}`));
295
729
  }
296
- async function stopProxy(store, proxyPort) {
730
+ async function stopProxy(store, proxyPort, tls2) {
297
731
  const pidPath = store.pidPath;
298
732
  const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
299
733
  const sudoHint = needsSudo ? "sudo " : "";
300
- if (!fs2.existsSync(pidPath)) {
301
- if (await isProxyRunning(proxyPort)) {
734
+ if (!fs3.existsSync(pidPath)) {
735
+ if (await isProxyRunning(proxyPort, tls2)) {
302
736
  console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
303
737
  const pid = findPidOnPort(proxyPort);
304
738
  if (pid !== null) {
305
739
  try {
306
740
  process.kill(pid, "SIGTERM");
307
741
  try {
308
- fs2.unlinkSync(store.portFilePath);
742
+ fs3.unlinkSync(store.portFilePath);
309
743
  } catch {
310
744
  }
311
745
  console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
@@ -336,37 +770,37 @@ async function stopProxy(store, proxyPort) {
336
770
  return;
337
771
  }
338
772
  try {
339
- const pid = parseInt(fs2.readFileSync(pidPath, "utf-8"), 10);
773
+ const pid = parseInt(fs3.readFileSync(pidPath, "utf-8"), 10);
340
774
  if (isNaN(pid)) {
341
775
  console.error(chalk.red("Corrupted PID file. Removing it."));
342
- fs2.unlinkSync(pidPath);
776
+ fs3.unlinkSync(pidPath);
343
777
  return;
344
778
  }
345
779
  try {
346
780
  process.kill(pid, 0);
347
781
  } catch {
348
782
  console.log(chalk.yellow("Proxy process is no longer running. Cleaning up stale files."));
349
- fs2.unlinkSync(pidPath);
783
+ fs3.unlinkSync(pidPath);
350
784
  try {
351
- fs2.unlinkSync(store.portFilePath);
785
+ fs3.unlinkSync(store.portFilePath);
352
786
  } catch {
353
787
  }
354
788
  return;
355
789
  }
356
- if (!await isProxyRunning(proxyPort)) {
790
+ if (!await isProxyRunning(proxyPort, tls2)) {
357
791
  console.log(
358
792
  chalk.yellow(
359
793
  `PID file exists but port ${proxyPort} is not listening. The PID may have been recycled.`
360
794
  )
361
795
  );
362
796
  console.log(chalk.yellow("Removing stale PID file."));
363
- fs2.unlinkSync(pidPath);
797
+ fs3.unlinkSync(pidPath);
364
798
  return;
365
799
  }
366
800
  process.kill(pid, "SIGTERM");
367
- fs2.unlinkSync(pidPath);
801
+ fs3.unlinkSync(pidPath);
368
802
  try {
369
- fs2.unlinkSync(store.portFilePath);
803
+ fs3.unlinkSync(store.portFilePath);
370
804
  } catch {
371
805
  }
372
806
  console.log(chalk.green("Proxy stopped."));
@@ -383,7 +817,7 @@ async function stopProxy(store, proxyPort) {
383
817
  }
384
818
  }
385
819
  }
386
- function listRoutes(store, proxyPort) {
820
+ function listRoutes(store, proxyPort, tls2) {
387
821
  const routes = store.loadRoutes();
388
822
  if (routes.length === 0) {
389
823
  console.log(chalk.yellow("No active routes."));
@@ -392,23 +826,23 @@ function listRoutes(store, proxyPort) {
392
826
  }
393
827
  console.log(chalk.blue.bold("\nActive routes:\n"));
394
828
  for (const route of routes) {
395
- const url = formatUrl(route.hostname, proxyPort);
829
+ const url = formatUrl(route.hostname, proxyPort, tls2);
396
830
  console.log(
397
831
  ` ${chalk.cyan(url)} ${chalk.gray("->")} ${chalk.white(`localhost:${route.port}`)} ${chalk.gray(`(pid ${route.pid})`)}`
398
832
  );
399
833
  }
400
834
  console.log();
401
835
  }
402
- async function runApp(store, proxyPort, stateDir, name, commandArgs) {
836
+ async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2) {
403
837
  const hostname = parseHostname(name);
404
- const appUrl = formatUrl(hostname, proxyPort);
405
838
  console.log(chalk.blue.bold(`
406
839
  portless
407
840
  `));
408
841
  console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
409
- if (!await isProxyRunning(proxyPort)) {
842
+ if (!await isProxyRunning(proxyPort, tls2)) {
410
843
  const defaultPort = getDefaultPort();
411
844
  const needsSudo = defaultPort < PRIVILEGED_PORT_THRESHOLD;
845
+ const wantHttps = isHttpsEnvEnabled();
412
846
  if (needsSudo) {
413
847
  if (!process.stdin.isTTY) {
414
848
  console.error(chalk.red("Proxy is not running."));
@@ -429,7 +863,9 @@ portless
429
863
  return;
430
864
  }
431
865
  console.log(chalk.yellow("Starting proxy (requires sudo)..."));
432
- const result = spawnSync("sudo", [process.execPath, process.argv[1], "proxy", "start"], {
866
+ const startArgs = [process.execPath, process.argv[1], "proxy", "start"];
867
+ if (wantHttps) startArgs.push("--https");
868
+ const result = spawnSync("sudo", startArgs, {
433
869
  stdio: "inherit",
434
870
  timeout: SUDO_SPAWN_TIMEOUT_MS
435
871
  });
@@ -441,7 +877,9 @@ portless
441
877
  }
442
878
  } else {
443
879
  console.log(chalk.yellow("Starting proxy..."));
444
- const result = spawnSync(process.execPath, [process.argv[1], "proxy", "start"], {
880
+ const startArgs = [process.argv[1], "proxy", "start"];
881
+ if (wantHttps) startArgs.push("--https");
882
+ const result = spawnSync(process.execPath, startArgs, {
445
883
  stdio: "inherit",
446
884
  timeout: SUDO_SPAWN_TIMEOUT_MS
447
885
  });
@@ -452,16 +890,18 @@ portless
452
890
  process.exit(1);
453
891
  }
454
892
  }
455
- if (!await waitForProxy(defaultPort)) {
893
+ const autoTls = readTlsMarker(stateDir);
894
+ if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
456
895
  console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
457
- const logPath = path2.join(stateDir, "proxy.log");
896
+ const logPath = path3.join(stateDir, "proxy.log");
458
897
  console.error(chalk.blue("Try starting the proxy manually to see the error:"));
459
898
  console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
460
- if (fs2.existsSync(logPath)) {
899
+ if (fs3.existsSync(logPath)) {
461
900
  console.error(chalk.gray(`Logs: ${logPath}`));
462
901
  }
463
902
  process.exit(1);
464
903
  }
904
+ tls2 = autoTls;
465
905
  console.log(chalk.green("Proxy started in background"));
466
906
  } else {
467
907
  console.log(chalk.gray("-- Proxy is running"));
@@ -469,13 +909,20 @@ portless
469
909
  const port = await findFreePort();
470
910
  console.log(chalk.green(`-- Using port ${port}`));
471
911
  store.addRoute(hostname, port, process.pid);
912
+ const finalUrl = formatUrl(hostname, proxyPort, tls2);
472
913
  console.log(chalk.cyan.bold(`
473
- -> ${appUrl}
914
+ -> ${finalUrl}
474
915
  `));
475
- console.log(chalk.gray(`Running: PORT=${port} ${commandArgs.join(" ")}
916
+ injectFrameworkFlags(commandArgs, port);
917
+ console.log(chalk.gray(`Running: PORT=${port} HOST=127.0.0.1 ${commandArgs.join(" ")}
476
918
  `));
477
919
  spawnCommand(commandArgs, {
478
- env: { ...process.env, PORT: port.toString() },
920
+ env: {
921
+ ...process.env,
922
+ PORT: port.toString(),
923
+ HOST: "127.0.0.1",
924
+ __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: ".localhost"
925
+ },
479
926
  onCleanup: () => {
480
927
  try {
481
928
  store.removeRoute(hostname);
@@ -512,14 +959,18 @@ ${chalk.bold("Install:")}
512
959
 
513
960
  ${chalk.bold("Usage:")}
514
961
  ${chalk.cyan("portless proxy start")} Start the proxy (background daemon)
962
+ ${chalk.cyan("portless proxy start --https")} Start with HTTP/2 + TLS (auto-generates certs)
515
963
  ${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
516
964
  ${chalk.cyan("portless proxy stop")} Stop the proxy
517
965
  ${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
518
966
  ${chalk.cyan("portless list")} Show active routes
967
+ ${chalk.cyan("portless trust")} Add local CA to system trust store
519
968
 
520
969
  ${chalk.bold("Examples:")}
521
970
  portless proxy start # Start proxy on port 1355
971
+ portless proxy start --https # Start with HTTPS/2 (faster page loads)
522
972
  portless myapp next dev # -> http://myapp.localhost:1355
973
+ portless myapp vite dev # -> http://myapp.localhost:1355
523
974
  portless api.myapp pnpm start # -> http://api.myapp.localhost:1355
524
975
 
525
976
  ${chalk.bold("In package.json:")}
@@ -534,14 +985,26 @@ ${chalk.bold("How it works:")}
534
985
  2. Run your apps - they auto-start the proxy and register automatically
535
986
  3. Access via http://<name>.localhost:1355
536
987
  4. .localhost domains auto-resolve to 127.0.0.1
988
+ 5. Frameworks that ignore PORT (Vite, Astro, React Router, Angular) get
989
+ --port and --host flags injected automatically
990
+
991
+ ${chalk.bold("HTTP/2 + HTTPS:")}
992
+ Use --https for HTTP/2 multiplexing (faster dev server page loads).
993
+ On first use, portless generates a local CA and adds it to your
994
+ system trust store. No browser warnings. No sudo required on macOS.
537
995
 
538
996
  ${chalk.bold("Options:")}
539
997
  -p, --port <number> Port for the proxy to listen on (default: 1355)
540
998
  Ports < 1024 require sudo
999
+ --https Enable HTTP/2 + TLS with auto-generated certs
1000
+ --cert <path> Use a custom TLS certificate (implies --https)
1001
+ --key <path> Use a custom TLS private key (implies --https)
1002
+ --no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
541
1003
  --foreground Run proxy in foreground (for debugging)
542
1004
 
543
1005
  ${chalk.bold("Environment variables:")}
544
1006
  PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
1007
+ PORTLESS_HTTPS=1 Always enable HTTPS (set in .bashrc / .zshrc)
545
1008
  PORTLESS_STATE_DIR=<path> Override the state directory
546
1009
  PORTLESS=0 | PORTLESS=skip Run command directly without proxy
547
1010
 
@@ -552,24 +1015,40 @@ ${chalk.bold("Skip portless:")}
552
1015
  process.exit(0);
553
1016
  }
554
1017
  if (args[0] === "--version" || args[0] === "-v") {
555
- console.log("0.3.0");
1018
+ console.log("0.4.1");
556
1019
  process.exit(0);
557
1020
  }
1021
+ if (args[0] === "trust") {
1022
+ const { dir: dir2 } = await discoverState();
1023
+ const result = trustCA(dir2);
1024
+ if (result.trusted) {
1025
+ console.log(chalk.green("Local CA added to system trust store."));
1026
+ console.log(chalk.gray("Browsers will now trust portless HTTPS certificates."));
1027
+ } else {
1028
+ console.error(chalk.red(`Failed to trust CA: ${result.error}`));
1029
+ if (result.error?.includes("sudo")) {
1030
+ console.error(chalk.blue("Run with sudo:"));
1031
+ console.error(chalk.cyan(" sudo portless trust"));
1032
+ }
1033
+ process.exit(1);
1034
+ }
1035
+ return;
1036
+ }
558
1037
  if (args[0] === "list") {
559
- const { dir: dir2, port: port2 } = await discoverState();
1038
+ const { dir: dir2, port: port2, tls: tls3 } = await discoverState();
560
1039
  const store2 = new RouteStore(dir2, {
561
1040
  onWarning: (msg) => console.warn(chalk.yellow(msg))
562
1041
  });
563
- listRoutes(store2, port2);
1042
+ listRoutes(store2, port2, tls3);
564
1043
  return;
565
1044
  }
566
1045
  if (args[0] === "proxy") {
567
1046
  if (args[1] === "stop") {
568
- const { dir: dir2, port: port2 } = await discoverState();
1047
+ const { dir: dir2, port: port2, tls: tls3 } = await discoverState();
569
1048
  const store3 = new RouteStore(dir2, {
570
1049
  onWarning: (msg) => console.warn(chalk.yellow(msg))
571
1050
  });
572
- await stopProxy(store3, port2);
1051
+ await stopProxy(store3, port2, tls3);
573
1052
  return;
574
1053
  }
575
1054
  if (args[1] !== "start") {
@@ -577,6 +1056,7 @@ ${chalk.bold("Skip portless:")}
577
1056
  ${chalk.bold("Usage: portless proxy <command>")}
578
1057
 
579
1058
  ${chalk.cyan("portless proxy start")} Start the proxy (daemon)
1059
+ ${chalk.cyan("portless proxy start --https")} Start with HTTP/2 + TLS
580
1060
  ${chalk.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
581
1061
  ${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
582
1062
  ${chalk.cyan("portless proxy stop")} Stop the proxy
@@ -602,6 +1082,32 @@ ${chalk.bold("Usage: portless proxy <command>")}
602
1082
  process.exit(1);
603
1083
  }
604
1084
  }
1085
+ const hasNoTls = args.includes("--no-tls");
1086
+ const hasHttpsFlag = args.includes("--https");
1087
+ const wantHttps = !hasNoTls && (hasHttpsFlag || isHttpsEnvEnabled());
1088
+ let customCertPath = null;
1089
+ let customKeyPath = null;
1090
+ const certIdx = args.indexOf("--cert");
1091
+ if (certIdx !== -1) {
1092
+ customCertPath = args[certIdx + 1] || null;
1093
+ if (!customCertPath || customCertPath.startsWith("-")) {
1094
+ console.error(chalk.red("Error: --cert requires a file path."));
1095
+ process.exit(1);
1096
+ }
1097
+ }
1098
+ const keyIdx = args.indexOf("--key");
1099
+ if (keyIdx !== -1) {
1100
+ customKeyPath = args[keyIdx + 1] || null;
1101
+ if (!customKeyPath || customKeyPath.startsWith("-")) {
1102
+ console.error(chalk.red("Error: --key requires a file path."));
1103
+ process.exit(1);
1104
+ }
1105
+ }
1106
+ if (customCertPath && !customKeyPath || !customCertPath && customKeyPath) {
1107
+ console.error(chalk.red("Error: --cert and --key must be used together."));
1108
+ process.exit(1);
1109
+ }
1110
+ const useHttps = wantHttps || !!(customCertPath && customKeyPath);
605
1111
  const stateDir = resolveStateDir(proxyPort);
606
1112
  const store2 = new RouteStore(stateDir, {
607
1113
  onWarning: (msg) => console.warn(chalk.yellow(msg))
@@ -626,23 +1132,90 @@ ${chalk.bold("Usage: portless proxy <command>")}
626
1132
  console.error(chalk.cyan(" portless proxy start"));
627
1133
  process.exit(1);
628
1134
  }
1135
+ let tlsOptions;
1136
+ if (useHttps) {
1137
+ store2.ensureDir();
1138
+ if (customCertPath && customKeyPath) {
1139
+ try {
1140
+ const cert = fs3.readFileSync(customCertPath);
1141
+ const key = fs3.readFileSync(customKeyPath);
1142
+ const certStr = cert.toString("utf-8");
1143
+ const keyStr = key.toString("utf-8");
1144
+ if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
1145
+ console.error(chalk.red(`Error: ${customCertPath} is not a valid PEM certificate.`));
1146
+ console.error(chalk.gray("Expected a file starting with -----BEGIN CERTIFICATE-----"));
1147
+ process.exit(1);
1148
+ }
1149
+ if (!keyStr.match(/-----BEGIN [\w\s]*PRIVATE KEY-----/)) {
1150
+ console.error(chalk.red(`Error: ${customKeyPath} is not a valid PEM private key.`));
1151
+ console.error(
1152
+ chalk.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----")
1153
+ );
1154
+ process.exit(1);
1155
+ }
1156
+ tlsOptions = { cert, key };
1157
+ } catch (err) {
1158
+ const message = err instanceof Error ? err.message : String(err);
1159
+ console.error(chalk.red(`Error reading certificate files: ${message}`));
1160
+ process.exit(1);
1161
+ }
1162
+ } else {
1163
+ console.log(chalk.gray("Ensuring TLS certificates..."));
1164
+ const certs = ensureCerts(stateDir);
1165
+ if (certs.caGenerated) {
1166
+ console.log(chalk.green("Generated local CA certificate."));
1167
+ }
1168
+ if (!isCATrusted(stateDir)) {
1169
+ console.log(chalk.yellow("Adding CA to system trust store..."));
1170
+ const trustResult = trustCA(stateDir);
1171
+ if (trustResult.trusted) {
1172
+ console.log(
1173
+ chalk.green("CA added to system trust store. Browsers will trust portless certs.")
1174
+ );
1175
+ } else {
1176
+ console.warn(chalk.yellow("Could not add CA to system trust store."));
1177
+ if (trustResult.error) {
1178
+ console.warn(chalk.gray(trustResult.error));
1179
+ }
1180
+ console.warn(
1181
+ chalk.yellow("Browsers will show certificate warnings. To fix this later, run:")
1182
+ );
1183
+ console.warn(chalk.cyan(" portless trust"));
1184
+ }
1185
+ }
1186
+ const cert = fs3.readFileSync(certs.certPath);
1187
+ const key = fs3.readFileSync(certs.keyPath);
1188
+ tlsOptions = {
1189
+ cert,
1190
+ key,
1191
+ SNICallback: createSNICallback(stateDir, cert, key)
1192
+ };
1193
+ }
1194
+ }
629
1195
  if (isForeground) {
630
1196
  console.log(chalk.blue.bold("\nportless proxy\n"));
631
- startProxyServer(store2, proxyPort);
1197
+ startProxyServer(store2, proxyPort, tlsOptions);
632
1198
  return;
633
1199
  }
634
1200
  store2.ensureDir();
635
- const logPath = path2.join(stateDir, "proxy.log");
636
- const logFd = fs2.openSync(logPath, "a");
1201
+ const logPath = path3.join(stateDir, "proxy.log");
1202
+ const logFd = fs3.openSync(logPath, "a");
637
1203
  try {
638
1204
  try {
639
- fs2.chmodSync(logPath, FILE_MODE);
1205
+ fs3.chmodSync(logPath, FILE_MODE);
640
1206
  } catch {
641
1207
  }
642
1208
  const daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
643
1209
  if (portFlagIndex !== -1) {
644
1210
  daemonArgs.push("--port", proxyPort.toString());
645
1211
  }
1212
+ if (useHttps) {
1213
+ if (customCertPath && customKeyPath) {
1214
+ daemonArgs.push("--cert", customCertPath, "--key", customKeyPath);
1215
+ } else {
1216
+ daemonArgs.push("--https");
1217
+ }
1218
+ }
646
1219
  const child = spawn2(process.execPath, daemonArgs, {
647
1220
  detached: true,
648
1221
  stdio: ["ignore", logFd, logFd],
@@ -650,19 +1223,20 @@ ${chalk.bold("Usage: portless proxy <command>")}
650
1223
  });
651
1224
  child.unref();
652
1225
  } finally {
653
- fs2.closeSync(logFd);
1226
+ fs3.closeSync(logFd);
654
1227
  }
655
- if (!await waitForProxy(proxyPort)) {
1228
+ if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
656
1229
  console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
657
1230
  console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
658
1231
  const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
659
1232
  console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
660
- if (fs2.existsSync(logPath)) {
1233
+ if (fs3.existsSync(logPath)) {
661
1234
  console.error(chalk.gray(`Logs: ${logPath}`));
662
1235
  }
663
1236
  process.exit(1);
664
1237
  }
665
- console.log(chalk.green(`Proxy started on port ${proxyPort}`));
1238
+ const proto = useHttps ? "HTTPS/2" : "HTTP";
1239
+ console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
666
1240
  return;
667
1241
  }
668
1242
  const name = args[0];
@@ -675,11 +1249,11 @@ ${chalk.bold("Usage: portless proxy <command>")}
675
1249
  console.error(chalk.cyan(" portless myapp next dev"));
676
1250
  process.exit(1);
677
1251
  }
678
- const { dir, port } = await discoverState();
1252
+ const { dir, port, tls: tls2 } = await discoverState();
679
1253
  const store = new RouteStore(dir, {
680
1254
  onWarning: (msg) => console.warn(chalk.yellow(msg))
681
1255
  });
682
- await runApp(store, port, dir, name, commandArgs);
1256
+ await runApp(store, port, dir, name, commandArgs, tls2);
683
1257
  }
684
1258
  main().catch((err) => {
685
1259
  const message = err instanceof Error ? err.message : String(err);