Files
dotfiles/docs/shell/scripts.mdx
T

334 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Scripts
description: The shell-agnostic executables in ~/.local/bin — usage, behavior, and dependencies for each.
---
`dot_local/bin/` holds standalone `sh`, `bash`, and `python3` executables. The `executable_` prefix is a chezmoi attribute that sets the executable bit and is dropped from the target name, so `dot_local/bin/executable_pj` becomes `~/.local/bin/pj`. bash, zsh, and fish all put `~/.local/bin` on `PATH`, so every shell runs the same implementation.
This page covers the general-purpose scripts. The desktop scripts in the same directory are documented with their compositor:
- `niri-launch`, `niri-launch-ai`, `niri-launch-browser`, `niri-launch-terminal`, `niri-launch-webapp`, `cp-appid`: [niri](/desktop/niri)
- `omarchy-*`: [Hyprland](/desktop/hyprland)
## Why some helpers are shell functions
A script runs as a child process, and a child cannot change its parent shell's working directory or read the parent's in-memory history. Helpers that need to do either are therefore defined as shell functions, ported three times: `fish/functions/*.fish`, `bash/functions/*.bash`, and `zsh/functions/*.zsh`.
| Helper | Why it must be a function |
| --- | --- |
| `yy` (and fish's `y`) | cds into the directory yazi exited in |
| `c` | cds when given an argument |
| `cd` | Replaces the `cd` builtin with a zoxide fallback |
| `ghg`, `ghn`, `gho` | cd into a repo that was picked or cloned |
| `clip-last` | Reads the shell's own history (`$history[1]` in fish, `fc` in bash/zsh) |
For `ghg` and `ghn`, the work lives in a script that prints a directory on stdout (and everything else on stderr). The shell function captures that path and cds into it. `ghg`'s script is named `ghg-clone` because an unrelated cargo binary already uses the name `ghg`. `gho` has no script: the function reads the path from `tv ghq-repos` ([television](/tools/television#ghq-repos)).
Several scripts below also exist as fish functions of the same name (`ai`, `c2p`, `extract_agents`, `fab`, `gdc`, `git-summary`, `lc-elir`, `mdcp`, `mdp`, `launch_omp_livediff_overlay`, `gh-sonr-org-migrate`). In fish the function runs instead of the script. Both versions behave the same. See [fish functions](/shell/fish#functions).
## Overview
| Script | Language | One line |
| --- | --- | --- |
| [`ai`](#ai) | sh | Pick an AI CLI with gum and launch it |
| [`atuin-pick`](#atuin-pick) | python3 | atuin history picker for a kitty or tmux split |
| [`c2p`](#c2p) | sh | code2prompt with an fzf-picked template |
| [`extract_agents`](#extract_agents) | sh | Copy all `AGENTS.md` files into a directory tree |
| [`fab`](#fab) | sh | fzf picker for Fabric patterns |
| [`gdc`](#gdc) | sh | Discard all git changes after confirmation |
| [`gh-sonr-org-migrate`](#gh-sonr-org-migrate) | bash | Consolidate `*sonr*` GitHub orgs into `sonr-io` |
| [`ghg-clone`](#ghg-clone) | sh | `ghq get` and print the clone path |
| [`ghn`](#ghn) | sh | Create a private repo, clone it, print the path |
| [`git-summary`](#git-summary) | sh | Aggregate a repo's `AGENTS.md` files with code2prompt |
| [`gop`](#gop) | sh | Open the current repo's web page |
| [`launch_omp_livediff_overlay`](#launch_omp_livediff_overlay) | sh | omp overlay plus a livediff split in kitty |
| [`lc-elir`](#lc-elir) | sh | Run `/leetcode-eli5` via omp on picked solutions |
| [`mdcp`](#mdcp) | sh | Fetch a URL as Markdown, copy it, preview it |
| [`mdp`](#mdp) | sh | fzf Markdown picker with mdfried preview |
| [`mtime-sort`](#mtime-sort) | sh + perl | Sort path lines newest-first by mtime |
| [`omp-session`](#omp-session) | python3 | Resume or start omp for the current repo; backend of the `omp-sessions` cable |
| [`pj`](#pj) | bash | Task runner across every project task source, on a tv cable |
| [`yazi-nvim-open`](#yazi-nvim-open) | sh | yazi opener that targets the host Neovim |
## ai
```sh
ai
```
Shows `gum choose --header 'AI CLI'` with `claude`, `opencode`, `codex`, `hermes`, and `fractal`, then `exec`s the choice. `claude` runs as `claude --dangerously-skip-permissions`; the others run as the bare binary name. Esc or Ctrl-C exits 0.
**Needs:** `gum` and the chosen CLI.
## atuin-pick
```sh
atuin-pick
```
Opens atuin's interactive search (`atuin search -i --filter-mode directory`), limited to history from the current directory. It then `exec`s an interactive `fish`. If you picked a command, the new fish runs `echo '❯ <cmd>'; <cmd>` via `-C` (`--init-command`), so aliases resolve, a `cd` persists, and the prompt appears below the output. Esc starts a plain fish.
It is written for launch contexts where atuin's shell init never ran. It runs `atuin` from `PATH` and seeds `ATUIN_SESSION` from `atuin uuid` if unset.
**Launched by:** <kbd>ctrl+shift+p</kbd> in kitty. `kitty.conf` opens it in a horizontal split (`launch_hs atuin-pick`). Under the kitty-tmux profile, `tmux.conf` runs it with `tmux split-window -v -l 25%` in the current pane's directory. See [kitty](/terminal/kitty) and [atuin](/shell/atuin).
**Needs:** `python3`, `atuin`, `fish`.
## c2p
```sh
c2p
```
Lists `*.hbs` files under `~/.config/code2prompt` with `fd`, lets you pick one with fzf, and `exec`s `code2prompt --template <file>`.
**Needs:** `fd`, `fzf`, `code2prompt`.
## extract_agents
```sh
extract_agents <output-dir>
```
Creates `<output-dir>` and copies every `AGENTS.md` below the current directory into it with `cp --parents`, keeping the relative paths.
**Needs:** `fd`, GNU `cp` (`--parents`).
## fab
```sh
fab
some-command | fab
```
Reads `~/.config/fabric/pattern_explainations.json`. fzf lists pattern names, the preview pane (right, 60%, wrapped) shows each description, and <kbd>ctrl-/</kbd> toggles the preview. The picked pattern runs as `fabric-ai --pattern <name>`, falling back to `fabric` if `fabric-ai` is not installed. Piped stdin is buffered to a temp file and fed to the pattern; the temp file is removed on exit.
**Needs:** `jq`, `fzf`, `fabric-ai` or `fabric`, and the pattern JSON.
## gdc
```sh
gdc
```
Asks `gum confirm "Discard all uncommitted changes? This is irreversible."`. On yes, runs `git reset --hard HEAD` and `git clean -fd`.
**Needs:** `gum`, `git`.
## gh-sonr-org-migrate
```sh
gh-sonr-org-migrate
```
A bash port of `fish/functions/gh-sonr-org-migrate.fish`, with the same flow:
<Steps>
<Step title="Preflight">Checks that `gh`, `gum`, and `jq` exist and that `gh auth status` succeeds.</Step>
<Step title="Discover">Lists your GitHub orgs whose login contains `sonr`, excluding the target `sonr-io`. You pick which to migrate with `gum choose --no-limit`.</Step>
<Step title="Transfer">For each repo in each org, runs `gh api -X POST repos/<org>/<repo>/transfer -f new_owner=sonr-io`. If the name already exists in `sonr-io`, `gum input` asks for a new name (default `<org>-<repo>`). An empty name skips the repo.</Step>
<Step title="Settle">Waits 30 seconds (`gum spin`), because GitHub transfers are asynchronous.</Step>
<Step title="Delete">For every selected org that had no failed transfers and now has zero repos, asks `gum confirm` and then `gh api -X DELETE orgs/<org>`.</Step>
</Steps>
**Needs:** `gh` authenticated as owner of every org involved, `gum`, `jq`.
:::warning
In fish, run this script by its full path (`~/.local/bin/gh-sonr-org-migrate`). The fish file of the same name is a script body with no function in it, so typing just the name runs the fish flow and then this script. See [fish](/shell/fish#functions).
:::
## ghg-clone
```sh
ghg-clone <repo>
```
Runs `ghq get <repo>`, with its output sent to stderr. It then works out the on-disk path: it strips the scheme, any `user@`, the scp-style colon, and a trailing `.git`, and prefixes `github.com/` when the input has no host. It checks that `$(ghq root)/<host>/<user>/<repo>` exists, runs `zoxide add` on it if zoxide is installed, and prints the path.
Accepts `https://…`, `ssh://…`, `git@host:user/repo`, `host/user/repo`, or `user/repo`.
**Used by:** the `ghg` shell function, which cds into the path, clears the screen, and runs `onefetch`. The fish function `gtm` also uses it to clone migrated repos.
**Needs:** `ghq` (`GHQ_ROOT=~/Code`, exported by fish, zsh, and bash), optional `zoxide`.
## ghn
```sh
ghn <repo-name>
```
Creates a new **private** repo and clones it:
- **prad.codes** (Gitea): pick an org from `tea organizations list`, then `tea repos create --private` and `ghq get https://prad.codes/<org>/<name>`.
- **github.com**: pick the owner `prdlk` or `wltbs`, then `gh repo create <owner>/<name> --private` and `ghq get`.
You pick the host with `gum choose`. The script prints the clone directory, and the `ghn` shell function cds into it.
**Needs:** `gum`, `ghq`, and `tea` or `gh`.
## git-summary
```sh
git-summary [code2prompt flags…]
```
From anywhere inside a git repo, runs `code2prompt <repo-root> --include AGENTS.md --include "**/AGENTS.md" --template ~/.config/code2prompt/agents-md.hbs`. Extra flags such as `--output-file` or `--tokens` are passed through.
**Needs:** `git`, `code2prompt`, and the `agents-md.hbs` template.
## gop
```sh
gop # origin, else upstream, else first remote
gop upstream # a named remote
gop owner/repo # github.com/owner/repo
gop <url> # any https/ssh/scp remote URL
```
Opens a repo's web page for any forge whose URL layout is `https://<host>/<owner>/<repo>`, such as github.com and prad.codes. It reduces any remote form to `<host>/<owner>/<repo>`: it drops credentials, drops the port on `ssh://` and `git://` remotes, keeps the port and plain `http` on `http://` remotes, and strips `.git`. It then runs `$BROWSER` (default `xdg-open`) on the result.
**Needs:** `git`, a browser or `xdg-open`.
## launch_omp_livediff_overlay
```sh
launch_omp_livediff_overlay
```
Inside kitty with remote control enabled, runs `kitty @ launch --type=overlay-main --cwd=current omp`, then opens `livediff` in a `vsplit` next to that window with `--keep-focus`. It relies on `$KITTY_LISTEN_ON` rather than a hard-coded socket, because kitty appends `-PID` to `listen_on` socket names. It exits with an error if the variable is unset.
**Needs:** kitty with `allow_remote_control`/`listen_on` ([kitty](/terminal/kitty)), `omp` ([AI agents](/ai)), `livediff`.
## lc-elir
```sh
lc-elir [root]
```
Finds numbered LeetCode solution files (`^\d+\..+\.(js|ts)$`, for example `1365.some-slug.js`) under `root` (default `.`). fzf multi-selects them, showing the last three path segments with a `bat` preview of the first 40 lines. Each pick runs `omp -p "/leetcode-eli5 <file>"`.
**Needs:** `fd`, `fzf`, `bat`, `omp`, and the `/leetcode-eli5` omp command.
## mdcp
```sh
mdcp <url>
```
Fetches the URL as Markdown with `curl.md`, copies it with `wl-copy` or `pbcopy`, then pastes it back into `mdfried` for a rendered preview.
**Needs:** `curl.md`, `mdfried`, and `wl-clipboard` or `pbcopy`.
## mdp
```sh
mdp
```
Lists `*.md` files up to 3 directories deep with `fd`. fzf shows an `mdfried` preview (right, 60%, wrapped), and the pick opens in `mdfried`.
**Needs:** `fd`, `fzf`, `mdfried`.
## mtime-sort
```sh
… | mtime-sort [--repo]
```
Reads lines on stdin and prints them newest-first by the mtime of each line's last tab-separated field (the whole line when it has no tab). With `--repo`, it stats `<path>/.git/index` first, so a repo sorts by its last git activity. Lines whose path does not exist are dropped. Ties keep input order.
**Used by:** every custom [television](/tools/television) cable except `agents` and `omp-sessions`, and `pj --list`. tv layers match quality and frecency on top of this order.
**Needs:** `perl`.
## omp-session
```sh
omp-session
```
Starts or resumes omp for the current git repo by fixed rules, so <kbd>ctrl-a</kbd> never asks when the answer is obvious.
| HEAD | Does |
| --- | --- |
| Default branch | Resumes the newest session on it when that session was written less than 3 hours ago. Otherwise opens the `omp-sessions` [television](/tools/television#omp-sessions) cable over all of the repo's sessions; <kbd>enter</kbd> resumes the pick, <kbd>esc</kbd> returns to the prompt. With no sessions at all it starts a new one |
| Any other branch | Resumes the newest session on that branch, or starts a new one |
| Detached | Opens the `omp-sessions` cable (a new session when there are none) |
| Outside a git repo | Plain `omp` |
Resuming runs `omp --resume <session file>`; omp switches to the session's own cwd. `omp-session --new` runs plain `omp`; it is the cable's <kbd>ctrl-n</kbd> action and marks `OMP_SESSION_STATE` like a resume does, so the prompt is not moved after a new session.
**Sessions.** Every `*/*.jsonl` under `$PI_CODING_AGENT_DIR/sessions` (default `~/.omp/agent/sessions`) whose `session` header `cwd` is the worktree root or below it, excluding worktrees nested inside it. Session age is the file's mtime (the last write).
**Branches.** omp does not record the git branch. A session's branch is the one HEAD was on when the file was last written, replayed from the worktree's `git reflog` (`checkout: moving from A to B`, `Branch: renamed`, `rebase (finish): returning to`). Sessions older than the oldest reflog move belong to the branch that move left; with no moves they all belong to the current branch. The default branch is `origin/HEAD`, else `init.defaultBranch`, `main`, or `master`, whichever exists.
**Entry points.** The cable calls back into the script:
| Command | Does |
| --- | --- |
| `omp-session --list` | Prints `<age> <branch> <title>[ · <subdir>]<TAB><session file>` per session, newest first. The title is omp's session title, else the first user message |
| `omp-session --preview FILE` | Title, last-write time, cwd, then the text of every user and assistant message (thinking, tool calls, and tool results skipped) |
| `omp-session --resume FILE` | Writes `ran` to `$OMP_SESSION_STATE` when set, then `exec omp --resume FILE` (enter) |
**Prompt redraw.** The launcher runs `tv omp-sessions --inline` with `OMP_SESSION_STATE` set to a temp file. When the picker is cancelled (no `ran`), it prints `\033[A` to `/dev/tty` so the shell redraws the prompt in place; the key bindings just repaint.
**Launched by:** <kbd>ctrl-a</kbd> in fish and zsh. <kbd>ctrl-n</kbd> at the prompt runs `omp` directly, not this script.
**Needs:** `python3`, `git`, `omp`, `tv`.
## pj
```sh
pj [extra args…]
```
A task runner that searches every task source in the project and lets you pick one in the `pj` [television](/tools/television#pj) cable.
**Sources.** Each is listed only when its runner is installed:
| Source | Found in | Runner |
| --- | --- | --- |
| npm scripts | `package.json` | package manager from `packageManager`, else from the lockfile (`bun.lock[b]`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`, searched upward), else `npm`, then `<pm> run` |
| Make | `GNUmakefile`, `makefile`, `Makefile` | `make`. Targets come from a text parse of rules and `.PHONY`; `make -p` is avoided because it would run `$(shell …)` |
| just | `justfile`, `.justfile`, `Justfile`, `JUSTFILE` | `just` |
| mise | `mise.toml`, `.mise.toml`, `mise/config.toml`, `.mise/config.toml`, `.config/mise/config.toml`, or a `mise-tasks`/`.mise/tasks`/`mise/tasks` dir | `mise run`. Hidden tasks are skipped, and tasks inherited from parent directories are filtered out |
| devbox | `devbox.json` `shell.scripts` | `devbox run` |
| poe | `pyproject.toml` `[tool.poe.tasks]` | `poe`, else `uv run poe` |
| pdm | `[tool.pdm.scripts]` (skips `_*` and `post_*`) | `pdm run` |
| rye | `[tool.rye.scripts]` | `rye run` |
| taskipy | `[tool.taskipy.tasks]` | `task`, else `uv run task` |
| hatch | `[tool.hatch.envs.<env>.scripts]` as `env:name` | `hatch run` |
| console scripts | `[tool.poetry.scripts]`, `[project.scripts]` | `poetry run` for Poetry projects, else `uv run` |
**Scanning.** Walks from `$PWD` up to the git toplevel, inclusive. Outside a repo it stops at the first directory that produced tasks.
**Picker.** `pj` writes the records to a temp file and runs `tv pj --inline --source-command "cat <tmp>" --input-header <project root>`. Rows are tagged with the tool (`npm`, `pnpm`, `bun`, `make`, `just`, `mise`, `poe`, `py`, …), so typing a tool name filters to it. A directory column appears when more than one project contributed tasks. Tasks are sorted by their manifest's mtime, newest first, through `mtime-sort`. The preview shows the task body: the npm script string, the Make recipe, `just --show`, `mise tasks info`, the devbox script, or the pyproject entry.
**Entry points.** The cable calls back into the script:
| Command | Does |
| --- | --- |
| `pj --list` | Prints tab-separated records: display, kind, target, root, runner, manifest |
| `pj --preview KIND TARGET ROOT FILE` | Prints the task body for the preview pane |
| `pj --run KIND TARGET ROOT RUNNER` | Runs the task (enter) |
**Run.** cds into the task's directory and `exec`s the runner. Extra arguments given to `pj` travel to the task through `$PJ_EXTRA` (shell-quoted), with `--` inserted for `npm run`, `mise`, and `devbox`. Tasks named `dev`, `start`, `serve`, `watch`, or namespaced variants like `dev:web` are wrapped in `openlogs` (found on `PATH`, via `mise which`, or in mise's node installs) for a live log tail. Without openlogs they run unwrapped, with a note.
**Env.** `PJ_QUIET=1` suppresses the "no task sources found" error (exit status stays 1). The <kbd>down</kbd> bindings use it. `PJ_STATE=FILE` makes `pj` write `open` to `FILE` before launching the picker, and `pj --run` writes `ran`; the file stays empty when there are no task sources. The key bindings read it to tell no tasks, a cancelled picker, and a task that printed output apart and redraw the prompt to match. `pj` itself does no cursor handling, so typing `pj` behaves like any command.
**Launched by:** <kbd>ctrl-p</kbd>, and <kbd>down</kbd> on an empty prompt, in fish, zsh, and bash.
**Needs:** `bash`, `tv`, `mtime-sort`, `jq`, `python3` 3.11+ (for `tomllib`), plus whichever runners the project uses.
## yazi-nvim-open
```sh
yazi-nvim-open [vsplit|split|tabedit] <file>…
```
yazi's `edit` opener. When run inside a Neovim terminal (`$NVIM` set), each file's absolute path goes to the **host** Neovim through `nvim --server "$NVIM" --remote-expr 'v:lua.EditFromYazi(mode, path)'` (defined in the nvim config's `yazi_session.lua`). Outside Neovim it `exec`s a normal `nvim`, and the split modes do nothing. Set `YAZI_OPEN_TRACE=<file>` to log each call.
**Used by:** `yazi.toml` (the `edit` opener) and `keymap.toml` (<kbd>ctrl-v</kbd> vsplit, <kbd>ctrl-x</kbd> split). See [yazi](/tools/yazi).
**Needs:** `nvim`, `realpath`.