c8ctl-plugin-nano 1.12.0 → 1.13.1

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/README.md CHANGED
@@ -79,12 +79,13 @@ c8ctl nano status
79
79
  # Inspect a cluster c8ctl did NOT start (queries /v2/topology on the given port)
80
80
  c8ctl nano status --port 8080
81
81
 
82
- # Tail a node's log (-f / --follow to stream)
83
- c8ctl nano logs 1 --follow
82
+ # Tail a node's log (-f / --follow to stream). Node ids are 0-indexed,
83
+ # so a single-node cluster is node 0.
84
+ c8ctl nano logs 0 --follow
84
85
 
85
86
  # Simulate a node failing (freeze it) and recovering (resume it)
86
- c8ctl nano pause 1
87
- c8ctl nano resume 1
87
+ c8ctl nano pause 0
88
+ c8ctl nano resume 0
88
89
 
89
90
  # Stop the cluster (engine data is retained)
90
91
  c8ctl nano stop
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,
@@ -1127,16 +1127,17 @@ function controlNode(req, { signal, verb, paused }) {
1127
1127
  }
1128
1128
 
1129
1129
  const nodeIds = state.nodes.map((n) => n.id).join(', ');
1130
+ const exampleId = state.nodes[0].id;
1130
1131
  const idArg = req.positional[0];
1131
1132
  if (idArg === undefined) {
1132
- logger.error(`Specify a node id, e.g. "c8ctl nano ${verb} 1". Nodes: ${nodeIds}`);
1133
+ logger.error(`Specify a node id, e.g. "c8ctl nano ${verb} ${exampleId}". Running nodes: [${nodeIds}]`);
1133
1134
  process.exit(1);
1134
1135
  }
1135
1136
 
1136
1137
  const id = Number.parseInt(idArg, 10);
1137
1138
  const node = Number.isFinite(id) ? state.nodes.find((n) => n.id === id) : undefined;
1138
1139
  if (!node) {
1139
- logger.error(`No node "${idArg}" in the running cluster. Nodes: ${nodeIds}`);
1140
+ logger.error(`No node "${idArg}" in the running cluster. Running nodes: [${nodeIds}]`);
1140
1141
  process.exit(1);
1141
1142
  }
1142
1143
 
@@ -2877,21 +2878,90 @@ function compareSemver(a, b) {
2877
2878
  return 0;
2878
2879
  }
2879
2880
 
2881
+ /**
2882
+ * Resolve how npm must be spawned on the given platform. Spawning `npm`
2883
+ * directly is not portable: on Windows npm is a `npm.cmd` shim, so bare
2884
+ * `"npm"` fails with ENOENT and `"npm.cmd"` fails with EINVAL under the
2885
+ * CVE-2024-27980 hardening. On Windows the shim is therefore run through
2886
+ * cmd.exe (`shell: true`) with every argument double-quoted, and the two
2887
+ * constructs that survive double quotes — an embedded `"` and a `%VAR%`
2888
+ * reference — are rejected rather than escaped.
2889
+ *
2890
+ * This mirrors the host CLI's own `buildNpmInvocation`; it is the local
2891
+ * fallback for `runNpm` when the host runner (`c8ctl.npm`) is unavailable.
2892
+ * `platform` is a parameter so the Windows branch is unit-testable on POSIX.
2893
+ */
2894
+ function buildNpmInvocation(args, platform = process.platform) {
2895
+ if (platform !== 'win32') {
2896
+ return { command: 'npm', args: [...args], shell: false };
2897
+ }
2898
+ for (const arg of args) {
2899
+ if (/["\r\n\0]/.test(arg)) {
2900
+ throw new Error(
2901
+ `Refusing to run npm: argument contains a quote or line break that cannot be passed safely to cmd.exe: ${JSON.stringify(arg)}`,
2902
+ );
2903
+ }
2904
+ if (/%[A-Z_][^%]*?%/i.test(arg)) {
2905
+ throw new Error(
2906
+ `Refusing to run npm: argument contains a cmd.exe environment variable reference: ${JSON.stringify(arg)}`,
2907
+ );
2908
+ }
2909
+ }
2910
+ return {
2911
+ command: 'npm.cmd',
2912
+ args: args.map((arg) => `"${arg.replace(/(\\+)$/, '$1$1')}"`),
2913
+ shell: true,
2914
+ };
2915
+ }
2916
+
2917
+ /** Local, platform-aware npm runner used when the host `c8ctl.npm` is absent. */
2918
+ function runNpmLocal(args, { stdout = false, stdio } = {}) {
2919
+ const { command, args: resolved, shell } = buildNpmInvocation(args);
2920
+ if (shell) {
2921
+ const cmdLine = [command, ...resolved].join(' ');
2922
+ if (stdout) {
2923
+ return { stdout: execSync(cmdLine, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }) };
2924
+ }
2925
+ execSync(cmdLine, { stdio });
2926
+ return undefined;
2927
+ }
2928
+ if (stdout) {
2929
+ return {
2930
+ stdout: execFileSync(command, resolved, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', shell: false }),
2931
+ };
2932
+ }
2933
+ execFileSync(command, resolved, { stdio, shell: false });
2934
+ return undefined;
2935
+ }
2936
+
2937
+ /**
2938
+ * Run npm portably. Prefers the host CLI's cross-platform runner
2939
+ * (`globalThis.c8ctl.npm`, added in c8ctl's plugin runtime); falls back to the
2940
+ * local platform-aware invocation for older hosts and for the detached update
2941
+ * refresh, which runs without the host runtime. Throws on a nonzero exit.
2942
+ */
2943
+ function runNpm(args, { stdout = false, stdio } = {}) {
2944
+ const host = globalThis.c8ctl;
2945
+ if (host && typeof host.npm === 'function') {
2946
+ return stdout ? host.npm({ args, stdout: true }) : host.npm({ args, stdio });
2947
+ }
2948
+ return runNpmLocal(args, { stdout, stdio });
2949
+ }
2950
+
2880
2951
  /** Latest published version of `name` per the npm registry (throws on failure). */
2881
2952
  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();
2953
+ const { stdout } = runNpm(['view', name, 'version'], { stdout: true });
2954
+ return stdout.trim();
2888
2955
  }
