@aikidosec/safe-chain 1.4.9 → 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.
- package/README.md +25 -5
- package/bin/aikido-uvx.js +16 -0
- package/bin/safe-chain.js +18 -1
- package/docs/Release.md +25 -0
- package/docs/shell-integration.md +4 -4
- package/npm-shrinkwrap.json +54 -943
- package/package.json +3 -4
- package/src/config/configFile.js +2 -2
- package/src/config/safeChainDir.js +71 -0
- package/src/installLocation.js +42 -0
- package/src/packagemanager/currentPackageManager.js +3 -0
- package/src/packagemanager/uvx/createUvxPackageManager.js +18 -0
- package/src/registryProxy/certUtils.js +3 -3
- package/src/registryProxy/interceptors/pip/modifyPipInfo.js +17 -0
- package/src/registryProxy/interceptors/pip/pipInterceptor.js +2 -0
- package/src/shell-integration/helpers.js +6 -14
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +16 -1
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +3 -2
- package/src/shell-integration/setup-ci.js +9 -29
- package/src/shell-integration/setup.js +3 -22
- package/src/shell-integration/startup-scripts/init-fish.fish +8 -1
- package/src/shell-integration/startup-scripts/init-posix.sh +17 -1
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +6 -1
- package/src/shell-integration/supported-shells/bash.js +75 -5
- package/src/shell-integration/supported-shells/fish.js +7 -5
- package/src/shell-integration/supported-shells/powershell.js +7 -5
- package/src/shell-integration/supported-shells/windowsPowershell.js +7 -5
- package/src/shell-integration/supported-shells/zsh.js +7 -5
- package/src/shell-integration/teardown.js +3 -1
- 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.
|
|
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",
|
package/src/config/configFile.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|
-
$
|
|
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
|
-
//
|
|
37
|
+
// Remove sourcing line to disable safe-chain shell integration
|
|
36
38
|
removeLinesMatchingPattern(
|
|
37
39
|
startupFile,
|
|
38
|
-
/^source\s
|
|
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
|
|
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
|
|
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
|
|
207
|
+
` source ${path.posix.join(scriptsDir, "init-posix.sh")}`,
|
|
138
208
|
`Then restart your terminal or run: source ~/.bashrc`,
|
|
139
209
|
];
|
|
140
210
|
}
|