diff --git a/packages/sudo-tools/package.json b/packages/sudo-tools/package.json new file mode 100644 index 00000000..baf9be20 --- /dev/null +++ b/packages/sudo-tools/package.json @@ -0,0 +1,39 @@ +{ + "name": "sudo-tools", + "version": "1.0.0", + "description": "Tools for working with sudo: executing command as sudo if available, detecting root, etc.", + "homepage": "https://github.com/aminya/setup-cpp", + "license": "Apache-2.0", + "author": "Amin Yahyaabadi", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "source": "./src/index.ts", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "execa": "^5.1.1", + "which": "^2.0.2" + }, + "devDependencies": { + "@types/which": "^2.0.1" + }, + "keywords": [ + "sudo", + "root", + "is-root", + "is-sudo", + "exec-sudo", + "exec", + "execa", + "spawn", + "system", + "unix", + "linux", + "github-actions", + "github", + "actions", + "gitlab", + "ci" + ] +} diff --git a/packages/sudo-tools/src/index.ts b/packages/sudo-tools/src/index.ts new file mode 100644 index 00000000..2c71b566 --- /dev/null +++ b/packages/sudo-tools/src/index.ts @@ -0,0 +1,41 @@ +import which from "which" +import execa from "execa" + +let isSudoCache: boolean | undefined = undefined + +/** + * Detect if sudo is available and the user has root privileges + * + * @note it caches the result for the subsequent calls to this function. + */ +export function isRoot(): boolean { + if (isSudoCache !== undefined) { + return isSudoCache + } + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition + isSudoCache = (Boolean(process.env.CI) || process.getuid?.() === 0) && which.sync("sudo", { nothrow: true }) !== null + return isSudoCache +} + +/** Prepend `sudo` to the command if sudo is available */ +export function prependSudo(command: string) { + if (isRoot()) { + return `sudo ${command}` + } + return command +} + +/** + * Execute a command as sudo if sudo is available. Otherwise executes the command without sudo. + * + * @param file The file to spawn + * @param args The command arguments + * @param execOptions The options passed to `execa`. + */ +export function execSudo(file: string, args: string[], execOptions: execa.SyncOptions = { stdio: "inherit" }) { + if (isRoot()) { + return execa.commandSync(`sudo ${[file, ...args].map((arg) => `'${arg}'`).join(" ")}`, execOptions) + } else { + return execa.sync(file, args, execOptions) + } +} diff --git a/packages/sudo-tools/tsconfig.json b/packages/sudo-tools/tsconfig.json new file mode 100644 index 00000000..6a62dbc4 --- /dev/null +++ b/packages/sudo-tools/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["./src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d833a1c7..0e5c34d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,17 @@ importers: devDependencies: '@types/which': 2.0.1 + packages/sudo-tools: + specifiers: + '@types/which': ^2.0.1 + execa: ^5.1.1 + which: ^2.0.2 + dependencies: + execa: 5.1.1 + which: 2.0.2 + devDependencies: + '@types/which': 2.0.1 + packages: /@actions/cache/3.0.0: