buncargo 1.0.26 → 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 (222) hide show
  1. package/dist/bin.d.ts +1 -12
  2. package/dist/bin.js +261 -252
  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 -74
  22. package/dist/core/docker.js +35 -26
  23. package/dist/core/index.d.ts +1 -1
  24. package/dist/core/index.js +123 -108
  25. package/dist/core/network.js +2 -2
  26. package/dist/core/ports.d.ts +22 -0
  27. package/dist/core/ports.js +5 -1
  28. package/dist/core/process.js +1 -1
  29. package/dist/core/tunnel.d.ts +33 -0
  30. package/dist/core/utils.js +2 -2
  31. package/dist/core/watchdog-runner.js +45 -42
  32. package/dist/core/watchdog.d.ts +1 -0
  33. package/dist/core/watchdog.js +4 -2
  34. package/dist/docker/index.d.ts +1 -0
  35. package/dist/docker/index.js +38 -0
  36. package/dist/docker/runtime.d.ts +87 -0
  37. package/dist/docker/runtime.js +37 -0
  38. package/dist/docker-compose/compose.d.ts +1 -0
  39. package/dist/docker-compose/generated-file.d.ts +7 -0
  40. package/dist/docker-compose/index.d.ts +3 -0
  41. package/dist/docker-compose/index.js +15 -0
  42. package/dist/docker-compose/model.d.ts +6 -0
  43. package/dist/docker-compose/services/clickhouse.d.ts +16 -0
  44. package/dist/docker-compose/services/define-docker-service.d.ts +41 -0
  45. package/dist/docker-compose/services/index.d.ts +23 -0
  46. package/dist/docker-compose/services/index.js +17 -0
  47. package/dist/docker-compose/services/postgres.d.ts +12 -0
  48. package/dist/docker-compose/services/redis.d.ts +12 -0
  49. package/dist/docker-compose/services/shared.d.ts +7 -0
  50. package/dist/docker-compose/yaml.d.ts +2 -0
  51. package/dist/environment/create-dev-environment.d.ts +23 -0
  52. package/dist/environment/index.d.ts +1 -0
  53. package/dist/environment/index.js +15 -0
  54. package/dist/environment/logging.d.ts +17 -0
  55. package/dist/environment/seeding.d.ts +9 -0
  56. package/dist/environment.d.ts +1 -23
  57. package/dist/environment.js +12 -14
  58. package/dist/index-045jksh5.js +147 -0
  59. package/dist/index-08wa79cs.js +125 -117
  60. package/dist/index-0kxnae3z.js +335 -0
  61. package/dist/index-1mdrf7nz.js +66 -0
  62. package/dist/index-1yvbwj4k.js +262 -242
  63. package/dist/index-23ev345g.js +475 -0
  64. package/dist/index-2ckr49sf.js +228 -0
  65. package/dist/index-2f47khe5.js +376 -369
  66. package/dist/index-2fr3g85b.js +220 -183
  67. package/dist/index-38xnzpa6.js +450 -0
  68. package/dist/index-3h3dhtf2.js +51 -43
  69. package/dist/index-42x95209.js +51 -43
  70. package/dist/index-4gp0az1g.js +145 -0
  71. package/dist/index-4xrxh8yv.js +72 -0
  72. package/dist/index-5gmws6ah.js +181 -0
  73. package/dist/index-5hka0tff.js +78 -76
  74. package/dist/index-5rfqps4b.js +3 -0
  75. package/dist/index-5t9jxqm0.js +428 -0
  76. package/dist/index-6c1w1xk5.js +101 -0
  77. package/dist/index-6fm7mvwj.js +118 -97
  78. package/dist/index-6srpc523.js +127 -128
  79. package/dist/index-731rzzfp.js +187 -0
  80. package/dist/index-75y4cg2z.js +51 -43
  81. package/dist/index-7ja4ywyj.js +126 -127
  82. package/dist/index-8bw1cmz4.js +531 -0
  83. package/dist/index-8hbbj1mp.js +120 -121
  84. package/dist/index-8xj2p5n5.js +145 -0
  85. package/dist/index-bj79tw5w.js +0 -0
  86. package/dist/index-bnk6nr0g.js +73 -0
  87. package/dist/index-brbbzyks.js +72 -0
  88. package/dist/index-c0dr6mcv.js +123 -0
  89. package/dist/index-cty0bcry.js +235 -218
  90. package/dist/index-d8tyv5se.js +228 -0
  91. package/dist/index-d9efy0n4.js +176 -150
  92. package/dist/index-etfmqjjf.js +427 -0
  93. package/dist/index-fb29934k.js +172 -0
  94. package/dist/index-g50jw1yf.js +72 -0
  95. package/dist/index-g6eb5wdw.js +118 -117
  96. package/dist/index-ggq3yryx.js +99 -95
  97. package/dist/index-h70tce00.js +177 -0
  98. package/dist/index-hkxtfqtc.js +333 -0
  99. package/dist/index-kf3dhser.js +146 -143
  100. package/dist/index-ma6tgdb2.js +500 -0
  101. package/dist/index-mam0bcyz.js +123 -0
  102. package/dist/index-mm412dkp.js +274 -0
  103. package/dist/index-n8v18aeb.js +0 -0
  104. package/dist/index-ndnmnsej.js +378 -371
  105. package/dist/index-p8wty0e2.js +389 -379
  106. package/dist/index-qfphr2fd.js +100 -0
  107. package/dist/index-qqmms8rs.js +51 -43
  108. package/dist/index-qw4093g2.js +51 -43
  109. package/dist/index-qzwpzjbx.js +121 -122
  110. package/dist/index-segbnm0h.js +146 -143
  111. package/dist/index-t0fj6gg1.js +112 -0
  112. package/dist/index-thdkwnv7.js +122 -0
  113. package/dist/index-tjbx2r2t.js +270 -0
  114. package/dist/index-tjqw9vtj.js +62 -54
  115. package/dist/index-vbpb89jy.js +248 -0
  116. package/dist/index-vhs88xhe.js +99 -95
  117. package/dist/index-w8zxnjka.js +249 -0
  118. package/dist/index-wk2na3t9.js +404 -0
  119. package/dist/index-wz9x8g7z.js +383 -373
  120. package/dist/index-x249gyde.js +388 -378
  121. package/dist/index-xkvd0nsd.js +187 -0
  122. package/dist/index-yedqxm1z.js +80 -0
  123. package/dist/index-zfjzzjkf.js +266 -0
  124. package/dist/index.d.ts +12 -8
  125. package/dist/index.js +66 -35
  126. package/dist/lint.d.ts +1 -46
  127. package/dist/lint.js +3 -7
  128. package/dist/loader/cache.d.ts +4 -0
  129. package/dist/loader/find-config-file.d.ts +2 -0
  130. package/dist/loader/index.d.ts +5 -0
  131. package/dist/loader/index.js +24 -0
  132. package/dist/loader/load-dev-env.d.ts +5 -0
  133. package/dist/loader/loader.d.ts +1 -0
  134. package/dist/loader.d.ts +1 -45
  135. package/dist/loader.js +22 -20
  136. package/dist/prisma/index.d.ts +1 -0
  137. package/dist/prisma/prisma.d.ts +29 -0
  138. package/dist/prisma.d.ts +1 -29
  139. package/dist/prisma.js +6 -10
  140. package/dist/src/bin.js +309 -0
  141. package/dist/src/cli.js +5 -0
  142. package/dist/src/config.js +15 -0
  143. package/dist/src/core/docker.js +38 -0
  144. package/dist/src/core/index.js +130 -0
  145. package/dist/src/core/network.js +9 -0
  146. package/dist/src/core/ports.js +23 -0
  147. package/dist/src/core/process.js +31 -0
  148. package/dist/src/core/utils.js +11 -0
  149. package/dist/src/core/watchdog-runner.js +69 -0
  150. package/dist/src/core/watchdog.js +28 -0
  151. package/dist/src/docker/runtime.js +37 -0
  152. package/dist/src/docker-compose/index.js +16 -0
  153. package/dist/src/docker-compose/services/index.js +17 -0
  154. package/dist/src/environment.js +12 -0
  155. package/dist/src/index.js +122 -0
  156. package/dist/src/lint.js +3 -0
  157. package/dist/src/loader.js +25 -0
  158. package/dist/src/prisma.js +6 -0
  159. package/dist/src/types.js +0 -0
  160. package/dist/typecheck/index.d.ts +1 -0
  161. package/dist/typecheck/index.js +7 -0
  162. package/dist/typecheck/typecheck.d.ts +46 -0
  163. package/dist/types/all-types.d.ts +501 -0
  164. package/dist/types/cli.d.ts +1 -0
  165. package/dist/types/config.d.ts +6 -0
  166. package/dist/types/docker.d.ts +15 -0
  167. package/dist/types/environment.d.ts +8 -0
  168. package/dist/types/hooks.d.ts +9 -0
  169. package/dist/types/index.d.ts +1 -0
  170. package/dist/types/index.js +0 -0
  171. package/dist/types/prisma.d.ts +1 -0
  172. package/dist/types.d.ts +1 -393
  173. package/package.json +145 -140
  174. package/readme.md +358 -105
  175. package/src/cli/bin.ts +77 -0
  176. package/src/cli/commands/help.ts +39 -0
  177. package/src/cli/commands/runtime.ts +72 -0
  178. package/src/cli/commands/version.ts +4 -0
  179. package/src/cli/index.ts +1 -0
  180. package/{cli.ts → src/cli/run-cli.ts} +95 -6
  181. package/src/config/define-config.ts +30 -0
  182. package/src/config/index.ts +3 -0
  183. package/src/config/merge-configs.ts +33 -0
  184. package/src/config/validate-config.ts +136 -0
  185. package/{core → src/core}/index.ts +2 -2
  186. package/{core → src/core}/ports.ts +68 -1
  187. package/{core → src/core}/process.ts +6 -2
  188. package/src/core/tunnel.ts +151 -0
  189. package/{core → src/core}/utils.ts +1 -0
  190. package/{core → src/core}/watchdog.ts +5 -1
  191. package/src/docker/index.ts +1 -0
  192. package/{core/docker.ts → src/docker/runtime.ts} +40 -4
  193. package/src/docker-compose/generated-file.ts +45 -0
  194. package/src/docker-compose/index.ts +7 -0
  195. package/src/docker-compose/model.ts +197 -0
  196. package/src/docker-compose/services/clickhouse.ts +79 -0
  197. package/src/docker-compose/services/define-docker-service.ts +109 -0
  198. package/src/docker-compose/services/index.ts +67 -0
  199. package/src/docker-compose/services/postgres.ts +60 -0
  200. package/src/docker-compose/services/redis.ts +48 -0
  201. package/src/docker-compose/services/shared.ts +79 -0
  202. package/src/docker-compose/yaml.ts +88 -0
  203. package/{environment.ts → src/environment/create-dev-environment.ts} +101 -146
  204. package/src/environment/index.ts +1 -0
  205. package/src/environment/logging.ts +101 -0
  206. package/src/environment/seeding.ts +57 -0
  207. package/{index.ts → src/index.ts} +49 -15
  208. package/src/loader/cache.ts +23 -0
  209. package/src/loader/find-config-file.ts +29 -0
  210. package/src/loader/index.ts +17 -0
  211. package/src/loader/load-dev-env.ts +38 -0
  212. package/src/prisma/index.ts +1 -0
  213. package/{prisma.ts → src/prisma/prisma.ts} +4 -2
  214. package/src/typecheck/index.ts +1 -0
  215. package/{types.ts → src/types/all-types.ts} +137 -6
  216. package/src/types/index.ts +1 -0
  217. package/bin.ts +0 -191
  218. package/config.ts +0 -194
  219. package/loader.ts +0 -126
  220. /package/{core → src/core}/network.ts +0 -0
  221. /package/{core → src/core}/watchdog-runner.ts +0 -0
  222. /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";
