buncargo 1.0.29 → 3.0.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 (221) 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 +315 -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 +22 -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/tunnel.d.ts +33 -0
  29. package/dist/core/utils.js +2 -2
  30. package/dist/core/watchdog-runner.js +45 -42
  31. package/dist/core/watchdog.d.ts +1 -0
  32. package/dist/core/watchdog.js +4 -2
  33. package/dist/docker/index.d.ts +1 -0
  34. package/dist/docker/index.js +38 -0
  35. package/dist/docker/runtime.d.ts +87 -0
  36. package/dist/docker/runtime.js +37 -0
  37. package/dist/docker-compose/compose.d.ts +1 -0
  38. package/dist/docker-compose/generated-file.d.ts +7 -0
  39. package/dist/docker-compose/index.d.ts +3 -0
  40. package/dist/docker-compose/index.js +15 -0
  41. package/dist/docker-compose/model.d.ts +6 -0
  42. package/dist/docker-compose/services/clickhouse.d.ts +16 -0
  43. package/dist/docker-compose/services/define-docker-service.d.ts +41 -0
  44. package/dist/docker-compose/services/index.d.ts +23 -0
  45. package/dist/docker-compose/services/index.js +17 -0
  46. package/dist/docker-compose/services/postgres.d.ts +12 -0
  47. package/dist/docker-compose/services/redis.d.ts +12 -0
  48. package/dist/docker-compose/services/shared.d.ts +7 -0
  49. package/dist/docker-compose/yaml.d.ts +2 -0
  50. package/dist/environment/create-dev-environment.d.ts +23 -0
  51. package/dist/environment/index.d.ts +1 -0
  52. package/dist/environment/index.js +15 -0
  53. package/dist/environment/logging.d.ts +17 -0
  54. package/dist/environment/seeding.d.ts +9 -0
  55. package/dist/environment.d.ts +1 -23
  56. package/dist/environment.js +12 -14
  57. package/dist/index-045jksh5.js +147 -0
  58. package/dist/index-08wa79cs.js +125 -117
  59. package/dist/index-0kxnae3z.js +335 -0
  60. package/dist/index-1mdrf7nz.js +51 -43
  61. package/dist/index-1yvbwj4k.js +262 -242
  62. package/dist/index-23ev345g.js +475 -0
  63. package/dist/index-2ckr49sf.js +228 -0
  64. package/dist/index-2f47khe5.js +376 -369
  65. package/dist/index-2fr3g85b.js +220 -183
  66. package/dist/index-38xnzpa6.js +450 -0
  67. package/dist/index-3h3dhtf2.js +51 -43
  68. package/dist/index-42x95209.js +51 -43
  69. package/dist/index-4gp0az1g.js +145 -0
  70. package/dist/index-4xrxh8yv.js +72 -0
  71. package/dist/index-5gmws6ah.js +181 -0
  72. package/dist/index-5hka0tff.js +78 -76
  73. package/dist/index-5rfqps4b.js +3 -0
  74. package/dist/index-5t9jxqm0.js +428 -0
  75. package/dist/index-6c1w1xk5.js +101 -0
  76. package/dist/index-6fm7mvwj.js +118 -97
  77. package/dist/index-6srpc523.js +127 -128
  78. package/dist/index-731rzzfp.js +157 -142
  79. package/dist/index-75y4cg2z.js +51 -43
  80. package/dist/index-7ja4ywyj.js +126 -127
  81. package/dist/index-8bw1cmz4.js +531 -0
  82. package/dist/index-8hbbj1mp.js +120 -121
  83. package/dist/index-8xj2p5n5.js +118 -97
  84. package/dist/index-bj79tw5w.js +0 -0
  85. package/dist/index-bnk6nr0g.js +73 -0
  86. package/dist/index-brbbzyks.js +72 -0
  87. package/dist/index-c0dr6mcv.js +123 -0
  88. package/dist/index-cty0bcry.js +235 -218
  89. package/dist/index-d8tyv5se.js +228 -0
  90. package/dist/index-d9efy0n4.js +176 -150
  91. package/dist/index-etfmqjjf.js +427 -0
  92. package/dist/index-fb29934k.js +172 -0
  93. package/dist/index-g50jw1yf.js +72 -0
  94. package/dist/index-g6eb5wdw.js +118 -117
  95. package/dist/index-ggq3yryx.js +99 -95
  96. package/dist/index-h70tce00.js +177 -0
  97. package/dist/index-hkxtfqtc.js +333 -0
  98. package/dist/index-kf3dhser.js +146 -143
  99. package/dist/index-ma6tgdb2.js +500 -0
  100. package/dist/index-mam0bcyz.js +123 -0
  101. package/dist/index-mm412dkp.js +274 -0
  102. package/dist/index-n8v18aeb.js +0 -0
  103. package/dist/index-ndnmnsej.js +378 -371
  104. package/dist/index-p8wty0e2.js +389 -379
  105. package/dist/index-qfphr2fd.js +78 -76
  106. package/dist/index-qqmms8rs.js +51 -43
  107. package/dist/index-qw4093g2.js +51 -43
  108. package/dist/index-qzwpzjbx.js +121 -122
  109. package/dist/index-segbnm0h.js +146 -143
  110. package/dist/index-t0fj6gg1.js +112 -0
  111. package/dist/index-thdkwnv7.js +122 -0
  112. package/dist/index-tjbx2r2t.js +270 -0
  113. package/dist/index-tjqw9vtj.js +62 -54
  114. package/dist/index-vbpb89jy.js +248 -0
  115. package/dist/index-vhs88xhe.js +99 -95
  116. package/dist/index-w8zxnjka.js +249 -0
  117. package/dist/index-wk2na3t9.js +385 -375
  118. package/dist/index-wz9x8g7z.js +383 -373
  119. package/dist/index-x249gyde.js +388 -378
  120. package/dist/index-xkvd0nsd.js +187 -0
  121. package/dist/index-yedqxm1z.js +80 -0
  122. package/dist/index-zfjzzjkf.js +240 -199
  123. package/dist/index.d.ts +12 -8
  124. package/dist/index.js +56 -35
  125. package/dist/lint.d.ts +1 -46
  126. package/dist/lint.js +3 -7
  127. package/dist/loader/cache.d.ts +4 -0
  128. package/dist/loader/find-config-file.d.ts +2 -0
  129. package/dist/loader/index.d.ts +5 -0
  130. package/dist/loader/index.js +24 -0
  131. package/dist/loader/load-dev-env.d.ts +5 -0
  132. package/dist/loader/loader.d.ts +1 -0
  133. package/dist/loader.d.ts +1 -45
  134. package/dist/loader.js +22 -20
  135. package/dist/prisma/index.d.ts +1 -0
  136. package/dist/prisma/prisma.d.ts +29 -0
  137. package/dist/prisma.d.ts +1 -29
  138. package/dist/prisma.js +6 -10
  139. package/dist/src/bin.js +309 -0
  140. package/dist/src/cli.js +5 -0
  141. package/dist/src/config.js +15 -0
  142. package/dist/src/core/docker.js +38 -0
  143. package/dist/src/core/index.js +130 -0
  144. package/dist/src/core/network.js +9 -0
  145. package/dist/src/core/ports.js +23 -0
  146. package/dist/src/core/process.js +31 -0
  147. package/dist/src/core/utils.js +11 -0
  148. package/dist/src/core/watchdog-runner.js +69 -0
  149. package/dist/src/core/watchdog.js +28 -0
  150. package/dist/src/docker/runtime.js +37 -0
  151. package/dist/src/docker-compose/index.js +16 -0
  152. package/dist/src/docker-compose/services/index.js +17 -0
  153. package/dist/src/environment.js +12 -0
  154. package/dist/src/index.js +122 -0
  155. package/dist/src/lint.js +3 -0
  156. package/dist/src/loader.js +25 -0
  157. package/dist/src/prisma.js +6 -0
  158. package/dist/src/types.js +0 -0
  159. package/dist/typecheck/index.d.ts +1 -0
  160. package/dist/typecheck/index.js +7 -0
  161. package/dist/typecheck/typecheck.d.ts +46 -0
  162. package/dist/types/all-types.d.ts +501 -0
  163. package/dist/types/cli.d.ts +1 -0
  164. package/dist/types/config.d.ts +6 -0
  165. package/dist/types/docker.d.ts +15 -0
  166. package/dist/types/environment.d.ts +8 -0
  167. package/dist/types/hooks.d.ts +9 -0
  168. package/dist/types/index.d.ts +1 -0
  169. package/dist/types/index.js +0 -0
  170. package/dist/types/prisma.d.ts +1 -0
  171. package/dist/types.d.ts +1 -399
  172. package/package.json +145 -140
  173. package/readme.md +349 -109
  174. package/src/cli/bin.ts +77 -0
  175. package/src/cli/commands/help.ts +39 -0
  176. package/src/cli/commands/runtime.ts +72 -0
  177. package/src/cli/commands/version.ts +4 -0
  178. package/src/cli/index.ts +1 -0
  179. package/{cli.ts → src/cli/run-cli.ts} +95 -6
  180. package/src/config/define-config.ts +30 -0
  181. package/src/config/index.ts +3 -0
  182. package/src/config/merge-configs.ts +33 -0
  183. package/src/config/validate-config.ts +136 -0
  184. package/{core → src/core}/index.ts +2 -2
  185. package/{core → src/core}/ports.ts +5 -2
  186. package/{core → src/core}/process.ts +6 -2
  187. package/src/core/tunnel.ts +151 -0
  188. package/{core → src/core}/utils.ts +1 -0
  189. package/{core → src/core}/watchdog.ts +5 -1
  190. package/src/docker/index.ts +1 -0
  191. package/{core/docker.ts → src/docker/runtime.ts} +11 -4
  192. package/src/docker-compose/generated-file.ts +45 -0
  193. package/src/docker-compose/index.ts +7 -0
  194. package/src/docker-compose/model.ts +197 -0
  195. package/src/docker-compose/services/clickhouse.ts +79 -0
  196. package/src/docker-compose/services/define-docker-service.ts +109 -0
  197. package/src/docker-compose/services/index.ts +67 -0
  198. package/src/docker-compose/services/postgres.ts +60 -0
  199. package/src/docker-compose/services/redis.ts +48 -0
  200. package/src/docker-compose/services/shared.ts +79 -0
  201. package/src/docker-compose/yaml.ts +88 -0
  202. package/{environment.ts → src/environment/create-dev-environment.ts} +93 -130
  203. package/src/environment/index.ts +1 -0
  204. package/src/environment/logging.ts +101 -0
  205. package/src/environment/seeding.ts +57 -0
  206. package/{index.ts → src/index.ts} +49 -20
  207. package/src/loader/cache.ts +23 -0
  208. package/src/loader/find-config-file.ts +29 -0
  209. package/src/loader/index.ts +17 -0
  210. package/src/loader/load-dev-env.ts +38 -0
  211. package/src/prisma/index.ts +1 -0
  212. package/{prisma.ts → src/prisma/prisma.ts} +4 -2
  213. package/src/typecheck/index.ts +1 -0
  214. package/{types.ts → src/types/all-types.ts} +130 -5
  215. package/src/types/index.ts +1 -0
  216. package/bin.ts +0 -192
  217. package/config.ts +0 -194
  218. package/loader.ts +0 -126
  219. /package/{core → src/core}/network.ts +0 -0
  220. /package/{core → src/core}/watchdog-runner.ts +0 -0
  221. /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,19 @@
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";
10
+ import { logPublicUrls } from "../environment/logging";
4
11
  import type {
5
12
  AppConfig,
6
13
  CliOptions,
7
14
  DevEnvironment,
8
15
  ServiceConfig,
9
- } from "./types";
16
+ } from "../types";
10
17
 
