Compare commits

...

5 Commits

Author SHA1 Message Date
b1a543092a 3.1.1
Some checks failed
Default (tags) / security (push) Successful in 59s
Default (tags) / test (push) Failing after 1m23s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-28 18:34:56 +00:00
4ee4bcdda2 fix(cli): Fix internal imports, centralize IPC types and improve daemon entry/start behavior 2025-08-28 18:34:56 +00:00
529a403c4b 3.1.0
Some checks failed
Default (tags) / security (push) Successful in 1m1s
Default (tags) / test (push) Failing after 1m25s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-28 18:17:41 +00:00
ece16b75e2 feat(daemon): Reorganize and refactor core into client/daemon/shared modules; add IPC protocol and tests 2025-08-28 18:17:41 +00:00
1516185c4d prepare refactor 2025-08-28 18:10:33 +00:00
38 changed files with 444 additions and 158 deletions

View File

@@ -1,5 +1,25 @@
# Changelog # Changelog
## 2025-08-28 - 3.1.1 - fix(cli)
Fix internal imports, centralize IPC types and improve daemon entry/start behavior
- Corrected import paths in CLI commands and utilities to use client/tspm.ipcclient and shared/common/utils.errorhandler
- Centralized process/IPC type definitions into ts/shared/protocol/ipc.types.ts and updated references across daemon and client code
- Refactored ts/daemon/index.ts to export startDaemon and only auto-start the daemon when the module is executed directly
- Adjusted ts/index.ts exports to expose client API, shared protocol types, and daemon start entrypoint
## 2025-08-28 - 3.1.0 - feat(daemon)
Reorganize and refactor core into client/daemon/shared modules; add IPC protocol and tests
- Reorganized core code: split daemon and client logic into ts/daemon and ts/client directories
- Moved process management into ProcessManager, ProcessMonitor and ProcessWrapper under ts/daemon
- Added a dedicated IPC client and service manager under ts/client (tspm.ipcclient, tspm.servicemanager)
- Introduced shared protocol and error handling: ts/shared/protocol/ipc.types.ts, protocol.version.ts and ts/shared/common/utils.errorhandler.ts
- Updated CLI to import Logger from shared/common utils and updated related helpers
- Added daemon entrypoint at ts/daemon/index.ts and reorganized daemon startup/shutdown/heartbeat handling
- Added test assets (test/testassets/simple-test.ts, simple-script2.ts) and expanded test files under test/
- Removed legacy top-level class files (classes.*) in favor of the new structured layout
## 2025-08-28 - 3.0.2 - fix(daemon) ## 2025-08-28 - 3.0.2 - fix(daemon)
Ensure TSPM runtime dir exists and improve daemon startup/debug output Ensure TSPM runtime dir exists and improve daemon startup/debug output

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tspm", "name": "@git.zone/tspm",
"version": "3.0.2", "version": "3.1.1",
"private": false, "private": false,
"description": "a no fuzz process manager", "description": "a no fuzz process manager",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@@ -1,56 +1,249 @@
# TSPM SmartDaemon Service Management Refactor # TSPM Architecture Refactoring Plan
## Problem ## Current Problems
The current architecture has several issues that make the codebase confusing:
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. 1. **Flat structure confusion**: All classes are mixed together in the `ts/` directory with a `classes.` prefix naming convention
2. **Unclear boundaries**: It's hard to tell what code runs in the daemon vs the client
3. **Misleading naming**: The `Tspm` class is actually the core ProcessManager, not the overall system
4. **Coupling risk**: Client code could accidentally import daemon internals, bloating bundles
5. **No architectural enforcement**: Nothing prevents cross-boundary imports
## Solution ## Goal
Refactor into a clean 3-folder architecture (daemon/client/shared) with proper separation of concerns and enforced boundaries.
Refactor to use SmartDaemon for proper systemd service integration. ## Key Insights from Architecture Review
## Implementation Tasks ### Why This Separation Makes Sense
After discussion with GPT-5, we identified that:
### Phase 1: Remove Auto-Spawn Behavior 1. **ProcessManager/Monitor/Wrapper are daemon-only**: These classes actually spawn and manage processes. Clients never need them - they only communicate via IPC.
- [x] Remove spawn import from ts/classes.ipcclient.ts 2. **The client is just an IPC bridge**: The client (CLI and library users) only needs to send messages to the daemon and receive responses. It should never directly manage processes.
- [x] Delete startDaemon() method from IpcClient
- [x] Update connect() to throw error when daemon not running
- [x] Remove auto-reconnect logic from request() method
### Phase 2: Create Service Manager 3. **Shared should be minimal**: Only the IPC protocol types and pure utilities should be shared. No Node.js APIs, no file system access.
- [x] Create new file ts/classes.servicemanager.ts 4. **Protocol is the contract**: The IPC types are the only coupling between client and daemon. This allows independent evolution.
- [x] Implement TspmServiceManager class
- [x] Add getOrCreateService() method
- [x] Add enableService() method
- [x] Add disableService() method
- [x] Add getServiceStatus() method
### Phase 3: Update CLI Commands ## Architecture Overview
- [x] Add 'enable' command to CLI ### Folder Structure
- [x] Add 'disable' command to CLI - **ts/daemon/** - Process orchestration (runs in daemon process only)
- [x] Update 'daemon start' to work without systemd - Contains all process management logic
- [x] Add 'daemon start-service' internal command for systemd - Spawns and monitors actual system processes
- [x] Update all commands to handle missing daemon gracefully - Manages configuration and state
- [x] Add proper error messages with hints - Never imported by client code
### Phase 4: Update Documentation - **ts/client/** - IPC communication (runs in CLI/client process)
- Only knows how to talk to the daemon via IPC
- Lightweight - no process management logic
- What library users import when they use TSPM
- Can work in any Node.js environment (or potentially browser)
- [x] Update help text in CLI - **ts/shared/** - Minimal shared contract (protocol & pure utilities)
- [ ] Update command descriptions - **protocol/** - IPC request/response types, error codes, version
- [x] Add service management section - **common/** - Pure utilities with no environment dependencies
- No fs, net, child_process, or Node-specific APIs
- Keep as small as possible to minimize coupling
### Phase 5: Testing ## File Organization Rationale
- [x] Test enable command ### What Goes in Daemon
- [x] Test disable command These files are daemon-only because they actually manage processes:
- [x] Test daemon commands - `processmanager.ts` (was Tspm) - Core process orchestration logic
- [x] Test error handling when daemon not running - `processmonitor.ts` - Monitors memory and restarts processes
- [x] Build and verify TypeScript compilation - `processwrapper.ts` - Wraps child processes with logging
- `tspm.config.ts` - Persists process configurations to disk
- `tspm.daemon.ts` - Wires everything together, handles IPC requests
## Migration Notes ### What Goes in Client
These files are client-only because they just communicate:
- `tspm.ipcclient.ts` - Sends requests to daemon via Unix socket
- `tspm.servicemanager.ts` - Manages systemd service (delegates to smartdaemon)
- CLI files - Command-line interface that uses the IPC client
- Users will need to run `tspm enable` once after update ### What Goes in Shared
- Existing daemon instances will stop working Only the absolute minimum needed by both:
- Documentation needs updating to explain new behavior - `protocol/ipc.types.ts` - Request/response type definitions
- `protocol/error.codes.ts` - Standardized error codes
- `common/utils.errorhandler.ts` - If it's pure (no I/O)
- Parts of `paths.ts` - Constants like socket path (not OS-specific resolution)
- Plugin interfaces only (not loading logic)
### Critical Design Decisions
1. **Rename Tspm to ProcessManager**: The class name should reflect what it does
2. **No process management in shared**: ProcessManager, ProcessMonitor, ProcessWrapper are daemon-only
3. **Protocol versioning**: Add version to allow client/daemon compatibility
4. **Enforce boundaries**: Use TypeScript project references to prevent violations
5. **Control exports**: Package.json exports map ensures library users can't import daemon code
## Detailed Task List
### Phase 1: Create New Structure
- [ ] Create directory `ts/daemon/`
- [ ] Create directory `ts/client/`
- [ ] Create directory `ts/shared/`
- [ ] Create directory `ts/shared/protocol/`
- [ ] Create directory `ts/shared/common/`
### Phase 2: Move Daemon Files
- [ ] Move `ts/daemon.ts``ts/daemon/index.ts`
- [ ] Move `ts/classes.daemon.ts``ts/daemon/tspm.daemon.ts`
- [ ] Move `ts/classes.tspm.ts``ts/daemon/processmanager.ts`
- [ ] Move `ts/classes.processmonitor.ts``ts/daemon/processmonitor.ts`
- [ ] Move `ts/classes.processwrapper.ts``ts/daemon/processwrapper.ts`
- [ ] Move `ts/classes.config.ts``ts/daemon/tspm.config.ts`
### Phase 3: Move Client Files
- [ ] Move `ts/classes.ipcclient.ts``ts/client/tspm.ipcclient.ts`
- [ ] Move `ts/classes.servicemanager.ts``ts/client/tspm.servicemanager.ts`
- [ ] Create `ts/client/index.ts` barrel export file
### Phase 4: Move Shared Files
- [ ] Move `ts/ipc.types.ts``ts/shared/protocol/ipc.types.ts`
- [ ] Create `ts/shared/protocol/protocol.version.ts` with version constant
- [ ] Create `ts/shared/protocol/error.codes.ts` with standardized error codes
- [ ] Move `ts/utils.errorhandler.ts``ts/shared/common/utils.errorhandler.ts`
- [ ] Analyze `ts/paths.ts` - split into constants (shared) vs resolvers (daemon)
- [ ] Move/split `ts/plugins.ts` - interfaces to shared, loaders to daemon
### Phase 5: Rename Classes
- [ ] In `processmanager.ts`: Rename class `Tspm``ProcessManager`
- [ ] Update all references to `Tspm` class to use `ProcessManager`
- [ ] Update constructor in `tspm.daemon.ts` to use `ProcessManager`
### Phase 6: Update Imports - Daemon Files
- [ ] Update imports in `ts/daemon/index.ts`
- [ ] Update imports in `ts/daemon/tspm.daemon.ts`
- [ ] Change `'./classes.tspm.js'``'./processmanager.js'`
- [ ] Change `'./paths.js'` → appropriate shared/daemon path
- [ ] Change `'./ipc.types.js'``'../shared/protocol/ipc.types.js'`
- [ ] Update imports in `ts/daemon/processmanager.ts`
- [ ] Change `'./classes.processmonitor.js'``'./processmonitor.js'`
- [ ] Change `'./classes.processwrapper.js'``'./processwrapper.js'`
- [ ] Change `'./classes.config.js'``'./tspm.config.js'`
- [ ] Change `'./utils.errorhandler.js'``'../shared/common/utils.errorhandler.js'`
- [ ] Update imports in `ts/daemon/processmonitor.ts`
- [ ] Change `'./classes.processwrapper.js'``'./processwrapper.js'`
- [ ] Update imports in `ts/daemon/processwrapper.ts`
- [ ] Update imports in `ts/daemon/tspm.config.ts`
### Phase 7: Update Imports - Client Files
- [ ] Update imports in `ts/client/tspm.ipcclient.ts`
- [ ] Change `'./paths.js'` → appropriate shared/daemon path
- [ ] Change `'./ipc.types.js'``'../shared/protocol/ipc.types.js'`
- [ ] Update imports in `ts/client/tspm.servicemanager.ts`
- [ ] Change `'./paths.js'` → appropriate shared/daemon path
- [ ] Create exports in `ts/client/index.ts`
- [ ] Export TspmIpcClient
- [ ] Export TspmServiceManager
### Phase 8: Update Imports - CLI Files
- [ ] Update imports in `ts/cli/index.ts`
- [ ] Change `'../classes.ipcclient.js'``'../client/tspm.ipcclient.js'`
- [ ] Update imports in `ts/cli/commands/service/enable.ts`
- [ ] Change `'../../../classes.servicemanager.js'``'../../../client/tspm.servicemanager.js'`
- [ ] Update imports in `ts/cli/commands/service/disable.ts`
- [ ] Change `'../../../classes.servicemanager.js'``'../../../client/tspm.servicemanager.js'`
- [ ] Update imports in `ts/cli/commands/daemon/index.ts`
- [ ] Change `'../../../classes.daemon.js'``'../../../daemon/tspm.daemon.js'`
- [ ] Change `'../../../classes.ipcclient.js'``'../../../client/tspm.ipcclient.js'`
- [ ] Update imports in `ts/cli/commands/process/*.ts` files
- [ ] Change all `'../../../classes.ipcclient.js'``'../../../client/tspm.ipcclient.js'`
- [ ] Change all `'../../../classes.tspm.js'``'../../../shared/protocol/ipc.types.js'` (for types)
- [ ] Update imports in `ts/cli/registration/index.ts`
- [ ] Change `'../../classes.ipcclient.js'``'../../client/tspm.ipcclient.js'`
### Phase 9: Update Main Exports
- [ ] Update `ts/index.ts`
- [ ] Remove `export * from './classes.tspm.js'`
- [ ] Remove `export * from './classes.processmonitor.js'`
- [ ] Remove `export * from './classes.processwrapper.js'`
- [ ] Remove `export * from './classes.daemon.js'`
- [ ] Remove `export * from './classes.ipcclient.js'`
- [ ] Remove `export * from './classes.servicemanager.js'`
- [ ] Add `export * from './client/index.js'`
- [ ] Add `export * from './shared/protocol/ipc.types.js'`
- [ ] Add `export { startDaemon } from './daemon/index.js'`
### Phase 10: Update Package.json
- [ ] Add exports map to package.json:
```json
"exports": {
".": "./dist_ts/client/index.js",
"./client": "./dist_ts/client/index.js",
"./daemon": "./dist_ts/daemon/index.js",
"./protocol": "./dist_ts/shared/protocol/ipc.types.js"
}
```
### Phase 11: TypeScript Configuration
- [ ] Create `tsconfig.base.json` with common settings
- [ ] Create `tsconfig.shared.json` for shared code
- [ ] Create `tsconfig.client.json` with reference to shared
- [ ] Create `tsconfig.daemon.json` with reference to shared
- [ ] Update main `tsconfig.json` to use references
### Phase 12: Testing
- [ ] Run `pnpm run build` and fix any compilation errors
- [ ] Test daemon startup: `./cli.js daemon start`
- [ ] Test process management: `./cli.js start "echo test"`
- [ ] Test client commands: `./cli.js list`
- [ ] Run existing tests: `pnpm test`
- [ ] Update test imports if needed
### Phase 13: Documentation
- [ ] Update README.md if needed
- [ ] Document the new architecture in a comment at top of ts/index.ts
- [ ] Add comments explaining the separation in each index.ts file
### Phase 14: Cleanup
- [ ] Delete empty directories from old structure
- [ ] Verify no broken imports remain
- [ ] Run linter and fix any issues
- [ ] Commit with message: "refactor(architecture): reorganize into daemon/client/shared structure"
## Benefits After Completion
### Immediate Benefits
- **Clear separation**: Instantly obvious what runs where (daemon vs client)
- **Smaller client bundles**: Client code won't accidentally include ProcessMonitor, ProcessWrapper, etc.
- **Better testing**: Can test client and daemon independently
- **Cleaner imports**: No more confusing `classes.` prefix on everything
### Architecture Benefits
- **Enforced boundaries**: TypeScript project references prevent cross-imports
- **Protocol as contract**: Client and daemon can evolve independently
- **Version compatibility**: Protocol versioning allows client/daemon version skew
- **Security**: Internal daemon errors don't leak to clients over IPC
### Future Benefits
- **Browser support**: Clean client could potentially work in browser
- **Embedded mode**: Could add option to run ProcessManager in-process
- **Plugin system**: Clear boundary for plugin interfaces vs implementation
- **Multi-language clients**: Other languages only need to implement IPC protocol
## Implementation Safeguards (from GPT-5 Review)
### Boundary Enforcement
- **TypeScript project references**: Separate tsconfig files prevent illegal imports
- **ESLint rules**: Use `import/no-restricted-paths` to catch violations
- **Package.json exports**: Control what external consumers can import
### Keep Shared Minimal
- **No Node.js APIs**: No fs, net, child_process in shared
- **No environment access**: No process.env, no OS-specific code
- **Pure functions only**: Shared utilities must be environment-agnostic
- **Protocol-focused**: Mainly type definitions and constants
### Prevent Environment Bleed
- **Split paths.ts**: Constants (shared) vs OS-specific resolution (daemon)
- **Plugin interfaces only**: Loading/discovery stays in daemon
- **No dynamic imports**: Keep shared statically analyzable
### Future-Proofing
- **Protocol versioning**: Add version field for compatibility
- **Error codes**: Standardized errors instead of string messages
- **Capability negotiation**: Client can query daemon capabilities
- **Subpath exports**: Different entry points for different use cases

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tspm', name: '@git.zone/tspm',
version: '3.0.2', version: '3.1.1',
description: 'a no fuzz process manager' description: 'a no fuzz process manager'
} }

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';

View File

@@ -1,7 +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 '../../../client/tspm.ipcclient.js';
import { Logger } from '../../../utils.errorhandler.js'; import { Logger } from '../../../shared/common/utils.errorhandler.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { formatMemory } from '../../helpers/memory.js'; import { formatMemory } from '../../helpers/memory.js';
@@ -83,7 +83,7 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
case 'start-service': case 'start-service':
// This is called by systemd - start the daemon directly // This is called by systemd - start the daemon directly
console.log('Starting TSPM daemon for systemd service...'); console.log('Starting TSPM daemon for systemd service...');
const { startDaemon } = await import('../../../classes.daemon.js'); const { startDaemon } = await import('../../../daemon/tspm.daemon.js');
await startDaemon(); await startDaemon();
break; break;

View File

@@ -1,7 +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 '../../client/tspm.ipcclient.js';
import { Logger } from '../../utils.errorhandler.js'; import { Logger } from '../../shared/common/utils.errorhandler.js';
import type { CliArguments } from '../types.js'; import type { CliArguments } from '../types.js';
import { pad } from '../helpers/formatting.js'; import { pad } from '../helpers/formatting.js';
import { formatMemory } from '../helpers/memory.js'; import { formatMemory } from '../helpers/memory.js';

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';
import { formatMemory } from '../../helpers/memory.js'; import { formatMemory } from '../../helpers/memory.js';

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';
import { pad } from '../../helpers/formatting.js'; import { pad } from '../../helpers/formatting.js';

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';
import { getBool, getNumber } from '../../helpers/argv.js'; import { getBool, getNumber } from '../../helpers/argv.js';

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';

View File

@@ -1,6 +1,6 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { IProcessConfig } from '../../../classes.tspm.js'; import type { IProcessConfig } from '../../../shared/protocol/ipc.types.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { parseMemoryString, formatMemory } from '../../helpers/memory.js'; import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';
@@ -10,10 +10,16 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
smartcli, smartcli,
'start', 'start',
async (argvArg: CliArguments) => { async (argvArg: CliArguments) => {
const script = argvArg._[1]; // Get all arguments after 'start' command
if (!script) { const commandArgs = argvArg._.slice(1);
console.error('Error: Please provide a script to run'); if (commandArgs.length === 0) {
console.log('Usage: tspm start <script> [options]'); console.error('Error: Please provide a command to run');
console.log('Usage: tspm start <command> [options]');
console.log('\nExamples:');
console.log(' tspm start "npm run dev"');
console.log(' tspm start pnpm start');
console.log(' tspm start node server.js');
console.log(' tspm start script.ts');
console.log('\nOptions:'); console.log('\nOptions:');
console.log(' --name <name> Name for the process'); console.log(' --name <name> Name for the process');
console.log( console.log(
@@ -27,17 +33,25 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
console.log(' --autorestart Auto-restart on crash'); console.log(' --autorestart Auto-restart on crash');
return; return;
} }
// Join all command parts to form the full command
const script = commandArgs.join(' ');
const memoryLimit = argvArg.memory const memoryLimit = argvArg.memory
? parseMemoryString(argvArg.memory) ? parseMemoryString(argvArg.memory)
: 512 * 1024 * 1024; : 512 * 1024 * 1024;
const projectDir = argvArg.cwd || process.cwd(); const projectDir = argvArg.cwd || process.cwd();
// Direct .ts support via tsx (bundled with TSPM) // Parse the command to determine if we need to handle .ts files
let actualCommand = script; let actualCommand: string;
let commandArgs: string[] | undefined = undefined; let processArgs: string[] | undefined = undefined;
if (script.endsWith('.ts')) { // Split the script to check if it's a single .ts file or a full command
const scriptParts = script.split(' ');
const firstPart = scriptParts[0];
// Check if this is a direct .ts file execution (single argument ending in .ts)
if (scriptParts.length === 1 && firstPart.endsWith('.ts')) {
try { try {
const tsxPath = await (async () => { const tsxPath = await (async () => {
const { createRequire } = await import('module'); const { createRequire } = await import('module');
@@ -45,15 +59,20 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
return require.resolve('tsx/dist/cli.mjs'); return require.resolve('tsx/dist/cli.mjs');
})(); })();
const scriptPath = plugins.path.isAbsolute(script) const scriptPath = plugins.path.isAbsolute(firstPart)
? script ? firstPart
: plugins.path.join(projectDir, script); : plugins.path.join(projectDir, firstPart);
actualCommand = tsxPath; actualCommand = tsxPath;
commandArgs = [scriptPath]; processArgs = [scriptPath];
} catch { } catch {
actualCommand = 'tsx'; actualCommand = 'tsx';
commandArgs = [script]; processArgs = [firstPart];
} }
} else {
// For multi-word commands, use the entire script as the command
// This handles cases like "pnpm start", "npm run dev", etc.
actualCommand = script;
processArgs = undefined;
} }
const name = argvArg.name || script; const name = argvArg.name || script;
@@ -69,7 +88,7 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'), id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
name, name,
command: actualCommand, command: actualCommand,
args: commandArgs, args: processArgs,
projectDir, projectDir,
memoryLimitBytes: memoryLimit, memoryLimitBytes: memoryLimit,
autorestart, autorestart,
@@ -79,7 +98,7 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
console.log(`Starting process: ${name}`); console.log(`Starting process: ${name}`);
console.log( console.log(
` Command: ${script}${script.endsWith('.ts') ? ' (via tsx)' : ''}`, ` Command: ${script}${scriptParts.length === 1 && firstPart.endsWith('.ts') ? ' (via tsx)' : ''}`,
); );
console.log(` Directory: ${projectDir}`); console.log(` Directory: ${projectDir}`);
console.log(` Memory limit: ${formatMemory(memoryLimit)}`); console.log(` Memory limit: ${formatMemory(memoryLimit)}`);

View File

@@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';

View File

@@ -1,6 +1,6 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { TspmServiceManager } from '../../../classes.servicemanager.js'; import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
import { Logger } from '../../../utils.errorhandler.js'; import { Logger } from '../../../shared/common/utils.errorhandler.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
export function registerDisableCommand(smartcli: plugins.smartcli.Smartcli) { export function registerDisableCommand(smartcli: plugins.smartcli.Smartcli) {

View File

@@ -1,6 +1,6 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { TspmServiceManager } from '../../../classes.servicemanager.js'; import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
import { Logger } from '../../../utils.errorhandler.js'; import { Logger } from '../../../shared/common/utils.errorhandler.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
export function registerEnableCommand(smartcli: plugins.smartcli.Smartcli) { export function registerEnableCommand(smartcli: plugins.smartcli.Smartcli) {

View File

@@ -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 { Logger, LogLevel } from '../utils.errorhandler.js'; import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
// Import command registration functions // Import command registration functions
import { registerDefaultCommand } from './commands/default.js'; import { registerDefaultCommand } from './commands/default.js';

View File

@@ -1,4 +1,4 @@
import { tspmIpcClient } from '../../classes.ipcclient.js'; import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
/** /**
* Preflight the daemon if required. Uses getDaemonStatus() which is safe and cheap: * Preflight the daemon if required. Uses getDaemonStatus() which is safe and cheap:

View File

@@ -1,4 +1,4 @@
import { tspmIpcClient } from '../../classes.ipcclient.js'; import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
// Helper function to run IPC commands with automatic disconnect // Helper function to run IPC commands with automatic disconnect
export async function runIpcCommand<T>(body: () => Promise<T>): Promise<T> { export async function runIpcCommand<T>(body: () => Promise<T>): Promise<T> {

8
ts/client/index.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* Client-side exports for TSPM
* These are the only components that client applications should use
* They only communicate with the daemon via IPC, never directly manage processes
*/
export * from './tspm.ipcclient.js';
export * from './tspm.servicemanager.js';

