buncargo 3.2.4 → 3.2.5

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.
@@ -0,0 +1,72 @@
1
+ import {
2
+ createDevEnvironment
3
+ } from "./index-ymdvr5sn.js";
4
+
5
+ // src/loader/cache.ts
6
+ var cachedEnv = null;
7
+ function setCachedDevEnv(env) {
8
+ cachedEnv = env;
9
+ }
10
+ function getCachedDevEnv() {
11
+ return cachedEnv;
12
+ }
13
+ function clearDevEnvCache() {
14
+ cachedEnv = null;
15
+ }
16
+ // src/loader/find-config-file.ts
17
+ import { existsSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ var CONFIG_FILES = [
20
+ "dev.config.ts",
21
+ "dev.config.js",
22
+ "dev-tools.config.ts",
23
+ "dev-tools.config.js"
24
+ ];
25
+ function findConfigFile(startDir) {
26
+ let currentDir = startDir;
27
+ while (true) {
28
+ for (const file of CONFIG_FILES) {
29
+ const configPath = join(currentDir, file);
30
+ if (existsSync(configPath)) {
31
+ return configPath;
32
+ }
33
+ }
34
+ const parentDir = dirname(currentDir);
35
+ if (parentDir === currentDir) {
36
+ return null;
37
+ }
38
+ currentDir = parentDir;
39
+ }
40
+ }
41
+ // src/loader/load-dev-env.ts
42
+ async function loadDevEnv(options) {
43
+ if (!options?.reload) {
44
+ const cached = getCachedDevEnv();
45
+ if (cached)
46
+ return cached;
47
+ }
48
+ const cwd = options?.cwd ?? process.cwd();
49
+ const configPath = findConfigFile(cwd);
50
+ if (configPath) {
51
+ const mod = await import(configPath);
52
+ const config = mod.default;
53
+ if (!config?.projectPrefix || !config?.services) {
54
+ throw new Error(`Invalid config in "${configPath}". Use defineDevConfig() and export as default.`);
55
+ }
56
+ const env = createDevEnvironment(config);
57
+ setCachedDevEnv(env);
58
+ return env;
59
+ }
60
+ throw new Error("No config file found. Create dev.config.ts with: export default defineDevConfig({ ... })");
61
+ }
62
+
63
+ // src/loader/index.ts
64
+ function getDevEnv() {
65
+ const env = getCachedDevEnv();
66
+ if (!env) {
67
+ throw new Error("Dev environment not loaded. Call loadDevEnv() first.");
68
+ }
69
+ return env;
70
+ }
71
+
72
+ export { clearDevEnvCache, CONFIG_FILES, findConfigFile, loadDevEnv, getDevEnv };
package/dist/index.js CHANGED
@@ -3,15 +3,15 @@ import {
3
3
  getFlagValue,
4
4
  hasFlag,
5
5
  runCli
6
- } from "./index-emcawhxm.js";
6
+ } from "./index-c28x1pjb.js";
7
7
  import {
8
8
  clearDevEnvCache,
9
9
  getDevEnv,
10
10
  loadDevEnv
11
- } from "./index-1fset27q.js";
11
+ } from "./index-znaek8z2.js";
12
12
  import {
13
13
  createDevEnvironment
14
- } from "./index-tyk17rfn.js";
14
+ } from "./index-ymdvr5sn.js";
15
15
  import {
16
16
  DOCKER_NOT_RUNNING_MESSAGE,
17
17
  MAX_ATTEMPTS,
@@ -33,7 +33,7 @@ import {
33
33
  resolveExposeTargets,
34
34
  startPublicTunnels,
35
35
  stopPublicTunnels
36
- } from "./index-vr4ygtyj.js";
36
+ } from "./index-bgcx898h.js";
37
37
  import {
38
38
  getHeartbeatFile,
39
39
  getWatchdogPidFile,
@@ -4,11 +4,11 @@ import {
4
4
  findConfigFile,
5
5
  getDevEnv,
6
6
  loadDevEnv
7
- } from "../index-1fset27q.js";
8
- import"../index-tyk17rfn.js";
7
+ } from "../index-znaek8z2.js";
8
+ import"../index-ymdvr5sn.js";
9
9
  import"../index-twwcjn9p.js";
10
10
  import"../index-5t9jxqm0.js";
11
- import"../index-vr4ygtyj.js";
11
+ import"../index-bgcx898h.js";
12
12
  import"../index-mam0bcyz.js";
13
13
  import"../index-mm412dkp.js";
14
14
  import"../index-fkgqg6w2.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buncargo",
