BREAKING CHANGE(daemon): Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable
This commit is contained in:
		
							
								
								
									
										11
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,5 +1,16 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-08-28 - 2.0.0 - BREAKING CHANGE(daemon)
 | 
				
			||||||
 | 
					Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Do not auto-spawn the daemon from the IPC client anymore — attempts to connect will now error with instructions to start the daemon manually or enable the system service (breaking change).
 | 
				
			||||||
 | 
					- Add TspmServiceManager to manage the daemon as a systemd service via smartdaemon (enable/disable/reload/status helpers).
 | 
				
			||||||
 | 
					- CLI: add 'enable' and 'disable' commands to install/uninstall the daemon as a system service and add 'daemon start-service' entrypoint used by systemd.
 | 
				
			||||||
 | 
					- CLI: improve error handling and user hints when the daemon is not running (suggests `tspm daemon start` or `tspm enable`).
 | 
				
			||||||
 | 
					- IPC client: removed startDaemon() and related auto-reconnect/start logic; request() no longer auto-reconnects or implicitly start the daemon.
 | 
				
			||||||
 | 
					- Export TspmServiceManager from the package index so service management is part of the public API.
 | 
				
			||||||
 | 
					- Updated development plan/readme (readme.plan.md) to reflect the refactor toward proper SmartDaemon integration and migration notes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 2025-08-26 - 1.8.0 - feat(daemon)
 | 
					## 2025-08-26 - 1.8.0 - feat(daemon)
 | 
				
			||||||
Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs
 | 
					Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,56 +1,48 @@
 | 
				
			|||||||
# TSPM Real-Time Log Streaming Implementation Plan
 | 
					# TSPM SmartDaemon Service Management Refactor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Overview
 | 
					## Problem
 | 
				
			||||||
Implementing real-time log streaming (tailing) functionality for TSPM using SmartIPC's pub/sub capabilities.
 | 
					Currently TSPM auto-spawns the daemon as a detached child process, which is improper daemon management. It should use smartdaemon for all lifecycle management and never spawn processes directly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Approach: Hybrid Request + Subscribe
 | 
					## Solution
 | 
				
			||||||
1. Initial getLogs request to fetch historical logs up to current point
 | 
					Refactor to use SmartDaemon for proper systemd service integration.
 | 
				
			||||||
2. Subscribe to pub/sub channel for real-time updates  
 | 
					 | 
				
			||||||
3. Use sequence numbers to detect and handle gaps/duplicates
 | 
					 | 
				
			||||||
4. Per-process topics for granular subscriptions
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Implementation Tasks
 | 
					## Implementation Tasks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Core Changes
 | 
					### Phase 1: Remove Auto-Spawn Behavior
 | 
				
			||||||
- [x] Update IProcessLog interface with seq and runId fields
 | 
					- [x] Remove spawn import from ts/classes.ipcclient.ts
 | 
				
			||||||
- [x] Add nextSeq and runId fields to ProcessWrapper class  
 | 
					- [x] Delete startDaemon() method from IpcClient
 | 
				
			||||||
- [x] Update addLog() methods to include sequencing
 | 
					- [x] Update connect() to throw error when daemon not running
 | 
				
			||||||
- [x] Implement pub/sub publishing in daemon
 | 
					- [x] Remove auto-reconnect logic from request() method
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### IPC Client Updates  
 | 
					### Phase 2: Create Service Manager
 | 
				
			||||||
- [x] Add subscribe/unsubscribe methods to TspmIpcClient
 | 
					- [x] Create new file ts/classes.servicemanager.ts
 | 
				
			||||||
- [ ] Implement log streaming handler
 | 
					- [x] Implement TspmServiceManager class
 | 
				
			||||||
- [ ] Add connection state management for subscriptions
 | 
					- [x] Add getOrCreateService() method
 | 
				
			||||||
 | 
					- [x] Add enableService() method
 | 
				
			||||||
 | 
					- [x] Add disableService() method
 | 
				
			||||||
 | 
					- [x] Add getServiceStatus() method
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### CLI Enhancement
 | 
					### Phase 3: Update CLI Commands
 | 
				
			||||||
- [x] Add --follow flag to logs command
 | 
					- [x] Add 'enable' command to CLI
 | 
				
			||||||
