buncargo 1.0.29 → 3.2.0

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 (246) hide show
  1. package/dist/bin.d.ts +1 -12
  2. package/dist/bin.js +261 -253
  3. package/dist/cli/bin.d.ts +13 -0
  4. package/dist/cli/bin.js +317 -0
  5. package/dist/cli/commands/help.d.ts +1 -0
  6. package/dist/cli/commands/runtime.d.ts +5 -0
  7. package/dist/cli/commands/version.d.ts +1 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +14 -0
  10. package/dist/cli/run-cli.d.ts +30 -0
  11. package/dist/cli.d.ts +1 -22
  12. package/dist/cli.js +5 -13
  13. package/dist/config/config.d.ts +1 -0
  14. package/dist/config/define-config.d.ts +13 -0
  15. package/dist/config/index.d.ts +3 -0
  16. package/dist/config/index.js +15 -0
  17. package/dist/config/merge-configs.d.ts +3 -0
  18. package/dist/config/validate-config.d.ts +3 -0
  19. package/dist/config.d.ts +1 -72
  20. package/dist/config.js +12 -12
  21. package/dist/core/docker.d.ts +1 -83
  22. package/dist/core/docker.js +35 -32
  23. package/dist/core/index.d.ts +1 -1
  24. package/dist/core/index.js +123 -118
  25. package/dist/core/network.js +2 -2
  26. package/dist/core/ports.js +1 -1
  27. package/dist/core/process.js +1 -1
  28. package/dist/core/quick-tunnel/cloudflared-process.d.ts +10 -0
  29. package/dist/core/quick-tunnel/constants.d.ts +9 -0
  30. package/dist/core/quick-tunnel/index.d.ts +17 -0
  31. package/dist/core/quick-tunnel/install.d.ts +1 -0
  32. package/dist/core/tunnel.d.ts +34 -0
  33. package/dist/core/utils.js +2 -2
  34. package/dist/core/watchdog-runner.js +45 -42
  35. package/dist/core/watchdog.d.ts +1 -0
  36. package/dist/core/watchdog.js +4 -2
  37. package/dist/docker/index.d.ts +1 -0
  38. package/dist/docker/index.js +38 -0
  39. package/dist/docker/runtime.d.ts +87 -0
  40. package/dist/docker/runtime.js +37 -0
  41. package/dist/docker-compose/compose.d.ts +1 -0
  42. package/dist/docker-compose/generated-file.d.ts +7 -0
  43. package/dist/docker-compose/index.d.ts +3 -0
  44. package/dist/docker-compose/index.js +15 -0
  45. package/dist/docker-compose/model.d.ts +6 -0
  46. package/dist/docker-compose/services/clickhouse.d.ts +16 -0
  47. package/dist/docker-compose/services/define-docker-service.d.ts +41 -0
  48. package/dist/docker-compose/services/index.d.ts +23 -0
  49. package/dist/docker-compose/services/index.js +17 -0
  50. package/dist/docker-compose/services/postgres.d.ts +12 -0
  51. package/dist/docker-compose/services/redis.d.ts +12 -0
  52. package/dist/docker-compose/services/shared.d.ts +7 -0
  53. package/dist/docker-compose/yaml.d.ts +2 -0
  54. package/dist/environment/create-dev-environment.d.ts +23 -0
  55. package/dist/environment/index.d.ts +1 -0
  56. package/dist/environment/index.js +15 -0
  57. package/dist/environment/logging.d.ts +17 -0
  58. package/dist/environment/only-apps.d.ts +10 -0
  59. package/dist/environment/seeding.d.ts +9 -0
  60. package/dist/environment.d.ts +1 -23
  61. package/dist/environment.js +12 -14
  62. package/dist/index-045jksh5.js +147 -0
  63. package/dist/index-08wa79cs.js +125 -117
  64. package/dist/index-0kxnae3z.js +335 -0
  65. package/dist/index-1mdrf7nz.js +51 -43
  66. package/dist/index-1yvbwj4k.js +262 -242
  67. package/dist/index-23ev345g.js +475 -0
  68. package/dist/index-2ckr49sf.js +228 -0
  69. package/dist/index-2f47khe5.js +376 -369
  70. package/dist/index-2fr3g85b.js +220 -183
  71. package/dist/index-38xnzpa6.js +450 -0
  72. package/dist/index-3eyrdxw9.js +577 -0
  73. package/dist/index-3h3dhtf2.js +51 -43
  74. package/dist/index-42x95209.js +51 -43
  75. package/dist/index-4gp0az1g.js +145 -0
  76. package/dist/index-4xrxh8yv.js +72 -0
  77. package/dist/index-5aq985p4.js +250 -0
  78. package/dist/index-5gmws6ah.js +181 -0
  79. package/dist/index-5hka0tff.js +78 -76
  80. package/dist/index-5rfqps4b.js +3 -0
  81. package/dist/index-5t9jxqm0.js +428 -0
  82. package/dist/index-6c1w1xk5.js +101 -0
  83. package/dist/index-6cmex7m5.js +72 -0
  84. package/dist/index-6d6x175r.js +572 -0
  85. package/dist/index-6fm7mvwj.js +118 -97
  86. package/dist/index-6srpc523.js +127 -128
  87. package/dist/index-731rzzfp.js +157 -142
  88. package/dist/index-75y4cg2z.js +51 -43
  89. package/dist/index-7ja4ywyj.js +126 -127
  90. package/dist/index-7v19es2e.js +666 -0
  91. package/dist/index-8bw1cmz4.js +531 -0
  92. package/dist/index-8hbbj1mp.js +120 -121
  93. package/dist/index-8xj2p5n5.js +118 -97
  94. package/dist/index-9wyhzw0h.js +574 -0
  95. package/dist/index-ag90ry8t.js +576 -0
  96. package/dist/index-bj79tw5w.js +0 -0
  97. package/dist/index-bnk6nr0g.js +73 -0
  98. package/dist/index-brbbzyks.js +72 -0
  99. package/dist/index-byeqyjrz.js +72 -0
  100. package/dist/index-c0dr6mcv.js +123 -0
  101. package/dist/index-cty0bcry.js +235 -218
  102. package/dist/index-d8tyv5se.js +228 -0
  103. package/dist/index-d9efy0n4.js +176 -150
  104. package/dist/index-enj4zdma.js +574 -0
  105. package/dist/index-etfmqjjf.js +427 -0
  106. package/dist/index-fb29934k.js +172 -0
  107. package/dist/index-g50jw1yf.js +72 -0
  108. package/dist/index-g6eb5wdw.js +118 -117
  109. package/dist/index-ggq3yryx.js +99 -95
  110. package/dist/index-h70tce00.js +177 -0
  111. package/dist/index-hkxtfqtc.js +333 -0
  112. package/dist/index-k370bech.js +72 -0
  113. package/dist/index-kf3dhser.js +146 -143
  114. package/dist/index-ma6tgdb2.js +500 -0
  115. package/dist/index-mam0bcyz.js +123 -0
  116. package/dist/index-mm412dkp.js +274 -0
  117. package/dist/index-n8v18aeb.js +0 -0
  118. package/dist/index-ndnmnsej.js +378 -371
  119. package/dist/index-p8wty0e2.js +389 -379
  120. package/dist/index-qa8akv6y.js +666 -0
  121. package/dist/index-qfphr2fd.js +78 -76
  122. package/dist/index-qqmms8rs.js +51 -43
  123. package/dist/index-qw4093g2.js +51 -43
  124. package/dist/index-qzwpzjbx.js +121 -122
  125. package/dist/index-segbnm0h.js +146 -143
  126. package/dist/index-t0fj6gg1.js +112 -0
  127. package/dist/index-thdkwnv7.js +122 -0
  128. package/dist/index-tjbx2r2t.js +270 -0
  129. package/dist/index-tjqw9vtj.js +62 -54
  130. package/dist/index-vbpb89jy.js +248 -0
  131. package/dist/index-vg55rq0y.js +250 -0
  132. package/dist/index-vhs88xhe.js +99 -95
  133. package/dist/index-vs81yaks.js +244 -0
  134. package/dist/index-w8zxnjka.js +249 -0
  135. package/dist/index-wk2na3t9.js +385 -375
  136. package/dist/index-wz9x8g7z.js +383 -373
  137. package/dist/index-x249gyde.js +388 -378
  138. package/dist/index-x54nbgs7.js +355 -0
  139. package/dist/index-xkvd0nsd.js +187 -0
  140. package/dist/index-yedqxm1z.js +80 -0
  141. package/dist/index-yz4jfz7z.js +338 -0
  142. package/dist/index-zfjzzjkf.js +240 -199
  143. package/dist/index.d.ts +12 -8
  144. package/dist/index.js +56 -34
  145. package/dist/lint.d.ts +1 -46
  146. package/dist/lint.js +3 -7
  147. package/dist/loader/cache.d.ts +4 -0
  148. package/dist/loader/find-config-file.d.ts +2 -0
  149. package/dist/loader/index.d.ts +5 -0
  150. package/dist/loader/index.js +24 -0
  151. package/dist/loader/load-dev-env.d.ts +5 -0
  152. package/dist/loader/loader.d.ts +1 -0
  153. package/dist/loader.d.ts +1 -45
  154. package/dist/loader.js +22 -20
  155. package/dist/prisma/index.d.ts +1 -0
  156. package/dist/prisma/prisma.d.ts +29 -0
  157. package/dist/prisma.d.ts +1 -29
  158. package/dist/prisma.js +6 -10
  159. package/dist/src/bin.js +309 -0
  160. package/dist/src/cli.js +5 -0
  161. package/dist/src/config.js +15 -0
  162. package/dist/src/core/docker.js +38 -0
  163. package/dist/src/core/index.js +130 -0
  164. package/dist/src/core/network.js +9 -0
  165. package/dist/src/core/ports.js +23 -0
  166. package/dist/src/core/process.js +31 -0
  167. package/dist/src/core/utils.js +11 -0
  168. package/dist/src/core/watchdog-runner.js +69 -0
  169. package/dist/src/core/watchdog.js +28 -0
  170. package/dist/src/docker/runtime.js +37 -0
  171. package/dist/src/docker-compose/index.js +16 -0
  172. package/dist/src/docker-compose/services/index.js +17 -0
  173. package/dist/src/environment.js +12 -0
  174. package/dist/src/index.js +122 -0
  175. package/dist/src/lint.js +3 -0
  176. package/dist/src/loader.js +25 -0
  177. package/dist/src/prisma.js +6 -0
  178. package/dist/src/types.js +0 -0
  179. package/dist/typecheck/index.d.ts +1 -0
  180. package/dist/typecheck/index.js +7 -0
  181. package/dist/typecheck/typecheck.d.ts +46 -0
  182. package/dist/types/all-types.d.ts +544 -0
  183. package/dist/types/cli.d.ts +1 -0
  184. package/dist/types/config.d.ts +6 -0
  185. package/dist/types/docker.d.ts +15 -0
  186. package/dist/types/environment.d.ts +8 -0
  187. package/dist/types/hooks.d.ts +9 -0
  188. package/dist/types/index.d.ts +1 -0
  189. package/dist/types/index.js +0 -0
  190. package/dist/types/prisma.d.ts +1 -0
  191. package/dist/types.d.ts +1 -399
  192. package/package.json +55 -48
  193. package/readme.md +365 -109
  194. package/src/cli/bin.ts +77 -0
  195. package/src/cli/commands/help.ts +39 -0
  196. package/src/cli/commands/runtime.ts +72 -0
  197. package/src/cli/commands/version.ts +4 -0
  198. package/src/cli/index.ts +1 -0
  199. package/{cli.ts → src/cli/run-cli.ts} +114 -10
  200. package/src/config/define-config.ts +30 -0
  201. package/src/config/index.ts +3 -0
  202. package/src/config/merge-configs.ts +33 -0
  203. package/src/config/validate-config.ts +136 -0
  204. package/{core → src/core}/index.ts +2 -2
  205. package/{core → src/core}/ports.ts +5 -2
  206. package/{core → src/core}/process.ts +6 -2
  207. package/src/core/quick-tunnel/cloudflared-process.ts +83 -0
  208. package/src/core/quick-tunnel/constants.ts +31 -0
  209. package/src/core/quick-tunnel/index.ts +96 -0
  210. package/src/core/quick-tunnel/install.ts +160 -0
  211. package/src/core/tunnel.ts +165 -0
  212. package/{core → src/core}/utils.ts +1 -0
  213. package/{core → src/core}/watchdog.ts +5 -1
  214. package/src/docker/index.ts +1 -0
  215. package/{core/docker.ts → src/docker/runtime.ts} +11 -4
  216. package/src/docker-compose/generated-file.ts +45 -0
  217. package/src/docker-compose/index.ts +7 -0
  218. package/src/docker-compose/model.ts +197 -0
  219. package/src/docker-compose/services/clickhouse.ts +79 -0
  220. package/src/docker-compose/services/define-docker-service.ts +109 -0
  221. package/src/docker-compose/services/index.ts +67 -0
  222. package/src/docker-compose/services/postgres.ts +60 -0
  223. package/src/docker-compose/services/redis.ts +48 -0
  224. package/src/docker-compose/services/shared.ts +79 -0
  225. package/src/docker-compose/yaml.ts +88 -0
  226. package/{environment.ts → src/environment/create-dev-environment.ts} +214 -141
  227. package/src/environment/index.ts +1 -0
  228. package/src/environment/logging.ts +115 -0
  229. package/src/environment/only-apps.ts +34 -0
  230. package/src/environment/seeding.ts +57 -0
  231. package/{index.ts → src/index.ts} +52 -20
  232. package/src/loader/cache.ts +23 -0
  233. package/src/loader/find-config-file.ts +29 -0
  234. package/src/loader/index.ts +17 -0
  235. package/src/loader/load-dev-env.ts +38 -0
  236. package/src/prisma/index.ts +1 -0
  237. package/{prisma.ts → src/prisma/prisma.ts} +4 -2
  238. package/src/typecheck/index.ts +1 -0
  239. package/{types.ts → src/types/all-types.ts} +186 -8
  240. package/src/types/index.ts +1 -0
  241. package/bin.ts +0 -192
  242. package/config.ts +0 -194
  243. package/loader.ts +0 -126
  244. /package/{core → src/core}/network.ts +0 -0
  245. /package/{core → src/core}/watchdog-runner.ts +0 -0
  246. /package/{lint.ts → src/typecheck/typecheck.ts} +0 -0
