Initial commit
This commit is contained in:
commit
2fcdb11783
7 changed files with 518 additions and 0 deletions
178
.gitignore
vendored
Normal file
178
.gitignore
vendored
Normal 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
29
README.md
Normal 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
BIN
bun.lockb
Executable file
Binary file not shown.
15
package.json
Normal file
15
package.json
Normal 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
102
src/cli.ts
Normal 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
167
src/scriptFile.ts
Normal 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
27
tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue