node-power-user 2.2.1 → 2.3.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.
package/README.md CHANGED
@@ -74,23 +74,50 @@ npu global
74
74
  ```
75
75
 
76
76
  ### Install Packages
77
- Install packages with supply chain protection via [Socket](https://socket.dev/). Every install is wrapped with Socket to detect malicious or compromised packages — including transitive dependencies — before they're added to your project. After install, a full `socket npm audit` runs against your entire dependency tree.
77
+ Install packages with supply chain protection via [Socket Firewall](https://socket.dev/) (`sfw`). Every install is wrapped with sfw, which blocks malicious or compromised packages — including transitive dependencies — before they're downloaded.
78
78
 
79
79
  ```shell
80
80
  npu install
81
81
  npu i <package>
82
- npu i <package> --save-dev
83
- npu i <package> --save-exact
82
+ npu i <package> --save-dev --legacy-peer-deps
84
83
  ```
85
84
 
86
- **If Socket CLI is not installed, `npu install` will refuse to run.** Install it globally to enable protection:
85
+ **Every npm flag passes through verbatim** (`--save-dev`, `--save-exact`, `--legacy-peer-deps`, `--dry-run`, ...) — npu doesn't maintain a whitelist.
86
+
87
+ **If Socket Firewall is not installed, `npu install` will refuse to run.** Install it globally to enable protection:
88
+ ```shell
89
+ npm install -g sfw
90
+ ```
91
+
92
+ Use `--force` (before the command) to bypass Socket protection (not recommended):
93
+ ```shell
94
+ npu --force i <package>
95
+ ```
96
+
97
+ ### Run Packages (npx)
98
+ Run a package binary with supply chain protection. When the binary would be **downloaded from npm**, the run is wrapped with Socket Firewall. When it's **already installed locally** (`node_modules/.bin/`), it runs directly — nothing is downloaded, so there's nothing to protect and no firewall overhead.
99
+ ```shell
100
+ npu npx <command> [args...]
101
+ ```
102
+
103
+ * Output streams **live** in both modes.
104
+ * The child command's exit code is **propagated** (CI-safe).
105
+ * **Everything after the command passes through to the child** — e.g. `npu npx rimraf dist --force` sends `--force` to rimraf.
106
+ * npu's own flags go **before** the `npx` token:
87
107
  ```shell
88
- npm install -g @socketsecurity/cli --save-exact
108
+ npu --force npx <command> # bypass Socket protection (not recommended)
89
109
  ```
90
110
 
91
- Use `--force` to bypass Socket protection (not recommended):
111
+ A run is only reported as **blocked** when Socket Firewall actually blocked a download — an ordinary failure (like a failing test suite) is reported as a plain command failure with its exit code.
112
+
113
+ Two edge behaviors: `--package`/`-p` runs always stay behind the firewall (they download regardless of local binaries), and bare flag queries (`npx --version`, `npx --help`) pass through silently with no wrapper output at all.
114
+
115
+ ### Wrapped npm (exec / create / init)
116
+ Run any npm command wrapped with Socket Firewall, args untouched. This is what the shell shims use to protect `npm exec|create|init` without rewriting their semantics — `npm create X` maps to package `create-X`, so it must never be turned into `npx X`.
92
117
  ```shell