@@ -0,0 +1,72 @@
1
+ import { loadDevEnv } from "../../loader";
2
+ import { runCli } from "../run-cli";
3
+
4
+ export async function loadEnv() {
5
+ try {
6
+ return await loadDevEnv();
7
+ } catch (error) {
8
+ console.error(`❌ ${error instanceof Error ? error.message : error}`);
9
+ process.exit(1);
10
+ }
11
+ }
12
+
13
+ export async function handleDev(args: string[]): Promise<void> {
14
+ const env = await loadEnv();
15
+ await runCli(env, { args });
16
+ }
17
+
18
+ export async function handlePrisma(args: string[]): Promise<void> {
19
+ const env = await loadEnv();
20
+
21
+ if (!env.prisma) {
22
+ console.error("❌ Prisma is not configured in your dev config.");
23
+ console.error("");
24
+ console.error(" Add prisma to your config:");
25
+ console.error("");
26
+ console.error(" export default defineDevConfig({");
27
+ console.error(" ...");
28
+ console.error(" prisma: {");
29
+ console.error(" cwd: 'packages/prisma'");
30
+ console.error(" }");
31
+ console.error(" })");
32
+ process.exit(1);
33
+ }
34
+
35
+ const running = await env.isRunning();
36
+ if (!running) {
37
+ console.log("🐳 Starting database container...");
38
+ await env.start({ startServers: false, wait: true });
39
+ }
40
+
41
+ const exitCode = await env.prisma.run(args);
42
+ process.exit(exitCode);
43
+ }
44
+
45
+ export async function handleEnv(): Promise<void> {
46
+ const env = await loadEnv();
47
+ console.log(
48
+ JSON.stringify(
49
+ {
50
+ projectName: env.projectName,
51
+ ports: env.ports,
52
+ urls: env.urls,
53
+ portOffset: env.portOffset,
54
+ isWorktree: env.isWorktree,
55
+ localIp: env.localIp,
56
+ root: env.root,
57
+ },
58
+ null,
59
+ 2,
60
+ ),
61
+ );
62
+ }
63
+
64
+ export async function handleTypecheck(): Promise<void> {
65
+ const env = await loadEnv();
66
+ const { runWorkspaceTypecheck } = await import("../../typecheck");
67
+ const result = await runWorkspaceTypecheck({
68
+ root: env.root,
69
+ verbose: true,
70
+ });
71
+ process.exit(result.success ? 0 : 1);
72
+ }
@@ -0,0 +1,4 @@
1
+ export function showVersion(): void {
2
+ const pkg = require("../../../package.json");
3
+ console.log(`buncargo v${pkg.version}`);
4
+ }
@@ -0,0 +1 @@
1
+ export * from "./run-cli";
@@ -1,12 +1,18 @@
1
1
  import { spawn } from "node:child_process";
