buncargo 3.0.0 → 3.2.3

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.
Files changed (47) hide show
  1. package/dist/cli/bin.js +10 -8
  2. package/dist/cli/index.js +2 -2
  3. package/dist/cli/run-cli.d.ts +10 -2
  4. package/dist/core/quick-tunnel/cloudflared-process.d.ts +10 -0
  5. package/dist/core/quick-tunnel/constants.d.ts +9 -0
  6. package/dist/core/quick-tunnel/index.d.ts +17 -0
  7. package/dist/core/quick-tunnel/install.d.ts +1 -0
  8. package/dist/core/tunnel.d.ts +3 -2
  9. package/dist/environment/index.js +2 -2
  10. package/dist/environment/logging.d.ts +6 -6
  11. package/dist/environment/only-apps.d.ts +10 -0
  12. package/dist/index-3eyrdxw9.js +577 -0
  13. package/dist/index-5aq985p4.js +250 -0
  14. package/dist/index-6cmex7m5.js +72 -0
  15. package/dist/index-6d6x175r.js +572 -0
  16. package/dist/index-7v19es2e.js +666 -0
  17. package/dist/index-9wyhzw0h.js +574 -0
  18. package/dist/index-ag90ry8t.js +576 -0
  19. package/dist/index-bycj26kj.js +72 -0
  20. package/dist/index-byeqyjrz.js +72 -0
  21. package/dist/index-enj4zdma.js +574 -0
  22. package/dist/index-k370bech.js +72 -0
  23. package/dist/index-mf4vjhm3.js +362 -0
  24. package/dist/index-n5g93an7.js +250 -0
  25. package/dist/index-n6z0qw70.js +666 -0
  26. package/dist/index-qa8akv6y.js +666 -0
  27. package/dist/index-vg55rq0y.js +250 -0
  28. package/dist/index-vs81yaks.js +244 -0
  29. package/dist/index-x54nbgs7.js +355 -0
  30. package/dist/index-yz4jfz7z.js +338 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +9 -8
  33. package/dist/loader/index.js +3 -3
  34. package/dist/types/all-types.d.ts +46 -3
  35. package/package.json +147 -145
  36. package/readme.md +16 -0
  37. package/src/cli/run-cli.ts +27 -12
  38. package/src/core/quick-tunnel/cloudflared-process.ts +83 -0
  39. package/src/core/quick-tunnel/constants.ts +31 -0
  40. package/src/core/quick-tunnel/index.ts +96 -0
  41. package/src/core/quick-tunnel/install.ts +160 -0
  42. package/src/core/tunnel.ts +42 -16
  43. package/src/environment/create-dev-environment.ts +123 -13
  44. package/src/environment/logging.ts +34 -20
  45. package/src/environment/only-apps.ts +34 -0
  46. package/src/index.ts +3 -0
  47. package/src/types/all-types.ts +56 -3
