Add doubynet skill and extend check_versions.py to cover installed skills
This commit is contained in:
@@ -7,6 +7,32 @@ ships documents, so it uses the same `v#` scheme as the documents themselves.
|
|||||||
Individual guideline documents carry their own `Document Version` header, and the skills carry
|
Individual guideline documents carry their own `Document Version` header, and the skills carry
|
||||||
`metadata.version`; both are independent of the revisions below.
|
`metadata.version`; both are independent of the revisions below.
|
||||||
|
|
||||||
|
## v9 — 2026-10-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `Claude/skills/doubynet/` — new personal skill, reconciles the DoubyNet home-network
|
||||||
|
documentation (a separate Syncthing-synced tree, not part of this repository) against
|
||||||
|
the live state of its hosts, reports discrepancies, and updates the affected `.md`
|
||||||
|
files. Versioned `v1`, following the established `metadata.version` convention. Was
|
||||||
|
already in use directly from `~/.claude/skills/doubynet/` without a master copy here;
|
||||||
|
brought in after a session surfaced several gaps (stale hardcoded path, unclear
|
||||||
|
historical-vs-live document handling, no mention of CHANGELOG.md, no guidance for
|
||||||
|
facts that can't be verified from a machine at all) that were fixed in the same pass
|
||||||
|
- `check_versions.py` now also checks the master skills in `Claude/skills/<name>/SKILL.md`
|
||||||
|
against their installed copies in `~/.claude/skills/<name>/SKILL.md` — a fixed location,
|
||||||
|
independent of `--root`. Reads `metadata.version` from the YAML frontmatter instead of
|
||||||
|
the `**Document Version:**` marker used by guideline documents. A skill missing from
|
||||||
|
`~/.claude/skills/` entirely is reported as a new status, `MISSING`, and is safe to
|
||||||
|
`--sync` like `BEHIND`. Prompted by running the tool for the first time since `doubynet`
|
||||||
|
was added: it found four of the five pre-existing skills installed without any version
|
||||||
|
header at all, and `documentation_context` missing from `~/.claude/skills/` entirely
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `--sync`/report wording generalised from "document(s)" to "item(s)" now that the same
|
||||||
|
machinery covers both guideline documents and skills
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v8 — 2026-09-29
|
## v8 — 2026-09-29
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
name: doubynet
|
||||||
|
description: Update doubynet dokumentation
|
||||||
|
metadata:
|
||||||
|
version: v1
|
||||||
|
---
|
||||||
|
|
||||||
|
Tvým úkolem je zkontrolovat a případně aktualizovat dokumentaci sítě DoubyNet.
|
||||||
|
|
||||||
|
## 1. Načti dokumentaci
|
||||||
|
`/home/honza/Syncthing/Programy/DoubyNet/` (lokální, Syncthing-synchronizovaná
|
||||||
|
složka na Honza-PCL — žádný manuální upload/download, synchronizace na ostatní
|
||||||
|
zařízení běží na pozadí sama):
|
||||||
|
- `README.md` (index služeb + sekce "Router — port-forwarding")
|
||||||
|
- `CLAUDE.md` (konvence zápisu dokumentace — dodržuj ji při jakékoli úpravě)
|
||||||
|
- všechny `<Hostname>/<Služba>/<Služba>.md` soubory
|
||||||
|
- `<Hostname>/CHANGELOG.md` (jeden na hosta, průběžný záznam změn — viz krok 4)
|
||||||
|
|
||||||
|
Vytvoř si z toho kompletní obraz zdokumentovaného stavu: hosty, služby, jak se spouští,
|
||||||
|
sítě/porty, VM, autostart, propojení mezi službami (např. Caddy → cíl).
|
||||||
|
|
||||||
|
**Pozor na historické dokumenty.** Některé `.md` soubory popisují něco, co už
|
||||||
|
neexistuje (např. staré, fyzicky rozebrané disky/pole), a mají to nahoře jasně
|
||||||
|
označené (banner typu "⚠️ pole už neexistuje, dokument je od teď čistě
|
||||||
|
historický"). Takový dokument **needituj aktuálními fakty** — zjištěné aktuální
|
||||||
|
informace patří do dokumentu, který nahrazuje popisovanou věc (banner na to
|
||||||
|
obvykle přímo odkazuje). Smysl historického dokumentu je zůstat nezměněný jako
|
||||||
|
záznam toho, co bylo.
|
||||||
|
|
||||||
|
## 2. Ověř realitu
|
||||||
|
Pro každé tvrzení v dokumentaci ověř skutečný stav na příslušném stroji (SSH, `systemctl`,
|
||||||
|
`virsh`, `ss -tlnp`, obsah configů apod.). Ověřuj z relevantního hlediska — např. dostupnost
|
||||||
|
webu přes DNAT/reverse proxy testuj z jiného stroje než je cíl (self-test ze stejného hostu
|
||||||
|
může dát falešně negativní výsledek kvůli tomu, jak netfilter zachází s lokálně generovaným
|
||||||
|
provozem).
|
||||||
|
|
||||||
|
**Některá fakta nejdou ověřit žádným příkazem** — typicky cokoliv, co žije jen v UI
|
||||||
|
zařízení bez API/SSH přístupu (např. nastavení port-forwardingu na routeru). V
|
||||||
|
takovém případě si o autoritativní export/screenshot řekni uživateli přímo, místo
|
||||||
|
abys realitu odhadoval nebo nechal needokumentovanou.
|
||||||
|
|
||||||
|
Neprováděj destruktivní ani produkci ovlivňující příkazy bez potvrzení uživatele. Pokud
|
||||||
|
narazíš na to, že nemáš potřebná oprávnění (sudo, SSH klíč), zeptej se uživatele, jak
|
||||||
|
pokračovat — nezkoušej to obcházet.
|
||||||
|
|
||||||
|
## 3. Porovnej a nahlas rozdíly
|
||||||
|
Sestav přehledný seznam nesrovnalostí mezi dokumentací a realitou (co dokument tvrdí vs.
|
||||||
|
co je skutečně nastavené/běžící). Tenhle přehled ukaž uživateli **před** jakoukoli úpravou
|
||||||
|
souborů.
|
||||||
|
|
||||||
|
## 4. Aktualizuj dokumentaci
|
||||||
|
Až uživatel potvrdí (nebo pokud jde o triviální/jednoznačné doplnění), uprav příslušné
|
||||||
|
`.md` soubory tak, aby odpovídaly realitě:
|
||||||
|
- Dodržuj šablonu a konvence z `CLAUDE.md` (formát tabulek, PascalCase názvy, atd.)
|
||||||
|
- Nikdy nezapisuj hesla/tokeny/klíče v plaintextu — nahraď je `<PLACEHOLDER>` tokenem
|
||||||
|
- Pokud přidáváš novou službu/VM, přidej ji i do řádku v `README.md`
|
||||||
|
- Fakta, co se netýkají jedné služby na jednom hostu (např. síťová topologie,
|
||||||
|
port-forwarding na routeru), patří do vlastní sekce v `README.md` — nenuť je
|
||||||
|
do složky jedné služby jen proto, že model "jedna složka = jedna služba" jinam
|
||||||
|
nesedí
|
||||||
|
- Zapiš stručný záznam do `<Hostname>/CHANGELOG.md` dotčeného hosta (nejnovější
|
||||||
|
nahoře) — co se zjistilo, co se změnilo, proč
|
||||||
|
- Po úpravách ověř, že žádný relativní markdown odkaz (`](...)`) nevede na
|
||||||
|
neexistující soubor — procházka stromem a kontrola existence cíle každého odkazu
|
||||||
|
|
||||||
|
Žádný manuální "upload" na server není potřeba — soubory žijí lokálně v Syncthing
|
||||||
|
složce, synchronizace běží sama.
|
||||||
|
|
||||||
|
Na konci stručně shrň, co bylo nekonzistentní a co jsi opravil.
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@ Dokumentace/
|
|||||||
│ ├── check/ # /check — run tests, investigate failures
|
│ ├── check/ # /check — run tests, investigate failures
|
||||||
│ ├── commit/ # /commit — changelog + commit message proposal
|
│ ├── commit/ # /commit — changelog + commit message proposal
|
||||||
│ ├── documentation/ # /documentation — write docs/
|
│ ├── documentation/ # /documentation — write docs/
|
||||||
│ └── documentation_context/ # /documentation_context — compress docs/ into CONTEXT.md
|
│ ├── documentation_context/ # /documentation_context — compress docs/ into CONTEXT.md
|
||||||
|
│ └── doubynet/ # /doubynet — reconcile DoubyNet network docs with reality
|
||||||
│
|
│
|
||||||
├── Python/ # Python development guidelines
|
├── Python/ # Python development guidelines
|
||||||
│ ├── DESIGN_DOCUMENT_PYTHON.md # Guidelines for Python applications
|
│ ├── DESIGN_DOCUMENT_PYTHON.md # Guidelines for Python applications
|
||||||
|
|||||||
+145
-28
@@ -1,18 +1,25 @@
|
|||||||
"""Report sibling projects whose copies of the guideline documents are out of date, and offer to sync them.
|
"""Report stale copies of this repository's guideline documents and skills, and offer to sync them.
|
||||||
|
|
||||||
This repository is the source of truth for ``CLAUDE.md``, ``AGENTS.md`` and the
|
This repository is the source of truth for two independent things, both checked by this script:
|
||||||
``DESIGN_DOCUMENT*.md`` family. Every project keeps a committed copy in its root, so the copies
|
|
||||||
drift as soon as a document here is bumped. This script walks the sibling directories of this
|
|
||||||
repository, reads the ``Document Version`` header of every copy it finds and reports the ones that
|
|
||||||
are behind, missing, or modified locally.
|
|
||||||
|
|
||||||
It also finds copies still using a pre-rename file name (``DESIGN_DOCUMENT.md`` and friends, from
|
1. ``CLAUDE.md``, ``AGENTS.md`` and the ``DESIGN_DOCUMENT*.md`` family. Every project keeps a
|
||||||
before the language became part of the name) and reports them as ``OLD NAME``.
|
committed copy in its root, so the copies drift as soon as a document here is bumped. This part
|
||||||
|
walks the sibling directories of this repository (or ``--root``), reads the ``Document Version``
|
||||||
|
header of every copy it finds and reports the ones that are behind, missing, or modified locally.
|
||||||
|
2. The personal skills under ``Claude/skills/<name>/SKILL.md``, whose installed copies Claude Code
|
||||||
|
actually runs from ``~/.claude/skills/<name>/SKILL.md`` — a fixed location, independent of
|
||||||
|
``--root``. Versions live in the YAML frontmatter as ``metadata.version``. A skill missing from
|
||||||
|
``~/.claude/skills/`` entirely is reported as ``MISSING`` and treated like ``BEHIND``: both are
|
||||||
|
safe to install/overwrite with ``--sync``.
|
||||||
|
|
||||||
``--sync`` offers to overwrite the outdated copies with the master version, renaming the old names on
|
It also finds guideline copies still using a pre-rename file name (``DESIGN_DOCUMENT.md`` and
|
||||||
the way. Only ``BEHIND`` and ``OLD NAME`` copies are ever touched: a ``MODIFIED`` copy holds local
|
friends, from before the language became part of the name) and reports them as ``OLD NAME``.
|
||||||
edits that overwriting would discard, an unversioned copy cannot be compared, and ``DUPLICATE`` means
|
|
||||||
both the old and the new name exist, which needs a human to decide.
|
``--sync`` offers to overwrite the outdated copies with the master version (installing missing
|
||||||
|
skills fresh), renaming the old guideline names on the way. Only ``BEHIND``, ``MISSING`` and
|
||||||
|
``OLD NAME`` copies are ever touched: a ``MODIFIED`` copy holds local edits that overwriting would
|
||||||
|
discard, an unversioned copy cannot be compared, and ``DUPLICATE`` means both the old and the new
|
||||||
|
guideline name exist, which needs a human to decide.
|
||||||
|
|
||||||
Standard library only, so it runs without a virtual environment:
|
Standard library only, so it runs without a virtual environment:
|
||||||
|
|
||||||
@@ -20,8 +27,8 @@ Standard library only, so it runs without a virtual environment:
|
|||||||
python check_versions.py --sync # report, then offer to copy the outdated ones
|
python check_versions.py --sync # report, then offer to copy the outdated ones
|
||||||
python check_versions.py --sync --yes # copy without asking (for scripts)
|
python check_versions.py --sync --yes # copy without asking (for scripts)
|
||||||
python check_versions.py --sync --dry-run # show what --sync would copy, change nothing
|
python check_versions.py --sync --dry-run # show what --sync would copy, change nothing
|
||||||
python check_versions.py --root .. # scan a different directory
|
python check_versions.py --root .. # scan a different directory for guideline copies
|
||||||
python check_versions.py --quiet # print only projects needing attention
|
python check_versions.py --quiet # print only projects/skills needing attention
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -64,12 +71,21 @@ LEGACY_RUST_NAMES: dict[str, str] = {"DESIGN_DOCUMENT.md": "DESIGN_DOCUMENT_RUST
|
|||||||
|
|
||||||
VERSION_PATTERN = re.compile(r"^\*\*Document Version:\*\*\s*(v\d+)", re.MULTILINE)
|
VERSION_PATTERN = re.compile(r"^\*\*Document Version:\*\*\s*(v\d+)", re.MULTILINE)
|
||||||
|
|
||||||
|
# Where the master skills live in this repository, and where Claude Code actually runs them from.
|
||||||
|
# The installed location is fixed - unlike guideline copies, it does not move with --root.
|
||||||
|
SKILLS_SUBDIR = "Claude/skills"
|
||||||
|
INSTALLED_SKILLS_DIR = Path.home() / ".claude" / "skills"
|
||||||
|
|
||||||
|
FRONTMATTER_PATTERN = re.compile(r"\A---\n(.*?)\n---", re.DOTALL)
|
||||||
|
SKILL_VERSION_PATTERN = re.compile(r"^\s*version:\s*(v\d+)\s*$", re.MULTILINE)
|
||||||
|
|
||||||
STATUS_OK = "ok"
|
STATUS_OK = "ok"
|
||||||
STATUS_BEHIND = "behind"
|
STATUS_BEHIND = "behind"
|
||||||
STATUS_MODIFIED = "modified"
|
STATUS_MODIFIED = "modified"
|
||||||
STATUS_UNVERSIONED = "unversioned"
|
STATUS_UNVERSIONED = "unversioned"
|
||||||
STATUS_LEGACY = "legacy"
|
STATUS_LEGACY = "legacy"
|
||||||
STATUS_DUPLICATE = "duplicate"
|
STATUS_DUPLICATE = "duplicate"
|
||||||
|
STATUS_MISSING = "missing"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -91,8 +107,8 @@ class Result:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def can_sync(self) -> bool:
|
def can_sync(self) -> bool:
|
||||||
"""Behind and legacy copies can be replaced; anything else may hold work we would destroy."""
|
"""Behind, missing and legacy copies can be replaced; anything else may hold work we would destroy."""
|
||||||
return self.status in {STATUS_BEHIND, STATUS_LEGACY}
|
return self.status in {STATUS_BEHIND, STATUS_LEGACY, STATUS_MISSING}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_rename(self) -> bool:
|
def is_rename(self) -> bool:
|
||||||
@@ -115,6 +131,19 @@ def read_version(path: Path) -> str | None:
|
|||||||
return match.group(1) if match else None
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def read_skill_version(path: Path) -> str | None:
|
||||||
|
"""Return the ``metadata.version`` declared in a skill's YAML frontmatter, if any.
|
||||||
|
|
||||||
|
Deliberately restricted to the frontmatter block (between the first pair of ``---`` lines) so
|
||||||
|
the word "version" appearing in a skill's instructions is never mistaken for the real field.
|
||||||
|
"""
|
||||||
|
frontmatter = FRONTMATTER_PATTERN.match(read_text(path))
|
||||||
|
if not frontmatter:
|
||||||
|
return None
|
||||||
|
match = SKILL_VERSION_PATTERN.search(frontmatter.group(1))
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
def version_key(version: str | None) -> int:
|
def version_key(version: str | None) -> int:
|
||||||
"""Order versions numerically; a missing version sorts lowest."""
|
"""Order versions numerically; a missing version sorts lowest."""
|
||||||
return int(version[1:]) if version else -1
|
return int(version[1:]) if version else -1
|
||||||
@@ -160,6 +189,21 @@ def sync_document(master: Path, copy: Path, target: Path) -> None:
|
|||||||
backup.unlink()
|
backup.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def install_or_sync(master: Path, copy: Path, target: Path) -> None:
|
||||||
|
"""Install a missing file fresh, or overwrite an outdated one via :func:`sync_document`.
|
||||||
|
|
||||||
|
A missing skill has no existing copy to detect line endings from or back up, so it is simply
|
||||||
|
written out using the master's own line endings instead of going through the backup/restore
|
||||||
|
dance that only makes sense when a previous version is there to protect.
|
||||||
|
"""
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if copy.is_file():
|
||||||
|
sync_document(master, copy, target)
|
||||||
|
else:
|
||||||
|
newline = detect_newline(master)
|
||||||
|
target.write_text(normalise(read_text(master)) + "\n", encoding="utf-8", newline=newline)
|
||||||
|
|
||||||
|
|
||||||
def load_masters(repo: Path) -> dict[str, tuple[Path, str | None]]:
|
def load_masters(repo: Path) -> dict[str, tuple[Path, str | None]]:
|
||||||
"""Map each document name to its master path and version."""
|
"""Map each document name to its master path and version."""
|
||||||
masters: dict[str, tuple[Path, str | None]] = {}
|
masters: dict[str, tuple[Path, str | None]] = {}
|
||||||
@@ -224,6 +268,51 @@ def check_legacy(project: Path, masters: dict[str, tuple[Path, str | None]]) ->
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def find_skill_masters(repo: Path) -> dict[str, tuple[Path, str | None]]:
|
||||||
|
"""Map each skill name (a folder under ``Claude/skills/``) to its master ``SKILL.md`` and version."""
|
||||||
|
masters: dict[str, tuple[Path, str | None]] = {}
|
||||||
|
skills_dir = repo / SKILLS_SUBDIR
|
||||||
|
if not skills_dir.is_dir():
|
||||||
|
return masters
|
||||||
|
for entry in sorted(skills_dir.iterdir()):
|
||||||
|
skill_file = entry / "SKILL.md"
|
||||||
|
if entry.is_dir() and skill_file.is_file():
|
||||||
|
masters[entry.name] = (skill_file, read_skill_version(skill_file))
|
||||||
|
return masters
|
||||||
|
|
||||||
|
|
||||||
|
def check_skills(masters: dict[str, tuple[Path, str | None]], installed_dir: Path) -> list[Result]:
|
||||||
|
"""Compare every master skill against its installed copy in ``~/.claude/skills/``."""
|
||||||
|
results: list[Result] = []
|
||||||
|
|
||||||
|
for name, (master_path, master_version) in masters.items():
|
||||||
|
installed_path = installed_dir / name / "SKILL.md"
|
||||||
|
|
||||||
|
if not installed_path.is_file():
|
||||||
|
results.append(
|
||||||
|
Result(name, "SKILL.md", None, master_version, STATUS_MISSING, installed_path, master_path, installed_path)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
installed_version = read_skill_version(installed_path)
|
||||||
|
|
||||||
|
if installed_version is None or master_version is None:
|
||||||
|
status = STATUS_UNVERSIONED
|
||||||
|
elif version_key(installed_version) < version_key(master_version):
|
||||||
|
status = STATUS_BEHIND
|
||||||
|
elif normalise(read_text(installed_path)) != normalise(read_text(master_path)):
|
||||||
|
# Same version number but different content - someone edited the installed copy in place.
|
||||||
|
status = STATUS_MODIFIED
|
||||||
|
else:
|
||||||
|
status = STATUS_OK
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
Result(name, "SKILL.md", installed_version, master_version, status, installed_path, master_path, installed_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def find_projects(root: Path, repo: Path) -> list[Path]:
|
def find_projects(root: Path, repo: Path) -> list[Path]:
|
||||||
"""Return candidate project directories: siblings of this repository, excluding itself."""
|
"""Return candidate project directories: siblings of this repository, excluding itself."""
|
||||||
return sorted(
|
return sorted(
|
||||||
@@ -241,6 +330,7 @@ def format_row(result: Result) -> str:
|
|||||||
STATUS_UNVERSIONED: "NO VER. ",
|
STATUS_UNVERSIONED: "NO VER. ",
|
||||||
STATUS_LEGACY: "OLD NAME ",
|
STATUS_LEGACY: "OLD NAME ",
|
||||||
STATUS_DUPLICATE: "DUPLICATE",
|
STATUS_DUPLICATE: "DUPLICATE",
|
||||||
|
STATUS_MISSING: "MISSING ",
|
||||||
}[result.status]
|
}[result.status]
|
||||||
versions = f"{result.project_version or '-'} -> {result.master_version or '-'}"
|
versions = f"{result.project_version or '-'} -> {result.master_version or '-'}"
|
||||||
return f" {marker} {result.document:<50} {versions}"
|
return f" {marker} {result.document:<50} {versions}"
|
||||||
@@ -259,14 +349,14 @@ def confirm(prompt: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def run_sync(pending: list[Result], assume_yes: bool, dry_run: bool) -> int:
|
def run_sync(pending: list[Result], assume_yes: bool, dry_run: bool) -> int:
|
||||||
"""Copy the masters over the outdated project copies. Returns the number of documents left stale."""
|
"""Copy the masters over outdated or missing copies. Returns the number of items left stale."""
|
||||||
renames = sum(1 for result in pending if result.is_rename)
|
renames = sum(1 for result in pending if result.is_rename)
|
||||||
print(f"{len(pending)} document(s) can be updated from this repository:")
|
print(f"{len(pending)} item(s) can be updated from this repository:")
|
||||||
labels = {result: f"{result.project}/{result.document}" for result in pending}
|
labels = {result: f"{result.project}/{result.document}" for result in pending}
|
||||||
width = max(len(label) for label in labels.values())
|
width = max(len(label) for label in labels.values())
|
||||||
for result in pending:
|
for result in pending:
|
||||||
note = " (rename)" if result.is_rename else ""
|
note = " (rename)" if result.is_rename else ""
|
||||||
print(f" {labels[result]:<{width}} {result.project_version} -> {result.master_version}{note}")
|
print(f" {labels[result]:<{width}} {result.project_version or '-'} -> {result.master_version}{note}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
@@ -283,17 +373,23 @@ def run_sync(pending: list[Result], assume_yes: bool, dry_run: bool) -> int:
|
|||||||
failed = 0
|
failed = 0
|
||||||
for result in pending:
|
for result in pending:
|
||||||
try:
|
try:
|
||||||
sync_document(result.master_path, result.copy_path, result.target_path)
|
install_or_sync(result.master_path, result.copy_path, result.target_path)
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
print(f" FAILED {result.project}/{result.document}: {error}", file=sys.stderr)
|
print(f" FAILED {result.project}/{result.document}: {error}", file=sys.stderr)
|
||||||
failed += 1
|
failed += 1
|
||||||
else:
|
else:
|
||||||
verb = "renamed" if result.is_rename else "updated"
|
if result.is_rename:
|
||||||
|
verb = "renamed"
|
||||||
|
elif result.status == STATUS_MISSING:
|
||||||
|
verb = "installed"
|
||||||
|
else:
|
||||||
|
verb = "updated"
|
||||||
print(f" {verb} {result.project}/{result.target_path.name} -> {result.master_version}")
|
print(f" {verb} {result.project}/{result.target_path.name} -> {result.master_version}")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"{len(pending) - failed} document(s) updated." if not failed else f"{failed} document(s) failed to update.")
|
print(f"{len(pending) - failed} item(s) updated." if not failed else f"{failed} item(s) failed to update.")
|
||||||
print("Review and commit the changes in each project (a sync is its own commit).")
|
print("Review and commit the changes (a sync is its own commit; for a newly-installed skill,")
|
||||||
|
print("there is nothing in ~/.claude/skills/ to commit, but consider committing it here too if new).")
|
||||||
return failed
|
return failed
|
||||||
|
|
||||||
|
|
||||||
@@ -349,8 +445,29 @@ def main() -> int:
|
|||||||
print(f"No guideline documents: {', '.join(unmanaged)}")
|
print(f"No guideline documents: {', '.join(unmanaged)}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
# Second, independent check: master skills in Claude/skills/ vs. what is actually installed in
|
||||||
|
# ~/.claude/skills/. Unlike guideline copies this location is fixed, not relative to --root.
|
||||||
|
skill_masters = find_skill_masters(repo)
|
||||||
|
if skill_masters:
|
||||||
|
print(f"Skill masters in {repo.name}/{SKILLS_SUBDIR}:")
|
||||||
|
width = max(len(name) for name in skill_masters)
|
||||||
|
for name, (_, version) in sorted(skill_masters.items()):
|
||||||
|
print(f" {name:<{width}} {version or '(no version header)'}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
skill_results = check_skills(skill_masters, INSTALLED_SKILLS_DIR)
|
||||||
|
stale_skills = [result for result in skill_results if result.needs_attention]
|
||||||
|
attention.extend(stale_skills)
|
||||||
|
|
||||||
|
if not (args.quiet and not stale_skills):
|
||||||
|
print(f"Installed in {INSTALLED_SKILLS_DIR}:")
|
||||||
|
for result in skill_results if not args.quiet else stale_skills:
|
||||||
|
print(f"{result.project}")
|
||||||
|
print(format_row(result))
|
||||||
|
print()
|
||||||
|
|
||||||
if not attention:
|
if not attention:
|
||||||
print("All projects are up to date.")
|
print("Everything is up to date.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
syncable = [result for result in attention if result.can_sync]
|
syncable = [result for result in attention if result.can_sync]
|
||||||
@@ -360,18 +477,18 @@ def main() -> int:
|
|||||||
remaining = run_sync(syncable, args.yes, args.dry_run) + blocked
|
remaining = run_sync(syncable, args.yes, args.dry_run) + blocked
|
||||||
else:
|
else:
|
||||||
if syncable and not args.sync:
|
if syncable and not args.sync:
|
||||||
print(f"Run with --sync to copy {len(syncable)} outdated document(s) from this repository.")
|
print(f"Run with --sync to copy {len(syncable)} outdated item(s) from this repository.")
|
||||||
remaining = len(attention)
|
remaining = len(attention)
|
||||||
|
|
||||||
if blocked:
|
if blocked:
|
||||||
print(f"{blocked} document(s) left alone - MODIFIED holds local edits, NO VER. cannot be compared,")
|
print(f"{blocked} item(s) left alone - MODIFIED holds local edits, NO VER. cannot be compared,")
|
||||||
print("and DUPLICATE means both the old and the new file name exist; remove the old one by hand.")
|
print("and DUPLICATE means both the old and the new file name exist; remove the old one by hand.")
|
||||||
|
|
||||||
if remaining:
|
if remaining:
|
||||||
print(f"{remaining} document(s) need attention.")
|
print(f"{remaining} item(s) need attention.")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print("All projects are up to date.")
|
print("Everything is up to date.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user