cloud-ide-cide 2.0.34 → 2.0.36

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.
@@ -4,16 +4,24 @@
4
4
  *
5
5
  * Reads config from: ../.cide/config.json (token, paths)
6
6
  * Writes state to: ../.cide/deployments.json
7
- * Stores releases in: ../.cide/releases/{appCode}/{version}/
8
- * Stores backups in: ../.cide/backups/{appCode}/{version}.zip
7
+ * Extracts code to: {server_root}/{appCode}/ (live app directory)
8
+ * Stores zips in: ../.cide/releases/{appCode}/{version}.zip (for rollback)
9
9
  * Logs to: ../.cide/logs/deploy.log
10
- * Symlinks: {server_root}/current/{appCode} → active release
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
11
16
  *
12
17
  * Endpoints (all except /health require Authorization: Bearer <token>):
13
18
  *
14
- * POST /upload (multipart/form-data) — upload & deploy a zip
19
+ * POST /upload (multipart/form-data) — upload, deploy & start app
15
20
  * GET /history?appCode=xxx — deployment history
16
- * 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
17
25
  * GET /health — uptime check (no auth)
18
26
  */
19
27
 
@@ -22,6 +30,7 @@ const multer = require('multer');
22
30
  const path = require('path');
23
31
  const fs = require('fs');
24
32
  const AdmZip = require('adm-zip');
33
+ const { execSync, spawn } = require('child_process');
25
34
 
26
35
  const app = express();
27
36
  app.use(express.json());
@@ -36,10 +45,9 @@ try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch { /* no
36
45
  const TOKEN = config.token || process.env.CIDE_UPLOAD_TOKEN || '';
37
46
  const PORT = process.env.CIDE_UPLOAD_PORT || process.env.PORT || 4500;
38
47
  const RELEASES = config.paths?.releases || path.join(CIDE_DIR, 'releases');
39
- const BACKUPS = config.paths?.backups || path.join(CIDE_DIR, 'backups');
40
48
  const LOGS = config.paths?.logs || path.join(CIDE_DIR, 'logs');
41
- const CURRENT_DIR = config.paths?.current || path.join(path.dirname(CIDE_DIR), 'current');
42
49
  const DEPLOYMENTS = path.join(CIDE_DIR, 'deployments.json');
50
+ const SERVER_ROOT = config.serverRoot || path.dirname(CIDE_DIR);
43
51
 
44
52
  const upload = multer({ dest: path.join(CIDE_DIR, 'tmp') });
45
53
 
@@ -59,35 +67,117 @@ function appendLog(message) {
59
67
  try { fs.appendFileSync(logFile, line); } catch { /* logs dir may not exist yet */ }
60
68
  }
61
69
 
62
- function switchCurrent(currentPath, releasePath) {
63
- fs.mkdirSync(path.dirname(currentPath), { recursive: true });
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) {
64
81
  try {
65
- const stat = fs.lstatSync(currentPath);
66
- if (stat.isSymbolicLink()) {
67
- fs.unlinkSync(currentPath);
68
- } else if (stat.isDirectory()) {
69
- fs.renameSync(currentPath, `${currentPath}_bak_${Date.now()}`);
70
- }
71
- } catch { /* doesn't exist yet */ }
82
+ const pid = parseInt(fs.readFileSync(getAppPidFile(appCode), 'utf8').trim(), 10);
83
+ return isNaN(pid) ? null : pid;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
72
88
 
89
+ function isProcessRunning(pid) {
73
90
  try {
74
- fs.symlinkSync(releasePath, currentPath, 'junction');
91
+ process.kill(pid, 0);
92
+ return true;
75
93
  } catch {
76
- copyRecursive(releasePath, currentPath);
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)' };
77
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 };
78
153
  }
79
154
 
