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,151 @@
|
|
|
1
|
+
import { startTunnel } from "untun";
|
|
2
|
+
import type { AppConfig, DevEnvironment, ServiceConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface PublicExposeTarget {
|
|
5
|
+
kind: "service" | "app";
|
|
6
|
+
name: string;
|
|
7
|
+
port: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PublicTunnel {
|
|
11
|
+
kind: "service" | "app";
|
|
12
|
+
name: string;
|
|
13
|
+
localUrl: string;
|
|
14
|
+
publicUrl: string;
|
|
15
|
+
close: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UntunTunnelLike {
|
|
19
|
+
url?: string;
|
|
20
|
+
publicUrl?: string;
|
|
21
|
+
tunnelUrl?: string;
|
|
22
|
+
close?: () => void | Promise<void>;
|
|
23
|
+
stop?: () => void | Promise<void>;
|
|
24
|
+
destroy?: () => void | Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseExposeNames(exposeValue?: string): Set<string> | null {
|
|
28
|
+
if (exposeValue === undefined) return null;
|
|
29
|
+
const names = exposeValue
|
|
30
|
+
.split(",")
|
|
31
|
+
.map((name) => name.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
return new Set(names);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function asPublicUrl(tunnel: UntunTunnelLike): string | null {
|
|
37
|
+
return tunnel.url ?? tunnel.publicUrl ?? tunnel.tunnelUrl ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toCloseFn(tunnel: UntunTunnelLike): () => Promise<void> {
|
|
41
|
+
const close = tunnel.close ?? tunnel.stop ?? tunnel.destroy;
|
|
42
|
+
if (!close) return async () => {};
|
|
43
|
+
return async () => {
|
|
44
|
+
await close();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveExposeTargets<
|
|
49
|
+
TServices extends Record<string, ServiceConfig>,
|
|
50
|
+
TApps extends Record<string, AppConfig>,
|
|
51
|
+
>(
|
|
52
|
+
env: DevEnvironment<TServices, TApps>,
|
|
53
|
+
exposeValue?: string,
|
|
54
|
+
): {
|
|
55
|
+
targets: PublicExposeTarget[];
|
|
56
|
+
unknownNames: string[];
|
|
57
|
+
notEnabledNames: string[];
|
|
58
|
+
} {
|
|
59
|
+
const requestedNames = parseExposeNames(exposeValue);
|
|
60
|
+
const knownTargets = new Map<string, PublicExposeTarget>();
|
|
61
|
+
const enabledTargets = new Map<string, PublicExposeTarget>();
|
|
62
|
+
|
|
63
|
+
for (const [name, config] of Object.entries(env.services)) {
|
|
64
|
+
const port = env.ports[name];
|
|
65
|
+
if (port === undefined) continue;
|
|
66
|
+
const target: PublicExposeTarget = { kind: "service", name, port };
|
|
67
|
+
knownTargets.set(name, target);
|
|
68
|
+
if (config.expose === true) {
|
|
69
|
+
enabledTargets.set(name, target);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const [name, config] of Object.entries(env.apps)) {
|
|
74
|
+
const port = env.ports[name];
|
|
75
|
+
if (port === undefined) continue;
|
|
76
|
+
const target: PublicExposeTarget = { kind: "app", name, port };
|
|
77
|
+
knownTargets.set(name, target);
|
|
78
|
+
if (config.expose === true) {
|
|
79
|
+
enabledTargets.set(name, target);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (requestedNames === null) {
|
|
84
|
+
return {
|
|
85
|
+
targets: Array.from(enabledTargets.values()),
|
|
86
|
+
unknownNames: [],
|
|
87
|
+
notEnabledNames: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const unknownNames: string[] = [];
|
|
92
|
+
const notEnabledNames: string[] = [];
|
|
93
|
+
const targets: PublicExposeTarget[] = [];
|
|
94
|
+
|
|
95
|
+
for (const name of requestedNames) {
|
|
96
|
+
if (!knownTargets.has(name)) {
|
|
97
|
+
unknownNames.push(name);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const enabledTarget = enabledTargets.get(name);
|
|
101
|
+
if (!enabledTarget) {
|
|
102
|
+
notEnabledNames.push(name);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
targets.push(enabledTarget);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { targets, unknownNames, notEnabledNames };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function startPublicTunnels(
|
|
112
|
+
targets: PublicExposeTarget[],
|
|
113
|
+
options: {
|
|
114
|
+
start?: (input: { url: string }) => Promise<UntunTunnelLike>;
|
|
115
|
+
} = {},
|
|
116
|
+
): Promise<PublicTunnel[]> {
|
|
117
|
+
const start = options.start ?? ((input) => startTunnel(input));
|
|
118
|
+
const tunnels: PublicTunnel[] = [];
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
for (const target of targets) {
|
|
122
|
+
const localUrl = `http://localhost:${target.port}`;
|
|
123
|
+
const tunnel = (await start({
|
|
124
|
+
url: localUrl,
|
|
125
|
+
})) as UntunTunnelLike;
|
|
126
|
+
const publicUrl = asPublicUrl(tunnel);
|
|
127
|
+
if (!publicUrl) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Tunnel for "${target.name}" did not provide a public URL`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
tunnels.push({
|
|
133
|
+
kind: target.kind,
|
|
134
|
+
name: target.name,
|
|
135
|
+
localUrl,
|
|
136
|
+
publicUrl,
|
|
137
|
+
close: toCloseFn(tunnel),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return tunnels;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await stopPublicTunnels(tunnels);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function stopPublicTunnels(
|
|
148
|
+
tunnels: PublicTunnel[],
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
await Promise.allSettled(tunnels.map((tunnel) => tunnel.close()));
|
|
151
|
+
}
|
|
@@ -120,6 +120,10 @@ export function getWatchdogPid(projectName: string): number | null {
|
|
|
120
120
|
* Spawn watchdog as a detached process.
|
|
121
121
|
* The watchdog monitors the heartbeat file and shuts down containers after idle timeout.
|
|
122
122
|
*/
|
|
123
|
+
export function getWatchdogComposeArg(composeFile?: string): string {
|
|
124
|
+
return composeFile ? `-f "${composeFile}"` : "";
|
|
125
|
+
}
|
|
126
|
+
|
|
123
127
|
export async function spawnWatchdog(
|
|
124
128
|
projectName: string,
|
|
125
129
|
root: string,
|
|
@@ -162,7 +166,7 @@ export async function spawnWatchdog(
|
|
|
162
166
|
WATCHDOG_HEARTBEAT_FILE: getHeartbeatFile(projectName),
|
|
163
167
|
WATCHDOG_PID_FILE: pidFile,
|
|
164
168
|
WATCHDOG_TIMEOUT_MS: String(timeoutMinutes * 60 * 1000),
|
|
165
|
-
WATCHDOG_COMPOSE_ARG: composeFile
|
|
169
|
+
WATCHDOG_COMPOSE_ARG: getWatchdogComposeArg(composeFile),
|
|
166
170
|
},
|
|
167
171
|
});
|
|
168
172
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./runtime";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { sleep } from "../core/utils";
|
|
2
3
|
import type {
|
|
3
4
|
BuiltInHealthCheck,
|
|
4
5
|
HealthCheckFn,
|
|
5
6
|
ServiceConfig,
|
|
6
7
|
} from "../types";
|
|
7
|
-
import { sleep } from "./utils";
|
|
8
8
|
|
|
9
9
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
10
|
// Constants
|
|
@@ -12,6 +12,8 @@ import { sleep } from "./utils";
|
|
|
12
12
|
|
|
13
13
|
export const POLL_INTERVAL = 250; // Fast polling for quicker startup
|
|
14
14
|
export const MAX_ATTEMPTS = 120; // 30 seconds total (120 * 250ms)
|
|
15
|
+
export const DOCKER_NOT_RUNNING_MESSAGE =
|
|
16
|
+
"Docker is not running. Please start Docker and try again.";
|
|
15
17
|
|
|
16
18
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
19
|
// Container Status Checks
|
|
@@ -35,6 +37,30 @@ export async function isContainerRunning(
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Check if Docker daemon is running and reachable.
|
|
42
|
+
*/
|
|
43
|
+
export function isDockerRunning(): boolean {
|
|
44
|
+
try {
|
|
45
|
+
execSync('docker info --format "{{.ServerVersion}}"', {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ensure Docker is running before attempting compose operations.
|
|
57
|
+
*/
|
|
58
|
+
export function assertDockerRunning(): void {
|
|
59
|
+
if (!isDockerRunning()) {
|
|
60
|
+
throw new Error(DOCKER_NOT_RUNNING_MESSAGE);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
/**
|
|
39
65
|
* Check if all expected containers are running.
|
|
40
66
|
*/
|
|
@@ -65,6 +91,13 @@ export interface StartContainersOptions {
|
|
|
65
91
|
composeFile?: string;
|
|
66
92
|
}
|
|
67
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Build `-f` argument for docker compose.
|
|
96
|
+
*/
|
|
97
|
+
export function getComposeArg(composeFile?: string): string {
|
|
98
|
+
return composeFile ? `-f "${composeFile}"` : "";
|
|
99
|
+
}
|
|
100
|
+
|
|
68
101
|
/**
|
|
69
102
|
* Start Docker Compose containers.
|
|
70
103
|
*/
|
|
@@ -75,10 +108,11 @@ export function startContainers(
|
|
|
75
108
|
options: StartContainersOptions = {},
|
|
76
109
|
): void {
|
|
77
110
|
const { verbose = true, wait = true, composeFile } = options;
|
|
111
|
+
assertDockerRunning();
|
|
78
112
|
|
|
79
113
|
if (verbose) console.log("🐳 Starting Docker containers...");
|
|
80
114
|
|
|
81
|
-
const composeArg = composeFile
|
|
115
|
+
const composeArg = getComposeArg(composeFile);
|
|
82
116
|
const waitFlag = wait ? "--wait" : "";
|
|
83
117
|
const cmd = `docker compose ${composeArg} up -d ${waitFlag}`.trim();
|
|
84
118
|
|
|
@@ -106,6 +140,7 @@ export function stopContainers(
|
|
|
106
140
|
options: StopContainersOptions = {},
|
|
107
141
|
): void {
|
|
108
142
|
const { verbose = true, removeVolumes = false, composeFile } = options;
|
|
143
|
+
assertDockerRunning();
|
|
109
144
|
|
|
110
145
|
if (verbose) {
|
|
111
146
|
console.log(
|
|
@@ -115,7 +150,7 @@ export function stopContainers(
|
|
|
115
150
|
);
|
|
116
151
|
}
|
|
117
152
|
|
|
118
|
-
const composeArg = composeFile
|
|
153
|
+
const composeArg = getComposeArg(composeFile);
|
|
119
154
|
const volumeFlag = removeVolumes ? "-v" : "";
|
|
120
155
|
const cmd = `docker compose ${composeArg} down ${volumeFlag}`.trim();
|
|
121
156
|
|
|
@@ -139,10 +174,11 @@ export function startService(
|
|
|
139
174
|
options: { verbose?: boolean; composeFile?: string } = {},
|
|
140
175
|
): void {
|
|
141
176
|
const { verbose = true, composeFile } = options;
|
|
177
|
+
assertDockerRunning();
|
|
142
178
|
|
|
143
179
|
if (verbose) console.log(`🐳 Starting ${serviceName}...`);
|
|
144
180
|
|
|
145
|
-
const composeArg = composeFile
|
|
181
|
+
const composeArg = getComposeArg(composeFile);
|
|
146
182
|
const cmd = `docker compose ${composeArg} up -d ${serviceName}`.trim();
|
|
147
183
|
|
|
148
184
|
execSync(cmd, {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import type { DockerComposeGenerationOptions, ServiceConfig } from "../types";
|
|
4
|
+
import { buildComposeModel } from "./model";
|
|
5
|
+
import { composeToYaml } from "./yaml";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_GENERATED_COMPOSE_FILE =
|
|
8
|
+
".buncargo/docker-compose.generated.yml";
|
|
9
|
+
|
|
10
|
+
export function getGeneratedComposePath(
|
|
11
|
+
root: string,
|
|
12
|
+
docker?: DockerComposeGenerationOptions,
|
|
13
|
+
): { absolutePath: string; composeFileArg: string } {
|
|
14
|
+
const generatedFile = docker?.generatedFile ?? DEFAULT_GENERATED_COMPOSE_FILE;
|
|
15
|
+
const absolutePath = isAbsolute(generatedFile)
|
|
16
|
+
? generatedFile
|
|
17
|
+
: resolve(root, generatedFile);
|
|
18
|
+
const relativePath = relative(root, absolutePath);
|
|
19
|
+
const composeFileArg =
|
|
20
|
+
relativePath && !relativePath.startsWith("..")
|
|
21
|
+
? relativePath
|
|
22
|
+
: absolutePath;
|
|
23
|
+
return { absolutePath, composeFileArg };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeGeneratedComposeFile(
|
|
27
|
+
root: string,
|
|
28
|
+
services: Record<string, ServiceConfig>,
|
|
29
|
+
docker?: DockerComposeGenerationOptions,
|
|
30
|
+
): string {
|
|
31
|
+
const { absolutePath, composeFileArg } = getGeneratedComposePath(
|
|
32
|
+
root,
|
|
33
|
+
docker,
|
|
34
|
+
);
|
|
35
|
+
const writeStrategy = docker?.writeStrategy ?? "always";
|
|
36
|
+
const shouldWrite = writeStrategy === "always" || !existsSync(absolutePath);
|
|
37
|
+
if (shouldWrite) {
|
|
38
|
+
const composeModel = buildComposeModel(services, docker);
|
|
39
|
+
const yaml = composeToYaml(composeModel);
|
|
40
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
41
|
+
writeFileSync(absolutePath, yaml, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return composeFileArg;
|
|
45
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DockerComposeGenerationOptions,
|
|
3
|
+
DockerComposeNode,
|
|
4
|
+
DockerComposeServiceRaw,
|
|
5
|
+
DockerComposeVolumeRaw,
|
|
6
|
+
DockerPresetName,
|
|
7
|
+
DockerPresetServiceDefinition,
|
|
8
|
+
DockerServiceDefinition,
|
|
9
|
+
ServiceConfig,
|
|
10
|
+
} from "../types";
|
|
11
|
+
import { buildPresetDockerService, inferDockerPreset } from "./services";
|
|
12
|
+
import { getDefaultPortBindings } from "./services/shared";
|
|
13
|
+
|
|
14
|
+
export type ComposeDocument = {
|
|
15
|
+
services: Record<string, DockerComposeServiceRaw>;
|
|
16
|
+
volumes?: Record<string, DockerComposeVolumeRaw>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function isObject(
|
|
20
|
+
value: DockerComposeNode,
|
|
21
|
+
): value is Record<string, DockerComposeNode | undefined> {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function deepMergeNode(
|
|
26
|
+
base: DockerComposeNode,
|
|
27
|
+
override: DockerComposeNode,
|
|
28
|
+
): DockerComposeNode {
|
|
29
|
+
if (Array.isArray(base) || Array.isArray(override)) {
|
|
30
|
+
return override;
|
|
31
|
+
}
|
|
32
|
+
if (!isObject(base) || !isObject(override)) {
|
|
33
|
+
return override;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const merged: Record<string, DockerComposeNode | undefined> = { ...base };
|
|
37
|
+
for (const key of Object.keys(override)) {
|
|
38
|
+
const baseValue = merged[key];
|
|
39
|
+
const overrideValue = override[key];
|
|
40
|
+
if (baseValue === undefined || overrideValue === undefined) {
|
|
41
|
+
merged[key] = overrideValue;
|
|
42
|
+
} else {
|
|
43
|
+
merged[key] = deepMergeNode(baseValue, overrideValue);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isPresetDefinition(
|
|
50
|
+
value: DockerServiceDefinition | undefined,
|
|
51
|
+
): value is DockerPresetServiceDefinition {
|
|
52
|
+
return Boolean(
|
|
53
|
+
value &&
|
|
54
|
+
typeof value === "object" &&
|
|
55
|
+
"kind" in value &&
|
|
56
|
+
(value as { kind?: string }).kind === "preset",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeRawService(
|
|
61
|
+
name: string,
|
|
62
|
+
config: ServiceConfig,
|
|
63
|
+
service: DockerComposeServiceRaw,
|
|
64
|
+
): DockerComposeServiceRaw {
|
|
65
|
+
const normalized = { ...service };
|
|
66
|
+
if (!normalized.ports || normalized.ports.length === 0) {
|
|
67
|
+
normalized.ports = getDefaultPortBindings(name, config);
|
|
68
|
+
}
|
|
69
|
+
if (config.healthCheck === false) {
|
|
70
|
+
delete normalized.healthcheck;
|
|
71
|
+
}
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type NormalizedServiceConfig =
|
|
76
|
+
| {
|
|
77
|
+
kind: "preset";
|
|
78
|
+
serviceName: string;
|
|
79
|
+
preset: DockerPresetName;
|
|
80
|
+
serviceOverride?: DockerComposeServiceRaw;
|
|
81
|
+
}
|
|
82
|
+
| {
|
|
83
|
+
kind: "raw";
|
|
84
|
+
serviceName: string;
|
|
85
|
+
service: DockerComposeServiceRaw;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function normalizeServiceConfig(
|
|
89
|
+
name: string,
|
|
90
|
+
config: ServiceConfig,
|
|
91
|
+
): NormalizedServiceConfig {
|
|
92
|
+
const serviceName = config.serviceName ?? name;
|
|
93
|
+
const rawDefinition = config.docker;
|
|
94
|
+
|
|
95
|
+
if (isPresetDefinition(rawDefinition)) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "preset",
|
|
98
|
+
serviceName,
|
|
99
|
+
preset: rawDefinition.preset,
|
|
100
|
+
serviceOverride: rawDefinition.service,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (rawDefinition) {
|
|
105
|
+
const inferredPreset = inferDockerPreset(name);
|
|
106
|
+
if (inferredPreset) {
|
|
107
|
+
return {
|
|
108
|
+
kind: "preset",
|
|
109
|
+
serviceName,
|
|
110
|
+
preset: inferredPreset,
|
|
111
|
+
serviceOverride: rawDefinition,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
kind: "raw",
|
|
116
|
+
serviceName,
|
|
117
|
+
service: normalizeRawService(name, config, rawDefinition),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const preset = inferDockerPreset(name);
|
|
122
|
+
if (!preset) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Service "${name}" has no docker preset and no docker definition. Add service.docker using helper or raw mode.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
kind: "preset",
|
|
130
|
+
serviceName,
|
|
131
|
+
preset,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveServiceDefinition(
|
|
136
|
+
name: string,
|
|
137
|
+
config: ServiceConfig,
|
|
138
|
+
): {
|
|
139
|
+
serviceName: string;
|
|
140
|
+
service: DockerComposeServiceRaw;
|
|
141
|
+
volume?: string;
|
|
142
|
+
} {
|
|
143
|
+
const normalized = normalizeServiceConfig(name, config);
|
|
144
|
+
if (normalized.kind === "raw") {
|
|
145
|
+
return {
|
|
146
|
+
serviceName: normalized.serviceName,
|
|
147
|
+
service: normalized.service,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { service, volume } = buildPresetDockerService(normalized.preset, {
|
|
152
|
+
serviceKey: name,
|
|
153
|
+
config,
|
|
154
|
+
});
|
|
155
|
+
const mergedService = normalized.serviceOverride
|
|
156
|
+
? (deepMergeNode(
|
|
157
|
+
service as DockerComposeNode,
|
|
158
|
+
normalized.serviceOverride as DockerComposeNode,
|
|
159
|
+
) as DockerComposeServiceRaw)
|
|
160
|
+
: service;
|
|
161
|
+
return {
|
|
162
|
+
serviceName: normalized.serviceName,
|
|
163
|
+
service: mergedService,
|
|
164
|
+
volume,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildComposeModel(
|
|
169
|
+
services: Record<string, ServiceConfig>,
|
|
170
|
+
docker?: DockerComposeGenerationOptions,
|
|
171
|
+
): ComposeDocument {
|
|
172
|
+
const composeServices: Record<string, DockerComposeServiceRaw> = {};
|
|
173
|
+
const composeVolumes: Record<string, DockerComposeVolumeRaw> = {};
|
|
174
|
+
|
|
175
|
+
for (const [name, serviceConfig] of Object.entries(services)) {
|
|
176
|
+
const { serviceName, service, volume } = resolveServiceDefinition(
|
|
177
|
+
name,
|
|
178
|
+
serviceConfig,
|
|
179
|
+
);
|
|
180
|
+
composeServices[serviceName] = service;
|
|
181
|
+
if (volume) {
|
|
182
|
+
composeVolumes[volume] = {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const [volumeName, volume] of Object.entries(docker?.volumes ?? {})) {
|
|
187
|
+
composeVolumes[volumeName] = volume;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const document: ComposeDocument = {
|
|
191
|
+
services: composeServices,
|
|
192
|
+
};
|
|
193
|
+
if (Object.keys(composeVolumes).length > 0) {
|
|
194
|
+
document.volumes = composeVolumes;
|
|
195
|
+
}
|
|
196
|
+
return document;
|
|
197
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuiltInHealthCheck,
|
|
3
|
+
DockerComposeHealthcheckRaw,
|
|
4
|
+
DockerComposeServiceRaw,
|
|
5
|
+
ServiceConfig,
|
|
6
|
+
} from "../../types";
|
|
7
|
+
import { defineDockerService } from "./define-docker-service";
|
|
8
|
+
import { getDefaultPortBindings, resolveHealthcheck } from "./shared";
|
|
9
|
+
|
|
10
|
+
export type ClickhouseServiceOptions = {
|
|
11
|
+
port?: number;
|
|
12
|
+
secondaryPort?: number;
|
|
13
|
+
expose?: boolean;
|
|
14
|
+
healthCheck?: BuiltInHealthCheck | false;
|
|
15
|
+
serviceName?: string;
|
|
16
|
+
database?: string;
|
|
17
|
+
user?: string;
|
|
18
|
+
password?: string;
|
|
19
|
+
docker?: DockerComposeServiceRaw;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ClickhouseServiceConfig = ServiceConfig & {
|
|
23
|
+
secondaryPort: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const clickhouseDockerService = defineDockerService<
|
|
27
|
+
ClickhouseServiceOptions,
|
|
28
|
+
ClickhouseServiceConfig
|
|
29
|
+
>({
|
|
30
|
+
preset: "clickhouse",
|
|
31
|
+
defaults: {
|
|
32
|
+
port: 8123,
|
|
33
|
+
secondaryPort: 9000,
|
|
34
|
+
healthCheck: "http",
|
|
35
|
+
},
|
|
36
|
+
enhanceServiceConfig: (base, options): ClickhouseServiceConfig => ({
|
|
37
|
+
...base,
|
|
38
|
+
secondaryPort: options.secondaryPort ?? 9000,
|
|
39
|
+
}),
|
|
40
|
+
build: ({ serviceKey, config }) => {
|
|
41
|
+
const user = config.user ?? "default";
|
|
42
|
+
const password = config.password ?? "clickhouse";
|
|
43
|
+
const database = config.database ?? "default";
|
|
44
|
+
const defaultHealthcheck: DockerComposeHealthcheckRaw = {
|
|
45
|
+
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8123/ping || exit 1"],
|
|
46
|
+
interval: "250ms",
|
|
47
|
+
timeout: "5s",
|
|
48
|
+
retries: 20,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
service: {
|
|
53
|
+
image: "clickhouse/clickhouse-server:24-alpine",
|
|
54
|
+
ports: getDefaultPortBindings(serviceKey, config, "clickhouse"),
|
|
55
|
+
volumes: [`${serviceKey}_data:/var/lib/clickhouse`],
|
|
56
|
+
environment: {
|
|
57
|
+
CLICKHOUSE_USER: user,
|
|
58
|
+
CLICKHOUSE_PASSWORD: password,
|
|
59
|
+
CLICKHOUSE_DB: database,
|
|
60
|
+
},
|
|
61
|
+
ulimits: {
|
|
62
|
+
nofile: {
|
|
63
|
+
soft: 262144,
|
|
64
|
+
hard: 262144,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
healthcheck: resolveHealthcheck(
|
|
68
|
+
config.healthCheck,
|
|
69
|
+
defaultHealthcheck,
|
|
70
|
+
{
|
|
71
|
+
internalPort: 8123,
|
|
72
|
+
user,
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
volume: `${serviceKey}_data`,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|