11
18
  // ═══════════════════════════════════════════════════════════════════════════
12
19
  // CLI Runner
@@ -20,6 +27,7 @@ const ACCEPTED_FLAGS = [
20
27
  "--migrate",
21
28
  "--seed",
22
29
  "--up-only",
30
+ "--expose",
23
31
  ] as const;
24
32
 
25
33
  /**
@@ -36,12 +44,15 @@ Options:
36
44
  --migrate Run migrations and exit
37
45
  --seed Run migrations and seeders, then exit
38
46
  --up-only Start containers and run migrations, then exit (no dev servers)
47
+ --expose Expose configured targets via public quick tunnels
39
48
 
40
49
  Examples:
41
50
  bun dev Start dev environment with all services
42
51
  bun dev --seed Run migrations and seed the database
43
52
  bun dev --down Stop all containers
44
53
  bun dev --reset Stop containers and remove all data
54
+ bun dev --expose Expose all targets with expose: true
55
+ bun dev --expose=api,web Expose specific targets
45
56
  `);
46
57
  }
47
58
 
@@ -50,7 +61,13 @@ Examples:
50
61
  */
51
62
  function getUnknownFlags(args: string[]): string[] {
52
63
  return args.filter(
53
- (arg) => arg.startsWith("--") && !ACCEPTED_FLAGS.includes(arg as typeof ACCEPTED_FLAGS[number]),
64
+ (arg) =>
65
+ arg.startsWith("--") &&
66
+ !ACCEPTED_FLAGS.includes(
67
+ (arg.includes("=")
68
+ ? arg.split("=")[0]
69
+ : arg) as (typeof ACCEPTED_FLAGS)[number],
70
+ ),
54
71
  );
55
72
  }