View File

@@ -1,11 +1,11 @@
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 type { import type {
IpcMethodMap, IpcMethodMap,
RequestForMethod, RequestForMethod,
ResponseForMethod, ResponseForMethod,
} from './ipc.types.js'; } from '../shared/protocol/ipc.types.js';
/** /**
* IPC client for communicating with the TSPM daemon * IPC client for communicating with the TSPM daemon

View File

@@ -1,5 +1,5 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
import * as paths from './paths.js'; import * as paths from '../paths.js';
/** /**
* Manages TSPM daemon as a systemd service via smartdaemon * Manages TSPM daemon as a systemd service via smartdaemon

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env node
import { startDaemon } from './classes.daemon.js';
// Start the daemon
startDaemon().catch((error) => {
console.error('Failed to start daemon:', error);
process.exit(1);
});

18
ts/daemon/index.ts Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
/**
* Daemon entry point - runs process management server
* This should only be run directly by the CLI or as a systemd service
*/
export { startDaemon } from './tspm.daemon.js';
// When executed directly (not imported), start the daemon
if (import.meta.url === `file://${process.argv[1]}`) {
import('./tspm.daemon.js').then(({ startDaemon }) => {
startDaemon().catch((error) => {
console.error('Failed to start daemon:', error);
process.exit(1);
});
});
}

