From 2fcdb11783332f85b6050f7e2e856c9cfea65f4b Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 13 Nov 2024 15:46:58 -0500 Subject: [PATCH] Initial commit --- .gitignore | 178 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 29 ++++++++ bun.lockb | Bin 0 -> 6965 bytes package.json | 15 ++++ src/cli.ts | 102 ++++++++++++++++++++++++++ src/scriptFile.ts | 167 +++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 27 +++++++ 7 files changed, 518 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 package.json create mode 100644 src/cli.ts create mode 100644 src/scriptFile.ts create mode 100644 tsconfig.json 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 0000000000000000000000000000000000000000..94404a3de3a767d460ef84f3f49f47c30988eda8 GIT binary patch literal 6965 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 literal 0 HcmV?d00001 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 + } +}