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 +5 -4
- package/c8ctl-plugin.js +106 -19
- package/package.json +8 -8
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
|
-
|
|
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
|
|
87
|
-
c8ctl nano resume
|
|
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}
|
|
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.
|
|
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
|
|
2883
|
-
|
|
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
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
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
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
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
|
|
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{
|
|
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
|
-
|
|
3090
|
-
'
|
|
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.
|
|
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.
|
|
53
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
54
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
55
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
56
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
57
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
58
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
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
|
}
|