cloud-ide-cide 2.0.35 → 2.0.37

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.
@@ -8,11 +8,20 @@
8
8
  * Stores zips in: ../.cide/releases/{appCode}/{version}.zip (for rollback)
9
9
  * Logs to: ../.cide/logs/deploy.log
10
10
  *
11
+ * After upload/rollback the listener automatically:
12
+ * 1. Runs npm install in the app directory
13
+ * 2. Stops the previous app process (if any)
14
+ * 3. Starts the app with `node server.js`
15
+ * 4. Tracks the app PID for future stop/restart
16
+ *
11
17
  * Endpoints (all except /health require Authorization: Bearer <token>):
12
18
  *
13
- * POST /upload (multipart/form-data) — upload & deploy a zip
19
+ * POST /upload (multipart/form-data) — upload, deploy & start app
14
20
  * GET /history?appCode=xxx — deployment history
15
- * POST /rollback { appCode, version } — switch to previous release
21
+ * POST /rollback { appCode, version } — rollback & restart app
22
+ * POST /app/stop { appCode } — stop a running app
23
+ * POST /app/start { appCode } — start a stopped app
24
+ * GET /app/status?appCode=xxx — check app process status
16
25
  * GET /health — uptime check (no auth)
17
26
  */
18
27
 
@@ -21,6 +30,7 @@ const multer = require('multer');
21
30
  const path = require('path');
22
31
  const fs = require('fs');
23
32
  const AdmZip = require('adm-zip');
33
+ const { execSync, spawn } = require('child_process');
24
34
 
25
35
  const app = express();
26
36
  app.use(express.json());
@@ -37,6 +47,7 @@ const PORT = process.env.CIDE_UPLOAD_PORT || process.env.PORT || 4500;
37
47
  const RELEASES = config.paths?.releases || path.join(CIDE_DIR, 'releases');
38
48
  const LOGS = config.paths?.logs || path.join(CIDE_DIR, 'logs');
39
49
  const DEPLOYMENTS = path.join(CIDE_DIR, 'deployments.json');
50
+ const SERVER_ROOT = config.serverRoot || path.dirname(CIDE_DIR);
40
51
 
41
52
  const upload = multer({ dest: path.join(CIDE_DIR, 'tmp') });
42
53
 
@@ -56,6 +67,119 @@ function appendLog(message) {
56
67
  try { fs.appendFileSync(logFile, line); } catch { /* logs dir may not exist yet */ }
57
68
  }
58
69
 
