@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,222 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ } from "../helpers.js";
6
+ import { getScriptsDir } from "../../config/safeChainDir.js";
7
+ import { execSync, spawnSync } from "child_process";
8
+ import * as os from "os";
9
+ import path from "path";
10
+
11
+ const shellName = "Bash";
12
+ const executableName = "bash";
13
+ const startupFileCommand = "echo ~/.bashrc";
14
+ const eol = "\n"; // When bash runs on Windows (e.g., Git Bash or WSL), it expects LF line endings.
15
+
16
+ function isInstalled() {
17
+ return doesExecutableExistOnSystem(executableName);
18
+ }
19
+
20
+ /**
21
+ * @param {import("../helpers.js").AikidoTool[]} tools
22
+ *
23
+ * @returns {boolean}
24
+ */
25
+ function teardown(tools) {
26
+ const startupFile = getStartupFile();
27
+
28
+ for (const { tool } of tools) {
29
+ // Remove any existing alias for the tool
30
+ removeLinesMatchingPattern(
31
+ startupFile,
32
+ new RegExp(`^alias\\s+${tool}=`),
33
+ eol
34
+ );
35
+ }
36
+
37
+ // Remove sourcing line to disable safe-chain shell integration
38
+ removeLinesMatchingPattern(
39
+ startupFile,
40
+ /^source\s+.*init-posix\.sh.*#\s*Safe-chain/,
41
+ eol
42
+ );
43
+
44
+ return true;
45
+ }
46
+
47
+ function setup() {
48
+ const startupFile = getStartupFile();
49
+ const scriptsDir = getShellScriptsDir();
50
+
51
+ addLineToFile(
52
+ startupFile,
53
+ `source ${path.posix.join(scriptsDir, "init-posix.sh")} # Safe-chain bash initialization script`,
54
+ eol
55
+ );
56
+
57
+ return true;
58
+ }
59
+
60
+ function getStartupFile() {
61
+ try {
62
+ var path = execSync(startupFileCommand, {
63
+ encoding: "utf8",
64
+ shell: executableName,
65
+ }).trim();
66
+
67
+ return windowsFixPath(path);
68
+ } catch (/** @type {any} */ error) {
69
+ throw new Error(
70
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
71
+ );
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @param {string} path
77
+ *
78
+ * @returns {string}
79
+ */
80
+ function windowsFixPath(path) {
81
+ try {
82
+ if (os.platform() !== "win32") {
83
+ return path;
84
+ }
85
+
86
+ // On windows cygwin bash, paths are returned in format /c/user/..., but we need it in format C:\user\...
87
+ // To convert, the cygpath -w command can be used to convert to the desired format.
88
+ // Cygpath only exists on Cygwin, so we first check if the command is available.
89
+ // If it is, we use it to convert the path.
90
+ if (hasCygpath()) {
91
+ return cygpathw(path);
92
+ }
93
+
94
+ return path;
95
+ } catch {
96
+ return path;
97
+ }
98
+ }
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
+
145
+ function hasCygpath() {
146
+ try {
147
+ var result = spawnSync("where", ["cygpath"], { shell: executableName });
148
+ return result.status === 0;
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * @param {string} path
156
+ *
157
+ * @returns {string}
158
+ */
159
+ function cygpathw(path) {
160
+ try {
161
+ var result = spawnSync("cygpath", ["-w", path], {
162
+ encoding: "utf8",
163
+ shell: executableName,
164
+ });
165
+ if (result.status === 0) {
166
+ return result.stdout.trim();
167
+ }
168
+ return path;
169
+ } catch {
170
+ return path;
171
+ }
172
+ }
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
+
194
+ function getManualTeardownInstructions() {
195
+ const scriptsDir = getShellScriptsDir();
196
+ return [
197
+ `Remove the following line from your ~/.bashrc file:`,
198
+ ` source ${path.posix.join(scriptsDir, "init-posix.sh")}`,
199
+ `Then restart your terminal or run: source ~/.bashrc`,
200
+ ];
201
+ }
202
+
203
+ function getManualSetupInstructions() {
204
+ const scriptsDir = getShellScriptsDir();
205
+ return [
206
+ `Add the following line to your ~/.bashrc file:`,
207
+ ` source ${path.posix.join(scriptsDir, "init-posix.sh")}`,
208
+ `Then restart your terminal or run: source ~/.bashrc`,
209
+ ];
210
+ }
211
+
212
+ /**
213
+ * @type {import("../shellDetection.js").Shell}
214
+ */
215
+ export default {
216
+ name: shellName,
217
+ isInstalled,
218
+ setup,
219
+ teardown,
220
+ getManualSetupInstructions,
221
+ getManualTeardownInstructions,
222
+ };
@@ -0,0 +1,97 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ } from "../helpers.js";
6
+ import { getScriptsDir } from "../../config/safeChainDir.js";
7
+ import { execSync } from "child_process";
8
+ import path from "path";
9
+
10
+ const shellName = "Fish";
11
+ const executableName = "fish";
12
+ const startupFileCommand = "echo ~/.config/fish/config.fish";
13
+ const eol = "\n"; // When fish runs on Windows (e.g., Git Bash or WSL), it expects LF line endings.
14
+
15
+ function isInstalled() {
16
+ return doesExecutableExistOnSystem(executableName);
17
+ }
18
+
19
+ /**
20
+ * @param {import("../helpers.js").AikidoTool[]} tools
21
+ *
22
+ * @returns {boolean}
23
+ */
24
+ function teardown(tools) {
25
+ const startupFile = getStartupFile();
26
+
27
+ for (const { tool } of tools) {
28
+ // Remove any existing alias for the tool
29
+ removeLinesMatchingPattern(
30
+ startupFile,
31
+ new RegExp(`^alias\\s+${tool}\\s+`),
32
+ eol
33
+ );
34
+ }
35
+
36
+ // Remove sourcing line to prevent safe-chain initialization in future shell sessions
37
+ removeLinesMatchingPattern(
38
+ startupFile,
39
+ /^source\s+.*init-fish\.fish.*#\s*Safe-chain/,
40
+ eol
41
+ );
42
+
43
+ return true;
44
+ }
45
+
46
+ function setup() {
47
+ const startupFile = getStartupFile();
48
+
49
+ addLineToFile(
50
+ startupFile,
51
+ `source ${path.join(getScriptsDir(), "init-fish.fish")} # Safe-chain Fish initialization script`,
52
+ eol
53
+ );
54
+
55
+ return true;
56
+ }
57
+
58
+ function getStartupFile() {
59
+ try {
60
+ return execSync(startupFileCommand, {
61
+ encoding: "utf8",
62
+ shell: executableName,
63
+ }).trim();
64
+ } catch (/** @type {any} */ error) {
65
+ throw new Error(
66
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
67
+ );
68
+ }
69
+ }
70
+
71
+ function getManualTeardownInstructions() {
72
+ return [
73
+ `Remove the following line from your ~/.config/fish/config.fish file:`,
74
+ ` source ${path.join(getScriptsDir(), "init-fish.fish")}`,
75
+ `Then restart your terminal or run: source ~/.config/fish/config.fish`,
76
+ ];
77
+ }
78
+
79
+ function getManualSetupInstructions() {
80
+ return [
81
+ `Add the following line to your ~/.config/fish/config.fish file:`,
82
+ ` source ${path.join(getScriptsDir(), "init-fish.fish")}`,
83
+ `Then restart your terminal or run: source ~/.config/fish/config.fish`,
84
+ ];
85
+ }
86
+
87
+ /**
88
+ * @type {import("../shellDetection.js").Shell}
89
+ */
90
+ export default {
91
+ name: shellName,
92
+ isInstalled,
93
+ setup,
94
+ teardown,
95
+ getManualSetupInstructions,
96
+ getManualTeardownInstructions,
97
+ };
@@ -0,0 +1,102 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ validatePowerShellExecutionPolicy,
6
+ } from "../helpers.js";
7
+ import { getScriptsDir } from "../../config/safeChainDir.js";
8
+ import { execSync } from "child_process";
9
+ import path from "path";
10
+
11
+ const shellName = "PowerShell Core";
12
+ const executableName = "pwsh";
13
+ const startupFileCommand = "echo $PROFILE";
14
+
15
+ function isInstalled() {
16
+ return doesExecutableExistOnSystem(executableName);
17
+ }
18
+
19
+ /**
20
+ * @param {import("../helpers.js").AikidoTool[]} tools
21
+ *
22
+ * @returns {boolean}
23
+ */
24
+ function teardown(tools) {
25
+ const startupFile = getStartupFile();
26
+
27
+ for (const { tool } of tools) {
28
+ // Remove any existing alias for the tool
29
+ removeLinesMatchingPattern(
30
+ startupFile,
31
+ new RegExp(`^Set-Alias\\s+${tool}\\s+`),
32
+ );
33
+ }
34
+
35
+ // Remove sourcing line to prevent shell from loading safe-chain after uninstallation
36
+ removeLinesMatchingPattern(
37
+ startupFile,
38
+ /^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/,
39
+ );
40
+
41
+ return true;
42
+ }
43
+
44
+ async function setup() {
45
+ const { isValid, policy } =
46
+ await validatePowerShellExecutionPolicy(executableName);
47
+ if (!isValid) {
48
+ throw new Error(
49
+ `PowerShell execution policy is set to '${policy}', which prevents safe-chain from running.\n -> To fix this, open PowerShell as Administrator and run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned.\n For more information, see: https://help.aikido.dev/code-scanning/aikido-malware-scanning/safe-chain-troubleshooting#powershell-execution-policy-blocks-scripts-windows`,
50
+ );
51
+ }
52
+
53
+ const startupFile = getStartupFile();
54
+
55
+ addLineToFile(
56
+ startupFile,
57
+ `. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`,
58
+ );
59
+
60
+ return true;
61
+ }
62
+
63
+ function getStartupFile() {
64
+ try {
65
+ return execSync(startupFileCommand, {
66
+ encoding: "utf8",
67
+ shell: executableName,
68
+ }).trim();
69
+ } catch (/** @type {any} */ error) {
70
+ throw new Error(
71
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`,
72
+ );
73
+ }
74
+ }
75
+
76
+ function getManualTeardownInstructions() {
77
+ return [
78
+ `Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
79
+ ` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`,
80
+ `Then restart your terminal or run: . $PROFILE`,
81
+ ];
82
+ }
83
+
84
+ function getManualSetupInstructions() {
85
+ return [
86
+ `Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
87
+ ` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`,
88
+ `Then restart your terminal or run: . $PROFILE`,
89
+ ];
90
+ }
91
+
92
+ /**
93
+ * @type {import("../shellDetection.js").Shell}
94
+ */
95
+ export default {
96
+ name: shellName,
97
+ isInstalled,
98
+ setup,
99
+ teardown,
100
+ getManualSetupInstructions,
101
+ getManualTeardownInstructions,
102
+ };
@@ -0,0 +1,102 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ validatePowerShellExecutionPolicy,
6
+ } from "../helpers.js";
7
+ import { getScriptsDir } from "../../config/safeChainDir.js";
8
+ import { execSync } from "child_process";
9
+ import path from "path";
10
+
11
+ const shellName = "Windows PowerShell";
12
+ const executableName = "powershell";
13
+ const startupFileCommand = "echo $PROFILE";
14
+
15
+ function isInstalled() {
16
+ return doesExecutableExistOnSystem(executableName);
17
+ }
18
+
19
+ /**
20
+ * @param {import("../helpers.js").AikidoTool[]} tools
21
+ *
22
+ * @returns {boolean}
23
+ */
24
+ function teardown(tools) {
25
+ const startupFile = getStartupFile();
26
+
27
+ for (const { tool } of tools) {
28
+ // Remove any existing alias for the tool
29
+ removeLinesMatchingPattern(
30
+ startupFile,
31
+ new RegExp(`^Set-Alias\\s+${tool}\\s+`),
32
+ );
33
+ }
34
+
35
+ // Remove sourcing line to clean up safe-chain integration from the shell profile
36
+ removeLinesMatchingPattern(
37
+ startupFile,
38
+ /^\.\s+["']?.*init-pwsh\.ps1["']?.*#\s*Safe-chain/,
39
+ );
40
+
41
+ return true;
42
+ }
43
+
44
+ async function setup() {
45
+ const { isValid, policy } =
46
+ await validatePowerShellExecutionPolicy(executableName);
47
+ if (!isValid) {
48
+ throw new Error(
49
+ `PowerShell execution policy is set to '${policy}', which prevents safe-chain from running.\n -> To fix this, open PowerShell as Administrator and run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned.\n For more information, see: https://help.aikido.dev/code-scanning/aikido-malware-scanning/safe-chain-troubleshooting#powershell-execution-policy-blocks-scripts-windows`,
50
+ );
51
+ }
52
+
53
+ const startupFile = getStartupFile();
54
+
55
+ addLineToFile(
56
+ startupFile,
57
+ `. "${path.join(getScriptsDir(), "init-pwsh.ps1")}" # Safe-chain PowerShell initialization script`,
58
+ );
59
+
60
+ return true;
61
+ }
62
+
63
+ function getStartupFile() {
64
+ try {
65
+ return execSync(startupFileCommand, {
66
+ encoding: "utf8",
67
+ shell: executableName,
68
+ }).trim();
69
+ } catch (/** @type {any} */ error) {
70
+ throw new Error(
71
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`,
72
+ );
73
+ }
74
+ }
75
+
76
+ function getManualTeardownInstructions() {
77
+ return [
78
+ `Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
79
+ ` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`,
80
+ `Then restart your terminal or run: . $PROFILE`,
81
+ ];
82
+ }
83
+
84
+ function getManualSetupInstructions() {
85
+ return [
86
+ `Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
87
+ ` . "${path.join(getScriptsDir(), "init-pwsh.ps1")}"`,
88
+ `Then restart your terminal or run: . $PROFILE`,
89
+ ];
90
+ }
91
+
92
+ /**
93
+ * @type {import("../shellDetection.js").Shell}
94
+ */
95
+ export default {
96
+ name: shellName,
97
+ isInstalled,
98
+ setup,
99
+ teardown,
100
+ getManualSetupInstructions,
101
+ getManualTeardownInstructions,
102
+ };
@@ -0,0 +1,94 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ } from "../helpers.js";
6
+ import { getScriptsDir } from "../../config/safeChainDir.js";
7
+ import { execSync } from "child_process";
8
+ import path from "path";
9
+
10
+ const shellName = "Zsh";
11
+ const executableName = "zsh";
12
+ const startupFileCommand = "echo ${ZDOTDIR:-$HOME}/.zshrc";
13
+ const eol = "\n"; // When zsh runs on Windows (e.g., Git Bash or WSL), it expects LF line endings.
14
+
15
+ function isInstalled() {
16
+ return doesExecutableExistOnSystem(executableName);
17
+ }
18
+
19
+ /**
20
+ * @param {import("../helpers.js").AikidoTool[]} tools
21
+ *
22
+ * @returns {boolean}
23
+ */
24
+ function teardown(tools) {
25
+ const startupFile = getStartupFile();
26
+
27
+ for (const { tool } of tools) {
28
+ // Remove any existing alias for the tool
29
+ removeLinesMatchingPattern(
30
+ startupFile,
31
+ new RegExp(`^alias\\s+${tool}=`),
32
+ eol
33
+ );
34
+ }
35
+
36
+ // Remove sourcing line to complete shell integration cleanup
37
+ removeLinesMatchingPattern(
38
+ startupFile,
39
+ /^source\s+.*init-posix\.sh.*#\s*Safe-chain/,
40
+ eol
41
+ );
42
+
43
+ return true;
44
+ }
45
+
46
+ function setup() {
47
+ const startupFile = getStartupFile();
48
+
49
+ addLineToFile(
50
+ startupFile,
51
+ `source ${path.join(getScriptsDir(), "init-posix.sh")} # Safe-chain Zsh initialization script`,
52
+ eol
53
+ );
54
+
55
+ return true;
56
+ }
57
+
58
+ function getStartupFile() {
59
+ try {
60
+ return execSync(startupFileCommand, {
61
+ encoding: "utf8",
62
+ shell: executableName,
63
+ }).trim();
64
+ } catch (/** @type {any} */ error) {
65
+ throw new Error(
66
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
67
+ );
68
+ }
69
+ }
70
+
71
+ function getManualTeardownInstructions() {
72
+ return [
73
+ `Remove the following line from your ~/.zshrc file:`,
74
+ ` source ${path.join(getScriptsDir(), "init-posix.sh")}`,
75
+ `Then restart your terminal or run: source ~/.zshrc`,
76
+ ];
77
+ }
78
+
79
+ function getManualSetupInstructions() {
80
+ return [
81
+ `Add the following line to your ~/.zshrc file:`,
82
+ ` source ${path.join(getScriptsDir(), "init-posix.sh")}`,
83
+ `Then restart your terminal or run: source ~/.zshrc`,
84
+ ];
85
+ }
86
+
87
+ export default {
88
+ name: shellName,
89
+ isInstalled,
90
+ setup,
91
+ teardown,
92
+ getManualSetupInstructions,
93
+ getManualTeardownInstructions,
94
+ };