93
- npu i <package> --force
118
+ npu npm <args...>
119
+ npu npm create vite my-app
120
+ npu --force npm <args...> # bypass Socket protection (not recommended)
94
121
  ```
95
122
 
96
123
  ### Audit
@@ -1,6 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  const { hideBin } = require('yargs/helpers');
3
- const argv = require('yargs')(hideBin(process.argv)).parseSync();
3
+ const rawArgs = hideBin(process.argv);
4
+
5
+ // help(false)/version(false): yargs must not intercept --help/--version —
6
+ // commands like `npu npx <cmd> --help` need those flags to reach the child.
7
+ // boolean('force'): without it, `npu --force npx ...` parses as --force=npx
8
+ // and swallows the command token.
9
+ const argv = require('yargs')(rawArgs).help(false).version(false).boolean('force').parseSync();
10
+
11
+ // Commands that pass args through to a child (npx) re-parse from raw argv
12
+ argv.raw = rawArgs;
4
13
 
5
14
  // --cwd / -C: run as if invoked from a different directory
6
15
  const targetDir = argv.cwd || argv.C;
@@ -1,32 +1,32 @@
1
1
  // Libraries
2
2
  const logger = new (require('../lib/logger'))('node-power-user');
3
3
  const socket = require('../lib/socket');
4
+ const npxArgs = require('../lib/npx-args');
4
5
  const npm = require('../lib/npm');
5
6
 
7
+ // Tokens that select the install command (positional + flag aliases)
8
+ const INSTALL_TOKENS = ['install', 'i', '--install', '-i'];
9
+
6
10
  // Module
7
11
  module.exports = async function (options) {
8
- // Get packages from positional args (everything after "install" / "i")
9
- const packages = (options._ || []).slice(1);
10
-
11
- // Build the npm command
12
- const flags = [];
13
-
14
- if (options.D || options['save-dev']) {
15
- flags.push('--save-dev');
16
- }
12
+ // Parse from RAW argv so every npm flag passes through verbatim
13
+ // (--save-dev, --legacy-peer-deps, --dry-run, ...). npu's own --force goes
14
+ // BEFORE the command token: `npu --force i <package>`.
15
+ const raw = options.raw || [
16
+ ...(options.force ? ['--force'] : []),
17
+ 'install',
18
+ ...(options._ || []).slice(1).map(String),
19
+ ];
20
+ const { childArgs, force } = npxArgs.parsePassthroughArgs(raw, INSTALL_TOKENS);
17
21
 
18
- if (options.E || options['save-exact']) {
19
- flags.push('--save-exact');
20
- }
21
-
22
- if (options.g || options.global) {
23
- flags.push('--global');
24
- }
22
+ // Non-flag tokens = the packages being installed (empty = full-tree install)
23
+ const packages = childArgs.filter((arg) => !arg.startsWith('-'));
24
+ const isGlobal = childArgs.includes('-g') || childArgs.includes('--global');
25
25
 
26
26
  // When installing specific packages with a version/tag (e.g. @latest, @^2.0.0),
27
27
  // remove existing node_modules copies first so npm actually re-fetches them
28
28
  // instead of reporting "up to date" with a stale cached version.
29
- if (packages.length > 0 && !flags.includes('--global')) {
29
+ if (packages.length > 0 && !isGlobal) {
30
30
  const names = [];
31
31
 
32
32
  for (const pkg of packages) {
@@ -43,30 +43,30 @@ module.exports = async function (options) {
43
43
  npm.removeInstalledCopies(names);
44
44
  }
45
45
 
46
- const command = packages.length > 0
47
- ? `npm install ${packages.join(' ')} ${flags.join(' ')}`.trim()
48
- : `npm install ${flags.join(' ')}`.trim();
46
+ const command = `npm install ${childArgs.map(npxArgs.quoteArg).join(' ')}`.trim();
49
47
 
50
48
  // Check socket status upfront (blocks if not installed unless --force)
51
- await socket.check({ force: options.force });
49
+ await socket.check({ force });
52
50
 
53
51
  // Log
54
52
  logger.log(`Running: ${logger.format.cyan(command)}`);
55
53
 
56
54
  // Wrap with Socket Firewall
57
55
  try {
58
- await socket.wrap(command, { force: options.force });
56
+ await socket.wrap(command, { force });
59
57
  } catch (e) {
60
- if (e.reason === 'npm-failed') {
58
+ process.exitCode = e.code || 1;
59
+
60
+ if (e.reason === 'sfw-blocked') {
61
61
  logger.log('');
62
- logger.log('Fix the npm error above (e.g. resolve peer-dep conflicts) and retry.');
62
+ logger.log('Install blocked. See the output above for details.');
63
+ logger.log('To retry without firewall protection:');
64
+ logger.log(logger.format.cyan(` npu --force i ${childArgs.join(' ')}`.trim()));
63
65
  return;
64
66
  }
65
67
 
66
68
  logger.log('');
67
- logger.log('Install blocked. See the output above for details.');
68
- logger.log('To retry without firewall protection:');
69
- logger.log(logger.format.cyan(` npu i ${packages.join(' ')} ${flags.join(' ')} --force`.trim()));
69
+ logger.log('Fix the npm error above (e.g. resolve peer-dep conflicts) and retry.');
70
70
  return;
71
71
  }
72
72
 
@@ -0,0 +1,55 @@
1
+ // Libraries
2
+ const logger = new (require('../lib/logger'))('node-power-user');
3
+ const socket = require('../lib/socket');
4
+ const npxArgs = require('../lib/npx-args');
5
+
6
+ // Module — `npu npm <args...>`: run ANY npm command wrapped with Socket
7
+ // Firewall, args untouched. Used by the shell shims to protect
8
+ // `npm exec|create|init` without rewriting their semantics (`npm create X`
9
+ // must stay `npm create X` — it maps to package create-X, not X).
10
+ module.exports = async function (options) {
11
+ // Parse from RAW argv so npm's own flags pass through verbatim; npu's
12
+ // --force goes BEFORE the npm token: `npu --force npm <args...>`
13
+ const raw = options.raw || [
14
+ ...(options.force ? ['--force'] : []),
15
+ 'npm',
16
+ ...(options._ || []).slice(1).map(String),
17
+ ];
18
+ const { childArgs, force } = npxArgs.parsePassthroughArgs(raw, ['npm']);
19
+
20
+ if (childArgs.length === 0) {
21
+ logger.error('Usage: npu [--force] npm <args...>');
22
+ logger.log(`Example: ${logger.format.cyan('npu npm create vite my-app')}`);
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+
27
+ const command = `npm ${childArgs.map(npxArgs.quoteArg).join(' ')}`;
28
+
29
+ // Check firewall status upfront (blocks if not installed unless --force)
30
+ await socket.check({ force });
31
+
32
+ // Log
33
+ logger.log(`Running: ${logger.format.cyan(command)}`);
34
+
35
+ // Wrap with Socket Firewall
36
+ try {
37
+ await socket.wrap(command, { force });
38
+ } catch (e) {
39
+ process.exitCode = e.code || 1;
40
+ logger.log('');
41
+
42
+ if (e.reason === 'sfw-blocked') {
43
+ logger.log('Command blocked by Socket Firewall. See the output above for details.');
44
+ logger.log('');
45
+ logger.log('To retry without firewall protection:');
46
+ logger.log(logger.format.cyan(` npu --force ${command}`));
47
+ return;
48
+ }
49
+
50
+ logger.log(`Command failed (exit ${e.code || 1}). See the error output above.`);
51
+ return;
52
+ }
53
+
54
+ logger.log(logger.format.green('Done.'));
55
+ };
@@ -2,6 +2,7 @@
2
2
  const logger = new (require('../lib/logger'))('node-power-user');
3
3
  const chalk = require('chalk').default;
4
4
  const socket = require('../lib/socket');
5
+ const npxArgs = require('../lib/npx-args');
5
6
  const jetpack = require('fs-jetpack');
6
7
  const path = require('path');
7
8
 
@@ -9,58 +10,101 @@ const projectPath = process.cwd();
9
10
 
10
11
  // Module
11
12
  module.exports = async function (options) {
12
- // Get everything after "npx" as the command to run
13
- const args = (options._ || []).slice(1);
13
+ // Parse from RAW argv so the child command's flags pass through verbatim
14
+ // (yargs would otherwise consume them). npu's own flags go BEFORE the npx
15
+ // token: `npu [--force] npx <command> [args...]` — a trailing --force
16
+ // belongs to the child (e.g. `npu npx rimraf dist --force`).
17
+ const raw = options.raw || [
18
+ ...(options.force ? ['--force'] : []),
19
+ 'npx',
20
+ ...(options._ || []).slice(1).map(String),
21
+ ];
22
+ const { childArgs, force } = npxArgs.parseNpxArgs(raw);
14
23
 
15
- if (args.length === 0) {
16
- logger.error('Usage: npu npx <command> [args...]');
24
+ if (childArgs.length === 0) {
25
+ logger.error('Usage: npu [--force] npx <command> [args...]');
17
26
  logger.log(`Example: ${logger.format.cyan('npu npx create-react-app my-app')}`);
27
+ process.exitCode = 1;
18
28
  return;
19
29
  }
20
30
 
21
- const command = `npx ${args.join(' ')}`;
22
- const binaryName = args[0];
31
+ const command = npxArgs.buildCommand(childArgs);
32
+ const binaryName = npxArgs.firstPositional(childArgs);
23
33
 
24
- // Check firewall status upfront (blocks if not installed unless --force)
25
- await socket.check({ force: options.force });
34
+ // No package named (pure flag queries like `npx --version`) = nothing to
35
+ // download = nothing to protect — silent plain passthrough, no ceremony.
36
+ // (--package still downloads even without a positional, so it stays wrapped.)
37
+ if (!binaryName && !npxArgs.forcesDownload(childArgs)) {
38
+ try {
39
+ await socket.wrap(command, { force: true });
40
+ } catch (e) {
41
+ process.exitCode = e.code || 1;
42
+ }
43
+ return;
44
+ }
45
+
46
+ const localBin = binaryName && path.join(projectPath, 'node_modules', '.bin', binaryName);
47
+
48
+ // --package/-p forces npx to download that package even when the named
49
+ // binary exists locally — those runs must stay behind the firewall
50
+ const isLocal = !npxArgs.forcesDownload(childArgs)
51
+ && !!(localBin && jetpack.exists(localBin));
52
+
53
+ // Local binaries download nothing, so the firewall has nothing to protect —
54
+ // run plain (streams live, skips the sfw startup cost)
55
+ if (isLocal) {
56
+ logger.log(`'${binaryName}' is installed locally — running it directly (no download to protect).`);
57
+ } else {
58
+ // Check firewall status upfront (blocks if not installed unless --force)
59
+ await socket.check({ force });
60
+ }
26
61
 
27
62
  // Log
28
63
  logger.log(`Running: ${logger.format.cyan(command)}`);
29
64
 
30
- // Wrap with Socket Firewall
65
+ // Wrap with Socket Firewall (plain run for local binaries / --force)
31
66
  try {
32
- await socket.wrap(command, { force: options.force });
67
+ await socket.wrap(command, { force: force || isLocal });
33
68
  } catch (e) {
34
- if (e.reason === 'npm-failed') {
35
- logger.log('');
36
- logger.log('npx command failed. See the error output above.');
37
- return;
38
- }
39
-
69
+ process.exitCode = e.code || 1;
40
70
  logger.log('');
41
71
 
42
- // Check if the binary exists locally — if not, npx tried to download from npm
43
- const localBin = path.join(projectPath, 'node_modules', '.bin', binaryName);
44
- if (!jetpack.exists(localBin)) {
72
+ // A real block — only reachable on the sfw path, i.e. npx tried to
73
+ // download '<binary>' from npm and Socket Firewall stopped it
74
+ if (e.reason === 'sfw-blocked') {
45
75
  logger.error(`'${binaryName}' was not found in node_modules/.bin/`);
46
76
  logger.log(`npx tried to download package '${binaryName}' from npm, which was blocked by Socket Firewall.`);
77
+ suggestOwnBin(binaryName, childArgs);
47
78
 
48
- // Check if it's defined in this project's own bin field
49
- const pkgJson = jetpack.read(path.join(projectPath, 'package.json'), 'json');
50
- if (pkgJson?.bin?.[binaryName]) {
51
- logger.log('');
52
- logger.log(`'${binaryName}' is defined in this project's ${chalk.cyan('bin')} field but is not installed as a command.`);
53
- logger.log(`Run it directly: ${logger.format.cyan(`${pkgJson.bin[binaryName]} ${args.slice(1).join(' ')}`.trim())}`);
54
- }
55
- } else {
56
- logger.log('Command blocked by Socket Firewall. See the output above for details.');
79
+ logger.log('');
80
+ logger.log('To retry without firewall protection:');
81
+ logger.log(logger.format.cyan(` npu --force ${command}`));
82
+ return;
83
+ }
84
+
85
+ // The command itself failed — not a firewall matter
86
+ logger.log(`Command failed (exit ${e.code || 1}). See the error output above.`);
87
+
88
+ if (!isLocal) {
89
+ suggestOwnBin(binaryName, childArgs);
57
90
  }
