bgrun 3.12.12 → 3.12.14
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/README.md +2 -2
- package/dashboard/lib/runtime.ts +49 -0
- package/package.json +2 -17
- package/src/api.ts +0 -116
- package/src/build.ts +0 -31
- package/src/commands/cleanup.ts +0 -141
- package/src/commands/details.ts +0 -60
- package/src/commands/list.ts +0 -133
- package/src/commands/logs.ts +0 -49
- package/src/commands/run.ts +0 -217
- package/src/commands/watch.ts +0 -223
- package/src/config.ts +0 -37
- package/src/db.ts +0 -422
- package/src/deploy.ts +0 -163
- package/src/deps.ts +0 -126
- package/src/guard.ts +0 -208
- package/src/index.ts +0 -623
- package/src/log-rotation.ts +0 -93
- package/src/logger.ts +0 -40
- package/src/platform.ts +0 -665
- package/src/server.ts +0 -217
- package/src/table.ts +0 -232
- package/src/types.ts +0 -14
- package/src/utils.ts +0 -96
package/src/commands/run.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import type { CommandOptions } from "../types";
|
|
2
|
-
import { getProcess, removeProcessByName, retryDatabaseOperation, insertProcess } from "../db";
|
|
3
|
-
import { isProcessRunning, terminateProcess, getHomeDir, getShellCommand, killProcessOnPort, findChildPid, getProcessPorts, waitForPortFree, psExec } from "../platform";
|
|
4
|
-
import { error, announce } from "../logger";
|
|
5
|
-
import { validateDirectory, parseEnvString } from "../utils";
|
|
6
|
-
import { parseConfigFile } from "../config";
|
|
7
|
-
import { $ } from "bun";
|
|
8
|
-
import { sleep } from "bun";
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
import { createMeasure } from "measure-fn";
|
|
11
|
-
|
|
12
|
-
const homePath = getHomeDir();
|
|
13
|
-
const run = createMeasure('run');
|
|
14
|
-
|
|
15
|
-
export async function handleRun(options: CommandOptions) {
|
|
16
|
-
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
17
|
-
|
|
18
|
-
const existingProcess = name ? getProcess(name) : null;
|
|
19
|
-
|
|
20
|
-
// Auto-start unmet dependencies before starting this process
|
|
21
|
-
if (name && existingProcess) {
|
|
22
|
-
const { getUnmetDeps } = await import('../deps');
|
|
23
|
-
const unmet = await getUnmetDeps(name);
|
|
24
|
-
if (unmet.length > 0) {
|
|
25
|
-
await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
|
|
26
|
-
for (const depName of unmet) {
|
|
27
|
-
const depProc = getProcess(depName);
|
|
28
|
-
if (depProc) {
|
|
29
|
-
announce(`📦 Starting dependency "${depName}" for "${name}"`, 'Dependency');
|
|
30
|
-
await handleRun({ action: 'run', name: depName, force: true, remoteName: '' });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (existingProcess) {
|
|
38
|
-
const finalDirectory = directory || existingProcess.workdir;
|
|
39
|
-
validateDirectory(finalDirectory);
|
|
40
|
-
$.cwd(finalDirectory);
|
|
41
|
-
|
|
42
|
-
if (fetch) {
|
|
43
|
-
if (!require('fs').existsSync(require('path').join(finalDirectory, '.git'))) {
|
|
44
|
-
error(`Cannot --fetch: '${finalDirectory}' is not a Git repository.`);
|
|
45
|
-
}
|
|
46
|
-
await run.measure(`Git fetch "${name}"`, async () => {
|
|
47
|
-
try {
|
|
48
|
-
await $`git fetch origin`;
|
|
49
|
-
const localHash = (await $`git rev-parse HEAD`.text()).trim();
|
|
50
|
-
const remoteHash = (await $`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
|
|
51
|
-
|
|
52
|
-
if (localHash !== remoteHash) {
|
|
53
|
-
await $`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
|
|
54
|
-
announce("📥 Pulled latest changes", "Git Update");
|
|
55
|
-
}
|
|
56
|
-
} catch (err) {
|
|
57
|
-
error(`Failed to pull latest changes: ${err}`);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const isRunning = await isProcessRunning(existingProcess.pid);
|
|
63
|
-
if (isRunning && !force) {
|
|
64
|
-
error(`Process '${name}' is currently running. Use --force to restart.`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Detect ports BEFORE killing so we can clean them up
|
|
68
|
-
let detectedPorts: number[] = [];
|
|
69
|
-
if (isRunning) {
|
|
70
|
-
detectedPorts = await getProcessPorts(existingProcess.pid);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (isRunning) {
|
|
74
|
-
await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
|
|
75
|
-
await terminateProcess(existingProcess.pid);
|
|
76
|
-
announce(`🔥 Terminated existing process '${name}'`, "Process Terminated");
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Kill anything still on the ports the old process was using
|
|
81
|
-
if (detectedPorts.length > 0) {
|
|
82
|
-
await run.measure(`Port cleanup [${detectedPorts.join(', ')}]`, async () => {
|
|
83
|
-
for (const port of detectedPorts) {
|
|
84
|
-
await killProcessOnPort(port);
|
|
85
|
-
}
|
|
86
|
-
for (const port of detectedPorts) {
|
|
87
|
-
const freed = await waitForPortFree(port, 5000);
|
|
88
|
-
if (!freed) {
|
|
89
|
-
await killProcessOnPort(port);
|
|
90
|
-
await waitForPortFree(port, 3000);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Zombie sweep: kill any remaining bun processes matching this command
|
|
97
|
-
// This catches orphaned children that survived taskkill when the parent shell exited
|
|
98
|
-
// IMPORTANT: Exclude the current bgrun process and dashboard to avoid self-kill
|
|
99
|
-
const cmdToMatch = existingProcess.command;
|
|
100
|
-
if (cmdToMatch) {
|
|
101
|
-
await run.measure('Zombie sweep', async () => {
|
|
102
|
-
try {
|
|
103
|
-
const cmdKeyword = cmdToMatch.split(' ')[1] || cmdToMatch;
|
|
104
|
-
// Skip sweep if keyword is too generic (would match unrelated processes)
|
|
105
|
-
const GENERIC_KEYWORDS = ['dev', 'run', 'start', 'serve', 'build', 'test'];
|
|
106
|
-
if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
|
|
107
|
-
return; // Too dangerous — skip zombie sweep for generic commands
|
|
108
|
-
}
|
|
109
|
-
const currentPid = process.pid;
|
|
110
|
-
const result = await psExec(
|
|
111
|
-
`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${cmdKeyword.replace(/'/g, "''")}' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`,
|
|
112
|
-
3000
|
|
113
|
-
);
|
|
114
|
-
const zombiePids = result.split('\n')
|
|
115
|
-
.map((l: string) => parseInt(l.trim()))
|
|
116
|
-
.filter((n: number) => !isNaN(n) && n > 0 && n !== currentPid);
|
|
117
|
-
for (const zPid of zombiePids) {
|
|
118
|
-
await $`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
119
|
-
}
|
|
120
|
-
if (zombiePids.length > 0) {
|
|
121
|
-
announce(`🧹 Swept ${zombiePids.length} zombie process(es)`, 'Zombie Cleanup');
|
|
122
|
-
}
|
|
123
|
-
} catch { /* best effort */ }
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
await retryDatabaseOperation(() =>
|
|
128
|
-
removeProcessByName(name!)
|
|
129
|
-
);
|
|
130
|
-
} else {
|
|
131
|
-
if (!directory || !name || !command) {
|
|
132
|
-
error("'directory', 'name', and 'command' parameters are required for new processes.");
|
|
133
|
-
}
|
|
134
|
-
validateDirectory(directory!);
|
|
135
|
-
$.cwd(directory!);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const finalCommand = command || existingProcess!.command;
|
|
139
|
-
const finalDirectory = directory || (existingProcess?.workdir!);
|
|
140
|
-
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
141
|
-
|
|
142
|
-
// Default BGR_KEEP_ALIVE=true for new processes unless explicitly disabled
|
|
143
|
-
if (!('BGR_KEEP_ALIVE' in finalEnv)) {
|
|
144
|
-
finalEnv.BGR_KEEP_ALIVE = 'true';
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let finalConfigPath: string | undefined | null;
|
|
148
|
-
if (configPath !== undefined) {
|
|
149
|
-
finalConfigPath = configPath;
|
|
150
|
-
} else if (existingProcess) {
|
|
151
|
-
finalConfigPath = existingProcess.configPath;
|
|
152
|
-
} else {
|
|
153
|
-
finalConfigPath = '.config.toml';
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (finalConfigPath) {
|
|
157
|
-
const fullConfigPath = join(finalDirectory, finalConfigPath);
|
|
158
|
-
|
|
159
|
-
if (await Bun.file(fullConfigPath).exists()) {
|
|
160
|
-
const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
|
|
161
|
-
try {
|
|
162
|
-
return await parseConfigFile(fullConfigPath);
|
|
163
|
-
} catch (err: any) {
|
|
164
|
-
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
if (configEnv) {
|
|
169
|
-
finalEnv = { ...finalEnv, ...configEnv };
|
|
170
|
-
console.log(`Loaded config from ${finalConfigPath}`);
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const stdoutPath = stdout || existingProcess?.stdout_path || join(homePath, ".bgr", `${name}-out.txt`);
|
|
178
|
-
Bun.write(stdoutPath, '');
|
|
179
|
-
const stderrPath = stderr || existingProcess?.stderr_path || join(homePath, ".bgr", `${name}-err.txt`);
|
|
180
|
-
Bun.write(stderrPath, '');
|
|
181
|
-
|
|
182
|
-
const actualPid = await run.measure(`Spawn "${name}" → ${finalCommand}`, async () => {
|
|
183
|
-
const newProcess = Bun.spawn(getShellCommand(finalCommand!), {
|
|
184
|
-
env: { ...Bun.env, ...finalEnv },
|
|
185
|
-
cwd: finalDirectory,
|
|
186
|
-
stdout: Bun.file(stdoutPath),
|
|
187
|
-
stderr: Bun.file(stderrPath),
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
newProcess.unref();
|
|
191
|
-
// Give shell a moment to spawn child, then find PID before shell exits
|
|
192
|
-
await sleep(100);
|
|
193
|
-
// Find the actual child PID (shell wrapper exits immediately after spawning)
|
|
194
|
-
const pid = await findChildPid(newProcess.pid);
|
|
195
|
-
// Wait more for subprocess to initialize
|
|
196
|
-
await sleep(400);
|
|
197
|
-
return pid;
|
|
198
|
-
}) ?? 0;
|
|
199
|
-
|
|
200
|
-
await retryDatabaseOperation(() =>
|
|
201
|
-
insertProcess({
|
|
202
|
-
pid: actualPid,
|
|
203
|
-
workdir: finalDirectory,
|
|
204
|
-
command: finalCommand!,
|
|
205
|
-
name: name!,
|
|
206
|
-
env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
|
|
207
|
-
configPath: finalConfigPath || '',
|
|
208
|
-
stdout_path: stdoutPath,
|
|
209
|
-
stderr_path: stderrPath,
|
|
210
|
-
})
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
announce(
|
|
214
|
-
`${existingProcess ? '🔄 Restarted' : '🚀 Launched'} process "${name}" with PID ${actualPid}`,
|
|
215
|
-
"Process Started"
|
|
216
|
-
);
|
|
217
|
-
}
|
package/src/commands/watch.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import type { CommandOptions } from "../types";
|
|
2
|
-
import type { Process } from "../db";
|
|
3
|
-
import { getProcess } from "../db";
|
|
4
|
-
import { isProcessRunning } from "../platform";
|
|
5
|
-
import { error, announce } from "../logger";
|
|
6
|
-
import { tailFile } from "../utils";
|
|
7
|
-
import { handleRun } from "./run";
|
|
8
|
-
import * as fs from "fs";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import chalk, { type ChalkInstance } from "chalk";
|
|
11
|
-
|
|
12
|
-
export async function handleWatch(options: CommandOptions, logOptions: { showLogs: boolean; logType: 'stdout' | 'stderr' | 'both', lines?: number }) {
|
|
13
|
-
let currentProcess: Process | null = null;
|
|
14
|
-
let isRestarting = false;
|
|
15
|
-
let debounceTimeout: Timer | null = null;
|
|
16
|
-
let tailStops: (() => void)[] = [];
|
|
17
|
-
let lastRestartPath: string | null = null; // Track if restart was due to file change
|
|
18
|
-
|
|
19
|
-
const dumpLogsIfDead = async (proc: Process, reason: string) => {
|
|
20
|
-
const isDead = !(await isProcessRunning(proc.pid));
|
|
21
|
-
if (!isDead) return false;
|
|
22
|
-
|
|
23
|
-
console.log(chalk.yellow(`💀 Process '${options.name}' died immediately after ${reason}—dumping logs:`));
|
|
24
|
-
|
|
25
|
-
const readAndDump = (path: string, color: ChalkInstance, label: string) => {
|
|
26
|
-
try {
|
|
27
|
-
if (fs.existsSync(path)) {
|
|
28
|
-
const content = fs.readFileSync(path, 'utf8').trim();
|
|
29
|
-
if (content) {
|
|
30
|
-
console.log(`${color.bold(label)}:\n${color(content)}\n`);
|
|
31
|
-
} else {
|
|
32
|
-
console.log(`${color(label)}: (empty)`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.warn(chalk.gray(`Could not read ${label} log: ${err}`));
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
if (logOptions.logType === 'both' || logOptions.logType === 'stdout') {
|
|
41
|
-
readAndDump(proc.stdout_path, chalk.white, '📄 Stdout');
|
|
42
|
-
}
|
|
43
|
-
if (logOptions.logType === 'both' || logOptions.logType === 'stderr') {
|
|
44
|
-
readAndDump(proc.stderr_path, chalk.red, '📄 Stderr');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return true;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const waitForLogReady = (logPath: string, timeoutMs = 5000): Promise<void> => {
|
|
51
|
-
return new Promise((resolve, reject) => {
|
|
52
|
-
const checkReady = (): boolean => {
|
|
53
|
-
try {
|
|
54
|
-
if (fs.existsSync(logPath)) {
|
|
55
|
-
const stat = fs.statSync(logPath);
|
|
56
|
-
if (stat.size > 0) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Ignore errors during check
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
if (checkReady()) {
|
|
67
|
-
resolve();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const dir = path.dirname(logPath); // path module needs import. 'path' var name conflict?
|
|
72
|
-
// Need import path from 'path'
|
|
73
|
-
const filename = path.basename(logPath);
|
|
74
|
-
// ERROR: 'path' refers to module or var?
|
|
75
|
-
// I need to import { dirname, basename } from "path";
|
|
76
|
-
|
|
77
|
-
const watcher = fs.watch(dir, (eventType, changedFilename) => {
|
|
78
|
-
if (changedFilename === filename && eventType === 'change') {
|
|
79
|
-
if (checkReady()) {
|
|
80
|
-
watcher.close();
|
|
81
|
-
resolve();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
setTimeout(() => {
|
|
87
|
-
watcher.close();
|
|
88
|
-
reject(new Error(`Log file ${logPath} did not become ready within ${timeoutMs}ms`));
|
|
89
|
-
}, timeoutMs);
|
|
90
|
-
});
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const startTails = async (): Promise<(() => void)[]> => {
|
|
94
|
-
const stops: (() => void)[] = [];
|
|
95
|
-
|
|
96
|
-
if (!logOptions.showLogs || !currentProcess) return stops;
|
|
97
|
-
|
|
98
|
-
console.log(chalk.gray("\n" + '─'.repeat(50) + "\n"));
|
|
99
|
-
|
|
100
|
-
if (logOptions.logType === 'both' || logOptions.logType === 'stdout') {
|
|
101
|
-
console.log(chalk.green.bold(`📄 Tailing stdout for ${options.name}:`));
|
|
102
|
-
console.log(chalk.gray('═'.repeat(50)));
|
|
103
|
-
try {
|
|
104
|
-
await waitForLogReady(currentProcess.stdout_path);
|
|
105
|
-
} catch (err: any) {
|
|
106
|
-
console.warn(chalk.yellow(`⚠️ Stdout log not ready yet for ${options.name}—starting tail anyway: ${err.message}`));
|
|
107
|
-
}
|
|
108
|
-
const stop = tailFile(currentProcess.stdout_path, '', chalk.white, logOptions.lines);
|
|
109
|
-
stops.push(stop);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (logOptions.logType === 'both' || logOptions.logType === 'stderr') {
|
|
113
|
-
console.log(chalk.red.bold(`📄 Tailing stderr for ${options.name}:`));
|
|
114
|
-
console.log(chalk.gray('═'.repeat(50)));
|
|
115
|
-
try {
|
|
116
|
-
await waitForLogReady(currentProcess.stderr_path);
|
|
117
|
-
} catch (err: any) {
|
|
118
|
-
console.warn(chalk.yellow(`⚠️ Stderr log not ready yet for ${options.name}—starting tail anyway: ${err.message}`));
|
|
119
|
-
}
|
|
120
|
-
const stop = tailFile(currentProcess.stderr_path, '', chalk.red, logOptions.lines);
|
|
121
|
-
stops.push(stop);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return stops;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const restartProcess = async (path?: string) => {
|
|
128
|
-
if (isRestarting) return;
|
|
129
|
-
isRestarting = true;
|
|
130
|
-
const restartReason = path ? `restart (change in ${path})` : 'initial start';
|
|
131
|
-
lastRestartPath = path || null;
|
|
132
|
-
|
|
133
|
-
tailStops.forEach(stop => stop());
|
|
134
|
-
tailStops = [];
|
|
135
|
-
|
|
136
|
-
console.clear();
|
|
137
|
-
announce(`🔄 Restarting process '${options.name}'... [${restartReason}]`, "Watch Mode");
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
await handleRun({ ...options, force: true });
|
|
141
|
-
currentProcess = getProcess(options.name!); // Need to ensure name is string
|
|
142
|
-
|
|
143
|
-
if (!currentProcess) {
|
|
144
|
-
error(`Failed to find process '${options.name}' after restart.`);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Quick post-mortem if it died on startup
|
|
149
|
-
const died = await dumpLogsIfDead(currentProcess, restartReason);
|
|
150
|
-
if (died) {
|
|
151
|
-
if (lastRestartPath) {
|
|
152
|
-
console.log(chalk.yellow(`⚠️ Compile error on change—pausing restarts until manual fix.`));
|
|
153
|
-
return; // Avoid loop on bad code
|
|
154
|
-
} else {
|
|
155
|
-
error(`Failed to start process '${options.name}'. Aborting watch mode.`);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
tailStops = await startTails();
|
|
161
|
-
} catch (err) {
|
|
162
|
-
error(`Error during restart: ${err}`);
|
|
163
|
-
} finally {
|
|
164
|
-
isRestarting = false;
|
|
165
|
-
if (currentProcess) {
|
|
166
|
-
console.log(chalk.cyan(`\n👀 Watching for file changes in: ${currentProcess.workdir}`));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
// Initial start
|
|
172
|
-
console.clear();
|
|
173
|
-
announce(`🚀 Starting initial process '${options.name}' in watch mode...`, "Watch Mode");
|
|
174
|
-
await handleRun(options);
|
|
175
|
-
currentProcess = getProcess(options.name!);
|
|
176
|
-
|
|
177
|
-
if (!currentProcess) {
|
|
178
|
-
error(`Could not start or find process '${options.name}'. Aborting watch mode.`);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Quick post-mortem if initial died
|
|
183
|
-
const initialDied = await dumpLogsIfDead(currentProcess, 'initial start');
|
|
184
|
-
if (initialDied) {
|
|
185
|
-
error(`Failed to start process '${options.name}'. Aborting watch mode.`);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
tailStops = await startTails();
|
|
190
|
-
|
|
191
|
-
const workdir = currentProcess.workdir;
|
|
192
|
-
console.log(chalk.cyan(`\n👀 Watching for file changes in: ${workdir}`));
|
|
193
|
-
|
|
194
|
-
const watcher = fs.watch(workdir, { recursive: true }, (eventType, filename) => {
|
|
195
|
-
if (filename == null) return;
|
|
196
|
-
const fullPath = path.join(workdir, filename as string);
|
|
197
|
-
if (fullPath.includes(".git") || fullPath.includes("node_modules")) return;
|
|
198
|
-
if (debounceTimeout) clearTimeout(debounceTimeout);
|
|
199
|
-
debounceTimeout = setTimeout(() => restartProcess(fullPath), 500);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const cleanup = async () => {
|
|
203
|
-
console.log(chalk.magenta('\nSIGINT received...'));
|
|
204
|
-
watcher.close();
|
|
205
|
-
tailStops.forEach(stop => stop());
|
|
206
|
-
if (debounceTimeout) clearTimeout(debounceTimeout);
|
|
207
|
-
|
|
208
|
-
const procToKill = getProcess(options.name!);
|
|
209
|
-
if (procToKill) {
|
|
210
|
-
const isRunning = await isProcessRunning(procToKill.pid);
|
|
211
|
-
if (isRunning) {
|
|
212
|
-
// @note avoid "await terminateProcess(procToKill.pid)" because we can re-attach --watch mode to running process
|
|
213
|
-
console.log(`process ${procToKill.name} (PID: ${procToKill.pid}) still running`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
process.exit(0);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
process.on('SIGINT', cleanup);
|
|
220
|
-
process.on('SIGTERM', cleanup);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
package/src/config.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
function formatEnvKey(key: string): string {
|
|
5
|
-
return key.toUpperCase().replace(/\./g, '_');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function flattenConfig(obj: any, prefix = ''): Record<string, string> {
|
|
9
|
-
return Object.keys(obj).reduce((acc: Record<string, string>, key: string) => {
|
|
10
|
-
const value = obj[key];
|
|
11
|
-
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
12
|
-
|
|
13
|
-
if (Array.isArray(value)) {
|
|
14
|
-
value.forEach((item, index) => {
|
|
15
|
-
const indexedPrefix = `${newPrefix}.${index}`;
|
|
16
|
-
if (typeof item === 'object' && item !== null) {
|
|
17
|
-
Object.assign(acc, flattenConfig(item, indexedPrefix));
|
|
18
|
-
} else {
|
|
19
|
-
acc[formatEnvKey(indexedPrefix)] = String(item);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
23
|
-
Object.assign(acc, flattenConfig(value, newPrefix));
|
|
24
|
-
} else {
|
|
25
|
-
acc[formatEnvKey(newPrefix)] = String(value);
|
|
26
|
-
}
|
|
27
|
-
return acc;
|
|
28
|
-
}, {});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export async function parseConfigFile(configPath: string): Promise<Record<string, string>> {
|
|
33
|
-
// @note t suffix solves caching issue with env variables when using --watch flag
|
|
34
|
-
const importPath = `${configPath}?t=${Date.now()}`;
|
|
35
|
-
const parsedConfig = await import(importPath).then(m => m.default);
|
|
36
|
-
return flattenConfig(parsedConfig);
|
|
37
|
-
}
|