Compare commits

...
40 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
104 changed files with 3773 additions and 549 deletions
+136 -12
View File
@@ -4,7 +4,7 @@ on:
push: push:
branches: branches:
- main - main
paths: paths: &ci-paths
- "docs/manual/**" - "docs/manual/**"
- "docs/build-manual.py" - "docs/build-manual.py"
- "docs/manualtools.py" - "docs/manualtools.py"
@@ -16,6 +16,17 @@ on:
- "completions/**" - "completions/**"
- "integrations/**" - "integrations/**"
- "tests/**" - "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: workflow_dispatch:
inputs: inputs:
job: job:
@@ -26,7 +37,7 @@ on:
options: options:
- all - all
- test - test
- build-docs - docs
jobs: jobs:
# This workflow file is mirrored to GitHub as-is, but the runner label # This workflow file is mirrored to GitHub as-is, but the runner label
@@ -44,8 +55,41 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
token: ${{ secrets.GITEA_TOKEN }} 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 - name: Install fish
if: steps.relevance.outputs.run == 'true'
run: | run: |
sudo apt-get -o Acquire::Retries=3 update -qq sudo apt-get -o Acquire::Retries=3 update -qq
# apt-utils, so debconf has a target for the "delaying package # apt-utils, so debconf has a target for the "delaying package
@@ -63,16 +107,27 @@ jobs:
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y fish sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y fish
- name: Run fish config tests - name: Run fish config tests
if: steps.relevance.outputs.run == 'true'
run: fish tests/run-tests.fish run: fish tests/run-tests.fish
build-docs: # Documentation tests/build, and (push/dispatch only) publish. Split
needs: test # 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: | if: |
github.server_url != 'https://github.com' && github.server_url != 'https://github.com' &&
always() && (github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all' || github.event.inputs.job == 'docs')
(github.event.inputs.job == 'build-docs' ||
((github.event_name != 'workflow_dispatch' || github.event.inputs.job == 'all') &&
needs.test.result == 'success'))
runs-on: racknerd-mini runs-on: racknerd-mini
env: env:
# Silences Node's internal "punycode module is deprecated" notice # Silences Node's internal "punycode module is deprecated" notice
@@ -83,9 +138,43 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: 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 - name: Install dependencies
if: steps.relevance.outputs.run == 'true'
run: | run: |
sudo apt-get -o Acquire::Retries=3 update -qq sudo apt-get -o Acquire::Retries=3 update -qq
# apt-utils: see the "Install fish" step's identical comment in # apt-utils: see the "Install fish" step's identical comment in
@@ -97,6 +186,7 @@ jobs:
sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y pandoc python3-yaml fish sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y pandoc python3-yaml fish
- name: Generate concatenated markdown - name: Generate concatenated markdown
if: steps.relevance.outputs.run == 'true'
run: python3 docs/build-manual.py --concat -o docs/fish-config.md run: python3 docs/build-manual.py --concat -o docs/fish-config.md
# Regeneration MUST run before verification: verify-manual.py's # Regeneration MUST run before verification: verify-manual.py's
@@ -104,13 +194,19 @@ jobs:
# against docs/fish-config.md on disk. Before this step ran, that # 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 # file was still the stale pre-push copy, so any ordinary edit under
# docs/manual/** failed the round-trip check before anything was # 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 # pandoc and the auto-commit below, it just no longer requires a
# contributor to hand-sync the generated file before pushing. # 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 - name: Verify manual integrity
if: steps.relevance.outputs.run == 'true'
run: python3 docs/verify-manual.py run: python3 docs/verify-manual.py
- name: Compile man page - name: Compile man page
if: steps.relevance.outputs.run == 'true'
run: | run: |
pandoc --standalone \ pandoc --standalone \
--from markdown \ --from markdown \
@@ -118,21 +214,30 @@ jobs:
docs/fish-config.md \ docs/fish-config.md \
-o docs/fish-config.1 -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 - name: Set up Node
if: github.event_name != 'pull_request'
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "24" node-version: "24"
- name: Generate site content - name: Generate site content
if: github.event_name != 'pull_request'
run: python3 docs/build-manual.py --site run: python3 docs/build-manual.py --site
- name: Build project wiki - name: Build project wiki
if: github.event_name != 'pull_request'
working-directory: docs/site working-directory: docs/site
run: | run: |
npm ci --no-fund npm ci --no-fund
npx astro build npx astro build
- name: Deploy to Cloudflare Pages - name: Deploy to Cloudflare Pages
if: github.event_name != 'pull_request'
working-directory: docs/site working-directory: docs/site
env: env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
@@ -144,6 +249,7 @@ jobs:
--commit-dirty=true --commit-dirty=true
- name: Commit generated docs - name: Commit generated docs
if: github.event_name != 'pull_request'
env: env:
BOT_GPG_KEY: ${{ secrets.CI_GPG_PRIVATE_KEY }} BOT_GPG_KEY: ${{ secrets.CI_GPG_PRIVATE_KEY }}
run: | run: |
@@ -171,7 +277,25 @@ jobs:
git add docs/fish-config.md docs/fish-config.1 conf.d/__fish_config_op_registry.fish 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 diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "chore(docs): regenerate manual, man page, and component registry" 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 # Stand-in for the GitHub mirror so the commit gets a completed status
# instead of the real jobs above sitting queued forever for a # instead of the real jobs above sitting queued forever for a
@@ -183,4 +307,4 @@ jobs:
- name: Note that CI runs on Gitea - name: Note that CI runs on Gitea
run: | run: |
echo "This repository mirrors from Gitea (git.rootiest.dev), where CI actually runs." 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."
+71 -6
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 approval before doing this** — there's no standing exception, no matter
how trivial the change looks. 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 ### Pull request descriptions
Fill in `.github/PULL_REQUEST_TEMPLATE.md` — Gitea pre-loads it into the Fill in `.github/PULL_REQUEST_TEMPLATE.md` — Gitea pre-loads it into the
@@ -390,8 +417,8 @@ all optional except where noted:
|---|---| |---|---|
| `CATEGORY` | **Required to appear in the manual at all** — see below. | | `CATEGORY` | **Required to appear in the manual at all** — see below. |
| `COMPONENT` | Only for functions gated by the [opinionated-component system](#opinionated-components). | | `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 — see below. | | `CLASSIFICATION` | Hazard/shadow-interaction tags, plus a couple of general-purpose ones (`manual-section`) — see below. |
| `SYNOPSIS` | One-line usage form. | | `SYNOPSIS` | One-line usage form. |
| `DESCRIPTION` | Prose description; can span multiple paragraphs. | | `DESCRIPTION` | Prose description; can span multiple paragraphs. |
| `ARGUMENTS` | Flags/positional args, one per line. | | `ARGUMENTS` | Flags/positional args, one per line. |
@@ -447,13 +474,51 @@ If your function genuinely doesn't fit any of these, add a new
`docs/manual/05-functions/NN-your-category.md` stub (with frontmatter `docs/manual/05-functions/NN-your-category.md` stub (with frontmatter
matching its siblings) rather than force-fitting it into an existing one. matching its siblings) rather than force-fitting it into an existing one.
**`CLASSIFICATION` flags hazards and shadow interactions, optional and **`CLASSIFICATION` is the general-purpose tag field, optional and omitted
omitted when nothing applies:** whether the function calls a 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) [C1-shadowed command](docs/manual/08-components-reference/01-c1-command-shadows.md)
bare wanting the override (`uses-shadow(ls)`) or bypasses it deliberately bare wanting the override (`uses-shadow(ls)`) or bypasses it deliberately
via `command`/`builtin` (`bypasses-shadow(cat)`), and general hazards — via `command`/`builtin` (`bypasses-shadow(cat)`), and general hazards —
`destructive`, `network`, `blocking-prompt`. Full tag definitions and `destructive`, `network`, `blocking-prompt` — but not exclusively: a
placement rule: [`docs/function-classification-schema.md`](docs/function-classification-schema.md). 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 ### Private/internal helper functions
+6 -5
View File
@@ -1,8 +1,9 @@
# Fish Shell Configuration # Rootiest Fish Configuration
A feature-rich Fish shell configuration for CachyOS (Arch Linux), This isn't a generic Fish shell configuration — it's the Rootiest Fish
built around a Catppuccin Mocha aesthetic with a curated set of modern Configuration: a feature-rich setup for CachyOS (Arch Linux), built
CLI tool integrations, smart shell functions, and a heavily customized 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. abbreviation system for keyboard-driven workflows.
📖 **[Project Wiki](https://fish.rootiest.fyi/)** 📖 **[Project Wiki](https://fish.rootiest.fyi/)**
@@ -132,7 +133,7 @@ silent until you enable logging.
## Documentation ## 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 A Starlight-powered site rebuilt on every push to `main`. It covers
configuration variables, key bindings, abbreviations, all functions, the configuration variables, key bindings, abbreviations, all functions, the
-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)"
+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
# ──────────────────────────────────────────────────────────────
+66 -8
View File
@@ -202,7 +202,7 @@ def build_concat(root: Path) -> str:
Only bodies are passed: the pandoc metadata block above is not prose Only bodies are passed: the pandoc metadata block above is not prose
and must survive byte-for-byte. 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] = [] chunks: list[str] = []
pandoc_path = root / "_pandoc.yml" pandoc_path = root / "_pandoc.yml"
if pandoc_path.exists(): if pandoc_path.exists():
@@ -726,7 +726,44 @@ def _classification_tags(raw: list[str]) -> list[str]:
return [t for t in tags if t] return [t for t in tags if t]
def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str: 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. """Render one parsed function header as a manual entry body.
Emits the same man-page shape Section 5 was authored in — one 4-space Emits the same man-page shape Section 5 was authored in — one 4-space
@@ -759,15 +796,22 @@ def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str
def names(raw: list[str]) -> list[str]: def names(raw: list[str]) -> list[str]:
return [n for n in re.split(r"[,\s]+", " ".join(raw)) if n] return [n for n in re.split(r"[,\s]+", " ".join(raw)) if n]
classification = _classification_tags(fn.get("CLASSIFICATION", []))
refs = [] refs = []
for label, values in ( for label, values in (
("Dependencies", names(fn.get("DEPENDENCIES", []))), ("Dependencies", names(fn.get("DEPENDENCIES", []))),
("Classification", _classification_tags(fn.get("CLASSIFICATION", []))), ("Classification", classification),
("Used by", sorted(used_by)), ("Used by", sorted(used_by)),
): ):
if values: if values:
rendered = ", ".join(link(v) if link else f"`{v}`" for v in values) rendered = ", ".join(link(v) if link else f"`{v}`" for v in values)
refs.append(f"**{label}:** {rendered}") 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: if refs:
block += "\n\n" + "\n\n".join(refs) block += "\n\n" + "\n\n".join(refs)
return block return block
@@ -871,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. """Render one parsed function header as a manual entry body for the site.
Unlike `render_entry` (the single indented man-page block pandoc wants, Unlike `render_entry` (the single indented man-page block pandoc wants,
@@ -905,15 +951,22 @@ def render_entry_site(fn: dict[str, list[str]], used_by: list[str], link=None) -
def names(raw: list[str]) -> list[str]: def names(raw: list[str]) -> list[str]:
return [n for n in re.split(r"[,\s]+", " ".join(raw)) if n] return [n for n in re.split(r"[,\s]+", " ".join(raw)) if n]
classification = _classification_tags(fn.get("CLASSIFICATION", []))
refs = [] refs = []
for label, values in ( for label, values in (
("Dependencies", names(fn.get("DEPENDENCIES", []))), ("Dependencies", names(fn.get("DEPENDENCIES", []))),
("Classification", _classification_tags(fn.get("CLASSIFICATION", []))), ("Classification", classification),
("Used by", sorted(used_by)), ("Used by", sorted(used_by)),
): ):
if values: if values:
rendered = ", ".join(link(v) if link else f"`{v}`" for v in values) rendered = ", ".join(link(v) if link else f"`{v}`" for v in values)
refs.append(f"**{label}:** {rendered}") 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: if refs:
parts.append("\n\n".join(refs)) parts.append("\n\n".join(refs))
@@ -921,7 +974,7 @@ def render_entry_site(fn: dict[str, list[str]], used_by: list[str], link=None) -
def build_entries( 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]]]: ) -> dict[str, list[tuple[str, str]]]:
"""Group rendered entries by category stem, ordered by function name. """Group rendered entries by category stem, ordered by function name.
@@ -929,6 +982,9 @@ def build_entries(
authored: a bidirectional link maintained by hand drifts the moment one authored: a bidirectional link maintained by hand drifts the moment one
side is edited. `site` selects `render_entry_site` (headings + tables) side is edited. `site` selects `render_entry_site` (headings + tables)
over `render_entry` (the man-page indented block `build_concat` needs). 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]] = {} used_by: dict[str, list[str]] = {}
for name, fn in functions.items(): for name, fn in functions.items():
@@ -940,7 +996,7 @@ def build_entries(
out: dict[str, list[tuple[str, str]]] = {} out: dict[str, list[tuple[str, str]]] = {}
for name in sorted(functions): for name in sorted(functions):
fn = functions[name] 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)) out.setdefault(fn["CATEGORY"][0], []).append((name, body))
return out return out
@@ -1041,7 +1097,9 @@ def build_site(root: Path, out: Path) -> list[dict]:
out.mkdir(parents=True) out.mkdir(parents=True)
functions = mt.parse_functions(FUNCTIONS) 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": "/"}] sidebar: list[dict] = [{"label": "Home", "link": "/"}]
standard_groups: dict = {} standard_groups: dict = {}
+681 -38
View File
File diff suppressed because it is too large Load Diff
+15 -8
View File
@@ -423,14 +423,21 @@ contributing=# 15. CONTRIBUTING
contribute=# 15. CONTRIBUTING contribute=# 15. CONTRIBUTING
forge=# 15. CONTRIBUTING forge=# 15. CONTRIBUTING
# ── Section 16: Attribution ─────────────────────────────────── # ── Section 16: AI Agent Tooling ──────────────────────────────
attribution=# 16. ATTRIBUTION agent=# 16. AI AGENT TOOLING
credits=# 16. ATTRIBUTION 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 ─────────────────────────────────────── # ── Section 17: Attribution ───────────────────────────────────
license=# 17. LICENSE attribution=# 17. ATTRIBUTION
licensing=# 17. LICENSE credits=# 17. ATTRIBUTION
agpl=# 17. LICENSE
copyright=# 17. LICENSE # ── Section 18: License ────────────────────────────────────────
license=# 18. LICENSE
licensing=# 18. LICENSE
agpl=# 18. LICENSE
copyright=# 18. LICENSE
+608 -35
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -5,6 +5,14 @@ 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 link here, not to anything under `AGENTS/` (that tree is git-ignored local
agent state, not part of the repo). 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) See [Public function documentation header](../CONTRIBUTING.md#public-function-documentation-header)
in `CONTRIBUTING.md` for where `CLASSIFICATION` fits among the other 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) labels, and [C1 — Command Shadows](manual/08-components-reference/01-c1-command-shadows.md)
@@ -72,6 +80,22 @@ it empty as a placeholder.
handler, an fzf-driven picker) — the hazard this tag exists for is a handler, an fzf-driven picker) — the hazard this tag exists for is a
script or another function calling it unexpectedly, not a human running script or another function calling it unexpectedly, not a human running
it themselves. 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 ## Placement
@@ -24,7 +24,7 @@ all of these commands.
grep/fgrep/egrep forced --color=auto system grep variants grep/fgrep/egrep forced --color=auto system grep variants
dir / vdir forced --color=auto system dir / vdir dir / vdir forced --color=auto system dir / vdir
help config intercepts "help config" → config-help fish builtin help 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 edit multi-editor launcher (GUI/term + fallbacks) $EDITOR/nvim/nano/vi
When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files
@@ -64,7 +64,7 @@ and the `help config` interception.
## dev-tools ## dev-tools
`claude` (AGENTS.md/CLAUDE.md auto-linking) and `edit` (multi-editor `claude` (AGENTS/ scaffolding) and `edit` (multi-editor
launcher), plus `agy`. launcher), plus `agy`.
## For function authors ## For function authors
+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 title: Attribution
manTitle: 16. ATTRIBUTION manTitle: 17. ATTRIBUTION
sidebar: sidebar:
order: 20 order: 21
helpKeywords: helpKeywords:
- attribution - attribution
- credits - credits
@@ -1,8 +1,8 @@
--- ---
title: License title: License
manTitle: 17. LICENSE manTitle: 18. LICENSE
sidebar: sidebar:
order: 21 order: 22
helpKeywords: helpKeywords:
- license - license
- licensing - 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. description: Reference manual for the rootiest fish configuration.
manTitle: DESCRIPTION manTitle: DESCRIPTION
sidebar: sidebar:
+125 -2
View File
@@ -202,13 +202,25 @@ def test_dependencies_resolve():
Catches typos, and catches stale entries when a dependency is renamed Catches typos, and catches stale entries when a dependency is renamed
or deleted. External binaries are accepted when some file in the tree 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 repo = Path(__file__).parent.parent
functions = _parsed_functions() functions = _parsed_functions()
known = {p.stem for p in (repo / "functions").glob("*.fish")} | set(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")): 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 = [] dangling = []
for name, fn in functions.items(): for name, fn in functions.items():
for dep in (d for d in re.split(r"[,\s]+", " ".join(fn.get("DEPENDENCIES", []))) if d): 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]}" 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_")] TESTS = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
@@ -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
+99 -95
View File
@@ -5,10 +5,10 @@
# 12-ai-and-developer-tools # 12-ai-and-developer-tools
# #
# DEPENDENCIES # 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 # CLASSIFICATION
# self-limiting(rm,mkdir), bypasses-shadow(mv) # self-limiting(rm,mkdir,grep), bypasses-shadow(mv), manual-section(16-agent-tooling)
# #
# SYNOPSIS # SYNOPSIS
# agents-init [-a | --agents] [-p | --plugins] [-v | --verbose] # agents-init [-a | --agents] [-p | --plugins] [-v | --verbose]
@@ -17,8 +17,18 @@
# DESCRIPTION # DESCRIPTION
# Scaffolds an AGENTS/ sub-repository inside a project directory. Creates # Scaffolds an AGENTS/ sub-repository inside a project directory. Creates
# a self-contained git repo for agent specifications, moves any existing # a self-contained git repo for agent specifications, moves any existing
# agent-related files into it, and replaces them with symlinks so the outer # agent-related files into it, and replaces them with symlinks so the
# project never tracks agent files directly. # 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 # 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 # already has an AGENTS.md, CLAUDE.md, or AGENTS/. Elsewhere it is a
@@ -26,11 +36,12 @@
# create a repository there. # create a repository there.
# #
# File layout after setup: # File layout after setup:
# AGENTS/AGENTS.md canonical agent spec (real file) # AGENTS/AGENTS.md canonical root agent spec (real file)
# AGENTS/CLAUDE.md real file (if CLAUDE.md existed separately) # AGENTS/<subdir>/AGENTS.md canonical spec for any subdir with its own
# or symlink → AGENTS.md (single-source case) # scoped instructions (real file, discovered
# automatically -- see above)
# <root>/AGENTS.md → AGENTS/AGENTS.md # <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/plans superpowers plans (real dir, .gitkeep)
# AGENTS/specs superpowers specs (real dir, .gitkeep) # AGENTS/specs superpowers specs (real dir, .gitkeep)
# AGENTS/devlogs agent development logs (real dir, .gitkeep) # AGENTS/devlogs agent development logs (real dir, .gitkeep)
@@ -42,6 +53,12 @@
# docs/specs → ../AGENTS/specs (only if docs/specs existed) # docs/specs → ../AGENTS/specs (only if docs/specs existed)
# docs/devlogs → ../AGENTS/devlogs (only if docs/devlogs 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>, # plans/ and specs/ are merged from every legacy location (docs/<tgt>,
# docs/superpowers/<tgt>, and the old AGENTS/plugins/ layout) into the # docs/superpowers/<tgt>, and the old AGENTS/plugins/ layout) into the
# canonical AGENTS/<tgt>; the AGENTS/plugins/ layer is removed. # canonical AGENTS/<tgt>; the AGENTS/plugins/ layer is removed.
@@ -75,7 +92,8 @@
# Called automatically by the claude and agy wrappers on every invocation. # Called automatically by the claude and agy wrappers on every invocation.
# #
# ARGUMENTS # 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 # -p, --plugins Set up AGENTS/ repo + plans/specs/devlogs dirs + docs/ symlinks only
# -v, --verbose Print all per-step output (default) # -v, --verbose Print all per-step output (default)
# -q, --quiet Print one summary line only if changes were made # -q, --quiet Print one summary line only if changes were made
@@ -92,6 +110,15 @@
# agents-init --agents # agents-init --agents
# agents-init --plugins # agents-init --plugins
# agents-init --quiet # 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' function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec files and plugin dirs'
__fish_palette __fish_palette
@@ -105,7 +132,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
echo echo
echo "$c_head""Options:$c_reset" echo "$c_head""Options:$c_reset"
echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message" 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-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-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" echo " $c_flag-q$c_reset, $c_flag--quiet$c_reset Print one summary line only if changes were made"
@@ -140,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/ # directory created an AGENTS/ repo, two root symlinks, and a docs/
# tree there. # tree there.
set -l root (git rev-parse --show-toplevel 2>/dev/null) set -l root (git rev-parse --show-toplevel 2>/dev/null)
set -l in_git 1
if test -z "$root" if test -z "$root"
set in_git 0
if test -e (pwd)/AGENTS.md -o -e (pwd)/CLAUDE.md -o -d (pwd)/AGENTS if test -e (pwd)/AGENTS.md -o -e (pwd)/CLAUDE.md -o -d (pwd)/AGENTS
set root (pwd) set root (pwd)
else else
@@ -201,109 +230,84 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
# ──────────────────────────── --agents mode ────────────────────────────── # ──────────────────────────── --agents mode ──────────────────────────────
if test $do_agents -eq 1 if test $do_agents -eq 1
# Detect which root-level files are real (not symlinks) # Discover every directory carrying agent instructions -- root
set -l has_agents 0 # included, subdirectories found automatically rather than by a
set -l has_claude 0 # hardcoded list. A real file, an already-migrated symlink, or a
if test -f "$root/AGENTS.md"; and not test -L "$root/AGENTS.md" # leftover inverted-mirror survivor all match, so one pass covers
set has_agents 1 # 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 end
if test -f "$root/CLAUDE.md"; and not test -L "$root/CLAUDE.md" set -l rels "."
set has_claude 1 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 end
# ── Move real files into AGENTS/ ────────────────────────────────────── for rel in $rels
if test $has_agents -eq 1; and test $has_claude -eq 1 set -l out (_agents_init_sync_instructions "$root" "$agents_dir" "$rel")
# Both exist: preserve each as its own file in AGENTS/ set -l rc $status
if not test -f "$agents_dir/AGENTS.md" if test $rc -ne 0
if not command mv "$root/AGENTS.md" "$agents_dir/AGENTS.md" echo "$c_err""Error: could not sync AGENTS.md for $rel$c_reset" >&2
echo "$c_err""Error: could not move AGENTS.md → AGENTS/AGENTS.md$c_reset" >&2
return 1 return 1
end end
if test -n "$out"
set changed 1 set changed 1
test $verbose -eq 1; and echo "$c_ok→ Moved AGENTS.md → AGENTS/AGENTS.md$c_reset" if test $verbose -eq 1
for line in $out
echo "$c_ok$line$c_reset"
end end
if not test -f "$agents_dir/CLAUDE.md"; and not test -L "$agents_dir/CLAUDE.md"
if not command 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 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 command 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 command 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
end end
# ── Ensure AGENTS/CLAUDE.md exists ──────────────────────────────────── # ── Migrate stale anchored gitignore lines ──────────────────────────────
# When both files existed, AGENTS/CLAUDE.md is already a real file. # A project scaffolded by the old agents-init already has anchored
# Otherwise, create it as a symlink → AGENTS.md (within AGENTS/). # /AGENTS.md and/or /CLAUDE.md lines in .gitignore. git check-ignore
if not test -f "$agents_dir/CLAUDE.md"; and not test -L "$agents_dir/CLAUDE.md" # sees those as covering the literal path "AGENTS.md", so the new
if not ln -s AGENTS.md "$agents_dir/CLAUDE.md" # unanchored pattern below would be judged already-covered and never
echo "$c_err""Error: could not create AGENTS/CLAUDE.md symlink$c_reset" >&2 # added -- leaving any newly discovered subdirectory AGENTS.md with no
return 1 # gitignore coverage at all. Strip the stale exact lines first so the
end # 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 set changed 1
test $verbose -eq 1; and echo "$c_ok→ Linked AGENTS/CLAUDE.md → AGENTS/AGENTS.md$c_reset" test $verbose -eq 1; and echo "$c_warn→ Removed stale /AGENTS.md line from .gitignore$c_reset"
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 end
if grep -qxF "/CLAUDE.md" "$gitignore"
sed -i '/^\/CLAUDE\.md$/d' "$gitignore"
set changed 1 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 /CLAUDE.md line from .gitignore$c_reset"
end end
end end
# ── .gitignore ──────────────────────────────────────────────────────── # ── .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" if test -n "$_gi"
set changed 1 set changed 1
test $verbose -eq 1; and echo $_gi test $verbose -eq 1; and echo $_gi
+8 -1
View File
@@ -10,7 +10,7 @@
# _agents_repo_install_tools, git, hostname # _agents_repo_install_tools, git, hostname
# #
# CLASSIFICATION # CLASSIFICATION
# self-limiting(rm,mkdir) # self-limiting(rm,mkdir), manual-section(16-agent-tooling)
# #
# SYNOPSIS # SYNOPSIS
# agents-vault [--link] [--push] [--restore] [--status] # agents-vault [--link] [--push] [--restore] [--status]
@@ -188,6 +188,13 @@
# machine that has a real global memory directory would move it into a # machine that has a real global memory directory would move it into a
# throwaway directory and leave a dangling symlink behind, which is # throwaway directory and leave a dangling symlink behind, which is
# strictly worse than having had no backup at all. # 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' function agents-vault --description 'track curated agent memory in a host-scoped vault repo'
__fish_palette __fish_palette
+2 -2
View File
@@ -17,8 +17,8 @@
# Wrapper for the agy Antigravity AI CLI that ensures the AGENTS/ # Wrapper for the agy Antigravity AI CLI that ensures the AGENTS/
# sub-repository is initialized and any agent-made changes are committed # sub-repository is initialized and any agent-made changes are committed
# before launch. Delegates all scaffold and commit logic to agents-init # before launch. Delegates all scaffold and commit logic to agents-init
# --quiet (full setup), which ensures AGENTS/ is scaffolded and CLAUDE.md # --quiet (full setup), which ensures AGENTS.md (root and every scoped
# is symlinked to AGENTS/AGENTS.md in the current project. # subdirectory) is symlinked into AGENTS/ in the current project.
# #
# Also syncs the host-scoped agent memory vault (agents-vault). agy has # 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 # no session-end hook, so its memory is captured on the next launch
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# autoexec/sync # autoexec/sync
# #
# DEPENDENCIES
# git
#
# SYNOPSIS # SYNOPSIS
# auto-pull [list] # auto-pull [list]
# auto-pull add [PATH] # auto-pull add [PATH]
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# curl, jq, git
#
# SYNOPSIS # SYNOPSIS
# bd-pull <owner/repo> # bd-pull <owner/repo>
# #
+8
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 08-terminal-management # 08-terminal-management
# #
# DEPENDENCIES
# nohup
#
# SYNOPSIS # SYNOPSIS
# bkg <command> [args...] # bkg <command> [args...]
# #
@@ -32,6 +35,11 @@ function bkg --description 'Execute bkg'
return 1 return 1
end 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). # 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. # Redirect both stdout and stderr to /dev/null to discard all output.
# The final ampersand (&) sends the entire process to the background. # The final ampersand (&) sends the entire process to the background.
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 04-git-and-version-control # 04-git-and-version-control
# #
# DEPENDENCIES
# git
#
# SYNOPSIS # SYNOPSIS
# branch <branch_name> # branch <branch_name>
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/filesystem # aliases/filesystem
# #
# DEPENDENCIES
# bat
#
# CLASSIFICATION # CLASSIFICATION
# uses-shadow(ls), bypasses-shadow(cat) # uses-shadow(ls), bypasses-shadow(cat)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 02-navigation # 02-navigation
# #
# DEPENDENCIES
# zoxide
#
# SYNOPSIS # SYNOPSIS
# cdi [query] # cdi [query]
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# fastfetch, neofetch
#
# CLASSIFICATION # CLASSIFICATION
# uses-shadow(ls) # uses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# cheat, tldr, man
#
# SYNOPSIS # SYNOPSIS
# cheat <topic> [args...] # cheat <topic> [args...]
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 06-dependency-management # 06-dependency-management
# #
# DEPENDENCIES
# fish-deps
#
# SYNOPSIS # SYNOPSIS
# check_fish_deps # check_fish_deps
# #
+4 -3
View File
@@ -20,8 +20,9 @@
# Wrapper for the claude CLI that ensures the AGENTS/ sub-repository is # Wrapper for the claude CLI that ensures the AGENTS/ sub-repository is
# initialized and any agent-made changes are committed before launch. # initialized and any agent-made changes are committed before launch.
# Delegates all scaffold and commit logic to agents-init --quiet (full # Delegates all scaffold and commit logic to agents-init --quiet (full
# setup), which ensures AGENTS/ is scaffolded and CLAUDE.md is symlinked # setup), which ensures AGENTS.md (root and every scoped subdirectory)
# to AGENTS/AGENTS.md in the current project. # 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 # Also syncs the host-scoped agent memory vault (agents-vault), which
# tracks curated memory living outside the project tree. The vault # tracks curated memory living outside the project tree. The vault
@@ -44,7 +45,7 @@
# claude # claude
# claude --resume # claude --resume
# claude "Explain the recent changes" # 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) if not __fish_config_op_enabled (status current-function)
command claude $argv command claude $argv
return $status return $status
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 05-package-management # 05-package-management
# #
# DEPENDENCIES
# pacman
#
# CLASSIFICATION # CLASSIFICATION
# self-limiting(grep) # self-limiting(grep)
# #
+11 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 02-navigation # 02-navigation
# #
# DEPENDENCIES
# clone-in-kitty
#
# SYNOPSIS # SYNOPSIS
# clone [args...] # clone [args...]
# #
@@ -16,7 +19,7 @@
# #
# EXIT STATUS # EXIT STATUS
# 0 Repository cloned # 0 Repository cloned
# 1 Not running inside Kitty terminal # 1 Not running inside Kitty terminal, or clone-in-kitty isn't available
# #
# EXAMPLE # EXAMPLE
# clone https://github.com/user/repo.git # 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 echo "Error: The 'clone' command requires Kitty terminal." >&2
return 1 return 1
end 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 clone-in-kitty $argv
end end
+11 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 02-navigation # 02-navigation
# #
# DEPENDENCIES
# clone-in-kitty
#
# SYNOPSIS # SYNOPSIS
# clonet [args...] # clonet [args...]
# #
@@ -16,7 +19,7 @@
# #
# EXIT STATUS # EXIT STATUS
# 0 Repository cloned # 0 Repository cloned
# 1 Not running inside Kitty terminal # 1 Not running inside Kitty terminal, or clone-in-kitty isn't available
# #
# EXAMPLE # EXAMPLE
# clonet https://github.com/user/repo.git # 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 echo "Error: The 'clonet' command requires Kitty terminal." >&2
return 1 return 1
end 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 clone-in-kitty --type=tab $argv
end end
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# xdg-open, man, ov, bat
#
# CLASSIFICATION # CLASSIFICATION
# self-limiting(grep), bypasses-shadow(less) # self-limiting(grep), bypasses-shadow(less)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# git
#
# SYNOPSIS # SYNOPSIS
# config-update [-h | --help] [-f | --force] [-n | --dry-run] # config-update [-h | --help] [-f | --force] [-n | --dry-run]
# #
+8
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 08-terminal-management # 08-terminal-management
# #
# DEPENDENCIES
# nohup
#
# SYNOPSIS # SYNOPSIS
# detach [-h] [--version] <command> [args...] # detach [-h] [--version] <command> [args...]
# #
@@ -66,5 +69,10 @@ function detach --description 'Execute detach'
return 1 return 1
end 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 & nohup $args >/dev/null 2>&1 &
end end
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 13-media-and-utilities # 13-media-and-utilities
# #
# DEPENDENCIES
# magick, ffmpeg, avifenc, exiftool
#
# CLASSIFICATION # CLASSIFICATION
# self-limiting(rm) # self-limiting(rm)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# docker
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/filesystem # aliases/filesystem
# #
# DEPENDENCIES
# duf, dust, dua
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(du) # bypasses-shadow(du)
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/dev-tools # aliases/dev-tools
# #
# DEPENDENCIES
# p, nano, nvim
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(rm) # bypasses-shadow(rm)
# #
+11
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 10-network # 10-network
# #
# DEPENDENCIES
# fast
#
# SYNOPSIS # SYNOPSIS
# fast-cli [args...] # fast-cli [args...]
# #
@@ -13,8 +16,16 @@
# ARGUMENTS # ARGUMENTS
# args... Arguments forwarded to the fast command # args... Arguments forwarded to the fast command
# #
# EXIT STATUS
# 1 fast is not installed
# * Exit status of fast otherwise
#
# EXAMPLE # EXAMPLE
# fast-cli # fast-cli
function fast-cli --description "Run a speed test using fast.com" 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 command fast $argv
end end
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# fastfetch, neofetch
#
# CLASSIFICATION # CLASSIFICATION
# uses-shadow(ls) # uses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 06-dependency-management # 06-dependency-management
# #
# DEPENDENCIES
# _fish_deps_status, _fish_deps_install, _fish_deps_update
#
# SYNOPSIS # SYNOPSIS
# fish-deps [status|install|update|sync] [--optional] [--terminals] [--all] # fish-deps [status|install|update|sync] [--optional] [--terminals] [--all]
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# overrides/prompt # overrides/prompt
# #
# DEPENDENCIES
# docker, starship
#
# SYNOPSIS # SYNOPSIS
# fish_right_prompt # 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
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 06-dependency-management # 06-dependency-management
# #
# DEPENDENCIES
# git, fzf
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+4
View File
@@ -4,6 +4,10 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# _fzf_search_directory, _fzf_search_git_log, _fzf_search_git_status,
# _fzf_search_history, _fzf_search_processes, _fzf_search_variables
#
# SYNOPSIS # SYNOPSIS
# fzf_configure_bindings [--directory=<key>] [--git_log=<key>] [--git_status=<key>] # fzf_configure_bindings [--directory=<key>] [--git_log=<key>] [--git_status=<key>]
# [--history=<key>] [--processes=<key>] [--variables=<key>] [-h] # [--history=<key>] [--processes=<key>] [--variables=<key>] [-h]
+118 -27
View File
@@ -4,41 +4,53 @@
# CATEGORY # CATEGORY
# 04-git-and-version-control # 04-git-and-version-control
# #
# DEPENDENCIES
# curl, md5sum, md5, gitignore-scrub
#
# CLASSIFICATION # CLASSIFICATION
# self-limiting(grep,cat), network, blocking-prompt # self-limiting(grep,cat), network, blocking-prompt
# #
# SYNOPSIS # SYNOPSIS
# gi [-h] [-b] [-p] [-s] [-l] [targets...] # gi [-h] [-b] [-p] [-o] [-s] [-f] [-c TEMPLATE] [-l] [targets...]
# #
# DESCRIPTION # DESCRIPTION
# Generates .gitignore content by querying the gitignore.io API. Appends # Generates .gitignore content by querying the gitignore.io API. Appends
# results to the repository's .gitignore with MD5-based deduplication — # results to the repository's .gitignore with MD5-based deduplication —
# patterns already present are not re-appended — or prints to stdout with # 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 # ARGUMENTS
# -h, --help Show help message # -h, --help Show help message
# -d, --description Show the function description # -d, --description Show the function description
# -l, --list List all supported targets from the API # -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 # -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 # targets Comma- or space-separated list of language/tool names
# #
# EXIT STATUS # 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 # 1 Not in a git repository or API fetch failed
# #
# RETURNS # 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. # With -l/--list, the supported target list, printed to stdout.
# #
# EXAMPLE # EXAMPLE
# gi python,venv # gi python,venv
# gi -b -p # 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' 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 or return 1
if set -q _flag_help if set -q _flag_help
@@ -53,9 +65,18 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
echo " $c_flag-h, --help $c_reset Show this help message" 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-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-l, --list $c_reset List all supported targets from the API"
echo " $c_flag-b, --boilerplate $c_reset Append boilerplate from $c_arg""\$GITIGNORE_BOILERPLATE$c_reset to .gitignore" 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-p, --prompt $c_reset Prompt for patterns and append them to .gitignore"
echo " $c_flag-s, --stdout $c_reset Print API output to stdout instead of appending 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 ""
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 ""
echo "$c_head""Examples:$c_reset" 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$c_reset $c_dim""# Append boilerplate and prompt for patterns (default)$c_reset"
@@ -63,7 +84,9 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
echo " $c_cmd""gi -p$c_reset $c_dim""# Prompt for patterns and append to .gitignore$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""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$c_reset $c_arg""python,venv$c_reset $c_dim""# Append Python+venv patterns to .gitignore$c_reset"
echo " $c_cmd""gi -s$c_reset $c_arg""python,venv$c_reset $c_dim""# Print Python+venv patterns to stdout$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" echo " $c_cmd""gi -l$c_reset | grep -i linux $c_dim""# Search for specific OS support$c_reset"
return 0 return 0
end end
@@ -78,11 +101,14 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
return 0 return 0
end end
set -l silent_flag 0
set -q _flag_silent; and set silent_flag 1
# Determine which modes to run # Determine which modes to run
set -l do_boilerplate 0 set -l do_boilerplate 0
set -l do_prompt 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 set do_boilerplate 1
end end
if set -q _flag_prompt if set -q _flag_prompt
@@ -95,15 +121,18 @@ function gi --description 'Generate .gitignore files using the gitignore.io API'
set do_prompt 1 set do_prompt 1
end 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 gitignore_path ""
set -l readable_path "" set -l readable_path ""
set -l needs_git 0 set -l needs_git 0
if not set -q _flag_stdout
if test $do_boilerplate -eq 1; or test $do_prompt -eq 1 if test $do_boilerplate -eq 1; or test $do_prompt -eq 1
set needs_git 1 set needs_git 1
else if set -q argv[1]; and not set -q _flag_stdout else if set -q argv[1]
set needs_git 1 set needs_git 1
end end
end
if test $needs_git -eq 1 if test $needs_git -eq 1
if not git rev-parse --is-inside-work-tree >/dev/null 2>&1 if not git rev-parse --is-inside-work-tree >/dev/null 2>&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) set readable_path (string replace -r "^$HOME" "~" $gitignore_path)
end 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 test $do_boilerplate -eq 1
if not set -q GITIGNORE_BOILERPLATE set -l boilerplate_path ""
set -l boilerplate_ok 1
if set -q _flag_custom
if test -f "$_flag_custom"
set boilerplate_path "$_flag_custom"
else
set_color red --bold set_color red --bold
echo "Error:" (set_color normal)"\$GITIGNORE_BOILERPLATE environment variable is not defined" >&2 echo "Error:" (set_color normal)"Custom boilerplate file not found at '$_flag_custom'" >&2
else if not test -f "$GITIGNORE_BOILERPLATE" 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 set_color red --bold
echo "Error:" (set_color normal)"Boilerplate file not found at '$GITIGNORE_BOILERPLATE'" >&2 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 else
set -l template_hash "" set -l template_hash ""
if command -q md5sum if command -q md5sum
set template_hash (md5sum "$GITIGNORE_BOILERPLATE" | string split ' ')[1] set template_hash (md5sum "$boilerplate_path" | string split ' ')[1]
else if command -q md5 else if command -q md5
set template_hash (md5 -q "$GITIGNORE_BOILERPLATE") set template_hash (md5 -q "$boilerplate_path")
end end
set -l sig "# id: gitig-boilerplate-$template_hash" set -l sig "# id: gitig-boilerplate-$template_hash"
if test -f "$gitignore_path"; and grep -qF "$sig" "$gitignore_path" if test -f "$gitignore_path"; and grep -qF "$sig" "$gitignore_path"
if not set -q _flag_silent
set_color yellow --bold set_color yellow --bold
echo "Notice:" (set_color normal)"Boilerplate already present in "(set_color cyan)"$readable_path"(set_color normal)"." echo "Notice:" (set_color normal)"Boilerplate already present in "(set_color cyan)"$readable_path"(set_color normal)"."
end
else else
printf "\n%s\n" "$sig" >>"$gitignore_path" printf "\n%s\n" "$sig" >>"$gitignore_path"
cat "$GITIGNORE_BOILERPLATE" >>"$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) echo (set_color green)"✔"(set_color normal)" Appended boilerplate to "(set_color cyan)"$readable_path"(set_color normal)
end end
end 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 if test $do_prompt -eq 1
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 read -P "Enter gitignore patterns (comma-separated, e.g. python,vim): " patterns
or return 0 or return 0
end
set patterns (string trim -- $patterns) set patterns (string trim -- $patterns)
if test -n "$patterns" if test -n "$patterns"
for pattern in (string split "," -- $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 echo "Error: Failed to fetch gitignore for '$pattern'. Is the target spelled correctly?" >&2
continue continue
end end
__gi_append_dedup "$content" "$pattern" "$gitignore_path" "$readable_path" if set -q _flag_stdout
end echo "$content"
else else
__gi_append_dedup "$content" "$pattern" "$gitignore_path" "$readable_path" $silent_flag
end
end
else if not set -q _flag_silent
echo (set_color brblack)"No patterns selected. Skipping API fetch."(set_color normal) echo (set_color brblack)"No patterns selected. Skipping API fetch."(set_color normal)
end end
test $needs_git -eq 1; and gitignore-scrub
return 0 return 0
end 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 echo "Error: Failed to fetch gitignore for '$target'. Is the target spelled correctly?" >&2
continue continue
end end
__gi_append_dedup "$content" "$target" "$gitignore_path" "$readable_path" __gi_append_dedup "$content" "$target" "$gitignore_path" "$readable_path" $silent_flag
end end
end end
end end
if test $needs_git -eq 1
gitignore-scrub
end
return 0
end end
# SYNOPSIS # SYNOPSIS
# __gi_append_dedup <content> <label> <gitignore_path> <readable_path> # __gi_append_dedup <content> <label> <gitignore_path> <readable_path> [silent]
# #
# DESCRIPTION # DESCRIPTION
# Appends gitignore content to a .gitignore file using MD5-based deduplication. # Appends gitignore content to a .gitignore file using MD5-based deduplication.
@@ -207,14 +292,16 @@ end
# label Human-readable label for the pattern set # label Human-readable label for the pattern set
# gitignore_path Absolute path to the .gitignore file # gitignore_path Absolute path to the .gitignore file
# readable_path Home-abbreviated path shown in output messages # readable_path Home-abbreviated path shown in output messages
# silent 1 to suppress progress output, 0/omitted to show it
# #
# EXAMPLE # EXAMPLE
# __gi_append_dedup "$content" "python" "$root/.gitignore" "~/.gitignore" # __gi_append_dedup "$content" "python" "$root/.gitignore" "~/.gitignore" 0
function __gi_append_dedup function __gi_append_dedup
set -l content $argv[1] set -l content $argv[1]
set -l label $argv[2] set -l label $argv[2]
set -l gitignore_path $argv[3] set -l gitignore_path $argv[3]
set -l readable_path $argv[4] set -l readable_path $argv[4]
set -l silent $argv[5]
set -l content_hash "" set -l content_hash ""
if command -q md5sum if command -q md5sum
@@ -226,10 +313,14 @@ function __gi_append_dedup
set -l sig "# id: gi-patterns-$content_hash" set -l sig "# id: gi-patterns-$content_hash"
if test -f "$gitignore_path"; and grep -qF "$sig" "$gitignore_path" if test -f "$gitignore_path"; and grep -qF "$sig" "$gitignore_path"
if test "$silent" != 1
set_color yellow --bold set_color yellow --bold
echo "Notice:" (set_color normal)"$label patterns already present in "(set_color cyan)"$readable_path"(set_color normal)"." echo "Notice:" (set_color normal)"$label patterns already present in "(set_color cyan)"$readable_path"(set_color normal)"."
end
else else
printf "\n%s\n%s\n" "$sig" "$content" >>"$gitignore_path" printf "\n%s\n%s\n" "$sig" "$content" >>"$gitignore_path"
if test "$silent" != 1
echo (set_color green)"✔"(set_color normal)" Appended $label patterns to "(set_color cyan)"$readable_path"(set_color normal) echo (set_color green)"✔"(set_color normal)" Appended $label patterns to "(set_color cyan)"$readable_path"(set_color normal)
end end
end
end end
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 10-network # 10-network
# #
# DEPENDENCIES
# curl
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 10-network # 10-network
# #
# DEPENDENCIES
# curl
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 10-network # 10-network
# #
# DEPENDENCIES
# curl
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 04-git-and-version-control # 04-git-and-version-control
# #
# DEPENDENCIES
# git
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+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 # CATEGORY
# 04-git-and-version-control # 04-git-and-version-control
# #
# DEPENDENCIES
# gitui
#
# SYNOPSIS # SYNOPSIS
# gitui [args...] # gitui [args...]
# #
@@ -14,9 +17,17 @@
# ARGUMENTS # ARGUMENTS
# args... Arguments forwarded to the gitui command # args... Arguments forwarded to the gitui command
# #
# EXIT STATUS
# 1 gitui is not installed
# * Exit status of gitui otherwise
#
# EXAMPLE # EXAMPLE
# gitui # gitui
function gitui --wraps='gitui' --description 'alias gitui=gitui -t mocha.ron' 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 end
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 04-git-and-version-control # 04-git-and-version-control
# #
# DEPENDENCIES
# git
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# joplin
#
# SYNOPSIS # SYNOPSIS
# joplin [args...] # joplin [args...]
# #
+4
View File
@@ -7,6 +7,10 @@
# COMPONENT # COMPONENT
# logging/terminal-capture # logging/terminal-capture
# #
# DEPENDENCIES
# kitty, __kitty_logging_dir, __kitty_logging_has_watcher,
# __kitty_logging_version
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(grep,mkdir,rm) # bypasses-shadow(grep,mkdir,rm)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ls) # bypasses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# docker, lazydocker
#
# SYNOPSIS # SYNOPSIS
# ld # ld
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/shell-tools # aliases/shell-tools
# #
# DEPENDENCIES
# ov, more
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(cat,less) # bypasses-shadow(cat,less)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 07-system-and-monitoring # 07-system-and-monitoring
# #
# DEPENDENCIES
# sbctl
#
# SYNOPSIS # SYNOPSIS
# limine-edit # limine-edit
# #
+10 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 07-system-and-monitoring # 07-system-and-monitoring
# #
# DEPENDENCIES
# loginctl
#
# SYNOPSIS # SYNOPSIS
# lock # lock
# #
@@ -11,12 +14,18 @@
# Locks the current desktop session using loginctl lock-session. # Locks the current desktop session using loginctl lock-session.
# #
# EXIT STATUS # EXIT STATUS
# Exit status of loginctl lock-session # 1 loginctl is not installed
# * Exit status of loginctl lock-session otherwise
# #
# EXAMPLE # EXAMPLE
# lock # lock
function lock --wraps='loginctl' --description 'alias lock=loginctl' function lock --wraps='loginctl' --description 'alias lock=loginctl'
__fish_help_header (status current-function) $argv; and return 0 __fish_help_header (status current-function) $argv; and return 0
if not type -q loginctl
echo (set_color red)"Error: loginctl is not installed."(set_color normal) >&2
return 1
end
loginctl lock-session loginctl lock-session
end end
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# integrations/history-logs # integrations/history-logs
# #
# DEPENDENCIES
# _scrollback_prune_junk, fzf, nvim, ov
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(cat), self-limiting(rm), network # bypasses-shadow(cat), self-limiting(rm), network
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/filesystem # aliases/filesystem
# #
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ls) # bypasses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ls) # bypasses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ls) # bypasses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ls) # bypasses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ls) # bypasses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ls) # bypasses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# eza, lsd
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ls) # bypasses-shadow(ls)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# _fish_mkdir_p
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(cd) # bypasses-shadow(cd)
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/filesystem # aliases/filesystem
# #
# DEPENDENCIES
# _fish_mkdir_p
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(mkdir) # bypasses-shadow(mkdir)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# xdg-mime, xdg-open
#
# SYNOPSIS # SYNOPSIS
# open-url [-s|--silent] [-v|--verbose] <url> # open-url [-s|--silent] [-v|--verbose] <url>
# open-url --help # open-url --help
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 05-package-management # 05-package-management
# #
# DEPENDENCIES
# fzf, pacman, paru, yay
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/network # aliases/network
# #
# DEPENDENCIES
# prettyping
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ping) # bypasses-shadow(ping)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 05-package-management # 05-package-management
# #
# DEPENDENCIES
# _fish_deps_detect_pm, pacman, paru, yay
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 13-media-and-utilities # 13-media-and-utilities
# #
# DEPENDENCIES
# _fzf_preview_media, _fzf_wrapper, fd, fdfind, file, xdg-mime, mpv, vlc
#
# SYNOPSIS # SYNOPSIS
# play-media [-p|--player <cmd>] # play-media [-p|--player <cmd>]
# play-media --help # play-media --help
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# _fish_mkdir_p
#
# SYNOPSIS # SYNOPSIS
# poke <file> [file...] # poke <file> [file...]
# #
+10 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 07-system-and-monitoring # 07-system-and-monitoring
# #
# DEPENDENCIES
# lsof
#
# SYNOPSIS # SYNOPSIS
# ports # ports
# #
@@ -12,12 +15,18 @@
# port numbers and addresses without hostname resolution. # port numbers and addresses without hostname resolution.
# #
# EXIT STATUS # EXIT STATUS
# Exit status of lsof # 1 lsof is not installed
# * Exit status of lsof otherwise
# #
# EXAMPLE # EXAMPLE
# ports # ports
function ports --wraps='sudo' --description 'Show active network listeners' function ports --wraps='sudo' --description 'Show active network listeners'
__fish_help_header (status current-function) $argv; and return 0 __fish_help_header (status current-function) $argv; and return 0
if not type -q lsof
echo (set_color red)"Error: lsof is not installed."(set_color normal) >&2
return 1
end
sudo lsof -iTCP -sTCP:LISTEN -P -n sudo lsof -iTCP -sTCP:LISTEN -P -n
end end
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 12-ai-and-developer-tools # 12-ai-and-developer-tools
# #
# DEPENDENCIES
# aichat
#
# SYNOPSIS # SYNOPSIS
# qc [prompt...] # qc [prompt...]
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 10-network # 10-network
# #
# DEPENDENCIES
# curl, qrencode
#
# CLASSIFICATION # CLASSIFICATION
# self-limiting(cat), network # self-limiting(cat), network
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# shuf
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(cat) # bypasses-shadow(cat)
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 03-editors-and-viewers # 03-editors-and-viewers
# #
# DEPENDENCIES
# fish
#
# SYNOPSIS # SYNOPSIS
# rawfish [args...] # rawfish [args...]
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/filesystem # aliases/filesystem
# #
# DEPENDENCIES
# trash
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(rm), destructive # bypasses-shadow(rm), destructive
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 07-system-and-monitoring # 07-system-and-monitoring
# #
# DEPENDENCIES
# sbctl
#
# CLASSIFICATION # CLASSIFICATION
# self-limiting(grep) # self-limiting(grep)
# #
+10 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 07-system-and-monitoring # 07-system-and-monitoring
# #
# DEPENDENCIES
# busctl
#
# SYNOPSIS # SYNOPSIS
# screensleep # screensleep
# #
@@ -12,13 +15,19 @@
# PowerDevil "Turn Off Screen" global shortcut via busctl. # PowerDevil "Turn Off Screen" global shortcut via busctl.
# #
# EXIT STATUS # EXIT STATUS
# Exit status of busctl # 1 busctl is not installed
# * Exit status of busctl otherwise
# #
# EXAMPLE # EXAMPLE
# screensleep # screensleep
function screensleep --description 'Turn off the display using KDE PowerDevil' function screensleep --description 'Turn off the display using KDE PowerDevil'
__fish_help_header (status current-function) $argv; and return 0 __fish_help_header (status current-function) $argv; and return 0
if not type -q busctl
echo (set_color red)"Error: busctl is not installed."(set_color normal) >&2
return 1
end
# Optional: 1-second delay to ensure no keystrokes wake it immediately # Optional: 1-second delay to ensure no keystrokes wake it immediately
sleep 1 sleep 1
busctl --user call \ busctl --user call \
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 01-file-and-directory # 01-file-and-directory
# #
# DEPENDENCIES
# fd, trash
#
# CLASSIFICATION # CLASSIFICATION
# uses-shadow(rm), bypasses-shadow(rm), destructive # uses-shadow(rm), bypasses-shadow(rm), destructive
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 05-package-management # 05-package-management
# #
# DEPENDENCIES
# paru, yay
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -8,6 +8,9 @@
# site exit-plain: overrides/key-bindings # site exit-plain: overrides/key-bindings
# site logging-guard: logging/terminal-capture # site logging-guard: logging/terminal-capture
# #
# DEPENDENCIES
# kitty, ps, _scrollback_prune_junk
#
# CLASSIFICATION # CLASSIFICATION
# self-limiting(rm,mkdir), destructive # self-limiting(rm,mkdir), destructive
# #
+14
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# integrations/window-mgmt # integrations/window-mgmt
# #
# DEPENDENCIES
# kitty, wezterm
#
# SYNOPSIS # SYNOPSIS
# split [-h | -v] [command...] # split [-h | -v] [command...]
# #
@@ -52,6 +55,17 @@ function split --description 'Run a command in a new terminal split'
return 1 return 1
end end
# $TERM/$TERM_PROGRAM only prove the terminal type, not that its CLI
# binary is on $PATH -- e.g. sshing out from Kitty/WezTerm inherits the
# env var on the remote host without the binary. Check explicitly.
if test $is_kitty -eq 1; and not type -q kitty
echo "Error: 'split' detected Kitty but the kitty binary is not installed." >&2
return 1
else if test $is_wezterm -eq 1; and not type -q wezterm
echo "Error: 'split' detected WezTerm but the wezterm binary is not installed." >&2
return 1
end
set -l kitty_loc hsplit set -l kitty_loc hsplit
set -l wez_loc --bottom set -l wez_loc --bottom
+14 -1
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# integrations/window-mgmt # integrations/window-mgmt
# #
# DEPENDENCIES
# kitty, wezterm
#
# SYNOPSIS # SYNOPSIS
# spwin [args...] # spwin [args...]
# #
@@ -33,13 +36,23 @@ function spwin --wraps='~/.config/kitty/spawn-window.sh' --description 'spawn wi
return 1 return 1
end end
# $TERM/$TERM_PROGRAM only prove the terminal type, not that its CLI
# binary is on $PATH -- e.g. sshing out from Kitty/WezTerm inherits the
# env var on the remote host without the binary. Check explicitly.
if test "$TERM" = xterm-kitty if test "$TERM" = xterm-kitty
if test -x ~/.config/kitty/spawn-window.sh if test -x ~/.config/kitty/spawn-window.sh
~/.config/kitty/spawn-window.sh $argv ~/.config/kitty/spawn-window.sh $argv
else else if type -q kitty
kitty @ launch --type=window $argv kitty @ launch --type=window $argv
else
echo "Error: 'spwin' detected Kitty but neither spawn-window.sh nor the kitty binary is available." >&2
return 1
end end
else if test "$TERM_PROGRAM" = WezTerm else if test "$TERM_PROGRAM" = WezTerm
if not type -q wezterm
echo "Error: 'spwin' detected WezTerm but the wezterm binary is not installed." >&2
return 1
end
wezterm cli spawn $argv wezterm cli spawn $argv
else else
echo "Error: The 'spwin' command requires Kitty or WezTerm." >&2 echo "Error: The 'spwin' command requires Kitty or WezTerm." >&2
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/network # aliases/network
# #
# DEPENDENCIES
# kitten
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(ssh), network # bypasses-shadow(ssh), network
# #
+12 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 13-media-and-utilities # 13-media-and-utilities
# #
# DEPENDENCIES
# systemd-inhibit, steam
#
# SYNOPSIS # SYNOPSIS
# steam-dl # steam-dl
# #
@@ -12,13 +15,21 @@
# or sleeping during active downloads. # or sleeping during active downloads.
# #
# EXIT STATUS # EXIT STATUS
# Exit status of steam (via systemd-inhibit) # 1 systemd-inhibit or steam is not installed
# * Exit status of steam (via systemd-inhibit) otherwise
# #
# EXAMPLE # EXAMPLE
# steam-dl # steam-dl
function steam-dl --description 'Run Steam while inhibiting system sleep' function steam-dl --description 'Run Steam while inhibiting system sleep'
__fish_help_header (status current-function) $argv; and return 0 __fish_help_header (status current-function) $argv; and return 0
for cmd in systemd-inhibit steam
if not type -q $cmd
echo (set_color red)"Error: $cmd is not installed."(set_color normal) >&2
return 1
end
end
echo "Inhibiting sleep while Steam downloads..." echo "Inhibiting sleep while Steam downloads..."
systemd-inhibit --why="Active Download" --who="User" --what=idle:sleep steam systemd-inhibit --why="Active Download" --who="User" --what=idle:sleep steam
end end
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 12-ai-and-developer-tools # 12-ai-and-developer-tools
# #
# DEPENDENCIES
# agy
#
# CLASSIFICATION # CLASSIFICATION
# uses-shadow(claude) # uses-shadow(claude)
# #
+19
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# integrations/window-mgmt # integrations/window-mgmt
# #
# DEPENDENCIES
# kitty, wezterm, konsole
#
# SYNOPSIS # SYNOPSIS
# tab [args...] # tab [args...]
# #
@@ -39,11 +42,27 @@ function tab --description 'Spawn a new tab in the current terminal'
set dir "$PWD" set dir "$PWD"
end end
# $TERM/$TERM_PROGRAM/$KONSOLE_VERSION only prove the terminal type,
# not that its CLI binary is on $PATH -- e.g. sshing out from one of
# these inherits the env var on the remote host without the binary.
# Check explicitly.
if test "$TERM" = xterm-kitty if test "$TERM" = xterm-kitty
if not type -q kitty
echo "Error: 'tab' detected Kitty but the kitty binary is not installed." >&2
return 1
end
kitty @ launch --type=tab --cwd="$dir" $argv kitty @ launch --type=tab --cwd="$dir" $argv
else if test "$TERM_PROGRAM" = WezTerm else if test "$TERM_PROGRAM" = WezTerm
if not type -q wezterm
echo "Error: 'tab' detected WezTerm but the wezterm binary is not installed." >&2
return 1
end
wezterm cli spawn --cwd "$dir" $argv wezterm cli spawn --cwd "$dir" $argv
else if set -q KONSOLE_VERSION else if set -q KONSOLE_VERSION
if not type -q konsole
echo "Error: 'tab' detected Konsole but the konsole binary is not installed." >&2
return 1
end
konsole --new-tab --workdir "$dir" $argv konsole --new-tab --workdir "$dir" $argv
else else
echo "Error: No supported terminal found. Try Kitty, WezTerm, or Konsole." >&2 echo "Error: No supported terminal found. Try Kitty, WezTerm, or Konsole." >&2
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 14-miscellaneous # 14-miscellaneous
# #
# DEPENDENCIES
# tmux
#
# SYNOPSIS # SYNOPSIS
# tmux-clean # tmux-clean
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# aliases/monitor # aliases/monitor
# #
# DEPENDENCIES
# btop
#
# CLASSIFICATION # CLASSIFICATION
# bypasses-shadow(top) # bypasses-shadow(top)
# #
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT # COMPONENT
# integrations/pkg-upgrade # integrations/pkg-upgrade
# #
# DEPENDENCIES
# paru, yay
#
# CLASSIFICATION # CLASSIFICATION
# network # network
# #
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY # CATEGORY
# 03-editors-and-viewers # 03-editors-and-viewers
# #
# DEPENDENCIES
# nvim
#
# CLASSIFICATION # CLASSIFICATION
# uses-shadow(less) # uses-shadow(less)
# #

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