openmoneta-dev-kit 1.11.1 → 1.13.0

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Biến **Cursor IDE / OpenCode** thành một **team developer hoàn chỉnh** với quy trình 6 bước (Lean Mode), adaptive planning, hooks enforcement, token-aware doc routing và sub-agents on-demand.
4
4
 
5
- [![version](https://img.shields.io/badge/version-1.9.0-blue.svg)](./VERSION)
5
+ [![version](https://img.shields.io/badge/version-1.12.0-blue.svg)](./VERSION)
6
6
  [![docs](https://img.shields.io/badge/docs-Vietnamese-green.svg)](./docs/INDEX.md)
7
7
  [![platform](https://img.shields.io/badge/platform-macOS%20|%20Linux%20|%20Windows-lightgrey.svg)]()
8
8
  [![npm](https://img.shields.io/npm/v/openmoneta-dev-kit)](https://www.npmjs.com/package/openmoneta-dev-kit)
@@ -41,6 +41,7 @@ openmoneta init
41
41
  openmoneta update # global + sync project hiện tại
42
42
  openmoneta update --yes # auto-sync, skip nếu đã latest
43
43
  openmoneta check # chỉ kiểm tra version mới
44
+ openmoneta docs # audit module README thiếu/lỗi thời + sync INDEX
44
45
  ```
45
46
 
46
47
  ## Cài đặt thủ công (git clone)
@@ -54,9 +55,10 @@ bash ~/OpenMoneta-Dev-Kit/install.sh
54
55
 
55
56
  ## Có gì trong này?
56
57
 
57
- - **10 skills** (4 core + 1 core conditional + 5 on-demand) — phân tích yêu cầu, thiết kế module, plan, systematic debugging, safe push, test, security
58
+ - **11 skills** (4 core + 1 core conditional + 6 on-demand) — phân tích yêu cầu, thiết kế module, plan, systematic debugging, safe push, test, security, bảo trì docs
59
+ - **`openmoneta docs`** — audit module README thiếu/lỗi thời (stale) + skill `docs-maintenance` backfill docs cho dự án cũ
58
60
  - **4 sub-agents** — 1 core + 3 on-demand (security-auditor, qa-autonomous, ui-tester)
59
- - **4 hooks + plugin** — enforce token-aware reading, adaptive plan scope
61
+ - **4 hooks + plugin** — enforce token-aware reading, adaptive plan scope, và verify cuối phiên (module README + close plan); OpenCode đạt parity qua plugin guard `session.idle`
60
62
  - **Token Routing** — bảng map keyword → module giúp AI giảm 70-90% token đọc
61
63
  - **npm package** — `npm install -g openmoneta-dev-kit`, không cần git, không cần clone
62
64
  - **Auto-update check** — notify khi có version mới (cache 24h)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.11.1
1
+ 1.13.0
@@ -10,12 +10,30 @@ set -euo pipefail
10
10
 
11
11
  INPUT_JSON=$(cat 2>/dev/null || echo '{}')
12
12
 
13
+ # Auto-register project vào Cursor registry (seed cho `update --all-projects`).
14
+ # Im lặng. Chỉ append khi: workspace có docs/INDEX.md, Cursor install home đã cài,
15
+ # và path chưa có trong registry (dedup, chuẩn hoá pwd -P).
16
+ register_project_cursor() {
17
+ local ws="$1"
18
+ [[ -n "$ws" && -f "$ws/docs/INDEX.md" ]] || return 0
19
+ [[ -f "$HOME/.cursor/.openmoneta-version" ]] || return 0
20
+ local reg="$HOME/.cursor/.openmoneta-projects"
21
+ local abs
22
+ abs="$(cd "$ws" 2>/dev/null && pwd -P)" || return 0
23
+ [[ -n "$abs" ]] || return 0
24
+ if [[ -f "$reg" ]] && grep -qFx "$abs" "$reg" 2>/dev/null; then
25
+ return 0
26
+ fi
27
+ echo "$abs" >> "$reg" 2>/dev/null || true
28
+ }
29
+
13
30
  # Reset docs-index marker mỗi session (force re-read INDEX.md)
14
31
  if command -v jq >/dev/null 2>&1; then
15
32
  WORKSPACE_FROM_INPUT=$(echo "$INPUT_JSON" | jq -r '.workspace_roots[0] // ""' 2>/dev/null || echo "")
16
33
  if [[ -n "$WORKSPACE_FROM_INPUT" && -d "$WORKSPACE_FROM_INPUT/.cursor" ]]; then
17
34
  rm -f "$WORKSPACE_FROM_INPUT/.cursor/.docs-index-read" 2>/dev/null || true
18
35
  fi
36
+ register_project_cursor "$WORKSPACE_FROM_INPUT" 2>/dev/null || true
19
37
  fi
20
38
 
21
39
  # === Summary 6 bước (~45 dòng, ~650 tokens) ===
@@ -44,7 +62,7 @@ SUMMARY='# OpenMoneta Dev Kit v1.7.0 — Quy trình 6 bước (Adaptive Planning
44
62
  - `security-auditor`, `qa-autonomous`, `ui-tester` — **ON-DEMAND ONLY** (chỉ khi user yêu cầu explicit).
45
63
 
46
64
  ## Skills on-demand (KHÔNG tự trigger)
47
- `security-checklist`, `test-strategy`, `automated-testing`, `auth-bypass-testing`, `ui-test-loop` — chỉ kích hoạt khi user yêu cầu rõ ("audit security X", "viết test cho Y", "Playwright UI test Z").
65
+ `security-checklist`, `test-strategy`, `automated-testing`, `auth-bypass-testing`, `ui-test-loop`, `docs-maintenance` — chỉ kích hoạt khi user yêu cầu rõ ("audit security X", "viết test cho Y", "Playwright UI test Z", "tạo/bảo trì docs dự án cũ").
48
66
 
49
67
  ## Skill core conditional
50
68
  `safe-push` — bắt buộc khi user yêu cầu push/đẩy code để tránh đè code người khác trong repo nhiều contributor.
@@ -29,6 +29,7 @@
29
29
  - OpenCode đọc global rules từ `~/.config/opencode/AGENTS.md`.
30
30
  - OpenCode đọc project rules từ `AGENTS.md` ở project root. Project rules là source of truth cho từng repo.
31
31
  - OpenMoneta skills được cài vào `~/.config/opencode/skills/*/SKILL.md` và agent nên load bằng skill tool khi cần.
32
+ - **Bảo trì doc dự án cũ** (ON-DEMAND): khi user yêu cầu tạo/refresh docs còn thiếu hoặc lỗi thời, chạy `bash ~/.config/opencode/scripts/docs-audit.sh` để soi rồi load skill `docs-maintenance` (backfill MISSING + refresh STALE + sync `docs/INDEX.md`).
32
33
  - OpenMoneta subagents được cài vào `~/.config/opencode/agents/`.
33
34
  - OpenCode plugin guard trong `~/.config/opencode/plugins/openmoneta-guard.ts` enforce workflow tương đương Cursor hooks: `tool.execute.before` (docs-first + plan gate), `tool.execute.after` (track changes), và `event:session.idle` (verify B2/B5: module README + close plan, re-prompt nhắc hoàn tất với loop guard ≤4 lần).
34
35
 
@@ -400,6 +400,33 @@ function clearVerifyLoop(root: string) {
400
400
  fs.rmSync(path.join(root, ".cursor", ".openmoneta-verify-loop.json"), { force: true })
401
401
  }
402
402
 
403
+ // Auto-register project vào OpenCode registry (seed cho `update --all-projects`).
404
+ // Im lặng + try/catch — seed không được phép làm fail guard. Chỉ append khi:
405
+ // project có docs/INDEX.md, OpenCode install home đã cài, path chưa có trong registry.
406
+ function registerProjectOpenCode(root: string) {
407
+ try {
408
+ if (!fs.existsSync(path.join(root, "docs", "INDEX.md"))) return
409
+ const home = process.env.HOME || process.env.USERPROFILE || ""
410
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(home, ".config")
411
+ const ocHome = path.join(xdg, "opencode")
412
+ if (!fs.existsSync(path.join(ocHome, ".openmoneta-version"))) return
413
+ const reg = path.join(ocHome, ".openmoneta-projects")
414
+ const abs = fs.realpathSync(root)
415
+ let lines: string[] = []
416
+ if (fs.existsSync(reg)) {
417
+ lines = fs
418
+ .readFileSync(reg, "utf8")
419
+ .split("\n")
420
+ .map((l) => l.trim())
421
+ .filter(Boolean)
422
+ }
423
+ if (lines.includes(abs)) return
424
+ fs.appendFileSync(reg, `${abs}\n`)
425
+ } catch {
426
+ // im lặng
427
+ }
428
+ }
429
+
403
430
  type SessionPromptClient = {
404
431
  session?: {
405
432
  prompt?: (input: {
@@ -434,7 +461,7 @@ export const OpenMonetaGuard = async (ctx: GuardContext) => {
434
461
  {
435
462
  loaded_at: new Date().toISOString(),
436
463
  root,
437
- version: "1.11.0",
464
+ version: "1.13.0",
438
465
  load_count: globalState[globalKey],
439
466
  },
440
467
  null,
@@ -442,6 +469,9 @@ export const OpenMonetaGuard = async (ctx: GuardContext) => {
442
469
  )}\n`,
443
470
  )
444
471
 
472
+ // Seed project vào OpenCode registry để `update --all-projects` thấy project này.
473
+ registerProjectOpenCode(root)
474
+
445
475
  // OpenCode does not have Cursor's sessionStart hook, so reset the docs-first
446
476
  // marker when the plugin is loaded for a new OpenCode process/session.
447
477
  if (fs.existsSync(marker)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmoneta-dev-kit",
3
- "version": "1.11.1",
3
+ "version": "1.13.0",
4
4
  "description": "OpenMoneta Dev Kit — Biến Cursor IDE / OpenCode thành team developer hoàn chỉnh với quy trình 6 bước, adaptive planning, hooks enforcement, và token-aware doc routing",
5
5
  "keywords": [
6
6
  "cursor",
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bash
2
+ # docs-audit.sh — Quét module trong project hiện tại và đối chiếu với docs/modules/<slug>/README.md.
3
+ #
4
+ # Mục đích: phát hiện module thiếu doc (MISSING), doc lỗi thời so với code (STALE),
5
+ # và doc/INDEX trỏ tới module không còn tồn tại (GHOST). Deterministic, không sửa file.
6
+ #
7
+ # Usage (chạy ở thư mục gốc project):
8
+ # bash docs-audit.sh # report bảng (mặc định)
9
+ # bash docs-audit.sh --report # như trên
10
+ # bash docs-audit.sh --worklist # TSV: <slug>\t<docsPath>\t<sourcePath>\t<missing|stale|ok>
11
+ # bash docs-audit.sh --validate # liệt kê GHOST (doc trỏ source/module không tồn tại)
12
+ #
13
+ # Stale detection: so commit mới nhất của source vs README (git); fallback mtime filesystem.
14
+
15
+ set -uo pipefail
16
+
17
+ MODE="report"
18
+ case "${1:-}" in
19
+ --worklist) MODE="worklist" ;;
20
+ --validate) MODE="validate" ;;
21
+ --report|"") MODE="report" ;;
22
+ *) echo "[!] Mode không hợp lệ: $1 (dùng --report|--worklist|--validate)" >&2; exit 2 ;;
23
+ esac
24
+
25
+ GIT_OK=0
26
+ if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
27
+ GIT_OK=1
28
+ fi
29
+
30
+ file_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; }
31
+
32
+ newest_mtime() {
33
+ local target="$1" newest=0 m
34
+ while IFS= read -r f; do
35
+ m=$(file_mtime "$f")
36
+ [[ "$m" -gt "$newest" ]] && newest=$m
37
+ done < <(find "$target" -type f 2>/dev/null)
38
+ echo "$newest"
39
+ }
40
+
41
+ git_last_commit() { git log -1 --format=%ct -- "$1" 2>/dev/null || echo ""; }
42
+
43
+ # In status missing|stale|ok cho 1 module
44
+ status_for() {
45
+ local readme="$1" src="$2" tr ts
46
+ if [[ ! -f "$readme" ]]; then echo "missing"; return; fi
47
+ if [[ "$GIT_OK" -eq 1 ]]; then
48
+ tr=$(git_last_commit "$readme")
49
+ ts=$(git_last_commit "$src")
50
+ fi
51
+ if [[ -z "${tr:-}" || -z "${ts:-}" ]]; then
52
+ tr=$(file_mtime "$readme")
53
+ ts=$(newest_mtime "$src")
54
+ fi
55
+ if [[ "${ts:-0}" -gt "${tr:-0}" ]]; then echo "stale"; else echo "ok"; fi
56
+ }
57
+
58
+ # Liệt kê module trong filesystem → "<slug>|<docsPath>|<sourcePath>"
59
+ collect_modules() {
60
+ local d n e
61
+ if [[ -d apps ]]; then
62
+ for d in apps/*/; do
63
+ [[ -d "$d" ]] || continue; n=$(basename "$d"); [[ "$n" == .* ]] && continue
64
+ echo "apps-$n|docs/modules/apps-$n|apps/$n"
65
+ done
66
+ fi
67
+ if [[ -d packages ]]; then
68
+ for d in packages/*/; do
69
+ [[ -d "$d" ]] || continue; n=$(basename "$d"); [[ "$n" == .* ]] && continue
70
+ echo "packages-$n|docs/modules/packages-$n|packages/$n"
71
+ done
72
+ fi
73
+ if [[ -d src/modules ]]; then
74
+ for d in src/modules/*/; do
75
+ [[ -d "$d" ]] || continue; n=$(basename "$d"); [[ "$n" == .* ]] && continue
76
+ echo "$n|docs/modules/$n|src/modules/$n"
77
+ done
78
+ elif [[ -d src ]]; then
79
+ for e in src/*; do
80
+ [[ -e "$e" ]] || continue; n=$(basename "$e")
81
+ [[ "$n" == .* ]] && continue
82
+ [[ "$n" == "modules" ]] && continue
83
+ echo "$n|docs/modules/$n|$e"
84
+ done
85
+ fi
86
+ }
87
+
88
+ in_index() {
89
+ [[ -f docs/INDEX.md ]] || { echo "no"; return; }
90
+ if grep -q "modules/$1" docs/INDEX.md 2>/dev/null; then echo "yes"; else echo "no"; fi
91
+ }
92
+
93
+ # Map slug docs → candidate source path (cho validate/ghost)
94
+ slug_to_source() {
95
+ local slug="$1"
96
+ if [[ "$slug" == apps-* ]]; then echo "apps/${slug#apps-}"
97
+ elif [[ "$slug" == packages-* ]]; then echo "packages/${slug#packages-}"
98
+ elif [[ -e "src/modules/$slug" ]]; then echo "src/modules/$slug"
99
+ else echo "src/$slug"
100
+ fi
101
+ }
102
+
103
+ MODULES=$(collect_modules | sort -u)
104
+
105
+ # ---------- MODE: validate (ghost) ----------
106
+ if [[ "$MODE" == "validate" ]]; then
107
+ ghosts=0
108
+ if [[ -d docs/modules ]]; then
109
+ for d in docs/modules/*/; do
110
+ [[ -d "$d" ]] || continue
111
+ slug=$(basename "$d")
112
+ src=$(slug_to_source "$slug")
113
+ if [[ ! -e "$src" ]]; then
114
+ echo "GHOST docs/modules/$slug → source '$src' không tồn tại"
115
+ ghosts=$((ghosts+1))
116
+ fi
117
+ done
118
+ fi
119
+ # INDEX trỏ tới docs/modules/<x> không tồn tại
120
+ if [[ -f docs/INDEX.md ]]; then
121
+ while IFS= read -r ref; do
122
+ slug="${ref#modules/}"; slug="${slug%%/*}"
123
+ [[ -z "$slug" ]] && continue
124
+ if [[ ! -d "docs/modules/$slug" ]]; then
125
+ echo "GHOST docs/INDEX.md → 'modules/$slug' nhưng thư mục docs/modules/$slug không tồn tại"
126
+ ghosts=$((ghosts+1))
127
+ fi
128
+ done < <(grep -oE 'modules/[A-Za-z0-9._-]+' docs/INDEX.md 2>/dev/null | sort -u)
129
+ fi
130
+ if [[ "$ghosts" -eq 0 ]]; then
131
+ echo "✓ Không phát hiện GHOST doc (mọi docs/modules + tham chiếu INDEX đều có source)."
132
+ else
133
+ echo ""
134
+ echo "Tổng: $ghosts cảnh báo GHOST."
135
+ fi
136
+ exit 0
137
+ fi
138
+
139
+ # ---------- MODE: worklist (TSV cho skill) ----------
140
+ if [[ "$MODE" == "worklist" ]]; then
141
+ [[ -z "$MODULES" ]] && exit 0
142
+ while IFS='|' read -r slug docs src; do
143
+ [[ -z "$slug" ]] && continue
144
+ st=$(status_for "$docs/README.md" "$src")
145
+ printf '%s\t%s\t%s\t%s\n' "$slug" "$docs" "$src" "$st"
146
+ done <<< "$MODULES"
147
+ exit 0
148
+ fi
149
+
150
+ # ---------- MODE: report (mặc định) ----------
151
+ echo ""
152
+ echo " Module doc audit — $(pwd)"
153
+ echo " ──────────────────────────────────────────────────────────────"
154
+ if [[ -z "$MODULES" ]]; then
155
+ echo " 0 module (không thấy apps/ packages/ src/). Không có gì để audit."
156
+ echo ""
157
+ exit 0
158
+ fi
159
+
160
+ printf " %-22s %-22s %-8s %-7s %-8s\n" "slug" "source" "README" "INDEX" "status"
161
+ printf " %-22s %-22s %-8s %-7s %-8s\n" "----" "------" "------" "-----" "------"
162
+
163
+ n_missing=0; n_stale=0; n_ok=0; n_noindex=0
164
+ while IFS='|' read -r slug docs src; do
165
+ [[ -z "$slug" ]] && continue
166
+ st=$(status_for "$docs/README.md" "$src")
167
+ idx=$(in_index "$slug")
168
+ rd="ok"; [[ "$st" == "missing" ]] && rd="MISSING"
169
+ printf " %-22s %-22s %-8s %-7s %-8s\n" "$slug" "$src" "$rd" "$idx" "$st"
170
+ case "$st" in
171
+ missing) n_missing=$((n_missing+1)) ;;
172
+ stale) n_stale=$((n_stale+1)) ;;
173
+ ok) n_ok=$((n_ok+1)) ;;
174
+ esac
175
+ [[ "$idx" == "no" ]] && n_noindex=$((n_noindex+1))
176
+ done <<< "$MODULES"
177
+
178
+ echo " ──────────────────────────────────────────────────────────────"
179
+ echo " Tổng: $((n_missing+n_stale+n_ok)) module · $n_missing thiếu README · $n_stale lỗi thời (stale) · $n_noindex chưa có trong INDEX"
180
+ if [[ "$n_missing" -gt 0 || "$n_stale" -gt 0 ]]; then
181
+ echo ""
182
+ echo " → Để SINH/CẬP NHẬT nội dung: mở Cursor/OpenCode và yêu cầu chạy skill 'docs-maintenance'."
183
+ fi
184
+ echo ""
185
+ exit 0
@@ -398,6 +398,25 @@ else
398
398
  fi
399
399
  fi
400
400
 
401
+ # === Bước 4: Đăng ký project vào registry của mỗi install home đang tồn tại ===
402
+ # Registry gắn với install home CỐ ĐỊNH (qua marker .openmoneta-version), KHÔNG dùng
403
+ # $OPENMONETA_HOME (vì khi chạy từ repo kit, biến này trỏ về repo dir).
404
+ register_in_home() {
405
+ local home="$1"
406
+ [[ -f "$home/.openmoneta-version" ]] || return 0
407
+ local reg="$home/.openmoneta-projects"
408
+ local abs
409
+ abs="$(cd "$PROJECT_DIR" && pwd -P)"
410
+ if [[ -f "$reg" ]] && grep -qFx "$abs" "$reg"; then
411
+ return 0
412
+ fi
413
+ echo "$abs" >> "$reg"
414
+ CREATED+=("registry: $reg ← $abs")
415
+ }
416
+
417
+ register_in_home "$HOME/.cursor"
418
+ register_in_home "${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
419
+
401
420
  # === Báo cáo ===
402
421
  echo ""
403
422
  echo "================================================"
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: docs-maintenance
3
+ description: "ON-DEMAND ONLY: dùng khi user yêu cầu tạo docs còn thiếu / backfill module README / refresh docs lỗi thời / audit docs / bảo trì docs dự án cũ. KHÔNG tự trigger trong quy trình bình thường."
4
+ ---
5
+
6
+ # Docs Maintenance
7
+
8
+ Quét cả repo một lượt để **backfill module README còn thiếu** và **refresh README đã lỗi thời** so với code, rồi **đồng bộ `docs/INDEX.md`** (Modules hiện có + Token Routing).
9
+
10
+ Dùng khi onboard/bảo trì **dự án cũ** đã tích lũy nợ doc (code đụng nhiều nhưng README không được cập nhật), hoặc khi user yêu cầu rõ. Đây là phiên bản "chạy hàng loạt cho cả repo" của skill `module-architect` (vốn chạy từng module/session).
11
+
12
+ ## Iron Law
13
+
14
+ ```
15
+ CHỈ VIẾT NHỮNG GÌ VERIFY ĐƯỢC TRONG CODE. KHÔNG BỊA API / PATH / DEPENDENCY.
16
+ ```
17
+
18
+ Stale docs còn tệ hơn không có docs — session sau AI đọc sai, làm sai. Mỗi dòng trong README phải truy về một symbol/import có thật trong source. Không chắc → mô tả mức ý định cao, KHÔNG bịa signature.
19
+
20
+ ## Ranh giới (Lean — KHÔNG làm gì)
21
+
22
+ - **KHÔNG** sinh bộ doc tổng quan kiểu khác (`system-architecture.md`, `project-roadmap.md`, `codebase-summary.md`...). Mô hình OM là **per-module README 3 sections + `docs/INDEX.md`**, đủ để navigate token-aware.
23
+ - **KHÔNG** sửa code. Chỉ ghi file dưới `docs/`.
24
+ - **KHÔNG** viết lại từ đầu README còn đúng — chỉ sửa phần lệch.
25
+ - Cấu trúc README 3 sections theo skill [`module-architect`](../module-architect/SKILL.md) — KHÔNG lặp lại spec ở đây, đọc nó để biết format chuẩn.
26
+
27
+ ## 3 Modes
28
+
29
+ | Mode | Xử lý | Ghi file? |
30
+ |---|---|---|
31
+ | `audit` | Report: liệt kê module MISSING / STALE / GHOST | Không |
32
+ | `init` | **Tạo MISSING + refresh STALE** (full backfill) | Có |
33
+ | `sync` | Chỉ refresh STALE | Có |
34
+
35
+ > User nói "tạo docs thiếu" / "bảo trì docs cũ" → mặc định `init`. Nói "chỉ xem thiếu gì" → `audit`. Nói "cập nhật doc cho khớp code" → `sync`.
36
+
37
+ ## Phân loại trạng thái module
38
+
39
+ - **MISSING**: module có source nhưng KHÔNG có `docs/modules/<slug>/README.md`.
40
+ - **STALE**: README có rồi nhưng code module đổi SAU lần doc update cuối (so commit git source vs README; fallback mtime). Heuristic → phải đọc xác nhận trước khi sửa.
41
+ - **GHOST**: `docs/modules/<slug>/` hoặc dòng INDEX trỏ tới source/module không còn tồn tại.
42
+
43
+ ## Workflow
44
+
45
+ ### 1. Lấy danh sách + soi ghost (deterministic)
46
+
47
+ ```bash
48
+ bash ~/.cursor/scripts/docs-audit.sh --worklist # MISSING/STALE/ok (TSV: slug\tdocs\tsource\tstatus)
49
+ bash ~/.cursor/scripts/docs-audit.sh --validate # liệt kê GHOST (doc/INDEX trỏ source/module đã mất)
50
+ ```
51
+
52
+ (OpenCode: `~/.config/opencode/scripts/docs-audit.sh`.) **BẮT BUỘC chạy cả `--validate`** ở mọi mode để không bỏ sót ghost. Mode `audit` in cả 2 kết quả rồi dừng, không qua các bước tạo/sửa dưới.
53
+
54
+ ### 2. (Repo lớn) đọc song song
55
+
56
+ Nếu nhiều module MISSING/STALE → delegate subagent `explore` đọc song song từng cụm module và trả summary (Public API thật + imports). Repo nhỏ → đọc trực tiếp.
57
+
58
+ ### 3. MISSING → tạo mới
59
+
60
+ Cho mỗi module status `missing`:
61
+
62
+ 1. Đọc source thật (`sourcePath`) — entry point + các export.
63
+ 2. **Verify trước khi viết**: grep tên hàm/class/route trong source để chắc nó tồn tại + đúng signature.
64
+ 3. Viết `docs/modules/<slug>/README.md` 3 sections (Trách nhiệm + Public API + Dependencies) theo `module-architect`.
65
+
66
+ ### 4. STALE → refresh (không viết lại từ đầu)
67
+
68
+ Cho mỗi module status `stale`:
69
+
70
+ 1. Đọc source + README cũ.
71
+ 2. Đối chiếu **Public API**: export mới chưa có trong README → thêm; symbol đã xóa/đổi signature → sửa/xóa.
72
+ 3. Đối chiếu **Dependencies**: import mới → thêm; import đã bỏ → xóa.
73
+ 4. Giữ nguyên phần còn đúng. Chỉ sửa phần lệch.
74
+
75
+ ### 5. Sync `docs/INDEX.md`
76
+
77
+ - Module mới → thêm dòng vào bảng "Modules hiện có" + **3-5 keyword (VN+EN)** vào bảng "Token Routing" (bắt buộc, để hook docs-first + token-aware reading hoạt động).
78
+ - **GHOST** (từ `--validate` ở Bước 1) → LIỆT KÊ rõ từng ghost cho user. **KHÔNG tự xóa** — map slug→source là heuristic, có thể là module vừa đổi tên/di chuyển. Hỏi user bằng `AskQuestion` (xóa doc/dòng INDEX, hay map lại sang source mới); chỉ thao tác sau khi user xác nhận từng cái.
79
+
80
+ ### 6. Report cuối
81
+
82
+ In tóm tắt: `created / updated / skipped` + cảnh báo (module không verify được, ghost). Gợi ý chạy lại `openmoneta docs --validate` để xác nhận sạch.
83
+
84
+ ## Bảng Rationalization (cái cớ → sự thật)
85
+
86
+ | Cái cớ | Sự thật |
87
+ |---|---|
88
+ | "README cũ chắc vẫn đúng, khỏi đọc source" | Stale là lý do bạn được gọi. Phải đọc source verify, không tin README cũ. |
89
+ | "Đoán signature cho nhanh" | Bịa API = doc sai = tệ hơn không có. Grep verify hoặc bỏ. |
90
+ | "Viết lại cả README cho gọn" | Phá nội dung đang đúng + tốn token. Chỉ sửa phần lệch. |
91
+ | "Thêm doc tổng quan cho đầy đủ" | Trái Lean. OM chỉ module README + INDEX. |
92
+ | "Module mới khỏi thêm Token Routing" | Không có keyword → AI không tìm thấy module → đọc loạn. Bắt buộc thêm. |
93
+
94
+ ## Red Flags — DỪNG
95
+
96
+ - Đang định viết `funcName()` mà chưa thấy nó trong source.
97
+ - Đang refresh STALE mà chưa mở source, chỉ sửa theo trí nhớ.
98
+ - Đang định sinh `system-architecture.md` / `codebase-summary.md`.
99
+ - Đang định xóa GHOST mà chưa hỏi user.
100
+ - Tạo module mới trong INDEX nhưng quên Token Routing keyword.
101
+
102
+ ## Examples
103
+
104
+ - User: "Dự án này nhiều module thiếu docs, tạo giúp" → `init`: chạy worklist → tạo mọi MISSING + refresh STALE → sync INDEX → report.
105
+ - User: "Doc còn khớp code không?" → `audit`: chỉ report.
106
+ - User: "Cập nhật doc 3 module vừa refactor" → `sync` (giới hạn các slug đó).
package/src/cli.js CHANGED
@@ -19,6 +19,8 @@ async function cli(pkgRoot) {
19
19
  case "check":
20
20
  case "--check":
21
21
  return require("./commands/check").run(args)
22
+ case "docs":
23
+ return require("./commands/docs").run(args)
22
24
  case "--version":
23
25
  case "-v":
24
26
  console.log(`v${getLocalVersion() || "unknown"}`)
@@ -52,6 +54,9 @@ Biến Cursor IDE / OpenCode thành team developer hoàn chỉnh.
52
54
 
53
55
  openmoneta check Kiểm tra version mới nhất từ npm
54
56
 
57
+ openmoneta docs Audit module README (thiếu/lỗi thời) + sync INDEX
58
+ openmoneta docs --validate Phát hiện doc trỏ module/source không tồn tại (ghost)
59
+
55
60
  openmoneta uninstall Gỡ cài đặt
56
61
 
57
62
  Trạng thái: Cursor: ${isInstalled("cursor") ? "đã cài" : "chưa cài"} | OpenCode: ${isInstalled("opencode") ? "đã cài" : "chưa cài"}
@@ -0,0 +1,37 @@
1
+ const { execSync } = require("node:child_process")
2
+ const path = require("node:path")
3
+ const fs = require("node:fs")
4
+ const { getPkgRoot, isWindows } = require("../lib/paths")
5
+
6
+ async function run(args) {
7
+ const validate = args.includes("--validate")
8
+ const worklist = args.includes("--worklist")
9
+
10
+ const pkgRoot = getPkgRoot()
11
+ const script = path.join(pkgRoot, "scripts", "docs-audit.sh")
12
+ const cwd = process.cwd()
13
+
14
+ if (!fs.existsSync(script)) {
15
+ console.error(`\n ❌ Không tìm thấy script: ${script}`)
16
+ process.exit(1)
17
+ }
18
+
19
+ let mode = "--report"
20
+ if (validate) mode = "--validate"
21
+ else if (worklist) mode = "--worklist"
22
+
23
+ try {
24
+ execSync(`bash "${script}" ${mode}`, {
25
+ stdio: "inherit",
26
+ cwd,
27
+ shell: isWindows() ? true : undefined,
28
+ })
29
+ } catch (err) {
30
+ if (isWindows()) {
31
+ console.log(`\n ⚠ Cần Git Bash để chạy audit trên Windows: https://git-scm.com/download/win`)
32
+ }
33
+ process.exit(typeof err.status === "number" ? err.status : 1)
34
+ }
35
+ }
36
+
37
+ module.exports = { run }
@@ -1,12 +1,180 @@
1
1
  const { execSync } = require("node:child_process")
2
2
  const path = require("node:path")
3
- const { getPkgRoot, isInstalled, isWindows, runScript } = require("../lib/paths")
3
+ const fs = require("node:fs")
4
+ const { getPkgRoot, isInstalled, isWindows, runScript, getProjectRegistry } = require("../lib/paths")
4
5
  const { getLocalVersion, getLatestVersion } = require("../lib/version")
5
6
 
7
+ function isOpenmonetaProject(dir) {
8
+ try {
9
+ fs.accessSync(path.join(dir, "docs", "INDEX.md"))
10
+ return true
11
+ } catch {
12
+ return false
13
+ }
14
+ }
15
+
16
+ function readRegistry() {
17
+ const seen = new Set()
18
+ for (const target of ["cursor", "opencode"]) {
19
+ let content
20
+ try {
21
+ content = fs.readFileSync(getProjectRegistry(target), "utf8")
22
+ } catch {
23
+ continue
24
+ }
25
+ for (const line of content.split("\n")) {
26
+ const p = line.trim()
27
+ if (!p) continue
28
+ try {
29
+ seen.add(fs.realpathSync(p))
30
+ } catch {
31
+ // path không còn tồn tại → bỏ qua, prune sẽ dọn
32
+ }
33
+ }
34
+ }
35
+ return [...seen]
36
+ }
37
+
38
+ function pruneRegistry() {
39
+ let pruned = 0
40
+ for (const target of ["cursor", "opencode"]) {
41
+ const reg = getProjectRegistry(target)
42
+ let content
43
+ try {
44
+ content = fs.readFileSync(reg, "utf8")
45
+ } catch {
46
+ continue
47
+ }
48
+ const kept = []
49
+ for (const line of content.split("\n")) {
50
+ const p = line.trim()
51
+ if (!p) continue
52
+ if (isOpenmonetaProject(p)) kept.push(p)
53
+ else pruned++
54
+ }
55
+ fs.writeFileSync(reg, kept.length ? kept.join("\n") + "\n" : "")
56
+ }
57
+ return pruned
58
+ }
59
+
60
+ const SCAN_PRUNE_DIRS = new Set([
61
+ "node_modules",
62
+ ".git",
63
+ "Library",
64
+ ".Trash",
65
+ ".cache",
66
+ ".npm",
67
+ ".cargo",
68
+ ".gradle",
69
+ ".venv",
70
+ "dist",
71
+ "build",
72
+ ])
73
+
74
+ function findProjects(root, maxDepth) {
75
+ const out = []
76
+ function walk(dir, depth) {
77
+ if (depth > maxDepth) return
78
+ if (isOpenmonetaProject(dir)) {
79
+ try {
80
+ fs.accessSync(path.join(dir, "AGENTS.md"))
81
+ out.push(dir)
82
+ } catch {
83
+ // thiếu AGENTS.md → không tính là OM project khi scan
84
+ }
85
+ }
86
+ let entries
87
+ try {
88
+ entries = fs.readdirSync(dir, { withFileTypes: true })
89
+ } catch {
90
+ return
91
+ }
92
+ for (const e of entries) {
93
+ if (!e.isDirectory()) continue
94
+ if (SCAN_PRUNE_DIRS.has(e.name) || e.name.startsWith(".")) continue
95
+ walk(path.join(dir, e.name), depth + 1)
96
+ }
97
+ }
98
+ walk(root, 0)
99
+ return out
100
+ }
101
+
102
+ function scanSeed(root, pkgRoot) {
103
+ let absRoot
104
+ try {
105
+ absRoot = fs.realpathSync(root)
106
+ } catch {
107
+ console.log(` [!] Scan root không tồn tại: ${root}`)
108
+ return
109
+ }
110
+ const targets = ["cursor", "opencode"].filter((t) => isInstalled(t))
111
+ if (targets.length === 0) {
112
+ console.log(` [!] Chưa có install home nào để seed registry.`)
113
+ return
114
+ }
115
+ let kitRoot
116
+ try {
117
+ kitRoot = fs.realpathSync(pkgRoot)
118
+ } catch {
119
+ kitRoot = pkgRoot
120
+ }
121
+ const uniq = [
122
+ ...new Set(
123
+ findProjects(absRoot, 5)
124
+ .map((p) => {
125
+ try {
126
+ return fs.realpathSync(p)
127
+ } catch {
128
+ return p
129
+ }
130
+ })
131
+ .filter((p) => p !== kitRoot)
132
+ ),
133
+ ]
134
+ let seeded = 0
135
+ for (const t of targets) {
136
+ const reg = getProjectRegistry(t)
137
+ let existing = new Set()
138
+ try {
139
+ existing = new Set(
140
+ fs
141
+ .readFileSync(reg, "utf8")
142
+ .split("\n")
143
+ .map((l) => l.trim())
144
+ .filter(Boolean)
145
+ )
146
+ } catch {
147
+ // registry chưa tồn tại → tạo mới khi append
148
+ }
149
+ const toAdd = uniq.filter((p) => !existing.has(p))
150
+ if (toAdd.length) {
151
+ fs.appendFileSync(reg, toAdd.map((p) => `${p}\n`).join(""))
152
+ seeded += toAdd.length
153
+ }
154
+ }
155
+ console.log(` Scan: thấy ${uniq.length} OM project, seed ${seeded} entry mới.`)
156
+ }
157
+
158
+ function syncProject(initScript, dir) {
159
+ if (isWindows()) {
160
+ execSync(`bash "${initScript}" "${dir}"`, { stdio: "inherit", shell: true })
161
+ } else {
162
+ execSync(`bash "${initScript}" "${dir}"`, { stdio: "inherit" })
163
+ }
164
+ }
165
+
6
166
  async function run(args) {
7
167
  const checkOnly = args.includes("--check")
8
168
  const autoYes = args.includes("--yes") || args.includes("-y")
9
169
  const skipProjects = args.includes("--skip-projects")
170
+ const scanIdx = args.indexOf("--scan")
171
+ const scan = scanIdx !== -1
172
+ let scanRoot = process.env.HOME || process.env.USERPROFILE || "."
173
+ if (scan) {
174
+ const next = args[scanIdx + 1]
175
+ if (next && !next.startsWith("-")) scanRoot = next
176
+ }
177
+ const allProjects = args.includes("--all-projects") || scan
10
178
  const force = args.includes("--force")
11
179
 
12
180
  const local = getLocalVersion()
@@ -76,24 +244,61 @@ async function run(args) {
76
244
  if (!skipProjects) {
77
245
  const initScript = path.join(pkgRoot, "scripts", "init-project.sh")
78
246
  const cwd = process.cwd()
79
- const docsIndex = path.join(cwd, "docs", "INDEX.md")
80
247
 
81
- try {
82
- require("node:fs").accessSync(docsIndex)
83
- console.log(`\n ▶ Syncing project: ${path.basename(cwd)}`)
84
- if (isWindows()) {
85
- execSync(`bash "${initScript}" "${cwd}"`, { stdio: "inherit", shell: true })
86
- } else {
87
- execSync(`bash "${initScript}" "${cwd}"`, { stdio: "inherit" })
248
+ if (scan) {
249
+ console.log(`\n ▶ --scan: dò OM project để seed registry...`)
250
+ scanSeed(scanRoot, pkgRoot)
251
+ }
252
+
253
+ if (allProjects) {
254
+ const pruned = pruneRegistry()
255
+ if (pruned > 0) console.log(`\n ℹ Registry: prune ${pruned} entry chết.`)
256
+
257
+ const projects = readRegistry()
258
+ if (isOpenmonetaProject(cwd) && !projects.includes(fs.realpathSync(cwd))) {
259
+ projects.push(fs.realpathSync(cwd))
88
260
  }
89
- console.log(`\n ✅ Project synced.`)
90
- } catch (err) {
91
- if (isWindows()) {
92
- console.log(`\n ⚠ Không thể sync project (cần Git Bash). Cài tại: https://git-scm.com/download/win`)
93
- console.log(` Sau đó chạy thủ công: bash "${initScript}" "${cwd}"`)
261
+
262
+ if (projects.length === 0) {
263
+ console.log(`\n ℹ Registry trống. Init project trước: openmoneta init`)
94
264
  } else {
95
- console.log(`\n Không phát hiện project OpenMoneta ở thư mục hiện tại.`)
96
- console.log(` Sync thủ công: openmoneta init`)
265
+ console.log(`\n Syncing ${projects.length} project (registry)...`)
266
+ let ok = 0
267
+ let fail = 0
268
+ for (const dir of projects) {
269
+ try {
270
+ console.log(`\n ▶ ${path.basename(dir)} (${dir})`)
271
+ syncProject(initScript, dir)
272
+ ok++
273
+ } catch {
274
+ console.error(` ⚠ Sync fail: ${dir}`)
275
+ fail++
276
+ }
277
+ }
278
+ console.log(`\n ✅ Projects synced: ${ok}${fail ? `, failed: ${fail}` : ""}`)
279
+ }
280
+ } else {
281
+ try {
282
+ fs.accessSync(path.join(cwd, "docs", "INDEX.md"))
283
+ console.log(`\n ▶ Syncing project: ${path.basename(cwd)}`)
284
+ syncProject(initScript, cwd)
285
+ console.log(`\n ✅ Project synced.`)
286
+ } catch (err) {
287
+ if (isWindows()) {
288
+ console.log(`\n ⚠ Không thể sync project (cần Git Bash). Cài tại: https://git-scm.com/download/win`)
289
+ console.log(` Sau đó chạy thủ công: bash "${initScript}" "${cwd}"`)
290
+ } else {
291
+ console.log(`\n ℹ Không phát hiện project OpenMoneta ở thư mục hiện tại.`)
292
+ console.log(` Sync thủ công: openmoneta init`)
293
+ }
294
+ }
295
+
296
+ const others = readRegistry().filter(
297
+ (p) => isOpenmonetaProject(p) && !(isOpenmonetaProject(cwd) && p === fs.realpathSync(cwd))
298
+ )
299
+ if (others.length > 0) {
300
+ console.log(`\n ℹ Có ${others.length} project khác trong registry chưa sync. Sync tất cả:`)
301
+ console.log(` openmoneta update --all-projects`)
97
302
  }
98
303
  }
99
304
  }
@@ -101,4 +306,7 @@ async function run(args) {
101
306
  console.log(`\n 🎉 Hoàn tất. Restart Cursor/OpenCode để áp dụng.`)
102
307
  }
103
308
 
104
- module.exports = { run }
309
+ module.exports = {
310
+ run,
311
+ _internal: { isOpenmonetaProject, readRegistry, pruneRegistry, findProjects, scanSeed },
312
+ }
package/src/lib/paths.js CHANGED
@@ -19,6 +19,11 @@ function versionFilePath(target) {
19
19
  return path.join(CURSOR_DIR, ".openmoneta-version")
20
20
  }
21
21
 
22
+ function getProjectRegistry(target) {
23
+ if (target === "opencode") return path.join(OPENCODE_DIR, ".openmoneta-projects")
24
+ return path.join(CURSOR_DIR, ".openmoneta-projects")
25
+ }
26
+
22
27
  function installedVersion(target) {
23
28
  try {
24
29
  return fs.readFileSync(versionFilePath(target), "utf8").trim()
@@ -52,6 +57,7 @@ module.exports = {
52
57
  OPENCODE_DIR,
53
58
  REPO_DIR,
54
59
  versionFilePath,
60
+ getProjectRegistry,
55
61
  installedVersion,
56
62
  isInstalled,
57
63
  isWindows,
@@ -87,7 +87,7 @@
87
87
 
88
88
  **Core conditional**: `safe-push` (Bước 6 — chỉ khi user yêu cầu push).
89
89
 
90
- **On-demand (chỉ trigger khi user yêu cầu rõ)**: `security-checklist`, `test-strategy`, `automated-testing`, `auth-bypass-testing`, `ui-test-loop`.
90
+ **On-demand (chỉ trigger khi user yêu cầu rõ)**: `security-checklist`, `test-strategy`, `automated-testing`, `auth-bypass-testing`, `ui-test-loop`, `docs-maintenance`.
91
91
 
92
92
  ## Hooks enforce
93
93
 
package/update.ps1 CHANGED
@@ -6,6 +6,8 @@
6
6
  # .\update.ps1 -Yes -Force # global + auto-sync, luôn cài lại
7
7
  # .\update.ps1 -Check # chỉ check, không cài
8
8
  # .\update.ps1 -Project C:\path # global + sync thêm project chỉ định
9
+ # .\update.ps1 -AllProjects # global + sync MỌI project trong registry
10
+ # .\update.ps1 -Scan [-ScanRoot path] # dò OM project (mặc định $HOME) → seed registry → sync all
9
11
  # .\update.ps1 -SkipProjects # global only (old behavior)
10
12
 
11
13
  param(
@@ -13,6 +15,9 @@ param(
13
15
  [switch]$Force,
14
16
  [switch]$Check,
15
17
  [switch]$SkipProjects,
18
+ [switch]$AllProjects,
19
+ [switch]$Scan,
20
+ [string]$ScanRoot,
16
21
  [string[]]$Project
17
22
  )
18
23
 
@@ -83,6 +88,88 @@ function Sync-Project {
83
88
  return $true
84
89
  }
85
90
 
91
+ # Helper: registry files (gắn với install home cố định, không dùng OPENMONETA_HOME)
92
+ function Get-RegistryFiles {
93
+ $cursorReg = Join-Path $env:USERPROFILE ".cursor\.openmoneta-projects"
94
+ $openCodeBase = if ($env:XDG_CONFIG_HOME) { "$env:XDG_CONFIG_HOME\opencode" } else { "$env:USERPROFILE\.config\opencode" }
95
+ $openCodeReg = Join-Path $openCodeBase ".openmoneta-projects"
96
+ return @($cursorReg, $openCodeReg)
97
+ }
98
+
99
+ # Helper: đọc registry (merge 2 home, dedup)
100
+ function Read-Registry {
101
+ $seen = [System.Collections.Generic.HashSet[string]]::new()
102
+ foreach ($f in Get-RegistryFiles) {
103
+ if (-not (Test-Path $f)) { continue }
104
+ foreach ($line in (Get-Content $f)) {
105
+ $p = $line.Trim()
106
+ if (-not $p) { continue }
107
+ $abs = (Resolve-Path $p -ErrorAction SilentlyContinue).Path
108
+ if ($abs) { [void]$seen.Add($abs) }
109
+ }
110
+ }
111
+ return @($seen)
112
+ }
113
+
114
+ # Helper: prune entry chết khỏi registry
115
+ function Invoke-PruneRegistry {
116
+ $pruned = 0
117
+ foreach ($f in Get-RegistryFiles) {
118
+ if (-not (Test-Path $f)) { continue }
119
+ $kept = @()
120
+ foreach ($line in (Get-Content $f)) {
121
+ $p = $line.Trim()
122
+ if (-not $p) { continue }
123
+ if (Test-OpenMonetaProject $p) { $kept += $p } else { $pruned++ }
124
+ }
125
+ Set-Content -Path $f -Value $kept
126
+ }
127
+ return $pruned
128
+ }
129
+
130
+ # Helper: scan tìm OM project (docs\INDEX.md + AGENTS.md) → seed registry mỗi home tồn tại
131
+ function Invoke-ScanSeed {
132
+ $root = if ($ScanRoot) { $ScanRoot } else { $env:USERPROFILE }
133
+ $absRoot = (Resolve-Path $root -ErrorAction SilentlyContinue).Path
134
+ if (-not $absRoot) { Write-Host " [!] Scan root không tồn tại: $root"; return }
135
+
136
+ $homes = @()
137
+ if (Test-Path (Join-Path $env:USERPROFILE ".cursor\.openmoneta-version")) {
138
+ $homes += (Join-Path $env:USERPROFILE ".cursor\.openmoneta-projects")
139
+ }
140
+ $ocBase = if ($env:XDG_CONFIG_HOME) { "$env:XDG_CONFIG_HOME\opencode" } else { "$env:USERPROFILE\.config\opencode" }
141
+ if (Test-Path (Join-Path $ocBase ".openmoneta-version")) {
142
+ $homes += (Join-Path $ocBase ".openmoneta-projects")
143
+ }
144
+ if ($homes.Count -eq 0) { Write-Host " [!] Chưa có install home nào để seed registry."; return }
145
+
146
+ $prune = @('node_modules', '.git', 'Library', '.Trash', '.cache', '.npm', '.cargo', '.gradle', '.venv', 'dist', 'build')
147
+ Write-Host " Scanning '$absRoot' (depth ~6)..."
148
+ $indexes = Get-ChildItem -Path $absRoot -Recurse -Depth 6 -Filter "INDEX.md" -File -ErrorAction SilentlyContinue |
149
+ Where-Object { (Split-Path $_.DirectoryName -Leaf) -eq "docs" }
150
+
151
+ $found = 0; $seeded = 0
152
+ $absKit = (Resolve-Path $RepoDir -ErrorAction SilentlyContinue).Path
153
+ foreach ($idx in $indexes) {
154
+ $proj = Split-Path (Split-Path $idx.FullName -Parent) -Parent
155
+ $skip = $false
156
+ foreach ($d in $prune) { if ($proj -like "*\$d\*" -or $proj -like "*\$d") { $skip = $true; break } }
157
+ if ($skip) { continue }
158
+ if (-not (Test-Path (Join-Path $proj "AGENTS.md"))) { continue }
159
+ $abs = (Resolve-Path $proj -ErrorAction SilentlyContinue).Path
160
+ if (-not $abs -or $abs -eq $absKit) { continue }
161
+ $found++
162
+ foreach ($reg in $homes) {
163
+ $existing = @()
164
+ if (Test-Path $reg) { $existing = Get-Content $reg | ForEach-Object { $_.Trim() } | Where-Object { $_ } }
165
+ if ($existing -contains $abs) { continue }
166
+ Add-Content -Path $reg -Value $abs
167
+ $seeded++
168
+ }
169
+ }
170
+ Write-Host " Scan: thấy $found OM project, seed $seeded entry mới vào registry."
171
+ }
172
+
86
173
  # Helper: collect projects to sync
87
174
  function Get-OpenMonetaProjects {
88
175
  $found = @()
@@ -106,9 +193,34 @@ function Get-OpenMonetaProjects {
106
193
  }
107
194
  }
108
195
 
196
+ # Add registry projects khi -AllProjects
197
+ if ($AllProjects) {
198
+ foreach ($rp in (Read-Registry)) {
199
+ if (-not (Test-OpenMonetaProject $rp)) { continue }
200
+ if ($found -notcontains $rp) { $found += $rp }
201
+ }
202
+ }
203
+
109
204
  return $found
110
205
  }
111
206
 
207
+ # Helper: nhắc user còn project trong registry chưa sync (chỉ khi không -AllProjects)
208
+ function Write-RegistryHint {
209
+ param([string[]]$Synced)
210
+ if ($AllProjects) { return }
211
+ $count = 0
212
+ foreach ($rp in (Read-Registry)) {
213
+ if (-not (Test-OpenMonetaProject $rp)) { continue }
214
+ if ($Synced -contains $rp) { continue }
215
+ $count++
216
+ }
217
+ if ($count -gt 0) {
218
+ Write-Host ""
219
+ Write-Host "ℹ Có $count project khác trong registry chưa sync. Sync tất cả:"
220
+ Write-Host " .\update.ps1 -AllProjects"
221
+ }
222
+ }
223
+
112
224
  # === Step 1: Git pull ===
113
225
  $Status = git status --porcelain 2>$null
114
226
  if ($Status) {
@@ -196,6 +308,20 @@ if ($SkipProjects) {
196
308
  exit 0
197
309
  }
198
310
 
311
+ if ($Scan) {
312
+ $AllProjects = $true
313
+ Write-Host ""
314
+ Write-Host "==> -Scan: dò OM project để seed registry..."
315
+ Invoke-ScanSeed
316
+ }
317
+
318
+ if ($AllProjects) {
319
+ Write-Host ""
320
+ Write-Host "==> -AllProjects: dọn registry + gom mọi project đã đăng ký..."
321
+ $pruned = Invoke-PruneRegistry
322
+ if ($pruned -gt 0) { Write-Host " (registry: prune $pruned entry chết)" }
323
+ }
324
+
199
325
  $Projects = Get-OpenMonetaProjects
200
326
 
201
327
  if ($Projects.Count -eq 0) {
@@ -209,6 +335,7 @@ if ($Projects.Count -eq 0) {
209
335
  Write-Host "Hoặc sync thủ công từng project:"
210
336
  Write-Host " bash $RepoDir\scripts\init-project.sh <path>"
211
337
  }
338
+ Write-RegistryHint -Synced $Projects
212
339
  exit 0
213
340
  }
214
341
 
@@ -250,5 +377,6 @@ if ($SyncFail -gt 0) {
250
377
  Write-Host " Failed: $SyncFail"
251
378
  }
252
379
  Write-Host "================================================"
380
+ Write-RegistryHint -Synced $Projects
253
381
  Write-Host ""
254
382
  Write-Host "Restart Cursor để áp dụng changes."
package/update.sh CHANGED
@@ -8,6 +8,8 @@
8
8
  # bash update.sh --yes --force # global + auto-sync, luôn cài lại
9
9
  # bash update.sh --check # chỉ check, không cài
10
10
  # bash update.sh --project /path # global + sync thêm project chỉ định
11
+ # bash update.sh --all-projects # global + sync MỌI project trong registry
12
+ # bash update.sh --scan [root] # dò OM project (mặc định $HOME) → seed registry → sync all
11
13
  # bash update.sh --skip-projects # global only (old behavior)
12
14
 
13
15
  set -euo pipefail
@@ -20,30 +22,43 @@ AUTO_YES=0
20
22
  SKIP_PROJECTS=0
21
23
  FORCE=0
22
24
  CHECK_ONLY=0
25
+ ALL_PROJECTS=0
26
+ SCAN=0
27
+ SCAN_ROOT=""
23
28
  PROJECT_PATHS=()
24
29
 
25
- for arg in "$@"; do
26
- case "$arg" in
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
27
32
  --yes|-y) AUTO_YES=1 ;;
28
33
  --force) FORCE=1 ;;
29
34
  --check) CHECK_ONLY=1 ;;
35
+ --all-projects) ALL_PROJECTS=1 ;;
30
36
  --skip-projects) SKIP_PROJECTS=1 ;;
37
+ --scan)
38
+ SCAN=1
39
+ ALL_PROJECTS=1
40
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
41
+ SCAN_ROOT="$2"
42
+ shift
43
+ fi
44
+ ;;
31
45
  --help|-h)
32
- grep '^#' "$0" | sed 's/^# \?//' | head -17
46
+ grep '^#' "$0" | sed 's/^# \?//' | head -18
33
47
  exit 0
34
48
  ;;
35
49
  --project)
36
- shift
37
- if [[ -n "${1:-}" && "${1:-}" != -* ]]; then
38
- PROJECT_PATHS+=("$1")
50
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
51
+ PROJECT_PATHS+=("$2")
52
+ shift
39
53
  fi
40
54
  ;;
41
55
  *)
42
- if [[ "$arg" != -* ]]; then
43
- PROJECT_PATHS+=("$arg")
56
+ if [[ "$1" != -* ]]; then
57
+ PROJECT_PATHS+=("$1")
44
58
  fi
45
59
  ;;
46
60
  esac
61
+ shift
47
62
  done
48
63
 
49
64
  cd "$REPO_DIR"
@@ -108,6 +123,80 @@ sync_project() {
108
123
  fi
109
124
  }
110
125
 
126
+ # === Helper: registry files (gắn với install home cố định, không dùng OPENMONETA_HOME) ===
127
+ registry_files() {
128
+ printf '%s\n' \
129
+ "$HOME/.cursor/.openmoneta-projects" \
130
+ "${XDG_CONFIG_HOME:-$HOME/.config}/opencode/.openmoneta-projects"
131
+ }
132
+
133
+ # === Helper: đọc registry (merge 2 home, bỏ dòng trống, dedup) ===
134
+ read_registry() {
135
+ local f
136
+ while IFS= read -r f; do
137
+ [[ -f "$f" ]] && cat "$f"
138
+ done < <(registry_files) | awk 'NF' | sort -u
139
+ }
140
+
141
+ # === Helper: prune entry chết khỏi registry (path mất / không còn OpenMoneta project) ===
142
+ prune_registry() {
143
+ local f tmp pruned=0 line
144
+ while IFS= read -r f; do
145
+ [[ -f "$f" ]] || continue
146
+ tmp=$(mktemp)
147
+ while IFS= read -r line; do
148
+ [[ -z "$line" ]] && continue
149
+ if is_openmoneta_project "$line"; then
150
+ echo "$line" >> "$tmp"
151
+ else
152
+ pruned=$((pruned + 1))
153
+ fi
154
+ done < "$f"
155
+ mv "$tmp" "$f"
156
+ done < <(registry_files)
157
+ [[ $pruned -gt 0 ]] && echo " (registry: prune $pruned entry chết)"
158
+ return 0
159
+ }
160
+
161
+ # === Helper: scan tìm OM project (docs/INDEX.md + AGENTS.md) → seed registry mỗi home tồn tại ===
162
+ scan_seed() {
163
+ local root="${SCAN_ROOT:-$HOME}"
164
+ local abs_root
165
+ abs_root="$(cd "$root" 2>/dev/null && pwd -P)" || { echo " [!] Scan root không tồn tại: $root"; return 0; }
166
+
167
+ local homes=()
168
+ [[ -f "$HOME/.cursor/.openmoneta-version" ]] && homes+=("$HOME/.cursor/.openmoneta-projects")
169
+ [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/opencode/.openmoneta-version" ]] && \
170
+ homes+=("${XDG_CONFIG_HOME:-$HOME/.config}/opencode/.openmoneta-projects")
171
+ if [[ ${#homes[@]} -eq 0 ]]; then
172
+ echo " [!] Chưa có install home nào để seed registry."
173
+ return 0
174
+ fi
175
+
176
+ echo " Scanning '$abs_root' (maxdepth project ~5)..."
177
+ local found=0 seeded=0 idx proj abs reg
178
+ while IFS= read -r idx; do
179
+ proj="$(dirname "$(dirname "$idx")")"
180
+ [[ -f "$proj/AGENTS.md" ]] || continue
181
+ abs="$(cd "$proj" 2>/dev/null && pwd -P)" || continue
182
+ [[ -z "$abs" || "$abs" == "$REPO_DIR" ]] && continue
183
+ found=$((found + 1))
184
+ for reg in "${homes[@]}"; do
185
+ if [[ -f "$reg" ]] && grep -qFx "$abs" "$reg" 2>/dev/null; then
186
+ continue
187
+ fi
188
+ echo "$abs" >> "$reg"
189
+ seeded=$((seeded + 1))
190
+ done
191
+ done < <(find "$abs_root" -maxdepth 7 \
192
+ \( -name node_modules -o -name .git -o -name Library -o -name .Trash \
193
+ -o -name .cache -o -name .npm -o -name .cargo -o -name .gradle \
194
+ -o -name .venv -o -name dist -o -name build \) -prune -o \
195
+ -type f -path '*/docs/INDEX.md' -print 2>/dev/null)
196
+
197
+ echo " Scan: thấy $found OM project, seed $seeded entry mới vào registry."
198
+ }
199
+
111
200
  # === Helper: collect projects to sync ===
112
201
  collect_projects() {
113
202
  local found=()
@@ -116,7 +205,7 @@ collect_projects() {
116
205
  local abs_kit
117
206
  abs_kit="$(cd "$REPO_DIR" && pwd -P)"
118
207
  if is_openmoneta_project "$ORIGINAL_PWD" && [[ "$(cd "$ORIGINAL_PWD" && pwd -P)" != "$abs_kit" ]]; then
119
- found+=("$ORIGINAL_PWD")
208
+ found+=("$(cd "$ORIGINAL_PWD" && pwd -P)")
120
209
  fi
121
210
  fi
122
211
 
@@ -132,6 +221,20 @@ collect_projects() {
132
221
  fi
133
222
  done
134
223
 
224
+ if [[ $ALL_PROJECTS -eq 1 ]]; then
225
+ local rp abs_rp
226
+ while IFS= read -r rp; do
227
+ [[ -z "$rp" ]] && continue
228
+ is_openmoneta_project "$rp" || continue
229
+ abs_rp="$(cd "$rp" && pwd -P)"
230
+ local already=0
231
+ for f in "${found[@]}"; do
232
+ [[ "$f" == "$abs_rp" ]] && already=1 && break
233
+ done
234
+ [[ $already -eq 0 ]] && found+=("$abs_rp")
235
+ done < <(read_registry)
236
+ fi
237
+
135
238
  printf '%s\n' "${found[@]}"
136
239
  }
137
240
 
@@ -220,6 +323,39 @@ if [[ $SKIP_PROJECTS -eq 1 ]]; then
220
323
  exit 0
221
324
  fi
222
325
 
326
+ # === Helper: nhắc user còn project trong registry chưa sync (chỉ khi không --all-projects) ===
327
+ print_registry_hint() {
328
+ [[ $ALL_PROJECTS -eq 1 ]] && return 0
329
+ local count=0 rp abs_rp
330
+ while IFS= read -r rp; do
331
+ [[ -z "$rp" ]] && continue
332
+ is_openmoneta_project "$rp" || continue
333
+ abs_rp="$(cd "$rp" && pwd -P)"
334
+ if [[ -n "$PROJECTS" ]] && printf '%s\n' "$PROJECTS" | grep -qFx "$abs_rp"; then
335
+ continue
336
+ fi
337
+ count=$((count + 1))
338
+ done < <(read_registry)
339
+ if [[ $count -gt 0 ]]; then
340
+ echo ""
341
+ echo "ℹ Có $count project khác trong registry chưa sync. Sync tất cả:"
342
+ echo " bash $REPO_DIR/update.sh --all-projects"
343
+ fi
344
+ return 0
345
+ }
346
+
347
+ if [[ $SCAN -eq 1 ]]; then
348
+ echo ""
349
+ echo "==> --scan: dò OM project để seed registry..."
350
+ scan_seed
351
+ fi
352
+
353
+ if [[ $ALL_PROJECTS -eq 1 ]]; then
354
+ echo ""
355
+ echo "==> --all-projects: dọn registry + gom mọi project đã đăng ký..."
356
+ prune_registry
357
+ fi
358
+
223
359
  PROJECTS=$(collect_projects)
224
360
 
225
361
  if [[ -z "$PROJECTS" ]]; then
@@ -233,6 +369,7 @@ if [[ -z "$PROJECTS" ]]; then
233
369
  echo "Hoặc sync thủ công từng project:"
234
370
  echo " bash $REPO_DIR/scripts/init-project.sh /path/to/project"
235
371
  fi
372
+ print_registry_hint
236
373
  exit 0
237
374
  fi
238
375
 
@@ -274,5 +411,6 @@ if [[ $SYNC_FAIL -gt 0 ]]; then
274
411
  echo " Failed: $SYNC_FAIL"
275
412
  fi
276
413
  echo "================================================"
414
+ print_registry_hint
277
415
  echo ""
278
416
  echo "Restart Cursor để áp dụng changes."