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 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.2.0.tgz && rm pomitu-1.2.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();
@@ -1,60 +1,28 @@
1
1
  import { Command } from 'commander';
2
- import { ProcessManager, ConfigManager } from '../services/index.js';
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 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) => {
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
- 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;
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
- // Check if it's a config file path
27
- let isConfigFile = false;
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) {
@@ -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
- processManager.startApp(app, {
20
- daemon: options.daemon,
21
- clearLogs: options.clearLogs
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
@@ -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 stoppedCount = await processManager.stopAllApps();
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
- const success = await processManager.stopApp(name);
20
- if (!success) {
21
- console.warn(`No running process found for ${name}`);
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 [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);
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 success = await processManager.stopApp(appName, { quiet: true });
196
- if (success) {
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: ` └─ Restart ${proc.name}`,
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: ` └─ Start ${proc.name}`,
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(() => getMenuItems(), [getMenuItems]);
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 config, 'q' or Ctrl+C to quit")),
270
- React.createElement(SelectInput, { items: items, onSelect: handleMenuSelect, isFocused: !isProcessing && !isReloading }))) : (React.createElement(Text, null, "Loading processes...")),
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.2.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,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",