buncargo 3.2.0 → 3.2.4

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/index.js CHANGED
@@ -3,15 +3,15 @@ import {
3
3
  getFlagValue,
4
4
  hasFlag,
5
5
  runCli
6
- } from "./index-5aq985p4.js";
6
+ } from "./index-emcawhxm.js";
7
7
  import {
8
8
  clearDevEnvCache,
9
9
  getDevEnv,
10
10
  loadDevEnv
11
- } from "./index-byeqyjrz.js";
11
+ } from "./index-1fset27q.js";
12
12
  import {
13
13
  createDevEnvironment
14
- } from "./index-7v19es2e.js";
14
+ } from "./index-tyk17rfn.js";
15
15
  import {
16
16
  DOCKER_NOT_RUNNING_MESSAGE,
17
17
  MAX_ATTEMPTS,
@@ -20,27 +20,7 @@ import {
20
20
  assertDockerRunning,
21
21
  isContainerRunning,
22
22
  isDockerRunning
23
- } from "./index-d8tyv5se.js";
24
- import {
25
- getEnvVar,
26
- getLocalIp,
27
- isCI,
28
- isPortAvailable,
29
- logApiUrl,
30
- logExpoApiUrl,
31
- logFrontendPort,
32
- sleep,
33
- waitForServer
34
- } from "./index-c0dr6mcv.js";
35
- import {
36
- calculatePortOffset,
37
- computeDevIdentity,
38
- findMonorepoRoot,
39
- getProjectName,
40
- getWorktreeName,
41
- getWorktreeProjectSuffix,
42
- isWorktree
43
- } from "./index-fb29934k.js";
23
+ } from "./index-twwcjn9p.js";
44
24
  import {
45
25
  DEFAULT_GENERATED_COMPOSE_FILE,
46
26
  buildComposeModel,
@@ -53,7 +33,7 @@ import {
53
33
  resolveExposeTargets,
54
34
  startPublicTunnels,
55
35
  stopPublicTunnels
56
- } from "./index-x54nbgs7.js";
36
+ } from "./index-vr4ygtyj.js";
57
37
  import {
58
38
  getHeartbeatFile,
59
39
  getWatchdogPidFile,
@@ -71,6 +51,26 @@ import {
71
51
  killProcessOnPortAndWait,
72
52
  killProcessesOnAppPorts
73
53
  } from "./index-mm412dkp.js";
54
+ import {
55
+ getEnvVar,
56
+ getLocalIp,
57
+ isCI,
58
+ isPortAvailable,
59
+ logApiUrl,
60
+ logExpoApiUrl,
61
+ logFrontendPort,
62
+ sleep,
63
+ waitForServer
64
+ } from "./index-fkgqg6w2.js";
65
+ import {
66
+ calculatePortOffset,
67
+ computeDevIdentity,
68
+ findMonorepoRoot,
69
+ getProjectName,
70
+ getWorktreeName,
71
+ getWorktreeProjectSuffix,
72
+ isWorktree
73
+ } from "./index-fb29934k.js";
74
74
  import {
75
75
  assertValidConfig,
76
76
  defineDevConfig,
@@ -4,15 +4,15 @@ import {
4
4
  findConfigFile,
5
5
  getDevEnv,
6
6
  loadDevEnv
7
- } from "../index-byeqyjrz.js";
8
- import"../index-7v19es2e.js";
9
- import"../index-d8tyv5se.js";
10
- import"../index-c0dr6mcv.js";
11
- import"../index-fb29934k.js";
7
+ } from "../index-1fset27q.js";
8
+ import"../index-tyk17rfn.js";
9
+ import"../index-twwcjn9p.js";
12
10
  import"../index-5t9jxqm0.js";
13
- import"../index-x54nbgs7.js";
11
+ import"../index-vr4ygtyj.js";
14
12
  import"../index-mam0bcyz.js";
15
13
  import"../index-mm412dkp.js";
14
+ import"../index-fkgqg6w2.js";
15
+ import"../index-fb29934k.js";
16
16
  import"../index-t0fj6gg1.js";
17
17
  import"../index-qnx9j3qa.js";
18
18
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buncargo",
3
- "version": "3.2.0",
3
+ "version": "3.2.4",
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",
@@ -133,8 +133,8 @@
133
133
  "lint:write": "bun run typecheck && biome check --fix src example && biome format src example",
134
134
  "typecheck": "tsgo --incremental",
135
135
  "test": "bun test",
136
- "test:integration-cloudflared": "bun test src/core/quick-tunnel/quick-tunnel.test.ts",
137
- "test:integration-cloudflared-e2e": "BUNCARGO_TEST_CLOUDFLARED_E2E=1 bun test src/core/quick-tunnel/quick-tunnel.test.ts"
136
+ "test:integration-cloudflared": "BUNCARGO_TEST_CLOUDFLARED_SMOKE=1 bun test src/core/quick-tunnel/quick-tunnel.test.ts",
137
+ "test:integration-cloudflared-e2e": "BUNCARGO_TEST_CLOUDFLARED_SMOKE=1 BUNCARGO_TEST_CLOUDFLARED_E2E=1 bun test src/core/quick-tunnel/quick-tunnel.test.ts"
138
138
  },
