@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.
- package/LICENSE +674 -0
- package/README.md +257 -0
- package/bin/aikido-bun.js +14 -0
- package/bin/aikido-bunx.js +14 -0
- package/bin/aikido-npm.js +14 -0
- package/bin/aikido-npx.js +14 -0
- package/bin/aikido-pip.js +20 -0
- package/bin/aikido-pip3.js +21 -0
- package/bin/aikido-pnpm.js +14 -0
- package/bin/aikido-pnpx.js +14 -0
- package/bin/aikido-python.js +30 -0
- package/bin/aikido-python3.js +30 -0
- package/bin/aikido-uv.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +190 -0
- package/docs/banner.svg +151 -0
- package/docs/npm-to-binary-migration.md +89 -0
- package/docs/safe-package-manager-demo.gif +0 -0
- package/docs/safe-package-manager-demo.png +0 -0
- package/docs/shell-integration.md +149 -0
- package/package.json +68 -0
- package/src/api/aikido.js +54 -0
- package/src/api/npmApi.js +71 -0
- package/src/config/cliArguments.js +138 -0
- package/src/config/configFile.js +192 -0
- package/src/config/environmentVariables.js +7 -0
- package/src/config/settings.js +100 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/main.js +104 -0
- package/src/packagemanager/_shared/matchesCommand.js +18 -0
- package/src/packagemanager/bun/createBunPackageManager.js +53 -0
- package/src/packagemanager/currentPackageManager.js +72 -0
- package/src/packagemanager/npm/createPackageManager.js +72 -0
- package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
- package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
- package/src/packagemanager/npm/runNpmCommand.js +25 -0
- package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
- package/src/packagemanager/npm/utils/cmd-list.js +174 -0
- package/src/packagemanager/npm/utils/npmCommands.js +34 -0
- package/src/packagemanager/npx/createPackageManager.js +15 -0
- package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
- package/src/packagemanager/npx/runNpxCommand.js +25 -0
- package/src/packagemanager/pip/createPackageManager.js +21 -0
- package/src/packagemanager/pip/pipSettings.js +30 -0
- package/src/packagemanager/pip/runPipCommand.js +175 -0
- package/src/packagemanager/pnpm/createPackageManager.js +57 -0
- package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
- package/src/packagemanager/pnpm/runPnpmCommand.js +36 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +71 -0
- package/src/packagemanager/yarn/createPackageManager.js +41 -0
- package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
- package/src/packagemanager/yarn/runYarnCommand.js +41 -0
- package/src/registryProxy/certBundle.js +95 -0
- package/src/registryProxy/certUtils.js +128 -0
- package/src/registryProxy/http-utils.js +17 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
- package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
- package/src/registryProxy/mitmRequestHandler.js +231 -0
- package/src/registryProxy/plainHttpProxy.js +95 -0
- package/src/registryProxy/registryProxy.js +184 -0
- package/src/registryProxy/tunnelRequestHandler.js +180 -0
- package/src/scanning/audit/index.js +129 -0
- package/src/scanning/index.js +82 -0
- package/src/scanning/malwareDatabase.js +131 -0
- package/src/shell-integration/helpers.js +213 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
- package/src/shell-integration/setup-ci.js +170 -0
- package/src/shell-integration/setup.js +127 -0
- package/src/shell-integration/shellDetection.js +37 -0
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
- package/src/shell-integration/supported-shells/bash.js +134 -0
- package/src/shell-integration/supported-shells/fish.js +77 -0
- package/src/shell-integration/supported-shells/powershell.js +73 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
- package/src/shell-integration/supported-shells/zsh.js +74 -0
- package/src/shell-integration/teardown.js +64 -0
- package/src/utils/safeSpawn.js +137 -0
- package/tsconfig.json +21 -0
package/src/main.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
process.on("SIGINT", handleProcessTermination);
|
|
17
|
+
process.on("SIGTERM", handleProcessTermination);
|
|
18
|
+
|
|
19
|
+
const proxy = createSafeChainProxy();
|
|
20
|
+
await proxy.startServer();
|
|
21
|
+
|
|
22
|
+
// Global error handlers to log unhandled errors
|
|
23
|
+
process.on("uncaughtException", (error) => {
|
|
24
|
+
ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`);
|
|
25
|
+
ui.writeVerbose(`Stack trace: ${error.stack}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
process.on("unhandledRejection", (reason) => {
|
|
30
|
+
ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`);
|
|
31
|
+
if (reason instanceof Error) {
|
|
32
|
+
ui.writeVerbose(`Stack trace: ${reason.stack}`);
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// This parses all the --safe-chain arguments and removes them from the args array
|
|
39
|
+
args = initializeCliArguments(args);
|
|
40
|
+
|
|
41
|
+
if (shouldScanCommand(args)) {
|
|
42
|
+
const commandScanResult = await scanCommand(args);
|
|
43
|
+
|
|
44
|
+
// Returning the exit code back to the caller allows the promise
|
|
45
|
+
// to be awaited in the bin files and return the correct exit code
|
|
46
|
+
if (commandScanResult !== 0) {
|
|
47
|
+
return commandScanResult;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Buffer logs during package manager execution, this avoids interleaving
|
|
52
|
+
// of logs from the package manager and safe-chain
|
|
53
|
+
// Not doing this could cause bugs to disappear when cursor movement codes
|
|
54
|
+
// are written by the package manager while safe-chain is writing logs
|
|
55
|
+
ui.startBufferingLogs();
|
|
56
|
+
const packageManagerResult = await getPackageManager().runCommand(args);
|
|
57
|
+
|
|
58
|
+
// Write all buffered logs
|
|
59
|
+
ui.writeBufferedLogsAndStopBuffering();
|
|
60
|
+
|
|
61
|
+
if (!proxy.verifyNoMaliciousPackages()) {
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const auditStats = getAuditStats();
|
|
66
|
+
if (auditStats.totalPackages > 0) {
|
|
67
|
+
ui.emptyLine();
|
|
68
|
+
ui.writeInformation(
|
|
69
|
+
`${chalk.green("✔")} Safe-chain: Scanned ${
|
|
70
|
+
auditStats.totalPackages
|
|
71
|
+
} packages, no malware found.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (proxy.hasSuppressedVersions()) {
|
|
76
|
+
ui.writeInformation(
|
|
77
|
+
`${chalk.yellow(
|
|
78
|
+
"ℹ"
|
|
79
|
+
)} Safe-chain: Some package versions were suppressed due to minimum age requirement.`
|
|
80
|
+
);
|
|
81
|
+
ui.writeInformation(
|
|
82
|
+
` To disable this check, use: ${chalk.cyan(
|
|
83
|
+
"--safe-chain-skip-minimum-package-age"
|
|
84
|
+
)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Returning the exit code back to the caller allows the promise
|
|
89
|
+
// to be awaited in the bin files and return the correct exit code
|
|
90
|
+
return packageManagerResult.status;
|
|
91
|
+
} catch (/** @type any */ error) {
|
|
92
|
+
ui.writeError("Failed to check for malicious packages:", error.message);
|
|
93
|
+
|
|
94
|
+
// Returning the exit code back to the caller allows the promise
|
|
95
|
+
// to be awaited in the bin files and return the correct exit code
|
|
96
|
+
return 1;
|
|
97
|
+
} finally {
|
|
98
|
+
await proxy.stopServer();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleProcessTermination() {
|
|
103
|
+
ui.writeBufferedLogsAndStopBuffering();
|
|
104
|
+
}
|
|
@@ -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,53 @@
|
|
|
1
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.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
|
+
if (error.status) {
|
|
47
|
+
return { status: error.status };
|
|
48
|
+
} else {
|
|
49
|
+
ui.writeError("Error executing command:", error.message);
|
|
50
|
+
return { status: 1 };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @type {{packageManagerName: PackageManager | null}}
|
|
17
|
+
*/
|
|
18
|
+
const state = {
|
|
19
|
+
packageManagerName: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} GetDependencyUpdatesResult
|
|
24
|
+
* @property {string} name
|
|
25
|
+
* @property {string} version
|
|
26
|
+
* @property {string} type
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} PackageManager
|
|
31
|
+
* @property {(args: string[]) => Promise<{ status: number }>} runCommand
|
|
32
|
+
* @property {(args: string[]) => boolean} isSupportedCommand
|
|
33
|
+
* @property {(args: string[]) => Promise<GetDependencyUpdatesResult[]> | GetDependencyUpdatesResult[]} getDependencyUpdatesForCommand
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} packageManagerName
|
|
38
|
+
*
|
|
39
|
+
* @return {PackageManager}
|
|
40
|
+
*/
|
|
41
|
+
export function initializePackageManager(packageManagerName) {
|
|
42
|
+
if (packageManagerName === "npm") {
|
|
43
|
+
state.packageManagerName = createNpmPackageManager();
|
|
44
|
+
} else if (packageManagerName === "npx") {
|
|
45
|
+
state.packageManagerName = createNpxPackageManager();
|
|
46
|
+
} else if (packageManagerName === "yarn") {
|
|
47
|
+
state.packageManagerName = createYarnPackageManager();
|
|
48
|
+
} else if (packageManagerName === "pnpm") {
|
|
49
|
+
state.packageManagerName = createPnpmPackageManager();
|
|
50
|
+
} else if (packageManagerName === "pnpx") {
|
|
51
|
+
state.packageManagerName = createPnpxPackageManager();
|
|
52
|
+
} else if (packageManagerName === "bun") {
|
|
53
|
+
state.packageManagerName = createBunPackageManager();
|
|
54
|
+
} else if (packageManagerName === "bunx") {
|
|
55
|
+
state.packageManagerName = createBunxPackageManager();
|
|
56
|
+
} else if (packageManagerName === "pip") {
|
|
57
|
+
state.packageManagerName = createPipPackageManager();
|
|
58
|
+
} else if (packageManagerName === "uv") {
|
|
59
|
+
state.packageManagerName = createUvPackageManager();
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error("Unsupported package manager: " + packageManagerName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return state.packageManagerName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getPackageManager() {
|
|
68
|
+
if (!state.packageManagerName) {
|
|
69
|
+
throw new Error("Package manager not initialized.");
|
|
70
|
+
}
|
|
71
|
+
return state.packageManagerName;
|
|
72
|
+
}
|
|
@@ -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,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} PackageDetail
|
|
3
|
+
* @property {string} name
|
|
4
|
+
* @property {string} version
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} NpmOption
|
|
9
|
+
* @property {string} name
|
|
10
|
+
* @property {number} numberOfParameters
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string[]} args
|
|
15
|
+
* @returns {PackageDetail[]}
|
|
16
|
+
*/
|
|
17
|
+
export function parsePackagesFromInstallArgs(args) {
|
|
18
|
+
/** @type {{name: string, version: string | null}[]} */
|
|
19
|
+
const changes = [];
|
|
20
|
+
let defaultTag = "latest";
|
|
21
|
+
|
|
22
|
+
// Skip first argument (install command)
|
|
23
|
+
for (let i = 1; i < args.length; i++) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
const npmOption = getNpmOption(arg);
|
|
26
|
+
|
|
27
|
+
if (npmOption) {
|
|
28
|
+
// If the option has a parameter, skip the next argument as well
|
|
29
|
+
i += npmOption.numberOfParameters;
|
|
30
|
+
|
|
31
|
+
// it a tag is specified, set the default tag
|
|
32
|
+
if (npmOption.name === "--tag") {
|
|
33
|
+
defaultTag = args[i];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const packageDetails = parsePackagename(arg);
|
|
40
|
+
if (packageDetails) {
|
|
41
|
+
changes.push(packageDetails);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const change of changes) {
|
|
47
|
+
if (!change.version) {
|
|
48
|
+
change.version = defaultTag;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return /** @type {PackageDetail[]} */ (changes);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} arg
|
|
57
|
+
* @returns {NpmOption | undefined}
|
|
58
|
+
*/
|
|
59
|
+
function getNpmOption(arg) {
|
|
60
|
+
if (isNpmOptionWithParameter(arg)) {
|
|
61
|
+
return {
|
|
62
|
+
name: arg,
|
|
63
|
+
numberOfParameters: 1,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Arguments starting with "-" or "--" are considered npm options
|
|
68
|
+
if (arg.startsWith("-")) {
|
|
69
|
+
return {
|
|
70
|
+
name: arg,
|
|
71
|
+
numberOfParameters: 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} arg
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function isNpmOptionWithParameter(arg) {
|
|
83
|
+
const optionsWithParameters = [
|
|
84
|
+
"--access",
|
|
85
|
+
"--auth-type",
|
|
86
|
+
"--cache",
|
|
87
|
+
"--fetch-retries",
|
|
88
|
+
"--fetch-retry-mintimeout",
|
|
89
|
+
"--fetch-retry-maxtimeout",
|
|
90
|
+
"--fetch-retry-factor",
|
|
91
|
+
"--fetch-timeout",
|
|
92
|
+
"--https-proxy",
|
|
93
|
+
"--include",
|
|
94
|
+
"--location",
|
|
95
|
+
"--lockfile-version",
|
|
96
|
+
"--loglevel",
|
|
97
|
+
"--omit",
|
|
98
|
+
"--proxy",
|
|
99
|
+
"--registry",
|
|
100
|
+
"--replace-registry-host",
|
|
101
|
+
"--tag",
|
|
102
|
+
"--user-config",
|
|
103
|
+
"--workspace",
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
return optionsWithParameters.includes(arg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} arg
|
|
111
|
+
* @returns {{name: string, version: string | null}}
|
|
112
|
+
*/
|
|
113
|
+
function parsePackagename(arg) {
|
|
114
|
+
arg = removeAlias(arg);
|
|
115
|
+
const lastAtIndex = arg.lastIndexOf("@");
|
|
116
|
+
|
|
117
|
+
let name, version;
|
|
118
|
+
// The index of the last "@" should be greater than 0
|
|
119
|
+
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
|
120
|
+
if (lastAtIndex > 0) {
|
|
121
|
+
name = arg.slice(0, lastAtIndex);
|
|
122
|
+
version = arg.slice(lastAtIndex + 1);
|
|
123
|
+
} else {
|
|
124
|
+
name = arg;
|
|
125
|
+
version = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
name,
|
|
130
|
+
version,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {string} arg
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
function removeAlias(arg) {
|
|
139
|
+
const aliasIndex = arg.indexOf("@npm:");
|
|
140
|
+
if (aliasIndex !== -1) {
|
|
141
|
+
return arg.slice(aliasIndex + 5);
|
|
142
|
+
}
|
|
143
|
+
return arg;
|
|
144
|
+
}
|
|
@@ -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 runNpm(args) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await safeSpawn("npm", 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
|
+
}
|