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 +34 -7
- package/bin/node-power-user +10 -1
- package/dist/commands/install.js +27 -27
- package/dist/commands/npm.js +55 -0
- package/dist/commands/npx.js +75 -31
- package/dist/commands/outdated.js +3 -3
- package/dist/lib/npx-args.js +72 -0
- package/dist/lib/socket.js +58 -27
- package/package.json +1 -1
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
|
|
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
|
-
**
|
|
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
|
-
|
|
108
|
+
npu --force npx <command> # bypass Socket protection (not recommended)
|
|
89
109
|
```
|
|
90
110
|
|
|
91
|
-
|
|
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
|
|
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
|
package/bin/node-power-user
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const { hideBin } = require('yargs/helpers');
|
|
3
|
-
const
|
|
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;
|
package/dist/commands/install.js
CHANGED
|
@@ -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
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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 && !
|
|
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 =
|
|
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
|
|
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
|
|
56
|
+
await socket.wrap(command, { force });
|
|
59
57
|
} catch (e) {
|
|
60
|
-
|
|
58
|
+
process.exitCode = e.code || 1;
|
|
59
|
+
|
|
60
|
+
if (e.reason === 'sfw-blocked') {
|
|
61
61
|
logger.log('');
|
|
62
|
-
logger.log('
|
|
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('
|
|
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
|
+
};
|
package/dist/commands/npx.js
CHANGED
|
@@ -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
|
-
//
|
|
13
|
-
|
|
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 (
|
|
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 =
|
|
22
|
-
const binaryName =
|
|
31
|
+
const command = npxArgs.buildCommand(childArgs);
|
|
32
|
+
const binaryName = npxArgs.firstPositional(childArgs);
|
|
23
33
|
|
|
24
|
-
//
|
|
25
|
-
|
|
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:
|
|
67
|
+
await socket.wrap(command, { force: force || isLocal });
|
|
33
68
|
} catch (e) {
|
|
34
|
-
|
|
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
|
-
//
|
|
43
|
-
|
|
44
|
-
if (
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
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 === '
|
|
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 === '
|
|
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 === '
|
|
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
|
+
};
|
package/dist/lib/socket.js
CHANGED
|
@@ -41,42 +41,72 @@ async function check(options) {
|
|
|
41
41
|
return available;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
//
|
|
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
|
|
66
|
+
const wrapped = !options.force && await isFirewallAvailable();
|
|
67
|
+
const finalCommand = wrapped ? `sfw ${command}` : command;
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
63
|
-
let output;
|
|
64
|
-
let exitedWithError = false;
|
|
72
|
+
const executeOptions = { log: !wrapped, config: {} };
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
};
|