@@ -57,6 +57,28 @@ export function isWorktree(root?: string): boolean {
57
57
  return getWorktreeName(root) !== null;
58
58
  }
59
59
 
60
+ /**
61
+ * Sanitize a string for use as a Docker Compose project suffix.
62
+ */
63
+ function sanitizeProjectSuffix(value: string): string {
64
+ return value
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9-]/g, "-")
67
+ .replace(/-+/g, "-")
68
+ .replace(/^-+|-+$/g, "");
69
+ }
70
+
71
+ /**
72
+ * Get a sanitized worktree-derived suffix for Docker project isolation.
73
+ * Returns null when not running in a worktree.
74
+ */
75
+ export function getWorktreeProjectSuffix(root?: string): string | null {
76
+ const worktreeName = getWorktreeName(root);
77
+ if (!worktreeName) return null;
78
+ const sanitized = sanitizeProjectSuffix(worktreeName);
79
+ return sanitized || "worktree";
80
+ }
81
+
60
82
  // ═══════════════════════════════════════════════════════════════════════════
61
83
  // Port Offset Calculation
62
84
  // ═══════════════════════════════════════════════════════════════════════════
@@ -104,6 +126,49 @@ export function getProjectName(
104
126
  return suffix ? `${baseName}-${suffix}` : baseName;
105
127
  }
