@hamedb89/localghost 0.1.13 → 0.2.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/README.md +30 -4
- package/dist/cli.js +657 -146
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +117 -3
- package/dist/index.js +399 -40
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +231 -19
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +3 -1
- package/docs/ghost-tunnel.md +4 -1
- package/docs/localghost.1.md +35 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -86,6 +86,7 @@ function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
|
|
|
86
86
|
...input2.configPath ? { configPath: input2.configPath } : {},
|
|
87
87
|
...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
|
|
88
88
|
...input2.caddyPid ? { caddyPid: input2.caddyPid } : {},
|
|
89
|
+
...input2.caddyPgid ? { caddyPgid: input2.caddyPgid } : {},
|
|
89
90
|
...input2.childPid ? { childPid: input2.childPid } : {},
|
|
90
91
|
...input2.childCommand ? { childCommand: input2.childCommand } : {},
|
|
91
92
|
...typeof input2.https === "boolean" ? { https: input2.https } : {},
|
|
@@ -250,7 +251,12 @@ function getProjectName(cwd = process.cwd()) {
|
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
function sanitizeProjectName(value) {
|
|
253
|
-
const
|
|
254
|
+
const sanitized = value.replace(/[^\w.-]+/g, "-");
|
|
255
|
+
let start = 0;
|
|
256
|
+
let end = sanitized.length;
|
|
257
|
+
while (start < end && sanitized.charCodeAt(start) === 45) start += 1;
|
|
258
|
+
while (end > start && sanitized.charCodeAt(end - 1) === 45) end -= 1;
|
|
259
|
+
const projectName = sanitized.slice(start, end);
|
|
254
260
|
return projectName || "app";
|
|
255
261
|
}
|
|
256
262
|
|
|
@@ -331,9 +337,30 @@ async function validateCaddyfile(path) {
|
|
|
331
337
|
function startCaddy(path) {
|
|
332
338
|
return execa("caddy", ["run", "--config", path], {
|
|
333
339
|
cwd: dirname3(path),
|
|
334
|
-
stdio: caddyStdio()
|
|
340
|
+
stdio: caddyStdio(),
|
|
341
|
+
detached: process.platform !== "win32"
|
|
335
342
|
});
|
|
336
343
|
}
|
|
344
|
+
function stopCaddyProcesses(pids, killProcess = (pid, signal) => process.kill(pid, signal)) {
|
|
345
|
+
const result = {
|
|
346
|
+
stopped: [],
|
|
347
|
+
alreadyExited: [],
|
|
348
|
+
failed: []
|
|
349
|
+
};
|
|
350
|
+
for (const pid of new Set(pids)) {
|
|
351
|
+
try {
|
|
352
|
+
killProcess(pid, "SIGINT");
|
|
353
|
+
result.stopped.push(pid);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
356
|
+
result.alreadyExited.push(pid);
|
|
357
|
+
} else {
|
|
358
|
+
result.failed.push({ pid, error });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
337
364
|
async function trustCaddy(path) {
|
|
338
365
|
await execa("caddy", ["trust", "--config", path], {
|
|
339
366
|
cwd: dirname3(path),
|
|
@@ -454,19 +481,19 @@ function formatDetectedDevServices(services) {
|
|
|
454
481
|
|
|
455
482
|
// src/context.ts
|
|
456
483
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
457
|
-
import { join as
|
|
484
|
+
import { join as join6 } from "path";
|
|
458
485
|
import { pathToFileURL } from "url";
|
|
459
486
|
|
|
460
487
|
// src/port.ts
|
|
461
488
|
import { createServer } from "net";
|
|
462
489
|
async function isPortAvailable(port, host = "127.0.0.1") {
|
|
463
|
-
return new Promise((
|
|
490
|
+
return new Promise((resolve4) => {
|
|
464
491
|
const server = createServer();
|
|
465
492
|
server.once("error", () => {
|
|
466
|
-
|
|
493
|
+
resolve4(false);
|
|
467
494
|
});
|
|
468
495
|
server.once("listening", () => {
|
|
469
|
-
server.close(() =>
|
|
496
|
+
server.close(() => resolve4(true));
|
|
470
497
|
});
|
|
471
498
|
server.listen(port, host);
|
|
472
499
|
});
|
|
@@ -483,6 +510,188 @@ async function findAvailablePort(startPort, options = {}) {
|
|
|
483
510
|
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
484
511
|
}
|
|
485
512
|
|
|
513
|
+
// src/registry.ts
|
|
514
|
+
import { randomUUID } from "crypto";
|
|
515
|
+
import { mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "fs/promises";
|
|
516
|
+
import { homedir as homedir2 } from "os";
|
|
517
|
+
import { join as join5, normalize, resolve as resolve3 } from "path";
|
|
518
|
+
var LOCALGHOST_REGISTRY_FILE = "registry.json";
|
|
519
|
+
var LOCALGHOST_REGISTRY_LOCK_FILE = "registry.lock";
|
|
520
|
+
function defaultProcessRunning(pid) {
|
|
521
|
+
if (pid <= 0) return false;
|
|
522
|
+
try {
|
|
523
|
+
process.kill(pid, 0);
|
|
524
|
+
return true;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
return error.code === "EPERM";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function getLocalghostRegistryRoot(env = process.env) {
|
|
530
|
+
return resolve3(env.LOCALGHOST_HOME || join5(homedir2(), ".localghost"));
|
|
531
|
+
}
|
|
532
|
+
function canonicalizeLocalghostProjectCwd(cwd = process.cwd()) {
|
|
533
|
+
return normalize(resolve3(cwd));
|
|
534
|
+
}
|
|
535
|
+
function emptyRegistry() {
|
|
536
|
+
return { version: 1, allocations: [], leases: [] };
|
|
537
|
+
}
|
|
538
|
+
function leaseKey(projectCwd, instanceKey) {
|
|
539
|
+
return `${projectCwd}\0${instanceKey}`;
|
|
540
|
+
}
|
|
541
|
+
function validRegistry(value) {
|
|
542
|
+
if (!value || typeof value !== "object") return false;
|
|
543
|
+
const candidate = value;
|
|
544
|
+
return candidate.version === 1 && Array.isArray(candidate.allocations) && Array.isArray(candidate.leases);
|
|
545
|
+
}
|
|
546
|
+
function pruneRegistry(registry, now, isRunning) {
|
|
547
|
+
registry.leases = registry.leases.filter((lease) => lease.expiresAt > now && isRunning(lease.pid));
|
|
548
|
+
}
|
|
549
|
+
async function readJson(path) {
|
|
550
|
+
try {
|
|
551
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (error.code === "ENOENT") return void 0;
|
|
554
|
+
return void 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function createLocalghostRegistry(options = {}) {
|
|
558
|
+
const root = resolve3(options.stateRoot ?? getLocalghostRegistryRoot());
|
|
559
|
+
const registryPath = join5(root, LOCALGHOST_REGISTRY_FILE);
|
|
560
|
+
const lockPath = join5(root, LOCALGHOST_REGISTRY_LOCK_FILE);
|
|
561
|
+
const cwd = canonicalizeLocalghostProjectCwd(options.cwd);
|
|
562
|
+
const now = options.now ?? Date.now;
|
|
563
|
+
const pid = options.pid ?? process.pid;
|
|
564
|
+
const ownerToken = options.ownerToken ?? randomUUID();
|
|
565
|
+
const isRunning = options.isProcessRunning ?? defaultProcessRunning;
|
|
566
|
+
const availabilityCheck = options.availabilityCheck ?? isPortAvailable;
|
|
567
|
+
const lockTimeoutMs = options.lockTimeoutMs ?? 5e3;
|
|
568
|
+
const lockRetryMs = options.lockRetryMs ?? 25;
|
|
569
|
+
const lockStaleMs = options.lockStaleMs ?? 3e4;
|
|
570
|
+
async function readRegistry() {
|
|
571
|
+
const value = await readJson(registryPath);
|
|
572
|
+
return validRegistry(value) ? value : emptyRegistry();
|
|
573
|
+
}
|
|
574
|
+
async function writeRegistry(registry) {
|
|
575
|
+
await mkdir(root, { recursive: true });
|
|
576
|
+
const temporaryPath = join5(root, `.registry.${process.pid}.${randomUUID()}.tmp`);
|
|
577
|
+
await writeFile(temporaryPath, `${JSON.stringify(registry, null, 2)}
|
|
578
|
+
`, { mode: 384 });
|
|
579
|
+
await rename(temporaryPath, registryPath);
|
|
580
|
+
}
|
|
581
|
+
async function lock() {
|
|
582
|
+
await mkdir(root, { recursive: true });
|
|
583
|
+
const deadline = now() + lockTimeoutMs;
|
|
584
|
+
const token = randomUUID();
|
|
585
|
+
while (true) {
|
|
586
|
+
try {
|
|
587
|
+
const handle = await open(lockPath, "wx", 384);
|
|
588
|
+
await handle.writeFile(`${JSON.stringify({ pid, createdAt: now(), token })}
|
|
589
|
+
`);
|
|
590
|
+
await handle.close();
|
|
591
|
+
return async () => {
|
|
592
|
+
const current = await readJson(lockPath);
|
|
593
|
+
if (current?.token === token) await unlink(lockPath).catch(() => void 0);
|
|
594
|
+
};
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (error.code !== "EEXIST") throw error;
|
|
597
|
+
const lockInfo = await readJson(lockPath);
|
|
598
|
+
let stale = false;
|
|
599
|
+
if (lockInfo && typeof lockInfo.pid === "number") {
|
|
600
|
+
stale = !isRunning(lockInfo.pid) && now() - lockInfo.createdAt >= 0;
|
|
601
|
+
} else {
|
|
602
|
+
try {
|
|
603
|
+
stale = now() - (await stat(lockPath)).mtimeMs > lockStaleMs;
|
|
604
|
+
} catch {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (stale) {
|
|
609
|
+
await rm(lockPath, { force: true }).catch(() => void 0);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (now() >= deadline) throw new Error(`Timed out waiting for Localghost registry lock: ${lockPath}`);
|
|
613
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, lockRetryMs));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async function withLock(operation) {
|
|
618
|
+
const releaseLock = await lock();
|
|
619
|
+
try {
|
|
620
|
+
const registry = await readRegistry();
|
|
621
|
+
pruneRegistry(registry, now(), isRunning);
|
|
622
|
+
return await operation(registry);
|
|
623
|
+
} finally {
|
|
624
|
+
await releaseLock();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
root,
|
|
629
|
+
registryPath,
|
|
630
|
+
lockPath,
|
|
631
|
+
ownerToken,
|
|
632
|
+
read: readRegistry,
|
|
633
|
+
async prune() {
|
|
634
|
+
const releaseLock = await lock();
|
|
635
|
+
try {
|
|
636
|
+
const registry = await readRegistry();
|
|
637
|
+
const before = registry.leases.length;
|
|
638
|
+
pruneRegistry(registry, now(), isRunning);
|
|
639
|
+
await writeRegistry(registry);
|
|
640
|
+
return { removedLeases: before - registry.leases.length };
|
|
641
|
+
} finally {
|
|
642
|
+
await releaseLock();
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
async acquirePort(acquireOptions) {
|
|
646
|
+
const projectCwd = canonicalizeLocalghostProjectCwd(acquireOptions.projectCwd ?? cwd);
|
|
647
|
+
if (!acquireOptions.instanceKey) throw new Error("instanceKey is required");
|
|
648
|
+
return withLock(async (registry) => {
|
|
649
|
+
const key = leaseKey(projectCwd, acquireOptions.instanceKey);
|
|
650
|
+
const existing = registry.allocations.find((entry2) => leaseKey(entry2.projectCwd, entry2.instanceKey) === key);
|
|
651
|
+
const reserved = new Set(acquireOptions.reservedPorts ?? []);
|
|
652
|
+
const activePorts = new Set(registry.leases.map((lease2) => lease2.port));
|
|
653
|
+
const port = existing?.port;
|
|
654
|
+
const ownsActiveLease = registry.leases.some((lease2) => lease2.port === port && leaseKey(lease2.projectCwd, lease2.instanceKey) === key && lease2.ownerToken === ownerToken);
|
|
655
|
+
const reusable = port !== void 0 && !reserved.has(port) && (!activePorts.has(port) || ownsActiveLease) && (ownsActiveLease || await availabilityCheck(port, acquireOptions.host));
|
|
656
|
+
let selectedPort = reusable ? port : void 0;
|
|
657
|
+
if (selectedPort === void 0) {
|
|
658
|
+
const startPort = acquireOptions.startPort ?? 3e3;
|
|
659
|
+
const maxAttempts = acquireOptions.maxAttempts ?? 50;
|
|
660
|
+
for (let offset = 0; offset < maxAttempts; offset += 1) {
|
|
661
|
+
const candidate = startPort + offset;
|
|
662
|
+
if (reserved.has(candidate) || activePorts.has(candidate)) continue;
|
|
663
|
+
if (await availabilityCheck(candidate, acquireOptions.host)) {
|
|
664
|
+
selectedPort = candidate;
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (selectedPort === void 0) throw new Error(`No available registry port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
669
|
+
}
|
|
670
|
+
const timestamp = now();
|
|
671
|
+
const entry = existing ?? { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, updatedAt: timestamp };
|
|
672
|
+
entry.port = selectedPort;
|
|
673
|
+
entry.updatedAt = timestamp;
|
|
674
|
+
if (!existing) registry.allocations.push(entry);
|
|
675
|
+
registry.leases = registry.leases.filter((lease2) => leaseKey(lease2.projectCwd, lease2.instanceKey) !== key);
|
|
676
|
+
const lease = { projectCwd, instanceKey: acquireOptions.instanceKey, port: selectedPort, pid, acquiredAt: timestamp, expiresAt: timestamp + (acquireOptions.leaseTtlMs ?? 30 * 60 * 1e3), ownerToken };
|
|
677
|
+
registry.leases.push(lease);
|
|
678
|
+
await writeRegistry(registry);
|
|
679
|
+
return lease;
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
async releasePort(releaseOptions) {
|
|
683
|
+
const projectCwd = canonicalizeLocalghostProjectCwd(releaseOptions.projectCwd ?? cwd);
|
|
684
|
+
return withLock(async (registry) => {
|
|
685
|
+
const key = leaseKey(projectCwd, releaseOptions.instanceKey);
|
|
686
|
+
const before = registry.leases.length;
|
|
687
|
+
registry.leases = registry.leases.filter((lease) => leaseKey(lease.projectCwd, lease.instanceKey) !== key || lease.ownerToken !== ownerToken);
|
|
688
|
+
if (registry.leases.length !== before) await writeRegistry(registry);
|
|
689
|
+
return registry.leases.length !== before;
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
486
695
|
// src/tunnel.ts
|
|
487
696
|
import { domainToASCII } from "url";
|
|
488
697
|
var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
@@ -860,7 +1069,7 @@ function envHttps() {
|
|
|
860
1069
|
}
|
|
861
1070
|
function getPackageName(cwd) {
|
|
862
1071
|
try {
|
|
863
|
-
const pkg = JSON.parse(readFileSync5(
|
|
1072
|
+
const pkg = JSON.parse(readFileSync5(join6(cwd, "package.json"), "utf8"));
|
|
864
1073
|
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
865
1074
|
} catch {
|
|
866
1075
|
return void 0;
|
|
@@ -943,7 +1152,24 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
943
1152
|
const autoRepair = merged.autoRepair ?? true;
|
|
944
1153
|
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
945
1154
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
946
|
-
|
|
1155
|
+
let port = requestedPort;
|
|
1156
|
+
let releasePort;
|
|
1157
|
+
const reservePort = merged.reservePort ?? false;
|
|
1158
|
+
const instanceKey = merged.instanceKey ?? "run";
|
|
1159
|
+
if (reservePort && dynamicPort) {
|
|
1160
|
+
const registry = createLocalghostRegistry({ cwd, ...merged.registryOwnerToken ? { ownerToken: merged.registryOwnerToken } : {} });
|
|
1161
|
+
const lease = await registry.acquirePort({
|
|
1162
|
+
projectCwd: cwd,
|
|
1163
|
+
instanceKey,
|
|
1164
|
+
startPort: requestedPort,
|
|
1165
|
+
host: probeHost,
|
|
1166
|
+
...options.reservedPorts ? { reservedPorts: options.reservedPorts } : {}
|
|
1167
|
+
});
|
|
1168
|
+
port = lease.port;
|
|
1169
|
+
releasePort = () => registry.releasePort({ projectCwd: cwd, instanceKey });
|
|
1170
|
+
} else if (dynamicPort) {
|
|
1171
|
+
port = await findAvailablePort(requestedPort, { host: probeHost });
|
|
1172
|
+
}
|
|
947
1173
|
const wwwAlias = merged.wwwAlias ?? true;
|
|
948
1174
|
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
949
1175
|
const hosts = uniqueHosts(entries);
|
|
@@ -972,7 +1198,8 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
972
1198
|
https: merged.https ?? envHttps() ?? false,
|
|
973
1199
|
wwwAlias,
|
|
974
1200
|
ghostTunnel,
|
|
975
|
-
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
1201
|
+
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {},
|
|
1202
|
+
...releasePort ? { releasePort } : {}
|
|
976
1203
|
};
|
|
977
1204
|
}
|
|
978
1205
|
|
|
@@ -994,11 +1221,52 @@ async function checkCaddy() {
|
|
|
994
1221
|
};
|
|
995
1222
|
}
|
|
996
1223
|
}
|
|
997
|
-
async function runDoctor() {
|
|
1224
|
+
async function runDoctor(options = {}) {
|
|
998
1225
|
const caddy = await checkCaddy();
|
|
1226
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1227
|
+
const registry = createLocalghostRegistry({ cwd });
|
|
1228
|
+
const data = await registry.read();
|
|
1229
|
+
const now = Date.now();
|
|
1230
|
+
const staleLeases = data.leases.filter((lease) => lease.expiresAt <= now || !isProcessRunning(lease.pid)).map(({ projectCwd, instanceKey, port, pid }) => ({ projectCwd, instanceKey, port, pid }));
|
|
1231
|
+
const allocationsByPort = /* @__PURE__ */ new Map();
|
|
1232
|
+
for (const allocation of data.allocations) {
|
|
1233
|
+
const projects = allocationsByPort.get(allocation.port) ?? [];
|
|
1234
|
+
projects.push(`${allocation.projectCwd}#${allocation.instanceKey}`);
|
|
1235
|
+
allocationsByPort.set(allocation.port, projects);
|
|
1236
|
+
}
|
|
1237
|
+
const duplicateAllocations = [...allocationsByPort.entries()].filter(([, projects]) => projects.length > 1).map(([port, projects]) => ({ port, projects }));
|
|
1238
|
+
let configured;
|
|
1239
|
+
let available;
|
|
1240
|
+
try {
|
|
1241
|
+
const context = await resolveLocalghostContext({
|
|
1242
|
+
cwd,
|
|
1243
|
+
...options.configFiles ? { configFiles: options.configFiles } : {},
|
|
1244
|
+
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1245
|
+
dynamicPort: false
|
|
1246
|
+
});
|
|
1247
|
+
configured = context.requestedPort;
|
|
1248
|
+
available = await isPortAvailable(configured);
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
const currentProjectCwd = canonicalizeLocalghostProjectCwd(cwd);
|
|
1252
|
+
const currentAllocation = data.allocations.find((allocation) => allocation.projectCwd === currentProjectCwd);
|
|
999
1253
|
return {
|
|
1000
|
-
ok: caddy.found,
|
|
1001
|
-
caddy
|
|
1254
|
+
ok: caddy.found && available !== false && staleLeases.length === 0 && duplicateAllocations.length === 0,
|
|
1255
|
+
caddy,
|
|
1256
|
+
ports: {
|
|
1257
|
+
...configured !== void 0 ? { configured } : {},
|
|
1258
|
+
...available !== void 0 ? { available } : {},
|
|
1259
|
+
registryPath: registry.registryPath,
|
|
1260
|
+
staleLeases,
|
|
1261
|
+
duplicateAllocations,
|
|
1262
|
+
...currentAllocation ? {
|
|
1263
|
+
currentAllocation: {
|
|
1264
|
+
projectCwd: currentAllocation.projectCwd,
|
|
1265
|
+
instanceKey: currentAllocation.instanceKey,
|
|
1266
|
+
port: currentAllocation.port
|
|
1267
|
+
}
|
|
1268
|
+
} : {}
|
|
1269
|
+
}
|
|
1002
1270
|
};
|
|
1003
1271
|
}
|
|
1004
1272
|
|
|
@@ -1041,7 +1309,7 @@ function listGhostTunnelEntries(options = {}) {
|
|
|
1041
1309
|
}
|
|
1042
1310
|
|
|
1043
1311
|
// src/ghost-agent.ts
|
|
1044
|
-
import { randomUUID as
|
|
1312
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1045
1313
|
|
|
1046
1314
|
// src/relay.ts
|
|
1047
1315
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
@@ -1153,7 +1421,7 @@ function stripRelayForwardHeaders(headers) {
|
|
|
1153
1421
|
}
|
|
1154
1422
|
|
|
1155
1423
|
// src/ghost-tunnel-store.ts
|
|
1156
|
-
import { randomUUID } from "crypto";
|
|
1424
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1157
1425
|
function base64Encode(value) {
|
|
1158
1426
|
return value.toString("base64");
|
|
1159
1427
|
}
|
|
@@ -1191,13 +1459,18 @@ function parseJson(value) {
|
|
|
1191
1459
|
function keyPart(value) {
|
|
1192
1460
|
return value.toLowerCase().replace(/[^a-z0-9._:-]/g, "_");
|
|
1193
1461
|
}
|
|
1462
|
+
function removeTrailingSlashes(value) {
|
|
1463
|
+
let end = value.length;
|
|
1464
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
1465
|
+
return value.slice(0, end);
|
|
1466
|
+
}
|
|
1194
1467
|
var RedisGhostTunnelStore = class {
|
|
1195
1468
|
url;
|
|
1196
1469
|
token;
|
|
1197
1470
|
namespace;
|
|
1198
1471
|
fetchImpl;
|
|
1199
1472
|
constructor(options) {
|
|
1200
|
-
this.url = options.url
|
|
1473
|
+
this.url = removeTrailingSlashes(options.url);
|
|
1201
1474
|
this.token = options.token;
|
|
1202
1475
|
this.namespace = options.namespace ?? "localghost";
|
|
1203
1476
|
this.fetchImpl = options.fetch ?? fetch;
|
|
@@ -1285,11 +1558,11 @@ function isStopped(signal, localSignal) {
|
|
|
1285
1558
|
}
|
|
1286
1559
|
function wait(ms, signal, localSignal) {
|
|
1287
1560
|
if (isStopped(signal, localSignal)) return Promise.resolve();
|
|
1288
|
-
return new Promise((
|
|
1289
|
-
const timeout = setTimeout(
|
|
1561
|
+
return new Promise((resolve4) => {
|
|
1562
|
+
const timeout = setTimeout(resolve4, ms);
|
|
1290
1563
|
const stop = () => {
|
|
1291
1564
|
clearTimeout(timeout);
|
|
1292
|
-
|
|
1565
|
+
resolve4();
|
|
1293
1566
|
};
|
|
1294
1567
|
signal?.addEventListener("abort", stop, { once: true });
|
|
1295
1568
|
localSignal.addEventListener("abort", stop, { once: true });
|
|
@@ -1375,7 +1648,7 @@ function startGhostTunnelAgent(options) {
|
|
|
1375
1648
|
const controller = new AbortController();
|
|
1376
1649
|
const localSignal = controller.signal;
|
|
1377
1650
|
const signal = options.signal;
|
|
1378
|
-
const agentId = options.agentId ?? `localghost-${
|
|
1651
|
+
const agentId = options.agentId ?? `localghost-${randomUUID3()}`;
|
|
1379
1652
|
const targetHost = options.targetHost ?? "127.0.0.1";
|
|
1380
1653
|
const routeTtlSeconds = options.routeTtlSeconds ?? 30;
|
|
1381
1654
|
const requestTtlSeconds = options.requestTtlSeconds ?? 60;
|
|
@@ -1430,7 +1703,7 @@ function startGhostTunnelAgent(options) {
|
|
|
1430
1703
|
// src/hosts-file.ts
|
|
1431
1704
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
1432
1705
|
import { tmpdir } from "os";
|
|
1433
|
-
import { join as
|
|
1706
|
+
import { join as join7 } from "path";
|
|
1434
1707
|
import { execa as execa3 } from "execa";
|
|
1435
1708
|
function escapeRegExp(value) {
|
|
1436
1709
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1473,7 +1746,7 @@ function removeManagedBlock(existing, projectName) {
|
|
|
1473
1746
|
}
|
|
1474
1747
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
1475
1748
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
1476
|
-
const tempPath =
|
|
1749
|
+
const tempPath = join7(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
1477
1750
|
writeFileSync3(tempPath, next, "utf8");
|
|
1478
1751
|
if (process.env.LOCALGHOST_HOSTS_PATH) {
|
|
1479
1752
|
writeFileSync3(hostsPath, next, "utf8");
|
|
@@ -1511,11 +1784,11 @@ async function removeSystemHosts(projectName) {
|
|
|
1511
1784
|
|
|
1512
1785
|
// src/init.ts
|
|
1513
1786
|
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1514
|
-
import { join as
|
|
1787
|
+
import { join as join8 } from "path";
|
|
1515
1788
|
function detectPackageManager(cwd = process.cwd()) {
|
|
1516
|
-
if (existsSync5(
|
|
1517
|
-
if (existsSync5(
|
|
1518
|
-
if (existsSync5(
|
|
1789
|
+
if (existsSync5(join8(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1790
|
+
if (existsSync5(join8(cwd, "yarn.lock"))) return "yarn";
|
|
1791
|
+
if (existsSync5(join8(cwd, "bun.lock")) || existsSync5(join8(cwd, "bun.lockb"))) return "bun";
|
|
1519
1792
|
return "npm";
|
|
1520
1793
|
}
|
|
1521
1794
|
function packageRunCommand(packageManager, script) {
|
|
@@ -1589,7 +1862,7 @@ function initLocalghost(options = {}) {
|
|
|
1589
1862
|
const apiPort = options.apiPort ?? 8787;
|
|
1590
1863
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
1591
1864
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
1592
|
-
const configPath =
|
|
1865
|
+
const configPath = join8(cwd, configFile);
|
|
1593
1866
|
const configExists = existsSync5(configPath);
|
|
1594
1867
|
if (configExists && !options.force) {
|
|
1595
1868
|
return {
|
|
@@ -1606,7 +1879,7 @@ function initLocalghost(options = {}) {
|
|
|
1606
1879
|
};
|
|
1607
1880
|
}
|
|
1608
1881
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
1609
|
-
const packageJsonPath =
|
|
1882
|
+
const packageJsonPath = join8(cwd, "package.json");
|
|
1610
1883
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
1611
1884
|
return {
|
|
1612
1885
|
configPath,
|
|
@@ -1623,6 +1896,63 @@ function initLocalghost(options = {}) {
|
|
|
1623
1896
|
};
|
|
1624
1897
|
}
|
|
1625
1898
|
|
|
1899
|
+
// src/guide.ts
|
|
1900
|
+
var LOCALGHOST_AGENT_GUIDE = `# Localghost agent guide
|
|
1901
|
+
|
|
1902
|
+
Localghost owns the local development proxy and the app process boundary.
|
|
1903
|
+
|
|
1904
|
+
## Preferred repository setup
|
|
1905
|
+
|
|
1906
|
+
For a normal repository, use this package script:
|
|
1907
|
+
|
|
1908
|
+
"dev": "localghost"
|
|
1909
|
+
|
|
1910
|
+
For an explicit app command, keep the raw command separate:
|
|
1911
|
+
|
|
1912
|
+
"dev": "localghost run -- vite"
|
|
1913
|
+
"dev:raw": "vite"
|
|
1914
|
+
|
|
1915
|
+
Use \`localghost dev\` only when the Caddy proxy should run without starting the app.
|
|
1916
|
+
|
|
1917
|
+
## Useful commands
|
|
1918
|
+
|
|
1919
|
+
- \`localghost\`: detect and run the repository development command.
|
|
1920
|
+
- \`localghost run -- <command>\`: wrap an explicit app command.
|
|
1921
|
+
- \`localghost dev\`: run only the local Caddy proxy.
|
|
1922
|
+
- \`localghost status --ready\`: check project setup.
|
|
1923
|
+
- \`localghost repair\`: repair managed hosts and Caddy setup.
|
|
1924
|
+
- \`localghost ps --json\`: inspect Localghost-managed repositories, instances, and ports.
|
|
1925
|
+
- \`localghost routes\`: inspect hostname-to-port routing.
|
|
1926
|
+
- \`localghost doctor\`: check machine prerequisites, ports, and registry state.
|
|
1927
|
+
- \`localghost repair --reallocate-port\`: move an occupied project port to a stable available port.
|
|
1928
|
+
|
|
1929
|
+
## Configuration
|
|
1930
|
+
|
|
1931
|
+
- Commit repository defaults in \`localghost.config.mjs\`.
|
|
1932
|
+
- Keep hostname and requested-port routes in \`.localghost\`.
|
|
1933
|
+
- CLI flags override repository configuration for one invocation.
|
|
1934
|
+
- Localghost remembers active project and instance port assignments in user state under \`~/.localghost\`.
|
|
1935
|
+
- Do not edit the registry manually and do not start Caddy separately.
|
|
1936
|
+
|
|
1937
|
+
## Port behavior
|
|
1938
|
+
|
|
1939
|
+
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.
|
|
1940
|
+
`;
|
|
1941
|
+
function formatLocalghostAgentGuide(format = "text") {
|
|
1942
|
+
if (format === "json") {
|
|
1943
|
+
return JSON.stringify({
|
|
1944
|
+
preferredScript: "localghost",
|
|
1945
|
+
explicitScript: "localghost run -- <command>",
|
|
1946
|
+
proxyOnlyCommand: "localghost dev",
|
|
1947
|
+
inspectionCommands: ["localghost status --ready", "localghost ps --json", "localghost routes", "localghost doctor"],
|
|
1948
|
+
projectConfig: "localghost.config.mjs",
|
|
1949
|
+
routeConfig: ".localghost",
|
|
1950
|
+
userState: "~/.localghost"
|
|
1951
|
+
}, null, 2);
|
|
1952
|
+
}
|
|
1953
|
+
return LOCALGHOST_AGENT_GUIDE;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1626
1956
|
// src/prompt.ts
|
|
1627
1957
|
import { stdin as input, stdout as output } from "process";
|
|
1628
1958
|
import { createInterface } from "readline/promises";
|
|
@@ -1711,10 +2041,10 @@ function formatGhostTunnel(config, options = {}) {
|
|
|
1711
2041
|
|
|
1712
2042
|
// src/state.ts
|
|
1713
2043
|
import { existsSync as existsSync6 } from "fs";
|
|
1714
|
-
import { join as
|
|
2044
|
+
import { join as join9 } from "path";
|
|
1715
2045
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
1716
2046
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
1717
|
-
return
|
|
2047
|
+
return join9(cwd, LOCALGHOST_STATE_FILE);
|
|
1718
2048
|
}
|
|
1719
2049
|
function readLocalghostState(cwd = process.cwd()) {
|
|
1720
2050
|
const path = getLocalghostStatePath(cwd);
|
|
@@ -1735,10 +2065,10 @@ function patchLocalghostState(cwd, patch) {
|
|
|
1735
2065
|
|
|
1736
2066
|
// src/update-check.ts
|
|
1737
2067
|
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1738
|
-
import { homedir as
|
|
1739
|
-
import { dirname as dirname4, join as
|
|
2068
|
+
import { homedir as homedir3 } from "os";
|
|
2069
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
1740
2070
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
1741
|
-
var LOCALGHOST_VERSION = "0.
|
|
2071
|
+
var LOCALGHOST_VERSION = "0.2.0";
|
|
1742
2072
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1743
2073
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1744
2074
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -1750,8 +2080,8 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
1750
2080
|
}
|
|
1751
2081
|
function getUpdateCheckCachePath(env = process.env) {
|
|
1752
2082
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
1753
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
1754
|
-
return
|
|
2083
|
+
const cacheRoot = env.XDG_CACHE_HOME || join10(homedir3(), ".cache");
|
|
2084
|
+
return join10(cacheRoot, "localghost", "update-check.json");
|
|
1755
2085
|
}
|
|
1756
2086
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
1757
2087
|
if (!existsSync7(path)) return null;
|
|
@@ -1803,7 +2133,7 @@ function isNewerVersion(candidate, current = LOCALGHOST_VERSION) {
|
|
|
1803
2133
|
return Boolean(candidate && compareVersions(candidate, current) > 0);
|
|
1804
2134
|
}
|
|
1805
2135
|
async function fetchLatestVersion(packageName, timeoutMs) {
|
|
1806
|
-
const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).
|
|
2136
|
+
const encodedName = packageName.startsWith("@") ? `@${packageName.slice(1).replaceAll("/", "%2f")}` : packageName;
|
|
1807
2137
|
const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
|
|
1808
2138
|
signal: AbortSignal.timeout(timeoutMs),
|
|
1809
2139
|
headers: {
|
|
@@ -1902,6 +2232,27 @@ ${message}`);
|
|
|
1902
2232
|
|
|
1903
2233
|
// src/cli.ts
|
|
1904
2234
|
import { execa as execa4 } from "execa";
|
|
2235
|
+
|
|
2236
|
+
// src/process.ts
|
|
2237
|
+
function signalManagedProcessPid(pid, signal, killProcess = (value, processSignal) => process.kill(value, processSignal)) {
|
|
2238
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid < 1) return false;
|
|
2239
|
+
try {
|
|
2240
|
+
killProcess(process.platform === "win32" ? pid : -pid, signal);
|
|
2241
|
+
return true;
|
|
2242
|
+
} catch (error) {
|
|
2243
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") return false;
|
|
2244
|
+
throw error;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
function signalManagedProcess(child, signal) {
|
|
2248
|
+
if (process.platform === "win32") {
|
|
2249
|
+
if (!child.killed) child.kill(signal);
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
2252
|
+
return signalManagedProcessPid(child.pid, signal);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/cli.ts
|
|
1905
2256
|
function warnAboutLocalMdns(entries) {
|
|
1906
2257
|
const localHosts = findLocalMdnsHosts(entries);
|
|
1907
2258
|
if (localHosts.length > 0) {
|
|
@@ -1938,6 +2289,10 @@ function parsePackageManager(value) {
|
|
|
1938
2289
|
if (value === "npm" || value === "yarn" || value === "pnpm" || value === "bun") return value;
|
|
1939
2290
|
throw new InvalidArgumentError("Package manager must be npm, pnpm, yarn, or bun.");
|
|
1940
2291
|
}
|
|
2292
|
+
function parseReleaseBump(value) {
|
|
2293
|
+
if (value === "patch" || value === "minor" || value === "major") return value;
|
|
2294
|
+
throw new InvalidArgumentError("Release bump must be patch, minor, or major.");
|
|
2295
|
+
}
|
|
1941
2296
|
function collect(value, previous = []) {
|
|
1942
2297
|
return [...previous, value];
|
|
1943
2298
|
}
|
|
@@ -1975,6 +2330,33 @@ async function assertCaddyReady() {
|
|
|
1975
2330
|
"Localghost will not install it for you. No surprise spells."
|
|
1976
2331
|
].join("\n"));
|
|
1977
2332
|
}
|
|
2333
|
+
function cleanManagedCaddyProcesses() {
|
|
2334
|
+
const runs = listLocalghostRuns();
|
|
2335
|
+
const legacyPids = runs.flatMap((run) => run.caddyPid && !run.caddyPgid ? [run.caddyPid] : []);
|
|
2336
|
+
const managedPgid = runs.flatMap((run) => run.caddyPgid ? [run.caddyPgid] : []);
|
|
2337
|
+
const legacyResult = stopCaddyProcesses(legacyPids);
|
|
2338
|
+
const managedResult = stopCaddyProcesses(managedPgid, signalManagedProcessPid);
|
|
2339
|
+
const result = {
|
|
2340
|
+
stopped: [...legacyResult.stopped, ...managedResult.stopped],
|
|
2341
|
+
alreadyExited: [...legacyResult.alreadyExited, ...managedResult.alreadyExited],
|
|
2342
|
+
failed: [...legacyResult.failed, ...managedResult.failed]
|
|
2343
|
+
};
|
|
2344
|
+
for (const run of runs) {
|
|
2345
|
+
const caddyIdentity = run.caddyPgid ?? run.caddyPid;
|
|
2346
|
+
if (caddyIdentity && (result.stopped.includes(caddyIdentity) || result.alreadyExited.includes(caddyIdentity))) {
|
|
2347
|
+
unregisterLocalghostRun(run.id);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
if (result.stopped.length > 0) {
|
|
2351
|
+
console.log(`Stopped ${result.stopped.length} Localghost-managed Caddy process${result.stopped.length === 1 ? "" : "es"}.`);
|
|
2352
|
+
}
|
|
2353
|
+
if (result.alreadyExited.length > 0) {
|
|
2354
|
+
console.log(`Removed ${result.alreadyExited.length} stale Localghost Caddy record${result.alreadyExited.length === 1 ? "" : "s"}.`);
|
|
2355
|
+
}
|
|
2356
|
+
if (result.failed.length > 0) {
|
|
2357
|
+
throw new Error(`Could not stop Localghost-managed Caddy PID(s): ${result.failed.map(({ pid }) => pid).join(", ")}.`);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
1978
2360
|
function existingTrustMarkers(cwd) {
|
|
1979
2361
|
const state = readLocalghostState(cwd);
|
|
1980
2362
|
return {
|
|
@@ -2079,7 +2461,7 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
2079
2461
|
});
|
|
2080
2462
|
}
|
|
2081
2463
|
function wait2(ms) {
|
|
2082
|
-
return new Promise((
|
|
2464
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
2083
2465
|
}
|
|
2084
2466
|
async function runTrust(cwd, caddyfilePath) {
|
|
2085
2467
|
await wait2(350);
|
|
@@ -2126,25 +2508,19 @@ function registerCleanup(id) {
|
|
|
2126
2508
|
process.off("exit", cleanup);
|
|
2127
2509
|
};
|
|
2128
2510
|
}
|
|
2129
|
-
async function resolveServiceRuntimeEntries(services, dynamicPort) {
|
|
2511
|
+
async function resolveServiceRuntimeEntries(services, dynamicPort, projectCwd) {
|
|
2130
2512
|
const usedPorts = /* @__PURE__ */ new Set();
|
|
2131
2513
|
const resolved = [];
|
|
2514
|
+
const registry = dynamicPort ? createLocalghostRegistry({ cwd: projectCwd, ownerToken: `${process.pid}:services:${projectCwd}` }) : void 0;
|
|
2132
2515
|
for (const service of services) {
|
|
2133
2516
|
let port = service.requestedPort;
|
|
2134
2517
|
if (dynamicPort) {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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
|
-
}
|
|
2518
|
+
const lease = await registry.acquirePort({
|
|
2519
|
+
instanceKey: `service:${service.name}`,
|
|
2520
|
+
startPort: service.requestedPort,
|
|
2521
|
+
reservedPorts: usedPorts
|
|
2522
|
+
});
|
|
2523
|
+
port = lease.port;
|
|
2148
2524
|
} else if (usedPorts.has(port)) {
|
|
2149
2525
|
throw new Error(`Services cannot start separate commands on the same fixed port: ${port}.`);
|
|
2150
2526
|
}
|
|
@@ -2159,7 +2535,13 @@ async function resolveServiceRuntimeEntries(services, dynamicPort) {
|
|
|
2159
2535
|
}
|
|
2160
2536
|
});
|
|
2161
2537
|
}
|
|
2162
|
-
return
|
|
2538
|
+
return {
|
|
2539
|
+
services: resolved,
|
|
2540
|
+
release: async () => {
|
|
2541
|
+
if (!registry) return;
|
|
2542
|
+
await Promise.all(resolved.map((service) => registry.releasePort({ instanceKey: `service:${service.name}` })));
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2163
2545
|
}
|
|
2164
2546
|
async function waitForServicePorts(entries, timeoutMs = 1e4) {
|
|
2165
2547
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2171,84 +2553,104 @@ async function waitForServicePorts(entries, timeoutMs = 1e4) {
|
|
|
2171
2553
|
}
|
|
2172
2554
|
return false;
|
|
2173
2555
|
}
|
|
2556
|
+
async function waitForPortsToBeAvailable(entries, timeoutMs = 1e4) {
|
|
2557
|
+
const deadline = Date.now() + timeoutMs;
|
|
2558
|
+
const ports = [...new Set(entries.map((entry) => entry.port))];
|
|
2559
|
+
while (Date.now() < deadline) {
|
|
2560
|
+
const availability = await Promise.all(ports.map((port) => isPortAvailable(port)));
|
|
2561
|
+
if (availability.every(Boolean)) return true;
|
|
2562
|
+
await wait2(50);
|
|
2563
|
+
}
|
|
2564
|
+
return false;
|
|
2565
|
+
}
|
|
2174
2566
|
async function runDetectedServices(options) {
|
|
2175
2567
|
assertLocalDevelopment("run");
|
|
2176
2568
|
await assertCaddyReady();
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
if (service.port !== service.requestedPort) {
|
|
2200
|
-
console.log(`${service.name}: port ${service.requestedPort} is busy; using ${service.port}.`);
|
|
2569
|
+
const runtime = await resolveServiceRuntimeEntries(options.services, options.dynamicPort, options.cwd);
|
|
2570
|
+
try {
|
|
2571
|
+
const runtimeServices = runtime.services;
|
|
2572
|
+
const entries = runtimeServices.map((service) => service.entry);
|
|
2573
|
+
const readiness = getSetupReadiness({
|
|
2574
|
+
cwd: options.cwd,
|
|
2575
|
+
https: options.https,
|
|
2576
|
+
ignoreCaddyfile: true,
|
|
2577
|
+
entries,
|
|
2578
|
+
configPath: options.configPath,
|
|
2579
|
+
projectName: options.projectName
|
|
2580
|
+
});
|
|
2581
|
+
if (!readiness.ready) {
|
|
2582
|
+
if (!options.autoRepair) {
|
|
2583
|
+
throw new Error([
|
|
2584
|
+
"Localghost setup is missing or stale.",
|
|
2585
|
+
...readiness.reasons.map((reason) => `- ${reason}`),
|
|
2586
|
+
"Automatic repair is disabled. Enable autoRepair or run localghost repair."
|
|
2587
|
+
].join("\n"));
|
|
2588
|
+
}
|
|
2589
|
+
console.log("Localghost setup is stale; repairing it now.");
|
|
2590
|
+
await runSetupFromReadiness(options.cwd, options.https, readiness);
|
|
2201
2591
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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)
|
|
2592
|
+
for (const service of runtimeServices) {
|
|
2593
|
+
if (service.port !== service.requestedPort) {
|
|
2594
|
+
console.log(`${service.name}: port ${service.requestedPort} is busy; using ${service.port}.`);
|
|
2595
|
+
}
|
|
2218
2596
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2597
|
+
const caddyfile = await writeCaddyfile(entries, options.cwd, { https: options.https });
|
|
2598
|
+
await validateCaddyfile(caddyfile);
|
|
2599
|
+
const caddy = startCaddy(caddyfile);
|
|
2600
|
+
const caddyExit = caddy.catch((error) => {
|
|
2601
|
+
if (!caddy.killed) throw error;
|
|
2602
|
+
});
|
|
2603
|
+
const children = runtimeServices.map((service) => execa4(service.command[0], service.command.slice(1), {
|
|
2604
|
+
cwd: service.cwd,
|
|
2605
|
+
stdio: "inherit",
|
|
2606
|
+
detached: process.platform !== "win32",
|
|
2607
|
+
env: {
|
|
2608
|
+
...process.env,
|
|
2609
|
+
LOCALGHOST_PORT: String(service.port),
|
|
2610
|
+
LOCALGHOST_DYNAMIC_PORT: options.dynamicPort ? "1" : "0",
|
|
2611
|
+
LOCALGHOST_SERVICE: service.name,
|
|
2612
|
+
VITE_PORT: String(service.port)
|
|
2613
|
+
}
|
|
2614
|
+
}));
|
|
2615
|
+
const caddyPid = maybePid(caddy.pid);
|
|
2616
|
+
const runRecord = registerLocalghostRun({
|
|
2617
|
+
mode: "run",
|
|
2618
|
+
cwd: options.cwd,
|
|
2619
|
+
projectName: options.projectName,
|
|
2620
|
+
configPath: options.configPath,
|
|
2621
|
+
caddyfilePath: caddyfile,
|
|
2622
|
+
...caddyPid ? { caddyPid } : {},
|
|
2623
|
+
...caddyPid ? { caddyPgid: caddyPid } : {},
|
|
2624
|
+
childCommand: ["services", ...runtimeServices.map((service) => service.name)],
|
|
2625
|
+
https: options.https,
|
|
2626
|
+
dynamicPort: options.dynamicPort,
|
|
2627
|
+
entries
|
|
2628
|
+
});
|
|
2629
|
+
const cleanupRun = registerCleanup(runRecord.id);
|
|
2630
|
+
const processExit = Promise.race([caddyExit, ...children]);
|
|
2631
|
+
try {
|
|
2632
|
+
const ready = await Promise.race([
|
|
2633
|
+
waitForServicePorts(entries),
|
|
2634
|
+
processExit.then(() => false)
|
|
2635
|
+
]);
|
|
2636
|
+
if (ready) {
|
|
2637
|
+
console.log("");
|
|
2638
|
+
logDomainRoutes(entries, { https: options.https });
|
|
2639
|
+
}
|
|
2640
|
+
await processExit;
|
|
2641
|
+
} finally {
|
|
2642
|
+
for (const child of children) {
|
|
2643
|
+
signalManagedProcess(child, "SIGINT");
|
|
2644
|
+
}
|
|
2645
|
+
signalManagedProcess(caddy, "SIGINT");
|
|
2646
|
+
await Promise.allSettled([caddyExit, ...children]);
|
|
2647
|
+
if (!await waitForPortsToBeAvailable(entries)) {
|
|
2648
|
+
console.warn("Localghost: timed out waiting for service ports to be released.");
|
|
2649
|
+
}
|
|
2650
|
+
cleanupRun();
|
|
2243
2651
|
}
|
|
2244
|
-
await processExit;
|
|
2245
2652
|
} finally {
|
|
2246
|
-
|
|
2247
|
-
if (!child.killed) child.kill("SIGINT");
|
|
2248
|
-
}
|
|
2249
|
-
if (!caddy.killed) caddy.kill("SIGINT");
|
|
2250
|
-
await Promise.allSettled([caddyExit, ...children]);
|
|
2251
|
-
cleanupRun();
|
|
2653
|
+
await runtime.release();
|
|
2252
2654
|
}
|
|
2253
2655
|
}
|
|
2254
2656
|
async function getRouteViews(entries) {
|
|
@@ -2344,7 +2746,7 @@ function formatInstanceViews(instances) {
|
|
|
2344
2746
|
var program = new Command();
|
|
2345
2747
|
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
2748
|
program.hook("postAction", async (_thisCommand, actionCommand) => {
|
|
2347
|
-
if (actionCommand.name() === "update") return;
|
|
2749
|
+
if (actionCommand.name() === "update" || actionCommand.name() === "release") return;
|
|
2348
2750
|
const options = program.opts();
|
|
2349
2751
|
await maybeNotifyAboutUpdate({ disabled: options.updateCheck === false });
|
|
2350
2752
|
});
|
|
@@ -2369,8 +2771,20 @@ program.command("init").description("Create a .localghost config for this projec
|
|
|
2369
2771
|
console.log(` ${step}`);
|
|
2370
2772
|
}
|
|
2371
2773
|
});
|
|
2372
|
-
program.command("
|
|
2373
|
-
|
|
2774
|
+
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) => {
|
|
2775
|
+
console.log(formatLocalghostAgentGuide(options.json ? "json" : "text"));
|
|
2776
|
+
});
|
|
2777
|
+
program.command("doctor").description("Check machine prerequisites, ports, and Localghost registry state").option("--cwd <path>", "Project directory", process.cwd()).option("--config <file>", "Config file to inspect. Can be repeated.", collect, []).option("--config-pattern <regex>", "Regex for config filenames in the project root").option("--json", "Print raw JSON").action(async (options) => {
|
|
2778
|
+
const result = await runDoctor({
|
|
2779
|
+
cwd: options.cwd,
|
|
2780
|
+
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
2781
|
+
...options.configPattern ? { configPattern: options.configPattern } : {}
|
|
2782
|
+
});
|
|
2783
|
+
if (options.json) {
|
|
2784
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2785
|
+
if (!result.ok) process.exitCode = 1;
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2374
2788
|
if (result.caddy.found) {
|
|
2375
2789
|
console.log(`Caddy: ${result.caddy.version ?? "found"}`);
|
|
2376
2790
|
} else {
|
|
@@ -2378,6 +2792,17 @@ program.command("doctor").description("Check machine prerequisites").action(asyn
|
|
|
2378
2792
|
console.log(`Run: ${result.caddy.installHint}`);
|
|
2379
2793
|
console.log("Localghost will not install it for you. No surprise spells.");
|
|
2380
2794
|
}
|
|
2795
|
+
if (result.ports.configured === void 0) {
|
|
2796
|
+
console.log("Port: could not resolve project configuration.");
|
|
2797
|
+
} else {
|
|
2798
|
+
console.log(`Port ${result.ports.configured}: ${result.ports.available ? "available" : "occupied"}`);
|
|
2799
|
+
}
|
|
2800
|
+
if (result.ports.staleLeases.length > 0) {
|
|
2801
|
+
console.log(`Registry: ${result.ports.staleLeases.length} stale lease(s); run localghost repair --prune-registry.`);
|
|
2802
|
+
}
|
|
2803
|
+
for (const duplicate of result.ports.duplicateAllocations) {
|
|
2804
|
+
console.log(`Registry: port ${duplicate.port} is allocated to ${duplicate.projects.join(", ")}.`);
|
|
2805
|
+
}
|
|
2381
2806
|
if (!result.ok) {
|
|
2382
2807
|
process.exitCode = 1;
|
|
2383
2808
|
}
|
|
@@ -2400,6 +2825,30 @@ program.command("update").description("Check npm for a newer localghost release"
|
|
|
2400
2825
|
}
|
|
2401
2826
|
console.log(`localghost is up to date. Current: ${result.currentVersion}`);
|
|
2402
2827
|
});
|
|
2828
|
+
program.command("release").description("Dispatch an automated Localghost CLI release").argument("<bump>", "Semantic version increment: patch, minor, or major", parseReleaseBump).action(async (bump) => {
|
|
2829
|
+
const repository = "hamedb89/localghost";
|
|
2830
|
+
try {
|
|
2831
|
+
await execa4("gh", [
|
|
2832
|
+
"workflow",
|
|
2833
|
+
"run",
|
|
2834
|
+
"release.yml",
|
|
2835
|
+
"--repo",
|
|
2836
|
+
repository,
|
|
2837
|
+
"--ref",
|
|
2838
|
+
"main",
|
|
2839
|
+
"-f",
|
|
2840
|
+
`bump=${bump}`
|
|
2841
|
+
]);
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2844
|
+
throw new Error(
|
|
2845
|
+
`Could not dispatch the Localghost release workflow. Install and authenticate GitHub CLI with \`gh auth login\`, then retry.
|
|
2846
|
+
${detail}`
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
console.log(`Dispatched a ${bump} Localghost release from main.`);
|
|
2850
|
+
console.log(`Track it at https://github.com/${repository}/actions/workflows/release.yml`);
|
|
2851
|
+
});
|
|
2403
2852
|
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
2853
|
assertLocalDevelopment("setup");
|
|
2405
2854
|
printLocalghostBanner();
|
|
@@ -2460,11 +2909,20 @@ program.command("trust").description("Trust Caddy's local HTTPS CA for this proj
|
|
|
2460
2909
|
await validateCaddyfile(caddyfile);
|
|
2461
2910
|
await runTrust(options.cwd, caddyfile);
|
|
2462
2911
|
});
|
|
2463
|
-
program.command("repair").description("Reconcile stale
|
|
2912
|
+
program.command("repair").description("Reconcile stale setup, ports, registry state, and optional HTTPS trust").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", "Repair an HTTPS Caddy setup").option("--ssl", "Alias for --https").option("--trust", "Re-run Caddy's local HTTPS trust step").option("--reallocate-port", "Persist a stable replacement for an occupied port").option("--prune-registry", "Remove expired or dead registry leases").action(async (options) => {
|
|
2464
2913
|
assertLocalDevelopment("repair");
|
|
2465
2914
|
printLocalghostBanner();
|
|
2466
2915
|
await assertCaddyReady();
|
|
2467
|
-
const
|
|
2916
|
+
const registry = createLocalghostRegistry({ cwd: options.cwd });
|
|
2917
|
+
if (options.pruneRegistry) {
|
|
2918
|
+
const result = await registry.prune();
|
|
2919
|
+
console.log(`Pruned ${result.removedLeases} stale registry lease${result.removedLeases === 1 ? "" : "s"}.`);
|
|
2920
|
+
}
|
|
2921
|
+
const context = await resolveLocalghostContext({
|
|
2922
|
+
...contextOptionsFromCli(options),
|
|
2923
|
+
dynamicPort: options.reallocatePort ? true : false,
|
|
2924
|
+
...options.reallocatePort ? { reservePort: true, instanceKey: "run" } : {}
|
|
2925
|
+
});
|
|
2468
2926
|
const readiness = getSetupReadiness({
|
|
2469
2927
|
...options,
|
|
2470
2928
|
https: context.https,
|
|
@@ -2477,14 +2935,21 @@ program.command("repair").description("Reconcile stale hosts, Caddyfile, setup s
|
|
|
2477
2935
|
}
|
|
2478
2936
|
warnAboutLocalMdns(context.entries);
|
|
2479
2937
|
logDomainRoutes(context.entries, { https: context.https, ghostTunnel: context.ghostTunnel });
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2938
|
+
try {
|
|
2939
|
+
await runSetupFromReadiness(options.cwd, context.https, readiness);
|
|
2940
|
+
if (options.trust) {
|
|
2941
|
+
await runTrust(options.cwd, readiness.caddyfilePath);
|
|
2942
|
+
}
|
|
2943
|
+
if (context.port !== context.requestedPort) {
|
|
2944
|
+
console.log(`Reallocated port ${context.requestedPort} -> ${context.port}.`);
|
|
2945
|
+
}
|
|
2946
|
+
console.log(`Repaired hosts: ${getSystemHostsPath()}`);
|
|
2947
|
+
console.log(`Repaired Caddyfile: ${readiness.caddyfilePath}`);
|
|
2948
|
+
console.log(`Repaired state: ${readiness.statePath}`);
|
|
2949
|
+
console.log("Repair complete.");
|
|
2950
|
+
} finally {
|
|
2951
|
+
await context.releasePort?.();
|
|
2952
|
+
}
|
|
2488
2953
|
});
|
|
2489
2954
|
program.command("reset").description("Remove Localghost setup state without deleting .localghost").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).action(async (options) => {
|
|
2490
2955
|
assertLocalDevelopment("reset");
|
|
@@ -2600,9 +3065,10 @@ program.command("routes").description("Print domain to upstream routes").option(
|
|
|
2600
3065
|
}));
|
|
2601
3066
|
}
|
|
2602
3067
|
});
|
|
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) => {
|
|
3068
|
+
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
3069
|
assertLocalDevelopment("dev");
|
|
2605
3070
|
await assertCaddyReady();
|
|
3071
|
+
if (options.cleanCaddy) cleanManagedCaddyProcesses();
|
|
2606
3072
|
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
2607
3073
|
const https = context.https;
|
|
2608
3074
|
const readiness = getSetupReadiness({
|
|
@@ -2639,7 +3105,7 @@ program.command("dev").description("Run the Localghost Caddy proxy, repairing st
|
|
|
2639
3105
|
...typeof options.trust === "boolean" ? { trust: options.trust } : {}
|
|
2640
3106
|
});
|
|
2641
3107
|
} catch (error) {
|
|
2642
|
-
|
|
3108
|
+
signalManagedProcess(caddy, "SIGINT");
|
|
2643
3109
|
throw error;
|
|
2644
3110
|
}
|
|
2645
3111
|
const caddyPid = maybePid(caddy.pid);
|
|
@@ -2650,6 +3116,7 @@ program.command("dev").description("Run the Localghost Caddy proxy, repairing st
|
|
|
2650
3116
|
configPath: readiness.configPath,
|
|
2651
3117
|
caddyfilePath: caddyfile,
|
|
2652
3118
|
...caddyPid ? { caddyPid } : {},
|
|
3119
|
+
...caddyPid ? { caddyPgid: caddyPid } : {},
|
|
2653
3120
|
https,
|
|
2654
3121
|
entries: readiness.entries
|
|
2655
3122
|
});
|
|
@@ -2660,15 +3127,18 @@ program.command("dev").description("Run the Localghost Caddy proxy, repairing st
|
|
|
2660
3127
|
cleanupRun();
|
|
2661
3128
|
}
|
|
2662
3129
|
});
|
|
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) => {
|
|
3130
|
+
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
3131
|
assertLocalDevelopment("run");
|
|
2665
3132
|
await assertCaddyReady();
|
|
3133
|
+
if (options.cleanCaddy) cleanManagedCaddyProcesses();
|
|
2666
3134
|
const context = await resolveLocalghostContext({
|
|
2667
3135
|
cwd: options.cwd,
|
|
2668
3136
|
...options.project ? { project: options.project } : {},
|
|
2669
3137
|
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
2670
3138
|
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
2671
3139
|
...options.port ? { port: options.port } : {},
|
|
3140
|
+
reservePort: true,
|
|
3141
|
+
instanceKey: "run",
|
|
2672
3142
|
...useHttps(options) ? { https: true } : {},
|
|
2673
3143
|
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {},
|
|
2674
3144
|
...typeof options.autoRepair === "boolean" ? { autoRepair: options.autoRepair } : {}
|
|
@@ -2716,7 +3186,7 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
2716
3186
|
...typeof options.trust === "boolean" ? { trust: options.trust } : {}
|
|
2717
3187
|
});
|
|
2718
3188
|
} catch (error) {
|
|
2719
|
-
|
|
3189
|
+
signalManagedProcess(caddy, "SIGINT");
|
|
2720
3190
|
throw error;
|
|
2721
3191
|
}
|
|
2722
3192
|
const [binary, ...args] = command;
|
|
@@ -2726,6 +3196,7 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
2726
3196
|
const child = execa4(binary, args, {
|
|
2727
3197
|
cwd: options.cwd,
|
|
2728
3198
|
stdio: "inherit",
|
|
3199
|
+
detached: process.platform !== "win32",
|
|
2729
3200
|
env: {
|
|
2730
3201
|
...process.env,
|
|
2731
3202
|
LOCALGHOST_PORT: String(context.port),
|
|
@@ -2742,6 +3213,7 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
2742
3213
|
configPath: context.configPath,
|
|
2743
3214
|
caddyfilePath: caddyfile,
|
|
2744
3215
|
...caddyPid ? { caddyPid } : {},
|
|
3216
|
+
...caddyPid ? { caddyPgid: caddyPid } : {},
|
|
2745
3217
|
...childPid ? { childPid } : {},
|
|
2746
3218
|
childCommand: command,
|
|
2747
3219
|
https,
|
|
@@ -2752,10 +3224,10 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
2752
3224
|
});
|
|
2753
3225
|
const cleanupRun = registerCleanup(runRecord.id);
|
|
2754
3226
|
const stopCaddy = () => {
|
|
2755
|
-
|
|
3227
|
+
signalManagedProcess(caddy, "SIGINT");
|
|
2756
3228
|
};
|
|
2757
3229
|
const stopChild = () => {
|
|
2758
|
-
|
|
3230
|
+
signalManagedProcess(child, "SIGINT");
|
|
2759
3231
|
};
|
|
2760
3232
|
try {
|
|
2761
3233
|
await Promise.race([child, caddyExit]);
|
|
@@ -2763,10 +3235,14 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
2763
3235
|
stopChild();
|
|
2764
3236
|
stopCaddy();
|
|
2765
3237
|
await Promise.allSettled([child, caddyExit]);
|
|
3238
|
+
if (!await waitForPortsToBeAvailable(context.entries)) {
|
|
3239
|
+
console.warn("Localghost: timed out waiting for service ports to be released.");
|
|
3240
|
+
}
|
|
2766
3241
|
cleanupRun();
|
|
3242
|
+
await context.releasePort?.();
|
|
2767
3243
|
}
|
|
2768
3244
|
});
|
|
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) => {
|
|
3245
|
+
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
3246
|
assertLocalDevelopment("tunnel");
|
|
2771
3247
|
const context = await resolveLocalghostContext({
|
|
2772
3248
|
cwd: options.cwd,
|
|
@@ -2833,6 +3309,7 @@ function readImplicitInvocation(args) {
|
|
|
2833
3309
|
let cwd = process.cwd();
|
|
2834
3310
|
let dryRun = false;
|
|
2835
3311
|
let updateCheck = true;
|
|
3312
|
+
const forwardedArgs = [];
|
|
2836
3313
|
for (let index = 0; index < args.length; index += 1) {
|
|
2837
3314
|
const arg = args[index];
|
|
2838
3315
|
if (!arg) continue;
|
|
@@ -2844,6 +3321,30 @@ function readImplicitInvocation(args) {
|
|
|
2844
3321
|
updateCheck = false;
|
|
2845
3322
|
continue;
|
|
2846
3323
|
}
|
|
3324
|
+
if (["--clean-caddy", "--https", "--ssl", "--setup", "--trust"].includes(arg)) {
|
|
3325
|
+
forwardedArgs.push(arg);
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
if (arg === "--auto-repair" || arg === "--dynamic-port") {
|
|
3329
|
+
const value = args[index + 1];
|
|
3330
|
+
forwardedArgs.push(arg);
|
|
3331
|
+
if (value && !value.startsWith("--")) {
|
|
3332
|
+
forwardedArgs.push(value);
|
|
3333
|
+
index += 1;
|
|
3334
|
+
}
|
|
3335
|
+
continue;
|
|
3336
|
+
}
|
|
3337
|
+
if (arg === "--port" || arg === "--project" || arg === "--config" || arg === "--config-pattern") {
|
|
3338
|
+
const value = args[index + 1];
|
|
3339
|
+
if (!value) throw new Error(`${arg} requires a value.`);
|
|
3340
|
+
forwardedArgs.push(arg, value);
|
|
3341
|
+
index += 1;
|
|
3342
|
+
continue;
|
|
3343
|
+
}
|
|
3344
|
+
if (["--auto-repair=", "--dynamic-port=", "--port=", "--project=", "--config=", "--config-pattern="].some((prefix) => arg.startsWith(prefix))) {
|
|
3345
|
+
forwardedArgs.push(arg);
|
|
3346
|
+
continue;
|
|
3347
|
+
}
|
|
2847
3348
|
if (arg === "--cwd") {
|
|
2848
3349
|
const value = args[index + 1];
|
|
2849
3350
|
if (!value) throw new Error("--cwd requires a path.");
|
|
@@ -2857,7 +3358,15 @@ function readImplicitInvocation(args) {
|
|
|
2857
3358
|
}
|
|
2858
3359
|
return null;
|
|
2859
3360
|
}
|
|
2860
|
-
return { cwd, dryRun, updateCheck };
|
|
3361
|
+
return { cwd, dryRun, updateCheck, forwardedArgs };
|
|
3362
|
+
}
|
|
3363
|
+
function hasForwardedFlag(args, ...flags) {
|
|
3364
|
+
return args.some((arg) => flags.includes(arg));
|
|
3365
|
+
}
|
|
3366
|
+
function forwardedBoolean(args, name) {
|
|
3367
|
+
const inline = args.find((arg) => arg.startsWith(`${name}=`));
|
|
3368
|
+
if (inline) return parseBooleanLike(inline.slice(name.length + 1));
|
|
3369
|
+
return args.includes(name) ? true : void 0;
|
|
2861
3370
|
}
|
|
2862
3371
|
async function main() {
|
|
2863
3372
|
const implicit = readImplicitInvocation(process.argv.slice(2));
|
|
@@ -2875,14 +3384,15 @@ async function main() {
|
|
|
2875
3384
|
});
|
|
2876
3385
|
console.log(formatDetectedDevServices(services));
|
|
2877
3386
|
if (implicit.dryRun) return;
|
|
3387
|
+
if (hasForwardedFlag(implicit.forwardedArgs, "--clean-caddy")) cleanManagedCaddyProcesses();
|
|
2878
3388
|
await runDetectedServices({
|
|
2879
3389
|
cwd: implicit.cwd,
|
|
2880
3390
|
services,
|
|
2881
3391
|
configPath: projectConfig.path,
|
|
2882
3392
|
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
|
|
3393
|
+
https: hasForwardedFlag(implicit.forwardedArgs, "--https", "--ssl") || (projectConfig.config.https ?? false),
|
|
3394
|
+
dynamicPort: forwardedBoolean(implicit.forwardedArgs, "--dynamic-port") ?? projectConfig.config.dynamicPort ?? true,
|
|
3395
|
+
autoRepair: forwardedBoolean(implicit.forwardedArgs, "--auto-repair") ?? projectConfig.config.autoRepair ?? true
|
|
2886
3396
|
});
|
|
2887
3397
|
await maybeNotifyAboutUpdate({ disabled: !implicit.updateCheck });
|
|
2888
3398
|
return;
|
|
@@ -2898,6 +3408,7 @@ async function main() {
|
|
|
2898
3408
|
process.argv[1] ?? "localghost",
|
|
2899
3409
|
...implicit.updateCheck ? [] : ["--no-update-check"],
|
|
2900
3410
|
"run",
|
|
3411
|
+
...implicit.forwardedArgs,
|
|
2901
3412
|
"--cwd",
|
|
2902
3413
|
implicit.cwd,
|
|
2903
3414
|
"--",
|