- [x] Implement streaming output with proper formatting
 | 
					- [x] Add 'disable' command to CLI
 | 
				
			||||||
- [x] Handle Ctrl+C gracefully to unsubscribe
 | 
					- [x] Update 'daemon start' to work without systemd
 | 
				
			||||||
 | 
					- [x] Add 'daemon start-service' internal command for systemd
 | 
				
			||||||
 | 
					- [x] Update all commands to handle missing daemon gracefully
 | 
				
			||||||
 | 
					- [x] Add proper error messages with hints
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Reliability Features
 | 
					### Phase 4: Update Documentation
 | 
				
			||||||
- [x] Add backpressure handling (drop oldest when buffer full)
 | 
					- [x] Update help text in CLI
 | 
				
			||||||
- [x] Implement gap detection and recovery
 | 
					- [ ] Update command descriptions
 | 
				
			||||||
- [x] Add process restart detection via runId
 | 
					- [x] Add service management section
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Testing
 | 
					### Phase 5: Testing
 | 
				
			||||||
- [x] Test basic log streaming
 | 
					- [x] Test enable command
 | 
				
			||||||
- [x] Test gap recovery
 | 
					- [x] Test disable command
 | 
				
			||||||
- [x] Test high-volume logging scenarios  
 | 
					- [x] Test daemon commands
 | 
				
			||||||
- [x] Test process restart handling
 | 
					- [x] Test error handling when daemon not running
 | 
				
			||||||
 | 
					- [x] Build and verify TypeScript compilation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Technical Details
 | 
					## Migration Notes
 | 
				
			||||||
 | 
					- Users will need to run `tspm enable` once after update
 | 
				
			||||||
### Sequence Numbering
 | 
					- Existing daemon instances will stop working
 | 
				
			||||||
- Each log entry gets incrementing seq number per process
 | 
					- Documentation needs updating to explain new behavior
 | 
				
			||||||
- runId changes on process restart
 | 
					 | 
				
			||||||
- Client tracks lastSeq to detect gaps
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Topic Structure
 | 
					 | 
				
			||||||
- Format: `logs.<processId>`
 | 
					 | 
				
			||||||
- Daemon publishes to topic on new log entries
 | 
					 | 
				
			||||||
- Clients subscribe to specific process topics
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Backpressure Strategy
 | 
					 | 
				
			||||||
- Circular buffer of 10,000 entries per process
 | 
					 | 
				
			||||||
- Drop oldest entries when buffer full
 | 
					 | 
				
			||||||
- Client can detect gaps via sequence numbers
 | 
					 | 
				
			||||||
