@hamedb89/localghost 0.1.13 → 0.1.15

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
@@ -250,7 +250,12 @@ function getProjectName(cwd = process.cwd()) {
250
250
  }
251
251
  }
252
252
  function sanitizeProjectName(value) {
253
- const projectName = value.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
253
+ const sanitized = value.replace(/[^\w.-]+/g, "-");
254
+ let start = 0;
255
+ let end = sanitized.length;
256
+ while (start < end && sanitized.charCodeAt(start) === 45) start += 1;
257
+ while (end > start && sanitized.charCodeAt(end - 1) === 45) end -= 1;
258
+ const projectName = sanitized.slice(start, end);
254
259
  return projectName || "app";
255
260
  }
256
261
 
@@ -334,6 +339,26 @@ function startCaddy(path) {
334
339
  stdio: caddyStdio()
335
340
  });
336
341
  }
342
+ function stopCaddyProcesses(pids, killProcess = (pid, signal) => process.kill(pid, signal)) {
343
+ const result = {
344
+ stopped: [],
345
+ alreadyExited: [],
346
+ failed: []
347
+ };
348
+ for (const pid of new Set(pids)) {
349
+ try {
350
+ killProcess(pid, "SIGINT");
351
+ result.stopped.push(pid);
352
+ } catch (error) {
353
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
354
+ result.alreadyExited.push(pid);
355
+ } else {
356
+ result.failed.push({ pid, error });
357
+ }
358
+ }
359
+ }
360
+ return result;
361
+ }
337
362
  async function trustCaddy(path) {
338
363
  await execa("caddy", ["trust", "--config", path], {
339
364
  cwd: dirname3(path),
@@ -454,19 +479,19 @@ function formatDetectedDevServices(services) {
454
479
 
455
480
  // src/context.ts
456
481
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
457
- import { join as join5 } from "path";
482
+ import { join as join6 } from "path";
458
483
  import { pathToFileURL } from "url";
459
484
 
460
485
  // src/port.ts
461
486
  import { createServer } from "net";
462
487
  async function isPortAvailable(port, host = "127.0.0.1") {
463
- return new Promise((resolve3) => {
488
+ return new Promise((resolve4) => {
464
489
  const server = createServer();
465
490
  server.once("error", () => {
466
- resolve3(false);
491
+ resolve4(false);
467
492
  });
468
493
  server.once("listening", () => {
469
- server.close(() => resolve3(true));
494
+ server.close(() => resolve4(true));
470
495
  });
471
496
  server.listen(port, host);
472
497
  });
@@ -483,6 +508,176 @@ async function findAvailablePort(startPort, options = {}) {
483
508
  throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
484
509
  }
485
510
 
511
+ // src/registry.ts
512
+ import { randomUUID } from "crypto";
513
+ import { mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "fs/promises";
514
+ import { homedir as homedir2 } from "os";
515
+ import { join as join5, normalize, resolve as resolve3 } from "path";
516
+ var LOCALGHOST_REGISTRY_FILE = "registry.json";
517
+ var LOCALGHOST_REGISTRY_LOCK_FILE = "registry.lock";
518
+ function defaultProcessRunning(pid) {
519
+ if (pid <= 0) return false;
520
+ try {
521
+ process.kill(pid, 0);
522
+ return true;
523
+ } catch (error) {
524
+ return error.code === "EPERM";
525
+ }
526
+ }
527
+ function getLocalghostRegistryRoot(env = process.env) {
528
+ return resolve3(env.LOCALGHOST_HOME || join5(homedir2(), ".localghost"));
529
+ }
530
+ function canonicalizeLocalghostProjectCwd(cwd = process.cwd()) {
531
+ return normalize(resolve3(cwd));
532
+ }
533
+ function emptyRegistry() {
534
+ return { version: 1, allocations: [], leases: [] };
535
+ }
536
+ function leaseKey(projectCwd, instanceKey) {
537
+ return `${projectCwd}\0${instanceKey}`;
538
+ }
539
+ function validRegistry(value) {
540
+ if (!value || typeof value !== "object") return false;
541
+ const candidate = value;
542
+ return candidate.version === 1 && Array.isArray(candidate.allocations) && Array.isArray(candidate.leases);
543
+ }
544
+ function pruneRegistry(registry, now, isRunning) {
545
+ registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
546
+ }
547
+ async function readJson(path) {
548
+ try {
549
+ return JSON.parse(await readFile(path, "utf8"));
550
+ } catch (error) {
551
+ if (error.code === "ENOENT") return void 0;
552
+ return void 0;
553
+ }
554
+ }
555
+ function createLocalghostRegistry(options = {}) {
556
+ const root = resolve3(options.stateRoot ?? getLocalghostRegistryRoot());
557
+ const registryPath = join5(root, LOCALGHOST_REGISTRY_FILE);
558
+ const lockPath = join5(root, LOCALGHOST_REGISTRY_LOCK_FILE);
559
+ const cwd = canonicalizeLocalghostProjectCwd(options.cwd);
560
+ const now = options.now ?? Date.now;
561
+ const pid = options.pid ?? process.pid;
562
+ const ownerToken = options.ownerToken ?? randomUUID();
563
+ const isRunning = options.isProcessRunning ?? defaultProcessRunning;
564
+ const availabilityCheck = options.availabilityCheck ?? isPortAvailable;
565
+ const lockTimeoutMs = options.lockTimeoutMs ?? 5e3;
566
+ const lockRetryMs = options.lockRetryMs ?? 25;
567
+ const lockStaleMs = options.lockStaleMs ?? 3e4;
568
+ async function readRegistry() {
569
+ const value = await readJson(registryPath);
570
+ return validRegistry(value) ? value : emptyRegistry();
571
+ }
572
+ async function writeRegistry(registry) {
573
+ await mkdir(root, { recursive: true });
574
+ const temporaryPath = join5(root, `.registry.${process.pid}.${randomUUID()}.tmp`);
575
+ await writeFile(temporaryPath, `${JSON.stringify(registry, null, 2)}
576
+ `, { mode: 384 });
577
+ await rename(temporaryPath, registryPath);
578
+ }
579
+ async function lock() {
580
+ await mkdir(root, { recursive: true });
581
+ const deadline = now() + lockTimeoutMs;
582
+ const token = randomUUID();
583
+ while (true) {
584
+ try {
585
+ const handle = await open(lockPath, "wx", 384);
586
+ await handle.writeFile(`${JSON.stringify({ pid, createdAt: now(), token })}
587
+ `);
588
+ await handle.close();
589
+ return async () => {
590
+ const current = await readJson(lockPath);
591
+ if (current?.token === token) await unlink(lockPath).catch(() => void 0);
592
+ };
593
+ } catch (error) {
594
+ if (error.code !== "EEXIST") throw error;
595
+ const lockInfo = await readJson(lockPath);
596
+ let stale = false;
597
+ if (lockInfo && typeof lockInfo.pid === "number") {
598
+ stale = !isRunning(lockInfo.pid) && now() - lockInfo.createdAt >= 0;
599
+ } else {
600
+ try {
601
+ stale = now() - (await stat(lockPath)).mtimeMs > lockStaleMs;
602
+ } catch {
603
+ continue;
604
+ }
605
+ }
606
+ if (stale) {
607
+ await rm(lockPath, { force: true }).catch(() => void 0);
608
+ continue;
609
+ }
610
+ if (now() >= deadline) throw new Error(`Timed out waiting for Localghost registry lock: ${lockPath}`);
611
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, lockRetryMs));
612
+ }
613
+ }
614
+ }
615
+ async function withLock(operation) {
616
+ const releaseLock = await lock();
617
+ try {
618
+ const registry = await readRegistry();
619
+ pruneRegistry(registry, now(), isRunning);
620
+ return await operation(registry);
621
+ } finally {
622
+ await releaseLock();
623
+ }
624
+ }
625
+ return {
626
+ root,
627
+ registryPath,
628
+ lockPath,
629
+ ownerToken,
630
+ read: readRegistry,
631
+ async acquirePort(acquireOptions) {
632
+ const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
633
+ if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
634
+ return withLock(async (registry) => {
635
+ const key = leaseKey(projectCwd, acquireOptions.instanceKey);
636
+ const existing = registry.allocations.find((entry2) => leaseKey(entry2.projectCwd, entry2.instanceKey) === key);
637
+ const reserved = new Set(acquireOptions.reservedPorts ?? []);
638
+ const activePorts = new Set(registry.leases.map((lease2) => lease2.port));
639
+ const port = existing?.port;
640
+ const ownsActiveLease = registry.leases.some((lease2) => lease2.port === port && leaseKey(lease2.projectCwd, lease2.instanceKey) === key && lease2.ownerToken === ownerToken);
641
+ const reusable = port !== void 0 && !reserved.has(port) && (!activePorts.has(port) || ownsActiveLease) && (ownsActiveLease || await availabilityCheck(port, acquireOptions.host));
642
+ let selectedPort = reusable ? port : void 0;
643
+ if (selectedPort === void 0) {
644
+ const startPort = acquireOptions.startPort ?? 3e3;
645
+ const maxAttempts = acquireOptions.maxAttempts ?? 50;
646
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
647
+ const candidate = startPort + offset;
648
+ if (reserved.has(candidate) || activePorts.has(candidate)) continue;
649
+ if (await availabilityCheck(candidate, acquireOptions.host)) {
650
+ selectedPort = candidate;
651
+ break;
652
+ }
653
+ }
654
+ if (selectedPort === void 0) throw new Error(`No available registry port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
655
+ }
656
+ const timestamp = now();
657
+ const entry = existing ?? { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, updatedAt: timestamp };
658
+ entry.port = selectedPort;
659
+ entry.updatedAt = timestamp;
660
+ if (!existing) registry.allocations.push(entry);
661
+ registry.leases = registry.leases.filter((lease2) => leaseKey(lease2.projectCwd, lease2.instanceKey) !== key);
662
+ const lease = { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, pid, acquiredAt: timestamp, expiresAt: timestamp + (acquireOptions.leaseTtlMs ?? 30 * 60 * 1e3), ownerToken };
663
+ registry.leases.push(lease);
664
+ await writeRegistry(registry);
665
+ return lease;
666
+ });
667
+ },
668
+ async releasePort(releaseOptions) {
669
+ const projectCwd = canonicalizeLocalghostProjectCwd(releaseOptions.projectCwd ?? cwd);
670
+ return withLock(async (registry) => {
671
+ const key = leaseKey(projectCwd, releaseOptions.instanceKey);
672
+ const before = registry.leases.length;
673
+ registry.leases = registry.leases.filter((lease) => leaseKey(lease.projectCwd, lease.instanceKey) !== key || lease.ownerToken !== ownerToken);
674
+ if (registry.leases.length !== before) await writeRegistry(registry);
675
+ return registry.leases.length !== before;
676
+ });
677
+ }
678
+ };
679
+ }
680
+
486
681
  // src/tunnel.ts
487
682
  import { domainToASCII } from "url";
488
683
  var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
@@ -860,7 +1055,7 @@ function envHttps() {
860
1055
  }
861
1056
  function getPackageName(cwd) {
862
1057
  try {
863
- const pkg = JSON.parse(readFileSync5(join5(cwd, "package.json"), "utf8"));
1058
+ const pkg = JSON.parse(readFileSync5(join6(cwd, "package.json"), "utf8"));
864
1059
  return typeof pkg.name === "string" ? pkg.name : void 0;
865
1060
  } catch {
866
1061
  return void 0;
@@ -943,7 +1138,24 @@ async function resolveLocalghostContext(options = {}) {
943
1138
  const autoRepair = merged.autoRepair ?? true;
944
1139
  const bindHost = merged.bindHost ?? "127.0.0.1";
945
1140
  const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
946
- const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
1141
+ let port = requestedPort;
1142
+ let releasePort;
1143
+ const reservePort = merged.reservePort ?? false;
1144
+ const instanceKey = merged.instanceKey ?? "run";
1145
+ if (reservePort && dynamicPort) {
1146
+ const registry = createLocalghostRegistry({ cwd, ...merged.registryOwnerToken ? { ownerToken: merged.registryOwnerToken } : {} });
1147
+ const lease = await registry.acquirePort({
1148
+ projectCwd: cwd,
1149
+ instanceKey,
1150
+ startPort: requestedPort,
1151
+ host: probeHost,
1152
+ ...options.reservedPorts ? { reservedPorts: options.reservedPorts } : {}
1153
+ });
1154
+ port = lease.port;
1155
+ releasePort = () => registry.releasePort({ projectCwd: cwd, instanceKey });
1156
+ } else if (dynamicPort) {
1157
+ port = await findAvailablePort(requestedPort, { host: probeHost });
1158
+ }
947
1159
  const wwwAlias = merged.wwwAlias ?? true;
948
1160
  const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
949
1161
  const hosts = uniqueHosts(entries);
@@ -972,7 +1184,8 @@ async function resolveLocalghostContext(options = {}) {
972
1184
  https: merged.https ?? envHttps() ?? false,
973
1185
  wwwAlias,
974
1186
  ghostTunnel,
975
- ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
1187
+ ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {},
1188
+ ...releasePort ? { releasePort } : {}
976
1189
  };
977
1190
  }
978
1191
 
@@ -1041,7 +1254,7 @@ function listGhostTunnelEntries(options = {}) {
1041
1254
  }
1042
1255
 
1043
1256
  // src/ghost-agent.ts
1044
- import { randomUUID as randomUUID2 } from "crypto";
1257
+ import { randomUUID as randomUUID3 } from "crypto";
1045
1258
 
1046
1259
  // src/relay.ts
1047
1260
  import { createHmac, timingSafeEqual } from "crypto";
@@ -1153,7 +1366,7 @@ function stripRelayForwardHeaders(headers) {
1153
1366
  }
1154
1367
 
1155
1368
  // src/ghost-tunnel-store.ts
1156
- import { randomUUID } from "crypto";
1369
+ import { randomUUID as randomUUID2 } from "crypto";
1157
1370
  function base64Encode(value) {
1158
1371
  return value.toString("base64");
1159
1372
  }
@@ -1191,13 +1404,18 @@ function parseJson(value) {
1191
1404
  function keyPart(value) {
1192
1405
  return value.toLowerCase().replace(/[^a-z0-9._:-]/g, "_");
1193
1406
  }
1407
+ function removeTrailingSlashes(value) {
1408
+ let end = value.length;
1409
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
1410
+ return value.slice(0, end);
1411
+ }
1194
1412
  var RedisGhostTunnelStore = class {
1195
1413
  url;
1196
1414
  token;
1197
1415
  namespace;
1198
1416
  fetchImpl;
1199
1417
  constructor(options) {
1200
- this.url = options.url.replace(/\/+$/, "");
1418
+ this.url = removeTrailingSlashes(options.url);
1201
1419
  this.token = options.token;
1202
1420
  this.namespace = options.namespace ?? "localghost";
1203
1421
  this.fetchImpl = options.fetch ?? fetch;
@@ -1285,11 +1503,11 @@ function isStopped(signal, localSignal) {
1285
1503
  }
1286
1504
  function wait(ms, signal, localSignal) {
1287
1505
  if (isStopped(signal, localSignal)) return Promise.resolve();
1288
- return new Promise((resolve3) => {
1289
- const timeout = setTimeout(resolve3, ms);
1506
+ return new Promise((resolve4) => {
1507
+ const timeout = setTimeout(resolve4, ms);
1290
1508
  const stop = () => {
1291
1509
  clearTimeout(timeout);
1292
- resolve3();
1510
+ resolve4();
1293
1511
  };
1294
1512
  signal?.addEventListener("abort", stop, { once: true });
1295
1513
  localSignal.addEventListener("abort", stop, { once: true });
@@ -1375,7 +1593,7 @@ function startGhostTunnelAgent(options) {
1375
1593
  const controller = new AbortController();
1376
1594
  const localSignal = controller.signal;
1377
1595
  const signal = options.signal;
1378
- const agentId = options.agentId ?? `localghost-${randomUUID2()}`;
1596
+ const agentId = options.agentId ?? `localghost-${randomUUID3()}`;
1379
1597
  const targetHost = options.targetHost ?? "127.0.0.1";
1380
1598
  const routeTtlSeconds = options.routeTtlSeconds ?? 30;
1381
1599
  const requestTtlSeconds = options.requestTtlSeconds ?? 60;
@@ -1430,7 +1648,7 @@ function startGhostTunnelAgent(options) {
1430
1648
  // src/hosts-file.ts
1431
1649
  import { writeFileSync as writeFileSync3 } from "fs";
1432
1650
  import { tmpdir } from "os";
1433
- import { join as join6 } from "path";
1651
+ import { join as join7 } from "path";
1434
1652
  import { execa as execa3 } from "execa";
1435
1653
  function escapeRegExp(value) {
1436
1654
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1473,7 +1691,7 @@ function removeManagedBlock(existing, projectName) {
1473
1691
  }
1474
1692
  async function writeSystemHostsFile(hostsPath, next, projectName) {
1475
1693
  const sanitizedProjectName = sanitizeProjectName(projectName);
1476
- const tempPath = join6(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
1694
+ const tempPath = join7(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
1477
1695
  writeFileSync3(tempPath, next, "utf8");
1478
1696
  if (process.env.LOCALGHOST_HOSTS_PATH) {
1479
1697
  writeFileSync3(hostsPath, next, "utf8");
@@ -1511,11 +1729,11 @@ async function removeSystemHosts(projectName) {
1511
1729
 
1512
1730
  // src/init.ts
1513
1731
  import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1514
- import { join as join7 } from "path";
1732
+ import { join as join8 } from "path";
1515
1733
  function detectPackageManager(cwd = process.cwd()) {
1516
- if (existsSync5(join7(cwd, "pnpm-lock.yaml"))) return "pnpm";
1517
- if (existsSync5(join7(cwd, "yarn.lock"))) return "yarn";
1518
- if (existsSync5(join7(cwd, "bun.lock")) || existsSync5(join7(cwd, "bun.lockb"))) return "bun";
1734
+ if (existsSync5(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
1735
+ if (existsSync5(join8(cwd, "yarn.lock"))) return "yarn";
1736
+ if (existsSync5(join8(cwd, "bun.lock")) || existsSync5(join8(cwd, "bun.lockb"))) return "bun";
1519
1737
  return "npm";
1520
1738
  }
1521
1739
  function packageRunCommand(packageManager, script) {
@@ -1589,7 +1807,7 @@ function initLocalghost(options = {}) {
1589
1807
  const apiPort = options.apiPort ?? 8787;
1590
1808
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
1591
1809
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
1592
- const configPath = join7(cwd, configFile);
1810
+ const configPath = join8(cwd, configFile);
1593
1811
  const configExists = existsSync5(configPath);
1594
1812
  if (configExists && !options.force) {
1595
1813
  return {
@@ -1606,7 +1824,7 @@ function initLocalghost(options = {}) {
1606
1824
  };
1607
1825
  }
1608
1826
  writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
1609
- const packageJsonPath = join7(cwd, "package.json");
1827
+ const packageJsonPath = join8(cwd, "package.json");
1610
1828
  const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
1611
1829
  return {
1612
1830
  configPath,
@@ -1623,6 +1841,62 @@ function initLocalghost(options = {}) {
1623
1841
  };
1624
1842
  }
1625
1843
 
1844
+ // src/guide.ts
1845
+ var LOCALGHOST_AGENT_GUIDE = `# Localghost agent guide
1846
+
1847
+ Localghost owns the local development proxy and the app process boundary.
1848
+
1849
+ ## Preferred repository setup
1850
+
1851
+ For a normal repository, use this package script:
1852
+
1853
+ "dev": "localghost"
1854
+
1855
+ For an explicit app command, keep the raw command separate:
1856
+
1857
+ "dev": "localghost run -- vite"
1858
+ "dev:raw": "vite"
1859
+
1860
+ Use \`localghost dev\` only when the Caddy proxy should run without starting the app.
1861
+
1862
+ ## Useful commands
1863
+
1864
+ - \`localghost\`: detect and run the repository development command.
1865
+ - \`localghost run -- <command>\`: wrap an explicit app command.
1866
+ - \`localghost dev\`: run only the local Caddy proxy.
1867
+ - \`localghost status --ready\`: check project setup.
1868
+ - \`localghost repair\`: repair managed hosts and Caddy setup.
1869
+ - \`localghost ps --json\`: inspect Localghost-managed repositories, instances, and ports.
1870
+ - \`localghost routes\`: inspect hostname-to-port routing.
1871
+ - \`localghost doctor\`: check machine prerequisites.
1872
+
1873
+ ## Configuration
1874
+
1875
+ - Commit repository defaults in \`localghost.config.mjs\`.
1876
+ - Keep hostname and requested-port routes in \`.localghost\`.
1877
+ - CLI flags override repository configuration for one invocation.
1878
+ - Localghost remembers active project and instance port assignments in user state under \`~/.localghost\`.
1879
+ - Do not edit the registry manually and do not start Caddy separately.
1880
+
1881
+ ## Port behavior
1882
+
1883
+ Localghost remembers ports by canonical repository path and instance key. Concurrent Localghost instances receive distinct ports. The operating-system bind check remains authoritative when another tool already owns a port.
1884
+ `;
1885
+ function formatLocalghostAgentGuide(format = "text") {
1886
+ if (format === "json") {
1887
+ return JSON.stringify({
1888
+ preferredScript: "localghost",
1889
+ explicitScript: "localghost run -- <command>",
1890
+ proxyOnlyCommand: "localghost dev",
1891
+ inspectionCommands: ["localghost status --ready", "localghost ps --json", "localghost routes", "localghost doctor"],
1892
+ projectConfig: "localghost.config.mjs",
1893
+ routeConfig: ".localghost",
1894
+ userState: "~/.localghost"
1895
+ }, null, 2);
1896
+ }
1897
+ return LOCALGHOST_AGENT_GUIDE;
1898
+ }
1899
+
1626
1900
  // src/prompt.ts
1627
1901
  import { stdin as input, stdout as output } from "process";
1628
1902
  import { createInterface } from "readline/promises";
@@ -1711,10 +1985,10 @@ function formatGhostTunnel(config, options = {}) {
1711
1985
 
1712
1986
  // src/state.ts
1713
1987
  import { existsSync as existsSync6 } from "fs";
1714
- import { join as join8 } from "path";
1988
+ import { join as join9 } from "path";
1715
1989
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
1716
1990
  function getLocalghostStatePath(cwd = process.cwd()) {
1717
- return join8(cwd, LOCALGHOST_STATE_FILE);
1991
+ return join9(cwd, LOCALGHOST_STATE_FILE);
1718
1992
  }
1719
1993
  function readLocalghostState(cwd = process.cwd()) {
1720
1994
  const path = getLocalghostStatePath(cwd);
@@ -1735,10 +2009,10 @@ function patchLocalghostState(cwd, patch) {
1735
2009
 
1736
2010
  // src/update-check.ts
1737
2011
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
1738
- import { homedir as homedir2 } from "os";
1739
- import { dirname as dirname4, join as join9 } from "path";
2012
+ import { homedir as homedir3 } from "os";
2013
+ import { dirname as dirname4, join as join10 } from "path";
1740
2014
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
1741
- var LOCALGHOST_VERSION = "0.1.13";
2015
+ var LOCALGHOST_VERSION = "0.1.15";
1742
2016
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
1743
2017
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
1744
2018
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -1750,8 +2024,8 @@ function isUpdateCheckDisabled(env = process.env) {
1750
2024
  }
1751
2025
  function getUpdateCheckCachePath(env = process.env) {
1752
2026
  if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
1753
- const cacheRoot = env.XDG_CACHE_HOME || join9(homedir2(), ".cache");
1754
- return join9(cacheRoot, "localghost", "update-check.json");
2027
+ const cacheRoot = env.XDG_CACHE_HOME || join10(homedir3(), ".cache");
2028
+ return join10(cacheRoot, "localghost", "update-check.json");
1755
2029
  }
1756
2030
  function readCache(path = getUpdateCheckCachePath()) {
1757
2031
  if (!existsSync7(path)) return null;
@@ -1803,7 +2077,7 @@ function isNewerVersion(candidate, current = LOCALGHOST_VERSION) {
1803
2077
  return Boolean(candidate && compareVersions(candidate, current) > 0);
1804
2078
  }
1805
2079
  async function fetchLatestVersion(packageName, timeoutMs) {
1806
- const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replace("/", "%2f")}` : packageName;
2080
+ const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replaceAll("/", "%2f")}` : packageName;
1807
2081
  const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
1808
2082
  signal: AbortSignal.timeout(timeoutMs),
1809
2083
  headers: {
@@ -1938,6 +2212,10 @@ function parsePackageManager(value) {
1938
2212
  if (value === "npm" || value === "yarn" || value === "pnpm" || value === "bun") return value;
1939
2213
  throw new InvalidArgumentError("Package manager must be npm, pnpm, yarn, or bun.");
1940
2214
  }
2215
+ function parseReleaseBump(value) {
2216
+ if (value === "patch" || value === "minor" || value === "major") return value;
2217
+ throw new InvalidArgumentError("Release bump must be patch, minor, or major.");
2218
+ }
1941
2219
  function collect(value, previous = []) {
1942
2220
  return [...previous, value];
1943
2221
  }
@@ -1975,6 +2253,25 @@ async function assertCaddyReady() {
1975
2253
  "Localghost will not install it for you. No surprise spells."
1976
2254
  ].join("\n"));
1977
2255
  }
2256
+ function cleanManagedCaddyProcesses() {
2257
+ const runs = listLocalghostRuns();
2258
+ const caddyPids = runs.flatMap((run) => run.caddyPid ? [run.caddyPid] : []);
2259
+ const result = stopCaddyProcesses(caddyPids);
2260
+ for (const run of runs) {
2261
+ if (run.caddyPid && (result.stopped.includes(run.caddyPid) || result.alreadyExited.includes(run.caddyPid))) {
2262
+ unregisterLocalghostRun(run.id);
2263
+ }
2264
+ }
2265
+ if (result.stopped.length > 0) {
2266
+ console.log(`Stopped ${result.stopped.length} Localghost-managed Caddy process${result.stopped.length === 1 ? "" : "es"}.`);
2267
+ }
2268
+ if (result.alreadyExited.length > 0) {
2269
+ console.log(`Removed ${result.alreadyExited.length} stale Localghost Caddy record${result.alreadyExited.length === 1 ? "" : "s"}.`);
2270
+ }
2271
+ if (result.failed.length > 0) {
2272
+ throw new Error(`Could not stop Localghost-managed Caddy PID(s): ${result.failed.map(({ pid }) => pid).join(", ")}.`);
2273
+ }
2274
+ }
1978
2275
  function existingTrustMarkers(cwd) {
1979
2276
  const state = readLocalghostState(cwd);
1980
2277
  return {
@@ -2079,7 +2376,7 @@ async function runSetupFromReadiness(cwd, https, readiness) {
2079
2376
  });
2080
2377
  }
2081
2378
  function wait2(ms) {
2082
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2379
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
2083
2380
  }
2084
2381
  async function runTrust(cwd, caddyfilePath) {
2085
2382
  await wait2(350);
@@ -2126,25 +2423,19 @@ function registerCleanup(id) {
2126
2423
  process.off("exit", cleanup);
2127
2424
  };
2128
2425
  }
2129
- async function resolveServiceRuntimeEntries(services, dynamicPort) {
2426
+ async function resolveServiceRuntimeEntries(services, dynamicPort, projectCwd) {
2130
2427
  const usedPorts = /* @__PURE__ */ new Set();
2131
2428
  const resolved = [];
2429
+ const registry = dynamicPort ? createLocalghostRegistry({ cwd: projectCwd, ownerToken: `${process.pid}:services:${projectCwd}` }) : void 0;
2132
2430
  for (const service of services) {
2133
2431
  let port = service.requestedPort;
2134
2432
  if (dynamicPort) {
2135
- let found = false;
2136
- for (let offset = 0; offset < 50; offset += 1) {
2137
- const candidate = service.requestedPort + offset;
2138
- if (candidate > 65535 || usedPorts.has(candidate)) continue;
2139
- if (await isPortAvailable(candidate)) {
2140
- port = candidate;
2141
- found = true;
2142
- break;
2143
- }
2144
- }
2145
- if (!found) {
2146
- throw new Error(`No available port found for service ${service.name} from ${service.requestedPort}.`);
2147
- }
2433
+ const lease = await registry.acquirePort({
2434
+ instanceKey: `service:${service.name}`,
2435
+ startPort: service.requestedPort,
2436
+ reservedPorts: usedPorts
2437
+ });
2438
+ port = lease.port;
2148
2439
  } else if (usedPorts.has(port)) {
2149
2440
  throw new Error(`Services cannot start separate commands on the same fixed port: ${port}.`);
2150
2441
  }
@@ -2159,7 +2450,13 @@ async function resolveServiceRuntimeEntries(services, dynamicPort) {
2159
2450
  }
2160
2451
  });
2161
2452
  }
2162
- return resolved;
2453
+ return {
2454
+ services: resolved,
2455
+ release: async () => {
2456
+ if (!registry) return;
2457
+ await Promise.all(resolved.map((service) => registry.releasePort({ instanceKey: `service:${service.name}` })));
2458
+ }
2459
+ };
2163
2460
  }
2164
2461
  async function waitForServicePorts(entries, timeoutMs = 1e4) {
2165
2462
  const deadline = Date.now() + timeoutMs;
@@ -2174,81 +2471,86 @@ async function waitForServicePorts(entries, timeoutMs = 1e4) {
2174
2471
  async function runDetectedServices(options) {
2175
2472
  assertLocalDevelopment("run");
2176
2473
  await assertCaddyReady();
2177
- const runtimeServices = await resolveServiceRuntimeEntries(options.services, options.dynamicPort);
2178
- const entries = runtimeServices.map((service) => service.entry);
2179
- const readiness = getSetupReadiness({
2180
- cwd: options.cwd,
2181
- https: options.https,
2182
- ignoreCaddyfile: true,
2183
- entries,
2184
- configPath: options.configPath,
2185
- projectName: options.projectName
2186
- });
2187
- if (!readiness.ready) {
2188
- if (!options.autoRepair) {
2189
- throw new Error([
2190
- "Localghost setup is missing or stale.",
2191
- ...readiness.reasons.map((reason) => `- ${reason}`),
2192
- "Automatic repair is disabled. Enable autoRepair or run localghost repair."
2193
- ].join("\n"));
2194
- }
2195
- console.log("Localghost setup is stale; repairing it now.");
2196
- await runSetupFromReadiness(options.cwd, options.https, readiness);
2197
- }
2198
- for (const service of runtimeServices) {
2199
- if (service.port !== service.requestedPort) {
2200
- console.log(`${service.name}: port ${service.requestedPort} is busy; using ${service.port}.`);
2474
+ const runtime = await resolveServiceRuntimeEntries(options.services, options.dynamicPort, options.cwd);
2475
+ try {
2476
+ const runtimeServices = runtime.services;
2477
+ const entries = runtimeServices.map((service) => service.entry);
2478
+ const readiness = getSetupReadiness({
2479
+ cwd: options.cwd,
2480
+ https: options.https,
2481
+ ignoreCaddyfile: true,
2482
+ entries,
2483
+ configPath: options.configPath,
2484
+ projectName: options.projectName
2485
+ });
2486
+ if (!readiness.ready) {
2487
+ if (!options.autoRepair) {
2488
+ throw new Error([
2489
+ "Localghost setup is missing or stale.",
2490
+ ...readiness.reasons.map((reason) => `- ${reason}`),
2491
+ "Automatic repair is disabled. Enable autoRepair or run localghost repair."
2492
+ ].join("\n"));
2493
+ }
2494
+ console.log("Localghost setup is stale; repairing it now.");
2495
+ await runSetupFromReadiness(options.cwd, options.https, readiness);
2201
2496
  }
2202
- }
2203
- const caddyfile = await writeCaddyfile(entries, options.cwd, { https: options.https });
2204
- await validateCaddyfile(caddyfile);
2205
- const caddy = startCaddy(caddyfile);
2206
- const caddyExit = caddy.catch((error) => {
2207
- if (!caddy.killed) throw error;
2208
- });
2209
- const children = runtimeServices.map((service) => execa4(service.command[0], service.command.slice(1), {
2210
- cwd: service.cwd,
2211
- stdio: "inherit",
2212
- env: {
2213
- ...process.env,
2214
- LOCALGHOST_PORT: String(service.port),
2215
- LOCALGHOST_DYNAMIC_PORT: options.dynamicPort ? "1" : "0",
2216
- LOCALGHOST_SERVICE: service.name,
2217
- VITE_PORT: String(service.port)
2497
+ for (const service of runtimeServices) {
2498
+ if (service.port !== service.requestedPort) {
2499
+ console.log(`${service.name}: port ${service.requestedPort} is busy; using ${service.port}.`);
2500
+ }
2218
2501
  }
2219
- }));
2220
- const caddyPid = maybePid(caddy.pid);
2221
- const runRecord = registerLocalghostRun({
2222
- mode: "run",
2223
- cwd: options.cwd,
2224
- projectName: options.projectName,
2225
- configPath: options.configPath,
2226
- caddyfilePath: caddyfile,
2227
- ...caddyPid ? { caddyPid } : {},
2228
- childCommand: ["services", ...runtimeServices.map((service) => service.name)],
2229
- https: options.https,
2230
- dynamicPort: options.dynamicPort,
2231
- entries
2232
- });
2233
- const cleanupRun = registerCleanup(runRecord.id);
2234
- const processExit = Promise.race([caddyExit, ...children]);
2235
- try {
2236
- const ready = await Promise.race([
2237
- waitForServicePorts(entries),
2238
- processExit.then(() => false)
2239
- ]);
2240
- if (ready) {
2241
- console.log("");
2242
- logDomainRoutes(entries, { https: options.https });
2502
+ const caddyfile = await writeCaddyfile(entries, options.cwd, { https: options.https });
2503
+ await validateCaddyfile(caddyfile);
2504
+ const caddy = startCaddy(caddyfile);
2505
+ const caddyExit = caddy.catch((error) => {
2506
+ if (!caddy.killed) throw error;
2507
+ });
2508
+ const children = runtimeServices.map((service) => execa4(service.command[0], service.command.slice(1), {
2509
+ cwd: service.cwd,
2510
+ stdio: "inherit",
2511
+ env: {
2512
+ ...process.env,
2513
+ LOCALGHOST_PORT: String(service.port),
2514
+ LOCALGHOST_DYNAMIC_PORT: options.dynamicPort ? "1" : "0",
2515
+ LOCALGHOST_SERVICE: service.name,
2516
+ VITE_PORT: String(service.port)
2517
+ }
2518
+ }));
2519
+ const caddyPid = maybePid(caddy.pid);
2520
+ const runRecord = registerLocalghostRun({
2521
+ mode: "run",
2522
+ cwd: options.cwd,
2523
+ projectName: options.projectName,
2524
+ configPath: options.configPath,
2525
+ caddyfilePath: caddyfile,
2526
+ ...caddyPid ? { caddyPid } : {},
2527
+ childCommand: ["services", ...runtimeServices.map((service) => service.name)],
2528
+ https: options.https,
2529
+ dynamicPort: options.dynamicPort,
2530
+ entries
2531
+ });
2532
+ const cleanupRun = registerCleanup(runRecord.id);
2533
+ const processExit = Promise.race([caddyExit, ...children]);
2534
+ try {
2535
+ const ready = await Promise.race([
2536
+ waitForServicePorts(entries),
2537
+ processExit.then(() => false)
2538
+ ]);
2539
+ if (ready) {
2540
+ console.log("");
2541
+ logDomainRoutes(entries, { https: options.https });
2542
+ }
2543
+ await processExit;
2544
+ } finally {
2545
+ for (const child of children) {
2546
+ if (!child.killed) child.kill("SIGINT");
2547
+ }
2548
+ if (!caddy.killed) caddy.kill("SIGINT");
2549
+ await Promise.allSettled([caddyExit, ...children]);
2550
+ cleanupRun();
2243
2551
  }
2244
- await processExit;
2245
2552
  } finally {
2246
- for (const child of children) {
2247
- if (!child.killed) child.kill("SIGINT");
2248
- }
2249
- if (!caddy.killed) caddy.kill("SIGINT");
2250
- await Promise.allSettled([caddyExit, ...children]);
2251
- cleanupRun();
2553
+ await runtime.release();
2252
2554
  }
2253
2555
  }
2254
2556
  async function getRouteViews(entries) {
@@ -2344,7 +2646,7 @@ function formatInstanceViews(instances) {
2344
2646
  var program = new Command();
2345
2647
  program.name("localghost").description("Buh. Friendly local hostnames for app repos.").version(LOCALGHOST_VERSION).option("--no-update-check", "Skip the npm update check for this run");
2346
2648
  program.hook("postAction", async (_thisCommand, actionCommand) => {
2347
- if (actionCommand.name() === "update") return;
2649
+ if (actionCommand.name() === "update" || actionCommand.name() === "release") return;
2348
2650
  const options = program.opts();
2349
2651
  await maybeNotifyAboutUpdate({ disabled: options.updateCheck === false });
2350
2652
  });
@@ -2369,6 +2671,9 @@ program.command("init").description("Create a .localghost config for this projec
2369
2671
  console.log(` ${step}`);
2370
2672
  }
2371
2673
  });
2674
+ program.command("guide").description("Explain the recommended Localghost workflow to humans or agents").option("--agent", "Print the agent-oriented workflow guide").option("--json", "Print the guide as JSON").action((options) => {
2675
+ console.log(formatLocalghostAgentGuide(options.json ? "json" : "text"));
2676
+ });
2372
2677
  program.command("doctor").description("Check machine prerequisites").action(async () => {
2373
2678
  const result = await runDoctor();
2374
2679
  if (result.caddy.found) {
@@ -2400,6 +2705,30 @@ program.command("update").description("Check npm for a newer localghost release"
2400
2705
  }
2401
2706
  console.log(`localghost is up to date. Current: ${result.currentVersion}`);
2402
2707
  });
2708
+ program.command("release").description("Dispatch an automated Localghost CLI release").argument("<bump>", "Semantic version increment: patch, minor, or major", parseReleaseBump).action(async (bump) => {
2709
+ const repository = "hamedb89/localghost";
2710
+ try {
2711
+ await execa4("gh", [
2712
+ "workflow",
2713
+ "run",
2714
+ "release.yml",
2715
+ "--repo",
2716
+ repository,
2717
+ "--ref",
2718
+ "main",
2719
+ "-f",
2720
+ `bump=${bump}`
2721
+ ]);
2722
+ } catch (error) {
2723
+ const detail = error instanceof Error ? error.message : String(error);
2724
+ throw new Error(
2725
+ `Could not dispatch the Localghost release workflow. Install and authenticate GitHub CLI with \`gh auth login\`, then retry.
2726
+ ${detail}`
2727
+ );
2728
+ }
2729
+ console.log(`Dispatched a ${bump} Localghost release from main.`);
2730
+ console.log(`Track it at https://github.com/${repository}/actions/workflows/release.yml`);
2731
+ });
2403
2732
  program.command("setup").description("Update /etc/hosts and generate/validate Caddyfile").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--https", "Generate a local HTTPS Caddy proxy with Caddy local certificates").option("--ssl", "Alias for --https").action(async (options) => {
2404
2733
  assertLocalDevelopment("setup");
2405
2734
  printLocalghostBanner();
@@ -2600,9 +2929,10 @@ program.command("routes").description("Print domain to upstream routes").option(
2600
2929
  }));
2601
2930
  }
2602
2931
  });
2603
- program.command("dev").description("Run the Localghost Caddy proxy, repairing stale setup when needed").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Alias for automatic repair when setup is missing or stale").option("--auto-repair [yes|no]", "Repair stale setup before starting (default: yes)", parseBooleanLike).option("--trust", "Trust Caddy's local HTTPS CA before starting the proxy").action(async (options) => {
2932
+ program.command("dev").description("Run the Localghost Caddy proxy, repairing stale setup when needed").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Alias for automatic repair when setup is missing or stale").option("--auto-repair [yes|no]", "Repair stale setup before starting (default: yes)", parseBooleanLike).option("--clean-caddy", "Stop Localghost-managed Caddy processes before starting").option("--trust", "Trust Caddy's local HTTPS CA before starting the proxy").action(async (options) => {
2604
2933
  assertLocalDevelopment("dev");
2605
2934
  await assertCaddyReady();
2935
+ if (options.cleanCaddy) cleanManagedCaddyProcesses();
2606
2936
  const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
2607
2937
  const https = context.https;
2608
2938
  const readiness = getSetupReadiness({
@@ -2660,15 +2990,18 @@ program.command("dev").description("Run the Localghost Caddy proxy, repairing st
2660
2990
  cleanupRun();
2661
2991
  }
2662
2992
  });
2663
- program.command("run").description("Run Caddy and a dev command from the same Localghost context").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--port <number>", "Initial app port", parsePort2).option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Alias for automatic repair when setup is missing or stale").option("--auto-repair [yes|no]", "Repair stale setup before starting (default: yes)", parseBooleanLike).option("--trust", "Trust Caddy's local HTTPS CA before starting the child command").option("--dynamic-port [yes|no]", "Use the requested port if free, otherwise continue upward", parseBooleanLike).argument("<command...>", "Command to run after --, for example: localghost run -- vite").action(async (command, options) => {
2993
+ program.command("run").description("Run Caddy and a dev command from the same Localghost context").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--port <number>", "Initial app port", parsePort2).option("--https", "Run a local HTTPS proxy with Caddy local certificates").option("--ssl", "Alias for --https").option("--setup", "Alias for automatic repair when setup is missing or stale").option("--auto-repair [yes|no]", "Repair stale setup before starting (default: yes)", parseBooleanLike).option("--clean-caddy", "Stop Localghost-managed Caddy processes before starting").option("--trust", "Trust Caddy's local HTTPS CA before starting the child command").option("--dynamic-port [yes|no]", "Use the requested port if free, otherwise continue upward", parseBooleanLike).argument("<command...>", "Command to run after --, for example: localghost run -- vite").action(async (command, options) => {
2664
2994
  assertLocalDevelopment("run");
2665
2995
  await assertCaddyReady();
2996
+ if (options.cleanCaddy) cleanManagedCaddyProcesses();
2666
2997
  const context = await resolveLocalghostContext({
2667
2998
  cwd: options.cwd,
2668
2999
  ...options.project ? { project: options.project } : {},
2669
3000
  ...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
2670
3001
  ...options.configPattern ? { configPattern: options.configPattern } : {},
2671
3002
  ...options.port ? { port: options.port } : {},
3003
+ reservePort: true,
3004
+ instanceKey: "run",
2672
3005
  ...useHttps(options) ? { https: true } : {},
2673
3006
  ...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {},
2674
3007
  ...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
@@ -2764,9 +3097,10 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
2764
3097
  stopCaddy();
2765
3098
  await Promise.allSettled([child, caddyExit]);
2766
3099
  cleanupRun();
3100
+ await context.releasePort?.();
2767
3101
  }
2768
3102
  });
2769
- program.command("tunnel").description("Run the local Ghost Tunnel agent").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--ghost-config <file>", "Exact Ghost Tunnel route file", ".ghosttunnel").option("--target-host <host>", "Local target host for .ghosttunnel ports", "127.0.0.1").action(async (options) => {
3103
+ program.command("tunnel").description("Run the experimental local Ghost Tunnel agent").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to look for. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--ghost-config <file>", "Exact Ghost Tunnel route file", ".ghosttunnel").option("--target-host <host>", "Local target host for .ghosttunnel ports", "127.0.0.1").action(async (options) => {
2770
3104
  assertLocalDevelopment("tunnel");
2771
3105
  const context = await resolveLocalghostContext({
2772
3106
  cwd: options.cwd,
@@ -2833,6 +3167,7 @@ function readImplicitInvocation(args) {
2833
3167
  let cwd = process.cwd();
2834
3168
  let dryRun = false;
2835
3169
  let updateCheck = true;
3170
+ const forwardedArgs = [];
2836
3171
  for (let index = 0; index < args.length; index += 1) {
2837
3172
  const arg = args[index];
2838
3173
  if (!arg) continue;
@@ -2844,6 +3179,30 @@ function readImplicitInvocation(args) {
2844
3179
  updateCheck = false;
2845
3180
  continue;
2846
3181
  }
3182
+ if (["--clean-caddy", "--https", "--ssl", "--setup", "--trust"].includes(arg)) {
3183
+ forwardedArgs.push(arg);
3184
+ continue;
3185
+ }
3186
+ if (arg === "--auto-repair" || arg === "--dynamic-port") {
3187
+ const value = args[index + 1];
3188
+ forwardedArgs.push(arg);
3189
+ if (value && !value.startsWith("--")) {
3190
+ forwardedArgs.push(value);
3191
+ index += 1;
3192
+ }
3193
+ continue;
3194
+ }
3195
+ if (arg === "--port" || arg === "--project" || arg === "--config" || arg === "--config-pattern") {
3196
+ const value = args[index + 1];
3197
+ if (!value) throw new Error(`${arg} requires a value.`);
3198
+ forwardedArgs.push(arg, value);
3199
+ index += 1;
3200
+ continue;
3201
+ }
3202
+ if (["--auto-repair=", "--dynamic-port=", "--port=", "--project=", "--config=", "--config-pattern="].some((prefix) => arg.startsWith(prefix))) {
3203
+ forwardedArgs.push(arg);
3204
+ continue;
3205
+ }
2847
3206
  if (arg === "--cwd") {
2848
3207
  const value = args[index + 1];
2849
3208
  if (!value) throw new Error("--cwd requires a path.");
@@ -2857,7 +3216,15 @@ function readImplicitInvocation(args) {
2857
3216
  }
2858
3217
  return null;
2859
3218
  }
2860
- return { cwd, dryRun, updateCheck };
3219
+ return { cwd, dryRun, updateCheck, forwardedArgs };
3220
+ }
3221
+ function hasForwardedFlag(args, ...flags) {
3222
+ return args.some((arg) => flags.includes(arg));
3223
+ }
3224
+ function forwardedBoolean(args, name) {
3225
+ const inline = args.find((arg) => arg.startsWith(`${name}=`));
3226
+ if (inline) return parseBooleanLike(inline.slice(name.length + 1));
3227
+ return args.includes(name) ? true : void 0;
2861
3228
  }
2862
3229
  async function main() {
2863
3230
  const implicit = readImplicitInvocation(process.argv.slice(2));
@@ -2875,14 +3242,15 @@ async function main() {
2875
3242
  });
2876
3243
  console.log(formatDetectedDevServices(services));
2877
3244
  if (implicit.dryRun) return;
3245
+ if (hasForwardedFlag(implicit.forwardedArgs, "--clean-caddy")) cleanManagedCaddyProcesses();
2878
3246
  await runDetectedServices({
2879
3247
  cwd: implicit.cwd,
2880
3248
  services,
2881
3249
  configPath: projectConfig.path,
2882
3250
  projectName: sanitizeProjectName(projectConfig.config.project ?? getProjectName(implicit.cwd)),
2883
- https: projectConfig.config.https ?? false,
2884
- dynamicPort: projectConfig.config.dynamicPort ?? true,
2885
- autoRepair: projectConfig.config.autoRepair ?? true
3251
+ https: hasForwardedFlag(implicit.forwardedArgs, "--https", "--ssl") || (projectConfig.config.https ?? false),
3252
+ dynamicPort: forwardedBoolean(implicit.forwardedArgs, "--dynamic-port") ?? projectConfig.config.dynamicPort ?? true,
3253
+ autoRepair: forwardedBoolean(implicit.forwardedArgs, "--auto-repair") ?? projectConfig.config.autoRepair ?? true
2886
3254
  });
2887
3255
  await maybeNotifyAboutUpdate({ disabled: !implicit.updateCheck });
2888
3256
  return;
@@ -2898,6 +3266,7 @@ async function main() {
2898
3266
  process.argv[1] ?? "localghost",
2899
3267
  ...implicit.updateCheck ? [] : ["--no-update-check"],
2900
3268
  "run",
3269
+ ...implicit.forwardedArgs,
2901
3270
  "--cwd",
2902
3271
  implicit.cwd,
2903
3272
  "--",