@cliftonc/cfld 0.2.0 → 0.2.2
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 +36 -6
- package/dist/cli.js +193 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
# cfld
|
|
2
2
|
|
|
3
|
+
**`cfld` = cloudflare local dev.**
|
|
4
|
+
|
|
3
5
|
**One command to give your localhost a permanent public HTTPS URL — no API tokens, no teardown.**
|
|
4
6
|
|
|
5
7
|
`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
8
|
|
|
9
|
+
## Getting started
|
|
10
|
+
|
|
11
|
+
**New here? Just run this — it walks you through everything, no account or config needed to start:**
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @cliftonc/cfld setup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`setup` is the front door. It checks `cloudflared` for you, then lets you pick:
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
Already know what you want? Skip straight to it:
|
|
25
|
+
|
|
7
26
|
```bash
|
|
8
|
-
#
|
|
9
|
-
npx @cliftonc/cfld
|
|
27
|
+
# instant public URL in seconds — no account, no config
|
|
28
|
+
npx @cliftonc/cfld --quick 3000 # temporary trycloudflare.com URL
|
|
29
|
+
|
|
30
|
+
# the everyday command — reuse-or-create your persistent tunnel and go live
|
|
31
|
+
npx @cliftonc/cfld # detect your dev port, go live on the same URL
|
|
10
32
|
npx @cliftonc/cfld 3000 # explicit port
|
|
11
|
-
npx @cliftonc/cfld --quick # ephemeral trycloudflare.com URL — no domain needed
|
|
12
33
|
|
|
13
34
|
# or install once and use the short `cfld` command everywhere
|
|
14
35
|
npm i -g @cliftonc/cfld
|
|
15
|
-
cfld
|
|
36
|
+
cfld setup
|
|
16
37
|
```
|
|
17
38
|
|
|
39
|
+
### What it costs
|
|
40
|
+
|
|
41
|
+
`cfld` is built to get you going at zero → near-zero cost, and it's honest about the one thing it can't automate:
|
|
42
|
+
|
|
43
|
+
- **Instant URL** — `cfld --quick`: **free**, no account, no domain. The URL is temporary and changes each run.
|
|
44
|
+
- **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.
|
|
45
|
+
- **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.
|
|
46
|
+
|
|
18
47
|
## Why not just `cloudflared`?
|
|
19
48
|
|
|
20
49
|
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 +75,11 @@ A machine-wide registry at `~/.cfld/registry.json` indexes every tunnel, so you
|
|
|
46
75
|
|
|
47
76
|
| Command | What it does |
|
|
48
77
|
|---|---|
|
|
78
|
+
| `cfld --quick [port]` | Instant `trycloudflare.com` URL — no account, no cert/domain (temporary). |
|
|
79
|
+
| `cfld setup` | Guided first-run — pick the instant free URL or set up a persistent one. |
|
|
49
80
|
| `cfld [port]` | Ensure + run a persistent tunnel (reuses the same URL). |
|
|
50
|
-
| `cfld --quick [port]` | Ephemeral `trycloudflare.com` URL — no cert/domain. |
|
|
51
81
|
| `cfld login [--reauth]` | Authorize with Cloudflare in your browser. `--reauth` adds another domain. |
|
|
52
|
-
| `cfld init` |
|
|
82
|
+
| `cfld init` | Configure a persistent tunnel without running it. |
|
|
53
83
|
| `cfld list` | List all tunnels across projects/domains. |
|
|
54
84
|
| `cfld status [name]` | Show details for a tunnel. |
|
|
55
85
|
| `cfld up <name>` | Run a registered tunnel by name from anywhere. |
|
package/dist/cli.js
CHANGED
|
@@ -700,9 +700,20 @@ function startChild(options) {
|
|
|
700
700
|
const exited = new Promise((resolve) => {
|
|
701
701
|
markExited = resolve;
|
|
702
702
|
});
|
|
703
|
+
const trackedPgids = /* @__PURE__ */ new Set();
|
|
704
|
+
const trackTree = () => {
|
|
705
|
+
const pid = child.pid;
|
|
706
|
+
if (pid === void 0) return;
|
|
707
|
+
for (const g of descendantPgids(pid)) {
|
|
708
|
+
if (g > 1 && g !== process.pid) trackedPgids.add(g);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
const poll = setInterval(trackTree, options.pollTreeMs ?? 1500);
|
|
712
|
+
poll.unref();
|
|
703
713
|
wireLines2(child, "stdout", options.onLine);
|
|
704
714
|
wireLines2(child, "stderr", options.onLine);
|
|
705
715
|
child.on("error", (err) => {
|
|
716
|
+
clearInterval(poll);
|
|
706
717
|
markExited();
|
|
707
718
|
if (stopped) return;
|
|
708
719
|
stopped = true;
|
|
@@ -710,11 +721,18 @@ function startChild(options) {
|
|
|
710
721
|
options.onLine?.(String(err.message ?? err), "stderr");
|
|
711
722
|
});
|
|
712
723
|
child.on("exit", (code, signal) => {
|
|
724
|
+
clearInterval(poll);
|
|
713
725
|
markExited();
|
|
714
726
|
if (stopped) return;
|
|
715
727
|
stopped = true;
|
|
716
728
|
options.onExit?.(code, signal);
|
|
717
729
|
});
|
|
730
|
+
const signalTree = (signal) => {
|
|
731
|
+
trackTree();
|
|
732
|
+
const pid = child.pid;
|
|
733
|
+
if (pid !== void 0) trySignalGroup(pid, signal);
|
|
734
|
+
for (const g of trackedPgids) trySignalGroup(g, signal);
|
|
735
|
+
};
|
|
718
736
|
return {
|
|
719
737
|
exited,
|
|
720
738
|
get pid() {
|
|
@@ -723,63 +741,64 @@ function startChild(options) {
|
|
|
723
741
|
stop() {
|
|
724
742
|
if (stopped) return;
|
|
725
743
|
stopped = true;
|
|
726
|
-
|
|
744
|
+
clearInterval(poll);
|
|
745
|
+
signalTree("SIGINT");
|
|
727
746
|
const t = setTimeout(() => {
|
|
728
|
-
if (child.exitCode === null && !child.signalCode)
|
|
747
|
+
if (child.exitCode === null && !child.signalCode) signalTree("SIGKILL");
|
|
729
748
|
}, 4e3);
|
|
730
749
|
t.unref();
|
|
731
750
|
},
|
|
732
751
|
kill() {
|
|
733
|
-
|
|
752
|
+
clearInterval(poll);
|
|
753
|
+
signalTree("SIGKILL");
|
|
734
754
|
}
|
|
735
755
|
};
|
|
736
756
|
}
|
|
737
|
-
function
|
|
757
|
+
function descendantPgids(pid) {
|
|
738
758
|
let out = "";
|
|
739
759
|
try {
|
|
740
|
-
out = execSync("ps -A -o pid=,ppid=", { encoding: "utf8" });
|
|
760
|
+
out = execSync("ps -A -o pid=,ppid=,pgid=", { encoding: "utf8" });
|
|
741
761
|
} catch {
|
|
742
762
|
return [];
|
|
743
763
|
}
|
|
744
764
|
const childrenOf = /* @__PURE__ */ new Map();
|
|
765
|
+
const pgidOf = /* @__PURE__ */ new Map();
|
|
745
766
|
for (const line of out.split("\n")) {
|
|
746
|
-
const m = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
767
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)$/);
|
|
747
768
|
if (!m) continue;
|
|
748
|
-
const
|
|
769
|
+
const proc = Number(m[1]);
|
|
749
770
|
const parent = Number(m[2]);
|
|
771
|
+
pgidOf.set(proc, Number(m[3]));
|
|
750
772
|
const list = childrenOf.get(parent);
|
|
751
|
-
if (list) list.push(
|
|
752
|
-
else childrenOf.set(parent, [
|
|
773
|
+
if (list) list.push(proc);
|
|
774
|
+
else childrenOf.set(parent, [proc]);
|
|
753
775
|
}
|
|
754
|
-
const
|
|
776
|
+
const pgids = /* @__PURE__ */ new Set();
|
|
777
|
+
const seen = /* @__PURE__ */ new Set();
|
|
755
778
|
const stack = [pid];
|
|
756
779
|
while (stack.length) {
|
|
757
780
|
for (const child of childrenOf.get(stack.pop()) ?? []) {
|
|
758
|
-
if (
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
781
|
+
if (seen.has(child)) continue;
|
|
782
|
+
seen.add(child);
|
|
783
|
+
stack.push(child);
|
|
784
|
+
const g = pgidOf.get(child);
|
|
785
|
+
if (g !== void 0) pgids.add(g);
|
|
762
786
|
}
|
|
763
787
|
}
|
|
764
|
-
return
|
|
788
|
+
return [...pgids];
|
|
765
789
|
}
|
|
766
|
-
function
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
} catch {
|
|
771
|
-
}
|
|
790
|
+
function trySignalGroup(pgid, signal) {
|
|
791
|
+
try {
|
|
792
|
+
process.kill(-pgid, signal);
|
|
793
|
+
} catch {
|
|
772
794
|
}
|
|
773
795
|
}
|
|
774
796
|
function reapChild(child, graceMs = 3e3) {
|
|
775
|
-
const tree = child.pid ? descendantPids(child.pid) : [];
|
|
776
797
|
child.stop();
|
|
777
|
-
signalPids(tree, "SIGINT");
|
|
778
798
|
return new Promise((resolve) => {
|
|
779
799
|
const done2 = () => {
|
|
780
800
|
clearTimeout(grace);
|
|
781
801
|
child.kill();
|
|
782
|
-
signalPids(tree, "SIGKILL");
|
|
783
802
|
resolve();
|
|
784
803
|
};
|
|
785
804
|
const grace = setTimeout(done2, graceMs);
|
|
@@ -787,18 +806,6 @@ function reapChild(child, graceMs = 3e3) {
|
|
|
787
806
|
child.exited.then(done2);
|
|
788
807
|
});
|
|
789
808
|
}
|
|
790
|
-
function killGroup(child, signal) {
|
|
791
|
-
const pid = child.pid;
|
|
792
|
-
if (pid === void 0) return;
|
|
793
|
-
try {
|
|
794
|
-
process.kill(-pid, signal);
|
|
795
|
-
} catch {
|
|
796
|
-
try {
|
|
797
|
-
child.kill(signal);
|
|
798
|
-
} catch {
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
809
|
function wireLines2(child, stream, onLine) {
|
|
803
810
|
const source = stream === "stdout" ? child.stdout : child.stderr;
|
|
804
811
|
if (!source || !onLine) return;
|
|
@@ -1555,6 +1562,11 @@ async function initCommand(flags) {
|
|
|
1555
1562
|
zone = normalizeZone(await askText("Domain (zone) on Cloudflare?", "example.com"));
|
|
1556
1563
|
}
|
|
1557
1564
|
const certPath = await ensureCert(zone, { interactive: true });
|
|
1565
|
+
await configureProject(flags, { zone, certPath, cwd });
|
|
1566
|
+
note("Run `cfld` to go live.");
|
|
1567
|
+
}
|
|
1568
|
+
async function configureProject(flags, ctx) {
|
|
1569
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1558
1570
|
const defaultSlug = deriveSlug(cwd, flags.name);
|
|
1559
1571
|
const name = slugify(await askText("Project name (used in the URL)", defaultSlug) || defaultSlug);
|
|
1560
1572
|
const detected = await detectDevPorts();
|
|
@@ -1566,13 +1578,13 @@ async function initCommand(flags) {
|
|
|
1566
1578
|
if (!Number.isInteger(port) || port <= 0) {
|
|
1567
1579
|
throw new CfldError(`"${portStr}" is not a valid port.`);
|
|
1568
1580
|
}
|
|
1569
|
-
const hostname = flags.host ?? buildHostname(name, zone);
|
|
1581
|
+
const hostname = flags.host ?? buildHostname(name, ctx.zone);
|
|
1570
1582
|
const result = await reconcile(
|
|
1571
1583
|
{
|
|
1572
1584
|
slug: name,
|
|
1573
1585
|
tunnelName: tunnelNameForSlug(name),
|
|
1574
|
-
zone,
|
|
1575
|
-
certPath,
|
|
1586
|
+
zone: ctx.zone,
|
|
1587
|
+
certPath: ctx.certPath,
|
|
1576
1588
|
routes: [{ hostname, port }],
|
|
1577
1589
|
projectDir: cwd,
|
|
1578
1590
|
force: flags.force
|
|
@@ -1584,7 +1596,7 @@ async function initCommand(flags) {
|
|
|
1584
1596
|
);
|
|
1585
1597
|
writeProjectConfig(cwd, {
|
|
1586
1598
|
name,
|
|
1587
|
-
zone,
|
|
1599
|
+
zone: ctx.zone,
|
|
1588
1600
|
uuid: result.uuid,
|
|
1589
1601
|
hostname,
|
|
1590
1602
|
port,
|
|
@@ -1592,7 +1604,138 @@ async function initCommand(flags) {
|
|
|
1592
1604
|
});
|
|
1593
1605
|
if (ensureGitignored(cwd, ".cfld.json")) info("Added .cfld.json to .gitignore.");
|
|
1594
1606
|
done(`Configured https://${hostname} \u2192 localhost:${port}`);
|
|
1595
|
-
|
|
1607
|
+
return { name, hostname, port };
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/commands/setup.ts
|
|
1611
|
+
async function setupCommand(flags) {
|
|
1612
|
+
if (!isInteractive()) {
|
|
1613
|
+
throw new CfldError(
|
|
1614
|
+
"`cfld setup` is interactive.",
|
|
1615
|
+
"In scripts, use `cfld --quick`, or `cfld` with --zone/--host/--name flags."
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
step("Let's get cfld set up.");
|
|
1619
|
+
await preflightCloudflared();
|
|
1620
|
+
const cwd = process.cwd();
|
|
1621
|
+
const knownZones = Array.from(
|
|
1622
|
+
/* @__PURE__ */ new Set([...listStoredZones(), ...listEntries().map((e) => e.zone)])
|
|
1623
|
+
);
|
|
1624
|
+
const alreadyAuthorized = knownZones.length > 0 || certHasZone(defaultCertPath());
|
|
1625
|
+
if (alreadyAuthorized) {
|
|
1626
|
+
const config = readProjectConfig(cwd);
|
|
1627
|
+
if (config?.uuid || config?.zone) {
|
|
1628
|
+
done("You're already set up \u2014 run `cfld` to go live.");
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
info("You're already authorized with Cloudflare. Let's configure this project.");
|
|
1632
|
+
const zone2 = await askAuthorizedZone(knownZones);
|
|
1633
|
+
const certPath2 = await ensureCert(zone2, { interactive: true });
|
|
1634
|
+
await configureProject(flags, { zone: zone2, certPath: certPath2, cwd });
|
|
1635
|
+
await offerGoLive(flags, zone2);
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const path = await askSelect("How do you want to start?", [
|
|
1639
|
+
{
|
|
1640
|
+
value: "quick",
|
|
1641
|
+
label: "Get a public URL right now \u2014 free, no account (temporary URL)"
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
value: "persistent",
|
|
1645
|
+
label: "Set up a persistent URL \u2014 free Cloudflare account + a domain"
|
|
1646
|
+
}
|
|
1647
|
+
]);
|
|
1648
|
+
if (path === "quick") {
|
|
1649
|
+
note(
|
|
1650
|
+
" Starting a temporary tunnel. Run `cfld setup` again anytime to set up a persistent URL."
|
|
1651
|
+
);
|
|
1652
|
+
await quickCommand(flags);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
note(
|
|
1656
|
+
[
|
|
1657
|
+
"",
|
|
1658
|
+
" A browser window will open to authorize cfld with Cloudflare.",
|
|
1659
|
+
` \u2022 No account yet? Click "Sign up" on that page \u2014 it's free.`,
|
|
1660
|
+
" \u2022 Then pick (or add) the domain you want to use.",
|
|
1661
|
+
" Prefer to create the account first? Open:",
|
|
1662
|
+
" https://dash.cloudflare.com/sign-up",
|
|
1663
|
+
""
|
|
1664
|
+
].join("\n")
|
|
1665
|
+
);
|
|
1666
|
+
if (!await askConfirm("Open the browser to authorize now?")) {
|
|
1667
|
+
note(
|
|
1668
|
+
"No problem \u2014 run `cfld setup` again when you're ready, or `cfld --quick` for a temporary URL."
|
|
1669
|
+
);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
await runLogin();
|
|
1673
|
+
if (!certHasZone(defaultCertPath())) {
|
|
1674
|
+
printZeroZoneGuidance();
|
|
1675
|
+
if (await askConfirm("Start a temporary quick tunnel now instead?")) {
|
|
1676
|
+
await quickCommand(flags);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
throw new CfldError(
|
|
1680
|
+
"Your Cloudflare account has no domain yet, so a persistent URL isn't possible.",
|
|
1681
|
+
"Add a domain (see above), then run `cfld setup` again."
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
const zone = await askAuthorizedZone(knownZones);
|
|
1685
|
+
const certPath = importCert(zone);
|
|
1686
|
+
info(`Saved cert for ${zone}.`);
|
|
1687
|
+
await configureProject(flags, { zone, certPath, cwd });
|
|
1688
|
+
await offerGoLive(flags, zone);
|
|
1689
|
+
}
|
|
1690
|
+
async function preflightCloudflared() {
|
|
1691
|
+
try {
|
|
1692
|
+
const bin = await resolveBinary();
|
|
1693
|
+
const res = await exec(bin, ["--version"]);
|
|
1694
|
+
const version = (res.stdout.trim() || res.stderr.trim()).split("\n")[0];
|
|
1695
|
+
done(`cloudflared ready${version ? ` \u2014 ${version}` : ""}`);
|
|
1696
|
+
} catch {
|
|
1697
|
+
warn("cloudflared isn't installed, and the bundled binary couldn't load.");
|
|
1698
|
+
note(
|
|
1699
|
+
[
|
|
1700
|
+
"",
|
|
1701
|
+
" Install it, then run `cfld setup` again:",
|
|
1702
|
+
" macOS brew install cloudflared",
|
|
1703
|
+
" Linux apt / dnf / pacman install cloudflared",
|
|
1704
|
+
" Windows winget install --id Cloudflare.cloudflared",
|
|
1705
|
+
" Any OS https://developers.cloudflare.com/tunnel/downloads/",
|
|
1706
|
+
""
|
|
1707
|
+
].join("\n")
|
|
1708
|
+
);
|
|
1709
|
+
throw new CfldError(
|
|
1710
|
+
"cloudflared is required to continue.",
|
|
1711
|
+
"Install it (see above), or set CFLD_CLOUDFLARED to its path."
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function isBlankFirstRun(flags, cwd, interactive = isInteractive()) {
|
|
1716
|
+
if (!interactive) return false;
|
|
1717
|
+
if (flags.zone || flags.host || flags.quick) return false;
|
|
1718
|
+
if (readProjectConfig(cwd)) return false;
|
|
1719
|
+
if (listStoredZones().length > 0) return false;
|
|
1720
|
+
if (certHasZone(defaultCertPath())) return false;
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
1723
|
+
async function askAuthorizedZone(knownZones) {
|
|
1724
|
+
if (knownZones.length) {
|
|
1725
|
+
const picked = await askSelect("Which domain did you authorize?", [
|
|
1726
|
+
...knownZones.map((z) => ({ value: z, label: z })),
|
|
1727
|
+
{ value: "__new__", label: "A different domain\u2026" }
|
|
1728
|
+
]);
|
|
1729
|
+
if (picked !== "__new__") return picked;
|
|
1730
|
+
}
|
|
1731
|
+
return normalizeZone(await askText("Which domain did you authorize?", "example.com"));
|
|
1732
|
+
}
|
|
1733
|
+
async function offerGoLive(flags, zone) {
|
|
1734
|
+
if (await askConfirm("Go live now?")) {
|
|
1735
|
+
await runCommand({ ...flags, zone });
|
|
1736
|
+
} else {
|
|
1737
|
+
note("Run `cfld` to go live.");
|
|
1738
|
+
}
|
|
1596
1739
|
}
|
|
1597
1740
|
|
|
1598
1741
|
// src/commands/list.ts
|
|
@@ -2061,6 +2204,7 @@ function resolveExec(afterDash, execFlag) {
|
|
|
2061
2204
|
return void 0;
|
|
2062
2205
|
}
|
|
2063
2206
|
var SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
2207
|
+
"setup",
|
|
2064
2208
|
"login",
|
|
2065
2209
|
"init",
|
|
2066
2210
|
"list",
|
|
@@ -2074,11 +2218,12 @@ var SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
2074
2218
|
var HELP = `${pc7.bold("cfld")} \u2014 persistent Cloudflare tunnels for local dev
|
|
2075
2219
|
|
|
2076
2220
|
${pc7.bold("Usage")}
|
|
2221
|
+
cfld --quick [port] Instant public URL, no account needed (temporary)
|
|
2222
|
+
cfld setup Guided first-run: free instant URL, or persistent setup
|
|
2077
2223
|
cfld [port] Ensure + run a persistent tunnel (reuses the same URL)
|
|
2078
2224
|
cfld [port] -- <cmd> Run + supervise your dev server too (one lifecycle)
|
|
2079
|
-
cfld --quick [port] Ephemeral trycloudflare.com URL (no domain needed)
|
|
2080
2225
|
cfld login [--reauth] Authorize with Cloudflare in your browser
|
|
2081
|
-
cfld init
|
|
2226
|
+
cfld init Configure a persistent tunnel (don't run)
|
|
2082
2227
|
cfld list List all tunnels across projects/domains
|
|
2083
2228
|
cfld status [name] Show details for a tunnel
|
|
2084
2229
|
cfld up <name> Run a registered tunnel by name from anywhere
|
|
@@ -2151,6 +2296,9 @@ async function main(rawArgv) {
|
|
|
2151
2296
|
case "help":
|
|
2152
2297
|
process.stdout.write(HELP);
|
|
2153
2298
|
return;
|
|
2299
|
+
case "setup":
|
|
2300
|
+
await setupCommand(flags);
|
|
2301
|
+
return;
|
|
2154
2302
|
case "login":
|
|
2155
2303
|
await loginCommand(flags);
|
|
2156
2304
|
return;
|
|
@@ -2185,6 +2333,8 @@ async function main(rawArgv) {
|
|
|
2185
2333
|
}
|
|
2186
2334
|
if (flags.quick) {
|
|
2187
2335
|
await quickCommand(flags);
|
|
2336
|
+
} else if (isBlankFirstRun(flags, process.cwd())) {
|
|
2337
|
+
await setupCommand(flags);
|
|
2188
2338
|
} else {
|
|
2189
2339
|
await runCommand(flags);
|
|
2190
2340
|
}
|