@aikidosec/safe-chain 1.4.9 → 1.5.1

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 (32) hide show
  1. package/README.md +34 -6
  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 +53 -942
  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/scanning/malwareDatabase.js +41 -38
  17. package/src/scanning/newPackagesListCache.js +16 -19
  18. package/src/shell-integration/helpers.js +6 -14
  19. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +16 -1
  20. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +3 -2
  21. package/src/shell-integration/setup-ci.js +9 -29
  22. package/src/shell-integration/setup.js +3 -22
  23. package/src/shell-integration/startup-scripts/init-fish.fish +8 -1
  24. package/src/shell-integration/startup-scripts/init-posix.sh +17 -1
  25. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +6 -1
  26. package/src/shell-integration/supported-shells/bash.js +75 -5
  27. package/src/shell-integration/supported-shells/fish.js +7 -5
  28. package/src/shell-integration/supported-shells/powershell.js +7 -5
  29. package/src/shell-integration/supported-shells/windowsPowershell.js +7 -5
  30. package/src/shell-integration/supported-shells/zsh.js +7 -5
  31. package/src/shell-integration/teardown.js +3 -1
  32. 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.9",
3
+ "version": "1.5.1",
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,
@@ -15,8 +15,12 @@ import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
15
15
  * @property {function(string, string): boolean} isMalware
16
16
  */
17
17
 
18
- /** @type {MalwareDatabase | null} */
19
- let cachedMalwareDatabase = null;
18
+ // Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
19
+ // value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
20
+ // control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
21
+ // concurrent callers see it immediately and share a single fetch.
22
+ /** @type {Promise<MalwareDatabase> | null} */
23
+ let cachedMalwareDatabasePromise = null;
20
24
 
21
25
  /**
22
26
  * Normalize package name for comparison.
@@ -34,45 +38,44 @@ function normalizePackageName(name) {
34
38
  return name;
35
39
  }
36
40
 
37
- export async function openMalwareDatabase() {
38
- if (cachedMalwareDatabase) {
39
- return cachedMalwareDatabase;
40
- }
41
-
42
- const malwareDatabase = await getMalwareDatabase();
43
-
44
- /**
45
- * @param {string} name
46
- * @param {string} version
47
- * @returns {string}
48
- */
49
- function getPackageStatus(name, version) {
50
- const normalizedName = normalizePackageName(name);
51
- const packageData = malwareDatabase.find(
52
- (pkg) => {
53
- const normalizedPkgName = normalizePackageName(pkg.package_name);
54
- return normalizedPkgName === normalizedName &&
55
- (pkg.version === version || pkg.version === "*");
41
+ export function openMalwareDatabase() {
42
+ if (!cachedMalwareDatabasePromise) {
43
+ cachedMalwareDatabasePromise = getMalwareDatabase().then((malwareDatabase) => {
44
+ /**
45
+ * @param {string} name
46
+ * @param {string} version
47
+ * @returns {string}
48
+ */
49
+ function getPackageStatus(name, version) {
50
+ const normalizedName = normalizePackageName(name);
51
+ const packageData = malwareDatabase.find(
52
+ (pkg) => {
53
+ const normalizedPkgName = normalizePackageName(pkg.package_name);
54
+ return normalizedPkgName === normalizedName &&
55
+ (pkg.version === version || pkg.version === "*");
56
+ }
57
+ );
58
+
59
+ if (!packageData) {
60
+ return MALWARE_STATUS_OK;
61
+ }
62
+
63
+ return packageData.reason;
56
64
  }
57
- );
58
65
 
59
- if (!packageData) {
60
- return MALWARE_STATUS_OK;
61
- }
62
-
63
- return packageData.reason;
66
+ return {
67
+ getPackageStatus,
68
+ isMalware: (/** @type {string} */ name, /** @type {string} */ version) => {
69
+ const status = getPackageStatus(name, version);
70
+ return isMalwareStatus(status);
71
+ },
72
+ };
73
+ }).catch((error) => {
74
+ cachedMalwareDatabasePromise = null;
75
+ throw error;
76
+ });
64
77
  }
65
-
66
- // This implicitly caches the malware database
67
- // that's closed over by the getPackageStatus function
68
- cachedMalwareDatabase = {
69
- getPackageStatus,
70
- isMalware: (name, version) => {
71
- const status = getPackageStatus(name, version);
72
- return isMalwareStatus(status);
73
- },
74
- };
75
- return cachedMalwareDatabase;
78
+ return cachedMalwareDatabasePromise;
76
79
  }
77
80
 
78
81
  /**
@@ -16,30 +16,27 @@ import { warnOnceAboutUnavailableDatabase } from "./newPackagesDatabaseWarnings.
16
16
  */
17
17
 
18
18
  // Shared per-process cache to avoid rebuilding the same feed-backed database on each request.
19
- /** @type {NewPackagesDatabase | null} */
20
- let cachedNewPackagesDatabase = null;
19
+ // Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
20
+ // value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
21
+ // control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
22
+ // concurrent callers see it immediately and share a single fetch.
23
+ /** @type {Promise<NewPackagesDatabase> | null} */
24
+ let cachedNewPackagesDatabasePromise = null;
21
25
 
22
26
  /**
23
27
  * @returns {Promise<NewPackagesDatabase>}
24
28
  */
25
- export async function openNewPackagesDatabase() {
26
- if (cachedNewPackagesDatabase) {
27
- return cachedNewPackagesDatabase;
29
+ export function openNewPackagesDatabase() {
30
+ if (!cachedNewPackagesDatabasePromise) {
31
+ cachedNewPackagesDatabasePromise = getNewPackagesList()
32
+ .then((newPackagesList) => buildNewPackagesDatabase(newPackagesList))
33
+ .catch((/** @type {any} */ error) => {
34
+ warnOnceAboutUnavailableDatabase(error);
35
+ cachedNewPackagesDatabasePromise = null;
36
+ return { isNewlyReleasedPackage: () => false };
37
+ });
28
38
  }
29
-
30
- /** @type {import("../api/aikido.js").NewPackageEntry[]} */
31
- let newPackagesList;
32
-
33
- try {
34
- newPackagesList = await getNewPackagesList();
35
- } catch (/** @type {any} */ error) {
36
- warnOnceAboutUnavailableDatabase(error);
37
- cachedNewPackagesDatabase = { isNewlyReleasedPackage: () => false };
38
- return cachedNewPackagesDatabase;
39
- }
40
-
41
- cachedNewPackagesDatabase = buildNewPackagesDatabase(newPackagesList);
42
- return cachedNewPackagesDatabase;
39
+ return cachedNewPackagesDatabasePromise;
43
40
  }
44
41
 
45
42
  /**
@@ -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
  }