cloudflared-manager 0.1.1 → 0.1.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 CHANGED
@@ -58,6 +58,14 @@ node ./cloudflared_manager.mjs --profile-dir /opt/cloudflare-prod interactive
58
58
  npx cloudflared-manager
59
59
  ```
60
60
 
61
+ 首次进入交互模式时,如果当前机器还没有安装 `cloudflared`,会先主动提示安装。对 `login`、`tunnels`、`add`、`adopt`、`modify`、`start`、`restart`、`delete --delete-tunnel` 这类依赖 `cloudflared` 的动作,交互层也会在执行前自动拦一下,避免你先报错再回头安装。
62
+
63
+ 在 macOS 上,交互层会先调用脚本内置的 `install`:
64
+
65
+ - 可以选择直接 `sudo` 安装官方 `pkg`
66
+ - 如果不想立刻提权,也可以只打开官方安装器
67
+ - 如果 GitHub 官方安装包下载失败,会自动回退到 `brew install cloudflared`
68
+
61
69
  交互模式里提供了常用菜单;如果某些高级参数菜单里还没覆盖,可以用“执行自定义命令”直接透传到底层 shell 脚本。
62
70
  主菜单同时支持两种选择方式:
63
71
 
@@ -135,6 +143,18 @@ npx cloudflared-manager
135
143
 
136
144
  ## 快速开始
137
145
 
146
+ 如果你要从零开始初始化一台新机器,最短路径通常就是这一条:
147
+
148
+ ```bash
149
+ ./cloudflared_manager.sh --profile-dir /opt/cloudflare-prod init --install --login
150
+ ```
151
+
152
+ 如果你走的是 npm 入口,也可以直接:
153
+
154
+ ```bash
155
+ npx cloudflared-manager init --install --login
156
+ ```
157
+
138
158
  ### 1. 保存默认 profile
139
159
 
140
160
  ```bash
@@ -213,5 +233,5 @@ npx cloudflared-manager
213
233
  - `modify` 既支持旧的单条写法,也支持批量 `--set N:hostname=service`
214
234
  - 如果你只是同一台机器上暴露多个域名/服务,通常优先考虑“1 个 Tunnel + 多 ingress”
215
235
  - `modify` 和 `delete --delete-tunnel` 不会自动清理旧 DNS 记录,这仍需要你到 Cloudflare 侧手工处理
216
- - `install` 目前按 macOS 设计
236
+ - `install` 目前按 macOS 设计,优先官方 `pkg`,下载失败时会回退到 Homebrew
217
237
  - 仓库只保留 shell 版,不再维护 Python 入口
@@ -285,6 +285,99 @@ function runShell(baseArgs, commandArgs, options = {}) {
285
285
  };
286
286
  }
287
287
 