2
- import { killProcessesOnAppPorts } from "./core/process";
3
- import { spawnWatchdog, startHeartbeat, stopHeartbeat } from "./core/watchdog";
2
+ import { killProcessesOnAppPorts } from "../core/process";
3
+ import {
4
+ type PublicTunnel,
5
+ resolveExposeTargets,
6
+ startPublicTunnels,
7
+ stopPublicTunnels,
8
+ } from "../core/tunnel";
9
+ import { spawnWatchdog, startHeartbeat, stopHeartbeat } from "../core/watchdog";
4
10
  import type {
5
11
  AppConfig,
6
12
  CliOptions,
7
13
  DevEnvironment,
8
14
  ServiceConfig,
9
- } from "./types";
15
+ } from "../types";
10
16
 
11
17
  // ═══════════════════════════════════════════════════════════════════════════
12
18
  // CLI Runner
@@ -20,6 +26,7 @@ const ACCEPTED_FLAGS = [
20
26
  "--migrate",
21
27
  "--seed",
22
28
  "--up-only",
29
+ "--expose",
23
30
  ] as const;
24
31
 
25
32
  /**
@@ -36,12 +43,15 @@ Options:
36
43
  --migrate Run migrations and exit
37
44
  --seed Run migrations and seeders, then exit
38
45
  --up-only Start containers and run migrations, then exit (no dev servers)
46
+ --expose Expose configured targets via public quick tunnels
39
47
 
40
48
  Examples:
41
49
  bun dev Start dev environment with all services
42
50
  bun dev --seed Run migrations and seed the database
43
51
  bun dev --down Stop all containers
44
52
  bun dev --reset Stop containers and remove all data
53
+ bun dev --expose Expose all targets with expose: true
54
+ bun dev --expose=api,web Expose specific targets
45
55
  `);
46
56
  }
47
57
 
@@ -50,7 +60,13 @@ Examples:
50
60
  */
