ezpm2gui 1.5.0 → 1.8.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.
Files changed (45) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +330 -295
  3. package/bin/ezpm2gui.js +10 -10
  4. package/bin/ezpm2gui.ts +51 -51
  5. package/bin/generate-ecosystem.js +36 -36
  6. package/bin/generate-ecosystem.ts +56 -56
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/server/config/cron-jobs.json +1 -0
  10. package/dist/server/config/project-configs.json +235 -236
  11. package/dist/server/config/remote-connections.json +3 -0
  12. package/dist/server/index.js +44 -3
  13. package/dist/server/routes/deployApplication.js +47 -45
  14. package/dist/server/routes/logStreaming.js +31 -24
  15. package/dist/server/routes/modules.js +55 -0
  16. package/dist/server/routes/pageAuth.d.ts +3 -0
  17. package/dist/server/routes/pageAuth.js +177 -0
  18. package/dist/server/routes/remoteConnections.js +13 -9
  19. package/dist/server/routes/remoteMetrics.d.ts +3 -0
  20. package/dist/server/routes/remoteMetrics.js +84 -0
  21. package/dist/server/services/ProjectSetupService.d.ts +1 -1
  22. package/dist/server/services/ProjectSetupService.js +25 -9
  23. package/dist/server/utils/metrics-history.d.ts +21 -0
  24. package/dist/server/utils/metrics-history.js +68 -0
  25. package/dist/server/utils/remote-metrics-db.d.ts +29 -0
  26. package/dist/server/utils/remote-metrics-db.js +134 -0
  27. package/dist/server/utils/remote-metrics-poller.d.ts +8 -0
  28. package/dist/server/utils/remote-metrics-poller.js +67 -0
  29. package/package.json +86 -73
  30. package/scripts/postinstall.js +36 -36
  31. package/src/client/build/asset-manifest.json +6 -6
  32. package/src/client/build/favicon.ico +2 -2
  33. package/src/client/build/index.html +1 -1
  34. package/src/client/build/logo192.svg +7 -7
  35. package/src/client/build/logo512.svg +7 -7
  36. package/src/client/build/manifest.json +24 -24
  37. package/src/client/build/static/css/main.9decb204.css +5 -0
  38. package/src/client/build/static/css/main.9decb204.css.map +1 -0
  39. package/src/client/build/static/js/main.28a4a583.js +3 -0
  40. package/src/client/build/static/js/main.28a4a583.js.map +1 -0
  41. package/src/client/build/static/css/main.2d095544.css +0 -5
  42. package/src/client/build/static/css/main.2d095544.css.map +0 -1
  43. package/src/client/build/static/js/main.17e17668.js +0 -3
  44. package/src/client/build/static/js/main.17e17668.js.map +0 -1
  45. /package/src/client/build/static/js/{main.17e17668.js.LICENSE.txt → main.28a4a583.js.LICENSE.txt} +0 -0
@@ -9,60 +9,69 @@ const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const ProjectSetupService_1 = require("../services/ProjectSetupService");
11
11
  const router = (0, express_1.Router)();
