node-power-user 2.3.1 → 2.3.2
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/dist/commands/install.js +1 -1
- package/dist/commands/npm.js +1 -1
- package/dist/commands/npx.js +2 -2
- package/dist/commands/outdated.js +7 -7
- package/dist/lib/npx-args.js +6 -5
- package/dist/lib/socket.js +57 -43
- package/dist/lib/spawn.js +61 -0
- package/package.json +1 -1
package/dist/commands/install.js
CHANGED
package/dist/commands/npm.js
CHANGED
package/dist/commands/npx.js
CHANGED
|
@@ -36,7 +36,7 @@ module.exports = async function (options) {
|
|
|
36
36
|
// (--package still downloads even without a positional, so it stays wrapped.)
|
|
37
37
|
if (!binaryName && !npxArgs.forcesDownload(childArgs)) {
|
|
38
38
|
try {
|
|
39
|
-
await socket.wrap(
|
|
39
|
+
await socket.wrap('npx', childArgs, { force: true });
|
|
40
40
|
} catch (e) {
|
|
41
41
|
process.exitCode = e.code || 1;
|
|
42
42
|
}
|
|
@@ -64,7 +64,7 @@ module.exports = async function (options) {
|
|
|
64
64
|
|
|
65
65
|
// Wrap with Socket Firewall (plain run for local binaries / --force)
|
|
66
66
|
try {
|
|
67
|
-
await socket.wrap(
|
|
67
|
+
await socket.wrap('npx', childArgs, { force: force || isLocal });
|
|
68
68
|
} catch (e) {
|
|
69
69
|
process.exitCode = e.code || 1;
|
|
70
70
|
logger.log('');
|
|
@@ -324,7 +324,7 @@ module.exports = async function (options) {
|
|
|
324
324
|
npm.removeLocations(desynced.map(d => d.loc), projectPath);
|
|
325
325
|
|
|
326
326
|
try {
|
|
327
|
-
await socket.wrap('npm install', { force: options.force });
|
|
327
|
+
await socket.wrap('npm', ['install'], { force: options.force });
|
|
328
328
|
} catch (e) {
|
|
329
329
|
// Restore the lockfile so the failed run can't advance it further
|
|
330
330
|
if (lockfileBackup) {
|
|
@@ -369,8 +369,8 @@ module.exports = async function (options) {
|
|
|
369
369
|
return { allPackages, desynced, synced: false };
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
const
|
|
373
|
-
logger.log(logger.format.cyan(`\nRunning ${
|
|
372
|
+
const installArgs = ['install', ...toSync.map(pkg => `${pkg.name}@${pkg.packageVersion}`)];
|
|
373
|
+
logger.log(logger.format.cyan(`\nRunning npm ${installArgs.join(' ')}...`));
|
|
374
374
|
|
|
375
375
|
const lockfileBackup = jetpack.read(lockfilePath);
|
|
376
376
|
|
|
@@ -379,7 +379,7 @@ module.exports = async function (options) {
|
|
|
379
379
|
npm.removeInstalledCopies(toSync.map(pkg => pkg.name), projectPath);
|
|
380
380
|
|
|
381
381
|
try {
|
|
382
|
-
await socket.wrap(
|
|
382
|
+
await socket.wrap('npm', installArgs, { force: options.force });
|
|
383
383
|
} catch (e) {
|
|
384
384
|
// Restore the lockfile so a blocked/failed install can't leave it
|
|
385
385
|
// advanced past the physical files (that desync is what mints loops)
|
|
@@ -497,15 +497,15 @@ module.exports = async function (options) {
|
|
|
497
497
|
|
|
498
498
|
// Install the specific upgraded packages so npm actually pulls them in
|
|
499
499
|
// (plain `npm install` won't upgrade packages that already satisfy the range)
|
|
500
|
-
const
|
|
501
|
-
logger.log(logger.format.cyan(`\nRunning ${
|
|
500
|
+
const installArgs = ['install', ...packageNames.map(name => `${name}@${version.clean(upgrades[name])}`)];
|
|
501
|
+
logger.log(logger.format.cyan(`\nRunning npm ${installArgs.join(' ')}...`));
|
|
502
502
|
|
|
503
503
|
// Remove stale node_modules copies first so npm actually re-fetches them
|
|
504
504
|
// instead of trusting the hidden lockfile and reporting "up to date"
|
|
505
505
|
npm.removeInstalledCopies(packageNames, projectPath);
|
|
506
506
|
|
|
507
507
|
try {
|
|
508
|
-
await socket.wrap(
|
|
508
|
+
await socket.wrap('npm', installArgs, { force: options.force });
|
|
509
509
|
} catch (e) {
|
|
510
510
|
// Restore both manifests since the bulk install failed
|
|
511
511
|
jetpack.write(packageJsonPath, packageJsonBackup);
|
package/dist/lib/npx-args.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// Tokens that select the npx command (positional + flag alias)
|
|
7
7
|
const NPX_TOKENS = ['npx', '--npx'];
|
|
8
8
|
|
|
9
|
-
// Args matching this
|
|
9
|
+
// Args matching this go unquoted, in a shown command and in the cmd.exe line
|
|
10
|
+
// spawn.js builds, so a char added here must be safe on that line too
|
|
10
11
|
const SAFE_ARG = /^[A-Za-z0-9_\-./@=:,+]+$/;
|
|
11
12
|
|
|
12
13
|
// Split raw argv into npu's own flags (before the command token) and the
|
|
@@ -47,8 +48,8 @@ function firstPositional(args) {
|
|
|
47
48
|
return (args || []).find((arg) => !arg.startsWith('-')) || null;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
// Quote an arg for the
|
|
51
|
-
// for POSIX sh; double quotes for cmd.exe,
|
|
51
|
+
// Quote an arg for a shown command (the Running line, a retry hint) and for
|
|
52
|
+
// spawn.js's cmd.exe line when it holds anything unsafe: single quotes for POSIX sh; double quotes for cmd.exe,
|
|
52
53
|
// escaped the way the C runtime splits argv (backslashes before a quote double)
|
|
53
54
|
function quoteArg(arg, platform = process.platform) {
|
|
54
55
|
if (SAFE_ARG.test(arg)) {
|
|
@@ -62,12 +63,12 @@ function quoteArg(arg, platform = process.platform) {
|
|
|
62
63
|
return `'${String(arg).replace(/'/g, `'\\''`)}'`;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
// Quote each arg and join them, the one way every command
|
|
66
|
+
// Quote each arg and join them, the one way every shown command is built
|
|
66
67
|
function quoteArgs(args, platform = process.platform) {
|
|
67
68
|
return args.map((arg) => quoteArg(arg, platform)).join(' ');
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
//
|
|
71
|
+
// The npx command as shown to the user
|
|
71
72
|
function buildCommand(childArgs, platform = process.platform) {
|
|
72
73
|
return `npx ${quoteArgs(childArgs, platform)}`.trim();
|
|
73
74
|
}
|
package/dist/lib/socket.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
// Libraries
|
|
2
|
+
const fs = require('fs');
|
|
2
3
|
const { execute } = require('node-powertools');
|
|
4
|
+
const { spawn } = require('./spawn');
|
|
3
5
|
const logger = new (require('./logger'))('node-power-user');
|
|
4
6
|
|
|
7
|
+
// Whether npu's stdin is a file (`npu npx tool < input.txt`); a closed fd 0 is not
|
|
8
|
+
function stdinIsFile() {
|
|
9
|
+
try {
|
|
10
|
+
return fs.fstatSync(0).isFile();
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
// Check if sfw (Socket Firewall) is installed
|
|
6
17
|
let _sfwVersion = undefined;
|
|
7
18
|
async function isFirewallAvailable() {
|
|
@@ -56,62 +67,65 @@ function isBlockOutput(output) {
|
|
|
56
67
|
return BLOCK_MARKERS.some((marker) => marker.test(output || ''));
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
// Wrap a
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
async function wrap(
|
|
70
|
+
// Wrap a bin and its args with sfw if available; force (or no sfw) runs it
|
|
71
|
+
// plain, output streaming live, each arg reaching the child as written. A
|
|
72
|
+
// non-zero exit throws with err.reason 'sfw-blocked' or 'command-failed', and
|
|
73
|
+
// err.code the exit code.
|
|
74
|
+
async function wrap(bin, args, options) {
|
|
64
75
|
options = options || {};
|
|
65
76
|
|
|
66
77
|
const wrapped = !options.force && await isFirewallAvailable();
|
|
67
|
-
const
|
|
78
|
+
const [file, fileArgs] = wrapped ? ['sfw', [bin, ...args]] : [bin, args];
|
|
68
79
|
|
|
69
80
|
let output = '';
|
|
70
|
-
let exitCode = null;
|
|
71
81
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
const
|
|
82
|
+
// sfw waits for its stdin to close, so it shares npu's stdin only when that
|
|
83
|
+
// is a terminal or a redirected file; a pipe may never close, as Git Bash's
|
|
84
|
+
// mintty gives. Only stdout/stderr are piped, for the live echo below.
|
|
85
|
+
const stdin = process.stdin.isTTY || stdinIsFile() ? 'inherit' : 'ignore';
|
|
86
|
+
const config = { stdio: wrapped ? [stdin, 'pipe', 'pipe'] : 'inherit' };
|
|
76
87
|
|
|
77
88
|
// The wrapped child runs on pipes (not a TTY), which disables color in most
|
|
78
|
-
// CLIs
|
|
89
|
+
// CLIs: restore it, but only when npu itself is printing to a real TTY
|
|
79
90
|
if (wrapped && process.stdout.isTTY) {
|
|
80
|
-
|
|
91
|
+
config.env = { ...process.env, FORCE_COLOR: '1' };
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (child.stderr) {
|
|
95
|
-
child.stderr.on('data', (chunk) => {
|
|
96
|
-
output += chunk.toString();
|
|
97
|
-
process.stderr.write(chunk);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
child.on('close', (code) => {
|
|
102
|
-
exitCode = code;
|
|
94
|
+
const { code, spawnError } = await new Promise((resolve) => {
|
|
95
|
+
const child = spawn(file, fileArgs, config);
|
|
96
|
+
|
|
97
|
+
// Echo piped output through LIVE (and capture it for block detection)
|
|
98
|
+
// instead of letting it buffer invisibly until the process exits
|
|
99
|
+
if (child.stdout) {
|
|
100
|
+
child.stdout.on('data', (chunk) => {
|
|
101
|
+
output += chunk.toString();
|
|
102
|
+
process.stdout.write(chunk);
|
|
103
103
|
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (child.stderr) {
|
|
107
|
+
child.stderr.on('data', (chunk) => {
|
|
108
|
+
output += chunk.toString();
|
|
109
|
+
process.stderr.write(chunk);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
child.on('error', (e) => resolve({ code: null, spawnError: e }));
|
|
114
|
+
child.on('close', (exitCode) => resolve({ code: exitCode }));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (code === 0) {
|
|
118
|
+
return;
|
|
114
119
|
}
|
|
120
|
+
|
|
121
|
+
const blocked = wrapped && isBlockOutput(output);
|
|
122
|
+
const err = blocked
|
|
123
|
+
? new Error('Blocked by Socket Firewall: it flagged a risky package.')
|
|
124
|
+
: new Error(spawnError ? `Could not run ${file}: ${spawnError.message}` : `Command failed with exit code ${code === null ? 1 : code}.`);
|
|
125
|
+
|
|
126
|
+
err.reason = blocked ? 'sfw-blocked' : 'command-failed';
|
|
127
|
+
err.code = code === null ? 1 : code;
|
|
128
|
+
throw err;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
// Socket Firewall handles protection at install time — no separate audit step needed
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Libraries
|
|
2
|
+
const childProcess = require('child_process');
|
|
3
|
+
const jetpack = require('fs-jetpack');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { quoteArg } = require('./npx-args');
|
|
6
|
+
|
|
7
|
+
// cmd.exe metacharacters: each is escaped with ^ so cmd keeps it as text
|
|
8
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
9
|
+
|
|
10
|
+
// Windows env keys are case-insensitive, but a copied env object is not
|
|
11
|
+
function envValue(env, name) {
|
|
12
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === name);
|
|
13
|
+
return key ? env[key] : '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The file cmd.exe runs for a bin: the first PATH dir holding it under a
|
|
17
|
+
// PATHEXT extension (npm, npx and sfw resolve to their .cmd shims)
|
|
18
|
+
function resolveWindowsBin(bin, env) {
|
|
19
|
+
const exts = (envValue(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD').split(';');
|
|
20
|
+
|
|
21
|
+
for (const dir of envValue(env, 'PATH').split(';')) {
|
|
22
|
+
for (const ext of exts) {
|
|
23
|
+
const file = path.join(dir, `${bin}${ext}`);
|
|
24
|
+
if (dir && jetpack.exists(file) === 'file') {
|
|
25
|
+
return file;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// One arg on a cmd.exe line: C-runtime quoting, then every metacharacter
|
|
34
|
+
// escaped, twice for a batch file, whose %* line cmd parses a second time
|
|
35
|
+
function cmdArg(arg, batch) {
|
|
36
|
+
const once = quoteArg(arg, 'win32').replace(CMD_META, '^$1');
|
|
37
|
+
return batch ? once.replace(CMD_META, '^$1') : once;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Spawn a bin with its args, each reaching the child exactly as written.
|
|
41
|
+
// Windows runs bins through cmd.exe, as a .cmd shim needs.
|
|
42
|
+
function spawn(bin, args, options, platform = process.platform) {
|
|
43
|
+
if (platform !== 'win32') {
|
|
44
|
+
return childProcess.spawn(bin, args, options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const env = options.env || process.env;
|
|
48
|
+
const batch = /\.(?:cmd|bat)$/i.test(resolveWindowsBin(bin, env) || '');
|
|
49
|
+
const line = [bin, ...args.map((arg) => cmdArg(arg, batch))].join(' ');
|
|
50
|
+
|
|
51
|
+
return childProcess.spawn(envValue(env, 'COMSPEC') || 'cmd.exe', ['/d', '/s', '/c', `"${line}"`], {
|
|
52
|
+
...options,
|
|
53
|
+
windowsVerbatimArguments: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Export
|
|
58
|
+
module.exports = {
|
|
59
|
+
cmdArg,
|
|
60
|
+
spawn,
|
|
61
|
+
};
|