80
- function copyRecursive(src, dst) {
81
- fs.mkdirSync(dst, { recursive: true });
82
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
83
- const srcPath = path.join(src, entry.name);
84
- const dstPath = path.join(dst, entry.name);
85
- if (entry.isDirectory()) {
86
- copyRecursive(srcPath, dstPath);
87
- } else {
88
- fs.copyFileSync(srcPath, dstPath);
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}`);
89
169
  }
90
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 };
91
181
  }
92
182
 
93
183
  // ── Auth middleware ───────────────────────────────────────────────────────────
@@ -122,31 +212,35 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
122
212
  }
123
213
 
124
214
  const version = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
125
- const releasePath = path.join(RELEASES, appCode, version);
126
- const currentPath = path.join(CURRENT_DIR, appCode);
127
215
 
128
- // ── Save backup zip ──────────────────────────────────────────────────
129
- const backupDir = path.join(BACKUPS, appCode);
130
- fs.mkdirSync(backupDir, { recursive: true });
131
- fs.copyFileSync(req.file.path, path.join(backupDir, `${version}.zip`));
216
+ // ── Live app directory at server root ────────────────────────────────
217
+ const appDir = path.join(SERVER_ROOT, appCode);
218
+
219
+ // ── Save zip to releases for rollback ────────────────────────────────
220
+ const releaseDir = path.join(RELEASES, appCode);
221
+ fs.mkdirSync(releaseDir, { recursive: true });
222
+ const releaseZipPath = path.join(releaseDir, `${version}.zip`);
223
+ fs.copyFileSync(req.file.path, releaseZipPath);
132
224
 
133
- // ── Extract ──────────────────────────────────────────────────────────
225
+ // ── Extract directly to {server_root}/{appCode}/ ─────────────────────
226
+ fs.mkdirSync(appDir, { recursive: true });
134
227
  const zip = new AdmZip(req.file.path);
135
- fs.mkdirSync(releasePath, { recursive: true });
136
- zip.extractAllTo(releasePath, true);
228
+ zip.extractAllTo(appDir, true);
137
229
  fs.unlinkSync(req.file.path);
138
230
 
231
+ // ── Install dependencies & start the app ─────────────────────────────
232
+ const appResult = installAndStart(appCode);
233
+
139
234
  // ── Track state ──────────────────────────────────────────────────────
140
235
  const deployments = loadDeployments();
141
236
  const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
142
237
 
143
- switchCurrent(currentPath, releasePath);
144
-
145
- // ── Record ───────────────────────────────────────────────────────────
146
238
  const record = {
147
239
  version,
148
240
  environment,
149
- releasePath,
241
+ appDir,
242
+ releaseZip: releaseZipPath,
243
+ appPid: appResult.pid || null,
150
244
  deployedAt: new Date().toISOString(),
151
245
  action: 'upload',
152
246
  message: deployMsg,
@@ -164,12 +258,14 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
164
258
 
165
259
  res.json({
166
260
  status: 'SUCCESS',
167
- message: 'API code uploaded and deployed.',
261
+ message: 'Uploaded, installed & started.',
168
262
  appCode,
169
263
  environment,
170
264
  releaseVersion: version,
171
265
  previousVersion,
172
- releasePath,
266
+ appDir,
267
+ releaseZip: releaseZipPath,
268
+ appProcess: appResult,
173
269
  completedAt: new Date().toISOString(),
174
270
  });
175
271
  } catch (err) {
@@ -211,21 +307,29 @@ app.post('/rollback', authGuard, (req, res) => {
211
307
  return res.status(422).json({ status: 'FAILED', message: 'Missing required fields: appCode, version' });
212
308
  }
213
309
 
214
- const releasePath = path.join(RELEASES, appCode, version);
215
- if (!fs.existsSync(releasePath)) {
216
- return res.status(404).json({ status: 'FAILED', message: `Release not found: ${version}` });
310
+ const releaseZipPath = path.join(RELEASES, appCode, `${version}.zip`);
311
+ if (!fs.existsSync(releaseZipPath)) {
312
+ return res.status(404).json({ status: 'FAILED', message: `Release zip not found: ${version}` });
217
313
  }
218
314
 
219
- const currentPath = path.join(CURRENT_DIR, appCode);
315
+ const appDir = path.join(SERVER_ROOT, appCode);
220
316
  const deployments = loadDeployments();
221
317
  const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
222
318
 
223
- switchCurrent(currentPath, releasePath);
319
+ // Re-extract the zip to the app directory
320
+ fs.mkdirSync(appDir, { recursive: true });
321
+ const zip = new AdmZip(releaseZipPath);
322
+ zip.extractAllTo(appDir, true);
323
+
324
+ // Install dependencies & restart the app
325
+ const appResult = installAndStart(appCode);
224
326
 
225
327
  const record = {
226
328
  version,
227
329
  environment: deployments.apps?.[appCode]?.current?.environment || 'production',
228
- releasePath,
330
+ appDir,
331
+ releaseZip: releaseZipPath,
332
+ appPid: appResult.pid || null,
229
333
  deployedAt: new Date().toISOString(),
230
334
  action: 'rollback',
231
335
  previousVersion,
@@ -242,10 +346,11 @@ app.post('/rollback', authGuard, (req, res) => {
242
346
 
243
347
  res.json({
244
348
  status: 'SUCCESS',
245
- message: `Rolled back to version ${version}.`,
349
+ message: `Rolled back to ${version} & restarted.`,
246
350
  appCode,
247
351
  rolledBackTo: version,
248
352
  previousVersion,
353
+ appProcess: appResult,
249
354
  completedAt: new Date().toISOString(),
250
355
  });
251
356
  } catch (err) {
@@ -253,6 +358,49 @@ app.post('/rollback', authGuard, (req, res) => {
253
358
  }
254
359
  });
255
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
+
256
404
  // ── Health check ──────────────────────────────────────────────────────────────
257
405
  app.get('/health', (_req, res) => {
258
406
  res.json({ status: 'ok', uptime: process.uptime() });
@@ -262,4 +410,5 @@ app.get('/health', (_req, res) => {
262
410
  app.listen(PORT, () => {
263
411
  console.log(`Cloud IDE upload-api listening on port ${PORT}`);
264
412
  console.log(`.cide dir: ${CIDE_DIR}`);
413
+ console.log(`Server root: ${SERVER_ROOT}`);
265
414
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloud-ide-cide",
3
- "version": "2.0.34",
3
+ "version": "2.0.36",
4
4
  "description": "Cloud IDE CLI — create, build, publish, upload and deploy Cloud IDE projects.",
5
5
  "main": "cli.js",
6
6
  "bin": {