buncargo 1.0.5
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/bin.ts +253 -0
- package/cli.ts +238 -0
- package/config.ts +194 -0
- package/core/docker.ts +384 -0
- package/core/index.ts +7 -0
- package/core/network.ts +152 -0
- package/core/ports.ts +253 -0
- package/core/process.ts +253 -0
- package/core/utils.ts +127 -0
- package/core/watchdog-runner.ts +111 -0
- package/core/watchdog.ts +196 -0
- package/dist/bin.d.ts +12 -0
- package/dist/cli.d.ts +22 -0
- package/dist/config.d.ts +72 -0
- package/dist/core/docker.d.ts +74 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/network.d.ts +30 -0
- package/dist/core/ports.d.ts +30 -0
- package/dist/core/process.d.ts +52 -0
- package/dist/core/utils.d.ts +60 -0
- package/dist/core/watchdog-runner.d.ts +14 -0
- package/dist/core/watchdog.d.ts +46 -0
- package/dist/environment.d.ts +23 -0
- package/dist/index.d.ts +12 -0
- package/dist/lint.d.ts +46 -0
- package/dist/loader.d.ts +39 -0
- package/dist/prisma.d.ts +29 -0
- package/dist/types.d.ts +391 -0
- package/environment.ts +604 -0
- package/index.ts +103 -0
- package/lint.ts +277 -0
- package/loader.ts +100 -0
- package/package.json +124 -0
- package/prisma.ts +138 -0
- package/readme.md +198 -0
- package/types.ts +538 -0
package/bin.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI Entry Point for buncargo
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bunx buncargo dev # Start containers + dev servers
|
|
8
|
+
* bunx buncargo dev --down # Stop containers
|
|
9
|
+
* bunx buncargo dev --reset # Stop + remove volumes
|
|
10
|
+
* bunx buncargo prisma ... # Run prisma commands
|
|
11
|
+
* bunx buncargo help # Show help
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { runCli } from "./cli";
|
|
15
|
+
import { createDevEnvironment } from "./environment";
|
|
16
|
+
import type { AppConfig, DevEnvironment, ServiceConfig } from "./types";
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// Config Discovery
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
const CONFIG_FILES = [
|
|
23
|
+
"dev.config.ts",
|
|
24
|
+
"dev.config.js",
|
|
25
|
+
"dev-tools.config.ts",
|
|
26
|
+
"dev-tools.config.js",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find and load the dev config file from the current directory.
|
|
31
|
+
* Returns the DevEnvironment created from the config.
|
|
32
|
+
*/
|
|
33
|
+
async function loadEnv(): Promise<
|
|
34
|
+
DevEnvironment<Record<string, ServiceConfig>, Record<string, AppConfig>>
|
|
35
|
+
> {
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
|
|
38
|
+
for (const file of CONFIG_FILES) {
|
|
39
|
+
const path = `${cwd}/${file}`;
|
|
40
|
+
const exists = await Bun.file(path).exists();
|
|
41
|
+
|
|
42
|
+
if (exists) {
|
|
43
|
+
try {
|
|
44
|
+
const mod = await import(path);
|
|
45
|
+
const config = mod.default;
|
|
46
|
+
|
|
47
|
+
if (!config) {
|
|
48
|
+
console.error(
|
|
49
|
+
`❌ Config file "${file}" found but no default export.`,
|
|
50
|
+
);
|
|
51
|
+
console.error("");
|
|
52
|
+
console.error(" Export your config as default:");
|
|
53
|
+
console.error("");
|
|
54
|
+
console.error(" import { defineDevConfig } from 'buncargo'");
|
|
55
|
+
console.error("");
|
|
56
|
+
console.error(" export default defineDevConfig({ ... })");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate it looks like a config
|
|
61
|
+
if (!config.projectPrefix || !config.services) {
|
|
62
|
+
console.error(`❌ Config file "${file}" is not a valid dev config.`);
|
|
63
|
+
console.error("");
|
|
64
|
+
console.error(" Make sure to use defineDevConfig:");
|
|
65
|
+
console.error("");
|
|
66
|
+
console.error(" export default defineDevConfig({");
|
|
67
|
+
console.error(" projectPrefix: 'myapp',");
|
|
68
|
+
console.error(" services: { ... }");
|
|
69
|
+
console.error(" })");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create environment from config
|
|
74
|
+
return createDevEnvironment(config);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`❌ Failed to load config file "${file}":`);
|
|
77
|
+
console.error(error);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.error("❌ No config file found.");
|
|
84
|
+
console.error("");
|
|
85
|
+
console.error(" Create a dev.config.ts file in your project root:");
|
|
86
|
+
console.error("");
|
|
87
|
+
console.error(" import { defineDevConfig } from 'buncargo'");
|
|
88
|
+
console.error("");
|
|
89
|
+
console.error(" export default defineDevConfig({");
|
|
90
|
+
console.error(" projectPrefix: 'myapp',");
|
|
91
|
+
console.error(" services: {");
|
|
92
|
+
console.error(' postgres: { port: 5432, healthCheck: "pg_isready" }');
|
|
93
|
+
console.error(" }");
|
|
94
|
+
console.error(" })");
|
|
95
|
+
console.error("");
|
|
96
|
+
console.error(" Supported config files:");
|
|
97
|
+
for (const file of CONFIG_FILES) {
|
|
98
|
+
console.error(` - ${file}`);
|
|
99
|
+
}
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
|
+
// Command Handlers
|
|
105
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
106
|
+
|
|
107
|
+
async function handleDev(args: string[]): Promise<void> {
|
|
108
|
+
const env = await loadEnv();
|
|
109
|
+
await runCli(env, { args });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handlePrisma(args: string[]): Promise<void> {
|
|
113
|
+
const env = await loadEnv();
|
|
114
|
+
|
|
115
|
+
if (!env.prisma) {
|
|
116
|
+
console.error("❌ Prisma is not configured in your dev config.");
|
|
117
|
+
console.error("");
|
|
118
|
+
console.error(" Add prisma to your config:");
|
|
119
|
+
console.error("");
|
|
120
|
+
console.error(" export default defineDevConfig({");
|
|
121
|
+
console.error(" ...");
|
|
122
|
+
console.error(" prisma: {");
|
|
123
|
+
console.error(" cwd: 'packages/prisma'");
|
|
124
|
+
console.error(" }");
|
|
125
|
+
console.error(" })");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Ensure database is running
|
|
130
|
+
const running = await env.isRunning();
|
|
131
|
+
if (!running) {
|
|
132
|
+
console.log("🐳 Starting database container...");
|
|
133
|
+
await env.start({ startServers: false, wait: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const exitCode = await env.prisma.run(args);
|
|
137
|
+
process.exit(exitCode);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function handleEnv(): Promise<void> {
|
|
141
|
+
const env = await loadEnv();
|
|
142
|
+
console.log(
|
|
143
|
+
JSON.stringify(
|
|
144
|
+
{
|
|
145
|
+
projectName: env.projectName,
|
|
146
|
+
ports: env.ports,
|
|
147
|
+
urls: env.urls,
|
|
148
|
+
portOffset: env.portOffset,
|
|
149
|
+
isWorktree: env.isWorktree,
|
|
150
|
+
localIp: env.localIp,
|
|
151
|
+
root: env.root,
|
|
152
|
+
},
|
|
153
|
+
null,
|
|
154
|
+
2,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function showHelp(): void {
|
|
160
|
+
console.log(`
|
|
161
|
+
buncargo - Development environment CLI
|
|
162
|
+
|
|
163
|
+
USAGE:
|
|
164
|
+
bunx buncargo <command> [options]
|
|
165
|
+
|
|
166
|
+
COMMANDS:
|
|
167
|
+
dev Start the development environment
|
|
168
|
+
prisma <args> Run Prisma CLI with correct DATABASE_URL
|
|
169
|
+
env Print environment info as JSON
|
|
170
|
+
help Show this help message
|
|
171
|
+
version Show version
|
|
172
|
+
|
|
173
|
+
DEV OPTIONS:
|
|
174
|
+
--up-only Start containers only (no dev servers)
|
|
175
|
+
--down Stop containers
|
|
176
|
+
--reset Stop containers and remove volumes
|
|
177
|
+
--migrate Run migrations only
|
|
178
|
+
--seed Run seeders
|
|
179
|
+
--lint Run typecheck (no Docker required)
|
|
180
|
+
|
|
181
|
+
EXAMPLES:
|
|
182
|
+
bunx buncargo dev # Start everything
|
|
183
|
+
bunx buncargo dev --down # Stop containers
|
|
184
|
+
bunx buncargo prisma studio # Open Prisma Studio
|
|
185
|
+
bunx buncargo env # Get ports/urls as JSON
|
|
186
|
+
|
|
187
|
+
CONFIG:
|
|
188
|
+
Create a dev.config.ts with a default export:
|
|
189
|
+
|
|
190
|
+
import { defineDevConfig } from 'buncargo'
|
|
191
|
+
|
|
192
|
+
export default defineDevConfig({
|
|
193
|
+
projectPrefix: 'myapp',
|
|
194
|
+
services: { ... },
|
|
195
|
+
apps: { ... }
|
|
196
|
+
})
|
|
197
|
+
`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function showVersion(): void {
|
|
201
|
+
const pkg = require("./package.json");
|
|
202
|
+
console.log(`buncargo v${pkg.version}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
206
|
+
// Main
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
208
|
+
|
|
209
|
+
async function main(): Promise<void> {
|
|
210
|
+
const args = process.argv.slice(2);
|
|
211
|
+
const command = args[0];
|
|
212
|
+
const commandArgs = args.slice(1);
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
!command ||
|
|
216
|
+
command === "help" ||
|
|
217
|
+
command === "--help" ||
|
|
218
|
+
command === "-h"
|
|
219
|
+
) {
|
|
220
|
+
showHelp();
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
225
|
+
showVersion();
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
switch (command) {
|
|
230
|
+
case "dev":
|
|
231
|
+
await handleDev(commandArgs);
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case "prisma":
|
|
235
|
+
await handlePrisma(commandArgs);
|
|
236
|
+
break;
|
|
237
|
+
|
|
238
|
+
case "env":
|
|
239
|
+
await handleEnv();
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
default:
|
|
243
|
+
console.error(`❌ Unknown command: ${command}`);
|
|
244
|
+
console.error("");
|
|
245
|
+
console.error(' Run "bunx buncargo help" for available commands.');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
main().catch((error) => {
|
|
251
|
+
console.error("❌ Fatal error:", error);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
});
|
package/cli.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { spawnWatchdog, startHeartbeat, stopHeartbeat } from "./core/watchdog";
|
|
3
|
+
import type {
|
|
4
|
+
AppConfig,
|
|
5
|
+
CliOptions,
|
|
6
|
+
DevEnvironment,
|
|
7
|
+
ServiceConfig,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// CLI Runner
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run the CLI for a dev environment.
|
|
16
|
+
* Handles common flags like --down, --reset, --up-only, --migrate, --seed, --lint.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { dev } from './dev.config'
|
|
21
|
+
* import { runCli } from 'buncargo'
|
|
22
|
+
*
|
|
23
|
+
* await runCli(dev)
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export async function runCli<
|
|
27
|
+
TServices extends Record<string, ServiceConfig>,
|
|
28
|
+
TApps extends Record<string, AppConfig>,
|
|
29
|
+
>(
|
|
30
|
+
env: DevEnvironment<TServices, TApps>,
|
|
31
|
+
options: CliOptions = {},
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const {
|
|
34
|
+
args = process.argv.slice(2),
|
|
35
|
+
watchdog = true,
|
|
36
|
+
watchdogTimeout = 10,
|
|
37
|
+
devServersCommand,
|
|
38
|
+
} = options;
|
|
39
|
+
|
|
40
|
+
// Log environment info
|
|
41
|
+
env.logInfo();
|
|
42
|
+
|
|
43
|
+
// Handle --lint (no Docker required)
|
|
44
|
+
if (args.includes("--lint")) {
|
|
45
|
+
const { runWorkspaceTypecheck } = await import("./lint");
|
|
46
|
+
const result = await runWorkspaceTypecheck({
|
|
47
|
+
root: env.root,
|
|
48
|
+
verbose: true,
|
|
49
|
+
});
|
|
50
|
+
process.exit(result.success ? 0 : 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle --down
|
|
54
|
+
if (args.includes("--down")) {
|
|
55
|
+
await env.stop();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Handle --reset
|
|
60
|
+
if (args.includes("--reset")) {
|
|
61
|
+
await env.stop({ removeVolumes: true });
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Start containers if not already running
|
|
66
|
+
const running = await env.isRunning();
|
|
67
|
+
if (running) {
|
|
68
|
+
console.log("✓ Containers already running");
|
|
69
|
+
} else {
|
|
70
|
+
await env.start({ startServers: false, wait: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle --migrate (just run hooks, then exit)
|
|
74
|
+
if (args.includes("--migrate")) {
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log("✅ Migrations applied successfully");
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle --seed (force run seeders via hook context)
|
|
81
|
+
if (args.includes("--seed")) {
|
|
82
|
+
console.log("🌱 Running seeders...");
|
|
83
|
+
const result = await env.exec("bun run run:seeder", {
|
|
84
|
+
throwOnError: false,
|
|
85
|
+
});
|
|
86
|
+
if (result.exitCode !== 0) {
|
|
87
|
+
console.error("❌ Seeding failed");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log("✅ Seeding complete");
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle --up-only
|
|
96
|
+
if (args.includes("--up-only")) {
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log("✅ Containers started. Environment ready.");
|
|
99
|
+
console.log("");
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Start watchdog and heartbeat for interactive mode
|
|
104
|
+
if (watchdog) {
|
|
105
|
+
await spawnWatchdog(env.projectName, env.root, {
|
|
106
|
+
timeoutMinutes: watchdogTimeout,
|
|
107
|
+
verbose: true,
|
|
108
|
+
});
|
|
109
|
+
startHeartbeat(env.projectName);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Build command: use provided command or auto-build from apps config
|
|
113
|
+
const command = devServersCommand ?? buildDevServersCommand(env.apps);
|
|
114
|
+
|
|
115
|
+
if (!command) {
|
|
116
|
+
console.log("✅ Containers ready. No apps configured.");
|
|
117
|
+
// Keep process alive if no apps
|
|
118
|
+
await new Promise(() => {});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Start dev servers interactively
|
|
123
|
+
console.log("");
|
|
124
|
+
console.log("🔧 Starting dev servers...");
|
|
125
|
+
console.log("");
|
|
126
|
+
|
|
127
|
+
await runCommand(command, env.root, env.buildEnvVars());
|
|
128
|
+
|
|
129
|
+
// Clean up heartbeat on exit
|
|
130
|
+
stopHeartbeat();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
134
|
+
// Command Building
|
|
135
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build a concurrently command from the apps config.
|
|
139
|
+
*/
|
|
140
|
+
function buildDevServersCommand(
|
|
141
|
+
apps: Record<string, AppConfig>,
|
|
142
|
+
): string | null {
|
|
143
|
+
const appEntries = Object.entries(apps);
|
|
144
|
+
if (appEntries.length === 0) return null;
|
|
145
|
+
|
|
146
|
+
// Build commands for each app
|
|
147
|
+
const commands: string[] = [];
|
|
148
|
+
const names: string[] = [];
|
|
149
|
+
const colors = ["blue", "green", "yellow", "magenta", "cyan", "red"];
|
|
150
|
+
|
|
151
|
+
for (const [name, config] of appEntries) {
|
|
152
|
+
names.push(name);
|
|
153
|
+
const cwdPart = config.cwd ? `--cwd ${config.cwd}` : "";
|
|
154
|
+
commands.push(
|
|
155
|
+
`"bun run ${cwdPart} ${config.devCommand}"`.replace(/\s+/g, " ").trim(),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Use concurrently to run all apps
|
|
160
|
+
const namesArg = `-n ${names.join(",")}`;
|
|
161
|
+
const colorsArg = `-c ${colors.slice(0, names.length).join(",")}`;
|
|
162
|
+
const commandsArg = commands.join(" ");
|
|
163
|
+
|
|
164
|
+
return `bun concurrently ${namesArg} ${colorsArg} ${commandsArg}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
168
|
+
// Interactive Command Runner
|
|
169
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Run a command interactively (inherits stdio).
|
|
173
|
+
*/
|
|
174
|
+
function runCommand(
|
|
175
|
+
command: string,
|
|
176
|
+
cwd: string,
|
|
177
|
+
envVars: Record<string, string>,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const proc = spawn(command, [], {
|
|
181
|
+
cwd,
|
|
182
|
+
env: { ...process.env, ...envVars },
|
|
183
|
+
stdio: "inherit",
|
|
184
|
+
shell: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
proc.on("close", (code) => {
|
|
188
|
+
if (code === 0 || code === null) {
|
|
189
|
+
resolve();
|
|
190
|
+
} else {
|
|
191
|
+
reject(new Error(`Command exited with code ${code}`));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
proc.on("error", reject);
|
|
196
|
+
|
|
197
|
+
// Handle SIGINT/SIGTERM
|
|
198
|
+
const cleanup = () => {
|
|
199
|
+
proc.kill("SIGTERM");
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
process.on("SIGINT", cleanup);
|
|
203
|
+
process.on("SIGTERM", cleanup);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
208
|
+
// Utility Functions
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if a CLI flag is present.
|
|
213
|
+
*/
|
|
214
|
+
export function hasFlag(args: string[], flag: string): boolean {
|
|
215
|
+
return args.includes(flag);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get a flag value (e.g., --timeout=10 or --timeout 10).
|
|
220
|
+
*/
|
|
221
|
+
export function getFlagValue(args: string[], flag: string): string | undefined {
|
|
222
|
+
// Check --flag=value format
|
|
223
|
+
const prefixed = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
224
|
+
if (prefixed) {
|
|
225
|
+
return prefixed.split("=")[1];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check --flag value format
|
|
229
|
+
const index = args.indexOf(flag);
|
|
230
|
+
if (index !== -1 && index + 1 < args.length) {
|
|
231
|
+
const nextArg = args[index + 1];
|
|
232
|
+
if (nextArg !== undefined && !nextArg.startsWith("-")) {
|
|
233
|
+
return nextArg;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppConfig,
|
|
3
|
+
DevConfig,
|
|
4
|
+
DevHooks,
|
|
5
|
+
DevOptions,
|
|
6
|
+
EnvVarsBuilder,
|
|
7
|
+
MigrationConfig,
|
|
8
|
+
PrismaConfig,
|
|
9
|
+
SeedConfig,
|
|
10
|
+
ServiceConfig,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// Config Factory
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Define a dev environment configuration with full TypeScript inference.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const config = defineDevConfig({
|
|
23
|
+
* projectPrefix: 'myapp',
|
|
24
|
+
* services: {
|
|
25
|
+
* postgres: { port: 5432, healthCheck: 'pg_isready' },
|
|
26
|
+
* redis: { port: 6379 },
|
|
27
|
+
* },
|
|
28
|
+
* apps: {
|
|
29
|
+
* api: { port: 3000, devCommand: 'bun run dev', cwd: 'apps/backend' },
|
|
30
|
+
* web: { port: 5173, devCommand: 'bun run dev', cwd: 'apps/frontend' },
|
|
31
|
+
* },
|
|
32
|
+
* envVars: (ports, urls) => ({
|
|
33
|
+
* DATABASE_URL: urls.postgres,
|
|
34
|
+
* REDIS_URL: urls.redis,
|
|
35
|
+
* API_PORT: String(ports.api),
|
|
36
|
+
* }),
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function defineDevConfig<
|
|
41
|
+
TServices extends Record<string, ServiceConfig>,
|
|
42
|
+
TApps extends Record<string, AppConfig> = Record<string, never>,
|
|
43
|
+
>(config: {
|
|
44
|
+
/** Prefix for Docker project name (e.g., 'myapp' -> 'myapp-main') */
|
|
45
|
+
projectPrefix: string;
|
|
46
|
+
/** Docker Compose services to manage */
|
|
47
|
+
services: TServices;
|
|
48
|
+
/** Applications to start (optional) */
|
|
49
|
+
apps?: TApps;
|
|
50
|
+
/**
|
|
51
|
+
* Environment variables builder. Define all env vars here.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* envVars: (ports, urls, { localIp }) => ({
|
|
56
|
+
* DATABASE_URL: urls.postgres,
|
|
57
|
+
* BASE_URL: urls.api,
|
|
58
|
+
* VITE_PORT: ports.platform,
|
|
59
|
+
* EXPO_API_URL: `http://${localIp}:${ports.api}`
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
envVars?: EnvVarsBuilder<TServices, TApps>;
|
|
64
|
+
/** Lifecycle hooks (optional) */
|
|
65
|
+
hooks?: DevHooks<TServices, TApps>;
|
|
66
|
+
/** Migrations to run after containers are ready (optional). Runs in parallel. */
|
|
67
|
+
migrations?: MigrationConfig[];
|
|
68
|
+
/** Seed configuration (optional). Runs after migrations, before servers. */
|
|
69
|
+
seed?: SeedConfig<TServices, TApps>;
|
|
70
|
+
/** Prisma configuration (optional). When set, dev.prisma is available. */
|
|
71
|
+
prisma?: PrismaConfig;
|
|
72
|
+
/** Additional options (optional) */
|
|
73
|
+
options?: DevOptions;
|
|
74
|
+
}): DevConfig<TServices, TApps> {
|
|
75
|
+
return config as DevConfig<TServices, TApps>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
+
// Config Validation
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate a dev config and return any errors.
|
|
84
|
+
*/
|
|
85
|
+
export function validateConfig<
|
|
86
|
+
TServices extends Record<string, ServiceConfig>,
|
|
87
|
+
TApps extends Record<string, AppConfig>,
|
|
88
|
+
>(config: DevConfig<TServices, TApps>): string[] {
|
|
89
|
+
const errors: string[] = [];
|
|
90
|
+
|
|
91
|
+
// Check project prefix
|
|
92
|
+
if (!config.projectPrefix) {
|
|
93
|
+
errors.push("projectPrefix is required");
|
|
94
|
+
} else if (!/^[a-z][a-z0-9-]*$/.test(config.projectPrefix)) {
|
|
95
|
+
errors.push(
|
|
96
|
+
"projectPrefix must start with a letter and contain only lowercase letters, numbers, and hyphens",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check services
|
|
101
|
+
if (!config.services || Object.keys(config.services).length === 0) {
|
|
102
|
+
errors.push("At least one service is required");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [name, service] of Object.entries(config.services ?? {})) {
|
|
106
|
+
if (!service.port || typeof service.port !== "number") {
|
|
107
|
+
errors.push(`Service "${name}" must have a valid port number`);
|
|
108
|
+
}
|
|
109
|
+
if (service.port < 1 || service.port > 65535) {
|
|
110
|
+
errors.push(`Service "${name}" port must be between 1 and 65535`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check apps
|
|
115
|
+
for (const [name, app] of Object.entries(config.apps ?? {})) {
|
|
116
|
+
if (!app.port || typeof app.port !== "number") {
|
|
117
|
+
errors.push(`App "${name}" must have a valid port number`);
|
|
118
|
+
}
|
|
119
|
+
if (!app.devCommand) {
|
|
120
|
+
errors.push(`App "${name}" must have a devCommand`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check migrations
|
|
125
|
+
for (const migration of config.migrations ?? []) {
|
|
126
|
+
if (!migration.name) {
|
|
127
|
+
errors.push("Migration must have a name");
|
|
128
|
+
}
|
|
129
|
+
if (!migration.command) {
|
|
130
|
+
errors.push(`Migration "${migration.name}" must have a command`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check seed
|
|
135
|
+
if (config.seed && !config.seed.command) {
|
|
136
|
+
errors.push("Seed must have a command");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return errors;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate config and throw if invalid.
|
|
144
|
+
*/
|
|
145
|
+
export function assertValidConfig<
|
|
146
|
+
TServices extends Record<string, ServiceConfig>,
|
|
147
|
+
TApps extends Record<string, AppConfig>,
|
|
148
|
+
>(config: DevConfig<TServices, TApps>): void {
|
|
149
|
+
const errors = validateConfig(config);
|
|
150
|
+
if (errors.length > 0) {
|
|
151
|
+
throw new Error(`Invalid dev config:\n - ${errors.join("\n - ")}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
// Config Helpers
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Merge two configs, with the second taking precedence.
|
|
161
|
+
*/
|
|
162
|
+
export function mergeConfigs<
|
|
163
|
+
TServices extends Record<string, ServiceConfig>,
|
|
164
|
+
TApps extends Record<string, AppConfig>,
|
|
165
|
+
>(
|
|
166
|
+
base: DevConfig<TServices, TApps>,
|
|
167
|
+
overrides: Partial<DevConfig<TServices, TApps>>,
|
|
168
|
+
): DevConfig<TServices, TApps> {
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
...overrides,
|
|
172
|
+
services: { ...base.services, ...overrides.services } as TServices,
|
|
173
|
+
apps: { ...base.apps, ...overrides.apps } as TApps,
|
|
174
|
+
hooks: { ...base.hooks, ...overrides.hooks },
|
|
175
|
+
migrations: overrides.migrations ?? base.migrations,
|
|
176
|
+
seed: overrides.seed ?? base.seed,
|
|
177
|
+
options: { ...base.options, ...overrides.options },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create a partial config that can be merged later.
|
|
183
|
+
*/
|
|
184
|
+
export function definePartialConfig<
|
|
185
|
+
TServices extends Record<string, ServiceConfig> = Record<
|
|
186
|
+
string,
|
|
187
|
+
ServiceConfig
|
|
188
|
+
>,
|
|
189
|
+
TApps extends Record<string, AppConfig> = Record<string, AppConfig>,
|
|
190
|
+
>(
|
|
191
|
+
config: Partial<DevConfig<TServices, TApps>>,
|
|
192
|
+
): Partial<DevConfig<TServices, TApps>> {
|
|
193
|
+
return config;
|
|
194
|
+
}
|