@iamken/cloudtunnel 0.3.0 → 0.5.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/dist/index.js CHANGED
@@ -2,15 +2,16 @@
2
2
  import {
3
3
  listZones,
4
4
  resolveZone
5
- } from "./chunk-2TCFCMJS.js";
5
+ } from "./chunk-CWJA4L7J.js";
6
6
  import {
7
7
  createCname,
8
8
  deleteDnsRecord,
9
9
  findCname,
10
10
  isManagedDns
11
- } from "./chunk-YLTQB4F7.js";
11
+ } from "./chunk-NZKGIDIW.js";
12
12
  import {
13
13
  CliError,
14
+ __export,
14
15
  binDir,
15
16
  cfPaginate,
16
17
  cfRequest,
@@ -24,15 +25,15 @@ import {
24
25
  reportError,
25
26
  resolveCf,
26
27
  saveConfig
27
- } from "./chunk-UPBVRXLF.js";
28
+ } from "./chunk-RWR6VXNB.js";
28
29
 
29
30
  // src/index.ts
30
31
  import { Command } from "commander";
31
32
  import { createRequire } from "module";
32
33
  import pc2 from "picocolors";
33
34
 
34
- // src/commands/login.ts
35
- import * as clack from "@clack/prompts";
35
+ // src/config/legacy-migrate.ts
36
+ import { existsSync as existsSync3, readFileSync, renameSync, writeFileSync as writeFileSync4 } from "fs";
36
37
 
37
38
  // src/ui/output.ts
38
39
  import pc from "picocolors";
@@ -66,10 +67,10 @@ function printTable(head, rows) {
66
67
  for (const row of rows) table.push(row);
67
68
  console.log(table.toString());
68
69
  }
