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