51
61
  function getUnknownFlags(args: string[]): string[] {
52
62
  return args.filter(
53
- (arg) => arg.startsWith("--") && !ACCEPTED_FLAGS.includes(arg as typeof ACCEPTED_FLAGS[number]),
63
+ (arg) =>
64
+ arg.startsWith("--") &&
65
+ !ACCEPTED_FLAGS.includes(
66
+ (arg.includes("=")
67
+ ? arg.split("=")[0]
68
+ : arg) as (typeof ACCEPTED_FLAGS)[number],
69
+ ),
54
70
  );
55
71
  }
56
72
 
@@ -71,14 +87,37 @@ export async function runCli<
71
87
  TApps extends Record<string, AppConfig>,
72
88
  >(
73
89
  env: DevEnvironment<TServices, TApps>,
74
- 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
+ } = {},
75
98
  ): Promise<void> {
76
99
  const {
77
100
  args = process.argv.slice(2),
78
101
  watchdog = true,
79
102
  watchdogTimeout = 10,
80
103
  devServersCommand,
104
+ cliTestTunnel,
81
105
  } = options;
106
+ const tunnelApi = cliTestTunnel ?? {
107
+ resolveExposeTargets,
108
+ startPublicTunnels,
109
+ stopPublicTunnels,
110
+ };
111
+ const exposeRequested = hasFlag(args, "--expose");
112
+ const exposeValue = getFlagValue(args, "--expose");
113
+ let tunnels: PublicTunnel[] = [];
114
+
115
+ async function cleanupTunnels(): Promise<void> {
116
+ env.clearPublicUrls();
117
+ if (tunnels.length === 0) return;
118
+ await tunnelApi.stopPublicTunnels(tunnels);
119
+ tunnels = [];
120
+ }
82
121
 
83
122
  // Handle --help
84
123
  if (args.includes("--help")) {
@@ -89,7 +128,9 @@ export async function runCli<
89
128
  // Validate flags
90
129
  const unknownFlags = getUnknownFlags(args);
91
130
  if (unknownFlags.length > 0) {
92
- console.error(`❌ Unknown flag${unknownFlags.length > 1 ? "s" : ""}: ${unknownFlags.join(", ")}`);
131
+ console.error(
132
+ `❌ Unknown flag${unknownFlags.length > 1 ? "s" : ""}: ${unknownFlags.join(", ")}`,
133
+ );
93
134
  console.error("");
94
135
  printHelp();
95
136
  process.exit(1);
@@ -98,6 +139,7 @@ export async function runCli<
98
139
  // Handle --down (no need to start anything)
99
140
  if (args.includes("--down")) {
100
141
  env.logInfo();
142
+ await cleanupTunnels();
101
143
  await env.stop();
102
144
  process.exit(0);
103
145
  }
@@ -105,6 +147,7 @@ export async function runCli<
105
147
  // Handle --reset (no need to start anything)
106
148
  if (args.includes("--reset")) {
107
149
  env.logInfo();
150
+ await cleanupTunnels();
108
151
  await env.stop({ removeVolumes: true });
109
152
  process.exit(0);
110
153
  }
@@ -112,12 +155,55 @@ export async function runCli<
112
155
  // All other paths need containers + migrations
113
156
  // Skip automatic seeding when --seed flag is used (CLI handles it explicitly)
114
157
  const skipSeed = args.includes("--seed");
115
- await env.start({ startServers: false, wait: true, skipSeed });
158
+ await env.start({
159
+ startServers: false,
160
+ wait: true,
161
+ skipSeed,
162
+ skipEnvironmentLog: exposeRequested,
163
+ });
164
+
165
+ if (exposeRequested) {
166
+ const { targets, unknownNames, notEnabledNames } =
167
+ tunnelApi.resolveExposeTargets(env, exposeValue);
168
+ if (unknownNames.length > 0) {
169
+ console.error(
170
+ `❌ Unknown expose target${unknownNames.length > 1 ? "s" : ""}: ${unknownNames.join(", ")}`,
171
+ );
172
+ await cleanupTunnels();
173
+ process.exit(1);
174
+ }
175
+ if (notEnabledNames.length > 0) {
176
+ console.error(
177
+ `❌ Target${notEnabledNames.length > 1 ? "s" : ""} missing expose: true: ${notEnabledNames.join(", ")}`,
178
+ );
179
+ console.error(
180
+ " Mark these in dev.config.ts with expose: true or remove them from --expose.",
181
+ );
182
+ await cleanupTunnels();
183
+ process.exit(1);
184
+ }
185
+ if (targets.length === 0) {
186
+ console.error(
187
+ "❌ No expose targets selected. Add expose: true to services/apps or pass names with --expose=<name>.",
188
+ );
189
+ await cleanupTunnels();
190
+ process.exit(1);
191
+ }
192
+
193
+ tunnels = await tunnelApi.startPublicTunnels(targets);
194
+ env.setPublicUrls(
195
+ Object.fromEntries(
196
+ tunnels.map((tunnel) => [tunnel.name, tunnel.publicUrl]),
197
+ ) as typeof env.publicUrls,
198
+ );
199
+ env.logInfo("Dev Environment", tunnels);
200
+ }
116
201
 
117
202
  // Handle --migrate (exit after migrations)
118
203
  if (args.includes("--migrate")) {
119
204
  console.log("");
120
205
  console.log("✅ Migrations applied successfully");
206
+ await cleanupTunnels();
121
207
  process.exit(0);
122
208
  }
123
209
 
@@ -135,10 +221,12 @@ export async function runCli<
135
221
  if (result.stdout) {
136
222
  console.error(result.stdout);
137
223
  }
224
+ await cleanupTunnels();
138
225
  process.exit(1);
139
226
  }
140
227
  console.log("");
141
228
  console.log("✅ Seeding complete");
229
+ await cleanupTunnels();
142
230
  process.exit(0);
143
231
  }
144
232
 
@@ -147,6 +235,7 @@ export async function runCli<
147
235
  console.log("");
148
236
  console.log("✅ Containers started. Environment ready.");
149
237
  console.log("");
238
+ await cleanupTunnels();
150
239
  process.exit(0);
151
240
  }
