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/environment.ts
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { assertValidConfig } from "./config";
|
|
3
|
+
import {
|
|
4
|
+
areContainersRunning,
|
|
5
|
+
startContainers,
|
|
6
|
+
stopContainers,
|
|
7
|
+
waitForAllServices,
|
|
8
|
+
} from "./core/docker";
|
|
9
|
+
import { getLocalIp, waitForDevServers, waitForServer } from "./core/network";
|
|
10
|
+
import {
|
|
11
|
+
calculatePortOffset,
|
|
12
|
+
computePorts,
|
|
13
|
+
computeUrls,
|
|
14
|
+
findMonorepoRoot,
|
|
15
|
+
getProjectName,
|
|
16
|
+
isWorktree,
|
|
17
|
+
} from "./core/ports";
|
|
18
|
+
import {
|
|
19
|
+
buildApps,
|
|
20
|
+
execAsync,
|
|
21
|
+
startDevServers,
|
|
22
|
+
stopProcess as stopProcessFn,
|
|
23
|
+
} from "./core/process";
|
|
24
|
+
import { isCI as isCIEnv, logExpoApiUrl, logFrontendPort } from "./core/utils";
|
|
25
|
+
import {
|
|
26
|
+
spawnWatchdog as spawnWatchdogFn,
|
|
27
|
+
startHeartbeat as startHeartbeatFn,
|
|
28
|
+
stopHeartbeat as stopHeartbeatFn,
|
|
29
|
+
stopWatchdog as stopWatchdogFn,
|
|
30
|
+
} from "./core/watchdog";
|
|
31
|
+
import { createPrismaRunner } from "./prisma";
|
|
32
|
+
import type {
|
|
33
|
+
AppConfig,
|
|
34
|
+
ComputedPorts,
|
|
35
|
+
ComputedUrls,
|
|
36
|
+
DevConfig,
|
|
37
|
+
DevEnvironment,
|
|
38
|
+
DevServerPids,
|
|
39
|
+
ExecOptions,
|
|
40
|
+
HookContext,
|
|
41
|
+
PrismaRunner,
|
|
42
|
+
ServiceConfig,
|
|
43
|
+
StartOptions,
|
|
44
|
+
StopOptions,
|
|
45
|
+
} from "./types";
|
|
46
|
+
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
// Console Output Formatting (Vite-inspired)
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format a URL with colored port number (Vite-style).
|
|
53
|
+
*/
|
|
54
|
+
function formatUrl(url: string): string {
|
|
55
|
+
return pc.cyan(
|
|
56
|
+
url.replace(/:(\d+)(\/?)/, (_, port, slash) => `:${pc.bold(port)}${slash}`),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format a label with arrow prefix (Vite-style).
|
|
62
|
+
*/
|
|
63
|
+
function formatLabel(label: string, value: string, arrow = "➜"): string {
|
|
64
|
+
return ` ${pc.green(arrow)} ${pc.bold(label.padEnd(10))} ${value}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format a dim label (for secondary info).
|
|
69
|
+
*/
|
|
70
|
+
function formatDimLabel(label: string, value: string): string {
|
|
71
|
+
return ` ${pc.dim("•")} ${pc.dim(label.padEnd(10))} ${pc.dim(value)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
// Environment Factory
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a dev environment from a configuration.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* import { defineDevConfig, createDevEnvironment } from 'buncargo'
|
|
84
|
+
*
|
|
85
|
+
* const config = defineDevConfig({
|
|
86
|
+
* projectPrefix: 'myapp',
|
|
87
|
+
* services: { postgres: { port: 5432 } },
|
|
88
|
+
* apps: { api: { port: 3000, devCommand: 'bun run dev' } }
|
|
89
|
+
* })
|
|
90
|
+
*
|
|
91
|
+
* export const dev = createDevEnvironment(config)
|
|
92
|
+
*
|
|
93
|
+
* // Usage
|
|
94
|
+
* await dev.start()
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function createDevEnvironment<
|
|
98
|
+
TServices extends Record<string, ServiceConfig>,
|
|
99
|
+
TApps extends Record<string, AppConfig>,
|
|
100
|
+
>(
|
|
101
|
+
config: DevConfig<TServices, TApps>,
|
|
102
|
+
options: { suffix?: string } = {},
|
|
103
|
+
): DevEnvironment<TServices, TApps> {
|
|
104
|
+
// Validate config
|
|
105
|
+
assertValidConfig(config);
|
|
106
|
+
|
|
107
|
+
// Compute environment values
|
|
108
|
+
const root = findMonorepoRoot();
|
|
109
|
+
const suffix = options.suffix;
|
|
110
|
+
const worktree = isWorktree(root);
|
|
111
|
+
const portOffset = calculatePortOffset(suffix, root);
|
|
112
|
+
const projectName = getProjectName(config.projectPrefix, suffix, root);
|
|
113
|
+
const localIp = getLocalIp();
|
|
114
|
+
|
|
115
|
+
const services = config.services;
|
|
116
|
+
const apps = (config.apps ?? {}) as TApps;
|
|
117
|
+
|
|
118
|
+
// Compute ports and URLs
|
|
119
|
+
const ports = computePorts(services, apps, portOffset) as ComputedPorts<
|
|
120
|
+
TServices,
|
|
121
|
+
TApps
|
|
122
|
+
>;
|
|
123
|
+
const urls = computeUrls(services, apps, ports, localIp) as ComputedUrls<
|
|
124
|
+
TServices,
|
|
125
|
+
TApps
|
|
126
|
+
>;
|
|
127
|
+
|
|
128
|
+
// Build environment variables
|
|
129
|
+
function buildEnvVars(production = false): Record<string, string> {
|
|
130
|
+
const baseEnv: Record<string, string> = {
|
|
131
|
+
COMPOSE_PROJECT_NAME: projectName,
|
|
132
|
+
NODE_ENV: production ? "production" : "development",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Add port environment variables for docker-compose
|
|
136
|
+
for (const [name, port] of Object.entries(ports)) {
|
|
137
|
+
const envName = `${name.toUpperCase()}_PORT`;
|
|
138
|
+
baseEnv[envName] = String(port);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add URL environment variables
|
|
142
|
+
for (const [name, url] of Object.entries(urls)) {
|
|
143
|
+
const envName = `${name.toUpperCase()}_URL`;
|
|
144
|
+
baseEnv[envName] = url;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Call user's envVars function if provided
|
|
148
|
+
if (config.envVars) {
|
|
149
|
+
const userEnv = config.envVars(ports, urls, {
|
|
150
|
+
projectName,
|
|
151
|
+
localIp,
|
|
152
|
+
portOffset,
|
|
153
|
+
});
|
|
154
|
+
for (const [key, value] of Object.entries(userEnv)) {
|
|
155
|
+
baseEnv[key] = String(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return baseEnv;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Memoized hook context (created once, reused)
|
|
163
|
+
let hookContext: HookContext<TServices, TApps> | null = null;
|
|
164
|
+
|
|
165
|
+
function getHookContext(): HookContext<TServices, TApps> {
|
|
166
|
+
if (!hookContext) {
|
|
167
|
+
hookContext = {
|
|
168
|
+
projectName,
|
|
169
|
+
ports,
|
|
170
|
+
urls,
|
|
171
|
+
root,
|
|
172
|
+
isCI: isCIEnv(),
|
|
173
|
+
portOffset,
|
|
174
|
+
localIp,
|
|
175
|
+
exec: async (cmd, opts) => {
|
|
176
|
+
const envVars = buildEnvVars();
|
|
177
|
+
return execAsync(cmd, root, envVars, opts);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return hookContext;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Execute command helper
|
|
185
|
+
function exec(cmd: string, options?: ExecOptions) {
|
|
186
|
+
const envVars = buildEnvVars();
|
|
187
|
+
return execAsync(cmd, root, envVars, options);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
191
|
+
// Container Management
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
async function start(
|
|
195
|
+
startOptions: StartOptions = {},
|
|
196
|
+
): Promise<DevServerPids | null> {
|
|
197
|
+
const isCI = process.env.CI === "true";
|
|
198
|
+
const {
|
|
199
|
+
verbose = config.options?.verbose ?? true,
|
|
200
|
+
wait = true,
|
|
201
|
+
startServers: shouldStartServers = true,
|
|
202
|
+
productionBuild = isCI,
|
|
203
|
+
} = startOptions;
|
|
204
|
+
|
|
205
|
+
const envVars = buildEnvVars(productionBuild);
|
|
206
|
+
|
|
207
|
+
// Log environment info
|
|
208
|
+
if (verbose) {
|
|
209
|
+
logInfo(productionBuild ? "Production Environment" : "Dev Environment");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Start containers
|
|
213
|
+
const serviceCount = Object.keys(services).length;
|
|
214
|
+
const alreadyRunning = await areContainersRunning(
|
|
215
|
+
projectName,
|
|
216
|
+
serviceCount,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (alreadyRunning) {
|
|
220
|
+
if (verbose) console.log("✓ Containers already running");
|
|
221
|
+
} else {
|
|
222
|
+
startContainers(root, projectName, envVars, {
|
|
223
|
+
verbose,
|
|
224
|
+
wait,
|
|
225
|
+
composeFile: config.options?.composeFile,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Wait for services to be healthy
|
|
230
|
+
if (wait) {
|
|
231
|
+
await waitForAllServices(services, ports, {
|
|
232
|
+
verbose,
|
|
233
|
+
projectName,
|
|
234
|
+
root,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Build migrations list (auto-add prisma if configured)
|
|
239
|
+
const allMigrations = [
|
|
240
|
+
// Auto-add prisma migration if prisma is configured
|
|
241
|
+
...(config.prisma
|
|
242
|
+
? [
|
|
243
|
+
{
|
|
244
|
+
name: "prisma",
|
|
245
|
+
command: "bunx prisma migrate deploy",
|
|
246
|
+
cwd: config.prisma.cwd ?? "packages/prisma",
|
|
247
|
+
},
|
|
248
|
+
]
|
|
249
|
+
: []),
|
|
250
|
+
// Add user-defined migrations
|
|
251
|
+
...(config.migrations ?? []),
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
// Run migrations if any
|
|
255
|
+
if (allMigrations.length > 0) {
|
|
256
|
+
if (verbose) console.log("📦 Running migrations...");
|
|
257
|
+
|
|
258
|
+
const migrationResults = await Promise.all(
|
|
259
|
+
allMigrations.map(async (migration) => {
|
|
260
|
+
const result = await exec(migration.command, {
|
|
261
|
+
cwd: migration.cwd,
|
|
262
|
+
throwOnError: false,
|
|
263
|
+
});
|
|
264
|
+
return { name: migration.name, result };
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Check for failures
|
|
269
|
+
for (const { name, result } of migrationResults) {
|
|
270
|
+
if (result.exitCode !== 0) {
|
|
271
|
+
console.error(`❌ Migration "${name}" failed`);
|
|
272
|
+
console.error(result.stderr);
|
|
273
|
+
throw new Error(`Migration "${name}" failed`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (verbose) console.log("✓ Migrations complete");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Run afterContainersReady hook
|
|
281
|
+
if (config.hooks?.afterContainersReady) {
|
|
282
|
+
await config.hooks.afterContainersReady(getHookContext());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Run seed if configured
|
|
286
|
+
if (config.seed) {
|
|
287
|
+
let shouldSeed = true;
|
|
288
|
+
|
|
289
|
+
// Check if seeding is needed using check function
|
|
290
|
+
if (config.seed.check) {
|
|
291
|
+
// Create checkTable helper function with typed service parameter
|
|
292
|
+
const checkTable = async (
|
|
293
|
+
tableName: string,
|
|
294
|
+
service?: keyof TServices,
|
|
295
|
+
): Promise<boolean> => {
|
|
296
|
+
const serviceName = (service ?? "postgres") as string;
|
|
297
|
+
const serviceUrl = (urls as Record<string, string>)[serviceName];
|
|
298
|
+
if (!serviceUrl) {
|
|
299
|
+
console.warn(`⚠️ Service "${serviceName}" not found for checkTable`);
|
|
300
|
+
return true; // Default to seeding if service not found
|
|
301
|
+
}
|
|
302
|
+
const checkResult = await exec(
|
|
303
|
+
`psql "${serviceUrl}" -tAc 'SELECT COUNT(*) FROM "${tableName}" LIMIT 1'`,
|
|
304
|
+
{ throwOnError: false },
|
|
305
|
+
);
|
|
306
|
+
const count = checkResult.stdout.trim();
|
|
307
|
+
return checkResult.exitCode !== 0 || count === "0" || count === "";
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Build seed check context with helpers
|
|
311
|
+
const seedCheckContext = {
|
|
312
|
+
...getHookContext(),
|
|
313
|
+
checkTable,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
shouldSeed = await config.seed.check(seedCheckContext);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (shouldSeed) {
|
|
320
|
+
if (verbose) console.log("🌱 Running seeders...");
|
|
321
|
+
const seedResult = await exec(config.seed.command, {
|
|
322
|
+
cwd: config.seed.cwd,
|
|
323
|
+
verbose,
|
|
324
|
+
throwOnError: false,
|
|
325
|
+
});
|
|
326
|
+
if (seedResult.exitCode !== 0) {
|
|
327
|
+
console.error("❌ Seeding failed");
|
|
328
|
+
console.error(seedResult.stderr);
|
|
329
|
+
// Don't throw - seeding failure shouldn't stop the environment
|
|
330
|
+
} else {
|
|
331
|
+
if (verbose) console.log("✓ Seeding complete");
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
if (verbose)
|
|
335
|
+
console.log("✓ Database already has data, skipping seeders");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Start servers if requested
|
|
340
|
+
if (shouldStartServers && Object.keys(apps).length > 0) {
|
|
341
|
+
// Run beforeServers hook
|
|
342
|
+
if (config.hooks?.beforeServers) {
|
|
343
|
+
await config.hooks.beforeServers(getHookContext());
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Build if production
|
|
347
|
+
if (productionBuild) {
|
|
348
|
+
buildApps(apps, root, envVars, { verbose });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Start servers
|
|
352
|
+
const pids = startDevServers(apps, root, envVars, {
|
|
353
|
+
verbose,
|
|
354
|
+
productionBuild,
|
|
355
|
+
isCI,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Wait for servers to be ready
|
|
359
|
+
if (verbose) console.log("⏳ Waiting for servers to be ready...");
|
|
360
|
+
await waitForDevServers(apps, ports, {
|
|
361
|
+
timeout: isCI ? 120000 : 60000,
|
|
362
|
+
verbose,
|
|
363
|
+
productionBuild,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Run afterServers hook
|
|
367
|
+
if (config.hooks?.afterServers) {
|
|
368
|
+
await config.hooks.afterServers(getHookContext());
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (verbose) console.log("✅ Environment ready\n");
|
|
372
|
+
return pids;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (verbose) console.log("✅ Containers ready\n");
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function stop(stopOptions: StopOptions = {}): Promise<void> {
|
|
380
|
+
const { verbose = true, removeVolumes = false } = stopOptions;
|
|
381
|
+
|
|
382
|
+
// Run beforeStop hook
|
|
383
|
+
if (config.hooks?.beforeStop) {
|
|
384
|
+
await config.hooks.beforeStop(getHookContext());
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
stopContainers(root, projectName, {
|
|
388
|
+
verbose,
|
|
389
|
+
removeVolumes,
|
|
390
|
+
composeFile: config.options?.composeFile,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function restart(): Promise<void> {
|
|
395
|
+
await stop();
|
|
396
|
+
await start({ startServers: false });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function isRunning(): Promise<boolean> {
|
|
400
|
+
const serviceCount = Object.keys(services).length;
|
|
401
|
+
return areContainersRunning(projectName, serviceCount);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
405
|
+
// Server Management
|
|
406
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
async function startServersOnly(
|
|
409
|
+
options: { productionBuild?: boolean; verbose?: boolean } = {},
|
|
410
|
+
): Promise<DevServerPids> {
|
|
411
|
+
const { productionBuild = false, verbose = true } = options;
|
|
412
|
+
const envVars = buildEnvVars(productionBuild);
|
|
413
|
+
const isCI = process.env.CI === "true";
|
|
414
|
+
|
|
415
|
+
// Build if production
|
|
416
|
+
if (productionBuild) {
|
|
417
|
+
buildApps(apps, root, envVars, { verbose });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return startDevServers(apps, root, envVars, {
|
|
421
|
+
verbose,
|
|
422
|
+
productionBuild,
|
|
423
|
+
isCI,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function waitForServersReady(
|
|
428
|
+
options: { timeout?: number; productionBuild?: boolean } = {},
|
|
429
|
+
): Promise<void> {
|
|
430
|
+
const { timeout = 60000, productionBuild = false } = options;
|
|
431
|
+
await waitForDevServers(apps, ports, { timeout, productionBuild });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
435
|
+
// Utilities
|
|
436
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
function logInfo(label = "Docker Dev"): void {
|
|
439
|
+
const serviceNames = Object.keys(services);
|
|
440
|
+
const appNames = Object.keys(apps);
|
|
441
|
+
|
|
442
|
+
console.log("");
|
|
443
|
+
console.log(` ${pc.cyan(pc.bold(`🐳 ${label}`))}`);
|
|
444
|
+
console.log(formatLabel("Project:", pc.white(projectName)));
|
|
445
|
+
|
|
446
|
+
// Services section (Docker containers)
|
|
447
|
+
if (serviceNames.length > 0) {
|
|
448
|
+
console.log("");
|
|
449
|
+
console.log(` ${pc.dim("─── Services ───")}`);
|
|
450
|
+
for (const name of serviceNames) {
|
|
451
|
+
const port = (ports as Record<string, number>)[name];
|
|
452
|
+
const url = `localhost:${port}`;
|
|
453
|
+
console.log(formatLabel(`${name}:`, formatUrl(`http://${url}`)));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Apps section (Dev servers)
|
|
458
|
+
if (appNames.length > 0) {
|
|
459
|
+
console.log("");
|
|
460
|
+
console.log(` ${pc.dim("─── Applications ───")}`);
|
|
461
|
+
for (const name of appNames) {
|
|
462
|
+
const port = (ports as Record<string, number>)[name];
|
|
463
|
+
const localUrl = `http://localhost:${port}`;
|
|
464
|
+
const networkUrl = `http://${localIp}:${port}`;
|
|
465
|
+
|
|
466
|
+
console.log(` ${pc.green("➜")} ${pc.bold(pc.cyan(name))}`);
|
|
467
|
+
console.log(` ${pc.dim("Local:")} ${formatUrl(localUrl)}`);
|
|
468
|
+
console.log(` ${pc.dim("Network:")} ${formatUrl(networkUrl)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Environment info
|
|
473
|
+
console.log("");
|
|
474
|
+
console.log(` ${pc.dim("─── Environment ───")}`);
|
|
475
|
+
console.log(formatDimLabel("Worktree:", worktree ? "yes" : "no"));
|
|
476
|
+
console.log(
|
|
477
|
+
formatDimLabel(
|
|
478
|
+
"Port offset:",
|
|
479
|
+
portOffset > 0 ? `+${portOffset}` : "none",
|
|
480
|
+
),
|
|
481
|
+
);
|
|
482
|
+
if (suffix) {
|
|
483
|
+
console.log(formatDimLabel("Suffix:", suffix));
|
|
484
|
+
}
|
|
485
|
+
console.log(formatDimLabel("Local IP:", localIp));
|
|
486
|
+
console.log("");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function waitForServerUrl(
|
|
490
|
+
url: string,
|
|
491
|
+
timeout?: number,
|
|
492
|
+
): Promise<void> {
|
|
493
|
+
await waitForServer(url, { timeout });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
497
|
+
// Watchdog / Heartbeat
|
|
498
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
function startHeartbeat(intervalMs?: number): void {
|
|
501
|
+
startHeartbeatFn(projectName, intervalMs);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function stopHeartbeat(): void {
|
|
505
|
+
stopHeartbeatFn();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function spawnWatchdog(timeoutMinutes?: number): Promise<void> {
|
|
509
|
+
await spawnWatchdogFn(projectName, root, {
|
|
510
|
+
timeoutMinutes,
|
|
511
|
+
verbose: true,
|
|
512
|
+
composeFile: config.options?.composeFile,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function stopWatchdog(): void {
|
|
517
|
+
stopWatchdogFn(projectName);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
521
|
+
// Vibe Kanban Integration
|
|
522
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
function getExpoApiUrl(): string {
|
|
525
|
+
const apiPort = (ports as Record<string, number>).api;
|
|
526
|
+
const url = `http://${localIp}:${apiPort}`;
|
|
527
|
+
logExpoApiUrl(url);
|
|
528
|
+
return url;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function getFrontendPort(): number | undefined {
|
|
532
|
+
const port = (ports as Record<string, number>).platform;
|
|
533
|
+
logFrontendPort(port);
|
|
534
|
+
return port;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
538
|
+
// Advanced
|
|
539
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
540
|
+
|
|
541
|
+
function withSuffix(newSuffix: string): DevEnvironment<TServices, TApps> {
|
|
542
|
+
return createDevEnvironment(config, { suffix: newSuffix });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
546
|
+
// Return Environment Object
|
|
547
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
// Build base environment
|
|
550
|
+
const env: DevEnvironment<TServices, TApps> = {
|
|
551
|
+
// Configuration access
|
|
552
|
+
projectName,
|
|
553
|
+
ports,
|
|
554
|
+
urls,
|
|
555
|
+
apps,
|
|
556
|
+
portOffset,
|
|
557
|
+
isWorktree: worktree,
|
|
558
|
+
localIp,
|
|
559
|
+
root,
|
|
560
|
+
|
|
561
|
+
// Container management
|
|
562
|
+
start,
|
|
563
|
+
stop,
|
|
564
|
+
restart,
|
|
565
|
+
isRunning,
|
|
566
|
+
|
|
567
|
+
// Server management
|
|
568
|
+
startServers: startServersOnly,
|
|
569
|
+
stopProcess: stopProcessFn,
|
|
570
|
+
waitForServers: waitForServersReady,
|
|
571
|
+
|
|
572
|
+
// Utilities
|
|
573
|
+
buildEnvVars,
|
|
574
|
+
exec,
|
|
575
|
+
waitForServer: waitForServerUrl,
|
|
576
|
+
logInfo,
|
|
577
|
+
|
|
578
|
+
// Vibe Kanban Integration
|
|
579
|
+
getExpoApiUrl,
|
|
580
|
+
getFrontendPort,
|
|
581
|
+
|
|
582
|
+
// Watchdog / Heartbeat
|
|
583
|
+
startHeartbeat,
|
|
584
|
+
stopHeartbeat,
|
|
585
|
+
spawnWatchdog,
|
|
586
|
+
stopWatchdog,
|
|
587
|
+
|
|
588
|
+
// Prisma (created below if configured)
|
|
589
|
+
prisma: undefined,
|
|
590
|
+
|
|
591
|
+
// Advanced
|
|
592
|
+
withSuffix,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// Create prisma runner if configured
|
|
596
|
+
if (config.prisma) {
|
|
597
|
+
(env as { prisma: PrismaRunner }).prisma = createPrismaRunner(
|
|
598
|
+
env,
|
|
599
|
+
config.prisma,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return env;
|
|
604
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// Main Exports
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
// CLI runner
|
|
6
|
+
export { getFlagValue, hasFlag, runCli } from "./cli";
|
|
7
|
+
// Config factory
|
|
8
|
+
export {
|
|
9
|
+
assertValidConfig,
|
|
10
|
+
defineDevConfig,
|
|
11
|
+
mergeConfigs,
|
|
12
|
+
validateConfig,
|
|
13
|
+
} from "./config";
|
|
14
|
+
// Environment factory
|
|
15
|
+
export { createDevEnvironment } from "./environment";
|
|
16
|
+
// Lint / Typecheck
|
|
17
|
+
export {
|
|
18
|
+
runWorkspaceTypecheck,
|
|
19
|
+
type TypecheckResult,
|
|
20
|
+
type WorkspaceTypecheckOptions,
|
|
21
|
+
type WorkspaceTypecheckResult,
|
|
22
|
+
} from "./lint";
|
|
23
|
+
// Config loader (for programmatic access)
|
|
24
|
+
export { clearDevEnvCache, getDevEnv, loadDevEnv } from "./loader";
|
|
25
|
+
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
// Types
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
AppConfig,
|
|
32
|
+
BuiltInHealthCheck,
|
|
33
|
+
// CLI
|
|
34
|
+
CliOptions,
|
|
35
|
+
// Computed types
|
|
36
|
+
ComputedPorts,
|
|
37
|
+
ComputedUrls,
|
|
38
|
+
// Main config
|
|
39
|
+
DevConfig,
|
|
40
|
+
// Environment interface
|
|
41
|
+
DevEnvironment,
|
|
42
|
+
DevHooks,
|
|
43
|
+
DevOptions,
|
|
44
|
+
DevServerPids,
|
|
45
|
+
EnvVarsBuilder,
|
|
46
|
+
ExecOptions,
|
|
47
|
+
HealthCheckFn,
|
|
48
|
+
HookContext,
|
|
49
|
+
// Migrations & Seed
|
|
50
|
+
MigrationConfig,
|
|
51
|
+
// Prisma
|
|
52
|
+
PrismaConfig,
|
|
53
|
+
PrismaRunner,
|
|
54
|
+
SeedCheckContext,
|
|
55
|
+
SeedCheckHelpers,
|
|
56
|
+
SeedConfig,
|
|
57
|
+
// Service & App configs
|
|
58
|
+
ServiceConfig,
|
|
59
|
+
// Start/Stop options
|
|
60
|
+
StartOptions,
|
|
61
|
+
StopOptions,
|
|
62
|
+
UrlBuilderContext,
|
|
63
|
+
UrlBuilderFn,
|
|
64
|
+
} from "./types";
|
|
65
|
+
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// Core Utilities (for advanced use cases)
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
areContainersRunning,
|
|
72
|
+
isContainerRunning,
|
|
73
|
+
MAX_ATTEMPTS,
|
|
74
|
+
POLL_INTERVAL,
|
|
75
|
+
} from "./core/docker";
|
|
76
|
+
|
|
77
|
+
export { getLocalIp, isPortAvailable, waitForServer } from "./core/network";
|
|
78
|
+
export {
|
|
79
|
+
calculatePortOffset,
|
|
80
|
+
findMonorepoRoot,
|
|
81
|
+
getProjectName,
|
|
82
|
+
getWorktreeName,
|
|
83
|
+
isWorktree,
|
|
84
|
+
} from "./core/ports";
|
|
85
|
+
|
|
86
|
+
export { isProcessAlive } from "./core/process";
|
|
87
|
+
export {
|
|
88
|
+
getEnvVar,
|
|
89
|
+
isCI,
|
|
90
|
+
logApiUrl,
|
|
91
|
+
logExpoApiUrl,
|
|
92
|
+
logFrontendPort,
|
|
93
|
+
sleep,
|
|
94
|
+
} from "./core/utils";
|
|
95
|
+
export {
|
|
96
|
+
getHeartbeatFile,
|
|
97
|
+
getWatchdogPidFile,
|
|
98
|
+
isWatchdogRunning,
|
|
99
|
+
spawnWatchdog,
|
|
100
|
+
startHeartbeat,
|
|
101
|
+
stopHeartbeat,
|
|
102
|
+
stopWatchdog,
|
|
103
|
+
} from "./core/watchdog";
|