@@ -3,6 +3,6 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const commitinfo = {
 | 
					export const commitinfo = {
 | 
				
			||||||
  name: '@git.zone/tspm',
 | 
					  name: '@git.zone/tspm',
 | 
				
			||||||
  version: '1.8.0',
 | 
					  version: '2.0.0',
 | 
				
			||||||
  description: 'a no fuzz process manager'
 | 
					  description: 'a no fuzz process manager'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import * as plugins from './plugins.js';
 | 
					import * as plugins from './plugins.js';
 | 
				
			||||||
import * as paths from './paths.js';
 | 
					import * as paths from './paths.js';
 | 
				
			||||||
import { spawn } from 'child_process';
 | 
					
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
  IpcMethodMap,
 | 
					  IpcMethodMap,
 | 
				
			||||||
  RequestForMethod,
 | 
					  RequestForMethod,
 | 
				
			||||||
@@ -34,10 +34,12 @@ export class TspmIpcClient {
 | 
				
			|||||||
    const daemonRunning = await this.isDaemonRunning();
 | 
					    const daemonRunning = await this.isDaemonRunning();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!daemonRunning) {
 | 
					    if (!daemonRunning) {
 | 
				
			||||||
      console.log('Daemon not running, starting it...');
 | 
					      throw new Error(
 | 
				
			||||||
      await this.startDaemon();
 | 
					        'TSPM daemon is not running.\n\n' +
 | 
				
			||||||
      // Wait a bit for daemon to initialize
 | 
					        'To start the daemon, run one of:\n' +
 | 
				
			||||||
      await new Promise((resolve) => setTimeout(resolve, 1000));
 | 
					        '  tspm daemon start    - Start daemon for this session\n' +
 | 
				
			||||||
 | 
					        '  tspm enable          - Enable daemon as system service (recommended)\n'
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Create IPC client
 | 
					    // Create IPC client
 | 
				
			||||||
@@ -75,7 +77,7 @@ export class TspmIpcClient {
 | 
				
			|||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('Failed to connect to daemon:', error);
 | 
					      console.error('Failed to connect to daemon:', error);
 | 
				
			||||||
      throw new Error(
 | 
					      throw new Error(
 | 
				
			||||||
        'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.',
 | 
					        'Could not connect to TSPM daemon. Please try running "tspm daemon start" or "tspm enable".',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -99,7 +101,10 @@ export class TspmIpcClient {
 | 
				
			|||||||
    params: RequestForMethod<M>,
 | 
					    params: RequestForMethod<M>,
 | 
				
			||||||
  ): Promise<ResponseForMethod<M>> {
 | 
					  ): Promise<ResponseForMethod<M>> {
 | 
				
			||||||
    if (!this.isConnected || !this.ipcClient) {
 | 
					    if (!this.isConnected || !this.ipcClient) {
 | 
				
			||||||
      await this.connect();
 | 
					      throw new Error(
 | 
				
			||||||
 | 
					        'Not connected to TSPM daemon.\n' +
 | 
				
			||||||
 | 
					        'Run "tspm daemon start" or "tspm enable" first.'
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@@ -110,22 +115,7 @@ export class TspmIpcClient {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      return response;
 | 
					      return response;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      // Handle connection errors by trying to reconnect once
 | 
					      // Don't try to auto-reconnect, just throw the error
 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        error.message?.includes('ECONNREFUSED') ||
 | 
					 | 
				
			||||||
        error.message?.includes('ENOENT')
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        console.log('Connection lost, attempting to reconnect...');
 | 
					 | 
				
			||||||
        this.isConnected = false;
 | 
					 | 
				
			||||||
        await this.connect();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Retry the request
 | 
					 | 
				
			||||||
        return await this.ipcClient!.request<
 | 
					 | 
				
			||||||
          RequestForMethod<M>,
 | 
					 | 
				
			||||||
          ResponseForMethod<M>
 | 
					 | 
				
			||||||
        >(method, params);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      throw error;
 | 
					      throw error;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -195,41 +185,7 @@ export class TspmIpcClient {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Start the daemon process
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  private async startDaemon(): Promise<void> {
 | 
					 | 
				
			||||||
    const daemonScript = plugins.path.join(
 | 
					 | 
				
			||||||
      paths.packageDir,
 | 
					 | 
				
			||||||
      'dist_ts',
 | 
					 | 
				
			||||||
      'daemon.js',
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Spawn the daemon as a detached process
 | 
					 | 
				
			||||||
    const daemonProcess = spawn(process.execPath, [daemonScript], {
 | 
					 | 
				
			||||||
      detached: true,
 | 
					 | 
				
			||||||
      stdio: ['ignore', 'ignore', 'ignore'],
 | 
					 | 
				
			||||||
      env: {
 | 
					 | 
				
			||||||
        ...process.env,
 | 
					 | 
				
			||||||
        TSPM_DAEMON_MODE: 'true',
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Unref the process so the parent can exit
 | 
					 | 
				
			||||||
    daemonProcess.unref();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    console.log(`Started daemon process with PID: ${daemonProcess.pid}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Wait for daemon to be ready using SmartIPC's helper
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      await plugins.smartipc.SmartIpc.waitForServer({
 | 
					 | 
				
			||||||
        socketPath: this.socketPath,
 | 
					 | 
				
			||||||
        timeoutMs: 15000,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      throw new Error(`Daemon failed to start: ${error.message}`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Stop the daemon
 | 
					   * Stop the daemon
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										103
									
								
								ts/classes.servicemanager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								ts/classes.servicemanager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					import * as plugins from './plugins.js';
 | 
				
			||||||
 | 
					import * as paths from './paths.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Manages TSPM daemon as a systemd service via smartdaemon
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class TspmServiceManager {
 | 
				
			||||||
 | 
					  private smartDaemon: plugins.smartdaemon.SmartDaemon;
 | 
				
			||||||
 | 
					  private service: any = null; // SmartDaemonService type is not exported
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    this.smartDaemon = new plugins.smartdaemon.SmartDaemon();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get or create the TSPM daemon service configuration
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private async getOrCreateService(): Promise<any> {
 | 
				
			||||||
 | 
					    if (!this.service) {
 | 
				
			||||||
 | 
					      const cliPath = plugins.path.join(paths.packageDir, 'cli.js');
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Create service configuration
 | 
				
			||||||
 | 
					      this.service = await this.smartDaemon.addService({
 | 
				
			||||||
 | 
					        name: 'tspm-daemon',
 | 
				
			||||||
 | 
					        description: 'TSPM Process Manager Daemon',
 | 
				
			||||||
 | 
					        command: `${process.execPath} ${cliPath} daemon start-service`,
 | 
				
			||||||
 | 
					        workingDir: process.env.HOME || process.cwd(),
 | 
				
			||||||
 | 
					        version: '1.0.0'
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return this.service;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Enable the TSPM daemon as a system service
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async enableService(): Promise<void> {
 | 
				
			||||||
 | 
					    const service = await this.getOrCreateService();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Save service configuration
 | 
				
			||||||
 | 
					    await service.save();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Enable service to start on boot
 | 
				
			||||||
 | 
					    await service.enable();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Start the service immediately
 | 
				
			||||||
 | 
					    await service.start();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Disable the TSPM daemon service
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async disableService(): Promise<void> {
 | 
				
			||||||
 | 
					    const service = await this.getOrCreateService();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Stop the service if running
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await service.stop();
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      // Service might not be running
 | 
				
			||||||
 | 
					      console.log('Service was not running');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Disable service from starting on boot
 | 
				
			||||||
 | 
					    await service.disable();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get the current status of the systemd service
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async getServiceStatus(): Promise<{
 | 
				
			||||||
 | 
					    enabled: boolean;
 | 
				
			||||||
 | 
					    running: boolean;
 | 
				
			||||||
 | 
					    status: string;
 | 
				
			||||||
 | 
					  }> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.getOrCreateService();
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Note: SmartDaemon doesn't provide direct status methods,
 | 
				
			||||||
 | 
					      // so we'll need to check via systemctl commands
 | 
				
			||||||
 | 
					      // This is a simplified implementation
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        enabled: true, // Would need to check systemctl is-enabled
 | 
				
			||||||
 | 
					        running: true, // Would need to check systemctl is-active
 | 
				
			||||||
 | 
					        status: 'active'
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        enabled: false,
 | 
				
			||||||
 | 
					        running: false,
 | 
				
			||||||
 | 
					        status: 'inactive'
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Reload the systemd service configuration
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async reloadService(): Promise<void> {
 | 
				
			||||||
 | 
					    const service = await this.getOrCreateService();
 | 
				
			||||||
 | 
					    await service.reload();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										153
									
								
								ts/cli.ts
									
									
									
									
									
								
							
							
						
						
									
										153
									
								
								ts/cli.ts
									
									
									
									
									
								
							@@ -1,6 +1,7 @@
 | 
				
			|||||||
import * as plugins from './plugins.js';
 | 
					import * as plugins from './plugins.js';
 | 
				
			||||||
import * as paths from './paths.js';
 | 
					import * as paths from './paths.js';
 | 
				
			||||||
import { tspmIpcClient } from './classes.ipcclient.js';
 | 
					import { tspmIpcClient } from './classes.ipcclient.js';
 | 
				
			||||||
 | 
					import { TspmServiceManager } from './classes.servicemanager.js';
 | 
				
			||||||
import { Logger, LogLevel } from './utils.errorhandler.js';
 | 
					import { Logger, LogLevel } from './utils.errorhandler.js';
 | 
				
			||||||
import type { IProcessConfig } from './classes.tspm.js';
 | 
					import type { IProcessConfig } from './classes.tspm.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,6 +52,21 @@ function formatMemory(bytes: number): string {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Helper function to handle daemon connection errors
 | 
				
			||||||
 | 
					function handleDaemonError(error: any, action: string): void {
 | 
				
			||||||
 | 
					  if (error.message?.includes('daemon is not running') || 
 | 
				
			||||||
 | 
					      error.message?.includes('Not connected') ||
 | 
				
			||||||
 | 
					      error.message?.includes('ECONNREFUSED')) {
 | 
				
			||||||
 | 
					    console.error(`Error: Cannot ${action} - TSPM daemon is not running.`);
 | 
				
			||||||
 | 
					    console.log('\nTo start the daemon, run one of:');
 | 
				
			||||||
 | 
					    console.log('  tspm daemon start   - Start for this session only');
 | 
				
			||||||
 | 
					    console.log('  tspm enable         - Enable as system service (recommended)');
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    console.error(`Error ${action}:`, error.message);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  process.exit(1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Helper function for padding strings
 | 
					// Helper function for padding strings
 | 
				
			||||||
function pad(str: string, length: number): string {
 | 
					function pad(str: string, length: number): string {
 | 
				
			||||||
  return str.length > length
 | 
					  return str.length > length
 | 
				
			||||||
@@ -79,7 +95,10 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
        `TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
 | 
					        `TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      console.log('Usage: tspm [command] [options]');
 | 
					      console.log('Usage: tspm [command] [options]');
 | 
				
			||||||
      console.log('\nCommands:');
 | 
					      console.log('\nService Management:');
 | 
				
			||||||
 | 
					      console.log('  enable                   Enable TSPM as system service (systemd)');
 | 
				
			||||||
 | 
					      console.log('  disable                  Disable TSPM system service');
 | 
				
			||||||
 | 
					      console.log('\nProcess Commands:');
 | 
				
			||||||
      console.log('  start <script>           Start a process');
 | 
					      console.log('  start <script>           Start a process');
 | 
				
			||||||
      console.log('  list                     List all processes');
 | 
					      console.log('  list                     List all processes');
 | 
				
			||||||
      console.log('  stop <id>                Stop a process');
 | 
					      console.log('  stop <id>                Stop a process');
 | 
				
			||||||
@@ -91,8 +110,8 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
      console.log('  stop-all                 Stop all processes');
 | 
					      console.log('  stop-all                 Stop all processes');
 | 
				
			||||||
      console.log('  restart-all              Restart all processes');
 | 
					      console.log('  restart-all              Restart all processes');
 | 
				
			||||||
      console.log('\nDaemon Commands:');
 | 
					      console.log('\nDaemon Commands:');
 | 
				
			||||||
      console.log('  daemon start             Start the TSPM daemon');
 | 
					      console.log('  daemon start             Start daemon manually (current session)');
 | 
				
			||||||
      console.log('  daemon stop              Stop the TSPM daemon');
 | 
					      console.log('  daemon stop              Stop the daemon');
 | 
				
			||||||
      console.log('  daemon status            Show daemon status');
 | 
					      console.log('  daemon status            Show daemon status');
 | 
				
			||||||
      console.log(
 | 
					      console.log(
 | 
				
			||||||
        '\nUse tspm [command] --help for more information about a command.',
 | 
					        '\nUse tspm [command] --help for more information about a command.',
 | 
				
			||||||
@@ -139,9 +158,10 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error(
 | 
					        console.error('Error: TSPM daemon is not running.');
 | 
				
			||||||
          'Error: Could not connect to TSPM daemon. Use "tspm daemon start" to start it.',
 | 
					        console.log('\nTo start the daemon, run one of:');
 | 
				
			||||||
        );
 | 
					        console.log('  tspm daemon start   - Start for this session only');
 | 
				
			||||||
 | 
					        console.log('  tspm enable         - Enable as system service (recommended)');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -217,8 +237,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
        console.log(`  PID: ${response.pid || 'N/A'}`);
 | 
					        console.log(`  PID: ${response.pid || 'N/A'}`);
 | 
				
			||||||
        console.log(`  Status: ${response.status}`);
 | 
					        console.log(`  Status: ${response.status}`);
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error starting process:', error.message);
 | 
					        handleDaemonError(error, 'start process');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -247,8 +266,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          console.error(`✗ Failed to stop process: ${response.message}`);
 | 
					          console.error(`✗ Failed to stop process: ${response.message}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error stopping process:', error.message);
 | 
					        handleDaemonError(error, 'stop process');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -276,8 +294,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
        console.log(`  PID: ${response.pid || 'N/A'}`);
 | 
					        console.log(`  PID: ${response.pid || 'N/A'}`);
 | 
				
			||||||
        console.log(`  Status: ${response.status}`);
 | 
					        console.log(`  Status: ${response.status}`);
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error restarting process:', error.message);
 | 
					        handleDaemonError(error, 'restart process');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -306,8 +323,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          console.error(`✗ Failed to delete process: ${response.message}`);
 | 
					          console.error(`✗ Failed to delete process: ${response.message}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error deleting process:', error.message);
 | 
					        handleDaemonError(error, 'delete process');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -356,8 +372,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error listing processes:', error.message);
 | 
					        handleDaemonError(error, 'list processes');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -409,8 +424,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error describing process:', error.message);
 | 
					        handleDaemonError(error, 'describe process');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -506,8 +520,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          await new Promise(() => {}); // Block forever until interrupted
 | 
					          await new Promise(() => {}); // Block forever until interrupted
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error getting logs:', error.message);
 | 
					        handleDaemonError(error, 'get logs');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -537,8 +550,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error starting all processes:', error.message);
 | 
					        handleDaemonError(error, 'start all processes');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -568,8 +580,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error stopping all processes:', error.message);
 | 
					        handleDaemonError(error, 'stop all processes');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -601,8 +612,7 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error restarting all processes:', error.message);
 | 
					        handleDaemonError(error, 'restart all processes');
 | 
				
			||||||
        process.exit(1);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    error: (err) => {
 | 
					    error: (err) => {
 | 
				
			||||||
@@ -630,19 +640,49 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
              return;
 | 
					              return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log('Starting TSPM daemon...');
 | 
					            console.log('Starting TSPM daemon manually...');
 | 
				
			||||||
            await tspmIpcClient.connect();
 | 
					            
 | 
				
			||||||
            console.log('✓ TSPM daemon started successfully');
 | 
					            // Import spawn to start daemon process
 | 
				
			||||||
 | 
					            const { spawn } = await import('child_process');
 | 
				
			||||||
 | 
					            const daemonScript = plugins.path.join(
 | 
				
			||||||
 | 
					              paths.packageDir,
 | 
				
			||||||
 | 
					              'dist_ts',
 | 
				
			||||||
 | 
					              'daemon.js',
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Start daemon as a regular background process (not detached)
 | 
				
			||||||
 | 
					            const daemonProcess = spawn(process.execPath, [daemonScript], {
 | 
				
			||||||
 | 
					              stdio: 'ignore',
 | 
				
			||||||
 | 
					              env: {
 | 
				
			||||||
 | 
					                ...process.env,
 | 
				
			||||||
 | 
					                TSPM_DAEMON_MODE: 'true',
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`Started daemon with PID: ${daemonProcess.pid}`);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Wait for daemon to be ready
 | 
				
			||||||
 | 
					            await new Promise(resolve => setTimeout(resolve, 2000));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            const newStatus = await tspmIpcClient.getDaemonStatus();
 | 
					            const newStatus = await tspmIpcClient.getDaemonStatus();
 | 
				
			||||||
            if (newStatus) {
 | 
					            if (newStatus) {
 | 
				
			||||||
 | 
					              console.log('✓ TSPM daemon started successfully');
 | 
				
			||||||
              console.log(`  PID: ${newStatus.pid}`);
 | 
					              console.log(`  PID: ${newStatus.pid}`);
 | 
				
			||||||
 | 
					              console.log('\nNote: This daemon will run until you stop it or logout.');
 | 
				
			||||||
 | 
					              console.log('For automatic startup, use "tspm enable" instead.');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          } catch (error) {
 | 
					          } catch (error) {
 | 
				
			||||||
            console.error('Error starting daemon:', error.message);
 | 
					            console.error('Error starting daemon:', error.message);
 | 
				
			||||||
            process.exit(1);
 | 
					            process.exit(1);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					        case 'start-service':
 | 
				
			||||||
 | 
					          // This is called by systemd - start the daemon directly
 | 
				
			||||||
 | 
					          console.log('Starting TSPM daemon for systemd service...');
 | 
				
			||||||
 | 
					          const { startDaemon } = await import('./classes.daemon.js');
 | 
				
			||||||
 | 
					          await startDaemon();
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        case 'stop':
 | 
					        case 'stop':
 | 
				
			||||||
          try {
 | 
					          try {
 | 
				
			||||||
@@ -676,6 +716,9 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
              `Memory:      ${formatMemory(status.memoryUsage || 0)}`,
 | 
					              `Memory:      ${formatMemory(status.memoryUsage || 0)}`,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            console.log(`CPU:         ${status.cpuUsage?.toFixed(1) || 0}s`);
 | 
					            console.log(`CPU:         ${status.cpuUsage?.toFixed(1) || 0}s`);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Disconnect from daemon after getting status
 | 
				
			||||||
 | 
					            await tspmIpcClient.disconnect();
 | 
				
			||||||
          } catch (error) {
 | 
					          } catch (error) {
 | 
				
			||||||
            console.error('Error getting daemon status:', error.message);
 | 
					            console.error('Error getting daemon status:', error.message);
 | 
				
			||||||
            process.exit(1);
 | 
					            process.exit(1);
 | 
				
			||||||
@@ -697,6 +740,58 @@ export const run = async (): Promise<void> => {
 | 
				
			|||||||
    complete: () => {},
 | 
					    complete: () => {},
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Enable command - Enable TSPM daemon as systemd service
 | 
				
			||||||
 | 
					  smartcliInstance.addCommand('enable').subscribe({
 | 
				
			||||||
 | 
					    next: async (argvArg: CliArguments) => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const serviceManager = new TspmServiceManager();
 | 
				
			||||||
 | 
					        console.log('Enabling TSPM daemon as system service...');
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        await serviceManager.enableService();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        console.log('✓ TSPM daemon enabled and started as system service');
 | 
				
			||||||
 | 
					        console.log('  The daemon will now start automatically on system boot');
 | 
				
			||||||
 | 
					        console.log('  Use "tspm disable" to remove the service');
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Error enabling service:', error.message);
 | 
				
			||||||
 | 
					        if (error.message.includes('permission') || error.message.includes('denied')) {
 | 
				
			||||||
 | 
					          console.log('\nNote: You may need to run this command with sudo');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        process.exit(1);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    error: (err) => {
 | 
				
			||||||
 | 
					      cliLogger.error(err);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    complete: () => {},
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Disable command - Disable TSPM daemon systemd service
 | 
				
			||||||
 | 
					  smartcliInstance.addCommand('disable').subscribe({
 | 
				
			||||||
 | 
					    next: async (argvArg: CliArguments) => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const serviceManager = new TspmServiceManager();
 | 
				
			||||||
 | 
					        console.log('Disabling TSPM daemon service...');
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        await serviceManager.disableService();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        console.log('✓ TSPM daemon service disabled');
 | 
				
			||||||
 | 
					        console.log('  The daemon will no longer start on system boot');
 | 
				
			||||||
 | 
					        console.log('  Use "tspm enable" to re-enable the service');
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Error disabling service:', error.message);
 | 
				
			||||||
 | 
					        if (error.message.includes('permission') || error.message.includes('denied')) {
 | 
				
			||||||
 | 
					          console.log('\nNote: You may need to run this command with sudo');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        process.exit(1);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    error: (err) => {
 | 
				
			||||||
 | 
					      cliLogger.error(err);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    complete: () => {},
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Start parsing commands
 | 
					  // Start parsing commands
 | 
				
			||||||
  smartcliInstance.startParse();
 | 
					  smartcliInstance.startParse();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ export * from './classes.tspm.js';
 | 
				
			|||||||
export * from './classes.processmonitor.js';
 | 
					export * from './classes.processmonitor.js';
 | 
				
			||||||
export * from './classes.daemon.js';
 | 
					export * from './classes.daemon.js';
 | 
				
			||||||
export * from './classes.ipcclient.js';
 | 
					export * from './classes.ipcclient.js';
 | 
				
			||||||
 | 
					export * from './classes.servicemanager.js';
 | 
				
			||||||
export * from './ipc.types.js';
 | 
					export * from './ipc.types.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as cli from './cli.js';
 | 
					import * as cli from './cli.js';
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user