152
241
 
@@ -155,6 +244,7 @@ export async function runCli<
155
244
  await spawnWatchdog(env.projectName, env.root, {
156
245
  timeoutMinutes: watchdogTimeout,
157
246
  verbose: true,
247
+ composeFile: env.composeFile,
158
248
  });
159
249
  startHeartbeat(env.projectName);
160
250
  }
@@ -166,6 +256,7 @@ export async function runCli<
166
256
  console.log("✅ Containers ready. No apps configured.");
167
257
  // Keep process alive if no apps
168
258
  await new Promise(() => {});
259
+ await cleanupTunnels();
169
260
  return;
170
261
  }
171
262
 
@@ -177,10 +268,16 @@ export async function runCli<
177
268
  console.log("🔧 Starting dev servers...");
178
269
  console.log("");
179
270
 
180
- await runCommand(command, env.root, env.buildEnvVars());
271
+ await runCommand(command, env.root, env.buildEnvVars(), {
272
+ onSignal: async () => {
273
+ await cleanupTunnels();
274
+ stopHeartbeat();
275
+ },
276
+ });
181
277
 
182
278
  // Clean up heartbeat on exit
183
279
  stopHeartbeat();
280
+ await cleanupTunnels();
184
281
  }
185
282
 
186
283
  // ═══════════════════════════════════════════════════════════════════════════
