@selvajs/cli 2.0.4 → 2.0.5

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": "2.0.4",
3
+ "version": "2.0.5",
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
  "publishConfig": {
@@ -269,8 +269,19 @@ function buildPackageJson(name /*, values */) {
269
269
  '@selvajs/selva': 'latest',
270
270
  // pm2 lives in the deployment's own node_modules so `selva start` can
271
271
  // resolve it via node_modules/.bin/pm2 without a global install. The
272
- // pm2.js wrapper already prefers the local binary (see pm2Bin()).
273
- pm2: '^5.4.0'
272
+ // pm2.js wrapper requires this local binary (see pm2Bin()) — we never
273
+ // fall back to a global pm2 because two pm2 binaries managing the
274
+ // same daemon produces persistent CLI-vs-daemon version-skew
275
+ // warnings and stops/restarts that mysteriously hang.
276
+ //
277
+ // PINNED EXACT (no caret) on purpose. PM2's daemon and CLI must be
278
+ // the same version, and the daemon is sticky — once a daemon is
279
+ // running, only `pm2 update` swaps it. A caret range means two
280
+ // deployments scaffolded weeks apart can install different 5.x
281
+ // versions, and any host that briefly ran a different pm2 (e.g. a
282
+ // stray `npm i -g pm2`) is left with a daemon that won't match
283
+ // either. Bump this deliberately, in lockstep with documentation.
284
+ pm2: '5.4.3'
274
285
  };
275
286
 
276
287
  // Use `selva` (resolved via node_modules/.bin) in the npm scripts. Running
@@ -4,9 +4,13 @@
4
4
  // • `pm2 restart` without --update-env silently keeps the old env, even
5
5
  // after the operator edited .env. We always pass --update-env.
6
6
  //
7
- // • The PM2 binary may live in node_modules/.bin (project-local install)
8
- // or globally. We resolve to the local one first so a fresh deployment
9
- // works without `npm i -g pm2`.
7
+ // • PM2's daemon and CLI must be the same version. The daemon is sticky:
8
+ // once forked it runs that version forever, regardless of what the on-
9
+ // disk binary later becomes. We resolve pm2 to ONE binary (the project-
10
+ // local one) so every command — interactive, scripted, admin endpoint —
11
+ // hits the same code path. If a different pm2 ever started the daemon
12
+ // (e.g. a stray `npm i -g pm2`), `ensurePm2InSync` detects the skew
13
+ // and runs `pm2 update` to respawn the daemon under our binary.
10
14
 
11
15
  import { existsSync, readFileSync } from 'node:fs';
12
16
  import { join } from 'node:path';
@@ -17,10 +21,53 @@ import { requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
17
21
 
18
22
  const APP_NAME = 'selva-compute';
19
23
 
24
+ // Resolve pm2 to the deployment's own copy. NO global fallback — having two
25
+ // pm2 binaries on the same host and letting them both manage the daemon
26
+ // produces the exact version-skew bug this wrapper exists to prevent.
20
27
  function pm2Bin(dir) {
21
28
  const local = join(dir, 'node_modules', '.bin', process.platform === 'win32' ? 'pm2.cmd' : 'pm2');
22
- if (existsSync(local)) return local;
23
- return 'pm2'; // fall back to PATH
29
+ if (!existsSync(local)) {
30
+ throw new Error(
31
+ `pm2 not found at ${local}. The deployment owns its own pm2 — run ` +
32
+ `\`npm install\` in ${dir} to install it. (We deliberately don't ` +
33
+ `fall back to a global pm2; two pm2s managing the same daemon causes ` +
34
+ `persistent skew warnings and hung restarts.)`
35
+ );
36
+ }
37
+ return local;
38
+ }
39
+
40
+ // Check whether the in-memory PM2 daemon was forked by a different pm2 than
41
+ // the one we're about to invoke. PM2 prints "In-memory PM2 is out-of-date" on
42
+ // every command in that state and process operations may stall. `pm2 update`
43
+ // is the only fix: dump → kill daemon → respawn under the current binary →
44
+ // resurrect dump. We run it here before any state-changing command so the
45
+ // caller never gets a half-applied stop/restart against a stale daemon.
46
+ function ensurePm2InSync(dir) {
47
+ const bin = pm2Bin(dir);
48
+ const probe = spawnSync(bin, ['ping'], {
49
+ cwd: dir,
50
+ encoding: 'utf8',
51
+ shell: process.platform === 'win32'
52
+ });
53
+ const output = (probe.stdout ?? '') + (probe.stderr ?? '');
54
+ if (!/out-of-date/i.test(output)) return;
55
+
56
+ p.log.warn(
57
+ 'PM2 in-memory daemon is a different version than the deployment-local pm2 — ' +
58
+ 'running `pm2 update` to resync (this briefly restarts managed processes).'
59
+ );
60
+ const result = spawnSync(bin, ['update'], {
61
+ cwd: dir,
62
+ stdio: 'inherit',
63
+ shell: process.platform === 'win32'
64
+ });
65
+ if ((result.status ?? 1) !== 0) {
66
+ throw new Error(
67
+ '`pm2 update` failed — daemon and CLI remain out of sync. ' +
68
+ 'Investigate manually: `pm2 ping`, `pm2 -v`, `which -a pm2`.'
69
+ );
70
+ }
24
71
  }
25
72
 
26
73
  function runPm2(dir, args, { inherit = true } = {}) {
@@ -33,7 +80,7 @@ function runPm2(dir, args, { inherit = true } = {}) {
33
80
  if (result.error) {
34
81
  throw new Error(
35
82
  `Failed to invoke pm2 (${bin}): ${result.error.message}. ` +
36
- `Install pm2 with \`npm install pm2\` in this directory.`
83
+ `Install pm2 with \`npm install\` in this directory.`
37
84
  );
38
85
  }
39
86
  return result.status ?? 0;
@@ -42,6 +89,7 @@ function runPm2(dir, args, { inherit = true } = {}) {
42
89
  export async function runStart() {
43
90
  const dir = resolveDeploymentDir();
44
91
  requireDeploymentDir(dir);
92
+ ensurePm2InSync(dir);
45
93
  const exit = runPm2(dir, ['start', 'ecosystem.config.cjs']);
46
94
  process.exit(exit);
47
95
  }
@@ -49,6 +97,7 @@ export async function runStart() {
49
97
  export async function runStop() {
50
98
  const dir = resolveDeploymentDir();
51
99
  requireDeploymentDir(dir);
100
+ ensurePm2InSync(dir);
52
101
  const exit = runPm2(dir, ['stop', APP_NAME]);
53
102
  process.exit(exit);
54
103
  }
@@ -56,6 +105,7 @@ export async function runStop() {
56
105
  export async function runRestart() {
57
106
  const dir = resolveDeploymentDir();
58
107
  requireDeploymentDir(dir);
108
+ ensurePm2InSync(dir);
59
109
  // --update-env is the whole point of this wrapper — without it, edits to
60
110
  // .env have no effect on the running process.
61
111
  const exit = runPm2(dir, ['restart', APP_NAME, '--update-env']);
@@ -92,6 +142,13 @@ export async function runUpdate() {
92
142
  return;
93
143
  }
94
144
 
145
+ // Resync the daemon BEFORE we touch the running app — if the daemon is a
146
+ // different version than the local CLI, `pm2 stop` may report success
147
+ // while leaving the process group in a half-state, and the subsequent
148
+ // `pm2 start` then hangs. Running `pm2 update` here puts everything on
149
+ // the same version (and survives the dump+resurrect cycle).
150
+ ensurePm2InSync(dir);
151
+
95
152
  // Stop the running process BEFORE npm rewrites node_modules/@selvajs/selva/build/.
96
153
  // SvelteKit's node adapter lazy-imports chunks from build/server/chunks/ on every
97
154
  // request; if we let npm replace them while the old process is still serving