@xluos/dev-assets-cli 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dev_asset_common.py +1 -1
- package/package.json +6 -1
- package/scripts/hooks/_common.py +140 -0
- package/scripts/hooks/pre_compact.py +23 -0
- package/scripts/hooks/session_end.py +20 -0
- package/scripts/hooks/session_start.py +37 -0
- package/scripts/hooks/stop.py +20 -0
- package/skills/dev-assets-context/scripts/dev_asset_common.py +1 -1
- package/skills/dev-assets-setup/scripts/dev_asset_common.py +1 -1
- package/skills/dev-assets-sync/scripts/dev_asset_common.py +1 -1
- package/skills/dev-assets-update/scripts/dev_asset_common.py +1 -1
package/lib/dev_asset_common.py
CHANGED
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xluos/dev-assets-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "CLI for dev-assets hooks and repo-local setup",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
"hooks/*.json",
|
|
20
20
|
"hooks/README.md",
|
|
21
21
|
"lib/*.py",
|
|
22
|
+
"scripts/hooks/_common.py",
|
|
23
|
+
"scripts/hooks/session_start.py",
|
|
24
|
+
"scripts/hooks/pre_compact.py",
|
|
25
|
+
"scripts/hooks/stop.py",
|
|
26
|
+
"scripts/hooks/session_end.py",
|
|
22
27
|
"skills/dev-assets-context/SKILL.md",
|
|
23
28
|
"skills/dev-assets-context/agents/*.yaml",
|
|
24
29
|
"skills/dev-assets-context/references/*",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
REPO_ROOT = Path(os.environ.get("DEV_ASSETS_HOOK_REPO_ROOT", ".")).expanduser().resolve()
|
|
13
|
+
LIB_ROOT = PACKAGE_ROOT / "lib"
|
|
14
|
+
if str(LIB_ROOT) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(LIB_ROOT))
|
|
16
|
+
|
|
17
|
+
from dev_asset_common import AUTO_END, AUTO_START, PLACEHOLDER_MARKERS, asset_paths, get_branch_paths
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CONTEXT_SCRIPT = PACKAGE_ROOT / "skills" / "dev-assets-context" / "scripts" / "dev_asset_context.py"
|
|
21
|
+
SYNC_SCRIPT = PACKAGE_ROOT / "skills" / "dev-assets-sync" / "scripts" / "dev_asset_sync.py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_python(script_path, *args):
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
["python3", str(script_path), *args],
|
|
27
|
+
cwd=REPO_ROOT,
|
|
28
|
+
check=False,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
)
|
|
32
|
+
if result.returncode != 0:
|
|
33
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"command failed: {script_path}")
|
|
34
|
+
return result.stdout.strip()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def log(message):
|
|
38
|
+
print(message, file=sys.stderr)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_assets():
|
|
42
|
+
repo_root, branch_name, branch_key, storage_root, repo_key, repo_dir, branch_dir = get_branch_paths(str(REPO_ROOT))
|
|
43
|
+
return {
|
|
44
|
+
"repo_root": repo_root,
|
|
45
|
+
"branch_name": branch_name,
|
|
46
|
+
"branch_key": branch_key,
|
|
47
|
+
"storage_root": storage_root,
|
|
48
|
+
"repo_key": repo_key,
|
|
49
|
+
"repo_dir": repo_dir,
|
|
50
|
+
"branch_dir": branch_dir,
|
|
51
|
+
"paths": asset_paths(repo_dir, branch_dir),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def strip_managed_markers(text):
|
|
56
|
+
return text.replace(AUTO_START, "").replace(AUTO_END, "").replace("_尚未同步_", "").strip()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_placeholder(text):
|
|
60
|
+
stripped = strip_managed_markers(text)
|
|
61
|
+
if not stripped:
|
|
62
|
+
return True
|
|
63
|
+
return any(marker in stripped for marker in PLACEHOLDER_MARKERS)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def extract_section(path, title):
|
|
67
|
+
if not path.exists():
|
|
68
|
+
return None
|
|
69
|
+
content = path.read_text(encoding="utf-8")
|
|
70
|
+
match = re.search(rf"^## {re.escape(title)}\n\n(.*?)(?=^## |\Z)", content, flags=re.MULTILINE | re.DOTALL)
|
|
71
|
+
if not match:
|
|
72
|
+
return None
|
|
73
|
+
body = strip_managed_markers(match.group(1)).strip()
|
|
74
|
+
return None if is_placeholder(body) else body
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compact_body(text, max_lines=8, max_chars=700):
|
|
78
|
+
normalized = "\n".join(line.rstrip() for line in text.splitlines()).strip()
|
|
79
|
+
lines = [line for line in normalized.splitlines() if line.strip()]
|
|
80
|
+
if len(lines) > max_lines:
|
|
81
|
+
lines = lines[:max_lines]
|
|
82
|
+
if not lines[-1].endswith("..."):
|
|
83
|
+
lines.append("...")
|
|
84
|
+
compacted = "\n".join(lines)
|
|
85
|
+
if len(compacted) > max_chars:
|
|
86
|
+
compacted = compacted[: max_chars - 3].rstrip() + "..."
|
|
87
|
+
return compacted
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def maybe_sync_context():
|
|
91
|
+
return json.loads(run_python(CONTEXT_SCRIPT, "sync", "--repo", str(REPO_ROOT)))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def maybe_sync_working_tree():
|
|
95
|
+
return json.loads(run_python(SYNC_SCRIPT, "sync-working-tree", "--repo", str(REPO_ROOT)))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def maybe_record_head():
|
|
99
|
+
return json.loads(run_python(SYNC_SCRIPT, "record-head", "--repo", str(REPO_ROOT)))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def build_session_start_context():
|
|
103
|
+
assets = resolve_assets()
|
|
104
|
+
if not assets["branch_dir"].exists():
|
|
105
|
+
return (
|
|
106
|
+
"当前仓库尚未初始化 dev-assets 分支记忆。\n"
|
|
107
|
+
"如果这是需要跨会话继续的开发线,请先使用 `dev-assets-setup` 建立 repo+branch 存储。"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
maybe_sync_context()
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
log(f"[dev-assets][SessionStart] refresh skipped: {exc}")
|
|
114
|
+
|
|
115
|
+
paths = assets["paths"]
|
|
116
|
+
sections = [
|
|
117
|
+
("当前目标", extract_section(paths["overview"], "当前目标")),
|
|
118
|
+
("范围边界", extract_section(paths["overview"], "范围边界")),
|
|
119
|
+
("当前阶段", extract_section(paths["overview"], "当前阶段")),
|
|
120
|
+
("关键约束", extract_section(paths["overview"], "关键约束")),
|
|
121
|
+
("建议优先查看的目录", extract_section(paths["development"], "建议优先查看的目录")),
|
|
122
|
+
("当前进展", extract_section(paths["development"], "当前进展")),
|
|
123
|
+
("阻塞与注意点", extract_section(paths["development"], "阻塞与注意点")),
|
|
124
|
+
("下一步", extract_section(paths["development"], "下一步")),
|
|
125
|
+
("当前有效上下文", extract_section(paths["context"], "当前有效上下文")),
|
|
126
|
+
("关键决策与原因", extract_section(paths["context"], "关键决策与原因")),
|
|
127
|
+
("后续继续前要注意", extract_section(paths["context"], "后续继续前要注意")),
|
|
128
|
+
("仓库级长期目标与边界", extract_section(paths["repo_overview"], "长期目标与边界")),
|
|
129
|
+
("仓库级关键约束", extract_section(paths["repo_overview"], "仓库级关键约束")),
|
|
130
|
+
("共享入口", extract_section(paths["repo_sources"], "共享入口")),
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
parts = [
|
|
134
|
+
f"已加载 dev-assets:repo `{assets['repo_key']}`,branch `{assets['branch_name']}`。",
|
|
135
|
+
f"主存储目录:`{assets['branch_dir']}`",
|
|
136
|
+
]
|
|
137
|
+
for title, body in sections:
|
|
138
|
+
if body:
|
|
139
|
+
parts.append(f"{title}:\n{compact_body(body)}")
|
|
140
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from _common import log, maybe_sync_working_tree, resolve_assets
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
try:
|
|
8
|
+
assets = resolve_assets()
|
|
9
|
+
if not assets["branch_dir"].exists():
|
|
10
|
+
log("[dev-assets][PreCompact] branch memory not initialized, skip")
|
|
11
|
+
return 0
|
|
12
|
+
payload = maybe_sync_working_tree()
|
|
13
|
+
log(
|
|
14
|
+
"[dev-assets][PreCompact] refreshed working-tree navigation for "
|
|
15
|
+
f"{payload['branch']} ({payload['files_considered']} files)"
|
|
16
|
+
)
|
|
17
|
+
except Exception as exc:
|
|
18
|
+
log(f"[dev-assets][PreCompact] skipped: {exc}")
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from _common import log, maybe_record_head, resolve_assets
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
try:
|
|
8
|
+
assets = resolve_assets()
|
|
9
|
+
if not assets["branch_dir"].exists():
|
|
10
|
+
log("[dev-assets][SessionEnd] branch memory not initialized, skip")
|
|
11
|
+
return 0
|
|
12
|
+
payload = maybe_record_head()
|
|
13
|
+
log(f"[dev-assets][SessionEnd] finalized HEAD marker {payload['last_seen_head']} for {payload['branch']}")
|
|
14
|
+
except Exception as exc:
|
|
15
|
+
log(f"[dev-assets][SessionEnd] skipped: {exc}")
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from _common import build_session_start_context, log
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
try:
|
|
11
|
+
additional_context = build_session_start_context()
|
|
12
|
+
payload = {
|
|
13
|
+
"hookSpecificOutput": {
|
|
14
|
+
"hookEventName": "SessionStart",
|
|
15
|
+
"additionalContext": additional_context,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
19
|
+
return 0
|
|
20
|
+
except Exception as exc:
|
|
21
|
+
log(f"[dev-assets][SessionStart] skipped: {exc}")
|
|
22
|
+
print(
|
|
23
|
+
json.dumps(
|
|
24
|
+
{
|
|
25
|
+
"hookSpecificOutput": {
|
|
26
|
+
"hookEventName": "SessionStart",
|
|
27
|
+
"additionalContext": "dev-assets SessionStart hook 未能加载上下文,本轮按普通会话继续。",
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
ensure_ascii=False,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from _common import log, maybe_record_head, resolve_assets
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
try:
|
|
8
|
+
assets = resolve_assets()
|
|
9
|
+
if not assets["branch_dir"].exists():
|
|
10
|
+
log("[dev-assets][Stop] branch memory not initialized, skip")
|
|
11
|
+
return 0
|
|
12
|
+
payload = maybe_record_head()
|
|
13
|
+
log(f"[dev-assets][Stop] recorded HEAD {payload['last_seen_head']} for {payload['branch']}")
|
|
14
|
+
except Exception as exc:
|
|
15
|
+
log(f"[dev-assets][Stop] skipped: {exc}")
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
raise SystemExit(main())
|
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|
|
@@ -153,7 +153,7 @@ def detect_repo_identity(repo_root):
|
|
|
153
153
|
identity = repo_root.resolve().as_posix()
|
|
154
154
|
source = "path"
|
|
155
155
|
|
|
156
|
-
repo_slug = sanitize_repo_name(repo_root.name)
|
|
156
|
+
repo_slug = sanitize_repo_name(Path(identity).name or repo_root.name)
|
|
157
157
|
digest = hashlib.sha1(identity.encode("utf-8")).hexdigest()[:12]
|
|
158
158
|
return {
|
|
159
159
|
"repo_identity": identity,
|