@@ -228,7 +325,11 @@ function runCommand(
228
325
  command: string,
229
326
  cwd: string,
230
327
  envVars: Record<string, string>,
328
+ options: {
329
+ onSignal?: () => void | Promise<void>;
330
+ } = {},
231
331
  ): Promise<void> {
332
+ const { onSignal } = options;
232
333
  return new Promise((resolve, reject) => {
233
334
  const proc = spawn(command, [], {
234
335
  cwd,
@@ -249,6 +350,9 @@ function runCommand(
249
350
 
250
351
  // Handle SIGINT/SIGTERM
251
352
  const cleanup = () => {
353
+ if (onSignal) {
354
+ void onSignal();
355
+ }
252
356
  proc.kill("SIGTERM");
253
357
  };
254
358
 
@@ -262,10 +366,10 @@ function runCommand(
262
366
  // ═══════════════════════════════════════════════════════════════════════════
263
367
 
264
368
  /**
265
- * Check if a CLI flag is present.
369
+ * Check if a CLI flag is present (including `--flag=value` form).
266
370
  */
267
371
  export function hasFlag(args: string[], flag: string): boolean {
268
- return args.includes(flag);
372
+ return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
269
373
  }
270
374
 
271
375
  /**
@@ -0,0 +1,30 @@
1
+ import type {
2
+ AppConfig,
3
+ DevConfig,
4
+ DevHooks,
5
+ DevOptions,
6
+ DockerComposeGenerationOptions,
7
+ EnvVarsBuilder,
8
+ MigrationConfig,
9
+ PrismaConfig,
10
+ SeedConfig,
11
+ ServiceConfig,
12
+ } from "../types";
13
+
14
+ export function defineDevConfig<
15
+ TServices extends Record<string, ServiceConfig>,
16
+ TApps extends Record<string, AppConfig> = Record<string, never>,
17
+ >(config: {
18
+ projectPrefix: string;
19
+ services: TServices;
20
+ apps?: TApps;
21
+ envVars?: EnvVarsBuilder<TServices, TApps>;
22
+ hooks?: DevHooks<TServices, TApps>;
23
+ migrations?: MigrationConfig[];
24
+ seed?: SeedConfig<TServices, TApps>;
25
+ prisma?: PrismaConfig;
26
+ options?: DevOptions;
27
+ docker?: DockerComposeGenerationOptions;
28
+ }): DevConfig<TServices, TApps> {
29
+ return config as DevConfig<TServices, TApps>;
30
+ }
@@ -0,0 +1,3 @@
1
+ export { defineDevConfig } from "./define-config";
2
+ export { definePartialConfig, mergeConfigs } from "./merge-configs";
3
+ export { assertValidConfig, validateConfig } from "./validate-config";
@@ -0,0 +1,33 @@
1
+ import type { AppConfig, DevConfig, ServiceConfig } from "../types";
2
+
3
+ export function mergeConfigs<
4
+ TServices extends Record<string, ServiceConfig>,
5
+ TApps extends Record<string, AppConfig>,
6
+ >(
7
+ base: DevConfig<TServices, TApps>,
8
+ overrides: Partial<DevConfig<TServices, TApps>>,
9
+ ): DevConfig<TServices, TApps> {
10
+ return {
11
+ ...base,
12
+ ...overrides,
13
+ services: { ...base.services, ...overrides.services } as TServices,
14
+ apps: { ...base.apps, ...overrides.apps } as TApps,
15
+ hooks: { ...base.hooks, ...overrides.hooks },
16
+ migrations: overrides.migrations ?? base.migrations,
17
+ seed: overrides.seed ?? base.seed,
18
+ options: { ...base.options, ...overrides.options },
19
+ docker: { ...base.docker, ...overrides.docker },
20
+ };
21
+ }
22
+
23
+ export function definePartialConfig<
24
+ TServices extends Record<string, ServiceConfig> = Record<
25
+ string,
26
+ ServiceConfig
27
+ >,
28
+ TApps extends Record<string, AppConfig> = Record<string, AppConfig>,
29
+ >(
30
+ config: Partial<DevConfig<TServices, TApps>>,
31
+ ): Partial<DevConfig<TServices, TApps>> {
32
+ return config;
33
+ }
@@ -0,0 +1,136 @@
1
+ import { isAbsolute, normalize } from "node:path";
2
+ import type { AppConfig, DevConfig, ServiceConfig } from "../types";
3
+
4
+ const BUILTIN_DOCKER_PRESETS = new Set(["postgres", "redis", "clickhouse"]);
5
+
6
+ function inferBuiltInPreset(serviceName: string): string | null {
7
+ const normalized = serviceName.toLowerCase();
8
+ return BUILTIN_DOCKER_PRESETS.has(normalized) ? normalized : null;
9
+ }
10
+
11
+ export function validateConfig<
12
+ TServices extends Record<string, ServiceConfig>,
13
+ TApps extends Record<string, AppConfig>,
14
+ >(config: DevConfig<TServices, TApps>): string[] {
15
+ const errors: string[] = [];
16
+ const composeServiceNames = new Set<string>();
17
+
18
+ if (!config.projectPrefix) {
19
+ errors.push("projectPrefix is required");
20
+ } else if (!/^[a-z][a-z0-9-]*$/.test(config.projectPrefix)) {
21
+ errors.push(
22
+ "projectPrefix must start with a letter and contain only lowercase letters, numbers, and hyphens",
23
+ );
24
+ }
25
+
26
+ if (!config.services || Object.keys(config.services).length === 0) {
27
+ errors.push("At least one service is required");
28
+ }
29
+
30
+ for (const [name, service] of Object.entries(config.services ?? {})) {
31
+ if (!service.port || typeof service.port !== "number") {
32
+ errors.push(`Service "${name}" must have a valid port number`);
33
+ }
34
+ if (service.port < 1 || service.port > 65535) {
35
+ errors.push(`Service "${name}" port must be between 1 and 65535`);
36
+ }
37
+ if (
38
+ service.secondaryPort !== undefined &&
39
+ (service.secondaryPort < 1 || service.secondaryPort > 65535)
40
+ ) {
41
+ errors.push(
42
+ `Service "${name}" secondaryPort must be between 1 and 65535`,
43
+ );
44
+ }
45
+
46
+ const composeServiceName = service.serviceName ?? name;
47
+ if (composeServiceNames.has(composeServiceName)) {
48
+ errors.push(
49
+ `Duplicate compose service name "${composeServiceName}". Use unique serviceName values.`,
50
+ );
51
+ }
52
+ composeServiceNames.add(composeServiceName);
53
+
54
+ const dockerConfig = service.docker;
55
+ const preset = inferBuiltInPreset(name);
56
+ if (!dockerConfig && !preset) {
57
+ errors.push(
58
+ `Service "${name}" must define docker config (helper or raw) because it has no built-in preset.`,
59
+ );
60
+ }
61
+ if (
62
+ dockerConfig &&
63
+ typeof dockerConfig === "object" &&
64
+ "kind" in dockerConfig &&
65
+ dockerConfig.kind === "preset"
66
+ ) {
67
+ const presetName = dockerConfig.preset;
68
+ if (
69
+ typeof presetName !== "string" ||
70
+ !BUILTIN_DOCKER_PRESETS.has(presetName)
71
+ ) {
72
+ errors.push(
73
+ `Service "${name}" has invalid docker preset "${presetName}".`,
74
+ );
75
+ }
76
+ }
77
+ }
78
+
79
+ if (config.docker?.writeStrategy) {
80
+ const writeStrategy = config.docker.writeStrategy;
81
+ if (writeStrategy !== "always" && writeStrategy !== "if-missing") {
82
+ errors.push(
83
+ `docker.writeStrategy "${String(writeStrategy)}" is invalid. Use "always" or "if-missing".`,
84
+ );
85
+ }
86
+ }
87
+
88
+ if (config.docker?.generatedFile) {
89
+ const generatedFile = config.docker.generatedFile;
90
+ if (isAbsolute(generatedFile)) {
91
+ errors.push(
92
+ "docker.generatedFile must be a relative path inside the repo.",
93
+ );
94
+ }
95
+ const normalized = normalize(generatedFile).replace(/\\/g, "/");
96
+ if (normalized === ".." || normalized.startsWith("../")) {
97
+ errors.push(
98
+ "docker.generatedFile cannot point outside the repository root.",
99
+ );
100
+ }
101
+ }
102
+
103
+ for (const [name, app] of Object.entries(config.apps ?? {})) {
104
+ if (!app.port || typeof app.port !== "number") {
105
+ errors.push(`App "${name}" must have a valid port number`);
106
+ }
107
+ if (!app.devCommand) {
108
+ errors.push(`App "${name}" must have a devCommand`);
109
+ }
110
+ }
111
+
112
+ for (const migration of config.migrations ?? []) {
113
+ if (!migration.name) {
114
+ errors.push("Migration must have a name");
115
+ }
116
+ if (!migration.command) {
117
+ errors.push(`Migration "${migration.name}" must have a command`);
118
+ }
119
+ }
120
+
121
+ if (config.seed && !config.seed.command) {
122
+ errors.push("Seed must have a command");
123
+ }
124
+
125
+ return errors;
126
+ }
127
+
128
+ export function assertValidConfig<
129
+ TServices extends Record<string, ServiceConfig>,
130
+ TApps extends Record<string, AppConfig>,
131
+ >(config: DevConfig<TServices, TApps>): void {
132
+ const errors = validateConfig(config);
133
+ if (errors.length > 0) {
134
+ throw new Error(`Invalid dev config:\n - ${errors.join("\n - ")}`);
135
+ }
136
+ }
@@ -1,7 +1,7 @@
1
- // Re-export all core utilities
2
- export * from "./docker";
1
+ // Re-export core runtime utilities only.
3
2
  export * from "./network";
4
3
  export * from "./ports";
5
4
  export * from "./process";
5
+ export * from "./tunnel";
6
6
  export * from "./utils";
7
7
  export * from "./watchdog";
@@ -155,7 +155,8 @@ export function computeDevIdentity(options: DevIdentityOptions): DevIdentity {
155
155
  const worktree = isWorktree(root);
156
156
  const worktreeSuffix =
157
157
  worktree && worktreeIsolation ? getWorktreeProjectSuffix(root) : null;
158
- const projectSuffix = [suffix, worktreeSuffix].filter(Boolean).join("-") || undefined;
158
+ const projectSuffix =
159
+ [suffix, worktreeSuffix].filter(Boolean).join("-") || undefined;
159
160
  const projectName = getProjectName(projectPrefix, projectSuffix, root);
160
161
  const portOffset = calculatePortOffset(suffix, root);
161
162
 
@@ -249,7 +250,9 @@ function buildServiceUrl(
249
250
  case "mysql":
250
251
  return `mysql://${user}:${password}@${ctx.host}:${ctx.port}/${database}`;
251
252
  case "mongodb":
252
- return `mongodb://${ctx.host}:${ctx.port}/${database}`;
253
+ return database
254
+ ? `mongodb://${ctx.host}:${ctx.port}/${database}`
255
+ : `mongodb://${ctx.host}:${ctx.port}`;
253
256
  default:
254
257
  return null;
255
258
  }
@@ -395,11 +395,15 @@ export async function killProcessesOnAppPorts(
395
395
  killedAny = true;
396
396
  }
397
397
  if (verbose) {
398
- console.log(`⚠️ Port ${port} (${name}) is in use by process ${existingPid}`);
398
+ console.log(
399
+ `⚠️ Port ${port} (${name}) is in use by process ${existingPid}`,
400
+ );
399
401
  }
400
402
  const killed = await killProcessOnPortAndWait(port, { verbose });
401
403
  if (!killed && verbose) {
402
- console.log(` ⚠️ Could not kill process on port ${port}, server may fail to start`);
404
+ console.log(
405
+ ` ⚠️ Could not kill process on port ${port}, server may fail to start`,
406
+ );
403
407
  }
404
408
  }
405
409
  }
@@ -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
+ }