56
73
 
@@ -79,6 +96,16 @@ export async function runCli<
79
96
  watchdogTimeout = 10,
80
97
  devServersCommand,
81
98
  } = options;
99
+ const exposeRequested = hasFlag(args, "--expose");
100
+ const exposeValue = getFlagValue(args, "--expose");
101
+ let tunnels: PublicTunnel[] = [];
102
+
103
+ async function cleanupTunnels(): Promise<void> {
104
+ env.clearPublicUrls();
105
+ if (tunnels.length === 0) return;
106
+ await stopPublicTunnels(tunnels);
107
+ tunnels = [];
108
+ }
82
109
 
83
110
  // Handle --help
84
111
  if (args.includes("--help")) {
@@ -89,7 +116,9 @@ export async function runCli<
89
116
  // Validate flags
90
117
  const unknownFlags = getUnknownFlags(args);
91
118
  if (unknownFlags.length > 0) {
92
- console.error(`❌ Unknown flag${unknownFlags.length > 1 ? "s" : ""}: ${unknownFlags.join(", ")}`);
119
+ console.error(
120
+ `❌ Unknown flag${unknownFlags.length > 1 ? "s" : ""}: ${unknownFlags.join(", ")}`,
121
+ );
93
122
  console.error("");
94
123
  printHelp();
95
124
  process.exit(1);
@@ -98,6 +127,7 @@ export async function runCli<
98
127
  // Handle --down (no need to start anything)
99
128
  if (args.includes("--down")) {
100
129
  env.logInfo();
130
+ await cleanupTunnels();
101
131
  await env.stop();
102
132
  process.exit(0);
103
133
  }
@@ -105,6 +135,7 @@ export async function runCli<
105
135
  // Handle --reset (no need to start anything)
106
136
  if (args.includes("--reset")) {
107
137
  env.logInfo();
138
+ await cleanupTunnels();
108
139
  await env.stop({ removeVolumes: true });
109
140
  process.exit(0);
110
141
  }
@@ -114,10 +145,50 @@ export async function runCli<
114
145
  const skipSeed = args.includes("--seed");
115
146
  await env.start({ startServers: false, wait: true, skipSeed });
116
147
 
148
+ if (exposeRequested) {
149
+ const { targets, unknownNames, notEnabledNames } = resolveExposeTargets(
150
+ env,
151
+ exposeValue,
152
+ );
153
+ if (unknownNames.length > 0) {
154
+ console.error(
155
+ `❌ Unknown expose target${unknownNames.length > 1 ? "s" : ""}: ${unknownNames.join(", ")}`,
156
+ );
157
+ await cleanupTunnels();
158
+ process.exit(1);
159
+ }
160
+ if (notEnabledNames.length > 0) {
161
+ console.error(
162
+ `❌ Target${notEnabledNames.length > 1 ? "s" : ""} missing expose: true: ${notEnabledNames.join(", ")}`,
163
+ );
164
+ console.error(
165
+ " Mark these in dev.config.ts with expose: true or remove them from --expose.",
166
+ );
167
+ await cleanupTunnels();
168
+ process.exit(1);
169
+ }
170
+ if (targets.length === 0) {
171
+ console.error(
172
+ "❌ No expose targets selected. Add expose: true to services/apps or pass names with --expose=<name>.",
173
+ );
174
+ await cleanupTunnels();
175
+ process.exit(1);
176
+ }
177
+
178
+ tunnels = await startPublicTunnels(targets);
179
+ env.setPublicUrls(
180
+ Object.fromEntries(
181
+ tunnels.map((tunnel) => [tunnel.name, tunnel.publicUrl]),
182
+ ) as typeof env.publicUrls,
183
+ );
184
+ logPublicUrls(tunnels);
185
+ }
186
+
117
187
  // Handle --migrate (exit after migrations)
118
188
  if (args.includes("--migrate")) {
119
189
  console.log("");
120
190
  console.log("✅ Migrations applied successfully");
191
+ await cleanupTunnels();
121
192
  process.exit(0);
122
193
  }
123
194
 
@@ -135,10 +206,12 @@ export async function runCli<
135
206
  if (result.stdout) {
136
207
  console.error(result.stdout);
137
208
  }
209
+ await cleanupTunnels();
138
210
  process.exit(1);
139
211
  }
140
212
  console.log("");
141
213
  console.log("✅ Seeding complete");
214
+ await cleanupTunnels();
142
215
  process.exit(0);
143
216
  }
