@iamken/cloudtunnel 0.4.0 → 0.6.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,7 +25,7 @@ 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";
@@ -32,7 +33,7 @@ import { createRequire } from "module";
32
33
  import pc2 from "picocolors";
33
34
 
34
35
  // src/config/legacy-migrate.ts
35
- import { existsSync as existsSync2, readFileSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
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,11 +79,13 @@ async function selectOne(message, items, label) {
78
79
  return items[Number(value)];
79
80
  }
80
81
 
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";
82
+ // src/core/service.ts
83
+ import { join as join5 } from "path";
84
+
85
+ // src/core/service-exec.ts
86
+ import { realpathSync } from "fs";
87
+ import os from "os";
88
+ import { join } from "path";
86
89
 
87
90
  // src/core/ingress.ts
88
91
  var HOSTNAME_RE = /^[a-zA-Z0-9.-]+$/;
@@ -157,29 +160,74 @@ function formatTunnelSpec(s) {
157
160
  return `${s.subdomain}:${s.port}${s.host ? `@${s.host}` : ""}`;
158
161
  }
159
162
 
160
- // src/core/systemd.ts
161
- function serviceName(fqdn) {
162
- return `cloudtunnel-${fqdn.replace(/[^a-zA-Z0-9]+/g, "-")}.service`;
163
+ // src/core/service-exec.ts
164
+ var fqdnFor = (subdomain, zone) => subdomain === "@" ? zone : `${subdomain}.${zone}`;
165
+ var serviceSlug = (fqdn) => fqdn.replace(/[^a-zA-Z0-9]+/g, "-");
166
+ function buildUpArgs(p) {
167
+ const spec = formatTunnelSpec({ subdomain: p.subdomain, port: p.port, host: p.host });
168
+ return [
169
+ "up",
170
+ spec,
171
+ "-d",
172
+ p.zone,
173
+ ...p.proto === "https" ? ["--proto", "https"] : [],
174
+ ...p.protocol ? ["--protocol", p.protocol] : [],
175
+ "-f",
176
+ "-y"
177
+ ];
178
+ }
179
+ function entryScript() {
180
+ const p = process.argv[1];
181
+ if (!p) throw new CliError("Cannot resolve the cloudtunnel executable path.");
182
+ return realpathSync(p);
163
183
  }
164
- function unitPath(fqdn) {
165
- return `/etc/systemd/system/${serviceName(fqdn)}`;
184
+ function describeService(p) {
185
+ const fqdn = fqdnFor(p.subdomain, p.zone);
186
+ const slug = serviceSlug(fqdn);
187
+ return {
188
+ fqdn,
189
+ slug,
190
+ argv: buildUpArgs(p),
191
+ nodePath: process.execPath,
192
+ scriptPath: entryScript(),
193
+ user: os.userInfo().username,
194
+ home: os.homedir(),
195
+ logFile: join(logDir, `${slug}.service.log`)
196
+ };
166
197
  }
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}` : "";
198
+
199
+ // src/core/service-systemd.ts
200
+ var service_systemd_exports = {};
201
+ __export(service_systemd_exports, {
202
+ assertSupported: () => assertSupported,
203
+ buildUnit: () => buildUnit,
204
+ install: () => install,
205
+ label: () => label,
206
+ legacyUnitExists: () => legacyUnitExists,
207
+ removeLegacyUnit: () => removeLegacyUnit,
208
+ state: () => state,
209
+ uninstall: () => uninstall
210
+ });
211
+ import { execFileSync } from "child_process";
212
+ import { existsSync, writeFileSync } from "fs";
213
+ import { tmpdir } from "os";
214
+ import { dirname, join as join2 } from "path";
215
+ var label = (fqdn) => `cloudtunnel-${serviceSlug(fqdn)}.service`;
216
+ var unitPath = (fqdn) => `/etc/systemd/system/${label(fqdn)}`;
217
+ function buildUnit(d) {
218
+ const nodeBin = dirname(d.nodePath);
171
219
  return [
172
220
  "[Unit]",
173
- `Description=cloudtunnel ${p.fqdn} (Cloudflare Tunnel)`,
221
+ `Description=cloudtunnel ${d.fqdn} (Cloudflare Tunnel)`,
174
222
  "After=network-online.target",
175
223
  "Wants=network-online.target",
176
224
  "",
177
225
  "[Service]",
178
226
  "Type=simple",
179
- `User=${p.user}`,
180
- `Environment=HOME=${p.home}`,
227
+ `User=${d.user}`,
228
+ `Environment=HOME=${d.home}`,
181
229
  `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`,
230
+ `ExecStart=${d.nodePath} ${d.scriptPath} ${d.argv.join(" ")}`,
183
231
  "Restart=on-failure",
184
232
  "RestartSec=5",
185
233
  "",
@@ -188,18 +236,6 @@ function buildUnit(p) {
188
236
  ""
189
237
  ].join("\n");
190
238
  }
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
239
  function privileged(args) {
204
240
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
205
241
  const argv = isRoot ? args : ["sudo", ...args];
@@ -213,40 +249,37 @@ function query(args) {
213
249
  return out ? out.toString().trim() : "";
214
250
  }
215
251
  }
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);
252
+ function assertSupported() {
253
+ try {
254
+ execFileSync("systemctl", ["--version"], { stdio: "ignore" });
255
+ } catch {
256
+ throw new CliError("systemd (systemctl) was not found on this host.");
257
+ }
220
258
  }
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)]);
259
+ function install(d) {
260
+ assertSupported();
261
+ const tmp = join2(tmpdir(), label(d.fqdn));
262
+ writeFileSync(tmp, buildUnit(d), { mode: 420 });
263
+ privileged(["install", "-m", "0644", tmp, unitPath(d.fqdn)]);
238
264
  privileged(["systemctl", "daemon-reload"]);
239
- privileged(["systemctl", "enable", "--now", serviceName(fqdn)]);
265
+ privileged(["systemctl", "enable", "--now", label(d.fqdn)]);
240
266
  }
241
- function uninstallService(fqdn) {
242
- assertSystemd();
267
+ function uninstall(fqdn) {
243
268
  try {
244
- privileged(["systemctl", "disable", "--now", serviceName(fqdn)]);
269
+ privileged(["systemctl", "disable", "--now", label(fqdn)]);
245
270
  } catch {
246
271
  }
247
272
  privileged(["rm", "-f", unitPath(fqdn)]);
248
273
  privileged(["systemctl", "daemon-reload"]);
249
274
  }
275
+ function state(fqdn) {
276
+ const name = label(fqdn);
277
+ if (query(["is-active", name]) === "active") return "active";
278
+ const enabled = query(["is-enabled", name]);
279
+ if (enabled === "enabled" || enabled === "enabled-runtime") return "enabled";
280
+ if (enabled === "disabled" || enabled === "static") return "disabled";
281
+ return "none";
282
+ }
250
283
  function legacyUnitExists(profile) {
251
284
  return existsSync(`/etc/systemd/system/cloudtunnel-${profile}.service`);
252
285
  }
@@ -259,27 +292,219 @@ function removeLegacyUnit(profile) {
259
292
  privileged(["rm", "-f", `/etc/systemd/system/${name}`]);
260
293
  privileged(["systemctl", "daemon-reload"]);
261
294
  }
295
+
296
+ // src/core/service-launchd.ts
297
+ var service_launchd_exports = {};
298
+ __export(service_launchd_exports, {
299
+ assertSupported: () => assertSupported2,
300
+ buildPlist: () => buildPlist,
301
+ install: () => install2,
302
+ label: () => label2,
303
+ state: () => state2,
304
+ uninstall: () => uninstall2
305
+ });
306
+ import { execFileSync as execFileSync2 } from "child_process";
307
+ import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
308
+ import { dirname as dirname2, join as join3 } from "path";
309
+ import os2 from "os";
310
+ var label2 = (fqdn) => `com.cloudtunnel.${serviceSlug(fqdn)}`;
311
+ var agentsDir = () => join3(os2.homedir(), "Library", "LaunchAgents");
312
+ var plistPath = (fqdn) => join3(agentsDir(), `${label2(fqdn)}.plist`);
313
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
314
+ function buildPlist(d) {
315
+ const args = [d.nodePath, d.scriptPath, ...d.argv].map((a) => ` <string>${xml(a)}</string>`).join("\n");
316
+ const nodeBin = dirname2(d.nodePath);
317
+ const path = `${nodeBin}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
318
+ return [
319
+ '<?xml version="1.0" encoding="UTF-8"?>',
320
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
321
+ '<plist version="1.0">',
322
+ "<dict>",
323
+ ` <key>Label</key><string>${xml(label2(d.fqdn))}</string>`,
324
+ " <key>ProgramArguments</key>",
325
+ " <array>",
326
+ args,
327
+ " </array>",
328
+ " <key>RunAtLoad</key><true/>",
329
+ " <key>KeepAlive</key><true/>",
330
+ " <key>EnvironmentVariables</key>",
331
+ " <dict>",
332
+ ` <key>PATH</key><string>${xml(path)}</string>`,
333
+ ` <key>HOME</key><string>${xml(d.home)}</string>`,
334
+ " </dict>",
335
+ ` <key>StandardOutPath</key><string>${xml(d.logFile)}</string>`,
336
+ ` <key>StandardErrorPath</key><string>${xml(d.logFile)}</string>`,
337
+ "</dict>",
338
+ "</plist>",
339
+ ""
340
+ ].join("\n");
341
+ }
342
+ function launchctl(args) {
343
+ try {
344
+ return execFileSync2("launchctl", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
345
+ } catch (err) {
346
+ const out = err.stdout;
347
+ return out ? out.toString() : "";
348
+ }
349
+ }
350
+ function assertSupported2() {
351
+ }
352
+ function install2(d) {
353
+ ensureDirs();
354
+ mkdirSync(agentsDir(), { recursive: true });
355
+ const plist = plistPath(d.fqdn);
356
+ writeFileSync2(plist, buildPlist(d), { mode: 420 });
357
+ launchctl(["unload", "-w", plist]);
358
+ execFileSync2("launchctl", ["load", "-w", plist], { stdio: "inherit" });
359
+ }
360
+ function uninstall2(fqdn) {
361
+ const plist = plistPath(fqdn);
362
+ launchctl(["unload", "-w", plist]);
363
+ rmSync(plist, { force: true });
364
+ }
365
+ function state2(fqdn) {
366
+ const info = launchctl(["list", label2(fqdn)]);
367
+ if (/"PID"\s*=/.test(info)) return "active";
368
+ return existsSync2(plistPath(fqdn)) ? "enabled" : "none";
369
+ }
370
+
371
+ // src/core/service-windows.ts
372
+ var service_windows_exports = {};
373
+ __export(service_windows_exports, {
374
+ assertSupported: () => assertSupported3,
375
+ buildTaskXml: () => buildTaskXml,
376
+ install: () => install3,
377
+ label: () => label3,
378
+ state: () => state3,
379
+ uninstall: () => uninstall3
380
+ });
381
+ import { execFileSync as execFileSync3 } from "child_process";
382
+ import { writeFileSync as writeFileSync3 } from "fs";
383
+ import { tmpdir as tmpdir2 } from "os";
384
+ import { join as join4 } from "path";
385
+ var label3 = (fqdn) => `cloudtunnel\\${serviceSlug(fqdn)}`;
386
+ var xml2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
387
+ function buildTaskXml(d) {
388
+ const args = `"${d.scriptPath}" ${d.argv.join(" ")}`;
389
+ return [
390
+ '<?xml version="1.0" encoding="UTF-16"?>',
391
+ '<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
392
+ ` <RegistrationInfo><Description>cloudtunnel ${xml2(d.fqdn)} (Cloudflare Tunnel)</Description></RegistrationInfo>`,
393
+ ` <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml2(d.user)}</UserId></LogonTrigger></Triggers>`,
394
+ ` <Principals><Principal id="Author"><UserId>${xml2(d.user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>`,
395
+ " <Settings>",
396
+ " <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>",
397
+ " <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>",
398
+ " <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>",
399
+ " <StartWhenAvailable>true</StartWhenAvailable>",
400
+ " <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
401
+ " <RestartOnFailure><Interval>PT1M</Interval><Count>3</Count></RestartOnFailure>",
402
+ " <Enabled>true</Enabled>",
403
+ " </Settings>",
404
+ ' <Actions Context="Author">',
405
+ ` <Exec><Command>${xml2(d.nodePath)}</Command><Arguments>${xml2(args)}</Arguments></Exec>`,
406
+ " </Actions>",
407
+ "</Task>",
408
+ ""
409
+ ].join("\r\n");
410
+ }
411
+ function schtasks(args) {
412
+ try {
413
+ return execFileSync3("schtasks", args, { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
414
+ } catch (err) {
415
+ const out = err.stdout;
416
+ return out ? out.toString() : "";
417
+ }
418
+ }
419
+ function assertSupported3() {
420
+ }
421
+ function install3(d) {
422
+ const file = join4(tmpdir2(), `${d.slug}.task.xml`);
423
+ writeFileSync3(file, "\uFEFF" + buildTaskXml(d), { encoding: "utf16le" });
424
+ execFileSync3("schtasks", ["/Create", "/TN", label3(d.fqdn), "/XML", file, "/F"], { stdio: "inherit" });
425
+ schtasks(["/Run", "/TN", label3(d.fqdn)]);
426
+ }
427
+ function uninstall3(fqdn) {
428
+ schtasks(["/Delete", "/TN", label3(fqdn), "/F"]);
429
+ }
430
+ function state3(fqdn) {
431
+ const out = schtasks(["/Query", "/TN", label3(fqdn), "/FO", "LIST"]);
432
+ if (!out) return "none";
433
+ if (/\bRunning\b/.test(out)) return "active";
434
+ if (/\bDisabled\b/.test(out)) return "disabled";
435
+ if (/\bReady\b/.test(out)) return "enabled";
436
+ return "enabled";
437
+ }
438
+
439
+ // src/core/service.ts
440
+ function pick() {
441
+ switch (process.platform) {
442
+ case "linux":
443
+ return service_systemd_exports;
444
+ case "darwin":
445
+ return service_launchd_exports;
446
+ case "win32":
447
+ return service_windows_exports;
448
+ default:
449
+ return null;
450
+ }
451
+ }
452
+ function required() {
453
+ const b = pick();
454
+ if (!b) {
455
+ throw new CliError(`Boot services aren't supported on ${process.platform}.`, {
456
+ hint: "run `cloudtunnel up <spec> --detach` and use your OS's own autostart"
457
+ });
458
+ }
459
+ return b;
460
+ }
461
+ function assertServiceSupported() {
462
+ required().assertSupported();
463
+ }
464
+ function serviceName(fqdn) {
465
+ return pick()?.label(fqdn) ?? `cloudtunnel-${fqdn}`;
466
+ }
467
+ function installServiceForSpec(params) {
468
+ const b = required();
469
+ b.assertSupported();
470
+ b.install(describeService(params));
471
+ }
472
+ function uninstallService(fqdn) {
473
+ pick()?.uninstall(fqdn);
474
+ }
262
475
  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";
476
+ return pick()?.state(fqdn) ?? "none";
477
+ }
478
+ function serviceLogsHint(fqdn) {
479
+ switch (process.platform) {
480
+ case "linux":
481
+ return `journalctl -u ${serviceName(fqdn)} -n 50 --no-pager`;
482
+ case "darwin":
483
+ return `tail ${join5(logDir, `${serviceSlug(fqdn)}.service.log`)}`;
484
+ case "win32":
485
+ return `schtasks /Query /TN "${serviceName(fqdn)}" /V /FO LIST`;
486
+ default:
487
+ return "check your OS service logs";
488
+ }
489
+ }
490
+ function legacyUnitExists2(profile) {
491
+ return process.platform === "linux" ? legacyUnitExists(profile) : false;
492
+ }
493
+ function removeLegacyUnit2(profile) {
494
+ if (process.platform === "linux") removeLegacyUnit(profile);
270
495
  }
271
496
 
272
497
  // src/config/legacy-migrate.ts
273
498
  var skipMarker = `${profilesFile}.migrate-skip`;
274
499
  async function migrateLegacyProfiles() {
275
- if (!existsSync2(profilesFile) || existsSync2(skipMarker)) return;
500
+ if (!existsSync3(profilesFile) || existsSync3(skipMarker)) return;
276
501
  let profiles;
277
502
  try {
278
503
  profiles = JSON.parse(readFileSync(profilesFile, "utf8"));
279
504
  } catch {
280
505
  return;
281
506
  }
282
- const legacy = Object.entries(profiles).filter(([name]) => legacyUnitExists(name));
507
+ const legacy = Object.entries(profiles).filter(([name]) => legacyUnitExists2(name));
283
508
  if (legacy.length === 0) {
284
509
  try {
285
510
  renameSync(profilesFile, `${profilesFile}.migrated`);
@@ -289,7 +514,7 @@ async function migrateLegacyProfiles() {
289
514
  }
290
515
  const ok = await confirm(`Found ${legacy.length} boot service(s) from an older cloudtunnel. Migrate them now? (needs sudo)`);
291
516
  if (!ok) {
292
- writeFileSync2(skipMarker, "");
517
+ writeFileSync4(skipMarker, "");
293
518
  say.dim(` Skipped. Delete ${skipMarker} to be asked again.`);
294
519
  return;
295
520
  }
@@ -309,12 +534,12 @@ async function migrateLegacyProfiles() {
309
534
  });
310
535
  migrated++;
311
536
  }
312
- removeLegacyUnit(name);
537
+ removeLegacyUnit2(name);
313
538
  }
314
539
  renameSync(profilesFile, `${profilesFile}.migrated`);
315
540
  say.ok(`Migrated ${migrated} boot service(s). See them with: cloudtunnel ls`);
316
541
  } catch (err) {
317
- writeFileSync2(skipMarker, "");
542
+ writeFileSync4(skipMarker, "");
318
543
  say.warn(`Migration incomplete: ${err.message}. Won't retry automatically (delete ${skipMarker} to retry).`);
319
544
  }
320
545
  }
@@ -478,10 +703,10 @@ async function ensureAuth() {
478
703
  }
479
704
 
480
705
  // src/connector/binary.ts
481
- import { execFileSync as execFileSync2 } from "child_process";
706
+ import { execFileSync as execFileSync4 } from "child_process";
482
707
  import { createHash } from "crypto";
483
- import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
484
- import { join as join2 } from "path";
708
+ import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
709
+ import { join as join6 } from "path";
485
710
  var PINNED_VERSION = "2025.1.0";
486
711
  var RELEASE_BASE = `https://github.com/cloudflare/cloudflared/releases/download/${PINNED_VERSION}`;
487
712
  var ASSETS = {
@@ -493,14 +718,14 @@ var ASSETS = {
493
718
  };
494
719
  function binaryWorks(bin) {
495
720
  try {
496
- execFileSync2(bin, ["--version"], { stdio: "ignore" });
721
+ execFileSync4(bin, ["--version"], { stdio: "ignore" });
497
722
  return true;
498
723
  } catch {
499
724
  return false;
500
725
  }
501
726
  }
502
727
  function cachedPath() {
503
- return join2(binDir, process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
728
+ return join6(binDir, process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
504
729
  }
505
730
  function isMusl() {
506
731
  try {
@@ -512,7 +737,7 @@ function isMusl() {
512
737
  async function ensureCloudflared() {
513
738
  if (binaryWorks("cloudflared")) return "cloudflared";
514
739
  const cached = cachedPath();
515
- if (existsSync3(cached) && binaryWorks(cached)) return cached;
740
+ if (existsSync4(cached) && binaryWorks(cached)) return cached;
516
741
  return downloadCloudflared(cached);
517
742
  }
518
743
  async function downloadCloudflared(dest) {
@@ -540,7 +765,7 @@ async function downloadCloudflared(dest) {
540
765
  }
541
766
  ensureDirs();
542
767
  const binary = asset.archive ? extractTgz(bytes) : bytes;
543
- writeFileSync3(dest, binary, { mode: 493 });
768
+ writeFileSync5(dest, binary, { mode: 493 });
544
769
  chmodSync(dest, 493);
545
770
  if (!binaryWorks(dest)) throw new CliError("Downloaded cloudflared is not runnable.");
546
771
  return dest;
@@ -552,17 +777,17 @@ function extractTgz(_bytes) {
552
777
  }
553
778
 
554
779
  // src/core/up-runner.ts
555
- import { join as join3 } from "path";
780
+ import { join as join7 } from "path";
556
781
  import * as clack2 from "@clack/prompts";
557
782
 
558
783
  // src/connector/process.ts
559
- import { execFileSync as execFileSync3, spawn as spawn2 } from "child_process";
784
+ import { execFileSync as execFileSync5, spawn as spawn2 } from "child_process";
560
785
  import { openSync } from "fs";
561
786
 
562
787
  // src/connector/registry.ts
563
- import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
788
+ import { existsSync as existsSync5, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
564
789
  import { readFile } from "fs/promises";
565
- import os2 from "os";
790
+ import os3 from "os";
566
791
  import lockfile from "proper-lockfile";
567
792
  function entryFqdn(e) {
568
793
  return e.subdomain === "@" ? e.zone : `${e.subdomain}.${e.zone}`;
@@ -571,8 +796,8 @@ function currentBootId() {
571
796
  try {
572
797
  return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
573
798
  } catch {
574
- const bootMinute = Math.floor((Date.now() - os2.uptime() * 1e3) / 6e4);
575
- return `boot-${bootMinute}-${os2.hostname()}`;
799
+ const bootMinute = Math.floor((Date.now() - os3.uptime() * 1e3) / 6e4);
800
+ return `boot-${bootMinute}-${os3.hostname()}`;
576
801
  }
577
802
  }
578
803
  function readRegistry() {
@@ -585,12 +810,12 @@ function readRegistry() {
585
810
  function writeRegistry(reg) {
586
811
  ensureDirs();
587
812
  const tmp = `${registryFile}.tmp`;
588
- writeFileSync4(tmp, JSON.stringify(reg, null, 2), { mode: 384 });
813
+ writeFileSync6(tmp, JSON.stringify(reg, null, 2), { mode: 384 });
589
814
  renameSync2(tmp, registryFile);
590
815
  }
591
816
  async function mutateRegistry(fn) {
592
817
  ensureDirs();
593
- if (!existsSync4(registryFile)) writeFileSync4(registryFile, "{}", { mode: 384 });
818
+ if (!existsSync5(registryFile)) writeFileSync6(registryFile, "{}", { mode: 384 });
594
819
  const release = await lockfile.lock(registryFile, { retries: { retries: 10, minTimeout: 50 } });
595
820
  try {
596
821
  const reg = readRegistry();
@@ -698,7 +923,7 @@ async function stopConnector(entry) {
698
923
  const pid = entry.pid;
699
924
  if (process.platform === "win32") {
700
925
  try {
701
- execFileSync3("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
926
+ execFileSync5("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
702
927
  } catch {
703
928
  return false;
704
929
  }
@@ -829,10 +1054,10 @@ var NOUNS = [
829
1054
  "tiger",
830
1055
  "walnut"
831
1056
  ];
832
- var pick = (arr) => arr[randomInt(arr.length)];
1057
+ var pick2 = (arr) => arr[randomInt(arr.length)];
833
1058
  function randomSlug() {
834
1059
  const suffix = randomInt(65536).toString(16).padStart(4, "0");
835
- return `${pick(ADJECTIVES)}-${pick(NOUNS)}-${suffix}`;
1060
+ return `${pick2(ADJECTIVES)}-${pick2(NOUNS)}-${suffix}`;
836
1061
  }
837
1062
  function resolveHostSpec(opts, defaultZone) {
838
1063
  if (opts.hostname) {
@@ -889,8 +1114,8 @@ async function createTunnelSubdomain(cf, opts) {
889
1114
  let dnsRecordId;
890
1115
  try {
891
1116
  const suffix = randomInt2(65536).toString(16).padStart(4, "0");
892
- const label = host.subdomain === "@" ? "root" : host.subdomain;
893
- const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${label}-${suffix}`);
1117
+ const label4 = host.subdomain === "@" ? "root" : host.subdomain;
1118
+ const tunnel = await createTunnel(cf, `${MANAGED_TUNNEL_PREFIX}${label4}-${suffix}`);
894
1119
  tunnelId = tunnel.id;
895
1120
  const token = await getTunnelToken(cf, tunnelId);
896
1121
  await putIngress(cf, tunnelId, buildIngress({ hostname: host.hostname, port: opts.port, proto: opts.proto, host: opts.host }));
@@ -1037,8 +1262,8 @@ async function listAll(cf, opts = {}) {
1037
1262
  };
1038
1263
  });
1039
1264
  if (opts.all) {
1040
- const { listCargoCnames } = await import("./dns-PAPFSYFP.js");
1041
- const { listZones: listZones3 } = await import("./zones-YNGQYXAF.js");
1265
+ const { listCargoCnames } = await import("./dns-5OXFAQ4D.js");
1266
+ const { listZones: listZones3 } = await import("./zones-QYV3DSEY.js");
1042
1267
  const tracked = new Set(entries.map(entryFqdn));
1043
1268
  for (const zone of await listZones3(cf.token)) {
1044
1269
  for (const rec of await listCargoCnames(cf.token, zone.id)) {
@@ -1053,7 +1278,7 @@ async function listAll(cf, opts = {}) {
1053
1278
 
1054
1279
  // src/core/up-runner.ts
1055
1280
  function logFileFor(subdomain) {
1056
- return join3(logDir, `${subdomain === "@" ? "root" : subdomain}.log`);
1281
+ return join7(logDir, `${subdomain === "@" ? "root" : subdomain}.log`);
1057
1282
  }
1058
1283
  async function startTunnels(cf, bin, items, opts = {}) {
1059
1284
  const started = [];
@@ -1196,30 +1421,56 @@ async function runUp(specArgs, opts) {
1196
1421
  });
1197
1422
  }
1198
1423
  if (opts.service) {
1199
- registerServices(items, domain, opts.proto, protocol);
1424
+ await registerServices(cf, items, domain, opts.proto, protocol);
1200
1425
  return;
1201
1426
  }
1202
1427
  await startTunnels(cf, bin, items, { detach: opts.detach, protocol });
1203
1428
  }
1204
- function registerServices(items, domain, proto, protocol) {
1205
- assertSystemd();
1429
+ var SERVICE_UP_TIMEOUT_MS = 2e4;
1430
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1431
+ async function registerServices(cf, items, domain, proto, protocol) {
1432
+ assertServiceSupported();
1206
1433
  if (!protocol) {
1207
1434
  say.warn("No edge protocol set \u2014 cloudflared will pick QUIC, which some networks drop.");
1208
1435
  say.dim(" \u2192 add --protocol http2 for UDP-hostile networks");
1209
1436
  }
1210
- const done = [];
1437
+ const fqdns = [];
1211
1438
  for (const item of items) {
1212
1439
  const subdomain = item.name;
1213
1440
  const fqdn = subdomain === "@" ? domain : `${subdomain}.${domain}`;
1214
1441
  installServiceForSpec({ subdomain, port: item.port, host: item.host, zone: domain, proto, protocol });
1215
- done.push(`${serviceName(fqdn)} \u2192 https://${fqdn}`);
1442
+ fqdns.push(fqdn);
1443
+ }
1444
+ say.ok(`Registered ${fqdns.length} boot service(s) \u2014 waiting for them to come up\u2026`);
1445
+ const notUp = await waitServicesUp(fqdns, SERVICE_UP_TIMEOUT_MS);
1446
+ const rows = await listAll(cf);
1447
+ if (rows.length) {
1448
+ printTable(
1449
+ ["#", "URL", "TARGET", "STATE", "SERVICE", "PID"],
1450
+ rows.map((r) => [r.num, r.url, r.target, r.state, r.service, r.pid])
1451
+ );
1452
+ }
1453
+ if (notUp.length) {
1454
+ say.warn(`${notUp.length} service(s) didn't report up within ${SERVICE_UP_TIMEOUT_MS / 1e3}s:`);
1455
+ for (const fqdn of notUp) say.dim(` ${fqdn} \u2192 ${serviceLogsHint(fqdn)}`);
1216
1456
  }
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 <#>");
1457
+ say.dim(" \u2192 manage: cloudtunnel ls \xB7 remove: cloudtunnel delete <#>");
1458
+ }
1459
+ async function waitServicesUp(fqdns, timeoutMs) {
1460
+ const deadline = Date.now() + timeoutMs;
1461
+ const pending = new Set(fqdns);
1462
+ while (pending.size > 0) {
1463
+ for (const fqdn of [...pending]) {
1464
+ const entry = getEntry(fqdn);
1465
+ if (entry?.state === "running" && entry.pid) pending.delete(fqdn);
1466
+ }
1467
+ if (pending.size === 0 || Date.now() >= deadline) break;
1468
+ await delay(500);
1469
+ }
1470
+ return [...pending];
1220
1471
  }
1221
1472
  function registerUp(program) {
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));
1473
+ 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));
1223
1474
  }
1224
1475
 
1225
1476
  // src/commands/ls.ts
@@ -1241,14 +1492,12 @@ function registerLs(program) {
1241
1492
 
1242
1493
  // src/commands/delete.ts
1243
1494
  async function deleteOne(cf, fqdn, opts) {
1495
+ const hasService = serviceState(fqdn) !== "none";
1496
+ if (hasService && !opts.dryRun) uninstallService(fqdn);
1244
1497
  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)}`);
1498
+ if (!hasService) return;
1499
+ if (opts.dryRun) say.info(`Would also remove boot service ${serviceName(fqdn)}`);
1500
+ else say.ok(`Removed boot service ${serviceName(fqdn)}`);
1252
1501
  }
1253
1502
  function registerDelete(program) {
1254
1503
  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) => {
@@ -1279,7 +1528,7 @@ function registerDelete(program) {
1279
1528
  }
1280
1529
 
1281
1530
  // src/commands/logs.ts
1282
- import { closeSync, existsSync as existsSync5, openSync as openSync2, readFileSync as readFileSync4, readSync, statSync, watch } from "fs";
1531
+ import { closeSync, existsSync as existsSync6, openSync as openSync2, readFileSync as readFileSync4, readSync, statSync, watch } from "fs";
1283
1532
  function printTail(file, n) {
1284
1533
  const lines = readFileSync4(file, "utf8").split("\n");
1285
1534
  const tail = lines.slice(-n).join("\n");
@@ -1313,7 +1562,7 @@ function follow(file, fromPos) {
1313
1562
  function registerLogs(program) {
1314
1563
  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) => {
1315
1564
  const { fqdn, entry } = resolveTarget(name);
1316
- if (!entry?.logFile || !existsSync5(entry.logFile)) {
1565
+ if (!entry?.logFile || !existsSync6(entry.logFile)) {
1317
1566
  throw new CliError(`No logs for ${fqdn} yet.`, { hint: "start it with `cloudtunnel up` or `cloudtunnel run`" });
1318
1567
  }
1319
1568
  const n = Math.max(1, Number(opts.lines) || 50);