🐛 fix(playbook): address reported repo issues
normalize Windows path-like TOML config values, regenerate .agents/index.md on sync, keep the SKILLS.md superpowers section route-only, and ignore generated Python cache dirs. add regression coverage for load_config() and the standards sync behavior touched by these fixes.
This commit is contained in:
parent
c3f81371e2
commit
94395056ea
|
|
@ -60,58 +60,7 @@ done
|
||||||
|
|
||||||
printf "%s\n" "${names[@]}" | sort > "$SUPERPOWERS_LIST"
|
printf "%s\n" "${names[@]}" | sort > "$SUPERPOWERS_LIST"
|
||||||
|
|
||||||
update_block() {
|
git add codex/skills "$SUPERPOWERS_LIST"
|
||||||
local file="$1"
|
|
||||||
local start="<!-- superpowers:skills:start -->"
|
|
||||||
local end="<!-- superpowers:skills:end -->"
|
|
||||||
local tmp
|
|
||||||
|
|
||||||
tmp="$(mktemp)"
|
|
||||||
{
|
|
||||||
echo "### Third-party Skills (superpowers)"
|
|
||||||
echo ""
|
|
||||||
echo "$start"
|
|
||||||
while IFS= read -r name; do
|
|
||||||
[ -n "$name" ] || continue
|
|
||||||
echo "- $name"
|
|
||||||
done < "$SUPERPOWERS_LIST"
|
|
||||||
echo "$end"
|
|
||||||
} > "$tmp"
|
|
||||||
|
|
||||||
if grep -q "$start" "$file"; then
|
|
||||||
awk -v start="$start" -v end="$end" -v block="$tmp" '
|
|
||||||
BEGIN {
|
|
||||||
while ((getline line < block) > 0) { buf[++n] = line }
|
|
||||||
close(block)
|
|
||||||
inblock=0
|
|
||||||
replaced=0
|
|
||||||
}
|
|
||||||
{
|
|
||||||
if (!replaced && $0 == start) {
|
|
||||||
for (i=1; i<=n; i++) print buf[i]
|
|
||||||
inblock=1
|
|
||||||
replaced=1
|
|
||||||
next
|
|
||||||
}
|
|
||||||
if (inblock) {
|
|
||||||
if ($0 == end) { inblock=0 }
|
|
||||||
next
|
|
||||||
}
|
|
||||||
print
|
|
||||||
}
|
|
||||||
' "$file" > "${file}.tmp"
|
|
||||||
mv "${file}.tmp" "$file"
|
|
||||||
else
|
|
||||||
echo "" >> "$file"
|
|
||||||
cat "$tmp" >> "$file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
update_block "SKILLS.md"
|
|
||||||
|
|
||||||
git add codex/skills SKILLS.md "$SUPERPOWERS_LIST"
|
|
||||||
|
|
||||||
if git diff --cached --quiet; then
|
if git diff --cached --quiet; then
|
||||||
echo "No changes to sync."
|
echo "No changes to sync."
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,7 @@ tags
|
||||||
|
|
||||||
reports/
|
reports/
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
scripts/__pycache__
|
||||||
|
tests/__pycache__
|
||||||
|
tests/cli/__pycache__
|
||||||
|
|
|
||||||
26
SKILLS.md
26
SKILLS.md
|
|
@ -159,32 +159,6 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml
|
||||||
## 9. Third-party Skills (superpowers)
|
## 9. Third-party Skills (superpowers)
|
||||||
|
|
||||||
来源:`codex/skills/.sources/superpowers.list`(第三方来源清单)。
|
来源:`codex/skills/.sources/superpowers.list`(第三方来源清单)。
|
||||||
本节仅列出 superpowers 体系 skills,与本 Playbook 原生 skills 分离。
|
|
||||||
|
|
||||||
### Third-party Skills (superpowers)
|
|
||||||
|
|
||||||
### Third-party Skills (superpowers)
|
|
||||||
|
|
||||||
### Third-party Skills (superpowers)
|
|
||||||
|
|
||||||
### Third-party Skills (superpowers)
|
|
||||||
|
|
||||||
<!-- superpowers:skills:start -->
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
- \
|
|
||||||
<!-- superpowers:skills:end -->
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ ORDER = [
|
||||||
]
|
]
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
PLAYBOOK_ROOT = SCRIPT_DIR.parent
|
PLAYBOOK_ROOT = SCRIPT_DIR.parent
|
||||||
|
PATH_CONFIG_KEYS = {"project_root", "target_dir", "agents_home", "codex_home"}
|
||||||
|
|
||||||
|
|
||||||
def usage() -> str:
|
def usage() -> str:
|
||||||
|
|
@ -141,8 +142,63 @@ def loads_toml_minimal(raw: str) -> dict:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_path_config_strings(raw: str) -> str:
|
||||||
|
normalized_lines: list[str] = []
|
||||||
|
for line in raw.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#") or "=" not in line:
|
||||||
|
normalized_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
key_part, value_part = line.split("=", 1)
|
||||||
|
key = key_part.strip()
|
||||||
|
if key not in PATH_CONFIG_KEYS:
|
||||||
|
normalized_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = strip_inline_comment(value_part.strip())
|
||||||
|
if len(value) < 2 or value[0] != '"' or value[-1] != '"' or "\\" not in value[1:-1]:
|
||||||
|
normalized_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
inner = value[1:-1]
|
||||||
|
has_lone_backslash = False
|
||||||
|
probe_idx = 0
|
||||||
|
while probe_idx < len(inner):
|
||||||
|
if inner[probe_idx] != "\\":
|
||||||
|
probe_idx += 1
|
||||||
|
continue
|
||||||
|
if probe_idx + 1 < len(inner) and inner[probe_idx + 1] == "\\":
|
||||||
|
probe_idx += 2
|
||||||
|
continue
|
||||||
|
has_lone_backslash = True
|
||||||
|
break
|
||||||
|
if not has_lone_backslash:
|
||||||
|
normalized_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
escaped: list[str] = []
|
||||||
|
idx = 0
|
||||||
|
while idx < len(inner):
|
||||||
|
ch = inner[idx]
|
||||||
|
if ch != "\\":
|
||||||
|
escaped.append(ch)
|
||||||
|
idx += 1
|
||||||
|
continue
|
||||||
|
if idx + 1 < len(inner) and inner[idx + 1] == "\\":
|
||||||
|
escaped.extend(["\\", "\\"])
|
||||||
|
idx += 2
|
||||||
|
continue
|
||||||
|
escaped.extend(["\\", "\\"])
|
||||||
|
idx += 1
|
||||||
|
normalized_lines.append(f'{key_part}= "{"".join(escaped)}"')
|
||||||
|
|
||||||
|
suffix = "\n" if raw.endswith("\n") else ""
|
||||||
|
return "\n".join(normalized_lines) + suffix
|
||||||
|
|
||||||
|
|
||||||
def load_config(path: Path) -> dict:
|
def load_config(path: Path) -> dict:
|
||||||
raw = path.read_text(encoding="utf-8")
|
raw = normalize_path_config_strings(path.read_text(encoding="utf-8"))
|
||||||
if tomllib is not None:
|
if tomllib is not None:
|
||||||
return tomllib.loads(raw)
|
return tomllib.loads(raw)
|
||||||
return loads_toml_minimal(raw)
|
return loads_toml_minimal(raw)
|
||||||
|
|
@ -830,8 +886,6 @@ def update_agents_block(agents_md: Path, block_lines: list[str]) -> None:
|
||||||
|
|
||||||
def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str | None) -> None:
|
def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str | None) -> None:
|
||||||
agents_index = agents_root / "index.md"
|
agents_index = agents_root / "index.md"
|
||||||
if agents_index.exists():
|
|
||||||
return
|
|
||||||
lines = [
|
lines = [
|
||||||
"# .agents(多语言)",
|
"# .agents(多语言)",
|
||||||
"",
|
"",
|
||||||
|
|
@ -859,7 +913,7 @@ def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str |
|
||||||
f"- {docs_prefix or 'docs/standards/playbook/docs/'}",
|
f"- {docs_prefix or 'docs/standards/playbook/docs/'}",
|
||||||
]
|
]
|
||||||
agents_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
agents_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
log("Created .agents/index.md")
|
log("Synced .agents/index.md")
|
||||||
|
|
||||||
|
|
||||||
def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None:
|
def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None:
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,46 @@ langs = ["tsl"]
|
||||||
self.assertEqual(result.returncode, 0)
|
self.assertEqual(result.returncode, 0)
|
||||||
self.assertTrue(agents_index.is_file())
|
self.assertTrue(agents_index.is_file())
|
||||||
|
|
||||||
|
def test_sync_standards_updates_agents_index_when_langs_expand(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
root = Path(tmp_dir)
|
||||||
|
|
||||||
|
first_config = root / "playbook-first.toml"
|
||||||
|
first_config.write_text(
|
||||||
|
f"""
|
||||||
|
[playbook]
|
||||||
|
project_root = "{tmp_dir}"
|
||||||
|
|
||||||
|
[sync_standards]
|
||||||
|
langs = ["tsl"]
|
||||||
|
no_backup = true
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
first_result = run_cli("-config", str(first_config))
|
||||||
|
self.assertEqual(first_result.returncode, 0)
|
||||||
|
|
||||||
|
second_config = root / "playbook-second.toml"
|
||||||
|
second_config.write_text(
|
||||||
|
f"""
|
||||||
|
[playbook]
|
||||||
|
project_root = "{tmp_dir}"
|
||||||
|
|
||||||
|
[sync_standards]
|
||||||
|
langs = ["tsl", "cpp"]
|
||||||
|
no_backup = true
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
second_result = run_cli("-config", str(second_config))
|
||||||
|
self.assertEqual(second_result.returncode, 0)
|
||||||
|
|
||||||
|
agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("`.agents/tsl/index.md`", agents_index)
|
||||||
|
self.assertIn("`.agents/cpp/index.md`", agents_index)
|
||||||
|
|
||||||
def test_sync_standards_agents_block_has_blank_lines(self):
|
def test_sync_standards_agents_block_has_blank_lines(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
config_body = f"""
|
config_body = f"""
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
SKILLS_MD = ROOT / "SKILLS.md"
|
SKILLS_MD = ROOT / "SKILLS.md"
|
||||||
SOURCES_LIST = ROOT / "codex" / "skills" / ".sources" / "superpowers.list"
|
SOURCES_LIST = ROOT / "codex" / "skills" / ".sources" / "superpowers.list"
|
||||||
|
SOURCE_REF = "来源:`codex/skills/.sources/superpowers.list`(第三方来源清单)。"
|
||||||
|
|
||||||
|
|
||||||
def read_sources_list() -> list[str]:
|
def read_sources_list() -> list[str]:
|
||||||
|
|
@ -14,28 +15,20 @@ def read_sources_list() -> list[str]:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def read_skills_md_list() -> list[str]:
|
def read_skills_md() -> str:
|
||||||
lines = SKILLS_MD.read_text(encoding="utf-8").splitlines()
|
return SKILLS_MD.read_text(encoding="utf-8")
|
||||||
start = "<!-- superpowers:skills:start -->"
|
|
||||||
end = "<!-- superpowers:skills:end -->"
|
|
||||||
try:
|
|
||||||
start_idx = lines.index(start) + 1
|
|
||||||
end_idx = lines.index(end)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise AssertionError("superpowers markers missing in SKILLS.md") from exc
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for line in lines[start_idx:end_idx]:
|
|
||||||
stripped = line.strip()
|
|
||||||
if not stripped.startswith("-"):
|
|
||||||
continue
|
|
||||||
items.append(stripped.lstrip("- ").strip())
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
class SuperpowersListSyncTests(unittest.TestCase):
|
class SuperpowersListSyncTests(unittest.TestCase):
|
||||||
def test_superpowers_list_matches_skills_md(self):
|
def test_superpowers_section_routes_to_source_list(self):
|
||||||
self.assertEqual(read_sources_list(), read_skills_md_list())
|
self.assertTrue(read_sources_list())
|
||||||
|
|
||||||
|
text = read_skills_md()
|
||||||
|
|
||||||
|
self.assertIn(SOURCE_REF, text)
|
||||||
|
self.assertEqual(text.count("Third-party Skills (superpowers)"), 1)
|
||||||
|
self.assertNotIn("<!-- superpowers:skills:start -->", text)
|
||||||
|
self.assertNotIn("<!-- superpowers:skills:end -->", text)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from scripts import playbook
|
from scripts import playbook
|
||||||
|
|
||||||
|
|
@ -18,6 +20,44 @@ key = 1
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
playbook.loads_toml_minimal(raw)
|
playbook.loads_toml_minimal(raw)
|
||||||
|
|
||||||
|
def test_load_config_preserves_windows_users_path_in_basic_string(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
config_path = Path(tmp_dir) / "playbook.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
'[playbook]\nproject_root = "C:\\Users\\demo\\workspace"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
data = playbook.load_config(config_path)
|
||||||
|
|
||||||
|
self.assertEqual(data["playbook"]["project_root"], r"C:\Users\demo\workspace")
|
||||||
|
|
||||||
|
def test_load_config_preserves_windows_escape_like_segments_for_path_keys(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
config_path = Path(tmp_dir) / "playbook.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
'[playbook]\nproject_root = "C:\\tmp\\notes"\n\n'
|
||||||
|
'[install_skills]\nagents_home = "D:\\new\\tab"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
data = playbook.load_config(config_path)
|
||||||
|
|
||||||
|
self.assertEqual(data["playbook"]["project_root"], r"C:\tmp\notes")
|
||||||
|
self.assertEqual(data["install_skills"]["agents_home"], r"D:\new\tab")
|
||||||
|
|
||||||
|
def test_load_config_keeps_already_escaped_windows_path(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
config_path = Path(tmp_dir) / "playbook.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
'[playbook]\nproject_root = "C:\\\\Users\\\\demo\\\\workspace"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
data = playbook.load_config(config_path)
|
||||||
|
|
||||||
|
self.assertEqual(data["playbook"]["project_root"], r"C:\Users\demo\workspace")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue