0.1.0
This commit is contained in:
parent
cc37b06d8b
commit
cc19888102
14 changed files with 529 additions and 322 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -173,3 +173,6 @@ dist
|
|||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
bin
|
||||
build
|
||||
|
|
28
.run/build
28
.run/build
|
@ -1,3 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
#DESCRIPTION# Compile the binary cli
|
||||
bun build --target bun --compile ./src/cli.ts --outfile rundir
|
||||
# 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
|
||||
|
|
3
.run/clean
Executable file
3
.run/clean
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
# Clean build artifacts
|
||||
rm -rf build
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
#DESCRIPTION# Format the repo, writing the formatted files to disk
|
||||
# Format the repo, writing the formatted files to disk
|
||||
bunx biome format . --write
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
#DESCRIPTION# A rundir script
|
||||
# A rundir script
|
||||
echo 'Hello, world!'
|
||||
|
|
24
LICENSE
Normal file
24
LICENSE
Normal file
|
@ -0,0 +1,24 @@
|
|||
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 <https://unlicense.org/>
|
42
README.md
42
README.md
|
@ -26,40 +26,28 @@ $ rundir x build # runs the `build` script in the original `.run` directory
|
|||
|
||||
## CLI
|
||||
|
||||
### List scripts
|
||||
```
|
||||
Usage: rundir [command]
|
||||
|
||||
```shell
|
||||
$ rundir
|
||||
Commands:
|
||||
x <name> Run a script from the nearest rundir (alias: run)
|
||||
new <name> Create a new script in the rundir (alias: create)
|
||||
ls List all scripts in the rundir
|
||||
ls <name> Print the absolute path to a specific script
|
||||
which Print the absolute path of the rundir
|
||||
help Show this help message
|
||||
```
|
||||
|
||||
### Create script
|
||||
## `rdx` alias
|
||||
|
||||
Consider setting an alias for `rundir x` to `rdx` for convenience:
|
||||
|
||||
```shell
|
||||
$ rundir create myscript
|
||||
alias rdx="rundir x"
|
||||
```
|
||||
|
||||
### Run a script
|
||||
Then you can run scripts with `rdx` instead of `rundir x`:
|
||||
|
||||
```shell
|
||||
$ rundir x myscript
|
||||
|
||||
# ... or if you're in the root directory just...
|
||||
$ .run/myscript
|
||||
```
|
||||
|
||||
### Print the path of the current rundir
|
||||
|
||||
```shell
|
||||
$ rundir which
|
||||
```
|
||||
|
||||
### Open a script in your editor
|
||||
|
||||
```shell
|
||||
$ rundir edit myscript
|
||||
```
|
||||
|
||||
### Print help message
|
||||
```shell
|
||||
$ rundir help
|
||||
rdx build
|
||||
```
|
37
biome.json
Normal file
37
biome.json
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
49
bun.lock
Normal file
49
bun.lock
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"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=="],
|
||||
}
|
||||
}
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
16
package.json
16
package.json
|
@ -1,16 +1,22 @@
|
|||
{
|
||||
"name": "rundir",
|
||||
"module": "src/cli.ts",
|
||||
"version": "0.0.1",
|
||||
"name": "@endeavorance/rundir",
|
||||
"type": "module",
|
||||
"module": "src/cli.ts",
|
||||
"version": "0.1.0",
|
||||
"bin": {
|
||||
"rundir": "build/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0"
|
||||
"chalk": "^5.4.1"
|
||||
}
|
||||
}
|
||||
|
|
390
src/cli.ts
390
src/cli.ts
|
@ -1,38 +1,291 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { edit, ls, newScript, runScript, which } from "./scriptFile";
|
||||
import { $, Glob } from "bun";
|
||||
import chalk from "chalk";
|
||||
import pkg from "../package.json" assert { type: "json" };
|
||||
|
||||
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);
|
||||
interface ScriptData {
|
||||
name: string;
|
||||
description: string;
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
function help() {
|
||||
class CLIError extends Error {
|
||||
name = "CLIError";
|
||||
|
||||
constructor(message: string) {
|
||||
super(`rundir error: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const TEMPLATE_FILE_PATH = process.env["RUNDIR_TEMPLATE"] ?? null;
|
||||
const DEFAULT_TEMPLATE = `
|
||||
#!/usr/bin/env bash
|
||||
# Description...
|
||||
`.trim();
|
||||
const VERSION = pkg.version;
|
||||
const RUNDIR_PATH = path.join(process.cwd(), ".run");
|
||||
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 findNearestRundir(fromPath?: string): string | null {
|
||||
const currentDir = fromPath ?? process.cwd();
|
||||
const potentialPath = path.join(currentDir, ".run");
|
||||
|
||||
if (currentDir === path.dirname(currentDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (existsSync(potentialPath)) {
|
||||
return potentialPath;
|
||||
}
|
||||
|
||||
return findNearestRundir(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 makeScriptPath(name: string): string {
|
||||
const rundir = findNearestRundir();
|
||||
|
||||
if (!rundir) {
|
||||
throw new CLIError("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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ScriptData> {
|
||||
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 = await findScriptPath(scriptName);
|
||||
if (pathname === null) {
|
||||
throw new CLIError(`Script "${scriptName}" not found.`);
|
||||
}
|
||||
|
||||
console.log(pathname);
|
||||
return;
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the absolute path of the rundir
|
||||
* @command
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new script in the rundir
|
||||
* @command
|
||||
*/
|
||||
async function newScript(name: string) {
|
||||
const scriptPath = makeScriptPath(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(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
await $`${scriptPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the help message
|
||||
* @command
|
||||
*/
|
||||
async function help() {
|
||||
console.log(
|
||||
`
|
||||
rundir ${VERSION}
|
||||
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
|
||||
x <name> Run a script from the nearest rundir (alias: run)
|
||||
new <name> Create a new script in the rundir (alias: create)
|
||||
ls List all scripts in the rundir
|
||||
ls <name> Print the absolute path to a specific script
|
||||
which Print the absolute path of 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,
|
||||
|
@ -43,60 +296,45 @@ function readArgs(): string[] {
|
|||
return positionals.slice(2);
|
||||
}
|
||||
|
||||
const args = readArgs();
|
||||
type Handler = (...args: string[]) => void | Promise<void>;
|
||||
type HandlerDescriptor = [number, Handler];
|
||||
const COMMAND_HANDLERS: Record<Command, HandlerDescriptor> = {
|
||||
ls: [0, ls],
|
||||
help: [0, help],
|
||||
which: [0, which],
|
||||
new: [1, newScript],
|
||||
create: [1, newScript],
|
||||
x: [1, runScript],
|
||||
run: [1, runScript],
|
||||
};
|
||||
|
||||
// Default functionality is to list
|
||||
if (args.length === 0) {
|
||||
await ls();
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (e) {
|
||||
if (e instanceof CLIError) {
|
||||
console.error(chalk.red(e.message));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error(chalk.red("An unexpected error occurred:"));
|
||||
console.error(e);
|
||||
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);
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
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}`;
|
||||
}
|
|
@ -1,27 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"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
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue