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.
- package/dist/bin.d.ts +1 -12
- package/dist/bin.js +261 -252
- package/dist/cli/bin.d.ts +13 -0
- package/dist/cli/bin.js +315 -0
- package/dist/cli/commands/help.d.ts +1 -0
- package/dist/cli/commands/runtime.d.ts +5 -0
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/run-cli.d.ts +22 -0
- package/dist/cli.d.ts +1 -22
- package/dist/cli.js +5 -13
- package/dist/config/config.d.ts +1 -0
- package/dist/config/define-config.d.ts +13 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +15 -0
- package/dist/config/merge-configs.d.ts +3 -0
- package/dist/config/validate-config.d.ts +3 -0
- package/dist/config.d.ts +1 -72
- package/dist/config.js +12 -12
- package/dist/core/docker.d.ts +1 -74
- package/dist/core/docker.js +35 -26
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +123 -108
- package/dist/core/network.js +2 -2
- package/dist/core/ports.d.ts +22 -0
- package/dist/core/ports.js +5 -1
- package/dist/core/process.js +1 -1
- package/dist/core/tunnel.d.ts +33 -0
- package/dist/core/utils.js +2 -2
- package/dist/core/watchdog-runner.js +45 -42
- package/dist/core/watchdog.d.ts +1 -0
- package/dist/core/watchdog.js +4 -2
- package/dist/docker/index.d.ts +1 -0
- package/dist/docker/index.js +38 -0
- package/dist/docker/runtime.d.ts +87 -0
- package/dist/docker/runtime.js +37 -0
- package/dist/docker-compose/compose.d.ts +1 -0
- package/dist/docker-compose/generated-file.d.ts +7 -0
- package/dist/docker-compose/index.d.ts +3 -0
- package/dist/docker-compose/index.js +15 -0
- package/dist/docker-compose/model.d.ts +6 -0
- package/dist/docker-compose/services/clickhouse.d.ts +16 -0
- package/dist/docker-compose/services/define-docker-service.d.ts +41 -0
- package/dist/docker-compose/services/index.d.ts +23 -0
- package/dist/docker-compose/services/index.js +17 -0
- package/dist/docker-compose/services/postgres.d.ts +12 -0
- package/dist/docker-compose/services/redis.d.ts +12 -0
- package/dist/docker-compose/services/shared.d.ts +7 -0
- package/dist/docker-compose/yaml.d.ts +2 -0
- package/dist/environment/create-dev-environment.d.ts +23 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +15 -0
- package/dist/environment/logging.d.ts +17 -0
- package/dist/environment/seeding.d.ts +9 -0
- package/dist/environment.d.ts +1 -23
- package/dist/environment.js +12 -14
- package/dist/index-045jksh5.js +147 -0
- package/dist/index-08wa79cs.js +125 -117
- package/dist/index-0kxnae3z.js +335 -0
- package/dist/index-1mdrf7nz.js +66 -0
- package/dist/index-1yvbwj4k.js +262 -242
- package/dist/index-23ev345g.js +475 -0
- package/dist/index-2ckr49sf.js +228 -0
- package/dist/index-2f47khe5.js +376 -369
- package/dist/index-2fr3g85b.js +220 -183
- package/dist/index-38xnzpa6.js +450 -0
- package/dist/index-3h3dhtf2.js +51 -43
- package/dist/index-42x95209.js +51 -43
- package/dist/index-4gp0az1g.js +145 -0
- package/dist/index-4xrxh8yv.js +72 -0
- package/dist/index-5gmws6ah.js +181 -0
- package/dist/index-5hka0tff.js +78 -76
- package/dist/index-5rfqps4b.js +3 -0
- package/dist/index-5t9jxqm0.js +428 -0
- package/dist/index-6c1w1xk5.js +101 -0
- package/dist/index-6fm7mvwj.js +118 -97
- package/dist/index-6srpc523.js +127 -128
- package/dist/index-731rzzfp.js +187 -0
- package/dist/index-75y4cg2z.js +51 -43
- package/dist/index-7ja4ywyj.js +126 -127
- package/dist/index-8bw1cmz4.js +531 -0
- package/dist/index-8hbbj1mp.js +120 -121
- package/dist/index-8xj2p5n5.js +145 -0
- package/dist/index-bj79tw5w.js +0 -0
- package/dist/index-bnk6nr0g.js +73 -0
- package/dist/index-brbbzyks.js +72 -0
- package/dist/index-c0dr6mcv.js +123 -0
- package/dist/index-cty0bcry.js +235 -218
- package/dist/index-d8tyv5se.js +228 -0
- package/dist/index-d9efy0n4.js +176 -150
- package/dist/index-etfmqjjf.js +427 -0
- package/dist/index-fb29934k.js +172 -0
- package/dist/index-g50jw1yf.js +72 -0
- package/dist/index-g6eb5wdw.js +118 -117
- package/dist/index-ggq3yryx.js +99 -95
- package/dist/index-h70tce00.js +177 -0
- package/dist/index-hkxtfqtc.js +333 -0
- package/dist/index-kf3dhser.js +146 -143
- package/dist/index-ma6tgdb2.js +500 -0
- package/dist/index-mam0bcyz.js +123 -0
- package/dist/index-mm412dkp.js +274 -0
- package/dist/index-n8v18aeb.js +0 -0
- package/dist/index-ndnmnsej.js +378 -371
- package/dist/index-p8wty0e2.js +389 -379
- package/dist/index-qfphr2fd.js +100 -0
- package/dist/index-qqmms8rs.js +51 -43
- package/dist/index-qw4093g2.js +51 -43
- package/dist/index-qzwpzjbx.js +121 -122
- package/dist/index-segbnm0h.js +146 -143
- package/dist/index-t0fj6gg1.js +112 -0
- package/dist/index-thdkwnv7.js +122 -0
- package/dist/index-tjbx2r2t.js +270 -0
- package/dist/index-tjqw9vtj.js +62 -54
- package/dist/index-vbpb89jy.js +248 -0
- package/dist/index-vhs88xhe.js +99 -95
- package/dist/index-w8zxnjka.js +249 -0
- package/dist/index-wk2na3t9.js +404 -0
- package/dist/index-wz9x8g7z.js +383 -373
- package/dist/index-x249gyde.js +388 -378
- package/dist/index-xkvd0nsd.js +187 -0
- package/dist/index-yedqxm1z.js +80 -0
- package/dist/index-zfjzzjkf.js +266 -0
- package/dist/index.d.ts +12 -8
- package/dist/index.js +66 -35
- package/dist/lint.d.ts +1 -46
- package/dist/lint.js +3 -7
- package/dist/loader/cache.d.ts +4 -0
- package/dist/loader/find-config-file.d.ts +2 -0
- package/dist/loader/index.d.ts +5 -0
- package/dist/loader/index.js +24 -0
- package/dist/loader/load-dev-env.d.ts +5 -0
- package/dist/loader/loader.d.ts +1 -0
- package/dist/loader.d.ts +1 -45
- package/dist/loader.js +22 -20
- package/dist/prisma/index.d.ts +1 -0
- package/dist/prisma/prisma.d.ts +29 -0
- package/dist/prisma.d.ts +1 -29
- package/dist/prisma.js +6 -10
- package/dist/src/bin.js +309 -0
- package/dist/src/cli.js +5 -0
- package/dist/src/config.js +15 -0
- package/dist/src/core/docker.js +38 -0
- package/dist/src/core/index.js +130 -0
- package/dist/src/core/network.js +9 -0
- package/dist/src/core/ports.js +23 -0
- package/dist/src/core/process.js +31 -0
- package/dist/src/core/utils.js +11 -0
- package/dist/src/core/watchdog-runner.js +69 -0
- package/dist/src/core/watchdog.js +28 -0
- package/dist/src/docker/runtime.js +37 -0
- package/dist/src/docker-compose/index.js +16 -0
- package/dist/src/docker-compose/services/index.js +17 -0
- package/dist/src/environment.js +12 -0
- package/dist/src/index.js +122 -0
- package/dist/src/lint.js +3 -0
- package/dist/src/loader.js +25 -0
- package/dist/src/prisma.js +6 -0
- package/dist/src/types.js +0 -0
- package/dist/typecheck/index.d.ts +1 -0
- package/dist/typecheck/index.js +7 -0
- package/dist/typecheck/typecheck.d.ts +46 -0
- package/dist/types/all-types.d.ts +501 -0
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config.d.ts +6 -0
- package/dist/types/docker.d.ts +15 -0
- package/dist/types/environment.d.ts +8 -0
- package/dist/types/hooks.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +0 -0
- package/dist/types/prisma.d.ts +1 -0
- package/dist/types.d.ts +1 -393
- package/package.json +145 -140
- package/readme.md +358 -105
- package/src/cli/bin.ts +77 -0
- package/src/cli/commands/help.ts +39 -0
- package/src/cli/commands/runtime.ts +72 -0
- package/src/cli/commands/version.ts +4 -0
- package/src/cli/index.ts +1 -0
- package/{cli.ts → src/cli/run-cli.ts} +95 -6
- package/src/config/define-config.ts +30 -0
- package/src/config/index.ts +3 -0
- package/src/config/merge-configs.ts +33 -0
- package/src/config/validate-config.ts +136 -0
- package/{core → src/core}/index.ts +2 -2
- package/{core → src/core}/ports.ts +68 -1
- package/{core → src/core}/process.ts +6 -2
- package/src/core/tunnel.ts +151 -0
- package/{core → src/core}/utils.ts +1 -0
- package/{core → src/core}/watchdog.ts +5 -1
- package/src/docker/index.ts +1 -0
- package/{core/docker.ts → src/docker/runtime.ts} +40 -4
- package/src/docker-compose/generated-file.ts +45 -0
- package/src/docker-compose/index.ts +7 -0
- package/src/docker-compose/model.ts +197 -0
- package/src/docker-compose/services/clickhouse.ts +79 -0
- package/src/docker-compose/services/define-docker-service.ts +109 -0
- package/src/docker-compose/services/index.ts +67 -0
- package/src/docker-compose/services/postgres.ts +60 -0
- package/src/docker-compose/services/redis.ts +48 -0
- package/src/docker-compose/services/shared.ts +79 -0
- package/src/docker-compose/yaml.ts +88 -0
- package/{environment.ts → src/environment/create-dev-environment.ts} +101 -146
- package/src/environment/index.ts +1 -0
- package/src/environment/logging.ts +101 -0
- package/src/environment/seeding.ts +57 -0
- package/{index.ts → src/index.ts} +49 -15
- package/src/loader/cache.ts +23 -0
- package/src/loader/find-config-file.ts +29 -0
- package/src/loader/index.ts +17 -0
- package/src/loader/load-dev-env.ts +38 -0
- package/src/prisma/index.ts +1 -0
- package/{prisma.ts → src/prisma/prisma.ts} +4 -2
- package/src/typecheck/index.ts +1 -0
- package/{types.ts → src/types/all-types.ts} +137 -6
- package/src/types/index.ts +1 -0
- package/bin.ts +0 -191
- package/config.ts +0 -194
- package/loader.ts +0 -126
- /package/{core → src/core}/network.ts +0 -0
- /package/{core → src/core}/watchdog-runner.ts +0 -0
- /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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./run-cli";
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { killProcessesOnAppPorts } from "
|
|
3
|
-
import {
|
|
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 "
|
|
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) =>
|
|
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(
|
|
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,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
|
|
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
|
|
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(
|
|
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(
|
|
404
|
+
console.log(
|
|
405
|
+
` ⚠️ Could not kill process on port ${port}, server may fail to start`,
|
|
406
|
+
);
|
|
403
407
|
}
|
|
404
408
|
}
|
|
405
409
|
}
|