@hamedb89/localghost 0.1.6 → 0.1.9
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 +202 -22
- package/apps/macos-widget/LocalghostWidget.entitlements +12 -0
- package/apps/macos-widget/LocalghostWidget.swift +871 -0
- package/apps/macos-widget/Resources/localghost-logo-source.png +0 -0
- package/apps/macos-widget/Resources/localghost-widget-ui-reference.png +0 -0
- package/apps/macos-widget/Shared/LocalghostWidgetSnapshot.swift +50 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidget.swift +176 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidgetExtension.entitlements +12 -0
- package/apps/macos-widget/build.sh +60 -0
- package/apps/macos-widget/project.yml +40 -0
- package/assets/ghost-tunnel-app-icon.png +0 -0
- package/assets/ghost-tunnel-portal.png +0 -0
- package/assets/ghost-tunnel-wordmark.png +0 -0
- package/dist/cli.js +731 -91
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +138 -5
- package/dist/index.js +833 -37
- package/dist/index.js.map +1 -1
- package/dist/tunnel-DzfLXZ8O.d.ts +126 -0
- package/dist/vite.d.ts +5 -1
- package/dist/vite.js +717 -43
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +19 -0
- package/docs/ghost-tunnel.md +249 -0
- package/docs/github.md +6 -6
- package/docs/localghost.1.md +28 -7
- package/docs/macos-widget.md +108 -0
- package/package.json +8 -2
- package/dist/config-Cde1Bich.d.ts +0 -31
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, unlinkSync } from "fs";
|
|
5
5
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/activity.ts
|
|
@@ -25,7 +25,7 @@ function isProcessRunning(pid) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
function emptyActivity() {
|
|
28
|
-
return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [] };
|
|
28
|
+
return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [], setups: [] };
|
|
29
29
|
}
|
|
30
30
|
function readLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
31
31
|
if (!existsSync(path)) return emptyActivity();
|
|
@@ -33,7 +33,8 @@ function readLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
|
33
33
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
34
34
|
return {
|
|
35
35
|
version: LOCALGHOST_ACTIVITY_VERSION,
|
|
36
|
-
runs: Array.isArray(parsed.runs) ? parsed.runs : []
|
|
36
|
+
runs: Array.isArray(parsed.runs) ? parsed.runs : [],
|
|
37
|
+
setups: Array.isArray(parsed.setups) ? parsed.setups : []
|
|
37
38
|
};
|
|
38
39
|
} catch {
|
|
39
40
|
return emptyActivity();
|
|
@@ -48,22 +49,29 @@ function writeLocalghostActivity(activity, path = getLocalghostActivityPath()) {
|
|
|
48
49
|
function createRunId(input2, pid) {
|
|
49
50
|
return `${input2.projectName}:${input2.mode}:${pid}:${Date.now()}`;
|
|
50
51
|
}
|
|
52
|
+
function createSetupId(input2) {
|
|
53
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
54
|
+
}
|
|
51
55
|
function pruneLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
52
56
|
const activity = readLocalghostActivity(path);
|
|
53
57
|
const activeRuns = activity.runs.filter((run) => isProcessRunning(run.pid));
|
|
54
58
|
const pruned = activeRuns.length !== activity.runs.length;
|
|
55
59
|
if (pruned) {
|
|
56
|
-
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
|
|
60
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
|
|
57
61
|
}
|
|
58
62
|
return {
|
|
59
63
|
path,
|
|
60
64
|
pruned,
|
|
61
|
-
runs: activeRuns
|
|
65
|
+
runs: activeRuns,
|
|
66
|
+
setups: activity.setups
|
|
62
67
|
};
|
|
63
68
|
}
|
|
64
69
|
function listLocalghostRuns(path = getLocalghostActivityPath()) {
|
|
65
70
|
return pruneLocalghostActivity(path).runs;
|
|
66
71
|
}
|
|
72
|
+
function listLocalghostSetups(path = getLocalghostActivityPath()) {
|
|
73
|
+
return pruneLocalghostActivity(path).setups;
|
|
74
|
+
}
|
|
67
75
|
function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
|
|
68
76
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
69
77
|
const pid = input2.pid ?? process.pid;
|
|
@@ -87,14 +95,43 @@ function registerLocalghostRun(input2, path = getLocalghostActivityPath()) {
|
|
|
87
95
|
entries: input2.entries
|
|
88
96
|
};
|
|
89
97
|
const current = pruneLocalghostActivity(path).runs.filter((run) => run.id !== record.id);
|
|
90
|
-
|
|
98
|
+
const activity = readLocalghostActivity(path);
|
|
99
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record], setups: activity.setups }, path);
|
|
100
|
+
return record;
|
|
101
|
+
}
|
|
102
|
+
function registerLocalghostSetup(input2, path = getLocalghostActivityPath()) {
|
|
103
|
+
const record = {
|
|
104
|
+
id: input2.id ?? createSetupId(input2),
|
|
105
|
+
cwd: input2.cwd,
|
|
106
|
+
projectName: input2.projectName,
|
|
107
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
108
|
+
...input2.configPath ? { configPath: input2.configPath } : {},
|
|
109
|
+
...input2.caddyfilePath ? { caddyfilePath: input2.caddyfilePath } : {},
|
|
110
|
+
...typeof input2.https === "boolean" ? { https: input2.https } : {},
|
|
111
|
+
entries: input2.entries
|
|
112
|
+
};
|
|
113
|
+
const activity = pruneLocalghostActivity(path);
|
|
114
|
+
const setups = activity.setups.filter((setup) => setup.id !== record.id);
|
|
115
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activity.runs, setups: [...setups, record] }, path);
|
|
91
116
|
return record;
|
|
92
117
|
}
|
|
93
118
|
function unregisterLocalghostRun(id, path = getLocalghostActivityPath()) {
|
|
94
119
|
const activity = readLocalghostActivity(path);
|
|
95
120
|
const runs = activity.runs.filter((run) => run.id !== id);
|
|
96
121
|
if (runs.length !== activity.runs.length) {
|
|
97
|
-
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
|
|
122
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function unregisterLocalghostSetup(options, path = getLocalghostActivityPath()) {
|
|
126
|
+
const activity = readLocalghostActivity(path);
|
|
127
|
+
const setups = activity.setups.filter((setup) => {
|
|
128
|
+
if (setup.cwd !== options.cwd) return true;
|
|
129
|
+
if (options.projectName && setup.projectName !== options.projectName) return true;
|
|
130
|
+
if (options.configPath && setup.configPath !== options.configPath) return true;
|
|
131
|
+
return false;
|
|
132
|
+
});
|
|
133
|
+
if (setups.length !== activity.setups.length) {
|
|
134
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, setups }, path);
|
|
98
135
|
}
|
|
99
136
|
}
|
|
100
137
|
|
|
@@ -280,6 +317,17 @@ function startCaddy(path) {
|
|
|
280
317
|
stdio: "inherit"
|
|
281
318
|
});
|
|
282
319
|
}
|
|
320
|
+
async function trustCaddy(path) {
|
|
321
|
+
await execa("caddy", ["trust", "--config", path], {
|
|
322
|
+
cwd: dirname3(path),
|
|
323
|
+
stdio: "inherit"
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/context.ts
|
|
328
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
329
|
+
import { join as join4 } from "path";
|
|
330
|
+
import { pathToFileURL } from "url";
|
|
283
331
|
|
|
284
332
|
// src/port.ts
|
|
285
333
|
import { createServer } from "net";
|
|
@@ -307,7 +355,263 @@ async function findAvailablePort(startPort, options = {}) {
|
|
|
307
355
|
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
308
356
|
}
|
|
309
357
|
|
|
358
|
+
// src/tunnel.ts
|
|
359
|
+
import { domainToASCII } from "url";
|
|
360
|
+
var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
361
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
|
|
362
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR = "-";
|
|
363
|
+
var DEFAULT_GHOST_TUNNEL_MODE = "manual";
|
|
364
|
+
function isResolvedGhostTunnelConfig(value) {
|
|
365
|
+
return typeof value === "object" && value !== null && "enabled" in value;
|
|
366
|
+
}
|
|
367
|
+
function toGhostTunnelConfig(options) {
|
|
368
|
+
return isResolvedGhostTunnelConfig(options) ? options : resolveGhostTunnelConfig(options);
|
|
369
|
+
}
|
|
370
|
+
function stripHostPort(value) {
|
|
371
|
+
const trimmed = value.trim().toLowerCase();
|
|
372
|
+
if (trimmed.startsWith("[") || trimmed.includes("/")) return "";
|
|
373
|
+
const portSeparator = trimmed.lastIndexOf(":");
|
|
374
|
+
if (portSeparator === -1) return trimmed;
|
|
375
|
+
const port = trimmed.slice(portSeparator + 1);
|
|
376
|
+
return /^\d+$/.test(port) ? trimmed.slice(0, portSeparator) : trimmed;
|
|
377
|
+
}
|
|
378
|
+
function normalizeDomain(value) {
|
|
379
|
+
const host = stripHostPort(value.replace(/^\*\./, ""));
|
|
380
|
+
const ascii = domainToASCII(host);
|
|
381
|
+
if (!ascii || ascii.length > 253 || ascii.includes("..")) return null;
|
|
382
|
+
if (ascii.startsWith(".") || ascii.endsWith(".")) return null;
|
|
383
|
+
if (ascii.includes("*")) return null;
|
|
384
|
+
if (!ascii.split(".").every(isValidHostLabel)) return null;
|
|
385
|
+
return ascii;
|
|
386
|
+
}
|
|
387
|
+
function isValidHostLabel(value) {
|
|
388
|
+
return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
|
|
389
|
+
}
|
|
390
|
+
function isValidNamespaceTag(value) {
|
|
391
|
+
return /^[a-z][a-z0-9_]*$/i.test(value);
|
|
392
|
+
}
|
|
393
|
+
function isNamespaceTagList(options) {
|
|
394
|
+
return Array.isArray(options);
|
|
395
|
+
}
|
|
396
|
+
function assertValidSubdomain(value) {
|
|
397
|
+
if (!isValidHostLabel(value)) {
|
|
398
|
+
throw new Error(`Invalid ghost tunnel subdomain: ${value}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function normalizeDomains(domains) {
|
|
402
|
+
const values = typeof domains === "string" ? [domains] : [...domains ?? []];
|
|
403
|
+
const normalized = values.map((value) => value.trim()).filter(Boolean).map((value) => {
|
|
404
|
+
const domain = normalizeDomain(value);
|
|
405
|
+
if (!domain) throw new Error(`Invalid ghost tunnel domain: ${value}`);
|
|
406
|
+
return domain;
|
|
407
|
+
});
|
|
408
|
+
return [...new Set(normalized)];
|
|
409
|
+
}
|
|
410
|
+
function parseGhostTunnelMode(value) {
|
|
411
|
+
return value ?? DEFAULT_GHOST_TUNNEL_MODE;
|
|
412
|
+
}
|
|
413
|
+
function resolveNamespaceConfig(options) {
|
|
414
|
+
const tags = isNamespaceTagList(options) ? [...options] : [...options?.tags ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS];
|
|
415
|
+
let separator = DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
416
|
+
let spreadTag = tags.includes("project") ? "project" : void 0;
|
|
417
|
+
if (options && !isNamespaceTagList(options)) {
|
|
418
|
+
separator = options.separator ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
419
|
+
spreadTag = options.spreadTag === false ? false : options.spreadTag ?? spreadTag;
|
|
420
|
+
}
|
|
421
|
+
if (tags.length === 0) {
|
|
422
|
+
throw new Error("Ghost tunnel namespace must include at least one tag.");
|
|
423
|
+
}
|
|
424
|
+
for (const tag of tags) {
|
|
425
|
+
if (!isValidNamespaceTag(tag)) {
|
|
426
|
+
throw new Error(`Invalid ghost tunnel namespace tag: ${tag}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (spreadTag && !tags.includes(spreadTag)) {
|
|
430
|
+
throw new Error(`Ghost tunnel namespace spreadTag must be listed in tags: ${spreadTag}`);
|
|
431
|
+
}
|
|
432
|
+
if (!separator || separator.length > 8 || !/^[a-z0-9-]+$/.test(separator)) {
|
|
433
|
+
throw new Error(`Invalid ghost tunnel namespace separator: ${separator}`);
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
tags,
|
|
437
|
+
separator,
|
|
438
|
+
...spreadTag ? { spreadTag } : {}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function normalizeNamespaceValue(tag, value, separator, options = {}) {
|
|
442
|
+
const normalized = normalizeDomain(value);
|
|
443
|
+
if (!normalized || normalized.includes(".")) {
|
|
444
|
+
throw new Error(`Invalid ghost tunnel namespace value for ${tag}: ${value}`);
|
|
445
|
+
}
|
|
446
|
+
if (!options.allowSeparator && normalized.includes(separator)) {
|
|
447
|
+
throw new Error(`Ghost tunnel namespace value for ${tag} cannot include separator "${separator}": ${value}`);
|
|
448
|
+
}
|
|
449
|
+
return normalized;
|
|
450
|
+
}
|
|
451
|
+
function createNamespaceSlug(config, values) {
|
|
452
|
+
const parts = config.tags.map((tag) => {
|
|
453
|
+
const value = values[tag];
|
|
454
|
+
if (!value) {
|
|
455
|
+
throw new Error(`Missing ghost tunnel namespace value: ${tag}`);
|
|
456
|
+
}
|
|
457
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
458
|
+
});
|
|
459
|
+
const slug = parts.join(config.separator);
|
|
460
|
+
if (!isValidHostLabel(slug)) {
|
|
461
|
+
throw new Error(`Ghost tunnel namespace is too long for a DNS label: ${slug}`);
|
|
462
|
+
}
|
|
463
|
+
return slug;
|
|
464
|
+
}
|
|
465
|
+
function createNamespaceDisplaySlug(config, values = {}) {
|
|
466
|
+
return config.tags.map((tag) => {
|
|
467
|
+
const value = values[tag];
|
|
468
|
+
if (!value) return `<${tag}>`;
|
|
469
|
+
try {
|
|
470
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
471
|
+
} catch {
|
|
472
|
+
return `<${tag}>`;
|
|
473
|
+
}
|
|
474
|
+
}).join(config.separator);
|
|
475
|
+
}
|
|
476
|
+
function getPreviewDefaults(preview, defaults) {
|
|
477
|
+
return {
|
|
478
|
+
domain: preview?.domain ?? defaults?.domain,
|
|
479
|
+
route: preview?.route ?? defaults?.route,
|
|
480
|
+
project: preview?.project ?? defaults?.project,
|
|
481
|
+
owner: preview?.owner ?? defaults?.owner,
|
|
482
|
+
values: {
|
|
483
|
+
...defaults?.values ?? {},
|
|
484
|
+
...preview?.values ?? {}
|
|
485
|
+
},
|
|
486
|
+
path: preview?.path,
|
|
487
|
+
protocol: preview?.protocol
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function getDisplayValues(input2) {
|
|
491
|
+
return {
|
|
492
|
+
...input2.route ? { route: input2.route } : {},
|
|
493
|
+
...input2.project ? { project: input2.project } : {},
|
|
494
|
+
...input2.owner ? { owner: input2.owner } : {},
|
|
495
|
+
...input2.values
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function createDisplayUrl(config, defaults, domain) {
|
|
499
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
500
|
+
const protocol = input2.protocol ?? "https";
|
|
501
|
+
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input2));
|
|
502
|
+
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input2.domain ? getGhostTunnelEntryHost(input2.domain, config) : `${config.subdomain}.*`;
|
|
503
|
+
const url = `${protocol}://${slug}.${entryHost}/`;
|
|
504
|
+
if (!input2.path) return url;
|
|
505
|
+
return `${url}${input2.path.replace(/^\/+/, "")}`;
|
|
506
|
+
}
|
|
507
|
+
function createDisplayUrls(config, defaults) {
|
|
508
|
+
const domains = config.domains.length > 0 ? config.domains : defaults?.domain ? [defaults.domain] : [];
|
|
509
|
+
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, defaults, domain)) : [createDisplayUrl(config, defaults)];
|
|
510
|
+
return [...new Set(urls)];
|
|
511
|
+
}
|
|
512
|
+
function maybeConstructPreviewUrl(config, defaults) {
|
|
513
|
+
if (!config.preview) return void 0;
|
|
514
|
+
const input2 = getPreviewDefaults(config.preview, defaults);
|
|
515
|
+
if (!input2.domain || !input2.route || !input2.project || !input2.owner) return void 0;
|
|
516
|
+
return constructGhostTunnelUrl({
|
|
517
|
+
domain: input2.domain,
|
|
518
|
+
route: input2.route,
|
|
519
|
+
project: input2.project,
|
|
520
|
+
owner: input2.owner,
|
|
521
|
+
values: input2.values,
|
|
522
|
+
...input2.path ? { path: input2.path } : {},
|
|
523
|
+
...input2.protocol ? { protocol: input2.protocol } : {},
|
|
524
|
+
ghostTunnel: config
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function resolveGhostTunnelConfig(options, defaults) {
|
|
528
|
+
if (options === false || typeof options === "undefined") {
|
|
529
|
+
return {
|
|
530
|
+
enabled: false,
|
|
531
|
+
mode: DEFAULT_GHOST_TUNNEL_MODE,
|
|
532
|
+
domains: [],
|
|
533
|
+
subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
|
|
534
|
+
namespace: resolveNamespaceConfig(void 0),
|
|
535
|
+
displayUrls: [],
|
|
536
|
+
requireHttps: true,
|
|
537
|
+
requireAuth: true
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const config = typeof options === "string" ? { mode: options } : options;
|
|
541
|
+
const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
|
|
542
|
+
assertValidSubdomain(subdomain);
|
|
543
|
+
const domains = normalizeDomains(config.domains);
|
|
544
|
+
const enabled = config.enabled ?? true;
|
|
545
|
+
const resolved = {
|
|
546
|
+
enabled,
|
|
547
|
+
mode: parseGhostTunnelMode(config.mode),
|
|
548
|
+
domains,
|
|
549
|
+
subdomain,
|
|
550
|
+
namespace: resolveNamespaceConfig(config.namespace),
|
|
551
|
+
...config.preview ? { preview: config.preview } : {},
|
|
552
|
+
displayUrls: [],
|
|
553
|
+
requireHttps: config.requireHttps ?? true,
|
|
554
|
+
requireAuth: config.requireAuth ?? true
|
|
555
|
+
};
|
|
556
|
+
if (!enabled) {
|
|
557
|
+
return resolved;
|
|
558
|
+
}
|
|
559
|
+
const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
|
|
560
|
+
const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
|
|
561
|
+
return {
|
|
562
|
+
...resolved,
|
|
563
|
+
displayUrls,
|
|
564
|
+
...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
|
|
565
|
+
...previewUrl ? { previewUrl } : {}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function getGhostTunnelEntryHost(domain, options = {}) {
|
|
569
|
+
const config = toGhostTunnelConfig(options);
|
|
570
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
571
|
+
if (!normalizedDomain) {
|
|
572
|
+
throw new Error(`Invalid ghost tunnel domain: ${domain}`);
|
|
573
|
+
}
|
|
574
|
+
return `${config.subdomain}.${normalizedDomain}`;
|
|
575
|
+
}
|
|
576
|
+
function constructGhostTunnelHost(input2) {
|
|
577
|
+
const config = toGhostTunnelConfig(input2.ghostTunnel ?? {});
|
|
578
|
+
if (!config.enabled) {
|
|
579
|
+
throw new Error("Ghost tunnel is not enabled.");
|
|
580
|
+
}
|
|
581
|
+
const namespaceValues = {
|
|
582
|
+
route: input2.route,
|
|
583
|
+
project: input2.project,
|
|
584
|
+
owner: input2.owner,
|
|
585
|
+
...input2.values ?? {}
|
|
586
|
+
};
|
|
587
|
+
const slug = createNamespaceSlug(config.namespace, namespaceValues);
|
|
588
|
+
return `${slug}.${getGhostTunnelEntryHost(input2.domain, config)}`;
|
|
589
|
+
}
|
|
590
|
+
function constructGhostTunnelUrl(input2) {
|
|
591
|
+
const protocol = input2.protocol ?? "https";
|
|
592
|
+
const host = constructGhostTunnelHost(input2);
|
|
593
|
+
const url = new URL(`${protocol}://${host}/`);
|
|
594
|
+
if (input2.path) {
|
|
595
|
+
url.pathname = `/${input2.path.replace(/^\/+/, "")}`;
|
|
596
|
+
}
|
|
597
|
+
if (input2.searchParams instanceof URLSearchParams) {
|
|
598
|
+
url.search = input2.searchParams.toString();
|
|
599
|
+
} else if (input2.searchParams) {
|
|
600
|
+
for (const [key, value] of Object.entries(input2.searchParams)) {
|
|
601
|
+
if (typeof value !== "undefined" && value !== null) {
|
|
602
|
+
url.searchParams.set(key, String(value));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return url.toString();
|
|
607
|
+
}
|
|
608
|
+
|
|
310
609
|
// src/context.ts
|
|
610
|
+
var LOCALGHOST_PROJECT_CONFIG_FILES = [
|
|
611
|
+
"localghost.config.mjs",
|
|
612
|
+
"localghost.config.js",
|
|
613
|
+
"localghost.config.cjs"
|
|
614
|
+
];
|
|
311
615
|
function parsePort(value) {
|
|
312
616
|
if (!value) return void 0;
|
|
313
617
|
const port = Number.parseInt(value, 10);
|
|
@@ -321,6 +625,30 @@ function envDynamicPort() {
|
|
|
321
625
|
if (!value) return void 0;
|
|
322
626
|
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
323
627
|
}
|
|
628
|
+
function envHttps() {
|
|
629
|
+
const value = process.env.LOCALGHOST_HTTPS;
|
|
630
|
+
if (!value) return void 0;
|
|
631
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
632
|
+
}
|
|
633
|
+
function getPackageName(cwd) {
|
|
634
|
+
try {
|
|
635
|
+
const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf8"));
|
|
636
|
+
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
637
|
+
} catch {
|
|
638
|
+
return void 0;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function getPackageOwner(cwd) {
|
|
642
|
+
const packageName = getPackageName(cwd);
|
|
643
|
+
if (!packageName?.startsWith("@")) return void 0;
|
|
644
|
+
return packageName.slice(1).split("/")[0];
|
|
645
|
+
}
|
|
646
|
+
function getLocalOwner(cwd) {
|
|
647
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
648
|
+
}
|
|
649
|
+
function getRouteName(primaryHost, fallback) {
|
|
650
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
651
|
+
}
|
|
324
652
|
function readOptionsFromContext(options) {
|
|
325
653
|
return {
|
|
326
654
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -338,22 +666,68 @@ function withRuntimePort(entries, requestedPort, port) {
|
|
|
338
666
|
function uniqueHosts(entries) {
|
|
339
667
|
return [...new Set(entries.map((entry) => entry.host))];
|
|
340
668
|
}
|
|
669
|
+
function isAliasableHost(host) {
|
|
670
|
+
return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
|
|
671
|
+
}
|
|
672
|
+
function getDefaultWwwAlias(host) {
|
|
673
|
+
return isAliasableHost(host) ? `www.${host}` : null;
|
|
674
|
+
}
|
|
675
|
+
function addDefaultWwwAliases(entries) {
|
|
676
|
+
const seen = new Set(entries.map((entry) => entry.host));
|
|
677
|
+
const aliases = [];
|
|
678
|
+
for (const entry of entries) {
|
|
679
|
+
const alias = getDefaultWwwAlias(entry.host);
|
|
680
|
+
if (alias && !seen.has(alias)) {
|
|
681
|
+
aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
|
|
682
|
+
seen.add(alias);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return [...entries, ...aliases];
|
|
686
|
+
}
|
|
687
|
+
function defined(input2) {
|
|
688
|
+
return Object.fromEntries(Object.entries(input2).filter(([, value]) => typeof value !== "undefined"));
|
|
689
|
+
}
|
|
690
|
+
async function readLocalghostProjectConfig(options = {}) {
|
|
691
|
+
const cwd = options.cwd ?? process.cwd();
|
|
692
|
+
if (options.configFile === false) return { config: {} };
|
|
693
|
+
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
694
|
+
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
|
|
695
|
+
if (!path) return { config: {} };
|
|
696
|
+
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
697
|
+
const config = imported.default ?? imported;
|
|
698
|
+
return { config, path };
|
|
699
|
+
}
|
|
341
700
|
async function resolveLocalghostContext(options = {}) {
|
|
342
701
|
const cwd = options.cwd ?? process.cwd();
|
|
343
|
-
const
|
|
702
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
703
|
+
cwd,
|
|
704
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
705
|
+
});
|
|
706
|
+
const merged = {
|
|
707
|
+
...projectConfig.config,
|
|
708
|
+
...defined(options)
|
|
709
|
+
};
|
|
710
|
+
const readOptions = readOptionsFromContext({ ...merged, cwd });
|
|
344
711
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
345
712
|
const configEntries = readDevHosts(readOptions);
|
|
346
|
-
const requestedPort =
|
|
347
|
-
const dynamicPort =
|
|
348
|
-
const bindHost =
|
|
713
|
+
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
714
|
+
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
715
|
+
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
349
716
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
350
717
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
351
|
-
const
|
|
718
|
+
const wwwAlias = merged.wwwAlias ?? true;
|
|
719
|
+
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
352
720
|
const hosts = uniqueHosts(entries);
|
|
353
|
-
const primaryHost =
|
|
721
|
+
const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
722
|
+
const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
|
|
723
|
+
const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
|
|
724
|
+
route: getRouteName(primaryHost, projectName),
|
|
725
|
+
project: projectName,
|
|
726
|
+
owner: getLocalOwner(cwd)
|
|
727
|
+
});
|
|
354
728
|
return {
|
|
355
729
|
cwd,
|
|
356
|
-
projectName
|
|
730
|
+
projectName,
|
|
357
731
|
readOptions,
|
|
358
732
|
configPath: resolvedPath.path,
|
|
359
733
|
configFileName: resolvedPath.fileName,
|
|
@@ -365,7 +739,10 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
365
739
|
dynamicPort,
|
|
366
740
|
bindHost,
|
|
367
741
|
primaryHost,
|
|
368
|
-
https:
|
|
742
|
+
https: merged.https ?? envHttps() ?? false,
|
|
743
|
+
wwwAlias,
|
|
744
|
+
ghostTunnel,
|
|
745
|
+
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
369
746
|
};
|
|
370
747
|
}
|
|
371
748
|
|
|
@@ -415,7 +792,7 @@ function assertLocalDevelopment(command, env = process.env) {
|
|
|
415
792
|
// src/hosts-file.ts
|
|
416
793
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
417
794
|
import { tmpdir } from "os";
|
|
418
|
-
import { join as
|
|
795
|
+
import { join as join5 } from "path";
|
|
419
796
|
import { execa as execa3 } from "execa";
|
|
420
797
|
function escapeRegExp(value) {
|
|
421
798
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -457,7 +834,7 @@ function removeManagedBlock(existing, projectName) {
|
|
|
457
834
|
}
|
|
458
835
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
459
836
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
460
|
-
const tempPath =
|
|
837
|
+
const tempPath = join5(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
461
838
|
writeFileSync3(tempPath, next, "utf8");
|
|
462
839
|
if (process.platform === "win32") {
|
|
463
840
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
@@ -490,11 +867,11 @@ async function removeSystemHosts(projectName) {
|
|
|
490
867
|
}
|
|
491
868
|
|
|
492
869
|
// src/init.ts
|
|
493
|
-
import { existsSync as
|
|
494
|
-
import { join as
|
|
870
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
871
|
+
import { join as join6 } from "path";
|
|
495
872
|
function detectPackageManager(cwd = process.cwd()) {
|
|
496
|
-
if (
|
|
497
|
-
if (
|
|
873
|
+
if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
874
|
+
if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
|
|
498
875
|
return "npm";
|
|
499
876
|
}
|
|
500
877
|
function packageRunCommand(packageManager, script) {
|
|
@@ -514,7 +891,7 @@ function renderConfig(options) {
|
|
|
514
891
|
}
|
|
515
892
|
function readPackageJson(path) {
|
|
516
893
|
try {
|
|
517
|
-
return JSON.parse(
|
|
894
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
518
895
|
} catch {
|
|
519
896
|
return null;
|
|
520
897
|
}
|
|
@@ -538,6 +915,7 @@ function updatePackageScripts(packageJsonPath, configFile) {
|
|
|
538
915
|
"localghost:proxy:https": scripts["localghost:proxy:https"] ?? `localghost dev${configFlag} --https`,
|
|
539
916
|
"localghost:run": scripts["localghost:run"] ?? `localghost run${configFlag} --`,
|
|
540
917
|
"localghost:ready": scripts["localghost:ready"] ?? `localghost status${configFlag} --ready`,
|
|
918
|
+
"localghost:trust": scripts["localghost:trust"] ?? `localghost trust${configFlag}`,
|
|
541
919
|
"localghost:ps": scripts["localghost:ps"] ?? "localghost ps",
|
|
542
920
|
"localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
|
|
543
921
|
"localghost:routes": scripts["localghost:routes"] ?? `localghost routes${configFlag}`,
|
|
@@ -565,8 +943,8 @@ function initLocalghost(options = {}) {
|
|
|
565
943
|
const apiPort = options.apiPort ?? 8787;
|
|
566
944
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
567
945
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
568
|
-
const configPath =
|
|
569
|
-
const configExists =
|
|
946
|
+
const configPath = join6(cwd, configFile);
|
|
947
|
+
const configExists = existsSync4(configPath);
|
|
570
948
|
if (configExists && !options.force) {
|
|
571
949
|
return {
|
|
572
950
|
configPath,
|
|
@@ -582,12 +960,12 @@ function initLocalghost(options = {}) {
|
|
|
582
960
|
};
|
|
583
961
|
}
|
|
584
962
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
585
|
-
const packageJsonPath =
|
|
963
|
+
const packageJsonPath = join6(cwd, "package.json");
|
|
586
964
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
587
965
|
return {
|
|
588
966
|
configPath,
|
|
589
967
|
configCreated: true,
|
|
590
|
-
...
|
|
968
|
+
...existsSync4(packageJsonPath) ? { packageJsonPath } : {},
|
|
591
969
|
packageJsonChanged,
|
|
592
970
|
packageManager,
|
|
593
971
|
nextSteps: [
|
|
@@ -623,6 +1001,20 @@ async function confirm(question, defaultValue = true) {
|
|
|
623
1001
|
}
|
|
624
1002
|
|
|
625
1003
|
// src/routes.ts
|
|
1004
|
+
var ansi = {
|
|
1005
|
+
cyan: "\x1B[36m",
|
|
1006
|
+
dim: "\x1B[2m",
|
|
1007
|
+
green: "\x1B[32m",
|
|
1008
|
+
reset: "\x1B[0m",
|
|
1009
|
+
yellow: "\x1B[33m"
|
|
1010
|
+
};
|
|
1011
|
+
function colorize(value, color, enabled) {
|
|
1012
|
+
return enabled ? `${color}${value}${ansi.reset}` : value;
|
|
1013
|
+
}
|
|
1014
|
+
function colorizeUrl(value, enabled) {
|
|
1015
|
+
if (!enabled) return value;
|
|
1016
|
+
return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
|
|
1017
|
+
}
|
|
626
1018
|
function getDomainRoutes(entries, options = {}) {
|
|
627
1019
|
const protocol = options.https === true ? "https" : "http";
|
|
628
1020
|
return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
|
|
@@ -642,32 +1034,64 @@ function formatDomainRoutes(entries, options = {}) {
|
|
|
642
1034
|
...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
|
|
643
1035
|
].join("\n");
|
|
644
1036
|
}
|
|
1037
|
+
function formatGhostTunnel(config, options = {}) {
|
|
1038
|
+
if (!config.enabled) return null;
|
|
1039
|
+
const color = options.color === true;
|
|
1040
|
+
const label = options.label ?? "expected";
|
|
1041
|
+
const labelColor = label === "running" ? ansi.green : ansi.dim;
|
|
1042
|
+
const lines = [
|
|
1043
|
+
"localghost ghost tunnel",
|
|
1044
|
+
` mode: ${config.mode}`
|
|
1045
|
+
];
|
|
1046
|
+
const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
|
|
1047
|
+
if (urls.length === 0) {
|
|
1048
|
+
lines.push(` ${label}: unavailable`);
|
|
1049
|
+
} else if (urls.length === 1) {
|
|
1050
|
+
lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
|
|
1051
|
+
} else {
|
|
1052
|
+
lines.push(` ${colorize(label, labelColor, color)}:`);
|
|
1053
|
+
for (const url of urls) {
|
|
1054
|
+
lines.push(` ${colorizeUrl(url, color)}`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (options.verbose) {
|
|
1058
|
+
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
1059
|
+
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
1060
|
+
lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
1061
|
+
}
|
|
1062
|
+
return lines.join("\n");
|
|
1063
|
+
}
|
|
645
1064
|
|
|
646
1065
|
// src/state.ts
|
|
647
|
-
import { existsSync as
|
|
648
|
-
import { join as
|
|
1066
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1067
|
+
import { join as join7 } from "path";
|
|
649
1068
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
650
1069
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
651
|
-
return
|
|
1070
|
+
return join7(cwd, LOCALGHOST_STATE_FILE);
|
|
652
1071
|
}
|
|
653
1072
|
function readLocalghostState(cwd = process.cwd()) {
|
|
654
1073
|
const path = getLocalghostStatePath(cwd);
|
|
655
|
-
if (!
|
|
1074
|
+
if (!existsSync5(path)) return null;
|
|
656
1075
|
return JSON.parse(readTextFile(path));
|
|
657
1076
|
}
|
|
658
1077
|
function writeLocalghostState(cwd, state) {
|
|
659
1078
|
const path = getLocalghostStatePath(cwd);
|
|
660
|
-
writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1079
|
+
writeTextFile(path, `${JSON.stringify({ ...state, version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
661
1080
|
`);
|
|
662
1081
|
return path;
|
|
663
1082
|
}
|
|
1083
|
+
function patchLocalghostState(cwd, patch) {
|
|
1084
|
+
const current = readLocalghostState(cwd);
|
|
1085
|
+
if (!current) return null;
|
|
1086
|
+
return writeLocalghostState(cwd, { ...current, ...patch });
|
|
1087
|
+
}
|
|
664
1088
|
|
|
665
1089
|
// src/update-check.ts
|
|
666
|
-
import { existsSync as
|
|
1090
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
667
1091
|
import { homedir as homedir2 } from "os";
|
|
668
|
-
import { dirname as dirname4, join as
|
|
1092
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
669
1093
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
670
|
-
var LOCALGHOST_VERSION = "0.1.
|
|
1094
|
+
var LOCALGHOST_VERSION = "0.1.9";
|
|
671
1095
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
672
1096
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
673
1097
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -679,13 +1103,13 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
679
1103
|
}
|
|
680
1104
|
function getUpdateCheckCachePath(env = process.env) {
|
|
681
1105
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
682
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
683
|
-
return
|
|
1106
|
+
const cacheRoot = env.XDG_CACHE_HOME || join8(homedir2(), ".cache");
|
|
1107
|
+
return join8(cacheRoot, "localghost", "update-check.json");
|
|
684
1108
|
}
|
|
685
1109
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
686
|
-
if (!
|
|
1110
|
+
if (!existsSync6(path)) return null;
|
|
687
1111
|
try {
|
|
688
|
-
return JSON.parse(
|
|
1112
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
689
1113
|
} catch {
|
|
690
1114
|
return null;
|
|
691
1115
|
}
|
|
@@ -839,8 +1263,18 @@ function warnAboutLocalMdns(entries) {
|
|
|
839
1263
|
);
|
|
840
1264
|
}
|
|
841
1265
|
}
|
|
1266
|
+
function shouldColor() {
|
|
1267
|
+
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1268
|
+
}
|
|
842
1269
|
function logDomainRoutes(entries, options = {}) {
|
|
843
1270
|
console.log(formatDomainRoutes(entries, options));
|
|
1271
|
+
if (options.ghostTunnel?.enabled) {
|
|
1272
|
+
console.log(formatGhostTunnel(options.ghostTunnel, {
|
|
1273
|
+
color: shouldColor(),
|
|
1274
|
+
label: "expected",
|
|
1275
|
+
verbose: options.verbose === true
|
|
1276
|
+
}));
|
|
1277
|
+
}
|
|
844
1278
|
}
|
|
845
1279
|
function parsePort2(value) {
|
|
846
1280
|
const port = Number.parseInt(value, 10);
|
|
@@ -864,6 +1298,15 @@ function parseBooleanLike(value) {
|
|
|
864
1298
|
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
865
1299
|
throw new InvalidArgumentError("Value must be yes or no.");
|
|
866
1300
|
}
|
|
1301
|
+
function contextOptionsFromCli(options) {
|
|
1302
|
+
return {
|
|
1303
|
+
cwd: options.cwd,
|
|
1304
|
+
...options.project ? { project: options.project } : {},
|
|
1305
|
+
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
1306
|
+
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1307
|
+
...useHttps(options) ? { https: true } : {}
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
867
1310
|
function readOptionsFromCli(options) {
|
|
868
1311
|
return {
|
|
869
1312
|
cwd: options.cwd,
|
|
@@ -880,10 +1323,22 @@ async function assertCaddyReady() {
|
|
|
880
1323
|
"Localghost will not install it for you. No surprise spells."
|
|
881
1324
|
].join("\n"));
|
|
882
1325
|
}
|
|
1326
|
+
function existingTrustMarkers(cwd) {
|
|
1327
|
+
const state = readLocalghostState(cwd);
|
|
1328
|
+
return {
|
|
1329
|
+
...state?.caddyTrustedAt ? { caddyTrustedAt: state.caddyTrustedAt } : {},
|
|
1330
|
+
...state?.caddyTrustPromptedAt ? { caddyTrustPromptedAt: state.caddyTrustPromptedAt } : {}
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
883
1333
|
function explainHostsPassword() {
|
|
884
1334
|
console.log("Localghost may ask for your password to update its managed block in /etc/hosts.");
|
|
885
1335
|
console.log("It will only touch the lines between # localghost:start and # localghost:end.");
|
|
886
1336
|
}
|
|
1337
|
+
function explainTrustPassword() {
|
|
1338
|
+
console.log("Localghost can trust Caddy's local HTTPS CA so browsers stop showing local certificate warnings.");
|
|
1339
|
+
console.log("macOS may ask for your password to add that local CA to Keychain.");
|
|
1340
|
+
console.log("This only affects Caddy's local development certificates on this machine.");
|
|
1341
|
+
}
|
|
887
1342
|
function useHttps(options) {
|
|
888
1343
|
return options.https === true || options.ssl === true;
|
|
889
1344
|
}
|
|
@@ -895,10 +1350,10 @@ function getSetupCommand(options) {
|
|
|
895
1350
|
return `localghost setup${configFlags}${options.https ? " --https" : ""}`;
|
|
896
1351
|
}
|
|
897
1352
|
function getSetupReadiness(options) {
|
|
898
|
-
const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
|
|
1353
|
+
const projectName = sanitizeProjectName(options.projectName ?? options.project ?? getProjectName(options.cwd));
|
|
899
1354
|
const readOptions = readOptionsFromCli(options);
|
|
900
|
-
const entries = readDevHosts(readOptions);
|
|
901
|
-
const configPath = resolveDevHostsPath(readOptions).path;
|
|
1355
|
+
const entries = options.entries ?? readDevHosts(readOptions);
|
|
1356
|
+
const configPath = options.configPath ?? resolveDevHostsPath(readOptions).path;
|
|
902
1357
|
const caddyfilePath = getCaddyfilePath(options.cwd);
|
|
903
1358
|
const statePath = getLocalghostStatePath(options.cwd);
|
|
904
1359
|
const state = readLocalghostState(options.cwd);
|
|
@@ -913,7 +1368,7 @@ function getSetupReadiness(options) {
|
|
|
913
1368
|
}
|
|
914
1369
|
const hostsPath = getSystemHostsPath();
|
|
915
1370
|
try {
|
|
916
|
-
const hosts =
|
|
1371
|
+
const hosts = readFileSync7(hostsPath, "utf8");
|
|
917
1372
|
const expectedHostsBlock = renderHostsBlock(projectName, entries).trimEnd();
|
|
918
1373
|
if (!hosts.includes(expectedHostsBlock)) {
|
|
919
1374
|
reasons.push(`The Localghost hosts block in ${hostsPath} is missing or stale.`);
|
|
@@ -923,11 +1378,11 @@ function getSetupReadiness(options) {
|
|
|
923
1378
|
reasons.push(`Could not read ${hostsPath}: ${message}`);
|
|
924
1379
|
}
|
|
925
1380
|
if (!options.ignoreCaddyfile) {
|
|
926
|
-
if (!
|
|
1381
|
+
if (!existsSync7(caddyfilePath)) {
|
|
927
1382
|
reasons.push(`Missing Caddyfile at ${caddyfilePath}.`);
|
|
928
1383
|
} else {
|
|
929
1384
|
const expectedCaddyfile = renderCaddyfile(entries, { https });
|
|
930
|
-
const currentCaddyfile =
|
|
1385
|
+
const currentCaddyfile = readFileSync7(caddyfilePath, "utf8");
|
|
931
1386
|
if (currentCaddyfile !== expectedCaddyfile) {
|
|
932
1387
|
reasons.push(`Caddyfile at ${caddyfilePath} is stale for ${https ? "HTTPS" : "HTTP"} mode.`);
|
|
933
1388
|
}
|
|
@@ -959,8 +1414,49 @@ async function runSetupFromReadiness(cwd, https, readiness) {
|
|
|
959
1414
|
...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
|
|
960
1415
|
caddyfilePath,
|
|
961
1416
|
caddyHttps: https,
|
|
1417
|
+
...existingTrustMarkers(cwd),
|
|
962
1418
|
entries: readiness.entries
|
|
963
1419
|
});
|
|
1420
|
+
registerLocalghostSetup({
|
|
1421
|
+
cwd,
|
|
1422
|
+
projectName: readiness.projectName,
|
|
1423
|
+
configPath: readiness.configPath,
|
|
1424
|
+
caddyfilePath,
|
|
1425
|
+
https,
|
|
1426
|
+
entries: readiness.entries
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
function wait(ms) {
|
|
1430
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1431
|
+
}
|
|
1432
|
+
async function runTrust(cwd, caddyfilePath) {
|
|
1433
|
+
await wait(350);
|
|
1434
|
+
try {
|
|
1435
|
+
await trustCaddy(caddyfilePath);
|
|
1436
|
+
} catch {
|
|
1437
|
+
await wait(750);
|
|
1438
|
+
await trustCaddy(caddyfilePath);
|
|
1439
|
+
}
|
|
1440
|
+
patchLocalghostState(cwd, { caddyTrustedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1441
|
+
console.log("Local HTTPS trust is ready.");
|
|
1442
|
+
}
|
|
1443
|
+
async function maybeTrustCaddy(options) {
|
|
1444
|
+
if (!options.https) return;
|
|
1445
|
+
const state = readLocalghostState(options.cwd);
|
|
1446
|
+
if (!options.trust && state?.caddyTrustedAt) return;
|
|
1447
|
+
let shouldTrust = options.trust === true;
|
|
1448
|
+
if (!shouldTrust) {
|
|
1449
|
+
if (state?.caddyTrustPromptedAt || !canPrompt()) return;
|
|
1450
|
+
explainTrustPassword();
|
|
1451
|
+
shouldTrust = await confirm("Trust local HTTPS certificates now?", true);
|
|
1452
|
+
}
|
|
1453
|
+
if (!shouldTrust) {
|
|
1454
|
+
patchLocalghostState(options.cwd, { caddyTrustPromptedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1455
|
+
console.log("Okay. Localghost will still run HTTPS, but the browser may show a certificate warning.");
|
|
1456
|
+
console.log("Run localghost trust when you want to trust Caddy's local CA.");
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
await runTrust(options.cwd, options.caddyfilePath);
|
|
964
1460
|
}
|
|
965
1461
|
function maybePid(pid) {
|
|
966
1462
|
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
@@ -978,35 +1474,91 @@ function registerCleanup(id) {
|
|
|
978
1474
|
process.off("exit", cleanup);
|
|
979
1475
|
};
|
|
980
1476
|
}
|
|
981
|
-
async function
|
|
1477
|
+
async function getRouteViews(entries) {
|
|
982
1478
|
const portStatus = /* @__PURE__ */ new Map();
|
|
983
|
-
for (const entry of
|
|
1479
|
+
for (const entry of entries) {
|
|
984
1480
|
if (!portStatus.has(entry.port)) {
|
|
985
1481
|
portStatus.set(entry.port, !await isPortAvailable(entry.port));
|
|
986
1482
|
}
|
|
987
1483
|
}
|
|
1484
|
+
return entries.map((entry) => ({
|
|
1485
|
+
host: entry.host,
|
|
1486
|
+
port: entry.port,
|
|
1487
|
+
target: `127.0.0.1:${entry.port}`,
|
|
1488
|
+
listening: portStatus.get(entry.port) ?? false
|
|
1489
|
+
}));
|
|
1490
|
+
}
|
|
1491
|
+
function setupKey(input2) {
|
|
1492
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
1493
|
+
}
|
|
1494
|
+
function runKey(input2) {
|
|
1495
|
+
return `${input2.projectName}:${input2.cwd}:${input2.configPath ?? ""}`;
|
|
1496
|
+
}
|
|
1497
|
+
async function getInstanceViews(setups, runs) {
|
|
1498
|
+
const runBySetup = new Map(runs.map((run) => [runKey(run), run]));
|
|
1499
|
+
const instances = [];
|
|
1500
|
+
for (const setup of setups) {
|
|
1501
|
+
const run = runBySetup.get(setupKey(setup));
|
|
1502
|
+
if (run) {
|
|
1503
|
+
instances.push(await getRunInstanceView(run, setup));
|
|
1504
|
+
runBySetup.delete(setupKey(setup));
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
instances.push({
|
|
1508
|
+
id: setup.id,
|
|
1509
|
+
cwd: setup.cwd,
|
|
1510
|
+
projectName: setup.projectName,
|
|
1511
|
+
running: false,
|
|
1512
|
+
mode: "setup",
|
|
1513
|
+
updatedAt: setup.updatedAt,
|
|
1514
|
+
...setup.configPath ? { configPath: setup.configPath } : {},
|
|
1515
|
+
...setup.caddyfilePath ? { caddyfilePath: setup.caddyfilePath } : {},
|
|
1516
|
+
...typeof setup.https === "boolean" ? { https: setup.https } : {},
|
|
1517
|
+
routes: await getRouteViews(setup.entries)
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
for (const run of runBySetup.values()) {
|
|
1521
|
+
instances.push(await getRunInstanceView(run));
|
|
1522
|
+
}
|
|
1523
|
+
return instances.sort((left, right) => {
|
|
1524
|
+
if (left.running !== right.running) return left.running ? -1 : 1;
|
|
1525
|
+
return left.projectName.localeCompare(right.projectName);
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
async function getRunInstanceView(run, setup) {
|
|
988
1529
|
return {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1530
|
+
id: setup?.id ?? run.id,
|
|
1531
|
+
cwd: run.cwd,
|
|
1532
|
+
projectName: run.projectName,
|
|
1533
|
+
running: true,
|
|
1534
|
+
mode: run.mode,
|
|
1535
|
+
updatedAt: setup?.updatedAt ?? run.updatedAt,
|
|
1536
|
+
startedAt: run.startedAt,
|
|
1537
|
+
pid: run.pid,
|
|
1538
|
+
...run.caddyPid ? { caddyPid: run.caddyPid } : {},
|
|
1539
|
+
...run.childPid ? { childPid: run.childPid } : {},
|
|
1540
|
+
...run.childCommand ? { childCommand: run.childCommand } : {},
|
|
1541
|
+
...run.configPath ? { configPath: run.configPath } : {},
|
|
1542
|
+
...run.caddyfilePath ? { caddyfilePath: run.caddyfilePath } : {},
|
|
1543
|
+
...typeof run.https === "boolean" ? { https: run.https } : {},
|
|
1544
|
+
routes: await getRouteViews(run.entries)
|
|
996
1545
|
};
|
|
997
1546
|
}
|
|
998
|
-
function
|
|
999
|
-
if (
|
|
1547
|
+
function formatInstanceViews(instances) {
|
|
1548
|
+
if (instances.length === 0) return "No Localghost setups found.";
|
|
1000
1549
|
const lines = ["localghost ps"];
|
|
1001
|
-
for (const
|
|
1002
|
-
const command =
|
|
1003
|
-
const mode = command ? `${
|
|
1550
|
+
for (const instance of instances) {
|
|
1551
|
+
const command = instance.childCommand?.length ? ` ${instance.childCommand.join(" ")}` : "";
|
|
1552
|
+
const mode = command ? `${instance.mode}:${command}` : instance.mode === "setup" ? "" : instance.mode;
|
|
1004
1553
|
lines.push("");
|
|
1005
|
-
lines.push(`${
|
|
1006
|
-
lines.push(` cwd: ${
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1554
|
+
lines.push(`${instance.projectName} ${instance.running ? "running" : "setup"}${mode ? ` ${mode}` : ""}`);
|
|
1555
|
+
lines.push(` cwd: ${instance.cwd}`);
|
|
1556
|
+
if (instance.pid) {
|
|
1557
|
+
lines.push(` pid: ${instance.pid}${instance.caddyPid ? `, caddy: ${instance.caddyPid}` : ""}${instance.childPid ? `, child: ${instance.childPid}` : ""}`);
|
|
1558
|
+
}
|
|
1559
|
+
if (instance.startedAt) lines.push(` started: ${instance.startedAt}`);
|
|
1560
|
+
if (!instance.startedAt && instance.updatedAt) lines.push(` setup: ${instance.updatedAt}`);
|
|
1561
|
+
for (const route of instance.routes) {
|
|
1010
1562
|
lines.push(` ${route.host} -> ${route.target} (${route.listening ? "listening" : "not listening"})`);
|
|
1011
1563
|
}
|
|
1012
1564
|
}
|
|
@@ -1074,13 +1626,13 @@ program.command("update").description("Check npm for a newer localghost release"
|
|
|
1074
1626
|
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) => {
|
|
1075
1627
|
assertLocalDevelopment("setup");
|
|
1076
1628
|
await assertCaddyReady();
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1079
|
-
const
|
|
1080
|
-
const configPath =
|
|
1081
|
-
const entries =
|
|
1629
|
+
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1630
|
+
const https = context.https;
|
|
1631
|
+
const projectName = context.projectName;
|
|
1632
|
+
const configPath = context.configPath;
|
|
1633
|
+
const entries = context.entries;
|
|
1082
1634
|
warnAboutLocalMdns(entries);
|
|
1083
|
-
logDomainRoutes(entries, { https });
|
|
1635
|
+
logDomainRoutes(entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1084
1636
|
explainHostsPassword();
|
|
1085
1637
|
const hostsResult = await updateSystemHosts(projectName, entries);
|
|
1086
1638
|
if (hostsResult.changed) {
|
|
@@ -1100,6 +1652,15 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
|
|
|
1100
1652
|
...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
|
|
1101
1653
|
caddyfilePath: caddyfile,
|
|
1102
1654
|
caddyHttps: https,
|
|
1655
|
+
...existingTrustMarkers(options.cwd),
|
|
1656
|
+
entries
|
|
1657
|
+
});
|
|
1658
|
+
registerLocalghostSetup({
|
|
1659
|
+
cwd: options.cwd,
|
|
1660
|
+
projectName,
|
|
1661
|
+
configPath,
|
|
1662
|
+
caddyfilePath: caddyfile,
|
|
1663
|
+
https,
|
|
1103
1664
|
entries
|
|
1104
1665
|
});
|
|
1105
1666
|
console.log(`Generated ${caddyfile}`);
|
|
@@ -1107,6 +1668,20 @@ program.command("setup").description("Update /etc/hosts and generate/validate Ca
|
|
|
1107
1668
|
console.log(`State ${statePath}`);
|
|
1108
1669
|
console.log("Setup complete.");
|
|
1109
1670
|
});
|
|
1671
|
+
program.command("trust").description("Trust Caddy's local HTTPS CA for this project's HTTPS proxy").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", "Use HTTPS mode for the Caddyfile").option("--ssl", "Alias for --https").action(async (options) => {
|
|
1672
|
+
assertLocalDevelopment("trust");
|
|
1673
|
+
await assertCaddyReady();
|
|
1674
|
+
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1675
|
+
if (!context.https) {
|
|
1676
|
+
throw new Error("Localghost HTTPS is not enabled for this context. Set https: true in localghost.config.mjs or pass --https.");
|
|
1677
|
+
}
|
|
1678
|
+
warnAboutLocalMdns(context.entries);
|
|
1679
|
+
logDomainRoutes(context.entries, { https: true, ghostTunnel: context.ghostTunnel });
|
|
1680
|
+
explainTrustPassword();
|
|
1681
|
+
const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https: true });
|
|
1682
|
+
await validateCaddyfile(caddyfile);
|
|
1683
|
+
await runTrust(options.cwd, caddyfile);
|
|
1684
|
+
});
|
|
1110
1685
|
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) => {
|
|
1111
1686
|
assertLocalDevelopment("reset");
|
|
1112
1687
|
const projectName = sanitizeProjectName(options.project ?? getProjectName(options.cwd));
|
|
@@ -1114,13 +1689,13 @@ program.command("reset").description("Remove Localghost setup state without dele
|
|
|
1114
1689
|
const statePath = getLocalghostStatePath(options.cwd);
|
|
1115
1690
|
explainHostsPassword();
|
|
1116
1691
|
const hostsResult = await removeSystemHosts(projectName);
|
|
1117
|
-
if (
|
|
1692
|
+
if (existsSync7(caddyfilePath)) {
|
|
1118
1693
|
unlinkSync(caddyfilePath);
|
|
1119
1694
|
console.log(`Removed ${caddyfilePath}`);
|
|
1120
1695
|
} else {
|
|
1121
1696
|
console.log(`${caddyfilePath} was not present`);
|
|
1122
1697
|
}
|
|
1123
|
-
if (
|
|
1698
|
+
if (existsSync7(statePath)) {
|
|
1124
1699
|
unlinkSync(statePath);
|
|
1125
1700
|
console.log(`Removed ${statePath}`);
|
|
1126
1701
|
} else {
|
|
@@ -1131,6 +1706,7 @@ program.command("reset").description("Remove Localghost setup state without dele
|
|
|
1131
1706
|
} else {
|
|
1132
1707
|
console.log(`No Localghost hosts block found in ${hostsResult.hostsPath}`);
|
|
1133
1708
|
}
|
|
1709
|
+
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
1134
1710
|
console.log(".localghost was left in place. Run localghost setup when you are ready.");
|
|
1135
1711
|
});
|
|
1136
1712
|
program.command("teardown").description("Remove Localghost's managed /etc/hosts block").option("--project <name>", "Managed /etc/hosts block name").option("--cwd <path>", "Project directory", process.cwd()).option("--remove-caddyfile", "Also remove ops/local/Caddyfile").action(async (options) => {
|
|
@@ -1140,7 +1716,7 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
1140
1716
|
const hostsResult = await removeSystemHosts(projectName);
|
|
1141
1717
|
const caddyfilePath = getCaddyfilePath(options.cwd);
|
|
1142
1718
|
let caddyfileRemoved = false;
|
|
1143
|
-
if (options.removeCaddyfile &&
|
|
1719
|
+
if (options.removeCaddyfile && existsSync7(caddyfilePath)) {
|
|
1144
1720
|
unlinkSync(caddyfilePath);
|
|
1145
1721
|
caddyfileRemoved = true;
|
|
1146
1722
|
}
|
|
@@ -1162,12 +1738,20 @@ program.command("teardown").description("Remove Localghost's managed /etc/hosts
|
|
|
1162
1738
|
if (options.removeCaddyfile) {
|
|
1163
1739
|
console.log(caddyfileRemoved ? `Removed ${caddyfilePath}` : `${caddyfilePath} was not present`);
|
|
1164
1740
|
}
|
|
1741
|
+
unregisterLocalghostSetup({ cwd: options.cwd, projectName });
|
|
1165
1742
|
console.log(`State ${statePath}`);
|
|
1166
1743
|
});
|
|
1167
|
-
program.command("status").description("Print Localghost's project-local state file").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("--ready", "Exit non-zero when setup is missing or stale").option("--https", "Check setup readiness for HTTPS mode").option("--ssl", "Alias for --https").option("--json", "Print raw JSON").action((options) => {
|
|
1744
|
+
program.command("status").description("Print Localghost's project-local state file").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("--ready", "Exit non-zero when setup is missing or stale").option("--https", "Check setup readiness for HTTPS mode").option("--ssl", "Alias for --https").option("--json", "Print raw JSON").action(async (options) => {
|
|
1168
1745
|
const state = readLocalghostState(options.cwd);
|
|
1169
1746
|
const statePath = getLocalghostStatePath(options.cwd);
|
|
1170
|
-
const
|
|
1747
|
+
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1748
|
+
const readiness = getSetupReadiness({
|
|
1749
|
+
...options,
|
|
1750
|
+
https: context.https,
|
|
1751
|
+
entries: context.entries,
|
|
1752
|
+
configPath: context.configPath,
|
|
1753
|
+
projectName: context.projectName
|
|
1754
|
+
});
|
|
1171
1755
|
if (options.json) {
|
|
1172
1756
|
console.log(JSON.stringify({ state, setup: readiness }, null, 2));
|
|
1173
1757
|
return;
|
|
@@ -1183,6 +1767,8 @@ program.command("status").description("Print Localghost's project-local state fi
|
|
|
1183
1767
|
if (state.hostsPath) console.log(`Hosts: ${state.hostsPath}`);
|
|
1184
1768
|
if (state.caddyfilePath) console.log(`Caddyfile: ${state.caddyfilePath}`);
|
|
1185
1769
|
if (typeof state.caddyHttps === "boolean") console.log(`Mode: ${state.caddyHttps ? "HTTPS" : "HTTP"}`);
|
|
1770
|
+
if (state.caddyTrustedAt) console.log(`HTTPS trust: yes (${state.caddyTrustedAt})`);
|
|
1771
|
+
if (!state.caddyTrustedAt && state.caddyTrustPromptedAt) console.log(`HTTPS trust: not enabled (asked ${state.caddyTrustPromptedAt})`);
|
|
1186
1772
|
if (typeof state.caddyfileRemoved === "boolean") console.log(`Caddyfile removed: ${state.caddyfileRemoved}`);
|
|
1187
1773
|
}
|
|
1188
1774
|
if (readiness.ready) {
|
|
@@ -1198,16 +1784,30 @@ program.command("status").description("Print Localghost's project-local state fi
|
|
|
1198
1784
|
process.exitCode = 1;
|
|
1199
1785
|
}
|
|
1200
1786
|
});
|
|
1201
|
-
program.command("routes").description("Print domain to upstream routes").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("--http", "Print domain URLs with http instead of https").option("--https", "Print domain URLs with https").option("--ssl", "Alias for --https").action((options) => {
|
|
1202
|
-
const
|
|
1203
|
-
warnAboutLocalMdns(entries);
|
|
1204
|
-
console.log(formatDomainRoutes(entries, { https: options.http ? false :
|
|
1787
|
+
program.command("routes").description("Print domain to upstream routes").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("--http", "Print domain URLs with http instead of https").option("--https", "Print domain URLs with https").option("--ssl", "Alias for --https").option("--verbose", "Print Ghost Tunnel mode, domains, and guardrails").action(async (options) => {
|
|
1788
|
+
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1789
|
+
warnAboutLocalMdns(context.entries);
|
|
1790
|
+
console.log(formatDomainRoutes(context.entries, { https: options.http ? false : context.https }));
|
|
1791
|
+
if (context.ghostTunnel.enabled) {
|
|
1792
|
+
console.log(formatGhostTunnel(context.ghostTunnel, {
|
|
1793
|
+
color: shouldColor(),
|
|
1794
|
+
label: "expected",
|
|
1795
|
+
verbose: options.verbose === true
|
|
1796
|
+
}));
|
|
1797
|
+
}
|
|
1205
1798
|
});
|
|
1206
|
-
program.command("dev").description("Run the Localghost Caddy proxy after setup").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", "Run setup before starting the proxy when setup is missing or stale").action(async (options) => {
|
|
1799
|
+
program.command("dev").description("Run the Localghost Caddy proxy after setup").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", "Run setup before starting the proxy when setup is missing or stale").option("--trust", "Trust Caddy's local HTTPS CA before starting the proxy").action(async (options) => {
|
|
1207
1800
|
assertLocalDevelopment("dev");
|
|
1208
1801
|
await assertCaddyReady();
|
|
1209
|
-
const
|
|
1210
|
-
const
|
|
1802
|
+
const context = await resolveLocalghostContext({ ...contextOptionsFromCli(options), dynamicPort: false });
|
|
1803
|
+
const https = context.https;
|
|
1804
|
+
const readiness = getSetupReadiness({
|
|
1805
|
+
...options,
|
|
1806
|
+
https,
|
|
1807
|
+
entries: context.entries,
|
|
1808
|
+
configPath: context.configPath,
|
|
1809
|
+
projectName: context.projectName
|
|
1810
|
+
});
|
|
1211
1811
|
if (!readiness.ready) {
|
|
1212
1812
|
if (!options.setup) {
|
|
1213
1813
|
throw new Error(
|
|
@@ -1233,14 +1833,34 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1233
1833
|
...hostsResult.tempPath ? { hostsTempPath: hostsResult.tempPath } : {},
|
|
1234
1834
|
caddyfilePath,
|
|
1235
1835
|
caddyHttps: https,
|
|
1836
|
+
...existingTrustMarkers(options.cwd),
|
|
1837
|
+
entries: readiness.entries
|
|
1838
|
+
});
|
|
1839
|
+
registerLocalghostSetup({
|
|
1840
|
+
cwd: options.cwd,
|
|
1841
|
+
projectName: readiness.projectName,
|
|
1842
|
+
configPath: readiness.configPath,
|
|
1843
|
+
caddyfilePath,
|
|
1844
|
+
https,
|
|
1236
1845
|
entries: readiness.entries
|
|
1237
1846
|
});
|
|
1238
1847
|
}
|
|
1239
1848
|
warnAboutLocalMdns(readiness.entries);
|
|
1240
|
-
logDomainRoutes(readiness.entries, { https });
|
|
1849
|
+
logDomainRoutes(readiness.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1241
1850
|
const caddyfile = await writeCaddyfile(readiness.entries, options.cwd, { https });
|
|
1242
1851
|
await validateCaddyfile(caddyfile);
|
|
1243
1852
|
const caddy = startCaddy(caddyfile);
|
|
1853
|
+
try {
|
|
1854
|
+
await maybeTrustCaddy({
|
|
1855
|
+
cwd: options.cwd,
|
|
1856
|
+
https,
|
|
1857
|
+
caddyfilePath: caddyfile,
|
|
1858
|
+
...typeof options.trust === "boolean" ? { trust: options.trust } : {}
|
|
1859
|
+
});
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
if (!caddy.killed) caddy.kill("SIGINT");
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1244
1864
|
const caddyPid = maybePid(caddy.pid);
|
|
1245
1865
|
const runRecord = registerLocalghostRun({
|
|
1246
1866
|
mode: "dev",
|
|
@@ -1259,20 +1879,27 @@ program.command("dev").description("Run the Localghost Caddy proxy after setup")
|
|
|
1259
1879
|
cleanupRun();
|
|
1260
1880
|
}
|
|
1261
1881
|
});
|
|
1262
|
-
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", "Run setup before starting when setup is missing or stale").option("--dynamic-port [yes|no]", "Use the requested port if free, otherwise continue upward", parseBooleanLike
|
|
1882
|
+
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", "Run setup before starting when setup is missing or stale").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) => {
|
|
1263
1883
|
assertLocalDevelopment("run");
|
|
1264
1884
|
await assertCaddyReady();
|
|
1265
|
-
const https = useHttps(options);
|
|
1266
1885
|
const context = await resolveLocalghostContext({
|
|
1267
1886
|
cwd: options.cwd,
|
|
1268
1887
|
...options.project ? { project: options.project } : {},
|
|
1269
1888
|
...options.config && options.config.length > 0 ? { configFiles: options.config } : {},
|
|
1270
1889
|
...options.configPattern ? { configPattern: options.configPattern } : {},
|
|
1271
1890
|
...options.port ? { port: options.port } : {},
|
|
1272
|
-
https,
|
|
1891
|
+
...useHttps(options) ? { https: true } : {},
|
|
1273
1892
|
...typeof options.dynamicPort === "boolean" ? { dynamicPort: options.dynamicPort } : {}
|
|
1274
1893
|
});
|
|
1275
|
-
const
|
|
1894
|
+
const https = context.https;
|
|
1895
|
+
const readiness = getSetupReadiness({
|
|
1896
|
+
...options,
|
|
1897
|
+
https,
|
|
1898
|
+
ignoreCaddyfile: true,
|
|
1899
|
+
entries: context.entries,
|
|
1900
|
+
configPath: context.configPath,
|
|
1901
|
+
projectName: context.projectName
|
|
1902
|
+
});
|
|
1276
1903
|
if (!readiness.ready) {
|
|
1277
1904
|
const shouldSetup = options.setup === true || canPrompt() && await confirm("Run caddy:setup now?", true);
|
|
1278
1905
|
if (!shouldSetup) {
|
|
@@ -1291,13 +1918,24 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1291
1918
|
console.log(`Port ${context.requestedPort} is busy; using ${context.port}.`);
|
|
1292
1919
|
}
|
|
1293
1920
|
warnAboutLocalMdns(context.entries);
|
|
1294
|
-
logDomainRoutes(context.entries, { https });
|
|
1921
|
+
logDomainRoutes(context.entries, { https, ghostTunnel: context.ghostTunnel });
|
|
1295
1922
|
const caddyfile = await writeCaddyfile(context.entries, options.cwd, { https });
|
|
1296
1923
|
await validateCaddyfile(caddyfile);
|
|
1297
1924
|
const caddy = startCaddy(caddyfile);
|
|
1298
1925
|
const caddyExit = caddy.catch((error) => {
|
|
1299
1926
|
if (!caddy.killed) throw error;
|
|
1300
1927
|
});
|
|
1928
|
+
try {
|
|
1929
|
+
await maybeTrustCaddy({
|
|
1930
|
+
cwd: options.cwd,
|
|
1931
|
+
https,
|
|
1932
|
+
caddyfilePath: caddyfile,
|
|
1933
|
+
...typeof options.trust === "boolean" ? { trust: options.trust } : {}
|
|
1934
|
+
});
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
if (!caddy.killed) caddy.kill("SIGINT");
|
|
1937
|
+
throw error;
|
|
1938
|
+
}
|
|
1301
1939
|
const [binary, ...args] = command;
|
|
1302
1940
|
if (!binary) {
|
|
1303
1941
|
throw new Error("Missing command. Use: localghost run -- vite");
|
|
@@ -1345,13 +1983,15 @@ program.command("run").description("Run Caddy and a dev command from the same Lo
|
|
|
1345
1983
|
cleanupRun();
|
|
1346
1984
|
}
|
|
1347
1985
|
});
|
|
1348
|
-
program.command("ps").description("Show Localghost
|
|
1349
|
-
const
|
|
1986
|
+
program.command("ps").description("Show Localghost setups and currently running sessions").option("--json", "Print raw JSON").action(async (options) => {
|
|
1987
|
+
const setups = listLocalghostSetups();
|
|
1988
|
+
const runs = listLocalghostRuns();
|
|
1989
|
+
const instances = await getInstanceViews(setups, runs);
|
|
1350
1990
|
if (options.json) {
|
|
1351
|
-
console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), runs }, null, 2));
|
|
1991
|
+
console.log(JSON.stringify({ activityPath: getLocalghostActivityPath(), setups, runs, instances }, null, 2));
|
|
1352
1992
|
return;
|
|
1353
1993
|
}
|
|
1354
|
-
console.log(
|
|
1994
|
+
console.log(formatInstanceViews(instances));
|
|
1355
1995
|
});
|
|
1356
1996
|
program.command("print").description("Print parsed host config").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").action((options) => {
|
|
1357
1997
|
const entries = readDevHosts(readOptionsFromCli(options));
|