@aikidosec/safe-chain 1.4.8 → 1.5.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.
Files changed (30) hide show
  1. package/README.md +25 -5
  2. package/bin/aikido-uvx.js +16 -0
  3. package/bin/safe-chain.js +18 -1
  4. package/docs/Release.md +25 -0
  5. package/docs/shell-integration.md +4 -4
  6. package/npm-shrinkwrap.json +3180 -0
  7. package/package.json +3 -4
  8. package/src/config/configFile.js +2 -2
  9. package/src/config/safeChainDir.js +71 -0
  10. package/src/installLocation.js +42 -0
  11. package/src/packagemanager/currentPackageManager.js +3 -0
  12. package/src/packagemanager/uvx/createUvxPackageManager.js +18 -0
  13. package/src/registryProxy/certUtils.js +3 -3
  14. package/src/registryProxy/interceptors/pip/modifyPipInfo.js +17 -0
  15. package/src/registryProxy/interceptors/pip/pipInterceptor.js +2 -0
  16. package/src/shell-integration/helpers.js +6 -14
  17. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +16 -1
  18. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +3 -2
  19. package/src/shell-integration/setup-ci.js +9 -29
  20. package/src/shell-integration/setup.js +3 -22
  21. package/src/shell-integration/startup-scripts/init-fish.fish +8 -1
  22. package/src/shell-integration/startup-scripts/init-posix.sh +17 -1
  23. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +6 -1
  24. package/src/shell-integration/supported-shells/bash.js +75 -5
  25. package/src/shell-integration/supported-shells/fish.js +7 -5
  26. package/src/shell-integration/supported-shells/powershell.js +7 -5
  27. package/src/shell-integration/supported-shells/windowsPowershell.js +7 -5
  28. package/src/shell-integration/supported-shells/zsh.js +7 -5
  29. package/src/shell-integration/teardown.js +3 -1
  30. package/src/ultimate/ultimateTroubleshooting.js +0 -111
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.4.8",
3
+ "version": "1.5.0",
4
4
  "scripts": {
5
5
  "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
6
6
  "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
@@ -16,6 +16,7 @@
16
16
  "aikido-bun": "bin/aikido-bun.js",
17
17
  "aikido-bunx": "bin/aikido-bunx.js",
18
18
  "aikido-uv": "bin/aikido-uv.js",
19
+ "aikido-uvx": "bin/aikido-uvx.js",
19
20
  "aikido-pip": "bin/aikido-pip.js",
20
21
  "aikido-pip3": "bin/aikido-pip3.js",
21
22
  "aikido-python": "bin/aikido-python.js",
@@ -36,9 +37,8 @@
36
37
  "keywords": [],
37
38
  "author": "Aikido Security",
38
39
  "license": "AGPL-3.0-or-later",
39
- "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, or pip/pip3 from downloading or running the malware.",
40
+ "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, uvx, or pip/pip3 from downloading or running the malware.",
40
41
  "dependencies": {
41
- "archiver": "^7.0.1",
42
42
  "certifi": "14.5.15",
43
43
  "chalk": "5.4.1",
44
44
  "https-proxy-agent": "7.0.6",
@@ -49,7 +49,6 @@
49
49
  "semver": "7.7.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@types/archiver": "^7.0.0",
53
52
  "@types/ini": "^4.1.1",
54
53
  "@types/make-fetch-happen": "^10.0.4",
55
54
  "@types/node": "^18.19.130",
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import os from "os";
4
4
  import { ui } from "../environment/userInteraction.js";
5
5
  import { getEcoSystem } from "./settings.js";
6
+ import { getSafeChainBaseDir } from "./safeChainDir.js";
6
7
 
7
8
  /**
8
9
  * @typedef {Object} SafeChainConfig
@@ -304,8 +305,7 @@ function getConfigFilePath() {
304
305
  * @returns {string}
305
306
  */
306
307
  export function getSafeChainDirectory() {
307
- const homeDir = os.homedir();
308
- const safeChainDir = path.join(homeDir, ".safe-chain");
308
+ const safeChainDir = getSafeChainBaseDir();
309
309
 
310
310
  if (!fs.existsSync(safeChainDir)) {
311
311
  fs.mkdirSync(safeChainDir, { recursive: true });
@@ -0,0 +1,71 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { getInstalledSafeChainDir } from "../installLocation.js";
5
+
6
+ /**
7
+ * @returns {string}
8
+ */
9
+ export function getSafeChainBaseDir() {
10
+ return getInstalledSafeChainDir() ?? path.join(os.homedir(), ".safe-chain");
11
+ }
12
+
13
+ /**
14
+ * @returns {string}
15
+ */
16
+ export function getBinDir() {
17
+ return path.join(getSafeChainBaseDir(), "bin");
18
+ }
19
+
20
+ /**
21
+ * @returns {string}
22
+ */
23
+ export function getShimsDir() {
24
+ return path.join(getSafeChainBaseDir(), "shims");
25
+ }
26
+
27
+ /**
28
+ * @returns {string}
29
+ */
30
+ export function getScriptsDir() {
31
+ return path.join(getSafeChainBaseDir(), "scripts");
32
+ }
33
+
34
+ /**
35
+ * @returns {string}
36
+ */
37
+ export function getCertsDir() {
38
+ return path.join(getSafeChainBaseDir(), "certs");
39
+ }
40
+
41
+ /**
42
+ * Resolves the directory of the calling module.
43
+ * Falls back to __dirname when import.meta.url is unavailable (pkg CJS binary).
44
+ * @param {string | undefined} moduleUrl
45
+ * @returns {string}
46
+ */
47
+ function resolveModuleDir(moduleUrl) {
48
+ if (moduleUrl) {
49
+ return path.dirname(fileURLToPath(moduleUrl));
50
+ }
51
+ // eslint-disable-next-line no-undef
52
+ return __dirname;
53
+ }
54
+
55
+ /**
56
+ * @param {string | undefined} moduleUrl
57
+ * @param {string} fileName
58
+ * @returns {string}
59
+ */
60
+ export function getStartupScriptSourcePath(moduleUrl, fileName) {
61
+ return path.join(resolveModuleDir(moduleUrl), "startup-scripts", fileName);
62
+ }
63
+
64
+ /**
65
+ * @param {string | undefined} moduleUrl
66
+ * @param {string} fileName
67
+ * @returns {string}
68
+ */
69
+ export function getPathWrapperTemplatePath(moduleUrl, fileName) {
70
+ return path.join(resolveModuleDir(moduleUrl), "path-wrappers", "templates", fileName);
71
+ }
@@ -0,0 +1,42 @@
1
+ import path from "path";
2
+
3
+ /** @type {NodeJS.Process & { pkg?: unknown }} */
4
+ const processWithPkg = process;
5
+
6
+ /**
7
+ * @param {string} executablePath
8
+ * @returns {string | undefined}
9
+ */
10
+ export function deriveInstallDirFromExecutablePath(executablePath) {
11
+ if (!executablePath) {
12
+ return undefined;
13
+ }
14
+
15
+ const pathLibrary = executablePath.includes("\\") ? path.win32 : path.posix;
16
+ const executableDir = pathLibrary.dirname(executablePath);
17
+ if (pathLibrary.basename(executableDir) !== "bin") {
18
+ return undefined;
19
+ }
20
+
21
+ return pathLibrary.dirname(executableDir);
22
+ }
23
+
24
+ /**
25
+ * Returns the install directory for a packaged safe-chain binary.
26
+ * Custom installation directories only apply to packaged binary installs.
27
+ * For npm/global/dev-script executions this intentionally returns undefined,
28
+ * which causes callers to fall back to the default ~/.safe-chain layout.
29
+ *
30
+ * @param {{ isPackaged?: boolean, executablePath?: string }} [options]
31
+ * @returns {string | undefined}
32
+ */
33
+ export function getInstalledSafeChainDir(options = {}) {
34
+ const isPackaged = options.isPackaged ?? Boolean(processWithPkg.pkg);
35
+ if (!isPackaged) {
36
+ return undefined;
37
+ }
38
+
39
+ return deriveInstallDirFromExecutablePath(
40
+ options.executablePath ?? process.execPath,
41
+ );
42
+ }
@@ -13,6 +13,7 @@ import { createPipPackageManager } from "./pip/createPackageManager.js";
13
13
  import { createUvPackageManager } from "./uv/createUvPackageManager.js";
14
14
  import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
15
15
  import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
16
+ import { createUvxPackageManager } from "./uvx/createUvxPackageManager.js";
16
17
 
17
18
  /**
18
19
  * @type {{packageManagerName: PackageManager | null}}
@@ -60,6 +61,8 @@ export function initializePackageManager(packageManagerName, context) {
60
61
  state.packageManagerName = createPipPackageManager(context);
61
62
  } else if (packageManagerName === "uv") {
62
63
  state.packageManagerName = createUvPackageManager();
64
+ } else if (packageManagerName === "uvx") {
65
+ state.packageManagerName = createUvxPackageManager();
63
66
  } else if (packageManagerName === "poetry") {
64
67
  state.packageManagerName = createPoetryPackageManager();
65
68
  } else if (packageManagerName === "pipx") {
@@ -0,0 +1,18 @@
1
+ import { runUv } from "../uv/runUvCommand.js";
2
+
3
+ /**
4
+ * @returns {import("../currentPackageManager.js").PackageManager}
5
+ */
6
+ export function createUvxPackageManager() {
7
+ return {
8
+ /**
9
+ * @param {string[]} args
10
+ */
11
+ runCommand: (args) => {
12
+ return runUv("uvx", args);
13
+ },
14
+ // For uvx, rely solely on MITM
15
+ isSupportedCommand: () => false,
16
+ getDependencyUpdatesForCommand: () => [],
17
+ };
18
+ }
@@ -1,9 +1,8 @@
1
1
  import forge from "node-forge";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
- import os from "os";
4
+ import { getCertsDir } from "../config/safeChainDir.js";
5
5
 
6
- const certFolder = path.join(os.homedir(), ".safe-chain", "certs");
7
6
  const ca = loadCa();
8
7
 
9
8
  const certCache = new Map();
@@ -20,7 +19,7 @@ function createKeyIdentifier(publicKey) {
20
19
  }
21
20
 
22
21
  export function getCaCertPath() {
23
- return path.join(certFolder, "ca-cert.pem");
22
+ return path.join(getCertsDir(), "ca-cert.pem");
24
23
  }
25
24
 
26
25
  /**
@@ -112,6 +111,7 @@ export function generateCertForHost(hostname) {
112
111
  }
113
112
 
114
113
  function loadCa() {
114
+ const certFolder = getCertsDir();
115
115
  const keyPath = path.join(certFolder, "ca-key.pem");
116
116
  const certPath = path.join(certFolder, "ca-cert.pem");
117
117
 
@@ -6,6 +6,23 @@ export { parsePipMetadataUrl, isPipPackageInfoUrl } from "./parsePipPackageUrl.j
6
6
  import { getPipMetadataContentType, logSuppressedVersion } from "./pipMetadataResponseUtils.js";
7
7
  import { modifyPipJsonResponse } from "./modifyPipJsonResponse.js";
8
8
 
9
+ /**
10
+ * Strip conditional GET headers so PyPI always returns a full 200 response
11
+ * with a body we can rewrite. Without this, pip sends If-None-Match /
12
+ * If-Modified-Since, PyPI responds 304 Not Modified (empty body), and
13
+ * safe-chain cannot rewrite it — leaving pip with a cached index that still
14
+ * lists too-young versions. Those versions are then blocked at direct-download
15
+ * time with a hard 403, preventing dependency resolution from completing.
16
+ *
17
+ * @param {NodeJS.Dict<string | string[]>} headers
18
+ * @returns {NodeJS.Dict<string | string[]>}
19
+ */
20
+ export function modifyPipInfoRequestHeaders(headers) {
21
+ delete headers["if-none-match"];
22
+ delete headers["if-modified-since"];
23
+ return headers;
24
+ }
25
+
9
26
  // Match simple-index anchor tags and capture their href so we can suppress
10
27
  // individual distribution links from PyPI HTML metadata responses.
11
28
  const HTML_ANCHOR_HREF_RE =
@@ -9,6 +9,7 @@ import { openNewPackagesDatabase } from "../../../scanning/newPackagesListCache.
9
9
  import { interceptRequests } from "../interceptorBuilder.js";
10
10
  import { isExcludedFromMinimumPackageAge } from "../minimumPackageAgeExclusions.js";
11
11
  import {
12
+ modifyPipInfoRequestHeaders,
12
13
  modifyPipInfoResponse,
13
14
  parsePipMetadataUrl,
14
15
  } from "./modifyPipInfo.js";
@@ -61,6 +62,7 @@ function createPipRequestHandler(registry) {
61
62
  !isExcludedFromMinimumPackageAge(metadataPackageName)
62
63
  ) {
63
64
  const newPackagesDatabase = await openNewPackagesDatabase();
65
+ reqContext.modifyRequestHeaders(modifyPipInfoRequestHeaders);
64
66
  reqContext.modifyBody((body, headers) =>
65
67
  modifyPipInfoResponse(
66
68
  body,
@@ -66,6 +66,12 @@ export const knownAikidoTools = [
66
66
  ecoSystem: ECOSYSTEM_PY,
67
67
  internalPackageManagerName: "uv",
68
68
  },
69
+ {
70
+ tool: "uvx",
71
+ aikidoCommand: "aikido-uvx",
72
+ ecoSystem: ECOSYSTEM_PY,
73
+ internalPackageManagerName: "uvx",
74
+ },
69
75
  {
70
76
  tool: "pip",
71
77
  aikidoCommand: "aikido-pip",
@@ -121,20 +127,6 @@ export function getPackageManagerList() {
121
127
  return `${tools.join(", ")}, and ${lastTool} commands`;
122
128
  }
123
129
 
124
- /**
125
- * @returns {string}
126
- */
127
- export function getShimsDir() {
128
- return path.join(os.homedir(), ".safe-chain", "shims");
129
- }
130
-
131
- /**
132
- * @returns {string}
133
- */
134
- export function getScriptsDir() {
135
- return path.join(os.homedir(), ".safe-chain", "scripts");
136
- }
137
-
138
130
  /**
139
131
  * @param {string} executableName
140
132
  *
@@ -4,13 +4,28 @@
4
4
 
5
5
  # Function to remove shim from PATH (POSIX-compliant)
6
6
  remove_shim_from_path() {
7
- echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g"
7
+ _safe_chain_phys=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P)
8
+ if [ -z "$_safe_chain_phys" ]; then
9
+ echo "$PATH"
10
+ return
11
+ fi
12
+ _path=$(echo "$PATH" | sed "s|${_safe_chain_phys}:||g")
13
+ # Also remove via dirname of $0 directly — on macOS /tmp is a symlink to /private/tmp,
14
+ # so pwd -P resolves to /private/tmp/… but PATH may still contain /tmp/….
15
+ _dir=$(dirname -- "$0")
16
+ case "$_dir" in
17
+ /*) [ "$_dir" != "$_safe_chain_phys" ] && _path=$(echo "$_path" | sed "s|${_dir}:||g") ;;
18
+ esac
19
+ echo "$_path"
8
20
  }
9
21
 
10
22
  if command -v safe-chain >/dev/null 2>&1; then
11
23
  # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
12
24
  PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
13
25
  else
26
+ # safe-chain is not reachable — warn the user so they know protection is inactive
27
+ printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. {{PACKAGE_MANAGER}} will run without it.\n" >&2
28
+
14
29
  # Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
15
30
  original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
16
31
  if [ -n "$original_cmd" ]; then
@@ -3,7 +3,8 @@ REM Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain
3
3
  REM This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
4
4
 
5
5
  REM Remove shim directory from PATH to prevent infinite loops
6
- set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims"
6
+ set "SHIM_DIR=%~dp0"
7
+ if "%SHIM_DIR:~-1%"=="\" set "SHIM_DIR=%SHIM_DIR:~0,-1%"
7
8
  call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
8
9
 
9
10
  REM Check if aikido command is available with clean PATH
@@ -21,4 +22,4 @@ if %errorlevel%==0 (
21
22
  REM If we get here, original command was not found
22
23
  echo Error: Could not find original {{PACKAGE_MANAGER}} >&2
23
24
  exit /b 1
24
- )
25
+ )
@@ -1,24 +1,14 @@
1
1
  import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
- import { getPackageManagerList, knownAikidoTools, getShimsDir } from "./helpers.js";
3
+ import { getPackageManagerList, knownAikidoTools } from "./helpers.js";
4
+ import {
5
+ getShimsDir,
6
+ getBinDir,
7
+ getPathWrapperTemplatePath,
8
+ } from "../config/safeChainDir.js";
4
9
  import fs from "fs";
5
10
  import os from "os";
6
11
  import path from "path";
7
- import { fileURLToPath } from "url";
8
-
9
- /** @type {string} */
10
- // This checks the current file's dirname in a way that's compatible with:
11
- // - Modulejs (import.meta.url)
12
- // - ES modules (__dirname)
13
- // This is needed because safe-chain's npm package is built using ES modules,
14
- // but building the binaries requires commonjs.
15
- let dirname;
16
- if (import.meta.url) {
17
- const filename = fileURLToPath(import.meta.url);
18
- dirname = path.dirname(filename);
19
- } else {
20
- dirname = __dirname;
21
- }
22
12
 
23
13
  /**
24
14
  * Loops over the detected shells and calls the setup function for each.
@@ -31,7 +21,7 @@ export async function setupCi() {
31
21
  ui.emptyLine();
32
22
 
33
23
  const shimsDir = getShimsDir();
34
- const binDir = path.join(os.homedir(), ".safe-chain", "bin");
24
+ const binDir = getBinDir();
35
25
  // Create the shims directory if it doesn't exist
36
26
  if (!fs.existsSync(shimsDir)) {
37
27
  fs.mkdirSync(shimsDir, { recursive: true });
@@ -50,12 +40,7 @@ export async function setupCi() {
50
40
  */
51
41
  function createUnixShims(shimsDir) {
52
42
  // Read the template file
53
- const templatePath = path.resolve(
54
- dirname,
55
- "path-wrappers",
56
- "templates",
57
- "unix-wrapper.template.sh"
58
- );
43
+ const templatePath = getPathWrapperTemplatePath(import.meta.url, "unix-wrapper.template.sh");
59
44
 
60
45
  if (!fs.existsSync(templatePath)) {
61
46
  ui.writeError(`Template file not found: ${templatePath}`);
@@ -89,12 +74,7 @@ function createUnixShims(shimsDir) {
89
74
  */
90
75
  function createWindowsShims(shimsDir) {
91
76
  // Read the template file
92
- const templatePath = path.resolve(
93
- dirname,
94
- "path-wrappers",
95
- "templates",
96
- "windows-wrapper.template.cmd"
97
- );
77
+ const templatePath = getPathWrapperTemplatePath(import.meta.url, "windows-wrapper.template.cmd");
98
78
 
99
79
  if (!fs.existsSync(templatePath)) {
100
80
  ui.writeError(`Windows template file not found: ${templatePath}`);
@@ -1,28 +1,10 @@
1
1
  import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
3
  import { detectShells } from "./shellDetection.js";
4
- import {
5
- knownAikidoTools,
6
- getPackageManagerList,
7
- getScriptsDir,
8
- } from "./helpers.js";
4
+ import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
5
+ import { getScriptsDir, getStartupScriptSourcePath } from "../config/safeChainDir.js";
9
6
  import fs from "fs";
10
7
  import path from "path";
11
- import { fileURLToPath } from "url";
12
-
13
- /** @type {string} */
14
- // This checks the current file's dirname in a way that's compatible with:
15
- // - Modulejs (import.meta.url)
16
- // - ES modules (__dirname)
17
- // This is needed because safe-chain's npm package is built using ES modules,
18
- // but building the binaries requires commonjs.
19
- let dirname;
20
- if (import.meta.url) {
21
- const filename = fileURLToPath(import.meta.url);
22
- dirname = path.dirname(filename);
23
- } else {
24
- dirname = __dirname;
25
- }
26
8
 
27
9
  /**
28
10
  * Loops over the detected shells and calls the setup function for each.
@@ -122,8 +104,7 @@ function copyStartupFiles() {
122
104
  fs.mkdirSync(targetDir, { recursive: true });
123
105
  }
124
106
 
125
- // Use absolute path for source
126
- const sourcePath = path.join(dirname, "startup-scripts", file);
107
+ const sourcePath = getStartupScriptSourcePath(import.meta.url, file);
127
108
  fs.copyFileSync(sourcePath, targetPath);
128
109
  }
129
110
  }
@@ -1,4 +1,7 @@
1
- set -gx PATH $PATH $HOME/.safe-chain/bin
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
2
5
 
3
6
  function npx
4
7
  wrapSafeChainCommand "npx" $argv
@@ -51,6 +54,10 @@ function uv
51
54
  wrapSafeChainCommand "uv" $argv
52
55
  end
53
56
 
57
+ function uvx
58
+ wrapSafeChainCommand "uvx" $argv
59
+ end
60
+
54
61
  function poetry
55
62
  wrapSafeChainCommand "poetry" $argv
56
63
  end
@@ -1,4 +1,16 @@
1
- export PATH="$PATH:$HOME/.safe-chain/bin"
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
2
14
 
3
15
  function npx() {
4
16
  wrapSafeChainCommand "npx" "$@"
@@ -47,6 +59,10 @@ function uv() {
47
59
  wrapSafeChainCommand "uv" "$@"
48
60
  }
49
61
 
62
+ function uvx() {
63
+ wrapSafeChainCommand "uvx" "$@"
64
+ }
65
+
50
66
  function poetry() {
51
67
  wrapSafeChainCommand "poetry" "$@"
52
68
  }
@@ -2,7 +2,8 @@
2
2
  # $IsWindows is only available in PowerShell Core 6.0+. If it doesn't exist, assume Windows PowerShell
3
3
  $isWindowsPlatform = if (Test-Path variable:IsWindows) { $IsWindows } else { $true }
4
4
  $pathSeparator = if ($isWindowsPlatform) { ';' } else { ':' }
5
- $safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin'
5
+ $safeChainBase = Split-Path -Parent $PSScriptRoot
6
+ $safeChainBin = Join-Path $safeChainBase 'bin'
6
7
  $env:PATH = "$env:PATH$pathSeparator$safeChainBin"
7
8
 
8
9
  function npx {
@@ -52,6 +53,10 @@ function uv {
52
53
  Invoke-WrappedCommand "uv" $args $MyInvocation.Line $MyInvocation.OffsetInLine
53
54
  }
54
55
 
56
+ function uvx {
57
+ Invoke-WrappedCommand "uvx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
58
+ }
59
+
55
60
  function poetry {
56
61
  Invoke-WrappedCommand "poetry" $args $MyInvocation.Line $MyInvocation.OffsetInLine
57
62
  }
@@ -3,8 +3,10 @@ import {
3
3
  doesExecutableExistOnSystem,
4
4
  removeLinesMatchingPattern,
5
5
  } from "../helpers.js";
6
+ import { getScriptsDir } from "../../config/safeChainDir.js";
6
7
  import { execSync, spawnSync } from "child_process";
7
8
  import * as os from "os";
9
+ import path from "path";
8
10
 
9
11
  const shellName = "Bash";
10
12
  const executableName = "bash";
@@ -32,10 +34,10 @@ function teardown(tools) {
32
34
  );
33
35
  }
34
36
 
35
- // Removes the line that sources the safe-chain bash initialization script (~/.safe-chain/scripts/init-posix.sh)
37
+ // Remove sourcing line to disable safe-chain shell integration
36
38
  removeLinesMatchingPattern(
37
39
  startupFile,
38
- /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,
40
+ /^source\s+.*init-posix\.sh.*#\s*Safe-chain/,
39
41
  eol
40
42
  );
41
43
 
@@ -44,10 +46,11 @@ function teardown(tools) {
44
46
 
45
47
  function setup() {
46
48
  const startupFile = getStartupFile();
49
+ const scriptsDir = getShellScriptsDir();
47
50
 
48
51
  addLineToFile(
49
52
  startupFile,
50
- `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script`,
53
+ `source ${path.posix.join(scriptsDir, "init-posix.sh")} # Safe-chain bash initialization script`,
51
54
  eol
52
55
  );
53
56
 
@@ -94,6 +97,51 @@ function windowsFixPath(path) {
94
97
  }
95
98
  }
96
99
 
100
+ function getShellScriptsDir() {
101
+ return toBashPath(getScriptsDir());
102
+ }
103
+
104
+ /**
105
+ * @param {string} path
106
+ *
107
+ * @returns {string}
108
+ */
109
+ function toBashPath(path) {
110
+ try {
111
+ if (os.platform() !== "win32") {
112
+ return path.replace(/\\/g, "/");
113
+ }
114
+
115
+ const directWindowsPath = windowsPathToBashPath(path);
116
+ if (directWindowsPath) {
117
+ return directWindowsPath;
118
+ }
119
+
120
+ if (hasCygpath()) {
121
+ return convertCygwinPathToUnix(path);
122
+ }
123
+
124
+ return path.replace(/\\/g, "/");
125
+ } catch {
126
+ return path.replace(/\\/g, "/");
127
+ }
128
+ }
129
+
130
+ /**
131
+ * @param {string} path
132
+ *
133
+ * @returns {string | undefined}
134
+ */
135
+ function windowsPathToBashPath(path) {
136
+ const match = /^([A-Za-z]):[\\/](.*)$/.exec(path);
137
+ if (!match) {
138
+ return undefined;
139
+ }
140
+
141
+ const [, driveLetter, rest] = match;
142
+ return `/${driveLetter.toLowerCase()}/${rest.replace(/\\/g, "/")}`;
143
+ }
144
+
97
145
  function hasCygpath() {
98
146
  try {
99
147
  var result = spawnSync("where", ["cygpath"], { shell: executableName });
@@ -123,18 +171,40 @@ function cygpathw(path) {
123
171
  }
124
172
  }
125
173
 
174
+ /**
175
+ * @param {string} path
176
+ *
177
+ * @returns {string}
178
+ */
179
+ function convertCygwinPathToUnix(path) {
180
+ try {
181
+ var result = spawnSync("cygpath", ["-u", path], {
182
+ encoding: "utf8",
183
+ shell: executableName,
184
+ });
185
+ if (result.status === 0) {
186
+ return result.stdout.trim();
187
+ }
188
+ return path.replace(/\\/g, "/");
189
+ } catch {
190
+ return path.replace(/\\/g, "/");
191
+ }
192
+ }
193
+
126
194
  function getManualTeardownInstructions() {
195
+ const scriptsDir = getShellScriptsDir();
127
196
  return [
128
197
  `Remove the following line from your ~/.bashrc file:`,
129
- ` source ~/.safe-chain/scripts/init-posix.sh`,
198
+ ` source ${path.posix.join(scriptsDir, "init-posix.sh")}`,
130
199
  `Then restart your terminal or run: source ~/.bashrc`,
131
200
  ];
132
201
  }
133
202
 
134
203
  function getManualSetupInstructions() {
204
+ const scriptsDir = getShellScriptsDir();
135
205
  return [
136
206
  `Add the following line to your ~/.bashrc file:`,
137
- ` source ~/.safe-chain/scripts/init-posix.sh`,
207
+ ` source ${path.posix.join(scriptsDir, "init-posix.sh")}`,
138
208
  `Then restart your terminal or run: source ~/.bashrc`,
139
209
  ];
140
210
  }