@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,122 @@
1
+ // oxlint-disable no-console
2
+ import chalk from "chalk";
3
+ import { isCi } from "./environment.js";
4
+ import {
5
+ getLoggingLevel,
6
+ LOGGING_SILENT,
7
+ LOGGING_VERBOSE,
8
+ } from "../config/settings.js";
9
+
10
+ /**
11
+ * @type {{ bufferOutput: boolean, bufferedMessages:(() => void)[]}}
12
+ */
13
+ const state = {
14
+ bufferOutput: false,
15
+ bufferedMessages: [],
16
+ };
17
+
18
+ function isSilentMode() {
19
+ return getLoggingLevel() === LOGGING_SILENT;
20
+ }
21
+
22
+ function isVerboseMode() {
23
+ return getLoggingLevel() === LOGGING_VERBOSE;
24
+ }
25
+
26
+ function emptyLine() {
27
+ if (isSilentMode()) return;
28
+
29
+ writeInformation("");
30
+ }
31
+
32
+ /**
33
+ * @param {string} message
34
+ * @param {...any} optionalParams
35
+ * @returns {void}
36
+ */
37
+ function writeInformation(message, ...optionalParams) {
38
+ if (isSilentMode()) return;
39
+
40
+ writeOrBuffer(() => console.log(message, ...optionalParams));
41
+ }
42
+
43
+ /**
44
+ * @param {string} message
45
+ * @param {...any} optionalParams
46
+ * @returns {void}
47
+ */
48
+ function writeWarning(message, ...optionalParams) {
49
+ if (isSilentMode()) return;
50
+
51
+ if (!isCi()) {
52
+ message = chalk.yellow(message);
53
+ }
54
+ writeOrBuffer(() => console.warn(message, ...optionalParams));
55
+ }
56
+
57
+ /**
58
+ * @param {string} message
59
+ * @param {...any} optionalParams
60
+ * @returns {void}
61
+ */
62
+ function writeError(message, ...optionalParams) {
63
+ if (!isCi()) {
64
+ message = chalk.red(message);
65
+ }
66
+ writeOrBuffer(() => console.error(message, ...optionalParams));
67
+ }
68
+
69
+ function writeExitWithoutInstallingMaliciousPackages() {
70
+ let message = "Safe-chain: Exiting without installing malicious packages.";
71
+ if (!isCi()) {
72
+ message = chalk.red(message);
73
+ }
74
+ writeOrBuffer(() => console.error(message));
75
+ }
76
+
77
+ /**
78
+ * @param {string} message
79
+ * @param {...any} optionalParams
80
+ * @returns {void}
81
+ */
82
+ function writeVerbose(message, ...optionalParams) {
83
+ if (!isVerboseMode()) return;
84
+
85
+ writeOrBuffer(() => console.log(message, ...optionalParams));
86
+ }
87
+
88
+ /**
89
+ *
90
+ * @param {() => void} messageFunction
91
+ */
92
+ function writeOrBuffer(messageFunction) {
93
+ if (state.bufferOutput) {
94
+ state.bufferedMessages.push(messageFunction);
95
+ } else {
96
+ messageFunction();
97
+ }
98
+ }
99
+
100
+ function startBufferingLogs() {
101
+ state.bufferOutput = true;
102
+ state.bufferedMessages = [];
103
+ }
104
+
105
+ function writeBufferedLogsAndStopBuffering() {
106
+ state.bufferOutput = false;
107
+ for (const log of state.bufferedMessages) {
108
+ log();
109
+ }
110
+ state.bufferedMessages = [];
111
+ }
112
+
113
+ export const ui = {
114
+ writeVerbose,
115
+ writeInformation,
116
+ writeWarning,
117
+ writeError,
118
+ writeExitWithoutInstallingMaliciousPackages,
119
+ emptyLine,
120
+ startBufferingLogs,
121
+ writeBufferedLogsAndStopBuffering,
122
+ };
@@ -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
+ }
package/src/main.js ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { scanCommand, shouldScanCommand } from "./scanning/index.js";
4
+ import { ui } from "./environment/userInteraction.js";
5
+ import { getPackageManager } from "./packagemanager/currentPackageManager.js";
6
+ import { initializeCliArguments } from "./config/cliArguments.js";
7
+ import { createSafeChainProxy } from "./registryProxy/registryProxy.js";
8
+ import chalk from "chalk";
9
+ import { getAuditStats } from "./scanning/audit/index.js";
10
+
11
+ /**
12
+ * @param {string[]} args
13
+ * @returns {Promise<number>}
14
+ */
15
+ export async function main(args) {
16
+ if (isSafeChainVerify(args)) {
17
+ return 0;
18
+ }
19
+
20
+ process.on("SIGINT", handleProcessTermination);
21
+ process.on("SIGTERM", handleProcessTermination);
22
+
23
+ const proxy = createSafeChainProxy();
24
+ await proxy.startServer();
25
+
26
+ // Global error handlers to log unhandled errors
27
+ process.on("uncaughtException", (error) => {
28
+ ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`);
29
+ ui.writeVerbose(`Stack trace: ${error.stack}`);
30
+ ui.writeBufferedLogsAndStopBuffering();
31
+ process.exit(1);
32
+ });
33
+
34
+ process.on("unhandledRejection", (reason) => {
35
+ ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`);
36
+ if (reason instanceof Error) {
37
+ ui.writeVerbose(`Stack trace: ${reason.stack}`);
38
+ }
39
+ ui.writeBufferedLogsAndStopBuffering();
40
+ process.exit(1);
41
+ });
42
+
43
+ try {
44
+ // This parses all the --safe-chain arguments and removes them from the args array
45
+ args = initializeCliArguments(args);
46
+
47
+ if (shouldScanCommand(args)) {
48
+ const commandScanResult = await scanCommand(args);
49
+
50
+ // Returning the exit code back to the caller allows the promise
51
+ // to be awaited in the bin files and return the correct exit code
52
+ if (commandScanResult !== 0) {
53
+ return commandScanResult;
54
+ }
55
+ }
56
+
57
+ // Buffer logs during package manager execution, this avoids interleaving
58
+ // of logs from the package manager and safe-chain
59
+ // Not doing this could cause bugs to disappear when cursor movement codes
60
+ // are written by the package manager while safe-chain is writing logs
61
+ ui.startBufferingLogs();
62
+ const packageManagerResult = await getPackageManager().runCommand(args);
63
+
64
+ // Write all buffered logs
65
+ ui.writeBufferedLogsAndStopBuffering();
66
+
67
+ if (proxy.hasBlockedMaliciousPackages()) {
68
+ return 1;
69
+ }
70
+
71
+ if (proxy.hasBlockedMinimumAgeRequests()) {
72
+ return 1;
73
+ }
74
+
75
+ const auditStats = getAuditStats();
76
+ if (auditStats.totalPackages > 0) {
77
+ ui.writeVerbose(
78
+ `${chalk.green("✔")} Safe-chain: Scanned ${
79
+ auditStats.totalPackages
80
+ } packages, no malware found.`,
81
+ );
82
+ }
83
+
84
+ if (proxy.hasSuppressedVersions()) {
85
+ ui.writeInformation(
86
+ `${chalk.yellow(
87
+ "ℹ",
88
+ )} Safe-chain: Some package versions were suppressed during package metadata resolution due to minimum package age.`,
89
+ );
90
+ ui.writeInformation(
91
+ ` To disable this check, use: ${chalk.cyan(
92
+ "--safe-chain-skip-minimum-package-age",
93
+ )}`,
94
+ );
95
+ }
96
+
97
+ // Returning the exit code back to the caller allows the promise
98
+ // to be awaited in the bin files and return the correct exit code
99
+ return packageManagerResult.status;
100
+ } catch (/** @type any */ error) {
101
+ ui.writeError("Failed to check for malicious packages:", error.message);
102
+ ui.writeBufferedLogsAndStopBuffering();
103
+
104
+ // Returning the exit code back to the caller allows the promise
105
+ // to be awaited in the bin files and return the correct exit code
106
+ return 1;
107
+ } finally {
108
+ await proxy.stopServer();
109
+ }
110
+ }
111
+
112
+ function handleProcessTermination() {
113
+ ui.writeBufferedLogsAndStopBuffering();
114
+ }
115
+
116
+ /** @param {string[]} args */
117
+ function isSafeChainVerify(args) {
118
+ const safeChainCheckCommand = "safe-chain-verify";
119
+ if (args.length > 0 && args[0] === safeChainCheckCommand) {
120
+ ui.writeInformation("OK: Safe-chain works!");
121
+ return true;
122
+ }
123
+ }
@@ -0,0 +1,17 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+
3
+ /**
4
+ * Centralized logging for package-manager command launch failures.
5
+ *
6
+ * @param {any} error - Error thrown by safeSpawn while preparing/running the command.
7
+ * @param {string} command - Command name that failed to execute.
8
+ * @returns {{status: number}}
9
+ */
10
+ export function reportCommandExecutionFailure(error, command) {
11
+ const message = typeof error?.message === "string" ? error.message : "Unknown error";
12
+ ui.writeError(`Error executing command: ${message}`);
13
+
14
+ ui.writeError(`Is '${command}' installed and available on your system?`);
15
+
16
+ return { status: typeof error?.status === "number" ? error.status : 1 };
17
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @param {string[]} args
3
+ * @param {...string} commandArgs
4
+ * @returns {boolean}
5
+ */
6
+ export function matchesCommand(args, ...commandArgs) {
7
+ if (args.length < commandArgs.length) {
8
+ return false;
9
+ }
10
+
11
+ for (var i = 0; i < commandArgs.length; i++) {
12
+ if (args[i].toLowerCase() !== commandArgs[i].toLowerCase()) {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ return true;
18
+ }
@@ -0,0 +1,48 @@
1
+ import { safeSpawn } from "../../utils/safeSpawn.js";
2
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
3
+ import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
4
+
5
+ /**
6
+ * @returns {import("../currentPackageManager.js").PackageManager}
7
+ */
8
+ export function createBunPackageManager() {
9
+ return {
10
+ runCommand: (args) => runBunCommand("bun", args),
11
+
12
+ // For bun, we use the proxy-only approach to block package downloads,
13
+ // so we don't need to analyze commands.
14
+ isSupportedCommand: () => false,
15
+ getDependencyUpdatesForCommand: () => [],
16
+ };
17
+ }
18
+
19
+ /**
20
+ * @returns {import("../currentPackageManager.js").PackageManager}
21
+ */
22
+ export function createBunxPackageManager() {
23
+ return {
24
+ runCommand: (args) => runBunCommand("bunx", args),
25
+
26
+ // For bunx, we use the proxy-only approach to block package downloads,
27
+ // so we don't need to analyze commands.
28
+ isSupportedCommand: () => false,
29
+ getDependencyUpdatesForCommand: () => [],
30
+ };
31
+ }
32
+
33
+ /**
34
+ * @param {string} command
35
+ * @param {string[]} args
36
+ * @returns {Promise<{status: number}>}
37
+ */
38
+ async function runBunCommand(command, args) {
39
+ try {
40
+ const result = await safeSpawn(command, args, {
41
+ stdio: "inherit",
42
+ env: mergeSafeChainProxyEnvironmentVariables(process.env),
43
+ });
44
+ return { status: result.status };
45
+ } catch (/** @type any */ error) {
46
+ return reportCommandExecutionFailure(error, command);
47
+ }
48
+ }
@@ -0,0 +1,82 @@
1
+ import {
2
+ createBunPackageManager,
3
+ createBunxPackageManager,
4
+ } from "./bun/createBunPackageManager.js";
5
+ import { createNpmPackageManager } from "./npm/createPackageManager.js";
6
+ import { createNpxPackageManager } from "./npx/createPackageManager.js";
7
+ import {
8
+ createPnpmPackageManager,
9
+ createPnpxPackageManager,
10
+ } from "./pnpm/createPackageManager.js";
11
+ import { createYarnPackageManager } from "./yarn/createPackageManager.js";
12
+ import { createPipPackageManager } from "./pip/createPackageManager.js";
13
+ import { createUvPackageManager } from "./uv/createUvPackageManager.js";
14
+ import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
15
+ import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
16
+ import { createUvxPackageManager } from "./uvx/createUvxPackageManager.js";
17
+
18
+ /**
19
+ * @type {{packageManagerName: PackageManager | null}}
20
+ */
21
+ const state = {
22
+ packageManagerName: null,
23
+ };
24
+
25
+ /**
26
+ * @typedef {Object} GetDependencyUpdatesResult
27
+ * @property {string} name
28
+ * @property {string} version
29
+ * @property {string} type
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} PackageManager
34
+ * @property {(args: string[]) => Promise<{ status: number }>} runCommand
35
+ * @property {(args: string[]) => boolean} isSupportedCommand
36
+ * @property {(args: string[]) => Promise<GetDependencyUpdatesResult[]> | GetDependencyUpdatesResult[]} getDependencyUpdatesForCommand
37
+ */
38
+
39
+ /**
40
+ * @param {string} packageManagerName
41
+ * @param {{ tool: string, args: string[] }} [context] - Optional tool context for package managers like pip
42
+ *
43
+ * @return {PackageManager}
44
+ */
45
+ export function initializePackageManager(packageManagerName, context) {
46
+ if (packageManagerName === "npm") {
47
+ state.packageManagerName = createNpmPackageManager();
48
+ } else if (packageManagerName === "npx") {
49
+ state.packageManagerName = createNpxPackageManager();
50
+ } else if (packageManagerName === "yarn") {
51
+ state.packageManagerName = createYarnPackageManager();
52
+ } else if (packageManagerName === "pnpm") {
53
+ state.packageManagerName = createPnpmPackageManager();
54
+ } else if (packageManagerName === "pnpx") {
55
+ state.packageManagerName = createPnpxPackageManager();
56
+ } else if (packageManagerName === "bun") {
57
+ state.packageManagerName = createBunPackageManager();
58
+ } else if (packageManagerName === "bunx") {
59
+ state.packageManagerName = createBunxPackageManager();
60
+ } else if (packageManagerName === "pip") {
61
+ state.packageManagerName = createPipPackageManager(context);
62
+ } else if (packageManagerName === "uv") {
63
+ state.packageManagerName = createUvPackageManager();
64
+ } else if (packageManagerName === "uvx") {
65
+ state.packageManagerName = createUvxPackageManager();
66
+ } else if (packageManagerName === "poetry") {
67
+ state.packageManagerName = createPoetryPackageManager();
68
+ } else if (packageManagerName === "pipx") {
69
+ state.packageManagerName = createPipXPackageManager();
70
+ } else {
71
+ throw new Error("Unsupported package manager: " + packageManagerName);
72
+ }
73
+
74
+ return state.packageManagerName;
75
+ }
76
+
77
+ export function getPackageManager() {
78
+ if (!state.packageManagerName) {
79
+ throw new Error("Package manager not initialized.");
80
+ }
81
+ return state.packageManagerName;
82
+ }
@@ -0,0 +1,72 @@
1
+ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
2
+ import { nullScanner } from "./dependencyScanner/nullScanner.js";
3
+ import { runNpm } from "./runNpmCommand.js";
4
+ import {
5
+ getNpmCommandForArgs,
6
+ npmInstallCommand,
7
+ npmUpdateCommand,
8
+ npmExecCommand,
9
+ } from "./utils/npmCommands.js";
10
+
11
+ /**
12
+ * @returns {import("../currentPackageManager.js").PackageManager}
13
+ */
14
+ export function createNpmPackageManager() {
15
+ /**
16
+ * @param {string[]} args
17
+ *
18
+ * @returns {boolean}
19
+ */
20
+ function isSupportedCommand(args) {
21
+ const scanner = findDependencyScannerForCommand(
22
+ commandScannerMapping,
23
+ args
24
+ );
25
+ return scanner.shouldScan(args);
26
+ }
27
+
28
+ /**
29
+ * @param {string[]} args
30
+ *
31
+ * @returns {ReturnType<import("../currentPackageManager.js").PackageManager["getDependencyUpdatesForCommand"]>}
32
+ */
33
+ function getDependencyUpdatesForCommand(args) {
34
+ const scanner = findDependencyScannerForCommand(
35
+ commandScannerMapping,
36
+ args
37
+ );
38
+ return scanner.scan(args);
39
+ }
40
+
41
+ return {
42
+ runCommand: runNpm,
43
+ isSupportedCommand,
44
+ getDependencyUpdatesForCommand,
45
+ };
46
+ }
47
+
48
+ /**
49
+ * @type {Record<string, import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner>}
50
+ */
51
+ const commandScannerMapping = {
52
+ [npmInstallCommand]: commandArgumentScanner(),
53
+ [npmUpdateCommand]: commandArgumentScanner(),
54
+ [npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
55
+ };
56
+
57
+ /**
58
+ *
59
+ * @param {Record<string, import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner>} scanners
60
+ * @param {string[]} args
61
+ *
62
+ * @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
63
+ */
64
+ function findDependencyScannerForCommand(scanners, args) {
65
+ const command = getNpmCommandForArgs(args);
66
+ if (!command) {
67
+ return nullScanner();
68
+ }
69
+
70
+ const scanner = scanners[command];
71
+ return scanner ? scanner : nullScanner();
72
+ }
@@ -0,0 +1,74 @@
1
+ import { resolvePackageVersion } from "../../../api/npmApi.js";
2
+ import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
3
+ import { hasDryRunArg } from "../utils/npmCommands.js";
4
+
5
+ /**
6
+ * @typedef {Object} ScanResult
7
+ * @property {string} name
8
+ * @property {string} version
9
+ * @property {string} type
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} ScannerOptions
14
+ * @property {boolean} [ignoreDryRun]
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} CommandArgumentScanner
19
+ * @property {(args: string[]) => Promise<ScanResult[]> | ScanResult[]} scan
20
+ * @property {(args: string[]) => boolean} shouldScan
21
+ */
22
+
23
+ /**
24
+ * @param {ScannerOptions} [opts]
25
+ *
26
+ * @returns {CommandArgumentScanner}
27
+ */
28
+ export function commandArgumentScanner(opts) {
29
+ const ignoreDryRun = opts?.ignoreDryRun ?? false;
30
+
31
+ return {
32
+ scan: (args) => scanDependencies(args),
33
+ shouldScan: (args) => shouldScanDependencies(args, ignoreDryRun),
34
+ };
35
+ }
36
+
37
+ /**
38
+ * @param {string[]} args
39
+ * @returns {Promise<ScanResult[]>}
40
+ */
41
+ function scanDependencies(args) {
42
+ return checkChangesFromArgs(args);
43
+ }
44
+
45
+ /**
46
+ * @param {string[]} args
47
+ * @param {boolean} ignoreDryRun
48
+ * @returns {boolean}
49
+ */
50
+ function shouldScanDependencies(args, ignoreDryRun) {
51
+ return ignoreDryRun || !hasDryRunArg(args);
52
+ }
53
+
54
+ /**
55
+ * @param {string[]} args
56
+ * @returns {Promise<ScanResult[]>}
57
+ */
58
+ export async function checkChangesFromArgs(args) {
59
+ const changes = [];
60
+ const packageUpdates = parsePackagesFromInstallArgs(args);
61
+
62
+ for (const packageUpdate of packageUpdates) {
63
+ var exactVersion = await resolvePackageVersion(
64
+ packageUpdate.name,
65
+ packageUpdate.version
66
+ );
67
+ if (exactVersion) {
68
+ packageUpdate.version = exactVersion;
69
+ }
70
+
71
+ changes.push({ ...packageUpdate, type: "add" });
72
+ }
73
+ return changes;
74
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @returns {import("./commandArgumentScanner.js").CommandArgumentScanner}
3
+ */
4
+ export function nullScanner() {
5
+ return {
6
+ scan: () => [],
7
+ shouldScan: () => false,
8
+ };
9
+ }