@cliftonc/cfld 0.2.5 → 0.2.7

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/cli.js CHANGED
@@ -3,15 +3,16 @@
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
5
  import { readFileSync as readFileSync7, realpathSync } from "fs";
6
- import { dirname, join as join7 } from "path";
6
+ import { dirname as dirname2, join as join9 } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import pc7 from "picocolors";
9
9
 
10
10
  // src/commands/run.ts
11
- import { join as join5 } from "path";
11
+ import { join as join7 } from "path";
12
12
 
13
13
  // src/core/state-machine.ts
14
- import { existsSync as existsSync2 } from "fs";
14
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync, unlinkSync } from "fs";
15
+ import { dirname, join as join2 } from "path";
15
16
 
16
17
  // src/util/paths.ts
17
18
  import { homedir } from "os";
@@ -229,7 +230,8 @@ function cfargoTunnelUuid(target) {
229
230
  import { writeFileSync } from "fs";
230
231
  function renderIngressConfig(cfg) {
231
232
  const rules = cfg.routes.flatMap((r) => {
232
- const lines = [` - hostname: ${r.hostname}`];
233
+ const host = r.hostname.startsWith("*") ? JSON.stringify(r.hostname) : r.hostname;
234
+ const lines = [` - hostname: ${host}`];
233
235
  if (r.path) lines.push(` path: ${r.path}`);
234
236
  lines.push(` service: http://localhost:${r.port}`);
235
237
  return lines;
@@ -300,15 +302,17 @@ async function reconcile(input, events = {}) {
300
302
  let created = false;
301
303
  if (existing) {
302
304
  uuid = existing.id;
303
- if (!existsSync2(credentialsPath(uuid))) {
304
- step2("Refetching tunnel credentials");
305
- await fetchToken(tunnelName, credentialsPath(uuid), certPath);
306
- }
307
305
  } else {
308
306
  step2(`Creating tunnel ${tunnelName}`);
309
307
  uuid = await createTunnel(tunnelName, certPath);
310
308
  created = true;
311
309
  }
310
+ if (!existsSync2(credentialsPath(uuid))) {
311
+ if (!adoptCredentialsBesideCert(uuid, certPath)) {
312
+ step2("Refetching tunnel credentials");
313
+ await fetchToken(tunnelName, credentialsPath(uuid), certPath);
314
+ }
315
+ }
312
316
  const prior = getEntry(tunnelName);
313
317
  const owned = /* @__PURE__ */ new Set([
314
318
  prior?.hostname,
@@ -358,9 +362,22 @@ async function reconcile(input, events = {}) {
358
362
  function nowIso() {
359
363
  return (/* @__PURE__ */ new Date()).toISOString();
360
364
  }
365
+ function adoptCredentialsBesideCert(uuid, certPath) {
366
+ const beside = join2(dirname(certPath), `${uuid}.json`);
367
+ const target = credentialsPath(uuid);
368
+ if (beside === target || !existsSync2(beside)) return false;
369
+ mkdirSync2(dirname(target), { recursive: true });
370
+ try {
371
+ renameSync(beside, target);
372
+ } catch {
373
+ copyFileSync(beside, target);
374
+ unlinkSync(beside);
375
+ }
376
+ return true;
377
+ }
361
378
 
362
379
  // src/core/cert-store.ts
363
- import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync } from "fs";
380
+ import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync } from "fs";
364
381
 
365
382
  // src/core/cert.ts
366
383
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
@@ -392,10 +409,28 @@ function certHasZone(certPath) {
392
409
  const token = readCertToken(certPath);
393
410
  return Boolean(token?.zoneID);
394
411
  }
412
+ async function certZoneName(certPath) {
413
+ const token = readCertToken(certPath);
414
+ if (!token?.zoneID || !token.apiToken) return void 0;
415
+ try {
416
+ const res = await fetch(
417
+ `https://api.cloudflare.com/client/v4/zones/${token.zoneID}`,
418
+ {
419
+ headers: { Authorization: `Bearer ${token.apiToken}` },
420
+ signal: AbortSignal.timeout(5e3)
421
+ }
422
+ );
423
+ if (!res.ok) return void 0;
424
+ const body = await res.json();
425
+ return body.result?.name?.toLowerCase();
426
+ } catch {
427
+ return void 0;
428
+ }
429
+ }
395
430
 
396
431
  // src/core/cert-store.ts
397
432
  function ensureStoreDir() {
398
- mkdirSync2(certStoreDir(), { recursive: true });
433
+ mkdirSync3(certStoreDir(), { recursive: true });
399
434
  }
400
435
  function storedCertForZone(zone) {
401
436
  const p = certPathForZone(zone);
@@ -408,25 +443,36 @@ function listStoredZones() {
408
443
  return [];
409
444
  }
410
445
  }
446
+ function storedZoneClaiming(certPath, except) {
447
+ const zoneID = readCertToken(certPath)?.zoneID;
448
+ if (!zoneID) return void 0;
449
+ return listStoredZones().find(
450
+ (z) => z !== except && readCertToken(certPathForZone(z))?.zoneID === zoneID
451
+ );
452
+ }
411
453
  function resolveCert(zone) {
412
- return storedCertForZone(zone) ?? (certExists(defaultCertPath()) ? defaultCertPath() : void 0);
454
+ const stored = storedCertForZone(zone);
455
+ if (stored) return stored;
456
+ const def = defaultCertPath();
457
+ if (!certExists(def) || storedZoneClaiming(def, zone)) return void 0;
458
+ return def;
413
459
  }
414
460
  function importCert(zone, fromPath = defaultCertPath()) {
415
461
  ensureStoreDir();
416
462
  const dest = certPathForZone(zone);
417
- copyFileSync(fromPath, dest);
463
+ copyFileSync2(fromPath, dest);
418
464
  return dest;
419
465
  }
420
466
 
421
467
  // src/core/hostname.ts
422
468
  import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
423
- import { basename, join as join2 } from "path";
469
+ import { basename, join as join3 } from "path";
424
470
  function slugify(input) {
425
471
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
426
472
  }
427
473
  function deriveSlug(cwd, nameFlag) {
428
474
  if (nameFlag) return slugify(nameFlag);
429
- const pkgPath = join2(cwd, "package.json");
475
+ const pkgPath = join3(cwd, "package.json");
430
476
  if (existsSync5(pkgPath)) {
431
477
  try {
432
478
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -512,12 +558,12 @@ function findFreePort() {
512
558
 
513
559
  // src/core/project-config.ts
514
560
  import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
515
- import { join as join3 } from "path";
561
+ import { join as join4 } from "path";
516
562
  function dotConfigPath(cwd) {
517
- return join3(cwd, PROJECT_CONFIG_FILE);
563
+ return join4(cwd, PROJECT_CONFIG_FILE);
518
564
  }
519
565
  function readAuthored(cwd) {
520
- const pkgPath = join3(cwd, "package.json");
566
+ const pkgPath = join4(cwd, "package.json");
521
567
  if (!existsSync6(pkgPath)) return void 0;
522
568
  try {
523
569
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
@@ -936,7 +982,8 @@ function colorLevel(level) {
936
982
 
937
983
  // src/commands/login.ts
938
984
  import { spawn as spawn4 } from "child_process";
939
- import { existsSync as existsSync8 } from "fs";
985
+ import { copyFileSync as copyFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync4, renameSync as renameSync2, rmSync } from "fs";
986
+ import { join as join5 } from "path";
940
987
 
941
988
  // src/ui/prompts.ts
942
989
  async function clack() {
@@ -1031,23 +1078,73 @@ function normalizeZone(input) {
1031
1078
  return input.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
1032
1079
  }
1033
1080
  async function loginCommand(flags) {
1034
- const interactive = isInteractive();
1035
1081
  const haveCert = certExists(defaultCertPath());
1082
+ let certPath = defaultCertPath();
1036
1083
  if (flags.reauth || !haveCert) {
1037
- await runLogin();
1084
+ certPath = await runLogin();
1038
1085
  } else {
1039
1086
  info("A Cloudflare certificate already exists. Use --reauth to authorize another domain.");
1040
1087
  }
1041
- if (!certHasZone(defaultCertPath())) {
1042
- printZeroZoneGuidance();
1088
+ try {
1089
+ if (!certHasZone(certPath)) {
1090
+ printZeroZoneGuidance();
1091
+ throw new CfldError(
1092
+ "Logged in, but your account has no domain to authorize.",
1093
+ "Add a domain (see above), then run `cfld login --reauth`."
1094
+ );
1095
+ }
1096
+ const zone = await saveCert(flags.zone ? normalizeZone(flags.zone) : void 0, certPath);
1097
+ done(`Saved cert for ${zone}. You're ready \u2014 run \`cfld\`.`);
1098
+ } finally {
1099
+ discardLoginCert(certPath);
1100
+ }
1101
+ }
1102
+ async function runLogin() {
1103
+ const bin = await resolveBinary();
1104
+ const def = defaultCertPath();
1105
+ const backup = existsSync8(def) ? `${def}.cfld-backup` : void 0;
1106
+ if (backup) renameSync2(def, backup);
1107
+ try {
1108
+ step("Opening your browser to authorize with Cloudflare (no API token needed)\u2026");
1109
+ note(" Pick the domain you want to use. Waiting for you to finish\u2026");
1110
+ await new Promise((resolve, reject) => {
1111
+ const child = spawn4(bin, ["tunnel", "login"], { stdio: "inherit" });
1112
+ child.on("error", reject);
1113
+ child.on("exit", (code) => {
1114
+ if (code === 0 || existsSync8(def)) resolve();
1115
+ else reject(new CfldError("Cloudflare login did not complete.", "Re-run `cfld login`."));
1116
+ });
1117
+ });
1118
+ if (!existsSync8(def)) {
1119
+ throw new CfldError(
1120
+ "Login finished but no certificate was written.",
1121
+ "Re-run `cfld login` and complete the browser step."
1122
+ );
1123
+ }
1124
+ if (!backup) return def;
1125
+ mkdirSync4(cfldDir(), { recursive: true });
1126
+ const fresh = join5(cfldDir(), "login-cert.tmp");
1127
+ copyFileSync3(def, fresh);
1128
+ rmSync(def);
1129
+ return fresh;
1130
+ } finally {
1131
+ if (backup) renameSync2(backup, def);
1132
+ }
1133
+ }
1134
+ function discardLoginCert(certPath) {
1135
+ if (certPath !== defaultCertPath()) rmSync(certPath, { force: true });
1136
+ }
1137
+ async function saveCert(expected, certPath) {
1138
+ const actual = await certZoneName(certPath);
1139
+ if (expected && actual && actual !== expected) {
1043
1140
  throw new CfldError(
1044
- "Logged in, but your account has no domain to authorize.",
1045
- "Add a domain (see above), then run `cfld login --reauth`."
1141
+ `You authorized ${actual} in the browser, not ${expected}.`,
1142
+ `Run \`cfld login --reauth --zone ${expected}\` and pick ${expected} this time.`
1046
1143
  );
1047
1144
  }
1048
- let zone = flags.zone ? normalizeZone(flags.zone) : void 0;
1145
+ let zone = expected ?? actual;
1049
1146
  if (!zone) {
1050
- if (!interactive) {
1147
+ if (!isInteractive()) {
1051
1148
  throw new CfldError(
1052
1149
  "Pass --zone to record which domain this cert is for.",
1053
1150
  "e.g. `cfld login --zone example.com`"
@@ -1055,48 +1152,57 @@ async function loginCommand(flags) {
1055
1152
  }
1056
1153
  zone = normalizeZone(await askText("Which domain did you authorize?", "example.com"));
1057
1154
  }
1058
- importCert(zone);
1059
- done(`Saved cert for ${zone}. You're ready \u2014 run \`cfld\`.`);
1060
- }
1061
- async function runLogin() {
1062
- const bin = await resolveBinary();
1063
- step("Opening your browser to authorize with Cloudflare (no API token needed)\u2026");
1064
- note(" Pick the domain you want to use. Waiting for you to finish\u2026");
1065
- await new Promise((resolve, reject) => {
1066
- const child = spawn4(bin, ["tunnel", "login"], { stdio: "inherit" });
1067
- child.on("error", reject);
1068
- child.on("exit", (code) => {
1069
- if (code === 0 || existsSync8(defaultCertPath())) resolve();
1070
- else reject(new CfldError("Cloudflare login did not complete.", "Re-run `cfld login`."));
1071
- });
1072
- });
1073
- if (!existsSync8(defaultCertPath())) {
1155
+ const clash = storedZoneClaiming(certPath, zone);
1156
+ if (clash && !actual) {
1074
1157
  throw new CfldError(
1075
- "Login finished but no certificate was written.",
1076
- "Re-run `cfld login` and complete the browser step."
1158
+ `That cert is for the same domain as your saved ${clash} cert, not ${zone}.`,
1159
+ `Run \`cfld login --reauth --zone ${zone}\` and pick ${zone} in the browser.`
1077
1160
  );
1078
1161
  }
1162
+ importCert(zone, certPath);
1163
+ return zone;
1079
1164
  }
1080
1165
  async function ensureCert(zone, opts = { interactive: true }) {
1081
- const existing = resolveCert(zone);
1082
- if (existing) return existing;
1166
+ const usable = await usableCert(zone);
1167
+ if (usable) return usable;
1083
1168
  if (!opts.interactive) {
1084
1169
  throw new CfldError(
1085
1170
  `No Cloudflare certificate found for ${zone}.`,
1086
- "Run `cfld login` first (needs a browser), or set TUNNEL_ORIGIN_CERT."
1171
+ `Run \`cfld login --reauth --zone ${zone}\` first (needs a browser), or set TUNNEL_ORIGIN_CERT.`
1087
1172
  );
1088
1173
  }
1089
- await runLogin();
1090
- if (!certHasZone(defaultCertPath())) {
1091
- printZeroZoneGuidance();
1092
- throw new CfldError(
1093
- "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1094
- "Add a domain (see above), then re-run. Or use `cfld --quick` for a throwaway URL now."
1095
- );
1174
+ info(`No certificate for ${zone} yet \u2014 authorize it in the browser.`);
1175
+ const fresh = await runLogin();
1176
+ try {
1177
+ if (!certHasZone(fresh)) {
1178
+ printZeroZoneGuidance();
1179
+ throw new CfldError(
1180
+ "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1181
+ "Add a domain (see above), then re-run. Or use `cfld --quick` for a throwaway URL now."
1182
+ );
1183
+ }
1184
+ await saveCert(zone, fresh);
1185
+ } finally {
1186
+ discardLoginCert(fresh);
1096
1187
  }
1097
- const stored = importCert(zone);
1098
1188
  info(`Saved cert for ${zone}.`);
1099
- return stored;
1189
+ return certPathForZone(zone);
1190
+ }
1191
+ async function usableCert(zone) {
1192
+ const stored = storedCertForZone(zone);
1193
+ if (stored) {
1194
+ if (!storedZoneClaiming(stored, zone)) return stored;
1195
+ const actual2 = await certZoneName(stored);
1196
+ if (!actual2 || actual2 === zone) return stored;
1197
+ warn(`The saved cert for ${zone} is actually for ${actual2} \u2014 discarding it.`);
1198
+ rmSync(stored);
1199
+ }
1200
+ const def = defaultCertPath();
1201
+ if (!certHasZone(def)) return void 0;
1202
+ const actual = await certZoneName(def);
1203
+ if (actual === zone) return importCert(zone, def);
1204
+ if (!actual && !storedZoneClaiming(def, zone)) return def;
1205
+ return void 0;
1100
1206
  }
1101
1207
  function printZeroZoneGuidance() {
1102
1208
  warn("cfld can't create a permanent custom URL yet \u2014 your account has no domains.");
@@ -1123,11 +1229,11 @@ function printZeroZoneGuidance() {
1123
1229
 
1124
1230
  // src/util/gitignore.ts
1125
1231
  import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1126
- import { join as join4 } from "path";
1232
+ import { join as join6 } from "path";
1127
1233
  function ensureGitignored(cwd, pattern) {
1128
- const gitignorePath = join4(cwd, ".gitignore");
1234
+ const gitignorePath = join6(cwd, ".gitignore");
1129
1235
  const hasGitignore = existsSync9(gitignorePath);
1130
- const inRepo = hasGitignore || existsSync9(join4(cwd, ".git"));
1236
+ const inRepo = hasGitignore || existsSync9(join6(cwd, ".git"));
1131
1237
  if (!inRepo) return false;
1132
1238
  const existing = hasGitignore ? readFileSync6(gitignorePath, "utf8") : "";
1133
1239
  const present = existing.split("\n").map((l) => l.trim()).includes(pattern);
@@ -1309,7 +1415,7 @@ ${opts.reason}`);
1309
1415
  );
1310
1416
  process.once("SIGHUP", () => shutdown({ reason: "Terminal closed \u2014 stopping." }));
1311
1417
  if (interactive) {
1312
- const { startDashboard } = await import("./dashboard-ZJVAPOTE.js");
1418
+ const { startDashboard } = await import("./dashboard-Z4AN7H5V.js");
1313
1419
  dashboard = startDashboard(
1314
1420
  {
1315
1421
  url,
@@ -1376,7 +1482,7 @@ ${opts.reason}`);
1376
1482
  if (!up) warn(`Nothing is listening on :${ctx.port} yet \u2014 going live anyway.`);
1377
1483
  }
1378
1484
  if (!ctx.noEnv) {
1379
- const { changed } = upsertEnv(join5(ctx.cwd, ".env"), ctx.envKey, url);
1485
+ const { changed } = upsertEnv(join7(ctx.cwd, ".env"), ctx.envKey, url);
1380
1486
  if (changed && !dashboard) info(`Wrote ${ctx.envKey}=${url} to .env`);
1381
1487
  }
1382
1488
  if (dashboard) {
@@ -1553,7 +1659,7 @@ ${opts.reason ?? "Stopped ephemeral tunnel."}`);
1553
1659
  });
1554
1660
  }
1555
1661
  if (interactive) {
1556
- const { startDashboard } = await import("./dashboard-ZJVAPOTE.js");
1662
+ const { startDashboard } = await import("./dashboard-Z4AN7H5V.js");
1557
1663
  dashboard = startDashboard({
1558
1664
  url,
1559
1665
  target,
@@ -1753,20 +1859,26 @@ async function setupCommand(flags) {
1753
1859
  );
1754
1860
  return;
1755
1861
  }
1756
- await runLogin();
1757
- if (!certHasZone(defaultCertPath())) {
1758
- printZeroZoneGuidance();
1759
- if (await askConfirm("Start a temporary quick tunnel now instead?")) {
1760
- await quickCommand(flags);
1761
- return;
1862
+ const fresh = await runLogin();
1863
+ let zone;
1864
+ try {
1865
+ if (!certHasZone(fresh)) {
1866
+ printZeroZoneGuidance();
1867
+ if (await askConfirm("Start a temporary quick tunnel now instead?")) {
1868
+ await quickCommand(flags);
1869
+ return;
1870
+ }
1871
+ throw new CfldError(
1872
+ "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1873
+ "Add a domain (see above), then run `cfld setup` again."
1874
+ );
1762
1875
  }
1763
- throw new CfldError(
1764
- "Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
1765
- "Add a domain (see above), then run `cfld setup` again."
1766
- );
1876
+ const detected = await certZoneName(fresh);
1877
+ zone = await saveCert(detected ?? await askAuthorizedZone(knownZones), fresh);
1878
+ } finally {
1879
+ discardLoginCert(fresh);
1767
1880
  }
1768
- const zone = await askAuthorizedZone(knownZones);
1769
- const certPath = importCert(zone);
1881
+ const certPath = certPathForZone(zone);
1770
1882
  info(`Saved cert for ${zone}.`);
1771
1883
  await configureProject(flags, { zone, certPath, cwd });
1772
1884
  await offerGoLive(flags, zone);
@@ -1997,9 +2109,9 @@ function resolveCurrentEntry2() {
1997
2109
  import pc5 from "picocolors";
1998
2110
 
1999
2111
  // src/core/service.ts
2000
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync6 } from "fs";
2112
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
2001
2113
  import { homedir as homedir2, platform } from "os";
2002
- import { join as join6 } from "path";
2114
+ import { join as join8 } from "path";
2003
2115
  function serviceSlug(name) {
2004
2116
  return name.replace(/^cfld-/, "");
2005
2117
  }
@@ -2007,16 +2119,16 @@ function launchAgentLabel(name) {
2007
2119
  return `com.cfld.${serviceSlug(name)}`;
2008
2120
  }
2009
2121
  function launchAgentPath(name) {
2010
- return join6(homedir2(), "Library", "LaunchAgents", `${launchAgentLabel(name)}.plist`);
2122
+ return join8(homedir2(), "Library", "LaunchAgents", `${launchAgentLabel(name)}.plist`);
2011
2123
  }
2012
2124
  function systemdUnitName(name) {
2013
2125
  return `cfld-${serviceSlug(name)}.service`;
2014
2126
  }
2015
2127
  function systemdUnitPath(name) {
2016
- return join6(homedir2(), ".config", "systemd", "user", systemdUnitName(name));
2128
+ return join8(homedir2(), ".config", "systemd", "user", systemdUnitName(name));
2017
2129
  }
2018
2130
  function logDir() {
2019
- return join6(cfldDir(), "logs");
2131
+ return join8(cfldDir(), "logs");
2020
2132
  }
2021
2133
  function renderLaunchAgent(spec) {
2022
2134
  const args = [
@@ -2030,8 +2142,8 @@ function renderLaunchAgent(spec) {
2030
2142
  spec.name
2031
2143
  ];
2032
2144
  const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
2033
- const out = join6(logDir(), `${serviceSlug(spec.name)}.out.log`);
2034
- const err = join6(logDir(), `${serviceSlug(spec.name)}.err.log`);
2145
+ const out = join8(logDir(), `${serviceSlug(spec.name)}.out.log`);
2146
+ const err = join8(logDir(), `${serviceSlug(spec.name)}.err.log`);
2035
2147
  return `<?xml version="1.0" encoding="UTF-8"?>
2036
2148
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2037
2149
  <plist version="1.0">
@@ -2096,10 +2208,10 @@ function assertSupported() {
2096
2208
  }
2097
2209
  async function installService(spec) {
2098
2210
  const p = assertSupported();
2099
- mkdirSync3(logDir(), { recursive: true });
2211
+ mkdirSync5(logDir(), { recursive: true });
2100
2212
  if (p === "darwin") {
2101
2213
  const path = launchAgentPath(spec.name);
2102
- mkdirSync3(join6(homedir2(), "Library", "LaunchAgents"), { recursive: true });
2214
+ mkdirSync5(join8(homedir2(), "Library", "LaunchAgents"), { recursive: true });
2103
2215
  writeFileSync6(path, renderLaunchAgent(spec), "utf8");
2104
2216
  await exec("launchctl", ["unload", path]);
2105
2217
  const res = await exec("launchctl", ["load", "-w", path]);
@@ -2111,7 +2223,7 @@ async function installService(spec) {
2111
2223
  }
2112
2224
  } else {
2113
2225
  const path = systemdUnitPath(spec.name);
2114
- mkdirSync3(join6(homedir2(), ".config", "systemd", "user"), { recursive: true });
2226
+ mkdirSync5(join8(homedir2(), ".config", "systemd", "user"), { recursive: true });
2115
2227
  writeFileSync6(path, renderSystemdUnit(spec), "utf8");
2116
2228
  await exec("systemctl", ["--user", "daemon-reload"]);
2117
2229
  const res = await exec("systemctl", [
@@ -2134,13 +2246,13 @@ async function uninstallService(name) {
2134
2246
  const path = launchAgentPath(name);
2135
2247
  if (existsSync10(path)) {
2136
2248
  await exec("launchctl", ["unload", "-w", path]);
2137
- rmSync(path, { force: true });
2249
+ rmSync2(path, { force: true });
2138
2250
  }
2139
2251
  } else {
2140
2252
  const unit = systemdUnitName(name);
2141
2253
  await exec("systemctl", ["--user", "disable", "--now", unit]);
2142
2254
  const path = systemdUnitPath(name);
2143
- if (existsSync10(path)) rmSync(path, { force: true });
2255
+ if (existsSync10(path)) rmSync2(path, { force: true });
2144
2256
  await exec("systemctl", ["--user", "daemon-reload"]);
2145
2257
  }
2146
2258
  }
@@ -2260,6 +2372,14 @@ async function doctorCommand() {
2260
2372
  const zones = listStoredZones();
2261
2373
  if (zones.length) out(` ${ok} cert store: ${zones.join(", ")}`);
2262
2374
  else out(` ${pc6.dim("\xB7")} cert store empty`);
2375
+ for (const zone of zones) {
2376
+ const actual = await certZoneName(certPathForZone(zone));
2377
+ if (actual && actual !== zone) {
2378
+ out(
2379
+ ` ${bad} ${zone}.pem is really a cert for ${actual} \u2014 fix with \`cfld login --reauth --zone ${zone}\``
2380
+ );
2381
+ }
2382
+ }
2263
2383
  const entries = listEntries();
2264
2384
  out(` ${ok} registry: ${entries.length} tunnel(s)`);
2265
2385
  out("");
@@ -2268,8 +2388,8 @@ async function doctorCommand() {
2268
2388
  // src/cli.ts
2269
2389
  function readVersion() {
2270
2390
  try {
2271
- const here = dirname(fileURLToPath(import.meta.url));
2272
- const pkg = JSON.parse(readFileSync7(join7(here, "../package.json"), "utf8"));
2391
+ const here = dirname2(fileURLToPath(import.meta.url));
2392
+ const pkg = JSON.parse(readFileSync7(join9(here, "../package.json"), "utf8"));
2273
2393
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
2274
2394
  } catch {
2275
2395
  return "0.0.0";
@@ -94,7 +94,12 @@ function LogColumn({
94
94
  borderLeft: Boolean(divider),
95
95
  children: [
96
96
  title ? /* @__PURE__ */ jsx(Text, { color: titleColor, bold: true, wrap: "truncate-end", children: title }) : null,
97
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "flex-end", overflow: "hidden", children: lines.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: placeholder }) : lines.slice(-rows).map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: line }, i)) })
97
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "flex-end", overflow: "hidden", children: lines.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: placeholder }) : (
98
+ // Wrap rather than truncate so long lines (e.g. magic-link / OAuth
99
+ // URLs) stay fully visible and selectable instead of ending in "…".
100
+ // The bottom-pinned box clips older wrapped lines off the top.
101
+ lines.slice(-rows).map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: line }, i))
102
+ ) })
98
103
  ]
99
104
  }
100
105
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliftonc/cfld",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Zero-token, persistent Cloudflare tunnel CLI for local dev — same public HTTPS URL every run.",
5
5
  "type": "module",
6
6
  "bin": {