69
- async function selectOne(message, items, label) {
70
+ async function selectOne(message, items, label4) {
70
71
  const value = await select({
71
72
  message,
72
- options: items.map((item, i) => ({ value: String(i), label: label(item) }))
73
+ options: items.map((item, i) => ({ value: String(i), label: label4(item) }))
73
74
  });
74
75
  if (isCancel(value)) {
75
76
  cancel("Cancelled.");
@@ -78,6 +79,459 @@ async function selectOne(message, items, label) {
78
79
  return items[Number(value)];
79
80
  }
80
81
 
82
+ // src/core/service-exec.ts
83
+ import { realpathSync } from "fs";
84
+ import os from "os";
85
+ import { 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/service-exec.ts
161
+ var fqdnFor = (subdomain, zone) => subdomain === "@" ? zone : `${subdomain}.${zone}`;
162
+ var serviceSlug = (fqdn) => fqdn.replace(/[^a-zA-Z0-9]+/g, "-");
163
+ function buildUpArgs(p) {
164
+ const spec = formatTunnelSpec({ subdomain: p.subdomain, port: p.port, host: p.host });
165
+ return [
166
+ "up",
167
+ spec,
168
+ "-d",
169
+ p.zone,
170
+ ...p.proto === "https" ? ["--proto", "https"] : [],
171
+ ...p.protocol ? ["--protocol", p.protocol] : [],
172
+ "-f",
173
+ "-y"
174
+ ];
175
+ }
176
+ function entryScript() {
177
+ const p = process.argv[1];
178
+ if (!p) throw new CliError("Cannot resolve the cloudtunnel executable path.");
179
+ return realpathSync(p);
180
+ }
181
+ function describeService(p) {
182
+ const fqdn = fqdnFor(p.subdomain, p.zone);
183
+ const slug = serviceSlug(fqdn);
184
+ return {
185
+ fqdn,
186
+ slug,
187
+ argv: buildUpArgs(p),
188
+ nodePath: process.execPath,
189
+ scriptPath: entryScript(),
190
+ user: os.userInfo().username,
191
+ home: os.homedir(),
192
+ logFile: join(logDir, `${slug}.service.log`)
193
+ };
194
+ }
195
+
196
+ // src/core/service-systemd.ts
197
+ var service_systemd_exports = {};
198
+ __export(service_systemd_exports, {
199
+ assertSupported: () => assertSupported,
200
+ buildUnit: () => buildUnit,
201
+ install: () => install,
202
+ label: () => label,
203
+ legacyUnitExists: () => legacyUnitExists,
204
+ removeLegacyUnit: () => removeLegacyUnit,
205
+ state: () => state,
206
+ uninstall: () => uninstall
207
+ });
208
+ import { execFileSync } from "child_process";
209
+ import { existsSync, writeFileSync } from "fs";
210
+ import { tmpdir } from "os";
211
+ import { dirname, join as join2 } from "path";
212
+ var label = (fqdn) => `cloudtunnel-${serviceSlug(fqdn)}.service`;
213
+ var unitPath = (fqdn) => `/etc/systemd/system/${label(fqdn)}`;
214
+ function buildUnit(d) {
215
+ const nodeBin = dirname(d.nodePath);
216
+ return [
217
+ "[Unit]",
218
+ `Description=cloudtunnel ${d.fqdn} (Cloudflare Tunnel)`,
219
+ "After=network-online.target",
220
+ "Wants=network-online.target",
221
+ "",
222
+ "[Service]",
223
+ "Type=simple",
224
+ `User=${d.user}`,
225
+ `Environment=HOME=${d.home}`,
226
+ `Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
227
+ `ExecStart=${d.nodePath} ${d.scriptPath} ${d.argv.join(" ")}`,
228
+ "Restart=on-failure",
229
+ "RestartSec=5",
230
+ "",
231
+ "[Install]",
232
+ "WantedBy=multi-user.target",
233
+ ""
234
+ ].join("\n");
235
+ }
236
+ function privileged(args) {
237
+ const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
238
+ const argv = isRoot ? args : ["sudo", ...args];
239
+ execFileSync(argv[0], argv.slice(1), { stdio: "inherit" });
240
+ }
241
+ function query(args) {
242
+ try {
243
+ return execFileSync("systemctl", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }).trim();
244
+ } catch (err) {
245
+ const out = err.stdout;
246
+ return out ? out.toString().trim() : "";
247
+ }
248
+ }
249
+ function assertSupported() {
250
+ try {
251
+ execFileSync("systemctl", ["--version"], { stdio: "ignore" });
252
+ } catch {
253
+ throw new CliError("systemd (systemctl) was not found on this host.");
254
+ }
255
+ }
256
+ function install(d) {
257
+ assertSupported();
258
+ const tmp = join2(tmpdir(), label(d.fqdn));
259
+ writeFileSync(tmp, buildUnit(d), { mode: 420 });
260
+ privileged(["install", "-m", "0644", tmp, unitPath(d.fqdn)]);
261
+ privileged(["systemctl", "daemon-reload"]);
262
+ privileged(["systemctl", "enable", "--now", label(d.fqdn)]);
263
+ }
264
+ function uninstall(fqdn) {
265
+ try {
266
+ privileged(["systemctl", "disable", "--now", label(fqdn)]);
267
+ } catch {
268
+ }
269
+ privileged(["rm", "-f", unitPath(fqdn)]);
270
+ privileged(["systemctl", "daemon-reload"]);
271
+ }
272
+ function state(fqdn) {
273
+ const name = label(fqdn);
274
+ if (query(["is-active", name]) === "active") return "active";
275
+ const enabled = query(["is-enabled", name]);
276
+ if (enabled === "enabled" || enabled === "enabled-runtime") return "enabled";
277
+ if (enabled === "disabled" || enabled === "static") return "disabled";
278
+ return "none";
279
+ }
280
+ function legacyUnitExists(profile) {
281
+ return existsSync(`/etc/systemd/system/cloudtunnel-${profile}.service`);
282
+ }
283
+ function removeLegacyUnit(profile) {
284
+ const name = `cloudtunnel-${profile}.service`;
285
+ try {
286
+ privileged(["systemctl", "disable", "--now", name]);
287
+ } catch {
288
+ }
289
+ privileged(["rm", "-f", `/etc/systemd/system/${name}`]);
290
+ privileged(["systemctl", "daemon-reload"]);
291
+ }
292
+
293
+ // src/core/service-launchd.ts
294
+ var service_launchd_exports = {};
295
+ __export(service_launchd_exports, {
296
+ assertSupported: () => assertSupported2,
297
+ buildPlist: () => buildPlist,
298
+ install: () => install2,
299
+ label: () => label2,
300
+ state: () => state2,
301
+ uninstall: () => uninstall2
302
+ });
303
+ import { execFileSync as execFileSync2 } from "child_process";
304
+ import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
305
+ import { dirname as dirname2, join as join3 } from "path";
306
+ import os2 from "os";
307
+ var label2 = (fqdn) => `com.cloudtunnel.${serviceSlug(fqdn)}`;
308
+ var agentsDir = () => join3(os2.homedir(), "Library", "LaunchAgents");
309
+ var plistPath = (fqdn) => join3(agentsDir(), `${label2(fqdn)}.plist`);
310
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
311
+ function buildPlist(d) {
312
+ const args = [d.nodePath, d.scriptPath, ...d.argv].map((a) => ` <string>${xml(a)}</string>`).join("\n");
313
+ const nodeBin = dirname2(d.nodePath);
314
+ const path = `${nodeBin}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
315
+ return [
316
+ '<?xml version="1.0" encoding="UTF-8"?>',
317
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
318
+ '<plist version="1.0">',
319
+ "<dict>",
320
+ ` <key>Label</key><string>${xml(label2(d.fqdn))}</string>`,
321
+ " <key>ProgramArguments</key>",
322
+ " <array>",
323
+ args,
324
+ " </array>",
325
+ " <key>RunAtLoad</key><true/>",
326
+ " <key>KeepAlive</key><true/>",
327
+ " <key>EnvironmentVariables</key>",
328
+ " <dict>",
329
+ ` <key>PATH</key><string>${xml(path)}</string>`,
330
+ ` <key>HOME</key><string>${xml(d.home)}</string>`,
331
+ " </dict>",
332
+ ` <key>StandardOutPath</key><string>${xml(d.logFile)}</string>`,
333
+ ` <key>StandardErrorPath</key><string>${xml(d.logFile)}</string>`,
334
+ "</dict>",
335
+ "</plist>",
336
+ ""
337
+ ].join("\n");
338
+ }
339
+ function launchctl(args) {
340
+ try {
341
+ return execFileSync2("launchctl", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
342
+ } catch (err) {
343
+ const out = err.stdout;
344
+ return out ? out.toString() : "";
345
+ }
346
+ }
347
+ function assertSupported2() {
348
+ }
349
+ function install2(d) {
350
+ ensureDirs();
351
+ mkdirSync(agentsDir(), { recursive: true });
352
+ const plist = plistPath(d.fqdn);
353
+ writeFileSync2(plist, buildPlist(d), { mode: 420 });
354
+ launchctl(["unload", "-w", plist]);
355
+ execFileSync2("launchctl", ["load", "-w", plist], { stdio: "inherit" });
356
+ }
357
+ function uninstall2(fqdn) {
358
+ const plist = plistPath(fqdn);
359
+ launchctl(["unload", "-w", plist]);
360
+ rmSync(plist, { force: true });
361
+ }
362
+ function state2(fqdn) {
363
+ const info = launchctl(["list", label2(fqdn)]);
364
+ if (/"PID"\s*=/.test(info)) return "active";
365
+ return existsSync2(plistPath(fqdn)) ? "enabled" : "none";
366
+ }
367
+
368
+ // src/core/service-windows.ts
369
+ var service_windows_exports = {};
370
+ __export(service_windows_exports, {
371
+ assertSupported: () => assertSupported3,
372
+ buildTaskXml: () => buildTaskXml,
373
+ install: () => install3,
374
+ label: () => label3,
375
+ state: () => state3,
376
+ uninstall: () => uninstall3
377
+ });
378
+ import { execFileSync as execFileSync3 } from "child_process";
379
+ import { writeFileSync as writeFileSync3 } from "fs";
380
+ import { tmpdir as tmpdir2 } from "os";
381
+ import { join as join4 } from "path";
382
+ var label3 = (fqdn) => `cloudtunnel\\${serviceSlug(fqdn)}`;
383
+ var xml2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
384
+ function buildTaskXml(d) {
385
+ const args = `"${d.scriptPath}" ${d.argv.join(" ")}`;
386
+ return [
387
+ '<?xml version="1.0" encoding="UTF-16"?>',
388
+ '<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
389
+ ` <RegistrationInfo><Description>cloudtunnel ${xml2(d.fqdn)} (Cloudflare Tunnel)</Description></RegistrationInfo>`,
390
+ ` <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml2(d.user)}</UserId></LogonTrigger></Triggers>`,
391
+ ` <Principals><Principal id="Author"><UserId>${xml2(d.user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>`,
392
+ " <Settings>",
393
+ " <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>",
394
+ " <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>",
395
+ " <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>",
396
+ " <StartWhenAvailable>true</StartWhenAvailable>",
397
+ " <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
398
+ " <RestartOnFailure><Interval>PT1M</Interval><Count>3</Count></RestartOnFailure>",
399
+ " <Enabled>true</Enabled>",
400
+ " </Settings>",
401
+ ' <Actions Context="Author">',
402
+ ` <Exec><Command>${xml2(d.nodePath)}</Command><Arguments>${xml2(args)}</Arguments></Exec>`,
403
+ " </Actions>",
404
+ "</Task>",
405
+ ""
406
+ ].join("\r\n");
407
+ }
408
+ function schtasks(args) {
409
+ try {
410
+ return execFileSync3("schtasks", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
411
+ } catch (err) {
412
+ const out = err.stdout;
413
+ return out ? out.toString() : "";
414
+ }
415
+ }
416
+ function assertSupported3() {
417
+ }
418
+ function install3(d) {
419
+ const file = join4(tmpdir2(), `${d.slug}.task.xml`);
420
+ writeFileSync3(file, "\uFEFF" + buildTaskXml(d), { encoding: "utf16le" });
421
+ execFileSync3("schtasks", ["/Create", "/TN", label3(d.fqdn), "/XML", file, "/F"], { stdio: "inherit" });
422
+ schtasks(["/Run", "/TN", label3(d.fqdn)]);
423
+ }
424
+ function uninstall3(fqdn) {
425
+ schtasks(["/Delete", "/TN", label3(fqdn), "/F"]);
426
+ }
427
+ function state3(fqdn) {
428
+ const out = schtasks(["/Query", "/TN", label3(fqdn), "/FO", "LIST"]);
429
+ if (!out) return "none";
430
+ if (/\bRunning\b/.test(out)) return "active";
431
+ if (/\bDisabled\b/.test(out)) return "disabled";
432
+ if (/\bReady\b/.test(out)) return "enabled";
433
+ return "enabled";
434
+ }
435
+
436
+ // src/core/service.ts
437
+ function pick() {
438
+ switch (process.platform) {
439
+ case "linux":
440
+ return service_systemd_exports;
441
+ case "darwin":
442
+ return service_launchd_exports;
443
+ case "win32":
444
+ return service_windows_exports;
445
+ default:
446
+ return null;
447
+ }
448
+ }
449
+ function required() {
450
+ const b = pick();
451
+ if (!b) {
452
+ throw new CliError(`Boot services aren't supported on ${process.platform}.`, {
453
+ hint: "run `cloudtunnel up <spec> --detach` and use your OS's own autostart"
454
+ });
455
+ }
456
+ return b;
457
+ }
458
+ function assertServiceSupported() {
459
+ required().assertSupported();
460
+ }
461
+ function serviceName(fqdn) {
462
+ return pick()?.label(fqdn) ?? `cloudtunnel-${fqdn}`;
463
+ }
464
+ function installServiceForSpec(params) {
465
+ const b = required();
466
+ b.assertSupported();
467
+ b.install(describeService(params));
468
+ }
469
+ function uninstallService(fqdn) {
470
+ pick()?.uninstall(fqdn);
471
+ }
472
+ function serviceState(fqdn) {
473
+ return pick()?.state(fqdn) ?? "none";
474
+ }
475
+ function legacyUnitExists2(profile) {
476
+ return process.platform === "linux" ? legacyUnitExists(profile) : false;
477
+ }
478
+ function removeLegacyUnit2(profile) {
479
+ if (process.platform === "linux") removeLegacyUnit(profile);
480
+ }
481
+
482
+ // src/config/legacy-migrate.ts
483
+ var skipMarker = `${profilesFile}.migrate-skip`;
484
+ async function migrateLegacyProfiles() {
485
+ if (!existsSync3(profilesFile) || existsSync3(skipMarker)) return;
486
+ let profiles;
487
+ try {
488
+ profiles = JSON.parse(readFileSync(profilesFile, "utf8"));
489
+ } catch {
490
+ return;
491
+ }
492
+ const legacy = Object.entries(profiles).filter(([name]) => legacyUnitExists2(name));
493
+ if (legacy.length === 0) {
494
+ try {
495
+ renameSync(profilesFile, `${profilesFile}.migrated`);
496
+ } catch {
497
+ }
498
+ return;
499
+ }
500
+ const ok = await confirm(`Found ${legacy.length} boot service(s) from an older cloudtunnel. Migrate them now? (needs sudo)`);
501
+ if (!ok) {
502
+ writeFileSync4(skipMarker, "");
503
+ say.dim(` Skipped. Delete ${skipMarker} to be asked again.`);
504
+ return;
505
+ }
506
+ let migrated = 0;
507
+ try {
508
+ for (const [name, profile] of legacy) {
509
+ for (const svc of profile.services ?? []) {
510
+ const zone = svc.domain ?? profile.domain;
511
+ if (!zone) continue;
512
+ installServiceForSpec({
513
+ subdomain: svc.name,
514
+ port: svc.port,
515
+ host: svc.host,
516
+ zone,
517
+ proto: svc.proto,
518
+ protocol: profile.protocol
519
+ });
520
+ migrated++;
521
+ }
522
+ removeLegacyUnit2(name);
523
+ }
524
+ renameSync(profilesFile, `${profilesFile}.migrated`);
525
+ say.ok(`Migrated ${migrated} boot service(s). See them with: cloudtunnel ls`);
526
+ } catch (err) {
527
+ writeFileSync4(skipMarker, "");
528
+ say.warn(`Migration incomplete: ${err.message}. Won't retry automatically (delete ${skipMarker} to retry).`);
529
+ }
530
+ }
531
+
532
+ // src/commands/login.ts
533
+ import * as clack from "@clack/prompts";
534
+
81
535
  // src/config/token-url.ts
82
536
  import { spawn } from "child_process";
83
537
  var REQUIRED_SCOPES = [
@@ -217,9 +671,7 @@ function registerLogin(program) {
217
671
  }
218
672
 
219
673
  // src/commands/up.ts
220
- import { join as join2 } from "path";
221
- import { readFileSync as readFileSync4 } from "fs";
222
- import * as clack2 from "@clack/prompts";
674
+ import * as clack3 from "@clack/prompts";
223
675
 
224
676
  // src/config/ensure-auth.ts
225
677
  async function ensureAuth() {
@@ -236,10 +688,10 @@ async function ensureAuth() {
236
688
  }
237
689
 
238
690
  // src/connector/binary.ts
239
- import { execFileSync } from "child_process";
691
+ import { execFileSync as execFileSync4 } from "child_process";
240
692
  import { createHash } from "crypto";
241
- import { chmodSync, existsSync, readFileSync, writeFileSync } from "fs";
242
- import { join } from "path";
693
+ import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
694
+ import { join as join5 } from "path";
243
695
  var PINNED_VERSION = "2025.1.0";
244
696
  var RELEASE_BASE = `https://github.com/cloudflare/cloudflared/releases/download/${PINNED_VERSION}`;
245
697
  var ASSETS = {
@@ -251,18 +703,18 @@ var ASSETS = {
251
703
  };
252
704
  function binaryWorks(bin) {
253
705
  try {
254
- execFileSync(bin, ["--version"], { stdio: "ignore" });
706
+ execFileSync4(bin, ["--version"], { stdio: "ignore" });
255
707
  return true;
256
708
  } catch {
257
709
  return false;
258
710
  }
259
711
  }
260
712
  function cachedPath() {
261
- return join(binDir, process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
713
+ return join5(binDir, process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
262
714
  }
263
715
  function isMusl() {
264
716
  try {
265
- return process.platform === "linux" && readFileSync("/usr/bin/ldd", "utf8").includes("musl");
717
+ return process.platform === "linux" && readFileSync2("/usr/bin/ldd", "utf8").includes("musl");
266
718
  } catch {
267
719
  return false;
268
720
  }
@@ -270,7 +722,7 @@ function isMusl() {
270
722
  async function ensureCloudflared() {
271
723
  if (binaryWorks("cloudflared")) return "cloudflared";
272
724
  const cached = cachedPath();
273
- if (existsSync(cached) && binaryWorks(cached)) return cached;
725
+ if (existsSync4(cached) && binaryWorks(cached)) return cached;
274
726
  return downloadCloudflared(cached);
275
727
  }
276
728
  async function downloadCloudflared(dest) {
@@ -298,7 +750,7 @@ async function downloadCloudflared(dest) {
298
750
  }
299
751
  ensureDirs();
300
752
  const binary = asset.archive ? extractTgz(bytes) : bytes;
301
- writeFileSync(dest, binary, { mode: 493 });
753
+ writeFileSync5(dest, binary, { mode: 493 });
302
754
  chmodSync(dest, 493);
303
755
  if (!binaryWorks(dest)) throw new CliError("Downloaded cloudflared is not runnable.");
304
756
  return dest;
@@ -309,26 +761,33 @@ function extractTgz(_bytes) {
309
761
  });
310
762
  }
311
763
 
764
+ // src/core/up-runner.ts
765
+ import { join as join6 } from "path";
766
+ import * as clack2 from "@clack/prompts";
767
+
312
768
  // src/connector/process.ts
313
- import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
769
+ import { execFileSync as execFileSync5, spawn as spawn2 } from "child_process";
314
770
  import { openSync } from "fs";
315
771
 
316
772
  // src/connector/registry.ts
317
- import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
773
+ import { existsSync as existsSync5, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
318
774
  import { readFile } from "fs/promises";
319
- import os from "os";
775
+ import os3 from "os";
320
776
  import lockfile from "proper-lockfile";
777
+ function entryFqdn(e) {
778
+ return e.subdomain === "@" ? e.zone : `${e.subdomain}.${e.zone}`;
779
+ }
321
780
  function currentBootId() {
322
781
  try {
323
- return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
782
+ return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
324
783
  } catch {
325
- const bootMinute = Math.floor((Date.now() - os.uptime() * 1e3) / 6e4);
326
- return `boot-${bootMinute}-${os.hostname()}`;
784
+ const bootMinute = Math.floor((Date.now() - os3.uptime() * 1e3) / 6e4);
785
+ return `boot-${bootMinute}-${os3.hostname()}`;
327
786
  }
328
787
  }
329
788
  function readRegistry() {
330
789
  try {
331
- return JSON.parse(readFileSync2(registryFile, "utf8"));
790
+ return JSON.parse(readFileSync3(registryFile, "utf8"));
332
791
  } catch {
333
792
  return {};
334
793
  }
@@ -336,12 +795,12 @@ function readRegistry() {
336
795
  function writeRegistry(reg) {
337
796
  ensureDirs();
338
797
  const tmp = `${registryFile}.tmp`;
339
- writeFileSync2(tmp, JSON.stringify(reg, null, 2), { mode: 384 });
340
- renameSync(tmp, registryFile);
798
+ writeFileSync6(tmp, JSON.stringify(reg, null, 2), { mode: 384 });
799
+ renameSync2(tmp, registryFile);
341
800
  }
342
801
  async function mutateRegistry(fn) {
343
802
  ensureDirs();
344
- if (!existsSync2(registryFile)) writeFileSync2(registryFile, "{}", { mode: 384 });
803
+ if (!existsSync5(registryFile)) writeFileSync6(registryFile, "{}", { mode: 384 });
345
804
  const release = await lockfile.lock(registryFile, { retries: { retries: 10, minTimeout: 50 } });
346
805
  try {
347
806
  const reg = readRegistry();
@@ -414,7 +873,7 @@ async function reconcile() {
414
873
  const entries = listEntries();
415
874
  for (const entry of entries) {
416
875
  if (entry.state === "running" && !await isOurConnector(entry)) {
417
- const fqdn = `${entry.subdomain}.${entry.zone}`;
876
+ const fqdn = entryFqdn(entry);
418
877
  await mutateRegistry((reg) => {
419
878
  const e = reg[fqdn];
420
879
  if (e) {
@@ -449,7 +908,7 @@ async function stopConnector(entry) {
449
908
  const pid = entry.pid;
450
909
  if (process.platform === "win32") {
451
910
  try {
452
- execFileSync2("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
911
+ execFileSync5("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
453
912
  } catch {
454
913
  return false;
455
914
  }
@@ -542,33 +1001,6 @@ async function waitHealthy(cf, tunnelId, opts = {}) {
542
1001
  // src/core/orchestrator-create.ts
543
1002
  import { randomInt as randomInt2 } from "crypto";
544
1003
 
545
- // src/core/ingress.ts
546
- var HOSTNAME_RE = /^[a-zA-Z0-9.-]+$/;
547
- var IPV6_RE = /^[0-9a-fA-F:.]+$/;
548
- function validateHost(host) {
549
- let h = host.trim();
550
- const bracketed = h.startsWith("[") && h.endsWith("]");
551
- if (bracketed) h = h.slice(1, -1);
552
- const isV6 = bracketed || h.includes("::") || (h.match(/:/g)?.length ?? 0) >= 2;
553
- const ok = h.length > 0 && (isV6 ? IPV6_RE.test(h) : HOSTNAME_RE.test(h));
554
- if (!ok) {
555
- throw new CliError(`Invalid host "${host}".`, {
556
- hint: "use a hostname, IPv4, or IPv6 literal (e.g. 192.168.1.5 or ::1) \u2014 no port, scheme, or path"
557
- });
558
- }
559
- return h;
560
- }
561
- function serviceUrl(proto, host, port) {
562
- const authority = host.includes(":") ? `[${host}]` : host;
563
- return `${proto}://${authority}:${port}`;
564
- }
565
- function buildIngress(opts) {
566
- return [
567
- { hostname: opts.hostname, service: serviceUrl(opts.proto, opts.host ?? "localhost", opts.port) },
568
- { service: "http_status:404" }
569
- ];
570
- }
571
-
572
1004
  // src/core/slug.ts
573
1005
  import { randomInt } from "crypto";
574
1006
  var ADJECTIVES = [
@@ -607,10 +1039,10 @@ var NOUNS = [
607
1039
  "tiger",
608
1040
  "walnut"
609
1041
  ];
610
- var pick = (arr) => arr[randomInt(arr.length)];
1042
+ var pick2 = (arr) => arr[randomInt(arr.length)];
611
1043
  function randomSlug() {
612
1044
  const suffix = randomInt(65536).toString(16).padStart(4, "0");
613
- return `${pick(ADJECTIVES)}-${pick(NOUNS)}-${suffix}`;
1045
+ return `${pick2(ADJECTIVES)}-${pick2(NOUNS)}-${suffix}`;
614
1046
  }
615
1047
  function resolveHostSpec(opts, defaultZone) {
616
1048
  if (opts.hostname) {
@@ -667,8 +1099,8 @@ async function createTunnelSubdomain(cf, opts) {
667
1099
  let dnsRecordId;
668
1100
  try {
669
1101
  const suffix = randomInt2(65536).toString(16).padStart(4, "0");
670
- const label = host.subdomain === "@" ? "root" : host.subdomain;
671
- const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${label}-${suffix}`);
1102
+ const label4 = host.subdomain === "@" ? "root" : host.subdomain;
1103
+ const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${label4}-${suffix}`);
672
1104
  tunnelId = tunnel.id;
673
1105
  const token = await getTunnelToken(cf, tunnelId);
674
1106
  await putIngress(cf, tunnelId, buildIngress({ hostname: host.hostname, port: opts.port, proto: opts.proto, host: opts.host }));
@@ -738,20 +1170,20 @@ function resolveTarget(target) {
738
1170
  const entries = listEntries();
739
1171
  if (/^\d+$/.test(target)) {
740
1172
  const byIndex = entries.find((e) => e.index === Number(target));
741
- if (byIndex) return { fqdn: `${byIndex.subdomain}.${byIndex.zone}`, entry: byIndex };
1173
+ if (byIndex) return { fqdn: entryFqdn(byIndex), entry: byIndex };
742
1174
  }
743
1175
  const byId = entries.filter((e) => e.tunnelId?.startsWith(target));
744
1176
  const matches = byId.length > 0 ? byId : entries.filter((e) => e.subdomain === target);
745
1177
  if (matches.length > 1) {
746
1178
  throw new CliError(`"${target}" matches multiple subdomains.`, {
747
- hint: `use a full hostname or a longer id: ${matches.map((m) => `${m.subdomain}.${m.zone}`).join(", ")}`
1179
+ hint: `use a full hostname or a longer id: ${matches.map(entryFqdn).join(", ")}`
748
1180
  });
749
1181
  }
750
1182
  const entry = matches[0];
751
1183
  if (!entry) {
752
1184
  throw new CliError(`No tracked subdomain matching "${target}".`, { hint: "see `cloudtunnel ls` for the #, name, or id" });
753
1185
  }
754
- return { fqdn: `${entry.subdomain}.${entry.zone}`, entry };
1186
+ return { fqdn: entryFqdn(entry), entry };
755
1187
  }
756
1188
  async function removeTunnelSubdomain(cf, target, opts = {}) {
757
1189
  const { fqdn, entry } = resolveTarget(target);
@@ -801,24 +1233,27 @@ async function listAll(cf, opts = {}) {
801
1233
  const entries = await reconcile();
802
1234
  const tunnels = new Map((await listTunnels(cf)).map((t) => [t.id, t]));
803
1235
  const rows = entries.map((e) => {
1236
+ const fqdn = entryFqdn(e);
804
1237
  const gone = e.tunnelId ? !tunnels.has(e.tunnelId) : false;
1238
+ const svc = serviceState(fqdn);
805
1239
  return {
806
1240
  num: e.index ? String(e.index) : "-",
807
- hostname: `${e.subdomain}.${e.zone}`,
808
- port: serviceUrl(e.proto, e.host ?? "localhost", e.port),
1241
+ url: `https://${fqdn}`,
1242
+ target: serviceUrl(e.proto, e.host ?? "localhost", e.port),
809
1243
  state: !gone && e.state === "running" ? "up" : "down",
1244
+ service: svc === "none" ? "-" : svc,
810
1245
  pid: e.state === "running" && e.pid ? String(e.pid) : "-",
811
1246
  managed: true
812
1247
  };
813
1248
  });
814
1249
  if (opts.all) {
815
- const { listCargoCnames } = await import("./dns-PAPFSYFP.js");
816
- const { listZones: listZones3 } = await import("./zones-YNGQYXAF.js");
817
- const tracked = new Set(entries.map((e) => `${e.subdomain}.${e.zone}`));
1250
+ const { listCargoCnames } = await import("./dns-5OXFAQ4D.js");
1251
+ const { listZones: listZones3 } = await import("./zones-QYV3DSEY.js");
1252
+ const tracked = new Set(entries.map(entryFqdn));
818
1253
  for (const zone of await listZones3(cf.token)) {
819
1254
  for (const rec of await listCargoCnames(cf.token, zone.id)) {
820
1255
  if (!tracked.has(rec.name)) {
821
- rows.push({ num: "-", hostname: rec.name, port: "-", state: "unmanaged", pid: "-", managed: false });
1256
+ rows.push({ num: "-", url: `https://${rec.name}`, target: "-", state: "unmanaged", service: "-", pid: "-", managed: false });
822
1257
  }
823
1258
  }
824
1259
  }
@@ -826,70 +1261,108 @@ async function listAll(cf, opts = {}) {
826
1261
  return rows;
827
1262
  }
828
1263
 
829
- // src/core/profiles.ts
830
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
831
- function parseTransportProtocol(value) {
832
- if (value === "auto" || value === "http2" || value === "quic") return value;
833
- throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
1264
+ // src/core/up-runner.ts
1265
+ function logFileFor(subdomain) {
1266
+ return join6(logDir, `${subdomain === "@" ? "root" : subdomain}.log`);
834
1267
  }
835
- function readProfiles() {
836
- try {
837
- return JSON.parse(readFileSync3(profilesFile, "utf8"));
838
- } catch {
839
- return {};
840
- }
841
- }
842
- function writeProfiles(profiles) {
843
- ensureDirs();
844
- writeFileSync3(profilesFile, JSON.stringify(profiles, null, 2), { mode: 384 });
845
- }
846
- function listProfiles() {
847
- return Object.entries(readProfiles()).map(([name, profile]) => ({ name, profile }));
848
- }
849
- function getProfile(name) {
850
- const profile = readProfiles()[name];
851
- if (!profile) {
852
- throw new CliError(`No profile named "${name}".`, { hint: "list them with `cloudtunnel profiles`" });
1268
+ async function startTunnels(cf, bin, items, opts = {}) {
1269
+ const started = [];
1270
+ let tornDown = false;
1271
+ const teardownAll = async (code) => {
1272
+ if (tornDown) return;
1273
+ tornDown = true;
1274
+ try {
1275
+ for (const s of started) {
1276
+ try {
1277
+ await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
1278
+ } catch {
1279
+ }
1280
+ }
1281
+ if (process.stdout.isTTY) clack2.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
1282
+ } catch (err) {
1283
+ reportError(err);
1284
+ } finally {
1285
+ process.exit(code);
1286
+ }
1287
+ };
1288
+ const spin = clack2.spinner();
1289
+ spin.start(items.length > 1 ? "Creating tunnels\u2026" : "Creating tunnel\u2026");
1290
+ for (const item of items) {
1291
+ spin.message(`Creating ${item.name ?? "tunnel"} (:${item.port})\u2026`);
1292
+ const result = await createTunnelSubdomain(cf, item);
1293
+ const fqdn = result.host.hostname;
1294
+ const logFile = logFileFor(result.host.subdomain);
1295
+ const conn = startConnector({
1296
+ bin,
1297
+ token: result.token,
1298
+ detach: !!opts.detach,
1299
+ logFile,
1300
+ protocol: opts.protocol,
1301
+ onExit: opts.detach ? void 0 : (code) => {
1302
+ if (!tornDown) {
1303
+ say.warn(`Connector for ${fqdn} exited.`);
1304
+ void teardownAll(code ?? 1);
1305
+ }
1306
+ }
1307
+ });
1308
+ await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile });
1309
+ started.push({
1310
+ fqdn,
1311
+ subdomain: result.host.subdomain,
1312
+ tunnelId: result.tunnelId,
1313
+ target: serviceUrl(item.proto, item.host ?? "localhost", item.port),
1314
+ pid: conn.pid
1315
+ });
853
1316
  }
854
- return profile;
855
- }
856
- function saveProfile(name, profile) {
857
- const profiles = readProfiles();
858
- profiles[name] = profile;
859
- writeProfiles(profiles);
860
- }
861
- function removeProfile(name) {
862
- const profiles = readProfiles();
863
- if (!profiles[name]) throw new CliError(`No profile named "${name}".`);
864
- delete profiles[name];
865
- writeProfiles(profiles);
866
- }
867
- function parseServiceSpec(spec) {
868
- const at = spec.indexOf("@");
869
- const host = at >= 0 ? validateHost(spec.slice(at + 1)) : void 0;
870
- const [name, portStr, proto] = (at >= 0 ? spec.slice(0, at) : spec).split(":");
871
- const port = Number(portStr);
872
- if (!name || !Number.isInteger(port) || port < 1 || port > 65535) {
873
- throw new CliError(`Invalid service "${spec}".`, { hint: "use name:port, e.g. api:3000 or web:5173:https" });
1317
+ if (opts.detach) {
1318
+ spin.stop(`${started.length} tunnel(s) started in the background`);
1319
+ const lines2 = started.map((s) => `${formatRoute(s.fqdn, s.target)} ${dim(`pid ${s.pid}`)}`);
1320
+ clack2.note(lines2.join("\n"), "running in background");
1321
+ if (process.stdout.isTTY) clack2.outro("Stop with: cloudtunnel delete <#|--all>");
1322
+ return;
874
1323
  }
875
- if (proto && proto !== "http" && proto !== "https") {
876
- throw new CliError(`Invalid protocol "${proto}" in "${spec}".`, { hint: "proto must be http or https" });
1324
+ for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
1325
+ process.on(sig, () => void teardownAll(0));
877
1326
  }
878
- return { name, port, proto: proto ?? "http", ...host ? { host } : {} };
1327
+ spin.message("Connecting to the Cloudflare edge\u2026");
1328
+ const healths = await Promise.all(started.map((s) => waitHealthy(cf, s.tunnelId, { timeoutMs: 3e4 })));
1329
+ const live = healths.filter((h) => h === "healthy").length;
1330
+ spin.stop(`${started.length} tunnel(s) started`);
1331
+ const lines = started.map((s, i) => `${formatRoute(s.fqdn, s.target)}${healths[i] === "healthy" ? "" : dim(` (${healths[i]})`)}`);
1332
+ clack2.note(lines.join("\n"), `${live}/${started.length} live`);
1333
+ say.dim("Ctrl-C stops and releases them.");
1334
+ }
1335
+
1336
+ // src/core/transport-protocol.ts
1337
+ function parseTransportProtocol(value) {
1338
+ if (value === "auto" || value === "http2" || value === "quic") return value;
1339
+ throw new CliError(`Invalid protocol "${value}".`, { hint: "use auto, http2, or quic" });
879
1340
  }
880
1341
 
881
1342
  // src/commands/up.ts
882
- function parsePort(port) {
883
- const n = Number(port);
884
- if (!Number.isInteger(n) || n < 1 || n > 65535) {
885
- throw new CliError(`Invalid port: ${port}`, { hint: "use a number 1\u201365535, e.g. `cloudtunnel 3000`" });
1343
+ function promptOrExit(value) {
1344
+ if (clack3.isCancel(value)) {
1345
+ clack3.cancel("Cancelled.");
1346
+ process.exit(130);
886
1347
  }
887
- return n;
1348
+ return value;
1349
+ }
1350
+ async function promptPort() {
1351
+ const input = promptOrExit(
1352
+ await clack3.text({
1353
+ message: "Port to expose",
1354
+ placeholder: "e.g. 3000",
1355
+ validate: (v) => {
1356
+ const n = Number(v);
1357
+ if (!Number.isInteger(n) || n < 1 || n > 65535) return "Enter a port 1\u201365535";
1358
+ return void 0;
1359
+ }
1360
+ })
1361
+ );
1362
+ return Number(input);
888
1363
  }
889
1364
  async function resolveDomain(cf, opts, creds) {
890
- if (opts.hostname) return void 0;
891
- const explicit = opts.domain ?? opts.zone;
892
- if (explicit) return explicit;
1365
+ if (opts.domain) return opts.domain;
893
1366
  const zones = await listZones(cf.token);
894
1367
  if (zones.length === 0) throw new CliError("No domains found in this Cloudflare account.");
895
1368
  if (zones.length === 1) return zones[0].name;
@@ -897,111 +1370,66 @@ async function resolveDomain(cf, opts, creds) {
897
1370
  if (creds.defaultZone) return creds.defaultZone;
898
1371
  throw new CliError("Multiple domains in this account \u2014 pick one.", { hint: "pass -d <domain>" });
899
1372
  }
900
- async function resolveSubdomain(opts) {
901
- const explicit = opts.subdomain ?? opts.name;
902
- if (explicit || opts.hostname) return explicit;
903
- if (!process.stdin.isTTY) return void 0;
904
- const input = await clack2.text({ message: "Subdomain", placeholder: "blank = random \xB7 @ = root domain" });
905
- if (clack2.isCancel(input)) {
906
- clack2.cancel("Cancelled.");
907
- process.exit(130);
908
- }
1373
+ async function resolveSpecSubdomain(spec, opts) {
1374
+ if (spec.subdomain !== void 0) return spec.subdomain;
1375
+ if (opts.yes || !process.stdin.isTTY) return void 0;
1376
+ const input = promptOrExit(
1377
+ await clack3.text({ message: `Subdomain for :${spec.port}`, placeholder: "blank = random \xB7 @ = root domain" })
1378
+ );
909
1379
  return input.trim() || void 0;
910
1380
  }
911
- function showLogTail(logFile) {
912
- try {
913
- const tail = readFileSync4(logFile, "utf8").trim().split("\n").slice(-8).join("\n");
914
- if (tail) say.dim(tail);
915
- } catch {
916
- }
917
- }
918
- async function runUp(portArg, opts) {
919
- const port = parsePort(portArg);
1381
+ async function runUp(specArgs, opts) {
920
1382
  const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : void 0;
921
- const host = opts.source ? validateHost(opts.source) : void 0;
1383
+ const parsed = specArgs.length ? specArgs.map(parseTunnelSpec) : null;
1384
+ if (parsed === null && !process.stdin.isTTY) {
1385
+ throw new CliError("No tunnel spec given.", { hint: "e.g. cloudtunnel api:8080" });
1386
+ }
922
1387
  const creds = await ensureAuth();
923
1388
  const cf = resolveCf();
924
1389
  const bin = await ensureCloudflared();
925
- if (process.stdout.isTTY) clack2.intro("cloudtunnel");
1390
+ if (process.stdout.isTTY) clack3.intro("cloudtunnel");
1391
+ const specs = parsed ?? [{ port: await promptPort() }];
926
1392
  const domain = await resolveDomain(cf, opts, creds);
927
- const subdomain = await resolveSubdomain(opts);
928
- const result = await createTunnelSubdomain(cf, {
929
- port,
930
- proto: opts.proto,
931
- name: subdomain,
932
- zone: domain,
933
- hostname: opts.hostname,
934
- host,
935
- defaultZone: creds.defaultZone,
936
- force: opts.force,
937
- yes: opts.yes
938
- });
939
- const fqdn = result.host.hostname;
940
- const logLabel = result.host.subdomain === "@" ? "root" : result.host.subdomain;
941
- const logFile = join2(logDir, `${logLabel}.log`);
942
- const target = serviceUrl(opts.proto, host ?? "localhost", port);
943
- if (opts.detach) {
944
- const started2 = startConnector({ bin, token: result.token, detach: true, logFile, protocol });
945
- await patchEntry(fqdn, { pid: started2.pid, bootId: currentBootId(), logFile });
946
- clack2.note(formatRoute(fqdn, target), `pid ${started2.pid}`);
947
- if (process.stdout.isTTY) clack2.outro(`Stop it with: cloudtunnel down ${result.host.subdomain}`);
1393
+ const items = [];
1394
+ for (const spec of specs) {
1395
+ let name = await resolveSpecSubdomain(spec, opts);
1396
+ if (opts.service && name === void 0) name = randomSlug();
1397
+ items.push({
1398
+ port: spec.port,
1399
+ proto: opts.proto,
1400
+ name,
1401
+ zone: domain,
1402
+ host: spec.host,
1403
+ defaultZone: creds.defaultZone,
1404
+ force: opts.force,
1405
+ yes: opts.yes
1406
+ });
1407
+ }
1408
+ if (opts.service) {
1409
+ registerServices(items, domain, opts.proto, protocol);
948
1410
  return;
949
1411
  }
950
- const spin = clack2.spinner();
951
- let spinnerActive = true;
952
- const stopSpin = (msg) => {
953
- if (spinnerActive) {
954
- spinnerActive = false;
955
- spin.stop(msg);
956
- }
957
- };
958
- spin.start("Connecting to the Cloudflare edge\u2026");
959
- const controller = new AbortController();
960
- let tornDown = false;
961
- const teardown = async (exitCode) => {
962
- if (tornDown) return;
963
- tornDown = true;
964
- controller.abort();
965
- stopSpin("Stopping\u2026");
966
- try {
967
- await removeTunnelSubdomain(cf, fqdn, { force: true, quiet: true });
968
- clack2.outro(`Stopped \xB7 ${fqdn} released`);
969
- } catch (err) {
970
- reportError(err);
971
- } finally {
972
- process.exit(exitCode);
973
- }
974
- };
975
- const started = startConnector({
976
- bin,
977
- token: result.token,
978
- detach: false,
979
- logFile,
980
- protocol,
981
- onExit: (code) => {
982
- if (!tornDown) {
983
- stopSpin("cloudflared exited");
984
- showLogTail(logFile);
985
- void teardown(code ?? 1);
986
- }
987
- }
988
- });
989
- await patchEntry(fqdn, { pid: started.pid, bootId: currentBootId(), logFile });
990
- for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
991
- process.on(sig, () => void teardown(0));
1412
+ await startTunnels(cf, bin, items, { detach: opts.detach, protocol });
1413
+ }
1414
+ function registerServices(items, domain, proto, protocol) {
1415
+ assertServiceSupported();
1416
+ if (!protocol) {
1417
+ say.warn("No edge protocol set \u2014 cloudflared will pick QUIC, which some networks drop.");
1418
+ say.dim(" \u2192 add --protocol http2 for UDP-hostile networks");
992
1419
  }
993
- const health = await waitHealthy(cf, result.tunnelId, { signal: controller.signal });
994
- if (health === "healthy") {
995
- stopSpin("Connected");
996
- clack2.note(`${formatRoute(fqdn, target)}
997
- ${dim("Ctrl-C stops and releases this subdomain")}`, "Live");
998
- } else if (health === "provisioning") {
999
- stopSpin("Provisioning");
1000
- say.warn(`${fqdn} is not healthy yet \u2014 it should be live shortly.`);
1420
+ const done = [];
1421
+ for (const item of items) {
1422
+ const subdomain = item.name;
1423
+ const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
1424
+ installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
1425
+ done.push(`${serviceName(fqdn)} \u2192 https://${fqdn}`);
1001
1426
  }
1427
+ say.ok(`Registered ${done.length} boot service(s):`);
1428
+ for (const line of done) say.dim(` ${line}`);
1429
+ say.dim(" \u2192 check them: cloudtunnel ls \xB7 remove: cloudtunnel delete <#>");
1002
1430
  }
1003
1431
  function registerUp(program) {
1004
- program.command("up").argument("<port>", "local port to expose (e.g. 3000)").description("Expose a local port at an HTTPS subdomain (also: `cloudtunnel <port>`)").option("-s, --subdomain <name>", "subdomain label (prompted, or random if left blank)").option("-d, --domain <domain>", "domain to create the subdomain under (prompted from a list if unset)").option("--name <name>", "alias of --subdomain").option("--zone <domain>", "alias of --domain").option("--hostname <fqdn>", "full hostname override (instead of --subdomain + --domain)").option("--source <host>", "forward to this host/IP instead of localhost (e.g. a LAN device or ::1)").option("--detach", "run the connector in the background").option("-f, --force", "replace a non-tunnel DNS record occupying the hostname").option("-y, --yes", "don't ask before replacing an existing record").option("--proto <proto>", "local service protocol: http | https", "http").option("--protocol <proto>", "cloudflared edge transport: auto | http2 | quic (http2 for UDP-hostile networks)").action((port, opts) => runUp(port, opts));
1432
+ 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 boot service (Linux systemd \xB7 macOS launchd \xB7 Windows Task Scheduler)").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));
1005
1433
  }
1006
1434
 
1007
1435
  // src/commands/ls.ts
@@ -1015,15 +1443,23 @@ function registerLs(program) {
1015
1443
  return;
1016
1444
  }
1017
1445
  printTable(
1018
- ["#", "SUBDOMAIN", "TARGET", "STATE", "PID"],
1019
- rows.map((r) => [r.num, r.hostname, r.port, r.state, r.pid])
1446
+ ["#", "URL", "TARGET", "STATE", "SERVICE", "PID"],
1447
+ rows.map((r) => [r.num, r.url, r.target, r.state, r.service, r.pid])
1020
1448
  );
1021
1449
  });
1022
1450
  }
1023
1451
 
1024
- // src/commands/down.ts
1025
- function registerDown(program) {
1026
- program.command("down").aliases(["rm", "remove", "delete", "stop"]).argument("[target]", "subdomain name / hostname / id / # to release (omit with --all)").description("Stop and release a subdomain \u2014 removes the tunnel + DNS on Cloudflare").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 (target, opts) => {
1452
+ // src/commands/delete.ts
1453
+ async function deleteOne(cf, fqdn, opts) {
1454
+ const hasService = serviceState(fqdn) !== "none";
1455
+ if (hasService && !opts.dryRun) uninstallService(fqdn);
1456
+ await removeTunnelSubdomain(cf, fqdn, { force: opts.force, dryRun: opts.dryRun });
1457
+ if (!hasService) return;
1458
+ if (opts.dryRun) say.info(`Would also remove boot service ${serviceName(fqdn)}`);
1459
+ else say.ok(`Removed boot service ${serviceName(fqdn)}`);
1460
+ }
1461
+ function registerDelete(program) {
1462
+ 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) => {
1027
1463
  await ensureAuth();
1028
1464
  const cf = resolveCf();
1029
1465
  if (opts.all) {
@@ -1033,259 +1469,27 @@ function registerDown(program) {
1033
1469
  return;
1034
1470
  }
1035
1471
  for (const e of entries) {
1472
+ const fqdn = entryFqdn(e);
1036
1473
  try {
1037
- await removeTunnelSubdomain(cf, `${e.subdomain}.${e.zone}`, { force: opts.force, dryRun: opts.dryRun });
1474
+ await deleteOne(cf, fqdn, opts);
1038
1475
  } catch (err) {
1039
- say.warn(`Could not release ${e.subdomain}.${e.zone}: ${err.message}`);
1476
+ say.warn(`Could not release ${fqdn}: ${err.message}`);
1040
1477
  }
1041
1478
  }
1042
1479
  return;
1043
1480
  }
1044
- if (!target) throw new CliError("Pass a subdomain (name / id / #) or --all.");
1045
- await removeTunnelSubdomain(cf, target, { force: opts.force, dryRun: opts.dryRun });
1046
- });
1047
- }
1048
-
1049
- // src/commands/zones.ts
1050
- function registerZones(program) {
1051
- program.command("zones").description("List the zones (domains) available in your Cloudflare account").action(async () => {
1052
- await ensureAuth();
1053
- const cf = resolveCf();
1054
- const zones = await listZones(cf.token);
1055
- if (zones.length === 0) {
1056
- say.info("No zones in this account.");
1057
- return;
1058
- }
1059
- printTable(
1060
- ["ZONE", "STATUS", "ID"],
1061
- zones.map((z) => [z.name, z.status ?? "-", z.id])
1062
- );
1063
- });
1064
- }
1065
-
1066
- // src/commands/save.ts
1067
- function registerSave(program) {
1068
- 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) => {
1069
- let services;
1070
- if (opts.fromRunning) {
1071
- const entries = listEntries().filter((e) => e.tunnelId);
1072
- if (entries.length === 0) {
1073
- throw new CliError("No tunnels to snapshot.", { hint: "start some with `cloudtunnel up`, or pass services like api:3000" });
1074
- }
1075
- services = entries.map((e) => ({ name: e.subdomain, port: e.port, proto: e.proto, domain: e.zone, ...e.host ? { host: e.host } : {} }));
1076
- } else {
1077
- if (specs.length === 0) {
1078
- throw new CliError("No services given.", { hint: "e.g. `cloudtunnel save mb api:3000 web:5173`" });
1079
- }
1080
- services = specs.map(parseServiceSpec);
1481
+ if (targets.length === 0) throw new CliError("Pass a subdomain (# / name / URL) or --all.");
1482
+ for (const target of targets) {
1483
+ const { fqdn } = resolveTarget(target);
1484
+ await deleteOne(cf, fqdn, opts);
1081
1485
  }
1082
- const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : void 0;
1083
- saveProfile(profile, { services, domain: opts.domain, protocol });
1084
- say.ok(`Saved profile "${profile}" (${services.length} service${services.length === 1 ? "" : "s"}). Run it: cloudtunnel run ${profile}`);
1085
- });
1086
- }
1087
-
1088
- // src/commands/run.ts
1089
- import { join as join3 } from "path";
1090
- import * as clack3 from "@clack/prompts";
1091
- async function runProfile(name, opts) {
1092
- const creds = await ensureAuth();
1093
- const cf = resolveCf();
1094
- const bin = await ensureCloudflared();
1095
- const profile = getProfile(name);
1096
- const protocol = opts.protocol ? parseTransportProtocol(opts.protocol) : profile.protocol;
1097
- if (process.stdout.isTTY) clack3.intro(`cloudtunnel \xB7 profile "${name}"`);
1098
- const spin = clack3.spinner();
1099
- spin.start("Creating tunnels\u2026");
1100
- const started = [];
1101
- for (const svc of profile.services) {
1102
- spin.message(`Creating ${svc.name} (:${svc.port})\u2026`);
1103
- const result = await createTunnelSubdomain(cf, {
1104
- port: svc.port,
1105
- proto: svc.proto,
1106
- name: svc.name,
1107
- host: svc.host,
1108
- zone: svc.domain ?? opts.domain ?? profile.domain,
1109
- defaultZone: creds.defaultZone,
1110
- force: opts.force,
1111
- yes: true
1112
- // batch: never prompt per service
1113
- });
1114
- const fqdn = result.host.hostname;
1115
- const logFile = join3(logDir, `${result.host.subdomain}.log`);
1116
- const conn = startConnector({
1117
- bin,
1118
- token: result.token,
1119
- detach: !!opts.detach,
1120
- logFile,
1121
- protocol,
1122
- onExit: opts.detach ? void 0 : () => say.warn(`Connector for ${fqdn} exited.`)
1123
- });
1124
- await patchEntry(fqdn, { pid: conn.pid, bootId: currentBootId(), logFile });
1125
- started.push({ fqdn, subdomain: result.host.subdomain, tunnelId: result.tunnelId, target: serviceUrl(svc.proto, svc.host ?? "localhost", svc.port), pid: conn.pid });
1126
- }
1127
- if (opts.detach) {
1128
- spin.stop(`${started.length} service(s) started in the background`);
1129
- const lines2 = started.map((s) => `${formatRoute(s.fqdn, s.target)} ${dim(`pid ${s.pid}`)}`);
1130
- clack3.note(lines2.join("\n"), `profile "${name}" \u2014 running in background`);
1131
- if (process.stdout.isTTY) clack3.outro("Stop them with: cloudtunnel down --all");
1132
- return;
1133
- }
1134
- spin.message("Connecting to the Cloudflare edge\u2026");
1135
- const healths = await Promise.all(started.map((s) => waitHealthy(cf, s.tunnelId, { timeoutMs: 3e4 })));
1136
- const live = healths.filter((h) => h === "healthy").length;
1137
- spin.stop(`${started.length} service(s) started`);
1138
- const lines = started.map((s, i) => `${formatRoute(s.fqdn, s.target)}${healths[i] === "healthy" ? "" : dim(` (${healths[i]})`)}`);
1139
- clack3.note(lines.join("\n"), `profile "${name}" \u2014 ${live}/${started.length} live`);
1140
- say.dim("Ctrl-C stops and releases all of them.");
1141
- let tornDown = false;
1142
- const teardownAll = async (code) => {
1143
- if (tornDown) return;
1144
- tornDown = true;
1145
- try {
1146
- for (const s of started) {
1147
- try {
1148
- await removeTunnelSubdomain(cf, s.fqdn, { force: true, quiet: true });
1149
- } catch {
1150
- }
1151
- }
1152
- if (process.stdout.isTTY) clack3.outro(`Stopped \xB7 released ${started.length} subdomain(s)`);
1153
- } catch (err) {
1154
- reportError(err);
1155
- } finally {
1156
- process.exit(code);
1157
- }
1158
- };
1159
- for (const sig of ["SIGINT", "SIGHUP", "SIGTERM"]) {
1160
- process.on(sig, () => void teardownAll(0));
1161
- }
1162
- }
1163
- function registerRun(program) {
1164
- 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));
1165
- }
1166
-
1167
- // src/core/systemd.ts
1168
- import { execFileSync as execFileSync3 } from "child_process";
1169
- import { writeFileSync as writeFileSync4 } from "fs";
1170
- import { tmpdir } from "os";
1171
- import { dirname, join as join4 } from "path";
1172
- function serviceName(profile) {
1173
- return `cloudtunnel-${profile}.service`;
1174
- }
1175
- function unitPath(profile) {
1176
- return `/etc/systemd/system/${serviceName(profile)}`;
1177
- }
1178
- function buildUnit(p) {
1179
- const nodeBin = dirname(p.nodePath);
1180
- const proto = p.protocol ? ` --protocol ${p.protocol}` : "";
1181
- return [
1182
- "[Unit]",
1183
- `Description=cloudtunnel profile "${p.profile}" (Cloudflare Tunnel)`,
1184
- "After=network-online.target",
1185
- "Wants=network-online.target",
1186
- "",
1187
- "[Service]",
1188
- "Type=simple",
1189
- `User=${p.user}`,
1190
- `Environment=HOME=${p.home}`,
1191
- `Environment=PATH=${nodeBin}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
1192
- `ExecStart=${p.nodePath} ${p.scriptPath} run ${p.profile} -f${proto}`,
1193
- "Restart=on-failure",
1194
- "RestartSec=5",
1195
- "",
1196
- "[Install]",
1197
- "WantedBy=multi-user.target",
1198
- ""
1199
- ].join("\n");
1200
- }
1201
- function assertSystemd() {
1202
- if (process.platform !== "linux") {
1203
- throw new CliError("Service registration is Linux/systemd only.", {
1204
- hint: "on macOS/Windows run `cloudtunnel run <profile> --detach` at login instead"
1205
- });
1206
- }
1207
- try {
1208
- execFileSync3("systemctl", ["--version"], { stdio: "ignore" });
1209
- } catch {
1210
- throw new CliError("systemd (systemctl) was not found on this host.");
1211
- }
1212
- }
1213
- function privileged(args) {
1214
- const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
1215
- const argv = isRoot ? args : ["sudo", ...args];
1216
- execFileSync3(argv[0], argv.slice(1), { stdio: "inherit" });
1217
- }
1218
- function query(args) {
1219
- try {
1220
- return execFileSync3("systemctl", args, {
1221
- stdio: ["ignore", "pipe", "ignore"],
1222
- encoding: "utf8"
1223
- }).trim();
1224
- } catch (err) {
1225
- const out = err.stdout;
1226
- return out ? out.toString().trim() : "";
1227
- }
1228
- }
1229
- function installService(p) {
1230
- assertSystemd();
1231
- const tmp = join4(tmpdir(), serviceName(p.profile));
1232
- writeFileSync4(tmp, buildUnit(p), { mode: 420 });
1233
- privileged(["install", "-m", "0644", tmp, unitPath(p.profile)]);
1234
- privileged(["systemctl", "daemon-reload"]);
1235
- privileged(["systemctl", "enable", "--now", serviceName(p.profile)]);
1236
- }
1237
- function uninstallService(profile) {
1238
- assertSystemd();
1239
- try {
1240
- privileged(["systemctl", "disable", "--now", serviceName(profile)]);
1241
- } catch {
1242
- }
1243
- privileged(["rm", "-f", unitPath(profile)]);
1244
- privileged(["systemctl", "daemon-reload"]);
1245
- }
1246
- function serviceState(profile) {
1247
- if (process.platform !== "linux") return "none";
1248
- const name = serviceName(profile);
1249
- if (query(["is-active", name]) === "active") return "active";
1250
- const enabled = query(["is-enabled", name]);
1251
- if (enabled === "enabled" || enabled === "enabled-runtime") return "enabled";
1252
- if (enabled === "disabled" || enabled === "static") return "disabled";
1253
- return "none";
1254
- }
1255
-
1256
- // src/commands/profiles.ts
1257
- function formatService(state) {
1258
- return state === "none" ? dim("\u2013") : state;
1259
- }
1260
- function registerProfiles(program) {
1261
- program.command("profiles").description("List saved profiles (or delete one with --rm)").option("--rm <name>", "delete a profile").action((opts) => {
1262
- if (opts.rm) {
1263
- removeProfile(opts.rm);
1264
- say.ok(`Deleted profile "${opts.rm}".`);
1265
- return;
1266
- }
1267
- const profiles = listProfiles();
1268
- if (profiles.length === 0) {
1269
- say.info("No profiles yet. Create one: `cloudtunnel save mb api:3000 web:5173`");
1270
- return;
1271
- }
1272
- printTable(
1273
- ["PROFILE", "SERVICES", "DOMAIN", "PROTOCOL", "SERVICE"],
1274
- profiles.map(({ name, profile }) => [
1275
- name,
1276
- profile.services.map((s) => `${s.name}:${s.port}${s.host ? `@${s.host}` : ""}`).join(", "),
1277
- profile.domain ?? "(default)",
1278
- profile.protocol ?? "auto",
1279
- formatService(serviceState(name))
1280
- ])
1281
- );
1282
1486
  });
1283
1487
  }
1284
1488
 
1285
1489
  // src/commands/logs.ts
1286
- import { closeSync, existsSync as existsSync3, openSync as openSync2, readFileSync as readFileSync5, readSync, statSync, watch } from "fs";
1490
+ import { closeSync, existsSync as existsSync6, openSync as openSync2, readFileSync as readFileSync4, readSync, statSync, watch } from "fs";
1287
1491
  function printTail(file, n) {
1288
- const lines = readFileSync5(file, "utf8").split("\n");
1492
+ const lines = readFileSync4(file, "utf8").split("\n");
1289
1493
  const tail = lines.slice(-n).join("\n");
1290
1494
  process.stdout.write(tail.endsWith("\n") ? tail : `${tail}
1291
1495
  `);
@@ -1317,7 +1521,7 @@ function follow(file, fromPos) {
1317
1521
  function registerLogs(program) {
1318
1522
  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) => {
1319
1523
  const { fqdn, entry } = resolveTarget(name);
1320
- if (!entry?.logFile || !existsSync3(entry.logFile)) {
1524
+ if (!entry?.logFile || !existsSync6(entry.logFile)) {
1321
1525
  throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
1322
1526
  }
1323
1527
  const n = Math.max(1, Number(opts.lines) || 50);
@@ -1326,113 +1530,39 @@ function registerLogs(program) {
1326
1530
  });
1327
1531
  }
1328
1532
 
1329
- // src/commands/service.ts
1330
- import os2 from "os";
1331
- import { realpathSync } from "fs";
1332
- function entryScript() {
1333
- const p = process.argv[1];
1334
- if (!p) throw new CliError("Cannot resolve the cloudtunnel executable path.");
1335
- return realpathSync(p);
1336
- }
1337
- function enable(name, opts) {
1338
- const profile = getProfile(name);
1339
- let protocol = profile.protocol;
1340
- if (opts.protocol) {
1341
- protocol = parseTransportProtocol(opts.protocol);
1342
- saveProfile(name, { ...profile, protocol });
1343
- }
1344
- if (!protocol) {
1345
- say.warn("No edge protocol set \u2014 cloudflared will pick QUIC, which some networks drop.");
1346
- say.dim(" \u2192 set one with: cloudtunnel service enable " + name + " --protocol http2");
1347
- }
1348
- installService({
1349
- profile: name,
1350
- user: os2.userInfo().username,
1351
- home: os2.homedir(),
1352
- nodePath: process.execPath,
1353
- scriptPath: entryScript(),
1354
- protocol
1355
- });
1356
- say.ok(`Service ${serviceName(name)} enabled \u2014 starts on boot.`);
1357
- say.dim(` \u2192 check it: cloudtunnel service status ${name}`);
1358
- }
1359
- function disable(name) {
1360
- getProfile(name);
1361
- uninstallService(name);
1362
- say.ok(`Service ${serviceName(name)} disabled and removed.`);
1363
- }
1364
- function status(name) {
1365
- getProfile(name);
1366
- say.info(`${serviceName(name)}: ${serviceState(name)}`);
1367
- }
1368
- function registerService(program) {
1369
- const svc = program.command("service").description("Register a profile as a systemd service that starts on boot");
1370
- 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));
1371
- svc.command("disable").argument("<profile>", "profile to unregister").description("Stop, disable, and remove the profile's boot service (needs sudo)").action((name) => disable(name));
1372
- svc.command("status").argument("<profile>", "profile to check").description("Show the systemd state of the profile's service").action((name) => status(name));
1373
- }
1374
-
1375
1533
  // src/index.ts
1376
1534
  var require2 = createRequire(import.meta.url);
1377
1535
  var pkg = require2("../package.json");
1378
- var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1379
- "login",
1380
- "up",
1381
- "ls",
1382
- "ps",
1383
- "down",
1384
- "rm",
1385
- "remove",
1386
- "delete",
1387
- "stop",
1388
- "logs",
1389
- "zones",
1390
- "save",
1391
- "run",
1392
- "profiles",
1393
- "service",
1394
- "help"
1395
- ]);
1396
- function applyBarePortAlias(argv) {
1397
- const args = argv.slice(2);
1398
- const first = args[0];
1399
- if (first && /^\d{1,5}$/.test(first) && !KNOWN_COMMANDS.has(first)) {
1400
- args.unshift("up");
1401
- }
1402
- return [argv[0], argv[1], ...args];
1403
- }
1404
1536
  function buildProgram() {
1405
1537
  const program = new Command();
1406
- program.name("cloudtunnel").description("Manage Cloudflare Tunnels and subdomains account-wide, from your terminal.").version(pkg.version, "-v, --version").showHelpAfterError();
1538
+ program.name("cloudtunnel").description("Expose local ports at HTTPS subdomains on your own Cloudflare domains.").version(pkg.version, "-v, --version").showHelpAfterError();
1407
1539
  program.addHelpText(
1408
1540
  "before",
1409
1541
  [
1410
1542
  pc2.bold("Quickstart:"),
1411
- ` ${pc2.cyan("cloudtunnel login")} once \u2014 paste a token (or set CLOUDFLARE_API_TOKEN)`,
1412
- ` ${pc2.cyan("cloudtunnel 3000")} \u2192 your local :3000 goes live at an HTTPS URL`,
1543
+ ` ${pc2.cyan("cloudtunnel login")} once \u2014 paste a token (or set CLOUDFLARE_API_TOKEN)`,
1544
+ ` ${pc2.cyan("cloudtunnel 8080")} your local :8080 goes live at an HTTPS URL`,
1545
+ ` ${pc2.cyan("cloudtunnel api:8080")} api.<domain> \u2192 localhost:8080`,
1546
+ ` ${pc2.cyan("cloudtunnel ls")} list tunnels ${pc2.dim("\xB7")} ${pc2.cyan("cloudtunnel delete <#>")} remove one`,
1413
1547
  ""
1414
1548
  ].join("\n")
1415
1549
  );
1416
- for (const register of [
1417
- registerLogin,
1418
- registerUp,
1419
- registerLs,
1420
- registerDown,
1421
- registerZones,
1422
- registerSave,
1423
- registerRun,
1424
- registerProfiles,
1425
- registerService,
1426
- registerLogs
1427
- ]) {
1550
+ for (const register of [registerLogin, registerUp, registerLs, registerDelete, registerLogs]) {
1428
1551
  register(program);
1429
1552
  }
1430
1553
  return program;
1431
1554
  }
1555
+ function shouldMigrate(argv) {
1556
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
1557
+ const rest = argv.slice(2);
1558
+ const infoFlag = /* @__PURE__ */ new Set(["-h", "--help", "-v", "--version", "help"]);
1559
+ return !rest.some((a) => infoFlag.has(a));
1560
+ }
1432
1561
  async function main() {
1562
+ if (shouldMigrate(process.argv)) await migrateLegacyProfiles();
1433
1563
  const program = buildProgram();
1434
1564
  try {
1435
- await program.parseAsync(applyBarePortAlias(process.argv));
1565
+ await program.parseAsync(process.argv);
1436
1566
  } catch (err) {
1437
1567
  process.exitCode = reportError(err);
1438
1568
  }