commit 2fcdb11783332f85b6050f7e2e856c9cfea65f4b Author: Endeavorance Date: Wed Nov 13 15:46:58 2024 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3814619 --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +# And of course, +.run \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a474774 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# rundir + +A CLI for managing `.run` script directories. + +## Concept + +Make a `.run` directory in your project (or wherever) which houses relevant scripts as extensionless executable files. The `rundir` cli can help create and manage these scripts. + +```shell +mkdir .run +rundir create build # Creates a template script at .run/build +``` + +When running a script, invoke them directly: + +```shell +$ .run/build +``` + +If you are in a child directory of the root project folder where the `.run` directory lives, you can use `rundir x`: + +```shell +$ cd some/nested/path +$ rundir x build # runs the `build` script in the original `.run` directory +``` + +## Why? + +Cuz \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..94404a3 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..15aae40 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "rundir", + "module": "src/cli.ts", + "type": "module", + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "chalk": "^5.3.0" + } +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..40c4507 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,102 @@ +import { parseArgs } from "node:util"; +import { edit, ls, newScript, runScript, which } from "./scriptFile"; + +const KNOWN_COMMANDS = [ + "ls", + "help", + "new", + "create", + "which", + "x", + "run", + "edit", +] as const; +type Command = (typeof KNOWN_COMMANDS)[number]; + +function isCommand(command: string): command is Command { + return KNOWN_COMMANDS.includes(command as Command); +} + +function help() { + console.log( + ` +Usage: rundir [command] + +Commands: + new - Create a new script in the rundir (alias: create) + edit - Open a script in the editor + x - Run a script from the nearest rundir (alias: run) + which - Print the absolute path of the rundir + ls - List all scripts in the rundir + help - Show this help message +`.trim(), + ); +} + +function readArgs(): string[] { + const { positionals } = parseArgs({ + args: Bun.argv, + allowPositionals: true, + strict: true, + }); + + return positionals.slice(2); +} + +const args = readArgs(); + +// Default functionality is to list +if (args.length === 0) { + await ls(); + process.exit(0); +} + +const command = args[0]; + +if (!isCommand(command)) { + console.error(`Unknown command: ${args[0]}`); + process.exit(1); +} + +switch (command) { + case "ls": + await ls(); + break; + case "help": + help(); + break; + case "which": + which(); + break; + case "new": + case "create": + if (args.length < 2) { + console.error("Missing script name"); + process.exit(1); + } + + await newScript(args[1]); + break; + case "x": + case "run": + if (args.length < 2) { + console.error("Missing script name"); + process.exit(1); + } + + await runScript(args[1]); + break; + case "edit": + if (args.length < 2) { + console.error("Missing script name"); + process.exit(1); + } + + edit(args[1]); + break; + default: + console.error(`Unknown command "${command}"`); + break; +} + +process.exit(0); diff --git a/src/scriptFile.ts b/src/scriptFile.ts new file mode 100644 index 0000000..8d323ed --- /dev/null +++ b/src/scriptFile.ts @@ -0,0 +1,167 @@ +import { $, Glob } from "bun"; +import chalk from "chalk"; +import path from "node:path"; +import { homedir } from "node:os"; +import { existsSync } from "node:fs"; + +const RUNDIR_PATH = path.join(process.cwd(), ".run"); + +interface ScriptData { + name: string; + description: string; + absolutePath: string; +} + +function findNearestRundir(): string | null { + let currentDir = process.cwd(); + + while (currentDir !== path.dirname(currentDir)) { + const potentialPath = path.join(currentDir, ".run"); + if (existsSync(potentialPath)) { + return potentialPath; + } + currentDir = path.dirname(currentDir); + } + + const homeDirPath = path.join(homedir(), ".run"); + if (existsSync(homeDirPath)) { + return homeDirPath; + } + + return null; +} + +function makeScriptPath(name: string): string { + const rundir = findNearestRundir(); + + if (!rundir) { + throw new Error("No rundir found in the current directory tree."); + } + + return path.join(rundir, name); +} + +/** + * Given the name of a script, search for the script in the rundir and + * return the absolute path to the script. + * Searches the current directory's `.run` directory first, then works + * up the directory tree until it reaches the home directory. + * + * If the script is not found, returns `null`. + * + * @param name The name of the script to find the path of + */ +async function findScriptPath(name: string): Promise { + let currentDir = process.cwd(); + + while (currentDir !== path.dirname(currentDir)) { + const potentialPath = path.join(currentDir, ".run", name); + if (await Bun.file(potentialPath).exists()) { + return potentialPath; + } + currentDir = path.dirname(currentDir); + } + + const homeDirPath = path.join(homedir(), ".run", name); + if (await Bun.file(homeDirPath).exists()) { + return homeDirPath; + } + + return null; +} + +export async function readScriptData(scriptName: string): Promise { + const scriptPath = await findScriptPath(scriptName); + + if (!scriptPath) { + throw new Error( + `Script "${scriptName}" not found in any rundir in the directory tree.`, + ); + } + + const scriptFile = Bun.file(scriptPath); + const scriptContent = await scriptFile.text(); + + const lines = scriptContent.split("\n"); + + const description = + lines.find((line) => line.startsWith("#DESCRIPTION#")) || + "No description provided."; + + return { + absolutePath: scriptPath, + name: path.basename(scriptPath), + description: description.replace("#DESCRIPTION#", "").trim(), + }; +} + +export async function ls() { + const filesInDir = new Glob(`${RUNDIR_PATH}/*`).scan(); + const scriptLines: string[] = []; + + for await (const file of filesInDir) { + const scriptName = path.basename(file); + const scriptData = await readScriptData(scriptName); + scriptLines.push( + `${chalk.dim("$")} ${scriptName} ${chalk.dim(`- ${scriptData.description}`)}`, + ); + } + + const sorted = scriptLines.sort((a, b) => a.localeCompare(b)); + + console.log(sorted.join("\n")); +} + +export async function edit(scriptName: string) { + const scriptPath = await findScriptPath(scriptName); + + if (!scriptPath) { + console.error(`Script "${scriptName}" not found.`); + process.exit(1); + } + + Bun.openInEditor(scriptPath); +} + +/** + * Prints the absolute path of the rundir + */ +export async function which() { + const nearest = findNearestRundir(); + + if (!nearest) { + console.error("No rundir found in the current directory tree."); + process.exit(1); + } + + console.log(path.resolve(nearest)); +} + +export async function newScript(name: string) { + const scriptPath = makeScriptPath(name); + const scriptFile = Bun.file(scriptPath); + + if (await scriptFile.exists()) { + console.error(`Script "${name}" already exists.`); + process.exit(1); + } + + await Bun.write( + scriptFile, + "#!/usr/bin/env bash\n#DESCRIPTION# A rundir script\necho 'Hello, world!'\n", + ); + await $`chmod +x ${scriptPath}`; + + console.log(`Created script "${name}"`); +} + +export async function runScript(name: string) { + const scriptPath = await findScriptPath(name); + + if (!scriptPath) { + console.error(`Script "${name}" not found.`); + process.exit(1); + } + + await $`${scriptPath}`; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ffc08ab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}