Compare commits

...
84 Commits
Author SHA1 Message Date
rootiest aa617ef455 Merge pull request 'docs: document main's branch protection rules in CONTRIBUTING.md' (#180) from docs/branch-protection-rules into main
Reviewed-on: #180
2026-09-25 02:57:40 +00:00
rootiest c82132aed0 docs: document main's branch protection rules in CONTRIBUTING.md
CI / test (pull_request) Successful in 18s
CI / github-mirror (pull_request) Skipped
CI / docs (pull_request) Successful in 1m21s
2026-09-24 22:53:11 -04:00
fishconfig-bot 15d3a7e4dc chore(docs): regenerate manual, man page, and component registry 2026-09-25 02:51:37 +00:00
rootiest ec700cece1 Merge pull request 'feat(agents-init): retire CLAUDE.md, discover and normalize AGENTS.md in every subdirectory' (#177) from docs/retire-claude-md-for-agents-md into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m25s
CI / docs (push) Successful in 4m11s
2026-09-24 04:56:01 +00:00
rootiest 86aea9d5ef Merge pull request 'feat(gi): bundled boilerplate fallback + -c/--custom template flag' (#179) from feat/gi-boilerplate-fallback-and-custom into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m26s
CI / docs (push) Failing after 4m8s
2026-09-24 04:54:37 +00:00
rootiest 1f3c8f41e7 Merge pull request 'fix(gi): stop --stdout from leaking to .gitignore, rework flags' (#178) from fix/gi-stdout-flag-rework into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m21s
CI / docs (push) Failing after 3m59s
2026-09-24 04:53:32 +00:00
rootiest 911c6e95ed feat(docs): add manual-section CLASSIFICATION tag, wire it into the build
CI / github-mirror (pull_request) Skipped
CI / test (pull_request) Successful in 2m26s
CI / docs (pull_request) Successful in 1m17s
A function with a dedicated manual section (docs/manual/16-agent-tooling.md,
so far) now carries manual-section(<slug>) in its own CLASSIFICATION line
instead of relying on a NOTES pointer nobody can grep for. Applied to
agents-init and agents-vault, both pointing at 16-agent-tooling.

docs/build-manual.py: _resolve_manual_section reads the target page's own
manTitle/title fresh at build time rather than duplicating a section
number into the tag, so a renumbered section (like this one, twice
already) never requires touching the tag -- only the slug (the filename)
does, and only if the page itself is renamed. render_entry and
render_entry_site both gained an optional root parameter and now emit a
'See also' line (plain text + relative path for the man page, a real
markdown link on the site) whenever the tag resolves; omitted silently
when it doesn't (a build isn't the place to fail on a bad slug).

docs/verify-manual.py: unit tests for the new resolver and both renderers,
plus a real-data scan (test_real_manual_section_tags_resolve) that fails
the suite if any function's manual-section(<slug>) tag points nowhere --
the actual enforcement half of the convention, since the build stays
silent about it.

docs/function-classification-schema.md, CONTRIBUTING.md: documents the
tag, and widens CLASSIFICATION's own framing from strictly hazard/shadow
tags to general-purpose (the user's call, not mine to make unilaterally --
scope-broadening an existing convention). The 'Dedicated manual sections'
subsection (added earlier this branch) now names the tag as the
machine-checked half of that convention, with NOTES demoted to a
nice-to-have for a header-only reader.
2026-09-24 00:50:36 -04:00
rootiest 8b08b4d5e5 feat(gi): bundled boilerplate fallback + -c/--custom template flag
CI / test (pull_request) Skipped
Boilerplate mode used to hard-error when $GITIGNORE_BOILERPLATE was unset,
so gi -b/-c only worked for users with a personal template configured
(typically via .user-dots). Add a resolution chain:

  1. -c/--custom PATH, if given
  2. $GITIGNORE_BOILERPLATE, if set
  3. bundled standard template (data/gi/boilerplate.gitignore)

The bundled template covers common OS junk, scratch/debug/temp dirs, and
AI tool session state (.claude*, .gemini*, .antigravity*, .agy*, .agents*,
.remember*) -- but deliberately does NOT ignore CLAUDE.md/AGENTS.md/
GEMINI.md/ANTIGRAVITY.md themselves, since committing those is increasingly
normal and agents-init already owns their placement.

-c implies boilerplate mode (like -b) so `gi -c template` works standalone.

Also fixes a latent bug in gi's final line: `test $needs_git -eq 1; and
gitignore-scrub` leaked its own boolean as the function's exit status
whenever needs_git was 0, so e.g. `gi -o python` returned 1 on success.
Same seam this change already touches; fixed with an explicit `return 0`.
2026-09-24 00:41:34 -04:00
rootiest 1d714a84d0 docs(agent-tooling): add scenario reference table, cross-reference from code
CI / github-mirror (pull_request) Skipped
CI / test (pull_request) Successful in 2m24s
CI / docs (pull_request) Successful in 1m37s
Adds a 'Scenario reference' subsection to docs/manual/16-agent-tooling.md:
two ruled tables covering every combination of what a directory can hold
(only AGENTS.md, only CLAUDE.md, both identical, both different, an
inverted mirror, an already-settled symlink) crossed with whether the
file is deliberately git-tracked, plus the settled-mirror/later-arrival
case separately. Verified against build-manual.py's actual table parser
and the generated Starlight site output, not just visual inspection.

Also closes the gap this section itself pointed out: nothing previously
linked a reader of agents-init.fish or agents-vault.fish's own doc-header
to this page, and nothing told a future contributor the page has to be
kept in sync. Adds:
- CONTRIBUTING.md: a new 'Dedicated manual sections for complex
  subsystems' subsection documenting the pattern in general (when to use
  one, and the update-it-in-the-same-change obligation verify-manual.py
  cannot check for you).
- functions/agents-init.fish, functions/agents-vault.fish: a NOTES
  pointer to the section from each function's own header, so a reader
  who only sees the header still finds the fuller page.
2026-09-23 22:10:36 -04:00
rootiest e01e83bd36 docs: add AI Agent Tooling manual section
CI / github-mirror (pull_request) Skipped
CI / test (pull_request) Successful in 2m26s
CI / docs (pull_request) Successful in 1m14s
Documents agents-init/agents-vault's concept, purpose, and complete
behavior as its own manual section (16), separate from the auto-generated
function reference: the AGENTS.md convention and CLAUDE.md retirement,
the AGENTS/ sub-repository (layout, versioning, hooks), per-directory
discovery and its four-state normalization, the two safety mechanisms
(discovery containment, deliberately-tracked-file protection), the
plans/specs/devlogs wiring, and the launch lifecycle.

Inserted before Attribution/License (now 17/18) rather than mid-document,
since that's the only placement that doesn't touch any of the manual's
prose cross-references to other section numbers. docs/fish-config.index
updated to match (new keywords, renumbered attribution/license entries;
agents-init and agy were already indexed to their own function-reference
entries and are left pointing there, not redirected to this new page).
2026-09-23 21:47:26 -04:00
rootiest 82bea2f539 fix(gi): stop --stdout from leaking to .gitignore, rework flags
CI / github-mirror (pull_request) Skipped
CI / docs (pull_request) Successful in 20s
CI / test (pull_request) Successful in 2m16s
--stdout silently fell through to the append path whenever gi ran in
its default (no-args) mode, since only the direct-target branch ever
checked it. A fresh-repo `gi --stdout` wrote boilerplate straight to
.gitignore instead of printing it.

Flags also get reworked for consistency:
- -s/--silent: suppress progress output only, errors and prompts still show
- -o/--stdout: print generated content to stdout instead of .gitignore (was -s)
- -f/--force: bypass the interactive prompt, proceed with no patterns

stdout mode also no longer requires a git repo, since it never
touches .gitignore.
2026-09-23 21:40:25 -04:00
rootiest de92277226 fix(agents-init): use --literal-pathspecs so glob characters in a path can't false-match tracked files
CI / github-mirror (pull_request) Skipped
CI / test (pull_request) Successful in 2m28s
CI / docs (pull_request) Successful in 1m21s
2026-09-23 21:07:57 -04:00
rootiest 2baa91f275 feat(agents-init): protect deliberately git-tracked instruction files from being replaced
A real AGENTS.md/CLAUDE.md that is in git's index, in a project with a
non-empty .gitignore, is now left in place (with a stderr warning) by
steps 2 and 4 of _agents_init_sync_instructions instead of being moved
into AGENTS/ and symlinked. Discovery also prunes build/, dist/, out/
and target/ outright.
2026-09-23 20:59:55 -04:00
rootiest 6a410a0fce fix(agents-init): contain discovery to the project tree, prevent silent overwrite of real files
CI / test (pull_request) Successful in 2m23s
CI / github-mirror (pull_request) Skipped
CI / docs (pull_request) Successful in 1m19s
2026-09-23 20:12:41 -04:00
rootiest c3f134b5b4 fix(agents-init): tag grep in CLASSIFICATION header 2026-09-23 20:00:52 -04:00
rootiest 4e5f0078ce docs: drop CLAUDE.md wording from wrapper and manual prose 2026-09-23 19:52:25 -04:00
rootiest 4240a04754 fix(agents-init): clean up stale anchored gitignore patterns on migration 2026-09-23 19:46:16 -04:00
rootiest ecfb93a818 feat(agents-init): discover and normalize AGENTS.md/CLAUDE.md in every subdirectory 2026-09-23 19:36:24 -04:00
rootiest a776d4d12d fix(agents-init): add missing branch test coverage and explicit terminal return 2026-09-23 19:32:36 -04:00
rootiest 731661b581 feat(agents-init): add per-directory AGENTS.md/CLAUDE.md sync helper 2026-09-23 19:25:01 -04:00
rootiest c48af2fa23 Merge pull request 'ci: push docs-regen commit with a real account token, not the default bot' (#176) from ci/bot-push-token into main
Reviewed-on: #176
2026-09-23 22:55:11 +00:00
rootiest 028478940f ci: push docs-regen commit with a real account token, not the default bot
CI / test (pull_request) Successful in 2m40s
CI / github-mirror (pull_request) Skipped
CI / docs (pull_request) Successful in 20s
The default `secrets.GITEA_TOKEN` is Gitea Actions' built-in synthetic
bot identity, not a whitelistable user account. main's branch protection
rejects its pushes outright regardless of retries (run 983, run 990) --
the retry/rebase loop in the next step was built for a non-fast-forward
race (run 976), not a bare permission rejection, so it can't recover
from this.

Point the docs job's checkout token at BOT_PUSH_TOKEN, a PAT on the
already-bypass-whitelisted rootiest account, so the later push
succeeds. Commit authorship and GPG signing (fishconfig-bot) are set
separately via git config a few steps later and are unaffected -- push
auth and commit identity are independent.
2026-09-23 18:50:30 -04:00
rootiest 27b6f5d263 Merge pull request 'docs: rename to Rootiest Fish Configuration in README and site title' (#175) from docs/rootiest-fish-configuration-title into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m28s
CI / docs (push) Failing after 4m5s
2026-09-23 21:14:18 +00:00
rootiest 59ace6fe6b docs: rename to Rootiest Fish Configuration in README and site title
CI / test (pull_request) Successful in 2m27s
CI / github-mirror (pull_request) Skipped
CI / docs (pull_request) Successful in 1m15s
Both said the generic 'Fish Shell Configuration' -- the site nav already
reads 'Rootiest Fish Config'. Reworded README's opening line to match
the sentiment, not just the name swap.
2026-09-23 16:35:37 -04:00
rootiest 344ee60acd Merge pull request 'docs: reword README's Documentation Site heading to Documentation Wiki' (#174) from docs/readme-documentation-wiki-wording into main 2026-09-23 20:30:12 +00:00
rootiest 1b9f1558c4 docs: reword README's Documentation Site heading to Documentation Wiki
CI / test (pull_request) Successful in 19s
CI / github-mirror (pull_request) Skipped
CI / docs (pull_request) Successful in 1m23s
Missed in the earlier site-to-wiki wording sweep -- line 8's Project Wiki
link already used the new term, this heading further down didn't.
2026-09-23 16:28:54 -04:00
rootiest ea1575f902 Merge pull request 'ci: gate docs job on PRs by label/path, gate test job by label too' (#173) from ci/label-and-path-gated-docs-and-tests into main
Reviewed-on: #173
2026-09-23 20:27:18 +00:00
rootiest 4797af85f3 ci: gate docs job on PRs by label/path, gate test job by label too
CI / test (pull_request) Successful in 2m34s
CI / github-mirror (pull_request) Skipped
CI / docs (pull_request) Successful in 24s
Adds scripts/** to the push path filter -- it was missing entirely, so
that directory never triggered CI regardless of what changed there.

pull_request no longer has a paths: filter (moved that check inside each
job, in shell, since a label-only PR with no relevant diff still needs
to trigger the workflow for the job-level label check to ever run).
Added labeled/unlabeled to pull_request types for the same reason.

Splits the old build-docs job into a docs job with two sections: doc
tests/build (generate concat, verify-manual.py, compile man page) run
whenever relevant on any event; publish (site build, Cloudflare deploy,
commit-back) is step-gated to push/dispatch only, as before. The docs
job now also runs on a PR when it's labeled Kind/Documentation or
Area/Docs, or its diff touches docs/manual/**, docs/build-manual.py,
docs/manualtools.py, docs/verify-manual.py, or docs/site/**.

test gains the same shape: also runs on a PR labeled Kind/Testing,
Area/Tests, Area/CI, or Area/Scripts, independent of what it touches.

docs no longer needs: test. main's branch protection already requires
test to pass before a PR merges, so by the time a push-to-main reaches
this job, test has necessarily already passed; re-checking it here
would be redundant. Leaves the same gap as
block_admin_merge_override=false on that rule: a direct admin push
bypasses it, an accepted trust boundary, not a new one.
2026-09-23 16:18:29 -04:00
rootiest 6074687a80 Merge pull request 'feat: add gitignore-scrub to catch tracked files newly matched by .gitignore' (#172) from feat/gitignore-scrub into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m31s
CI / build-docs (push) Failing after 4m20s
Reviewed-on: #172
2026-09-23 20:04:06 +00:00
rootiest 7e3d48ddac feat: add -r/--reset, -f/--force, -i/--individual to gitignore-scrub
CI / github-mirror (pull_request) Skipped
CI / test (pull_request) Successful in 2m15s
CI / build-docs (pull_request) Skipped
-r clears the repo's skip list first so declined files are reconsidered.
-f untracks every pending match immediately, no prompt. -i prompts once
per file instead of once for the whole group. -w, -f, -i are mutually
exclusive (argparse --exclusive), -r combines with any of them.
2026-09-23 15:52:01 -04:00
rootiest 37fea155b3 chore: untrack fisher-managed files left tracked since initial commit
CI / github-mirror (pull_request) Skipped
CI / test (pull_request) Successful in 2m36s
CI / build-docs (pull_request) Skipped
completions/fisher.fish and functions/fisher.fish match the Fisher-managed
ignore rule added in c77a52a but were never scrubbed from the index. Found
by gitignore-scrub itself (gi fish). Files remain on disk, untracked only.
2026-09-23 15:44:24 -04:00
rootiest 355688c134 feat: add gitignore-scrub to catch tracked files newly matched by .gitignore
CI / github-mirror (pull_request) Skipped
CI / test (pull_request) Successful in 2m26s
CI / build-docs (pull_request) Skipped
Standalone function, not gi-private: default mode prompts once for all
tracked-but-ignored files and offers git rm --cached, remembering a
decline per-path in the repo's local git config (gitignore-scrub.skip)
so the same file isn't re-asked. -w/--warn is read-only (prints Warning
lines, no prompt, no mutation) for non-interactive callers like a git
hook. Skips silently above $GITIGNORE_SCRUB_LIMIT tracked files (default
5000) to avoid latency on huge repos.

gi now calls gitignore-scrub at the end of any run that touched
.gitignore.
2026-09-23 14:59:58 -04:00
rootiest 9e8d29cc30 Merge pull request 'fix: guard and document the external tools PR #168 flagged as unguarded' (#170) from docs/guard-external-deps into main
CI / github-mirror (push) Skipped
CI / test (push) Failing after 53s
CI / build-docs (push) Skipped
2026-09-23 03:00:00 +00:00
rootiest af65092102 Merge pull request 'ci: retry the generated-docs push through a rebase on rejection' (#171) from ci/retry-docs-commit-push into main 2026-09-23 02:59:52 +00:00
rootiest 35a48ac868 ci: retry the generated-docs push through a rebase on rejection
build-docs's auto-commit step was seen rejected as non-fast-forward
(run 976, sha 3bbda31): npm ci + astro build + the Cloudflare Pages
deploy ahead of it can take several minutes, long enough for another
PR to merge into main first. A bare `git push` has no way to recover
from that -- the whole job just fails, even though every real step
(tests, manual verification, man page, site build, deploy) already
succeeded.

This commit only ever touches three generated files
(fish-config.md/.1, the component registry), so a rebase onto
whatever landed is always mechanical -- retry push up to 3 times,
rebasing onto origin/main between attempts. Ends on an explicit
`test "$pushed" -eq 1` rather than trailing off the for loop, so a
run that exhausts all three retries still fails loudly instead of
reporting success.
2026-09-22 22:54:47 -04:00
rootiest 8dcbc62359 fix: guard and document the external tools PR #168 flagged as unguarded
PR #168's Notes section named several functions with a real
external-tool dependency that no `type -q`/`command -q`/`command -v`/
`which` guard covers anywhere in the tree, deliberately left out of
DEPENDENCIES to avoid breaking test_dependencies_resolve. Adds the
guard each was missing, then declares the dependency now that it
resolves:

- bkg, detach: nohup
- gitui: gitui (self-shadow; type -q -f to skip the function itself)
- play-media: mpv, vlc -- already guarded via `type -q -f $p` in a
  loop, just never recognized as one (see next point)
- steam-dl: systemd-inhibit, steam
- wake-lock: systemd-inhibit
- split, spwin, tab: wezterm, konsole (kitty already declared)

docs/verify-manual.py's guard-detection regex only matched `type -q
<name>` immediately, so `type -q -f $p` (the `-f` flag excludes
functions from the match, needed wherever a wrapper shadows a binary
of its own name) was invisible to it -- both as a direct guard and
through the loop-variable indirection. Broadened both patterns to
skip over any flags between `-q` and the name/variable.

split/spwin/tab dispatch on $TERM/$TERM_PROGRAM/$KONSOLE_VERSION to
pick which terminal-specific binary to call, per this repo's C4
convention -- but those env vars only prove the terminal type, not
that its CLI binary is on $PATH: they propagate over ssh, so sshing
out from Kitty/WezTerm inherits the var on a remote host that never
installed the binary. Same latent gap in clone/clonet, whose
clone-in-kitty is a function Kitty's own shell integration injects,
not present on a remote shell that only inherited $TERM. All five now
check the actual thing they are about to call, not just the env var
that selects it.

Also guards and documents three more real, previously-undeclared
dependencies found by the same audit, unrelated to PR #168's named
list but the identical pattern: fast-cli (fast), lock (loginctl),
ports (lsof), screensleep (busctl).

docs/fish-config.md regenerated to match.
2026-09-22 22:40:33 -04:00
rootiest 17a95abebe Merge pull request 'ci: trigger CI on PR creation, gate build-docs to push/dispatch only' (#169) from ci/pr-trigger into main 2026-09-23 02:33:12 +00:00
rootiest b03ba7490e ci: trigger CI on PR creation, gate build-docs to push/dispatch only
Adds a pull_request trigger (same path filters, YAML anchor to share
them with push) so branches get CI feedback before merge instead of
only after. build-docs is excluded on pull_request: it auto-commits
generated docs straight to the checked-out ref and deploys the
Cloudflare Pages production site with --branch=main, neither of which
should run against PR content that is not main yet.
2026-09-22 22:32:11 -04:00
rootiest 3bbda31eff Merge pull request 'docs(functions): add DEPENDENCIES sections to doc headers' (#168) from claude/function-docs-dependencies-bfnnag into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 3m18s
CI / build-docs (push) Failing after 4m42s
Reviewed-on: #168
2026-09-23 02:31:09 +00:00
Claude fceddfc43f docs: add DEPENDENCIES sections to function doc headers
Documents what each function needs for full functionality -- other
repo functions it calls, and external CLI tools it uses or falls
back gracefully without (e.g. rm/trash, ls/eza+lsd) -- matching the
existing CLASSIFICATION convention's placement and the ~20 functions
that already carried this label.

Also broadens verify-manual.py's dependency-resolution check to
recognize this repo's other existence-check idioms (command -q,
command -v, which -- not just type -q) and dng2avif's dynamic
type -q $cmd loop, since several genuine dependencies (eza, lsd,
fastfetch, fd, ps, ...) are only ever guarded that way.
2026-09-22 22:22:27 -04:00
rootiest b10f076105 Merge pull request 'feat(functions): add -l/--local flag to mkrep' (#167) from feat/mkrep-local-flag into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m47s
CI / build-docs (push) Successful in 4m39s
2026-09-22 07:18:17 +00:00
rootiest 8b1689aa82 Merge pull request 'fix: silence stray CI/tool noise, fix real --silent leak in agents-vault/agents-init' (#166) from fix/ci-log-noise-cleanup into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m55s
CI / build-docs (push) Successful in 4m30s
2026-09-22 07:17:49 +00:00
rootiest 7724ec6d31 Merge pull request 'fix(tests): silence mkrep's git-init default-branch hint in CI' (#165) from fix/mkrep-test-defaultbranch-hint into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m43s
CI / build-docs (push) Successful in 5m12s
2026-09-22 07:17:27 +00:00
fishconfig-bot cde9396e70 chore(docs): regenerate manual, man page, and component registry 2026-09-22 07:14:38 +00:00
rootiest 9614ee17e1 fix(ci): stop chasing apt-utils' own debconf notice, document why
Verified live, twice: bundling apt-utils with another package (first
attempt) and installing it fully alone, first (second attempt) both
still print debconf's "delaying package configuration" notice exactly
once per job. It fires during apt-utils' own first-ever install, before
debconf considers it "installed" -- no install ordering this workflow
controls can pre-seed that. Reverted to the simpler bundled form (the
separate-step version added a step for zero measured benefit) and
documented it as accepted/unfixable, same class as the runs-on/
checkout-hint noise already left alone.
2026-09-22 02:58:55 -04:00
rootiest b0e09ef2a3 feat(functions): add -l/--local flag to mkrep
Add -l/--local flag to enforce strictly local repository creation in mkrep, overriding and ignoring any remote flags or environment variables that would link to or create a remote.
2026-09-22 02:54:14 -04:00
rootiest 135c375dcf fix(ci): install apt-utils in its own step, not bundled
The previous fix bundled apt-utils into the same apt-get install as
software-properties-common. Verified against a live CI run: debconf
still printed "delaying package configuration" once per job, because
apt-utils configures in the same transaction as everything else and
still lands after at least one other package -- bundling doesn't help.
Installing it alone, first, actually finishes it before anything else
runs.
2026-09-22 02:46:36 -04:00
rootiest af7502d6b1 fix(ci): silence CI/tool noise found auditing the test-job log
An audit of a full green run's test + build-docs logs (agy scan, spot-
checked) turned up stray output beyond the mkrep git-init hint already
fixed. Two real bugs, plus CI-config cosmetics:

- agents-vault.fish/agents-init.fish: `set -l x (some_fish_function ...)`
  command substitutions do not inherit a caller-scoped stderr redirect in
  fish (proven with a two-line repro: `outer 2>/dev/null` where outer
  does `set -l x (inner)` still leaks inner's stderr to the real
  terminal). This let _agents_repo_ensure_symlink's and
  _agents_repo_sync's raw internal error messages leak past `--silent`
  for real users too, always duplicating the clean summary message each
  caller already echoes on failure. Fixed at all 4 call sites by adding
  an explicit `2>/dev/null` directly on each command substitution, since
  a redirect on the outer call cannot reach it.
- ci.yml: apt-get install missing `apt-utils`, so debconf printed
  "delaying package configuration" on every install in both jobs --
  installing it first fixes the chicken-and-egg.
- ci.yml: added `NODE_OPTIONS: --no-deprecation` at the build-docs job
  level to silence Node's internal punycode-module deprecation notice
  (astro's toolchain still pulls it in transitively).
- ci.yml: `npm ci --no-fund` drops the funding nag.
- ci.yml: `gpg --batch --quiet --import` drops gpg's normal-case import
  status lines during the bot commit-signing setup.

Deliberately NOT silenced: npm's deprecated-glob warning, its audit
vulnerability summary, and its allow-scripts notice about esbuild's
postinstall -- these are genuine dependency-hygiene signal, not noise,
and no workflow-level flag exists to hide them without also hiding real
future findings. Also not fixable here: a Gitea Actions/act runner
warning ('runs-on' key not defined in CI/test) that traces to neither
workflow YAML in this repo -- both already set runs-on on every job,
confirmed twice; it's runner-internal, like actions/checkout's own
git-init hint in its Checkout-step preamble.

Verified: full suite 730/730 passing ($status 0); the exact mkdir-
collision repro that surfaced the command-substitution bug re-run
clean (rc=1, empty stderr); test-agents-vault.fish standalone,
320/320, zero occurrences of the previously-leaked messages.
2026-09-22 02:37:42 -04:00
rootiest 018309581d fix(tests): pin init.defaultBranch in test-mkrep to silence git's hint
Every mkrep call in this suite runs a bare `git init`. On a runner with
no init.defaultBranch configured, git prints its "Using 'master' as the
name for the initial branch" advice block on each one (20 occurrences in
CI's test-job log, all from this file). test-agents-vault.fish already
pins the same setting via GIT_CONFIG_COUNT/KEY/VALUE for the identical
reason; apply the same fix here rather than in mkrep.fish itself, so a
real user's own git config still wins in normal use.
2026-09-22 00:04:36 -04:00
rootiest 6156aa7cb9 Merge pull request 'fix: CI build-docs link validation + non-portable cp -n warning' (#164) from fix/classification-schema-link-validation into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 4m28s
CI / build-docs (push) Successful in 4m10s
2026-09-22 02:52:42 +00:00
fishconfig-bot 694b084ff1 chore(docs): regenerate manual, man page, and component registry 2026-09-22 02:42:06 +00:00
rootiest 86aecf0d9d fix(functions): use cp --update=none instead of non-portable -n
GNU cp warns that -n's behavior is non-portable and may change; the
long form is stable and silences the CI warning.
2026-09-21 22:33:44 -04:00
rootiest 18e04dd5fb fix(docs): point classification-schema link at Gitea, not a relative path
The schema file lives at docs/function-classification-schema.md but is
not a published Starlight page, so a relative link resolves fine in the
manual source tree but breaks once copied into
docs/site/src/content/docs/ — starlight-links-validator failed CI's
build-docs job on it. Rewrite it as an absolute Gitea blob URL, the same
pattern _rewrite_repo_links already uses for CONTRIBUTING.md/LICENSE.
2026-09-21 22:33:44 -04:00
rootiest abdb9cfacf Merge pull request 'feat: CLASSIFICATION function-header field + shadow-classification lint' (#163) from feat/classification-header-field into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m52s
CI / build-docs (push) Failing after 3m29s
Reviewed-on: #163
2026-09-22 02:12:56 +00:00
rootiest 2367f5749d docs(index): add config-help keyword aliases for CLASSIFICATION
The CLASSIFICATION schema (docs/function-classification-schema.md) had
no path into config-help's lookup: no fish-config.index keyword, and
the only in-pipeline section (the C1 doc's "For function authors")
doesn't contain the word classification itself, so even the
normalized-heading-scan fallback missed it on that term. Two aliases
added, pointing at the existing section -- no heading renamed, matching
the index file's own stated purpose.
2026-09-21 21:36:39 -04:00
rootiest 3414f81cb6 feat(tests): add shadow-classification lint; fix real cp/mv/less bugs
New Phase 1b in tests/run-tests.fish: catches a bare C1-shadowed-command
call in a functions/*.fish body with no matching uses-shadow(name) or
self-limiting(name) in that function's own CLASSIFICATION header. This
is exactly the check discussed after the rm and cd audits -- runtime
auto-unwrapping isn't viable in fish (there's no hook finer than
shadowing itself, and rewriting behavior invisibly at runtime is its
own footgun); a static lint using the CLASSIFICATION tag as the
declared-intentional marker is. Scoped to functions/*.fish only: the
one-function-per-file convention there makes body extraction exact
with no block-depth parser needed.

Added a new self-limiting(name) tag to the schema for the case a bare
call is safe not because the caller did anything, but because the
shadow's own logic already neutralizes the override: rm's and mkdir's
flag checks (verified precisely -- rm falls back to command rm for any
flag except a bare -r/-R/--recursive alone, which still routes to
trash; mkdir falls back to command mkdir -p for any flag, no
exception), and grep/fgrep/egrep/dir/vdir/cat's own tty auto-detection
(--color=auto, and bat's default color behavior -- verified
byte-identical to stock cat when piped, since bat also auto-disables
highlighting on a non-terminal). Explicit and durable rather than a
silent lint exemption: if a shadow's bypass condition is ever
weakened, every self-limiting site is one grep away instead of
silently wrong.

Running the first draft of the lint surfaced three more real bugs,
none previously audited:

- config-help.fish's --man pager path checks `type -q less` (proving
  it wants the real less binary specifically, for less-only -R/+N
  flag syntax) then called it bare, routing through our own
  $PAGER -> ov -> less -> more -> cat fallback chain instead -- which
  could hand those less-specific flags to a completely different
  program. Now command less.
- _fish_deps_install.fish and _fish_deps_update.fish's binary-upgrade
  paths cp a freshly downloaded binary over an already-installed one
  with no existence guard -- the update flow's target is guaranteed to
  already exist. Our cp shadow forces -i unconditionally (a plain
  alias, not flag-aware like rm's), so this would hang waiting on a
  confirmation prompt in any non-interactive run. Now command cp.
  Same two files' lazydocker install path piped curl output into bare
  bash, invoking our shell-switch wrapper instead of a plain
  subshell. Now command bash.
- agents-init.fish's AGENTS.md/CLAUDE.md relocation calls mv bare in
  four places; each is already guarded by a preceding test -f check on
  the destination, so the -i alias was unlikely to ever fire in
  practice, but explicit command mv removes the reliance on that guard
  entirely rather than leaving it as the only thing standing between a
  file move and an unattended hang.

The remaining ~65 flagged call sites across ~24 files were reviewed
individually and tagged self-limiting(rm)/self-limiting(mkdir)
(verified flagged with -f/-rf or -p) and self-limiting(grep)/
self-limiting(cat) (verified piped, captured, or -q/-c; none display
color to a human), plus uses-shadow(ls) for two existence-check-only
calls (cffetch.fish, ffetch.fish) whose output is redirected to
/dev/null.
2026-09-21 21:26:55 -04:00
rootiest 373917d002 fix(functions): stop routing deterministic cd through zoxide's shadow
Verified an agy audit of every bare cd call by hand. conf.d/zoxide.fish
gates alias cd=z behind status is-interactive plus the C1 toggle, and
_zoxide_hook fires on --on-variable PWD, so it tracks a directory
change no matter how PWD got there -- switching to builtin cd loses
zoxide's frecency tracking nothing.

mkcd.fish's single cd and mkrep.fish's 9 (entering the new repo, plus
8 rollback-to-original-directory sites on error paths and --no-cd)
were both intended as exact, deterministic path navigation, never a
zoxide query. The real risk was mkrep's rollback path: if $orig_pwd
ever failed cd's own -d check for any reason, z's fallback branch
queries zoxide for a *guessed* frecent directory instead -- landing a
failed run's cleanup in a directory the caller never asked for, not
the one it was trying to return to. All 9 sites now use builtin cd.

Corrected both functions' CLASSIFICATION from uses-shadow(cd) to
bypasses-shadow(cd) -- neither wanted zoxide's query, they were tagged
that way only because the header audit recorded what the code was
doing at the time, not what it needed.

integrations/fzf.fish's fzf-alt-c-widget also calls bare cd, but it's
vendored upstream code (PatrickF1/fzf.fish) and is itself an
interactive directory-jump binding, not a script/automation caller --
left alone, same as fisher.fish's rm calls.
2026-09-21 21:26:52 -04:00
rootiest 100cb478bc fix(functions): stop leaking scratch files to trash via bare rm
Verified an agy audit of every bare rm call (the trash-routing C1
shadow) by hand rather than trusting its report. Confirmed correct:
scrub.fish's custom_rm strategy and logs.fish's Ctrl-D delete both
deliberately want trash for a real, user-facing deletion.

Confirmed and fixed three cases where a function's own throwaway
scratch file was going to the user's trash instead of being wiped:
fc.fish's edited-command tmpfile, dng2avif.fish's intermediate PNM
(inconsistent with its own failure-path cleanup two lines up, which
already used -f), and _scrollback_prune_junk.fish's junk log files
(its sibling _prune_terminal_logs.fish already documents this exact
pitfall in its header).

Also went further than the report and classified every bypasses-shadow(rm)
caller found by grep that had never been audited at all:
config-settings.fish and edit.fish (own scratch cleanup, no destructive
data at stake) and key-crypt.fish (--remove deletes the user's real
input file after encryption, genuinely destructive, already documented
in its own header as 'not a secure wipe'). Corrected scrub.fish's tag,
which was missing uses-shadow(rm) for its deliberate trash-routing
branch alongside the bypass branch it already had tagged.

Added a note to the schema doc: rm's flag-based fallback lives inside
the shadow itself, so a bare rm -f/rm -rf call is not the caller
bypassing anything -- only an explicit command rm/builtin rm earns
the tag. This is why dng2avif.fish's fix needed no CLASSIFICATION
change: it already used rm -f, which was never actually the bug --
the missing -f on line 122 was.
2026-09-21 21:26:50 -04:00
rootiest 069a1f7743 docs(classification): move schema out of gitignored AGENTS/, into docs/
AGENTS/functions/CLAUDE.md is git-ignored local agent state, not part
of the repo -- a comment/commit referencing it as the schema's home
points contributors at a file they can't see. The canonical CLASSIFICATION
schema now lives at docs/function-classification-schema.md (tracked),
with CONTRIBUTING.md's existing function-header-conventions section
extended to introduce it, and the C1 shadow doc's pointer updated to
match. AGENTS/functions/CLAUDE.md keeps only a one-line pointer to the
tracked file instead of duplicating the definitions.
2026-09-21 21:26:47 -04:00
rootiest 859f14a6e9 feat(functions): tag CLASSIFICATION across functions/ and conf.d/
Audits every function's interaction with the C1-shadowed commands
(uses-shadow/bypasses-shadow) and general hazards (destructive,
network, blocking-prompt) per the CLASSIFICATION schema.

Delegated the initial mechanical sweep to agy, then reviewed every
file by hand: fixed a systemic double-blank-comment-line formatting
bug from the delegate pass, and corrected several judgment errors
found on review -- three false blocking-prompt tags where a fish
'read' was consuming piped input rather than waiting on a terminal
(open-url.fish, sbver.fish, play-media.fish, now untagged entirely),
a blocking-prompt tag on mkrep.fish despite its documented --yes
escape hatch, an untagged read in jobrunner.fish's own baseless
blocking-prompt claim (removed, along with a destructive tag on
cleanup of its own mktemp output -- the schema explicitly excludes
that), the same own-output-cleanup false positive on
_zellij_dump_log.fish's destructive tag, an interactive fzf-gated
confirmation on logs.fish and replay.fish's piped read misread the
same way as the first three, and a uses-shadow(mkdir) on mkcd.fish
that actually belongs to the _fish_mkdir_p helper it delegates to,
not to mkcd itself.
2026-09-21 21:26:44 -04:00
rootiest aef686af86 feat(docs): add CLASSIFICATION function-header field, rename history shadow
Rename the C1 history() shadow to pretty-history so it never collides
with the fish builtin -- every function expecting stock history
semantics (search, --max, merge, ...) would otherwise silently break.
hist.fish, which relied on the shadow's timestamp formatting, now
requests it explicitly via builtin history --show-time.

Add a CLASSIFICATION doc-header label so a function can declare its
interaction with C1-shadowed commands (uses-shadow/bypasses-shadow)
and general hazards (destructive, network, blocking-prompt) for
anyone deciding to disable an opinionated category or call the
function from automation. Wired into the manual/site build pipeline
(manualtools.py, build-manual.py) and the C1 shadow doc gets a new
"For function authors" bypass-mechanism reference table
(command/builtin/__original_help, and which shadows have no real
bypass target at all).
2026-09-21 21:26:40 -04:00
rootiest 392e6593ac fix(keybind): bypass history shadow in ctrl-alt-u empty-prompt recall
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m3s
CI / build-docs (push) Successful in 4m5s
The tricks.fish C1 history() shadow drops all args and always shows
timestamps. Calling plain history --max 1 hit that shadow, dumping the
entire history with timestamps instead of one plain entry. Use builtin
history --max 1, matching the convention already used by fc.fish,
_fzf_search_history.fish, and bash_expands.fish.
2026-09-21 19:29:37 -04:00
fishconfig-bot 6fd25028d9 chore(docs): regenerate manual, man page, and component registry 2026-09-21 23:20:18 +00:00
rootiest e2c25fac64 Merge pull request 'feat(keybind): recall last history entry on empty prompt for ctrl-alt-u' (#162) from feat/replace-command-token-empty-recall into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m11s
CI / build-docs (push) Successful in 3m55s
2026-09-21 23:14:16 +00:00
rootiest e4df160fc0 docs(key-bindings): document ctrl-alt-u empty-prompt history recall 2026-09-21 19:07:02 -04:00
rootiest 2630342d81 feat(keybind): recall last history entry on empty prompt for ctrl-alt-u 2026-09-21 19:06:59 -04:00
fishconfig-bot 21ef25bb56 chore(docs): regenerate manual, man page, and component registry 2026-09-21 07:03:41 +00:00
rootiest 85d13a6aaf Merge pull request 'feat(clipboard): fill the WSL2 clipboard gap with win32yank' (#161) from feat/win32yank-wsl2-clipboard into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m1s
CI / build-docs (push) Successful in 3m52s
2026-09-21 06:57:47 +00:00
rootiest 0cd4787674 Merge branch 'main' into feat/win32yank-wsl2-clipboard 2026-09-21 06:55:47 +00:00
rootiest 6da6999b90 docs(readme): sync OS compatibility section with win32yank fallback 2026-09-21 02:53:11 -04:00
rootiest 59d33e8c76 feat(fish-deps): add win32yank installer for WSL2
New optional-tier catalog entry, gated on WSL2 detection
(/proc/sys/kernel/osrelease) so it never surfaces on a plain Linux
box's install/sync prompts, only in the informational status
listing. Downloads the x86_64 binary from GitHub releases to
~/.local/bin/win32yank.exe; fish-deps update refreshes an
already-installed copy the same way.
2026-09-21 02:52:26 -04:00
rootiest b2be858d8c refactor(clipboard): share provider detection across y/p/paste/hist
Extracts _fish_clipboard_copy and _fish_clipboard_paste so the
wl-copy/xclip/win32yank fallback chain lives in one place instead of
four near-duplicates. hist now goes through the same chain, so it
also gets the xclip (X11) fallback it was missing before, alongside
win32yank on WSL2.
2026-09-21 02:52:17 -04:00
rootiest 07613c7889 feat(clipboard): add win32yank fallback for WSL2
y, p, paste, and hist now try win32yank.exe after wl-copy/wl-paste
and xclip, so clipboard access works under WSL2 once win32yank is
installed and reachable through WSL interop. Updates the OS
compatibility docs accordingly.
2026-09-21 02:44:12 -04:00
fishconfig-bot b59c73fc53 chore(docs): regenerate manual, man page, and component registry 2026-09-21 06:41:20 +00:00
rootiest 2686415db5 Merge pull request 'docs: state OS compatibility explicitly (Linux-only)' (#160) from docs/os-compatibility into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m57s
CI / build-docs (push) Successful in 3m50s
2026-09-21 06:35:36 +00:00
rootiest 7c59caebce docs: state OS compatibility explicitly (Linux-only)
Adds an OS Compatibility section to the README and the Installation
manual page: developed and tested on Arch Linux, macOS and Windows
unsupported, with the specific Linux-only calls that back that claim
(systemd-inhibit, zramctl/swapon, sbctl, wl-copy/xclip with no
pbcopy/clip.exe fallback, GNU-only stat/numfmt flags) and the baseline
packages (git, gpg, tar, coreutils) assumed present rather than tracked
by fish-deps.

Adds os/os-compatibility/operating-system/compatibility/linux/macos/
mac/windows/wsl keywords to fish-config.index so `help config os` and
friends jump straight to it.
2026-09-21 02:33:31 -04:00
fishconfig-bot 7a46881ca4 chore(docs): regenerate manual, man page, and component registry 2026-09-21 06:04:49 +00:00
rootiest cd8d6fce38 Merge pull request 'feat(functions): derive the shared help palette from the fish theme' (#159) from feat/theme-driven-help-palette into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 2m5s
CI / build-docs (push) Successful in 4m24s
2026-09-21 05:53:21 +00:00
rootiest 1608b9b527 Merge pull request 'feat(functions): port key-crypt to a fish function' (#158) from feat/key-crypt-fish-function into main
CI / github-mirror (push) Skipped
CI / test (push) Successful in 1m58s
CI / build-docs (push) Failing after 4m12s
2026-09-21 05:52:15 +00:00
rootiest 0b4ab30b40 test(session): catch --help color regressions against the live theme
Adds two checks to the shared-palette section, extending the existing
colored list with the functions converted in this branch (config-help,
fish-deps, gi, git-clean, mkrep):

- Every listed function's --help must contain the session's own c_head
  and c_cmd escapes -- not just some escape sequence, which the existing
  presence check already covered but which a wrong-role bug (still some
  color, just not the right one) sails through undetected.
- Every escape sequence found in --help output must be one of
  c_reset/c_head/c_cmd/c_flag/c_arg/c_dim. c_warn/c_err/c_ok/c_accent/
  c_sel/c_hi are legitimate elsewhere but never in --help text; reaching
  for one there is always a bug, which is exactly what logs and
  smart_exit did (static c_accent green for the command name).

Verified against the pre-fix smart_exit.fish (58d5cf3~1): both checks
fail on it -- missing c_head on "Options:", and a disallowed \e[32m for
the command name -- confirming this isn't a tautological pass.
2026-09-21 01:49:34 -04:00
rootiest 58d5cf3fae fix(functions): correct palette roles in logs and smart_exit help text
Both had headings (Usage:/Options:/Keys in fzf:) left completely
uncolored, flags rendered in the argument color instead of the flag
color, and the command name hardcoded to a static ANSI green (c_accent)
instead of the theme-derived c_cmd -- the last of the raw, unconverted
color usage the audit turned up.
2026-09-21 01:44:14 -04:00
rootiest cbf1a0a9a2 fix(functions): color tool/flag names in config-help's pager fallback chain
ov, bat, man, less, and cat were plain text, and man -l's -l wasn't
styled as a flag, in an otherwise-converted help block.
2026-09-21 01:39:01 -04:00
rootiest 1dcbd6498c fix(functions): color the command name in config-help's USAGE section
The USAGE block's "help config" was left as plain text while every
other section (EXAMPLES) correctly colored it with c_cmd.
2026-09-21 01:36:30 -04:00
rootiest 610714476d feat(functions): derive the shared help palette from the fish theme
__fish_palette's c_cmd/c_flag/c_arg/c_dim now read fish's own highlighter
variables (fish_color_command/option/param/autosuggestion) when set, and
c_head reads the pager's fish_pager_color_prefix, so --help text matches
whatever the user's actual theme renders at the prompt instead of a fixed
guess at it. Each role keeps its previous static value as a fallback
(e.g. a --no-config script, or the theme variable being unset or empty).

Routes __fish_help_header's title/section colors and config-help's,
gi's, and git-clean's own hand-rolled --help blocks through the palette
instead of their hardcoded set_color calls, which fixes the
--help-vs-actual-prompt color mismatch across most of the config's
functions in one place (__fish_help_header backs the majority of
user-facing --help output).

Also fixes role mismatches found along the way: several already-palette
functions (dockup, fish-deps, pkg, play-media, dng2avif) were coloring
their <placeholder>/[bracket] argument text with c_dim instead of c_arg,
and scrub's "Aggressive Targets:" heading was still a hardcoded
set_color call sitting next to an otherwise-converted help block.
2026-09-21 01:19:38 -04:00
157 changed files with 5581 additions and 791 deletions
+157 -16
View File
@@ -4,7 +4,7 @@ on:
push:
branches:
- main
paths:
paths: &ci-paths
- "docs/manual/**"
- "docs/build-manual.py"
- "docs/manualtools.py"
@@ -16,6 +16,17 @@ on:
- "completions/**"
- "integrations/**"
- "tests/**"
- "scripts/**"
pull_request:
branches:
- main
# No `paths:` filter here (unlike push, above): a PR carrying
# Kind/Testing, Area/Docs, etc. must still trigger this workflow even
# when its diff touches nothing in ci-paths, or the label-based gates
# in the test/docs jobs below would never get a chance to evaluate.
# `labeled`/`unlabeled` cover a label added after the PR is already
# open, without a new commit.
types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_dispatch:
inputs:
job:
@@ -26,7 +37,7 @@ on:
options:
- all
- test
- build-docs
- docs
jobs:
# This workflow file is mirrored to GitHub as-is, but the runner label
@@ -44,42 +55,138 @@ jobs:
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
fetch-depth: 0
# Runs unconditionally and always sets an output, so every step
# after it can gate on a single `steps.relevance.outputs.run`
# check instead of repeating the label/path OR-chain everywhere.
# push/workflow_dispatch are always relevant -- push is already
# path-filtered above, and a manual dispatch is explicit intent.
# A pull_request is relevant if it carries a testing-related label
# (independent of what it touches -- see the `on.pull_request`
# comment above) or if its diff touches a ci-paths pattern (the
# same list the push trigger above filters on; duplicated here in
# shell glob form since a PR event isn't pre-filtered by paths).
- name: Determine relevance
id: relevance
env:
PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }}
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "run=true" >>"$GITHUB_OUTPUT"
exit 0
fi
if printf '%s' "$PR_LABELS" | grep -qE '"name":[[:space:]]*"(Kind/Testing|Area/Tests|Area/CI|Area/Scripts)"'; then
echo "run=true" >>"$GITHUB_OUTPUT"
exit 0
fi
git fetch origin "${{ github.event.pull_request.base.ref }}"
if git diff --name-only "origin/${{ github.event.pull_request.base.ref }}...HEAD" \
| grep -qE '^(docs/manual/|docs/build-manual\.py$|docs/manualtools\.py$|docs/verify-manual\.py$|docs/site/|functions/|conf\.d/|config\.fish$|completions/|integrations/|tests/|scripts/)'; then
echo "run=true" >>"$GITHUB_OUTPUT"
else
echo "run=false" >>"$GITHUB_OUTPUT"
fi
- name: Install fish
if: steps.relevance.outputs.run == 'true'
run: |
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y software-properties-common
# apt-utils, so debconf has a target for the "delaying package
# configuration" notice's OWN follow-up run (irrelevant to
# anything else in this job). That notice still fires exactly
# once here regardless -- verified live against two orderings
# (bundled with another package, and installed fully alone,
# first) -- because it fires *during* apt-utils' own first
# install, before debconf considers it "installed". Nothing
# this workflow controls can pre-seed that; accepted as
# unavoidable, same as the runs-on/checkout hint noise below.
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y apt-utils software-properties-common
sudo add-apt-repository -y ppa:fish-shell/release-4
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y fish
- name: Run fish config tests
if: steps.relevance.outputs.run == 'true'
run: fish tests/run-tests.fish
build-docs:
needs: test
# Documentation tests/build, and (push/dispatch only) publish. Split
# into two sections within one job rather than two jobs: the publish
# steps need the files the build steps just generated, and passing
# those between separate jobs would need upload/download-artifact for
# no real benefit here.
#
# Does NOT `need: test` (the old build-docs job did, gating publish
# on it). main now has branch protection requiring the test job to
# pass before a PR can merge, so by the time a push-to-main reaches
# this job, test has already passed as a condition of getting here --
# re-checking it in-workflow would be redundant. The one gap that
# leaves is a direct admin push bypassing the PR flow entirely; that's
# the same trust already extended by leaving block_admin_merge_override
# off on the branch protection rule, not a new hole.
docs:
if: |
github.server_url != 'https://github.com' &&
always() &&
(github.event.inputs.job == 'build-docs' ||
((github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all') &&
needs.test.result == 'success'))
(github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all' || github.event.inputs.job == 'docs')
runs-on: racknerd-mini
env:
# Silences Node's internal "punycode module is deprecated" notice
# (astro's toolchain still requires it transitively) on every node
# invocation in this job, setup-node's own included.
NODE_OPTIONS: --no-deprecation
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
# The default GITEA_TOKEN is Gitea Actions' built-in synthetic
# bot identity, not a real account -- main's branch protection
# rejects its pushes outright (run 983, run 990), and it can't
# be whitelisted because it isn't an addable user. BOT_PUSH_TOKEN
# is a PAT on the rootiest account (already bypass-whitelisted)
# used only so this job's later push succeeds; commit authorship
# and GPG signing below still use the fishconfig-bot identity,
# which is unrelated to push auth.
token: ${{ secrets.BOT_PUSH_TOKEN }}
fetch-depth: 0
# Same shape as the test job's identical step; see its comment.
# Only the label set and path patterns differ, narrowed to the
# docs-specific subset of ci-paths.
- name: Determine relevance
id: relevance
env:
PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }}
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "run=true" >>"$GITHUB_OUTPUT"
exit 0
fi
if printf '%s' "$PR_LABELS" | grep -qE '"name":[[:space:]]*"(Kind/Documentation|Area/Docs)"'; then
echo "run=true" >>"$GITHUB_OUTPUT"
exit 0
fi
git fetch origin "${{ github.event.pull_request.base.ref }}"
if git diff --name-only "origin/${{ github.event.pull_request.base.ref }}...HEAD" \
| grep -qE '^(docs/manual/|docs/build-manual\.py$|docs/manualtools\.py$|docs/verify-manual\.py$|docs/site/)'; then
echo "run=true" >>"$GITHUB_OUTPUT"
else
echo "run=false" >>"$GITHUB_OUTPUT"
fi
- name: Install dependencies
if: steps.relevance.outputs.run == 'true'
run: |
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y software-properties-common
# apt-utils: see the "Install fish" step's identical comment in
# the test job for why its own debconf notice is accepted, not
# chased further.
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y apt-utils software-properties-common
sudo add-apt-repository -y ppa:fish-shell/release-4
sudo apt-get -o Acquire::Retries=3 update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y pandoc python3-yaml fish
- name: Generate concatenated markdown
if: steps.relevance.outputs.run == 'true'
run: python3 docs/build-manual.py --concat -o docs/fish-config.md
# Regeneration MUST run before verification: verify-manual.py's
@@ -87,13 +194,19 @@ jobs:
# against docs/fish-config.md on disk. Before this step ran, that
# file was still the stale pre-push copy, so any ordinary edit under
# docs/manual/** failed the round-trip check before anything was
# regenerated. Do not reorder this back — verification still gates
# regenerated. Do not reorder this back -- verification still gates
# pandoc and the auto-commit below, it just no longer requires a
# contributor to hand-sync the generated file before pushing.
#
# This is the "documentation tests" section: on a PR, it runs
# (and can fail the job) whenever relevant, without needing the
# publish steps below to run at all.
- name: Verify manual integrity
if: steps.relevance.outputs.run == 'true'
run: python3 docs/verify-manual.py
- name: Compile man page
if: steps.relevance.outputs.run == 'true'
run: |
pandoc --standalone \
--from markdown \
@@ -101,21 +214,30 @@ jobs:
docs/fish-config.md \
-o docs/fish-config.1
# ──────────────────────── Publish only ───────────────────────
# Everything below deploys the production site and commits
# generated files straight to the checked-out branch. Never runs
# from a pull_request -- PR content isn't main yet, and a
# fork/branch push shouldn't touch prod.
- name: Set up Node
if: github.event_name != 'pull_request'
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Generate site content
if: github.event_name != 'pull_request'
run: python3 docs/build-manual.py --site
- name: Build project wiki
if: github.event_name != 'pull_request'
working-directory: docs/site
run: |
npm ci
npm ci --no-fund
npx astro build
- name: Deploy to Cloudflare Pages
if: github.event_name != 'pull_request'
working-directory: docs/site
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
@@ -127,6 +249,7 @@ jobs:
--commit-dirty=true
- name: Commit generated docs
if: github.event_name != 'pull_request'
env:
BOT_GPG_KEY: ${{ secrets.CI_GPG_PRIVATE_KEY }}
run: |
@@ -146,7 +269,7 @@ jobs:
echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf"
echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf"
command -v gpg >/dev/null || sudo apt-get install -y --no-install-recommends gnupg
echo "$BOT_GPG_KEY" | base64 -d | gpg --batch --import
echo "$BOT_GPG_KEY" | base64 -d | gpg --batch --quiet --import
git config user.name "Gitea Actions Bot"
git config user.email "fishconfig-bot@git.rootiest.dev"
git config user.signingkey CAA082C2F3467E1F7217AD492075C120312D23F4
@@ -154,7 +277,25 @@ jobs:
git add docs/fish-config.md docs/fish-config.1 conf.d/__fish_config_op_registry.fish
git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "chore(docs): regenerate manual, man page, and component registry"
git push
# npm ci + astro build + the Cloudflare deploy above can take
# several minutes, so main can move (another PR merges) before
# this push lands -- a bare `git push` was seen rejected as
# non-fast-forward for exactly that reason (run 976). This
# commit only ever touches generated files, so a rebase onto
# whatever landed is always mechanical; retry it a few times
# against a live race instead of failing the whole job.
pushed=0
for attempt in 1 2 3; do
if git push; then
pushed=1
break
fi
echo "push rejected (attempt $attempt/3), rebasing onto origin/main..." >&2
git fetch origin main
git rebase origin/main
done
test "$pushed" -eq 1
# Stand-in for the GitHub mirror so the commit gets a completed status
# instead of the real jobs above sitting queued forever for a
@@ -166,4 +307,4 @@ jobs:
- name: Note that CI runs on Gitea
run: |
echo "This repository mirrors from Gitea (git.rootiest.dev), where CI actually runs."
echo "See the commit's status on the Gitea instance for the real test/build-docs results."
echo "See the commit's status on the Gitea instance for the real test/docs results."
+78 -1
View File
@@ -170,6 +170,33 @@ assumes you *do* have push access (maintainers, regular contributors).
approval before doing this** — there's no standing exception, no matter
how trivial the change looks.
### Branch protection on `main`
Gitea enforces these rules server-side — not just convention, so violating
one is a hard rejection, not a review comment:
- **No direct push**, except an allowlist: the repo owner and
`fishconfig-bot` (the docs-regen commit CI makes on every push — see
[Documentation Pipeline](#documentation-pipeline)). Everyone else merges
through a PR, which is what [the bullets above](#branching--pull-requests)
already describe.
- **Force-push is disabled outright** — no allowlist exception.
- **Signed commits are required.** An unsigned or bad-signature commit is
rejected at push time, which is what the tracked `pre-push` hook in
[Getting Started](#getting-started) catches locally before you find out
the hard way from Gitea.
- **`CI / test (pull_request)` must pass** before a PR can merge.
- **A PR with changes requested by a reviewer can't merge** until that's
resolved, even with enough approvals otherwise.
- **A PR behind `main` can't merge** — use Gitea's "Update branch" first.
This bites a stacked PR specifically: once the PR it was based on merges,
the stacked one now shows behind `main` and needs an update before it can
merge too, even though nothing about its own content changed.
- The repo owner is on the bypass allowlist and can override the above when
there's a genuine reason to (the emergency valve for [item 3 of keeping
feature branches focused](#branching--pull-requests)) — this is the
exception, not a way to routinely skip review.
### Pull request descriptions
Fill in `.github/PULL_REQUEST_TEMPLATE.md` — Gitea pre-loads it into the
@@ -390,7 +417,8 @@ all optional except where noted:
|---|---|
| `CATEGORY` | **Required to appear in the manual at all** — see below. |
| `COMPONENT` | Only for functions gated by the [opinionated-component system](#opinionated-components). |
| `DEPENDENCIES` | Other functions this one calls that a reader may want to look up. |
| `DEPENDENCIES` | Other functions, and external CLI tools, this one needs for full functionality — required or optional-with-fallback alike. |
| `CLASSIFICATION` | Hazard/shadow-interaction tags, plus a couple of general-purpose ones (`manual-section`) — see below. |
| `SYNOPSIS` | One-line usage form. |
| `DESCRIPTION` | Prose description; can span multiple paragraphs. |
| `ARGUMENTS` | Flags/positional args, one per line. |
@@ -414,6 +442,9 @@ A full example (`functions/claude.fish`):
# DEPENDENCIES
# agents-init
#
# CLASSIFICATION
# bypasses-shadow(claude)
#
# SYNOPSIS
# claude [ARGS...]
#
@@ -443,6 +474,52 @@ If your function genuinely doesn't fit any of these, add a new
`docs/manual/05-functions/NN-your-category.md` stub (with frontmatter
matching its siblings) rather than force-fitting it into an existing one.
**`CLASSIFICATION` is the general-purpose tag field, optional and omitted
when nothing applies:** mostly hazards and shadow interactions — whether
the function calls a
[C1-shadowed command](docs/manual/08-components-reference/01-c1-command-shadows.md)
bare wanting the override (`uses-shadow(ls)`) or bypasses it deliberately
via `command`/`builtin` (`bypasses-shadow(cat)`), and general hazards —
`destructive`, `network`, `blocking-prompt` — but not exclusively: a
function with its own dedicated manual section (below) carries
`manual-section(<slug>)` here too, so that fact is grep-able without
reading every `NOTES` field. Full tag definitions and placement rule:
[`docs/function-classification-schema.md`](docs/function-classification-schema.md).
### Dedicated manual sections for complex subsystems
A doc-header's `DESCRIPTION` is for that one function's usage — it stops
being the right place once a subsystem spans several functions, has its
own file layout, or has enough behavior (a decision table, a safety
model) that cramming it into one function's header would make that
header useless as a quick reference. When that happens, give the
subsystem its own numbered top-level section under `docs/manual/`
(follow the sibling sections' frontmatter shape: `title`, `manTitle`,
`sidebar.order`, `helpKeywords`) instead of stretching the header.
`docs/manual/16-agent-tooling.md` (`agents-init`/`agents-vault`/the
`AGENTS/` sub-repository) is the existing example — its own doc-headers
stay short and point there for the full picture, the same way this
document points at other reference files rather than repeating them.
A function with a dedicated section carries `manual-section(<slug>)` in
its own `# CLASSIFICATION` (see `functions/agents-init.fish`; full tag
definition in
[`docs/function-classification-schema.md`](docs/function-classification-schema.md))
— that's what makes the section discoverable without reading every
function's `NOTES` by hand, and it's what `docs/build-manual.py` reads to
render the "See also" line on the function's generated entry.
`docs/verify-manual.py` fails the build if the slug doesn't resolve to a
real page, so a typo or a renamed file can't go unnoticed — but it cannot
check the *content* is current. A `# NOTES` line pointing at the same page
(see the existing example) is worth adding too, for a reader who only
reads the header text rather than the generated docs, but the tag is the
part something else actually verifies.
This is a genuine exception to "the doc-header is the single source of
truth" above. **Whenever you change what one of these functions does,
update its dedicated section in the same commit or pull request** — not
as a follow-up.
### Private/internal helper functions
Functions named with a leading `_` (e.g. `_agents_init_ensure_gitignore`,
+42 -5
View File
@@ -1,8 +1,9 @@
# Fish Shell Configuration
# Rootiest Fish Configuration
A feature-rich Fish shell configuration for CachyOS (Arch Linux),
built around a Catppuccin Mocha aesthetic with a curated set of modern
CLI tool integrations, smart shell functions, and a heavily customized
This isn't a generic Fish shell configuration — it's the Rootiest Fish
Configuration: a feature-rich setup for CachyOS (Arch Linux), built
around a Catppuccin Mocha aesthetic with a curated set of modern CLI
tool integrations, smart shell functions, and a heavily customized
abbreviation system for keyboard-driven workflows.
📖 **[Project Wiki](https://fish.rootiest.fyi/)**
@@ -12,6 +13,7 @@ abbreviation system for keyboard-driven workflows.
- [Overview](#overview)
- [Session Logging](#session-logging)
- [Documentation](#documentation)
- [OS Compatibility](#os-compatibility)
- [Installation](#installation)
- [Personalization](#personalization)
- [Minimal Mode](#minimal-mode)
@@ -131,7 +133,7 @@ silent until you enable logging.
## Documentation
### [📖 Documentation Site](https://fish.rootiest.fyi/)
### [📖 Documentation Wiki](https://fish.rootiest.fyi/)
A Starlight-powered site rebuilt on every push to `main`. It covers
configuration variables, key bindings, abbreviations, all functions, the
@@ -170,6 +172,41 @@ The pager falls back through: **ov** → **bat** → **man -l** → **less** →
---
## OS Compatibility
This is a **Linux-only** configuration. It is developed and tested on an
Arch Linux system; `fish-deps` also detects `apt`, `dnf`, `zypper`, and `yum`
for broader distro support, but coverage outside Arch is thinner.
A number of functions call Linux-specific subsystems directly, with no
fallback: `systemd-inhibit` (`wake-lock`), `zramctl`/`swapon` (`swapstat`),
`sbctl` and UEFI Secure Boot state (`sbver`), and GNU coreutils flags like
`stat -c`/`numfmt` (`sudo-toggle`, `dng2avif`), which differ or don't exist
under a BSD userland.
Clipboard access (`y`, `p`, `paste`, `hist`) is the exception: it falls back
through `wl-copy`/`wl-paste` (Wayland), `xclip` (X11), and `win32yank.exe`
(WSL2), so it works on all three — there's still no `pbcopy`/`pbpaste`
fallback for macOS.
**macOS** is not supported — `_fish_deps_detect_pm` checks for `brew`, but
that alone doesn't make the functions above work; they have no macOS
equivalent path today.
**Windows** is not supported. Fish has no native Windows build, and this
config isn't tested under WSL either. WSL2 can run `systemd`, so basic shell
use may work; Secure Boot/zram state is still meaningless inside a VM, but
clipboard integration works via `win32yank.exe` once it's reachable through
WSL interop — `fish-deps install` can fetch it for you.
**Assumed present on any Linux system this runs on:** `git`, `gpg`, `tar`,
and GNU coreutils. These aren't tracked by `fish-deps` — they're base-system
utilities, not opt-in software with an install journey to manage.
See [OS Compatibility](https://fish.rootiest.fyi/10-installation/#os-compatibility) in the project wiki for more.
---
## Installation
This config is managed as a Git repository. To use it on a new machine:
-7
View File
@@ -1,7 +0,0 @@
complete --command fisher --exclusive --long help --description "Print help"
complete --command fisher --exclusive --long version --description "Print version"
complete --command fisher --exclusive --condition __fish_use_subcommand --arguments install --description "Install plugins"
complete --command fisher --exclusive --condition __fish_use_subcommand --arguments update --description "Update installed plugins"
complete --command fisher --exclusive --condition __fish_use_subcommand --arguments remove --description "Remove installed plugins"
complete --command fisher --exclusive --condition __fish_use_subcommand --arguments list --description "List installed plugins matching regex"
complete --command fisher --exclusive --condition "__fish_seen_subcommand_from update remove" --arguments "(fisher list)"
+3
View File
@@ -24,6 +24,9 @@ __fish_config_op_enabled (status basename); or exit
# COMPONENT
# autoexec/sync
#
# CLASSIFICATION
# bypasses-shadow(cat)
#
# SYNOPSIS
# __auto_pull_on_pwd (event handler, --on-variable PWD)
#
+6 -4
View File
@@ -97,11 +97,13 @@ if __fish_config_op_enabled (status basename) tricks-bang
end
end
# Fish command history override to show timestamps
# Shadowing the history command is opinionated (C1 aliasing); when disabled,
# the function is never defined and fish's stock history behavior applies.
# Timestamped history view. Named pretty-history (not history) so it never
# shadows the fish builtin -- every function in this config that expects
# stock `history` semantics (search, --max, merge, ...) would otherwise
# silently break, which has happened more than once. Opinionated (C1
# aliasing); when disabled, the function is never defined.
if __fish_config_op_enabled (status basename) aliases-tricks
function history
function pretty-history --description 'History with timestamps prepended to every entry'
builtin history --show-time='%F %T '
end
end
+119
View File
@@ -0,0 +1,119 @@
# ╭──────────────────────────────────────────────────────────╮
# │ GitIgnore Boilerplate Template │
# ╰──────────────────────────────────────────────────────────╯
#
# ──────────────────── OS-Generated Files ────────────────────
# automatic backup files created by some editors (e.g., Vim, Emacs)
*~
# temporary files created if a process still has a handle to a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# MacOS junk
.DS_Store
Thumbs.db
# Linux trash folder which might appear on any partition or disk
.Trash-*
# files created when an open file is removed but is still being accessed
.nfs*
# ─────────────────── Debug/Temporary/Testing ────────────────
# Matches OLD / .OLD
[Oo][Ll][Dd]/
.[Oo][Ll][Dd]/
# Matches DISABLE / .DISABLE
[Dd][Ii][Ss][Aa][Bb][Ll][Ee]/
.[Dd][Ii][Ss][Aa][Bb][Ll][Ee]/
# Matches DISABLED / .DISABLED
[Dd][Ii][Ss][Aa][Bb][Ll][Ee][Dd]/
.[Dd][Ii][Ss][Aa][Bb][Ll][Ee][Dd]/
# Matches DEBUG / .DEBUG
[Dd][Ee][Bb][Uu][Gg]/
.[Dd][Ee][Bb][Uu][Gg]/
# Matches TMP / .TMP
[Tt][Mm][Pp]/
.[Tt][Mm][Pp]/
# Matches TEMP / .TEMP
[Tt][Ee][Mm][Pp]/
.[Tt][Ee][Mm][Pp]/
# Matches TEMPORARY / .TEMPORARY
[Tt][Ee][Mm][Pp][Oo][Rr][Aa][Rr][Yy]/
.[Tt][Ee][Mm][Pp][Oo][Rr][Aa][Rr][Yy]/
# Matches TESTING / .TESTING
[Tt][Ee][Ss][Tt][Ii][Nn][Gg]/
.[Tt][Ee][Ss][Tt][Ii][Nn][Gg]/
# ─────────────────── Scratchpad / Scratch Files ───────────────
# Root-only files/folders starting with scratch or .scratch
/[Ss][Cc][Rr][Aa][Tt][Cc][Hh]*
/.[Ss][Cc][Rr][Aa][Tt][Cc][Hh]*
# Matches SCRATCH / .SCRATCH anywhere (directory)
[Ss][Cc][Rr][Aa][Tt][Cc][Hh]/
.[Ss][Cc][Rr][Aa][Tt][Cc][Hh]/
# Matches SCRATCHPAD / .SCRATCHPAD anywhere (file or directory)
[Ss][Cc][Rr][Aa][Tt][Cc][Hh][Pp][Aa][Dd]
.[Ss][Cc][Rr][Aa][Tt][Cc][Hh][Pp][Aa][Dd]
# Matches any directory starting with .SCRATCH anywhere
.[Ss][Cc][Rr][Aa][Tt][Cc][Hh]*/
# ─────────────────── Dev Notes / Working Notes ────────────────
# Root-only files/folders starting with devnote or .devnote
/[Dd][Ee][Vv][Nn][Oo][Tt][Ee]*
/.[Dd][Ee][Vv][Nn][Oo][Tt][Ee]*
# Matches DEVNOTE / .DEVNOTE anywhere (file or directory)
[Dd][Ee][Vv][Nn][Oo][Tt][Ee]
.[Dd][Ee][Vv][Nn][Oo][Tt][Ee]
# Matches DEVNOTES / .DEVNOTES anywhere (file or directory)
[Dd][Ee][Vv][Nn][Oo][Tt][Ee][Ss]
.[Dd][Ee][Vv][Nn][Oo][Tt][Ee][Ss]
# Matches any directory starting with .DEVNOTE anywhere
.[Dd][Ee][Vv][Nn][Oo][Tt][Ee]*/
# ─────────────────── AI Sessions and Rules ──────────────────
# CLAUDE.md / AGENTS.md / GEMINI.md / ANTIGRAVITY.md are deliberately NOT
# ignored here: an increasing number of projects commit these instruction
# files on purpose. Only the tool-owned session/state dirs are ignored.
# Matches .claude* anywhere (files or directories)
.[Cc][Ll][Aa][Uu][Dd][Ee]*
# Matches .gemini* anywhere (files or directories)
.[Gg][Ee][Mm][Ii][Nn][Ii]*
# Matches .antigravity* anywhere (files or directories)
.[Aa][Nn][Tt][Ii][Gg][Rr][Aa][Vv][Ii][Tt][Yy]*
# Matches .AGY* anywhere (files or directories)
.[Aa][Gg][Yy]*
# Matches .agents* anywhere (files or directories)
.[Aa][Gg][Ee][Nn][Tt][Ss]*
# Matches .REMEMBER* anywhere (files or directories)
.[Rr][Ee][Mm][Ee][Mm][Bb][Ee][Rr]*
# ──────────────────── Planning Artifacts ───────────────────
# Catalog files generated by pre-implementation analysis passes
.superpowers
docs/superpowers
docs/specs
docs/devlogs
# ──────────────────────────────────────────────────────────────
+89 -6
View File
@@ -202,7 +202,7 @@ def build_concat(root: Path) -> str:
Only bodies are passed: the pandoc metadata block above is not prose
and must survive byte-for-byte.
"""
entries = build_entries(mt.parse_functions(FUNCTIONS))
entries = build_entries(mt.parse_functions(FUNCTIONS), root=root)
chunks: list[str] = []
pandoc_path = root / "_pandoc.yml"
if pandoc_path.exists():
@@ -703,7 +703,67 @@ ENTRY_HEADS = {
}
def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str:
def _classification_tags(raw: list[str]) -> list[str]:
"""Split a CLASSIFICATION body into its comma-separated tags.
A plain comma split (as `names()` uses for DEPENDENCIES) would break on
the commas inside `uses-shadow(rm, cp)`-style tags, so this only splits
on commas at paren depth 0.
"""
text = " ".join(raw)
tags: list[str] = []
depth = 0
start = 0
for i, ch in enumerate(text):
if ch == "(":
depth += 1
elif ch == ")":
depth = max(0, depth - 1)
elif ch == "," and depth == 0:
tags.append(text[start:i].strip())
start = i + 1
tags.append(text[start:].strip())
return [t for t in tags if t]
MANUAL_SECTION_RE = re.compile(r"^manual-section\(([\w./-]+)\)$")
def _manual_section_slug(tags: list[str]) -> str | None:
"""Pull the slug out of a `manual-section(<slug>)` CLASSIFICATION tag, if present."""
for tag in tags:
m = MANUAL_SECTION_RE.match(tag)
if m:
return m.group(1)
return None
def _resolve_manual_section(root: Path | None, slug: str) -> tuple[str, str, str] | None:
"""Resolve a manual-section(<slug>) tag to (display label, site link, doc-relative path).
Looks for <slug>.md (a top-level single-file section) or <slug>/index.md
(a directory-based section), matching the two shapes docs/manual/
actually uses. The display label is read fresh from the target's own
frontmatter (manTitle, falling back to title) rather than duplicated in
the tag, so a renumbered section never needs its tag updated -- only
the slug (the filename) does, and that only changes if the page itself
is renamed. Returns None -- silently, this is a build, not a check;
verify-manual.py is where a dangling slug is a real failure -- when
<root> is unset or neither candidate exists.
"""
if root is None:
return None
for relpath in (f"{slug}.md", f"{slug}/index.md"):
if (root / relpath).exists():
fm, _ = mt.parse(root / relpath)
label = fm.get("manTitle") or fm.get("title", slug)
return label, f"/{slug}/", relpath
return None
def render_entry(
fn: dict[str, list[str]], used_by: list[str], link=None, root: Path | None = None
) -> str:
"""Render one parsed function header as a manual entry body.
Emits the same man-page shape Section 5 was authored in — one 4-space
@@ -736,14 +796,22 @@ def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str
def names(raw: list[str]) -> list[str]:
return [n for n in re.split(r"[,\s]+", " ".join(raw)) if n]
classification = _classification_tags(fn.get("CLASSIFICATION", []))
refs = []
for label, values in (
("Dependencies", names(fn.get("DEPENDENCIES", []))),
("Classification", classification),
("Used by", sorted(used_by)),
):
if values:
rendered = ", ".join(link(v) if link else f"`{v}`" for v in values)
refs.append(f"**{label}:** {rendered}")
slug = _manual_section_slug(classification)
if slug:
resolved = _resolve_manual_section(root, slug)
if resolved:
label, _href, relpath = resolved
refs.append(f"**See also:** {label} (`docs/manual/{relpath}`)")
if refs:
block += "\n\n" + "\n\n".join(refs)
return block
@@ -847,7 +915,9 @@ SITE_SECTIONS = (
)
def render_entry_site(fn: dict[str, list[str]], used_by: list[str], link=None) -> str:
def render_entry_site(
fn: dict[str, list[str]], used_by: list[str], link=None, root: Path | None = None
) -> str:
"""Render one parsed function header as a manual entry body for the site.
Unlike `render_entry` (the single indented man-page block pandoc wants,
@@ -881,14 +951,22 @@ def render_entry_site(fn: dict[str, list[str]], used_by: list[str], link=None) -
def names(raw: list[str]) -> list[str]:
return [n for n in re.split(r"[,\s]+", " ".join(raw)) if n]
classification = _classification_tags(fn.get("CLASSIFICATION", []))
refs = []
for label, values in (
("Dependencies", names(fn.get("DEPENDENCIES", []))),
("Classification", classification),
("Used by", sorted(used_by)),
):
if values:
rendered = ", ".join(link(v) if link else f"`{v}`" for v in values)
refs.append(f"**{label}:** {rendered}")
slug = _manual_section_slug(classification)
if slug:
resolved = _resolve_manual_section(root, slug)
if resolved:
label, href, _relpath = resolved
refs.append(f"**See also:** [{label}]({href})")
if refs:
parts.append("\n\n".join(refs))
@@ -896,7 +974,7 @@ def render_entry_site(fn: dict[str, list[str]], used_by: list[str], link=None) -
def build_entries(
functions: dict[str, dict], link=None, site: bool = False
functions: dict[str, dict], link=None, site: bool = False, root: Path | None = None
) -> dict[str, list[tuple[str, str]]]:
"""Group rendered entries by category stem, ordered by function name.
@@ -904,6 +982,9 @@ def build_entries(
authored: a bidirectional link maintained by hand drifts the moment one
side is edited. `site` selects `render_entry_site` (headings + tables)
over `render_entry` (the man-page indented block `build_concat` needs).
`root` (docs/manual/) resolves any `manual-section(<slug>)`
CLASSIFICATION tag to that page's own title -- omitted, the default,
an entry with the tag just gets no "See also" line rather than failing.
"""
used_by: dict[str, list[str]] = {}
for name, fn in functions.items():
@@ -915,7 +996,7 @@ def build_entries(
out: dict[str, list[tuple[str, str]]] = {}
for name in sorted(functions):
fn = functions[name]
body = render(fn, used_by.get(name, []), link)
body = render(fn, used_by.get(name, []), link, root=root)
out.setdefault(fn["CATEGORY"][0], []).append((name, body))
return out
@@ -1016,7 +1097,9 @@ def build_site(root: Path, out: Path) -> list[dict]:
out.mkdir(parents=True)
functions = mt.parse_functions(FUNCTIONS)
entries = build_entries(functions, link=lambda n: _entry_link(n, functions), site=True)
entries = build_entries(
functions, link=lambda n: _entry_link(n, functions), site=True, root=root
)
sidebar: list[dict] = [{"label": "Home", "link": "/"}]
standard_groups: dict = {}
+1020 -49
View File
File diff suppressed because it is too large Load Diff
+26 -8
View File
@@ -291,6 +291,8 @@ network-shadow=### network
monitor=### monitor
shell-tools=### shell-tools
dev-tools=### dev-tools
classification=### For function authors
shadow-classification=### For function authors
c2=## C2 — Startup Side-Effects
autoexec=## C2 — Startup Side-Effects
startup=## C2 — Startup Side-Effects
@@ -360,6 +362,15 @@ fish-plugins-manifest=## fish_plugins Manifest
# ── Section 10: Installation ────────────────────────────────────
installation=# 10. INSTALLATION
install=# 10. INSTALLATION
os=## OS Compatibility
os-compatibility=## OS Compatibility
operating-system=## OS Compatibility
compatibility=## OS Compatibility
linux=## OS Compatibility
macos=## OS Compatibility
mac=## OS Compatibility
windows=## OS Compatibility
wsl=## OS Compatibility
sentinel=## Return Sentinel
updating=## Updating
@@ -412,14 +423,21 @@ contributing=# 15. CONTRIBUTING
contribute=# 15. CONTRIBUTING
forge=# 15. CONTRIBUTING
# ── Section 16: Attribution ───────────────────────────────────
attribution=# 16. ATTRIBUTION
credits=# 16. ATTRIBUTION
# ── Section 16: AI Agent Tooling ──────────────────────────────
agent=# 16. AI AGENT TOOLING
agent-tooling=# 16. AI AGENT TOOLING
agents.md=# 16. AI AGENT TOOLING
claude-code=# 16. AI AGENT TOOLING
antigravity=# 16. AI AGENT TOOLING
# ── Section 17: License ───────────────────────────────────────
license=# 17. LICENSE
licensing=# 17. LICENSE
agpl=# 17. LICENSE
copyright=# 17. LICENSE
# ── Section 17: Attribution ───────────────────────────────────
attribution=# 17. ATTRIBUTION
credits=# 17. ATTRIBUTION
# ── Section 18: License ────────────────────────────────────────
license=# 18. LICENSE
licensing=# 18. LICENSE
agpl=# 18. LICENSE
copyright=# 18. LICENSE
+907 -47
View File
File diff suppressed because it is too large Load Diff
+125
View File
@@ -0,0 +1,125 @@
# Function CLASSIFICATION schema
This is the canonical definition of the `# CLASSIFICATION` function
doc-header label. It's referenced from code comments and commit messages —
link here, not to anything under `AGENTS/` (that tree is git-ignored local
agent state, not part of the repo).
`CLASSIFICATION` isn't limited to command-safety hazards, even though most
of the closed set below is exactly that — it's the general-purpose place to
tag what a function touches or how it behaves, whenever that's worth
surfacing without reading the function's own body. `manual-section(<slug>)`
is the one tag in the set that isn't a hazard at all: it marks a function
that has its own dedicated manual section beyond this header (see
[Dedicated manual sections for complex subsystems](../CONTRIBUTING.md#dedicated-manual-sections-for-complex-subsystems)).
See [Public function documentation header](../CONTRIBUTING.md#public-function-documentation-header)
in `CONTRIBUTING.md` for where `CLASSIFICATION` fits among the other header
labels, and [C1 — Command Shadows](manual/08-components-reference/01-c1-command-shadows.md)
for the full list of C1-shadowed commands this schema's shadow tags refer to.
## Format
Optional. Comma-separated tags from the closed set below, on the indented
body line directly under the label:
```fish
# CLASSIFICATION
# uses-shadow(ls), destructive
```
Omit the label entirely when nothing applies — omission means "nothing to
flag," not "not yet audited," so don't add it speculatively, and don't add
it empty as a placeholder.
## Tags
- **`uses-shadow(name[,name...])`** — calls a C1-shadowed command (see the
C1 doc linked above) bare, deliberately wanting the overridden behavior
(e.g. `ls` wanting eza's icons for a human to read).
- **`bypasses-shadow(name[,name...])`** — calls `command <name>`,
`builtin <name>`, or (for `help` specifically) `__original_help $argv`,
deliberately forcing stock behavior because the shadow's override would
break this function's logic: timestamps leaking into a parsed capture,
`-i` prompting on a path meant to run unattended, structural output
changes breaking a `string`/`sed` parse, etc.
- **`self-limiting(name[,name...])`** — calls a shadowed command bare, and
it's safe not because the caller did anything but because *the shadow's
own logic* already neutralizes the override for this call. Verify the
actual condition per shadow, it's not the same check for each one:
- `rm` falls back to `command rm` for any flag **except** a bare `-r`,
`-R`, or `--recursive` (those still route to `trash put`) — so
`rm -f`/`rm -rf` qualify, but `rm -r $dir` alone does not.
- `mkdir` falls back to `command mkdir -p` for *any* flag at all, no
exception.
- `--color=auto`/`bat`'s own tty auto-detection (`grep`, `fgrep`,
`egrep`, `dir`, `vdir`, `cat` — verified byte-identical to stock when
piped or captured, since none of these force color on a
non-terminal).
Document it explicitly rather than leaving the bare call untagged: if a
shadow's bypass condition is ever weakened, narrowed, or removed, every
`self-limiting` site is one grep away instead of silently wrong.
Don't use this for `ls` — eza's long-format/icon layout is structural,
not tty-gated, so it stays different from stock `ls` even piped; a
bare `ls` call still needs `uses-shadow(ls)` or a real bypass.
- **`destructive`** — can irreversibly delete or overwrite data: `rm -f`,
`rm -rf`, truncating or force-overwriting a file, `git push --force`.
Routine cleanup of the function's own `$tmpdir`/`$_tmpdir`/`mktemp`
output (or other output it just created in this same call) is expected
behavior, not a hazard — don't tag it.
- **`network`** — makes an outbound network call: `curl`, `wget`, `ssh`,
`git fetch`/`pull`/`push`/`clone`, `paru`/`yay` (package-manager network
ops), talking to an API, etc.
- **`blocking-prompt`** — can block waiting on interactive confirmation
with no non-interactive escape hatch: a shadow's forced `-i`, fish's
`read` (genuinely waiting on a terminal — not a `string split | read`
or `while read` consuming a pipe, which never blocks), a `confirm`-style
prompt with no `--yes`/`--force`/`--silent` bypass. Don't tag a function
that's only ever meant to be run interactively at a prompt (a keybinding
handler, an fzf-driven picker) — the hazard this tag exists for is a
script or another function calling it unexpectedly, not a human running
it themselves.
- **`manual-section(<slug>)`** — this function has a dedicated manual
section beyond its own header; `<slug>` is that page's filename under
`docs/manual/` with the extension dropped (`16-agent-tooling` for
`docs/manual/16-agent-tooling.md`; a directory-based section like
`08-components-reference` uses its directory name the same way, resolved
against its `index.md`). `docs/build-manual.py` reads the target page's
own `manTitle`/`title` at build time and renders a **See also** line on
the function's generated entry — the tag only needs to keep pointing at
the right *file*; the displayed section number is never duplicated into
the tag, so it can't go stale on its own if the manual gets renumbered.
`docs/verify-manual.py` fails if the slug doesn't resolve to a real page.
Multiple functions may carry the same slug (`agents-init` and
`agents-vault` both point at `16-agent-tooling`, one section covering
both). See [Dedicated manual sections for complex subsystems](../CONTRIBUTING.md#dedicated-manual-sections-for-complex-subsystems)
in `CONTRIBUTING.md` for when a function's behavior has outgrown its
header and belongs in one of these instead.
## Placement
Directly under `# DEPENDENCIES` if the header has one; otherwise directly
under `# COMPONENT`; otherwise directly under `# CATEGORY`; otherwise as
the first label in the header block (this is the common case for internal
`_`-prefixed helpers, which usually carry none of the three).
## Judgment calls
`uses-shadow` vs `bypasses-shadow` is the easiest place to get subtly
wrong — verify against the actual code, not just whether the name appears
in the file. A function that only calls a *helper* which itself interacts
with a shadow does not get the tag; the tag belongs on the helper. When
generating these tags in bulk (e.g. delegating the sweep to another
model), review every result against the source before trusting it — this
schema's own rollout caught several false positives this way: a piped
`read` misread as an interactive prompt, a documented `--yes` flag missed
as an escape hatch, and cleanup of a function's own temp output flagged
as `destructive` despite the explicit exclusion above.
`rm` specifically has its own internal flag check (any flag other than
`-r`/`-R`/`--recursive` falls back to `command rm` *inside the shadow
itself*, before it ever touches trash) — a caller writing plain `rm -f`
or `rm -rf` is not bypassing anything itself, the shadow is. Only tag
`bypasses-shadow(rm)` when the caller explicitly writes `command rm` or
`builtin rm`; a bare `rm -f`/`rm -rf` call gets no shadow tag at all.
+4 -1
View File
@@ -29,8 +29,11 @@ are active in Insert, Normal, and Visual modes unless noted.
Ctrl+Alt+U Strip the first token of the current command line,
leaving arguments in place with the cursor at the
start. Useful for quickly retyping the command.
start. Useful for quickly retyping the command. On
an empty command line, first recalls the most recent
history entry (like Up), then strips its first token.
Example: "mkdir new_folder" -> " new_folder"
Example: empty line after "cd /home/me" -> " /home/me"
Ctrl+Alt+= Evaluate the current command line buffer with
Qalculate! (qalc) and print the result inline.
+1
View File
@@ -59,6 +59,7 @@ matter if you already use that specific tool. Skipped by
| `screen` | GNU screen; fallback backend for `jobrunner` when `tmux` is unavailable. |
| `marktext` | Markdown editor; backs the `md` wrapper, which is the only thing that references it. No distro packages it under a common name, so `fish-deps` offers the AUR package (`marktext-bin`) on Arch and otherwise installs upstream's AppImage to `~/.local/bin/marktext`. |
| `firejail` | Sandbox; needed only by `md --read-only`, which uses it to make MarkText unable to save over the file it opened. Every other `md` invocation works without it. |
| `win32yank.exe` | Clipboard bridge for WSL2; backs the `y`/`p`/`paste`/`hist` clipboard fallback chain when neither `wl-copy`/`wl-paste` nor `xclip` are present. `fish-deps` only offers to install it when WSL2 is detected (`microsoft` in `/proc/sys/kernel/osrelease`), downloading the x86_64 binary from GitHub releases to `~/.local/bin`. |
## Terminal Emulators
@@ -19,18 +19,22 @@ all of these commands.
rg rg --hyperlink-format=kitty system rg
mkdir verbose path-tree display on creation mkdir -p silently
bash XDG bashrc + $SHELL reset on exit system bash
history timestamps prepended to every entry fish builtin history
cp / mv forced -i confirmation prompt cp / mv unmodified
wget forced --continue (resume downloads) system wget
grep/fgrep/egrep forced --color=auto system grep variants
dir / vdir forced --color=auto system dir / vdir
help config intercepts "help config" → config-help fish builtin help
claude auto-links AGENTS.md as CLAUDE.md before launch command claude
claude ensures AGENTS/ is scaffolded before launch command claude
edit multi-editor launcher (GUI/term + fallbacks) $EDITOR/nvim/nano/vi
When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files
are permanently deleted, not trashed. There is no intermediate safety net.
`history` itself is never shadowed — every function in this config that
reads history depends on its stock builtin semantics. `pretty-history`
(same `aliases-tricks` toggle) is a separate command that prints history
with a timestamp prepended to every entry.
## Sub-categories
`__fish_config_op_aliases` sub-divides into six sub-categories, each with
@@ -60,6 +64,51 @@ and the `help config` interception.
## dev-tools
`claude` (AGENTS.md/CLAUDE.md auto-linking) and `edit` (multi-editor
`claude` (AGENTS/ scaffolding) and `edit` (multi-editor
launcher), plus `agy`.
## For function authors
Calling one of these names bare from inside your own function means the
override runs whenever C1 (or its sub-category) is on — which may not be
what your function wants: a shadow can change stdout (`cat`'s syntax
highlighting, `mkdir`'s tree display), prompt interactively where none is
expected (`cp`/`mv`'s forced `-i`), or reshape output structurally (`ls`'s
icons/columns, `rg`'s hyperlink markers). If your function's logic depends
on stock behavior, bypass the shadow deterministically, regardless of the
toggle state:
Shadow Bypass Why
─────────────────────────────────────────────────────────────────────────
ls, cat, rm, less, du, command <name> Real external
top, ping, ssh, rg, binaries — a
mkdir, bash, cp, mv, real system command
wget, grep/fgrep/egrep, exists to fall
dir/vdir, claude back to.
cd builtin cd The one true
fish builtin
in this table.
help config __original_help $argv `help` is neither
a builtin nor an
external binary
(embedded in the
fish binary
itself) — see
conf.d/help.fish
for why the
wrapper keeps its
own backup copy.
edit (nothing to bypass to) Purely our own
invention, no
stock command
exists. Call
$EDITOR/$VISUAL
yourself if you
want a plain
editor launch.
A function's own doc header records which of these it depends on: see the
`CLASSIFICATION` label (`uses-shadow(...)` / `bypasses-shadow(...)`),
documented in full at
[`docs/function-classification-schema.md`](https://git.rootiest.dev/rootiest/fish-config/src/branch/main/docs/function-classification-schema.md).
@@ -13,7 +13,7 @@ These features couple the shell to specific external tools. Disabling
spwin Kitty or WezTerm
tab Kitty, WezTerm, or Konsole
split Kitty or WezTerm
hist fzf + wl-copy (Wayland clipboard)
hist fzf + wl-copy, xclip, or win32yank.exe (WSL2)
logs fzf + ov; reads from ~/.terminal_history/
upgrade paru or yay (Arch Linux only)
WakaTime hook wakatime CLI and a configured API key
+45
View File
@@ -6,6 +6,13 @@ sidebar:
helpKeywords:
- installation
- install
- os
- operating system
- compatibility
- linux
- macos
- windows
- wsl
---
This configuration is managed as a git repository. To deploy on a new machine:
@@ -17,6 +24,44 @@ Then open a new Fish shell. Fisher installs automatically on first launch
and the Catppuccin Mocha theme is applied. All other plugin functionality is
bundled directly with this config and requires no additional installation.
## OS Compatibility
This is a **Linux-only** configuration. It is developed and tested on an
Arch Linux system; `fish-deps` also detects `apt`, `dnf`, `zypper`, and
`yum` for broader distro support, but coverage outside Arch is thinner.
IMPORTANT: A number of functions call Linux-specific subsystems directly, with
no fallback:
- `systemd-inhibit` (`wake-lock`)
- `zramctl` / `swapon` (`swapstat`)
- `sbctl` and UEFI Secure Boot state (`sbver`)
- GNU coreutils flags such as `stat -c` and `numfmt` (`sudo-toggle`,
`dng2avif`), which differ or don't exist under a BSD userland
Clipboard access (`y`, `p`, `paste`, `hist`) is the exception: it falls back
through `wl-copy`/`wl-paste` (Wayland), `xclip` (X11), and `win32yank.exe`
(WSL2), so it works on all three. There is still no `pbcopy`/`pbpaste`
fallback for macOS.
**macOS** is not supported. `_fish_deps_detect_pm` does check for `brew`, but
that alone does not make the functions above work — they have no macOS
equivalent path today.
**Windows** is not supported. Fish itself has no native Windows build;
upstream's own "Windows" install docs are Cygwin/WSL workarounds, not a real
port. This config is not tested under WSL either. WSL2 runs a real Linux
kernel and can run `systemd`, so basic shell use may work; `zramctl`,
`sbctl`, and Secure Boot state are still meaningless inside a VM, but
clipboard integration works via `win32yank.exe` (see above) once it's
installed on the Windows side and reachable through WSL interop.
**Assumed present on any Linux system this runs on:** `git`, `gpg`, `tar`,
and GNU coreutils (for `stat`, `date`, `numfmt`). These are not tracked by
`fish-deps` — see the [Dependency Catalog](/06-dependency-catalog/) — because
they are base-system utilities, not opt-in software with an install journey
to manage. A system missing any of them is missing basic Linux tooling, not
a `fish-deps` gap.
## Return Sentinel
`config.fish` ends with a return sentinel guard. Any lines appended after it by
+312
View File
@@ -0,0 +1,312 @@
---
title: AI Agent Tooling
manTitle: 16. AI AGENT TOOLING
sidebar:
order: 20
helpKeywords:
- agent
- agents-init
- agents-vault
- AGENTS.md
- claude-code
- agy
- antigravity
---
This section explains the machinery behind AI coding agents (Claude Code,
Antigravity/agy) working in a project checked out from this configuration:
where their instructions live, how they get there, and the safety rules
that keep an agent's launch-time bookkeeping from touching a repository's
own tracked history. Command-line usage for the functions named here
(`agents-init`, `agents-vault`) is generated from their own doc headers —
see Section 5.
## The problem this solves
An AI coding agent needs a persistent, project-scoped place to keep
instructions, memory, and working notes. Committing that material directly
into a project's normal history mixes two concerns that change at
different rates and for different reasons: the project's own code, and an
agent's evolving working state. It also means every project accumulates
its own copy of agent tooling (hooks, version files, convention
documents) that has nothing to do with that project's actual purpose.
`agents-init` and `agents-vault` exist to keep that material out of the
main repository while still making it feel local: an agent reads and
writes `AGENTS.md` exactly where it would expect to find it, but the real
content and its history live in a separate, self-contained git repository
that the main project never tracks.
## The AGENTS.md convention
`AGENTS.md` is a plain-text file at a project's root (and, as this
configuration extends the idea, at the root of any subdirectory with its
own scoped conventions) that an AI agent reads for repository-specific
instructions. It has become a convention shared across coding agents, not
one tool's proprietary format.
Claude Code originally required its own `CLAUDE.md` filename specifically.
It now reads `AGENTS.md` natively whenever no `CLAUDE.md` is present, which
retired the need for this configuration to create, maintain, or symlink
`CLAUDE.md` at all. A project scaffolded by `agents-init` today carries
only `AGENTS.md` — at the root, and in any subdirectory that has grown its
own scoped conventions (`functions/`, `docs/`, and so on, in this
repository's own case). A leftover `CLAUDE.md` from before this change is
retired automatically the next time `agents-init` runs: renamed, not
preserved under its old name, so nothing is ever left tracking two copies
of the same instructions under two different filenames.
## The AGENTS/ sub-repository
`agents-init` scaffolds a directory named `AGENTS/` at a project's root.
It is a self-contained git repository — its own `.git`, its own commit
history, its own hooks — and it is gitignored from the project it lives
inside. The project's own `AGENTS.md` (and every subdirectory's) is a
symlink into it:
$PROJECT/AGENTS/
├── AGENTS.md Canonical root agent spec (real file)
├── functions/AGENTS.md Canonical spec for functions/, and likewise for any other scoped subdirectory
├── plans/ Superpowers implementation plans
├── specs/ Superpowers design specs
├── devlogs/ Agent development logs
├── .version MAJOR.MINOR.PATCH structure version
└── .agents-tools/ Version-bump script and git hook shims (committed)
An agent editing `$PROJECT/AGENTS.md` is, transparently, editing
`$PROJECT/AGENTS/AGENTS.md` — the file-editing tools most agents ship with
resolve a symlinked directory's contents normally, but they cannot write
*through* a symlinked file itself, which is why the seed content
`agents-init` writes for a brand-new project spells this out directly to
the agent reading it.
IMPORTANT: This means an agent must never try to write to a *symlink
named* `AGENTS.md` directly. The seed instructions `agents-init` writes
for a fresh project tell the agent this explicitly, pointing it at the
real file inside `AGENTS/`.
### Version tracking and hooks
Every `AGENTS/` repository carries a `.version` file (seeded `1.0.0`) and
a self-contained version bumper, wired through `core.hooksPath` rather
than the ordinary `.git/hooks/` directory:
- A **pre-commit** hook bumps `.version` on every commit: the MINOR
field moves when the set of tracked top-level directories changes
(a new subdirectory convention was adopted, or one was dropped), the
PATCH field otherwise. The MAJOR field is manual-only.
- A **prepare-commit-msg** hook appends `(vX.Y.Z)` to the commit
subject, so the version history is legible from `git log` alone.
Each hook shim then chains to whatever hook of the same name the
project's *global* or *system* `core.hooksPath` already points at — a
credential scanner like ggshield, Git LFS, or anything else already
wired in ahead of this. Pointing `core.hooksPath` at `.agents-tools/hooks`
locally does not shadow those; it runs both.
The `.agents-tools/` scripts themselves are copied in from this
configuration's own `scripts/agents-tools/` and refreshed automatically
whenever their version marker moves, so every project's `AGENTS/`
repository stays current with this configuration without any manual step.
Downstream tooling that wants to know whether a project's `AGENTS/`
*structure* changed — as opposed to just its content — can read the
`.version` file's MINOR field directly rather than diffing the tree.
## Per-directory discovery
The convention is not limited to a project's root. Any directory that
carries its own `AGENTS.md` — `functions/`, `docs/`, or a subdirectory of
a much larger project with genuinely distinct conventions of its own —
gets the identical treatment: a real file inside `AGENTS/<that path>/`,
and a symlink at the project location pointing back to it. `agents-init`
finds these automatically on every run, rather than working from a fixed
list, by walking the project tree for any file literally named
`AGENTS.md` or `CLAUDE.md`.
Each directory found is settled into exactly one of four states, in
order, so a later run only ever sees a directory that is already
consistent:
1. **An inverted mirror** (an older layout, where `CLAUDE.md` was the
real file inside `AGENTS/` and `AGENTS.md` was symlinked to it) is
flipped in place — same bytes, new name.
2. **A real file at the project level, with no real file inside
`AGENTS/` yet**, is adopted: a lone `AGENTS.md` moves in as-is; a
lone `CLAUDE.md` is renamed on the way in, never preserved under its
own name. When both `AGENTS.md` and `CLAUDE.md` are real files at
once, byte-identical content is deduplicated (the `AGENTS.md` side is
kept); different content is left exactly as it is, with a warning —
this function has no way to know which one is authoritative, and
guessing wrong would silently discard the other.
3. **A stray `CLAUDE.md` inside `AGENTS/`** left over once `AGENTS.md`
is settled there is removed — nothing named `CLAUDE.md` survives
inside the mirror.
4. **The project-level symlink** is created or repaired if missing or
stale, and any `CLAUDE.md` still at the project level is removed. A
real file that turns up here *after* the mirror already settled (for
instance, an agent's own `/init`-style command writing a fresh
`CLAUDE.md`) is held to the same identical-or-differ rule as step 2:
a duplicate is dropped, anything different is left alone with a
warning rather than silently overwritten.
## Safety: what discovery will never touch
Because discovery walks the whole project tree rather than a fixed list,
it deliberately prunes several classes of directory before it ever
considers what's inside them:
- **Anything outside the project entirely.** A directory that has no
git repository of its own, but happens to carry a lone `AGENTS.md` or
`CLAUDE.md` (a home directory scaffolded this way, for instance), is
synced at that single location only — no recursive walk runs at all.
Recursive discovery only ever runs inside a real git repository.
- **Nested repositories.** Any subdirectory that is itself a git
repository — a submodule, a nested clone, a plugin checked out inside
a tool's own state directory — belongs to a different project and is
never walked into.
- **Dot-directories.** Anything named starting with `.` (`.git`,
`.claude`, `.gemini`, `.github`, and so on) is a tool's own state or
configuration, not a project's own scoped convention, and is skipped
unconditionally.
- **Generated output.** `build/`, `dist/`, `out/`, and `target/`
directories are never inspected — nothing generated by a build step
is a source of hand-authored instructions.
- **`node_modules/`**, and any directory literally named `AGENTS` other
than the current project's own mirror.
## Safety: deliberately tracked files are left alone
Discovery can reach a directory whose `AGENTS.md` or `CLAUDE.md` is
already committed to the project's own history on purpose — a team's
shared conventions file in a monorepo subdirectory, for instance, tracked
long before this configuration's owner ever cloned it. Replacing that
file with a symlink would change it from an ordinary tracked file into a
link pointing outside the repository the moment `agents-init` next runs,
which is not a decision this tool should make unattended on someone
else's behalf.
A real file is left untouched, instead of adopted or replaced, whenever
**both** of the following hold:
- it is tracked in git's index — staged or committed, checked with
`git ls-files`. A file that has never been `git add`ed is not tracked
by this definition, even if it sits right next to files that are.
- the project's `.gitignore` actually exists and has content in it.
Neither condition alone is enough to protect a file. An untracked file is
always safe to adopt, regardless of what `.gitignore` says about it
(nothing has been committed yet, so nothing is lost). A tracked file in a
project with *no* established ignore conventions at all — no
`.gitignore`, or an empty one — is treated as the very first time this
convention has been applied to that project, rather than a deliberate
choice to keep tracking it: `agents-init` adopts it the same way it would
adopt any other real file, which is the same behavior this tool has
always had for a project's own root file.
NOTE: In practice, this means a mature project with an established
`.gitignore` will have any already-committed `AGENTS.md`/`CLAUDE.md` left
alone across the board — root included — and will only ever adopt one
during that project's first encounter with this convention, before a
`.gitignore` entry for it exists yet.
When a directory is skipped for this reason, `agents-init` prints a
warning naming the file and explaining why, rather than staying silent
about a directory it chose not to touch.
## Scenario reference
Every combination of what a directory can hold, laid out directly. "No"
in the tracked column also covers a tracked file in a project with no
populated `.gitignore` (the bootstrap case, above) — both behave the same
way. Whenever the tracked column reads "Yes", that reason always wins
over the identical-or-different comparison below it, and the warning
printed names the file as tracked rather than as differing — the outcome
(left alone) is the same either way, only the explanation differs.
Settling a directory for the first time — a real `AGENTS.md`, a real
`CLAUDE.md`, both, or neither, discovered fresh:
Found Deliberately tracked? Result
-------------------------------- --------------------- -----------------------------------------------
Only AGENTS.md (real) No Adopted into AGENTS/, symlinked back.
Only AGENTS.md (real) Yes Left exactly as it is; not adopted.
Only CLAUDE.md (real) No Adopted, renamed to AGENTS.md, symlinked back.
Only CLAUDE.md (real) Yes Left exactly as it is; not adopted or renamed.
Both, byte-identical No AGENTS.md adopted; duplicate CLAUDE.md dropped.
Both, byte-identical Yes (either) Left exactly as they are; neither touched.
Both, different content n/a Neither touched; warns, resolve by hand.
Mirror has CLAUDE.md (real) n/a Flipped in place: renamed, nothing lost.
Correct AGENTS.md symlink exists n/a Nothing happens -- already settled.
A new real file appearing after a directory's mirror has already settled
— an agent's own `/init`-style command, for instance, writing a fresh
`CLAUDE.md` where an `AGENTS.md` is already symlinked:
New file vs. mirror Deliberately tracked? Result
------------------- --------------------- --------------------------------------------------------
Byte-identical No Adopted as a duplicate; the new file is dropped.
Byte-identical Yes Left as it is; not adopted, even though content matches.
Different content No Left as it is; warns that it differs, resolve by hand.
Different content Yes Left as it is; warns that it's tracked, not adopted.
And whatever a directory holds, it never gets this far at all if
discovery pruned it outright — see the containment rules above: nested
repositories, dot-directories, `node_modules/`, generated-output
directories, and any directory literally named `AGENTS`.
## plans/, specs/, and devlogs/
`agents-init --plugins` (the second half of what a bare `agents-init` run
does) wires up three more directories inside `AGENTS/`: `plans/` and
`specs/` for the superpowers skills' implementation plans and design
documents, and `devlogs/` for agent-authored development notes. Real
content from every legacy location this configuration has ever used for
these (`docs/plans`, `docs/superpowers/plans`, and an older
`AGENTS/plugins/` layer from before the sub-repository consolidated them)
is merged into the canonical `AGENTS/plans` and `AGENTS/specs` on first
run, and the legacy locations are removed once merged.
`docs/superpowers/plans` and `docs/superpowers/specs` are always
symlinked to their `AGENTS/` counterparts, because the superpowers skills
expect to find them there by default. `docs/plans`, `docs/specs`, and
`docs/devlogs` are only created as symlinks when a project already had a
real directory by that name — nothing forces those paths to exist for a
project that never used them.
## The launch lifecycle
The `claude` and `agy` wrapper functions each run `agents-init --quiet`
(full setup: both the `AGENTS.md` symlink step and the plans/specs/devlogs
wiring) before launching the real CLI, on every invocation. This is what
makes the whole system self-healing: a project that has drifted from the
expected layout — a stale symlink, a newly-added subdirectory's
instructions not yet adopted, a leftover `CLAUDE.md` — is corrected
automatically the next time an agent is launched there, with no separate
setup step for a person to remember.
At the end of every `agents-init` run, any uncommitted change inside
`AGENTS/` is committed automatically, so whatever an agent wrote during
its session is captured without anyone needing to run `git add` on a
repository they were never meant to think about directly. That commit is
strictly local: `agents-init` never fetches or pushes, because a network
round trip running synchronously ahead of every agent launch would block
the launch itself for as long as an unreachable remote takes to time out.
A project's `AGENTS/` repository that has its own upstream is pulled and
pushed by hand, on its owner's own schedule.
`agents-vault` is a related but distinct tool: where `AGENTS/` holds
*one project's* agent state, `agents-vault` backs up curated agent memory
that lives *outside* any project tree entirely — `~/.claude/projects/*/memory`
and similar host-scoped locations — into its own host-scoped repository.
The `claude`/`agy` wrappers sync both on every launch. See
`__fish_agent_vault_autopush` in Section 7 for its one user-facing
configuration variable; command-line usage for both tools is in Section 5.
@@ -1,8 +1,8 @@
---
title: Attribution
manTitle: 16. ATTRIBUTION
manTitle: 17. ATTRIBUTION
sidebar:
order: 20
order: 21
helpKeywords:
- attribution
- credits
@@ -1,8 +1,8 @@
---
title: License
manTitle: 17. LICENSE
manTitle: 18. LICENSE
sidebar:
order: 21
order: 22
helpKeywords:
- license
- licensing
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: Fish Shell Configuration
title: Rootiest Fish Configuration
description: Reference manual for the rootiest fish configuration.
manTitle: DESCRIPTION
sidebar:
+1
View File
@@ -64,6 +64,7 @@ SECTIONS = (
"CATEGORY",
"COMPONENT",
"DEPENDENCIES",
"CLASSIFICATION",
"SYNOPSIS",
"DESCRIPTION",
"ARGUMENTS",
+125 -2
View File
@@ -202,13 +202,25 @@ def test_dependencies_resolve():
Catches typos, and catches stale entries when a dependency is renamed
or deleted. External binaries are accepted when some file in the tree
guards them with `type -q`, which is this repo's convention.
guards their availability -- `type -q name`, `command -q name`,
`command -v name`, or `which name`, this repo's four interchangeable
existence-check idioms -- either directly, or, for a multi-tool check
like dng2avif's `for cmd in magick ffmpeg avifenc exiftool; type -q
$cmd`, indirectly through a loop variable.
"""
repo = Path(__file__).parent.parent
functions = _parsed_functions()
known = {p.stem for p in (repo / "functions").glob("*.fish")} | set(functions)
# `type` takes its own flags (e.g. `-f` to exclude functions from the
# match, as in `type -q -f $p`) that can sit between `-q` and the name
# -- skip over any of those so the guard is still recognized.
guard_re = re.compile(r"(?:type -q(?:\s+-\w+)*|command -q|command -v|which)\s+([\w.\-]+)")
for path in list(repo.glob("conf.d/*.fish")) + list((repo / "functions").glob("*.fish")):
known |= set(re.findall(r"type -q\s+(\S+)", path.read_text(encoding="utf-8")))
text = path.read_text(encoding="utf-8")
known |= set(guard_re.findall(text))
for var, names in re.findall(r"for\s+(\w+)\s+in\s+([^\n;]+)", text):
if re.search(rf"type -q(?:\s+-\w+)*\s+\$\{{?{re.escape(var)}\}}?\b", text):
known |= set(names.split())
dangling = []
for name, fn in functions.items():
for dep in (d for d in re.split(r"[,\s]+", " ".join(fn.get("DEPENDENCIES", []))) if d):
@@ -1662,6 +1674,117 @@ def test_concat_section_five_stays_verbatim():
assert not offenders, f"backticks inside verbatim entries: {offenders[:3]}"
def test_manual_section_slug_extracts_tag():
"""`manual-section(<slug>)` is found among other CLASSIFICATION tags, or not at all."""
import build_manual
assert build_manual._manual_section_slug(["destructive", "manual-section(foo-bar)"]) == "foo-bar"
assert build_manual._manual_section_slug(["network"]) is None
assert build_manual._manual_section_slug([]) is None
def test_resolve_manual_section_reads_target_frontmatter():
"""Resolves both page shapes (top-level file, directory index) and reports None cleanly."""
import build_manual
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "solo.md").write_text(
"---\ntitle: Solo\nmanTitle: 9. SOLO\n---\nbody\n"
)
(root / "grouped").mkdir()
(root / "grouped" / "index.md").write_text(
"---\ntitle: Grouped\nmanTitle: 10. GROUPED\n---\nbody\n"
)
label, href, relpath = build_manual._resolve_manual_section(root, "solo")
assert label == "9. SOLO", label
assert href == "/solo/", href
assert relpath == "solo.md", relpath
label, href, relpath = build_manual._resolve_manual_section(root, "grouped")
assert label == "10. GROUPED", label
assert href == "/grouped/", href
assert relpath == "grouped/index.md", relpath
assert build_manual._resolve_manual_section(root, "missing") is None
assert build_manual._resolve_manual_section(None, "solo") is None
def test_render_entry_see_also_appears_only_when_root_resolves():
"""The man-page See-also line needs both the tag and a root that resolves it."""
import build_manual
fn = {
"SYNOPSIS": ["thing"],
"DESCRIPTION": ["Does a thing."],
"CLASSIFICATION": ["manual-section(deep-dive)"],
}
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "deep-dive.md").write_text(
"---\ntitle: Deep Dive\nmanTitle: 20. DEEP DIVE\n---\nbody\n"
)
out = build_manual.render_entry(fn, [], root=root)
assert "**See also:** 20. DEEP DIVE (`docs/manual/deep-dive.md`)" in out, out
# No root at all -- same as every other existing caller that never
# passes one -- silently omits the line rather than raising.
out_no_root = build_manual.render_entry(fn, [])
assert "See also" not in out_no_root, out_no_root
# A root that exists but doesn't have the target page: also silent.
with tempfile.TemporaryDirectory() as empty:
out_missing = build_manual.render_entry(fn, [], root=Path(empty))
assert "See also" not in out_missing, out_missing
def test_render_entry_site_see_also_is_a_real_link():
"""The site's See-also line is a markdown link to the resolved page's site path."""
import build_manual
fn = {
"SYNOPSIS": ["thing"],
"DESCRIPTION": ["Does a thing."],
"CLASSIFICATION": ["manual-section(deep-dive)"],
}
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / "deep-dive.md").write_text(
"---\ntitle: Deep Dive\nmanTitle: 20. DEEP DIVE\n---\nbody\n"
)
out = build_manual.render_entry_site(fn, [], root=root)
assert "**See also:** [20. DEEP DIVE](/deep-dive/)" in out, out
def test_real_manual_section_tags_resolve():
"""Every manual-section(<slug>) tag on a real function points at a real page.
This is the enforcement half of the convention: build-manual.py stays
silent about a dangling slug (it just skips the See-also line), so this
is the only thing that turns a typo'd or stale slug into a failure.
"""
import build_manual
functions = mt.parse_functions(build_manual.FUNCTIONS)
checked = 0
for name, fn in functions.items():
tags = build_manual._classification_tags(fn.get("CLASSIFICATION", []))
slug = build_manual._manual_section_slug(tags)
if slug is None:
continue
checked += 1
resolved = build_manual._resolve_manual_section(build_manual.MANUAL, slug)
assert resolved is not None, (
f"{name}'s manual-section({slug}) tag doesn't resolve to "
f"docs/manual/{slug}.md or docs/manual/{slug}/index.md"
)
assert checked > 0, "expected at least one real function to carry manual-section(...)"
TESTS = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
@@ -4,6 +4,9 @@
# COMPONENT
# logging/terminal-capture
#
# CLASSIFICATION
# self-limiting(rm,mkdir)
#
# SYNOPSIS
# __fish_config_sync_logging
#
+7 -3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# bypasses-shadow(cat)
#
# SYNOPSIS
# __fish_help_header <name> [args...]
#
@@ -45,9 +48,10 @@ function __fish_help_header --argument-names name
# First argument only -- see DESCRIPTION.
contains -- "$argv[2]" -h --help; or return 1
set -l c_ttl (set_color --bold)
set -l c_sec (set_color --bold brblue)
set -l c_rst (set_color normal)
__fish_palette
set -l c_ttl $c_cmd
set -l c_sec $c_head
set -l c_rst $c_reset
set -l miss " No documentation header found. Try: help config $name"
set -l file (functions -D -- $name 2>/dev/null)
+28 -9
View File
@@ -19,11 +19,18 @@
# TERM=dumb, which yields empty strings, and is unaffected by whether
# stdout is a tty or a pipe.)
#
# A role is a semantic slot, not a colour. c_flag and c_warn are both
# yellow but stay separate, as do c_ok and c_accent (both green) --
# merging either pair would foreclose ever restyling one without the
# other. c_accent is the command name in logs and smart_exit, which
# style it green where the rest of the config styles it bold.
# A role is a semantic slot, not a colour. c_flag and c_warn can both be
# yellow but stay separate, as do c_ok and c_accent -- merging either
# pair would foreclose ever restyling one without the other. c_accent is
# the command name in logs and smart_exit, which style it green where
# the rest of the config styles it bold.
#
# c_cmd, c_flag, c_arg, and c_dim are drawn from fish's own highlighter
# variables (fish_color_command/option/param/autosuggestion) when set,
# and c_head from the pager's fish_pager_color_prefix -- the same colour
# a real command, flag, argument, or pager heading gets at the prompt.
# Each falls back to its previous static default when the theme
# variable is unset or empty (e.g. a --no-config script).
#
# ARGUMENTS
# none
@@ -49,15 +56,27 @@
function __fish_palette --no-scope-shadowing --description 'Define the shared output colour palette in the caller scope'
set c_reset (set_color normal)
set c_head (set_color --bold cyan)
set c_cmd (set_color --bold)
set c_cmd (set_color brblue)
set c_flag (set_color brgreen)
set c_arg (set_color cyan)
set c_flag (set_color yellow)
set c_dim (set_color brblack)
set c_head (set_color --bold cyan)
set c_warn (set_color yellow)
set c_err (set_color red)
set c_ok (set_color green)
set c_accent (set_color green)
set c_dim (set_color brblack)
set c_sel (set_color --bold magenta)
set c_hi (set_color --bold white)
# Theme overrides: same roles fish's own highlighter/pager use, so
# --help text matches the prompt instead of a fixed guess at it.
set -l roles command option param autosuggestion
set -l vars c_cmd c_flag c_arg c_dim
for i in (seq (count $roles))
set -l themevar fish_color_$roles[$i]
set -q $themevar; and test (count $$themevar) -gt 0
and set $vars[$i] (set_color $$themevar)
end
set -q fish_pager_color_prefix; and test (count $fish_pager_color_prefix) -gt 0
and set c_head (set_color --bold $fish_pager_color_prefix)
end
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# __fish_real_command <name>
#
+3
View File
@@ -4,6 +4,9 @@
# COMPONENT
# autoexec/sync
#
# CLASSIFICATION
# self-limiting(rm), destructive
#
# SYNOPSIS
# __fish_user_dots_link
#
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# bypasses-shadow(grep)
#
# SYNOPSIS
# __kitty_logging_has_watcher
#
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# _agents_init_ensure_gitignore <root> <label> <pattern>...
#
@@ -0,0 +1,45 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _agents_init_path_is_protected <root> <path>
#
# DESCRIPTION
# Decides whether a real (non-symlink) file should be left alone rather
# than adopted into the AGENTS/ mirror or replaced with a symlink,
# because it looks like a deliberately tracked project file rather than
# an incidental one this project hasn't yet engaged agents-init's
# convention for.
#
# A file is protected only when BOTH are true:
# - it is tracked in git's index at <root> -- staged or committed, via
# `git ls-files`. A file that has never been `git add`ed (even if it
# sits right next to tracked files) is not tracked by this
# definition, and neither is one that is merely gitignored.
# - <root>/.gitignore exists and is non-empty -- a project with no
# ignore rules at all has never engaged with the convention this
# tool manages, so a tracked file there is more likely incidental
# (e.g. the very first agents-init run, before anyone thought to
# ignore it) than a deliberate choice to keep tracking it.
#
# Neither check alone is enough: an untracked file is always safe
# regardless of .gitignore state (nothing has been committed to protect),
# and a tracked file in a project with no established ignore
# conventions is treated as adoptable rather than deliberate.
#
# ARGUMENTS
# root Absolute path to the project root (may or may not be a git repo)
# path Absolute path to the file being considered
#
# EXIT STATUS
# 0 Protected -- leave this file alone
# 1 Not protected -- safe to adopt/replace
#
# EXAMPLE
# _agents_init_path_is_protected /path/to/project /path/to/project/functions/CLAUDE.md
function _agents_init_path_is_protected --argument-names root path
test -n "$root" -a -n "$path"; or return 1
git -C "$root" --literal-pathspecs ls-files --error-unmatch -- "$path" >/dev/null 2>&1; or return 1
test -s "$root/.gitignore"; or return 1
return 0
end
@@ -0,0 +1,251 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# DEPENDENCIES
# _agents_init_path_is_protected
#
# CLASSIFICATION
# self-limiting(rm,mkdir), bypasses-shadow(mv)
#
# SYNOPSIS
# _agents_init_sync_instructions <root> <agents_dir> <rel>
#
# DESCRIPTION
# Normalizes one directory's agent instruction file(s) into the
# AGENTS.md-only shape: <root>/<rel>/AGENTS.md becomes a symlink to the
# real file at <agents_dir>/<rel>/AGENTS.md (or, for the root itself,
# <agents_dir>/AGENTS.md directly), and no CLAUDE.md survives anywhere
# for that directory -- neither at the project level nor inside the
# mirror.
#
# The exception is a real file that is deliberately git-tracked -- in
# git's index, in a project whose .gitignore is non-empty (see
# _agents_init_path_is_protected). Such a file is never adopted,
# relinked, or removed: whenever steps 2 or 4 find one, they leave that
# directory's instruction files exactly as they are and warn on stderr.
#
# Four states of <rel> are handled, in order, so later steps only ever
# see a settled mirror:
#
# 1. The mirror itself is inverted (CLAUDE.md real, AGENTS.md symlinked
# to it). Flipped in place: same bytes, new name.
# 2. The mirror has no real AGENTS.md yet, and the project directory
# has one or both files. If either real file is protected, both are
# left untouched, the mirror is not populated, and a warning naming
# the protected file(s) goes to stderr. Otherwise a lone real file
# (either name) is adopted as the mirror's AGENTS.md -- a lone
# CLAUDE.md is renamed, never preserved under its own name. Both real and byte-identical: the
# AGENTS.md side is adopted and the duplicate CLAUDE.md is dropped.
# Both real and different: neither is touched and a warning is
# printed to stderr -- this function has no way to know which side
# is authoritative, and silently keeping one would silently discard
# the other.
# 3. Any CLAUDE.md still left in the mirror once AGENTS.md is settled
# (belt-and-suspenders past step 1) is removed.
# 4. The project-level AGENTS.md symlink is (re)created if missing or
# stale, and any CLAUDE.md left at the project level is removed. A
# real project-level file found here (written after the mirror
# settled) is checked for protection first, as in 2 -- a protected
# one is left alone even if byte-identical to the mirror. An
# unprotected one is removed only if byte-identical to the mirror; if
# it differs, nothing is touched and a warning goes to stderr, as in 2.
#
# ARGUMENTS
# root Absolute path to the project root
# agents_dir Absolute path to the project's AGENTS/ sub-repo
# rel Path of the directory being synced, relative to root
# ("." for the root itself)
#
# EXIT STATUS
# 0 <rel> is settled (including the both-real-and-different and
# protected-file skips, which are not failures of this function)
# 1 A filesystem operation (mkdir/mv/rm/ln) failed
#
# RETURNS
# One "→ ..." line per change made, on stdout; nothing when <rel> was
# already settled. A skip warning goes to stderr, never stdout, so it is
# never mistaken for a change.
#
# EXAMPLE
# _agents_init_sync_instructions /path/to/project /path/to/project/AGENTS .
# _agents_init_sync_instructions /path/to/project /path/to/project/AGENTS functions
function _agents_init_sync_instructions --argument-names root agents_dir rel
test -n "$root" -a -n "$agents_dir" -a -n "$rel"; or return 1
set -l proj_dir "$root"
set -l mirror_dir "$agents_dir"
if test "$rel" != "."
set proj_dir "$root/$rel"
set mirror_dir "$agents_dir/$rel"
end
set -l proj_agents "$proj_dir/AGENTS.md"
set -l proj_claude "$proj_dir/CLAUDE.md"
set -l mirror_agents "$mirror_dir/AGENTS.md"
set -l mirror_claude "$mirror_dir/CLAUDE.md"
# Display names for progress lines: bare at the root, "<rel>/..." below it.
set -l disp_agents AGENTS.md
set -l disp_claude CLAUDE.md
set -l mirror_rel AGENTS
if test "$rel" != "."
set disp_agents "$rel/AGENTS.md"
set disp_claude "$rel/CLAUDE.md"
set mirror_rel "AGENTS/$rel"
end
mkdir -p "$mirror_dir"
or begin
echo "_agents_init_sync_instructions: could not create $mirror_dir" >&2
return 1
end
# ── 1: an inverted mirror (CLAUDE.md real, AGENTS.md symlinked to it) ──
if test -f "$mirror_claude"; and not test -L "$mirror_claude"
if test -L "$mirror_agents"
rm -f "$mirror_agents"
or begin
echo "_agents_init_sync_instructions: could not remove $mirror_agents" >&2
return 1
end
end
if not test -e "$mirror_agents"
command mv "$mirror_claude" "$mirror_agents"
or begin
echo "_agents_init_sync_instructions: could not rename $mirror_claude" >&2
return 1
end
echo "→ Renamed $mirror_rel/CLAUDE.md → AGENTS.md"
end
end
# ── 2: adopt real project-level files, only if the mirror has none yet ──
if not test -f "$mirror_agents"
set -l has_agents 0
set -l has_claude 0
test -f "$proj_agents"; and not test -L "$proj_agents"; and set has_agents 1
test -f "$proj_claude"; and not test -L "$proj_claude"; and set has_claude 1
# A deliberately git-tracked file is left alone -- and so is its
# sibling, since adopting one of a pair would still relink or drop
# the tracked one.
set -l protected
test $has_agents -eq 1; and _agents_init_path_is_protected "$root" "$proj_agents"; and set -a protected $disp_agents
test $has_claude -eq 1; and _agents_init_path_is_protected "$root" "$proj_claude"; and set -a protected $disp_claude
if set -q protected[1]
echo "_agents_init_sync_instructions: "(string join ', ' -- $protected)" tracked by git; leaving this directory's instruction files untouched" >&2
return 0
end
if test $has_agents -eq 1; and test $has_claude -eq 1
if command diff -q "$proj_agents" "$proj_claude" >/dev/null 2>&1
command mv "$proj_agents" "$mirror_agents"
or begin
echo "_agents_init_sync_instructions: could not move $proj_agents" >&2
return 1
end
rm -f "$proj_claude"
or begin
echo "_agents_init_sync_instructions: could not remove $proj_claude" >&2
return 1
end
echo "→ Moved $disp_agents → $mirror_rel/AGENTS.md (dropped identical CLAUDE.md)"
else
echo "_agents_init_sync_instructions: $proj_agents and $proj_claude differ; leaving both, resolve by hand" >&2
return 0
end
else if test $has_agents -eq 1
command mv "$proj_agents" "$mirror_agents"
or begin
echo "_agents_init_sync_instructions: could not move $proj_agents" >&2
return 1
end
echo "→ Moved $disp_agents → $mirror_rel/AGENTS.md"
else if test $has_claude -eq 1
command mv "$proj_claude" "$mirror_agents"
or begin
echo "_agents_init_sync_instructions: could not move $proj_claude" >&2
return 1
end
echo "→ Moved $disp_claude → $mirror_rel/AGENTS.md"
else if test "$rel" = "."
printf '%s\n' \
'# AGENTS.md' \
'' \
'> ⚠️ **SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING**' \
'> You may be reading this file via a symlink (`AGENTS.md`) in' \
'> the root of the project. Your environment'\''s file-editing tools cannot write' \
'> through symlinks and will throw an error.' \
'>' \
'> **DO NOT** attempt to write to or edit `AGENTS.md` in the' \
'> project root. If you need to update these instructions, you **MUST write' \
'> directly to `AGENTS/AGENTS.md`**.' >"$mirror_agents"
echo "→ Created AGENTS/AGENTS.md with agent directive"
end
end
# ── 3: the mirror never carries a CLAUDE.md once AGENTS.md is settled ──
if test -f "$mirror_agents"; and test -e "$mirror_claude" -o -L "$mirror_claude"
rm -f "$mirror_claude"
or begin
echo "_agents_init_sync_instructions: could not remove $mirror_claude" >&2
return 1
end
echo "→ Removed $mirror_rel/CLAUDE.md"
end
# Nothing more to do for a conflicted or still-empty directory.
test -f "$mirror_agents"; or return 0
# ── 4: ensure the project-level AGENTS.md symlink, drop project CLAUDE.md ──
set -l target "AGENTS/AGENTS.md"
if test "$rel" != "."
set -l up (string repeat -n (count (string split / -- $rel)) "../")
set target "$up""AGENTS/$rel/AGENTS.md"
end
# A real (non-symlink) file here arrived after the mirror settled. Same
# rules as step 2. A deliberately git-tracked one is left alone first,
# even if byte-identical: turning a tracked regular file into a symlink
# is itself a change to it. Otherwise, byte-identical to the mirror is a
# duplicate and is replaced below; different means touch nothing and warn.
set -l protected
test -f "$proj_agents"; and not test -L "$proj_agents"; and _agents_init_path_is_protected "$root" "$proj_agents"; and set -a protected $disp_agents
test -f "$proj_claude"; and not test -L "$proj_claude"; and _agents_init_path_is_protected "$root" "$proj_claude"; and set -a protected $disp_claude
if set -q protected[1]
echo "_agents_init_sync_instructions: "(string join ', ' -- $protected)" tracked by git; leaving this directory's instruction files untouched" >&2
return 0
end
for f in $proj_agents $proj_claude
if test -f "$f"; and not test -L "$f"
if not command diff -q "$f" "$mirror_agents" >/dev/null 2>&1
echo "_agents_init_sync_instructions: $f and $mirror_agents differ; leaving both, resolve by hand" >&2
return 0
end
end
end
set -l need_link 1
if test -L "$proj_agents"
test (readlink "$proj_agents") = "$target"; and set need_link 0
end
if test $need_link -eq 1
rm -f "$proj_agents"
ln -s "$target" "$proj_agents"
or begin
echo "_agents_init_sync_instructions: could not link $proj_agents" >&2
return 1
end
echo "→ Linked $disp_agents → $target"
end
if test -e "$proj_claude" -o -L "$proj_claude"
rm -f "$proj_claude"
or begin
echo "_agents_init_sync_instructions: could not remove $proj_claude" >&2
return 1
end
echo "→ Removed $disp_claude"
end
return 0
end
+5 -2
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(rm,mkdir)
#
# SYNOPSIS
# _agents_repo_ensure_symlink <link> <target>
#
@@ -17,7 +20,7 @@
# having no backup at all.
#
# When <link> is an existing real directory, its contents are copied into
# <target> without clobbering (cp -n) before the directory is replaced by
# <target> without clobbering (cp --update=none) before the directory is replaced by
# the link, so adopting a populated live directory never overwrites the
# copy already in the vault.
#
@@ -57,7 +60,7 @@ function _agents_repo_ensure_symlink --argument-names link target
else if test -d "$link"
set -l contents (command ls -A "$link" 2>/dev/null)
if test (count $contents) -gt 0
command cp -rn "$link/." "$target/"; or return 1
command cp -r --update=none "$link/." "$target/"; or return 1
end
rm -rf "$link"; or return 1
else if test -e "$link"
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# uses-shadow(mkdir), bypasses-shadow(cp,grep)
#
# SYNOPSIS
# _agents_repo_install_tools <repo_dir>
#
+28
View File
@@ -0,0 +1,28 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _fish_clipboard_copy
#
# DESCRIPTION
# Copies stdin to the system clipboard. Tries wl-copy (Wayland), then
# xclip (X11), then win32yank.exe (WSL2).
#
# EXIT STATUS
# 0 Text copied to clipboard
# 1 No clipboard provider found
#
# EXAMPLE
# echo "hello" | _fish_clipboard_copy
function _fish_clipboard_copy --description 'Copy stdin to the system clipboard'
if type -q wl-copy
wl-copy
else if type -q xclip
xclip -selection clipboard
else if type -q win32yank.exe
win32yank.exe -i --crlf
else
echo "Error: No clipboard provider (wl-copy, xclip, or win32yank) found." >&2
return 1
end
end
+31
View File
@@ -0,0 +1,31 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _fish_clipboard_paste [args...]
#
# DESCRIPTION
# Prints the system clipboard contents to stdout. Tries wl-paste
# (Wayland), then xclip (X11), then win32yank.exe (WSL2).
#
# ARGUMENTS
# args... Arguments forwarded to the clipboard tool
#
# EXIT STATUS
# 0 Clipboard contents read successfully
# 1 No clipboard provider found
#
# EXAMPLE
# _fish_clipboard_paste
function _fish_clipboard_paste --description 'Print the system clipboard contents'
if type -q wl-paste
wl-paste $argv
else if type -q xclip
xclip -selection clipboard -o $argv
else if type -q win32yank.exe
win32yank.exe -o --lf $argv
else
echo "Error: No clipboard provider (wl-paste, xclip, or win32yank) found." >&2
return 1
end
end
+5 -5
View File
@@ -36,27 +36,27 @@ function _fish_deps_catalog
set -g _fdc_bins \
uv cargo fish starship fzf zoxide direnv paru yay \
wakatime tailscale \
eza lsd bat btop dust duf prettyping go ov rg lazygit lazydocker docker trash kitty wezterm python3 yt-dlp screen mpv vlc marktext firejail
eza lsd bat btop dust duf prettyping go ov rg lazygit lazydocker docker trash kitty wezterm python3 yt-dlp screen mpv vlc marktext firejail win32yank.exe
set -g _fdc_tiers \
rec rec req rec req rec rec rec rec \
int int \
rec rec rec opt opt opt opt opt rec rec opt opt opt rec term term rec opt opt opt opt opt opt
rec rec rec opt opt opt opt opt rec rec opt opt opt rec term term rec opt opt opt opt opt opt opt
set -g _fdc_cargo \
"" "" "" starship "" zoxide "" "" "" \
"" "" \
eza lsd bat "" du-dust "" "" "" "" ripgrep "" "" "" trashy "" "" "" "" "" "" "" "" ""
eza lsd bat "" du-dust "" "" "" "" ripgrep "" "" "" trashy "" "" "" "" "" "" "" "" "" ""
set -g _fdc_pm \
uv cargo fish starship fzf zoxide direnv "" yay \
wakatime tailscale \
eza lsd bat btop dust duf prettyping go ov ripgrep lazygit lazydocker docker trash kitty wezterm python yt-dlp screen mpv vlc "" firejail
eza lsd bat btop dust duf prettyping go ov ripgrep lazygit lazydocker docker trash kitty wezterm python yt-dlp screen mpv vlc "" firejail ""
set -g _fdc_special \
curl-uv rustup-installer git-cargo-fish curl-installer fzf-update "" "" paru-build yay-build \
wakatime-binary "" \
"" "" "" "" "" "" "" "" go-ov "" "" curl-lazydocker "" "" "" "" "" "" "" "" "" marktext-release ""
"" "" "" "" "" "" "" "" go-ov "" "" curl-lazydocker "" "" "" "" "" "" "" "" "" marktext-release "" win32yank-release
end
# SYNOPSIS
+34 -2
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(rm,mkdir,cat), bypasses-shadow(cp,bash), destructive, network, blocking-prompt
#
# SYNOPSIS
# _fish_deps_install
#
@@ -46,6 +49,14 @@ function _fish_deps_install
set -l i 1
for bin in $_fdc_bins
# win32yank only matters under WSL2 — skip the entry entirely
# elsewhere so a plain Linux box never sees it, not even as a
# "no install method available" note.
if test "$bin" = win32yank.exe; and not string match -qi '*microsoft*' (cat /proc/sys/kernel/osrelease 2>/dev/null)
set i (math $i + 1)
continue
end
# Optional-tier deps are opt-in: skip unless --optional/--all was passed.
if test "$_fdc_tiers[$i]" = opt; and test $include_optional -eq 0
if not command -q $bin
@@ -140,6 +151,15 @@ function _fish_deps_install
end
set -a methods special-marktext-appimage
set -a method_labels "AppImage download (~/.local/bin/marktext)"
case win32yank-release
if test (uname -m) = x86_64
set -a methods special-win32yank
set -a method_labels "binary download (github releases)"
else
set_color brblack
echo " note: win32yank only ships x86_64 builds (this is "(uname -m)")"
set_color normal
end
case go-ov
if type -q go
set -a methods special-go-ov
@@ -264,7 +284,7 @@ function _fish_deps_install
end
test $_go_status -eq 0
case special-lazydocker
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | command bash
case special-marktext-paru
paru -S --noconfirm marktext-bin
case special-marktext-yay
@@ -292,10 +312,22 @@ function _fish_deps_install
-o "$_tmpdir/$_zip"
and unzip -o "$_tmpdir/$_zip" -d "$_tmpdir"
and mkdir -p "$_wt_dir" "$HOME/.local/bin"
and cp "$_tmpdir/$_bin_src" "$_wt_bin"
and command cp "$_tmpdir/$_bin_src" "$_wt_bin"
and chmod +x "$_wt_bin"
and ln -sf "$_wt_bin" "$HOME/.local/bin/wakatime"
rm -rf "$_tmpdir"
case special-win32yank
set -l _zip win32yank-x64.zip
set -l _tmpdir (mktemp -d)
mkdir -p "$HOME/.local/bin"
and curl -fL "https://github.com/equalsraf/win32yank/releases/latest/download/$_zip" \
-o "$_tmpdir/$_zip"
and unzip -o "$_tmpdir/$_zip" -d "$_tmpdir"
and command cp "$_tmpdir/win32yank.exe" "$HOME/.local/bin/win32yank.exe"
and chmod +x "$HOME/.local/bin/win32yank.exe"
set -l _dl_status $status
rm -rf "$_tmpdir"
test $_dl_status -eq 0
case special-fzf
fzf-update
case special-curl
+4 -1
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(rm,mkdir), bypasses-shadow(mv), destructive, network
#
# SYNOPSIS
# _fish_deps_marktext_appimage
#
@@ -54,7 +57,7 @@ function _fish_deps_marktext_appimage
and chmod +x "$tmp/marktext"
# Replace via mv, not a write into $dest: overwriting a running AppImage
# in place corrupts the live mount.
and mv -f "$tmp/marktext" "$dest"
and command mv -f "$tmp/marktext" "$dest"
and set ok 1
rm -rf $tmp
+24 -2
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(rm), bypasses-shadow(cp,bash), destructive, network
#
# SYNOPSIS
# _fish_deps_update
#
@@ -90,7 +93,7 @@ function _fish_deps_update
# lazydocker: re-run the official install/update script
if test "$special" = curl-lazydocker
echo "Updating $bin..."
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | command bash
set updated_any 1
set i (math $i + 1)
continue
@@ -138,7 +141,7 @@ function _fish_deps_update
curl -L "https://github.com/wakatime/wakatime-cli/releases/latest/download/$_zip" \
-o "$_tmpdir/$_zip"
and unzip -o "$_tmpdir/$_zip" -d "$_tmpdir"
and cp "$_tmpdir/$_bin_src" "$_wt_bin"
and command cp "$_tmpdir/$_bin_src" "$_wt_bin"
and chmod +x "$_wt_bin"
rm -rf "$_tmpdir"
and set updated_any 1
@@ -146,6 +149,25 @@ function _fish_deps_update
continue
end
# win32yank: re-download the binary from github releases (WSL2 only;
# only reached if a copy is already on PATH, so no WSL check needed)
if test "$special" = win32yank-release
echo "Updating $bin..."
set -l _zip win32yank-x64.zip
set -l _tmpdir (mktemp -d)
curl -fL "https://github.com/equalsraf/win32yank/releases/latest/download/$_zip" \
-o "$_tmpdir/$_zip"
and unzip -o "$_tmpdir/$_zip" -d "$_tmpdir"
and command cp "$_tmpdir/win32yank.exe" "$HOME/.local/bin/win32yank.exe"
and chmod +x "$HOME/.local/bin/win32yank.exe"
set -l _up_status $status
rm -rf "$_tmpdir"
test $_up_status -eq 0
and set updated_any 1
set i (math $i + 1)
continue
end
# pipx tools
if test "$special" = pipx
if type -q pipx
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# uses-shadow(mkdir)
#
# SYNOPSIS
# _fish_mkdir_p [--path|--tree|--silent] <dir>
#
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# network
#
# SYNOPSIS
# _mkrep_default_remote_cmd <type>
#
+3
View File
@@ -4,6 +4,9 @@
# DEPENDENCIES
# gh, glab, tea
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# _mkrep_repo_exists <type> <user> <name>
#
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# bypasses-shadow(ls,rm), destructive
#
# SYNOPSIS
# _prune_terminal_logs <prefix>
#
+9 -1
View File
@@ -7,7 +7,9 @@
# DESCRIPTION
# Removes the first command token from the commandline buffer and
# positions the cursor for immediate replacement. If the command starts
# with sudo, preserves sudo and removes the token after it instead.
# with sudo, preserves sudo and removes the token after it instead. If
# the commandline is empty, first recalls the most recent history entry
# (like pressing Up) before doing the token replacement on it.
# Intended to be bound to a key in key_bindings.fish.
#
# EXAMPLE
@@ -15,6 +17,12 @@
function _replace_command_token --description 'Remove first command token (or first after sudo) and place cursor for replacement'
set -l cmd (commandline)
# Empty prompt: recall the last history entry first, same as it would
# behave if that command were already on the commandline.
if string match -rq '^\s*$' -- "$cmd"
set cmd (builtin history --max 1)
end
# 1. Logic for commands starting with sudo
if string match -rq '^sudo\s+' -- "$cmd"
# regex explanation:
+6 -3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# bypasses-shadow(cat,rm), self-limiting(grep), destructive
#
# SYNOPSIS
# _scrollback_prune_junk [dir]
#
@@ -26,7 +29,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty
# Remove any completely empty log file regardless of source
for f in $dir/*.log $dir/*.txt
test -f $f || continue
not test -s $f; and rm $f
not test -s $f; and command rm -f $f
end
# Remove any log with only a single meaningful line (e.g. [exited], a lone prompt, or a trivial error)
@@ -34,7 +37,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty
test -f $f || continue
set -l line_count (command cat $f | sed 's/\x1b\[[0-9;:]*[a-zA-Z]//g' | grep -cv '^\s*$')
if test $line_count -le 1
rm $f
command rm -f $f
end
end
@@ -42,7 +45,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty
for f in $dir/scrollback_*.log $dir/scrollback_*.txt
test -f $f || continue
if command cat $f | sed 's/\x1b\[[0-9;:]*[a-zA-Z]//g' | grep -q 'Enter the new title for this tab below'
rm $f
command rm -f $f
end
end
end
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# uses-shadow(mkdir)
#
# SYNOPSIS
# _tmux_pipe_log
#
+3
View File
@@ -4,6 +4,9 @@
# COMPONENT
# logging/multiplexer-capture
#
# CLASSIFICATION
# uses-shadow(mkdir), bypasses-shadow(rm)
#
# SYNOPSIS
# _zellij_dump_log
#
+114 -103
View File
@@ -5,7 +5,10 @@
# 12-ai-and-developer-tools
#
# DEPENDENCIES
# _agents_repo_install_tools, _agents_repo_sync, _agents_init_ensure_gitignore
# _agents_init_sync_instructions, _agents_repo_install_tools, _agents_repo_sync, _agents_init_ensure_gitignore
#
# CLASSIFICATION
# self-limiting(rm,mkdir,grep), bypasses-shadow(mv), manual-section(16-agent-tooling)
#
# SYNOPSIS
# agents-init [-a | --agents] [-p | --plugins] [-v | --verbose]
@@ -14,8 +17,18 @@
# DESCRIPTION
# Scaffolds an AGENTS/ sub-repository inside a project directory. Creates
# a self-contained git repo for agent specifications, moves any existing
# agent-related files into it, and replaces them with symlinks so the outer
# project never tracks agent files directly.
# agent-related files into it, and replaces them with symlinks so the
# outer project never tracks agent files directly. This applies at the
# project root and, automatically, to any subdirectory that carries its
# own scoped AGENTS.md or CLAUDE.md -- discovered by scanning the tree,
# not a hardcoded list. The scan prunes dot-directories (.git/, .claude/,
# ...), nested repos, AGENTS/ itself, node_modules/, and generated-output
# directories (build/, dist/, out/, target/).
#
# A real instruction file that the project deliberately tracks -- in
# git's index, in a project whose .gitignore is non-empty -- is left
# exactly where it is, with a warning, rather than moved into AGENTS/ and
# replaced by a symlink. See _agents_init_path_is_protected.
#
# Scaffolding runs only inside a git repository, or in a directory that
# already has an AGENTS.md, CLAUDE.md, or AGENTS/. Elsewhere it is a
@@ -23,11 +36,12 @@
# create a repository there.
#
# File layout after setup:
# AGENTS/AGENTS.md canonical agent spec (real file)
# AGENTS/CLAUDE.md real file (if CLAUDE.md existed separately)
# or symlink → AGENTS.md (single-source case)
# AGENTS/AGENTS.md canonical root agent spec (real file)
# AGENTS/<subdir>/AGENTS.md canonical spec for any subdir with its own
# scoped instructions (real file, discovered
# automatically -- see above)
# <root>/AGENTS.md → AGENTS/AGENTS.md
# <root>/CLAUDE.md → AGENTS/CLAUDE.md
# <root>/<subdir>/AGENTS.md → AGENTS/<subdir>/AGENTS.md
# AGENTS/plans superpowers plans (real dir, .gitkeep)
# AGENTS/specs superpowers specs (real dir, .gitkeep)
# AGENTS/devlogs agent development logs (real dir, .gitkeep)
@@ -39,6 +53,12 @@
# docs/specs → ../AGENTS/specs (only if docs/specs existed)
# docs/devlogs → ../AGENTS/devlogs (only if docs/devlogs existed)
#
# No CLAUDE.md survives anywhere in a managed tree: claude-code reads
# AGENTS.md natively when CLAUDE.md is absent, so CLAUDE.md exists here
# purely as a retirement target -- any found (root or subdirectory, real
# file or leftover symlink) is folded into the AGENTS.md-only shape
# above by _agents_init_sync_instructions.
#
# plans/ and specs/ are merged from every legacy location (docs/<tgt>,
# docs/superpowers/<tgt>, and the old AGENTS/plugins/ layout) into the
# canonical AGENTS/<tgt>; the AGENTS/plugins/ layer is removed.
@@ -72,7 +92,8 @@
# Called automatically by the claude and agy wrappers on every invocation.
#
# ARGUMENTS
# -a, --agents Set up AGENTS/ repo + AGENTS.md / CLAUDE.md symlinks only
# -a, --agents Set up AGENTS/ repo + AGENTS.md symlinks (root and every
# discovered subdirectory) only
# -p, --plugins Set up AGENTS/ repo + plans/specs/devlogs dirs + docs/ symlinks only
# -v, --verbose Print all per-step output (default)
# -q, --quiet Print one summary line only if changes were made
@@ -89,6 +110,15 @@
# agents-init --agents
# agents-init --plugins
# agents-init --quiet
#
# NOTES
# This header covers usage only. The full concept/behavior/purpose
# write-up -- the AGENTS.md convention, the AGENTS/ sub-repository, the
# discovery and safety model, and a complete scenario-by-scenario
# reference table -- lives in its own manual section:
# docs/manual/16-agent-tooling.md. Update that section in the same
# change whenever this function's behavior changes; see "Dedicated
# manual sections for complex subsystems" in CONTRIBUTING.md.
function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec files and plugin dirs'
__fish_palette
@@ -102,7 +132,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
echo
echo "$c_head""Options:$c_reset"
echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message"
echo " $c_flag-a$c_reset, $c_flag--agents$c_reset Set up AGENTS.md / CLAUDE.md symlinks only"
echo " $c_flag-a$c_reset, $c_flag--agents$c_reset Set up AGENTS.md symlinks only"
echo " $c_flag-p$c_reset, $c_flag--plugins$c_reset Set up plans/specs/devlogs dirs and docs/ symlinks only"
echo " $c_flag-v$c_reset, $c_flag--verbose$c_reset Print all per-step output (default)"
echo " $c_flag-q$c_reset, $c_flag--quiet$c_reset Print one summary line only if changes were made"
@@ -137,7 +167,9 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
# directory created an AGENTS/ repo, two root symlinks, and a docs/
# tree there.
set -l root (git rev-parse --show-toplevel 2>/dev/null)
set -l in_git 1
if test -z "$root"
set in_git 0
if test -e (pwd)/AGENTS.md -o -e (pwd)/CLAUDE.md -o -d (pwd)/AGENTS
set root (pwd)
else
@@ -198,109 +230,84 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
# ──────────────────────────── --agents mode ──────────────────────────────
if test $do_agents -eq 1
# Detect which root-level files are real (not symlinks)
set -l has_agents 0
set -l has_claude 0
if test -f "$root/AGENTS.md"; and not test -L "$root/AGENTS.md"
set has_agents 1
# Discover every directory carrying agent instructions -- root
# included, subdirectories found automatically rather than by a
# hardcoded list. A real file, an already-migrated symlink, or a
# leftover inverted-mirror survivor all match, so one pass covers
# fresh, migrated, and legacy state alike.
#
# Discovery stays inside this project: a non-git root (a lone
# agent file in, say, ~) syncs only itself -- walking it would
# reach into every unrelated tree below. In a git root, pruned:
# any AGENTS/ (a mirror, never a source), dot-directories (.git,
# .claude, .github: tool state, not scoped project dirs),
# node_modules, generated-output directories (build, dist, out,
# target: an instruction file there is a build artifact, never a
# source -- pruned outright, before tracked-file protection would
# even be consulted), and nested repos/submodules/worktrees (their
# own .git marks another project). -mindepth 1 keeps the root
# itself, which has a .git, from pruning the whole walk.
set -l found
if test $in_git -eq 1
set found (find "$root" -mindepth 1 \
-type d \( -name '.*' -o -name AGENTS -o -name node_modules \
-o -name build -o -name dist -o -name out -o -name target \
-o -exec test -e '{}/.git' \; \) -prune -o \
\( -name AGENTS.md -o -name CLAUDE.md \) -print)
end
if test -f "$root/CLAUDE.md"; and not test -L "$root/CLAUDE.md"
set has_claude 1
set -l rels "."
for f in $found
set -l d (path dirname "$f")
set -l rel (string replace "$root/" "" "$d")
test "$rel" = "$d"; and set rel "."
contains -- "$rel" $rels; or set -a rels "$rel"
end
# ── Move real files into AGENTS/ ──────────────────────────────────────
if test $has_agents -eq 1; and test $has_claude -eq 1
# Both exist: preserve each as its own file in AGENTS/
if not test -f "$agents_dir/AGENTS.md"
if not mv "$root/AGENTS.md" "$agents_dir/AGENTS.md"
echo "$c_err""Error: could not move AGENTS.md → AGENTS/AGENTS.md$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Moved AGENTS.md → AGENTS/AGENTS.md$c_reset"
end
if not test -f "$agents_dir/CLAUDE.md"; and not test -L "$agents_dir/CLAUDE.md"
if not mv "$root/CLAUDE.md" "$agents_dir/CLAUDE.md"
echo "$c_err""Error: could not move CLAUDE.md → AGENTS/CLAUDE.md$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Moved CLAUDE.md → AGENTS/CLAUDE.md$c_reset"
end
else if test $has_agents -eq 1
if not test -f "$agents_dir/AGENTS.md"
if not mv "$root/AGENTS.md" "$agents_dir/AGENTS.md"
echo "$c_err""Error: could not move AGENTS.md → AGENTS/AGENTS.md$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Moved AGENTS.md → AGENTS/AGENTS.md$c_reset"
end
else if test $has_claude -eq 1
# Only CLAUDE.md: treat it as the agent spec
if not test -f "$agents_dir/AGENTS.md"
if not mv "$root/CLAUDE.md" "$agents_dir/AGENTS.md"
echo "$c_err""Error: could not move CLAUDE.md → AGENTS/AGENTS.md$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Moved CLAUDE.md → AGENTS/AGENTS.md$c_reset"
end
else
# Neither exists: create AGENTS/AGENTS.md with the agent directive
if not test -f "$agents_dir/AGENTS.md"
printf '%s\n' \
'# AGENTS.md' \
'' \
'> ⚠️ **SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING**' \
'> You may be reading this file via a symlink (`CLAUDE.md` or `AGENTS.md`) in' \
'> the root of the project. Your environment'\''s file-editing tools cannot write' \
'> through symlinks and will throw an error.' \
'>' \
'> **DO NOT** attempt to write to or edit `CLAUDE.md` or `AGENTS.md` in the' \
'> project root. If you need to update these instructions, you **MUST write' \
'> directly to `AGENTS/AGENTS.md`**.' >"$agents_dir/AGENTS.md"
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Created AGENTS/AGENTS.md with agent directive$c_reset"
end
end
# ── Ensure AGENTS/CLAUDE.md exists ────────────────────────────────────
# When both files existed, AGENTS/CLAUDE.md is already a real file.
# Otherwise, create it as a symlink → AGENTS.md (within AGENTS/).
if not test -f "$agents_dir/CLAUDE.md"; and not test -L "$agents_dir/CLAUDE.md"
if not ln -s AGENTS.md "$agents_dir/CLAUDE.md"
echo "$c_err""Error: could not create AGENTS/CLAUDE.md symlink$c_reset" >&2
for rel in $rels
set -l out (_agents_init_sync_instructions "$root" "$agents_dir" "$rel")
set -l rc $status
if test $rc -ne 0
echo "$c_err""Error: could not sync AGENTS.md for $rel$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Linked AGENTS/CLAUDE.md → AGENTS/AGENTS.md$c_reset"
if test -n "$out"
set changed 1
if test $verbose -eq 1
for line in $out
echo "$c_ok$line$c_reset"
end
end
end
end
# Root symlinks point at files, not directories, so they cannot use
# _agents_repo_ensure_symlink (which is directory-only by design).
for pair in "AGENTS.md:AGENTS/AGENTS.md" "CLAUDE.md:AGENTS/CLAUDE.md"
set -l name (string split -f1 ':' -- $pair)
set -l want (string split -f2 ':' -- $pair)
set -l need 0
if not test -L "$root/$name"
set need 1
else if test (readlink "$root/$name") != "$want"
rm -f "$root/$name"
set need 1
end
if test $need -eq 1
if not ln -s "$want" "$root/$name"
echo "$c_err""Error: could not create $name symlink$c_reset" >&2
return 1
end
# ── Migrate stale anchored gitignore lines ──────────────────────────────
# A project scaffolded by the old agents-init already has anchored
# /AGENTS.md and/or /CLAUDE.md lines in .gitignore. git check-ignore
# sees those as covering the literal path "AGENTS.md", so the new
# unanchored pattern below would be judged already-covered and never
# added -- leaving any newly discovered subdirectory AGENTS.md with no
# gitignore coverage at all. Strip the stale exact lines first so the
# unanchored pattern always gets a chance to be added. No-op when
# neither stale line is present.
set -l gitignore "$root/.gitignore"
if test -f "$gitignore"
if grep -qxF "/AGENTS.md" "$gitignore"
sed -i '/^\/AGENTS\.md$/d' "$gitignore"
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Linked $name → $want$c_reset"
test $verbose -eq 1; and echo "$c_warn→ Removed stale /AGENTS.md line from .gitignore$c_reset"
end
if grep -qxF "/CLAUDE.md" "$gitignore"
sed -i '/^\/CLAUDE\.md$/d' "$gitignore"
set changed 1
test $verbose -eq 1; and echo "$c_warn→ Removed stale /CLAUDE.md line from .gitignore$c_reset"
end
end
# ── .gitignore ────────────────────────────────────────────────────────
set -l _gi (_agents_init_ensure_gitignore "$root" "agents-init --agents" "AGENTS/" "/AGENTS.md" "/CLAUDE.md")
# Unanchored: matches AGENTS.md at every depth, so a newly
# discovered subdirectory needs no additional gitignore entry.
# CLAUDE.md is dropped entirely -- nothing creates one anymore.
set -l _gi (_agents_init_ensure_gitignore "$root" "agents-init --agents" "AGENTS/" "AGENTS.md")
if test -n "$_gi"
set changed 1
test $verbose -eq 1; and echo $_gi
@@ -351,7 +358,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
set -l rel (string replace "$root/" "" "$src")
set -l contents (command ls -A "$src" 2>/dev/null)
if test (count $contents) -gt 0
if not command cp -rn "$src/." "$canonical/"
if not command cp -r --update=none "$src/." "$canonical/"
echo "$c_err""Error: could not merge $rel → AGENTS/$tgt$c_reset" >&2
return 1
end
@@ -434,7 +441,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
test -d "$devlogs_dir"; or mkdir -p "$devlogs_dir"
set -l contents (command ls -A "$docs_devlogs" 2>/dev/null)
if test (count $contents) -gt 0
if not command cp -rn "$docs_devlogs/." "$devlogs_dir/"
if not command cp -r --update=none "$docs_devlogs/." "$devlogs_dir/"
echo "$c_err""Error: could not copy docs/devlogs → AGENTS/devlogs$c_reset" >&2
return 1
end
@@ -479,7 +486,11 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
# a hook-rejected commit fell straight through to a reported success.
set -l msg "chore: sync AGENTS repository"
test $did_init -eq 1; and set msg "chore: initialize AGENTS repository"
set -l sync_out (_agents_repo_sync "$agents_dir" "$msg")
# 2>/dev/null: a command substitution's stderr does not inherit a
# caller-scoped redirect on this call (fish quirk), so _agents_repo_sync's
# own error message leaks past --silent regardless; it is redundant with
# the $sync_rc-driven echoes just below anyway.
set -l sync_out (_agents_repo_sync "$agents_dir" "$msg" 2>/dev/null)
set -l sync_rc $status
set -l failed 0
if test $sync_rc -eq 2
+29 -4
View File
@@ -9,6 +9,9 @@
# _agents_repo_ensure_symlink, _agents_repo_sync,
# _agents_repo_install_tools, git, hostname
#
# CLASSIFICATION
# self-limiting(rm,mkdir), manual-section(16-agent-tooling)
#
# SYNOPSIS
# agents-vault [--link] [--push] [--restore] [--status]
# [--adopt=SLUG] [--remote=URL]
@@ -185,6 +188,13 @@
# machine that has a real global memory directory would move it into a
# throwaway directory and leave a dangling symlink behind, which is
# strictly worse than having had no backup at all.
#
# This header covers usage only. The full concept/behavior/purpose
# write-up -- how this relates to the per-project AGENTS/ repository
# agents-init manages, and where each kind of agent state actually lives
# -- is in docs/manual/16-agent-tooling.md. Update that section in the
# same change whenever this function's behavior changes; see "Dedicated
# manual sections for complex subsystems" in CONTRIBUTING.md.
function agents-vault --description 'track curated agent memory in a host-scoped vault repo'
__fish_palette
@@ -766,7 +776,11 @@ function agents-vault --description 'track curated agent memory in a host-scoped
if not mkdir -p "$gvault"
echo "$c_warn""agents-vault: could not create $gvault; skipping global memory$c_reset" >&2
else
set -l gmsg (_agents_repo_ensure_symlink "$glive" "$gvault")
# 2>/dev/null: see the same call's comment further down this
# function -- a command substitution's stderr bypasses this
# call's own caller-scoped redirect, and the raw message is
# always redundant with the $grc-driven echo just below.
set -l gmsg (_agents_repo_ensure_symlink "$glive" "$gvault" 2>/dev/null)
set -l grc $status
if test $grc -ne 0
echo "$c_warn""agents-vault: could not link $glive; global memory not backed up$c_reset" >&2
@@ -890,7 +904,7 @@ function agents-vault --description 'track curated agent memory in a host-scoped
if test -n "$stash"
test -f "$stash/origin"
and command cat "$stash/origin" >>"$entry/origin" 2>/dev/null
command cp -rn "$stash/." "$entry/" 2>/dev/null
command cp -r --update=none "$stash/." "$entry/" 2>/dev/null
rm -rf "$stash"
end
printf 'renamed: %s → %s (%s)\n' "$prev_slug" "$slug" (date -I) >>"$entry/origin"
@@ -936,7 +950,14 @@ function agents-vault --description 'track curated agent memory in a host-scoped
# idempotent and makes its own parent directories, so there is
# nothing this guard would protect that the helper does not already
# handle on its own.
set -l link_msg (_agents_repo_ensure_symlink "$live" "$vmem")
#
# 2>/dev/null: a command substitution's stderr does not inherit a
# caller-scoped redirect on this call (fish quirk -- proven with a
# two-line repro: `outer 2>/dev/null` where outer does `set -l x
# (inner)` still leaks inner's stderr to the real terminal). The
# raw message below is always redundant: failure is re-announced on
# $link_rc below in this function's own voice.
set -l link_msg (_agents_repo_ensure_symlink "$live" "$vmem" 2>/dev/null)
set -l link_rc $status
if test $link_rc -ne 0
echo "$c_err""agents-vault: could not link $live$c_reset" >&2
@@ -970,7 +991,11 @@ function agents-vault --description 'track curated agent memory in a host-scoped
if not set -q _flag_link
set -l msg "chore: sync agent memory vault"
test $did_init -eq 1; and set msg "chore: initialize agent memory vault"
set -l sync_out (_agents_repo_sync "$vault" "$msg")
# 2>/dev/null: command-substitution stderr bypasses this call's
# caller-scoped redirect (see the _agents_repo_ensure_symlink calls
# above for the proof); the raw message is redundant with the
# $sync_rc-driven echoes just below.
set -l sync_out (_agents_repo_sync "$vault" "$msg" 2>/dev/null)
set -l sync_rc $status
if test $sync_rc -eq 2
echo "$c_err""agents-vault: unresolved rebase in the vault; nothing committed$c_reset" >&2
+2 -2
View File
@@ -17,8 +17,8 @@
# Wrapper for the agy Antigravity AI CLI that ensures the AGENTS/
# sub-repository is initialized and any agent-made changes are committed
# before launch. Delegates all scaffold and commit logic to agents-init
# --quiet (full setup), which ensures AGENTS/ is scaffolded and CLAUDE.md
# is symlinked to AGENTS/AGENTS.md in the current project.
# --quiet (full setup), which ensures AGENTS.md (root and every scoped
# subdirectory) is symlinked into AGENTS/ in the current project.
#
# Also syncs the host-scoped agent memory vault (agents-vault). agy has
# no session-end hook, so its memory is captured on the next launch
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# antigravity-ide [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# autoexec/sync
#
# DEPENDENCIES
# git
#
# SYNOPSIS
# auto-pull [list]
# auto-pull add [PATH]
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/shell-tools
#
# CLASSIFICATION
# bypasses-shadow(bash)
#
# SYNOPSIS
# bash [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# curl, jq, git
#
# SYNOPSIS
# bd-pull <owner/repo>
#
+8
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 08-terminal-management
#
# DEPENDENCIES
# nohup
#
# SYNOPSIS
# bkg <command> [args...]
#
@@ -32,6 +35,11 @@ function bkg --description 'Execute bkg'
return 1
end
if not type -q nohup
echo (set_color red)"Error: nohup is not installed."(set_color normal) >&2
return 1
end
# Run the command using nohup to make it immune to hangups (like closing the terminal).
# Redirect both stdout and stderr to /dev/null to discard all output.
# The final ampersand (&) sends the entire process to the background.
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 04-git-and-version-control
#
# DEPENDENCIES
# git
#
# SYNOPSIS
# branch <branch_name>
#
+6
View File
@@ -7,6 +7,12 @@
# COMPONENT
# aliases/filesystem
#
# DEPENDENCIES
# bat
#
# CLASSIFICATION
# uses-shadow(ls), bypasses-shadow(cat)
#
# SYNOPSIS
# cat [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 02-navigation
#
# DEPENDENCIES
# zoxide
#
# SYNOPSIS
# cdi [query]
#
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# fastfetch, neofetch
#
# CLASSIFICATION
# uses-shadow(ls)
#
# SYNOPSIS
# cffetch [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# cheat, tldr, man
#
# SYNOPSIS
# cheat <topic> [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 06-dependency-management
#
# DEPENDENCIES
# fish-deps
#
# SYNOPSIS
# check_fish_deps
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# CLASSIFICATION
# uses-shadow(claude)
#
# SYNOPSIS
# claude-docs
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# CLASSIFICATION
# uses-shadow(claude)
#
# SYNOPSIS
# claude-pr
#
+7 -3
View File
@@ -10,6 +10,9 @@
# DEPENDENCIES
# agents-init, agents-vault
#
# CLASSIFICATION
# bypasses-shadow(claude)
#
# SYNOPSIS
# claude [ARGS...]
#
@@ -17,8 +20,9 @@
# Wrapper for the claude CLI that ensures the AGENTS/ sub-repository is
# initialized and any agent-made changes are committed before launch.
# Delegates all scaffold and commit logic to agents-init --quiet (full
# setup), which ensures AGENTS/ is scaffolded and CLAUDE.md is symlinked
# to AGENTS/AGENTS.md in the current project.
# setup), which ensures AGENTS.md (root and every scoped subdirectory)
# is symlinked into AGENTS/ in the current project. claude-code reads
# AGENTS.md natively, so no CLAUDE.md is created or maintained.
#
# Also syncs the host-scoped agent memory vault (agents-vault), which
# tracks curated memory living outside the project tree. The vault
@@ -41,7 +45,7 @@
# claude
# claude --resume
# claude "Explain the recent changes"
function claude --wraps=claude --description 'claude wrapper: auto-links AGENTS.md as CLAUDE.md'
function claude --wraps=claude --description 'claude wrapper: ensures AGENTS/ is scaffolded before launch'
if not __fish_config_op_enabled (status current-function)
command claude $argv
return $status
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 05-package-management
#
# DEPENDENCIES
# pacman
#
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# cleanup
#
+11 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 02-navigation
#
# DEPENDENCIES
# clone-in-kitty
#
# SYNOPSIS
# clone [args...]
#
@@ -16,7 +19,7 @@
#
# EXIT STATUS
# 0 Repository cloned
# 1 Not running inside Kitty terminal
# 1 Not running inside Kitty terminal, or clone-in-kitty isn't available
#
# EXAMPLE
# clone https://github.com/user/repo.git
@@ -25,5 +28,12 @@ function clone --wraps='clone-in-kitty' --description 'alias clone=clone-in-kitt
echo "Error: The 'clone' command requires Kitty terminal." >&2
return 1
end
# $TERM only proves the terminal type -- clone-in-kitty is a function
# Kitty's own shell integration injects, which doesn't happen over an
# ssh session that merely inherits $TERM from the local Kitty.
if not type -q clone-in-kitty
echo "Error: 'clone' detected Kitty but clone-in-kitty isn't available (shell integration not loaded)." >&2
return 1
end
clone-in-kitty $argv
end
+11 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 02-navigation
#
# DEPENDENCIES
# clone-in-kitty
#
# SYNOPSIS
# clonet [args...]
#
@@ -16,7 +19,7 @@
#
# EXIT STATUS
# 0 Repository cloned
# 1 Not running inside Kitty terminal
# 1 Not running inside Kitty terminal, or clone-in-kitty isn't available
#
# EXAMPLE
# clonet https://github.com/user/repo.git
@@ -25,5 +28,12 @@ function clonet --wraps='clone-in-kitty --type=tab' --description 'alias clonet=
echo "Error: The 'clonet' command requires Kitty terminal." >&2
return 1
end
# $TERM only proves the terminal type -- clone-in-kitty is a function
# Kitty's own shell integration injects, which doesn't happen over an
# ssh session that merely inherits $TERM from the local Kitty.
if not type -q clone-in-kitty
echo "Error: 'clonet' detected Kitty but clone-in-kitty isn't available (shell integration not loaded)." >&2
return 1
end
clone-in-kitty --type=tab $argv
end
+42 -47
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# xdg-open, man, ov, bat
#
# CLASSIFICATION
# self-limiting(grep), bypasses-shadow(less)
#
# SYNOPSIS
# config-help [section]
# config-help --html
@@ -190,63 +196,52 @@ function config-help --description 'Open the offline fish shell configuration ma
# ── --help / -h ──────────────────────────────────────────────
if contains -- --help $argv; or contains -- -h $argv
set_color --bold
echo "help config / config-help"
set_color normal
__fish_palette
echo "$c_cmd""help config / config-help$c_reset"
echo " — view the offline fish shell configuration manual"
echo ""
set_color --bold brblue
echo USAGE
set_color normal
echo " help config "(set_color yellow)"[section]"(set_color normal)
echo " help config "(set_color yellow)"[section] --html"(set_color normal)
echo " help config "(set_color yellow)"[section] --man"(set_color normal)
echo " help config "(set_color yellow)"--help"(set_color normal)
echo "$c_head""USAGE$c_reset"
echo " $c_cmd""help config$c_reset $c_arg""[section]$c_reset"
echo " $c_cmd""help config$c_reset $c_arg""[section]$c_reset $c_flag--html$c_reset"
echo " $c_cmd""help config$c_reset $c_arg""[section]$c_reset $c_flag--man$c_reset"
echo " $c_cmd""help config$c_reset $c_flag--help$c_reset"
echo ""
set_color --bold brblue
echo ARGUMENTS
set_color normal
echo " "(set_color yellow)"section"(set_color normal)" Optional keyword to jump to a matching section heading."
echo "$c_head""ARGUMENTS$c_reset"
echo " $c_arg""section$c_reset Optional keyword to jump to a matching section heading."
echo " Searches docs/fish-config.index for aliases first, then"
echo " falls back to a normalized (case- and punctuation-insensitive)"
echo " scan of heading lines."
echo " "(set_color yellow)"-w, --html"(set_color normal)" Open the published documentation website in the default browser."
echo " $c_flag-w, --html$c_reset Open the published documentation website in the default browser."
echo " Deep links aren't supported — use the site's search box."
echo " "(set_color yellow)"-m, --man"(set_color normal)" Open the compiled man page via man -l."
echo " $c_flag-m, --man$c_reset Open the compiled man page via man -l."
echo " If a section keyword is given, jumps to the nearest match."
echo ""
set_color --bold brblue
echo EXAMPLES
set_color normal
echo " "(set_color green)"help config"(set_color normal)" open at top"
echo " "(set_color green)"help config keybindings"(set_color normal)" jump to Key Bindings section"
echo " "(set_color green)"help config pkg"(set_color normal)" jump to the pkg function entry"
echo " "(set_color green)"help config fish-deps"(set_color normal)" jump to fish-deps"
echo " "(set_color green)"help config abbreviations"(set_color normal)" jump to Abbreviations section"
echo " "(set_color green)"help config --html"(set_color normal)" open the documentation website"
echo " "(set_color green)"help config --man"(set_color normal)" open compiled man page"
echo " "(set_color green)"help config pkg --man"(set_color normal)" open man page at pkg section"
echo "$c_head""EXAMPLES$c_reset"
echo " $c_cmd""help config$c_reset open at top"
echo " $c_cmd""help config$c_reset $c_arg""keybindings$c_reset jump to Key Bindings section"
echo " $c_cmd""help config$c_reset $c_arg""pkg$c_reset jump to the pkg function entry"
echo " $c_cmd""help config$c_reset $c_arg""fish-deps$c_reset jump to fish-deps"
echo " $c_cmd""help config$c_reset $c_arg""abbreviations$c_reset jump to Abbreviations section"
echo " $c_cmd""help config$c_reset $c_flag--html$c_reset open the documentation website"
echo " $c_cmd""help config$c_reset $c_flag--man$c_reset open compiled man page"
echo " $c_cmd""help config$c_reset $c_arg""pkg$c_reset $c_flag--man$c_reset open man page at pkg section"
echo ""
set_color --bold brblue
echo "NAVIGATION (ov pager)"
set_color normal
echo " "(set_color cyan)"Space"(set_color normal)" next section"
echo " "(set_color cyan)"^"(set_color normal)" previous section"
echo " "(set_color cyan)"Alt+u"(set_color normal)" toggle section list sidebar"
echo " "(set_color cyan)"/"(set_color normal)" search forward"
echo " "(set_color cyan)"n"(set_color normal)" / "(set_color cyan)"N"(set_color normal)" next / previous search match"
echo " "(set_color cyan)"g"(set_color normal)" go to line number"
echo " "(set_color cyan)"q"(set_color normal)" quit"
echo "$c_head""NAVIGATION (ov pager)$c_reset"
echo " $c_arg""Space$c_reset next section"
echo " $c_arg""^$c_reset previous section"
echo " $c_arg""Alt+u$c_reset toggle section list sidebar"
echo " $c_arg""/$c_reset search forward"
echo " $c_arg""n$c_reset / $c_arg""N$c_reset next / previous search match"
echo " $c_arg""g$c_reset go to line number"
echo " $c_arg""q$c_reset quit"
echo ""
set_color --bold brblue
echo "PAGER FALLBACK CHAIN"
set_color normal
echo " "(set_color brblack)"1."(set_color normal)" ov + bat section nav + syntax highlighting "(set_color brblack)"(best)"(set_color normal)
echo " "(set_color brblack)"2."(set_color normal)" ov alone section nav, raw Markdown"
echo " "(set_color brblack)"3."(set_color normal)" bat alone syntax highlighting, use / to search"
echo " "(set_color brblack)"4."(set_color normal)" man -l pre-compiled man page (if available)"
echo " "(set_color brblack)"5."(set_color normal)" less plain text with line-jump"
echo " "(set_color brblack)"6."(set_color normal)" cat plain output"
echo "$c_head""PAGER FALLBACK CHAIN$c_reset"
echo " $c_dim""1.$c_reset $c_cmd""ov$c_reset + $c_cmd""bat$c_reset section nav + syntax highlighting $c_dim""(best)$c_reset"
echo " $c_dim""2.$c_reset $c_cmd""ov$c_reset alone section nav, raw Markdown"
echo " $c_dim""3.$c_reset $c_cmd""bat$c_reset alone syntax highlighting, use / to search"
echo " $c_dim""4.$c_reset $c_cmd""man$c_reset $c_flag-l$c_reset pre-compiled man page (if available)"
echo " $c_dim""5.$c_reset $c_cmd""less$c_reset plain text with line-jump"
echo " $c_dim""6.$c_reset $c_cmd""cat$c_reset plain output"
return 0
end
@@ -358,7 +353,7 @@ function config-help --description 'Open the offline fish shell configuration ma
else if type -q less
string replace -ra $span_raw $span_bold <"$doc_file" \
| less -R +"$start_line"
| command less -R +"$start_line"
else
string replace -ra $span_raw $span_bold <"$doc_file"
+3
View File
@@ -8,6 +8,9 @@
# __fish_palette, __config_settings_state, __config_settings_apply,
# __config_settings_set_value, python3
#
# CLASSIFICATION
# bypasses-shadow(rm)
#
# SYNOPSIS
# config-settings [-h | --help]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# git
#
# SYNOPSIS
# config-update [-h | --help] [-f | --force] [-n | --dry-run]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(cp)
#
# SYNOPSIS
# copy <source> <dest>
#
+8
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 08-terminal-management
#
# DEPENDENCIES
# nohup
#
# SYNOPSIS
# detach [-h] [--version] <command> [args...]
#
@@ -66,5 +69,10 @@ function detach --description 'Execute detach'
return 1
end
if not type -q nohup
echo (set_color red)"Error: nohup is not installed."(set_color normal) >&2
return 1
end
nohup $args >/dev/null 2>&1 &
end
+12 -6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 13-media-and-utilities
#
# DEPENDENCIES
# magick, ffmpeg, avifenc, exiftool
#
# CLASSIFICATION
# self-limiting(rm)
#
# SYNOPSIS
# dng2avif [-h] [-i <file>] [-o <file>] [-q <n>] [-s <n>] [input.dng]
#
@@ -39,13 +45,13 @@ function dng2avif --description 'Convert DNG raw to 10-bit HDR AVIF'
# Help Screen
if set -q _flag_help; or test (count $argv) -eq 0 -a -z "$_flag_input"
__fish_palette
echo "$c_head""Usage:$c_reset $c_cmd""dng2avif$c_reset $c_flag""[options]$c_reset $c_dim""[input.dng]$c_reset"
echo "$c_head""Usage:$c_reset $c_cmd""dng2avif$c_reset $c_flag""[options]$c_reset $c_arg""[input.dng]$c_reset"
echo ""
echo "$c_head""Options:$c_reset"
echo " $c_flag-i$c_reset, $c_flag--input$c_reset $c_dim""FILE$c_reset Input DNG file"
echo " $c_flag-o$c_reset, $c_flag--output$c_reset $c_dim""FILE$c_reset Output AVIF file (defaults to input name)"
echo " $c_flag-q$c_reset, $c_flag--quality$c_reset $c_dim""N$c_reset Encoding quality 0-100 (default: 92)"
echo " $c_flag-s$c_reset, $c_flag--speed$c_reset $c_dim""N$c_reset Encoder speed 0-10 (default: 3, 0=slowest)"
echo " $c_flag-i$c_reset, $c_flag--input$c_reset $c_arg""FILE$c_reset Input DNG file"
echo " $c_flag-o$c_reset, $c_flag--output$c_reset $c_arg""FILE$c_reset Output AVIF file (defaults to input name)"
echo " $c_flag-q$c_reset, $c_flag--quality$c_reset $c_arg""N$c_reset Encoding quality 0-100 (default: 92)"
echo " $c_flag-s$c_reset, $c_flag--speed$c_reset $c_arg""N$c_reset Encoder speed 0-10 (default: 3, 0=slowest)"
echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message"
return 0
end
@@ -119,7 +125,7 @@ function dng2avif --description 'Convert DNG raw to 10-bit HDR AVIF'
end
# Final Cleanup
test -f "$temp_pnm"; and rm "$temp_pnm"
test -f "$temp_pnm"; and rm -f "$temp_pnm"
set -l size (stat -c '%s' "$output" | numfmt --to=iec)
echo (set_color yellow)"Complete: $output ($size)"(set_color normal)
+8 -2
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# docker
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# dockup [-h] [directory]
#
@@ -31,13 +37,13 @@ function dockup --description 'Pull and restart docker compose containers'
# Handle help flags
if contains -- -h $argv; or contains -- --help $argv
__fish_palette
echo "$c_head""Usage:$c_reset $c_cmd""dockup$c_reset $c_dim""[DIRECTORY]$c_reset"
echo "$c_head""Usage:$c_reset $c_cmd""dockup$c_reset $c_arg""[DIRECTORY]$c_reset"
echo ""
echo "$c_head""Options:$c_reset"
echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message"
echo ""
echo "$c_head""Arguments:$c_reset"
echo " $c_dim""DIRECTORY$c_reset Optional path to the compose project (defaults to current dir)"
echo " $c_arg""DIRECTORY$c_reset Optional path to the compose project (defaults to current dir)"
return 0
end
+6
View File
@@ -7,6 +7,12 @@
# COMPONENT
# aliases/filesystem
#
# DEPENDENCIES
# duf, dust, dua
#
# CLASSIFICATION
# bypasses-shadow(du)
#
# SYNOPSIS
# du [--disk|--dir|--dua] [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# uses-shadow(du)
#
# SYNOPSIS
# dusize [dir]
#
+6
View File
@@ -7,6 +7,12 @@
# COMPONENT
# aliases/dev-tools
#
# DEPENDENCIES
# p, nano, nvim
#
# CLASSIFICATION
# bypasses-shadow(rm)
#
# SYNOPSIS
# edit [-V|-t] [-e EDITOR] [-c] [-x TEXT] [-n] [-v|-s] [FILE...]
#
+11
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 10-network
#
# DEPENDENCIES
# fast
#
# SYNOPSIS
# fast-cli [args...]
#
@@ -13,8 +16,16 @@
# ARGUMENTS
# args... Arguments forwarded to the fast command
#
# EXIT STATUS
# 1 fast is not installed
# * Exit status of fast otherwise
#
# EXAMPLE
# fast-cli
function fast-cli --description "Run a speed test using fast.com"
if not type -q -f fast
echo (set_color red)"Error: fast is not installed."(set_color normal) >&2
return 1
end
command fast $argv
end
+6 -3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 03-editors-and-viewers
#
# CLASSIFICATION
# bypasses-shadow(rm), self-limiting(cat)
#
# SYNOPSIS
# fc [command_prefix]
#
@@ -50,15 +53,15 @@ function fc --description 'Edit and execute the last command (Bash-style fc)'
# Final check if user cleared the file in the editor
if test -s $tmpfile
set -l command (cat $tmpfile)
rm $tmpfile
command rm -f $tmpfile
commandline -r "$command"
commandline -f execute
else
rm $tmpfile
command rm -f $tmpfile
echo "fc: Aborted (empty file)"
end
else
rm $tmpfile
command rm -f $tmpfile
echo "fc: Could not retrieve history"
end
end
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# fastfetch, neofetch
#
# CLASSIFICATION
# uses-shadow(ls)
#
# SYNOPSIS
# ffetch [args...]
#
+4 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 06-dependency-management
#
# DEPENDENCIES
# _fish_deps_status, _fish_deps_install, _fish_deps_update
#
# SYNOPSIS
# fish-deps [status|install|update|sync] [--optional] [--terminals] [--all]
#
@@ -105,7 +108,7 @@ function __fish_deps_help
echo "$c_head""fish-deps$c_reset — manage fish shell dependencies"
echo ""
echo "$c_head""Usage:$c_reset"
echo " $c_cmd""fish-deps$c_reset $c_dim""[status]$c_reset Check installed/missing deps (default)"
echo " $c_cmd""fish-deps$c_reset $c_arg""[status]$c_reset Check installed/missing deps (default)"
echo " $c_cmd""fish-deps$c_reset install Install missing deps interactively"
echo " $c_cmd""fish-deps$c_reset update Update all installed deps"
echo " $c_cmd""fish-deps$c_reset sync Install missing, then update all"
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# overrides/prompt
#
# DEPENDENCIES
# docker, starship
#
# SYNOPSIS
# fish_right_prompt
#
-251
View File
@@ -1,251 +0,0 @@
function fisher --argument-names cmd --description "A plugin manager for Fish"
set --query fisher_path || set --local fisher_path $__fish_config_dir
set --local fisher_version 4.4.8
set --local fish_plugins $__fish_config_dir/fish_plugins
switch "$cmd"
case -v --version
echo "fisher, version $fisher_version"
case "" -h --help
echo "Usage: fisher install <plugins...> Install plugins"
echo " fisher remove <plugins...> Remove installed plugins"
echo " fisher uninstall <plugins...> Remove installed plugins (alias)"
echo " fisher update <plugins...> Update installed plugins"
echo " fisher update Update all installed plugins"
echo " fisher list [<regex>] List installed plugins matching regex"
echo "Options:"
echo " -v, --version Print version"
echo " -h, --help Print this help message"
echo "Variables:"
echo " \$fisher_path Plugin installation path. Default: $__fish_config_dir" | string replace --regex -- $HOME \~
case ls list
string match --entire --regex -- "$argv[2]" $_fisher_plugins
case install update remove uninstall
isatty || read --local --null --array stdin && set --append argv $stdin
test "$cmd" = uninstall && set cmd remove
set --local install_plugins
set --local update_plugins
set --local remove_plugins
set --local arg_plugins $argv[2..-1]
set --local old_plugins $_fisher_plugins
set --local new_plugins
test -e $fish_plugins && set --local file_plugins (string match --regex -- '^[^\s]+$' <$fish_plugins | string replace -- \~ ~)
if ! set --query argv[2]
if test "$cmd" != update
echo "fisher: Not enough arguments for command: \"$cmd\"" >&2 && return 1
else if ! set --query file_plugins
echo "fisher: \"$fish_plugins\" file not found: \"$cmd\"" >&2 && return 1
end
set arg_plugins $file_plugins
else if test "$cmd" = install && ! set --query old_plugins[1]
set --append arg_plugins $file_plugins
end
for plugin in $arg_plugins
set plugin (test -e "$plugin" && realpath $plugin || string lower -- $plugin)
contains -- "$plugin" $new_plugins || set --append new_plugins $plugin
end
if set --query argv[2]
for plugin in $new_plugins
if contains -- "$plugin" $old_plugins
test "$cmd" = remove &&
set --append remove_plugins $plugin ||
set --append update_plugins $plugin
else if test "$cmd" = install
set --append install_plugins $plugin
else
echo "fisher: Plugin not installed: \"$plugin\"" >&2 && return 1
end
end
else
for plugin in $new_plugins
contains -- "$plugin" $old_plugins &&
set --append update_plugins $plugin ||
set --append install_plugins $plugin
end
for plugin in $old_plugins
contains -- "$plugin" $new_plugins || set --append remove_plugins $plugin
end
end
set --local pid_list
set --local source_plugins
set --local fetch_plugins $update_plugins $install_plugins
set --local fish_path (status fish-path)
echo (set_color --bold)fisher $cmd version $fisher_version(set_color normal)
for plugin in $fetch_plugins
set --local source (command mktemp -d)
set --append source_plugins $source
command mkdir -p $source/{completions,conf.d,themes,functions}
$fish_path --command "
if test -e $plugin
command cp -Rf $plugin/* $source
else
set resp (command mktemp)
set temp (command mktemp -d)
set repo (string split -- \@ $plugin) || set repo[2] HEAD
if set path (string replace --regex -- '^(https://)?gitlab.com/' '' \$repo[1])
set name (string split -- / \$path)[-1]
set url https://gitlab.com/\$path/-/archive/\$repo[2]/\$name-\$repo[2].tar.gz
else
set url https://api.github.com/repos/\$repo[1]/tarball/\$repo[2]
end
echo Fetching (set_color --underline)\$url(set_color normal)
set http (command curl -q --silent -L -o \$resp -w %{http_code} \$url)
if test \"\$http\" = 200 && command tar -xzC \$temp -f \$resp 2>/dev/null
command cp -Rf \$temp/*/* $source
else if test \"\$http\" = 403
echo fisher: GitHub API rate limit exceeded \(HTTP 403\) >&2
command rm -rf $source
else
echo fisher: Invalid plugin name or host unavailable: \\\"$plugin\\\" >&2
command rm -rf $source
end
command rm -rf \$temp
end
set files $source/* && string match --quiet --regex -- .+\.fish\\\$ \$files
" &
set --append pid_list (jobs --last --pid)
end
wait $pid_list 2>/dev/null
for plugin in $fetch_plugins
if set --local source $source_plugins[(contains --index -- "$plugin" $fetch_plugins)] && test ! -e $source
if set --local index (contains --index -- "$plugin" $install_plugins)
set --erase install_plugins[$index]
else
set --erase update_plugins[(contains --index -- "$plugin" $update_plugins)]
end
end
end
for plugin in $update_plugins $remove_plugins
if set --local index (contains --index -- "$plugin" $_fisher_plugins)
set --local plugin_files_var _fisher_(string escape --style=var -- $plugin)_files
if contains -- "$plugin" $remove_plugins
for name in (string replace --filter --regex -- '.+/conf\.d/([^/]+)\.fish$' '$1' $$plugin_files_var)
emit {$name}_uninstall
end
printf "%s\n" Removing\ (set_color red --bold)$plugin(set_color normal) " "$$plugin_files_var | string replace -- \~ ~
set --erase _fisher_plugins[$index]
end
command rm -rf (string replace -- \~ ~ $$plugin_files_var)
functions --erase (string replace --filter --regex -- '.+/functions/([^/]+)\.fish$' '$1' $$plugin_files_var)
for name in (string replace --filter --regex -- '.+/completions/([^/]+)\.fish$' '$1' $$plugin_files_var)
complete --erase --command $name
end
set --erase $plugin_files_var
end
end
if set --query update_plugins[1] || set --query install_plugins[1]
command mkdir -p $fisher_path/{functions,themes,conf.d,completions}
end
for plugin in $update_plugins $install_plugins
set --local source $source_plugins[(contains --index -- "$plugin" $fetch_plugins)]
set --local files $source/{functions,themes,conf.d,completions}/*
if set --local index (contains --index -- $plugin $install_plugins)
set --local user_files $fisher_path/{functions,themes,conf.d,completions}/*
set --local conflict_files
for file in (string replace -- $source/ $fisher_path/ $files)
contains -- $file $user_files && set --append conflict_files $file
end
if set --query conflict_files[1] && set --erase install_plugins[$index]
echo -s "fisher: Cannot install \"$plugin\": please remove or move conflicting files first:" \n" "$conflict_files >&2
continue
end
end
for file in (string replace -- $source/ "" $files)
command cp -RLf $source/$file $fisher_path/$file
end
set --local plugin_files_var _fisher_(string escape --style=var -- $plugin)_files
set --query files[1] && set --universal $plugin_files_var (string replace -- $source $fisher_path $files | string replace -- ~ \~)
contains -- $plugin $_fisher_plugins || set --universal --append _fisher_plugins $plugin
contains -- $plugin $install_plugins && set --local event install || set --local event update
printf "%s\n" Installing\ (set_color --bold)$plugin(set_color normal) " "$$plugin_files_var | string replace -- \~ ~
for file in (string match --regex -- '.+/[^/]+\.fish$' $$plugin_files_var | string replace -- \~ ~)
source $file
if set --local name (string replace --regex -- '.+conf\.d/([^/]+)\.fish$' '$1' $file)
emit {$name}_$event
end
end
end
command rm -rf $source_plugins
if set --query _fisher_plugins[1]
set --local commit_plugins
for plugin in $file_plugins
contains -- (string lower -- $plugin) (string lower -- $_fisher_plugins) && set --append commit_plugins $plugin
end
for plugin in $_fisher_plugins
contains -- (string lower -- $plugin) (string lower -- $commit_plugins) || set --append commit_plugins $plugin
end
string replace --regex -- $HOME \~ $commit_plugins >$fish_plugins
else
set --erase _fisher_plugins
command rm -f $fish_plugins
end
set --local total (count $install_plugins) (count $update_plugins) (count $remove_plugins)
test "$total" != "0 0 0" && echo (string join ", " (
test $total[1] = 0 || echo "Installed $total[1]") (
test $total[2] = 0 || echo "Updated $total[2]") (
test $total[3] = 0 || echo "Removed $total[3]")
) plugin/s
case \*
echo "fisher: Unknown command: \"$cmd\"" >&2 && return 1
end
end
if ! set --query _fisher_upgraded_to_4_4
set --universal _fisher_upgraded_to_4_4
if functions --query _fisher_list
set --query XDG_DATA_HOME[1] || set --local XDG_DATA_HOME ~/.local/share
command rm -rf $XDG_DATA_HOME/fisher
functions --erase _fisher_{list,plugin_parse}
fisher update >/dev/null 2>/dev/null
else
for var in (set --names | string match --entire --regex '^_fisher_.+_files$')
set $var (string replace -- ~ \~ $$var)
end
functions --erase _fisher_fish_postexec
end
end
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 06-dependency-management
#
# DEPENDENCIES
# git, fzf
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# fzf-update
#
+4
View File
@@ -4,6 +4,10 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# _fzf_search_directory, _fzf_search_git_log, _fzf_search_git_status,
# _fzf_search_history, _fzf_search_processes, _fzf_search_variables
#
# SYNOPSIS
# fzf_configure_bindings [--directory=<key>] [--git_log=<key>] [--git_status=<key>]
# [--history=<key>] [--processes=<key>] [--variables=<key>] [-h]
+161 -70
View File
@@ -4,67 +4,90 @@
# CATEGORY
# 04-git-and-version-control
#
# DEPENDENCIES
# curl, md5sum, md5, gitignore-scrub
#
# CLASSIFICATION
# self-limiting(grep,cat), network, blocking-prompt
#
# SYNOPSIS
# gi [-h] [-b] [-p] [-s] [-l] [targets...]
# gi [-h] [-b] [-p] [-o] [-s] [-f] [-c TEMPLATE] [-l] [targets...]
#
# DESCRIPTION
# Generates .gitignore content by querying the gitignore.io API. Appends
# results to the repository's .gitignore with MD5-based deduplication —
# patterns already present are not re-appended — or prints to stdout with
# -s. Supports generic boilerplate and interactive prompt modes.
# -o/--stdout. Boilerplate mode uses $GITIGNORE_BOILERPLATE if set, a
# -c/--custom template if given, or falls back to the bundled standard
# template (data/gi/boilerplate.gitignore) when neither is configured.
# Supports generic boilerplate and interactive prompt modes.
#
# ARGUMENTS
# -h, --help Show help message
# -d, --description Show the function description
# -l, --list List all supported targets from the API
# -b, --boilerplate Append boilerplate from $GITIGNORE_BOILERPLATE
# -b, --boilerplate Append boilerplate (implied by -c)
# -p, --prompt Prompt for patterns to append
# -s, --stdout Print API output to stdout instead of .gitignore
# -o, --stdout Print generated content to stdout instead of .gitignore
# -s, --silent Suppress progress output (errors and prompts still show)
# -f, --force Bypass prompts, proceeding with the default action
# -c, --custom PATH Use PATH as the boilerplate template instead of
# $GITIGNORE_BOILERPLATE
# targets Comma- or space-separated list of language/tool names
#
# EXIT STATUS
# 0 Patterns appended, or resolved with -s/--stdout or -l/--list
# 0 Patterns appended, or resolved with -o/--stdout or -l/--list
# 1 Not in a git repository or API fetch failed
#
# RETURNS
# With -s/--stdout, the fetched .gitignore pattern text, printed to stdout.
# With -o/--stdout, the fetched .gitignore pattern text, printed to stdout.
# With -l/--list, the supported target list, printed to stdout.
#
# EXAMPLE
# gi python,venv
# gi -b -p
# gi -s node > .gitignore
# gi -o node > .gitignore
# gi -f # skip prompt, proceed with no patterns
# gi -c ~/my-template.gitignore
function gi --description 'Generate .gitignore files using the gitignore.io API'
argparse h/help d/description l/list b/boilerplate p/prompt s/stdout -- $argv
argparse h/help d/description l/list b/boilerplate p/prompt o/stdout s/silent f/force c/custom= -- $argv
or return 1
if set -q _flag_help
set_color --bold
echo "Usage:"(set_color normal)" gi "(set_color cyan)"[TARGETS...]"(set_color yellow)" [FLAGS]"(set_color normal)
__fish_palette
echo "$c_head""Usage:$c_reset $c_cmd""gi$c_reset $c_arg""[TARGETS...]$c_reset $c_arg""[FLAGS]$c_reset"
echo ""
set_color --bold
echo "Arguments:"(set_color normal)
echo " "(set_color cyan)"TARGETS"(set_color normal)" Comma-separated list of languages or tools"
echo " "(set_color brblack)"e.g. c++,neovim,archlinux"(set_color normal)
echo "$c_head""Arguments:$c_reset"
echo " $c_arg""TARGETS$c_reset Comma-separated list of languages or tools"
echo " $c_dim""e.g. c++,neovim,archlinux$c_reset"
echo ""
set_color --bold
echo "Flags:"(set_color normal)
echo " "(set_color yellow)"-h, --help "(set_color normal)" Show this help message"
echo " "(set_color yellow)"-d, --description "(set_color normal)" Show the Fish function description"
echo " "(set_color yellow)"-l, --list "(set_color normal)" List all supported targets from the API"
echo " "(set_color yellow)"-b, --boilerplate "(set_color normal)" Append boilerplate from "(set_color cyan)"\$GITIGNORE_BOILERPLATE"(set_color normal)" to .gitignore"
echo " "(set_color yellow)"-p, --prompt "(set_color normal)" Prompt for patterns and append them to .gitignore"
echo " "(set_color yellow)"-s, --stdout "(set_color normal)" Print API output to stdout instead of appending to .gitignore"
echo "$c_head""Flags:$c_reset"
echo " $c_flag-h, --help $c_reset Show this help message"
echo " $c_flag-d, --description $c_reset Show the Fish function description"
echo " $c_flag-l, --list $c_reset List all supported targets from the API"
echo " $c_flag-b, --boilerplate $c_reset Append boilerplate (implied by "$c_flag"-c$c_reset)"
echo " $c_flag-p, --prompt $c_reset Prompt for patterns and append them to .gitignore"
echo " $c_flag-o, --stdout $c_reset Print generated content to stdout instead of .gitignore"
echo " $c_flag-s, --silent $c_reset Suppress progress output (errors and prompts still show)"
echo " $c_flag-f, --force $c_reset Bypass prompts, proceeding with the default action"
echo " $c_flag-c, --custom $c_reset $c_arg""PATH$c_reset Use PATH as the boilerplate template"
echo " $c_dim""instead of \$GITIGNORE_BOILERPLATE$c_reset"
echo ""
set_color --bold
echo "Examples:"(set_color normal)
echo " "(set_color green)"gi"(set_color normal)" "(set_color brblack)"# Append boilerplate and prompt for patterns (default)"(set_color normal)
echo " "(set_color green)"gi -b"(set_color normal)" "(set_color brblack)"# Append boilerplate only"(set_color normal)
echo " "(set_color green)"gi -p"(set_color normal)" "(set_color brblack)"# Prompt for patterns and append to .gitignore"(set_color normal)
echo " "(set_color green)"gi"(set_color normal)" "(set_color cyan)"c++"(set_color normal)" "(set_color brblack)"# Append C++ patterns to .gitignore"(set_color normal)
echo " "(set_color green)"gi"(set_color normal)" "(set_color cyan)"python,venv"(set_color normal)" "(set_color brblack)"# Append Python+venv patterns to .gitignore"(set_color normal)
echo " "(set_color green)"gi -s"(set_color normal)" "(set_color cyan)"python,venv"(set_color normal)" "(set_color brblack)"# Print Python+venv patterns to stdout"(set_color normal)
echo " "(set_color green)"gi -l"(set_color normal)" | grep -i linux "(set_color brblack)"# Search for specific OS support"(set_color normal)
echo "$c_head""Boilerplate source (in priority order):$c_reset"
echo " 1. "$c_flag"-c/--custom$c_reset PATH, if given"
echo " 2. "$c_arg"\$GITIGNORE_BOILERPLATE$c_reset, if set"
echo " 3. "$c_dim"the bundled standard template$c_reset"
echo ""
echo "$c_head""Examples:$c_reset"
echo " $c_cmd""gi$c_reset $c_dim""# Append boilerplate and prompt for patterns (default)$c_reset"
echo " $c_cmd""gi -b$c_reset $c_dim""# Append boilerplate only$c_reset"
echo " $c_cmd""gi -p$c_reset $c_dim""# Prompt for patterns and append to .gitignore$c_reset"
echo " $c_cmd""gi$c_reset $c_arg""c++$c_reset $c_dim""# Append C++ patterns to .gitignore$c_reset"
echo " $c_cmd""gi$c_reset $c_arg""python,venv$c_reset $c_dim""# Append Python+venv patterns to .gitignore$c_reset"
echo " $c_cmd""gi -o$c_reset $c_arg""python,venv$c_reset $c_dim""# Print Python+venv patterns to stdout$c_reset"
echo " $c_cmd""gi -f$c_reset $c_dim""# Skip prompt, proceed with no patterns$c_reset"
echo " $c_cmd""gi -c$c_reset $c_arg""~/my.gitignore$c_reset $c_dim""# Append a custom boilerplate template$c_reset"
echo " $c_cmd""gi -l$c_reset | grep -i linux $c_dim""# Search for specific OS support$c_reset"
return 0
end
@@ -78,11 +101,14 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
return 0
end
set -l silent_flag 0
set -q _flag_silent; and set silent_flag 1
# Determine which modes to run
set -l do_boilerplate 0
set -l do_prompt 0
if set -q _flag_boilerplate
if set -q _flag_boilerplate; or set -q _flag_custom
set do_boilerplate 1
end
if set -q _flag_prompt
@@ -95,14 +121,17 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
set do_prompt 1
end
# Resolve git context for anything that writes to .gitignore
# Resolve git context for anything that writes to .gitignore.
# --stdout never touches .gitignore, so it never needs a git repo.
set -l gitignore_path ""
set -l readable_path ""
set -l needs_git 0
if test $do_boilerplate -eq 1; or test $do_prompt -eq 1
set needs_git 1
else if set -q argv[1]; and not set -q _flag_stdout
set needs_git 1
if not set -q _flag_stdout
if test $do_boilerplate -eq 1; or test $do_prompt -eq 1
set needs_git 1
else if set -q argv[1]
set needs_git 1
end
end
if test $needs_git -eq 1
@@ -116,39 +145,85 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
set readable_path (string replace -r "^$HOME" "~" $gitignore_path)
end
# Boilerplate mode
# Boilerplate mode: resolve the template source, in priority order:
# 1. -c/--custom PATH
# 2. $GITIGNORE_BOILERPLATE
# 3. the bundled standard template (data/gi/boilerplate.gitignore)
if test $do_boilerplate -eq 1
if not set -q GITIGNORE_BOILERPLATE
set_color red --bold
echo "Error:" (set_color normal)"\$GITIGNORE_BOILERPLATE environment variable is not defined" >&2
else if not test -f "$GITIGNORE_BOILERPLATE"
set_color red --bold
echo "Error:" (set_color normal)"Boilerplate file not found at '$GITIGNORE_BOILERPLATE'" >&2
else
set -l template_hash ""
if command -q md5sum
set template_hash (md5sum "$GITIGNORE_BOILERPLATE" | string split ' ')[1]
else if command -q md5
set template_hash (md5 -q "$GITIGNORE_BOILERPLATE")
end
set -l boilerplate_path ""
set -l boilerplate_ok 1
set -l sig "# id: gitig-boilerplate-$template_hash"
if test -f "$gitignore_path"; and grep -qF "$sig" "$gitignore_path"
set_color yellow --bold
echo "Notice:" (set_color normal)"Boilerplate already present in "(set_color cyan)"$readable_path"(set_color normal)"."
if set -q _flag_custom
if test -f "$_flag_custom"
set boilerplate_path "$_flag_custom"
else
printf "\n%s\n" "$sig" >>"$gitignore_path"
cat "$GITIGNORE_BOILERPLATE" >>"$gitignore_path"
echo (set_color green)"✔"(set_color normal)" Appended boilerplate to "(set_color cyan)"$readable_path"(set_color normal)
set_color red --bold
echo "Error:" (set_color normal)"Custom boilerplate file not found at '$_flag_custom'" >&2
set boilerplate_ok 0
end
else if set -q GITIGNORE_BOILERPLATE
if test -f "$GITIGNORE_BOILERPLATE"
set boilerplate_path "$GITIGNORE_BOILERPLATE"
else
set_color red --bold
echo "Error:" (set_color normal)"Boilerplate file not found at '$GITIGNORE_BOILERPLATE'" >&2
set boilerplate_ok 0
end
else
if set -q __fish_config_dir
set boilerplate_path "$__fish_config_dir/data/gi/boilerplate.gitignore"
else
set boilerplate_path "$HOME/.config/fish/data/gi/boilerplate.gitignore"
end
if not test -f "$boilerplate_path"
set_color red --bold
echo "Error:" (set_color normal)"Bundled default boilerplate missing at '$boilerplate_path'" >&2
set boilerplate_ok 0
else if not set -q _flag_silent
set_color yellow --bold
echo "Notice:" (set_color normal)"\$GITIGNORE_BOILERPLATE not set; using the bundled default template."
end
end
if test $boilerplate_ok -eq 1
if set -q _flag_stdout
cat "$boilerplate_path"
else
set -l template_hash ""
if command -q md5sum
set template_hash (md5sum "$boilerplate_path" | string split ' ')[1]
else if command -q md5
set template_hash (md5 -q "$boilerplate_path")
end
set -l sig "# id: gitig-boilerplate-$template_hash"
if test -f "$gitignore_path"; and grep -qF "$sig" "$gitignore_path"
if not set -q _flag_silent
set_color yellow --bold
echo "Notice:" (set_color normal)"Boilerplate already present in "(set_color cyan)"$readable_path"(set_color normal)"."
end
else
printf "\n%s\n" "$sig" >>"$gitignore_path"
cat "$boilerplate_path" >>"$gitignore_path"
if not set -q _flag_silent
echo (set_color green)"✔"(set_color normal)" Appended boilerplate to "(set_color cyan)"$readable_path"(set_color normal)
end
end
end
end
end
# Prompt mode: ask for patterns, fetch and dedup each one individually
# Prompt mode: ask for patterns, fetch and dedup (or print) each one individually
if test $do_prompt -eq 1
read -P "Enter gitignore patterns (comma-separated, e.g. python,vim): " patterns
or return 0
set -l patterns ""
if set -q _flag_force
# Bypass the prompt: proceed with the default action (no patterns)
set patterns ""
else
read -P "Enter gitignore patterns (comma-separated, e.g. python,vim): " patterns
or return 0
end
set patterns (string trim -- $patterns)
if test -n "$patterns"
for pattern in (string split "," -- $patterns)
@@ -159,11 +234,16 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
echo "Error: Failed to fetch gitignore for '$pattern'. Is the target spelled correctly?" >&2
continue
end
__gi_append_dedup "$content" "$pattern" "$gitignore_path" "$readable_path"
if set -q _flag_stdout
echo "$content"
else
__gi_append_dedup "$content" "$pattern" "$gitignore_path" "$readable_path" $silent_flag
end
end
else
else if not set -q _flag_silent
echo (set_color brblack)"No patterns selected. Skipping API fetch."(set_color normal)
end
test $needs_git -eq 1; and gitignore-scrub
return 0
end
@@ -189,14 +269,19 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
echo "Error: Failed to fetch gitignore for '$target'. Is the target spelled correctly?" >&2
continue
end
__gi_append_dedup "$content" "$target" "$gitignore_path" "$readable_path"
__gi_append_dedup "$content" "$target" "$gitignore_path" "$readable_path" $silent_flag
end
end
end
if test $needs_git -eq 1
gitignore-scrub
end
return 0
end
# SYNOPSIS
# __gi_append_dedup <content> <label> <gitignore_path> <readable_path>
# __gi_append_dedup <content> <label> <gitignore_path> <readable_path> [silent]
#
# DESCRIPTION
# Appends gitignore content to a .gitignore file using MD5-based deduplication.
@@ -207,14 +292,16 @@ end
# label Human-readable label for the pattern set
# gitignore_path Absolute path to the .gitignore file
# readable_path Home-abbreviated path shown in output messages
# silent 1 to suppress progress output, 0/omitted to show it
#
# EXAMPLE
# __gi_append_dedup "$content" "python" "$root/.gitignore" "~/.gitignore"
# __gi_append_dedup "$content" "python" "$root/.gitignore" "~/.gitignore" 0
function __gi_append_dedup
set -l content $argv[1]
set -l label $argv[2]
set -l gitignore_path $argv[3]
set -l readable_path $argv[4]
set -l silent $argv[5]
set -l content_hash ""
if command -q md5sum
@@ -226,10 +313,14 @@ function __gi_append_dedup
set -l sig "# id: gi-patterns-$content_hash"
if test -f "$gitignore_path"; and grep -qF "$sig" "$gitignore_path"
set_color yellow --bold
echo "Notice:" (set_color normal)"$label patterns already present in "(set_color cyan)"$readable_path"(set_color normal)"."
if test "$silent" != 1
set_color yellow --bold
echo "Notice:" (set_color normal)"$label patterns already present in "(set_color cyan)"$readable_path"(set_color normal)"."
end
else
printf "\n%s\n%s\n" "$sig" "$content" >>"$gitignore_path"
echo (set_color green)"✔"(set_color normal)" Appended $label patterns to "(set_color cyan)"$readable_path"(set_color normal)
if test "$silent" != 1
echo (set_color green)"✔"(set_color normal)" Appended $label patterns to "(set_color cyan)"$readable_path"(set_color normal)
end
end
end
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 10-network
#
# DEPENDENCIES
# curl
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# gip
#
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 10-network
#
# DEPENDENCIES
# curl
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# gip4
#
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 10-network
#
# DEPENDENCIES
# curl
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# gip6
#
+9 -2
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 04-git-and-version-control
#
# DEPENDENCIES
# git
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# git-clean [-h] [-f]
#
@@ -29,9 +35,10 @@ function git-clean --description 'Sync main, prune remotes, and delete orphaned
or return
if set -q _flag_help
echo (set_color --bold blue)"Usage: "(set_color normal)"git-clean [OPTIONS]"
__fish_palette
echo "$c_head""Usage:$c_reset $c_cmd""git-clean$c_reset $c_arg""[OPTIONS]$c_reset"
echo
echo "Steps taken:"
echo "$c_head""Steps taken:$c_reset"
echo " 1. Fetches and prunes to find deleted remote branches."
echo " 2. Switches to main if you are on an orphaned branch."
echo " 3. Pulls the latest changes from the remote."
+145
View File
@@ -0,0 +1,145 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CATEGORY
# 04-git-and-version-control
#
# DEPENDENCIES
# git
#
# CLASSIFICATION
# blocking-prompt
#
# SYNOPSIS
# gitignore-scrub [-h] [-r] [-w | -f | -i]
#
# DESCRIPTION
# Finds files that are tracked by git but now match a .gitignore pattern
# (git ls-files -ci --exclude-standard) and offers to untrack them. In the
# default interactive mode, prompts once for all matches and runs
# git rm --cached on confirmation; a decline is remembered per-path in the
# repo's local git config (gitignore-scrub.skip) so the same file is not
# asked about again. -w/--warn is read-only: prints a Warning line per
# match and makes no changes, meant for non-interactive callers such as a
# git hook. -f/--force skips the prompt and untracks every match
# immediately. -i/--individual prompts once per file instead of once for
# the whole group. -w, -f, and -i are mutually exclusive. -r/--reset
# clears the repo's skip list first (combinable with any mode), so
# previously declined files are reconsidered. Silently does nothing on a
# repo with more tracked files than $GITIGNORE_SCRUB_LIMIT (default
# 5000), to avoid adding latency to huge repos.
#
# ARGUMENTS
# -h, --help Show help message
# -r, --reset Clear the remembered skip list before checking
# -w, --warn Read-only: print warnings instead of prompting, make no changes
# -f, --force Untrack every match immediately, no prompt
# -i, --individual Prompt once per file instead of once for the whole group
#
# EXIT STATUS
# 0 Clean, or a prompt/force run was handled
# 1 Not a git repository, or (-w only) unconfirmed matches were found
#
# EXAMPLE
# gitignore-scrub
# gitignore-scrub --warn
# gitignore-scrub --force
# gitignore-scrub --individual
# gitignore-scrub --reset
function gitignore-scrub --description 'Find and optionally untrack files newly matched by .gitignore'
argparse --exclusive w,f,i h/help r/reset w/warn f/force i/individual -- $argv
or return 1
if set -q _flag_help
__fish_palette
echo "$c_head""Usage:$c_reset $c_cmd""gitignore-scrub$c_reset $c_arg""[FLAGS]$c_reset"
echo ""
echo "$c_head""Flags:$c_reset"
echo " $c_flag-h, --help$c_reset Show this help message"
echo " $c_flag-r, --reset$c_reset Clear the remembered skip list before checking"
echo " $c_flag-w, --warn$c_reset Read-only: print warnings instead of prompting"
echo " $c_flag-f, --force$c_reset Untrack every match immediately, no prompt"
echo " $c_flag-i, --individual$c_reset Prompt once per file instead of once for the group"
echo ""
echo "$c_head""Examples:$c_reset"
echo " $c_cmd""gitignore-scrub$c_reset $c_dim""# Interactive: prompt to untrack matches$c_reset"
echo " $c_cmd""gitignore-scrub --warn$c_reset $c_dim""# Read-only: for use in a git hook$c_reset"
echo " $c_cmd""gitignore-scrub --force$c_reset $c_dim""# Untrack every match, no prompt$c_reset"
echo " $c_cmd""gitignore-scrub --individual$c_reset $c_dim""# Prompt per file instead of as a group$c_reset"
echo " $c_cmd""gitignore-scrub --reset$c_reset $c_dim""# Reconsider previously declined files$c_reset"
return 0
end
if not git rev-parse --is-inside-work-tree >/dev/null 2>&1
set_color red --bold
echo "Error:" (set_color normal)"Not a git repository (or any parent directories)" >&2
return 1
end
set -l limit 5000
set -q GITIGNORE_SCRUB_LIMIT; and set limit $GITIGNORE_SCRUB_LIMIT
set -l tracked_count (git ls-files | count)
if test $tracked_count -gt $limit
return 0
end
set -l offenders (git ls-files -ci --exclude-standard)
set -q offenders[1]; or return 0
if set -q _flag_reset
git config --local --remove-section gitignore-scrub 2>/dev/null
end
set -l skip_list (git config --local --get-all gitignore-scrub.skip 2>/dev/null)
set -l pending
for f in $offenders
contains -- "$f" $skip_list; or set -a pending $f
end
set -q pending[1]; or return 0
if set -q _flag_warn
for f in $pending
set_color yellow --bold
echo -n "Warning: "
set_color normal
echo "$f is tracked but ignored"
end
return 1
end
if set -q _flag_force
git rm --cached -- $pending >/dev/null
echo (set_color green)"✔"(set_color normal)" Untracked "(count $pending)" file(s)."
return 0
end
if set -q _flag_individual
for f in $pending
read -P "Remove '$f' from git tracking? [y/N] " confirm
if string match -qir '^y' -- "$confirm"
git rm --cached -- "$f" >/dev/null
echo (set_color green)"✔"(set_color normal)" Untracked $f"
else
git config --local --add gitignore-scrub.skip "$f"
end
end
return 0
end
set_color yellow
echo (count $pending)" tracked file(s) now match .gitignore:"(set_color normal)
for f in $pending
echo " $f"
end
read -P "Remove from git tracking? [y/N] " confirm
if string match -qir '^y' -- "$confirm"
git rm --cached -- $pending >/dev/null
echo (set_color green)"✔"(set_color normal)" Untracked "(count $pending)" file(s)."
else
for f in $pending
git config --local --add gitignore-scrub.skip "$f"
end
echo (set_color brblack)"Remembered — won't ask again for these files."(set_color normal)
end
end
+12 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 04-git-and-version-control
#
# DEPENDENCIES
# gitui
#
# SYNOPSIS
# gitui [args...]
#
@@ -14,9 +17,17 @@
# ARGUMENTS
# args... Arguments forwarded to the gitui command
#
# EXIT STATUS
# 1 gitui is not installed
# * Exit status of gitui otherwise
#
# EXAMPLE
# gitui
function gitui --wraps='gitui' --description 'alias gitui=gitui -t mocha.ron'
command gitui -t frappe.ron $argv
if not type -q -f gitui
echo (set_color red)"Error: gitui is not installed."(set_color normal) >&2
return 1
end
command gitui -t frappe.ron $argv
end
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 04-git-and-version-control
#
# DEPENDENCIES
# git
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# gitup [args...]
#
+7 -3
View File
@@ -12,7 +12,11 @@
#
# DESCRIPTION
# Searches fish history interactively using fzf, inserts the selected command
# into the command line, and copies it to the clipboard via wl-copy.
# into the command line, and copies it to the clipboard via wl-copy, xclip,
# or win32yank (WSL2).
#
# DEPENDENCIES
# _fish_clipboard_copy
#
# EXIT STATUS
# 0 Command selected and inserted, or fzf was cancelled
@@ -30,13 +34,13 @@ function hist --description 'Search fish history and put it in the prompt'
return 1
end
set -l selected (history | fzf --reverse --height 40% --with-nth 3..)
set -l selected (builtin history --show-time='%F %T ' | fzf --reverse --height 40% --with-nth 3..)
if test -n "$selected"
# Strip the timestamp for the final output
set -l command (echo $selected | string replace -r '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} ' '')
echo $command | wl-copy 2>/dev/null
echo $command | _fish_clipboard_copy 2>/dev/null
commandline -r $command
end
end
+3
View File
@@ -7,6 +7,9 @@
# DEPENDENCIES
# tmux, screen, __jobrunner_sessions
#
# CLASSIFICATION
# bypasses-shadow(cat,rm)
#
# SYNOPSIS
# jobrunner [-t <tool>] [<subcommand>] [<name>] [<command>...]
# jr [-t <tool>] [<subcommand>] [<name>] [<command>...]
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# DEPENDENCIES
# joplin
#
# SYNOPSIS
# joplin [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# DEPENDENCIES
# gpg, tar
#
# CLASSIFICATION
# bypasses-shadow(rm), destructive
#
# SYNOPSIS
# key-crypt [options] <input> [output]
# key-crypt -i <input> -o <output> [options]
+7
View File
@@ -7,6 +7,13 @@
# COMPONENT
# logging/terminal-capture
#
# DEPENDENCIES
# kitty, __kitty_logging_dir, __kitty_logging_has_watcher,
# __kitty_logging_version
#
# CLASSIFICATION
# bypasses-shadow(grep,mkdir,rm)
#
# SYNOPSIS
# kitty-logging [install | uninstall | status | dismiss] [-h]
#
+6
View File
@@ -4,6 +4,12 @@
# CATEGORY
# 01-file-and-directory
#
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# lD [args...]
#

Some files were not shown because too many files have changed in this diff Show More