2889
2956
 
2890
2957
  /** True when this plugin lives under npm's global node_modules (so `-g` updates it). */
2891
2958
  function isGlobalInstall() {
2892
- const res = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
2893
- if (res.status !== 0) return false;
2894
- const root = res.stdout.trim();
2959
+ let root;
2960
+ try {
2961
+ root = runNpm(['root', '-g'], { stdout: true }).stdout.trim();
2962
+ } catch {
2963
+ return false;
2964
+ }
2895
2965
  return Boolean(root) && pluginDir.startsWith(root);
2896
2966
  }
2897
2967
 
@@ -3009,9 +3079,9 @@ function updatePlugin(req) {
3009
3079
  const where = info.mode === 'managed' ? 'the c8ctl plugin store' : "npm's global prefix";
3010
3080
  console.log(`Pulling ${name}@${latest} into ${where}...`);
3011
3081
  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) {
3082
+ try {
3083
+ runNpm(installArgs, { stdio: 'inherit' });
3084
+ } catch (err) {
3015
3085
  let hint;
3016
3086
  if (info.mode === 'managed') {
3017
3087
  hint = `You can also run:\n${manual}`;
@@ -3020,8 +3090,9 @@ function updatePlugin(req) {
3020
3090
  } else {
3021
3091
  hint = `You may need elevated permissions: sudo ${manual.trim()}`;
3022
3092
  }
3093
+ const code = typeof err?.status === 'number' ? ` (exit ${err.status})` : '';
3023
3094
  throw new Error(
3024
- `npm ${installArgs.join(' ')} failed (exit ${res.status}). ${hint}`,
3095
+ `npm ${installArgs.join(' ')} failed${code}. ${hint}`,
3025
3096
  );
3026
3097
  }
3027
3098
  console.log('');
@@ -3081,13 +3152,28 @@ function updateNotifierDisabled() {
3081
3152
  * fresh result is used on the *next* invocation.
3082
3153
  */
3083
3154
  function spawnUpdateRefresh(name, cacheFile) {
3155
+ // The refresh runs in a detached bare-node child that has no host runtime, so
3156
+ // it cannot use c8ctl.npm. Resolve the portable npm invocation here (single
3157
+ // source of truth) and bake the decided command into a generic runner in the
3158
+ // child — the child makes no platform decision of its own.
3159
+ let inv;
3160
+ try {
3161
+ inv = buildNpmInvocation(['view', name, 'version']);
3162
+ } catch {
3163
+ return; /* unsafe argument for cmd.exe; skip this cycle */
3164
+ }
3084
3165
  const script =
3085
- 'const{spawnSync}=require("child_process");' +
3166
+ 'const{execFileSync,execSync}=require("child_process");' +
3086
3167
  'const{readFileSync,writeFileSync}=require("fs");' +
3168
+ `const cmd=${JSON.stringify(inv.command)},args=${JSON.stringify(inv.args)},shell=${JSON.stringify(inv.shell)};` +
3087
3169
  `let prev={};try{prev=JSON.parse(readFileSync(${JSON.stringify(cacheFile)},"utf8"))}catch{}` +
3088
3170
  '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()}' +
3171
+ 'try{' +
3172
+ 'const o=shell' +
3173
+ '?execSync([cmd,...args].join(" "),{stdio:["ignore","pipe","pipe"],encoding:"utf8"})' +
3174
+ ':execFileSync(cmd,args,{stdio:["ignore","pipe","pipe"],encoding:"utf8",shell:false});' +
3175
+ 'out.latest=String(o||"").trim()' +
3176
+ '}catch{}' +
3091
3177
  `try{writeFileSync(${JSON.stringify(cacheFile)},JSON.stringify(out))}catch{}`;
3092
3178
  try {
3093
3179
  const child = spawn(process.execPath, ['-e', script], { detached: true, stdio: 'ignore' });
@@ -4062,6 +4148,7 @@ function parseProcessosRequest(args, flags) {
4062
4148
  // Internal helpers exported for tests/tooling only. c8ctl consumes just
4063
4149
  // `metadata` and `commands`; these named exports are inert to it.
4064
4150
  export { resolveBinary, findBinary, launcherEnvMarkers };
4151
+ export { buildNpmInvocation };
4065
4152
  export {
4066
4153
  normalizeTaskEnvelope,
4067
4154
  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.1",
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.1",
53
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.13.1",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.13.1",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.13.1",
56
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.13.1",
57
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.13.1",
58
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.13.1"
59
59
  }
60
60
  }