@aikidosec/safe-chain 0.0.1-custom-install-dir

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.
Files changed (116) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +537 -0
  3. package/bin/aikido-bun.js +14 -0
  4. package/bin/aikido-bunx.js +14 -0
  5. package/bin/aikido-npm.js +14 -0
  6. package/bin/aikido-npx.js +14 -0
  7. package/bin/aikido-pip.js +17 -0
  8. package/bin/aikido-pip3.js +17 -0
  9. package/bin/aikido-pipx.js +16 -0
  10. package/bin/aikido-pnpm.js +14 -0
  11. package/bin/aikido-pnpx.js +14 -0
  12. package/bin/aikido-poetry.js +13 -0
  13. package/bin/aikido-python.js +19 -0
  14. package/bin/aikido-python3.js +19 -0
  15. package/bin/aikido-uv.js +16 -0
  16. package/bin/aikido-uvx.js +16 -0
  17. package/bin/aikido-yarn.js +14 -0
  18. package/bin/safe-chain.js +147 -0
  19. package/docs/Release.md +25 -0
  20. package/docs/banner.svg +151 -0
  21. package/docs/safe-package-manager-demo.gif +0 -0
  22. package/docs/safe-package-manager-demo.png +0 -0
  23. package/docs/shell-integration.md +149 -0
  24. package/docs/troubleshooting.md +321 -0
  25. package/npm-shrinkwrap.json +3180 -0
  26. package/package.json +71 -0
  27. package/src/api/aikido.js +187 -0
  28. package/src/api/npmApi.js +71 -0
  29. package/src/config/cliArguments.js +161 -0
  30. package/src/config/configFile.js +327 -0
  31. package/src/config/environmentVariables.js +57 -0
  32. package/src/config/safeChainDir.js +71 -0
  33. package/src/config/settings.js +247 -0
  34. package/src/environment/environment.js +14 -0
  35. package/src/environment/userInteraction.js +122 -0
  36. package/src/installLocation.js +42 -0
  37. package/src/main.js +123 -0
  38. package/src/packagemanager/_shared/commandErrors.js +17 -0
  39. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  40. package/src/packagemanager/bun/createBunPackageManager.js +48 -0
  41. package/src/packagemanager/currentPackageManager.js +82 -0
  42. package/src/packagemanager/npm/createPackageManager.js +72 -0
  43. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  44. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  45. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  46. package/src/packagemanager/npm/runNpmCommand.js +20 -0
  47. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  48. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  49. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  50. package/src/packagemanager/npx/createPackageManager.js +15 -0
  51. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  52. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  53. package/src/packagemanager/npx/runNpxCommand.js +20 -0
  54. package/src/packagemanager/pip/createPackageManager.js +25 -0
  55. package/src/packagemanager/pip/pipSettings.js +6 -0
  56. package/src/packagemanager/pip/runPipCommand.js +209 -0
  57. package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
  58. package/src/packagemanager/pipx/runPipXCommand.js +60 -0
  59. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  60. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  61. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  62. package/src/packagemanager/pnpm/runPnpmCommand.js +32 -0
  63. package/src/packagemanager/poetry/createPoetryPackageManager.js +72 -0
  64. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  65. package/src/packagemanager/uv/runUvCommand.js +66 -0
  66. package/src/packagemanager/uvx/createUvxPackageManager.js +18 -0
  67. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  68. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  69. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  70. package/src/packagemanager/yarn/runYarnCommand.js +36 -0
  71. package/src/registryProxy/certBundle.js +203 -0
  72. package/src/registryProxy/certUtils.js +178 -0
  73. package/src/registryProxy/getConnectTimeout.js +13 -0
  74. package/src/registryProxy/http-utils.js +80 -0
  75. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  76. package/src/registryProxy/interceptors/interceptorBuilder.js +179 -0
  77. package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
  78. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +180 -0
  79. package/src/registryProxy/interceptors/npm/npmInterceptor.js +101 -0
  80. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +60 -0
  81. package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
  82. package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
  83. package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
  84. package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
  85. package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
  86. package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
  87. package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
  88. package/src/registryProxy/isImdsEndpoint.js +13 -0
  89. package/src/registryProxy/mitmRequestHandler.js +240 -0
  90. package/src/registryProxy/plainHttpProxy.js +95 -0
  91. package/src/registryProxy/registryProxy.js +255 -0
  92. package/src/registryProxy/tunnelRequestHandler.js +213 -0
  93. package/src/scanning/audit/index.js +129 -0
  94. package/src/scanning/index.js +82 -0
  95. package/src/scanning/malwareDatabase.js +131 -0
  96. package/src/scanning/newPackagesDatabaseBuilder.js +71 -0
  97. package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
  98. package/src/scanning/newPackagesListCache.js +126 -0
  99. package/src/scanning/packageNameVariants.js +29 -0
  100. package/src/shell-integration/helpers.js +296 -0
  101. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +37 -0
  102. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +25 -0
  103. package/src/shell-integration/setup-ci.js +152 -0
  104. package/src/shell-integration/setup.js +110 -0
  105. package/src/shell-integration/shellDetection.js +39 -0
  106. package/src/shell-integration/startup-scripts/init-fish.fish +122 -0
  107. package/src/shell-integration/startup-scripts/init-posix.sh +112 -0
  108. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +176 -0
  109. package/src/shell-integration/supported-shells/bash.js +222 -0
  110. package/src/shell-integration/supported-shells/fish.js +97 -0
  111. package/src/shell-integration/supported-shells/powershell.js +102 -0
  112. package/src/shell-integration/supported-shells/windowsPowershell.js +102 -0
  113. package/src/shell-integration/supported-shells/zsh.js +94 -0
  114. package/src/shell-integration/teardown.js +114 -0
  115. package/src/utils/safeSpawn.js +153 -0
  116. package/tsconfig.json +21 -0