106
128
 
129
+ export interface DevIdentityOptions {
130
+ projectPrefix: string;
131
+ suffix?: string;
132
+ root?: string;
133
+ worktreeIsolation?: boolean;
134
+ }
135
+
136
+ export interface DevIdentity {
137
+ worktree: boolean;
138
+ worktreeSuffix: string | null;
139
+ projectSuffix?: string;
140
+ projectName: string;
141
+ portOffset: number;
142
+ }
143
+
144
+ /**
145
+ * Compute all identity values used by the dev environment in one place.
146
+ */
147
+ export function computeDevIdentity(options: DevIdentityOptions): DevIdentity {
148
+ const {
149
+ projectPrefix,
150
+ suffix,
151
+ root: providedRoot,
152
+ worktreeIsolation = true,
153
+ } = options;
154
+ const root = providedRoot ?? findMonorepoRoot();
155
+ const worktree = isWorktree(root);
156
+ const worktreeSuffix =
157
+ worktree && worktreeIsolation ? getWorktreeProjectSuffix(root) : null;
158
+ const projectSuffix =
159
+ [suffix, worktreeSuffix].filter(Boolean).join("-") || undefined;
160
+ const projectName = getProjectName(projectPrefix, projectSuffix, root);
161
+ const portOffset = calculatePortOffset(suffix, root);
162
+
163
+ return {
164
+ worktree,
165
+ worktreeSuffix,
166
+ projectSuffix,
167
+ projectName,
168
+ portOffset,
169
+ };
170
+ }
171
+
107
172
  // ═══════════════════════════════════════════════════════════════════════════
108
173
  // Port Computation
109
174
  // ═══════════════════════════════════════════════════════════════════════════
@@ -185,7 +250,9 @@ function buildServiceUrl(
185
250
  case "mysql":
186
251
  return `mysql://${user}:${password}@${ctx.host}:${ctx.port}/${database}`;
187
252
  case "mongodb":
188
- return `mongodb://${ctx.host}:${ctx.port}/${database}`;
253
+ return database
254
+ ? `mongodb://${ctx.host}:${ctx.port}/${database}`
255
+ : `mongodb://${ctx.host}:${ctx.port}`;
189
256
  default:
190
257
  return null;
191
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
  }