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.
@@ -53,7 +53,7 @@ module.exports = async function (options) {
53
53
 
54
54
  // Wrap with Socket Firewall
55
55
  try {
56
- await socket.wrap(command, { force });
56
+ await socket.wrap('npm', ['install', ...childArgs], { force });
57
57
  } catch (e) {
58
58
  process.exitCode = e.code || 1;
59
59
 
@@ -34,7 +34,7 @@ module.exports = async function (options) {
34
34
 
35
35
  // Wrap with Socket Firewall
36
36
  try {
37
- await socket.wrap(command, { force });
37
+ await socket.wrap('npm', childArgs, { force });
38
38
  } catch (e) {
39
39
  process.exitCode = e.code || 1;
40
40
  logger.log('');
@@ -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(command, { force: true });
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(command, { force: force || isLocal });
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 installCmd = `npm install ${toSync.map(pkg => `${pkg.name}@${pkg.packageVersion}`).join(' ')}`;
373
- logger.log(logger.format.cyan(`\nRunning ${installCmd}...`));
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(installCmd, { force: options.force });
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 installCmd = `npm install ${packageNames.map(name => `${name}@${version.clean(upgrades[name])}`).join(' ')}`;
501
- logger.log(logger.format.cyan(`\nRunning ${installCmd}...`));
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(installCmd, { force: options.force });
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);
@@ -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 need no quoting for POSIX sh or cmd.exe (commands run with shell: true)
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 shell when it contains anything unsafe: single quotes
51
- // for POSIX sh; double quotes for cmd.exe, which keeps single quotes as text,
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 string is built
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
- // Rebuild the npx command string with shell-safe quoting
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
  }
@@ -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 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
63
- async function wrap(command, options) {
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 finalCommand = wrapped ? `sfw ${command}` : command;
78
+ const [file, fileArgs] = wrapped ? ['sfw', [bin, ...args]] : [bin, args];
68
79
 
69
80
  let output = '';
70
- let exitCode = null;
71
81
 
72
- // The child shares npu's stdin: a pipe npu never closes leaves `sfw npm
73
- // install` waiting forever on Windows, and an install that prompts can read
74
- // the terminal. Only stdout/stderr are piped, for the live echo below.
75
- const executeOptions = { log: !wrapped, config: wrapped ? { stdio: ['inherit', 'pipe', 'pipe'] } : {} };
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 — restore it, but only when npu itself is printing to a real TTY
89
+ // CLIs: restore it, but only when npu itself is printing to a real TTY
79
90
  if (wrapped && process.stdout.isTTY) {
80
- executeOptions.config.env = { ...process.env, FORCE_COLOR: '1' };
91
+ config.env = { ...process.env, FORCE_COLOR: '1' };
81
92
  }
82
93
 
83
- try {
84
- await execute(finalCommand, executeOptions, (child) => {
85
- // Echo piped output through LIVE (and capture it for block detection)
86
- // instead of letting it buffer invisibly until the process exits
87
- if (child.stdout) {
88
- child.stdout.on('data', (chunk) => {
89
- output += chunk.toString();
90
- process.stdout.write(chunk);
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
- } catch (e) {
106
- const blocked = wrapped && isBlockOutput(output);
107
- const err = blocked
108
- ? new Error('Blocked by Socket Firewall — it flagged a risky package.')
109
- : new Error(`Command failed with exit code ${exitCode === null ? 1 : exitCode}.`);
110
-
111
- err.reason = blocked ? 'sfw-blocked' : 'command-failed';
112
- err.code = exitCode === null ? 1 : exitCode;
113
- throw err;
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-power-user",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Easy tools for every Node.js developer!",
5
5
  "private": false,
6
6
  "main": "dist/index.js",