@@ -0,0 +1,110 @@
1
+ import chalk from "chalk";
2
+ import { ui } from "../environment/userInteraction.js";
3
+ import { detectShells } from "./shellDetection.js";
4
+ import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
5
+ import { getScriptsDir, getStartupScriptSourcePath } from "../config/safeChainDir.js";
6
+ import fs from "fs";
7
+ import path from "path";
8
+
9
+ /**
10
+ * Loops over the detected shells and calls the setup function for each.
11
+ */
12
+ export async function setup() {
13
+ ui.writeInformation(
14
+ chalk.bold("Setting up shell aliases.") +
15
+ ` This will wrap safe-chain around ${getPackageManagerList()}.`,
16
+ );
17
+ ui.emptyLine();
18
+
19
+ copyStartupFiles();
20
+
21
+ try {
22
+ const shells = detectShells();
23
+ if (shells.length === 0) {
24
+ ui.writeError("No supported shells detected. Cannot set up aliases.");
25
+ return;
26
+ }
27
+
28
+ ui.writeInformation(
29
+ `Detected ${shells.length} supported shell(s): ${shells
30
+ .map((shell) => chalk.bold(shell.name))
31
+ .join(", ")}.`,
32
+ );
33
+
34
+ let updatedCount = 0;
35
+ for (const shell of shells) {
36
+ if (await setupShell(shell)) {
37
+ updatedCount++;
38
+ }
39
+ }
40
+
41
+ if (updatedCount > 0) {
42
+ ui.emptyLine();
43
+ ui.writeInformation(`Please restart your terminal to apply the changes.`);
44
+ }
45
+ } catch (/** @type {any} */ error) {
46
+ ui.writeError(
47
+ `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.`,
48
+ );
49
+ return;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Calls the setup function for the given shell and reports the result.
55
+ * @param {import("./shellDetection.js").Shell} shell
56
+ */
57
+ async function setupShell(shell) {
58
+ let success = false;
59
+ let error;
60
+ try {
61
+ shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases
62
+ success = await shell.setup(knownAikidoTools);
63
+ } catch (/** @type {any} */ err) {
64
+ success = false;
65
+ error = err;
66
+ }
67
+
68
+ if (success) {
69
+ ui.writeInformation(
70
+ `${chalk.bold("- " + shell.name + ":")} ${chalk.green(
71
+ "Setup successful",
72
+ )}`,
73
+ );
74
+ } else {
75
+ ui.writeError(
76
+ `${chalk.bold("- " + shell.name + ":")} ${chalk.red("Setup failed")}`,
77
+ );
78
+ if (error) {
79
+ let message = ` Error: ${error.message}`;
80
+ if (error.code) {
81
+ message += ` (code: ${error.code})`;
82
+ }
83
+ ui.writeError(message);
84
+ }
85
+ ui.emptyLine();
86
+ ui.writeInformation(` ${chalk.bold("To set up manually:")}`);
87
+ for (const instruction of shell.getManualSetupInstructions()) {
88
+ ui.writeInformation(` ${instruction}`);
89
+ }
90
+ ui.emptyLine();
91
+ }
92
+
93
+ return success;
94
+ }
95
+
96
+ function copyStartupFiles() {
97
+ const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"];
98
+ const targetDir = getScriptsDir();
99
+
100
+ for (const file of startupFiles) {
101
+ const targetPath = path.join(targetDir, file);
102
+
103
+ if (!fs.existsSync(targetDir)) {
104
+ fs.mkdirSync(targetDir, { recursive: true });
105
+ }
106
+
107
+ const sourcePath = getStartupScriptSourcePath(import.meta.url, file);
108
+ fs.copyFileSync(sourcePath, targetPath);
109
+ }
110
+ }
@@ -0,0 +1,39 @@
1
+ import zsh from "./supported-shells/zsh.js";
2
+ import bash from "./supported-shells/bash.js";
3
+ import powershell from "./supported-shells/powershell.js";
4
+ import windowsPowershell from "./supported-shells/windowsPowershell.js";
5
+ import fish from "./supported-shells/fish.js";
6
+ import { ui } from "../environment/userInteraction.js";
7
+
8
+ /**
9
+ * @typedef {Object} Shell
10
+ * @property {string} name
11
+ * @property {() => boolean} isInstalled
12
+ * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean|Promise<boolean>} setup
13
+ * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown
14
+ * @property {() => string[]} getManualSetupInstructions
15
+ * @property {() => string[]} getManualTeardownInstructions
16
+ */
17
+
18
+ /**
19
+ * @returns {Shell[]}
20
+ */
21
+ export function detectShells() {
22
+ let possibleShells = [zsh, bash, powershell, windowsPowershell, fish];
23
+ let availableShells = [];
24
+
25
+ try {
26
+ for (const shell of possibleShells) {
27
+ if (shell.isInstalled()) {
28
+ availableShells.push(shell);
29
+ }
30
+ }
31
+ } catch (/** @type {any} */ error) {
32
+ ui.writeError(
33
+ `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}`,
34
+ );
35
+ return [];
36
+ }
37
+
38
+ return availableShells;
39
+ }
@@ -0,0 +1,122 @@
1
+ set -l safe_chain_script (status filename)
2
+ set -l safe_chain_scripts_dir (dirname $safe_chain_script)
3
+ set -l safe_chain_base (dirname $safe_chain_scripts_dir)
4
+ set -gx PATH $PATH $safe_chain_base/bin
5
+
6
+ function npx
7
+ wrapSafeChainCommand "npx" $argv
8
+ end
9
+
10
+ function yarn
11
+ wrapSafeChainCommand "yarn" $argv
12
+ end
13
+
14
+ function pnpm
15
+ wrapSafeChainCommand "pnpm" $argv
16
+ end
17
+
18
+ function pnpx
19
+ wrapSafeChainCommand "pnpx" $argv
20
+ end
21
+
22
+ function bun
23
+ wrapSafeChainCommand "bun" $argv
24
+ end
25
+
26
+ function bunx
27
+ wrapSafeChainCommand "bunx" $argv
28
+ end
29
+
30
+ function npm
31
+ # If args is just -v or --version and nothing else, just run the `npm -v` command
32
+ # This is because nvm uses this to check the version of npm
33
+ set argc (count $argv)
34
+ if test $argc -eq 1
35
+ switch $argv[1]
36
+ case "-v" "--version"
37
+ command npm $argv
38
+ return
39
+ end
40
+ end
41
+
42
+ wrapSafeChainCommand "npm" $argv
43
+ end
44
+
45
+ function pip
46
+ wrapSafeChainCommand "pip" $argv
47
+ end
48
+
49
+ function pip3
50
+ wrapSafeChainCommand "pip3" $argv
51
+ end
52
+
53
+ function uv
54
+ wrapSafeChainCommand "uv" $argv
55
+ end
56
+
57
+ function uvx
58
+ wrapSafeChainCommand "uvx" $argv
59
+ end
60
+
61
+ function poetry
62
+ wrapSafeChainCommand "poetry" $argv
63
+ end
64
+
65
+ # `python -m pip`, `python -m pip3`.
66
+ function python
67
+ wrapSafeChainCommand "python" $argv
68
+ end
69
+
70
+ # `python3 -m pip`, `python3 -m pip3'.
71
+ function python3
72
+ wrapSafeChainCommand "python3" $argv
73
+ end
74
+
75
+ function pipx
76
+ wrapSafeChainCommand "pipx" $argv
77
+ end
78
+
79
+ function printSafeChainWarning
80
+ set original_cmd $argv[1]
81
+
82
+ # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:"
83
+ set_color -b yellow black
84
+ printf "Warning:"
85
+ set_color normal
86
+ printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd
87
+
88
+ # Cyan text for the install command
89
+ printf "Install safe-chain by using "
90
+ set_color cyan
91
+ printf "npm install -g @aikidosec/safe-chain"
92
+ set_color normal
93
+ printf ".\n"
94
+ end
95
+
96
+ function wrapSafeChainCommand
97
+ set original_cmd $argv[1]
98
+ set cmd_args $argv[2..-1]
99
+
100
+ if not type -fq $original_cmd
101
+ # If the original command is not available, don't try to wrap it: invoke
102
+ # it transparently, so the shell can report errors as if this wrapper
103
+ # didn't exist. fish always adds extra debug information when executing
104
+ # missing commands from within a function, so after the "command not
105
+ # found" handler, there will be information about how the
106
+ # wrapSafeChainCommand function errored out. To avoid users assuming this
107
+ # is a safe-chain bug, display an explicit error message afterwards.
108
+ command $original_cmd $cmd_args
109
+ set oldstatus $status
110
+ echo "safe-chain tried to run $original_cmd but it doesn't seem to be installed in your \$PATH." >&2
111
+ return $oldstatus
112
+ end
113
+
114
+ if type -q safe-chain
115
+ # If the safe-chain command is available, just run it with the provided arguments
116
+ safe-chain $original_cmd $cmd_args
117
+ else
118
+ # If the safe-chain command is not available, print a warning and run the original command
119
+ printSafeChainWarning $original_cmd
120
+ command $original_cmd $cmd_args
121
+ end
122
+ end
@@ -0,0 +1,112 @@
1
+ if [ -n "${BASH_SOURCE[0]:-}" ]; then
2
+ _sc_script_path="${BASH_SOURCE[0]}"
3
+ elif [ -n "${ZSH_VERSION:-}" ]; then
4
+ # ${(%):-%x} uses Zsh prompt expansion to get the sourced file's path.
5
+ # eval is required so other shells don't try to parse the Zsh-specific syntax.
6
+ eval '_sc_script_path="${(%):-%x}"'
7
+ else
8
+ _sc_script_path="$0"
9
+ fi
10
+ _sc_scripts_dir=$(CDPATH= cd -- "$(dirname -- "$_sc_script_path")" 2>/dev/null && pwd -P)
11
+ _sc_base=$(dirname -- "$_sc_scripts_dir")
12
+ export PATH="$PATH:${_sc_base}/bin"
13
+ unset _sc_base _sc_script_path _sc_scripts_dir
14
+
15
+ function npx() {
16
+ wrapSafeChainCommand "npx" "$@"
17
+ }
18
+
19
+ function yarn() {
20
+ wrapSafeChainCommand "yarn" "$@"
21
+ }
22
+
23
+ function pnpm() {
24
+ wrapSafeChainCommand "pnpm" "$@"
25
+ }
26
+
27
+ function pnpx() {
28
+ wrapSafeChainCommand "pnpx" "$@"
29
+ }
30
+
31
+ function bun() {
32
+ wrapSafeChainCommand "bun" "$@"
33
+ }
34
+
35
+ function bunx() {
36
+ wrapSafeChainCommand "bunx" "$@"
37
+ }
38
+
39
+ function npm() {
40
+ if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
41
+ # If args is just -v or --version and nothing else, just run the npm version command
42
+ # This is because nvm uses this to check the version of npm
43
+ command npm "$@"
44
+ return
45
+ fi
46
+
47
+ wrapSafeChainCommand "npm" "$@"
48
+ }
49
+
50
+ function pip() {
51
+ wrapSafeChainCommand "pip" "$@"
52
+ }
53
+
54
+ function pip3() {
55
+ wrapSafeChainCommand "pip3" "$@"
56
+ }
57
+
58
+ function uv() {
59
+ wrapSafeChainCommand "uv" "$@"
60
+ }
61
+
62
+ function uvx() {
63
+ wrapSafeChainCommand "uvx" "$@"
64
+ }
65
+
66
+ function poetry() {
67
+ wrapSafeChainCommand "poetry" "$@"
68
+ }
69
+
70
+ # `python -m pip`, `python -m pip3`.
71
+ function python() {
72
+ wrapSafeChainCommand "python" "$@"
73
+ }
74
+
75
+ # `python3 -m pip`, `python3 -m pip3'.
76
+ function python3() {
77
+ wrapSafeChainCommand "python3" "$@"
78
+ }
79
+
80
+ function pipx() {
81
+ wrapSafeChainCommand "pipx" "$@"
82
+ }
83
+
84
+ function printSafeChainWarning() {
85
+ # \033[43;30m is used to set the background color to yellow and text color to black
86
+ # \033[0m is used to reset the text formatting
87
+ printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1"
88
+ # \033[36m is used to set the text color to cyan
89
+ printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n"
90
+ }
91
+
92
+ function wrapSafeChainCommand() {
93
+ local original_cmd="$1"
94
+
95
+ if ! type -f "${original_cmd}" > /dev/null 2>&1; then
96
+ # If the original command is not available, don't try to wrap it: invoke it
97
+ # transparently, so the shell can report errors as if this wrapper didn't
98
+ # exist.
99
+ command "$@"
100
+ return $?
101
+ fi
102
+
103
+ if command -v safe-chain > /dev/null 2>&1; then
104
+ # If the aikido command is available, just run it with the provided arguments
105
+ safe-chain "$@"
106
+ else
107
+ # If the aikido command is not available, print a warning and run the original command
108
+ printSafeChainWarning "$original_cmd"
109
+
110
+ command "$@"
111
+ fi
112
+ }
@@ -0,0 +1,176 @@
1
+ # Use cross-platform path separator (: on Unix, ; on Windows)
2
+ # $IsWindows is only available in PowerShell Core 6.0+. If it doesn't exist, assume Windows PowerShell
3
+ $isWindowsPlatform = if (Test-Path variable:IsWindows) { $IsWindows } else { $true }
4
+ $pathSeparator = if ($isWindowsPlatform) { ';' } else { ':' }
5
+ $safeChainBase = Split-Path -Parent $PSScriptRoot
6
+ $safeChainBin = Join-Path $safeChainBase 'bin'
7
+ $env:PATH = "$env:PATH$pathSeparator$safeChainBin"
8
+
9
+ function npx {
10
+ Invoke-WrappedCommand "npx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
11
+ }
12
+
13
+ function yarn {
14
+ Invoke-WrappedCommand "yarn" $args $MyInvocation.Line $MyInvocation.OffsetInLine
15
+ }
16
+
17
+ function pnpm {
18
+ Invoke-WrappedCommand "pnpm" $args $MyInvocation.Line $MyInvocation.OffsetInLine
19
+ }
20
+
21
+ function pnpx {
22
+ Invoke-WrappedCommand "pnpx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
23
+ }
24
+
25
+ function bun {
26
+ Invoke-WrappedCommand "bun" $args $MyInvocation.Line $MyInvocation.OffsetInLine
27
+ }
28
+
29
+ function bunx {
30
+ Invoke-WrappedCommand "bunx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
31
+ }
32
+
33
+ function npm {
34
+ # If args is just -v or --version and nothing else, just run the npm version command
35
+ # This is because nvm uses this to check the version of npm
36
+ if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
37
+ Invoke-RealCommand "npm" $args
38
+ return
39
+ }
40
+
41
+ Invoke-WrappedCommand "npm" $args $MyInvocation.Line $MyInvocation.OffsetInLine
42
+ }
43
+
44
+ function pip {
45
+ Invoke-WrappedCommand "pip" $args $MyInvocation.Line $MyInvocation.OffsetInLine
46
+ }
47
+
48
+ function pip3 {
49
+ Invoke-WrappedCommand "pip3" $args $MyInvocation.Line $MyInvocation.OffsetInLine
50
+ }
51
+
52
+ function uv {
53
+ Invoke-WrappedCommand "uv" $args $MyInvocation.Line $MyInvocation.OffsetInLine
54
+ }
55
+
56
+ function uvx {
57
+ Invoke-WrappedCommand "uvx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
58
+ }
59
+
60
+ function poetry {
61
+ Invoke-WrappedCommand "poetry" $args $MyInvocation.Line $MyInvocation.OffsetInLine
62
+ }
63
+
64
+ # `python -m pip`, `python -m pip3`.
65
+ function python {
66
+ Invoke-WrappedCommand 'python' $args $MyInvocation.Line $MyInvocation.OffsetInLine
67
+ }
68
+
69
+ # `python3 -m pip`, `python3 -m pip3'.
70
+ function python3 {
71
+ Invoke-WrappedCommand 'python3' $args $MyInvocation.Line $MyInvocation.OffsetInLine
72
+ }
73
+
74
+ function pipx {
75
+ Invoke-WrappedCommand "pipx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
76
+ }
77
+
78
+ function Write-SafeChainWarning {
79
+ param([string]$Command)
80
+
81
+ # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:"
82
+ Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline
83
+ Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it."
84
+
85
+ # Cyan text for the install command
86
+ Write-Host "Install safe-chain by using " -NoNewline
87
+ Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline
88
+ Write-Host "."
89
+ }
90
+
91
+ function Test-CommandAvailable {
92
+ param([string]$Command)
93
+
94
+ try {
95
+ Get-Command $Command -ErrorAction Stop | Out-Null
96
+ return $true
97
+ }
98
+ catch {
99
+ return $false
100
+ }
101
+ }
102
+
103
+ function Invoke-RealCommand {
104
+ param(
105
+ [string]$Command,
106
+ [string[]]$Arguments
107
+ )
108
+
109
+ # Find the real executable to avoid calling our wrapped functions
110
+ $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1
111
+ if ($realCommand) {
112
+ & $realCommand.Source @Arguments
113
+ }
114
+ }
115
+
116
+ function Get-ReconstructedArguments {
117
+ param(
118
+ [string]$RawLine,
119
+ [int]$RawOffset
120
+ )
121
+
122
+ if (-not $RawLine) { return $null }
123
+
124
+ $tokens = [System.Management.Automation.PSParser]::Tokenize($RawLine, [ref]$null)
125
+ $newArgs = @()
126
+ $foundCommand = $false
127
+
128
+ foreach ($t in $tokens) {
129
+ if (-not $foundCommand) {
130
+ if ($t.Start -eq ($RawOffset - 1)) { $foundCommand = $true }
131
+ continue
132
+ }
133
+
134
+ if ($t.Type -eq 'Operator' -and $t.Content -match '[|;&]') { break }
135
+
136
+ # Stop if complex variable expansion is used
137
+ if ($t.Type -eq 'Variable' -or $t.Type -eq 'Group' -or $t.Type -eq 'SubExpression') {
138
+ return $null
139
+ }
140
+
141
+ $newArgs += $t.Content
142
+ }
143
+
144
+ if ($foundCommand) {
145
+ return ,$newArgs
146
+ }
147
+ return $null
148
+ }
149
+
150
+ function Invoke-WrappedCommand {
151
+ param(
152
+ [string]$OriginalCmd,
153
+ [string[]]$Arguments,
154
+ [string]$RawLine = $null,
155
+ [int]$RawOffset = 0
156
+ )
157
+
158
+ # Use raw line parsing to recover arguments like '--' that PowerShell consumes
159
+ if ($RawLine) {
160
+ $reconstructedArgs = Get-ReconstructedArguments $RawLine $RawOffset
161
+ if ($null -ne $reconstructedArgs) {
162
+ $Arguments = $reconstructedArgs
163
+ }
164
+ }
165
+
166
+ if ($isWindowsPlatform -and (Test-CommandAvailable "safe-chain.cmd")) {
167
+ & safe-chain.cmd $OriginalCmd @Arguments
168
+ }
169
+ elseif (Test-CommandAvailable "safe-chain") {
170
+ & safe-chain $OriginalCmd @Arguments
171
+ }
172
+ else {
173
+ Write-SafeChainWarning $OriginalCmd
174
+ Invoke-RealCommand $OriginalCmd $Arguments
175
+ }
176
+ }