@iamken/cloudtunnel 0.2.0 → 0.4.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 +76 -75
- package/dist/index.js +465 -517
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,8 +31,8 @@ import { Command } from "commander";
|
|
|
31
31
|
import { createRequire } from "module";
|
|
32
32
|
import pc2 from "picocolors";
|
|
33
33
|
|
|
34
|
-
// src/
|
|
35
|
-
import
|
|
34
|
+
// src/config/legacy-migrate.ts
|
|
35
|
+
import { existsSync as existsSync2, readFileSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
36
36
|
|
|
37
37
|
// src/ui/output.ts
|
|
38
38
|
import pc from "picocolors";
|
|
@@ -78,6 +78,250 @@ async function selectOne(message, items, label) {
|
|
|
78
78
|
return items[Number(value)];
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// src/core/systemd.ts
|
|
82
|
+
import { execFileSync } from "child_process";
|
|
83
|
+
import { existsSync, realpathSync, writeFileSync } from "fs";
|
|
84
|
+
import os, { tmpdir } from "os";
|
|
85
|
+
import { dirname, join } from "path";
|
|
86
|
+
|
|
87
|
+
// src/core/ingress.ts
|
|
88
|
+
var HOSTNAME_RE = /^[a-zA-Z0-9.-]+$/;
|
|
89
|
+
var IPV6_RE = /^[0-9a-fA-F:.]+$/;
|
|
90
|
+
function validateHost(host) {
|
|
91
|
+
let h = host.trim();
|
|
92
|
+
const bracketed = h.startsWith("[") && h.endsWith("]");
|
|
93
|
+
if (bracketed) h = h.slice(1, -1);
|
|
94
|
+
const isV6 = bracketed || h.includes("::") || (h.match(/:/g)?.length ?? 0) >= 2;
|
|
95
|
+
const ok = h.length > 0 && (isV6 ? IPV6_RE.test(h) : HOSTNAME_RE.test(h));
|
|
96
|
+
if (!ok) {
|
|
97
|
+
throw new CliError(`Invalid host "${host}".`, {
|
|
98
|
+
hint: "use a hostname, IPv4, or IPv6 literal (e.g. 192.168.1.5 or ::1) \u2014 no port, scheme, or path"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return h;
|
|
102
|
+
}
|
|
103
|
+
function serviceUrl(proto, host, port) {
|
|
104
|
+
const authority = host.includes(":") ? `[${host}]` : host;
|
|
105
|
+
return `${proto}://${authority}:${port}`;
|
|
106
|
+
}
|
|
107
|
+
function buildIngress(opts) {
|
|
108
|
+
return [
|
|
109
|
+
{ hostname: opts.hostname, service: serviceUrl(opts.proto, opts.host ?? "localhost", opts.port) },
|
|
110
|
+
{ service: "http_status:404" }
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/core/tunnel-spec.ts
|
|
115
|
+
function parseTunnelSpec(spec) {
|
|
116
|
+
const raw = spec.trim();
|
|
117
|
+
const bad = (hint) => new CliError(`Invalid spec "${spec}".`, { hint });
|
|
118
|
+
if (!raw) throw bad("use [subdomain:]port[@host], e.g. api:8080 or api:8080@192.168.1.20");
|
|
119
|
+
let rest = raw;
|
|
120
|
+
let subdomain;
|
|
121
|
+
if (rest.startsWith("@")) {
|
|
122
|
+
subdomain = "@";
|
|
123
|
+
rest = rest.slice(1);
|
|
124
|
+
if (rest.startsWith(":")) rest = rest.slice(1);
|
|
125
|
+
}
|
|
126
|
+
let host;
|
|
127
|
+
const at = rest.indexOf("@");
|
|
128
|
+
if (at >= 0) {
|
|
129
|
+
host = validateHost(rest.slice(at + 1));
|
|
130
|
+
rest = rest.slice(0, at);
|
|
131
|
+
}
|
|
132
|
+
const parts = rest.split(":");
|
|
133
|
+
let portStr;
|
|
134
|
+
if (parts.length === 1) {
|
|
135
|
+
portStr = parts[0];
|
|
136
|
+
} else if (parts.length === 2) {
|
|
137
|
+
if (subdomain === void 0) {
|
|
138
|
+
if (!parts[0]) throw bad("subdomain label is empty");
|
|
139
|
+
subdomain = parts[0];
|
|
140
|
+
} else if (parts[0]) {
|
|
141
|
+
throw bad("unexpected label after '@' root marker");
|
|
142
|
+
}
|
|
143
|
+
portStr = parts[1];
|
|
144
|
+
} else {
|
|
145
|
+
throw bad("too many ':' \u2014 spec is [subdomain:]port[@host] (protocol via --proto)");
|
|
146
|
+
}
|
|
147
|
+
const port = Number(portStr);
|
|
148
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
149
|
+
throw bad("port must be a number 1\u201365535");
|
|
150
|
+
}
|
|
151
|
+
if (subdomain !== void 0 && subdomain !== "@" && !/^[a-zA-Z0-9-]+$/.test(subdomain)) {
|
|
152
|
+
throw bad("subdomain may contain only letters, digits, and hyphens");
|
|
153
|
+
}
|
|
154
|
+
return { subdomain, port, ...host ? { host } : {} };
|
|
155
|
+
}
|
|
156
|
+
function formatTunnelSpec(s) {
|
|
157
|
+
return `${s.subdomain}:${s.port}${s.host ? `@${s.host}` : ""}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/core/systemd.ts
|
|
161
|
+
function serviceName(fqdn) {
|
|
162
|
+
return `cloudtunnel-${fqdn.replace(/[^a-zA-Z0-9]+/g, "-")}.service`;
|
|
163
|
+
}
|
|
164
|
+
function unitPath(fqdn) {
|
|
165
|
+
return `/etc/systemd/system/${serviceName(fqdn)}`;
|
|
166
|
+
}
|
|
167
|
+
function buildUnit(p) {
|
|
168
|
+
const nodeBin = dirname(p.nodePath);
|
|
169
|
+
const proto = p.proto === "https" ? " --proto https" : "";
|
|
170
|
+
const protocol = p.protocol ? ` --protocol ${p.protocol}` : "";
|
|
171
|
+
return [
|
|
172
|
+
"[Unit]",
|
|
173
|
+
`Description=cloudtunnel ${p.fqdn} (Cloudflare Tunnel)`,
|
|
174
|
+
"After=network-online.target",
|
|
175
|
+
"Wants=network-online.target",
|
|
176
|
+
"",
|
|
177
|
+
"[Service]",
|
|
178
|
+
"Type=simple",
|
|
179
|
+
`User=${p.user}`,
|
|
180
|
+
`Environment=HOME=${p.home}`,
|
|
181
|
+
`Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
182
|
+
`ExecStart=${p.nodePath} ${p.scriptPath} up ${p.spec} -d ${p.zone}${proto}${protocol} -f -y`,
|
|
183
|
+
"Restart=on-failure",
|
|
184
|
+
"RestartSec=5",
|
|
185
|
+
"",
|
|
186
|
+
"[Install]",
|
|
187
|
+
"WantedBy=multi-user.target",
|
|
188
|
+
""
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
function assertSystemd() {
|
|
192
|
+
if (process.platform !== "linux") {
|
|
193
|
+
throw new CliError("Service registration is Linux/systemd only.", {
|
|
194
|
+
hint: "on macOS/Windows run `cloudtunnel up <spec> --detach` at login instead"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
execFileSync("systemctl", ["--version"], { stdio: "ignore" });
|
|
199
|
+
} catch {
|
|
200
|
+
throw new CliError("systemd (systemctl) was not found on this host.");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function privileged(args) {
|
|
204
|
+
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
205
|
+
const argv = isRoot ? args : ["sudo", ...args];
|
|
206
|
+
execFileSync(argv[0], argv.slice(1), { stdio: "inherit" });
|
|
207
|
+
}
|
|
208
|
+
function query(args) {
|
|
209
|
+
try {
|
|
210
|
+
return execFileSync("systemctl", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim();
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const out = err.stdout;
|
|
213
|
+
return out ? out.toString().trim() : "";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function entryScript() {
|
|
217
|
+
const p = process.argv[1];
|
|
218
|
+
if (!p) throw new CliError("Cannot resolve the cloudtunnel executable path.");
|
|
219
|
+
return realpathSync(p);
|
|
220
|
+
}
|
|
221
|
+
function installServiceForSpec(params) {
|
|
222
|
+
assertSystemd();
|
|
223
|
+
const fqdn = params.subdomain === "@" ? params.zone : `${params.subdomain}.${params.zone}`;
|
|
224
|
+
const unit = buildUnit({
|
|
225
|
+
fqdn,
|
|
226
|
+
spec: formatTunnelSpec({ subdomain: params.subdomain, port: params.port, host: params.host }),
|
|
227
|
+
zone: params.zone,
|
|
228
|
+
proto: params.proto,
|
|
229
|
+
user: os.userInfo().username,
|
|
230
|
+
home: os.homedir(),
|
|
231
|
+
nodePath: process.execPath,
|
|
232
|
+
scriptPath: entryScript(),
|
|
233
|
+
protocol: params.protocol
|
|
234
|
+
});
|
|
235
|
+
const tmp = join(tmpdir(), serviceName(fqdn));
|
|
236
|
+
writeFileSync(tmp, unit, { mode: 420 });
|
|
237
|
+
privileged(["install", "-m", "0644", tmp, unitPath(fqdn)]);
|
|
238
|
+
privileged(["systemctl", "daemon-reload"]);
|
|
239
|
+
privileged(["systemctl", "enable", "--now", serviceName(fqdn)]);
|
|
240
|
+
}
|
|
241
|
+
function uninstallService(fqdn) {
|
|
242
|
+
assertSystemd();
|
|
243
|
+
try {
|
|
244
|
+
privileged(["systemctl", "disable", "--now", serviceName(fqdn)]);
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
privileged(["rm", "-f", unitPath(fqdn)]);
|
|
248
|
+
privileged(["systemctl", "daemon-reload"]);
|
|
249
|
+
}
|
|
250
|
+
function legacyUnitExists(profile) {
|
|
251
|
+
return existsSync(`/etc/systemd/system/cloudtunnel-${profile}.service`);
|
|
252
|
+
}
|
|
253
|
+
function removeLegacyUnit(profile) {
|
|
254
|
+
const name = `cloudtunnel-${profile}.service`;
|
|
255
|
+
try {
|
|
256
|
+
privileged(["systemctl", "disable", "--now", name]);
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
privileged(["rm", "-f", `/etc/systemd/system/${name}`]);
|
|
260
|
+
privileged(["systemctl", "daemon-reload"]);
|
|
261
|
+
}
|
|
262
|
+
function serviceState(fqdn) {
|
|
263
|
+
if (process.platform !== "linux") return "none";
|
|
264
|
+
const name = serviceName(fqdn);
|
|
265
|
+
if (query(["is-active", name]) === "active") return "active";
|
|
266
|
+
const enabled = query(["is-enabled", name]);
|
|
267
|
+
if (enabled === "enabled" || enabled === "enabled-runtime") return "enabled";
|
|
268
|
+
if (enabled === "disabled" || enabled === "static") return "disabled";
|
|
269
|
+
return "none";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/config/legacy-migrate.ts
|
|
273
|
+
var skipMarker = `${profilesFile}.migrate-skip`;
|
|
274
|
+
async function migrateLegacyProfiles() {
|
|
275
|
+
if (!existsSync2(profilesFile) || existsSync2(skipMarker)) return;
|
|
276
|
+
let profiles;
|
|
277
|
+
try {
|
|
278
|
+
profiles = JSON.parse(readFileSync(profilesFile, "utf8"));
|
|
279
|
+
} catch {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const legacy = Object.entries(profiles).filter(([name]) => legacyUnitExists(name));
|
|
283
|
+
if (legacy.length === 0) {
|
|
284
|
+
try {
|
|
285
|
+
renameSync(profilesFile, `${profilesFile}.migrated`);
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const ok = await confirm(`Found ${legacy.length} boot service(s) from an older cloudtunnel. Migrate them now? (needs sudo)`);
|
|
291
|
+
if (!ok) {
|
|
292
|
+
writeFileSync2(skipMarker, "");
|
|
293
|
+
say.dim(` Skipped. Delete ${skipMarker} to be asked again.`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
let migrated = 0;
|
|
297
|
+
try {
|
|
298
|
+
for (const [name, profile] of legacy) {
|
|
299
|
+
for (const svc of profile.services ?? []) {
|
|
300
|
+
const zone = svc.domain ?? profile.domain;
|
|
301
|
+
if (!zone) continue;
|
|
302
|
+
installServiceForSpec({
|
|
303
|
+
subdomain: svc.name,
|
|
304
|
+
port: svc.port,
|
|
305
|
+
host: svc.host,
|
|
306
|
+
zone,
|
|
307
|
+
proto: svc.proto,
|
|
308
|
+
protocol: profile.protocol
|
|
309
|
+
});
|
|
310
|
+
migrated++;
|
|
311
|
+
}
|
|
312
|
+
removeLegacyUnit(name);
|
|
313
|
+
}
|
|
314
|
+
renameSync(profilesFile, `${profilesFile}.migrated`);
|
|
315
|
+
say.ok(`Migrated ${migrated} boot service(s). See them with: cloudtunnel ls`);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
writeFileSync2(skipMarker, "");
|
|
318
|
+
say.warn(`Migration incomplete: ${err.message}. Won't retry automatically (delete ${skipMarker} to retry).`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/commands/login.ts
|
|
323
|
+
import * as clack from "@clack/prompts";
|
|
324
|
+
|
|
81
325
|
// src/config/token-url.ts
|
|
82
326
|
import { spawn } from "child_process";
|
|
83
327
|
var REQUIRED_SCOPES = [
|
|
@@ -217,9 +461,7 @@ function registerLogin(program) {
|
|
|
217
461
|
}
|
|
218
462
|
|
|
219
463
|
// src/commands/up.ts
|
|
220
|
-
import
|
|
221
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
222
|
-
import * as clack2 from "@clack/prompts";
|
|
464
|
+
import * as clack3 from "@clack/prompts";
|
|
223
465
|
|
|
224
466
|
// src/config/ensure-auth.ts
|
|
225
467
|
async function ensureAuth() {
|
|
@@ -236,10 +478,10 @@ async function ensureAuth() {
|
|
|
236
478
|
}
|
|
237
479
|
|
|
238
480
|
// src/connector/binary.ts
|
|
239
|
-
import { execFileSync } from "child_process";
|
|
481
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
240
482
|
import { createHash } from "crypto";
|
|
241
|
-
import { chmodSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
242
|
-
import { join } from "path";
|
|
483
|
+
import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
484
|
+
import { join as join2 } from "path";
|
|
243
485
|
var PINNED_VERSION = "2025.1.0";
|
|
244
486
|
var RELEASE_BASE = `https://github.com/cloudflare/cloudflared/releases/download/${PINNED_VERSION}`;
|
|
245
487
|
var ASSETS = {
|
|
@@ -251,18 +493,18 @@ var ASSETS = {
|
|
|
251
493
|
};
|
|
252
494
|
function binaryWorks(bin) {
|
|
253
495
|
try {
|
|
254
|
-
|
|
496
|
+
execFileSync2(bin, ["--version"], { stdio: "ignore" });
|
|
255
497
|
return true;
|
|
256
498
|
} catch {
|
|
257
499
|
return false;
|
|
258
500
|
}
|
|
259
501
|
}
|
|
260
502
|
function cachedPath() {
|
|
261
|
-
return
|
|
503
|
+
return join2(binDir, process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
|
|
262
504
|
}
|
|
263
505
|
function isMusl() {
|
|
264
506
|
try {
|
|
265
|
-
return process.platform === "linux" &&
|
|
507
|
+
return process.platform === "linux" && readFileSync2("/usr/bin/ldd", "utf8").includes("musl");
|
|
266
508
|
} catch {
|
|
267
509
|
return false;
|
|
268
510
|
}
|
|
@@ -270,7 +512,7 @@ function isMusl() {
|
|
|
270
512
|
async function ensureCloudflared() {
|
|
271
513
|
if (binaryWorks("cloudflared")) return "cloudflared";
|
|
272
514
|
const cached = cachedPath();
|
|
273
|
-
if (
|
|
515
|
+
if (existsSync3(cached) && binaryWorks(cached)) return cached;
|
|
274
516
|
return downloadCloudflared(cached);
|
|
275
517
|
}
|
|
276
518
|
async function downloadCloudflared(dest) {
|
|
@@ -298,7 +540,7 @@ async function downloadCloudflared(dest) {
|
|
|
298
540
|
}
|
|
299
541
|
ensureDirs();
|
|
300
542
|
const binary = asset.archive ? extractTgz(bytes) : bytes;
|
|
301
|
-
|
|
543
|
+
writeFileSync3(dest, binary, { mode: 493 });
|
|
302
544
|
chmodSync(dest, 493);
|
|
303
545
|
if (!binaryWorks(dest)) throw new CliError("Downloaded cloudflared is not runnable.");
|
|
304
546
|
return dest;
|
|
@@ -309,26 +551,33 @@ function extractTgz(_bytes) {
|
|
|
309
551
|
});
|
|
310
552
|
}
|
|
311
553
|
|
|
554
|
+
// src/core/up-runner.ts
|
|
555
|
+
import { join as join3 } from "path";
|
|
556
|
+
import * as clack2 from "@clack/prompts";
|
|
557
|
+
|
|
312
558
|
// src/connector/process.ts
|
|
313
|
-
import { execFileSync as
|
|
559
|
+
import { execFileSync as execFileSync3, spawn as spawn2 } from "child_process";
|
|
314
560
|
import { openSync } from "fs";
|
|
315
561
|
|
|
316
562
|
// src/connector/registry.ts
|
|
317
|
-
import { existsSync as
|
|
563
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
318
564
|
import { readFile } from "fs/promises";
|
|
319
|
-
import
|
|
565
|
+
import os2 from "os";
|
|
320
566
|
import lockfile from "proper-lockfile";
|
|
567
|
+
function entryFqdn(e) {
|
|
568
|
+
return e.subdomain === "@" ? e.zone : `${e.subdomain}.${e.zone}`;
|
|
569
|
+
}
|
|
321
570
|
function currentBootId() {
|
|
322
571
|
try {
|
|
323
|
-
return
|
|
572
|
+
return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
324
573
|
} catch {
|
|
325
|
-
const bootMinute = Math.floor((Date.now() -
|
|
326
|
-
return `boot-${bootMinute}-${
|
|
574
|
+
const bootMinute = Math.floor((Date.now() - os2.uptime() * 1e3) / 6e4);
|
|
575
|
+
return `boot-${bootMinute}-${os2.hostname()}`;
|
|
327
576
|
}
|
|
328
577
|
}
|
|
329
578
|
function readRegistry() {
|
|
330
579
|
try {
|
|
331
|
-
return JSON.parse(
|
|
580
|
+
return JSON.parse(readFileSync3(registryFile, "utf8"));
|
|
332
581
|
} catch {
|
|
333
582
|
return {};
|
|
334
583
|
}
|
|
@@ -336,12 +585,12 @@ function readRegistry() {
|
|
|
336
585
|
function writeRegistry(reg) {
|
|
337
586
|
ensureDirs();
|
|
338
587
|
const tmp = `${registryFile}.tmp`;
|
|
339
|
-
|
|
340
|
-
|
|
588
|
+
writeFileSync4(tmp, JSON.stringify(reg, null, 2), { mode: 384 });
|
|
589
|
+
renameSync2(tmp, registryFile);
|
|
341
590
|
}
|
|
342
591
|
async function mutateRegistry(fn) {
|
|
343
592
|
ensureDirs();
|
|
344
|
-
if (!
|
|
593
|
+
if (!existsSync4(registryFile)) writeFileSync4(registryFile, "{}", { mode: 384 });
|
|
345
594
|
const release = await lockfile.lock(registryFile, { retries: { retries: 10, minTimeout: 50 } });
|
|
346
595
|
try {
|
|
347
596
|
const reg = readRegistry();
|
|
@@ -414,7 +663,7 @@ async function reconcile() {
|
|
|
414
663
|
const entries = listEntries();
|
|
415
664
|
for (const entry of entries) {
|
|
416
665
|
if (entry.state === "running" && !await isOurConnector(entry)) {
|
|
417
|
-
const fqdn =
|
|
666
|
+
const fqdn = entryFqdn(entry);
|
|
418
667
|
await mutateRegistry((reg) => {
|
|
419
668
|
const e = reg[fqdn];
|
|
420
669
|
if (e) {
|
|
@@ -449,7 +698,7 @@ async function stopConnector(entry) {
|
|
|
449
698
|
const pid = entry.pid;
|
|
450
699
|
if (process.platform === "win32") {
|
|
451
700
|
try {
|
|
452
|
-
|
|
701
|
+
execFileSync3("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
453
702
|
} catch {
|
|
454
703
|
return false;
|
|
455
704
|
}
|
|
@@ -542,14 +791,6 @@ async function waitHealthy(cf, tunnelId, opts = {}) {
|
|
|
542
791
|
// src/core/orchestrator-create.ts
|
|
543
792
|
import { randomInt as randomInt2 } from "crypto";
|
|
544
793
|
|
|
545
|
-
// src/core/ingress.ts
|
|
546
|
-
function buildIngress(opts) {
|
|
547
|
-
return [
|
|
548
|
-
{ hostname: opts.hostname, service: `${opts.proto}://localhost:${opts.port}` },
|
|
549
|
-
{ service: "http_status:404" }
|
|
550
|
-
];
|
|
551
|
-
}
|
|
552
|
-
|
|
553
794
|
// src/core/slug.ts
|
|
554
795
|
import { randomInt } from "crypto";
|
|
555
796
|
var ADJECTIVES = [
|
|
@@ -641,6 +882,7 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
641
882
|
zoneId: zone.id,
|
|
642
883
|
port: opts.port,
|
|
643
884
|
proto: opts.proto,
|
|
885
|
+
host: opts.host,
|
|
644
886
|
state: "provisioning"
|
|
645
887
|
});
|
|
646
888
|
let tunnelId;
|
|
@@ -651,7 +893,7 @@ async function createTunnelSubdomain(cf, opts) {
|
|
|
651
893
|
const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${label}-${suffix}`);
|
|
652
894
|
tunnelId = tunnel.id;
|
|
653
895
|
const token = await getTunnelToken(cf, tunnelId);
|
|
654
|
-
await putIngress(cf, tunnelId, buildIngress({ hostname: host.hostname, port: opts.port, proto: opts.proto }));
|
|
896
|
+
await putIngress(cf, tunnelId, buildIngress({ hostname: host.hostname, port: opts.port, proto: opts.proto, host: opts.host }));
|
|
655
897
|
const record = await createCname(cf.token, zone.id, host.hostname, tunnelId);
|
|
656
898
|
dnsRecordId = record.id;
|
|
657
899
|
await recordRunning(host, zone.id, tunnelId, dnsRecordId, opts);
|
|
@@ -672,6 +914,7 @@ async function recordRunning(host, zoneId, tunnelId, dnsRecordId, opts) {
|
|
|
672
914
|
dnsRecordId,
|
|
673
915
|
port: opts.port,
|
|
674
916
|
proto: opts.proto,
|
|
917
|
+
host: opts.host,
|
|
675
918
|
bootId: currentBootId(),
|
|
676
919
|
state: "running"
|
|
677
920
|
});
|
|
@@ -717,20 +960,20 @@ function resolveTarget(target) {
|
|
|
717
960
|
const entries = listEntries();
|
|
718
961
|
if (/^\d+$/.test(target)) {
|
|
719
962
|
const byIndex = entries.find((e) => e.index === Number(target));
|
|
720
|
-
if (byIndex) return { fqdn:
|
|
963
|
+
if (byIndex) return { fqdn: entryFqdn(byIndex), entry: byIndex };
|
|
721
964
|
}
|
|
722
965
|
const byId = entries.filter((e) => e.tunnelId?.startsWith(target));
|
|
723
966
|
const matches = byId.length > 0 ? byId : entries.filter((e) => e.subdomain === target);
|
|
724
967
|
if (matches.length > 1) {
|
|
725
968
|
throw new CliError(`"${target}" matches multiple subdomains.`, {
|
|
726
|
-
hint: `use a full hostname or a longer id: ${matches.map(
|
|
969
|
+
hint: `use a full hostname or a longer id: ${matches.map(entryFqdn).join(", ")}`
|
|
727
970
|
});
|
|
728
971
|
}
|
|
729
972
|
const entry = matches[0];
|
|
730
973
|
if (!entry) {
|
|
731
974
|
throw new CliError(`No tracked subdomain matching "${target}".`, { hint: "see `cloudtunnel ls` for the #, name, or id" });
|
|
732
975
|
}
|
|
733
|
-
return { fqdn:
|
|
976
|
+
return { fqdn: entryFqdn(entry), entry };
|
|
734
977
|
}
|
|
735
978
|
async function removeTunnelSubdomain(cf, target, opts = {}) {
|
|
736
979
|
const { fqdn, entry } = resolveTarget(target);
|
|
@@ -780,12 +1023,15 @@ async function listAll(cf, opts = {}) {
|
|
|
780
1023
|
const entries = await reconcile();
|
|
781
1024
|
const tunnels = new Map((await listTunnels(cf)).map((t) => [t.id, t]));
|
|
782
1025
|
const rows = entries.map((e) => {
|
|
1026
|
+
const fqdn = entryFqdn(e);
|
|
783
1027
|
const gone = e.tunnelId ? !tunnels.has(e.tunnelId) : false;
|
|
1028
|
+
const svc = serviceState(fqdn);
|
|
784
1029
|
return {
|
|
785
1030
|
num: e.index ? String(e.index) : "-",
|
|
786
|
-
|
|
787
|
-
|
|
1031
|
+
url: `https://${fqdn}`,
|
|
1032
|
+
target: serviceUrl(e.proto, e.host ?? "localhost", e.port),
|
|
788
1033
|
state: !gone && e.state === "running" ? "up" : "down",
|
|
1034
|
+
service: svc === "none" ? "-" : svc,
|
|
789
1035
|
pid: e.state === "running" && e.pid ? String(e.pid) : "-",
|
|
790
1036
|
managed: true
|
|
791
1037
|
};
|
|
@@ -793,11 +1039,11 @@ async function listAll(cf, opts = {}) {
|
|
|
793
1039
|
if (opts.all) {
|
|
794
1040
|
const { listCargoCnames } = await import("./dns-PAPFSYFP.js");
|
|
795
1041
|
const { listZones: listZones3 } = await import("./zones-YNGQYXAF.js");
|
|
796
|
-
const tracked = new Set(entries.map(
|
|
1042
|
+
const tracked = new Set(entries.map(entryFqdn));
|
|
797
1043
|
for (const zone of await listZones3(cf.token)) {
|
|
798
1044
|
for (const rec of await listCargoCnames(cf.token, zone.id)) {
|
|
799
1045
|
if (!tracked.has(rec.name)) {
|
|
800
|
-
rows.push({ num: "-",
|
|
1046
|
+
rows.push({ num: "-", url: `https://${rec.name}`, target: "-", state: "unmanaged", service: "-", pid: "-", managed: false });
|
|
801
1047
|
}
|
|
802
1048
|
}
|
|
803
1049
|
}
|
|
@@ -805,68 +1051,108 @@ async function listAll(cf, opts = {}) {
|
|
|
805
1051
|
return rows;
|
|
806
1052
|
}
|
|
807
1053
|
|
|
808
|
-
// src/core/
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
812
|
-
throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
|
|
813
|
-
}
|
|
814
|
-
function readProfiles() {
|
|
815
|
-
try {
|
|
816
|
-
return JSON.parse(readFileSync3(profilesFile, "utf8"));
|
|
817
|
-
} catch {
|
|
818
|
-
return {};
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
function writeProfiles(profiles) {
|
|
822
|
-
ensureDirs();
|
|
823
|
-
writeFileSync3(profilesFile, JSON.stringify(profiles, null, 2), { mode: 384 });
|
|
824
|
-
}
|
|
825
|
-
function listProfiles() {
|
|
826
|
-
return Object.entries(readProfiles()).map(([name, profile]) => ({ name, profile }));
|
|
1054
|
+
// src/core/up-runner.ts
|
|
1055
|
+
function logFileFor(subdomain) {
|
|
1056
|
+
return join3(logDir, `${subdomain === "@" ? "root" : subdomain}.log`);
|
|
827
1057
|
}
|
|
828
|
-
function
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
1058
|
+
async function startTunnels(cf, bin, items, opts = {}) {
|
|
1059
|
+
const started = [];
|
|
1060
|
+
let tornDown = false;
|
|
1061
|
+
const teardownAll = async (code) => {
|
|
1062
|
+
if (tornDown) return;
|
|
1063
|
+
tornDown = true;
|
|
1064
|
+
try {
|
|
1065
|
+
for (const s of started) {
|
|
1066
|
+
try {
|
|
1067
|
+
await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (process.stdout.isTTY) clack2.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
reportError(err);
|
|
1074
|
+
} finally {
|
|
1075
|
+
process.exit(code);
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
const spin = clack2.spinner();
|
|
1079
|
+
spin.start(items.length > 1 ? "Creating tunnels\u2026" : "Creating tunnel\u2026");
|
|
1080
|
+
for (const item of items) {
|
|
1081
|
+
spin.message(`Creating ${item.name ?? "tunnel"} (:${item.port})\u2026`);
|
|
1082
|
+
const result = await createTunnelSubdomain(cf, item);
|
|
1083
|
+
const fqdn = result.host.hostname;
|
|
1084
|
+
const logFile = logFileFor(result.host.subdomain);
|
|
1085
|
+
const conn = startConnector({
|
|
1086
|
+
bin,
|
|
1087
|
+
token: result.token,
|
|
1088
|
+
detach: !!opts.detach,
|
|
1089
|
+
logFile,
|
|
1090
|
+
protocol: opts.protocol,
|
|
1091
|
+
onExit: opts.detach ? void 0 : (code) => {
|
|
1092
|
+
if (!tornDown) {
|
|
1093
|
+
say.warn(`Connector for ${fqdn} exited.`);
|
|
1094
|
+
void teardownAll(code ?? 1);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile });
|
|
1099
|
+
started.push({
|
|
1100
|
+
fqdn,
|
|
1101
|
+
subdomain: result.host.subdomain,
|
|
1102
|
+
tunnelId: result.tunnelId,
|
|
1103
|
+
target: serviceUrl(item.proto, item.host ?? "localhost", item.port),
|
|
1104
|
+
pid: conn.pid
|
|
1105
|
+
});
|
|
832
1106
|
}
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
function removeProfile(name) {
|
|
841
|
-
const profiles = readProfiles();
|
|
842
|
-
if (!profiles[name]) throw new CliError(`No profile named "${name}".`);
|
|
843
|
-
delete profiles[name];
|
|
844
|
-
writeProfiles(profiles);
|
|
845
|
-
}
|
|
846
|
-
function parseServiceSpec(spec) {
|
|
847
|
-
const [name, portStr, proto] = spec.split(":");
|
|
848
|
-
const port = Number(portStr);
|
|
849
|
-
if (!name || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
850
|
-
throw new CliError(`Invalid service "${spec}".`, { hint: "use name:port, e.g. api:3000 or web:5173:https" });
|
|
1107
|
+
if (opts.detach) {
|
|
1108
|
+
spin.stop(`${started.length} tunnel(s) started in the background`);
|
|
1109
|
+
const lines2 = started.map((s) => `${formatRoute(s.fqdn, s.target)} ${dim(`pid ${s.pid}`)}`);
|
|
1110
|
+
clack2.note(lines2.join("\n"), "running in background");
|
|
1111
|
+
if (process.stdout.isTTY) clack2.outro("Stop with: cloudtunnel delete <#|--all>");
|
|
1112
|
+
return;
|
|
851
1113
|
}
|
|
852
|
-
|
|
853
|
-
|
|
1114
|
+
for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
|
|
1115
|
+
process.on(sig, () => void teardownAll(0));
|
|
854
1116
|
}
|
|
855
|
-
|
|
1117
|
+
spin.message("Connecting to the Cloudflare edge\u2026");
|
|
1118
|
+
const healths = await Promise.all(started.map((s) => waitHealthy(cf, s.tunnelId, { timeoutMs: 3e4 })));
|
|
1119
|
+
const live = healths.filter((h) => h === "healthy").length;
|
|
1120
|
+
spin.stop(`${started.length} tunnel(s) started`);
|
|
1121
|
+
const lines = started.map((s, i) => `${formatRoute(s.fqdn, s.target)}${healths[i] === "healthy" ? "" : dim(` (${healths[i]})`)}`);
|
|
1122
|
+
clack2.note(lines.join("\n"), `${live}/${started.length} live`);
|
|
1123
|
+
say.dim("Ctrl-C stops and releases them.");
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/core/transport-protocol.ts
|
|
1127
|
+
function parseTransportProtocol(value) {
|
|
1128
|
+
if (value === "auto" || value === "http2" || value === "quic") return value;
|
|
1129
|
+
throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
|
|
856
1130
|
}
|
|
857
1131
|
|
|
858
1132
|
// src/commands/up.ts
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1133
|
+
function promptOrExit(value) {
|
|
1134
|
+
if (clack3.isCancel(value)) {
|
|
1135
|
+
clack3.cancel("Cancelled.");
|
|
1136
|
+
process.exit(130);
|
|
863
1137
|
}
|
|
864
|
-
return
|
|
1138
|
+
return value;
|
|
1139
|
+
}
|
|
1140
|
+
async function promptPort() {
|
|
1141
|
+
const input = promptOrExit(
|
|
1142
|
+
await clack3.text({
|
|
1143
|
+
message: "Port to expose",
|
|
1144
|
+
placeholder: "e.g. 3000",
|
|
1145
|
+
validate: (v) => {
|
|
1146
|
+
const n = Number(v);
|
|
1147
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) return "Enter a port 1\u201365535";
|
|
1148
|
+
return void 0;
|
|
1149
|
+
}
|
|
1150
|
+
})
|
|
1151
|
+
);
|
|
1152
|
+
return Number(input);
|
|
865
1153
|
}
|
|
866
1154
|
async function resolveDomain(cf, opts, creds) {
|
|
867
|
-
if (opts.
|
|
868
|
-
const explicit = opts.domain ?? opts.zone;
|
|
869
|
-
if (explicit) return explicit;
|
|
1155
|
+
if (opts.domain) return opts.domain;
|
|
870
1156
|
const zones = await listZones(cf.token);
|
|
871
1157
|
if (zones.length === 0) throw new CliError("No domains found in this Cloudflare account.");
|
|
872
1158
|
if (zones.length === 1) return zones[0].name;
|
|
@@ -874,109 +1160,66 @@ async function resolveDomain(cf, opts, creds) {
|
|
|
874
1160
|
if (creds.defaultZone) return creds.defaultZone;
|
|
875
1161
|
throw new CliError("Multiple domains in this account \u2014 pick one.", { hint: "pass -d <domain>" });
|
|
876
1162
|
}
|
|
877
|
-
async function
|
|
878
|
-
|
|
879
|
-
if (
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
clack2.cancel("Cancelled.");
|
|
884
|
-
process.exit(130);
|
|
885
|
-
}
|
|
1163
|
+
async function resolveSpecSubdomain(spec, opts) {
|
|
1164
|
+
if (spec.subdomain !== void 0) return spec.subdomain;
|
|
1165
|
+
if (opts.yes || !process.stdin.isTTY) return void 0;
|
|
1166
|
+
const input = promptOrExit(
|
|
1167
|
+
await clack3.text({ message: `Subdomain for :${spec.port}`, placeholder: "blank = random \xB7 @ = root domain" })
|
|
1168
|
+
);
|
|
886
1169
|
return input.trim() || void 0;
|
|
887
1170
|
}
|
|
888
|
-
function
|
|
889
|
-
try {
|
|
890
|
-
const tail = readFileSync4(logFile, "utf8").trim().split("\n").slice(-8).join("\n");
|
|
891
|
-
if (tail) say.dim(tail);
|
|
892
|
-
} catch {
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
async function runUp(portArg, opts) {
|
|
896
|
-
const port = parsePort(portArg);
|
|
1171
|
+
async function runUp(specArgs, opts) {
|
|
897
1172
|
const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : void 0;
|
|
1173
|
+
const parsed = specArgs.length ? specArgs.map(parseTunnelSpec) : null;
|
|
1174
|
+
if (parsed === null && !process.stdin.isTTY) {
|
|
1175
|
+
throw new CliError("No tunnel spec given.", { hint: "e.g. cloudtunnel api:8080" });
|
|
1176
|
+
}
|
|
898
1177
|
const creds = await ensureAuth();
|
|
899
1178
|
const cf = resolveCf();
|
|
900
1179
|
const bin = await ensureCloudflared();
|
|
901
|
-
if (process.stdout.isTTY)
|
|
1180
|
+
if (process.stdout.isTTY) clack3.intro("cloudtunnel");
|
|
1181
|
+
const specs = parsed ?? [{ port: await promptPort() }];
|
|
902
1182
|
const domain = await resolveDomain(cf, opts, creds);
|
|
903
|
-
const
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (opts.
|
|
919
|
-
|
|
920
|
-
await patchEntry(fqdn, { pid: started2.pid, bootId: currentBootId(), logFile });
|
|
921
|
-
clack2.note(formatRoute(fqdn, target), `pid ${started2.pid}`);
|
|
922
|
-
if (process.stdout.isTTY) clack2.outro(`Stop it with: cloudtunnel down ${result.host.subdomain}`);
|
|
1183
|
+
const items = [];
|
|
1184
|
+
for (const spec of specs) {
|
|
1185
|
+
let name = await resolveSpecSubdomain(spec, opts);
|
|
1186
|
+
if (opts.service && name === void 0) name = randomSlug();
|
|
1187
|
+
items.push({
|
|
1188
|
+
port: spec.port,
|
|
1189
|
+
proto: opts.proto,
|
|
1190
|
+
name,
|
|
1191
|
+
zone: domain,
|
|
1192
|
+
host: spec.host,
|
|
1193
|
+
defaultZone: creds.defaultZone,
|
|
1194
|
+
force: opts.force,
|
|
1195
|
+
yes: opts.yes
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
if (opts.service) {
|
|
1199
|
+
registerServices(items, domain, opts.proto, protocol);
|
|
923
1200
|
return;
|
|
924
1201
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
};
|
|
933
|
-
spin.start("Connecting to the Cloudflare edge\u2026");
|
|
934
|
-
const controller = new AbortController();
|
|
935
|
-
let tornDown = false;
|
|
936
|
-
const teardown = async (exitCode) => {
|
|
937
|
-
if (tornDown) return;
|
|
938
|
-
tornDown = true;
|
|
939
|
-
controller.abort();
|
|
940
|
-
stopSpin("Stopping\u2026");
|
|
941
|
-
try {
|
|
942
|
-
await removeTunnelSubdomain(cf, fqdn, { force: true, quiet: true });
|
|
943
|
-
clack2.outro(`Stopped \xB7 ${fqdn} released`);
|
|
944
|
-
} catch (err) {
|
|
945
|
-
reportError(err);
|
|
946
|
-
} finally {
|
|
947
|
-
process.exit(exitCode);
|
|
948
|
-
}
|
|
949
|
-
};
|
|
950
|
-
const started = startConnector({
|
|
951
|
-
bin,
|
|
952
|
-
token: result.token,
|
|
953
|
-
detach: false,
|
|
954
|
-
logFile,
|
|
955
|
-
protocol,
|
|
956
|
-
onExit: (code) => {
|
|
957
|
-
if (!tornDown) {
|
|
958
|
-
stopSpin("cloudflared exited");
|
|
959
|
-
showLogTail(logFile);
|
|
960
|
-
void teardown(code ?? 1);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
});
|
|
964
|
-
await patchEntry(fqdn, { pid: started.pid, bootId: currentBootId(), logFile });
|
|
965
|
-
for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
|
|
966
|
-
process.on(sig, () => void teardown(0));
|
|
1202
|
+
await startTunnels(cf, bin, items, { detach: opts.detach, protocol });
|
|
1203
|
+
}
|
|
1204
|
+
function registerServices(items, domain, proto, protocol) {
|
|
1205
|
+
assertSystemd();
|
|
1206
|
+
if (!protocol) {
|
|
1207
|
+
say.warn("No edge protocol set \u2014 cloudflared will pick QUIC, which some networks drop.");
|
|
1208
|
+
say.dim(" \u2192 add --protocol http2 for UDP-hostile networks");
|
|
967
1209
|
}
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
stopSpin("Provisioning");
|
|
975
|
-
say.warn(`${fqdn} is not healthy yet \u2014 it should be live shortly.`);
|
|
1210
|
+
const done = [];
|
|
1211
|
+
for (const item of items) {
|
|
1212
|
+
const subdomain = item.name;
|
|
1213
|
+
const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
|
|
1214
|
+
installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
|
|
1215
|
+
done.push(`${serviceName(fqdn)} \u2192 https://${fqdn}`);
|
|
976
1216
|
}
|
|
1217
|
+
say.ok(`Registered ${done.length} boot service(s):`);
|
|
1218
|
+
for (const line of done) say.dim(` ${line}`);
|
|
1219
|
+
say.dim(" \u2192 check them: cloudtunnel ls \xB7 remove: cloudtunnel delete <#>");
|
|
977
1220
|
}
|
|
978
1221
|
function registerUp(program) {
|
|
979
|
-
program.command("up").argument("
|
|
1222
|
+
program.command("up", { isDefault: true }).argument("[specs...]", "tunnels to start: [subdomain:]port[@host] (e.g. api:8080 web:8081@localhost)").description("Start one or more tunnels (also: `cloudtunnel 8080`)").option("-d, --domain <domain>", "domain for the subdomains (prompted from a list if unset)").option("--proto <proto>", "local service protocol: http | https", "http").option("--protocol <proto>", "cloudflared edge transport: auto | http2 | quic (http2 for UDP-hostile networks)").option("--detach", "run the connectors in the background").option("--service", "register each subdomain as a systemd boot service (Linux; needs sudo)").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't prompt; don't ask before replacing an existing record").action((specs, opts) => runUp(specs, opts));
|
|
980
1223
|
}
|
|
981
1224
|
|
|
982
1225
|
// src/commands/ls.ts
|
|
@@ -990,15 +1233,25 @@ function registerLs(program) {
|
|
|
990
1233
|
return;
|
|
991
1234
|
}
|
|
992
1235
|
printTable(
|
|
993
|
-
["#", "
|
|
994
|
-
rows.map((r) => [r.num, r.
|
|
1236
|
+
["#", "URL", "TARGET", "STATE", "SERVICE", "PID"],
|
|
1237
|
+
rows.map((r) => [r.num, r.url, r.target, r.state, r.service, r.pid])
|
|
995
1238
|
);
|
|
996
1239
|
});
|
|
997
1240
|
}
|
|
998
1241
|
|
|
999
|
-
// src/commands/
|
|
1000
|
-
function
|
|
1001
|
-
|
|
1242
|
+
// src/commands/delete.ts
|
|
1243
|
+
async function deleteOne(cf, fqdn, opts) {
|
|
1244
|
+
await removeTunnelSubdomain(cf, fqdn, { force: opts.force, dryRun: opts.dryRun });
|
|
1245
|
+
if (serviceState(fqdn) === "none") return;
|
|
1246
|
+
if (opts.dryRun) {
|
|
1247
|
+
say.info(`Would also remove boot service ${serviceName(fqdn)}`);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
uninstallService(fqdn);
|
|
1251
|
+
say.ok(`Removed boot service ${serviceName(fqdn)}`);
|
|
1252
|
+
}
|
|
1253
|
+
function registerDelete(program) {
|
|
1254
|
+
program.command("delete").argument("[targets...]", "subdomains to remove by # / name / URL (omit with --all)").description("Release tunnel(s) \u2014 deletes the tunnel + DNS, and any systemd boot service").option("--all", "release every tracked subdomain").option("-f, --force", "release even a resource not created by cloudtunnel").option("--dry-run", "show what would be released without doing it").action(async (targets, opts) => {
|
|
1002
1255
|
await ensureAuth();
|
|
1003
1256
|
const cf = resolveCf();
|
|
1004
1257
|
if (opts.all) {
|
|
@@ -1008,258 +1261,27 @@ function registerDown(program) {
|
|
|
1008
1261
|
return;
|
|
1009
1262
|
}
|
|
1010
1263
|
for (const e of entries) {
|
|
1264
|
+
const fqdn = entryFqdn(e);
|
|
1011
1265
|
try {
|
|
1012
|
-
await
|
|
1266
|
+
await deleteOne(cf, fqdn, opts);
|
|
1013
1267
|
} catch (err) {
|
|
1014
|
-
say.warn(`Could not release ${
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
if (!target) throw new CliError("Pass a subdomain (name / id / #) or --all.");
|
|
1020
|
-
await removeTunnelSubdomain(cf, target, { force: opts.force, dryRun: opts.dryRun });
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// src/commands/zones.ts
|
|
1025
|
-
function registerZones(program) {
|
|
1026
|
-
program.command("zones").description("List the zones (domains) available in your Cloudflare account").action(async () => {
|
|
1027
|
-
await ensureAuth();
|
|
1028
|
-
const cf = resolveCf();
|
|
1029
|
-
const zones = await listZones(cf.token);
|
|
1030
|
-
if (zones.length === 0) {
|
|
1031
|
-
say.info("No zones in this account.");
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
printTable(
|
|
1035
|
-
["ZONE", "STATUS", "ID"],
|
|
1036
|
-
zones.map((z) => [z.name, z.status ?? "-", z.id])
|
|
1037
|
-
);
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// src/commands/save.ts
|
|
1042
|
-
function registerSave(program) {
|
|
1043
|
-
program.command("save").argument("<profile>", "profile name, e.g. mb").argument("[services...]", "services as name:port[:proto], e.g. api:3000 web:5173").description("Save a group of services as a profile you can `run` together").option("--from-running", "snapshot the currently tracked tunnels instead of listing services").option("-d, --domain <domain>", "default domain for this profile").option("--protocol <proto>", "edge transport for this profile: auto | http2 | quic").action((profile, specs, opts) => {
|
|
1044
|
-
let services;
|
|
1045
|
-
if (opts.fromRunning) {
|
|
1046
|
-
const entries = listEntries().filter((e) => e.tunnelId);
|
|
1047
|
-
if (entries.length === 0) {
|
|
1048
|
-
throw new CliError("No tunnels to snapshot.", { hint: "start some with `cloudtunnel up`, or pass services like api:3000" });
|
|
1049
|
-
}
|
|
1050
|
-
services = entries.map((e) => ({ name: e.subdomain, port: e.port, proto: e.proto, domain: e.zone }));
|
|
1051
|
-
} else {
|
|
1052
|
-
if (specs.length === 0) {
|
|
1053
|
-
throw new CliError("No services given.", { hint: "e.g. `cloudtunnel save mb api:3000 web:5173`" });
|
|
1054
|
-
}
|
|
1055
|
-
services = specs.map(parseServiceSpec);
|
|
1056
|
-
}
|
|
1057
|
-
const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : void 0;
|
|
1058
|
-
saveProfile(profile, { services, domain: opts.domain, protocol });
|
|
1059
|
-
say.ok(`Saved profile "${profile}" (${services.length} service${services.length === 1 ? "" : "s"}). Run it: cloudtunnel run ${profile}`);
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// src/commands/run.ts
|
|
1064
|
-
import { join as join3 } from "path";
|
|
1065
|
-
import * as clack3 from "@clack/prompts";
|
|
1066
|
-
async function runProfile(name, opts) {
|
|
1067
|
-
const creds = await ensureAuth();
|
|
1068
|
-
const cf = resolveCf();
|
|
1069
|
-
const bin = await ensureCloudflared();
|
|
1070
|
-
const profile = getProfile(name);
|
|
1071
|
-
const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : profile.protocol;
|
|
1072
|
-
if (process.stdout.isTTY) clack3.intro(`cloudtunnel \xB7 profile "${name}"`);
|
|
1073
|
-
const spin = clack3.spinner();
|
|
1074
|
-
spin.start("Creating tunnels\u2026");
|
|
1075
|
-
const started = [];
|
|
1076
|
-
for (const svc of profile.services) {
|
|
1077
|
-
spin.message(`Creating ${svc.name} (:${svc.port})\u2026`);
|
|
1078
|
-
const result = await createTunnelSubdomain(cf, {
|
|
1079
|
-
port: svc.port,
|
|
1080
|
-
proto: svc.proto,
|
|
1081
|
-
name: svc.name,
|
|
1082
|
-
zone: svc.domain ?? opts.domain ?? profile.domain,
|
|
1083
|
-
defaultZone: creds.defaultZone,
|
|
1084
|
-
force: opts.force,
|
|
1085
|
-
yes: true
|
|
1086
|
-
// batch: never prompt per service
|
|
1087
|
-
});
|
|
1088
|
-
const fqdn = result.host.hostname;
|
|
1089
|
-
const logFile = join3(logDir, `${result.host.subdomain}.log`);
|
|
1090
|
-
const conn = startConnector({
|
|
1091
|
-
bin,
|
|
1092
|
-
token: result.token,
|
|
1093
|
-
detach: !!opts.detach,
|
|
1094
|
-
logFile,
|
|
1095
|
-
protocol,
|
|
1096
|
-
onExit: opts.detach ? void 0 : () => say.warn(`Connector for ${fqdn} exited.`)
|
|
1097
|
-
});
|
|
1098
|
-
await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile });
|
|
1099
|
-
started.push({ fqdn, subdomain: result.host.subdomain, tunnelId: result.tunnelId, target: `${svc.proto}://localhost:${svc.port}`, pid: conn.pid });
|
|
1100
|
-
}
|
|
1101
|
-
if (opts.detach) {
|
|
1102
|
-
spin.stop(`${started.length} service(s) started in the background`);
|
|
1103
|
-
const lines2 = started.map((s) => `${formatRoute(s.fqdn, s.target)} ${dim(`pid ${s.pid}`)}`);
|
|
1104
|
-
clack3.note(lines2.join("\n"), `profile "${name}" \u2014 running in background`);
|
|
1105
|
-
if (process.stdout.isTTY) clack3.outro("Stop them with: cloudtunnel down --all");
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
spin.message("Connecting to the Cloudflare edge\u2026");
|
|
1109
|
-
const healths = await Promise.all(started.map((s) => waitHealthy(cf, s.tunnelId, { timeoutMs: 3e4 })));
|
|
1110
|
-
const live = healths.filter((h) => h === "healthy").length;
|
|
1111
|
-
spin.stop(`${started.length} service(s) started`);
|
|
1112
|
-
const lines = started.map((s, i) => `${formatRoute(s.fqdn, s.target)}${healths[i] === "healthy" ? "" : dim(` (${healths[i]})`)}`);
|
|
1113
|
-
clack3.note(lines.join("\n"), `profile "${name}" \u2014 ${live}/${started.length} live`);
|
|
1114
|
-
say.dim("Ctrl-C stops and releases all of them.");
|
|
1115
|
-
let tornDown = false;
|
|
1116
|
-
const teardownAll = async (code) => {
|
|
1117
|
-
if (tornDown) return;
|
|
1118
|
-
tornDown = true;
|
|
1119
|
-
try {
|
|
1120
|
-
for (const s of started) {
|
|
1121
|
-
try {
|
|
1122
|
-
await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
|
|
1123
|
-
} catch {
|
|
1268
|
+
say.warn(`Could not release ${fqdn}: ${err.message}`);
|
|
1124
1269
|
}
|
|
1125
1270
|
}
|
|
1126
|
-
if (process.stdout.isTTY) clack3.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
|
|
1127
|
-
} catch (err) {
|
|
1128
|
-
reportError(err);
|
|
1129
|
-
} finally {
|
|
1130
|
-
process.exit(code);
|
|
1131
|
-
}
|
|
1132
|
-
};
|
|
1133
|
-
for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
|
|
1134
|
-
process.on(sig, () => void teardownAll(0));
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
function registerRun(program) {
|
|
1138
|
-
program.command("run").argument("<profile>", "name of a saved profile (see `cloudtunnel profiles`)").description("Start every service in a saved profile at once").option("-f, --force", "take over subdomains already occupied by another record").option("-d, --domain <domain>", "override the profile's domain for this run").option("--detach", "run all connectors in the background (stop with `cloudtunnel down --all`)").option("--protocol <proto>", "edge transport: auto | http2 | quic (overrides the profile's saved protocol)").action((name, opts) => runProfile(name, opts));
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// src/core/systemd.ts
|
|
1142
|
-
import { execFileSync as execFileSync3 } from "child_process";
|
|
1143
|
-
import { writeFileSync as writeFileSync4 } from "fs";
|
|
1144
|
-
import { tmpdir } from "os";
|
|
1145
|
-
import { dirname, join as join4 } from "path";
|
|
1146
|
-
function serviceName(profile) {
|
|
1147
|
-
return `cloudtunnel-${profile}.service`;
|
|
1148
|
-
}
|
|
1149
|
-
function unitPath(profile) {
|
|
1150
|
-
return `/etc/systemd/system/${serviceName(profile)}`;
|
|
1151
|
-
}
|
|
1152
|
-
function buildUnit(p) {
|
|
1153
|
-
const nodeBin = dirname(p.nodePath);
|
|
1154
|
-
const proto = p.protocol ? ` --protocol ${p.protocol}` : "";
|
|
1155
|
-
return [
|
|
1156
|
-
"[Unit]",
|
|
1157
|
-
`Description=cloudtunnel profile "${p.profile}" (Cloudflare Tunnel)`,
|
|
1158
|
-
"After=network-online.target",
|
|
1159
|
-
"Wants=network-online.target",
|
|
1160
|
-
"",
|
|
1161
|
-
"[Service]",
|
|
1162
|
-
"Type=simple",
|
|
1163
|
-
`User=${p.user}`,
|
|
1164
|
-
`Environment=HOME=${p.home}`,
|
|
1165
|
-
`Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
1166
|
-
`ExecStart=${p.nodePath} ${p.scriptPath} run ${p.profile} -f${proto}`,
|
|
1167
|
-
"Restart=on-failure",
|
|
1168
|
-
"RestartSec=5",
|
|
1169
|
-
"",
|
|
1170
|
-
"[Install]",
|
|
1171
|
-
"WantedBy=multi-user.target",
|
|
1172
|
-
""
|
|
1173
|
-
].join("\n");
|
|
1174
|
-
}
|
|
1175
|
-
function assertSystemd() {
|
|
1176
|
-
if (process.platform !== "linux") {
|
|
1177
|
-
throw new CliError("Service registration is Linux/systemd only.", {
|
|
1178
|
-
hint: "on macOS/Windows run `cloudtunnel run <profile> --detach` at login instead"
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
try {
|
|
1182
|
-
execFileSync3("systemctl", ["--version"], { stdio: "ignore" });
|
|
1183
|
-
} catch {
|
|
1184
|
-
throw new CliError("systemd (systemctl) was not found on this host.");
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
function privileged(args) {
|
|
1188
|
-
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
1189
|
-
const argv = isRoot ? args : ["sudo", ...args];
|
|
1190
|
-
execFileSync3(argv[0], argv.slice(1), { stdio: "inherit" });
|
|
1191
|
-
}
|
|
1192
|
-
function query(args) {
|
|
1193
|
-
try {
|
|
1194
|
-
return execFileSync3("systemctl", args, {
|
|
1195
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1196
|
-
encoding: "utf8"
|
|
1197
|
-
}).trim();
|
|
1198
|
-
} catch (err) {
|
|
1199
|
-
const out = err.stdout;
|
|
1200
|
-
return out ? out.toString().trim() : "";
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
function installService(p) {
|
|
1204
|
-
assertSystemd();
|
|
1205
|
-
const tmp = join4(tmpdir(), serviceName(p.profile));
|
|
1206
|
-
writeFileSync4(tmp, buildUnit(p), { mode: 420 });
|
|
1207
|
-
privileged(["install", "-m", "0644", tmp, unitPath(p.profile)]);
|
|
1208
|
-
privileged(["systemctl", "daemon-reload"]);
|
|
1209
|
-
privileged(["systemctl", "enable", "--now", serviceName(p.profile)]);
|
|
1210
|
-
}
|
|
1211
|
-
function uninstallService(profile) {
|
|
1212
|
-
assertSystemd();
|
|
1213
|
-
try {
|
|
1214
|
-
privileged(["systemctl", "disable", "--now", serviceName(profile)]);
|
|
1215
|
-
} catch {
|
|
1216
|
-
}
|
|
1217
|
-
privileged(["rm", "-f", unitPath(profile)]);
|
|
1218
|
-
privileged(["systemctl", "daemon-reload"]);
|
|
1219
|
-
}
|
|
1220
|
-
function serviceState(profile) {
|
|
1221
|
-
if (process.platform !== "linux") return "none";
|
|
1222
|
-
const name = serviceName(profile);
|
|
1223
|
-
if (query(["is-active", name]) === "active") return "active";
|
|
1224
|
-
const enabled = query(["is-enabled", name]);
|
|
1225
|
-
if (enabled === "enabled" || enabled === "enabled-runtime") return "enabled";
|
|
1226
|
-
if (enabled === "disabled" || enabled === "static") return "disabled";
|
|
1227
|
-
return "none";
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// src/commands/profiles.ts
|
|
1231
|
-
function formatService(state) {
|
|
1232
|
-
return state === "none" ? dim("\u2013") : state;
|
|
1233
|
-
}
|
|
1234
|
-
function registerProfiles(program) {
|
|
1235
|
-
program.command("profiles").description("List saved profiles (or delete one with --rm)").option("--rm <name>", "delete a profile").action((opts) => {
|
|
1236
|
-
if (opts.rm) {
|
|
1237
|
-
removeProfile(opts.rm);
|
|
1238
|
-
say.ok(`Deleted profile "${opts.rm}".`);
|
|
1239
1271
|
return;
|
|
1240
1272
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1273
|
+
if (targets.length === 0) throw new CliError("Pass a subdomain (# / name / URL) or --all.");
|
|
1274
|
+
for (const target of targets) {
|
|
1275
|
+
const { fqdn } = resolveTarget(target);
|
|
1276
|
+
await deleteOne(cf, fqdn, opts);
|
|
1245
1277
|
}
|
|
1246
|
-
printTable(
|
|
1247
|
-
["PROFILE", "SERVICES", "DOMAIN", "PROTOCOL", "SERVICE"],
|
|
1248
|
-
profiles.map(({ name, profile }) => [
|
|
1249
|
-
name,
|
|
1250
|
-
profile.services.map((s) => `${s.name}:${s.port}`).join(", "),
|
|
1251
|
-
profile.domain ?? "(default)",
|
|
1252
|
-
profile.protocol ?? "auto",
|
|
1253
|
-
formatService(serviceState(name))
|
|
1254
|
-
])
|
|
1255
|
-
);
|
|
1256
1278
|
});
|
|
1257
1279
|
}
|
|
1258
1280
|
|
|
1259
1281
|
// src/commands/logs.ts
|
|
1260
|
-
import { closeSync, existsSync as
|
|
1282
|
+
import { closeSync, existsSync as existsSync5, openSync as openSync2, readFileSync as readFileSync4, readSync, statSync, watch } from "fs";
|
|
1261
1283
|
function printTail(file, n) {
|
|
1262
|
-
const lines =
|
|
1284
|
+
const lines = readFileSync4(file, "utf8").split("\n");
|
|
1263
1285
|
const tail = lines.slice(-n).join("\n");
|
|
1264
1286
|
process.stdout.write(tail.endsWith("\n") ? tail : `${tail}
|
|
1265
1287
|
`);
|
|
@@ -1291,7 +1313,7 @@ function follow(file, fromPos) {
|
|
|
1291
1313
|
function registerLogs(program) {
|
|
1292
1314
|
program.command("logs").argument("<target>", "subdomain name / hostname / id / #").description("Show the connector log for a subdomain (use -f to follow)").option("-f, --follow", "keep printing new log lines (like tail -f)").option("-n, --lines <n>", "number of lines to show", "50").action((name, opts) => {
|
|
1293
1315
|
const { fqdn, entry } = resolveTarget(name);
|
|
1294
|
-
if (!entry?.logFile || !
|
|
1316
|
+
if (!entry?.logFile || !existsSync5(entry.logFile)) {
|
|
1295
1317
|
throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
|
|
1296
1318
|
}
|
|
1297
1319
|
const n = Math.max(1, Number(opts.lines) || 50);
|
|
@@ -1300,113 +1322,39 @@ function registerLogs(program) {
|
|
|
1300
1322
|
});
|
|
1301
1323
|
}
|
|
1302
1324
|
|
|
1303
|
-
// src/commands/service.ts
|
|
1304
|
-
import os2 from "os";
|
|
1305
|
-
import { realpathSync } from "fs";
|
|
1306
|
-
function entryScript() {
|
|
1307
|
-
const p = process.argv[1];
|
|
1308
|
-
if (!p) throw new CliError("Cannot resolve the cloudtunnel executable path.");
|
|
1309
|
-
return realpathSync(p);
|
|
1310
|
-
}
|
|
1311
|
-
function enable(name, opts) {
|
|
1312
|
-
const profile = getProfile(name);
|
|
1313
|
-
let protocol = profile.protocol;
|
|
1314
|
-
if (opts.protocol) {
|
|
1315
|
-
protocol = parseTransportProtocol(opts.protocol);
|
|
1316
|
-
saveProfile(name, { ...profile, protocol });
|
|
1317
|
-
}
|
|
1318
|
-
if (!protocol) {
|
|
1319
|
-
say.warn("No edge protocol set \u2014 cloudflared will pick QUIC, which some networks drop.");
|
|
1320
|
-
say.dim(" \u2192 set one with: cloudtunnel service enable " + name + " --protocol http2");
|
|
1321
|
-
}
|
|
1322
|
-
installService({
|
|
1323
|
-
profile: name,
|
|
1324
|
-
user: os2.userInfo().username,
|
|
1325
|
-
home: os2.homedir(),
|
|
1326
|
-
nodePath: process.execPath,
|
|
1327
|
-
scriptPath: entryScript(),
|
|
1328
|
-
protocol
|
|
1329
|
-
});
|
|
1330
|
-
say.ok(`Service ${serviceName(name)} enabled \u2014 starts on boot.`);
|
|
1331
|
-
say.dim(` \u2192 check it: cloudtunnel service status ${name}`);
|
|
1332
|
-
}
|
|
1333
|
-
function disable(name) {
|
|
1334
|
-
getProfile(name);
|
|
1335
|
-
uninstallService(name);
|
|
1336
|
-
say.ok(`Service ${serviceName(name)} disabled and removed.`);
|
|
1337
|
-
}
|
|
1338
|
-
function status(name) {
|
|
1339
|
-
getProfile(name);
|
|
1340
|
-
say.info(`${serviceName(name)}: ${serviceState(name)}`);
|
|
1341
|
-
}
|
|
1342
|
-
function registerService(program) {
|
|
1343
|
-
const svc = program.command("service").description("Register a profile as a systemd service that starts on boot");
|
|
1344
|
-
svc.command("enable").argument("<profile>", "profile to register").option("--protocol <proto>", "edge transport for the service: auto | http2 | quic").description("Install + enable a boot service for the profile (needs sudo)").action((name, opts) => enable(name, opts));
|
|
1345
|
-
svc.command("disable").argument("<profile>", "profile to unregister").description("Stop, disable, and remove the profile's boot service (needs sudo)").action((name) => disable(name));
|
|
1346
|
-
svc.command("status").argument("<profile>", "profile to check").description("Show the systemd state of the profile's service").action((name) => status(name));
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
1325
|
// src/index.ts
|
|
1350
1326
|
var require2 = createRequire(import.meta.url);
|
|
1351
1327
|
var pkg = require2("../package.json");
|
|
1352
|
-
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
1353
|
-
"login",
|
|
1354
|
-
"up",
|
|
1355
|
-
"ls",
|
|
1356
|
-
"ps",
|
|
1357
|
-
"down",
|
|
1358
|
-
"rm",
|
|
1359
|
-
"remove",
|
|
1360
|
-
"delete",
|
|
1361
|
-
"stop",
|
|
1362
|
-
"logs",
|
|
1363
|
-
"zones",
|
|
1364
|
-
"save",
|
|
1365
|
-
"run",
|
|
1366
|
-
"profiles",
|
|
1367
|
-
"service",
|
|
1368
|
-
"help"
|
|
1369
|
-
]);
|
|
1370
|
-
function applyBarePortAlias(argv) {
|
|
1371
|
-
const args = argv.slice(2);
|
|
1372
|
-
const first = args[0];
|
|
1373
|
-
if (first && /^\d{1,5}$/.test(first) && !KNOWN_COMMANDS.has(first)) {
|
|
1374
|
-
args.unshift("up");
|
|
1375
|
-
}
|
|
1376
|
-
return [argv[0], argv[1], ...args];
|
|
1377
|
-
}
|
|
1378
1328
|
function buildProgram() {
|
|
1379
1329
|
const program = new Command();
|
|
1380
|
-
program.name("cloudtunnel").description("
|
|
1330
|
+
program.name("cloudtunnel").description("Expose local ports at HTTPS subdomains on your own Cloudflare domains.").version(pkg.version, "-v, --version").showHelpAfterError();
|
|
1381
1331
|
program.addHelpText(
|
|
1382
1332
|
"before",
|
|
1383
1333
|
[
|
|
1384
1334
|
pc2.bold("Quickstart:"),
|
|
1385
|
-
` ${pc2.cyan("cloudtunnel login")}
|
|
1386
|
-
` ${pc2.cyan("cloudtunnel
|
|
1335
|
+
` ${pc2.cyan("cloudtunnel login")} once \u2014 paste a token (or set CLOUDFLARE_API_TOKEN)`,
|
|
1336
|
+
` ${pc2.cyan("cloudtunnel 8080")} your local :8080 goes live at an HTTPS URL`,
|
|
1337
|
+
` ${pc2.cyan("cloudtunnel api:8080")} api.<domain> \u2192 localhost:8080`,
|
|
1338
|
+
` ${pc2.cyan("cloudtunnel ls")} list tunnels ${pc2.dim("\xB7")} ${pc2.cyan("cloudtunnel delete <#>")} remove one`,
|
|
1387
1339
|
""
|
|
1388
1340
|
].join("\n")
|
|
1389
1341
|
);
|
|
1390
|
-
for (const register of [
|
|
1391
|
-
registerLogin,
|
|
1392
|
-
registerUp,
|
|
1393
|
-
registerLs,
|
|
1394
|
-
registerDown,
|
|
1395
|
-
registerZones,
|
|
1396
|
-
registerSave,
|
|
1397
|
-
registerRun,
|
|
1398
|
-
registerProfiles,
|
|
1399
|
-
registerService,
|
|
1400
|
-
registerLogs
|
|
1401
|
-
]) {
|
|
1342
|
+
for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs]) {
|
|
1402
1343
|
register(program);
|
|
1403
1344
|
}
|
|
1404
1345
|
return program;
|
|
1405
1346
|
}
|
|
1347
|
+
function shouldMigrate(argv) {
|
|
1348
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
1349
|
+
const rest = argv.slice(2);
|
|
1350
|
+
const infoFlag = /* @__PURE__ */ new Set(["-h", "--help", "-v", "--version", "help"]);
|
|
1351
|
+
return !rest.some((a) => infoFlag.has(a));
|
|
1352
|
+
}
|
|
1406
1353
|
async function main() {
|
|
1354
|
+
if (shouldMigrate(process.argv)) await migrateLegacyProfiles();
|
|
1407
1355
|
const program = buildProgram();
|
|
1408
1356
|
try {
|
|
1409
|
-
await program.parseAsync(
|
|
1357
|
+
await program.parseAsync(process.argv);
|
|
1410
1358
|
} catch (err) {
|
|
1411
1359
|
process.exitCode = reportError(err);
|
|
1412
1360
|
}
|