@cliftonc/cfld 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -6
- package/dist/cli.js +150 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,17 +4,44 @@
|
|
|
4
4
|
|
|
5
5
|
`cfld` wraps [`cloudflared`](https://github.com/cloudflare/cloudflared) into a frictionless dev experience: the **same** custom URL every run (not a random `trycloudflare.com` one), authorized with a **browser login** (zero API tokens), reusing the same named tunnel and DNS record across restarts instead of churning them. It runs a **live dashboard** (URL, connection count, request/latency counters, tailing logs), **auto-reconnects** on edge blips, and **guards** against stealing a DNS record that belongs to another tunnel.
|
|
6
6
|
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
**New here? Just run this — it walks you through everything, no account or config needed to start:**
|
|
10
|
+
|
|
7
11
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
npx @cliftonc/cfld setup
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`setup` is the front door. It checks `cloudflared` for you, then lets you pick:
|
|
16
|
+
|
|
17
|
+
1. **A public URL right now** — free, no account, no domain (a temporary `trycloudflare.com` URL). Great for a webhook, a demo, or just trying it.
|
|
18
|
+
2. **A persistent custom URL** — opens the browser to sign in (or **sign up** — it's free), then wires up the same URL for every future run.
|
|
19
|
+
|
|
20
|
+
You don't even have to remember `setup`: the **first time you run bare `cfld`** in a fresh project, it drops you into this same guided flow so you're never stuck guessing.
|
|
21
|
+
|
|
22
|
+
Already know what you want? Skip straight to it:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# instant public URL in seconds — no account, no config
|
|
26
|
+
npx @cliftonc/cfld --quick 3000 # temporary trycloudflare.com URL
|
|
27
|
+
|
|
28
|
+
# the everyday command — reuse-or-create your persistent tunnel and go live
|
|
29
|
+
npx @cliftonc/cfld # detect your dev port, go live on the same URL
|
|
10
30
|
npx @cliftonc/cfld 3000 # explicit port
|
|
11
|
-
npx @cliftonc/cfld --quick # ephemeral trycloudflare.com URL — no domain needed
|
|
12
31
|
|
|
13
32
|
# or install once and use the short `cfld` command everywhere
|
|
14
33
|
npm i -g @cliftonc/cfld
|
|
15
|
-
cfld
|
|
34
|
+
cfld setup
|
|
16
35
|
```
|
|
17
36
|
|
|
37
|
+
### What it costs
|
|
38
|
+
|
|
39
|
+
`cfld` is built to get you going at zero → near-zero cost, and it's honest about the one thing it can't automate:
|
|
40
|
+
|
|
41
|
+
- **Instant URL** — `cfld --quick`: **free**, no account, no domain. The URL is temporary and changes each run.
|
|
42
|
+
- **Cloudflare account** — **free**. `cfld` opens the browser to sign in (the same page has a **Sign up** link); there's no API token to create. Account signup itself can't be scripted — Cloudflare requires a browser, terms acceptance, and email verification.
|
|
43
|
+
- **Persistent custom URL** — needs a **domain on Cloudflare**. Free if you already own one (just add the site); otherwise a domain is a few dollars/year via [Cloudflare Registrar](https://dash.cloudflare.com/?to=/:account/domains/register). No account has a free permanent subdomain.
|
|
44
|
+
|
|
18
45
|
## Why not just `cloudflared`?
|
|
19
46
|
|
|
20
47
|
The persistent-tunnel flow normally means: create an API token in the dashboard, find your zone/account IDs, `tunnel create`, hand-write a `config.yml`, `route dns`, and remember not to delete anything. `cfld` collapses all of that into one idempotent command and adds the dev niceties (auto port detection, deterministic hostname, `.env` write-back, a live status view).
|
|
@@ -46,10 +73,11 @@ A machine-wide registry at `~/.cfld/registry.json` indexes every tunnel, so you
|
|
|
46
73
|
|
|
47
74
|
| Command | What it does |
|
|
48
75
|
|---|---|
|
|
76
|
+
| `cfld --quick [port]` | Instant `trycloudflare.com` URL — no account, no cert/domain (temporary). |
|
|
77
|
+
| `cfld setup` | Guided first-run — pick the instant free URL or set up a persistent one. |
|
|
49
78
|
| `cfld [port]` | Ensure + run a persistent tunnel (reuses the same URL). |
|
|
50
|
-
| `cfld --quick [port]` | Ephemeral `trycloudflare.com` URL — no cert/domain. |
|
|
51
79
|
| `cfld login [--reauth]` | Authorize with Cloudflare in your browser. `--reauth` adds another domain. |
|
|
52
|
-
| `cfld init` |
|
|
80
|
+
| `cfld init` | Configure a persistent tunnel without running it. |
|
|
53
81
|
| `cfld list` | List all tunnels across projects/domains. |
|
|
54
82
|
| `cfld status [name]` | Show details for a tunnel. |
|
|
55
83
|
| `cfld up <name>` | Run a registered tunnel by name from anywhere. |
|
package/dist/cli.js
CHANGED
|
@@ -1555,6 +1555,11 @@ async function initCommand(flags) {
|
|
|
1555
1555
|
zone = normalizeZone(await askText("Domain (zone) on Cloudflare?", "example.com"));
|
|
1556
1556
|
}
|
|
1557
1557
|
const certPath = await ensureCert(zone, { interactive: true });
|
|
1558
|
+
await configureProject(flags, { zone, certPath, cwd });
|
|
1559
|
+
note("Run `cfld` to go live.");
|
|
1560
|
+
}
|
|
1561
|
+
async function configureProject(flags, ctx) {
|
|
1562
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1558
1563
|
const defaultSlug = deriveSlug(cwd, flags.name);
|
|
1559
1564
|
const name = slugify(await askText("Project name (used in the URL)", defaultSlug) || defaultSlug);
|
|
1560
1565
|
const detected = await detectDevPorts();
|
|
@@ -1566,13 +1571,13 @@ async function initCommand(flags) {
|
|
|
1566
1571
|
if (!Number.isInteger(port) || port <= 0) {
|
|
1567
1572
|
throw new CfldError(`"${portStr}" is not a valid port.`);
|
|
1568
1573
|
}
|
|
1569
|
-
const hostname = flags.host ?? buildHostname(name, zone);
|
|
1574
|
+
const hostname = flags.host ?? buildHostname(name, ctx.zone);
|
|
1570
1575
|
const result = await reconcile(
|
|
1571
1576
|
{
|
|
1572
1577
|
slug: name,
|
|
1573
1578
|
tunnelName: tunnelNameForSlug(name),
|
|
1574
|
-
zone,
|
|
1575
|
-
certPath,
|
|
1579
|
+
zone: ctx.zone,
|
|
1580
|
+
certPath: ctx.certPath,
|
|
1576
1581
|
routes: [{ hostname, port }],
|
|
1577
1582
|
projectDir: cwd,
|
|
1578
1583
|
force: flags.force
|
|
@@ -1584,7 +1589,7 @@ async function initCommand(flags) {
|
|
|
1584
1589
|
);
|
|
1585
1590
|
writeProjectConfig(cwd, {
|
|
1586
1591
|
name,
|
|
1587
|
-
zone,
|
|
1592
|
+
zone: ctx.zone,
|
|
1588
1593
|
uuid: result.uuid,
|
|
1589
1594
|
hostname,
|
|
1590
1595
|
port,
|
|
@@ -1592,7 +1597,138 @@ async function initCommand(flags) {
|
|
|
1592
1597
|
});
|
|
1593
1598
|
if (ensureGitignored(cwd, ".cfld.json")) info("Added .cfld.json to .gitignore.");
|
|
1594
1599
|
done(`Configured https://${hostname} \u2192 localhost:${port}`);
|
|
1595
|
-
|
|
1600
|
+
return { name, hostname, port };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// src/commands/setup.ts
|
|
1604
|
+
async function setupCommand(flags) {
|
|
1605
|
+
if (!isInteractive()) {
|
|
1606
|
+
throw new CfldError(
|
|
1607
|
+
"`cfld setup` is interactive.",
|
|
1608
|
+
"In scripts, use `cfld --quick`, or `cfld` with --zone/--host/--name flags."
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
step("Let's get cfld set up.");
|
|
1612
|
+
await preflightCloudflared();
|
|
1613
|
+
const cwd = process.cwd();
|
|
1614
|
+
const knownZones = Array.from(
|
|
1615
|
+
/* @__PURE__ */ new Set([...listStoredZones(), ...listEntries().map((e) => e.zone)])
|
|
1616
|
+
);
|
|
1617
|
+
const alreadyAuthorized = knownZones.length > 0 || certHasZone(defaultCertPath());
|
|
1618
|
+
if (alreadyAuthorized) {
|
|
1619
|
+
const config = readProjectConfig(cwd);
|
|
1620
|
+
if (config?.uuid || config?.zone) {
|
|
1621
|
+
done("You're already set up \u2014 run `cfld` to go live.");
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
info("You're already authorized with Cloudflare. Let's configure this project.");
|
|
1625
|
+
const zone2 = await askAuthorizedZone(knownZones);
|
|
1626
|
+
const certPath2 = await ensureCert(zone2, { interactive: true });
|
|
1627
|
+
await configureProject(flags, { zone: zone2, certPath: certPath2, cwd });
|
|
1628
|
+
await offerGoLive(flags, zone2);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const path = await askSelect("How do you want to start?", [
|
|
1632
|
+
{
|
|
1633
|
+
value: "quick",
|
|
1634
|
+
label: "Get a public URL right now \u2014 free, no account (temporary URL)"
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
value: "persistent",
|
|
1638
|
+
label: "Set up a persistent URL \u2014 free Cloudflare account + a domain"
|
|
1639
|
+
}
|
|
1640
|
+
]);
|
|
1641
|
+
if (path === "quick") {
|
|
1642
|
+
note(
|
|
1643
|
+
" Starting a temporary tunnel. Run `cfld setup` again anytime to set up a persistent URL."
|
|
1644
|
+
);
|
|
1645
|
+
await quickCommand(flags);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
note(
|
|
1649
|
+
[
|
|
1650
|
+
"",
|
|
1651
|
+
" A browser window will open to authorize cfld with Cloudflare.",
|
|
1652
|
+
` \u2022 No account yet? Click "Sign up" on that page \u2014 it's free.`,
|
|
1653
|
+
" \u2022 Then pick (or add) the domain you want to use.",
|
|
1654
|
+
" Prefer to create the account first? Open:",
|
|
1655
|
+
" https://dash.cloudflare.com/sign-up",
|
|
1656
|
+
""
|
|
1657
|
+
].join("\n")
|
|
1658
|
+
);
|
|
1659
|
+
if (!await askConfirm("Open the browser to authorize now?")) {
|
|
1660
|
+
note(
|
|
1661
|
+
"No problem \u2014 run `cfld setup` again when you're ready, or `cfld --quick` for a temporary URL."
|
|
1662
|
+
);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
await runLogin();
|
|
1666
|
+
if (!certHasZone(defaultCertPath())) {
|
|
1667
|
+
printZeroZoneGuidance();
|
|
1668
|
+
if (await askConfirm("Start a temporary quick tunnel now instead?")) {
|
|
1669
|
+
await quickCommand(flags);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
throw new CfldError(
|
|
1673
|
+
"Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
|
|
1674
|
+
"Add a domain (see above), then run `cfld setup` again."
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
const zone = await askAuthorizedZone(knownZones);
|
|
1678
|
+
const certPath = importCert(zone);
|
|
1679
|
+
info(`Saved cert for ${zone}.`);
|
|
1680
|
+
await configureProject(flags, { zone, certPath, cwd });
|
|
1681
|
+
await offerGoLive(flags, zone);
|
|
1682
|
+
}
|
|
1683
|
+
async function preflightCloudflared() {
|
|
1684
|
+
try {
|
|
1685
|
+
const bin = await resolveBinary();
|
|
1686
|
+
const res = await exec(bin, ["--version"]);
|
|
1687
|
+
const version = (res.stdout.trim() || res.stderr.trim()).split("\n")[0];
|
|
1688
|
+
done(`cloudflared ready${version ? ` \u2014 ${version}` : ""}`);
|
|
1689
|
+
} catch {
|
|
1690
|
+
warn("cloudflared isn't installed, and the bundled binary couldn't load.");
|
|
1691
|
+
note(
|
|
1692
|
+
[
|
|
1693
|
+
"",
|
|
1694
|
+
" Install it, then run `cfld setup` again:",
|
|
1695
|
+
" macOS brew install cloudflared",
|
|
1696
|
+
" Linux apt / dnf / pacman install cloudflared",
|
|
1697
|
+
" Windows winget install --id Cloudflare.cloudflared",
|
|
1698
|
+
" Any OS https://developers.cloudflare.com/tunnel/downloads/",
|
|
1699
|
+
""
|
|
1700
|
+
].join("\n")
|
|
1701
|
+
);
|
|
1702
|
+
throw new CfldError(
|
|
1703
|
+
"cloudflared is required to continue.",
|
|
1704
|
+
"Install it (see above), or set CFLD_CLOUDFLARED to its path."
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function isBlankFirstRun(flags, cwd, interactive = isInteractive()) {
|
|
1709
|
+
if (!interactive) return false;
|
|
1710
|
+
if (flags.zone || flags.host || flags.quick) return false;
|
|
1711
|
+
if (readProjectConfig(cwd)) return false;
|
|
1712
|
+
if (listStoredZones().length > 0) return false;
|
|
1713
|
+
if (certHasZone(defaultCertPath())) return false;
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1716
|
+
async function askAuthorizedZone(knownZones) {
|
|
1717
|
+
if (knownZones.length) {
|
|
1718
|
+
const picked = await askSelect("Which domain did you authorize?", [
|
|
1719
|
+
...knownZones.map((z) => ({ value: z, label: z })),
|
|
1720
|
+
{ value: "__new__", label: "A different domain\u2026" }
|
|
1721
|
+
]);
|
|
1722
|
+
if (picked !== "__new__") return picked;
|
|
1723
|
+
}
|
|
1724
|
+
return normalizeZone(await askText("Which domain did you authorize?", "example.com"));
|
|
1725
|
+
}
|
|
1726
|
+
async function offerGoLive(flags, zone) {
|
|
1727
|
+
if (await askConfirm("Go live now?")) {
|
|
1728
|
+
await runCommand({ ...flags, zone });
|
|
1729
|
+
} else {
|
|
1730
|
+
note("Run `cfld` to go live.");
|
|
1731
|
+
}
|
|
1596
1732
|
}
|
|
1597
1733
|
|
|
1598
1734
|
// src/commands/list.ts
|
|
@@ -2061,6 +2197,7 @@ function resolveExec(afterDash, execFlag) {
|
|
|
2061
2197
|
return void 0;
|
|
2062
2198
|
}
|
|
2063
2199
|
var SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
2200
|
+
"setup",
|
|
2064
2201
|
"login",
|
|
2065
2202
|
"init",
|
|
2066
2203
|
"list",
|
|
@@ -2074,11 +2211,12 @@ var SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
2074
2211
|
var HELP = `${pc7.bold("cfld")} \u2014 persistent Cloudflare tunnels for local dev
|
|
2075
2212
|
|
|
2076
2213
|
${pc7.bold("Usage")}
|
|
2214
|
+
cfld --quick [port] Instant public URL, no account needed (temporary)
|
|
2215
|
+
cfld setup Guided first-run: free instant URL, or persistent setup
|
|
2077
2216
|
cfld [port] Ensure + run a persistent tunnel (reuses the same URL)
|
|
2078
2217
|
cfld [port] -- <cmd> Run + supervise your dev server too (one lifecycle)
|
|
2079
|
-
cfld --quick [port] Ephemeral trycloudflare.com URL (no domain needed)
|
|
2080
2218
|
cfld login [--reauth] Authorize with Cloudflare in your browser
|
|
2081
|
-
cfld init
|
|
2219
|
+
cfld init Configure a persistent tunnel (don't run)
|
|
2082
2220
|
cfld list List all tunnels across projects/domains
|
|
2083
2221
|
cfld status [name] Show details for a tunnel
|
|
2084
2222
|
cfld up <name> Run a registered tunnel by name from anywhere
|
|
@@ -2151,6 +2289,9 @@ async function main(rawArgv) {
|
|
|
2151
2289
|
case "help":
|
|
2152
2290
|
process.stdout.write(HELP);
|
|
2153
2291
|
return;
|
|
2292
|
+
case "setup":
|
|
2293
|
+
await setupCommand(flags);
|
|
2294
|
+
return;
|
|
2154
2295
|
case "login":
|
|
2155
2296
|
await loginCommand(flags);
|
|
2156
2297
|
return;
|
|
@@ -2185,6 +2326,8 @@ async function main(rawArgv) {
|
|
|
2185
2326
|
}
|
|
2186
2327
|
if (flags.quick) {
|
|
2187
2328
|
await quickCommand(flags);
|
|
2329
|
+
} else if (isBlankFirstRun(flags, process.cwd())) {
|
|
2330
|
+
await setupCommand(flags);
|
|
2188
2331
|
} else {
|
|
2189
2332
|
await runCommand(flags);
|
|
2190
2333
|
}
|