├── .gitignore ├── assets ├── demo.gif └── logo.png ├── bin ├── welcome.js ├── file-manager.js ├── detect-pkg-manager.js ├── prompts.js ├── main.js └── index.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/ 3 | 4 | # MacOS 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieldelcore/scriptpal/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieldelcore/scriptpal/HEAD/assets/logo.png -------------------------------------------------------------------------------- /bin/welcome.js: -------------------------------------------------------------------------------- 1 | const gradient = require("gradient-string"); 2 | 3 | function welcome() { 4 | const logo = gradient.pastel("ScriptPal 🤘\n"); 5 | 6 | console.log(logo); 7 | } 8 | 9 | module.exports = welcome; 10 | -------------------------------------------------------------------------------- /bin/file-manager.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | const chalk = require("chalk"); 5 | const findConfig = require("find-config"); 6 | 7 | function getPackageJson(path = "package.json") { 8 | if (!hasFile(path)) { 9 | throw new Error(`${chalk.bgRed(path)} ${chalk.red("not found")}\n`); 10 | } 11 | 12 | const configRaw = findConfig.read(path); 13 | 14 | return JSON.parse(configRaw); 15 | } 16 | 17 | function hasFile(path) { 18 | try { 19 | return findConfig(path); 20 | } catch (error) { 21 | return false; 22 | } 23 | } 24 | 25 | module.exports = { 26 | getPackageJson, 27 | hasFile, 28 | }; 29 | -------------------------------------------------------------------------------- /bin/detect-pkg-manager.js: -------------------------------------------------------------------------------- 1 | const { hasFile } = require("./file-manager"); 2 | 3 | function getPackageManager() { 4 | const packageManagers = [ 5 | { id: "yarn", file: hasFile("yarn.lock") }, 6 | { id: "bun", file: hasFile("bun.lockb") }, 7 | { id: "pnpm", file: hasFile("pnpm-lock.yaml") }, 8 | { id: "npm", file: hasFile("package-lock.json") }, 9 | ]; 10 | 11 | let largest = packageManagers[0]; 12 | 13 | for (let i = 0; i < packageManagers.length; i++) { 14 | if ( 15 | packageManagers[i].file && 16 | packageManagers[i].file.split("/").length > largest 17 | ) { 18 | largest = i; 19 | } 20 | } 21 | 22 | return (largest && largest.id) || "npm"; 23 | } 24 | 25 | module.exports = { getPackageManager }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptpal", 3 | "version": "1.4.3", 4 | "description": "A simple npm script palette for lazy people (like me)", 5 | "main": "./bin/index.js", 6 | "bin": { 7 | "scriptpal": "./bin/index.js", 8 | "spal": "./bin/index.js" 9 | }, 10 | "scripts": { 11 | "start": "node ./bin/index", 12 | "test": "echo \"Error: no test specified\"" 13 | }, 14 | "author": "Daniel Del Core", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/danieldelcore/scriptpal.git" 18 | }, 19 | "license": "MIT", 20 | "keywords": [ 21 | "list", 22 | "ls", 23 | "npm-ls", 24 | "palette", 25 | "command", 26 | "picker", 27 | "search", 28 | "script" 29 | ], 30 | "dependencies": { 31 | "chalk": "^3.0.0", 32 | "clipboardy": "^2.1.0", 33 | "commander": "^8.2.0", 34 | "conf": "^6.2.1", 35 | "enquirer": "^2.3.2", 36 | "find-config": "^1.0.0", 37 | "gradient-string": "^1.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bin/prompts.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const { AutoComplete, Snippet, Confirm } = require("enquirer"); 3 | 4 | const promptShouldRerunPrevious = async (previous) => { 5 | const previousCommand = `${previous.script} ${ 6 | previous.parameters || "" 7 | }`.trim(); 8 | 9 | return await new Confirm({ 10 | message: `Would you like to rerun the previous command?\n${chalk.greenBright( 11 | previousCommand 12 | )}`, 13 | }).run(); 14 | }; 15 | 16 | const promptGetCommand = async (choices) => { 17 | const script = await new AutoComplete({ 18 | message: "Which script would you like to run? 🤷‍♂️", 19 | limit: 18, 20 | choices, 21 | }).run(); 22 | 23 | const { 24 | values: { parameters }, 25 | } = await new Snippet({ 26 | message: "Would you like to add parameters?", 27 | required: false, 28 | fields: [ 29 | { 30 | name: "parameters", 31 | message: "parameters", 32 | }, 33 | ], 34 | template: `${script} \${parameters}`, 35 | }).run(); 36 | 37 | return { 38 | script, 39 | parameters, 40 | }; 41 | }; 42 | 43 | module.exports = { 44 | promptShouldRerunPrevious, 45 | promptGetCommand, 46 | }; 47 | -------------------------------------------------------------------------------- /bin/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const clipboardy = require("clipboardy"); 3 | const Conf = require("conf"); 4 | const welcome = require("./welcome"); 5 | const { promptShouldRerunPrevious, promptGetCommand } = require("./prompts"); 6 | const { getPackageManager } = require("./detect-pkg-manager"); 7 | const { packageJson } = require("."); 8 | 9 | async function main(flags) { 10 | if (!flags.nowelcome) welcome(); 11 | 12 | const choices = Object.keys(packageJson.scripts); 13 | 14 | const config = new Conf(); 15 | const previous = config.get(`${process.cwd()}.previous`); 16 | 17 | let shouldRerunPrevious = false; 18 | 19 | if (!previous && flags.last) { 20 | console.log("Previous command not found, continuing...\n"); 21 | } else if (previous && !flags.last) { 22 | shouldRerunPrevious = await promptShouldRerunPrevious(previous); 23 | } else { 24 | shouldRerunPrevious = true; 25 | } 26 | 27 | const { script, parameters } = 28 | (previous || flags.last) && shouldRerunPrevious 29 | ? previous 30 | : await promptGetCommand(choices); 31 | 32 | const pkgManager = getPackageManager(); 33 | let args = !pkgManager === "npm" ? ["run", script] : [script]; 34 | args = parameters ? [...args, parameters] : args; 35 | 36 | if (flags.clipboard) { 37 | await clipboardy.write(`${pkgManager} ${args.join(" ")}`); 38 | console.log("Copied to clipboard 👉 📋"); 39 | return 0; 40 | } 41 | 42 | // spawnScript(pkgManager, args); 43 | config.set(`${process.cwd()}.previous`, { script, parameters }); 44 | } 45 | exports.main = main; 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Script Palette 3 |

4 | 5 | # ScriptPal 🤘 6 | 7 | A simple npm script palette for lazy people who want a quick way to look through and pick npm scripts! 8 | 9 | - keyboard navigation 10 | - autocompletion 11 | - fuzzy finding 12 | 13 |

14 | Demo 15 |

16 | 17 | ## Install ⬇️ 18 | 19 | Install globally 20 | 21 | ```bash 22 | npm install -g scriptpal 23 | ``` 24 | 25 | ## Usage 🏁 26 | 27 | ```bash 28 | scriptpal 29 | ``` 30 | 31 | Usage with npx 32 | 33 | ```bash 34 | npx scriptpal 35 | ``` 36 | 37 | ## API 🤖 38 | 39 | - `--nowelcome`, `-n` Omit welcome message 40 | - `--last`, `-l` Run previous command 41 | - `--version`, `-v` Version number 42 | - `--clipboard`, `-c` Copy command to clipboard 43 | - `--help` Help me 🙏 44 | 45 | ### Subcommands 46 | 47 | `list` List all scripts found in local `package.json`. 48 | 49 | It's possible to also run arbitrary scripts from your `package.json` by passing them as sub-commands, similar to `yarn`. 50 | 51 | For example: `scriptpal test` will run `npm run test`. 52 | 53 | ## Examples 54 | 55 | - `$ scriptpal --nowelcome` 56 | - `$ npx scriptpal` 57 | - `$ scriptpal --last --preset="emoji"` 58 | - `$ scriptpal list` 59 | - `$ scriptpal start` 60 | 61 | ## You might also like... 62 | 63 | - [CommitPal](https://github.com/zeropoly/commitpal): A delightful CLI tool for building complex commit messages 64 | - [Enquirer](https://github.com/enquirer/enquirer): Stylish, intuitive and user-friendly prompts 65 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | const { spawnSync } = require("child_process"); 5 | const clipboardy = require("clipboardy"); 6 | const Conf = require("conf"); 7 | const chalk = require("chalk"); 8 | const { Command, Option, CommanderError } = require("commander"); 9 | 10 | const { version } = require("../package.json"); 11 | const welcome = require("./welcome"); 12 | const { getPackageJson } = require("./file-manager"); 13 | const { promptShouldRerunPrevious, promptGetCommand } = require("./prompts"); 14 | const { getPackageManager } = require("./detect-pkg-manager"); 15 | 16 | let packageJson; 17 | 18 | try { 19 | packageJson = getPackageJson(); 20 | 21 | if (!packageJson.scripts) { 22 | throw new Error(chalk.red('No "scripts" found in package.json')); 23 | } 24 | } catch (error) { 25 | console.error(error); 26 | process.exit(1); 27 | } 28 | 29 | function spawnScript(pkgManager, args) { 30 | const spawn = spawnSync(pkgManager, args, { stdio: "inherit" }); 31 | 32 | if (spawn.error) { 33 | throw new Error(spawn.error); 34 | } 35 | } 36 | 37 | async function main(flags) { 38 | if (!flags.nowelcome) welcome(); 39 | 40 | const choices = Object.keys(packageJson.scripts); 41 | const config = new Conf(); 42 | const previous = config.get(`${process.cwd()}.previous`); 43 | 44 | let shouldRerunPrevious = false; 45 | 46 | if (!previous && flags.last) { 47 | console.log("Previous command not found, continuing...\n"); 48 | } else if (previous && !flags.last) { 49 | shouldRerunPrevious = await promptShouldRerunPrevious(previous); 50 | } else { 51 | shouldRerunPrevious = true; 52 | } 53 | 54 | const { script, parameters } = 55 | (previous || flags.last) && shouldRerunPrevious 56 | ? previous 57 | : await promptGetCommand(choices); 58 | 59 | const pkgManager = getPackageManager(); 60 | let args = !pkgManager === "npm" ? ["run", script] : [script]; 61 | args = parameters ? [...args, parameters] : args; 62 | 63 | if (flags.clipboard) { 64 | await clipboardy.write(`${pkgManager} ${args.join(" ")}`); 65 | console.log("Copied to clipboard 👉 📋"); 66 | return 0; 67 | } 68 | 69 | config.set(`${process.cwd()}.previous`, { script, parameters }); 70 | 71 | spawnScript(pkgManager, args); 72 | } 73 | 74 | async function runLocalScript(script) { 75 | const pkgManager = getPackageManager(); 76 | const args = !pkgManager === "npm" ? ["run", script] : [script]; 77 | 78 | spawnScript(pkgManager, args); 79 | } 80 | 81 | async function list() { 82 | const packageJson = getPackageJson(); 83 | Object.entries(packageJson.scripts).forEach(([key, value]) => { 84 | console.log(`· ${chalk.greenBright(key)}: ${value}`); 85 | }); 86 | } 87 | 88 | const program = new Command(); 89 | 90 | program 91 | .enablePositionalOptions() 92 | .name("scriptpal") 93 | .version(version, "-v, --version") 94 | .option("-l, --last", "Run previous command") 95 | .option("-c, --clipboard", "Copy command to clipboard") 96 | .option("-n, --nowelcome", "Omit welcome message") 97 | .addHelpText( 98 | "after", 99 | ` 100 | Examples 101 | $ scriptpal --last 102 | $ scriptpal --clipboard 103 | $ scriptpal --last --clipboard 104 | $ scriptpal -lcn 105 | $ scriptpal --nowelcome 106 | $ npx scriptpal 107 | $ scriptpal --last --preset="emoji"` 108 | ) 109 | .action(async (options) => await main(options)); 110 | 111 | program 112 | .command("list") 113 | .description("List available scripts from package.json") 114 | .action(() => list()); 115 | 116 | // console.log(packageJson.scripts); 117 | Object.keys(packageJson.scripts) 118 | .filter((script) => !["list"].includes(script)) 119 | .forEach((script) => { 120 | program 121 | .usage("[global options] ...") 122 | .command(script) 123 | .description( 124 | `Runs local script "${script}" detected in local package.json` 125 | ) 126 | .action(() => runLocalScript(script)); 127 | }); 128 | 129 | (async () => { 130 | try { 131 | await program.parseAsync(process.argv); 132 | } catch (error) { 133 | console.error(chalk.red(error)); 134 | process.exit(1); 135 | } 136 | })(); 137 | --------------------------------------------------------------------------------