package/dist/index.js CHANGED
@@ -2,19 +2,16 @@ import"./index-bj79tw5w.js";
2
2
  import {
3
3
  getFlagValue,
4
4
  hasFlag,
5
- resolveExposeTargets,
6
- runCli,
7
- startPublicTunnels,
8
- stopPublicTunnels
9
- } from "./index-0kxnae3z.js";
5
+ runCli
6
+ } from "./index-n5g93an7.js";
10
7
  import {
11
8
  clearDevEnvCache,
12
9
  getDevEnv,
13
10
  loadDevEnv
14
- } from "./index-4xrxh8yv.js";
11
+ } from "./index-bycj26kj.js";
15
12
  import {
16
13
  createDevEnvironment
17
- } from "./index-ma6tgdb2.js";
14
+ } from "./index-n6z0qw70.js";
18
15
  import {
19
16
  DOCKER_NOT_RUNNING_MESSAGE,
20
17
  MAX_ATTEMPTS,
@@ -52,7 +49,11 @@ import {
52
49
  service,
53
50
  writeGeneratedComposeFile
54
51
  } from "./index-5t9jxqm0.js";
55
- import"./index-bnk6nr0g.js";
52
+ import {
53
+ resolveExposeTargets,
54
+ startPublicTunnels,
55
+ stopPublicTunnels
56
+ } from "./index-mf4vjhm3.js";
56
57
  import {
57
58
  getHeartbeatFile,
58
59
  getWatchdogPidFile,
@@ -4,13 +4,13 @@ import {
4
4
  findConfigFile,
5
5
  getDevEnv,
6
6
  loadDevEnv
7
- } from "../index-4xrxh8yv.js";
8
- import"../index-ma6tgdb2.js";
7
+ } from "../index-bycj26kj.js";
8
+ import"../index-n6z0qw70.js";
9
9
  import"../index-d8tyv5se.js";
10
10
  import"../index-c0dr6mcv.js";
11
11
  import"../index-fb29934k.js";
12
12
  import"../index-5t9jxqm0.js";
13
- import"../index-bnk6nr0g.js";
13
+ import"../index-mf4vjhm3.js";
14
14
  import"../index-mam0bcyz.js";
15
15
  import"../index-mm412dkp.js";
16
16
  import"../index-t0fj6gg1.js";
@@ -382,6 +382,10 @@ export interface StartOptions {
382
382
  suffix?: string;
383
383
  /** Skip automatic seeding (useful when CLI handles seeding separately). Default: false */
384
384
  skipSeed?: boolean;
385
+ /** Skip the initial `logInfo` banner (CLI uses this with `--expose`, then logs once with tunnel URLs). Default: false */
386
+ skipEnvironmentLog?: boolean;
387
+ /** If set, start and wait for only these app names (must exist in `apps`). */
388
+ onlyApps?: string[];
385
389
  }
386
390
  /**
387
391
  * Options for stopping the dev environment.
@@ -398,6 +402,33 @@ export interface StopOptions {
398
402
  export interface DevServerPids {
399
403
  [appName: string]: number;
400
404
  }
405
+ /** Tunnel rows passed to `logInfo` for public URL lines (matches `PublicTunnel` without `close`) */
406
+ export interface DevEnvironmentTunnelLog {
407
+ kind: "service" | "app";
408
+ name: string;
409
+ localUrl: string;
410
+ publicUrl: string;
411
+ }
412
+ /** Active tunnel with teardown — same shape as core `PublicTunnel`. */
413
+ export type PublicTunnelHandle = DevEnvironmentTunnelLog & {
414
+ close: () => Promise<void>;
415
+ };
416
+ /** Options for {@link DevEnvironment.openPublicTunnels}. */
417
+ export interface OpenPublicTunnelsOptions {
418
+ /** Subset of expose targets by name; omit for all `expose: true` services/apps. */
419
+ names?: string[];
420
+ /**
421
+ * Wait for these apps' HTTP health endpoints before opening tunnels.
422
+ * Servers must already be listening on their ports.
423
+ */
424
+ waitForHealthy?: string[];
425
+ }
426
+ /** Result of {@link DevEnvironment.openPublicTunnels}. */
427
+ export interface OpenPublicTunnelsResult<TServices extends Record<string, ServiceConfig>, TApps extends Record<string, AppConfig>> {
428
+ publicUrls: ComputedPublicUrls<TServices, TApps>;
429
+ tunnels: PublicTunnelHandle[];
430
+ close: () => Promise<void>;
431
+ }
401
432
  /**
402
433
  * The main dev environment interface returned by createDevEnvironment().
403
434
  */
@@ -436,6 +467,8 @@ export interface DevEnvironment<TServices extends Record<string, ServiceConfig>,
436
467
  startServers(options?: {
437
468
  productionBuild?: boolean;
438
469
  verbose?: boolean;
470
+ /** If set, start and wait for only these app names (must exist in `apps`). */
471
+ onlyApps?: string[];
439
472
  }): Promise<DevServerPids>;
440
473
  /** Stop a process by PID */
441
474
  stopProcess(pid: number): void;
@@ -443,8 +476,13 @@ export interface DevEnvironment<TServices extends Record<string, ServiceConfig>,
443
476
  waitForServers(options?: {
444
477
  timeout?: number;
445
478
  productionBuild?: boolean;
479
+ /** If set, wait only for these app names (must exist in `apps`). */
480
+ onlyApps?: string[];
446
481
  }): Promise<void>;
447
- /** Build environment variables for shell commands */
482
+ /**
483
+ * Build environment variables for shell commands.
484
+ * Call **after** {@link setPublicUrls} or {@link openPublicTunnels} so `envVars` and `*_PUBLIC_URL` reflect tunnel URLs.
485
+ */
448
486
  buildEnvVars(production?: boolean): Record<string, string>;
449
487
  /** Set public tunnel URLs used by envVars and *_PUBLIC_URL injection */
450
488
  setPublicUrls(urls: ComputedPublicUrls<TServices, TApps>): void;
@@ -460,8 +498,13 @@ export interface DevEnvironment<TServices extends Record<string, ServiceConfig>,
460
498
  }>;
461
499
  /** Wait for an HTTP server to respond */
462
500
  waitForServer(url: string, timeout?: number): Promise<void>;
463
- /** Log environment info to console */
464
- logInfo(label?: string): void;
501
+ /** Log environment info to console; pass `tunnels` to show public URLs next to services/apps */
502
+ logInfo(label?: string, tunnels?: DevEnvironmentTunnelLog[]): void;
503
+ /**
504
+ * Resolve expose targets, start public quick tunnels, and apply {@link setPublicUrls}.
505
+ * Call {@link buildEnvVars} after this resolves when spawning processes that need `EXPO_PUBLIC_*` / `*_PUBLIC_URL`.
506
+ */
507
+ openPublicTunnels(options?: OpenPublicTunnelsOptions): Promise<OpenPublicTunnelsResult<TServices, TApps>>;
465
508
  /**
466
509
  * Get the Expo API URL (http://<local-ip>:<api-port>) and log it for detection.
467
510
  * Used by tools like Vibe Kanban to find the API server for mobile testing.
package/package.json CHANGED
@@ -1,147 +1,149 @@
1
1
  {
2
- "name": "buncargo",
3
- "version": "3.0.0",
4
- "description": "A Bun-powered development environment CLI for managing Docker Compose services, dev servers, and environment variables",
5
- "type": "module",
6
- "module": "./dist/index.js",
7
- "main": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "license": "MIT",
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/HansKristoffer/buncargo.git"
13
- },
14
- "author": "Kristoffer Hansen",
15
- "keywords": [
16
- "bun",
17
- "dev-tools",
18
- "docker",
19
- "docker-compose",
20
- "development",
21
- "cli",
22
- "monorepo",
23
- "dev-server",
24
- "environment"
25
- ],
26
- "engines": {
27
- "bun": ">=1.0.0"
28
- },
29
- "files": [
30
- "src/**/*.ts",
31
- "dist",
32
- "!src/**/*.test.ts"
33
- ],
34
- "bin": {
35
- "dev-tools": "./dist/cli/bin.js",
36
- "buncargo": "./dist/cli/bin.js"
37
- },
38
- "exports": {
39
- ".": {
40
- "types": "./dist/index.d.ts",
41
- "bun": "./src/index.ts",
42
- "import": "./dist/index.js",
43
- "default": "./dist/index.js"
44
- },
45
- "./types": {
46
- "types": "./dist/types/index.d.ts",
47
- "bun": "./src/types/index.ts",
48
- "import": "./dist/types/index.js",
49
- "default": "./dist/types/index.js"
50
- },
51
- "./config": {
52
- "types": "./dist/config/index.d.ts",
53
- "bun": "./src/config/index.ts",
54
- "import": "./dist/config/index.js",
55
- "default": "./dist/config/index.js"
56
- },
57
- "./environment": {
58
- "types": "./dist/environment/index.d.ts",
59
- "bun": "./src/environment/index.ts",
60
- "import": "./dist/environment/index.js",
61
- "default": "./dist/environment/index.js"
62
- },
63
- "./core/ports": {
64
- "types": "./dist/core/ports.d.ts",
65
- "bun": "./src/core/ports.ts",
66
- "import": "./dist/core/ports.js",
67
- "default": "./dist/core/ports.js"
68
- },
69
- "./core/network": {
70
- "types": "./dist/core/network.d.ts",
71
- "bun": "./src/core/network.ts",
72
- "import": "./dist/core/network.js",
73
- "default": "./dist/core/network.js"
74
- },
75
- "./core/process": {
76
- "types": "./dist/core/process.d.ts",
77
- "bun": "./src/core/process.ts",
78
- "import": "./dist/core/process.js",
79
- "default": "./dist/core/process.js"
80
- },
81
- "./core/watchdog": {
82
- "types": "./dist/core/watchdog.d.ts",
83
- "bun": "./src/core/watchdog.ts",
84
- "import": "./dist/core/watchdog.js",
85
- "default": "./dist/core/watchdog.js"
86
- },
87
- "./core/utils": {
88
- "types": "./dist/core/utils.d.ts",
89
- "bun": "./src/core/utils.ts",
90
- "import": "./dist/core/utils.js",
91
- "default": "./dist/core/utils.js"
92
- },
93
- "./cli": {
94
- "types": "./dist/cli/index.d.ts",
95
- "bun": "./src/cli/index.ts",
96
- "import": "./dist/cli/index.js",
97
- "default": "./dist/cli/index.js"
98
- },
99
- "./typecheck": {
100
- "types": "./dist/typecheck/index.d.ts",
101
- "bun": "./src/typecheck/index.ts",
102
- "import": "./dist/typecheck/index.js",
103
- "default": "./dist/typecheck/index.js"
104
- },
105
- "./loader": {
106
- "types": "./dist/loader/index.d.ts",
107
- "bun": "./src/loader/index.ts",
108
- "import": "./dist/loader/index.js",
109
- "default": "./dist/loader/index.js"
110
- },
111
- "./docker": {
112
- "types": "./dist/docker/index.d.ts",
113
- "bun": "./src/docker/index.ts",
114
- "import": "./dist/docker/index.js",
115
- "default": "./dist/docker/index.js"
116
- },
117
- "./docker-compose": {
118
- "types": "./dist/docker-compose/index.d.ts",
119
- "bun": "./src/docker-compose/index.ts",
120
- "import": "./dist/docker-compose/index.js",
121
- "default": "./dist/docker-compose/index.js"
122
- }
123
- },
124
- "scripts": {
125
- "build": "bun run build:js && bun run build:types",
126
- "build:js": "bun build ./src/index.ts ./src/cli/bin.ts ./src/cli/index.ts ./src/config/index.ts ./src/environment/index.ts ./src/loader/index.ts ./src/typecheck/index.ts ./src/types/index.ts ./src/core/network.ts ./src/core/ports.ts ./src/core/process.ts ./src/core/utils.ts ./src/core/watchdog.ts ./src/docker/index.ts ./src/docker-compose/index.ts --outdir ./dist --root ./src --target node --packages external --splitting",
127
- "build:types": "tsc -p tsconfig.build.json",
128
- "prepublishOnly": "bun run build",
129
- "publish:patch": "npm version patch && npm publish",
130
- "publish:minor": "npm version minor && npm publish",
131
- "publish:major": "npm version major && npm publish",
132
- "lint": "bun run typecheck && biome check src example",
133
- "lint:write": "bun run typecheck && biome check --fix src example && biome format src example",
134
- "typecheck": "tsgo --incremental"
135
- },
136
- "devDependencies": {
137
- "@types/bun": "1.3.2",
138
- "@biomejs/biome": "2.3.4",
139
- "@typescript/native-preview": "7.0.0-dev.20260127.1",
140
- "typescript": "^5.7.0"
141
- },
142
- "dependencies": {
143
- "fast-glob": "^3.3.3",
144
- "picocolors": "^1.1.1",
145
- "untun": "^0.1.3"
146
- }
2
+ "name": "buncargo",
3
+ "version": "3.2.3",
4
+ "description": "A Bun-powered development environment CLI for managing Docker Compose services, dev servers, and environment variables",
5
+ "type": "module",
6
+ "module": "./dist/index.js",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/HansKristoffer/buncargo.git"
13
+ },
14
+ "author": "Kristoffer Hansen",
15
+ "keywords": [
16
+ "bun",
17
+ "dev-tools",
18
+ "docker",
19
+ "docker-compose",
20
+ "development",
21
+ "cli",
22
+ "monorepo",
23
+ "dev-server",
24
+ "environment"
25
+ ],
26
+ "engines": {
27
+ "bun": ">=1.0.0"
28
+ },
29
+ "files": [
30
+ "src/**/*.ts",
31
+ "dist",
32
+ "!src/**/*.test.ts"
33
+ ],
34
+ "bin": {
35
+ "dev-tools": "./dist/cli/bin.js",
36
+ "buncargo": "./dist/cli/bin.js"
37
+ },
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "bun": "./src/index.ts",
42
+ "import": "./dist/index.js",
43
+ "default": "./dist/index.js"
44
+ },
45
+ "./types": {
46
+ "types": "./dist/types/index.d.ts",
47
+ "bun": "./src/types/index.ts",
48
+ "import": "./dist/types/index.js",
49
+ "default": "./dist/types/index.js"
50
+ },
51
+ "./config": {
52
+ "types": "./dist/config/index.d.ts",
53
+ "bun": "./src/config/index.ts",
54
+ "import": "./dist/config/index.js",
55
+ "default": "./dist/config/index.js"
56
+ },
57
+ "./environment": {
58
+ "types": "./dist/environment/index.d.ts",
59
+ "bun": "./src/environment/index.ts",
60
+ "import": "./dist/environment/index.js",
61
+ "default": "./dist/environment/index.js"
62
+ },
63
+ "./core/ports": {
64
+ "types": "./dist/core/ports.d.ts",
65
+ "bun": "./src/core/ports.ts",
66
+ "import": "./dist/core/ports.js",
67
+ "default": "./dist/core/ports.js"
68
+ },
69
+ "./core/network": {
70
+ "types": "./dist/core/network.d.ts",
71
+ "bun": "./src/core/network.ts",
72
+ "import": "./dist/core/network.js",
73
+ "default": "./dist/core/network.js"
74
+ },
75
+ "./core/process": {
76
+ "types": "./dist/core/process.d.ts",
77
+ "bun": "./src/core/process.ts",
78
+ "import": "./dist/core/process.js",
79
+ "default": "./dist/core/process.js"
80
+ },
81
+ "./core/watchdog": {
82
+ "types": "./dist/core/watchdog.d.ts",
83
+ "bun": "./src/core/watchdog.ts",
84
+ "import": "./dist/core/watchdog.js",
85
+ "default": "./dist/core/watchdog.js"
86
+ },
87
+ "./core/utils": {
88
+ "types": "./dist/core/utils.d.ts",
89
+ "bun": "./src/core/utils.ts",
90
+ "import": "./dist/core/utils.js",
91
+ "default": "./dist/core/utils.js"
92
+ },
93
+ "./cli": {
94
+ "types": "./dist/cli/index.d.ts",
95
+ "bun": "./src/cli/index.ts",
96
+ "import": "./dist/cli/index.js",
97
+ "default": "./dist/cli/index.js"
98
+ },
99
+ "./typecheck": {
100
+ "types": "./dist/typecheck/index.d.ts",
101
+ "bun": "./src/typecheck/index.ts",
102
+ "import": "./dist/typecheck/index.js",
103
+ "default": "./dist/typecheck/index.js"
104
+ },
105
+ "./loader": {
106
+ "types": "./dist/loader/index.d.ts",
107
+ "bun": "./src/loader/index.ts",
108
+ "import": "./dist/loader/index.js",
109
+ "default": "./dist/loader/index.js"
110
+ },
111
+ "./docker": {
112
+ "types": "./dist/docker/index.d.ts",
113
+ "bun": "./src/docker/index.ts",
114
+ "import": "./dist/docker/index.js",
115
+ "default": "./dist/docker/index.js"
116
+ },
117
+ "./docker-compose": {
118
+ "types": "./dist/docker-compose/index.d.ts",
119
+ "bun": "./src/docker-compose/index.ts",
120
+ "import": "./dist/docker-compose/index.js",
121
+ "default": "./dist/docker-compose/index.js"
122
+ }
123
+ },
124
+ "scripts": {
125
+ "build": "bun run build:js && bun run build:types",
126
+ "build:js": "bun build ./src/index.ts ./src/cli/bin.ts ./src/cli/index.ts ./src/config/index.ts ./src/environment/index.ts ./src/loader/index.ts ./src/typecheck/index.ts ./src/types/index.ts ./src/core/network.ts ./src/core/ports.ts ./src/core/process.ts ./src/core/utils.ts ./src/core/watchdog.ts ./src/docker/index.ts ./src/docker-compose/index.ts --outdir ./dist --root ./src --target node --packages external --splitting",
127
+ "build:types": "tsc -p tsconfig.build.json",
128
+ "prepublishOnly": "bun run build",
129
+ "publish:patch": "npm version patch && npm publish",
130
+ "publish:minor": "npm version minor && npm publish",
131
+ "publish:major": "npm version major && npm publish",
132
+ "lint": "bun run typecheck && biome check src example",
133
+ "lint:write": "bun run typecheck && biome check --fix src example && biome format src example",
134
+ "typecheck": "tsgo --incremental",
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"
138
+ },
139
+ "devDependencies": {
140
+ "@types/bun": "1.3.2",
141
+ "@biomejs/biome": "2.3.4",
142
+ "@typescript/native-preview": "7.0.0-dev.20260127.1",
143
+ "typescript": "^5.7.0"
144
+ },
145
+ "dependencies": {
146
+ "fast-glob": "^3.3.3",
147
+ "picocolors": "^1.1.1"
148
+ }
147
149
  }