139
139
  "devDependencies": {
140
140
  "@types/bun": "1.3.2",
@@ -5,7 +5,22 @@
5
5
  import { type ChildProcess, spawn } from "node:child_process";
6
6
  import { cloudflaredBinPath } from "./constants";
7
7
 
8
- const urlRegex = /\|\s+(https?:\/\/\S+)/;
8
+ /** Primary: ASCII box line from cloudflared (`| https://… |`). */
9
+ const urlRegexPipe = /\|\s+(https?:\/\/\S+)/;
10
+ /** Fallback: URL may appear without the leading pipe if logs wrap or format changes. */
11
+ const urlRegexTryCloudflare =
12
+ /(https:\/\/[a-zA-Z0-9][-a-zA-Z0-9.]*\.trycloudflare\.com)\b/;
13
+
14
+ const MAX_CAPTURED_LOG = 24_000;
15
+
16
+ export function parseQuickTunnelUrlFromOutput(log: string): string | null {
17
+ const pipe = log.match(urlRegexPipe);
18
+ if (pipe?.[1]) {
19
+ return pipe[1];
20
+ }
21
+ const direct = log.match(urlRegexTryCloudflare);
22
+ return direct?.[1] ?? null;
23
+ }
9
24
 
10
25
  export function startCloudflaredTunnel(
11
26
  options: Record<string, string | number | null>,
@@ -55,22 +70,30 @@ export function startCloudflaredTunnel(
55
70
  };
56
71
  });
57
72
 
