pomitu 1.3.0 → 1.4.0
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 +1 -1
- package/dist/app.js +4 -1
- package/dist/commands/flush.js +15 -13
- package/dist/commands/ls.js +20 -18
- package/dist/commands/restart.js +33 -62
- package/dist/commands/start.js +45 -18
- package/dist/commands/stop.js +44 -14
- package/dist/components/ProcessTUI.js +96 -42
- package/dist/helpers.js +11 -1
- package/dist/services/ConfigManager.js +2 -1
- package/dist/services/IpcSignal.js +66 -0
- package/dist/services/PidManager.js +3 -2
- package/dist/services/ProcessManager.js +4 -13
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ If you want to contribute to Pomitu or run it in development mode:
|
|
|
98
98
|
Dev installation test:
|
|
99
99
|
|
|
100
100
|
```sh
|
|
101
|
-
npm run build && npm pack && npm install -g pomitu-1.
|
|
101
|
+
npm run build && npm pack && npm install -g pomitu-1.4.0.tgz && rm pomitu-1.4.0.tgz
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
### Linting
|
package/dist/app.js
CHANGED
|
@@ -3,13 +3,15 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { start } from './commands/start.js';
|
|
4
4
|
import { flush } from './commands/flush.js';
|
|
5
5
|
import { stop } from './commands/stop.js';
|
|
6
|
+
import { restart } from './commands/restart.js';
|
|
6
7
|
import { mkdirSync } from 'node:fs';
|
|
7
8
|
import { ls } from './commands/ls.js';
|
|
8
|
-
import { getPomituDirectory, getPomituLogsDirectory, getPomituPidsDirectory } from './helpers.js';
|
|
9
|
+
import { getPomituDirectory, getPomituLogsDirectory, getPomituPidsDirectory, getPomituSignalsDirectory } from './helpers.js';
|
|
9
10
|
function init() {
|
|
10
11
|
mkdirSync(getPomituDirectory(), { recursive: true });
|
|
11
12
|
mkdirSync(getPomituLogsDirectory(), { recursive: true });
|
|
12
13
|
mkdirSync(getPomituPidsDirectory(), { recursive: true });
|
|
14
|
+
mkdirSync(getPomituSignalsDirectory(), { recursive: true });
|
|
13
15
|
}
|
|
14
16
|
init();
|
|
15
17
|
const program = new Command();
|
|
@@ -19,5 +21,6 @@ program
|
|
|
19
21
|
.addCommand(start)
|
|
20
22
|
.addCommand(flush)
|
|
21
23
|
.addCommand(stop)
|
|
24
|
+
.addCommand(restart)
|
|
22
25
|
.addCommand(ls)
|
|
23
26
|
.parse();
|
package/dist/commands/flush.js
CHANGED
|
@@ -4,17 +4,19 @@ export const flush = new Command('flush')
|
|
|
4
4
|
.description('flush logs')
|
|
5
5
|
.argument('[name]', 'name of the app whose logs you want to flush')
|
|
6
6
|
.action((name) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} else {
|
|
13
|
-
console.log('Logs flushed');
|
|
14
|
-
}
|
|
15
|
-
} catch (error) {
|
|
16
|
-
const err = error;
|
|
17
|
-
console.error(`Error: ${err.message}`);
|
|
18
|
-
process.exit(1);
|
|
7
|
+
try {
|
|
8
|
+
const logManager = new LogManager();
|
|
9
|
+
const flushedFiles = logManager.flushLogs(name);
|
|
10
|
+
if (flushedFiles.length === 0) {
|
|
11
|
+
console.log('No log files found to flush');
|
|
19
12
|
}
|
|
20
|
-
|
|
13
|
+
else {
|
|
14
|
+
console.log('Logs flushed');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const err = error;
|
|
19
|
+
console.error(`Error: ${err.message}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
});
|
package/dist/commands/ls.js
CHANGED
|
@@ -3,24 +3,26 @@ import { ProcessManager } from '../services/index.js';
|
|
|
3
3
|
export const ls = new Command('ls')
|
|
4
4
|
.description('list all running apps')
|
|
5
5
|
.action(() => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
try {
|
|
7
|
+
const processManager = new ProcessManager();
|
|
8
|
+
const processes = processManager.listRunningProcesses();
|
|
9
|
+
if (processes.length === 0) {
|
|
10
|
+
console.log('No running processes found');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log('Running processes:');
|
|
14
|
+
for (const process of processes) {
|
|
15
|
+
if (process.isRunning) {
|
|
16
|
+
console.log(`- ${process.name} (pid: ${process.pid})`);
|
|
12
17
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (process.isRunning) {
|
|
16
|
-
console.log(`- ${process.name} (pid: ${process.pid})`);
|
|
17
|
-
} else {
|
|
18
|
-
console.warn(`- ${process.name} (pid: ${process.pid}) is not running`);
|
|
19
|
-
}
|
|
18
|
+
else {
|
|
19
|
+
console.warn(`- ${process.name} (pid: ${process.pid}) is not running`);
|
|
20
20
|
}
|
|
21
|
-
} catch (error) {
|
|
22
|
-
const err = error;
|
|
23
|
-
console.error(`Error: ${err.message}`);
|
|
24
|
-
process.exit(1);
|
|
25
21
|
}
|
|
26
|
-
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const err = error;
|
|
25
|
+
console.error(`Error: ${err.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
});
|
package/dist/commands/restart.js
CHANGED
|
@@ -1,62 +1,33 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.
|
|
7
|
-
.
|
|
8
|
-
.action(async (name
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (success) {
|
|
35
|
-
console.log(`Stopped ${app.name}`);
|
|
36
|
-
}
|
|
37
|
-
// Start the app again
|
|
38
|
-
await processManager.startApp(app, {
|
|
39
|
-
daemon: options.daemon,
|
|
40
|
-
clearLogs: options.clearLogs
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// Not a config file, treat as app name
|
|
46
|
-
}
|
|
47
|
-
if (!isConfigFile) {
|
|
48
|
-
// Treat as app name - stop it
|
|
49
|
-
const success = await processManager.stopApp(name);
|
|
50
|
-
if (!success) {
|
|
51
|
-
console.warn(`No running process found for ${name}`);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
console.warn(`Stopped ${name}. To restart, you need to use "pomitu start <config-file>" as app config is not stored.`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch (error) {
|
|
58
|
-
const err = error;
|
|
59
|
-
console.error(`Error: ${err.message}`);
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { PidManager } from '../services/PidManager.js';
|
|
3
|
+
import { isTuiActive, writeSignal, waitForPidState } from '../services/IpcSignal.js';
|
|
4
|
+
import { getFileNameFriendlyName } from '../helpers.js';
|
|
5
|
+
export const restart = new Command('restart')
|
|
6
|
+
.description('restart a running app (works whether managed by TUI or daemon)')
|
|
7
|
+
.argument('<name>', 'name of the app to restart')
|
|
8
|
+
.action(async (name) => {
|
|
9
|
+
try {
|
|
10
|
+
if (isTuiActive(name)) {
|
|
11
|
+
writeSignal(name, 'restart');
|
|
12
|
+
await waitForPidState(name, 'absent', 3000);
|
|
13
|
+
const confirmed = await waitForPidState(name, 'present', 3000);
|
|
14
|
+
if (confirmed) {
|
|
15
|
+
const pidManager = new PidManager();
|
|
16
|
+
const pid = pidManager.getPid(getFileNameFriendlyName(name));
|
|
17
|
+
console.log(`Restarted ${name} (PID: ${pid})`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.warn(`Restart signal sent but could not confirm ${name} restarted within timeout`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.error(`No active TUI session found for ${name}. For daemon-mode processes, use: pomitu stop ${name} && pomitu start <config>`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const err = error;
|
|
30
|
+
console.error(`Error: ${err.message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
});
|
package/dist/commands/start.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { ProcessManager, ConfigManager } from '../services/index.js';
|
|
3
|
+
import { isTuiActive, writeSignal, waitForPidState } from '../services/IpcSignal.js';
|
|
4
|
+
import { PidManager } from '../services/PidManager.js';
|
|
5
|
+
import { getFileNameFriendlyName, pidIsRunning } from '../helpers.js';
|
|
3
6
|
import React from 'react';
|
|
4
7
|
import { render } from 'ink';
|
|
5
8
|
import { ProcessTUI } from '../components/ProcessTUI.js';
|
|
@@ -9,27 +12,51 @@ export const start = new Command('start')
|
|
|
9
12
|
.option('--no-daemon', 'do not daemonize the app and show interactive TUI')
|
|
10
13
|
.option('--clear-logs', 'clear log files before starting the app')
|
|
11
14
|
.action(async (name, options) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
try {
|
|
16
|
+
const configManager = new ConfigManager();
|
|
17
|
+
const processManager = new ProcessManager();
|
|
18
|
+
const config = configManager.readConfig(name);
|
|
19
|
+
configManager.validateConfig(config);
|
|
20
|
+
const runInteractive = options.daemon === false;
|
|
21
|
+
let anyTuiActive = false;
|
|
22
|
+
const pidManager = new PidManager();
|
|
23
|
+
for (const app of config.apps) {
|
|
24
|
+
if (isTuiActive(app.name)) {
|
|
25
|
+
anyTuiActive = true;
|
|
26
|
+
const existingPid = pidManager.getPid(getFileNameFriendlyName(app.name));
|
|
27
|
+
const wasRunning = existingPid !== null && pidIsRunning(existingPid);
|
|
28
|
+
writeSignal(app.name, 'start');
|
|
29
|
+
if (!runInteractive) {
|
|
30
|
+
if (wasRunning) {
|
|
31
|
+
await waitForPidState(app.name, 'absent', 3000);
|
|
32
|
+
}
|
|
33
|
+
const confirmed = await waitForPidState(app.name, 'present', 3000);
|
|
34
|
+
if (confirmed) {
|
|
35
|
+
const pid = pidManager.getPid(getFileNameFriendlyName(app.name));
|
|
36
|
+
console.log(`Started ${app.name} with pid ${pid}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.warn(`Start signal sent but could not confirm ${app.name} started within timeout`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
19
44
|
processManager.startApp(app, {
|
|
20
45
|
daemon: options.daemon,
|
|
21
46
|
clearLogs: options.clearLogs
|
|
22
47
|
});
|
|
23
48
|
}
|
|
24
|
-
if (runInteractive) {
|
|
25
|
-
render(React.createElement(ProcessTUI, {
|
|
26
|
-
configPath: name,
|
|
27
|
-
clearLogs: options.clearLogs
|
|
28
|
-
}));
|
|
29
|
-
}
|
|
30
|
-
} catch (error) {
|
|
31
|
-
const err = error;
|
|
32
|
-
console.error(`Error: ${err.message}`);
|
|
33
|
-
process.exit(1);
|
|
34
49
|
}
|
|
35
|
-
|
|
50
|
+
if (runInteractive && !anyTuiActive) {
|
|
51
|
+
render(React.createElement(ProcessTUI, {
|
|
52
|
+
configPath: name,
|
|
53
|
+
clearLogs: options.clearLogs
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const err = error;
|
|
59
|
+
console.error(`Error: ${err.message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
package/dist/commands/stop.js
CHANGED
|
@@ -1,27 +1,57 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { ProcessManager } from '../services/index.js';
|
|
3
|
+
import { isTuiActive, writeSignal, waitForPidState } from '../services/IpcSignal.js';
|
|
3
4
|
export const stop = new Command('stop')
|
|
4
5
|
.description('stop a running app or all apps')
|
|
5
6
|
.argument('<name>', 'name of the app to stop or "all" to stop all apps')
|
|
6
7
|
.action(async (name) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
try {
|
|
9
|
+
const processManager = new ProcessManager();
|
|
10
|
+
if (name === 'all') {
|
|
11
|
+
const runningProcesses = processManager.listRunningProcesses();
|
|
12
|
+
let stoppedCount = 0;
|
|
13
|
+
for (const proc of runningProcesses) {
|
|
14
|
+
if (isTuiActive(proc.name)) {
|
|
15
|
+
writeSignal(proc.name, 'stop');
|
|
16
|
+
const confirmed = await waitForPidState(proc.name, 'absent');
|
|
17
|
+
if (confirmed)
|
|
18
|
+
stoppedCount++;
|
|
15
19
|
}
|
|
16
|
-
|
|
20
|
+
else {
|
|
21
|
+
const success = await processManager.stopApp(proc.name);
|
|
22
|
+
if (success)
|
|
23
|
+
stoppedCount++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (stoppedCount === 0) {
|
|
27
|
+
console.warn('No running processes found');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log(`Stopped ${stoppedCount} process(es)`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (isTuiActive(name)) {
|
|
35
|
+
writeSignal(name, 'stop');
|
|
36
|
+
const confirmed = await waitForPidState(name, 'absent');
|
|
37
|
+
if (confirmed) {
|
|
38
|
+
console.log(`Stopped ${name}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.warn(`Stop signal sent but could not confirm ${name} stopped within timeout`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
17
45
|
const success = await processManager.stopApp(name);
|
|
18
46
|
if (!success) {
|
|
19
47
|
console.warn(`No running process found for ${name}`);
|
|
20
48
|
}
|
|
21
49
|
}
|
|
22
|
-
} catch (error) {
|
|
23
|
-
const err = error;
|
|
24
|
-
console.error(`Error: ${err.message}`);
|
|
25
|
-
process.exit(1);
|
|
26
50
|
}
|
|
27
|
-
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const err = error;
|
|
54
|
+
console.error(`Error: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
@@ -5,7 +5,10 @@ import open from 'open';
|
|
|
5
5
|
import { Box, Text, useApp, useStdin } from 'ink';
|
|
6
6
|
import SelectInput from 'ink-select-input';
|
|
7
7
|
import { ProcessManager, ConfigManager } from '../services/index.js';
|
|
8
|
-
import { getFileNameFriendlyName, getProcessLogOutFilePath, getProcessLogErrorFilePath } from '../helpers.js';
|
|
8
|
+
import { getFileNameFriendlyName, getProcessLogOutFilePath, getProcessLogErrorFilePath, getPomituSignalsDirectory } from '../helpers.js';
|
|
9
|
+
import chokidar from 'chokidar';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { writeTuiPresence, clearTuiPresence, readAndClearSignal } from '../services/IpcSignal.js';
|
|
9
12
|
export function ProcessTUI({ configPath, clearLogs }) {
|
|
10
13
|
const { exit } = useApp();
|
|
11
14
|
const { stdin, setRawMode } = useStdin();
|
|
@@ -19,6 +22,8 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
19
22
|
const [searchQuery, setSearchQuery] = useState('');
|
|
20
23
|
const previousRawModeRef = useRef(false);
|
|
21
24
|
const rawModeCapturedRef = useRef(false);
|
|
25
|
+
const appsRef = useRef([]);
|
|
26
|
+
const ipcHandlerRef = useRef(null);
|
|
22
27
|
// Create managers only once
|
|
23
28
|
const processManager = useMemo(() => new ProcessManager(), []);
|
|
24
29
|
const configManager = useMemo(() => new ConfigManager(), []);
|
|
@@ -34,13 +39,15 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
34
39
|
await open(filePath);
|
|
35
40
|
setMessage('Opening log file...');
|
|
36
41
|
setMessageColor('green');
|
|
37
|
-
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
38
44
|
setMessage(`Failed to open log: ${error instanceof Error ? error.message : String(error)}`);
|
|
39
45
|
setMessageColor('red');
|
|
40
46
|
}
|
|
41
47
|
setTimeout(() => setMessage(''), 3000);
|
|
42
48
|
}, []);
|
|
43
49
|
const cleanExit = useCallback(() => {
|
|
50
|
+
appsRef.current.forEach(app => clearTuiPresence(app.name));
|
|
44
51
|
if (setRawMode) {
|
|
45
52
|
setRawMode(previousRawModeRef.current);
|
|
46
53
|
}
|
|
@@ -88,7 +95,8 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
88
95
|
if (success) {
|
|
89
96
|
stoppedApps.push(proc.name);
|
|
90
97
|
}
|
|
91
|
-
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
92
100
|
// Continue even if stop fails
|
|
93
101
|
console.error(`Failed to stop ${proc.name}:`, error);
|
|
94
102
|
}
|
|
@@ -98,17 +106,21 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
98
106
|
setApps(config.apps);
|
|
99
107
|
if (stoppedApps.length > 0) {
|
|
100
108
|
setMessage(`Configuration reloaded. Stopped ${stoppedApps.length} running app(s): ${stoppedApps.join(', ')}`);
|
|
101
|
-
}
|
|
109
|
+
}
|
|
110
|
+
else if (removedApps.length > 0) {
|
|
102
111
|
setMessage(`Configuration reloaded. ${removedApps.length} app(s) removed (none were running)`);
|
|
103
|
-
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
104
114
|
setMessage('Configuration reloaded successfully');
|
|
105
115
|
}
|
|
106
116
|
setMessageColor('green');
|
|
107
|
-
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
108
119
|
const err = error;
|
|
109
120
|
setMessage(`Error reloading config: ${err.message}`);
|
|
110
121
|
setMessageColor('red');
|
|
111
|
-
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
112
124
|
setIsReloading(false);
|
|
113
125
|
// Clear message after 3 seconds
|
|
114
126
|
setTimeout(() => setMessage(''), 3000);
|
|
@@ -120,7 +132,8 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
120
132
|
const config = configManager.readConfig(configPath);
|
|
121
133
|
configManager.validateConfig(config);
|
|
122
134
|
setApps(config.apps);
|
|
123
|
-
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
124
137
|
const err = error;
|
|
125
138
|
setMessage(`Error loading config: ${err.message}`);
|
|
126
139
|
setMessageColor('red');
|
|
@@ -130,6 +143,33 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
130
143
|
useEffect(() => {
|
|
131
144
|
setProcesses(computeStatuses());
|
|
132
145
|
}, [computeStatuses]);
|
|
146
|
+
// Sync appsRef and write TUI presence files whenever apps change
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
appsRef.current = apps;
|
|
149
|
+
apps.forEach(app => writeTuiPresence(app.name));
|
|
150
|
+
}, [apps]);
|
|
151
|
+
// Set up chokidar watcher for IPC signals (mounted once; uses refs for latest state)
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const signalsDir = getPomituSignalsDirectory();
|
|
154
|
+
const watcher = chokidar.watch(signalsDir, { ignoreInitial: true });
|
|
155
|
+
const handleSignalFile = (filePath) => {
|
|
156
|
+
const fileBaseName = path.basename(filePath, '.json');
|
|
157
|
+
const matchedApp = appsRef.current.find(a => getFileNameFriendlyName(a.name) === fileBaseName);
|
|
158
|
+
if (!matchedApp)
|
|
159
|
+
return;
|
|
160
|
+
const action = readAndClearSignal(matchedApp.name);
|
|
161
|
+
if (!action)
|
|
162
|
+
return;
|
|
163
|
+
if (ipcHandlerRef.current) {
|
|
164
|
+
ipcHandlerRef.current(matchedApp.name, action);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
watcher.on('add', handleSignalFile);
|
|
168
|
+
watcher.on('change', handleSignalFile);
|
|
169
|
+
return () => {
|
|
170
|
+
watcher.close();
|
|
171
|
+
};
|
|
172
|
+
}, []);
|
|
133
173
|
// Handle keyboard input - use keypress events to intercept BEFORE SelectInput
|
|
134
174
|
useEffect(() => {
|
|
135
175
|
if (!stdin)
|
|
@@ -208,7 +248,9 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
208
248
|
const handleSelect = useCallback(async (item) => {
|
|
209
249
|
if (isProcessing || isReloading)
|
|
210
250
|
return; // Prevent multiple simultaneous operations
|
|
211
|
-
const
|
|
251
|
+
const colonIndex = item.value.indexOf(':');
|
|
252
|
+
const action = item.value.slice(0, colonIndex);
|
|
253
|
+
const appName = item.value.slice(colonIndex + 1);
|
|
212
254
|
const app = apps.find(a => a.name === appName);
|
|
213
255
|
if (!app) {
|
|
214
256
|
setMessage(`App ${appName} not found`);
|
|
@@ -224,37 +266,39 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
224
266
|
});
|
|
225
267
|
setMessage(`Started ${appName}`);
|
|
226
268
|
setMessageColor('green');
|
|
227
|
-
}
|
|
269
|
+
}
|
|
270
|
+
else if (action === 'stop') {
|
|
228
271
|
const success = await processManager.stopApp(appName, { quiet: true });
|
|
229
272
|
if (success) {
|
|
230
273
|
setMessage(`Stopped ${appName}`);
|
|
231
274
|
setMessageColor('green');
|
|
232
|
-
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
233
277
|
setMessage(`Failed to stop ${appName}`);
|
|
234
278
|
setMessageColor('red');
|
|
235
279
|
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
280
|
+
}
|
|
281
|
+
else if (action === 'restart') {
|
|
282
|
+
const wasRunning = await processManager.stopApp(appName, { quiet: true });
|
|
283
|
+
if (wasRunning) {
|
|
239
284
|
// Wait a bit before restarting
|
|
240
285
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
241
|
-
await processManager.startApp(app, {
|
|
242
|
-
daemon: false,
|
|
243
|
-
clearLogs: clearLogs ?? false
|
|
244
|
-
});
|
|
245
|
-
setMessage(`Restarted ${appName}`);
|
|
246
|
-
setMessageColor('green');
|
|
247
|
-
} else {
|
|
248
|
-
setMessage(`Failed to stop ${appName} for restart`);
|
|
249
|
-
setMessageColor('red');
|
|
250
286
|
}
|
|
251
|
-
|
|
287
|
+
await processManager.startApp(app, {
|
|
288
|
+
daemon: false,
|
|
289
|
+
clearLogs: clearLogs ?? false
|
|
290
|
+
});
|
|
291
|
+
setMessage(`Restarted ${appName}`);
|
|
292
|
+
setMessageColor('green');
|
|
293
|
+
}
|
|
294
|
+
else if (action === 'viewout') {
|
|
252
295
|
const fileNameFriendly = getFileNameFriendlyName(appName);
|
|
253
296
|
const logPath = getProcessLogOutFilePath(fileNameFriendly);
|
|
254
297
|
openFileInNativeApp(logPath);
|
|
255
298
|
setIsProcessing(false);
|
|
256
299
|
return;
|
|
257
|
-
}
|
|
300
|
+
}
|
|
301
|
+
else if (action === 'viewerr') {
|
|
258
302
|
const fileNameFriendly = getFileNameFriendlyName(appName);
|
|
259
303
|
const logPath = getProcessLogErrorFilePath(fileNameFriendly);
|
|
260
304
|
openFileInNativeApp(logPath);
|
|
@@ -262,11 +306,13 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
262
306
|
return;
|
|
263
307
|
}
|
|
264
308
|
setProcesses(computeStatuses());
|
|
265
|
-
}
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
266
311
|
const err = error;
|
|
267
312
|
setMessage(`Error: ${err.message}`);
|
|
268
313
|
setMessageColor('red');
|
|
269
|
-
}
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
270
316
|
setIsProcessing(false);
|
|
271
317
|
}
|
|
272
318
|
// Clear message after 3 seconds
|
|
@@ -299,7 +345,8 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
299
345
|
label: ' └─ View Error Log',
|
|
300
346
|
value: `viewerr:${proc.name}`
|
|
301
347
|
});
|
|
302
|
-
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
303
350
|
items.push({
|
|
304
351
|
label: ` ├─ Start ${proc.name}`,
|
|
305
352
|
value: `start:${proc.name}`
|
|
@@ -327,34 +374,41 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
327
374
|
const handleMenuSelect = useCallback((item) => {
|
|
328
375
|
if (item.value === 'separator' || item.value.startsWith('info:')) {
|
|
329
376
|
// Informational rows are read-only
|
|
330
|
-
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
331
379
|
handleSelect(item);
|
|
332
380
|
}
|
|
333
381
|
}, [handleSelect]);
|
|
382
|
+
// Keep IPC signal handler up to date with current handleSelect
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
ipcHandlerRef.current = (appName, action) => {
|
|
385
|
+
handleSelect({ label: '', value: `${action}:${appName}` });
|
|
386
|
+
};
|
|
387
|
+
}, [handleSelect]);
|
|
334
388
|
const notificationText = isProcessing ? 'Processing...' : (isReloading ? 'Reloading...' : message);
|
|
335
389
|
const notificationColor = (isProcessing || isReloading) ? 'yellow' : message ? messageColor : undefined;
|
|
336
|
-
return (React.createElement(Box, { flexDirection:
|
|
337
|
-
React.createElement(Box, { borderStyle:
|
|
338
|
-
React.createElement(Text, { bold: true, color:
|
|
390
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
391
|
+
React.createElement(Box, { borderStyle: "round", borderColor: "cyan", padding: 1, marginBottom: 1 },
|
|
392
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Pomitu Process Manager - Interactive Mode")),
|
|
339
393
|
processes.length > 0 ? (React.createElement(React.Fragment, null,
|
|
340
394
|
React.createElement(Box, { marginBottom: 1 },
|
|
341
|
-
React.createElement(Text, { dimColor: true },
|
|
395
|
+
React.createElement(Text, { dimColor: true }, "Use arrow keys to navigate, Enter to select, '/' to search, 'r' to reload, 'q' or Ctrl+C to quit")),
|
|
342
396
|
searchMode && (React.createElement(Box, { marginBottom: 1 },
|
|
343
|
-
React.createElement(Text, { color:
|
|
344
|
-
|
|
397
|
+
React.createElement(Text, { color: "yellow" },
|
|
398
|
+
"Search: ",
|
|
345
399
|
searchQuery),
|
|
346
|
-
React.createElement(Text, { dimColor: true },
|
|
400
|
+
React.createElement(Text, { dimColor: true }, " (ESC to cancel, Enter to apply)"))),
|
|
347
401
|
searchQuery && !searchMode && (React.createElement(Box, { marginBottom: 1 },
|
|
348
|
-
React.createElement(Text, { color:
|
|
349
|
-
|
|
402
|
+
React.createElement(Text, { color: "green" },
|
|
403
|
+
"Filtering: ",
|
|
350
404
|
searchQuery),
|
|
351
|
-
React.createElement(Text, { dimColor: true },
|
|
405
|
+
React.createElement(Text, { dimColor: true }, " (/ to edit, ESC to clear)"))),
|
|
352
406
|
items.length > 15 && (React.createElement(Box, { marginBottom: 1 },
|
|
353
407
|
React.createElement(Text, { dimColor: true },
|
|
354
|
-
|
|
408
|
+
"Showing 15 of ",
|
|
355
409
|
items.length,
|
|
356
|
-
|
|
357
|
-
React.createElement(SelectInput, { items: items, onSelect: handleMenuSelect, isFocused: !isProcessing && !isReloading && !searchMode, limit: 15 }))) : (React.createElement(Text, null,
|
|
410
|
+
" items - scroll with \u2191\u2193 arrows"))),
|
|
411
|
+
React.createElement(SelectInput, { items: items, onSelect: handleMenuSelect, isFocused: !isProcessing && !isReloading && !searchMode, limit: 15 }))) : (React.createElement(Text, null, "Loading processes...")),
|
|
358
412
|
React.createElement(Box, { marginTop: 1 },
|
|
359
413
|
React.createElement(Text, { color: notificationColor ?? 'gray' }, notificationText ?? ' '))));
|
|
360
414
|
}
|
package/dist/helpers.js
CHANGED
|
@@ -13,6 +13,15 @@ export function getPomituPidsDirectory() {
|
|
|
13
13
|
export function getFileNameFriendlyName(name) {
|
|
14
14
|
return name.replaceAll(' ', '-').toLowerCase();
|
|
15
15
|
}
|
|
16
|
+
export function getPomituSignalsDirectory() {
|
|
17
|
+
return path.join(getPomituDirectory(), 'signals');
|
|
18
|
+
}
|
|
19
|
+
export function getProcessSignalPath(name) {
|
|
20
|
+
return path.join(getPomituSignalsDirectory(), `${name}.json`);
|
|
21
|
+
}
|
|
22
|
+
export function getTuiPidPath(name) {
|
|
23
|
+
return path.join(getPomituPidsDirectory(), `${name}-tui.pid`);
|
|
24
|
+
}
|
|
16
25
|
export function getProcessLogOutFilePath(name) {
|
|
17
26
|
return path.join(getPomituLogsDirectory(), `${name}-out.log`);
|
|
18
27
|
}
|
|
@@ -24,7 +33,8 @@ export function pidIsRunning(pid) {
|
|
|
24
33
|
try {
|
|
25
34
|
process.kill(pid, 0);
|
|
26
35
|
return true;
|
|
27
|
-
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
28
38
|
return false;
|
|
29
39
|
}
|
|
30
40
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { getProcessSignalPath, getTuiPidPath, getFileNameFriendlyName, pidIsRunning } from '../helpers.js';
|
|
3
|
+
import { PidManager } from './PidManager.js';
|
|
4
|
+
const SIGNAL_MAX_AGE_MS = 5000;
|
|
5
|
+
const WAIT_POLL_INTERVAL_MS = 100;
|
|
6
|
+
export function writeTuiPresence(appName) {
|
|
7
|
+
const tuiPidPath = getTuiPidPath(getFileNameFriendlyName(appName));
|
|
8
|
+
fs.writeFileSync(tuiPidPath, process.pid.toString());
|
|
9
|
+
}
|
|
10
|
+
export function clearTuiPresence(appName) {
|
|
11
|
+
const tuiPidPath = getTuiPidPath(getFileNameFriendlyName(appName));
|
|
12
|
+
if (fs.existsSync(tuiPidPath)) {
|
|
13
|
+
fs.unlinkSync(tuiPidPath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function isTuiActive(appName) {
|
|
17
|
+
const tuiPidPath = getTuiPidPath(getFileNameFriendlyName(appName));
|
|
18
|
+
if (!fs.existsSync(tuiPidPath)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const pid = parseInt(fs.readFileSync(tuiPidPath, 'utf-8'));
|
|
23
|
+
return !isNaN(pid) && pidIsRunning(pid);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function writeSignal(appName, action) {
|
|
30
|
+
const signalPath = getProcessSignalPath(getFileNameFriendlyName(appName));
|
|
31
|
+
const payload = { action, timestamp: Date.now() };
|
|
32
|
+
fs.writeFileSync(signalPath, JSON.stringify(payload));
|
|
33
|
+
}
|
|
34
|
+
export function readAndClearSignal(appName) {
|
|
35
|
+
const signalPath = getProcessSignalPath(getFileNameFriendlyName(appName));
|
|
36
|
+
if (!fs.existsSync(signalPath)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(signalPath, 'utf-8');
|
|
41
|
+
fs.unlinkSync(signalPath);
|
|
42
|
+
const payload = JSON.parse(raw);
|
|
43
|
+
if (Date.now() - payload.timestamp > SIGNAL_MAX_AGE_MS) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return payload.action;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function waitForPidState(appName, state, timeoutMs = 3000) {
|
|
53
|
+
const pidManager = new PidManager();
|
|
54
|
+
const fileNameFriendlyName = getFileNameFriendlyName(appName);
|
|
55
|
+
const deadline = Date.now() + timeoutMs;
|
|
56
|
+
while (Date.now() < deadline) {
|
|
57
|
+
const pid = pidManager.getPid(fileNameFriendlyName);
|
|
58
|
+
const isRunning = pid !== null && pidIsRunning(pid);
|
|
59
|
+
if (state === 'present' && isRunning)
|
|
60
|
+
return true;
|
|
61
|
+
if (state === 'absent' && !isRunning)
|
|
62
|
+
return true;
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, WAIT_POLL_INTERVAL_MS));
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
@@ -18,7 +18,8 @@ export class PidManager {
|
|
|
18
18
|
try {
|
|
19
19
|
const pidContent = fs.readFileSync(pidFilePath, 'utf-8');
|
|
20
20
|
return parseInt(pidContent);
|
|
21
|
-
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -32,7 +33,7 @@ export class PidManager {
|
|
|
32
33
|
if (!fs.existsSync(this.pidsDirectory)) {
|
|
33
34
|
return [];
|
|
34
35
|
}
|
|
35
|
-
return fs.readdirSync(this.pidsDirectory).filter(file => file.endsWith('.pid'));
|
|
36
|
+
return fs.readdirSync(this.pidsDirectory).filter(file => file.endsWith('.pid') && !file.endsWith('-tui.pid'));
|
|
36
37
|
}
|
|
37
38
|
getPidFilePath(appName) {
|
|
38
39
|
return path.join(this.pidsDirectory, `${appName}.pid`);
|
|
@@ -56,23 +56,13 @@ export class ProcessManager {
|
|
|
56
56
|
console.log(`${name} with pid ${pid} stopped`);
|
|
57
57
|
}
|
|
58
58
|
return true;
|
|
59
|
-
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
60
61
|
const err = error;
|
|
61
62
|
console.error(`Error stopping ${name} with pid ${pid}: ${err.message}`);
|
|
62
63
|
return false;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
|
-
async stopAllApps(options = {}) {
|
|
66
|
-
const runningProcesses = this.listRunningProcesses();
|
|
67
|
-
let stoppedCount = 0;
|
|
68
|
-
for (const processInfo of runningProcesses) {
|
|
69
|
-
const success = await this.stopApp(processInfo.name, options);
|
|
70
|
-
if (success) {
|
|
71
|
-
stoppedCount++;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return stoppedCount;
|
|
75
|
-
}
|
|
76
66
|
listRunningProcesses() {
|
|
77
67
|
const pidFiles = this.pidManager.getAllPidFiles();
|
|
78
68
|
const processes = [];
|
|
@@ -97,7 +87,8 @@ export class ProcessManager {
|
|
|
97
87
|
console.log(`Stopping ${appName} at pid ${existingPid}`);
|
|
98
88
|
try {
|
|
99
89
|
process.kill(existingPid);
|
|
100
|
-
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
101
92
|
const err = error;
|
|
102
93
|
console.error(`Error stopping ${appName}: ${err.message}`);
|
|
103
94
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pomitu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Pomitu is a process manager inspired by PM2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"author": "flawiddsouza",
|
|
32
32
|
"license": "ISC",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"chokidar": "^5.0.0",
|
|
34
35
|
"commander": "^12.1.0",
|
|
35
36
|
"ink": "^6.4.0",
|
|
36
37
|
"ink-select-input": "^6.2.0",
|