neex 0.1.8 → 0.2.6
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 +72 -238
- package/bun.lock +669 -0
- package/dist/src/cli.js +457 -24
- package/dist/src/index.js +1 -3
- package/dist/src/logger.js +0 -36
- package/dist/src/process-manager.js +146 -607
- package/dist/src/runner.js +78 -112
- package/package.json +7 -5
- package/dist/src/commands/dev-commands.js +0 -190
- package/dist/src/commands/index.js +0 -21
- package/dist/src/commands/process-commands.js +0 -679
- package/dist/src/commands/run-commands.js +0 -87
- package/dist/src/commands/server-commands.js +0 -50
- package/dist/src/dev-runner.js +0 -209
- package/dist/src/utils.js +0 -10
- package/dist/src/watcher.js +0 -245
- package/feet.txt +0 -16
|
@@ -1,669 +1,208 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
-
};
|
|
28
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.
|
|
30
|
-
// src/process-manager.ts - Enhanced PM2-like process manager
|
|
3
|
+
exports.processManager = void 0;
|
|
31
4
|
const child_process_1 = require("child_process");
|
|
32
|
-
const
|
|
5
|
+
const chokidar_1 = require("chokidar");
|
|
33
6
|
const fs_1 = require("fs");
|
|
34
|
-
const
|
|
7
|
+
const path_1 = require("path");
|
|
35
8
|
const events_1 = require("events");
|
|
36
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
37
|
-
const figures_1 = __importDefault(require("figures"));
|
|
38
|
-
const logger_js_1 = __importDefault(require("./logger.js"));
|
|
39
9
|
class ProcessManager extends events_1.EventEmitter {
|
|
40
|
-
constructor(
|
|
10
|
+
constructor() {
|
|
41
11
|
super();
|
|
42
12
|
this.processes = new Map();
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
46
|
-
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
this.pidDir = path.join(baseDir, 'pids');
|
|
50
|
-
this.ensureDirectories();
|
|
51
|
-
this.startMonitoring();
|
|
13
|
+
this.watchers = new Map();
|
|
14
|
+
this.defaultMaxRestarts = 5;
|
|
15
|
+
this.defaultRestartDelay = 1000; // 1 second
|
|
16
|
+
this.logDir = (0, path_1.resolve)(process.cwd(), '.neex-logs');
|
|
17
|
+
this.configFile = (0, path_1.resolve)(process.cwd(), '.neex-config.json');
|
|
18
|
+
this.initialize();
|
|
52
19
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
path.dirname(this.configPath),
|
|
57
|
-
this.logDir,
|
|
58
|
-
this.pidDir
|
|
59
|
-
];
|
|
60
|
-
for (const dir of dirs) {
|
|
61
|
-
await fs_1.promises.mkdir(dir, { recursive: true });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
logger_js_1.default.printLine(`Failed to create directories: ${error.message}`, 'error');
|
|
20
|
+
initialize() {
|
|
21
|
+
if (!(0, fs_1.existsSync)(this.logDir)) {
|
|
22
|
+
(0, fs_1.mkdirSync)(this.logDir, { recursive: true });
|
|
66
23
|
}
|
|
24
|
+
this.loadConfig();
|
|
67
25
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const config =
|
|
71
|
-
|
|
72
|
-
return ({
|
|
73
|
-
...proc.config,
|
|
74
|
-
status: proc.status,
|
|
75
|
-
created_at: proc.created_at.toISOString(),
|
|
76
|
-
started_at: (_a = proc.started_at) === null || _a === void 0 ? void 0 : _a.toISOString(),
|
|
77
|
-
restarts: proc.restarts,
|
|
78
|
-
last_restart: (_b = proc.last_restart) === null || _b === void 0 ? void 0 : _b.toISOString()
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
await fs_1.promises.writeFile(this.configPath, JSON.stringify(config, null, 2));
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
logger_js_1.default.printLine(`Failed to save config: ${error.message}`, 'error');
|
|
26
|
+
loadConfig() {
|
|
27
|
+
if ((0, fs_1.existsSync)(this.configFile)) {
|
|
28
|
+
const config = JSON.parse((0, fs_1.readFileSync)(this.configFile, 'utf-8'));
|
|
29
|
+
this.processes = new Map(Object.entries(config));
|
|
85
30
|
}
|
|
86
31
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const configs = JSON.parse(data);
|
|
91
|
-
for (const config of configs) {
|
|
92
|
-
if (config.id && !this.processes.has(config.id)) {
|
|
93
|
-
const processInfo = {
|
|
94
|
-
id: config.id,
|
|
95
|
-
name: config.name,
|
|
96
|
-
status: 'stopped',
|
|
97
|
-
uptime: 0,
|
|
98
|
-
restarts: config.restarts || 0,
|
|
99
|
-
memory: 0,
|
|
100
|
-
cpu: 0,
|
|
101
|
-
created_at: config.created_at ? new Date(config.created_at) : new Date(),
|
|
102
|
-
started_at: config.started_at ? new Date(config.started_at) : undefined,
|
|
103
|
-
last_restart: config.last_restart ? new Date(config.last_restart) : undefined,
|
|
104
|
-
config: {
|
|
105
|
-
...config,
|
|
106
|
-
// Ensure required fields have defaults
|
|
107
|
-
autorestart: config.autorestart !== false,
|
|
108
|
-
max_restarts: config.max_restarts || 10,
|
|
109
|
-
restart_delay: config.restart_delay || 1000,
|
|
110
|
-
instances: config.instances || 1
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
this.processes.set(config.id, processInfo);
|
|
114
|
-
// Check if process is actually running
|
|
115
|
-
if (config.pid) {
|
|
116
|
-
const isRunning = await this.isProcessRunning(config.pid);
|
|
117
|
-
if (isRunning) {
|
|
118
|
-
processInfo.status = 'online';
|
|
119
|
-
processInfo.pid = config.pid;
|
|
120
|
-
processInfo.started_at = processInfo.started_at || new Date();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
// Config file might not exist yet, that's fine
|
|
128
|
-
}
|
|
32
|
+
saveConfig() {
|
|
33
|
+
const config = Object.fromEntries(this.processes);
|
|
34
|
+
(0, fs_1.writeFileSync)(this.configFile, JSON.stringify(config, null, 2));
|
|
129
35
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
process.kill(pid, 0);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
generateId(name) {
|
|
140
|
-
let id = name.toLowerCase().replace(/[^a-z0-9-_]/g, '-');
|
|
141
|
-
let counter = 0;
|
|
142
|
-
while (this.processes.has(id)) {
|
|
143
|
-
counter++;
|
|
144
|
-
id = `${name.toLowerCase().replace(/[^a-z0-9-_]/g, '-')}-${counter}`;
|
|
145
|
-
}
|
|
146
|
-
return id;
|
|
147
|
-
}
|
|
148
|
-
setupLogStreams(processInfo) {
|
|
149
|
-
const { config } = processInfo;
|
|
150
|
-
// Close existing streams
|
|
151
|
-
this.closeLogStreams(processInfo.id);
|
|
152
|
-
// Setup default log paths
|
|
153
|
-
if (!config.out_file) {
|
|
154
|
-
config.out_file = path.join(this.logDir, `${processInfo.id}-out.log`);
|
|
155
|
-
}
|
|
156
|
-
if (!config.error_file) {
|
|
157
|
-
config.error_file = path.join(this.logDir, `${processInfo.id}-error.log`);
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
const outStream = fs.createWriteStream(config.out_file, { flags: 'a' });
|
|
161
|
-
const errStream = fs.createWriteStream(config.error_file, { flags: 'a' });
|
|
162
|
-
this.logStreams.set(processInfo.id, { out: outStream, err: errStream });
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
logger_js_1.default.printLine(`Failed to create log streams for ${processInfo.id}: ${error.message}`, 'error');
|
|
166
|
-
}
|
|
36
|
+
generateId() {
|
|
37
|
+
return Math.random().toString(36).substring(2, 15);
|
|
167
38
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const streams = this.logStreams.get(id);
|
|
171
|
-
if (streams) {
|
|
172
|
-
(_a = streams.out) === null || _a === void 0 ? void 0 : _a.end();
|
|
173
|
-
(_b = streams.err) === null || _b === void 0 ? void 0 : _b.end();
|
|
174
|
-
this.logStreams.delete(id);
|
|
175
|
-
}
|
|
39
|
+
getLogPath(id) {
|
|
40
|
+
return (0, path_1.join)(this.logDir, `${id}.log`);
|
|
176
41
|
}
|
|
177
|
-
async
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
logger_js_1.default.printLine(`Failed to write PID file: ${error.message}`, 'error');
|
|
42
|
+
async startx(path, options = {}) {
|
|
43
|
+
const id = this.generateId();
|
|
44
|
+
const name = options.name || `process-${id}`;
|
|
45
|
+
const fullPath = (0, path_1.resolve)(process.cwd(), path);
|
|
46
|
+
if (!(0, fs_1.existsSync)(fullPath)) {
|
|
47
|
+
throw new Error(`Path ${path} does not exist`);
|
|
186
48
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
49
|
+
const processInfo = {
|
|
50
|
+
id,
|
|
51
|
+
name,
|
|
52
|
+
pid: 0,
|
|
53
|
+
status: 'stopped',
|
|
54
|
+
startTime: Date.now(),
|
|
55
|
+
path: fullPath,
|
|
56
|
+
command: `node ${fullPath}`,
|
|
57
|
+
logs: [],
|
|
58
|
+
memory: 0,
|
|
59
|
+
cpu: 0,
|
|
60
|
+
uptime: 0,
|
|
61
|
+
restarts: 0,
|
|
62
|
+
maxRestarts: options.maxRestarts || this.defaultMaxRestarts,
|
|
63
|
+
restartDelay: options.restartDelay || this.defaultRestartDelay,
|
|
64
|
+
watch: options.watch
|
|
65
|
+
};
|
|
190
66
|
try {
|
|
191
|
-
await
|
|
67
|
+
await this.startProcess(processInfo);
|
|
68
|
+
return processInfo;
|
|
192
69
|
}
|
|
193
70
|
catch (error) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (script.includes(' ')) {
|
|
200
|
-
const parts = script.split(' ');
|
|
201
|
-
return {
|
|
202
|
-
command: parts[0],
|
|
203
|
-
args: parts.slice(1)
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
// Handle file extensions
|
|
207
|
-
if (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.cjs')) {
|
|
208
|
-
return {
|
|
209
|
-
command: 'node',
|
|
210
|
-
args: [script]
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (script.endsWith('.ts') || script.endsWith('.mts') || script.endsWith('.cts')) {
|
|
214
|
-
return {
|
|
215
|
-
command: 'npx',
|
|
216
|
-
args: ['ts-node', script]
|
|
217
|
-
};
|
|
71
|
+
processInfo.status = 'error';
|
|
72
|
+
processInfo.lastError = error.message;
|
|
73
|
+
this.processes.set(id, processInfo);
|
|
74
|
+
this.saveConfig();
|
|
75
|
+
throw error;
|
|
218
76
|
}
|
|
219
|
-
return {
|
|
220
|
-
command: script,
|
|
221
|
-
args: []
|
|
222
|
-
};
|
|
223
77
|
}
|
|
224
78
|
async startProcess(processInfo) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
processInfo.process = childProcess;
|
|
253
|
-
processInfo.pid = childProcess.pid;
|
|
254
|
-
processInfo.status = 'online';
|
|
255
|
-
// Allow the parent process to exit independently of the child
|
|
256
|
-
childProcess.unref();
|
|
257
|
-
// Write PID file
|
|
258
|
-
await this.writePidFile(processInfo);
|
|
259
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${processInfo.id}) started with PID ${processInfo.pid}`));
|
|
260
|
-
this.emit('process:start', processInfo);
|
|
261
|
-
// Setup logging
|
|
262
|
-
const streams = this.logStreams.get(processInfo.id);
|
|
263
|
-
if (childProcess.stdout && (streams === null || streams === void 0 ? void 0 : streams.out)) {
|
|
264
|
-
childProcess.stdout.pipe(streams.out);
|
|
265
|
-
childProcess.stdout.on('data', (data) => {
|
|
266
|
-
var _a;
|
|
267
|
-
const message = data.toString();
|
|
268
|
-
if (config.time) {
|
|
269
|
-
const timestamp = new Date().toISOString();
|
|
270
|
-
(_a = streams.out) === null || _a === void 0 ? void 0 : _a.write(`[${timestamp}] ${message}`);
|
|
271
|
-
}
|
|
272
|
-
this.emit('process:log', {
|
|
273
|
-
id: processInfo.id,
|
|
274
|
-
type: 'stdout',
|
|
275
|
-
data: message
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
if (childProcess.stderr && (streams === null || streams === void 0 ? void 0 : streams.err)) {
|
|
280
|
-
childProcess.stderr.pipe(streams.err);
|
|
281
|
-
childProcess.stderr.on('data', (data) => {
|
|
282
|
-
var _a;
|
|
283
|
-
const message = data.toString();
|
|
284
|
-
if (config.time) {
|
|
285
|
-
const timestamp = new Date().toISOString();
|
|
286
|
-
(_a = streams.err) === null || _a === void 0 ? void 0 : _a.write(`[${timestamp}] ${message}`);
|
|
287
|
-
}
|
|
288
|
-
this.emit('process:log', {
|
|
289
|
-
id: processInfo.id,
|
|
290
|
-
type: 'stderr',
|
|
291
|
-
data: message
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
// Handle process exit
|
|
296
|
-
childProcess.on('exit', async (code, signal) => {
|
|
297
|
-
processInfo.exit_code = code || undefined;
|
|
298
|
-
processInfo.exit_signal = signal || undefined;
|
|
299
|
-
processInfo.status = code === 0 ? 'stopped' : 'errored';
|
|
300
|
-
processInfo.process = undefined;
|
|
301
|
-
processInfo.pid = undefined;
|
|
302
|
-
await this.removePidFile(processInfo.id);
|
|
303
|
-
const exitMsg = signal
|
|
304
|
-
? `Process ${processInfo.name} killed by signal ${signal}`
|
|
305
|
-
: `Process ${processInfo.name} exited with code ${code}`;
|
|
306
|
-
console.log(chalk_1.default.yellow(`${figures_1.default.warning} ${exitMsg}`));
|
|
307
|
-
this.emit('process:exit', { processInfo, code, signal });
|
|
308
|
-
// Auto-restart if enabled and not manually stopped
|
|
309
|
-
if (config.autorestart &&
|
|
310
|
-
processInfo.status === 'errored' &&
|
|
311
|
-
processInfo.restarts < (config.max_restarts || 10)) {
|
|
312
|
-
const delay = config.restart_delay || 1000;
|
|
313
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Restarting ${processInfo.name} in ${delay}ms...`));
|
|
314
|
-
setTimeout(async () => {
|
|
315
|
-
try {
|
|
316
|
-
await this.restart(processInfo.id);
|
|
317
|
-
}
|
|
318
|
-
catch (error) {
|
|
319
|
-
console.error(chalk_1.default.red(`${figures_1.default.cross} Failed to restart ${processInfo.name}: ${error.message}`));
|
|
320
|
-
}
|
|
321
|
-
}, delay);
|
|
322
|
-
}
|
|
323
|
-
await this.saveConfig();
|
|
79
|
+
var _a, _b;
|
|
80
|
+
const child = (0, child_process_1.spawn)('node', [processInfo.path], {
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
env: { ...process.env, NODE_ENV: 'production' }
|
|
83
|
+
});
|
|
84
|
+
processInfo.pid = child.pid || 0;
|
|
85
|
+
processInfo.status = 'running';
|
|
86
|
+
processInfo.startTime = Date.now();
|
|
87
|
+
// Setup logging
|
|
88
|
+
const logStream = (0, fs_1.writeFileSync)(this.getLogPath(processInfo.id), '', { flag: 'a' });
|
|
89
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
90
|
+
const log = data.toString();
|
|
91
|
+
processInfo.logs.push(log);
|
|
92
|
+
(0, fs_1.writeFileSync)(this.getLogPath(processInfo.id), log, { flag: 'a' });
|
|
93
|
+
this.emit('log', { id: processInfo.id, log });
|
|
94
|
+
});
|
|
95
|
+
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
96
|
+
const log = data.toString();
|
|
97
|
+
processInfo.logs.push(log);
|
|
98
|
+
(0, fs_1.writeFileSync)(this.getLogPath(processInfo.id), log, { flag: 'a' });
|
|
99
|
+
this.emit('error', { id: processInfo.id, log });
|
|
100
|
+
});
|
|
101
|
+
// Setup monitoring
|
|
102
|
+
if (processInfo.watch) {
|
|
103
|
+
const watcher = (0, chokidar_1.watch)(processInfo.path, {
|
|
104
|
+
ignored: /(^|[\/\\])\../,
|
|
105
|
+
persistent: true
|
|
324
106
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
processInfo.status = 'errored';
|
|
328
|
-
console.error(chalk_1.default.red(`${figures_1.default.cross} Process ${processInfo.name} error: ${error.message}`));
|
|
329
|
-
this.emit('process:error', { processInfo, error });
|
|
107
|
+
watcher.on('change', () => {
|
|
108
|
+
this.restart(processInfo.id);
|
|
330
109
|
});
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
processInfo.status = 'errored';
|
|
335
|
-
console.error(chalk_1.default.red(`${figures_1.default.cross} Failed to start ${processInfo.name}: ${error.message}`));
|
|
336
|
-
this.emit('process:error', { processInfo, error });
|
|
337
|
-
throw error;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
startMonitoring() {
|
|
341
|
-
if (this.isMonitoring || this.isDisposed)
|
|
342
|
-
return;
|
|
343
|
-
this.isMonitoring = true;
|
|
344
|
-
this.monitoringInterval = setInterval(async () => {
|
|
345
|
-
await this.updateProcessStats();
|
|
346
|
-
}, 5000);
|
|
347
|
-
}
|
|
348
|
-
stopMonitoring() {
|
|
349
|
-
if (this.monitoringInterval) {
|
|
350
|
-
clearInterval(this.monitoringInterval);
|
|
351
|
-
this.monitoringInterval = undefined;
|
|
110
|
+
this.watchers.set(processInfo.id, watcher);
|
|
352
111
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
for (const processInfo of this.processes.values()) {
|
|
357
|
-
if (processInfo.status === 'online' && processInfo.pid) {
|
|
112
|
+
// Monitor process
|
|
113
|
+
const monitorInterval = setInterval(() => {
|
|
114
|
+
if (child.pid) {
|
|
358
115
|
try {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
processInfo.process = undefined;
|
|
364
|
-
processInfo.pid = undefined;
|
|
365
|
-
await this.removePidFile(processInfo.id);
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
// Update uptime
|
|
369
|
-
if (processInfo.started_at) {
|
|
370
|
-
processInfo.uptime = Math.floor((Date.now() - processInfo.started_at.getTime()) / 1000);
|
|
371
|
-
}
|
|
372
|
-
// Get memory and CPU usage (Unix/Linux only)
|
|
373
|
-
if (process.platform !== 'win32') {
|
|
374
|
-
try {
|
|
375
|
-
const stats = await this.getProcessStats(processInfo.pid);
|
|
376
|
-
processInfo.memory = stats.memory;
|
|
377
|
-
processInfo.cpu = stats.cpu;
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
// Process might have died
|
|
381
|
-
}
|
|
382
|
-
}
|
|
116
|
+
const stats = process.memoryUsage();
|
|
117
|
+
processInfo.memory = stats.heapUsed / 1024 / 1024; // MB
|
|
118
|
+
processInfo.uptime = (Date.now() - processInfo.startTime) / 1000;
|
|
119
|
+
this.emit('stats', { id: processInfo.id, stats: processInfo });
|
|
383
120
|
}
|
|
384
121
|
catch (error) {
|
|
385
|
-
|
|
122
|
+
console.error(`Error monitoring process ${processInfo.id}:`, error);
|
|
386
123
|
}
|
|
387
124
|
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
125
|
+
}, 1000);
|
|
126
|
+
child.on('exit', async (code) => {
|
|
127
|
+
clearInterval(monitorInterval);
|
|
128
|
+
if (code !== 0) {
|
|
129
|
+
processInfo.status = 'error';
|
|
130
|
+
processInfo.lastError = `Process exited with code ${code}`;
|
|
131
|
+
this.emit('error', { id: processInfo.id, code });
|
|
132
|
+
// Auto-restart logic
|
|
133
|
+
if (processInfo.restarts < processInfo.maxRestarts) {
|
|
134
|
+
processInfo.status = 'restarting';
|
|
135
|
+
this.emit('restart', { id: processInfo.id, attempt: processInfo.restarts + 1 });
|
|
136
|
+
await new Promise(resolve => setTimeout(resolve, processInfo.restartDelay));
|
|
137
|
+
processInfo.restarts++;
|
|
138
|
+
await this.startProcess(processInfo);
|
|
396
139
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
resolve({ memory: 0, cpu: 0 });
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
const stats = lines[1].trim().split(/\s+/);
|
|
403
|
-
const memory = parseInt(stats[1]) * 1024; // Convert KB to bytes
|
|
404
|
-
const cpu = parseFloat(stats[2]);
|
|
405
|
-
resolve({ memory, cpu });
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
// Public API methods
|
|
410
|
-
async start(config) {
|
|
411
|
-
const id = this.generateId(config.name);
|
|
412
|
-
const processInfo = {
|
|
413
|
-
id,
|
|
414
|
-
name: config.name,
|
|
415
|
-
status: 'stopped',
|
|
416
|
-
uptime: 0,
|
|
417
|
-
restarts: 0,
|
|
418
|
-
memory: 0,
|
|
419
|
-
cpu: 0,
|
|
420
|
-
created_at: new Date(),
|
|
421
|
-
config: { ...config, id }
|
|
422
|
-
};
|
|
423
|
-
this.processes.set(id, processInfo);
|
|
424
|
-
await this.startProcess(processInfo);
|
|
425
|
-
return id;
|
|
426
|
-
}
|
|
427
|
-
async stop(id) {
|
|
428
|
-
const processInfo = this.processes.get(id);
|
|
429
|
-
if (!processInfo) {
|
|
430
|
-
throw new Error(`Process ${id} not found`);
|
|
431
|
-
}
|
|
432
|
-
if (processInfo.status === 'stopped') {
|
|
433
|
-
console.log(chalk_1.default.yellow(`${figures_1.default.info} Process ${id} is already stopped`));
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
processInfo.status = 'stopping';
|
|
437
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Stopping process ${processInfo.name} (${id})...`));
|
|
438
|
-
if (processInfo.process && processInfo.pid) {
|
|
439
|
-
try {
|
|
440
|
-
// Try graceful shutdown first
|
|
441
|
-
processInfo.process.kill('SIGTERM');
|
|
442
|
-
// Wait for graceful shutdown
|
|
443
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
444
|
-
// Force kill if still running
|
|
445
|
-
if (processInfo.process && !processInfo.process.killed) {
|
|
446
|
-
processInfo.process.kill('SIGKILL');
|
|
140
|
+
else {
|
|
141
|
+
this.emit('max-restarts', { id: processInfo.id, maxRestarts: processInfo.maxRestarts });
|
|
447
142
|
}
|
|
448
143
|
}
|
|
449
|
-
|
|
450
|
-
|
|
144
|
+
else {
|
|
145
|
+
processInfo.status = 'stopped';
|
|
451
146
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
processInfo.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
this.closeLogStreams(id);
|
|
458
|
-
await this.saveConfig();
|
|
459
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${id}) stopped`));
|
|
460
|
-
this.emit('process:stop', processInfo);
|
|
461
|
-
}
|
|
462
|
-
async restart(id) {
|
|
463
|
-
const processInfo = this.processes.get(id);
|
|
464
|
-
if (!processInfo) {
|
|
465
|
-
throw new Error(`Process ${id} not found`);
|
|
466
|
-
}
|
|
467
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Restarting process ${processInfo.name} (${id})...`));
|
|
468
|
-
if (processInfo.status === 'online') {
|
|
469
|
-
await this.stop(id);
|
|
470
|
-
// Wait a bit before restarting
|
|
471
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
472
|
-
}
|
|
473
|
-
processInfo.restarts++;
|
|
474
|
-
processInfo.last_restart = new Date();
|
|
475
|
-
await this.startProcess(processInfo);
|
|
147
|
+
this.saveConfig();
|
|
148
|
+
});
|
|
149
|
+
this.processes.set(processInfo.id, processInfo);
|
|
150
|
+
this.saveConfig();
|
|
151
|
+
this.emit('start', { id: processInfo.id, processInfo });
|
|
476
152
|
}
|
|
477
|
-
async
|
|
153
|
+
async stopx(id) {
|
|
478
154
|
const processInfo = this.processes.get(id);
|
|
479
155
|
if (!processInfo) {
|
|
480
156
|
throw new Error(`Process ${id} not found`);
|
|
481
157
|
}
|
|
482
|
-
if (processInfo.status === 'online') {
|
|
483
|
-
await this.stop(id);
|
|
484
|
-
}
|
|
485
|
-
// Remove log files
|
|
486
158
|
try {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
159
|
+
process.kill(processInfo.pid);
|
|
160
|
+
const watcher = this.watchers.get(id);
|
|
161
|
+
if (watcher) {
|
|
162
|
+
watcher.close();
|
|
163
|
+
this.watchers.delete(id);
|
|
492
164
|
}
|
|
165
|
+
this.processes.delete(id);
|
|
166
|
+
this.saveConfig();
|
|
167
|
+
this.emit('stop', { id });
|
|
493
168
|
}
|
|
494
169
|
catch (error) {
|
|
495
|
-
|
|
170
|
+
throw new Error(`Failed to stop process ${id}: ${error.message}`);
|
|
496
171
|
}
|
|
497
|
-
this.closeLogStreams(id);
|
|
498
|
-
this.processes.delete(id);
|
|
499
|
-
await this.saveConfig();
|
|
500
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${id}) deleted`));
|
|
501
|
-
this.emit('process:delete', processInfo);
|
|
502
172
|
}
|
|
503
|
-
|
|
173
|
+
list() {
|
|
504
174
|
return Array.from(this.processes.values());
|
|
505
175
|
}
|
|
506
|
-
|
|
176
|
+
monit(id) {
|
|
507
177
|
const processInfo = this.processes.get(id);
|
|
508
178
|
if (!processInfo) {
|
|
509
179
|
throw new Error(`Process ${id} not found`);
|
|
510
180
|
}
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const logs = [];
|
|
515
|
-
const readLastLines = async (filePath) => {
|
|
516
|
-
try {
|
|
517
|
-
const content = await fs_1.promises.readFile(filePath, 'utf-8');
|
|
518
|
-
const fileLines = content.split('\n').filter(line => line.trim() !== '');
|
|
519
|
-
return fileLines.slice(-lines);
|
|
520
|
-
}
|
|
521
|
-
catch (error) {
|
|
522
|
-
return [];
|
|
523
|
-
}
|
|
181
|
+
return {
|
|
182
|
+
...processInfo,
|
|
183
|
+
logs: processInfo.logs.slice(-100) // Last 100 logs
|
|
524
184
|
};
|
|
525
|
-
if (config.out_file) {
|
|
526
|
-
const outLogs = await readLastLines(config.out_file);
|
|
527
|
-
logs.push(...outLogs.map(line => `[OUT] ${line}`));
|
|
528
|
-
}
|
|
529
|
-
if (config.error_file) {
|
|
530
|
-
const errLogs = await readLastLines(config.error_file);
|
|
531
|
-
logs.push(...errLogs.map(line => `[ERR] ${line}`));
|
|
532
|
-
}
|
|
533
|
-
return logs.slice(-lines);
|
|
534
185
|
}
|
|
535
|
-
|
|
186
|
+
log(id, lines = 100) {
|
|
536
187
|
const processInfo = this.processes.get(id);
|
|
537
188
|
if (!processInfo) {
|
|
538
|
-
|
|
539
|
-
return undefined;
|
|
189
|
+
throw new Error(`Process ${id} not found`);
|
|
540
190
|
}
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const outFile = config.out_file || path.join(this.logDir, `${processInfo.id}-out.log`);
|
|
545
|
-
const errFile = config.error_file || path.join(this.logDir, `${processInfo.id}-error.log`);
|
|
546
|
-
logFilesToWatch.push({ type: 'OUT', path: outFile });
|
|
547
|
-
if (errFile !== outFile) { // Avoid watching the same file path twice
|
|
548
|
-
logFilesToWatch.push({ type: 'ERR', path: errFile });
|
|
191
|
+
const logPath = this.getLogPath(id);
|
|
192
|
+
if (!(0, fs_1.existsSync)(logPath)) {
|
|
193
|
+
return [];
|
|
549
194
|
}
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
try {
|
|
553
|
-
const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : { size: 0 };
|
|
554
|
-
lastReadSizes[filePath] = stats.size;
|
|
555
|
-
}
|
|
556
|
-
catch (err) {
|
|
557
|
-
lastReadSizes[filePath] = 0;
|
|
558
|
-
// logger.printLine(`Could not get initial stats for ${filePath}: ${(err as Error).message}. Assuming size 0.`, 'debug');
|
|
559
|
-
}
|
|
560
|
-
const listener = (curr, prev) => {
|
|
561
|
-
if (curr.mtimeMs <= prev.mtimeMs && curr.size === prev.size) {
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
const newSize = curr.size;
|
|
565
|
-
let oldSize = lastReadSizes[filePath];
|
|
566
|
-
// Fallback for oldSize, though it should be initialized
|
|
567
|
-
if (typeof oldSize !== 'number')
|
|
568
|
-
oldSize = 0;
|
|
569
|
-
if (newSize > oldSize) {
|
|
570
|
-
const stream = fs.createReadStream(filePath, {
|
|
571
|
-
start: oldSize,
|
|
572
|
-
end: newSize - 1,
|
|
573
|
-
encoding: 'utf-8',
|
|
574
|
-
});
|
|
575
|
-
stream.on('data', (chunk) => {
|
|
576
|
-
chunk.split('\n').forEach(line => {
|
|
577
|
-
if (line.trim() !== '') {
|
|
578
|
-
onLogEntry(`[${type}] ${line}`);
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
stream.on('error', (streamErr) => {
|
|
583
|
-
logger_js_1.default.printLine(`Error reading log chunk for ${filePath}: ${streamErr.message}`, 'error');
|
|
584
|
-
});
|
|
585
|
-
lastReadSizes[filePath] = newSize;
|
|
586
|
-
}
|
|
587
|
-
else if (newSize < oldSize) {
|
|
588
|
-
// File was truncated or replaced
|
|
589
|
-
// logger.printLine(`Log file ${filePath} was truncated. Reading from start.`, 'debug');
|
|
590
|
-
lastReadSizes[filePath] = 0;
|
|
591
|
-
const stream = fs.createReadStream(filePath, {
|
|
592
|
-
start: 0,
|
|
593
|
-
end: newSize - 1,
|
|
594
|
-
encoding: 'utf-8',
|
|
595
|
-
});
|
|
596
|
-
stream.on('data', (chunk) => {
|
|
597
|
-
chunk.split('\n').forEach(line => {
|
|
598
|
-
if (line.trim() !== '') {
|
|
599
|
-
onLogEntry(`[${type}] ${line}`);
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
});
|
|
603
|
-
stream.on('error', (streamErr) => {
|
|
604
|
-
logger_js_1.default.printLine(`Error reading truncated log file ${filePath}: ${streamErr.message}`, 'error');
|
|
605
|
-
});
|
|
606
|
-
lastReadSizes[filePath] = newSize;
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
try {
|
|
610
|
-
fs.watchFile(filePath, { persistent: true, interval: 500 }, listener);
|
|
611
|
-
}
|
|
612
|
-
catch (watchError) {
|
|
613
|
-
logger_js_1.default.printLine(`Failed to watch log file ${filePath}: ${watchError.message}`, 'error');
|
|
614
|
-
}
|
|
615
|
-
};
|
|
616
|
-
logFilesToWatch.forEach(fileToWatch => {
|
|
617
|
-
if (fileToWatch.path) { // Ensure path is defined
|
|
618
|
-
setupWatcherForFile(fileToWatch.path, fileToWatch.type);
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
const stop = () => {
|
|
622
|
-
logFilesToWatch.forEach(fileToWatch => {
|
|
623
|
-
if (fileToWatch.path) { // Ensure path is defined before unwatching
|
|
624
|
-
try {
|
|
625
|
-
fs.unwatchFile(fileToWatch.path);
|
|
626
|
-
}
|
|
627
|
-
catch (unwatchError) {
|
|
628
|
-
// logger.printLine(`Error unwatching ${fileToWatch.path}: ${(unwatchError as Error).message}`, 'debug');
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
};
|
|
633
|
-
return { stop };
|
|
634
|
-
}
|
|
635
|
-
async save() {
|
|
636
|
-
await this.saveConfig();
|
|
637
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} Process configuration saved`));
|
|
638
|
-
}
|
|
639
|
-
async stopAll() {
|
|
640
|
-
const promises = Array.from(this.processes.keys()).map(id => this.stop(id));
|
|
641
|
-
await Promise.all(promises);
|
|
642
|
-
}
|
|
643
|
-
async restartAll() {
|
|
644
|
-
const promises = Array.from(this.processes.keys()).map(id => this.restart(id));
|
|
645
|
-
await Promise.all(promises);
|
|
646
|
-
}
|
|
647
|
-
async deleteAll() {
|
|
648
|
-
await this.stopAll();
|
|
649
|
-
this.processes.clear();
|
|
650
|
-
await this.saveConfig();
|
|
651
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} All processes deleted`));
|
|
652
|
-
}
|
|
653
|
-
getProcess(id) {
|
|
654
|
-
return this.processes.get(id);
|
|
195
|
+
const logs = (0, fs_1.readFileSync)(logPath, 'utf-8').split('\n');
|
|
196
|
+
return logs.slice(-lines);
|
|
655
197
|
}
|
|
656
|
-
async
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
this.stopMonitoring();
|
|
661
|
-
// Close all log streams
|
|
662
|
-
for (const id of this.logStreams.keys()) {
|
|
663
|
-
this.closeLogStreams(id);
|
|
198
|
+
async restart(id) {
|
|
199
|
+
const processInfo = this.processes.get(id);
|
|
200
|
+
if (!processInfo) {
|
|
201
|
+
throw new Error(`Process ${id} not found`);
|
|
664
202
|
}
|
|
665
|
-
|
|
666
|
-
|
|
203
|
+
await this.stopx(id);
|
|
204
|
+
processInfo.restarts++;
|
|
205
|
+
await this.startx(processInfo.path, { name: processInfo.name, watch: true });
|
|
667
206
|
}
|
|
668
207
|
}
|
|
669
|
-
exports.
|
|
208
|
+
exports.processManager = new ProcessManager();
|