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/core/ports.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
import type { AppConfig, ServiceConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
// Monorepo Root Detection
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Find the monorepo root by looking for package.json with workspaces.
|
|
11
|
+
*/
|
|
12
|
+
export function findMonorepoRoot(startDir?: string): string {
|
|
13
|
+
let dir = startDir ?? process.cwd();
|
|
14
|
+
while (dir !== "/") {
|
|
15
|
+
try {
|
|
16
|
+
const pkgPath = resolve(dir, "package.json");
|
|
17
|
+
if (existsSync(pkgPath)) {
|
|
18
|
+
const content = readFileSync(pkgPath, "utf-8");
|
|
19
|
+
const pkg = JSON.parse(content);
|
|
20
|
+
if (pkg.workspaces) {
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Continue searching
|
|
26
|
+
}
|
|
27
|
+
dir = dirname(dir);
|
|
28
|
+
}
|
|
29
|
+
return process.cwd();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
|
+
// Worktree Detection
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the worktree name from .git file (if in a worktree).
|
|
38
|
+
*/
|
|
39
|
+
export function getWorktreeName(root?: string): string | null {
|
|
40
|
+
const monorepoRoot = root ?? findMonorepoRoot();
|
|
41
|
+
const gitPath = resolve(monorepoRoot, ".git");
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(gitPath) || !statSync(gitPath).isFile()) return null;
|
|
44
|
+
const content = readFileSync(gitPath, "utf-8").trim();
|
|
45
|
+
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
46
|
+
if (!match?.[1]) return null;
|
|
47
|
+
return basename(match[1]);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if the current directory is a git worktree.
|
|
55
|
+
*/
|
|
56
|
+
export function isWorktree(root?: string): boolean {
|
|
57
|
+
return getWorktreeName(root) !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
61
|
+
// Port Offset Calculation
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Simple hash function for consistent port offsets.
|
|
66
|
+
*/
|
|
67
|
+
function simpleHash(str: string): number {
|
|
68
|
+
let hash = 0;
|
|
69
|
+
for (let i = 0; i < str.length; i++) {
|
|
70
|
+
const char = str.charCodeAt(i);
|
|
71
|
+
hash = (hash << 5) - hash + char;
|
|
72
|
+
hash = hash & hash;
|
|
73
|
+
}
|
|
74
|
+
return Math.abs(hash);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Calculate port offset based on worktree name and optional suffix.
|
|
79
|
+
* Returns 0 for main branch, 10-99 for worktrees.
|
|
80
|
+
*/
|
|
81
|
+
export function calculatePortOffset(suffix?: string, root?: string): number {
|
|
82
|
+
const worktreeName = getWorktreeName(root);
|
|
83
|
+
if (!worktreeName) return 0;
|
|
84
|
+
const hashInput = suffix ? `${worktreeName}-${suffix}` : worktreeName;
|
|
85
|
+
// Range 10-99 to avoid conflicts with main (0) and leave room
|
|
86
|
+
return 10 + (simpleHash(hashInput) % 90);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
90
|
+
// Project Naming
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate Docker project name from prefix and directory.
|
|
95
|
+
*/
|
|
96
|
+
export function getProjectName(
|
|
97
|
+
prefix: string,
|
|
98
|
+
suffix?: string,
|
|
99
|
+
root?: string,
|
|
100
|
+
): string {
|
|
101
|
+
const monorepoRoot = root ?? findMonorepoRoot();
|
|
102
|
+
const dirName = basename(monorepoRoot);
|
|
103
|
+
const baseName = `${prefix}-${dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
|
|
104
|
+
return suffix ? `${baseName}-${suffix}` : baseName;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
108
|
+
// Port Computation
|
|
109
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Compute all ports for services and apps with offset applied.
|
|
113
|
+
*/
|
|
114
|
+
export function computePorts<
|
|
115
|
+
TServices extends Record<string, ServiceConfig>,
|
|
116
|
+
TApps extends Record<string, AppConfig>,
|
|
117
|
+
>(
|
|
118
|
+
services: TServices,
|
|
119
|
+
apps: TApps | undefined,
|
|
120
|
+
offset: number,
|
|
121
|
+
): Record<string, number> {
|
|
122
|
+
const ports: Record<string, number> = {};
|
|
123
|
+
|
|
124
|
+
// Add service ports
|
|
125
|
+
for (const [name, config] of Object.entries(services)) {
|
|
126
|
+
ports[name] = config.port + offset;
|
|
127
|
+
// Handle secondary ports (e.g., clickhouseNative)
|
|
128
|
+
if (config.secondaryPort) {
|
|
129
|
+
ports[`${name}Secondary`] = config.secondaryPort + offset;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add app ports
|
|
134
|
+
if (apps) {
|
|
135
|
+
for (const [name, config] of Object.entries(apps)) {
|
|
136
|
+
ports[name] = config.port + offset;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return ports;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
144
|
+
// URL Generation
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Service defaults for common services.
|
|
149
|
+
*/
|
|
150
|
+
const SERVICE_DEFAULTS: Record<
|
|
151
|
+
string,
|
|
152
|
+
{ user: string; password: string; database: string }
|
|
153
|
+
> = {
|
|
154
|
+
postgres: { user: "postgres", password: "postgres", database: "postgres" },
|
|
155
|
+
postgresql: { user: "postgres", password: "postgres", database: "postgres" },
|
|
156
|
+
redis: { user: "", password: "", database: "" },
|
|
157
|
+
clickhouse: { user: "default", password: "clickhouse", database: "default" },
|
|
158
|
+
mysql: { user: "root", password: "root", database: "mysql" },
|
|
159
|
+
mongodb: { user: "", password: "", database: "" },
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build URL for a service with given credentials and database.
|
|
164
|
+
*/
|
|
165
|
+
function buildServiceUrl(
|
|
166
|
+
serviceName: string,
|
|
167
|
+
ctx: { port: number; host: string },
|
|
168
|
+
config: { database?: string; user?: string; password?: string },
|
|
169
|
+
): string | null {
|
|
170
|
+
const defaults = SERVICE_DEFAULTS[serviceName];
|
|
171
|
+
if (!defaults && !config.database) return null;
|
|
172
|
+
|
|
173
|
+
const user = config.user ?? defaults?.user ?? "";
|
|
174
|
+
const password = config.password ?? defaults?.password ?? "";
|
|
175
|
+
const database = config.database ?? defaults?.database ?? "";
|
|
176
|
+
|
|
177
|
+
switch (serviceName) {
|
|
178
|
+
case "postgres":
|
|
179
|
+
case "postgresql":
|
|
180
|
+
return `postgresql://${user}:${password}@${ctx.host}:${ctx.port}/${database}`;
|
|
181
|
+
case "redis":
|
|
182
|
+
return `redis://${ctx.host}:${ctx.port}`;
|
|
183
|
+
case "clickhouse":
|
|
184
|
+
return `http://${user}:${password}@${ctx.host}:${ctx.port}/${database}`;
|
|
185
|
+
case "mysql":
|
|
186
|
+
return `mysql://${user}:${password}@${ctx.host}:${ctx.port}/${database}`;
|
|
187
|
+
case "mongodb":
|
|
188
|
+
return `mongodb://${ctx.host}:${ctx.port}/${database}`;
|
|
189
|
+
default:
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Compute URLs for all services and apps.
|
|
196
|
+
*/
|
|
197
|
+
export function computeUrls<
|
|
198
|
+
TServices extends Record<string, ServiceConfig>,
|
|
199
|
+
TApps extends Record<string, AppConfig>,
|
|
200
|
+
>(
|
|
201
|
+
services: TServices,
|
|
202
|
+
apps: TApps | undefined,
|
|
203
|
+
ports: Record<string, number>,
|
|
204
|
+
localIp: string,
|
|
205
|
+
): Record<string, string> {
|
|
206
|
+
const urls: Record<string, string> = {};
|
|
207
|
+
const host = "localhost";
|
|
208
|
+
|
|
209
|
+
// Add service URLs
|
|
210
|
+
for (const [name, config] of Object.entries(services)) {
|
|
211
|
+
const port = ports[name];
|
|
212
|
+
const secondaryPort = ports[`${name}Secondary`];
|
|
213
|
+
|
|
214
|
+
// Skip if port is not defined
|
|
215
|
+
if (port === undefined) continue;
|
|
216
|
+
|
|
217
|
+
const ctx = { port, secondaryPort, host, localIp };
|
|
218
|
+
|
|
219
|
+
if (config.urlTemplate) {
|
|
220
|
+
// Use the provided function
|
|
221
|
+
urls[name] = config.urlTemplate(ctx);
|
|
222
|
+
} else {
|
|
223
|
+
// Try to build URL using service name and config options
|
|
224
|
+
const builtUrl = buildServiceUrl(
|
|
225
|
+
name,
|
|
226
|
+
{ port, host },
|
|
227
|
+
{
|
|
228
|
+
database: config.database,
|
|
229
|
+
user: config.user,
|
|
230
|
+
password: config.password,
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
if (builtUrl) {
|
|
234
|
+
urls[name] = builtUrl;
|
|
235
|
+
} else {
|
|
236
|
+
// Fallback to simple HTTP URL
|
|
237
|
+
urls[name] = `http://${host}:${port}`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add app URLs
|
|
243
|
+
if (apps) {
|
|
244
|
+
for (const [name, _config] of Object.entries(apps)) {
|
|
245
|
+
const port = ports[name];
|
|
246
|
+
urls[name] = `http://${host}:${port}`;
|
|
247
|
+
// Also add local IP version for mobile connectivity
|
|
248
|
+
urls[`${name}Local`] = `http://${localIp}:${port}`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return urls;
|
|
253
|
+
}
|
package/core/process.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChildProcess,
|
|
3
|
+
execSync,
|
|
4
|
+
type SpawnOptions,
|
|
5
|
+
spawn,
|
|
6
|
+
} from "node:child_process";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import type { AppConfig, DevServerPids, ExecOptions } from "../types";
|
|
9
|
+
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// Command Execution
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
export interface ExecResult {
|
|
15
|
+
exitCode: number;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Execute a shell command with environment variables.
|
|
22
|
+
*/
|
|
23
|
+
export function exec(
|
|
24
|
+
cmd: string,
|
|
25
|
+
root: string,
|
|
26
|
+
envVars: Record<string, string>,
|
|
27
|
+
options: ExecOptions = {},
|
|
28
|
+
): ExecResult {
|
|
29
|
+
const { cwd, verbose = false, env = {}, throwOnError = true } = options;
|
|
30
|
+
|
|
31
|
+
const workingDir = cwd ? resolve(root, cwd) : root;
|
|
32
|
+
const fullEnv = { ...process.env, ...envVars, ...env };
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const stdout = execSync(cmd, {
|
|
36
|
+
cwd: workingDir,
|
|
37
|
+
env: fullEnv,
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
stdio: verbose ? "inherit" : ["pipe", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
exitCode: 0,
|
|
44
|
+
stdout: typeof stdout === "string" ? stdout : "",
|
|
45
|
+
stderr: "",
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const execError = error as {
|
|
49
|
+
status?: number;
|
|
50
|
+
stdout?: string;
|
|
51
|
+
stderr?: string;
|
|
52
|
+
};
|
|
53
|
+
const result: ExecResult = {
|
|
54
|
+
exitCode: execError.status ?? 1,
|
|
55
|
+
stdout: execError.stdout ?? "",
|
|
56
|
+
stderr: execError.stderr ?? "",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (throwOnError) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Command failed with exit code ${result.exitCode}: ${cmd}\n${result.stderr}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute a shell command asynchronously.
|
|
71
|
+
*/
|
|
72
|
+
export async function execAsync(
|
|
73
|
+
cmd: string,
|
|
74
|
+
root: string,
|
|
75
|
+
envVars: Record<string, string>,
|
|
76
|
+
options: ExecOptions = {},
|
|
77
|
+
): Promise<ExecResult> {
|
|
78
|
+
// For now, wrap sync in Promise - can be optimized later with spawn
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const result = exec(cmd, root, envVars, {
|
|
81
|
+
...options,
|
|
82
|
+
throwOnError: false,
|
|
83
|
+
});
|
|
84
|
+
resolve(result);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
// Process Spawning
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
91
|
+
|
|
92
|
+
export interface SpawnDevServerOptions {
|
|
93
|
+
verbose?: boolean;
|
|
94
|
+
detached?: boolean;
|
|
95
|
+
isCI?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Spawn a dev server as a detached process.
|
|
100
|
+
*/
|
|
101
|
+
export function spawnDevServer(
|
|
102
|
+
command: string,
|
|
103
|
+
root: string,
|
|
104
|
+
appCwd: string | undefined,
|
|
105
|
+
envVars: Record<string, string>,
|
|
106
|
+
options: SpawnDevServerOptions = {},
|
|
107
|
+
): ChildProcess {
|
|
108
|
+
const { verbose = false, detached = true, isCI = false } = options;
|
|
109
|
+
|
|
110
|
+
// Parse command into parts
|
|
111
|
+
const parts = command.split(" ");
|
|
112
|
+
const cmd = parts[0];
|
|
113
|
+
const args = parts.slice(1);
|
|
114
|
+
|
|
115
|
+
if (!cmd) {
|
|
116
|
+
throw new Error("Command cannot be empty");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const workingDir = appCwd ? resolve(root, appCwd) : root;
|
|
120
|
+
|
|
121
|
+
const spawnOptions: SpawnOptions = {
|
|
122
|
+
cwd: workingDir,
|
|
123
|
+
env: { ...process.env, ...envVars },
|
|
124
|
+
detached,
|
|
125
|
+
stdio: isCI || verbose ? "inherit" : "ignore",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const proc = spawn(cmd, args, spawnOptions);
|
|
129
|
+
|
|
130
|
+
if (detached && proc.unref) {
|
|
131
|
+
proc.unref();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return proc;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Start all configured dev servers.
|
|
139
|
+
*/
|
|
140
|
+
export function startDevServers(
|
|
141
|
+
apps: Record<string, AppConfig>,
|
|
142
|
+
root: string,
|
|
143
|
+
envVars: Record<string, string>,
|
|
144
|
+
options: {
|
|
145
|
+
verbose?: boolean;
|
|
146
|
+
productionBuild?: boolean;
|
|
147
|
+
isCI?: boolean;
|
|
148
|
+
} = {},
|
|
149
|
+
): DevServerPids {
|
|
150
|
+
const { verbose = true, productionBuild = false, isCI = false } = options;
|
|
151
|
+
const pids: DevServerPids = {};
|
|
152
|
+
|
|
153
|
+
if (verbose) {
|
|
154
|
+
console.log(
|
|
155
|
+
productionBuild
|
|
156
|
+
? "🚀 Starting production servers..."
|
|
157
|
+
: "🔧 Starting dev servers...",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const [name, config] of Object.entries(apps)) {
|
|
162
|
+
const command = productionBuild
|
|
163
|
+
? (config.prodCommand ?? config.devCommand)
|
|
164
|
+
: config.devCommand;
|
|
165
|
+
|
|
166
|
+
const proc = spawnDevServer(command, root, config.cwd, envVars, {
|
|
167
|
+
verbose,
|
|
168
|
+
isCI,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (proc.pid) {
|
|
172
|
+
pids[name] = proc.pid;
|
|
173
|
+
if (verbose) {
|
|
174
|
+
console.log(` ${name} PID: ${proc.pid}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return pids;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
183
|
+
// Process Management
|
|
184
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Stop a process by PID.
|
|
188
|
+
*/
|
|
189
|
+
export function stopProcess(pid: number): void {
|
|
190
|
+
try {
|
|
191
|
+
process.kill(pid, "SIGTERM");
|
|
192
|
+
} catch {
|
|
193
|
+
// Process may already be dead
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Stop all processes by their PIDs.
|
|
199
|
+
*/
|
|
200
|
+
export function stopAllProcesses(
|
|
201
|
+
pids: DevServerPids,
|
|
202
|
+
options: { verbose?: boolean } = {},
|
|
203
|
+
): void {
|
|
204
|
+
const { verbose = true } = options;
|
|
205
|
+
|
|
206
|
+
for (const [name, pid] of Object.entries(pids)) {
|
|
207
|
+
if (pid) {
|
|
208
|
+
if (verbose) console.log(` Stopping ${name} (PID: ${pid})`);
|
|
209
|
+
stopProcess(pid);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if a process is alive by sending signal 0.
|
|
216
|
+
*/
|
|
217
|
+
export function isProcessAlive(pid: number): boolean {
|
|
218
|
+
try {
|
|
219
|
+
process.kill(pid, 0);
|
|
220
|
+
return true;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
227
|
+
// Build Commands
|
|
228
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Run production build for apps that have buildCommand configured.
|
|
232
|
+
*/
|
|
233
|
+
export function buildApps(
|
|
234
|
+
apps: Record<string, AppConfig>,
|
|
235
|
+
root: string,
|
|
236
|
+
envVars: Record<string, string>,
|
|
237
|
+
options: { verbose?: boolean } = {},
|
|
238
|
+
): void {
|
|
239
|
+
const { verbose = true } = options;
|
|
240
|
+
|
|
241
|
+
for (const [name, config] of Object.entries(apps)) {
|
|
242
|
+
if (config.buildCommand) {
|
|
243
|
+
if (verbose) console.log(`🔨 Building ${name}...`);
|
|
244
|
+
|
|
245
|
+
exec(config.buildCommand, root, envVars, {
|
|
246
|
+
cwd: config.cwd,
|
|
247
|
+
verbose,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (verbose) console.log("✓ Build complete");
|
|
253
|
+
}
|
package/core/utils.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { AppConfig, DevConfig, ServiceConfig } from "../types";
|
|
2
|
+
import { getLocalIp } from "./network";
|
|
3
|
+
import { calculatePortOffset, computePorts, computeUrls } from "./ports";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Core utility functions shared across modules.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sleep for a given number of milliseconds.
|
|
11
|
+
*/
|
|
12
|
+
export function sleep(ms: number): Promise<void> {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detect if running in a CI environment.
|
|
18
|
+
*/
|
|
19
|
+
export function isCI(): boolean {
|
|
20
|
+
return (
|
|
21
|
+
process.env.CI === "true" ||
|
|
22
|
+
process.env.CI === "1" ||
|
|
23
|
+
process.env.GITHUB_ACTIONS === "true" ||
|
|
24
|
+
process.env.GITLAB_CI === "true" ||
|
|
25
|
+
process.env.CIRCLECI === "true" ||
|
|
26
|
+
process.env.JENKINS_URL !== undefined
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
+
// Vibe Kanban Integration
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Log the frontend port in a format that Vibe Kanban can detect.
|
|
36
|
+
* This is used to communicate the dev server port to external tools.
|
|
37
|
+
*
|
|
38
|
+
* @param port - The port number the frontend is running on
|
|
39
|
+
*/
|
|
40
|
+
export function logFrontendPort(port: number | undefined): void {
|
|
41
|
+
console.log(`using_frontend_port:${port}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
45
|
+
// Config-based Env Var Helper
|
|
46
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get an environment variable value from the config.
|
|
50
|
+
* Computes ports/urls and runs envVars to get the value.
|
|
51
|
+
*
|
|
52
|
+
* @param config - The dev config object (from defineDevConfig)
|
|
53
|
+
* @param name - The environment variable name
|
|
54
|
+
* @param options - Optional settings (log for Vibe Kanban detection)
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* // In vite.config.ts
|
|
59
|
+
* import { getEnvVar } from 'buncargo'
|
|
60
|
+
* import config from '../../dev.config'
|
|
61
|
+
*
|
|
62
|
+
* export default defineConfig(async ({ command }) => {
|
|
63
|
+
* const isDev = command === 'serve'
|
|
64
|
+
* const vitePort = isDev ? getEnvVar(config, 'VITE_PORT') : undefined
|
|
65
|
+
* const apiUrl = getEnvVar(config, 'VITE_API_URL')
|
|
66
|
+
* return {
|
|
67
|
+
* server: { port: vitePort, strictPort: true }
|
|
68
|
+
* }
|
|
69
|
+
* })
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function getEnvVar<
|
|
73
|
+
TServices extends Record<string, ServiceConfig>,
|
|
74
|
+
TApps extends Record<string, AppConfig>,
|
|
75
|
+
>(
|
|
76
|
+
config: DevConfig<TServices, TApps>,
|
|
77
|
+
name: string,
|
|
78
|
+
options: { log?: boolean } = {},
|
|
79
|
+
): string | number | undefined {
|
|
80
|
+
const { log = true } = options;
|
|
81
|
+
const offset = calculatePortOffset();
|
|
82
|
+
const localIp = getLocalIp();
|
|
83
|
+
|
|
84
|
+
// Compute ports and urls
|
|
85
|
+
const ports = computePorts(config.services, config.apps, offset);
|
|
86
|
+
const urls = computeUrls(config.services, config.apps, ports, localIp);
|
|
87
|
+
|
|
88
|
+
// Build env vars from the function
|
|
89
|
+
const envVars = config.envVars?.(
|
|
90
|
+
ports as Parameters<NonNullable<typeof config.envVars>>[0],
|
|
91
|
+
urls as Parameters<NonNullable<typeof config.envVars>>[1],
|
|
92
|
+
{
|
|
93
|
+
projectName: config.projectPrefix,
|
|
94
|
+
localIp,
|
|
95
|
+
portOffset: offset,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const value = envVars?.[name];
|
|
100
|
+
|
|
101
|
+
// Log frontend port for Vibe Kanban detection
|
|
102
|
+
if (log && name === "VITE_PORT" && typeof value === "number") {
|
|
103
|
+
logFrontendPort(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Log the API URL in a format that tools can detect.
|
|
111
|
+
* This is used by Expo and other tools to find the API server.
|
|
112
|
+
*
|
|
113
|
+
* @param url - The API URL
|
|
114
|
+
*/
|
|
115
|
+
export function logApiUrl(url: string): void {
|
|
116
|
+
console.log(`using_api_url:${url}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Log the Expo API URL in a format that tools can detect.
|
|
121
|
+
* This is typically the local IP address for mobile device connectivity.
|
|
122
|
+
*
|
|
123
|
+
* @param url - The Expo API URL (usually http://<local-ip>:<port>)
|
|
124
|
+
*/
|
|
125
|
+
export function logExpoApiUrl(url: string): void {
|
|
126
|
+
console.log(`using_expo_api_url:${url}`);
|
|
127
|
+
}
|