58
91
 
59
- logger.log('');
60
- logger.log('To retry without firewall protection:');
61
- logger.log(logger.format.cyan(` npu npx ${args.join(' ')} --force`));
62
92
  return;
63
93
  }
64
94
 
65
95
  logger.log(logger.format.green('Done.'));
66
96
  };
97
+
98
+ // If the binary is defined in this project's own bin field but not installed
99
+ // as a command, point at running it directly
100
+ function suggestOwnBin(binaryName, childArgs) {
101
+ const pkgJson = jetpack.read(path.join(projectPath, 'package.json'), 'json');
102
+
103
+ if (!pkgJson?.bin?.[binaryName]) {
104
+ return;
105
+ }
106
+
107
+ logger.log('');
108
+ logger.log(`'${binaryName}' is defined in this project's ${chalk.cyan('bin')} field but is not installed as a command.`);
109
+ logger.log(`Run it directly: ${logger.format.cyan(`${pkgJson.bin[binaryName]} ${childArgs.slice(1).join(' ')}`.trim())}`);
110
+ }
@@ -331,7 +331,7 @@ module.exports = async function (options) {
331
331
  jetpack.write(lockfilePath, lockfileBackup);
332
332
  }
333
333
 
334
- if (e.reason === 'npm-failed') {
334
+ if (e.reason === 'command-failed') {
335
335
  logger.log('');
336
336
  logger.log('Fix the npm error above and re-run with --heal — the removed copies will reinstall then.');
337
337
  return { allPackages, desynced, healed: false };
@@ -387,7 +387,7 @@ module.exports = async function (options) {
387
387
  jetpack.write(lockfilePath, lockfileBackup);
388
388
  }
389
389
 
390
- if (e.reason === 'npm-failed') {
390
+ if (e.reason === 'command-failed') {
391
391
  logger.log('');
392
392
  logger.log('Fix the npm error above and retry.');
393
393
  return { allPackages, desynced, synced: false };
@@ -515,7 +515,7 @@ module.exports = async function (options) {
515
515
  logger.log('package.json and package-lock.json have been restored to their original state.');
516
516
  logger.log('The removed package copies will show as missing in the next check until reinstalled.');
517
517
 
518
- if (e.reason === 'npm-failed') {
518
+ if (e.reason === 'command-failed') {
519
519
  logger.log('');
520
520
  logger.log('Fix the npm error above (e.g. resolve peer-dep conflicts) and retry.');
521
521
  return { allPackages, desynced, updated: false, target: action };
@@ -0,0 +1,72 @@
1
+ // Pure raw-argv parsing for passthrough commands (`npu npx`, `npu install`,
2
+ // `npu npm`) — the child command's flags must pass through verbatim (yargs
3
+ // would otherwise consume them), so npu's own flags go BEFORE the command
4
+ // token: `npu [--force] npx <command> [args...]`
5
+
6
+ // Tokens that select the npx command (positional + flag alias)
7
+ const NPX_TOKENS = ['npx', '--npx'];
8
+
9
+ // Args matching this need no quoting for POSIX sh (commands run with shell: true)
10
+ const SAFE_ARG = /^[A-Za-z0-9_\-./@=:,+]+$/;
11
+
12
+ // Split raw argv into npu's own flags (before the command token) and the
13
+ // child command's args (everything after, verbatim)
14
+ function parsePassthroughArgs(rawArgs, tokens) {
15
+ const args = rawArgs || [];
16
+ const tokenIndex = args.findIndex((arg) => tokens.includes(arg));
17
+
18
+ if (tokenIndex === -1) {
19
+ return { childArgs: [], force: false };
20
+ }
21
+
22
+ return {
23
+ childArgs: args.slice(tokenIndex + 1),
24
+ force: args.slice(0, tokenIndex).includes('--force'),
25
+ };
26
+ }
27
+
28
+ // npx-flavored parse (the common case)
29
+ function parseNpxArgs(rawArgs) {
30
+ return parsePassthroughArgs(rawArgs, NPX_TOKENS);
31
+ }
32
+
33
+ // npx's --package/-p makes it download that package regardless of which
34
+ // binary is named — such runs must never skip the firewall. Scans the whole
35
+ // arg list (a -p meant for the child, e.g. `tsc -p tsconfig.json`, is a
36
+ // false positive — fail-closed: it just runs wrapped instead of fast).
37
+ function forcesDownload(args) {
38
+ return (args || []).some((arg) => {
39
+ return arg === '-p'
40
+ || arg === '--package'
41
+ || arg.startsWith('--package=');
42
+ });
43
+ }
44
+
45
+ // First non-flag token = the binary npx will run (null when only flags)
46
+ function firstPositional(args) {
47
+ return (args || []).find((arg) => !arg.startsWith('-')) || null;
48
+ }
49
+
50
+ // Single-quote an arg for the shell when it contains anything unsafe
51
+ function quoteArg(arg) {
52
+ if (SAFE_ARG.test(arg)) {
53
+ return arg;
54
+ }
55
+
56
+ return `'${String(arg).replace(/'/g, `'\\''`)}'`;
57
+ }
58
+
59
+ // Rebuild the npx command string with shell-safe quoting
60
+ function buildCommand(childArgs) {
61
+ return ['npx', ...childArgs.map(quoteArg)].join(' ');
62
+ }
63
+
64
+ // Export
65
+ module.exports = {
66
+ parsePassthroughArgs,
67
+ parseNpxArgs,
68
+ forcesDownload,
69
+ firstPositional,
70
+ quoteArg,
71
+ buildCommand,
72
+ };
@@ -41,42 +41,72 @@ async function check(options) {
41
41
  return available;
42
42
  }
43
43
 
44
- // Wrap an npm command with sfw if available
44
+ // Markers Socket Firewall prints when it ACTUALLY blocks something (sourced
45
+ // from the sfw-free v1.12.0 report output). sfw passes the wrapped command's
46
+ // exit code through, so a non-zero exit alone does NOT mean a block — a
47
+ // failing test suite exits non-zero too.
48
+ const BLOCK_MARKERS = [
49
+ /blocked [1-9]\d* packages?:/i, // " - blocked 1 package: <purl> - <reason>"
50
+ /Blocked (?:Packages|Hostnames) \([1-9]\d*\)/i, // report section headers
51
+ /SUMMARY: [1-9]\d* item\(s\) blocked/i, // report summary line
52
+ ];
53
+
54
+ // Check command output for sfw's block report
55
+ function isBlockOutput(output) {
56
+ return BLOCK_MARKERS.some((marker) => marker.test(output || ''));
57
+ }
58
+
59
+ // Wrap a command with sfw if available; force (or no sfw) runs it plain.
60
+ // Output always streams live. Throws on non-zero exit with:
61
+ // err.reason — 'sfw-blocked' (real firewall block) | 'command-failed'
62
+ // err.code — the child's exit code
45
63
  async function wrap(command, options) {
46
64
  options = options || {};
47
65
 
48
- const available = await isFirewallAvailable();
66
+ const wrapped = !options.force && await isFirewallAvailable();
67
+ const finalCommand = wrapped ? `sfw ${command}` : command;
49
68
 
50
- // If firewall is not installed or --force is used, run plain npm
51
- if (!available || options.force) {
52
- try {
53
- await execute(command, { log: true });
54
- } catch (e) {
55
- const err = new Error('npm install failed. See the error output above.');
56
- err.reason = 'npm-failed';
57
- throw err;
58
- }
59
- return;
60
- }
69
+ let output = '';
70
+ let exitCode = null;
61
71
 
62
- // Run with Socket Firewall
63
- let output;
64
- let exitedWithError = false;
72
+ const executeOptions = { log: !wrapped, config: {} };
65
73
 
66
- try {
67
- output = await execute(`sfw ${command}`, { log: false });
68
- } catch (e) {
69
- output = e.message || '';
70
- exitedWithError = true;
74
+ // The wrapped child runs on pipes (not a TTY), which disables color in most
75
+ // CLIs — restore it, but only when npu itself is printing to a real TTY
76
+ if (wrapped && process.stdout.isTTY) {
77
+ executeOptions.config.env = { ...process.env, FORCE_COLOR: '1' };
71
78
  }
72
79
 
73
- if (output) {
74
- console.log(output);
75
- }
80
+ try {
81
+ await execute(finalCommand, executeOptions, (child) => {
82
+ // Echo piped output through LIVE (and capture it for block detection)
83
+ // instead of letting it buffer invisibly until the process exits
84
+ if (child.stdout) {
85
+ child.stdout.on('data', (chunk) => {
86
+ output += chunk.toString();
87
+ process.stdout.write(chunk);
88
+ });
89
+ }
90
+
91
+ if (child.stderr) {
92
+ child.stderr.on('data', (chunk) => {
93
+ output += chunk.toString();
94
+ process.stderr.write(chunk);
95
+ });
96
+ }
97
+
98
+ child.on('close', (code) => {
99
+ exitCode = code;
100
+ });
101
+ });
102
+ } catch (e) {
103
+ const blocked = wrapped && isBlockOutput(output);
104
+ const err = blocked
105
+ ? new Error('Blocked by Socket Firewall — it flagged a risky package.')
106
+ : new Error(`Command failed with exit code ${exitCode === null ? 1 : exitCode}.`);
76
107
 
77
- if (exitedWithError) {
78
- const err = new Error('Install blocked — Socket Firewall may have flagged a risky package.');
79
- err.reason = 'sfw-blocked';
108
+ err.reason = blocked ? 'sfw-blocked' : 'command-failed';
109
+ err.code = exitCode === null ? 1 : exitCode;
80
110
  throw err;
81
111
  }
82
112
  }
@@ -90,4 +120,5 @@ module.exports = {
90
120
  check,
91
121
  wrap,
92
122
  audit,
123
+ isBlockOutput,
93
124
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-power-user",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Easy tools for every Node.js developer!",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {