DEV Community

Nasouh Mrstani
Nasouh Mrstani

Posted on

🚀 Build Your Own App Launcher CLI with Node.js

Managing multiple projects on your machine can get messy—especially if you’re constantly jumping between NestJS, Angular, and other apps. Wouldn’t it be great to have a command-line tool that lists all your projects, lets you choose one, and runs your desired command with a single keystroke?

That’s exactly what I built: a custom App Launcher CLI using Node.js. In this tutorial, I’ll show you how to make one yourself.

Features

📂 Automatically scans a folder for apps/projects.
📌 Sorts apps by last modified for quick access.
⚡ Run custom commands like npm run start:dev or ng serve --open.
🛠 Commands are fully configurable in .env.
🖥 Optional: Convert it into a desktop EXE app with pkg.

🛑 Prerequisites

Node.js installed
Basic knowledge of running CLI commands

📂 Project Setup

Create a new folder and install dependencies:

mkdir app-launcher
cd app-launcher
npm init -y
npm install inquirer dotenv
Enter fullscreen mode Exit fullscreen mode

⚙️ Add a .env File

Define your base path for apps and the commands you want available:
APPS_BASE_PATH=D:/apps

COMMAND_1_NAME=NestJS (Start Dev)
COMMAND_1_CMD=npm run start:dev

COMMAND_2_NAME=Angular (Serve with Open)
COMMAND_2_CMD=ng serve --open

COMMAND_3_NAME=Open Shell
COMMAND_3_CMD=cmd
Enter fullscreen mode Exit fullscreen mode

📝 The App Launcher Code
Here’s the main script app-launcher.js:

const cp = require("child_process");
const fs = require("fs");
const path = require("path");
const inquirer = require("inquirer");
require("dotenv").config();

const style = {
  reset: "\x1b[0m",
  bright: "\x1b[1m",
  fgGreen: "\x1b[32m",
  fgCyan: "\x1b[36m",
  fgYellow: "\x1b[33m",
  fgMagenta: "\x1b[35m"
};

const APP_NAME = "App Launcher";
const APP_VERSION = "1.0.0";
const APP_AUTHOR = "Nasouh Mrstani / Senior Web Developer";
const APP_EMAIL = "nasouhmra@gmail.com";
const baseDir = process.env.APPS_BASE_PATH || "D:/apps";

function boxText(text) {
  const line = "─".repeat(text.length + 2);
  return [
    style.fgCyan + `┌${line}┐` + style.reset,
    style.fgCyan + ` ${text} ` + style.reset,
    style.fgCyan + `└${line}┘` + style.reset,
  ].join("\n");
}

function showWelcome() {
  const line = style.fgGreen + "═".repeat(50) + style.reset;
  console.log(line);
  console.log(boxText(style.bright + APP_NAME + "  " + APP_VERSION + style.reset));
  console.log(style.fgMagenta + `Author: ${APP_AUTHOR}` + style.reset);
  console.log(style.fgCyan + `Email: ${APP_EMAIL}` + style.reset);
  console.log(style.fgGreen + `Apps path: ${baseDir}` + style.reset);
  console.log(line + "\n");
}

const COMMANDS = [];
for (let i = 1; ; i++) {
  const name = process.env[`COMMAND_${i}_NAME`];
  const cmd = process.env[`COMMAND_${i}_CMD`];
  if (!name || !cmd) break;
  const parts = cmd.split(" ");
  COMMANDS.push({ name, cmd: parts });
}

async function main() {
  showWelcome();

  const dirs = fs.readdirSync(baseDir, { withFileTypes: true })
    .filter(d => d.isDirectory())
    .map(d => {
      const folderPath = path.join(baseDir, d.name);
      const stats = fs.statSync(folderPath);
      return { name: d.name, value: folderPath, mtime: stats.mtime };
    });

  if (dirs.length === 0) {
    console.log(style.fgMagenta + "❌ No folders found in " + baseDir + style.reset);
    return;
  }

  dirs.sort((a, b) => b.mtime - a.mtime);

  const { projectPath } = await inquirer.prompt([
    {
      type: "list",
      name: "projectPath",
      message: style.fgGreen + "Select a folder" + style.reset,
      pageSize: 15,
      choices: dirs.map(d => ({
        name: style.fgCyan + d.name + style.reset,
        value: d.value
      })),
    }
  ]);

  console.log(boxText(path.basename(projectPath)));

  const { command } = await inquirer.prompt([
    {
      type: "list",
      name: "command",
      message: `Choose a command for "${path.basename(projectPath)}"`,
      choices: COMMANDS.map(c => ({ name: c.name, value: c.cmd })),
    },
  ]);

  console.log(style.fgYellow + `Running: ${command.join(" ")} in ${projectPath}` + style.reset);

  cp.spawn(command[0], command.slice(1), {
    cwd: projectPath,
    shell: true,
    stdio: "inherit"
  });
}

main().catch(err => console.error("Error:", err));
Enter fullscreen mode Exit fullscreen mode

▶️ Run the Launcher

Simply run:

node app-launcher.js
Enter fullscreen mode Exit fullscreen mode

🖥 Convert to Desktop App (Optional)
If you want a single .exe you can run from your desktop:

npm install -g pkg
pkg app-launcher.js --targets node18-win-x64 --output app-launcher.exe
Enter fullscreen mode Exit fullscreen mode

Now you have a portable Windows executable 🎉.

📬 Wrapping Up

With just a bit of Node.js and some CLI magic, you now have your own App Launcher that makes managing projects faster and easier.

✅ Clean interface
✅ Customizable with .env
✅ Runs on Windows as EXE with pkg

GitHub Repo

Top comments (0)