package/readme.md CHANGED
@@ -179,6 +179,8 @@ apps: {
179
179
  }
180
180
  ```
181
181
 
182
+ Use `onlyApps` on `start()` or `startServers()` to launch and wait for only those named apps (same env injection and health checks as when all apps run).
183
+
182
184
  ## Environment Variables
183
185
 
184
186
  The `envVars` function builds all env vars from computed ports and URLs:
@@ -193,6 +195,14 @@ envVars: (ports, urls, { localIp, publicUrls }) => ({
193
195
  })
194
196
  ```
195
197
 
198
+ `buildEnvVars()` always includes, for each service/app name `foo`:
199
+
200
+ - `FOO_PORT` — assigned port
201
+ - `FOO_URL` — local URL (LAN)
202
+ - `FOO_PUBLIC_URL` — only while a public tunnel is active for that name
203
+
204
+ Your `envVars` callback receives `publicUrls` and typically maps client bundles, e.g. `EXPO_PUBLIC_API_URL: publicUrls.api ?? urls.api`.
205
+
196
206
  These are injected into:
197
207
  - Docker Compose services
198
208
  - Dev server processes
@@ -246,6 +256,12 @@ envVars: (_ports, urls, { publicUrls }) => ({
246
256
  })
247
257
  ```
248
258
 
259
+ **CLI vs programmatic ordering:** `bunx buncargo dev --expose` starts Cloudflare quick tunnels after containers and migrations but **before** the interactive dev-server command (e.g. `concurrently`) runs. Until those servers are listening on their ports, tunnel traffic can briefly error. For “servers first, then public URLs,” use the API: e.g. `await dev.startServers({ onlyApps: ['api', 'platform'] })`, then `await dev.openPublicTunnels({ waitForHealthy: ['api', 'platform'] })` (optional; waits HTTP health first), then read `dev.buildEnvVars()` for spawned children.
260
+
261
+ **Expo hybrid:** buncargo is suited to exposing **API** and **platform** (Vite, etc.) for devices on cellular. **Metro** often uses Expo’s own tunnel (`expo start --tunnel`); you usually do **not** add Metro as a buncargo `app` unless you want buncargo to start it. Wire `EXPO_PUBLIC_*` from `publicUrls.* ?? urls.*` in `envVars`.
262
+
263
+ **Programmatic helper:** `openPublicTunnels({ names?, waitForHealthy? })` applies tunnel URLs via `setPublicUrls` and returns `close()` to stop tunnels and clear public URLs. Call `buildEnvVars()` after `openPublicTunnels` resolves so `envVars` and `*_PUBLIC_URL` see the tunnel origins.
264
+
249
265
  ## Lifecycle Hooks
250
266
 
251
267
  Run code at specific points in the startup/shutdown cycle:
@@ -7,7 +7,6 @@ import {
7
7
  stopPublicTunnels,
8
8
  } from "../core/tunnel";
9
9
  import { spawnWatchdog, startHeartbeat, stopHeartbeat } from "../core/watchdog";
10
- import { logPublicUrls } from "../environment/logging";
11
10
  import type {
12
11
  AppConfig,
13
12
  CliOptions,
@@ -88,14 +87,27 @@ export async function runCli<
88
87
  TApps extends Record<string, AppConfig>,
89
88
  >(
90
89
  env: DevEnvironment<TServices, TApps>,
91
- options: CliOptions = {},
90
+ options: CliOptions & {
91
+ /** Substitute tunnel helpers (used by CLI integration tests). */
92
+ cliTestTunnel?: {
93
+ resolveExposeTargets: typeof resolveExposeTargets;
94
+ startPublicTunnels: typeof startPublicTunnels;
95
+ stopPublicTunnels: typeof stopPublicTunnels;
96
+ };
97
+ } = {},
92
98
  ): Promise<void> {
93
99
  const {
94
100
  args = process.argv.slice(2),
95
101
  watchdog = true,
96
102
  watchdogTimeout = 10,
97
103
  devServersCommand,
104
+ cliTestTunnel,
98
105
  } = options;
106
+ const tunnelApi = cliTestTunnel ?? {
107
+ resolveExposeTargets,
108
+ startPublicTunnels,
109
+ stopPublicTunnels,
110
+ };
99
111
  const exposeRequested = hasFlag(args, "--expose");
100
112
  const exposeValue = getFlagValue(args, "--expose");
101
113
  let tunnels: PublicTunnel[] = [];
@@ -103,7 +115,7 @@ export async function runCli<
103
115
  async function cleanupTunnels(): Promise<void> {
104
116
  env.clearPublicUrls();
105
117
  if (tunnels.length === 0) return;
106
- await stopPublicTunnels(tunnels);
118
+ await tunnelApi.stopPublicTunnels(tunnels);
107
119
  tunnels = [];
108
120
  }
109
121
 
@@ -143,13 +155,16 @@ export async function runCli<
143
155
  // All other paths need containers + migrations
144
156
  // Skip automatic seeding when --seed flag is used (CLI handles it explicitly)
145
157
  const skipSeed = args.includes("--seed");
146
- await env.start({ startServers: false, wait: true, skipSeed });
158
+ await env.start({
159
+ startServers: false,
160
+ wait: true,
161
+ skipSeed,
162
+ skipEnvironmentLog: exposeRequested,
163
+ });
147
164
 
148
165
  if (exposeRequested) {
149
- const { targets, unknownNames, notEnabledNames } = resolveExposeTargets(
150
- env,
151
- exposeValue,
152
- );
166
+ const { targets, unknownNames, notEnabledNames } =
167
+ tunnelApi.resolveExposeTargets(env, exposeValue);
153
168
  if (unknownNames.length > 0) {
154
169
  console.error(
155
170
  `❌ Unknown expose target${unknownNames.length > 1 ? "s" : ""}: ${unknownNames.join(", ")}`,
@@ -175,13 +190,13 @@ export async function runCli<
175
190
  process.exit(1);
176
191
  }
177
192
 
178
- tunnels = await startPublicTunnels(targets);
193
+ tunnels = await tunnelApi.startPublicTunnels(targets);
179
194
  env.setPublicUrls(
180
195
  Object.fromEntries(
181
196
  tunnels.map((tunnel) => [tunnel.name, tunnel.publicUrl]),
182
197
  ) as typeof env.publicUrls,
183
198
  );
184
- logPublicUrls(tunnels);
199
+ env.logInfo("Dev Environment", tunnels);
185
200
  }
186
201
 
187
202
  // Handle --migrate (exit after migrations)
@@ -351,10 +366,10 @@ function runCommand(
351
366
  // ═══════════════════════════════════════════════════════════════════════════
352
367
 
353
368
  /**
354
- * Check if a CLI flag is present.
369
+ * Check if a CLI flag is present (including `--flag=value` form).
355
370
  */
356
371
  export function hasFlag(args: string[], flag: string): boolean {
357
- return args.includes(flag);
372
+ return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
358
373
  }
359
374
 
360
375
  /**
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Spawn cloudflared and parse the quick-tunnel public URL from output.
3
+ * Derived from unjs/untun (MIT), originally forked from node-cloudflared.
4
+ */
5
+ import { type ChildProcess, spawn } from "node:child_process";
6
+ import { cloudflaredBinPath } from "./constants";
7
+
8
+ const urlRegex = /\|\s+(https?:\/\/\S+)/;
9
+
10
+ export function startCloudflaredTunnel(
11
+ options: Record<string, string | number | null>,
12
+ ): {
13
+ url: Promise<string>;
14
+ child: ChildProcess;
15
+ stop: () => boolean;
16
+ } {
17
+ const args: string[] = ["tunnel"];
18
+ for (const [key, value] of Object.entries(options)) {
19
+ if (typeof value === "string") {
20
+ args.push(`${key}`, value);
21
+ } else if (typeof value === "number") {
22
+ args.push(`${key}`, value.toString());
23
+ } else if (value === null) {
24
+ args.push(`${key}`);
25
+ }
26
+ }
27
+ if (args.length === 1) {
28
+ args.push("--url", "localhost:8080");
29
+ }
30
+
31
+ const child = spawn(cloudflaredBinPath, args, {
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ });
34
+
35
+ if (process.env.DEBUG) {
36
+ child.stdout?.pipe(process.stdout);
37
+ child.stderr?.pipe(process.stderr);
38
+ }
39
+
40
+ let settled = false;
41
+ let urlResolver!: (value: string | PromiseLike<string>) => void;
42
+ let urlRejector!: (reason: unknown) => void;
43
+ const url = new Promise<string>((resolve, reject) => {
44
+ urlResolver = (v) => {
45
+ if (!settled) {
46
+ settled = true;
47
+ resolve(v);
48
+ }
49
+ };
50
+ urlRejector = (e) => {
51
+ if (!settled) {
52
+ settled = true;
53
+ reject(e);
54
+ }
55
+ };
56
+ });
57
+
58
+ const parser = (data: Buffer) => {
59
+ const str = data.toString();
60
+
61
+ const urlMatch = str.match(urlRegex);
62
+ if (urlMatch) {
63
+ urlResolver(urlMatch[1] ?? "");
64
+ }
65
+ };
66
+ child.stdout?.on("data", parser).on("error", urlRejector);
67
+ child.stderr?.on("data", parser).on("error", urlRejector);
68
+
69
+ child.on("exit", (code, signal) => {
70
+ if (!settled) {
71
+ urlRejector(
72
+ new Error(
73
+ `cloudflared exited before a tunnel URL was parsed (code=${code}, signal=${signal ?? "none"})`,
74
+ ),
75
+ );
76
+ }
77
+ });
78
+ child.on("error", urlRejector);
79
+
80
+ const stop = () => child.kill("SIGINT");
81
+
82
+ return { url, child, stop };
83
+ }