neex 0.1.5 → 0.1.7
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 +11 -11
- package/dist/src/cli.js +114 -28
- package/dist/src/dev-runner.js +21 -15
- package/dist/src/process-manager.js +109 -13
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</picture>
|
|
7
7
|
</a>
|
|
8
8
|
|
|
9
|
-
# Neex v0.1.
|
|
9
|
+
# Neex v0.1.7
|
|
10
10
|
|
|
11
11
|
### 🚀 Neex: The Modern Build System for Polyrepo-in-Monorepo Architecture
|
|
12
12
|
|
|
@@ -25,7 +25,7 @@ Neex is a modern build system and script runner designed for **Polyrepo-in-Monor
|
|
|
25
25
|
## ✨ Key Features
|
|
26
26
|
|
|
27
27
|
- 🎨 **Colored Output** - Distinguish commands with unique colors
|
|
28
|
-
- ⚡ **Dual Execution Modes** - Run commands in parallel (`
|
|
28
|
+
- ⚡ **Dual Execution Modes** - Run commands in parallel (`px`) or sequence (`s`, `run`)
|
|
29
29
|
- ⏱️ **Smart Timing** - Track execution time for each command
|
|
30
30
|
- 🛑 **Error Control** - Stop on first error (perfect for CI/CD)
|
|
31
31
|
- 🔢 **Parallel Control** - Limit concurrent processes with `--max-parallel`
|
|
@@ -70,7 +70,7 @@ bun add -D neex # bun
|
|
|
70
70
|
|
|
71
71
|
Neex provides several commands to manage and run your scripts:
|
|
72
72
|
|
|
73
|
-
- **`
|
|
73
|
+
- **`px <commands...>`** (*default command*)
|
|
74
74
|
- Runs specified commands in **parallel** by default.
|
|
75
75
|
- Use the `-q` or `--sequential` flag to run them sequentially.
|
|
76
76
|
- Ideal for build steps, tests, or any tasks that can run concurrently.
|
|
@@ -105,9 +105,9 @@ Neex provides several commands to manage and run your scripts:
|
|
|
105
105
|
### General Command Examples
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
# Parallel execution (default behavior for
|
|
109
|
-
neex
|
|
110
|
-
#
|
|
108
|
+
# Parallel execution (default behavior for px)
|
|
109
|
+
neex px "npm run build:api" "npm run build:frontend" "npm run lint"
|
|
110
|
+
# px is the command for parallel execution
|
|
111
111
|
neex px "npm run test:unit" "npm run test:integration"
|
|
112
112
|
|
|
113
113
|
# Sequential execution
|
|
@@ -115,8 +115,8 @@ neex run "npm run clean" "npm run build" "npm run deploy"
|
|
|
115
115
|
# Alias for sequential
|
|
116
116
|
neex s "echo First" "echo Second" "echo Third"
|
|
117
117
|
|
|
118
|
-
# Run '
|
|
119
|
-
neex
|
|
118
|
+
# Run 'px' commands sequentially using the -q flag
|
|
119
|
+
neex px -q "npm run step1" "npm run step2"
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
### `servers` Command Examples
|
|
@@ -167,9 +167,9 @@ neex restart my-app
|
|
|
167
167
|
neex delete my-api
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
### 🛠️ Global Options (for `run`, `
|
|
170
|
+
### 🛠️ Global Options (for `run`, `px`, `servers`)
|
|
171
171
|
|
|
172
|
-
These options can be used with `run`, `
|
|
172
|
+
These options can be used with `run`, `px`, and `servers` commands:
|
|
173
173
|
|
|
174
174
|
| Flag | Alias | Description | Default |
|
|
175
175
|
|--------------------------|-------|---------------------------------------------------|----------------|
|
|
@@ -180,7 +180,7 @@ These options can be used with `run`, `runx`, and `servers` commands:
|
|
|
180
180
|
| `--no-output` | `-o` | Hide all `stdout` and `stderr` from commands | `true` (output on) |
|
|
181
181
|
| `--minimal` | | Use minimal output format (less verbose) | `false` |
|
|
182
182
|
| `--max-parallel <number>`| | Maximum number of commands to run in parallel | CPU count |
|
|
183
|
-
| `--sequential` | `-q` | (For `
|
|
183
|
+
| `--sequential` | `-q` | (For `px`) Run commands sequentially instead of parallel | `false` |
|
|
184
184
|
| `--retry <count>` | | Number of times to retry a failed command | `0` |
|
|
185
185
|
| `--retry-delay <ms>` | | Delay in milliseconds between retries | `1000` |
|
|
186
186
|
| `--group-output` | `-g` | (For `servers`) Group output by server | `false` |
|
package/dist/src/cli.js
CHANGED
|
@@ -34,7 +34,49 @@ const process_manager_js_1 = require("./process-manager.js");
|
|
|
34
34
|
const chalk_1 = __importDefault(require("chalk"));
|
|
35
35
|
const figures_1 = __importDefault(require("figures"));
|
|
36
36
|
const path = __importStar(require("path"));
|
|
37
|
+
const fs = __importStar(require("fs/promises")); // Added for reading package.json
|
|
37
38
|
const { version } = require('../../package.json');
|
|
39
|
+
// Helper function to find default command from package.json
|
|
40
|
+
async function findDefaultCommand() {
|
|
41
|
+
var _a, _b;
|
|
42
|
+
try {
|
|
43
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
44
|
+
await fs.access(packageJsonPath);
|
|
45
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
|
|
46
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
47
|
+
if ((_a = packageJson.scripts) === null || _a === void 0 ? void 0 : _a.dev) {
|
|
48
|
+
console.log(chalk_1.default.blue(`${figures_1.default.info} No command provided. Using "dev" script from package.json: npm run dev`));
|
|
49
|
+
return 'npm run dev';
|
|
50
|
+
}
|
|
51
|
+
if ((_b = packageJson.scripts) === null || _b === void 0 ? void 0 : _b.start) {
|
|
52
|
+
console.log(chalk_1.default.blue(`${figures_1.default.info} No command provided. Using "start" script from package.json: npm run start`));
|
|
53
|
+
return 'npm run start';
|
|
54
|
+
}
|
|
55
|
+
if (packageJson.main) {
|
|
56
|
+
const mainFile = packageJson.main;
|
|
57
|
+
const mainFilePath = path.resolve(process.cwd(), mainFile);
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(mainFilePath);
|
|
60
|
+
if (mainFile.endsWith('.ts') || mainFile.endsWith('.mts') || mainFile.endsWith('.cts')) {
|
|
61
|
+
console.log(chalk_1.default.blue(`${figures_1.default.info} No command or script found. Using "main" field (TypeScript): npx ts-node ${mainFile}`));
|
|
62
|
+
return `npx ts-node ${mainFile}`;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(chalk_1.default.blue(`${figures_1.default.info} No command or script found. Using "main" field (JavaScript): node ${mainFile}`));
|
|
66
|
+
return `node ${mainFile}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
// Main file doesn't exist, do nothing, will fall through to return null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// package.json doesn't exist or other error, do nothing
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
38
80
|
function cli() {
|
|
39
81
|
const program = new commander_1.Command();
|
|
40
82
|
let cleanupRunner = null;
|
|
@@ -80,9 +122,8 @@ function cli() {
|
|
|
80
122
|
});
|
|
81
123
|
// runx command: parallel execution by default (with alias 'p'), can run sequentially with -q
|
|
82
124
|
program
|
|
83
|
-
.command('
|
|
84
|
-
.
|
|
85
|
-
.description('Run commands in parallel (default) or sequentially with -q. Alias: px')
|
|
125
|
+
.command('px <commands...>', { isDefault: true })
|
|
126
|
+
.description('Run commands in parallel (default) or sequentially with -q. This is the default command.')
|
|
86
127
|
.option('-c, --no-color', 'Disable colored output')
|
|
87
128
|
.option('-t, --no-timing', 'Hide timing information')
|
|
88
129
|
.option('-p, --no-prefix', 'Hide command prefix')
|
|
@@ -156,11 +197,11 @@ function cli() {
|
|
|
156
197
|
process.exit(1);
|
|
157
198
|
}
|
|
158
199
|
});
|
|
159
|
-
//
|
|
200
|
+
// Dev command (Nodemon functionality - formerly watch)
|
|
160
201
|
program
|
|
161
|
-
.command('
|
|
162
|
-
.alias('
|
|
163
|
-
.description('Run commands with file watching (nodemon functionality)')
|
|
202
|
+
.command('dev [commands...]') // Made commands optional
|
|
203
|
+
.alias('d')
|
|
204
|
+
.description('Run commands with file watching and auto-restart (nodemon functionality)')
|
|
164
205
|
.option('-c, --no-color', 'Disable colored output')
|
|
165
206
|
.option('-t, --no-timing', 'Hide timing information')
|
|
166
207
|
.option('-p, --no-prefix', 'Hide command prefix')
|
|
@@ -176,21 +217,66 @@ function cli() {
|
|
|
176
217
|
.option('--signal <signal>', 'Signal to send to processes on restart', 'SIGTERM')
|
|
177
218
|
.action(async (commands, options) => {
|
|
178
219
|
try {
|
|
179
|
-
|
|
220
|
+
let effectiveCommands = commands;
|
|
221
|
+
if (!effectiveCommands || effectiveCommands.length === 0) {
|
|
222
|
+
const foundCommand = await findDefaultCommand();
|
|
223
|
+
if (foundCommand) {
|
|
224
|
+
effectiveCommands = [foundCommand];
|
|
225
|
+
console.log(chalk_1.default.blue(`${figures_1.default.info} No command specified for 'neex dev', using default: "${foundCommand}"`));
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.error(chalk_1.default.red(`${figures_1.default.cross} No command specified for 'neex dev' and no default script (dev, start) or main file found in package.json.`));
|
|
229
|
+
console.error(chalk_1.default.yellow(`${figures_1.default.pointer} Please specify a command to run (e.g., neex dev "npm run dev") or define a "dev" or "start" script in your package.json.`));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else { // At least one command/argument is provided
|
|
234
|
+
const firstArg = effectiveCommands[0];
|
|
235
|
+
const remainingArgs = effectiveCommands.slice(1);
|
|
236
|
+
const isLikelyCommandOrScript = firstArg.includes(' ') || firstArg.startsWith('npm') || firstArg.startsWith('yarn') || firstArg.startsWith('pnpm');
|
|
237
|
+
if (!isLikelyCommandOrScript) {
|
|
238
|
+
const filePath = path.resolve(process.cwd(), firstArg);
|
|
239
|
+
try {
|
|
240
|
+
await fs.access(filePath); // Check if file exists
|
|
241
|
+
let commandToExecute = '';
|
|
242
|
+
if (firstArg.endsWith('.js') || firstArg.endsWith('.mjs') || firstArg.endsWith('.cjs')) {
|
|
243
|
+
commandToExecute = `node ${firstArg}`;
|
|
244
|
+
console.log(chalk_1.default.blue(`${figures_1.default.info} Detected .js file, prepending with node.`));
|
|
245
|
+
}
|
|
246
|
+
else if (firstArg.endsWith('.ts') || firstArg.endsWith('.mts') || firstArg.endsWith('.cts')) {
|
|
247
|
+
commandToExecute = `npx ts-node ${firstArg}`;
|
|
248
|
+
console.log(chalk_1.default.blue(`${figures_1.default.info} Detected .ts file, prepending with npx ts-node.`));
|
|
249
|
+
}
|
|
250
|
+
if (commandToExecute) {
|
|
251
|
+
effectiveCommands = [commandToExecute, ...remainingArgs];
|
|
252
|
+
console.log(chalk_1.default.cyan(`${figures_1.default.pointer} Executing: ${effectiveCommands.join(' ')}`));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Not a .js or .ts file, or file doesn't exist but doesn't look like a command either.
|
|
256
|
+
// We'll let it try to run as is. If it was meant to be a file, it will likely fail.
|
|
257
|
+
// If it was a command like 'echo', it will run.
|
|
258
|
+
console.log(chalk_1.default.yellow(`${figures_1.default.warning} First argument "${firstArg}" is not a recognized .js/.ts file and doesn't look like a script. Attempting to run as is.`));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
// File doesn't exist. Could be a command like 'echo foo' where 'echo' is firstArg.
|
|
263
|
+
// Or could be 'my-script --arg' where 'my-script' is not found.
|
|
264
|
+
// We'll let it try to run as is. DevRunner will handle if the command is not found.
|
|
265
|
+
console.log(chalk_1.default.yellow(`${figures_1.default.warning} File "${firstArg}" not found. Attempting to run as command.`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// If isLikelyCommandOrScript is true, or if the file auto-detection didn't apply,
|
|
269
|
+
// effectiveCommands remains as the user provided it (joined later by DevRunner).
|
|
270
|
+
}
|
|
271
|
+
console.log(chalk_1.default.blue(`${figures_1.default.info} Starting development server with file watching (neex dev) for command(s): ${effectiveCommands.map(cmd => `"${cmd}"`).join(' && ')}...`));
|
|
180
272
|
const watchPaths = options.watch || ['./'];
|
|
181
273
|
const ignorePatterns = options.ignore || [
|
|
182
|
-
'node_modules/**',
|
|
183
|
-
'.
|
|
184
|
-
'*.log',
|
|
185
|
-
'dist/**',
|
|
186
|
-
'build/**',
|
|
187
|
-
'coverage/**',
|
|
188
|
-
'.nyc_output/**',
|
|
189
|
-
'*.tmp',
|
|
190
|
-
'*.temp'
|
|
274
|
+
'node_modules/**', '.git/**', '*.log', 'dist/**', 'build/**',
|
|
275
|
+
'coverage/**', '.nyc_output/**', '*.tmp', '*.temp'
|
|
191
276
|
];
|
|
192
277
|
const extensions = options.ext || ['js', 'mjs', 'json', 'ts', 'tsx', 'jsx'];
|
|
193
278
|
devRunner = new dev_runner_js_1.DevRunner({
|
|
279
|
+
runnerName: 'neex dev',
|
|
194
280
|
parallel: false,
|
|
195
281
|
color: options.color,
|
|
196
282
|
showTiming: options.timing,
|
|
@@ -207,16 +293,16 @@ function cli() {
|
|
|
207
293
|
signal: options.signal,
|
|
208
294
|
restartOnChange: true,
|
|
209
295
|
groupOutput: false,
|
|
210
|
-
isServerMode: false
|
|
296
|
+
isServerMode: false
|
|
211
297
|
});
|
|
212
|
-
await devRunner.start(
|
|
298
|
+
await devRunner.start(effectiveCommands);
|
|
213
299
|
}
|
|
214
300
|
catch (error) {
|
|
215
301
|
if (error instanceof Error) {
|
|
216
|
-
console.error(chalk_1.default.red(`${figures_1.default.cross}
|
|
302
|
+
console.error(chalk_1.default.red(`${figures_1.default.cross} Dev Error: ${error.message}`));
|
|
217
303
|
}
|
|
218
304
|
else {
|
|
219
|
-
console.error(chalk_1.default.red(`${figures_1.default.cross} An unknown
|
|
305
|
+
console.error(chalk_1.default.red(`${figures_1.default.cross} An unknown dev error occurred`));
|
|
220
306
|
}
|
|
221
307
|
process.exit(1);
|
|
222
308
|
}
|
|
@@ -406,7 +492,7 @@ function cli() {
|
|
|
406
492
|
.command('logs [id]')
|
|
407
493
|
.description('Show process logs')
|
|
408
494
|
.option('-f, --follow', 'Follow log output')
|
|
409
|
-
.option('-n, --lines <number>', 'Number of lines to show', parseInt)
|
|
495
|
+
.option('-n, --lines <number>', 'Number of lines to show', (value) => parseInt(value, 10), 15)
|
|
410
496
|
.action(async (id, options) => {
|
|
411
497
|
try {
|
|
412
498
|
if (!processManager) {
|
|
@@ -414,7 +500,7 @@ function cli() {
|
|
|
414
500
|
await processManager.load();
|
|
415
501
|
}
|
|
416
502
|
if (id) {
|
|
417
|
-
const logs = await processManager.logs(id, options.lines
|
|
503
|
+
const logs = await processManager.logs(id, options.lines);
|
|
418
504
|
if (logs.length === 0) {
|
|
419
505
|
console.log(chalk_1.default.yellow(`${figures_1.default.info} No logs found for process ${id}`));
|
|
420
506
|
}
|
|
@@ -594,7 +680,7 @@ WantedBy=multi-user.target
|
|
|
594
680
|
program.help();
|
|
595
681
|
}
|
|
596
682
|
// Graceful shutdown handling
|
|
597
|
-
const handleSignal = (signal) => {
|
|
683
|
+
const handleSignal = async (signal) => {
|
|
598
684
|
console.log(`\n${chalk_1.default.yellow(`${figures_1.default.warning} Received ${signal}. Cleaning up...`)}`);
|
|
599
685
|
if (devRunner && devRunner.isActive()) {
|
|
600
686
|
devRunner.stop();
|
|
@@ -603,13 +689,13 @@ WantedBy=multi-user.target
|
|
|
603
689
|
cleanupRunner();
|
|
604
690
|
}
|
|
605
691
|
if (processManager) {
|
|
606
|
-
processManager.dispose();
|
|
692
|
+
await processManager.dispose();
|
|
607
693
|
}
|
|
608
694
|
setTimeout(() => process.exit(0), 500);
|
|
609
695
|
};
|
|
610
|
-
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
611
|
-
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
612
|
-
process.on('SIGQUIT', () => handleSignal('SIGQUIT'));
|
|
696
|
+
process.on('SIGINT', () => handleSignal('SIGINT').catch(err => console.error('SIGINT handler error:', err)));
|
|
697
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM').catch(err => console.error('SIGTERM handler error:', err)));
|
|
698
|
+
process.on('SIGQUIT', () => handleSignal('SIGQUIT').catch(err => console.error('SIGQUIT handler error:', err)));
|
|
613
699
|
}
|
|
614
700
|
exports.default = cli;
|
|
615
701
|
// Helper function to format uptime
|
package/dist/src/dev-runner.js
CHANGED
|
@@ -44,6 +44,7 @@ class DevRunner {
|
|
|
44
44
|
ext: ['js', 'mjs', 'json', 'ts', 'tsx', 'jsx'],
|
|
45
45
|
delay: 1000,
|
|
46
46
|
verbose: false,
|
|
47
|
+
runnerName: 'watch', // Default runner name if not specified
|
|
47
48
|
};
|
|
48
49
|
this.options = {
|
|
49
50
|
...defaultOptions,
|
|
@@ -66,7 +67,8 @@ class DevRunner {
|
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
69
|
async handleFileChange(event) {
|
|
69
|
-
|
|
70
|
+
const prefix = chalk_1.default.cyan(`[${this.options.runnerName}]`);
|
|
71
|
+
logger_1.default.printLine(`${prefix} File changed: ${chalk_1.default.yellow(event.relativePath)}`, 'info');
|
|
70
72
|
if (this.options.clearConsole) {
|
|
71
73
|
console.clear();
|
|
72
74
|
}
|
|
@@ -87,17 +89,18 @@ class DevRunner {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
printDevBanner() {
|
|
90
|
-
var _a;
|
|
92
|
+
var _a, _b;
|
|
93
|
+
const prefix = chalk_1.default.cyan(`[${this.options.runnerName}]`);
|
|
91
94
|
const uptime = Math.floor((Date.now() - this.startTime.getTime()) / 1000);
|
|
92
95
|
const uptimeStr = this.formatUptime(uptime);
|
|
93
|
-
console.log('\n' + chalk_1.default.bgGreen.black(
|
|
96
|
+
console.log('\n' + chalk_1.default.bgGreen.black(` ${(_a = this.options.runnerName) === null || _a === void 0 ? void 0 : _a.toUpperCase()} MODE `) + '\n');
|
|
94
97
|
if (this.restartCount > 0) {
|
|
95
|
-
console.log(chalk_1.default.green(`${figures_1.default.arrowUp} Restarted ${this.restartCount} times`));
|
|
98
|
+
console.log(`${prefix} ${chalk_1.default.green(`${figures_1.default.arrowUp} Restarted ${this.restartCount} times`)}`);
|
|
96
99
|
}
|
|
97
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Uptime: ${uptimeStr}`));
|
|
98
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Watching: ${((
|
|
100
|
+
console.log(`${prefix} ${chalk_1.default.blue(`${figures_1.default.info} Uptime: ${uptimeStr}`)}`);
|
|
101
|
+
console.log(`${prefix} ${chalk_1.default.blue(`${figures_1.default.info} Watching: ${((_b = this.options.watch) === null || _b === void 0 ? void 0 : _b.join(', ')) || 'current directory'}`)}`);
|
|
99
102
|
if (this.options.ext && this.options.ext.length > 0) {
|
|
100
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Extensions: ${this.options.ext.join(', ')}`));
|
|
103
|
+
console.log(`${prefix} ${chalk_1.default.blue(`${figures_1.default.info} Extensions: ${this.options.ext.join(', ')}`)}`);
|
|
101
104
|
}
|
|
102
105
|
console.log('');
|
|
103
106
|
}
|
|
@@ -129,18 +132,20 @@ class DevRunner {
|
|
|
129
132
|
await this.fileWatcher.start();
|
|
130
133
|
}
|
|
131
134
|
// Run initial commands
|
|
132
|
-
|
|
135
|
+
const prefix = chalk_1.default.cyan(`[${this.options.runnerName}]`);
|
|
136
|
+
logger_1.default.printLine(`${prefix} Starting development server...`, 'info');
|
|
133
137
|
await this.runCommands();
|
|
134
138
|
// Set up graceful shutdown
|
|
135
139
|
this.setupGracefulShutdown();
|
|
136
|
-
logger_1.default.printLine(
|
|
137
|
-
logger_1.default.printLine(
|
|
140
|
+
logger_1.default.printLine(`${prefix} Development server started. Watching for changes...`, 'info');
|
|
141
|
+
logger_1.default.printLine(`${prefix} Press ${chalk_1.default.cyan('Ctrl+C')} to stop`, 'info');
|
|
138
142
|
}
|
|
139
143
|
async restart() {
|
|
144
|
+
const prefix = chalk_1.default.cyan(`[${this.options.runnerName}]`);
|
|
140
145
|
if (!this.isRunning) {
|
|
141
146
|
return;
|
|
142
147
|
}
|
|
143
|
-
logger_1.default.printLine(
|
|
148
|
+
logger_1.default.printLine(`${prefix} Restarting due to file changes...`, 'info');
|
|
144
149
|
this.restartCount++;
|
|
145
150
|
// Stop current processes
|
|
146
151
|
if (this.runner) {
|
|
@@ -152,13 +157,14 @@ class DevRunner {
|
|
|
152
157
|
this.printDevBanner();
|
|
153
158
|
// Run commands again
|
|
154
159
|
await this.runCommands();
|
|
155
|
-
logger_1.default.printLine(
|
|
160
|
+
logger_1.default.printLine(`${prefix} Restart completed. Watching for changes...`, 'info');
|
|
156
161
|
}
|
|
157
162
|
async stop() {
|
|
163
|
+
const prefix = chalk_1.default.cyan(`[${this.options.runnerName}]`);
|
|
158
164
|
if (!this.isRunning) {
|
|
159
165
|
return;
|
|
160
166
|
}
|
|
161
|
-
logger_1.default.printLine(
|
|
167
|
+
logger_1.default.printLine(`${prefix} Stopping development server...`, 'info');
|
|
162
168
|
this.isRunning = false;
|
|
163
169
|
// Stop file watcher
|
|
164
170
|
if (this.fileWatcher) {
|
|
@@ -170,9 +176,9 @@ class DevRunner {
|
|
|
170
176
|
}
|
|
171
177
|
const uptime = Math.floor((Date.now() - this.startTime.getTime()) / 1000);
|
|
172
178
|
const uptimeStr = this.formatUptime(uptime);
|
|
173
|
-
logger_1.default.printLine(
|
|
179
|
+
logger_1.default.printLine(`${prefix} ${this.options.runnerName} development server stopped after ${uptimeStr}`, 'info');
|
|
174
180
|
if (this.restartCount > 0) {
|
|
175
|
-
logger_1.default.printLine(
|
|
181
|
+
logger_1.default.printLine(`${prefix} Total restarts: ${this.restartCount}`, 'info');
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
184
|
setupGracefulShutdown() {
|
|
@@ -29,26 +29,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.ProcessManager = void 0;
|
|
30
30
|
// src/process-manager.ts - PM2 alternative process manager
|
|
31
31
|
const child_process_1 = require("child_process");
|
|
32
|
-
const
|
|
32
|
+
const fsFull = __importStar(require("fs")); // Renamed to fsFull to avoid conflicts
|
|
33
|
+
const fs_1 = require("fs");
|
|
33
34
|
const path = __importStar(require("path"));
|
|
34
35
|
const events_1 = require("events");
|
|
35
36
|
const logger_1 = __importDefault(require("./logger"));
|
|
36
37
|
class ProcessManager extends events_1.EventEmitter {
|
|
37
38
|
constructor(configPath) {
|
|
38
39
|
super();
|
|
40
|
+
this.logStreams = new Map();
|
|
39
41
|
this.processes = new Map();
|
|
40
42
|
this.isMonitoring = false;
|
|
41
43
|
this.configPath = configPath || path.join(process.cwd(), '.neex', 'processes.json');
|
|
42
|
-
this.
|
|
44
|
+
this.logDir = path.join(path.dirname(this.configPath), 'logs');
|
|
45
|
+
this.ensureConfigDirAndLogDir(); // Renamed and updated
|
|
46
|
+
// loadConfig will be called by CLI commands as needed before operations like list, stop, etc.
|
|
43
47
|
this.startMonitoring();
|
|
44
48
|
}
|
|
45
|
-
async
|
|
49
|
+
async ensureConfigDirAndLogDir() {
|
|
46
50
|
const dir = path.dirname(this.configPath);
|
|
47
51
|
try {
|
|
48
|
-
await
|
|
52
|
+
await fs_1.promises.mkdir(dir, { recursive: true });
|
|
53
|
+
await fs_1.promises.mkdir(this.logDir, { recursive: true });
|
|
49
54
|
}
|
|
50
55
|
catch (error) {
|
|
51
|
-
|
|
56
|
+
logger_1.default.printLine(`Failed to create config/log directories: ${error.message}`, 'error');
|
|
57
|
+
// Directory might already exist or other error
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
async saveConfig() {
|
|
@@ -59,7 +65,7 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
59
65
|
created_at: proc.created_at,
|
|
60
66
|
restarts: proc.restarts
|
|
61
67
|
}));
|
|
62
|
-
await
|
|
68
|
+
await fs_1.promises.writeFile(this.configPath, JSON.stringify(config, null, 2));
|
|
63
69
|
}
|
|
64
70
|
catch (error) {
|
|
65
71
|
logger_1.default.printLine(`Failed to save config: ${error.message}`, 'error');
|
|
@@ -67,7 +73,7 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
67
73
|
}
|
|
68
74
|
async loadConfig() {
|
|
69
75
|
try {
|
|
70
|
-
const data = await
|
|
76
|
+
const data = await fs_1.promises.readFile(this.configPath, 'utf-8');
|
|
71
77
|
const configs = JSON.parse(data);
|
|
72
78
|
for (const config of configs) {
|
|
73
79
|
if (config.id && !this.processes.has(config.id)) {
|
|
@@ -99,8 +105,39 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
99
105
|
}
|
|
100
106
|
return id;
|
|
101
107
|
}
|
|
108
|
+
closeLogStreams(id) {
|
|
109
|
+
var _a, _b;
|
|
110
|
+
const streams = this.logStreams.get(id);
|
|
111
|
+
if (streams) {
|
|
112
|
+
(_a = streams.out) === null || _a === void 0 ? void 0 : _a.end();
|
|
113
|
+
(_b = streams.err) === null || _b === void 0 ? void 0 : _b.end();
|
|
114
|
+
this.logStreams.delete(id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
102
117
|
async startProcess(processInfo) {
|
|
103
118
|
const { config } = processInfo;
|
|
119
|
+
// Setup log files
|
|
120
|
+
const defaultLogPath = (type) => path.join(this.logDir, `${processInfo.id}.${type}.log`);
|
|
121
|
+
config.out_file = config.out_file || defaultLogPath('out');
|
|
122
|
+
config.error_file = config.error_file || defaultLogPath('err');
|
|
123
|
+
this.closeLogStreams(processInfo.id); // Ensure any previous streams for this ID are closed (e.g., on restart)
|
|
124
|
+
let outStream;
|
|
125
|
+
let errStream;
|
|
126
|
+
try {
|
|
127
|
+
if (config.out_file) {
|
|
128
|
+
await fs_1.promises.mkdir(path.dirname(config.out_file), { recursive: true }); // Ensure directory for custom log file exists
|
|
129
|
+
outStream = fsFull.createWriteStream(config.out_file, { flags: 'a' });
|
|
130
|
+
this.logStreams.set(processInfo.id, { ...this.logStreams.get(processInfo.id), out: outStream });
|
|
131
|
+
}
|
|
132
|
+
if (config.error_file) {
|
|
133
|
+
await fs_1.promises.mkdir(path.dirname(config.error_file), { recursive: true }); // Ensure directory for custom log file exists
|
|
134
|
+
errStream = fsFull.createWriteStream(config.error_file, { flags: 'a' });
|
|
135
|
+
this.logStreams.set(processInfo.id, { ...this.logStreams.get(processInfo.id), err: errStream });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (streamError) {
|
|
139
|
+
logger_1.default.printLine(`Failed to open log streams for ${processInfo.id}: ${streamError.message}`, 'error');
|
|
140
|
+
}
|
|
104
141
|
try {
|
|
105
142
|
processInfo.status = 'launching';
|
|
106
143
|
this.emit('process:launching', processInfo);
|
|
@@ -121,7 +158,18 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
121
158
|
processInfo.pid = childProcess.pid;
|
|
122
159
|
processInfo.status = 'online';
|
|
123
160
|
processInfo.created_at = new Date();
|
|
161
|
+
// Allow parent to exit independently of the child
|
|
162
|
+
if (childProcess.pid) { // unref only if process spawned successfully
|
|
163
|
+
childProcess.unref();
|
|
164
|
+
}
|
|
124
165
|
this.emit('process:start', processInfo);
|
|
166
|
+
// Pipe to log files if streams are available
|
|
167
|
+
if (outStream && childProcess.stdout) {
|
|
168
|
+
childProcess.stdout.pipe(outStream);
|
|
169
|
+
}
|
|
170
|
+
if (errStream && childProcess.stderr) {
|
|
171
|
+
childProcess.stderr.pipe(errStream);
|
|
172
|
+
}
|
|
125
173
|
// Handle stdout
|
|
126
174
|
if (childProcess.stdout) {
|
|
127
175
|
childProcess.stdout.on('data', (data) => {
|
|
@@ -147,6 +195,8 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
147
195
|
processInfo.status = code === 0 ? 'stopped' : 'errored';
|
|
148
196
|
processInfo.process = undefined;
|
|
149
197
|
processInfo.pid = undefined;
|
|
198
|
+
// Note: Streams are not closed here on 'exit' to allow logs to be read after a crash.
|
|
199
|
+
// They are closed on explicit stop/delete or neex shutdown.
|
|
150
200
|
this.emit('process:exit', { processInfo, code, signal });
|
|
151
201
|
// Auto-restart if enabled
|
|
152
202
|
if (config.autorestart && processInfo.restarts < (config.max_restarts || 10)) {
|
|
@@ -252,6 +302,7 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
252
302
|
processInfo.status = 'stopped';
|
|
253
303
|
processInfo.process = undefined;
|
|
254
304
|
processInfo.pid = undefined;
|
|
305
|
+
this.closeLogStreams(id);
|
|
255
306
|
await this.saveConfig();
|
|
256
307
|
this.emit('process:stop', processInfo);
|
|
257
308
|
}
|
|
@@ -278,9 +329,22 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
278
329
|
if (processInfo.status === 'online') {
|
|
279
330
|
await this.stop(id);
|
|
280
331
|
}
|
|
332
|
+
const currentProcessInfo = this.processes.get(id);
|
|
333
|
+
if (currentProcessInfo) {
|
|
334
|
+
if (currentProcessInfo.status === 'online' || currentProcessInfo.status === 'launching' || currentProcessInfo.status === 'stopping') {
|
|
335
|
+
// stop() will call closeLogStreams
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.closeLogStreams(id); // Ensure closed for other states before deleting
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Optional: Delete actual log files if `delete` implies full data removal
|
|
342
|
+
// if (config.out_file) await fsPromises.unlink(config.out_file).catch(() => {});
|
|
343
|
+
// if (config.error_file) await fsPromises.unlink(config.error_file).catch(() => {});
|
|
281
344
|
this.processes.delete(id);
|
|
282
345
|
await this.saveConfig();
|
|
283
|
-
|
|
346
|
+
if (currentProcessInfo)
|
|
347
|
+
this.emit('process:delete', currentProcessInfo);
|
|
284
348
|
}
|
|
285
349
|
async list() {
|
|
286
350
|
return Array.from(this.processes.values());
|
|
@@ -295,10 +359,42 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
295
359
|
}
|
|
296
360
|
return this.list();
|
|
297
361
|
}
|
|
298
|
-
async logs(id, lines =
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
362
|
+
async logs(id, lines = 15) {
|
|
363
|
+
let processInfo = this.processes.get(id);
|
|
364
|
+
if (!processInfo) {
|
|
365
|
+
await this.loadConfig(); // Attempt to load if not in memory
|
|
366
|
+
processInfo = this.processes.get(id);
|
|
367
|
+
if (!processInfo) {
|
|
368
|
+
throw new Error(`Process ${id} not found`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return this.readLogFiles(processInfo.config, lines);
|
|
372
|
+
}
|
|
373
|
+
async readLogFiles(config, linesToRead) {
|
|
374
|
+
const finalMessages = [];
|
|
375
|
+
const { out_file, error_file } = config;
|
|
376
|
+
const readFileLastLines = async (filePath) => {
|
|
377
|
+
if (!filePath)
|
|
378
|
+
return [];
|
|
379
|
+
try {
|
|
380
|
+
await fs_1.promises.access(filePath);
|
|
381
|
+
const content = await fs_1.promises.readFile(filePath, 'utf-8');
|
|
382
|
+
const fileLines = content.split('\n').filter((line) => line.trim() !== '');
|
|
383
|
+
return fileLines.slice(-linesToRead);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
// Log file might not exist or not be readable, return empty
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
const outLogs = await readFileLastLines(out_file);
|
|
391
|
+
const errLogs = await readFileLastLines(error_file);
|
|
392
|
+
outLogs.forEach(log => finalMessages.push(log));
|
|
393
|
+
if (errLogs.length > 0 && outLogs.length > 0) {
|
|
394
|
+
finalMessages.push('--- STDERR ---'); // Separator
|
|
395
|
+
}
|
|
396
|
+
errLogs.forEach(log => finalMessages.push(log));
|
|
397
|
+
return finalMessages;
|
|
302
398
|
}
|
|
303
399
|
async save() {
|
|
304
400
|
await this.saveConfig();
|
|
@@ -322,7 +418,7 @@ class ProcessManager extends events_1.EventEmitter {
|
|
|
322
418
|
getProcess(id) {
|
|
323
419
|
return this.processes.get(id);
|
|
324
420
|
}
|
|
325
|
-
dispose() {
|
|
421
|
+
async dispose() {
|
|
326
422
|
this.stopMonitoring();
|
|
327
423
|
this.stopAll();
|
|
328
424
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "The Modern Build System for Polyrepo-in-Monorepo Architecture",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"start": "node dist/bin/neex.js",
|
|
13
13
|
"prepublishOnly": "npm run build",
|
|
14
14
|
"test": "jest",
|
|
15
|
-
"
|
|
15
|
+
"dev": "neex w \"ts-node src/server.ts\"",
|
|
16
|
+
"w": "neex w \"ts-node src/server.ts\"",
|
|
17
|
+
"test:dev": "node ./dist/bin/neex.js px \"echo Starting frontend\" \"echo Starting backend\"",
|
|
16
18
|
"test:parallel": "node ./dist/src/cli.js parallel \"echo Building frontend\" \"echo Building backend\"",
|
|
17
19
|
"test:sequence": "node ./dist/src/cli.js run \"echo Step 1\" \"echo Step 2\" \"echo Step 3\""
|
|
18
20
|
},
|