3
- "version": "3.2.4",
3
+ "version": "3.2.5",
4
4
  "description": "A Bun-powered development environment CLI for managing Docker Compose services, dev servers, and environment variables",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -3,7 +3,7 @@
3
3
  * Derived from unjs/untun (MIT), originally forked from node-cloudflared.
4
4
  */
5
5
  import { type ChildProcess, spawn } from "node:child_process";
6
- import { cloudflaredBinPath } from "./constants";
6
+ import { resolvedCloudflaredBinPath } from "./constants";
7
7
 
8
8
  /** Primary: ASCII box line from cloudflared (`| https://… |`). */
9
9
  const urlRegexPipe = /\|\s+(https?:\/\/\S+)/;
@@ -13,6 +13,16 @@ const urlRegexTryCloudflare =
13
13
 
14
14
  const MAX_CAPTURED_LOG = 24_000;
15
15
 
16
+ /** Default 30s; set `BUNCARGO_QUICK_TUNNEL_TIMEOUT_MS=0` to disable. */
17
+ export function resolveQuickTunnelUrlTimeoutMs(): number {
18
+ const raw = process.env.BUNCARGO_QUICK_TUNNEL_TIMEOUT_MS;
19
+ if (raw === undefined || raw === "") {
20
+ return 30_000;
21
+ }
22
+ const n = Number.parseInt(raw, 10);
23
+ return Number.isFinite(n) && n >= 0 ? n : 30_000;
24
+ }
25
+
16
26
  export function parseQuickTunnelUrlFromOutput(log: string): string | null {
17
27
  const pipe = log.match(urlRegexPipe);
18
28
  if (pipe?.[1]) {
@@ -43,7 +53,8 @@ export function startCloudflaredTunnel(
43
53
  args.push("--url", "localhost:8080");
44
54
  }
45
55
 
46
- const child = spawn(cloudflaredBinPath, args, {
56
+ const binPath = resolvedCloudflaredBinPath();
57
+ const child = spawn(binPath, args, {
47
58
  stdio: ["ignore", "pipe", "pipe"],
48
59
  });
49
60
 
@@ -55,19 +66,46 @@ export function startCloudflaredTunnel(
55
66
  let settled = false;
56
67
  let urlResolver!: (value: string | PromiseLike<string>) => void;
57
68
  let urlRejector!: (reason: unknown) => void;
69
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
70
+
71
+ const clearUrlTimeout = () => {
72
+ if (timeoutId !== undefined) {
73
+ clearTimeout(timeoutId);
74
+ timeoutId = undefined;
75
+ }
76
+ };
77
+
58
78
  const url = new Promise<string>((resolve, reject) => {
59
79
  urlResolver = (v) => {
60
80
  if (!settled) {
61
81
  settled = true;
82
+ clearUrlTimeout();
62
83
  resolve(v);
63
84
  }
64
85
  };
65
86
  urlRejector = (e) => {
66
87
  if (!settled) {
67
88
  settled = true;
89
+ clearUrlTimeout();
68
90
  reject(e);
69
91
  }
70
92
  };
93
+
94
+ const timeoutMs = resolveQuickTunnelUrlTimeoutMs();
95
+ if (timeoutMs > 0) {
96
+ timeoutId = setTimeout(() => {
97
+ try {
98
+ child.kill("SIGINT");
99
+ } catch {
100
+ /* ignore */
101
+ }
102
+ urlRejector(
103
+ new Error(
104
+ `quick tunnel URL timed out after ${timeoutMs}ms (no public URL from cloudflared)`,
105
+ ),
106
+ );
107
+ }, timeoutMs);
108
+ }
71
109
  });
72
110
 
73
111
  const log: { buf: string } = { buf: "" };
@@ -3,11 +3,12 @@
3
3
  * Derived from unjs/untun (MIT), originally forked from node-cloudflared.
4
4
  */
5
5
 
6
+ import { existsSync } from "node:fs";
6
7
  import { tmpdir } from "node:os";
7
8
  import path from "node:path";
8
9
 
9
10
  export const CLOUDFLARED_VERSION =
10
- process.env.CLOUDFLARED_VERSION || "2023.10.0";
11
+ process.env.CLOUDFLARED_VERSION || "2026.3.0";
11
12
 
12
13
  export const RELEASE_BASE =
13
14
  "https://github.com/cloudflare/cloudflared/releases/";
@@ -21,6 +22,18 @@ export const cloudflaredBinPath = path.join(
21
22
  : `cloudflared.${CLOUDFLARED_VERSION}`,
22
23
  );
23
24
 
25
+ /** Spawn/install target: optional `BUNCARGO_CLOUDFLARED_PATH` overrides the bundled cache path. */
26
+ export function resolvedCloudflaredBinPath(): string {
27
+ const override = process.env.BUNCARGO_CLOUDFLARED_PATH?.trim();
28
+ if (override) {
29
+ if (!existsSync(override)) {
30
+ throw new Error(`BUNCARGO_CLOUDFLARED_PATH does not exist: ${override}`);
31
+ }
32
+ return path.resolve(override);
33
+ }
34
+ return cloudflaredBinPath;
35
+ }
36
+
24
37
  export const cloudflaredNotice = `
25
38
  🔥 Your installation of cloudflared software constitutes a symbol of your signature
26
39
  indicating that you accept the terms of the Cloudflare License, Terms and Privacy Policy.
@@ -3,39 +3,74 @@
3
3
  * License / download flow adapted from unjs/untun (MIT).
4
4
  */
5
5
  import { existsSync } from "node:fs";
6
- import { createInterface } from "node:readline";
7
6
  import { sleep } from "../utils";
8
7
  import { startCloudflaredTunnel } from "./cloudflared-process";
9
- import { cloudflaredBinPath, cloudflaredNotice } from "./constants";
8
+ import {
9
+ cloudflaredBinPath,
10
+ cloudflaredNotice,
11
+ resolvedCloudflaredBinPath,
12
+ } from "./constants";
10
13
  import { installCloudflared } from "./install";
11
14
 
12
- const MAX_QUICK_TUNNEL_ATTEMPTS = 5;
15
+ function resolveMaxQuickTunnelAttempts(): number {
16
+ const raw = process.env.BUNCARGO_QUICK_TUNNEL_MAX_ATTEMPTS;
17
+ if (raw === undefined || raw === "") {
18
+ return 5;
19
+ }
20
+ const n = Number.parseInt(raw, 10);
21
+ return Number.isFinite(n) && n >= 1 ? n : 5;
22
+ }
23
+
24
+ function resolveQuickTunnelRetryBaseMs(): number {
25
+ const raw = process.env.BUNCARGO_QUICK_TUNNEL_RETRY_BASE_MS;
26
+ if (raw === undefined || raw === "") {
27
+ return 2000;
28
+ }
29
+ const n = Number.parseInt(raw, 10);
30
+ return Number.isFinite(n) && n >= 0 ? n : 2000;
31
+ }
32
+
33
+ function usesBundledCloudflaredCache(): boolean {
34
+ return !process.env.BUNCARGO_CLOUDFLARED_PATH?.trim();
35
+ }
13
36
 
14
- function isQuickTunnelRateLimitedError(message: string): boolean {
37
+ /** True when trycloudflare.com is overloaded / rate-limited or returns non-JSON (cloudflared then errors on unmarshal). */
38
+ export function isRetryableQuickTunnelError(message: string): boolean {
15
39
  return (
16
40
  message.includes("429") ||
17
41
  message.includes("Too Many Requests") ||
18
- message.includes('status_code="429')
42
+ message.includes('status_code="429') ||
43
+ // Plain-text "error" or HTML error pages — see cloudflare/cloudflared#972
44
+ message.includes("failed to unmarshal quick Tunnel") ||
45
+ message.includes("failed to unmarshall quick Tunnel") ||
46
+ message.includes("Error unmarshaling QuickTunnel") ||
47
+ message.includes("invalid character '<'") ||
48
+ message.includes("quick tunnel URL timed out")
19
49
  );
20
50
  }
21
51
 
22
52
  async function startCloudflaredTunnelWithRetry(
23
53
  cfArgs: Record<string, string | number | null>,
24
54
  ): Promise<ReturnType<typeof startCloudflaredTunnel>> {
25
- for (let attempt = 1; attempt <= MAX_QUICK_TUNNEL_ATTEMPTS; attempt++) {
55
+ const maxAttempts = resolveMaxQuickTunnelAttempts();
56
+ const baseMs = resolveQuickTunnelRetryBaseMs();
57
+
58
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
26
59
  const tunnel = startCloudflaredTunnel(cfArgs);
27
60
  try {
28
61
  await tunnel.url;
29
62
  return tunnel;
30
63
  } catch (e) {
64
+ try {
65
+ tunnel.stop();
66
+ } catch {
67
+ /* ignore */
68
+ }
31
69
  const msg = String(e);
32
- if (
33
- attempt < MAX_QUICK_TUNNEL_ATTEMPTS &&
34
- isQuickTunnelRateLimitedError(msg)
35
- ) {
36
- const delayMs = 2000 * attempt;
70
+ if (attempt < maxAttempts && isRetryableQuickTunnelError(msg)) {
71
+ const delayMs = baseMs * attempt;
37
72
  console.log(
38
- `Cloudflare quick tunnel rate-limited (${attempt}/${MAX_QUICK_TUNNEL_ATTEMPTS}), retrying in ${delayMs}ms…`,
73
+ `Cloudflare quick tunnel temporarily unavailable (${attempt}/${maxAttempts}), retrying in ${delayMs}ms…`,
39
74
  );
40
75
  await sleep(delayMs);
41
76
  continue;
@@ -52,7 +87,6 @@ export interface QuickTunnelOptions {
52
87
  hostname?: string;
53
88
  protocol?: "http" | "https";
54
89
  verifyTLS?: boolean;
55
- acceptCloudflareNotice?: boolean;
56
90
  }
57
91
 
58
92
  export interface QuickTunnel {
@@ -67,52 +101,22 @@ function resolvedLocalUrl(opts: QuickTunnelOptions): string {
67
101
  );
68
102
  }
69
103
 
70
- function envAcceptsCloudflareNotice(): boolean {
71
- const v = process.env.BUNCARGO_ACCEPT_CLOUDFLARE_NOTICE;
72
- const u = process.env.UNTUN_ACCEPT_CLOUDFLARE_NOTICE;
73
- return v === "1" || v === "true" || u === "1" || u === "true";
74
- }
75
-
76
- async function promptInstallCloudflared(): Promise<boolean> {
77
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
78
- return false;
79
- }
80
- return new Promise((resolve) => {
81
- const rl = createInterface({
82
- input: process.stdin,
83
- output: process.stdout,
84
- });
85
- rl.question(
86
- "Do you agree with the above terms and wish to install the binary from GitHub? (y/N) ",
87
- (answer) => {
88
- rl.close();
89
- resolve(/^y(es)?$/i.test(answer.trim()));
90
- },
91
- );
92
- });
93
- }
94
-
95
104
  /**
96
105
  * Start a Cloudflare quick tunnel to a local HTTP(S) URL.
97
- * Returns undefined if the user declines the cloudflared install (when binary is missing).
106
+ * If the cloudflared binary is missing, prints the license notice and installs it from GitHub.
98
107
  */
99
108
  export async function startQuickTunnel(
100
109
  opts: QuickTunnelOptions,
101
- ): Promise<QuickTunnel | undefined> {
110
+ ): Promise<QuickTunnel> {
102
111
  const url = resolvedLocalUrl(opts);
103
112
 
104
113
  console.log(`Starting cloudflared tunnel to ${url}`);
105
114
 
106
- if (!existsSync(cloudflaredBinPath)) {
115
+ // Resolve path first (throws if BUNCARGO_CLOUDFLARED_PATH is invalid).
116
+ resolvedCloudflaredBinPath();
117
+
118
+ if (usesBundledCloudflaredCache() && !existsSync(cloudflaredBinPath)) {
107
119
  console.log(cloudflaredNotice);
108
- const canInstall =
109
- opts.acceptCloudflareNotice ||
110
- envAcceptsCloudflareNotice() ||
111
- (await promptInstallCloudflared());
112
- if (!canInstall) {
113
- console.error("Skipping tunnel setup.");
114
- return;
115
- }
116
120
  await installCloudflared();
117
121
  }
118
122
 
@@ -20,7 +20,7 @@ const LINUX_URL: Partial<Record<NodeJS.Architecture, string>> = {
20
20
  };
21
21
 
22
22
  const MACOS_URL: Partial<Record<NodeJS.Architecture, string>> = {
23
- arm64: "cloudflared-darwin-amd64.tgz",
23
+ arm64: "cloudflared-darwin-arm64.tgz",
24
24
  x64: "cloudflared-darwin-amd64.tgz",
25
25
  };
26
26
 
@@ -150,7 +150,7 @@ export async function startPublicTunnels(
150
150
  })) as TunnelBackendResult | undefined;
151
151
  if (tunnel === undefined) {
152
152
  throw new Error(
153
- `Tunnel for "${target.name}" could not be started (cloudflared missing or install declined)`,
153
+ `Tunnel for "${target.name}" could not be started (tunnel backend returned no instance)`,
154
154
  );
155
155
  }
156
156
  const publicUrl = await resolvePublicUrl(tunnel);