@aikidosec/safe-chain 0.0.4-connect-timeout-beta

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 (94) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +257 -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 +20 -0
  8. package/bin/aikido-pip3.js +21 -0
  9. package/bin/aikido-pnpm.js +14 -0
  10. package/bin/aikido-pnpx.js +14 -0
  11. package/bin/aikido-python.js +30 -0
  12. package/bin/aikido-python3.js +30 -0
  13. package/bin/aikido-uv.js +16 -0
  14. package/bin/aikido-yarn.js +14 -0
  15. package/bin/safe-chain.js +190 -0
  16. package/docs/banner.svg +151 -0
  17. package/docs/npm-to-binary-migration.md +89 -0
  18. package/docs/safe-package-manager-demo.gif +0 -0
  19. package/docs/safe-package-manager-demo.png +0 -0
  20. package/docs/shell-integration.md +149 -0
  21. package/package.json +68 -0
  22. package/src/api/aikido.js +54 -0
  23. package/src/api/npmApi.js +71 -0
  24. package/src/config/cliArguments.js +138 -0
  25. package/src/config/configFile.js +192 -0
  26. package/src/config/environmentVariables.js +7 -0
  27. package/src/config/settings.js +100 -0
  28. package/src/environment/environment.js +14 -0
  29. package/src/environment/userInteraction.js +122 -0
  30. package/src/main.js +104 -0
  31. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  32. package/src/packagemanager/bun/createBunPackageManager.js +53 -0
  33. package/src/packagemanager/currentPackageManager.js +72 -0
  34. package/src/packagemanager/npm/createPackageManager.js +72 -0
  35. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  36. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  37. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  38. package/src/packagemanager/npm/runNpmCommand.js +25 -0
  39. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  40. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  41. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  42. package/src/packagemanager/npx/createPackageManager.js +15 -0
  43. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  44. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  45. package/src/packagemanager/npx/runNpxCommand.js +25 -0
  46. package/src/packagemanager/pip/createPackageManager.js +21 -0
  47. package/src/packagemanager/pip/pipSettings.js +30 -0
  48. package/src/packagemanager/pip/runPipCommand.js +175 -0
  49. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  50. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  51. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  52. package/src/packagemanager/pnpm/runPnpmCommand.js +36 -0
  53. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  54. package/src/packagemanager/uv/runUvCommand.js +71 -0
  55. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  56. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  57. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  58. package/src/packagemanager/yarn/runYarnCommand.js +41 -0
  59. package/src/registryProxy/certBundle.js +95 -0
  60. package/src/registryProxy/certUtils.js +128 -0
  61. package/src/registryProxy/http-utils.js +17 -0
  62. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  63. package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
  64. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
  65. package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
  66. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
  67. package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
  68. package/src/registryProxy/mitmRequestHandler.js +231 -0
  69. package/src/registryProxy/plainHttpProxy.js +95 -0
  70. package/src/registryProxy/registryProxy.js +184 -0
  71. package/src/registryProxy/tunnelRequestHandler.js +180 -0
  72. package/src/scanning/audit/index.js +129 -0
  73. package/src/scanning/index.js +82 -0
  74. package/src/scanning/malwareDatabase.js +131 -0
  75. package/src/shell-integration/helpers.js +213 -0
  76. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
  77. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
  78. package/src/shell-integration/setup-ci.js +170 -0
  79. package/src/shell-integration/setup.js +127 -0
  80. package/src/shell-integration/shellDetection.js +37 -0
  81. package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
  82. package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
  83. package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
  84. package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
  85. package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
  86. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
  87. package/src/shell-integration/supported-shells/bash.js +134 -0
  88. package/src/shell-integration/supported-shells/fish.js +77 -0
  89. package/src/shell-integration/supported-shells/powershell.js +73 -0
  90. package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
  91. package/src/shell-integration/supported-shells/zsh.js +74 -0
  92. package/src/shell-integration/teardown.js +64 -0
  93. package/src/utils/safeSpawn.js +137 -0
  94. package/tsconfig.json +21 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @param {string[]} args
