viepilot 1.2.0 → 1.6.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/CHANGELOG.md +30 -0
- package/README.md +23 -19
- package/bin/vp-tools.cjs +221 -0
- package/dev-install.sh +29 -8
- package/docs/README.md +5 -3
- package/docs/dev/cli-reference.md +70 -1
- package/docs/dev/contributing.md +4 -0
- package/docs/skills-reference.md +47 -2
- package/docs/user/features/autonomous-mode.md +16 -0
- package/docs/user/features/brainstorm.md +28 -0
- package/docs/user/features/product-horizon.md +18 -0
- package/docs/user/features/ui-direction.md +63 -12
- package/docs/user/quick-start.md +22 -0
- package/install.sh +15 -3
- package/lib/viepilot-info.cjs +196 -0
- package/lib/viepilot-update.cjs +156 -0
- package/package.json +2 -1
- package/skills/vp-auto/SKILL.md +9 -0
- package/skills/vp-brainstorm/SKILL.md +9 -5
- package/skills/vp-crystallize/SKILL.md +17 -12
- package/skills/vp-info/SKILL.md +62 -0
- package/skills/vp-update/SKILL.md +59 -0
- package/templates/project/AI-GUIDE.md +23 -11
- package/templates/project/PROJECT-CONTEXT.md +22 -0
- package/templates/project/ROADMAP.md +27 -0
- package/workflows/autonomous.md +23 -0
- package/workflows/brainstorm.md +62 -11
- package/workflows/crystallize.md +36 -9
package/docs/skills-reference.md
CHANGED
|
@@ -26,6 +26,8 @@ Complete reference for all ViePilot skills.
|
|
|
26
26
|
|
|
27
27
|
### Output
|
|
28
28
|
- `docs/brainstorm/session-{YYYY-MM-DD}.md`
|
|
29
|
+
- **Product horizon:** session file giữ **`## Product horizon`** (MVP / Post-MVP / Future tags, deferred capabilities, hoặc single-release statement) để `/vp-crystallize` không bỏ sót post-MVP — xem `workflows/brainstorm.md`.
|
|
30
|
+
- UI Direction (optional): `.viepilot/ui-direction/{session-id}/` — legacy (`index.html`) hoặc multi-page (`pages/*.html` + hub + `## Pages inventory` trong `notes.md`). Chi tiết: [UI Direction](user/features/ui-direction.md).
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
@@ -33,6 +35,14 @@ Complete reference for all ViePilot skills.
|
|
|
33
35
|
|
|
34
36
|
**Purpose**: Chuyển đổi brainstorm thành executable artifacts
|
|
35
37
|
|
|
38
|
+
### UI direction intake
|
|
39
|
+
- Nếu có `.viepilot/ui-direction/{session-id}/` với `pages/*.html`: đọc `notes.md` (**Pages inventory**), từng file page, và hub `index.html` để architecture UI không bỏ sót màn hình.
|
|
40
|
+
|
|
41
|
+
### Product horizon (brainstorm → ROADMAP / context)
|
|
42
|
+
- Step 1: trích **`## Product horizon`** từ mọi session; **horizon inventory** + cổng single-release / thiếu section — `workflows/crystallize.md`.
|
|
43
|
+
- `ROADMAP.md`: luôn có block **Post-MVP / Product horizon** (hoặc ghi rõ single-release); `PROJECT-CONTEXT.md`: khối **`<product_vision>`** từ `templates/project/PROJECT-CONTEXT.md`.
|
|
44
|
+
- Hướng dẫn user tổng quan: [product-horizon.md](user/features/product-horizon.md). Thứ tự load cho AI: `templates/project/AI-GUIDE.md` (bản crystallize copy vào `.viepilot/AI-GUIDE.md`).
|
|
45
|
+
|
|
36
46
|
### Metadata Collection
|
|
37
47
|
- Project name, description
|
|
38
48
|
- Organization name, website
|
|
@@ -96,8 +106,9 @@ For each phase:
|
|
|
96
106
|
2. Create git tag
|
|
97
107
|
3. Execute implementation
|
|
98
108
|
4. Verify results
|
|
99
|
-
5.
|
|
100
|
-
6.
|
|
109
|
+
5. Enforce git persistence gate (`vp-tools git-persistence --strict`)
|
|
110
|
+
6. Handle outcome (pass/fail)
|
|
111
|
+
7. Update state
|
|
101
112
|
Mark phase complete
|
|
102
113
|
```
|
|
103
114
|
|
|
@@ -189,6 +200,23 @@ AI pauses for user input when:
|
|
|
189
200
|
|
|
190
201
|
---
|
|
191
202
|
|
|
203
|
+
## /vp-info
|
|
204
|
+
|
|
205
|
+
**Purpose**: Xem metadata bundle ViePilot — version đã cài, npm latest, danh sách skills/workflows (FEAT-008).
|
|
206
|
+
|
|
207
|
+
### CLI
|
|
208
|
+
| Invocation | Description |
|
|
209
|
+
|------------|-------------|
|
|
210
|
+
| `vp-tools info` | Bảng human-readable |
|
|
211
|
+
| `vp-tools info --json` | JSON parse được: `packageRoot`, `packageName`, `installedVersion`, `latestNpm`, `gitHead`, `skills[]`, `workflows[]` |
|
|
212
|
+
| `node node_modules/viepilot/bin/vp-tools.cjs info` | Từ project có dependency `viepilot` |
|
|
213
|
+
|
|
214
|
+
### JSON fields (tóm tắt)
|
|
215
|
+
- **`skills[]`**: `id`, `version`, `relativePath`
|
|
216
|
+
- **`workflows[]`**: `id`, `relativePath`, `semverInFile`, `note`
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
192
220
|
## /vp-docs
|
|
193
221
|
|
|
194
222
|
**Purpose**: Generate documentation
|
|
@@ -398,3 +426,20 @@ CHANGELOG.md (updated)
|
|
|
398
426
|
| `skip N --reason "..."` | Skip task N |
|
|
399
427
|
| `retry N` | Retry failed task N |
|
|
400
428
|
| `rollback N` | Rollback task N changes |
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## /vp-update
|
|
433
|
+
|
|
434
|
+
**Purpose**: Nâng cấp package `viepilot` qua npm — có dry-run và xác nhận non-interactive (FEAT-008).
|
|
435
|
+
|
|
436
|
+
### Flags
|
|
437
|
+
| Flag | Description |
|
|
438
|
+
|------|-------------|
|
|
439
|
+
| `--dry-run` | In planned npm command, không chạy |
|
|
440
|
+
| `--yes` | Bỏ qua prompt; **bắt buộc** trong non-interactive khi apply |
|
|
441
|
+
| `--global` | Ép `npm install -g viepilot@latest` |
|
|
442
|
+
|
|
443
|
+
### Lưu ý
|
|
444
|
+
- Trong repo **application** có `node_modules/viepilot`, update mặc định có thể target **local** — dùng **`--global`** nếu chỉ muốn global.
|
|
445
|
+
- Luồng an toàn: `vp-tools update --dry-run` → sau đó `vp-tools update --yes` (hoặc `--global --yes`).
|
|
@@ -18,6 +18,7 @@ Mỗi task:
|
|
|
18
18
|
3. Verify (automated + manual nếu cần)
|
|
19
19
|
4. Commit với conventional commit message
|
|
20
20
|
5. Tạo done tag (`{project}-vp-p{N}-t{T}-done`)
|
|
21
|
+
6. Pass Git persistence gate (clean + pushed) trước khi mark done
|
|
21
22
|
|
|
22
23
|
## Flags
|
|
23
24
|
|
|
@@ -82,6 +83,21 @@ Trước khi mark task done, ViePilot kiểm tra:
|
|
|
82
83
|
|
|
83
84
|
Nếu bất kỳ gate nào fail → control point.
|
|
84
85
|
|
|
86
|
+
## Git Persistence Gate (BUG-003)
|
|
87
|
+
|
|
88
|
+
Trước khi `/vp-auto` được phép mark task/phase là PASS:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
node bin/vp-tools.cjs git-persistence --strict
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Gate chỉ PASS khi:
|
|
95
|
+
- Working tree sạch (không còn thay đổi chưa commit)
|
|
96
|
+
- Nhánh hiện tại có upstream
|
|
97
|
+
- Không còn commit local chưa push (`ahead_count = 0`)
|
|
98
|
+
|
|
99
|
+
Nếu fail, workflow phải vào control point và **không được** cập nhật state sang `done/complete`.
|
|
100
|
+
|
|
85
101
|
## Checkpoints
|
|
86
102
|
|
|
87
103
|
Mỗi task tạo 2 git tags:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Brainstorm sessions — product horizon (MVP / Post-MVP / Future)
|
|
2
|
+
|
|
3
|
+
ViePilot brainstorm lưu session tại `docs/brainstorm/session-*.md`. Để **không mất** ý tưởng sau MVP khi chạy `/vp-crystallize`, mỗi session nên có section **`## Product horizon`** (workflow tự gợi ý cấu trúc khi lưu).
|
|
4
|
+
|
|
5
|
+
## Tag trên từng bullet
|
|
6
|
+
|
|
7
|
+
| Tag | Ý nghĩa |
|
|
8
|
+
|-----|---------|
|
|
9
|
+
| `(MVP)` | Ship trong bản đầu tiên |
|
|
10
|
+
| `(Post-MVP)` | Đã thống nhất nhưng sau release đầu |
|
|
11
|
+
| `(Future)` | Hướng thử nghiệm / chưa cam kết |
|
|
12
|
+
|
|
13
|
+
## Các subsection khuyến nghị
|
|
14
|
+
|
|
15
|
+
- **Non-goals for MVP** — Cố tình không làm ở bản đầu (tránh bị hiểu là “quên”).
|
|
16
|
+
- **Deferred capabilities** — Tính năng rõ ràng bị lùi khỏi MVP để crystallize map vào roadmap horizon.
|
|
17
|
+
|
|
18
|
+
## Single-release (không có post-MVP)
|
|
19
|
+
|
|
20
|
+
Ghi một dòng explicit trong `## Product horizon`, ví dụ: **Single-release product — no separate horizon epics.**
|
|
21
|
+
|
|
22
|
+
## Tiếp tục session cũ
|
|
23
|
+
|
|
24
|
+
Khi **Continue** session, giữ và cập nhật `## Product horizon`; không xóa section này trừ khi bạn chủ động đổi scope.
|
|
25
|
+
|
|
26
|
+
## Bước tiếp
|
|
27
|
+
|
|
28
|
+
Sau brainstorm: `/vp-crystallize` — horizon trong session được dùng để bổ sung roadmap và project context (xem workflow crystallize). Tổng quan end-to-end: [Product horizon](product-horizon.md).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Product horizon — từ brainstorm tới roadmap (không để post-MVP “chết” trong session)
|
|
2
|
+
|
|
3
|
+
Ý tưởng **sau MVP** dễ chỉ nằm trong file brainstorm rồi bị AI bỏ qua khi code. ViePilot chuẩn hóa **horizon** để mọi người (và AI) luôn thấy nó ở chỗ đúng.
|
|
4
|
+
|
|
5
|
+
## Quy tắc ngắn
|
|
6
|
+
|
|
7
|
+
1. Trong brainstorm: luôn có section **`## Product horizon`** với tag `(MVP)` / `(Post-MVP)` / `(Future)` — xem [Brainstorm & product horizon](brainstorm.md).
|
|
8
|
+
2. Sau **`/vp-crystallize`**: horizon phải xuất hiện trong **`ROADMAP.md`** (Post-MVP / Future) và **vision theo pha** trong **`PROJECT-CONTEXT.md`** — không được “chỉ để trong `docs/brainstorm/`”.
|
|
9
|
+
3. Khi AI bắt đầu task implementation sâu: đọc **vision + horizon trước**, rồi mới khóa kiến trúc chi tiết — xem thứ tự trong `AI-GUIDE.md` (file được crystallize tạo dưới `.viepilot/AI-GUIDE.md`).
|
|
10
|
+
|
|
11
|
+
## Single-release
|
|
12
|
+
|
|
13
|
+
Nếu không có lộ trình sau MVP, ghi rõ một dòng trong horizon (ví dụ *Single-release — no separate horizon epics*) để crystallize không bịa thêm epic.
|
|
14
|
+
|
|
15
|
+
## Liên kết
|
|
16
|
+
|
|
17
|
+
- Workflow brainstorm: `workflows/brainstorm.md` (trong repo ViePilot).
|
|
18
|
+
- Workflow crystallize: `workflows/crystallize.md` — bước trích horizon và gate “không được lặng lẽ bỏ sót”.
|
|
@@ -5,25 +5,76 @@
|
|
|
5
5
|
## Mục tiêu
|
|
6
6
|
- Chốt hướng UI/UX sớm bằng prototype định hướng
|
|
7
7
|
- Ghi quyết định thiết kế cùng ngữ cảnh nghiệp vụ
|
|
8
|
-
- Tạo đầu vào rõ ràng cho `/vp-crystallize
|
|
8
|
+
- Tạo đầu vào rõ ràng cho `/vp-crystallize`, kể cả khi sản phẩm có **nhiều page** (FEAT-007)
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
10
|
+
## Hai layout được hỗ trợ
|
|
11
|
+
|
|
12
|
+
### A) Legacy — một file chính (FEAT-002)
|
|
13
|
+
Dùng khi chỉ có một màn hình / một luồng prototype đơn.
|
|
12
14
|
|
|
13
15
|
```text
|
|
14
16
|
.viepilot/ui-direction/{session-id}/
|
|
15
|
-
index.html
|
|
17
|
+
index.html # toàn bộ direction
|
|
16
18
|
style.css
|
|
17
|
-
notes.md
|
|
19
|
+
notes.md # rationale, assumptions, references
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### B) Multi-page — mỗi page một HTML (khuyến nghị khi ≥2 màn)
|
|
23
|
+
Dễ diff, dễ review, dễ map sang routing thật sau này.
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
.viepilot/ui-direction/{session-id}/
|
|
27
|
+
index.html # hub: liên kết tới mọi page (nav / danh sách)
|
|
28
|
+
style.css # shared styles (tránh copy lớn giữa các file)
|
|
29
|
+
pages/
|
|
30
|
+
landing.html
|
|
31
|
+
dashboard.html
|
|
32
|
+
...
|
|
33
|
+
notes.md # rationale + bảng inventory (bắt buộc khi có pages/)
|
|
18
34
|
```
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
- **`index.html`**: không bắt buộc trùng nội dung với một page cụ thể; nên là **mục lục + link** tới `pages/*.html` để mở nhanh từng màn.
|
|
37
|
+
- **`pages/{slug}.html`**: một file cho một page/screen; đặt `slug` ổn định (trùng tên route dự kiến nếu đã biết).
|
|
38
|
+
|
|
39
|
+
## `notes.md` — nguồn sự thật
|
|
40
|
+
|
|
41
|
+
Luôn ghi:
|
|
42
|
+
- rationale, assumptions, references (21st.dev, v.v.)
|
|
43
|
+
|
|
44
|
+
### Hook bắt buộc khi thư mục `pages/` tồn tại
|
|
45
|
+
|
|
46
|
+
Sau **mỗi** lần thêm / đổi tên / xóa file trong `pages/`, assistant **phải** cập nhật:
|
|
47
|
+
|
|
48
|
+
1. Liên kết trong `index.html` (hub).
|
|
49
|
+
2. Section **`## Pages inventory`** trong `notes.md` (bảng đầy đủ mọi page hiện có).
|
|
50
|
+
|
|
51
|
+
Mẫu bảng (copy-paste và điền):
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
## Pages inventory
|
|
55
|
+
|
|
56
|
+
| Slug | File | Title | Purpose | Key sections | Nav to |
|
|
57
|
+
|------|------|-------|---------|--------------|--------|
|
|
58
|
+
| landing | pages/landing.html | Marketing home | Acquire signups | hero, features, CTA | signup, login |
|
|
59
|
+
| dashboard | pages/dashboard.html | App shell | Overview metrics | sidebar, charts, alerts | settings |
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **Slug**: định danh ngắn, ổn định.
|
|
63
|
+
- **File**: đường dẫn tương đối từ thư mục session.
|
|
64
|
+
- **Nav to**: slug hoặc tên page đích (comma-separated).
|
|
65
|
+
|
|
66
|
+
Nếu **không** có thư mục `pages/`, không cần section `## Pages inventory` (layout legacy).
|
|
24
67
|
|
|
25
68
|
## Flow khuyến nghị
|
|
26
69
|
1. `/vp-brainstorm --ui`
|
|
27
|
-
2.
|
|
28
|
-
3.
|
|
29
|
-
4. `/vp-crystallize` đọc
|
|
70
|
+
2. Chọn legacy hoặc multi-page theo số màn hình.
|
|
71
|
+
3. Mỗi thay đổi page → cập nhật HTML + **hub + `## Pages inventory`** trong cùng một lượt.
|
|
72
|
+
4. `/vp-crystallize` đọc `notes.md` trước, sau đó `index.html`, `style.css`, và **từng** `pages/*.html` (nếu có) để lên kiến trúc UI đủ page.
|
|
73
|
+
|
|
74
|
+
## Kiểm tra nhanh (optional)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node scripts/verify-ui-direction-pages.cjs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Script báo lỗi nếu có session có `pages/*.html` nhưng thiếu `## Pages inventory` hoặc thiếu tên file page trong `notes.md`.
|
package/docs/user/quick-start.md
CHANGED
|
@@ -54,6 +54,25 @@ npm run readme:sync
|
|
|
54
54
|
# refresh README Total LOC metrics via cloc
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
### Version check & update
|
|
58
|
+
|
|
59
|
+
Sau khi có `vp-tools` trên PATH (ví dụ `npm i -g viepilot` hoặc cài qua `npx viepilot install` copy bundle):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
vp-tools info
|
|
63
|
+
vp-tools info --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Cập nhật ViePilot qua npm — nên xem trước bằng dry-run; trong script/CI thêm `--yes` khi apply:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
vp-tools update --dry-run
|
|
70
|
+
vp-tools update --yes
|
|
71
|
+
vp-tools update --global --yes
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Trong project có dependency `viepilot`, có thể chạy: `node node_modules/viepilot/bin/vp-tools.cjs info`.
|
|
75
|
+
|
|
57
76
|
---
|
|
58
77
|
|
|
59
78
|
## Step 2: Create a New Project
|
|
@@ -103,6 +122,9 @@ ViePilot tạo `.viepilot/` directory với:
|
|
|
103
122
|
- `TRACKER.md` — Progress tracking
|
|
104
123
|
- `ARCHITECTURE.md` — System design
|
|
105
124
|
- `SYSTEM-RULES.md` — Coding standards
|
|
125
|
+
- `AI-GUIDE.md` — Thứ tự đọc context (kể cả **vision / horizon** trước khi khóa kiến trúc sâu)
|
|
126
|
+
|
|
127
|
+
**Post-MVP không chỉ nằm trong brainstorm:** sau crystallize, horizon phải vào `ROADMAP.md` + vision theo pha trong `PROJECT-CONTEXT.md`. Tóm tắt cho user: [Product horizon end-to-end](features/product-horizon.md).
|
|
106
128
|
|
|
107
129
|
---
|
|
108
130
|
|
package/install.sh
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# ViePilot Installation Script
|
|
4
4
|
# Installs ViePilot skills and tools to Cursor/Claude environment
|
|
5
|
+
#
|
|
6
|
+
# Optional: VIEPILOT_SYMLINK_SKILLS=1 — symlink skills/* into ~/.cursor/skills/ (absolute paths)
|
|
5
7
|
|
|
6
8
|
set -e
|
|
7
9
|
|
|
@@ -109,13 +111,23 @@ mkdir -p "$VIEPILOT_DIR/bin"
|
|
|
109
111
|
mkdir -p "$VIEPILOT_DIR/lib"
|
|
110
112
|
mkdir -p "$VIEPILOT_DIR/ui-components"
|
|
111
113
|
|
|
112
|
-
# Install skills
|
|
114
|
+
# Install skills (copy default; VIEPILOT_SYMLINK_SKILLS=1 for dev-style live links)
|
|
113
115
|
echo " Installing skills..."
|
|
114
116
|
for skill_dir in "$SCRIPT_DIR/skills"/*; do
|
|
115
117
|
if [ -d "$skill_dir" ]; then
|
|
116
118
|
skill_name=$(basename "$skill_dir")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
if [ "${VIEPILOT_SYMLINK_SKILLS:-0}" = "1" ]; then
|
|
120
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
121
|
+
skill_abs=$(realpath "$skill_dir")
|
|
122
|
+
else
|
|
123
|
+
skill_abs=$(cd "$skill_dir" && pwd)
|
|
124
|
+
fi
|
|
125
|
+
ln -sfn "$skill_abs" "$CURSOR_SKILLS_DIR/$skill_name"
|
|
126
|
+
echo " ✓ $skill_name (symlink)"
|
|
127
|
+
else
|
|
128
|
+
cp -r "$skill_dir" "$CURSOR_SKILLS_DIR/"
|
|
129
|
+
echo " ✓ $skill_name"
|
|
130
|
+
fi
|
|
119
131
|
fi
|
|
120
132
|
done
|
|
121
133
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViePilot bundle metadata for `vp-tools info` (FEAT-008).
|
|
3
|
+
* Resolves the viepilot package root from the CLI location — no `.viepilot/` project required.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execFileSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Walk upward from startDir (and optionally cwd) for package.json with name "viepilot".
|
|
12
|
+
* @param {string} startDir - e.g. path.join(__dirname, '..') from bin/vp-tools.cjs
|
|
13
|
+
* @returns {string|null} absolute package root or null
|
|
14
|
+
*/
|
|
15
|
+
function resolveViepilotPackageRoot(startDir) {
|
|
16
|
+
const tryRoots = [path.resolve(startDir), path.resolve(process.cwd())];
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
for (const base of tryRoots) {
|
|
19
|
+
if (seen.has(base)) continue;
|
|
20
|
+
seen.add(base);
|
|
21
|
+
let dir = base;
|
|
22
|
+
while (dir && dir !== path.dirname(dir)) {
|
|
23
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
24
|
+
if (fs.existsSync(pkgPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
27
|
+
if (pkg && pkg.name === 'viepilot') {
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
} catch (_e) {
|
|
31
|
+
/* ignore */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
dir = path.dirname(dir);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} root - viepilot package root
|
|
42
|
+
* @returns {string|null}
|
|
43
|
+
*/
|
|
44
|
+
function readInstalledVersion(root) {
|
|
45
|
+
const pkgPath = path.join(root, 'package.json');
|
|
46
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
49
|
+
return pkg.version || null;
|
|
50
|
+
} catch (_e) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Query npm registry for latest published version (requires network + npm on PATH).
|
|
57
|
+
* @returns {{ ok: true, version: string } | { ok: false, error: string }}
|
|
58
|
+
*/
|
|
59
|
+
function fetchLatestNpmVersion() {
|
|
60
|
+
try {
|
|
61
|
+
const out = execFileSync('npm', ['view', 'viepilot', 'version', '--json'], {
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
timeout: 15000,
|
|
64
|
+
windowsHide: true,
|
|
65
|
+
}).trim();
|
|
66
|
+
const parsed = JSON.parse(out);
|
|
67
|
+
const version = typeof parsed === 'string' ? parsed : String(parsed);
|
|
68
|
+
return { ok: true, version };
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return { ok: false, error: e.message || String(e) };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse `version:` from YAML-like frontmatter (first --- block).
|
|
76
|
+
* @param {string} content
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function parseSkillFileVersion(content) {
|
|
80
|
+
if (typeof content !== 'string' || !content.startsWith('---')) {
|
|
81
|
+
return 'unspecified';
|
|
82
|
+
}
|
|
83
|
+
const end = content.indexOf('\n---', 3);
|
|
84
|
+
if (end === -1) {
|
|
85
|
+
return 'unspecified';
|
|
86
|
+
}
|
|
87
|
+
const block = content.slice(3, end);
|
|
88
|
+
const m = block.match(/^version:\s*["']?([^"'\r\n]+)["']?/m);
|
|
89
|
+
return m ? m[1].trim() : 'unspecified';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} root - viepilot package root
|
|
94
|
+
* @returns {Array<{ id: string, version: string, relativePath: string }>}
|
|
95
|
+
*/
|
|
96
|
+
function listSkillsWithVersions(root) {
|
|
97
|
+
const skillsDir = path.join(root, 'skills');
|
|
98
|
+
if (!fs.existsSync(skillsDir)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const ent of entries) {
|
|
104
|
+
if (!ent.isDirectory()) continue;
|
|
105
|
+
const skillFile = path.join(skillsDir, ent.name, 'SKILL.md');
|
|
106
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
107
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
108
|
+
out.push({
|
|
109
|
+
id: ent.name,
|
|
110
|
+
version: parseSkillFileVersion(content),
|
|
111
|
+
relativePath: path.join('skills', ent.name, 'SKILL.md').replace(/\\/g, '/'),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
out.sort((a, b) => a.id.localeCompare(b.id));
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const WORKFLOW_SEMVER_NOTE = 'no semver in workflow markdown';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} root
|
|
122
|
+
* @returns {Array<{ id: string, relativePath: string, semverInFile: null, note: string }>}
|
|
123
|
+
*/
|
|
124
|
+
function listWorkflows(root) {
|
|
125
|
+
const wfDir = path.join(root, 'workflows');
|
|
126
|
+
if (!fs.existsSync(wfDir)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const files = fs
|
|
130
|
+
.readdirSync(wfDir)
|
|
131
|
+
.filter((f) => f.endsWith('.md'))
|
|
132
|
+
.sort((a, b) => a.localeCompare(b));
|
|
133
|
+
return files.map((f) => ({
|
|
134
|
+
id: f.replace(/\.md$/i, ''),
|
|
135
|
+
relativePath: path.join('workflows', f).replace(/\\/g, '/'),
|
|
136
|
+
semverInFile: null,
|
|
137
|
+
note: WORKFLOW_SEMVER_NOTE,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} root
|
|
143
|
+
* @returns {string|null}
|
|
144
|
+
*/
|
|
145
|
+
function tryGitHead(root) {
|
|
146
|
+
try {
|
|
147
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
148
|
+
cwd: root,
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
timeout: 8000,
|
|
151
|
+
windowsHide: true,
|
|
152
|
+
}).trim();
|
|
153
|
+
} catch (_e) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} root
|
|
160
|
+
* @param {{ includeLatestNpm?: boolean }} [options]
|
|
161
|
+
*/
|
|
162
|
+
function buildInfoReport(root, options = {}) {
|
|
163
|
+
const includeLatestNpm = options.includeLatestNpm !== false;
|
|
164
|
+
const installedVersion = readInstalledVersion(root);
|
|
165
|
+
let pkgName = 'viepilot';
|
|
166
|
+
try {
|
|
167
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
168
|
+
if (pkg.name) pkgName = pkg.name;
|
|
169
|
+
} catch (_e) {
|
|
170
|
+
/* keep default */
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const report = {
|
|
174
|
+
packageRoot: root,
|
|
175
|
+
packageName: pkgName,
|
|
176
|
+
installedVersion: installedVersion || 'unknown',
|
|
177
|
+
latestNpm: includeLatestNpm ? fetchLatestNpmVersion() : { ok: false, error: 'skipped' },
|
|
178
|
+
gitHead: tryGitHead(root),
|
|
179
|
+
skills: listSkillsWithVersions(root),
|
|
180
|
+
workflows: listWorkflows(root),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return report;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
resolveViepilotPackageRoot,
|
|
188
|
+
readInstalledVersion,
|
|
189
|
+
fetchLatestNpmVersion,
|
|
190
|
+
parseSkillFileVersion,
|
|
191
|
+
listSkillsWithVersions,
|
|
192
|
+
listWorkflows,
|
|
193
|
+
tryGitHead,
|
|
194
|
+
buildInfoReport,
|
|
195
|
+
WORKFLOW_SEMVER_NOTE,
|
|
196
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan and run `npm` upgrade for the viepilot package (FEAT-008 / vp-tools update).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync, execFileSync } = require('child_process');
|
|
7
|
+
const viepilotInfo = require('./viepilot-info.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @returns {string|null} absolute path to global .../node_modules/viepilot
|
|
11
|
+
*/
|
|
12
|
+
function tryGetNpmGlobalViepilotPath() {
|
|
13
|
+
try {
|
|
14
|
+
const root = execFileSync('npm', ['root', '-g'], {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
timeout: 10000,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
}).trim();
|
|
19
|
+
if (!root) return null;
|
|
20
|
+
return path.resolve(root, 'viepilot');
|
|
21
|
+
} catch (_e) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} viepilotPackageRoot
|
|
28
|
+
* @param {boolean} forceGlobal
|
|
29
|
+
* @param {string|null} globalViepilotPath
|
|
30
|
+
*/
|
|
31
|
+
function classifyInstall(viepilotPackageRoot, forceGlobal, globalViepilotPath) {
|
|
32
|
+
if (forceGlobal) {
|
|
33
|
+
return {
|
|
34
|
+
mode: 'global',
|
|
35
|
+
cwd: undefined,
|
|
36
|
+
npmArgs: ['install', '-g', 'viepilot@latest'],
|
|
37
|
+
ambiguous: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const r = path.resolve(viepilotPackageRoot);
|
|
41
|
+
if (globalViepilotPath) {
|
|
42
|
+
const g = path.resolve(globalViepilotPath);
|
|
43
|
+
if (r === g) {
|
|
44
|
+
return {
|
|
45
|
+
mode: 'global',
|
|
46
|
+
cwd: undefined,
|
|
47
|
+
npmArgs: ['install', '-g', 'viepilot@latest'],
|
|
48
|
+
ambiguous: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const localSuffix = path.join('node_modules', 'viepilot');
|
|
53
|
+
if (r.endsWith(localSuffix)) {
|
|
54
|
+
const projectRoot = path.resolve(viepilotPackageRoot, '..', '..');
|
|
55
|
+
return {
|
|
56
|
+
mode: 'local',
|
|
57
|
+
cwd: projectRoot,
|
|
58
|
+
npmArgs: ['install', 'viepilot@latest'],
|
|
59
|
+
ambiguous: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
mode: 'global',
|
|
64
|
+
cwd: undefined,
|
|
65
|
+
npmArgs: ['install', '-g', 'viepilot@latest'],
|
|
66
|
+
ambiguous: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Rough semver compare for x.y.z (numeric segments only).
|
|
72
|
+
* @returns {-1|0|1|null} null if either side empty
|
|
73
|
+
*/
|
|
74
|
+
function compareSemver(a, b) {
|
|
75
|
+
if (a == null || b == null || a === '' || b === '') return null;
|
|
76
|
+
const pa = String(a).split(/[.+]/).map((x) => parseInt(x, 10) || 0);
|
|
77
|
+
const pb = String(b).split(/[.+]/).map((x) => parseInt(x, 10) || 0);
|
|
78
|
+
const len = Math.max(pa.length, pb.length);
|
|
79
|
+
for (let i = 0; i < len; i++) {
|
|
80
|
+
const da = pa[i] || 0;
|
|
81
|
+
const db = pb[i] || 0;
|
|
82
|
+
if (da < db) return -1;
|
|
83
|
+
if (da > db) return 1;
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {{ startDir: string, forceGlobal?: boolean }} opts
|
|
90
|
+
*/
|
|
91
|
+
function buildUpdatePlan(opts) {
|
|
92
|
+
const startDir = opts.startDir;
|
|
93
|
+
const forceGlobal = Boolean(opts.forceGlobal);
|
|
94
|
+
const root = viepilotInfo.resolveViepilotPackageRoot(startDir);
|
|
95
|
+
if (!root) {
|
|
96
|
+
return { ok: false, error: 'Could not locate viepilot package root' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const installed = viepilotInfo.readInstalledVersion(root);
|
|
100
|
+
const latestResult = viepilotInfo.fetchLatestNpmVersion();
|
|
101
|
+
|
|
102
|
+
if (latestResult.ok && installed && compareSemver(installed, latestResult.version) >= 0) {
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
alreadyLatest: true,
|
|
106
|
+
installedVersion: installed,
|
|
107
|
+
latestVersion: latestResult.version,
|
|
108
|
+
viepilotRoot: root,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const globalViepilotPath = tryGetNpmGlobalViepilotPath();
|
|
113
|
+
const layout = classifyInstall(root, forceGlobal, globalViepilotPath);
|
|
114
|
+
const displayCommand = layout.cwd
|
|
115
|
+
? `(cwd ${layout.cwd}) npm ${layout.npmArgs.join(' ')}`
|
|
116
|
+
: `npm ${layout.npmArgs.join(' ')}`;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
alreadyLatest: false,
|
|
121
|
+
installedVersion: installed,
|
|
122
|
+
latestVersion: latestResult.ok ? latestResult.version : null,
|
|
123
|
+
latestNpmError: latestResult.ok ? null : latestResult.error,
|
|
124
|
+
viepilotRoot: root,
|
|
125
|
+
mode: layout.mode,
|
|
126
|
+
cwd: layout.cwd,
|
|
127
|
+
npmArgs: layout.npmArgs,
|
|
128
|
+
ambiguous: layout.ambiguous,
|
|
129
|
+
displayCommand,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {object} plan - from buildUpdatePlan (not alreadyLatest)
|
|
135
|
+
* @returns {{ ok: boolean, code?: number }}
|
|
136
|
+
*/
|
|
137
|
+
function runNpmUpdate(plan) {
|
|
138
|
+
const r = spawnSync('npm', plan.npmArgs, {
|
|
139
|
+
cwd: plan.cwd,
|
|
140
|
+
stdio: 'inherit',
|
|
141
|
+
shell: false,
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
});
|
|
144
|
+
if (r.status === 0) {
|
|
145
|
+
return { ok: true };
|
|
146
|
+
}
|
|
147
|
+
return { ok: false, code: r.status == null ? 1 : r.status };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
tryGetNpmGlobalViepilotPath,
|
|
152
|
+
classifyInstall,
|
|
153
|
+
compareSemver,
|
|
154
|
+
buildUpdatePlan,
|
|
155
|
+
runNpmUpdate,
|
|
156
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "**Autonomous Vibe Coding Framework / Bộ khung phát triển tự động có kiểm soát**",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"test:watch": "jest --watch",
|
|
17
17
|
"readme:sync": "node scripts/sync-readme-metrics.cjs",
|
|
18
18
|
"lint:cli": "node --check bin/vp-tools.cjs && node --check bin/viepilot.cjs",
|
|
19
|
+
"verify:ui-direction": "node scripts/verify-ui-direction-pages.cjs",
|
|
19
20
|
"verify:release": "npm run lint:cli && npm test && npm pack --dry-run",
|
|
20
21
|
"prepublishOnly": "npm run verify:release",
|
|
21
22
|
"smoke:published": "node scripts/smoke-published.cjs",
|