70
+ // ── App process management ───────────────────────────────────────────────────
71
+
72
+ function getAppPidFile(appCode) {
73
+ return path.join(CIDE_DIR, 'logs', `${appCode}.pid`);
74
+ }
75
+
76
+ function getAppLogFile(appCode) {
77
+ return path.join(LOGS, `${appCode}.log`);
78
+ }
79
+
80
+ function readAppPid(appCode) {
81
+ try {
82
+ const pid = parseInt(fs.readFileSync(getAppPidFile(appCode), 'utf8').trim(), 10);
83
+ return isNaN(pid) ? null : pid;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function isProcessRunning(pid) {
90
+ try {
91
+ process.kill(pid, 0);
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function stopApp(appCode) {
99
+ const pid = readAppPid(appCode);
100
+ if (!pid) return { stopped: false, reason: 'no pid file' };
101
+
102
+ if (!isProcessRunning(pid)) {
103
+ try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
104
+ return { stopped: false, reason: 'not running (stale pid cleaned)' };
105
+ }
106
+
107
+ try {
108
+ process.kill(pid, 'SIGTERM');
109
+ // Give it a moment to die, then force kill if needed
110
+ setTimeout(() => {
111
+ if (isProcessRunning(pid)) {
112
+ try { process.kill(pid, 'SIGKILL'); } catch {}
113
+ }
114
+ }, 3000);
115
+ try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
116
+ appendLog(`STOP app=${appCode} pid=${pid}`);
117
+ return { stopped: true, pid };
118
+ } catch (e) {
119
+ return { stopped: false, reason: e.message };
120
+ }
121
+ }
122
+
123
+ function startApp(appCode) {
124
+ const appDir = path.join(SERVER_ROOT, appCode);
125
+ const entryFile = path.join(appDir, 'server.js');
126
+
127
+ if (!fs.existsSync(entryFile)) {
128
+ return { started: false, reason: `Entry file not found: ${entryFile}` };
129
+ }
130
+
131
+ // Stop existing process first
132
+ stopApp(appCode);
133
+
134
+ const logFile = getAppLogFile(appCode);
135
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
136
+ const out = fs.openSync(logFile, 'a');
137
+ const err = fs.openSync(logFile, 'a');
138
+
139
+ const child = spawn('node', [entryFile], {
140
+ cwd: appDir,
141
+ detached: true,
142
+ stdio: ['ignore', out, err],
143
+ env: { ...process.env, NODE_ENV: 'production' },
144
+ });
145
+
146
+ child.unref();
147
+
148
+ // Save PID
149
+ fs.writeFileSync(getAppPidFile(appCode), String(child.pid));
150
+ appendLog(`START app=${appCode} pid=${child.pid} entry=${entryFile}`);
151
+
152
+ return { started: true, pid: child.pid, logFile };
153
+ }
154
+
155
+ function installAndStart(appCode) {
156
+ const appDir = path.join(SERVER_ROOT, appCode);
157
+ const steps = [];
158
+
159
+ // npm install if package.json exists
160
+ const pkgPath = path.join(appDir, 'package.json');
161
+ if (fs.existsSync(pkgPath)) {
162
+ try {
163
+ execSync('npm install --production', { cwd: appDir, stdio: 'pipe', shell: true, timeout: 120000 });
164
+ steps.push('npm install: ok');
165
+ appendLog(`INSTALL app=${appCode} npm install ok`);
166
+ } catch (e) {
167
+ steps.push(`npm install: failed (${e.message})`);
168
+ appendLog(`INSTALL app=${appCode} npm install FAILED: ${e.message}`);
169
+ }
170
+ }
171
+
172
+ // Start the app
173
+ const result = startApp(appCode);
174
+ if (result.started) {
175
+ steps.push(`started: pid ${result.pid}`);
176
+ } else {
177
+ steps.push(`start failed: ${result.reason}`);
178
+ }
179
+
180
+ return { steps, ...result };
181
+ }
182
+
59
183
  // ── Auth middleware ───────────────────────────────────────────────────────────
60
184
  function authGuard(req, res, next) {
61
185
  if (!TOKEN) {
@@ -69,9 +193,6 @@ function authGuard(req, res, next) {
69
193
  next();
70
194
  }
71
195
 
72
- // ── Server root (parent of .cide/) ───────────────────────────────────────────
73
- const SERVER_ROOT = config.serverRoot || path.dirname(CIDE_DIR);
74
-
75
196
  // ═══════════════════════════════════════════════════════════════════════════════
76
197
  // ── Upload ────────────────────────────────────────────────────────────────────
77
198
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -107,16 +228,19 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
107
228
  zip.extractAllTo(appDir, true);
108
229
  fs.unlinkSync(req.file.path);
109
230
 
231
+ // ── Install dependencies & start the app ─────────────────────────────
232
+ const appResult = installAndStart(appCode);
233
+
110
234
  // ── Track state ──────────────────────────────────────────────────────
111
235
  const deployments = loadDeployments();
112
236
  const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
113
237
 
114
- // ── Record ───────────────────────────────────────────────────────────
115
238
  const record = {
116
239
  version,
117
240
  environment,
118
241
  appDir,
119
242
  releaseZip: releaseZipPath,
243
+ appPid: appResult.pid || null,
120
244
  deployedAt: new Date().toISOString(),
121
245
  action: 'upload',
122
246
  message: deployMsg,
@@ -134,13 +258,14 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
134
258
 
135
259
  res.json({
136
260
  status: 'SUCCESS',
137
- message: 'API code uploaded and deployed.',
261
+ message: 'Uploaded, installed & started.',
138
262
  appCode,
139
263
  environment,
140
264
  releaseVersion: version,
141
265
  previousVersion,
142
266
  appDir,
143
267
  releaseZip: releaseZipPath,
268
+ appProcess: appResult,
144
269
  completedAt: new Date().toISOString(),
145
270
  });
146
271
  } catch (err) {
@@ -196,11 +321,15 @@ app.post('/rollback', authGuard, (req, res) => {
196
321
  const zip = new AdmZip(releaseZipPath);
197
322
  zip.extractAllTo(appDir, true);
198
323
 
324
+ // Install dependencies & restart the app
325
+ const appResult = installAndStart(appCode);
326
+
199
327
  const record = {
200
328
  version,
201
329
  environment: deployments.apps?.[appCode]?.current?.environment || 'production',
202
330
  appDir,
203
331
  releaseZip: releaseZipPath,
332
+ appPid: appResult.pid || null,
204
333
  deployedAt: new Date().toISOString(),
205
334
  action: 'rollback',
206
335
  previousVersion,
@@ -217,10 +346,11 @@ app.post('/rollback', authGuard, (req, res) => {
217
346
 
218
347
  res.json({
219
348
  status: 'SUCCESS',
220
- message: `Rolled back to version ${version}.`,
349
+ message: `Rolled back to ${version} & restarted.`,
221
350
  appCode,
222
351
  rolledBackTo: version,
223
352
  previousVersion,
353
+ appProcess: appResult,
224
354
  completedAt: new Date().toISOString(),
225
355
  });
226
356
  } catch (err) {
@@ -228,6 +358,49 @@ app.post('/rollback', authGuard, (req, res) => {
228
358
  }
229
359
  });
230
360
 
361
+ // ═══════════════════════════════════════════════════════════════════════════════
362
+ // ── App management ────────────────────────────────────────────────────────────
363
+ // ═══════════════════════════════════════════════════════════════════════════════
364
+
365
+ app.post('/app/stop', authGuard, (req, res) => {
366
+ const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
367
+ if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
368
+
369
+ const result = stopApp(appCode);
370
+ res.json({ status: 'SUCCESS', appCode, ...result });
371
+ });
372
+
373
+ app.post('/app/start', authGuard, (req, res) => {
374
+ const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
375
+ if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
376
+
377
+ const result = startApp(appCode);
378
+ res.json({ status: result.started ? 'SUCCESS' : 'FAILED', appCode, ...result });
379
+ });
380
+
381
+ app.get('/app/status', authGuard, (req, res) => {
382
+ const appCode = (req.query.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
383
+ if (!appCode) return res.status(422).json({ status: 'FAILED', message: 'Missing appCode' });
384
+
385
+ const pid = readAppPid(appCode);
386
+ const running = pid ? isProcessRunning(pid) : false;
387
+ const appDir = path.join(SERVER_ROOT, appCode);
388
+ const logFile = getAppLogFile(appCode);
389
+
390
+ if (pid && !running) {
391
+ try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
392
+ }
393
+
394
+ res.json({
395
+ status: 'SUCCESS',
396
+ appCode,
397
+ running,
398
+ pid: running ? pid : null,
399
+ appDir,
400
+ logFile,
401
+ });
402
+ });
403
+
231
404
  // ── Health check ──────────────────────────────────────────────────────────────
232
405
  app.get('/health', (_req, res) => {
233
406
  res.json({ status: 'ok', uptime: process.uptime() });
@@ -237,4 +410,5 @@ app.get('/health', (_req, res) => {
237
410
  app.listen(PORT, () => {
238
411
  console.log(`Cloud IDE upload-api listening on port ${PORT}`);
239
412
  console.log(`.cide dir: ${CIDE_DIR}`);
413
+ console.log(`Server root: ${SERVER_ROOT}`);
240
414
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloud-ide-cide",
3
- "version": "2.0.35",
3
+ "version": "2.0.37",
4
4
  "description": "Cloud IDE CLI — create, build, publish, upload and deploy Cloud IDE projects.",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/serverInit.js CHANGED
@@ -256,6 +256,7 @@ function listenerStatus(cideDir) {
256
256
  }
257
257
 
258
258
  console.log('');
259
+ console.log(' ── Upload Listener ──────────────────────────');
259
260
  console.log(` Server type : ${config.serverType}`);
260
261
  console.log(` Server root : ${config.serverRoot}`);
261
262
  console.log(` Port : ${config.port || 4500}`);
@@ -275,6 +276,41 @@ function listenerStatus(cideDir) {
275
276
  }
276
277
  console.log(` Log : ${getServerLog(cideDir)}`);
277
278
  }
279
+
280
+ // ── Show deployed apps status ─────────────────────────────────────────
281
+ const deployments = safeReadJson(path.join(cideDir, 'deployments.json'));
282
+ if (deployments && deployments.apps && Object.keys(deployments.apps).length > 0) {
283
+ console.log('');
284
+ console.log(' ── Deployed Apps ────────────────────────────');
285
+
286
+ for (const [appCode, appData] of Object.entries(deployments.apps)) {
287
+ const pidFile = path.join(cideDir, 'logs', `${appCode}.pid`);
288
+ let appPid = null;
289
+ try { appPid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); } catch {}
290
+ if (isNaN(appPid)) appPid = null;
291
+
292
+ const appRunning = appPid ? isProcessRunning(appPid) : false;
293
+ const current = appData.current || {};
294
+ const appLogFile = path.join(cideDir, 'logs', `${appCode}.log`);
295
+
296
+ console.log('');
297
+ console.log(` App : ${appCode}`);
298
+ console.log(` Path : ${current.appDir || 'N/A'}`);
299
+ console.log(` Version : ${current.version || 'N/A'}`);
300
+ console.log(` Deployed : ${current.deployedAt || 'N/A'}`);
301
+ if (appRunning) {
302
+ console.log(` Status : Running (PID: ${appPid})`);
303
+ } else {
304
+ console.log(' Status : Stopped');
305
+ if (appPid) {
306
+ try { fs.unlinkSync(pidFile); } catch {}
307
+ }
308
+ }
309
+ console.log(` Log : ${appLogFile}`);
310
+ console.log(` Releases : ${appData.history ? appData.history.length : 0}`);
311
+ }
312
+ }
313
+
278
314
  console.log('');
279
315
  }
280
316
 
package/uploadProject.js CHANGED
@@ -629,6 +629,17 @@ async function uploadProject(opts = {}) {
629
629
  console.log(' Rollback successful!');
630
630
  console.log(` Now active : ${result.rolledBackTo}`);
631
631
  console.log(` Previous : ${result.previousVersion}`);
632
+ if (result.appProcess) {
633
+ if (result.appProcess.started) {
634
+ console.log(` App : Running (PID: ${result.appProcess.pid})`);
635
+ } else {
636
+ console.log(` App : Failed to start`);
637
+ if (result.appProcess.reason) console.log(` Reason : ${result.appProcess.reason}`);
638
+ }
639
+ if (result.appProcess.steps) {
640
+ result.appProcess.steps.forEach(s => console.log(` Step : ${s}`));
641
+ }
642
+ }
632
643
  console.log('');
633
644
  } else {
634
645
  console.error(` Rollback failed: ${result.message || JSON.stringify(result)}`);
@@ -704,7 +715,21 @@ async function uploadProject(opts = {}) {
704
715
  console.log(` App : ${result.appCode}`);
705
716
  console.log(` Server : ${label}`);
706
717
  if (result.previousVersion) console.log(` Previous : ${result.previousVersion}`);
707
- if (result.releasePath) console.log(` Path : ${result.releasePath}`);
718
+ if (result.appDir) console.log(` Path : ${result.appDir}`);
719
+ else if (result.releasePath) console.log(` Path : ${result.releasePath}`);
720
+ if (result.appProcess) {
721
+ console.log('');
722
+ if (result.appProcess.started) {
723
+ console.log(` App : Running (PID: ${result.appProcess.pid})`);
724
+ if (result.appProcess.logFile) console.log(` App Log : ${result.appProcess.logFile}`);
725
+ } else {
726
+ console.log(` App : Failed to start`);
727
+ if (result.appProcess.reason) console.log(` Reason : ${result.appProcess.reason}`);
728
+ }
729
+ if (result.appProcess.steps) {
730
+ result.appProcess.steps.forEach(s => console.log(` Step : ${s}`));
731
+ }
732
+ }
708
733
  console.log('');
709
734
  } else {
710
735
  console.error(` Upload failed: ${result.message || JSON.stringify(result)}`);