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 +27 -0
- package/deployer/node/upload-api.js +93 -34
- package/package.json +1 -1
- package/uploadProject.js +18 -0
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
|
-
|
|
125
|
-
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const appDir = path.join(SERVER_ROOT, appCode);
|
|
126
|
+
const entryFile = path.join(appDir, 'server.js');
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
if (!fs.existsSync(entryFile)) {
|
|
129
|
+
return resolve({ started: false, reason: `Entry file not found: ${entryFile}` });
|
|
130
|
+
}
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
// Stop existing process first
|
|
133
|
+
stopApp(appCode);
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
+
const out = fs.openSync(logFile, 'a');
|
|
142
|
+
const err = fs.openSync(logFile, 'a');
|
|
147
143
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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
|
-
//
|
|
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
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;
|