cloud-ide-cide 2.0.37 → 2.0.39

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.
package/cli.js CHANGED
@@ -361,6 +361,33 @@ program
361
361
  });
362
362
  });
363
363
 
364
+ /* app — remote app management (status/start/stop/restart) via upload listener */
365
+ program
366
+ .command('app')
367
+ .description('Manage deployed apps remotely: status, start, stop, restart')
368
+ .option('--status', 'Check if the deployed app is running')
369
+ .option('--start', 'Start the deployed app')
370
+ .option('--stop', 'Stop the deployed app')
371
+ .option('--restart', 'Restart the deployed app (stop + start)')
372
+ .option('-p, --packages <spec>', 'Project number from the list (e.g. 1)')
373
+ .option('-s, --server <spec>', 'Server number or name (e.g. 1 or "Production")')
374
+ .addHelpText('after', `
375
+ Examples:
376
+ cide app --status Check if app is running on the server
377
+ cide app --start Start the app
378
+ cide app --stop Stop the app
379
+ cide app --restart Restart the app
380
+ cide app --status -p 1 -s 1 Non-interactive: pick project & server by number
381
+ `)
382
+ .action((opts) => {
383
+ checkUserLoggedIn(() => {
384
+ require('./appManager')(opts).catch((err) => {
385
+ console.error(err);
386
+ process.exitCode = 1;
387
+ });
388
+ });
389
+ });
390
+
364
391
  program
365
392
  .command('shell')
366
393
  .description('Start the interactive CloudIDE workspace shell (git, build, publish, server, npm, …)')
@@ -121,63 +121,110 @@ function stopApp(appCode) {
121
121
  }
122
122
 
123
123
  function startApp(appCode) {
124
- const appDir = path.join(SERVER_ROOT, appCode);
125
- const entryFile = path.join(appDir, 'server.js');
124
+ return new Promise((resolve) => {
125
+ const appDir = path.join(SERVER_ROOT, appCode);
126
+ const entryFile = path.join(appDir, 'server.js');
126
127
 
127
- if (!fs.existsSync(entryFile)) {
128
- return { started: false, reason: `Entry file not found: ${entryFile}` };
129
- }
128
+ if (!fs.existsSync(entryFile)) {
129
+ return resolve({ started: false, reason: `Entry file not found: ${entryFile}` });
130
+ }
130
131
 
131
- // Stop existing process first
132
- stopApp(appCode);
132
+ // Stop existing process first
133
+ stopApp(appCode);
133
134
 
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
- });
135
+ const logFile = getAppLogFile(appCode);
136
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
137
+
138
+ // Truncate log so we capture only this run's output
139
+ fs.writeFileSync(logFile, '');
145
140
 
146
- child.unref();
141
+ const out = fs.openSync(logFile, 'a');
142
+ const err = fs.openSync(logFile, 'a');
147
143
 
148
- // Save PID
149
- fs.writeFileSync(getAppPidFile(appCode), String(child.pid));
150
- appendLog(`START app=${appCode} pid=${child.pid} entry=${entryFile}`);
144
+ const child = spawn('node', [entryFile], {
145
+ cwd: appDir,
146
+ detached: true,
147
+ stdio: ['ignore', out, err],
148
+ env: { ...process.env, NODE_ENV: 'production' },
149
+ });
150
+
151
+ child.unref();
152
+
153
+ // Save PID
154
+ fs.writeFileSync(getAppPidFile(appCode), String(child.pid));
155
+ appendLog(`START app=${appCode} pid=${child.pid} entry=${entryFile}`);
156
+
157
+ // Wait 3 seconds then check if process is still alive & read its output
158
+ setTimeout(() => {
159
+ const alive = isProcessRunning(child.pid);
160
+ let output = '';
161
+ try { output = fs.readFileSync(logFile, 'utf8').slice(-2000); } catch {}
162
+
163
+ if (!alive) {
164
+ try { fs.unlinkSync(getAppPidFile(appCode)); } catch {}
165
+ }
151
166
 
152
- return { started: true, pid: child.pid, logFile };
167
+ resolve({
168
+ started: alive,
169
+ pid: alive ? child.pid : null,
170
+ logFile,
171
+ output,
172
+ reason: alive ? null : 'Process exited within 3s (see output)',
173
+ });
174
+ }, 3000);
175
+ });
153
176
  }
154
177
 
