# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
import pathlib
import shutil
from mozlint
import result
from mozversioncontrol
import (
InvalidRepoPath,
MissingVCSInfo,
MissingVCSTool,
get_repository_object,
)
CLAUDE_SKILLS =
".claude/skills"
AGENT_SKILLS =
".agents/skills"
def _error(config, path, message):
return result.from_config(
config,
path=str(path),
lineno=
0,
message=message,
level=
"error",
)
def _walk(base):
if not base.is_dir():
return []
return [p
for p
in base.rglob(
"*.md")
if p.is_file()]
def _collect_vcs_changes(root):
"""Return a mapping of repo-relative POSIX paths for added/modified and
deleted files
in the working copy,
or None if VCS state
is unavailable.
"""
try:
repo = get_repository_object(str(root))
added_or_modified = repo.get_changed_files(diff_filter=
"AM", mode=
"all")
deleted = repo.get_changed_files(diff_filter=
"D", mode=
"all")
except (InvalidRepoPath, MissingVCSTool, MissingVCSInfo):
return None
return {
"added_or_modified": {
pathlib.PurePath(p).as_posix()
for p
in added_or_modified
},
"deleted": {pathlib.PurePath(p).as_posix()
for p
in deleted},
}
def lint(paths, config, fix=
None, **lintargs):
root = pathlib.Path(lintargs[
"root"]).resolve()
claude_root = root / CLAUDE_SKILLS
agent_root = root / AGENT_SKILLS
vcs_changes = _collect_vcs_changes(root)
if fix
else None
claude_rels = {p.relative_to(claude_root).as_posix()
for p
in _walk(claude_root)}
agent_rels = {p.relative_to(agent_root).as_posix()
for p
in _walk(agent_root)}
results = []
fixed =
0
for rel
in sorted(claude_rels.symmetric_difference(agent_rels)):
claude_path = claude_root / rel
agent_path = agent_root / rel
claude_display = f
"{CLAUDE_SKILLS}/{rel}"
agent_display = f
"{AGENT_SKILLS}/{rel}"
if rel
in claude_rels:
existing_path, missing_path = claude_path, agent_path
existing_display, missing_display = claude_display, agent_display
else:
existing_path, missing_path = agent_path, claude_path
existing_display, missing_display = agent_display, claude_display
if fix
and vcs_changes
is not None:
existing_added = existing_display
in vcs_changes[
"added_or_modified"]
missing_deleted = missing_display
in vcs_changes[
"deleted"]
if existing_added
and not missing_deleted:
missing_path.parent.mkdir(parents=
True, exist_ok=
True)
shutil.copyfile(existing_path, missing_path)
fixed +=
1
continue
if missing_deleted
and not existing_added:
existing_path.unlink()
fixed +=
1
continue
if fix:
results.append(
_error(
config,
existing_path,
f
"{existing_display} has no counterpart {missing_display}. "
"Cannot determine from VCS whether to propagate an add or "
"a delete; resolve manually.",
)
)
else:
results.append(
_error(
config,
existing_path,
f
"Missing counterpart {missing_display}. Run "
"`./mach lint -l agent-skills-sync --fix`.",
)
)
for rel
in sorted(claude_rels.intersection(agent_rels)):
claude_path = claude_root / rel
agent_path = agent_root / rel
if claude_path.read_bytes() == agent_path.read_bytes():
continue
claude_display = f
"{CLAUDE_SKILLS}/{rel}"
agent_display = f
"{AGENT_SKILLS}/{rel}"
if fix
and vcs_changes
is not None:
claude_changed = claude_display
in vcs_changes[
"added_or_modified"]
agent_changed = agent_display
in vcs_changes[
"added_or_modified"]
if claude_changed
and not agent_changed:
shutil.copyfile(claude_path, agent_path)
fixed +=
1
continue
if agent_changed
and not claude_changed:
shutil.copyfile(agent_path, claude_path)
fixed +=
1
continue
if fix:
claude_msg = (
f
"This file differs from {agent_display}. Both (or neither) "
"sides modified in VCS; resolve manually."
)
agent_msg = (
f
"This file differs from {claude_display}. Both (or neither) "
"sides modified in VCS; resolve manually."
)
else:
claude_msg = (
f
"This file differs from {agent_display}. Try "
"`./mach lint -l agent-skills-sync --fix`, else resolve "
"manually."
)
agent_msg = (
f
"This file differs from {claude_display}. Try "
"`./mach lint -l agent-skills-sync --fix`, else resolve "
"manually."
)
results.append(_error(config, claude_path, claude_msg))
results.append(_error(config, agent_path, agent_msg))
return {
"results": results,
"fixed": fixed}