diff --git a/.gitignore b/.gitignore
index 505b3e4..3814619 100644
--- a/.gitignore
+++ b/.gitignore
@@ -174,5 +174,5 @@ dist
# Finder (MacOS) folder config
.DS_Store
-bin
-build
+# And of course,
+.run
\ No newline at end of file
diff --git a/.run/build b/.run/build
deleted file mode 100755
index 79ed72f..0000000
--- a/.run/build
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-# Cross build rundir for multiple platforms
-
-echo "Clean"
-rm -rf bin build
-
-mkdir bin
-
-echo "linux-x64"
-bun build --compile --minify --target=bun-linux-x64 ./src/cli.ts --outfile bin/rundir-linux-x64
-
-echo "linux-arm64"
-bun build --compile --minify --target=bun-linux-arm64 ./src/cli.ts --outfile bin/rundir-linux-arm64
-
-echo "windows-x64"
-bun build --compile --minify --target=bun-windows-x64 ./src/cli.ts --outfile bin/rundir-windows-x64
-
-echo "macos-arm"
-bun build --compile --minify --target=bun-darwin-arm64 ./src/cli.ts --outfile bin/rundir-macos-apple-silicon
-
-echo "macos-x64"
-bun build --compile --minify --target=bun-darwin-x64 ./src/cli.ts --outfile bin/rundir-macos-x64
-
-mkdir build
-
-echo "package"
-bun build --target bun --outdir build ./src/cli.ts
diff --git a/.run/clean b/.run/clean
deleted file mode 100755
index 5c5e0ae..0000000
--- a/.run/clean
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-# Clean build artifacts
-rm -rf build
diff --git a/.run/format b/.run/format
deleted file mode 100755
index 4570fa7..0000000
--- a/.run/format
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-# Format the repo, writing the formatted files to disk
-bunx biome format . --write
-
diff --git a/.run/test b/.run/test
deleted file mode 100755
index 5a3f48e..0000000
--- a/.run/test
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-# A rundir script
-echo 'Hello, world!'
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index efb9808..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,24 +0,0 @@
-This is free and unencumbered software released into the public domain.
-
-Anyone is free to copy, modify, publish, use, compile, sell, or
-distribute this software, either in source code form or as a compiled
-binary, for any purpose, commercial or non-commercial, and by any
-means.
-
-In jurisdictions that recognize copyright laws, the author or authors
-of this software dedicate any and all copyright interest in the
-software to the public domain. We make this dedication for the benefit
-of the public at large and to the detriment of our heirs and
-successors. We intend this dedication to be an overt act of
-relinquishment in perpetuity of all present and future rights to this
-software under copyright law.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-
-For more information, please refer to
diff --git a/README.md b/README.md
index e98b9b2..a474774 100644
--- a/README.md
+++ b/README.md
@@ -24,30 +24,6 @@ $ cd some/nested/path
$ rundir x build # runs the `build` script in the original `.run` directory
```
-## CLI
+## Why?
-```
-Usage: rundir [command]
-
-Commands:
- x Run a script from the nearest rundir (alias: run)
- new Create a new script in the rundir (alias: create)
- ls List all scripts in the rundir
- ls Print the absolute path to a specific script
- which Print the absolute path of the rundir
- help Show this help message
-```
-
-## `rdx` alias
-
-Consider setting an alias for `rundir x` to `rdx` for convenience:
-
-```shell
-alias rdx="rundir x"
-```
-
-Then you can run scripts with `rdx` instead of `rundir x`:
-
-```shell
-rdx build
-```
+Cuz
\ No newline at end of file
diff --git a/biome.json b/biome.json
deleted file mode 100644
index b6909ea..0000000
--- a/biome.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
- "vcs": {
- "enabled": false,
- "clientKind": "git",
- "useIgnoreFile": true
- },
- "files": {
- "ignoreUnknown": false,
- "ignore": ["dist", "*.d.ts", "*.json", "build"]
- },
- "formatter": {
- "enabled": true,
- "indentStyle": "space",
- "indentWidth": 2
- },
- "organizeImports": {
- "enabled": true
- },
- "linter": {
- "enabled": true,
- "rules": {
- "recommended": true,
- "suspicious": {
- "noArrayIndexKey": "off"
- },
- "complexity": {
- "useLiteralKeys": "off"
- }
- }
- },
- "javascript": {
- "formatter": {
- "quoteStyle": "double"
- }
- }
-}
diff --git a/bun.lock b/bun.lock
deleted file mode 100644
index eeafa95..0000000
--- a/bun.lock
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "lockfileVersion": 1,
- "workspaces": {
- "": {
- "name": "rundir",
- "dependencies": {
- "chalk": "^5.4.1",
- },
- "devDependencies": {
- "@biomejs/biome": "^1.9.4",
- "@types/bun": "latest",
- },
- "peerDependencies": {
- "typescript": "^5.8.3",
- },
- },
- },
- "packages": {
- "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
-
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
-
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
-
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
-
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
-
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
-
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
-
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
-
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
-
- "@types/bun": ["@types/bun@1.2.12", "", { "dependencies": { "bun-types": "1.2.12" } }, "sha512-lY/GQTXDGsolT/TiH72p1tuyUORuRrdV7VwOTOjDOt8uTBJQOJc5zz3ufwwDl0VBaoxotSk4LdP0hhjLJ6ypIQ=="],
-
- "@types/node": ["@types/node@22.15.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw=="],
-
- "bun-types": ["bun-types@1.2.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-tvWMx5vPqbRXgE8WUZI94iS1xAYs8bkqESR9cxBB1Wi+urvfTrF1uzuDgBHFAdO0+d2lmsbG3HmeKMvUyj6pWA=="],
-
- "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
-
- "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
-
- "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
- }
-}
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
index 1ecad94..15aae40 100644
--- a/package.json
+++ b/package.json
@@ -1,22 +1,15 @@
{
- "name": "@endeavorance/rundir",
- "type": "module",
- "module": "src/cli.ts",
- "version": "0.2.0",
- "bin": {
- "rundir": "build/cli.js"
- },
- "files": [
- "build"
- ],
- "devDependencies": {
- "@biomejs/biome": "^1.9.4",
- "@types/bun": "latest"
- },
- "peerDependencies": {
- "typescript": "^5.8.3"
- },
- "dependencies": {
- "chalk": "^5.4.1"
- }
+ "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
index d27544a..40c4507 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,357 +1,102 @@
-import { existsSync } from "node:fs";
-import path from "node:path";
import { parseArgs } from "node:util";
-import { $, Glob } from "bun";
-import chalk from "chalk";
-import pkg from "../package.json" assert { type: "json" };
+import { edit, ls, newScript, runScript, which } from "./scriptFile";
-interface ScriptData {
- name: string;
- description: string;
- absolutePath: string;
+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);
}
-class CLIError extends Error {
- name = "CLIError";
-
- constructor(message: string) {
- super(`rundir error: ${message}`);
- }
-}
-
-class NoRundirError extends CLIError {
- name = "NoRundirError";
-
- constructor() {
- super("No rundir found in current directory tree");
- }
-}
-
-const TEMPLATE_FILE_PATH = process.env["RUNDIR_TEMPLATE"] ?? null;
-const DEFAULT_TEMPLATE = `
-#!/usr/bin/env bash
-# Description...
-`.trim();
-const VERSION = pkg.version;
-const COMMENT_STARTERS = ["#", "//", "--"];
-const COMMANDS = ["ls", "help", "new", "create", "which", "x", "run"] as const;
-type Command = (typeof COMMANDS)[number];
-
-/**
- * Given an unknown string, parse a valid command or throw
- *
- * @param command The command to parse
- * @returns The parsed command
- */
-function parseCommand(command: string): Command {
- if (COMMANDS.includes(command as Command)) {
- return command as Command;
- }
-
- throw new CLIError(`Unknown command: ${command}`);
-}
-
-/**
- * Fund a rundir, checking the current or provided directory
- * path and walking up the tree if no dir is found
- *
- * @param fromPath The path to start searching from
- * @returns The path to the rundir, or null if not found
- */
-function getRundirPath(fromPath?: string): string {
- const currentDir = fromPath ?? process.cwd();
- const potentialPath = path.resolve(currentDir, ".run");
-
- if (currentDir === path.dirname(currentDir)) {
- throw new NoRundirError();
- }
-
- if (existsSync(potentialPath)) {
- return potentialPath;
- }
-
- return getRundirPath(path.dirname(currentDir));
-}
-
-/**
- * Given the name of a script, return the absolute path to the script
- *
- * @param name The name of the script
- * @returns The absolute path to the script
- */
-function getScriptPath(name: string): string {
- return path.resolve(getRundirPath(), name);
-}
-
-async function scriptExists(name: string): Promise {
- return Bun.file(getScriptPath(name)).exists();
-}
-
-/**
- * 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 {
- const currentRundir = getRundirPath();
-
- if (!currentRundir) {
- throw new NoRundirError();
- }
-
- const expectedPath = path.resolve(currentRundir, name);
-
- const checkFile = Bun.file(expectedPath);
-
- const exists = await checkFile.exists();
-
- if (!exists) {
- return null;
- }
-
- return expectedPath;
-}
-
-/**
- * Parses a line of text and returns the comment line or null
- * if the line does not start with a comment starter
- *
- * @param line The line of text to parse
- * @returns The comment line or null
- */
-function getCommentLine(line: string): string | null {
- for (const starter of COMMENT_STARTERS) {
- if (line.startsWith(starter)) {
- return line.replace(starter, "").trim();
- }
- }
-
- return null;
-}
-
-/**
- * Given a script name, read the script file and return
- *
- * @param scriptName The name of the script to read
- * @returns The script data
- */
-async function readScriptData(scriptName: string): Promise {
- const scriptPath = await findScriptPath(scriptName);
-
- if (!scriptPath) {
- throw new CLIError(
- `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 descriptionLines: string[] = [];
-
- for (const line of lines) {
- const commentLine = getCommentLine(line);
- if (!commentLine) {
- break;
- }
-
- descriptionLines.push(commentLine);
- }
-
- const description =
- descriptionLines.length > 0
- ? descriptionLines.join(" ")
- : "No description provided";
-
- return {
- absolutePath: scriptPath,
- name: path.basename(scriptPath),
- description: description.trim(),
- };
-}
-
-/**
- * List the scripts in the rundir or the path to a script if provided
- * @command
- */
-async function ls(scriptName?: string) {
- if (scriptName) {
- const pathname = getScriptPath(scriptName);
- const exists = await scriptExists(pathname);
- if (!exists) {
- throw new CLIError(`Script "${scriptName}" not found.`);
- }
-
- console.log(pathname);
- return;
- }
-
- const currentRundir = getRundirPath();
- const filesInDir = new Glob(`${currentRundir}/*`).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(chalk.dim(currentRundir));
- console.log(sorted.join("\n"));
-}
-
-/**
- * Prints the absolute path of the rundir
- * @command
- */
-async function which() {
- console.log(getRundirPath());
-}
-
-/**
- * Create a new script in the rundir
- * @command
- */
-async function newScript(name: string) {
- const scriptPath = getScriptPath(name);
- const scriptFile = Bun.file(scriptPath);
- let template = DEFAULT_TEMPLATE;
-
- if (await scriptFile.exists()) {
- throw new CLIError(`Script "${name}" already exists.`);
- }
-
- if (TEMPLATE_FILE_PATH) {
- const templateFile = Bun.file(TEMPLATE_FILE_PATH);
- if (await templateFile.exists()) {
- template = await templateFile.text();
- } else {
- console.warn(
- chalk.yellow(`Warn: Template "${TEMPLATE_FILE_PATH}" not found`),
- );
- }
- }
-
- await Bun.write(scriptFile, template);
- await $`chmod +x ${scriptPath}`;
- console.log(scriptFile);
-}
-
-/**
- * Executes a script from the rundir
- * @command
- */
-async function runScript(name: string) {
- const scriptPath = await findScriptPath(name);
-
- if (!scriptPath) {
- console.error(`Script "${name}" not found.`);
- process.exit(1);
- }
-
- const rundir = path.dirname(scriptPath);
- const cwd = path.resolve(rundir, "..");
-
- const proc = Bun.spawn([scriptPath], {
- cwd,
- env: {
- ...process.env,
- RUNDIR_PATH: rundir,
- RUNDIR_CWD: cwd,
- RUNDIR_SCRIPT_PATH: scriptPath,
- },
- stdout: Bun.stdout,
- stderr: Bun.stderr,
- stdin: Bun.stdin,
- });
-
- await proc.exited;
-}
-
-/**
- * Prints the help message
- * @command
- */
-async function help() {
- console.log(
- `
-rundir ${VERSION}
+function help() {
+ console.log(
+ `
Usage: rundir [command]
Commands:
- x Run a script from the nearest rundir (alias: run)
- new Create a new script in the rundir (alias: create)
- ls List all scripts in the rundir
- ls Print the absolute path to a specific script
- which Print the absolute path of the rundir
- help Show this help message
+ 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(),
- );
+ );
}
-/**
- * Parses the command line arguments and returns the positionals
- * @returns The positional arguments for the command
- */
function readArgs(): string[] {
- const { positionals } = parseArgs({
- args: Bun.argv,
- allowPositionals: true,
- strict: true,
- });
+ const { positionals } = parseArgs({
+ args: Bun.argv,
+ allowPositionals: true,
+ strict: true,
+ });
- return positionals.slice(2);
+ return positionals.slice(2);
}
-type Handler = (...args: string[]) => void | Promise;
-type HandlerDescriptor = [number, Handler];
-const COMMAND_HANDLERS: Record = {
- ls: [0, ls],
- help: [0, help],
- which: [0, which],
- new: [1, newScript],
- create: [1, newScript],
- x: [1, runScript],
- run: [1, runScript],
-};
+const args = readArgs();
-async function main() {
- const [rawCommand, ...params] = readArgs();
-
- // No arguments provided - list scripts
- if (rawCommand === undefined) {
- return ls();
- }
-
- const command = parseCommand(rawCommand);
- const [requiredArgs, handler] = COMMAND_HANDLERS[command];
-
- if (params.length < requiredArgs) {
- throw new CLIError(`Missing required arguments for command "${command}"`);
- }
-
- return handler(...params);
+// Default functionality is to list
+if (args.length === 0) {
+ await ls();
+ process.exit(0);
}
-try {
- await main();
-} catch (e) {
- if (e instanceof CLIError) {
- console.error(chalk.red(e.message));
- process.exit(0);
- }
+const command = args[0];
- console.error(chalk.red("An unexpected error occurred:"));
- console.error(e);
- process.exit(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
index 70725ca..ffc08ab 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,28 +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": true,
- "noUnusedParameters": true,
- "noPropertyAccessFromIndexSignature": true,
- "noUncheckedIndexedAccess": true,
- }
+ "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
+ }
}