12
- // Deploy a new application
12
+ // Deploy a new application (SSE streaming)
13
13
  router.post('/', async (req, res) => {
14
14
  const { name, script, cwd, namespace, instances, exec_mode, autorestart, watch, max_memory_restart, env, appType, autoSetup = true } = req.body;
15
- // Validate required fields
15
+ // Validate required fields before starting SSE
16
16
  if (!name || !script) {
17
17
  return res.status(400).json({ error: 'Name and script path are required' });
18
18
  }
19
- // Validate script path exists
20
19
  if (!fs_1.default.existsSync(script)) {
21
20
  return res.status(400).json({ error: `Script file not found: ${script}` });
22
21
  }
22
+ // Switch to SSE
23
+ res.setHeader('Content-Type', 'text/event-stream');
24
+ res.setHeader('Cache-Control', 'no-cache');
25
+ res.setHeader('Connection', 'keep-alive');
26
+ res.flushHeaders();
27
+ const send = (type, data) => {
28
+ if (!res.writableEnded) {
29
+ res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
30
+ }
31
+ };
32
+ const log = (message) => send('log', { message });
33
+ const done = (success, extra = {}) => {
34
+ send('done', { success, ...extra });
35
+ res.end();
36
+ };
23
37
  try {
24
38
  const projectPath = cwd || path_1.default.dirname(script);
25
- let setupResult = null;
26
39
  let finalEnv = env || {};
27
40
  let interpreterPath = '';
28
- // Auto-detect project type if not provided
41
+ let setupResult = null;
42
+ // Auto-detect project type
29
43
  let detectedType = appType;
30
44
  if (!detectedType) {
31
45
  detectedType = ProjectSetupService_1.projectSetupService.detectProjectType(projectPath);
32
46
  if (detectedType) {
33
- console.log(`Auto-detected project type: ${detectedType}`);
47
+ log(`Auto-detected project type: ${detectedType}`);
34
48
  }
35
49
  }
36
- // Run project setup if auto-setup is enabled and project type is detected
50
+ // Run project setup
37
51
  if (autoSetup && detectedType && ['node', 'python', 'dotnet'].includes(detectedType)) {
38
- console.log(`Running setup for ${detectedType} project...`);
52
+ log(`Running ${detectedType} project setup...`);
39
53
  try {
40
- setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType);
54
+ setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType, log);
41
55
  if (!setupResult.success) {
42
- return res.status(500).json({
56
+ return done(false, {
43
57
  error: 'Project setup failed',
44
58
  details: setupResult.errors,
45
- warnings: setupResult.warnings,
46
- steps: setupResult.steps
59
+ warnings: setupResult.warnings
47
60
  });
48
61
  }
49
- // Merge environment variables from setup
50
62
  finalEnv = { ...setupResult.environment, ...finalEnv };
51
- // Set interpreter path for Python projects
52
63
  if (setupResult.interpreterPath) {
53
64
  interpreterPath = setupResult.interpreterPath;
54
65
  }
55
- console.log('Project setup completed successfully');
56
66
  }
57
67
  catch (setupError) {
58
- console.error('Setup error:', setupError);
59
- return res.status(500).json({
68
+ return done(false, {
60
69
  error: 'Project setup failed',
61
70
  details: setupError instanceof Error ? setupError.message : 'Unknown setup error'
62
71
  });
63
72
  }
64
73
  }
65
- // Create deployment configuration
74
+ // Build PM2 config
66
75
  const appConfig = {
67
76
  name,
68
77
  script,
@@ -75,45 +84,38 @@ router.post('/', async (req, res) => {
75
84
  max_memory_restart: max_memory_restart || '150M',
76
85
  env: finalEnv
77
86
  };
78
- // Set interpreter for Python projects
79
87
  if (detectedType === 'python' && interpreterPath) {
80
88
  appConfig.interpreter = interpreterPath;
81
89
  }
82
90
  else if (detectedType === 'dotnet') {
83
91
  appConfig.interpreter = 'dotnet';
84
- // For .NET projects, update script to point to the published DLL if available
85
92
  const publishedDll = path_1.default.join(projectPath, 'publish', `${path_1.default.basename(projectPath)}.dll`);
86
93
  if (fs_1.default.existsSync(publishedDll)) {
87
94
  appConfig.script = publishedDll;
88
95
  }
89
96
  }
90
- pm2_1.default.connect((err) => {
91
- if (err) {
92
- console.error(err);
93
- return res.status(500).json({ error: 'Failed to connect to PM2' });
94
- }
95
- pm2_1.default.start(appConfig, (err) => {
96
- pm2_1.default.disconnect();
97
- if (err) {
98
- console.error('PM2 start error:', err);
99
- return res.status(500).json({
100
- error: `Failed to deploy application: ${err.message || 'Unknown error'}`
101
- });
102
- }
103
- res.json({
104
- success: true,
105
- message: `Application ${name} deployed successfully`,
106
- setupResult: setupResult ? {
107
- steps: setupResult.steps,
108
- warnings: setupResult.warnings
109
- } : null
97
+ log(`Starting PM2 process: ${name}`);
98
+ await new Promise((resolve, reject) => {
99
+ pm2_1.default.connect((err) => {
100
+ if (err)
101
+ return reject(err);
102
+ pm2_1.default.start(appConfig, (startErr) => {
103
+ pm2_1.default.disconnect();
104
+ if (startErr)
105
+ return reject(startErr);
106
+ resolve();
110
107
  });
111
108
  });
112
109
  });
110
+ log(`Process "${name}" started successfully.`);
111
+ done(true, {
112
+ message: `Application ${name} deployed successfully`,
113
+ setupResult: setupResult ? { steps: setupResult.steps, warnings: setupResult.warnings } : null
114
+ });
113
115
  }
114
116
  catch (error) {
115
117
  console.error('Deployment error:', error);
116
- return res.status(500).json({
118
+ done(false, {
117
119
  error: 'Deployment failed',
118
120
  details: error instanceof Error ? error.message : 'Unknown error'
119
121
  });
@@ -159,8 +161,8 @@ router.post('/generate-ecosystem', (req, res) => {
159
161
  env: pm2Env.env || {}
160
162
  };
161
163
  });
162
- const ecosystemConfig = `module.exports = {
163
- apps: ${JSON.stringify(apps, null, 2)}
164
+ const ecosystemConfig = `module.exports = {
165
+ apps: ${JSON.stringify(apps, null, 2)}
164
166
  };`;
165
167
  // Create the file (either at specified path or default location)
166
168
  const filePath = req.body.path || path_1.default.join(process.cwd(), 'ecosystem.config.js');
@@ -213,8 +215,8 @@ router.get('/generate-ecosystem-preview', (req, res) => {
213
215
  env: pm2Env.env || {}
214
216
  };
215
217
  });
216
- const ecosystemConfig = `module.exports = {
217
- apps: ${JSON.stringify(apps, null, 2)}
218
+ const ecosystemConfig = `module.exports = {
219
+ apps: ${JSON.stringify(apps, null, 2)}
218
220
  };`;
219
221
  pm2_1.default.disconnect();
220
222
  res.json({
@@ -7,7 +7,6 @@ exports.setupLogStreaming = void 0;
7
7
  const express_1 = require("express");
8
8
  const pm2_1 = __importDefault(require("pm2"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
- const child_process_1 = require("child_process");
11
10
  const remote_connection_1 = require("../utils/remote-connection");
12
11
  const router = (0, express_1.Router)();
13
12
  // This variable will hold references to active log streams
@@ -18,7 +17,7 @@ const getLogStream = (io, processId, logType) => {
18
17
  const streamKey = `${processId}-${logType}`;
19
18
  // If stream already exists, return it
20
19
  if (activeStreams[streamKey]) {
21
- return activeStreams[streamKey];
20
+ return Promise.resolve(activeStreams[streamKey]);
22
21
  }
23
22
  return new Promise((resolve, reject) => {
24
23
  pm2_1.default.describe(processId, (err, processDesc) => {
@@ -32,30 +31,38 @@ const getLogStream = (io, processId, logType) => {
32
31
  reject(new Error(`Log file not found: ${logPath}`));
33
32
  return;
34
33
  }
35
- // Create a tail process to stream the log file
36
- const tail = (0, child_process_1.spawn)('tail', ['-f', logPath]);
37
- // Setup event handlers
38
- tail.stdout.on('data', (data) => {
39
- const lines = data.toString().split('\n').filter((line) => line.trim() !== '');
40
- lines.forEach((line) => {
41
- // Emit only to clients subscribed to this specific log stream
42
- io.to(streamKey).emit('log-line', {
43
- processId,
44
- logType,
45
- line
34
+ // Cross-platform tail using fs.watch no Unix `tail` binary needed
35
+ let position = fs_1.default.statSync(logPath).size; // start at EOF, don't replay history
36
+ const watcher = fs_1.default.watch(logPath, (eventType) => {
37
+ if (eventType !== 'change')
38
+ return;
39
+ try {
40
+ const stat = fs_1.default.statSync(logPath);
41
+ // Handle log rotation / truncation
42
+ if (stat.size < position)
43
+ position = 0;
44
+ if (stat.size === position)
45
+ return;
46
+ const length = stat.size - position;
47
+ const buffer = Buffer.alloc(length);
48
+ const fd = fs_1.default.openSync(logPath, 'r');
49
+ fs_1.default.readSync(fd, buffer, 0, length, position);
50
+ fs_1.default.closeSync(fd);
51
+ position = stat.size;
52
+ const lines = buffer.toString('utf8').split('\n').filter((l) => l.trim() !== '');
53
+ lines.forEach((line) => {
54
+ io.to(streamKey).emit('log-line', { processId, logType, line });
46
55
  });
47
- });
48
- });
49
- tail.stderr.on('data', (data) => {
50
- console.error(`Tail error: ${data}`);
51
- });
52
- tail.on('close', (code) => {
53
- console.log(`Tail process exited with code ${code}`);
54
- delete activeStreams[streamKey];
56
+ }
57
+ catch (e) {
58
+ console.error('Error reading log file:', e);
59
+ }
55
60
  });
56
- // Store the tail process
57
- activeStreams[streamKey] = tail;
58
- resolve(tail);
61
+ watcher.on('error', (e) => console.error(`Watcher error for ${streamKey}:`, e));
62
+ // Expose a kill() so existing cleanup code works unchanged
63
+ const streamObj = { kill: () => { watcher.close(); delete activeStreams[streamKey]; } };
64
+ activeStreams[streamKey] = streamObj;
65
+ resolve(streamObj);
59
66
  });
60
67
  });
61
68
  };
@@ -5,6 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = require("express");
7
7
  const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const os_1 = __importDefault(require("os"));
8
11
  const pm2_1 = __importDefault(require("pm2"));
9
12
  const pm2_connection_1 = require("../utils/pm2-connection");
10
13
  const router = (0, express_1.Router)();
@@ -123,4 +126,56 @@ router.delete('/:moduleName', async (req, res) => {
123
126
  });
124
127
  }
125
128
  });
129
+ // @group Utilities : Resolve ~/.pm2/module_conf.json path
130
+ const moduleConfPath = () => path_1.default.join(os_1.default.homedir(), '.pm2', 'module_conf.json');
131
+ // @group APIEndpoints : Get config for a specific installed module
132
+ router.get('/:moduleName/config', (req, res) => {
133
+ var _a;
134
+ const { moduleName } = req.params;
135
+ if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
136
+ return res.status(400).json({ error: 'Invalid module name' });
137
+ }
138
+ const confFile = moduleConfPath();
139
+ if (!fs_1.default.existsSync(confFile)) {
140
+ return res.json({ config: {} });
141
+ }
142
+ try {
143
+ const raw = JSON.parse(fs_1.default.readFileSync(confFile, 'utf8'));
144
+ // PM2 stores module config under the module name key
145
+ const config = (_a = raw[moduleName]) !== null && _a !== void 0 ? _a : {};
146
+ res.json({ config });
147
+ }
148
+ catch {
149
+ res.status(500).json({ error: 'Failed to read module configuration' });
150
+ }
151
+ });
152
+ // @group APIEndpoints : Set one or more config keys for an installed module
153
+ router.put('/:moduleName/config', async (req, res) => {
154
+ const { moduleName } = req.params;
155
+ const { config } = req.body;
156
+ if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
157
+ return res.status(400).json({ error: 'Invalid module name' });
158
+ }
159
+ if (!config || typeof config !== 'object') {
160
+ return res.status(400).json({ error: 'config object is required' });
161
+ }
162
+ // Validate all keys — no shell metacharacters
163
+ for (const key of Object.keys(config)) {
164
+ if (!/^[a-zA-Z0-9_\-.]+$/.test(key)) {
165
+ return res.status(400).json({ error: `Invalid config key: ${key}` });
166
+ }
167
+ }
168
+ try {
169
+ // Run pm2 set for each key sequentially
170
+ for (const [key, value] of Object.entries(config)) {
171
+ // Wrap value in quotes to handle spaces; strip any embedded quotes first
172
+ const safeValue = String(value).replace(/"/g, '');
173
+ await runPM2CLI(`set ${moduleName}:${key} "${safeValue}"`);
174
+ }
175
+ res.json({ success: true, message: `Configuration updated for ${moduleName}` });
176
+ }
177
+ catch (error) {
178
+ res.status(500).json({ error: 'Failed to update configuration', details: error.message });
179
+ }
180
+ });
126
181
  exports.default = router;
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ declare const router: Router;
3
+ export default router;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ // @group Configuration : Path to the stored auth config file
11
+ const AUTH_FILE = path_1.default.join(__dirname, '../config/auth.json');
12
+ // @group Utilities : Load auth config — returns null when no password is set
13
+ function loadAuthConfig() {
14
+ try {
15
+ if (!fs_1.default.existsSync(AUTH_FILE))
16
+ return null;
17
+ const raw = fs_1.default.readFileSync(AUTH_FILE, 'utf8').trim();
18
+ if (!raw)
19
+ return null;
20
+ return JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ // @group Utilities : Persist auth config to disk
27
+ function saveAuthConfig(config) {
28
+ const dir = path_1.default.dirname(AUTH_FILE);
29
+ if (!fs_1.default.existsSync(dir))
30
+ fs_1.default.mkdirSync(dir, { recursive: true });
31
+ fs_1.default.writeFileSync(AUTH_FILE, JSON.stringify(config), 'utf8');
32
+ }
33
+ // @group Utilities : Hash a plaintext password with PBKDF2 + salt
34
+ function hashPassword(password, salt) {
35
+ return crypto_1.default.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
36
+ }
37
+ // @group Router : Express router for password-protection endpoints
38
+ const router = express_1.default.Router();
39
+ // @group Endpoints : GET /api/auth/status — is a password/PIN configured?
40
+ router.get('/status', (_req, res) => {
41
+ var _a;
42
+ const config = loadAuthConfig();
43
+ res.json({
44
+ passwordSet: config !== null,
45
+ pinSet: !!(config === null || config === void 0 ? void 0 : config.pinHash),
46
+ autoLockMinutes: (_a = config === null || config === void 0 ? void 0 : config.autoLockMinutes) !== null && _a !== void 0 ? _a : 0,
47
+ });
48
+ });
49
+ // @group Endpoints : PATCH /api/auth/settings — update non-password settings (e.g. autoLockMinutes)
50
+ router.patch('/settings', (req, res) => {
51
+ var _a;
52
+ const { autoLockMinutes } = req.body;
53
+ const config = loadAuthConfig();
54
+ if (!config) {
55
+ return res.status(400).json({ success: false, error: 'No password set — configure a password first' });
56
+ }
57
+ const minutes = typeof autoLockMinutes === 'number' && autoLockMinutes >= 0 ? Math.floor(autoLockMinutes) : (_a = config.autoLockMinutes) !== null && _a !== void 0 ? _a : 0;
58
+ saveAuthConfig({ ...config, autoLockMinutes: minutes });
59
+ res.json({ success: true, autoLockMinutes: minutes });
60
+ });
61
+ // @group Endpoints : POST /api/auth/set — set or change the password
62
+ router.post('/set', (req, res) => {
63
+ var _a;
64
+ const { password, currentPassword } = req.body;
65
+ if (!password || typeof password !== 'string' || password.length < 4) {
66
+ return res.status(400).json({ success: false, error: 'Password must be at least 4 characters' });
67
+ }
68
+ const existing = loadAuthConfig();
69
+ // If a password is already set, require the current one before changing
70
+ if (existing) {
71
+ if (!currentPassword) {
72
+ return res.status(401).json({ success: false, error: 'Current password required to change password' });
73
+ }
74
+ const currentHash = hashPassword(currentPassword, existing.salt);
75
+ if (!crypto_1.default.timingSafeEqual(Buffer.from(currentHash, 'hex'), Buffer.from(existing.hash, 'hex'))) {
76
+ return res.status(401).json({ success: false, error: 'Current password is incorrect' });
77
+ }
78
+ }
79
+ const salt = crypto_1.default.randomBytes(32).toString('hex');
80
+ const hash = hashPassword(password, salt);
81
+ // Preserve PIN and autoLock settings when changing password
82
+ saveAuthConfig({
83
+ hash,
84
+ salt,
85
+ autoLockMinutes: (_a = existing === null || existing === void 0 ? void 0 : existing.autoLockMinutes) !== null && _a !== void 0 ? _a : 0,
86
+ ...((existing === null || existing === void 0 ? void 0 : existing.pinHash) ? { pinHash: existing.pinHash, pinSalt: existing.pinSalt } : {}),
87
+ });
88
+ res.json({ success: true });
89
+ });
90
+ // @group Endpoints : POST /api/auth/verify — verify a password attempt
91
+ router.post('/verify', (req, res) => {
92
+ const { password } = req.body;
93
+ if (!password || typeof password !== 'string') {
94
+ return res.status(400).json({ success: false, error: 'Password is required' });
95
+ }
96
+ const config = loadAuthConfig();
97
+ if (!config) {
98
+ // No password set — treat as unlocked
99
+ return res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
100
+ }
101
+ const hash = hashPassword(password, config.salt);
102
+ const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
103
+ if (!match) {
104
+ return res.status(401).json({ success: false, error: 'Incorrect password' });
105
+ }
106
+ res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
107
+ });
108
+ // @group Endpoints : DELETE /api/auth/remove — remove the password (requires current password)
109
+ router.delete('/remove', (req, res) => {
110
+ const { password } = req.body;
111
+ const config = loadAuthConfig();
112
+ if (!config) {
113
+ return res.json({ success: true }); // nothing to remove
114
+ }
115
+ if (!password || typeof password !== 'string') {
116
+ return res.status(400).json({ success: false, error: 'Current password required' });
117
+ }
118
+ const hash = hashPassword(password, config.salt);
119
+ const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
120
+ if (!match) {
121
+ return res.status(401).json({ success: false, error: 'Incorrect password' });
122
+ }
123
+ fs_1.default.unlinkSync(AUTH_FILE);
124
+ res.json({ success: true });
125
+ });
126
+ // @group Endpoints : POST /api/auth/pin/set — set or change the PIN (4-digit)
127
+ router.post('/pin/set', (req, res) => {
128
+ const { pin } = req.body;
129
+ if (!pin || !/^\d{4}$/.test(pin)) {
130
+ return res.status(400).json({ success: false, error: 'PIN must be exactly 4 digits' });
131
+ }
132
+ const config = loadAuthConfig();
133
+ if (!config) {
134
+ return res.status(400).json({ success: false, error: 'Set a password first before adding a PIN' });
135
+ }
136
+ const pinSalt = crypto_1.default.randomBytes(32).toString('hex');
137
+ const pinHash = hashPassword(pin, pinSalt);
138
+ saveAuthConfig({ ...config, pinHash, pinSalt });
139
+ res.json({ success: true });
140
+ });
141
+ // @group Endpoints : POST /api/auth/pin/verify — verify a PIN attempt
142
+ router.post('/pin/verify', (req, res) => {
143
+ const { pin } = req.body;
144
+ if (!pin || typeof pin !== 'string') {
145
+ return res.status(400).json({ success: false, error: 'PIN is required' });
146
+ }
147
+ const config = loadAuthConfig();
148
+ if (!(config === null || config === void 0 ? void 0 : config.pinHash) || !(config === null || config === void 0 ? void 0 : config.pinSalt)) {
149
+ return res.status(400).json({ success: false, error: 'No PIN configured' });
150
+ }
151
+ const hash = hashPassword(pin, config.pinSalt);
152
+ const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.pinHash, 'hex'));
153
+ if (!match) {
154
+ return res.status(401).json({ success: false, error: 'Incorrect PIN' });
155
+ }
156
+ res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
157
+ });
158
+ // @group Endpoints : DELETE /api/auth/pin/remove — remove PIN (requires current password)
159
+ router.delete('/pin/remove', (req, res) => {
160
+ const { password } = req.body;
161
+ const config = loadAuthConfig();
162
+ if (!config) {
163
+ return res.json({ success: true }); // nothing to remove
164
+ }
165
+ if (!password || typeof password !== 'string') {
166
+ return res.status(400).json({ success: false, error: 'Current password required to remove PIN' });
167
+ }
168
+ const hash = hashPassword(password, config.salt);
169
+ const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
170
+ if (!match) {
171
+ return res.status(401).json({ success: false, error: 'Incorrect password' });
172
+ }
173
+ const { pinHash: _ph, pinSalt: _ps, ...rest } = config;
174
+ saveAuthConfig(rest);
175
+ res.json({ success: true });
176
+ });
177
+ exports.default = router;
@@ -819,15 +819,19 @@ router.get('/:connectionId/log-file/download', async (req, res) => {
819
819
  router.get('/connections', async (req, res) => {
820
820
  try {
821
821
  const connections = remote_connection_1.remoteConnectionManager.getAllConnections();
822
- const connectionsList = Array.from(connections.entries()).map(([id, conn]) => ({
823
- id,
824
- name: conn.name || `${conn.username}@${conn.host}`,
825
- host: conn.host,
826
- port: conn.port,
827
- username: conn.username,
828
- connected: conn.isConnected(),
829
- isPM2Installed: conn.isPM2Installed
830
- }));
822
+ const connectionsList = Array.from(connections.entries()).map(([id, conn]) => {
823
+ const connected = conn.isConnected();
824
+ return {
825
+ id,
826
+ name: conn.name || `${conn.username}@${conn.host}`,
827
+ host: conn.host,
828
+ port: conn.port,
829
+ username: conn.username,
830
+ connected,
831
+ isPM2Installed: conn.isPM2Installed,
832
+ status: connected ? 'connected' : 'disconnected',
833
+ };
834
+ });
831
835
  res.json(connectionsList);
832
836
  }
833
837
  catch (error) {
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ declare const router: Router;
3
+ export default router;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ const remote_metrics_db_1 = require("../utils/remote-metrics-db");
8
+ const router = express_1.default.Router();
9
+ // @group Constants : Default and maximum query bounds
10
+ const DEFAULT_RANGE_MS = 3600000; // 1 hour
11
+ const MAX_RANGE_MS = 7 * 24 * 3600000; // 7 days
12
+ const MAX_POINTS = 500;
13
+ // @group Utilities : Parse and clamp a query-string integer
14
+ const parseIntParam = (raw, fallback, min, max) => {
15
+ const n = parseInt(raw, 10);
16
+ if (!Number.isFinite(n))
17
+ return fallback;
18
+ return Math.max(min, Math.min(max, n));
19
+ };
20
+ /**
21
+ * List all remote connections that have recorded metrics
22
+ * GET /api/remote-metrics/connections
23
+ */
24
+ router.get('/connections', (_req, res) => {
25
+ try {
26
+ const connections = remote_metrics_db_1.remoteMetricsDB.getConnectionsWithData();
27
+ res.json({ success: true, connections });
28
+ }
29
+ catch (err) {
30
+ res.status(500).json({ success: false, error: err.message });
31
+ }
32
+ });
33
+ /**
34
+ * List all process names recorded for a connection
35
+ * GET /api/remote-metrics/:connectionId/processes
36
+ */
37
+ router.get('/:connectionId/processes', (req, res) => {
38
+ try {
39
+ const { connectionId } = req.params;
40
+ const names = remote_metrics_db_1.remoteMetricsDB.getProcessNames(connectionId);
41
+ res.json({ success: true, processes: names });
42
+ }
43
+ catch (err) {
44
+ res.status(500).json({ success: false, error: err.message });
45
+ }
46
+ });
47
+ /**
48
+ * Latest snapshot (most recent metric point per process) for a connection
49
+ * GET /api/remote-metrics/:connectionId/snapshot
50
+ */
51
+ router.get('/:connectionId/snapshot', (req, res) => {
52
+ try {
53
+ const { connectionId } = req.params;
54
+ const snapshot = remote_metrics_db_1.remoteMetricsDB.getLatestSnapshot(connectionId);
55
+ res.json({ success: true, snapshot });
56
+ }
57
+ catch (err) {
58
+ res.status(500).json({ success: false, error: err.message });
59
+ }
60
+ });
61
+ /**
62
+ * Time-series metrics for one process on a connection
63
+ * GET /api/remote-metrics/:connectionId/:processName
64
+ * ?from=<unix-ms> (default: now - 1h)
65
+ * ?to=<unix-ms> (default: now)
66
+ * ?maxPoints=<n> (default: 500, max: 1000)
67
+ */
68
+ router.get('/:connectionId/:processName', (req, res) => {
69
+ try {
70
+ const { connectionId, processName } = req.params;
71
+ const now = Date.now();
72
+ const to = parseIntParam(req.query.to, now, 0, now + 60000);
73
+ const from = parseIntParam(req.query.from, to - DEFAULT_RANGE_MS, 0, now);
74
+ // Clamp range to max window
75
+ const clampedFrom = Math.max(from, to - MAX_RANGE_MS);
76
+ const maxPoints = parseIntParam(req.query.maxPoints, MAX_POINTS, 50, 1000);
77
+ const metrics = remote_metrics_db_1.remoteMetricsDB.getMetrics(connectionId, processName, clampedFrom, to, maxPoints);
78
+ res.json({ success: true, metrics });
79
+ }
80
+ catch (err) {
81
+ res.status(500).json({ success: false, error: err.message });
82
+ }
83
+ });
84
+ exports.default = router;
@@ -62,7 +62,7 @@ export declare class ProjectSetupService {
62
62
  private ensureLogDirectory;
63
63
  private log;
64
64
  detectProjectType(projectPath: string): string | null;
65
- setupProject(projectPath: string, projectType: string): Promise<SetupResult>;
65
+ setupProject(projectPath: string, projectType: string, onLog?: (msg: string) => void): Promise<SetupResult>;
66
66
  private shouldSkipStep;
67
67
  private executeStep;
68
68
  private validateSetup;