144
217
 
@@ -147,6 +220,7 @@ export async function runCli<
147
220
  console.log("");
148
221
  console.log("✅ Containers started. Environment ready.");
149
222
  console.log("");
223
+ await cleanupTunnels();
150
224
  process.exit(0);
151
225
  }
152
226
 
@@ -155,6 +229,7 @@ export async function runCli<
155
229
  await spawnWatchdog(env.projectName, env.root, {
156
230
  timeoutMinutes: watchdogTimeout,
157
231
  verbose: true,
232
+ composeFile: env.composeFile,
158
233
  });
159
234
  startHeartbeat(env.projectName);
160
235
  }
@@ -166,6 +241,7 @@ export async function runCli<
166
241
  console.log("✅ Containers ready. No apps configured.");
167
242
  // Keep process alive if no apps
168
243
  await new Promise(() => {});
244
+ await cleanupTunnels();
169
245
  return;
170
246
  }
171
247
 
@@ -177,10 +253,16 @@ export async function runCli<
177
253
  console.log("🔧 Starting dev servers...");
178
254
  console.log("");
179
255
 
180
- await runCommand(command, env.root, env.buildEnvVars());
256
+ await runCommand(command, env.root, env.buildEnvVars(), {
257
+ onSignal: async () => {
258
+ await cleanupTunnels();
259
+ stopHeartbeat();
260
+ },
261
+ });
181
262
 
182
263
  // Clean up heartbeat on exit
183
264
  stopHeartbeat();
265
+ await cleanupTunnels();
184
266
  }
185
267
 
186
268
  // ═══════════════════════════════════════════════════════════════════════════
@@ -228,7 +310,11 @@ function runCommand(
228
310
  command: string,
229
311
  cwd: string,
230
312
  envVars: Record<string, string>,
313
+ options: {
314
+ onSignal?: () => void | Promise<void>;
315
+ } = {},
231
316
  ): Promise<void> {
317
+ const { onSignal } = options;
232
318
  return new Promise((resolve, reject) => {
233
319
  const proc = spawn(command, [], {
234
320
  cwd,
@@ -249,6 +335,9 @@ function runCommand(
249
335
 
250
336
  // Handle SIGINT/SIGTERM
251
337
  const cleanup = () => {
338
+ if (onSignal) {
339
+ void onSignal();
340
+ }
252
341
  proc.kill("SIGTERM");
253
342
  };
254
343
 
@@ -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
  }