155
- function installAndStart(appCode) {
178
+ function cleanDir(dir) {
179
+ if (!fs.existsSync(dir)) return;
180
+ for (const entry of fs.readdirSync(dir)) {
181
+ // preserve node_modules to speed up npm install
182
+ if (entry === 'node_modules') continue;
183
+ const fullPath = path.join(dir, entry);
184
+ fs.rmSync(fullPath, { recursive: true, force: true });
185
+ }
186
+ }
187
+
188
+ async function installAndStart(appCode) {
156
189
  const appDir = path.join(SERVER_ROOT, appCode);
157
190
  const steps = [];
191
+ let npmOutput = '';
158
192
 
159
193
  // npm install if package.json exists
160
194
  const pkgPath = path.join(appDir, 'package.json');
161
195
  if (fs.existsSync(pkgPath)) {
162
196
  try {
163
- execSync('npm install --production', { cwd: appDir, stdio: 'pipe', shell: true, timeout: 120000 });
197
+ const out = execSync('npm install --production 2>&1', { cwd: appDir, encoding: 'utf8', shell: true, timeout: 120000 });
198
+ npmOutput = (out || '').slice(-1000);
164
199
  steps.push('npm install: ok');
165
200
  appendLog(`INSTALL app=${appCode} npm install ok`);
166
201
  } catch (e) {
167
- steps.push(`npm install: failed (${e.message})`);
202
+ npmOutput = (e.stdout || '').slice(-500) + '\n' + (e.stderr || '').slice(-500);
203
+ steps.push(`npm install: failed`);
168
204
  appendLog(`INSTALL app=${appCode} npm install FAILED: ${e.message}`);
169
205
  }
170
206
  }
171
207
 
172
208
  // Start the app
173
- const result = startApp(appCode);
209
+ const result = await startApp(appCode);
174
210
  if (result.started) {
175
211
  steps.push(`started: pid ${result.pid}`);
176
212
  } else {
177
213
  steps.push(`start failed: ${result.reason}`);
178
214
  }
179
215
 
180
- return { steps, ...result };
216
+ return { steps, npmOutput, ...result };
217
+ }
218
+
219
+ function readAppPort(appCode) {
220
+ const envPath = path.join(SERVER_ROOT, appCode, '.env');
221
+ try {
222
+ const content = fs.readFileSync(envPath, 'utf8');
223
+ const match = content.match(/^PORT\s*=\s*(\d+)/m);
224
+ return match ? match[1] : null;
225
+ } catch {
226
+ return null;
227
+ }
181
228
  }
182
229
 
183
230
  // ── Auth middleware ───────────────────────────────────────────────────────────
@@ -196,7 +243,7 @@ function authGuard(req, res, next) {
196
243
  // ═══════════════════════════════════════════════════════════════════════════════
197
244
  // ── Upload ────────────────────────────────────────────────────────────────────
198
245
  // ═══════════════════════════════════════════════════════════════════════════════
199
- app.post('/upload', authGuard, upload.single('file'), (req, res) => {
246
+ app.post('/upload', authGuard, upload.single('file'), async (req, res) => {
200
247
  try {
201
248
  if (!req.file) {
202
249
  return res.status(400).json({ status: 'FAILED', message: 'No file uploaded.' });
@@ -222,14 +269,21 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
222
269
  const releaseZipPath = path.join(releaseDir, `${version}.zip`);
223
270
  fs.copyFileSync(req.file.path, releaseZipPath);
224
271
 
225
- // ── Extract directly to {server_root}/{appCode}/ ─────────────────────
272
+ // ── Clean old files & extract to {server_root}/{appCode}/ ─────────────
226
273
  fs.mkdirSync(appDir, { recursive: true });
274
+ cleanDir(appDir);
227
275
  const zip = new AdmZip(req.file.path);
228
276
  zip.extractAllTo(appDir, true);
229
277
  fs.unlinkSync(req.file.path);
230
278
 
231
279
  // ── Install dependencies & start the app ─────────────────────────────
232
- const appResult = installAndStart(appCode);
280
+ const appResult = await installAndStart(appCode);
281
+
282
+ // ── Build the full running URL ───────────────────────────────────────
283
+ const appPort = readAppPort(appCode);
284
+ const host = req.hostname || req.headers.host?.replace(/:\d+$/, '') || 'localhost';
285
+ const protocol = req.protocol || 'http';
286
+ const appUrl = appPort ? `${protocol}://${host}:${appPort}` : null;
233
287
 
234
288
  // ── Track state ──────────────────────────────────────────────────────
235
289
  const deployments = loadDeployments();
@@ -241,6 +295,8 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
241
295
  appDir,
242
296
  releaseZip: releaseZipPath,
243
297
  appPid: appResult.pid || null,
298
+ appUrl,
299
+ appPort,
244
300
  deployedAt: new Date().toISOString(),
245
301
  action: 'upload',
246
302
  message: deployMsg,
@@ -264,6 +320,8 @@ app.post('/upload', authGuard, upload.single('file'), (req, res) => {
264
320
  releaseVersion: version,
265
321
  previousVersion,
266
322
  appDir,
323
+ appUrl,
324
+ appPort,
267
325
  releaseZip: releaseZipPath,
268
326
  appProcess: appResult,
269
327
  completedAt: new Date().toISOString(),
@@ -298,7 +356,7 @@ app.get('/history', authGuard, (req, res) => {
298
356
  // ═══════════════════════════════════════════════════════════════════════════════
299
357
  // ── Rollback ──────────────────────────────────────────────────────────────────
300
358
  // ═══════════════════════════════════════════════════════════════════════════════
301
- app.post('/rollback', authGuard, (req, res) => {
359
+ app.post('/rollback', authGuard, async (req, res) => {
302
360
  try {
303
361
  const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
304
362
  const version = (req.body.version || '').replace(/[^a-zA-Z0-9\.\+\-_]/g, '');
@@ -316,13 +374,14 @@ app.post('/rollback', authGuard, (req, res) => {
316
374
  const deployments = loadDeployments();
317
375
  const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
318
376
 
319
- // Re-extract the zip to the app directory
377
+ // Clean old files & re-extract the zip
320
378
  fs.mkdirSync(appDir, { recursive: true });
379
+ cleanDir(appDir);
321
380
  const zip = new AdmZip(releaseZipPath);
322
381
  zip.extractAllTo(appDir, true);
323
382
 
324
383
  // Install dependencies & restart the app
325
- const appResult = installAndStart(appCode);
384
+ const appResult = await installAndStart(appCode);
326
385
 
327
386
  const record = {
328
387
  version,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloud-ide-cide",
3
- "version": "2.0.37",
3
+ "version": "2.0.39",
4
4
  "description": "Cloud IDE CLI — create, build, publish, upload and deploy Cloud IDE projects.",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/uploadProject.js CHANGED
@@ -715,6 +715,7 @@ async function uploadProject(opts = {}) {
715
715
  console.log(` App : ${result.appCode}`);
716
716
  console.log(` Server : ${label}`);
717
717
  if (result.previousVersion) console.log(` Previous : ${result.previousVersion}`);
718
+ if (result.appUrl) console.log(` Running : ${result.appUrl}`);
718
719
  if (result.appDir) console.log(` Path : ${result.appDir}`);
719
720
  else if (result.releasePath) console.log(` Path : ${result.releasePath}`);
720
721
  if (result.appProcess) {
@@ -729,6 +730,18 @@ async function uploadProject(opts = {}) {
729
730
  if (result.appProcess.steps) {
730
731
  result.appProcess.steps.forEach(s => console.log(` Step : ${s}`));
731
732
  }
733
+ if (result.appProcess.output) {
734
+ console.log('');
735
+ console.log(' ── App Console Output ──────────────────────');
736
+ console.log(result.appProcess.output.trim().split('\n').map(l => ` ${l}`).join('\n'));
737
+ console.log(' ─────────────────────────────────────────────');
738
+ }
739
+ if (result.appProcess.npmOutput) {
740
+ console.log('');
741
+ console.log(' ── npm install Output ──────────────────────');
742
+ console.log(result.appProcess.npmOutput.trim().split('\n').map(l => ` ${l}`).join('\n'));
743
+ console.log(' ─────────────────────────────────────────────');
744
+ }
732
745
  }
733
746
  console.log('');
734
747
  } else {
@@ -745,6 +758,11 @@ async function uploadProject(opts = {}) {
745
758
  module.exports = uploadProject;
746
759
  module.exports.discoverUploadableProjects = discoverUploadableProjects;
747
760
  module.exports.printProjectList = printProjectList;
761
+ module.exports.selectProject = selectProject;
762
+ module.exports.selectServer = selectServer;
763
+ module.exports.resolveEndpoint = resolveEndpoint;
764
+ module.exports.getToken = getToken;
765
+ module.exports.getAuthHeaders = getAuthHeaders;
748
766
  module.exports.findEnvFile = findEnvFile;
749
767
  module.exports.findHostManagerFile = findHostManagerFile;
750
768
  module.exports.parseExistingEnv = parseExistingEnv;