@selvajs/cli 4.20.0-beta.29 → 4.20.0-beta.30
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/package.json +1 -1
- package/src/checks/runtime.js +35 -3
- package/src/commands/pm2.js +49 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/cli",
|
|
3
|
-
"version": "4.20.0-beta.
|
|
3
|
+
"version": "4.20.0-beta.30",
|
|
4
4
|
"description": "Scaffold and operate a Selva white-label deployment. `npx @selvajs/cli <dir>` to bootstrap, `selva <cmd>` to manage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "VektorNode",
|
package/src/checks/runtime.js
CHANGED
|
@@ -17,6 +17,7 @@ import { join } from 'node:path';
|
|
|
17
17
|
import { homedir } from 'node:os';
|
|
18
18
|
import { fixable, green, red, yellow } from './result.js';
|
|
19
19
|
import { PM2_VERSION } from '../deployment-package.js';
|
|
20
|
+
import { APP_NAME } from '../commands/pm2.js';
|
|
20
21
|
|
|
21
22
|
/** The real OS. Tests pass a stand-in with the same shape. */
|
|
22
23
|
export const runtimeEnv = {
|
|
@@ -245,7 +246,12 @@ function checkPm2Daemon(dir, env) {
|
|
|
245
246
|
const output = String(probe.stdout ?? '') + String(probe.stderr ?? '');
|
|
246
247
|
|
|
247
248
|
if (!/out-of-date/i.test(output)) {
|
|
248
|
-
if (/pong/i.test(output))
|
|
249
|
+
if (/pong/i.test(output)) {
|
|
250
|
+
return [
|
|
251
|
+
green('pm2 daemon responds and matches the local pm2'),
|
|
252
|
+
checkAppProcess(bin, dir, env)
|
|
253
|
+
];
|
|
254
|
+
}
|
|
249
255
|
return [yellow('pm2 daemon did not answer `pm2 ping` — check `npx pm2 ping` by hand')];
|
|
250
256
|
}
|
|
251
257
|
|
|
@@ -267,10 +273,37 @@ function checkPm2Daemon(dir, env) {
|
|
|
267
273
|
yellow(
|
|
268
274
|
`pm2 daemon (v${daemon ?? 'unknown'}) differs from the local pm2 (v${local ?? PM2_VERSION}).\n ` +
|
|
269
275
|
'Resync it — this briefly restarts managed processes: npx pm2 update'
|
|
270
|
-
)
|
|
276
|
+
),
|
|
277
|
+
checkAppProcess(bin, dir, env)
|
|
271
278
|
];
|
|
272
279
|
}
|
|
273
280
|
|
|
281
|
+
// A live daemon says nothing about the app: an interrupted `pm2 update` leaves
|
|
282
|
+
// the daemon up with an empty process table, and every other check still
|
|
283
|
+
// passes. Only called once the daemon is confirmed alive, so `jlist` can't
|
|
284
|
+
// spawn one.
|
|
285
|
+
function checkAppProcess(bin, dir, env) {
|
|
286
|
+
const r = env.run(bin, ['jlist'], { cwd: dir });
|
|
287
|
+
// A skewed daemon prints its out-of-date banner ahead of the JSON.
|
|
288
|
+
const json = String(r.stdout ?? '')
|
|
289
|
+
.split('\n')
|
|
290
|
+
.find((line) => line.trimStart().startsWith('['));
|
|
291
|
+
let list;
|
|
292
|
+
try {
|
|
293
|
+
list = JSON.parse(json ?? '');
|
|
294
|
+
} catch {
|
|
295
|
+
return yellow(`could not read the pm2 process list — check \`npx pm2 list\` by hand`);
|
|
296
|
+
}
|
|
297
|
+
const app = list.find((proc) => proc.name === APP_NAME);
|
|
298
|
+
const status = app?.pm2_env?.status;
|
|
299
|
+
if (status === 'online') return green(`${APP_NAME} is online`);
|
|
300
|
+
return red(
|
|
301
|
+
`${APP_NAME} is ${app ? `'${status}'` : 'not registered with pm2'} — the app is down.\n ` +
|
|
302
|
+
'Start it, then save the list so a reboot brings it back:\n ' +
|
|
303
|
+
'npx selva start && npx pm2 save'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
274
307
|
// ── pm2 log rotation ────────────────────────────────────────────────────
|
|
275
308
|
|
|
276
309
|
// pm2 appends to `$PM2_HOME/logs/*.log` forever. Nothing in a default install
|
|
@@ -352,7 +385,6 @@ function installLogRotate(bin, dir, env) {
|
|
|
352
385
|
}
|
|
353
386
|
|
|
354
387
|
function parseSkew(output) {
|
|
355
|
-
// eslint-disable-next-line no-control-regex -- pm2 colourises via chalk on a TTY
|
|
356
388
|
const clean = output.replace(/\[[0-9;]*m/g, '');
|
|
357
389
|
return {
|
|
358
390
|
daemon: /In memory PM2 version:\s*v?(\d+\.\d+\.\d+)/i.exec(clean)?.[1] ?? null,
|
package/src/commands/pm2.js
CHANGED
|
@@ -188,6 +188,8 @@ export async function runUpdate() {
|
|
|
188
188
|
const after = readRuntimeVersion(dir);
|
|
189
189
|
p.log.info(`New @selvajs/selva: ${after ?? 'unknown'}`);
|
|
190
190
|
|
|
191
|
+
realignPm2Pin(dir, s);
|
|
192
|
+
|
|
191
193
|
if (supabase) {
|
|
192
194
|
// Provider code is bundled into @selvajs/selva, but the migration SQL ships
|
|
193
195
|
// here. Left behind, `selva-supabase` copies nothing new. Installed by tag,
|
|
@@ -245,6 +247,53 @@ export async function runUpdate() {
|
|
|
245
247
|
process.exitCode = 1;
|
|
246
248
|
}
|
|
247
249
|
|
|
250
|
+
// Moves the deployment onto the pm2 the just-installed CLI pins, then swaps the
|
|
251
|
+
// daemon. Without it a release that bumps pm2 needs `selva migrate` and
|
|
252
|
+
// `pm2 update` by hand on every host. Runs from the OLD CLI, so the pin is read
|
|
253
|
+
// from the new one on disk. The app is stopped here, so the swap costs no
|
|
254
|
+
// extra downtime. A failure leaves the old pm2 working: warn, don't abort.
|
|
255
|
+
function readPm2Pins(dir) {
|
|
256
|
+
const read = (path) => {
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
declared: read(join(dir, 'package.json'))?.dependencies?.pm2 ?? null,
|
|
265
|
+
pinned:
|
|
266
|
+
read(join(dir, 'node_modules', '@selvajs', 'cli', 'package.json'))?.devDependencies?.pm2 ??
|
|
267
|
+
null
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function realignPm2Pin(dir, s) {
|
|
272
|
+
const { declared, pinned } = readPm2Pins(dir);
|
|
273
|
+
if (!pinned || pinned === declared) return;
|
|
274
|
+
|
|
275
|
+
s.start(`npm install pm2@${pinned}`);
|
|
276
|
+
try {
|
|
277
|
+
execSync(`npm install --save --save-exact --prefer-online pm2@${pinned}`, {
|
|
278
|
+
cwd: dir,
|
|
279
|
+
stdio: 'pipe'
|
|
280
|
+
});
|
|
281
|
+
s.stop(`pm2 ${declared ?? '(none)'} → ${pinned}`);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
s.stop(`npm install pm2@${pinned} failed`);
|
|
284
|
+
p.log.warn(
|
|
285
|
+
`${String(err.stderr ?? err.message ?? err).trim()}\n` +
|
|
286
|
+
'The previous pm2 still runs the app. Retry with `npx selva migrate`.'
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
ensurePm2InSync(dir);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
p.log.warn(`${err instanceof Error ? err.message : err}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
248
297
|
// The app was stopped before the update, so the first answer is the new process.
|
|
249
298
|
async function waitForHealth(env, timeoutMs = 30_000) {
|
|
250
299
|
const host = !env.HOST || env.HOST === '0.0.0.0' || env.HOST === '::' ? '127.0.0.1' : env.HOST;
|