c8ctl-plugin-nano 1.12.0 → 1.13.0

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.
Files changed (2) hide show
  1. package/c8ctl-plugin.js +103 -17
  2. package/package.json +8 -8
package/c8ctl-plugin.js CHANGED
@@ -28,7 +28,7 @@
28
28
  * c8ctl nano restart [<nodes>] [--purge] ...
29
29
  */
30
30
 
31
- import { spawn, spawnSync } from 'node:child_process';
31
+ import { spawn, spawnSync, execFileSync, execSync } from 'node:child_process';
32
32
  import {
33
33
  existsSync,
34
34
  mkdirSync,
@@ -2877,21 +2877,90 @@ function compareSemver(a, b) {
2877
2877
  return 0;
2878
2878
  }
2879
2879
 
2880
+ /**
2881
+ * Resolve how npm must be spawned on the given platform. Spawning `npm`
2882
+ * directly is not portable: on Windows npm is a `npm.cmd` shim, so bare
2883
+ * `"npm"` fails with ENOENT and `"npm.cmd"` fails with EINVAL under the
2884
+ * CVE-2024-27980 hardening. On Windows the shim is therefore run through
2885
+ * cmd.exe (`shell: true`) with every argument double-quoted, and the two
2886
+ * constructs that survive double quotes — an embedded `"` and a `%VAR%`
2887
+ * reference — are rejected rather than escaped.
2888
+ *
2889
+ * This mirrors the host CLI's own `buildNpmInvocation`; it is the local
2890
+ * fallback for `runNpm` when the host runner (`c8ctl.npm`) is unavailable.
2891
+ * `platform` is a parameter so the Windows branch is unit-testable on POSIX.
2892
+ */
2893
+ function buildNpmInvocation(args, platform = process.platform) {
2894
+ if (platform !== 'win32') {
2895
+ return { command: 'npm', args: [...args], shell: false };
2896
+ }
2897
+ for (const arg of args) {
2898
+ if (/["\r\n\0]/.test(arg)) {
2899
+ throw new Error(
2900
+ `Refusing to run npm: argument contains a quote or line break that cannot be passed safely to cmd.exe: ${JSON.stringify(arg)}`,
2901
+ );
2902
+ }
2903
+ if (/%[A-Z_][^%]*?%/i.test(arg)) {
2904
+ throw new Error(
2905
+ `Refusing to run npm: argument contains a cmd.exe environment variable reference: ${JSON.stringify(arg)}`,
2906
+ );
2907
+ }
2908
+ }
2909
+ return {
2910
+ command: 'npm.cmd',
2911
+ args: args.map((arg) => `"${arg.replace(/(\\+)$/, '$1$1')}"`),
2912
+ shell: true,
2913
+ };
2914
+ }
2915
+
2916
+ /** Local, platform-aware npm runner used when the host `c8ctl.npm` is absent. */
2917
+ function runNpmLocal(args, { stdout = false, stdio } = {}) {
2918
+ const { command, args: resolved, shell } = buildNpmInvocation(args);
2919
+ if (shell) {
2920
+ const cmdLine = [command, ...resolved].join(' ');
2921
+ if (stdout) {
2922
+ return { stdout: execSync(cmdLine, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }) };
2923
+ }
2924
+ execSync(cmdLine, { stdio });
2925
+ return undefined;
2926
+ }
2927
+ if (stdout) {
2928
+ return {
2929
+ stdout: execFileSync(command, resolved, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', shell: false }),
2930
+ };
2931
+ }
2932
+ execFileSync(command, resolved, { stdio, shell: false });
2933
+ return undefined;
2934
+ }
2935
+
2936
+ /**
2937
+ * Run npm portably. Prefers the host CLI's cross-platform runner
2938
+ * (`globalThis.c8ctl.npm`, added in c8ctl's plugin runtime); falls back to the
2939
+ * local platform-aware invocation for older hosts and for the detached update
2940
+ * refresh, which runs without the host runtime. Throws on a nonzero exit.
2941
+ */
2942
+ function runNpm(args, { stdout = false, stdio } = {}) {
2943
+ const host = globalThis.c8ctl;
2944
+ if (host && typeof host.npm === 'function') {
2945
+ return stdout ? host.npm({ args, stdout: true }) : host.npm({ args, stdio });
2946
+ }
2947
+ return runNpmLocal(args, { stdout, stdio });
2948
+ }
2949
+
2880
2950
  /** Latest published version of `name` per the npm registry (throws on failure). */
2881
2951
  function npmLatestVersion(name) {
2882
- const res = spawnSync('npm', ['view', name, 'version'], { encoding: 'utf8' });
2883
- if (res.error) throw new Error(res.error.message);
2884
- if (res.status !== 0) {
2885
- throw new Error((res.stderr || '').trim() || `npm view exited ${res.status}`);
2886
- }
2887
- return res.stdout.trim();
2952
+ const { stdout } = runNpm(['view', name, 'version'], { stdout: true });
2953
+ return stdout.trim();
2888
2954
  }
2889
2955
 
2890
2956
  /** True when this plugin lives under npm's global node_modules (so `-g` updates it). */
2891
2957
  function isGlobalInstall() {
2892
- const res = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
2893
- if (res.status !== 0) return false;
2894
- const root = res.stdout.trim();
2958
+ let root;
2959
+ try {
2960
+ root = runNpm(['root', '-g'], { stdout: true }).stdout.trim();
2961
+ } catch {
2962
+ return false;
2963
+ }
2895
2964
  return Boolean(root) && pluginDir.startsWith(root);
2896
2965
  }
2897
2966
 
@@ -3009,9 +3078,9 @@ function updatePlugin(req) {
3009
3078
  const where = info.mode === 'managed' ? 'the c8ctl plugin store' : "npm's global prefix";
3010
3079
  console.log(`Pulling ${name}@${latest} into ${where}...`);
3011
3080
  console.log('');
3012
- const res = spawnSync('npm', installArgs, { stdio: 'inherit' });
3013
- if (res.error) throw new Error(res.error.message);
3014
- if (res.status !== 0) {
3081
+ try {
3082
+ runNpm(installArgs, { stdio: 'inherit' });
3083
+ } catch (err) {
3015
3084
  let hint;
3016
3085
  if (info.mode === 'managed') {
3017
3086
  hint = `You can also run:\n${manual}`;
@@ -3020,8 +3089,9 @@ function updatePlugin(req) {
3020
3089
  } else {
3021
3090
  hint = `You may need elevated permissions: sudo ${manual.trim()}`;
3022
3091
  }
3092
+ const code = typeof err?.status === 'number' ? ` (exit ${err.status})` : '';
3023
3093
  throw new Error(
3024
- `npm ${installArgs.join(' ')} failed (exit ${res.status}). ${hint}`,
3094
+ `npm ${installArgs.join(' ')} failed${code}. ${hint}`,
3025
3095
  );
3026
3096
  }
3027
3097
  console.log('');
@@ -3081,13 +3151,28 @@ function updateNotifierDisabled() {
3081
3151
  * fresh result is used on the *next* invocation.
3082
3152
  */
3083
3153
  function spawnUpdateRefresh(name, cacheFile) {
3154
+ // The refresh runs in a detached bare-node child that has no host runtime, so
3155
+ // it cannot use c8ctl.npm. Resolve the portable npm invocation here (single
3156
+ // source of truth) and bake the decided command into a generic runner in the
3157
+ // child — the child makes no platform decision of its own.
3158
+ let inv;
3159
+ try {
3160
+ inv = buildNpmInvocation(['view', name, 'version']);
3161
+ } catch {
3162
+ return; /* unsafe argument for cmd.exe; skip this cycle */
3163
+ }
3084
3164
  const script =
3085
- 'const{spawnSync}=require("child_process");' +
3165
+ 'const{execFileSync,execSync}=require("child_process");' +
3086
3166
  'const{readFileSync,writeFileSync}=require("fs");' +
3167
+ `const cmd=${JSON.stringify(inv.command)},args=${JSON.stringify(inv.args)},shell=${JSON.stringify(inv.shell)};` +
3087
3168
  `let prev={};try{prev=JSON.parse(readFileSync(${JSON.stringify(cacheFile)},"utf8"))}catch{}` +
3088
3169
  'const out=Object.assign({},prev,{lastCheck:Date.now()});' +
3089
- `const r=spawnSync("npm",["view",${JSON.stringify(name)},"version"],{encoding:"utf8"});` +
3090
- 'if(r.status===0){out.latest=String(r.stdout||"").trim()}' +
3170
+ 'try{' +
3171
+ 'const o=shell' +
3172
+ '?execSync([cmd,...args].join(" "),{stdio:["ignore","pipe","pipe"],encoding:"utf8"})' +
3173
+ ':execFileSync(cmd,args,{stdio:["ignore","pipe","pipe"],encoding:"utf8",shell:false});' +
3174
+ 'out.latest=String(o||"").trim()' +
3175
+ '}catch{}' +
3091
3176
  `try{writeFileSync(${JSON.stringify(cacheFile)},JSON.stringify(out))}catch{}`;
3092
3177
  try {
3093
3178
  const child = spawn(process.execPath, ['-e', script], { detached: true, stdio: 'ignore' });
@@ -4062,6 +4147,7 @@ function parseProcessosRequest(args, flags) {
4062
4147
  // Internal helpers exported for tests/tooling only. c8ctl consumes just
4063
4148
  // `metadata` and `commands`; these named exports are inert to it.
4064
4149
  export { resolveBinary, findBinary, launcherEnvMarkers };
4150
+ export { buildNpmInvocation };
4065
4151
  export {
4066
4152
  normalizeTaskEnvelope,
4067
4153
  collectEnvelopeFrom,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -49,12 +49,12 @@
49
49
  "semantic-release": "^25.0.3"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.12.0",
53
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.12.0",
54
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.12.0",
55
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.12.0",
56
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.12.0",
57
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.12.0",
58
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.12.0"
52
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.13.0",
53
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.13.0",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.13.0",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.13.0",
56
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.13.0",
57
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.13.0",
58
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.13.0"
59
59
  }
60
60
  }