@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/cli",
3
- "version": "4.20.0-beta.29",
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",
@@ -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)) return [green('pm2 daemon responds and matches the local pm2')];
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,
@@ -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;