@spssrl/sps-ps-agent 1.0.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.
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+
3
+ const config = require('../src/config');
4
+ const { spawn } = require('child_process');
5
+ const path = require('path');
6
+
7
+ const command = process.argv[2];
8
+
9
+ switch (command) {
10
+ case 'init':
11
+ handleInit();
12
+ break;
13
+ case 'start':
14
+ handleStart();
15
+ break;
16
+ case 'stop':
17
+ handleStop();
18
+ break;
19
+ case 'status':
20
+ handleStatus();
21
+ break;
22
+ case 'token':
23
+ handleToken();
24
+ break;
25
+ default:
26
+ showHelp();
27
+ }
28
+
29
+ function handleInit() {
30
+ const force = process.argv.includes('--force');
31
+ const result = config.init({ force });
32
+
33
+ if (result.created) {
34
+ console.log('\n SPS-PS Agent initialized!');
35
+ console.log(` Config: ${config.CONFIG_FILE}`);
36
+ console.log(` Port: ${result.config.port}`);
37
+ console.log(` Token: ${result.config.token}`);
38
+ console.log(` Server: ${result.config.serverName}`);
39
+ console.log('\n Copy the token above into your CRM server configuration.');
40
+ console.log(' Start the agent with: sps-ps-agent start\n');
41
+ } else {
42
+ console.log('\n Config already exists. Use --force to overwrite.');
43
+ console.log(` Config: ${config.CONFIG_FILE}`);
44
+ console.log(` Token: ${result.config.token}\n`);
45
+ }
46
+ }
47
+
48
+ function handleStart() {
49
+ const cfg = config.load();
50
+ if (!cfg) {
51
+ console.error('No configuration found. Run: sps-ps-agent init');
52
+ process.exit(1);
53
+ }
54
+
55
+ const existingPid = config.readPid();
56
+ if (existingPid && config.isRunning(existingPid)) {
57
+ console.log(`Agent already running (PID: ${existingPid})`);
58
+ process.exit(0);
59
+ }
60
+
61
+ if (process.argv.includes('--daemon') || process.argv.includes('-d')) {
62
+ // Daemonize
63
+ const serverPath = path.join(__dirname, '..', 'src', 'server.js');
64
+ const child = spawn(process.execPath, [serverPath], {
65
+ detached: true,
66
+ stdio: 'ignore',
67
+ env: { ...process.env }
68
+ });
69
+ child.unref();
70
+ config.savePid(child.pid);
71
+ console.log(`\n SPS-PS Agent started in background (PID: ${child.pid})`);
72
+ console.log(` Port: ${cfg.port}`);
73
+ console.log(` Stop with: sps-ps-agent stop\n`);
74
+ } else {
75
+ // Foreground
76
+ require('../src/server').start();
77
+ }
78
+ }
79
+
80
+ function handleStop() {
81
+ const pid = config.readPid();
82
+ if (!pid) {
83
+ console.log('Agent is not running (no PID file).');
84
+ return;
85
+ }
86
+
87
+ if (!config.isRunning(pid)) {
88
+ console.log(`Agent is not running (stale PID: ${pid}). Cleaning up.`);
89
+ config.removePid();
90
+ return;
91
+ }
92
+
93
+ try {
94
+ process.kill(pid, 'SIGTERM');
95
+ console.log(`Agent stopped (PID: ${pid}).`);
96
+ config.removePid();
97
+ } catch (err) {
98
+ console.error(`Failed to stop agent: ${err.message}`);
99
+ }
100
+ }
101
+
102
+ function handleStatus() {
103
+ const cfg = config.load();
104
+ if (!cfg) {
105
+ console.log('\n Not initialized. Run: sps-ps-agent init\n');
106
+ return;
107
+ }
108
+
109
+ const pid = config.readPid();
110
+ const running = pid && config.isRunning(pid);
111
+
112
+ console.log('\n SPS-PS Agent Status');
113
+ console.log(` Server: ${cfg.serverName}`);
114
+ console.log(` Port: ${cfg.port}`);
115
+ console.log(` Status: ${running ? `Running (PID: ${pid})` : 'Stopped'}`);
116
+ console.log(` Config: ${config.CONFIG_FILE}\n`);
117
+ }
118
+
119
+ function handleToken() {
120
+ const cfg = config.load();
121
+ if (!cfg) {
122
+ console.log('Not initialized. Run: sps-ps-agent init');
123
+ return;
124
+ }
125
+ console.log(cfg.token);
126
+ }
127
+
128
+ function showHelp() {
129
+ const version = require('../package.json').version;
130
+ console.log(`
131
+ SPS-PS Agent v${version}
132
+
133
+ Usage: sps-ps-agent <command>
134
+
135
+ Commands:
136
+ init Initialize configuration (generates token)
137
+ start Start the agent (add -d for daemon mode)
138
+ stop Stop the agent
139
+ status Show agent status
140
+ token Display the authentication token
141
+
142
+ Options:
143
+ init --force Overwrite existing configuration
144
+ start -d Run as background daemon
145
+ `);
146
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@spssrl/sps-ps-agent",
3
+ "version": "1.0.0",
4
+ "description": "SPS Process Services - Lightweight PM2 monitoring agent",
5
+ "main": "src/server.js",
6
+ "bin": {
7
+ "sps-ps-agent": "./bin/sps-ps-agent.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/server.js",
11
+ "dev": "node src/server.js"
12
+ },
13
+ "keywords": ["pm2", "process", "monitoring", "agent", "sps"],
14
+ "author": "SPS Srl",
15
+ "license": "MIT",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "src/",
22
+ "templates/",
23
+ "package.json"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "dependencies": {
29
+ "cors": "^2.8.5",
30
+ "express": "^4.21.0",
31
+ "pm2": "^5.4.0",
32
+ "systeminformation": "^5.23.0",
33
+ "uuid": "^10.0.0"
34
+ }
35
+ }
package/src/config.js ADDED
@@ -0,0 +1,89 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { v4: uuidv4 } = require('uuid');
5
+
6
+ const CONFIG_DIR = path.join(os.homedir(), '.sps-ps-agent');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
8
+ const PID_FILE = path.join(CONFIG_DIR, 'agent.pid');
9
+
10
+ const DEFAULT_CONFIG = {
11
+ port: 9876,
12
+ token: null,
13
+ serverName: os.hostname(),
14
+ bindAddress: '0.0.0.0',
15
+ corsOrigins: ['*']
16
+ };
17
+
18
+ function ensureConfigDir() {
19
+ if (!fs.existsSync(CONFIG_DIR)) {
20
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
21
+ }
22
+ }
23
+
24
+ function load() {
25
+ ensureConfigDir();
26
+ if (!fs.existsSync(CONFIG_FILE)) {
27
+ return null;
28
+ }
29
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
30
+ return JSON.parse(raw);
31
+ }
32
+
33
+ function save(config) {
34
+ ensureConfigDir();
35
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
36
+ }
37
+
38
+ function init(options = {}) {
39
+ const existing = load();
40
+ if (existing && !options.force) {
41
+ return { created: false, config: existing };
42
+ }
43
+ const config = {
44
+ ...DEFAULT_CONFIG,
45
+ token: uuidv4(),
46
+ ...options
47
+ };
48
+ save(config);
49
+ return { created: true, config };
50
+ }
51
+
52
+ function savePid(pid) {
53
+ ensureConfigDir();
54
+ fs.writeFileSync(PID_FILE, String(pid), 'utf-8');
55
+ }
56
+
57
+ function readPid() {
58
+ if (!fs.existsSync(PID_FILE)) return null;
59
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
60
+ return isNaN(pid) ? null : pid;
61
+ }
62
+
63
+ function removePid() {
64
+ if (fs.existsSync(PID_FILE)) {
65
+ fs.unlinkSync(PID_FILE);
66
+ }
67
+ }
68
+
69
+ function isRunning(pid) {
70
+ try {
71
+ process.kill(pid, 0);
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ module.exports = {
79
+ CONFIG_DIR,
80
+ CONFIG_FILE,
81
+ PID_FILE,
82
+ load,
83
+ save,
84
+ init,
85
+ savePid,
86
+ readPid,
87
+ removePid,
88
+ isRunning
89
+ };
@@ -0,0 +1,19 @@
1
+ const config = require('../config');
2
+
3
+ function authMiddleware(req, res, next) {
4
+ const authHeader = req.headers.authorization;
5
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
6
+ return res.status(401).json({ error: 'Missing or invalid authorization header' });
7
+ }
8
+
9
+ const token = authHeader.slice(7);
10
+ const cfg = config.load();
11
+
12
+ if (!cfg || token !== cfg.token) {
13
+ return res.status(403).json({ error: 'Invalid token' });
14
+ }
15
+
16
+ next();
17
+ }
18
+
19
+ module.exports = authMiddleware;
@@ -0,0 +1,44 @@
1
+ const { Router } = require('express');
2
+ const fs = require('fs');
3
+ const pm2Service = require('../services/pm2');
4
+
5
+ const router = Router();
6
+
7
+ // GET /api/processes/:name/logs?lines=100&type=out|err|all
8
+ router.get('/:name/logs', async (req, res) => {
9
+ const lines = Math.min(parseInt(req.query.lines) || 100, 5000);
10
+ const type = req.query.type || 'all'; // out, err, all
11
+
12
+ try {
13
+ const desc = await pm2Service.withConnection(() => pm2Service.describe(req.params.name));
14
+ const env = desc.pm2_env || {};
15
+ const outPath = env.pm_out_log_path;
16
+ const errPath = env.pm_err_log_path;
17
+
18
+ const result = {};
19
+
20
+ if ((type === 'out' || type === 'all') && outPath) {
21
+ result.out = tailFile(outPath, lines);
22
+ }
23
+ if ((type === 'err' || type === 'all') && errPath) {
24
+ result.err = tailFile(errPath, lines);
25
+ }
26
+
27
+ res.json({ success: true, process: req.params.name, logs: result });
28
+ } catch (err) {
29
+ res.status(500).json({ error: err.message });
30
+ }
31
+ });
32
+
33
+ function tailFile(filePath, numLines) {
34
+ try {
35
+ if (!fs.existsSync(filePath)) return '';
36
+ const content = fs.readFileSync(filePath, 'utf-8');
37
+ const allLines = content.split('\n');
38
+ return allLines.slice(-numLines).join('\n');
39
+ } catch {
40
+ return '';
41
+ }
42
+ }
43
+
44
+ module.exports = router;
@@ -0,0 +1,56 @@
1
+ const { Router } = require('express');
2
+ const pm2Service = require('../services/pm2');
3
+
4
+ const router = Router();
5
+
6
+ // GET /api/processes - List all PM2 processes
7
+ router.get('/', async (req, res) => {
8
+ try {
9
+ const processes = await pm2Service.withConnection(() => pm2Service.list());
10
+ res.json({ success: true, data: processes });
11
+ } catch (err) {
12
+ res.status(500).json({ error: err.message });
13
+ }
14
+ });
15
+
16
+ // POST /api/processes/:name/restart
17
+ router.post('/:name/restart', async (req, res) => {
18
+ try {
19
+ const result = await pm2Service.withConnection(() => pm2Service.restart(req.params.name));
20
+ res.json(result);
21
+ } catch (err) {
22
+ res.status(500).json({ error: err.message });
23
+ }
24
+ });
25
+
26
+ // POST /api/processes/:name/stop
27
+ router.post('/:name/stop', async (req, res) => {
28
+ try {
29
+ const result = await pm2Service.withConnection(() => pm2Service.stop(req.params.name));
30
+ res.json(result);
31
+ } catch (err) {
32
+ res.status(500).json({ error: err.message });
33
+ }
34
+ });
35
+
36
+ // POST /api/processes/:name/start
37
+ router.post('/:name/start', async (req, res) => {
38
+ try {
39
+ const result = await pm2Service.withConnection(() => pm2Service.start(req.params.name));
40
+ res.json(result);
41
+ } catch (err) {
42
+ res.status(500).json({ error: err.message });
43
+ }
44
+ });
45
+
46
+ // DELETE /api/processes/:name
47
+ router.delete('/:name', async (req, res) => {
48
+ try {
49
+ const result = await pm2Service.withConnection(() => pm2Service.deleteProcess(req.params.name));
50
+ res.json(result);
51
+ } catch (err) {
52
+ res.status(500).json({ error: err.message });
53
+ }
54
+ });
55
+
56
+ module.exports = router;
@@ -0,0 +1,66 @@
1
+ const { Router } = require('express');
2
+ const si = require('systeminformation');
3
+ const os = require('os');
4
+
5
+ const router = Router();
6
+
7
+ // GET /api/status - Quick health check
8
+ router.get('/status', async (req, res) => {
9
+ res.json({
10
+ success: true,
11
+ agent: 'sps-ps-agent',
12
+ version: require('../../package.json').version,
13
+ hostname: os.hostname(),
14
+ platform: os.platform(),
15
+ uptime: os.uptime(),
16
+ timestamp: new Date().toISOString()
17
+ });
18
+ });
19
+
20
+ // GET /api/system - Detailed system info
21
+ router.get('/system', async (req, res) => {
22
+ try {
23
+ const [cpu, mem, disk, osInfo, time] = await Promise.all([
24
+ si.currentLoad(),
25
+ si.mem(),
26
+ si.fsSize(),
27
+ si.osInfo(),
28
+ si.time()
29
+ ]);
30
+
31
+ res.json({
32
+ success: true,
33
+ data: {
34
+ hostname: os.hostname(),
35
+ platform: osInfo.platform,
36
+ distro: osInfo.distro,
37
+ release: osInfo.release,
38
+ arch: osInfo.arch,
39
+ uptime: os.uptime(),
40
+ cpu: {
41
+ load: Math.round(cpu.currentLoad * 100) / 100,
42
+ cores: os.cpus().length
43
+ },
44
+ memory: {
45
+ total: mem.total,
46
+ used: mem.used,
47
+ free: mem.free,
48
+ usage_percent: Math.round((mem.used / mem.total) * 10000) / 100
49
+ },
50
+ disk: disk.map(d => ({
51
+ fs: d.fs,
52
+ size: d.size,
53
+ used: d.used,
54
+ available: d.available,
55
+ usage_percent: d.use,
56
+ mount: d.mount
57
+ })),
58
+ timestamp: new Date().toISOString()
59
+ }
60
+ });
61
+ } catch (err) {
62
+ res.status(500).json({ error: err.message });
63
+ }
64
+ });
65
+
66
+ module.exports = router;
package/src/server.js ADDED
@@ -0,0 +1,69 @@
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const config = require('./config');
4
+ const authMiddleware = require('./middleware/auth');
5
+ const processRoutes = require('./routes/processes');
6
+ const logRoutes = require('./routes/logs');
7
+ const systemRoutes = require('./routes/system');
8
+
9
+ function createServer() {
10
+ const cfg = config.load();
11
+ if (!cfg) {
12
+ console.error('No configuration found. Run: sps-ps-agent init');
13
+ process.exit(1);
14
+ }
15
+
16
+ const app = express();
17
+
18
+ app.use(cors({ origin: cfg.corsOrigins || '*' }));
19
+ app.use(express.json());
20
+
21
+ // Public health endpoint (no auth)
22
+ app.get('/api/ping', (req, res) => {
23
+ res.json({ pong: true, timestamp: new Date().toISOString() });
24
+ });
25
+
26
+ // All other routes require auth
27
+ app.use('/api', authMiddleware);
28
+
29
+ // Routes
30
+ app.use('/api', systemRoutes);
31
+ app.use('/api/processes', processRoutes);
32
+ app.use('/api/processes', logRoutes);
33
+
34
+ return { app, cfg };
35
+ }
36
+
37
+ function start() {
38
+ const { app, cfg } = createServer();
39
+ const port = cfg.port || 9876;
40
+ const bind = cfg.bindAddress || '0.0.0.0';
41
+
42
+ const server = app.listen(port, bind, () => {
43
+ console.log(`\n SPS-PS Agent v${require('../package.json').version}`);
44
+ console.log(` Server: ${cfg.serverName}`);
45
+ console.log(` Listening: http://${bind}:${port}`);
46
+ console.log(` Token: ${cfg.token.substring(0, 8)}...`);
47
+ console.log('');
48
+
49
+ config.savePid(process.pid);
50
+ });
51
+
52
+ const shutdown = () => {
53
+ console.log('\nShutting down...');
54
+ config.removePid();
55
+ server.close(() => process.exit(0));
56
+ };
57
+
58
+ process.on('SIGINT', shutdown);
59
+ process.on('SIGTERM', shutdown);
60
+
61
+ return server;
62
+ }
63
+
64
+ module.exports = { createServer, start };
65
+
66
+ // Direct execution
67
+ if (require.main === module) {
68
+ start();
69
+ }
@@ -0,0 +1,114 @@
1
+ const pm2 = require('pm2');
2
+
3
+ function connect() {
4
+ return new Promise((resolve, reject) => {
5
+ pm2.connect((err) => {
6
+ if (err) reject(err);
7
+ else resolve();
8
+ });
9
+ });
10
+ }
11
+
12
+ function disconnect() {
13
+ pm2.disconnect();
14
+ }
15
+
16
+ function list() {
17
+ return new Promise((resolve, reject) => {
18
+ pm2.list((err, list) => {
19
+ if (err) reject(err);
20
+ else resolve(list.map(formatProcess));
21
+ });
22
+ });
23
+ }
24
+
25
+ function describe(nameOrId) {
26
+ return new Promise((resolve, reject) => {
27
+ pm2.describe(nameOrId, (err, desc) => {
28
+ if (err) reject(err);
29
+ else if (!desc || desc.length === 0) reject(new Error(`Process "${nameOrId}" not found`));
30
+ else resolve(desc[0]);
31
+ });
32
+ });
33
+ }
34
+
35
+ function restart(nameOrId) {
36
+ return new Promise((resolve, reject) => {
37
+ pm2.restart(nameOrId, (err) => {
38
+ if (err) reject(err);
39
+ else resolve({ success: true, action: 'restart', process: nameOrId });
40
+ });
41
+ });
42
+ }
43
+
44
+ function stop(nameOrId) {
45
+ return new Promise((resolve, reject) => {
46
+ pm2.stop(nameOrId, (err) => {
47
+ if (err) reject(err);
48
+ else resolve({ success: true, action: 'stop', process: nameOrId });
49
+ });
50
+ });
51
+ }
52
+
53
+ function start(nameOrId) {
54
+ return new Promise((resolve, reject) => {
55
+ pm2.restart(nameOrId, (err) => {
56
+ if (err) reject(err);
57
+ else resolve({ success: true, action: 'start', process: nameOrId });
58
+ });
59
+ });
60
+ }
61
+
62
+ function deleteProcess(nameOrId) {
63
+ return new Promise((resolve, reject) => {
64
+ pm2.delete(nameOrId, (err) => {
65
+ if (err) reject(err);
66
+ else resolve({ success: true, action: 'delete', process: nameOrId });
67
+ });
68
+ });
69
+ }
70
+
71
+ function formatProcess(proc) {
72
+ const env = proc.pm2_env || {};
73
+ const monit = proc.monit || {};
74
+ const startedAt = env.pm_uptime || null;
75
+ const uptime = startedAt ? Date.now() - startedAt : 0;
76
+
77
+ return {
78
+ pm_id: env.pm_id ?? proc.pm_id,
79
+ name: env.name || proc.name,
80
+ status: env.status || 'unknown',
81
+ cpu: monit.cpu || 0,
82
+ memory: monit.memory || 0,
83
+ uptime,
84
+ restarts: env.restart_time || 0,
85
+ pid: proc.pid || 0,
86
+ exec_mode: env.exec_mode || 'fork',
87
+ node_version: env.node_version || process.version,
88
+ script: env.pm_exec_path || '',
89
+ cwd: env.pm_cwd || '',
90
+ created_at: startedAt ? new Date(startedAt).toISOString() : null,
91
+ log_path: env.pm_out_log_path || null,
92
+ error_log_path: env.pm_err_log_path || null
93
+ };
94
+ }
95
+
96
+ async function withConnection(fn) {
97
+ await connect();
98
+ try {
99
+ return await fn();
100
+ } finally {
101
+ disconnect();
102
+ }
103
+ }
104
+
105
+ module.exports = {
106
+ withConnection,
107
+ list,
108
+ describe,
109
+ restart,
110
+ stop,
111
+ start,
112
+ deleteProcess,
113
+ formatProcess
114
+ };
@@ -0,0 +1,14 @@
1
+ [Unit]
2
+ Description=SPS Process Services Agent
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ User=root
8
+ ExecStart=/usr/bin/env node /usr/lib/node_modules/sps-ps-agent/src/server.js
9
+ Restart=always
10
+ RestartSec=5
11
+ Environment=NODE_ENV=production
12
+
13
+ [Install]
14
+ WantedBy=multi-user.target