288
+ // 返回当前可用的 cloudflared 可执行文件路径。
289
+ function resolveCloudflaredBinary() {
290
+ const explicitBin = (process.env.CLOUDFLARED_BIN ?? "").trim();
291
+ if (explicitBin.length > 0) {
292
+ const explicitResult = spawnSync("bash", ["-lc", '[[ -x "$CLOUDFLARED_BIN" ]]'], {
293
+ env: process.env,
294
+ });
295
+ if ((explicitResult.status ?? 1) === 0) {
296
+ return explicitBin;
297
+ }
298
+ }
299
+
300
+ const result = spawnSync("bash", ["-lc", "command -v cloudflared || true"], {
301
+ encoding: "utf8",
302
+ });
303
+ if (result.error) {
304
+ return "";
305
+ }
306
+ return (result.stdout ?? "").trim();
307
+ }
308
+
309
+ // 判断当前机器是否已经安装 cloudflared。
310
+ function isCloudflaredInstalled() {
311
+ return resolveCloudflaredBinary().length > 0;
312
+ }
313
+
314
+ // 在交互模式中引导安装 cloudflared,成功后返回 true。
315
+ async function installCloudflaredInteractive(rl, baseArgs, options = {}) {
316
+ if (isCloudflaredInstalled()) {
317
+ return true;
318
+ }
319
+
320
+ if (options.confirm !== false) {
321
+ const prompt = options.prompt ?? "是否现在安装 cloudflared";
322
+ if (!(await askYesNo(rl, prompt, true))) {
323
+ return false;
324
+ }
325
+ }
326
+
327
+ const installArgs = ["install"];
328
+ if (process.platform === "darwin") {
329
+ const useSudoInstall = await askYesNo(
330
+ rl,
331
+ "是否直接使用 sudo 安装(否则只打开官方 pkg 安装器)",
332
+ true,
333
+ );
334
+ if (useSudoInstall) {
335
+ installArgs.push("--sudo-install");
336
+ }
337
+ }
338
+
339
+ runShell(baseArgs, installArgs);
340
+
341
+ if (isCloudflaredInstalled()) {
342
+ console.log("已检测到 cloudflared,可继续后续操作。");
343
+ return true;
344
+ }
345
+
346
+ if (process.platform === "darwin" && !installArgs.includes("--sudo-install")) {
347
+ console.log("已打开 cloudflared 官方安装器。完成安装后,回到这里继续即可。");
348
+ } else {
349
+ console.log("安装命令已执行,但当前仍未检测到 cloudflared。请完成安装后再继续。");
350
+ }
351
+ return false;
352
+ }
353
+
354
+ // 在首次进入交互模式时,主动提示安装 cloudflared。
355
+ async function maybeOfferCloudflaredInstall(rl, baseArgs) {
356
+ if (isCloudflaredInstalled()) {
357
+ return;
358
+ }
359
+
360
+ console.log("检测到当前机器尚未安装 cloudflared。");
361
+ console.log("登录 Cloudflare、查看远端 Tunnel、创建或启动 Tunnel 之前,需要先安装它。");
362
+ if (await askYesNo(rl, "是否现在安装 cloudflared", true)) {
363
+ await installCloudflaredInteractive(rl, baseArgs, { confirm: false });
364
+ } else {
365
+ console.log("已跳过安装。你仍可先执行 doctor 或 init,后续需要时再安装。");
366
+ }
367
+ }
368
+
369
+ // 某些交互动作依赖 cloudflared,执行前先确保已安装。
370
+ async function ensureCloudflaredForAction(rl, baseArgs, label) {
371
+ if (isCloudflaredInstalled()) {
372
+ return true;
373
+ }
374
+
375
+ console.log(`${label}前需要先安装 cloudflared。`);
376
+ return installCloudflaredInteractive(rl, baseArgs, {
377
+ prompt: "是否现在安装 cloudflared",
378
+ });
379
+ }
380
+
288
381
  // 读取一行输入,可选提供默认值。
289
382
  async function ask(rl, label, options = {}) {
290
383
  const suffix =
@@ -1751,6 +1844,9 @@ async function handleAction(rl, baseArgs, action, context = {}) {
1751
1844
  runShell(baseArgs, ["list"]);
1752
1845
  return { keepRunning: true };
1753
1846
  case "3":
1847
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "查看远端 Tunnel"))) {
1848
+ return { keepRunning: true };
1849
+ }
1754
1850
  runShell(baseArgs, ["tunnels"]);
1755
1851
  return { keepRunning: true };
1756
1852
  case "4":
@@ -1766,6 +1862,9 @@ async function handleAction(rl, baseArgs, action, context = {}) {
1766
1862
  return { keepRunning: true };
1767
1863
  case "5":
1768
1864
  {
1865
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "创建新 Tunnel"))) {
1866
+ return { keepRunning: true };
1867
+ }
1769
1868
  const addState = await runAddWizard(rl, baseArgs);
1770
1869
  if (!addState) {
1771
1870
  console.log("已取消创建新 tunnel。");
@@ -1779,6 +1878,9 @@ async function handleAction(rl, baseArgs, action, context = {}) {
1779
1878
  }
1780
1879
  case "6":
1781
1880
  {
1881
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "接管已有 Tunnel"))) {
1882
+ return { keepRunning: true };
1883
+ }
1782
1884
  const adoptState = await runAdoptWizard(rl, baseArgs);
