Initial commit

This commit is contained in:
Endeavorance 2024-11-13 15:46:58 -05:00
commit 2fcdb11783
7 changed files with 518 additions and 0 deletions

178
.gitignore vendored Normal file
View file

@ -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

29
README.md Normal file
View file

@ -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

BIN
bun.lockb Executable file

Binary file not shown.

15
package.json Normal file
View file

@ -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"
}
}

102
src/cli.ts Normal file
View file

@ -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 <name> - Create a new script in the rundir (alias: create)
edit <name> - Open a script in the editor
x <name> - 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);

167
src/scriptFile.ts Normal file
View file

@ -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<string | null> {
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<ScriptData> {
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}`;
}

27
tsconfig.json Normal file
View file

@ -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
}
}