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 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.3.0.tgz && rm pomitu-1.3.0.tgz
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();
@@ -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
- 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');
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
+ });
@@ -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
- 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;
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
- console.log('Running processes:');
14
- for (const process of processes) {
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
+ });
@@ -1,62 +1,33 @@
1
- import { Command } from 'commander';
2
- import { ProcessManager, ConfigManager } from '../services/index.js';
3
- export const restart = new Command('restart')
4
- .description('restart a running app or all apps')
5
- .argument('<name>', 'name of the app to restart, "all" to restart all apps, or path to config file')
6
- .option('--no-daemon', 'do not daemonize the app')
7
- .option('--clear-logs', 'clear log files before restarting the app')
8
- .action(async (name, options) => {
9
- try {
10
- const processManager = new ProcessManager();
11
- const configManager = new ConfigManager();
12
- if (name === 'all') {
13
- // Get all running processes
14
- const runningProcesses = processManager.listRunningProcesses();
15
- if (runningProcesses.length === 0) {
16
- console.warn('No running processes found');
17
- return;
18
- }
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
- } else {
25
- // Check if it's a config file path
26
- let isConfigFile = false;
27
- try {
28
- const config = configManager.readConfig(name);
29
- if (config && config.apps) {
30
- isConfigFile = true;
31
- // Restart all apps from the config
32
- for (const app of config.apps) {
33
- const success = await processManager.stopApp(app.name);
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
+ });
@@ -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
- try {
13
- const configManager = new ConfigManager();
14
- const processManager = new ProcessManager();
15
- const config = configManager.readConfig(name);
16
- configManager.validateConfig(config);
17
- const runInteractive = options.daemon === false;
18
- for (const app of config.apps) {
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
+ });
@@ -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
- try {
8
- const processManager = new ProcessManager();
9
- if (name === 'all') {
10
- const stoppedCount = await processManager.stopAllApps();
11
- if (stoppedCount === 0) {
12
- console.warn('No running processes found');
13
- } else {
14
- console.log(`Stopped ${stoppedCount} process(es)`);
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
- } else {
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
- } catch (error) {
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
- } catch (error) {
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
- } else if (removedApps.length > 0) {
109
+ }
110
+ else if (removedApps.length > 0) {
102
111
  setMessage(`Configuration reloaded. ${removedApps.length} app(s) removed (none were running)`);
103
- } else {
112
+ }
113
+ else {
104
114
  setMessage('Configuration reloaded successfully');
105
115
  }
106
116
  setMessageColor('green');
107
- } catch (error) {
117
+ }
118
+ catch (error) {
108
119
  const err = error;
109
120
  setMessage(`Error reloading config: ${err.message}`);
110
121
  setMessageColor('red');
111
- } finally {
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
- } catch (error) {
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 [action, appName] = item.value.split(':');
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
- } else if (action === 'stop') {
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
- } else {
275
+ }
276
+ else {
233
277
  setMessage(`Failed to stop ${appName}`);
234
278
  setMessageColor('red');
235
279
  }
236
- } else if (action === 'restart') {
237
- const success = await processManager.stopApp(appName, { quiet: true });
238
- if (success) {
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
- } else if (action === 'viewout') {
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
- } else if (action === 'viewerr') {
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
- } catch (error) {
309
+ }
310
+ catch (error) {
266
311
  const err = error;
267
312
  setMessage(`Error: ${err.message}`);
268
313
  setMessageColor('red');
269
- } finally {
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
- } else {
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
- } else {
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: 'column' },
337
- React.createElement(Box, { borderStyle: 'round', borderColor: 'cyan', padding: 1, marginBottom: 1 },
338
- React.createElement(Text, { bold: true, color: 'cyan' }, 'Pomitu Process Manager - Interactive Mode')),
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 }, 'Use arrow keys to navigate, Enter to select, \'/\' to search, \'r\' to reload, \'q\' or Ctrl+C to quit')),
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: 'yellow' },
344
- 'Search: ',
397
+ React.createElement(Text, { color: "yellow" },
398
+ "Search: ",
345
399
  searchQuery),
346
- React.createElement(Text, { dimColor: true }, ' (ESC to cancel, Enter to apply)'))),
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: 'green' },
349
- 'Filtering: ',
402
+ React.createElement(Text, { color: "green" },
403
+ "Filtering: ",
350
404
  searchQuery),
351
- React.createElement(Text, { dimColor: true }, ' (/ to edit, ESC to clear)'))),
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
- 'Showing 15 of ',
408
+ "Showing 15 of ",
355
409
  items.length,
356
- ' items - scroll with \u2191\u2193 arrows'))),
357
- React.createElement(SelectInput, { items: items, onSelect: handleMenuSelect, isFocused: !isProcessing && !isReloading && !searchMode, limit: 15 }))) : (React.createElement(Text, null, 'Loading processes...')),
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
- } catch {
36
+ }
37
+ catch {
28
38
  return false;
29
39
  }
30
40
  }
@@ -14,7 +14,8 @@ export class ConfigManager {
14
14
  throw new Error(`Invalid config file: ${error.message}`);
15
15
  }
16
16
  return config;
17
- } catch (error) {
17
+ }
18
+ catch (error) {
18
19
  const err = error;
19
20
  throw new Error(`Error reading config file: ${err.message}`);
20
21
  }
@@ -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
- } catch {
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
- } catch (error) {
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
- } catch (error) {
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.0",
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",