1783
1885
  if (!adoptState) {
1784
1886
  console.log("已取消接管已有 tunnel。");
@@ -1800,6 +1902,9 @@ async function handleAction(rl, baseArgs, action, context = {}) {
1800
1902
  }
1801
1903
  if (!name) return { keepRunning: true };
1802
1904
  {
1905
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "修改 ingress"))) {
1906
+ return { keepRunning: true };
1907
+ }
1803
1908
  const modifyState = await runModifyWizard(rl, baseArgs, name);
1804
1909
  if (!modifyState) {
1805
1910
  console.log("已取消修改 ingress。");
@@ -1823,6 +1928,9 @@ async function handleAction(rl, baseArgs, action, context = {}) {
1823
1928
  }
1824
1929
  if (!name) return { keepRunning: true };
1825
1930
  {
1931
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "新增 ingress"))) {
1932
+ return { keepRunning: true };
1933
+ }
1826
1934
  const addState = await runIngressAddWizard(rl, baseArgs, name);
1827
1935
  if (!addState) {
1828
1936
  console.log("已取消新增 ingress。");
@@ -1862,6 +1970,9 @@ async function handleAction(rl, baseArgs, action, context = {}) {
1862
1970
  });
1863
1971
  }
1864
1972
  if (!name) return { keepRunning: true };
1973
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "启动应用"))) {
1974
+ return { keepRunning: true };
1975
+ }
1865
1976
  runShell(baseArgs, ["start", name]);
1866
1977
  return { keepRunning: true };
1867
1978
  case "11":
@@ -1884,6 +1995,9 @@ async function handleAction(rl, baseArgs, action, context = {}) {
1884
1995
  });
1885
1996
  }
1886
1997
  if (!name) return { keepRunning: true };
1998
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "重启应用"))) {
1999
+ return { keepRunning: true };
2000
+ }
1887
2001
  runShell(baseArgs, ["restart", name]);
1888
2002
  return { keepRunning: true };
1889
2003
  case "13":
@@ -1929,18 +2043,42 @@ async function handleAction(rl, baseArgs, action, context = {}) {
1929
2043
  }
1930
2044
  if (!name) return { keepRunning: true };
1931
2045
  args = ["delete", name];
1932
- if (await askYesNo(rl, "是否同时删除远端 tunnel", false)) args.push("--delete-tunnel");
2046
+ if (await askYesNo(rl, "是否同时删除远端 tunnel", false)) {
2047
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "删除远端 Tunnel"))) {
2048
+ return { keepRunning: true };
2049
+ }
2050
+ args.push("--delete-tunnel");
2051
+ }
1933
2052
  runShell(baseArgs, args);
1934
2053
  return { keepRunning: true, nextMenuKey: "app_picker", clearAppName: true };
1935
2054
  case "17":
1936
2055
  args = ["init"];
1937
- if (await askYesNo(rl, "是否同时执行 login", false)) args.push("--login");
2056
+ if (!isCloudflaredInstalled()) {
2057
+ if (await askYesNo(rl, "检测到未安装 cloudflared,是否在初始化时一并安装", true)) {
2058
+ args.push("--install");
2059
+ }
2060
+ }
2061
+ if (await askYesNo(rl, "是否同时执行 login", false)) {
2062
+ if (!isCloudflaredInstalled() && !args.includes("--install")) {
2063
+ if (await askYesNo(rl, "login 需要先安装 cloudflared,是否在初始化时一并安装", true)) {
2064
+ args.push("--install");
2065
+ } else {
2066
+ console.log("已跳过 login。完成安装后可在“环境与登录 -> 登录 Cloudflare”中继续。");
2067
+ }
2068
+ }
2069
+ if (isCloudflaredInstalled() || args.includes("--install")) {
2070
+ args.push("--login");
2071
+ }
2072
+ }
1938
2073
  runShell(baseArgs, args);
1939
2074
  return { keepRunning: true };
1940
2075
  case "18":
1941
2076
  runShell(baseArgs, ["use"]);
1942
2077
  return { keepRunning: true };
1943
2078
  case "19":
2079
+ if (!(await ensureCloudflaredForAction(rl, baseArgs, "登录 Cloudflare"))) {
2080
+ return { keepRunning: true };
2081
+ }
1944
2082
  runShell(baseArgs, ["login"]);