View File

@@ -1,38 +1,25 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import * as paths from './paths.js'; import * as paths from '../paths.js';
import { import { ProcessMonitor } from './processmonitor.js';
ProcessMonitor, import { TspmConfig } from './tspm.config.js';
type IMonitorConfig,
} from './classes.processmonitor.js';
import { type IProcessLog } from './classes.processwrapper.js';
import { TspmConfig } from './classes.config.js';
import { import {
Logger, Logger,
ProcessError, ProcessError,
ConfigError, ConfigError,
ValidationError, ValidationError,
handleError, handleError,
} from './utils.errorhandler.js'; } from '../shared/common/utils.errorhandler.js';
import type {
IProcessConfig,
IProcessInfo,
IProcessLog,
IMonitorConfig
} from '../shared/protocol/ipc.types.js';
export interface IProcessConfig extends IMonitorConfig {
id: string; // Unique identifier for the process
autorestart: boolean; // Whether to restart the process automatically on crash
watch?: boolean; // Whether to watch for file changes and restart
watchPaths?: string[]; // Paths to watch for changes
}
export interface IProcessInfo {
id: string;
pid?: number;
status: 'online' | 'stopped' | 'errored';
memory: number;
cpu?: number;
uptime?: number;
restarts: number;
}
export class Tspm extends EventEmitter { export class ProcessManager extends EventEmitter {
public processes: Map<string, ProcessMonitor> = new Map(); public processes: Map<string, ProcessMonitor> = new Map();
public processConfigs: Map<string, IProcessConfig> = new Map(); public processConfigs: Map<string, IProcessConfig> = new Map();
public processInfo: Map<string, IProcessInfo> = new Map(); public processInfo: Map<string, IProcessInfo> = new Map();

View File

@@ -1,18 +1,8 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js'; import { ProcessWrapper } from './processwrapper.js';
import { Logger, ProcessError, handleError } from './utils.errorhandler.js'; import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
import type { IMonitorConfig, IProcessLog } from '../shared/protocol/ipc.types.js';
export interface IMonitorConfig {
name?: string; // Optional name to identify the instance
projectDir: string; // Directory where the command will run
command: string; // Full command to run (e.g., "npm run xyz")
args?: string[]; // Optional: arguments for the command
memoryLimitBytes: number; // Maximum allowed memory (in bytes) for the process group
monitorIntervalMs?: number; // Interval (in ms) at which memory is checked (default: 5000)
env?: NodeJS.ProcessEnv; // Optional: custom environment variables
logBufferSize?: number; // Optional: number of log lines to keep (default: 100)
}
export class ProcessMonitor extends EventEmitter { export class ProcessMonitor extends EventEmitter {
private processWrapper: ProcessWrapper | null = null; private processWrapper: ProcessWrapper | null = null;

View File

@@ -1,6 +1,7 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Logger, ProcessError, handleError } from './utils.errorhandler.js'; import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
import type { IProcessLog } from '../shared/protocol/ipc.types.js';
export interface IProcessWrapperOptions { export interface IProcessWrapperOptions {
command: string; command: string;
@@ -11,14 +12,6 @@ export interface IProcessWrapperOptions {
logBuffer?: number; // Number of log lines to keep in memory (default: 100) logBuffer?: number; // Number of log lines to keep in memory (default: 100)
} }
export interface IProcessLog {
timestamp: Date;
type: 'stdout' | 'stderr' | 'system';
message: string;
seq: number;
runId: string;
}
export class ProcessWrapper extends EventEmitter { export class ProcessWrapper extends EventEmitter {
private process: plugins.childProcess.ChildProcess | null = null; private process: plugins.childProcess.ChildProcess | null = null;
private options: IProcessWrapperOptions; private options: IProcessWrapperOptions;

View File

@@ -1,4 +1,4 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
export class TspmConfig { export class TspmConfig {
public npmextraInstance = new plugins.npmextra.KeyValueStore({ public npmextraInstance = new plugins.npmextra.KeyValueStore({

View File

@@ -1,19 +1,19 @@
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 { Tspm } from './classes.tspm.js'; import { ProcessManager } from './processmanager.js';
import type { import type {
IpcMethodMap, IpcMethodMap,
RequestForMethod, RequestForMethod,
ResponseForMethod, ResponseForMethod,
DaemonStatusResponse, DaemonStatusResponse,
HeartbeatResponse, HeartbeatResponse,
} from './ipc.types.js'; } from '../shared/protocol/ipc.types.js';
/** /**
* Central daemon server that manages all TSPM processes * Central daemon server that manages all TSPM processes
*/ */
export class TspmDaemon { export class TspmDaemon {
private tspmInstance: Tspm; private tspmInstance: ProcessManager;
private ipcServer: plugins.smartipc.IpcServer; private ipcServer: plugins.smartipc.IpcServer;
private startTime: number; private startTime: number;
private isShuttingDown: boolean = false; private isShuttingDown: boolean = false;
@@ -22,7 +22,7 @@ export class TspmDaemon {
private daemonPidFile: string; private daemonPidFile: string;
constructor() { constructor() {
this.tspmInstance = new Tspm(); this.tspmInstance = new ProcessManager();
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock'); this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid'); this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
this.startTime = Date.now(); this.startTime = Date.now();

View File

@@ -1,9 +1,11 @@
export * from './classes.tspm.js'; // Client exports - for library consumers
export * from './classes.processmonitor.js'; export * from './client/index.js';
export * from './classes.daemon.js';
export * from './classes.ipcclient.js'; // Protocol types - shared between client and daemon
export * from './classes.servicemanager.js'; export * from './shared/protocol/ipc.types.js';
export * from './ipc.types.js';
// Daemon exports - for direct daemon control
export { startDaemon } from './daemon/index.js';
import * as cli from './cli.js'; import * as cli from './cli.js';

View File

@@ -0,0 +1,26 @@
/**
* Standardized error codes for IPC communication
* These are used instead of string messages for better error handling
*/
export enum ErrorCode {
// General errors
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
INVALID_REQUEST = 'INVALID_REQUEST',
// Process errors
PROCESS_NOT_FOUND = 'PROCESS_NOT_FOUND',
PROCESS_ALREADY_EXISTS = 'PROCESS_ALREADY_EXISTS',
PROCESS_START_FAILED = 'PROCESS_START_FAILED',
PROCESS_STOP_FAILED = 'PROCESS_STOP_FAILED',
// Daemon errors
DAEMON_NOT_RUNNING = 'DAEMON_NOT_RUNNING',
DAEMON_ALREADY_RUNNING = 'DAEMON_ALREADY_RUNNING',
// Memory errors
MEMORY_LIMIT_EXCEEDED = 'MEMORY_LIMIT_EXCEEDED',
// Config errors
CONFIG_INVALID = 'CONFIG_INVALID',
CONFIG_SAVE_FAILED = 'CONFIG_SAVE_FAILED',
}

View File

@@ -1,5 +1,39 @@
import type { IProcessConfig, IProcessInfo } from './classes.tspm.js'; // Process-related interfaces (used in IPC communication)
import type { IProcessLog } from './classes.processwrapper.js'; export interface IMonitorConfig {
name?: string; // Optional name to identify the instance
projectDir: string; // Directory where the command will run
command: string; // Full command to run (e.g., "npm run xyz")
args?: string[]; // Optional: arguments for the command
memoryLimitBytes: number; // Maximum allowed memory (in bytes) for the process group
monitorIntervalMs?: number; // Interval (in ms) at which memory is checked (default: 5000)
env?: NodeJS.ProcessEnv; // Optional: custom environment variables
logBufferSize?: number; // Optional: number of log lines to keep (default: 100)
}
export interface IProcessConfig extends IMonitorConfig {
id: string; // Unique identifier for the process
autorestart: boolean; // Whether to restart the process automatically on crash
watch?: boolean; // Whether to watch for file changes and restart
watchPaths?: string[]; // Paths to watch for changes
}
export interface IProcessInfo {
id: string;
pid?: number;
status: 'online' | 'stopped' | 'errored';
memory: number;
cpu?: number;
uptime?: number;
restarts: number;
}
export interface IProcessLog {
timestamp: Date;
type: 'stdout' | 'stderr' | 'system';
message: string;
seq: number;
runId: string;
}
// Base message types // Base message types
export interface IpcRequest<T = any> { export interface IpcRequest<T = any> {

View File

@@ -0,0 +1,5 @@
/**
* Protocol version for client-daemon communication
* This allows for version compatibility checks between client and daemon
*/
export const PROTOCOL_VERSION = '1.0.0';