3
+ *
4
+ * @returns {{name: string, version: string}[]}
5
+ */
6
+ export function parsePackagesFromArguments(args) {
7
+ let defaultTag = "latest";
8
+
9
+ for (let i = 0; i < args.length; i++) {
10
+ const arg = args[i];
11
+ const option = getOption(arg);
12
+
13
+ if (option) {
14
+ // If the option has a parameter, skip the next argument as well
15
+ i += option.numberOfParameters;
16
+
17
+ continue;
18
+ }
19
+
20
+ const packageDetails = parsePackagename(arg, defaultTag);
21
+ if (packageDetails) {
22
+ return [packageDetails];
23
+ }
24
+ }
25
+
26
+ return [];
27
+ }
28
+
29
+ /**
30
+ * @param {string} arg
31
+ * @returns {{name: string, numberOfParameters: number} | undefined}
32
+ */
33
+ function getOption(arg) {
34
+ if (isOptionWithParameter(arg)) {
35
+ return {
36
+ name: arg,
37
+ numberOfParameters: 1,
38
+ };
39
+ }
40
+
41
+ // Arguments starting with "-" or "--" are considered options
42
+ // except for "--package=" which contains the package name
43
+ if (arg.startsWith("-") && !arg.startsWith("--package=")) {
44
+ return {
45
+ name: arg,
46
+ numberOfParameters: 0,
47
+ };
48
+ }
49
+
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * @param {string} arg
55
+ * @returns {boolean}
56
+ */
57
+ function isOptionWithParameter(arg) {
58
+ const optionsWithParameters = [
59
+ "--access",
60
+ "--auth-type",
61
+ "--cache",
62
+ "--fetch-retries",
63
+ "--fetch-retry-mintimeout",
64
+ "--fetch-retry-maxtimeout",
65
+ "--fetch-retry-factor",
66
+ "--fetch-timeout",
67
+ "--https-proxy",
68
+ "--include",
69
+ "--location",
70
+ "--lockfile-version",
71
+ "--loglevel",
72
+ "--omit",
73
+ "--proxy",
74
+ "--registry",
75
+ "--replace-registry-host",
76
+ "--tag",
77
+ "--user-config",
78
+ "--workspace",
79
+ ];
80
+
81
+ return optionsWithParameters.includes(arg);
82
+ }
83
+
84
+ /**
85
+ * @param {string} arg
86
+ * @param {string} defaultTag
87
+ * @returns {{name: string, version: string}}
88
+ */
89
+ function parsePackagename(arg, defaultTag) {
90
+ // format can be --package=name@version
91
+ // in that case, we need to remove the --package= part
92
+ if (arg.startsWith("--package=")) {
93
+ arg = arg.slice(10);
94
+ }
95
+
96
+ arg = removeAlias(arg);
97
+
98
+ // Split at the last "@" to separate the package name and version
99
+ const lastAtIndex = arg.lastIndexOf("@");
100
+
101
+ let name, version;
102
+ // The index of the last "@" should be greater than 0
103
+ // If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
104
+ if (lastAtIndex > 0) {
105
+ name = arg.slice(0, lastAtIndex);
106
+ version = arg.slice(lastAtIndex + 1);
107
+ } else {
108
+ name = arg;
109
+ version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
110
+ }
111
+
112
+ return {
113
+ name,
114
+ version,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * @param {string} arg
120
+ * @returns {string}
121
+ */
122
+ function removeAlias(arg) {
123
+ // removes the alias.
124
+ // Eg.: server@npm:http-server@latest becomes http-server@latest
125
+ const aliasIndex = arg.indexOf("@npm:");
126
+ if (aliasIndex !== -1) {
127
+ return arg.slice(aliasIndex + 5);
128
+ }
129
+ return arg;
130
+ }
@@ -0,0 +1,25 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
4
+
5
+ /**
6
+ * @param {string[]} args
7
+ *
8
+ * @returns {Promise<{status: number}>}
9
+ */
10
+ export async function runNpx(args) {
11
+ try {
12
+ const result = await safeSpawn("npx", args, {
13
+ stdio: "inherit",
14
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
15
+ });
16
+ return { status: result.status };
17
+ } catch (/** @type any */ error) {
18
+ if (error.status) {
19
+ return { status: error.status };
20
+ } else {
21
+ ui.writeError("Error executing command:", error.message);
22
+ return { status: 1 };
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,21 @@
1
+ import { runPip } from "./runPipCommand.js";
2
+ import { getCurrentPipInvocation } from "./pipSettings.js";
3
+ /**
4
+ * @returns {import("../currentPackageManager.js").PackageManager}
5
+ */
6
+ export function createPipPackageManager() {
7
+ return {
8
+ /**
9
+ * @param {string[]} args
10
+ */
11
+ runCommand: (args) => {
12
+ const invocation = getCurrentPipInvocation();
13
+ const fullArgs = [...invocation.args, ...args];
14
+ return runPip(invocation.command, fullArgs);
15
+ },
16
+ // For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
17
+ isSupportedCommand: () => false,
18
+ getDependencyUpdatesForCommand: () => [],
19
+ };
20
+ }
21
+
@@ -0,0 +1,30 @@
1
+ export const PIP_PACKAGE_MANAGER = "pip";
2
+
3
+ // All supported python/pip invocations for Safe Chain interception
4
+ export const PIP_INVOCATIONS = {
5
+ PIP: { command: "pip", args: [] },
6
+ PIP3: { command: "pip3", args: [] },
7
+ PY_PIP: { command: "python", args: ["-m", "pip"] },
8
+ PY3_PIP: { command: "python3", args: ["-m", "pip"] },
9
+ PY_PIP3: { command: "python", args: ["-m", "pip3"] },
10
+ PY3_PIP3: { command: "python3", args: ["-m", "pip3"] }
11
+ };
12
+
13
+ /**
14
+ * @type {{ command: string, args: string[] }}
15
+ */
16
+ let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip
17
+
18
+ /**
19
+ * @param {{ command: string, args: string[] }} invocation
20
+ */
21
+ export function setCurrentPipInvocation(invocation) {
22
+ currentInvocation = invocation;
23
+ }
24
+
25
+ /**
26
+ * @returns {{ command: string, args: string[] }}
27
+ */
28
+ export function getCurrentPipInvocation() {
29
+ return currentInvocation;
30
+ }
@@ -0,0 +1,175 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
4
+ import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
5
+ import fs from "node:fs/promises";
6
+ import fsSync from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import ini from "ini";
10
+
11
+ /**
12
+ * Sets fallback CA bundle environment variables used by Python libraries.
13
+ * These are applied in addition to the PIP_CONFIG_FILE to ensure all Python
14
+ * network libraries respect the combined CA bundle, even if they don't read pip's config.
15
+ *
16
+ * @param {NodeJS.ProcessEnv} env - Environment object to modify
17
+ * @param {string} combinedCaPath - Path to the combined CA bundle
18
+ */
19
+ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
20
+ // REQUESTS_CA_BUNDLE: Used by the popular 'requests' library
21
+ if (env.REQUESTS_CA_BUNDLE) {
22
+ ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
23
+ }
24
+ env.REQUESTS_CA_BUNDLE = combinedCaPath;
25
+
26
+ // SSL_CERT_FILE: Used by some Python SSL libraries and urllib
27
+ if (env.SSL_CERT_FILE) {
28
+ ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
29
+ }
30
+ env.SSL_CERT_FILE = combinedCaPath;
31
+
32
+ // PIP_CERT: Pip's own environment variable for certificate verification
33
+ if (env.PIP_CERT) {
34
+ ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
35
+ }
36
+ env.PIP_CERT = combinedCaPath;
37
+ }
38
+
39
+ /**
40
+ * Runs a pip command with safe-chain's certificate bundle and proxy configuration.
41
+ *
42
+ * Creates a temporary pip config file to configure:
43
+ * - Cert bundle for HTTPS verification
44
+ * - Proxy settings
45
+ *
46
+ * If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges
47
+ * their settings with safe-chain's, leaving the original file unchanged.
48
+ *
49
+ * Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
50
+ * users to read/write persistent config. Only CA environment variables are set for these commands.
51
+ *
52
+ * @param {string} command - The pip command to execute (e.g., 'pip3')
53
+ * @param {string[]} args - Command line arguments to pass to pip
54
+ * @returns {Promise<{status: number}>} Exit status of the pip command
55
+ */
56
+ export async function runPip(command, args) {
57
+ try {
58
+ const env = mergeSafeChainProxyEnvironmentVariables(process.env);
59
+
60
+ // Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots)
61
+ // so that any network request made by pip, including those outside explicit CLI args,
62
+ // validates correctly under both MITM'd and tunneled HTTPS.
63
+ const combinedCaPath = getCombinedCaBundlePath();
64
+
65
+ // Commands that need access to persistent config/cache/state files
66
+ // These should not have PIP_CONFIG_FILE overridden as it would prevent them from
67
+ // reading/writing to the user's actual pip configuration and cache directories
68
+ const configRelatedCommands = ['config', 'cache', 'debug', 'completion'];
69
+ const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]);
70
+
71
+ // https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file)
72
+ // will tell pip to use the provided CA bundle for HTTPS verification.
73
+
74
+ // Proxy settings: GLOBAL_AGENT_HTTP_PROXY is our safe-chain proxy (if active),
75
+ // otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables
76
+ const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || '';
77
+
78
+ const tmpDir = os.tmpdir();
79
+ const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
80
+ let cleanupConfigPath = null; // Track temp file for cleanup
81
+
82
+ if (isConfigRelatedCommand) {
83
+ ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`);
84
+
85
+ // Still set the fallback CA bundle environment variables to avoid edge cases where a
86
+ // plugin or extension triggers a network call during config introspection
87
+ // This can do no harm
88
+ setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
89
+
90
+ const result = await safeSpawn(command, args, {
91
+ stdio: "inherit",
92
+ env,
93
+ });
94
+
95
+ return { status: result.status };
96
+ }
97
+
98
+ // Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order
99
+ if (!env.PIP_CONFIG_FILE) {
100
+ /** @type {{ global: { cert: string, proxy?: string } }} */
101
+ const configObj = { global: { cert: combinedCaPath } };
102
+ if (proxy) {
103
+ configObj.global.proxy = proxy;
104
+ }
105
+ const pipConfig = ini.stringify(configObj);
106
+ await fs.writeFile(pipConfigPath, pipConfig);
107
+ env.PIP_CONFIG_FILE = pipConfigPath;
108
+ cleanupConfigPath = pipConfigPath;
109
+
110
+ } else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) {
111
+ ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.");
112
+ const userConfig = env.PIP_CONFIG_FILE;
113
+
114
+ // Read the existing config without modifying it
115
+ let content = await fs.readFile(userConfig, "utf-8");
116
+ const parsed = ini.parse(content);
117
+
118
+ // Ensure [global] section exists
119
+ parsed.global = parsed.global || {};
120
+
121
+ // Cert
122
+ if (typeof parsed.global.cert !== "undefined") {
123
+ ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
124
+ }
125
+ parsed.global.cert = combinedCaPath;
126
+
127
+ // Proxy
128
+ if (typeof parsed.global.proxy !== "undefined") {
129
+ ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
130
+ }
131
+ if (proxy) {
132
+ parsed.global.proxy = proxy;
133
+ }
134
+
135
+ const updated = ini.stringify(parsed);
136
+
137
+ // Save to a new temp file to avoid overwriting user's original config
138
+ await fs.writeFile(pipConfigPath, updated, "utf-8");
139
+ env.PIP_CONFIG_FILE = pipConfigPath;
140
+ cleanupConfigPath = pipConfigPath;
141
+
142
+ } else {
143
+ // The user provided PIP_CONFIG_FILE does not exist on disk
144
+ // PIP will handle this as an error and inform the user
145
+ }
146
+
147
+ // Set fallback CA bundle environment variables for Python libraries that don't read pip config
148
+ setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
149
+
150
+ const result = await safeSpawn(command, args, {
151
+ stdio: "inherit",
152
+ env,
153
+ });
154
+
155
+ // Cleanup temporary config file if we created one
156
+ if (cleanupConfigPath) {
157
+ try {
158
+ await fs.unlink(cleanupConfigPath);
159
+ } catch {
160
+ // Ignore cleanup errors - the file may have already been deleted or is inaccessible
161
+ // Temp files in os.tmpdir() may eventually be cleaned by the OS, but timing varies by platform
162
+ }
163
+ }
164
+
165
+ return { status: result.status };
166
+ } catch (/** @type any */ error) {
167
+ if (error.status) {
168
+ return { status: error.status };
169
+ } else {
170
+ ui.writeError(`Error executing command: ${error.message}`);
171
+ ui.writeError(`Is '${command}' installed and available on your system?`);
172
+ return { status: 1 };
173
+ }
174
+ }
175
+ }
@@ -0,0 +1,57 @@
1
+ import { matchesCommand } from "../_shared/matchesCommand.js";
2
+ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
3
+ import { runPnpmCommand } from "./runPnpmCommand.js";
4
+
5
+ const scanner = commandArgumentScanner();
6
+
7
+ /**
8
+ * @returns {import("../currentPackageManager.js").PackageManager}
9
+ */
10
+ export function createPnpmPackageManager() {
11
+ return {
12
+ runCommand: (args) => runPnpmCommand(args, "pnpm"),
13
+ isSupportedCommand: (args) =>
14
+ matchesCommand(args, "add") ||
15
+ matchesCommand(args, "update") ||
16
+ matchesCommand(args, "upgrade") ||
17
+ matchesCommand(args, "up") ||
18
+ matchesCommand(args, "install") ||
19
+ matchesCommand(args, "i") ||
20
+ // dlx does not always come in the first position
21
+ // eg: pnpm --package=yo --package=generator-webapp dlx yo webapp
22
+ // documentation: https://pnpm.io/cli/dlx#--package-name
23
+ args.includes("dlx"),
24
+ getDependencyUpdatesForCommand: (args) =>
25
+ getDependencyUpdatesForCommand(args, false),
26
+ };
27
+ }
28
+
29
+ /**
30
+ * @returns {import("../currentPackageManager.js").PackageManager}
31
+ */
32
+ export function createPnpxPackageManager() {
33
+ return {
34
+ runCommand: (args) => runPnpmCommand(args, "pnpx"),
35
+ isSupportedCommand: () => true,
36
+ getDependencyUpdatesForCommand: (args) =>
37
+ getDependencyUpdatesForCommand(args, true),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * @param {string[]} args
43
+ * @param {boolean} isPnpx
44
+ * @returns {ReturnType<import("../currentPackageManager.js").PackageManager["getDependencyUpdatesForCommand"]>}
45
+ */
46
+ function getDependencyUpdatesForCommand(args, isPnpx) {
47
+ if (isPnpx) {
48
+ return scanner.scan(args);
49
+ }
50
+ if (args.includes("dlx")) {
51
+ // dlx is not always the first argument (eg: `pnpm --package=yo --package=generator-webapp dlx yo webapp`)
52
+ // so we need to filter it out instead of slicing the array
53
+ // documentation: https://pnpm.io/cli/dlx#--package-name
54
+ return scanner.scan(args.filter((arg) => arg !== "dlx"));
55
+ }
56
+ return scanner.scan(args.slice(1));
57
+ }
@@ -0,0 +1,35 @@
1
+ import { resolvePackageVersion } from "../../../api/npmApi.js";
2
+ import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
3
+
4
+ /**
5
+ * @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
6
+ */
7
+ export function commandArgumentScanner() {
8
+ return {
9
+ scan: (args) => scanDependencies(args),
10
+ shouldScan: () => true, // There's no dry run for pnpm, so we always scan
11
+ };
12
+ }
13
+
14
+ /**
15
+ * @param {string[]} args
16
+ * @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
17
+ */
18
+ async function scanDependencies(args) {
19
+ const changes = [];
20
+ const packageUpdates = parsePackagesFromArguments(args);
21
+
22
+ for (const packageUpdate of packageUpdates) {
23
+ var exactVersion = await resolvePackageVersion(
24
+ packageUpdate.name,
25
+ packageUpdate.version
26
+ );
27
+ if (exactVersion) {
28
+ packageUpdate.version = exactVersion;
29
+ }
30
+
31
+ changes.push({ ...packageUpdate, type: "add" });
32
+ }
33
+
34
+ return changes;
35
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @param {string[]} args
3
+ * @returns {{name: string, version: string}[]}
4
+ */
5
+ export function parsePackagesFromArguments(args) {
6
+ const changes = [];
7
+ let defaultTag = "latest";
8
+
9
+ for (let i = 0; i < args.length; i++) {
10
+ const arg = args[i];
11
+ const option = getOption(arg);
12
+
13
+ if (option) {
14
+ // If the option has a parameter, skip the next argument as well
15
+ i += option.numberOfParameters;
16
+
17
+ continue;
18
+ }
19
+
20
+ const packageDetails = parsePackagename(arg, defaultTag);
21
+ if (packageDetails) {
22
+ changes.push(packageDetails);
23
+ }
24
+ }
25
+
26
+ return changes;
27
+ }
28
+
29
+ /**
30
+ * @param {string} arg
31
+ * @returns {{name: string, numberOfParameters: number} | undefined}
32
+ */
33
+ function getOption(arg) {
34
+ if (isOptionWithParameter(arg)) {
35
+ return {
36
+ name: arg,
37
+ numberOfParameters: 1,
38
+ };
39
+ }
40
+
41
+ // Arguments starting with "-" or "--" are considered options
42
+ // except for "--package=" which contains the package name
43
+ if (arg.startsWith("-") && !arg.startsWith("--package=")) {
44
+ return {
45
+ name: arg,
46
+ numberOfParameters: 0,
47
+ };
48
+ }
49
+
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * @param {string} arg
55
+ * @returns {boolean}
56
+ */
57
+ function isOptionWithParameter(arg) {
58
+ const optionsWithParameters = ["--C", "--dir"];
59
+
60
+ return optionsWithParameters.includes(arg);
61
+ }
62
+
63
+ /**
64
+ * @param {string} arg
65
+ * @param {string} defaultTag
66
+ * @returns {{name: string, version: string}}
67
+ */
68
+ function parsePackagename(arg, defaultTag) {
69
+ // format can be --package=name@version
70
+ // in that case, we need to remove the --package= part
71
+ if (arg.startsWith("--package=")) {
72
+ arg = arg.slice(10);
73
+ }
74
+
75
+ arg = removeAlias(arg);
76
+
77
+ // Split at the last "@" to separate the package name and version
78
+ const lastAtIndex = arg.lastIndexOf("@");
79
+
80
+ let name, version;
81
+ // The index of the last "@" should be greater than 0
82
+ // If the index is 0, it means the package name starts with "@" (eg: "@aikidosec/package-name")
83
+ if (lastAtIndex > 0) {
84
+ name = arg.slice(0, lastAtIndex);
85
+ version = arg.slice(lastAtIndex + 1);
86
+ } else {
87
+ name = arg;
88
+ version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
89
+ }
90
+
91
+ return {
92
+ name,
93
+ version,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * @param {string} arg
99
+ * @returns {string}
100
+ */
101
+ function removeAlias(arg) {
102
+ // removes the alias.
103
+ // Eg.: server@npm:http-server@latest becomes http-server@latest
104
+ const aliasIndex = arg.indexOf("@npm:");
105
+ if (aliasIndex !== -1) {
106
+ return arg.slice(aliasIndex + 5);
107
+ }
108
+ return arg;
109
+ }
@@ -0,0 +1,36 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
3
+ import { safeSpawn } from "../../utils/safeSpawn.js";
4
+
5
+ /**
6
+ * @param {string[]} args
7
+ * @param {string} [toolName]
8
+ * @returns {Promise<{status: number}>}
9
+ */
10
+ export async function runPnpmCommand(args, toolName = "pnpm") {
11
+ try {
12
+ let result;
13
+ if (toolName === "pnpm") {
14
+ result = await safeSpawn("pnpm", args, {
15
+ stdio: "inherit",
16
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
17
+ });
18
+ } else if (toolName === "pnpx") {
19
+ result = await safeSpawn("pnpx", args, {
20
+ stdio: "inherit",
21
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
22
+ });
23
+ } else {
24
+ throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
25
+ }
26
+
27
+ return { status: result.status };
28
+ } catch (/** @type any */ error) {
29
+ if (error.status) {
30
+ return { status: error.status };
31
+ } else {
32
+ ui.writeError("Error executing command:", error.message);
33
+ return { status: 1 };
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,18 @@
1
+ import { runUv } from "./runUvCommand.js";
2
+
3
+ /**
4
+ * @returns {import("../currentPackageManager.js").PackageManager}
5
+ */
6
+ export function createUvPackageManager() {
7
+ return {
8
+ /**
9
+ * @param {string[]} args
10
+ */
11
+ runCommand: (args) => {
12
+ return runUv("uv", args);
13
+ },
14
+ // For uv, rely solely on MITM
15
+ isSupportedCommand: () => false,
16
+ getDependencyUpdatesForCommand: () => [],
17
+ };
18
+ }