58
- const parser = (data: Buffer) => {
59
- const str = data.toString();
60
-
61
- const urlMatch = str.match(urlRegex);
62
- if (urlMatch) {
63
- urlResolver(urlMatch[1] ?? "");
73
+ const log: { buf: string } = { buf: "" };
74
+ const append = (data: Buffer) => {
75
+ log.buf += data.toString();
76
+ if (log.buf.length > MAX_CAPTURED_LOG) {
77
+ log.buf = log.buf.slice(-MAX_CAPTURED_LOG);
78
+ }
79
+ const url = parseQuickTunnelUrlFromOutput(log.buf);
80
+ if (url) {
81
+ urlResolver(url);
64
82
  }
65
83
  };
66
- child.stdout?.on("data", parser).on("error", urlRejector);
67
- child.stderr?.on("data", parser).on("error", urlRejector);
84
+ child.stdout?.on("data", append).on("error", urlRejector);
85
+ child.stderr?.on("data", append).on("error", urlRejector);
68
86
 
69
87
  child.on("exit", (code, signal) => {
70
88
  if (!settled) {
89
+ const tail = log.buf.trimEnd();
90
+ const excerpt = tail.length > 1200 ? `…${tail.slice(-1200)}` : tail;
91
+ const detail = excerpt ? `\ncloudflared output (tail):\n${excerpt}` : "";
71
92
  urlRejector(
72
93
  new Error(
73
- `cloudflared exited before a tunnel URL was parsed (code=${code}, signal=${signal ?? "none"})`,
94
+ `cloudflared exited before a tunnel URL was parsed (code=${code}, signal=${signal ?? "none"}). ` +
95
+ `Parallel quick-tunnel requests are often rate-limited; buncargo starts tunnels sequentially with a short pause. ` +
96
+ `If this persists, try fewer expose targets or increase BUNCARGO_EXPOSE_TUNNEL_STAGGER_MS.${detail}`,
74
97
  ),
75
98
  );
76
99
  }
@@ -4,10 +4,48 @@
4
4
  */
5
5
  import { existsSync } from "node:fs";
6
6
  import { createInterface } from "node:readline";
7
+ import { sleep } from "../utils";
7
8
  import { startCloudflaredTunnel } from "./cloudflared-process";
8
9
  import { cloudflaredBinPath, cloudflaredNotice } from "./constants";
9
10
  import { installCloudflared } from "./install";
10
11
 
12
+ const MAX_QUICK_TUNNEL_ATTEMPTS = 5;
13
+
14
+ function isQuickTunnelRateLimitedError(message: string): boolean {
15
+ return (
16
+ message.includes("429") ||
17
+ message.includes("Too Many Requests") ||
18
+ message.includes('status_code="429')
19
+ );
20
+ }
21
+
22
+ async function startCloudflaredTunnelWithRetry(
23
+ cfArgs: Record<string, string | number | null>,
24
+ ): Promise<ReturnType<typeof startCloudflaredTunnel>> {
25
+ for (let attempt = 1; attempt <= MAX_QUICK_TUNNEL_ATTEMPTS; attempt++) {
26
+ const tunnel = startCloudflaredTunnel(cfArgs);
27
+ try {
28
+ await tunnel.url;
29
+ return tunnel;
30
+ } catch (e) {
31
+ const msg = String(e);
32
+ if (
33
+ attempt < MAX_QUICK_TUNNEL_ATTEMPTS &&
34
+ isQuickTunnelRateLimitedError(msg)
35
+ ) {
36
+ const delayMs = 2000 * attempt;
37
+ console.log(
38
+ `Cloudflare quick tunnel rate-limited (${attempt}/${MAX_QUICK_TUNNEL_ATTEMPTS}), retrying in ${delayMs}ms…`,
39
+ );
40
+ await sleep(delayMs);
41
+ continue;
42
+ }
43
+ throw e;
44
+ }
45
+ }
46
+ throw new Error("startCloudflaredTunnelWithRetry: exhausted attempts");
47
+ }
48
+
11
49
  export interface QuickTunnelOptions {
12
50
  url?: string;
13
51
  port?: number | string;
@@ -83,7 +121,7 @@ export async function startQuickTunnel(
83
121
  if (!opts.verifyTLS) {
84
122
  cfArgs["--no-tls-verify"] = null;
85
123
  }
86
- const tunnel = startCloudflaredTunnel(cfArgs);
124
+ const tunnel = await startCloudflaredTunnelWithRetry(cfArgs);
87
125
 
88
126
  const cleanup = async () => {
89
127
  tunnel.stop();
@@ -1,5 +1,6 @@
1
1
  import type { AppConfig, DevEnvironment, ServiceConfig } from "../types";
2
2
  import { startQuickTunnel } from "./quick-tunnel";
3
+ import { sleep } from "./utils";
3
4
 
4
5
  export interface PublicExposeTarget {
5
6
  kind: "service" | "app";
@@ -115,6 +116,15 @@ export function resolveExposeTargets<
115
116
  return { targets, unknownNames, notEnabledNames };
116
117
  }
117
118
 
119
+ function resolveExposeTunnelStaggerMs(): number {
120
+ const raw = process.env.BUNCARGO_EXPOSE_TUNNEL_STAGGER_MS;
121
+ if (raw === undefined || raw === "") {
122
+ return 900;
123
+ }
124
+ const n = Number.parseInt(raw, 10);
125
+ return Number.isFinite(n) && n >= 0 ? n : 900;
126
+ }
127
+
118
128
  export async function startPublicTunnels(
119
129
  targets: PublicExposeTarget[],
120
130
  options: {
@@ -124,10 +134,16 @@ export async function startPublicTunnels(
124
134
  } = {},
125
135
  ): Promise<PublicTunnel[]> {
126
136
  const start = options.start ?? ((input) => startQuickTunnel(input));
127
- const tunnels: PublicTunnel[] = [];
137
+ const staggerMs = resolveExposeTunnelStaggerMs();
128
138
 
139
+ const tunnels: PublicTunnel[] = [];
129
140
  try {
141
+ let index = 0;
130
142
  for (const target of targets) {
143
+ if (index > 0 && staggerMs > 0) {
144
+ await sleep(staggerMs);
145
+ }
146
+ index += 1;
131
147
  const localUrl = `http://localhost:${target.port}`;
132
148
  const tunnel = (await start({
133
149
  url: localUrl,
@@ -152,9 +168,9 @@ export async function startPublicTunnels(
152
168
  });
153
169
  }
154
170
  return tunnels;
155
- } catch (error) {
171
+ } catch (e) {
156
172
  await stopPublicTunnels(tunnels);
157
- throw error;
173
+ throw e;
158
174
  }
159
175
  }
160
176