ezpm2gui 1.6.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 (41) hide show
  1. package/README.md +330 -321
  2. package/bin/ezpm2gui.js +8 -8
  3. package/bin/ezpm2gui.ts +51 -51
  4. package/bin/generate-ecosystem.js +35 -35
  5. package/bin/generate-ecosystem.ts +56 -56
  6. package/dist/server/config/cron-jobs.json +1 -0
  7. package/dist/server/config/project-configs.json +235 -236
  8. package/dist/server/config/remote-connections.json +3 -0
  9. package/dist/server/index.js +42 -3
  10. package/dist/server/routes/deployApplication.js +47 -45
  11. package/dist/server/routes/logStreaming.js +31 -24
  12. package/dist/server/routes/modules.js +55 -0
  13. package/dist/server/routes/pageAuth.js +3 -3
  14. package/dist/server/routes/remoteConnections.js +13 -9
  15. package/dist/server/routes/remoteMetrics.d.ts +3 -0
  16. package/dist/server/routes/remoteMetrics.js +84 -0
  17. package/dist/server/services/ProjectSetupService.d.ts +1 -1
  18. package/dist/server/services/ProjectSetupService.js +25 -9
  19. package/dist/server/utils/metrics-history.d.ts +21 -0
  20. package/dist/server/utils/metrics-history.js +68 -0
  21. package/dist/server/utils/remote-metrics-db.d.ts +29 -0
  22. package/dist/server/utils/remote-metrics-db.js +134 -0
  23. package/dist/server/utils/remote-metrics-poller.d.ts +8 -0
  24. package/dist/server/utils/remote-metrics-poller.js +67 -0
  25. package/package.json +86 -73
  26. package/scripts/postinstall.js +36 -36
  27. package/src/client/build/asset-manifest.json +6 -6
  28. package/src/client/build/favicon.ico +2 -2
  29. package/src/client/build/index.html +1 -1
  30. package/src/client/build/logo192.svg +7 -7
  31. package/src/client/build/logo512.svg +7 -7
  32. package/src/client/build/manifest.json +24 -24
  33. package/src/client/build/static/css/main.9decb204.css +5 -0
  34. package/src/client/build/static/css/main.9decb204.css.map +1 -0
  35. package/src/client/build/static/js/main.28a4a583.js +3 -0
  36. package/src/client/build/static/js/main.28a4a583.js.map +1 -0
  37. package/src/client/build/static/css/main.775772ee.css +0 -5
  38. package/src/client/build/static/css/main.775772ee.css.map +0 -1
  39. package/src/client/build/static/js/main.cbcb09c9.js +0 -3
  40. package/src/client/build/static/js/main.cbcb09c9.js.map +0 -1
  41. /package/src/client/build/static/js/{main.cbcb09c9.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;
@@ -96,14 +96,14 @@ router.post('/verify', (req, res) => {
96
96
  const config = loadAuthConfig();
97
97
  if (!config) {
98
98
  // No password set — treat as unlocked
99
- return res.json({ success: true });
99
+ return res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
100
100
  }
101
101
  const hash = hashPassword(password, config.salt);
102
102
  const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
103
103
  if (!match) {
104
104
  return res.status(401).json({ success: false, error: 'Incorrect password' });
105
105
  }
106
- res.json({ success: true });
106
+ res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
107
107
  });
108
108
  // @group Endpoints : DELETE /api/auth/remove — remove the password (requires current password)
109
109
  router.delete('/remove', (req, res) => {
@@ -153,7 +153,7 @@ router.post('/pin/verify', (req, res) => {
153
153
  if (!match) {
154
154
  return res.status(401).json({ success: false, error: 'Incorrect PIN' });
155
155
  }
156
- res.json({ success: true });
156
+ res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
157
157
  });
158
158
  // @group Endpoints : DELETE /api/auth/pin/remove — remove PIN (requires current password)
159
159
  router.delete('/pin/remove', (req, res) => {
@@ -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;
@@ -78,8 +78,9 @@ class ProjectSetupService {
78
78
  this.log('Could not detect project type');
79
79
  return null;
80
80
  }
81
- async setupProject(projectPath, projectType) {
81
+ async setupProject(projectPath, projectType, onLog) {
82
82
  this.log(`Starting setup for ${projectType} project at: ${projectPath}`);
83
+ onLog === null || onLog === void 0 ? void 0 : onLog(`Starting ${projectType} project setup at: ${projectPath}`);
83
84
  const config = this.configs[projectType];
84
85
  if (!config) {
85
86
  throw new Error(`Unknown project type: ${projectType}`);
@@ -96,6 +97,7 @@ class ProjectSetupService {
96
97
  try {
97
98
  // Check if step should be skipped
98
99
  if (this.shouldSkipStep(step, projectPath)) {
100
+ onLog === null || onLog === void 0 ? void 0 : onLog(`[SKIP] ${step.name}`);
99
101
  result.steps.push({
100
102
  name: step.name,
101
103
  success: true,
@@ -106,24 +108,31 @@ class ProjectSetupService {
106
108
  continue;
107
109
  }
108
110
  this.log(`Executing step: ${step.name}`);
109
- const stepResult = await this.executeStep(step, projectPath, result.environment);
111
+ onLog === null || onLog === void 0 ? void 0 : onLog(`\n[STEP] ${step.name}`);
112
+ const stepResult = await this.executeStep(step, projectPath, result.environment, onLog);
110
113
  const duration = Date.now() - stepStart;
111
114
  result.steps.push({
112
115
  ...stepResult,
113
116
  duration
114
117
  });
115
118
  if (!stepResult.success && step.required) {
119
+ onLog === null || onLog === void 0 ? void 0 : onLog(`[ERROR] Step failed: ${step.name}`);
116
120
  result.success = false;
117
121
  result.errors.push(`Required step failed: ${step.name} - ${stepResult.error}`);
118
122
  break;
119
123
  }
120
124
  else if (!stepResult.success) {
125
+ onLog === null || onLog === void 0 ? void 0 : onLog(`[WARN] Optional step failed: ${step.name}`);
121
126
  result.warnings.push(`Optional step failed: ${step.name} - ${stepResult.error}`);
122
127
  }
128
+ else {
129
+ onLog === null || onLog === void 0 ? void 0 : onLog(`[OK] ${step.name} completed (${duration}ms)`);
130
+ }
123
131
  }
124
132
  catch (error) {
125
133
  const duration = Date.now() - stepStart;
126
134
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
135
+ onLog === null || onLog === void 0 ? void 0 : onLog(`[ERROR] ${step.name}: ${errorMessage}`);
127
136
  result.steps.push({
128
137
  name: step.name,
129
138
  success: false,
@@ -152,13 +161,13 @@ class ProjectSetupService {
152
161
  result.environment.PYTHON_INTERPRETER = venvPath;
153
162
  }
154
163
  }
155
- // Validate the setup
164
+ // Validate the setup (advisory only — step success/failure is the real gate)
156
165
  const validationResult = await this.validateSetup(projectPath, config);
157
166
  if (!validationResult.success) {
158
- result.success = false;
159
- result.errors.push(...validationResult.errors);
167
+ result.warnings.push(...validationResult.errors);
160
168
  }
161
169
  this.log(`Setup completed. Success: ${result.success}`);
170
+ onLog === null || onLog === void 0 ? void 0 : onLog(`\nSetup ${result.success ? 'completed successfully' : 'failed'}.`);
162
171
  return result;
163
172
  }
164
173
  shouldSkipStep(step, projectPath) {
@@ -216,7 +225,7 @@ class ProjectSetupService {
216
225
  }
217
226
  return false;
218
227
  }
219
- async executeStep(step, projectPath, environment) {
228
+ async executeStep(step, projectPath, environment, onLog) {
220
229
  const workingDir = step.workingDirectory === 'project' ? projectPath : process.cwd();
221
230
  let command = step.command;
222
231
  // Handle virtual environment activation for Python
@@ -245,13 +254,20 @@ class ProjectSetupService {
245
254
  (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
246
255
  const text = data.toString();
247
256
  output += text;
248
- // Log real-time output for debugging
249
- console.log(`[${step.name}] ${text.trim()}`);
257
+ for (const line of text.split('\n')) {
258
+ const trimmed = line.trim();
259
+ if (trimmed)
260
+ onLog === null || onLog === void 0 ? void 0 : onLog(trimmed);
261
+ }
250
262
  });
251
263
  (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
252
264
  const text = data.toString();
253
265
  error += text;
254
- console.error(`[${step.name}] ERROR: ${text.trim()}`);
266
+ for (const line of text.split('\n')) {
267
+ const trimmed = line.trim();
268
+ if (trimmed)
269
+ onLog === null || onLog === void 0 ? void 0 : onLog(`[WARN] ${trimmed}`);
270
+ }
255
271
  });
256
272
  child.on('close', (code) => {
257
273
  const success = code === 0;
@@ -0,0 +1,21 @@
1
+ export interface MetricPoint {
2
+ timestamp: number;
3
+ cpu: number;
4
+ memory: number;
5
+ memoryMB: number;
6
+ memoryPercent: number;
7
+ }
8
+ export interface ProcessHistory {
9
+ pm_id: number;
10
+ name: string;
11
+ status: string;
12
+ history: MetricPoint[];
13
+ }
14
+ declare class MetricsHistoryStore {
15
+ private readonly store;
16
+ record(processList: any[]): void;
17
+ getOne(pm_id: number): ProcessHistory | null;
18
+ getAll(): ProcessHistory[];
19
+ }
20
+ export declare const metricsHistory: MetricsHistoryStore;
21
+ export {};