pomitu 1.2.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/restart.js +19 -51
- package/dist/commands/start.js +31 -5
- package/dist/commands/stop.js +31 -4
- package/dist/components/ProcessTUI.js +161 -20
- package/dist/helpers.js +9 -0
- package/dist/services/IpcSignal.js +66 -0
- package/dist/services/PidManager.js +1 -1
- package/dist/services/ProcessManager.js +0 -11
- package/package.json +3 -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/restart.js
CHANGED
|
@@ -1,60 +1,28 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
2
|
+
import { PidManager } from '../services/PidManager.js';
|
|
3
|
+
import { isTuiActive, writeSignal, waitForPidState } from '../services/IpcSignal.js';
|
|
4
|
+
import { getFileNameFriendlyName } from '../helpers.js';
|
|
3
5
|
export const restart = new Command('restart')
|
|
4
|
-
.description('restart a running app or
|
|
5
|
-
.argument('<name>', 'name of the app to restart
|
|
6
|
-
.
|
|
7
|
-
.option('--clear-logs', 'clear log files before restarting the app')
|
|
8
|
-
.action(async (name, options) => {
|
|
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
9
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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`);
|
|
18
21
|
}
|
|
19
|
-
// Stop all processes
|
|
20
|
-
const stoppedCount = await processManager.stopAllApps();
|
|
21
|
-
console.log(`Stopped ${stoppedCount} process(es)`);
|
|
22
|
-
// We need the config to restart, but we don't have it for individual apps
|
|
23
|
-
console.warn('Note: Cannot restart apps without config file. Use "pomitu start <config-file>" to start them again.');
|
|
24
22
|
}
|
|
25
23
|
else {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const config = configManager.readConfig(name);
|
|
30
|
-
if (config && config.apps) {
|
|
31
|
-
isConfigFile = true;
|
|
32
|
-
// Restart all apps from the config
|
|
33
|
-
for (const app of config.apps) {
|
|
34
|
-
const success = await processManager.stopApp(app.name);
|
|
35
|
-
if (success) {
|
|
36
|
-
console.log(`Stopped ${app.name}`);
|
|
37
|
-
}
|
|
38
|
-
// Start the app again
|
|
39
|
-
await processManager.startApp(app, {
|
|
40
|
-
daemon: options.daemon,
|
|
41
|
-
clearLogs: options.clearLogs
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
// Not a config file, treat as app name
|
|
48
|
-
}
|
|
49
|
-
if (!isConfigFile) {
|
|
50
|
-
// Treat as app name - stop it
|
|
51
|
-
const success = await processManager.stopApp(name);
|
|
52
|
-
if (!success) {
|
|
53
|
-
console.warn(`No running process found for ${name}`);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
console.warn(`Stopped ${name}. To restart, you need to use "pomitu start <config-file>" as app config is not stored.`);
|
|
57
|
-
}
|
|
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);
|
|
58
26
|
}
|
|
59
27
|
}
|
|
60
28
|
catch (error) {
|
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';
|
|
@@ -15,13 +18,36 @@ export const start = new Command('start')
|
|
|
15
18
|
const config = configManager.readConfig(name);
|
|
16
19
|
configManager.validateConfig(config);
|
|
17
20
|
const runInteractive = options.daemon === false;
|
|
21
|
+
let anyTuiActive = false;
|
|
22
|
+
const pidManager = new PidManager();
|
|
18
23
|
for (const app of config.apps) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 {
|
|
44
|
+
processManager.startApp(app, {
|
|
45
|
+
daemon: options.daemon,
|
|
46
|
+
clearLogs: options.clearLogs
|
|
47
|
+
});
|
|
48
|
+
}
|
|
23
49
|
}
|
|
24
|
-
if (runInteractive) {
|
|
50
|
+
if (runInteractive && !anyTuiActive) {
|
|
25
51
|
render(React.createElement(ProcessTUI, {
|
|
26
52
|
configPath: name,
|
|
27
53
|
clearLogs: options.clearLogs
|
package/dist/commands/stop.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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')
|
|
@@ -7,7 +8,21 @@ export const stop = new Command('stop')
|
|
|
7
8
|
try {
|
|
8
9
|
const processManager = new ProcessManager();
|
|
9
10
|
if (name === 'all') {
|
|
10
|
-
const
|
|
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++;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const success = await processManager.stopApp(proc.name);
|
|
22
|
+
if (success)
|
|
23
|
+
stoppedCount++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
if (stoppedCount === 0) {
|
|
12
27
|
console.warn('No running processes found');
|
|
13
28
|
}
|
|
@@ -16,9 +31,21 @@ export const stop = new Command('stop')
|
|
|
16
31
|
}
|
|
17
32
|
}
|
|
18
33
|
else {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 {
|
|
45
|
+
const success = await processManager.stopApp(name);
|
|
46
|
+
if (!success) {
|
|
47
|
+
console.warn(`No running process found for ${name}`);
|
|
48
|
+
}
|
|
22
49
|
}
|
|
23
50
|
}
|
|
24
51
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
2
2
|
import readline from 'node:readline';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import open from 'open';
|
|
3
5
|
import { Box, Text, useApp, useStdin } from 'ink';
|
|
4
6
|
import SelectInput from 'ink-select-input';
|
|
5
7
|
import { ProcessManager, ConfigManager } from '../services/index.js';
|
|
6
|
-
import { getFileNameFriendlyName } 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';
|
|
7
12
|
export function ProcessTUI({ configPath, clearLogs }) {
|
|
8
13
|
const { exit } = useApp();
|
|
9
14
|
const { stdin, setRawMode } = useStdin();
|
|
@@ -13,12 +18,36 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
13
18
|
const [messageColor, setMessageColor] = useState('green');
|
|
14
19
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
15
20
|
const [isReloading, setIsReloading] = useState(false);
|
|
21
|
+
const [searchMode, setSearchMode] = useState(false);
|
|
22
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
16
23
|
const previousRawModeRef = useRef(false);
|
|
17
24
|
const rawModeCapturedRef = useRef(false);
|
|
25
|
+
const appsRef = useRef([]);
|
|
26
|
+
const ipcHandlerRef = useRef(null);
|
|
18
27
|
// Create managers only once
|
|
19
28
|
const processManager = useMemo(() => new ProcessManager(), []);
|
|
20
29
|
const configManager = useMemo(() => new ConfigManager(), []);
|
|
30
|
+
// Function to open file in native app
|
|
31
|
+
const openFileInNativeApp = useCallback(async (filePath) => {
|
|
32
|
+
if (!existsSync(filePath)) {
|
|
33
|
+
setMessage(`Log file not found: ${filePath}`);
|
|
34
|
+
setMessageColor('red');
|
|
35
|
+
setTimeout(() => setMessage(''), 3000);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
await open(filePath);
|
|
40
|
+
setMessage('Opening log file...');
|
|
41
|
+
setMessageColor('green');
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
setMessage(`Failed to open log: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
setMessageColor('red');
|
|
46
|
+
}
|
|
47
|
+
setTimeout(() => setMessage(''), 3000);
|
|
48
|
+
}, []);
|
|
21
49
|
const cleanExit = useCallback(() => {
|
|
50
|
+
appsRef.current.forEach(app => clearTuiPresence(app.name));
|
|
22
51
|
if (setRawMode) {
|
|
23
52
|
setRawMode(previousRawModeRef.current);
|
|
24
53
|
}
|
|
@@ -114,6 +143,33 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
114
143
|
useEffect(() => {
|
|
115
144
|
setProcesses(computeStatuses());
|
|
116
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
|
+
}, []);
|
|
117
173
|
// Handle keyboard input - use keypress events to intercept BEFORE SelectInput
|
|
118
174
|
useEffect(() => {
|
|
119
175
|
if (!stdin)
|
|
@@ -130,12 +186,41 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
130
186
|
setRawMode(true);
|
|
131
187
|
}
|
|
132
188
|
const handleKeypress = (str, key) => {
|
|
189
|
+
// Handle search mode
|
|
190
|
+
if (searchMode) {
|
|
191
|
+
if (key?.name === 'escape') {
|
|
192
|
+
setSearchMode(false);
|
|
193
|
+
setSearchQuery('');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (key?.name === 'backspace') {
|
|
197
|
+
setSearchQuery(prev => prev.slice(0, -1));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (key?.name === 'return') {
|
|
201
|
+
setSearchMode(false);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (str && str.length === 1 && !key?.ctrl && !key?.meta) {
|
|
205
|
+
setSearchQuery(prev => prev + str);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Normal mode
|
|
133
211
|
if (str === 'q') {
|
|
134
212
|
cleanExit();
|
|
135
213
|
}
|
|
136
214
|
if (str === 'r') {
|
|
137
215
|
reloadConfig();
|
|
138
216
|
}
|
|
217
|
+
if (str === '/') {
|
|
218
|
+
setSearchMode(true);
|
|
219
|
+
setSearchQuery('');
|
|
220
|
+
}
|
|
221
|
+
if (key?.name === 'escape' && searchQuery) {
|
|
222
|
+
setSearchQuery('');
|
|
223
|
+
}
|
|
139
224
|
if (key?.ctrl && key.name === 'c') {
|
|
140
225
|
cleanExit();
|
|
141
226
|
}
|
|
@@ -148,7 +233,7 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
148
233
|
setRawMode(previousRawModeRef.current);
|
|
149
234
|
}
|
|
150
235
|
};
|
|
151
|
-
}, [stdin, setRawMode, cleanExit, reloadConfig]);
|
|
236
|
+
}, [stdin, setRawMode, cleanExit, reloadConfig, searchMode, searchQuery]);
|
|
152
237
|
// Handle Ctrl+C signal directly - use prependListener to be first
|
|
153
238
|
useEffect(() => {
|
|
154
239
|
const handleSigInt = () => {
|
|
@@ -163,7 +248,9 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
163
248
|
const handleSelect = useCallback(async (item) => {
|
|
164
249
|
if (isProcessing || isReloading)
|
|
165
250
|
return; // Prevent multiple simultaneous operations
|
|
166
|
-
const
|
|
251
|
+
const colonIndex = item.value.indexOf(':');
|
|
252
|
+
const action = item.value.slice(0, colonIndex);
|
|
253
|
+
const appName = item.value.slice(colonIndex + 1);
|
|
167
254
|
const app = apps.find(a => a.name === appName);
|
|
168
255
|
if (!app) {
|
|
169
256
|
setMessage(`App ${appName} not found`);
|
|
@@ -192,21 +279,31 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
192
279
|
}
|
|
193
280
|
}
|
|
194
281
|
else if (action === 'restart') {
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
282
|
+
const wasRunning = await processManager.stopApp(appName, { quiet: true });
|
|
283
|
+
if (wasRunning) {
|
|
197
284
|
// Wait a bit before restarting
|
|
198
285
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
199
|
-
await processManager.startApp(app, {
|
|
200
|
-
daemon: false,
|
|
201
|
-
clearLogs: clearLogs ?? false
|
|
202
|
-
});
|
|
203
|
-
setMessage(`Restarted ${appName}`);
|
|
204
|
-
setMessageColor('green');
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
setMessage(`Failed to stop ${appName} for restart`);
|
|
208
|
-
setMessageColor('red');
|
|
209
286
|
}
|
|
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') {
|
|
295
|
+
const fileNameFriendly = getFileNameFriendlyName(appName);
|
|
296
|
+
const logPath = getProcessLogOutFilePath(fileNameFriendly);
|
|
297
|
+
openFileInNativeApp(logPath);
|
|
298
|
+
setIsProcessing(false);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
else if (action === 'viewerr') {
|
|
302
|
+
const fileNameFriendly = getFileNameFriendlyName(appName);
|
|
303
|
+
const logPath = getProcessLogErrorFilePath(fileNameFriendly);
|
|
304
|
+
openFileInNativeApp(logPath);
|
|
305
|
+
setIsProcessing(false);
|
|
306
|
+
return;
|
|
210
307
|
}
|
|
211
308
|
setProcesses(computeStatuses());
|
|
212
309
|
}
|
|
@@ -237,20 +334,43 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
237
334
|
value: `stop:${proc.name}`
|
|
238
335
|
});
|
|
239
336
|
items.push({
|
|
240
|
-
label: `
|
|
337
|
+
label: ` ├─ Restart ${proc.name}`,
|
|
241
338
|
value: `restart:${proc.name}`
|
|
242
339
|
});
|
|
340
|
+
items.push({
|
|
341
|
+
label: ' ├─ View Output Log',
|
|
342
|
+
value: `viewout:${proc.name}`
|
|
343
|
+
});
|
|
344
|
+
items.push({
|
|
345
|
+
label: ' └─ View Error Log',
|
|
346
|
+
value: `viewerr:${proc.name}`
|
|
347
|
+
});
|
|
243
348
|
}
|
|
244
349
|
else {
|
|
245
350
|
items.push({
|
|
246
|
-
label: `
|
|
351
|
+
label: ` ├─ Start ${proc.name}`,
|
|
247
352
|
value: `start:${proc.name}`
|
|
248
353
|
});
|
|
354
|
+
items.push({
|
|
355
|
+
label: ' ├─ View Output Log',
|
|
356
|
+
value: `viewout:${proc.name}`
|
|
357
|
+
});
|
|
358
|
+
items.push({
|
|
359
|
+
label: ' └─ View Error Log',
|
|
360
|
+
value: `viewerr:${proc.name}`
|
|
361
|
+
});
|
|
249
362
|
}
|
|
250
363
|
});
|
|
251
364
|
return items;
|
|
252
365
|
}, [processes]);
|
|
253
|
-
const items = useMemo(() =>
|
|
366
|
+
const items = useMemo(() => {
|
|
367
|
+
const allItems = getMenuItems();
|
|
368
|
+
if (!searchQuery)
|
|
369
|
+
return allItems;
|
|
370
|
+
const query = searchQuery.toLowerCase();
|
|
371
|
+
return allItems.filter(item => item.label.toLowerCase().includes(query) ||
|
|
372
|
+
item.value.toLowerCase().includes(query));
|
|
373
|
+
}, [getMenuItems, searchQuery]);
|
|
254
374
|
const handleMenuSelect = useCallback((item) => {
|
|
255
375
|
if (item.value === 'separator' || item.value.startsWith('info:')) {
|
|
256
376
|
// Informational rows are read-only
|
|
@@ -259,6 +379,12 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
259
379
|
handleSelect(item);
|
|
260
380
|
}
|
|
261
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]);
|
|
262
388
|
const notificationText = isProcessing ? 'Processing...' : (isReloading ? 'Reloading...' : message);
|
|
263
389
|
const notificationColor = (isProcessing || isReloading) ? 'yellow' : message ? messageColor : undefined;
|
|
264
390
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
@@ -266,8 +392,23 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
266
392
|
React.createElement(Text, { bold: true, color: "cyan" }, "Pomitu Process Manager - Interactive Mode")),
|
|
267
393
|
processes.length > 0 ? (React.createElement(React.Fragment, null,
|
|
268
394
|
React.createElement(Box, { marginBottom: 1 },
|
|
269
|
-
React.createElement(Text, { dimColor: true }, "Use arrow keys to navigate, Enter to select, 'r' to reload
|
|
270
|
-
React.createElement(
|
|
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")),
|
|
396
|
+
searchMode && (React.createElement(Box, { marginBottom: 1 },
|
|
397
|
+
React.createElement(Text, { color: "yellow" },
|
|
398
|
+
"Search: ",
|
|
399
|
+
searchQuery),
|
|
400
|
+
React.createElement(Text, { dimColor: true }, " (ESC to cancel, Enter to apply)"))),
|
|
401
|
+
searchQuery && !searchMode && (React.createElement(Box, { marginBottom: 1 },
|
|
402
|
+
React.createElement(Text, { color: "green" },
|
|
403
|
+
"Filtering: ",
|
|
404
|
+
searchQuery),
|
|
405
|
+
React.createElement(Text, { dimColor: true }, " (/ to edit, ESC to clear)"))),
|
|
406
|
+
items.length > 15 && (React.createElement(Box, { marginBottom: 1 },
|
|
407
|
+
React.createElement(Text, { dimColor: true },
|
|
408
|
+
"Showing 15 of ",
|
|
409
|
+
items.length,
|
|
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...")),
|
|
271
412
|
React.createElement(Box, { marginTop: 1 },
|
|
272
413
|
React.createElement(Text, { color: notificationColor ?? 'gray' }, notificationText ?? ' '))));
|
|
273
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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -33,7 +33,7 @@ export class PidManager {
|
|
|
33
33
|
if (!fs.existsSync(this.pidsDirectory)) {
|
|
34
34
|
return [];
|
|
35
35
|
}
|
|
36
|
-
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'));
|
|
37
37
|
}
|
|
38
38
|
getPidFilePath(appName) {
|
|
39
39
|
return path.join(this.pidsDirectory, `${appName}.pid`);
|
|
@@ -63,17 +63,6 @@ export class ProcessManager {
|
|
|
63
63
|
return false;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
async stopAllApps(options = {}) {
|
|
67
|
-
const runningProcesses = this.listRunningProcesses();
|
|
68
|
-
let stoppedCount = 0;
|
|
69
|
-
for (const processInfo of runningProcesses) {
|
|
70
|
-
const success = await this.stopApp(processInfo.name, options);
|
|
71
|
-
if (success) {
|
|
72
|
-
stoppedCount++;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return stoppedCount;
|
|
76
|
-
}
|
|
77
66
|
listRunningProcesses() {
|
|
78
67
|
const pidFiles = this.pidManager.getAllPidFiles();
|
|
79
68
|
const processes = [];
|
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,9 +31,11 @@
|
|
|
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",
|
|
38
|
+
"open": "^11.0.0",
|
|
37
39
|
"react": "^19.2.0",
|
|
38
40
|
"shell-quote": "^1.8.1",
|
|
39
41
|
"yaml": "^2.5.1",
|