buncargo 3.2.3 → 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/cli/bin.js +10 -10
- package/dist/cli/index.js +4 -2
- package/dist/core/network.js +1 -1
- package/dist/core/quick-tunnel/cloudflared-process.d.ts +1 -0
- package/dist/core/utils.js +1 -1
- package/dist/docker/index.js +2 -2
- package/dist/environment/index.js +5 -5
- package/dist/index-1fset27q.js +72 -0
- package/dist/index-2bcjw5n0.js +666 -0
- package/dist/index-5vg657rh.js +72 -0
- package/dist/index-c2v0t0y2.js +250 -0
- package/dist/index-cm05c27w.js +417 -0
- package/dist/index-emcawhxm.js +250 -0
- package/dist/index-fkgqg6w2.js +125 -0
- package/dist/index-gfjdt37q.js +391 -0
- package/dist/index-qz66apm2.js +250 -0
- package/dist/index-twwcjn9p.js +228 -0
- package/dist/index-tyk17rfn.js +666 -0
- package/dist/index-vj8kaz2d.js +72 -0
- package/dist/index-vr4ygtyj.js +415 -0
- package/dist/index-wmgx8rsm.js +666 -0
- package/dist/index.js +25 -25
- package/dist/loader/index.js +6 -6
- package/package.json +3 -3
- package/src/core/quick-tunnel/cloudflared-process.ts +33 -10
- package/src/core/quick-tunnel/index.ts +39 -1
- package/src/core/tunnel.ts +24 -20
package/dist/index.js
CHANGED
|
@@ -3,15 +3,15 @@ import {
|
|
|
3
3
|
getFlagValue,
|
|
4
4
|
hasFlag,
|
|
5
5
|
runCli
|
|
6
|
-
} from "./index-
|
|
6
|
+
} from "./index-emcawhxm.js";
|
|
7
7
|
import {
|
|
8
8
|
clearDevEnvCache,
|
|
9
9
|
getDevEnv,
|
|
10
10
|
loadDevEnv
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-1fset27q.js";
|
|
12
12
|
import {
|
|
13
13
|
createDevEnvironment
|
|
14
|
-
} from "./index-
|
|
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-
|
|
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-
|
|
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,
|
package/dist/loader/index.js
CHANGED
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
findConfigFile,
|
|
5
5
|
getDevEnv,
|
|
6
6
|
loadDevEnv
|
|
7
|
-
} from "../index-
|
|
8
|
-
import"../index-
|
|
9
|
-
import"../index-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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",
|
|
67
|
-
child.stderr?.on("data",
|
|
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 =
|
|
124
|
+
const tunnel = await startCloudflaredTunnelWithRetry(cfArgs);
|
|
87
125
|
|
|
88
126
|
const cleanup = async () => {
|
|
89
127
|
tunnel.stop();
|
package/src/core/tunnel.ts
CHANGED
|
@@ -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,9 +134,16 @@ export async function startPublicTunnels(
|
|
|
124
134
|
} = {},
|
|
125
135
|
): Promise<PublicTunnel[]> {
|
|
126
136
|
const start = options.start ?? ((input) => startQuickTunnel(input));
|
|
137
|
+
const staggerMs = resolveExposeTunnelStaggerMs();
|
|
127
138
|
|
|
128
|
-
const
|
|
129
|
-
|
|
139
|
+
const tunnels: PublicTunnel[] = [];
|
|
140
|
+
try {
|
|
141
|
+
let index = 0;
|
|
142
|
+
for (const target of targets) {
|
|
143
|
+
if (index > 0 && staggerMs > 0) {
|
|
144
|
+
await sleep(staggerMs);
|
|
145
|
+
}
|
|
146
|
+
index += 1;
|
|
130
147
|
const localUrl = `http://localhost:${target.port}`;
|
|
131
148
|
const tunnel = (await start({
|
|
132
149
|
url: localUrl,
|
|
@@ -142,32 +159,19 @@ export async function startPublicTunnels(
|
|
|
142
159
|
`Tunnel for "${target.name}" did not provide a public URL`,
|
|
143
160
|
);
|
|
144
161
|
}
|
|
145
|
-
|
|
162
|
+
tunnels.push({
|
|
146
163
|
kind: target.kind,
|
|
147
164
|
name: target.name,
|
|
148
165
|
localUrl,
|
|
149
166
|
publicUrl,
|
|
150
167
|
close: toCloseFn(tunnel),
|
|
151
|
-
};
|
|
152
|
-
}),
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
const tunnels: PublicTunnel[] = [];
|
|
156
|
-
const errors: unknown[] = [];
|
|
157
|
-
for (const result of settled) {
|
|
158
|
-
if (result.status === "fulfilled") {
|
|
159
|
-
tunnels.push(result.value);
|
|
160
|
-
} else {
|
|
161
|
-
errors.push(result.reason);
|
|
168
|
+
});
|
|
162
169
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (errors.length > 0) {
|
|
170
|
+
return tunnels;
|
|
171
|
+
} catch (e) {
|
|
166
172
|
await stopPublicTunnels(tunnels);
|
|
167
|
-
throw
|
|
173
|
+
throw e;
|
|
168
174
|
}
|
|
169
|
-
|
|
170
|
-
return tunnels;
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
export async function stopPublicTunnels(
|