1945
2083
  return { keepRunning: true };
1946
2084
  case "20": {
@@ -1996,6 +2134,7 @@ async function runInteractive(baseArgs) {
1996
2134
  });
1997
2135
 
1998
2136
  try {
2137
+ await maybeOfferCloudflaredInstall(rl, baseArgs);
1999
2138
  let keepRunning = true;
2000
2139
  let currentMenuKey = MAIN_MENU_KEY;
2001
2140
  let currentAppName = "";
@@ -339,7 +339,7 @@ ensure_manager_dirs() {
339
339
  # 确保系统已经安装 cloudflared。
340
340
  ensure_cloudflared() {
341
341
  if [[ -z "$CLOUDFLARED_BIN" ]]; then
342
- die "cloudflared 未安装,请先执行 install"
342
+ die "cloudflared 未安装。请先执行 install,或使用 init --install --login 完成首次初始化。"
343
343
  fi
344
344
  }
345
345
 
@@ -1636,6 +1636,7 @@ EOF
1636
1636
  # 在 macOS 上下载安装 cloudflared。
1637
1637
  cmd_install() {
1638
1638
  local sudo_install=0
1639
+ local arch pkg_name url pkg_path
1639
1640
  while [[ $# -gt 0 ]]; do
1640
1641
  case "$1" in
1641
1642
  --sudo-install) sudo_install=1; shift ;;
@@ -1660,7 +1661,6 @@ EOF
1660
1661
  die "自动安装目前只支持 macOS。"
1661
1662
  fi
1662
1663
 
1663
- local arch pkg_name url pkg_path
1664
1664
  arch="$(uname -m)"
1665
1665
  case "$arch" in
1666
1666
  x86_64) pkg_name="cloudflared-amd64.pkg" ;;
@@ -1669,13 +1669,32 @@ EOF
1669
1669
  esac
1670
1670
  url="https://github.com/cloudflare/cloudflared/releases/latest/download/$pkg_name"
1671
1671
  pkg_path="/tmp/$pkg_name"
1672
- curl -L --fail -o "$pkg_path" "$url"
1673
- if [[ "$sudo_install" == "1" ]]; then
1674
- sudo installer -pkg "$pkg_path" -target /
1675
- else
1672
+ if curl -L --fail -o "$pkg_path" "$url"; then
1673
+ if [[ "$sudo_install" == "1" ]]; then
1674
+ sudo installer -pkg "$pkg_path" -target /
1675
+ CLOUDFLARED_BIN="${CLOUDFLARED_BIN:-$(command -v cloudflared || true)}"
1676
+ if [[ -n "$CLOUDFLARED_BIN" ]]; then
1677
+ info "cloudflared 已安装:$("$CLOUDFLARED_BIN" --version)"
1678
+ fi
1679
+ return 0
1680
+ fi
1676
1681
  open "$pkg_path"
1677
1682
  info "已打开安装包: $pkg_path"
1683
+ return 0
1684
+ fi
1685
+
1686
+ warn "官方下载失败,尝试改用 Homebrew 安装。"
1687
+ if ! command_exists brew; then
1688
+ die "未找到 Homebrew,且 cloudflared 官方安装包下载失败。请稍后重试,或先安装 Homebrew。"
1689
+ fi
1690
+
1691
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install cloudflared
1692
+ CLOUDFLARED_BIN="${CLOUDFLARED_BIN:-$(command -v cloudflared || true)}"
1693
+ if [[ -n "$CLOUDFLARED_BIN" ]]; then
1694
+ info "cloudflared 已安装:$("$CLOUDFLARED_BIN" --version)"
1695
+ return 0
1678
1696
  fi
1697
+ die "Homebrew 安装 cloudflared 后仍未检测到可执行文件。"
1679
1698
  }
1680
1699
 
1681
1700
  # 执行 cloudflared tunnel login,并同步默认证书到目标位置。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflared-manager",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Interactive wrapper and shell-first manager for cloudflared tunnels",
5
5
  "type": "module",
6
6
  "bin": {