@relipa/ai-flow-kit 0.1.5 → 0.1.7-beta.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/bin/aiflow.js +71 -33
- package/custom/rules/ml-conventions.md +11 -8
- package/custom/rules/project-conventions.md +18 -2
- package/custom/skills/design-experiment/SKILL.md +2 -2
- package/custom/skills/evaluate-model/SKILL.md +2 -2
- package/custom/skills/explore-data/SKILL.md +1 -1
- package/custom/skills/figma-to-component/SKILL.md +222 -20
- package/custom/skills/frame-ml-problem/SKILL.md +1 -1
- package/custom/skills/generate-spec/SKILL.md +19 -0
- package/custom/skills/read-study-requirement/SKILL.md +69 -1
- package/custom/skills/review-plan/SKILL.md +42 -0
- package/custom/templates/memory/CODEOWNERS +8 -0
- package/custom/templates/memory/ci/memory-finalize.yml +9 -0
- package/custom/templates/memory/ci/memory-lint.yml +10 -0
- package/custom/templates/memory/gitlab/merge_request_templates/memory.md +18 -0
- package/custom/templates/memory/memory-item.md +25 -0
- package/custom/templates/memory/skeleton/00.Shared/architecture/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/00.Shared/decisions/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/00.Shared/domain/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/00.Shared/glossary/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/01.Lessons/ba/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/01.Lessons/dev/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/01.Lessons/pm/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/01.Lessons/qa/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/02.Instincts/approved/_global/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/03.Retro/.gitkeep +0 -0
- package/custom/templates/memory/skeleton/MEMORY.md +7 -0
- package/custom/templates/memory/skeleton/_deprecated/.gitkeep +0 -0
- package/custom/templates/shared/create-spec-workflow.md +68 -1
- package/custom/templates/shared/create-testcase-workflow.md +67 -0
- package/custom/templates/shared/gate-workflow.md +122 -3
- package/custom/templates/shared/ml-gate-workflow.md +16 -9
- package/docs/common/AIFLOW.md +11 -1
- package/docs/common/CHANGELOG.md +32 -0
- package/docs/common/cli-reference.md +3 -1
- package/docs/common/workflows/figma.md +176 -105
- package/package.json +2 -2
- package/scripts/create-score-excel.js +135 -14
- package/scripts/detect.js +11 -0
- package/scripts/docs-branch.js +264 -0
- package/scripts/hooks/figma-rate-limit.js +83 -0
- package/scripts/hooks/session-start.js +146 -8
- package/scripts/init.js +53 -1
- package/scripts/memory-store.js +391 -0
- package/scripts/memory.js +176 -247
- package/scripts/prompt.js +45 -0
- package/scripts/task.js +30 -15
- package/scripts/update.js +12 -0
- package/scripts/use.js +7 -5
|
@@ -1,105 +1,176 @@
|
|
|
1
|
-
# Figma → Component Workflow
|
|
2
|
-
|
|
3
|
-
Generate UI components directly from Figma designs using the `figma-to-component` skill.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Prerequisites
|
|
8
|
-
|
|
9
|
-
Choose one adapter based on your setup:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
1.
|
|
78
|
-
2.
|
|
79
|
-
3.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
# Figma → Component Workflow
|
|
2
|
+
|
|
3
|
+
Generate UI components directly from Figma designs using the `figma-to-component` skill.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
Choose one adapter based on your setup:
|
|
10
|
+
|
|
11
|
+
> **Which adapter?** The REST API is rate-limited **per seat** (see [Rate limits](#rate-limits-429)
|
|
12
|
+
> below): a View/Collab seat gets only ~6 heavy calls per MONTH — effectively unusable.
|
|
13
|
+
> - Token from a **Dev/Full seat** → Option A (REST) works fine with the batched strategy.
|
|
14
|
+
> - Only a View/Collab seat, or design-heavy work (many frames, art-heavy exports) →
|
|
15
|
+
> prefer **Option B (Figma Desktop)** — it uses the local app session and consumes no REST quota.
|
|
16
|
+
|
|
17
|
+
### Option A — REST API (no Desktop required; needs a Dev/Full-seat token)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
ak init -a figma
|
|
21
|
+
# Prompts for FIGMA_API_TOKEN
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Get your token: [figma.com/settings](https://www.figma.com/settings) → Security → Personal access tokens → Create new token (File content: read). Create it from an account with a **Dev or Full seat** — a View/Collab seat's token is capped at ~6 Tier 1 calls/month.
|
|
25
|
+
|
|
26
|
+
### Option B — Official Figma MCP (requires Figma Desktop app)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ak init -a figma-desktop
|
|
30
|
+
# No token needed — uses Desktop app session
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requirements: Figma Desktop app installed and the target file open before invoking the skill.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Getting a Figma Node URL
|
|
38
|
+
|
|
39
|
+
1. Open your file in Figma (browser or Desktop)
|
|
40
|
+
2. Right-click on a frame or component → **Copy link to selection**
|
|
41
|
+
3. The URL looks like:
|
|
42
|
+
```
|
|
43
|
+
https://www.figma.com/design/FILEKEY/FileName?node-id=123-456
|
|
44
|
+
```
|
|
45
|
+
- `FILEKEY` — the file identifier (between `/design/` and the next `/`)
|
|
46
|
+
- `node-id=123-456` — the specific frame or component to generate
|
|
47
|
+
|
|
48
|
+
You can also link to the entire file (no `node-id`) to let the skill list available frames.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Trigger the Skill
|
|
53
|
+
|
|
54
|
+
Paste the Figma URL into Claude Code with a generation request:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Generate component from this Figma frame:
|
|
58
|
+
https://www.figma.com/design/XXXXX/App?node-id=123-456
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Implement the UserCard UI from Figma:
|
|
63
|
+
https://www.figma.com/design/XXXXX/App?node-id=78-910
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Read Figma and generate component following my project conventions:
|
|
68
|
+
https://www.figma.com/design/XXXXX/App?node-id=42-0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## What to Expect
|
|
74
|
+
|
|
75
|
+
The skill outputs in this order:
|
|
76
|
+
|
|
77
|
+
1. **Design Summary** — layout structure, color palette, typography scale used in the design
|
|
78
|
+
2. **Component file** — complete code for your detected framework (React, Next.js App Router, Vue 3, or Angular)
|
|
79
|
+
3. **Usage example** — how to import and use the component
|
|
80
|
+
4. **Notes** — anything that couldn't be mapped 1:1 from Figma (custom fonts, unsupported effects, etc.)
|
|
81
|
+
|
|
82
|
+
**Framework auto-detection order:**
|
|
83
|
+
1. Read `CLAUDE.md` for framework identifier (`nextjs-app-router`, `reactjs`, `vue-nuxt`, `angular`)
|
|
84
|
+
2. Scan project files (`app/` dir → App Router, `angular.json` → Angular, `nuxt.config.*` → Vue)
|
|
85
|
+
3. Fallback → Next.js / React
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Figma trong Gate workflow
|
|
90
|
+
|
|
91
|
+
Khi làm việc theo quy trình 5 Gate (xem `CLAUDE.md`) với ticket liên quan UI, Figma được
|
|
92
|
+
nối tự động — không cần gọi skill thủ công:
|
|
93
|
+
|
|
94
|
+
| Gate | Vai trò Figma |
|
|
95
|
+
|------|----------------|
|
|
96
|
+
| **1** | `read-study-requirement` dò Figma URL trong ticket (hoặc hỏi DEV một lần), đọc design **một lần** qua MCP, lưu artifact `plan/[ticket-id]/design/` (design-context.md + images/ + nodes.json). |
|
|
97
|
+
| **2** | `generate-spec` đọc `design-context.md` → đưa component tree, design tokens, danh sách ảnh vào plan TDD. |
|
|
98
|
+
| **3** | `figma-to-component` chạy ở **Gate mode**: dùng lại cache `nodes.json`, copy ảnh sang `public/assets/figma/`, sinh component (không gọi lại MCP). |
|
|
99
|
+
| **4** | `review-plan` đối chiếu UI đã code với `design-context.md`. |
|
|
100
|
+
|
|
101
|
+
**Artifact dùng chung** — nguồn sự thật cho mọi Gate sau Gate 1:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
plan/[ticket-id]/design/
|
|
105
|
+
├── design-context.md # layout, tokens, component list, image map
|
|
106
|
+
├── images/ # ảnh export từ Figma
|
|
107
|
+
└── nodes.json # raw get_figma_data (cache)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Refresh khi design đổi:** gõ "reload figma" → Gate 1 fetch lại, ghi đè `design/`, cập nhật `Fetched`.
|
|
111
|
+
|
|
112
|
+
**Ticket không liên quan UI** hoặc không có Figma URL → toàn bộ nhánh Figma được bỏ qua.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Review Checklist
|
|
117
|
+
|
|
118
|
+
After the component is generated, verify:
|
|
119
|
+
|
|
120
|
+
- [ ] Layout matches Figma (flex direction, alignment, gap)
|
|
121
|
+
- [ ] Colors are correct (Tailwind class or arbitrary value)
|
|
122
|
+
- [ ] Typography matches (size, weight, line-height)
|
|
123
|
+
- [ ] Spacing correct (padding, margin, gap)
|
|
124
|
+
- [ ] Interactive states handled (`'use client'` in App Router, variants in React/Vue/Angular)
|
|
125
|
+
- [ ] Images use correct component (`next/image` for Next.js, `<img>` for others)
|
|
126
|
+
- [ ] `className` / `class` prop exposed for external overrides
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Rate limits (429)
|
|
131
|
+
|
|
132
|
+
Figma rate-limits the REST API by **endpoint tier × plan × seat** (since 2025-11-17).
|
|
133
|
+
`get_figma_data` (file/nodes) and image renders share the same **Tier 1** bucket:
|
|
134
|
+
|
|
135
|
+
| Seat of the token owner | Tier 1 budget (file / nodes / render images) |
|
|
136
|
+
|---|---|
|
|
137
|
+
| View / Collab | **~6 calls / MONTH** (any plan) |
|
|
138
|
+
| Dev / Full — Starter | 10 / min |
|
|
139
|
+
| Dev / Full — Professional | 15 / min |
|
|
140
|
+
| Dev / Full — Organization+ | 20 / min |
|
|
141
|
+
|
|
142
|
+
The fills-map endpoint (`/v1/files/:key/images`) is Tier 2 (25-100/min) — the skill prefers it.
|
|
143
|
+
Downloading image bytes from the returned S3 URLs costs nothing.
|
|
144
|
+
|
|
145
|
+
> **Measured on a free personal (Starter) account, 2026-07:** the API returned
|
|
146
|
+
> `x-figma-rate-limit-type: low` with `retry-after ≈ 4 days` after only ~4 Tier 1 calls —
|
|
147
|
+
> i.e. free accounts behave like the View/Collab monthly bucket, NOT 10/min. On a free
|
|
148
|
+
> account, budget ONE batched fetch per design and cache everything; for anything heavier
|
|
149
|
+
> use `figma-desktop` or a paid Dev/Full seat.
|
|
150
|
+
|
|
151
|
+
**Client-side guard:** `ak init` installs a PreToolUse hook
|
|
152
|
+
(`.claude/hooks/figma-rate-limit.js`) that throttles every `mcp__figma__*` call to
|
|
153
|
+
6 REST-call units per 60s (safe under the 10/min Starter budget; `download_figma_images`
|
|
154
|
+
counts as 3 units — it makes up to 3 REST calls internally). It sleeps instead of
|
|
155
|
+
failing, so calls are delayed — never dropped. Tune with `FIGMA_RATE_LIMIT_UNITS`
|
|
156
|
+
(e.g. `15` on a Professional plan).
|
|
157
|
+
|
|
158
|
+
**When you hit a 429, read the `Retry-After` header:**
|
|
159
|
+
|
|
160
|
+
- **≤ 60s** → per-minute bucket. Wait and retry once — this is normal on art-heavy frames.
|
|
161
|
+
- **Hours/days, or `X-Figma-Rate-Limit-Type: low`** → per-seat quota. A new token on the same
|
|
162
|
+
account will NOT help. Fix: use a token from a Dev/Full seat, or switch to
|
|
163
|
+
`ak init -a figma-desktop`.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Troubleshooting
|
|
168
|
+
|
|
169
|
+
| Problem | Fix |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `get_figma_data` tool not found | Run `ak init -a figma` to configure the MCP |
|
|
172
|
+
| "Invalid API token" | Token expired — create a new one at figma.com/settings |
|
|
173
|
+
| `429` immediately / every session | Token from a View/Collab seat (~6 calls/month) — see [Rate limits](#rate-limits-429) |
|
|
174
|
+
| Wrong node generated | Check the `node-id` in the URL matches your selection in Figma |
|
|
175
|
+
| Figma Desktop MCP not responding | Ensure Figma Desktop app is open with the file loaded |
|
|
176
|
+
| Component uses wrong framework | Add framework to `CLAUDE.md` (e.g. `nextjs-app-router`) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relipa/ai-flow-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7-beta.0",
|
|
4
4
|
"description": "All-in-one AI Flow Kit for team development with Claude AI - skills, templates, and MCP adapters",
|
|
5
5
|
"author": "Example Team",
|
|
6
6
|
"publishConfig": {
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
"<rootDir>/tests/**/*.test.js"
|
|
66
66
|
]
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
}
|
|
@@ -24,6 +24,25 @@ function norm(cmd) {
|
|
|
24
24
|
return ALIAS_MAP[c] || c;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// ── Version helpers ─────────────────────────────────────────────────────────
|
|
28
|
+
const CURRENT_VERSION = '0.1.5';
|
|
29
|
+
|
|
30
|
+
function parseVersion(v) {
|
|
31
|
+
if (!v || v === '-' || v === '' || v.toLowerCase() === 'unknown') return null;
|
|
32
|
+
const core = v.split('-')[0]; // strip prerelease suffix, e.g. 0.1.5-beta.1 -> 0.1.5
|
|
33
|
+
const parts = core.split('.').map(n => parseInt(n, 10));
|
|
34
|
+
if (parts.some(n => Number.isNaN(n))) return null;
|
|
35
|
+
return parts;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compareVersion(a, b) {
|
|
39
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
40
|
+
const diff = (a[i] || 0) - (b[i] || 0);
|
|
41
|
+
if (diff !== 0) return diff;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
// ── Parse log file ─────────────────────────────────────────────────────────
|
|
28
47
|
function parseLog(filePath) {
|
|
29
48
|
const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
|
|
@@ -39,17 +58,19 @@ function parseLog(filePath) {
|
|
|
39
58
|
if (!user || user === '-' || user === '') continue;
|
|
40
59
|
|
|
41
60
|
const timestamp = cols[0] || '';
|
|
61
|
+
const version = cols[5] || '';
|
|
42
62
|
const command = cols[6] || '';
|
|
43
63
|
const ticket = cols[9] || '';
|
|
44
64
|
|
|
45
65
|
const date = timestamp.split(' ')[0]; // dd/MM/yyyy
|
|
46
66
|
|
|
47
67
|
if (!users[user]) {
|
|
48
|
-
users[user] = { email: user, days: new Set(), events: 0, rawCmds: new Set(), gateCmds: [] };
|
|
68
|
+
users[user] = { email: user, days: new Set(), events: 0, rawCmds: new Set(), gateCmds: [], versions: new Set() };
|
|
49
69
|
}
|
|
50
70
|
const u = users[user];
|
|
51
71
|
u.events++;
|
|
52
72
|
if (date) u.days.add(date);
|
|
73
|
+
if (version.trim()) u.versions.add(version.trim());
|
|
53
74
|
|
|
54
75
|
const cmd = norm(command);
|
|
55
76
|
if (cmd) u.rawCmds.add(cmd);
|
|
@@ -130,6 +151,19 @@ const SCORE_LABELS = [
|
|
|
130
151
|
'Sử dụng thường xuyên workflow',
|
|
131
152
|
];
|
|
132
153
|
|
|
154
|
+
// ── Version status for a user ──────────────────────────────────────────────
|
|
155
|
+
function getVersionStatus(u) {
|
|
156
|
+
const all = [...u.versions];
|
|
157
|
+
const parsed = all.map(v => ({ raw: v, parts: parseVersion(v) })).filter(v => v.parts);
|
|
158
|
+
if (parsed.length === 0) {
|
|
159
|
+
return { status: all.length ? 'unknown' : 'none', latest: null, all };
|
|
160
|
+
}
|
|
161
|
+
parsed.sort((a, b) => compareVersion(a.parts, b.parts));
|
|
162
|
+
const latest = parsed[parsed.length - 1];
|
|
163
|
+
const isOutdated = compareVersion(latest.parts, parseVersion(CURRENT_VERSION)) < 0;
|
|
164
|
+
return { status: isOutdated ? 'outdated' : 'ok', latest: latest.raw, all };
|
|
165
|
+
}
|
|
166
|
+
|
|
133
167
|
// ── Build summary row for each user ───────────────────────────────────────
|
|
134
168
|
function buildRow(email, u) {
|
|
135
169
|
const { events, days, rawCmds, gateCmds } = u;
|
|
@@ -198,12 +232,12 @@ function buildRow(email, u) {
|
|
|
198
232
|
impactLabel: '',
|
|
199
233
|
totalScore: score,
|
|
200
234
|
notes,
|
|
235
|
+
versionStatus: getVersionStatus(u),
|
|
201
236
|
};
|
|
202
237
|
}
|
|
203
238
|
|
|
204
239
|
// ── Excel writer ───────────────────────────────────────────────────────────
|
|
205
|
-
|
|
206
|
-
const workbook = new ExcelJS.Workbook();
|
|
240
|
+
function writeExcel(rows, workbook, sheetLabel, fromLabel, toLabel) {
|
|
207
241
|
const sheet = workbook.addWorksheet(`Score ${sheetLabel}`);
|
|
208
242
|
|
|
209
243
|
sheet.columns = [
|
|
@@ -247,20 +281,23 @@ async function writeExcel(rows, outputPath, sheetLabel, fromLabel, toLabel) {
|
|
|
247
281
|
const hRow = sheet.addRow(HEADERS);
|
|
248
282
|
hRow.height = 36;
|
|
249
283
|
hRow.eachCell(cell => {
|
|
250
|
-
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '
|
|
284
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4F81BD' } };
|
|
251
285
|
cell.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11 };
|
|
252
286
|
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
|
253
287
|
cell.border = border;
|
|
254
288
|
});
|
|
255
289
|
|
|
256
|
-
// Score → row color
|
|
290
|
+
// Score → row color (matches requirements/GetPoints-format.png)
|
|
257
291
|
const COLORS = {
|
|
258
|
-
0: '
|
|
259
|
-
1: '
|
|
260
|
-
2: '
|
|
261
|
-
3: '
|
|
262
|
-
4: '
|
|
263
|
-
5: '
|
|
292
|
+
0: 'FFFFFFFF',
|
|
293
|
+
1: 'FFF5F8EE',
|
|
294
|
+
2: 'FFEBF1DE',
|
|
295
|
+
3: 'FFD8E4BC',
|
|
296
|
+
4: 'FFC4D79B',
|
|
297
|
+
5: 'FF76933C',
|
|
298
|
+
};
|
|
299
|
+
const FONT_COLORS = {
|
|
300
|
+
0: 'FFFF0000',
|
|
264
301
|
};
|
|
265
302
|
|
|
266
303
|
rows.forEach((row, idx) => {
|
|
@@ -274,9 +311,11 @@ async function writeExcel(rows, outputPath, sheetLabel, fromLabel, toLabel) {
|
|
|
274
311
|
r.height = 20;
|
|
275
312
|
|
|
276
313
|
const fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: COLORS[row.depthScore] || 'FFFFFFFF' } };
|
|
314
|
+
const fontColor = FONT_COLORS[row.depthScore] || 'FF000000';
|
|
277
315
|
r.eachCell({ includeEmpty: true }, (cell, col) => {
|
|
278
316
|
cell.border = border;
|
|
279
317
|
cell.fill = fill;
|
|
318
|
+
cell.font = { color: { argb: fontColor } };
|
|
280
319
|
cell.alignment = (col <= 4 || (col >= 9 && col <= 13))
|
|
281
320
|
? { vertical: 'middle', horizontal: 'center' }
|
|
282
321
|
: { vertical: 'middle', horizontal: 'left', wrapText: false };
|
|
@@ -289,8 +328,81 @@ async function writeExcel(rows, outputPath, sheetLabel, fromLabel, toLabel) {
|
|
|
289
328
|
sheet.views = [{ state: 'frozen', ySplit: 2 }];
|
|
290
329
|
sheet.autoFilter = { from: 'A2', to: 'N2' };
|
|
291
330
|
|
|
331
|
+
return sheet;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Version warning sheet ───────────────────────────────────────────────────
|
|
335
|
+
function writeVersionSheet(workbook, rows) {
|
|
336
|
+
const sheet = workbook.addWorksheet('Version Warning');
|
|
337
|
+
sheet.columns = [
|
|
338
|
+
{ key: 'no', width: 5 },
|
|
339
|
+
{ key: 'email', width: 30 },
|
|
340
|
+
{ key: 'status', width: 16 },
|
|
341
|
+
{ key: 'latest', width: 16 },
|
|
342
|
+
{ key: 'allVersions', width: 30 },
|
|
343
|
+
{ key: 'note', width: 50 },
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const border = {
|
|
347
|
+
top: { style: 'thin' }, left: { style: 'thin' },
|
|
348
|
+
bottom: { style: 'thin' }, right: { style: 'thin' },
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
sheet.addRow([`Cảnh báo phiên bản — Yêu cầu phiên bản hiện tại: ${CURRENT_VERSION}`]);
|
|
352
|
+
sheet.mergeCells('A1:F1');
|
|
353
|
+
const title = sheet.getCell('A1');
|
|
354
|
+
title.font = { bold: true, size: 13, color: { argb: 'FF1F3864' } };
|
|
355
|
+
title.alignment = { vertical: 'middle', horizontal: 'center' };
|
|
356
|
+
title.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD6DCE4' } };
|
|
357
|
+
sheet.getRow(1).height = 28;
|
|
358
|
+
|
|
359
|
+
const HEADERS = ['#', 'Email', 'Trạng thái', 'Phiên bản mới nhất', 'Các phiên bản dùng trong tuần', 'Ghi chú'];
|
|
360
|
+
const hRow = sheet.addRow(HEADERS);
|
|
361
|
+
hRow.height = 20;
|
|
362
|
+
hRow.eachCell(cell => {
|
|
363
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4F81BD' } };
|
|
364
|
+
cell.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11 };
|
|
365
|
+
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
|
366
|
+
cell.border = border;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const warnings = rows.filter(row => row.versionStatus.status === 'outdated' || row.versionStatus.status === 'unknown');
|
|
370
|
+
|
|
371
|
+
const STATUS_LABEL = { outdated: 'CŨ HƠN', unknown: 'KHÔNG XÁC ĐỊNH', ok: 'OK', none: 'Không có dữ liệu' };
|
|
372
|
+
const STATUS_COLOR = { outdated: 'FFFFC7CE', unknown: 'FFFFEB9C' };
|
|
373
|
+
|
|
374
|
+
warnings.forEach((row, idx) => {
|
|
375
|
+
const vs = row.versionStatus;
|
|
376
|
+
const note = vs.status === 'outdated'
|
|
377
|
+
? `Đang dùng ${vs.latest}, chưa cập nhật lên ${CURRENT_VERSION}`
|
|
378
|
+
: 'Không đọc được phiên bản (giá trị "-" hoặc "unknown") — cần kiểm tra lại';
|
|
379
|
+
const r = sheet.addRow([
|
|
380
|
+
idx + 1, row.email, STATUS_LABEL[vs.status], vs.latest || '-', vs.all.join(', ') || '-', note,
|
|
381
|
+
]);
|
|
382
|
+
r.height = 20;
|
|
383
|
+
const fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: STATUS_COLOR[vs.status] || 'FFFFFFFF' } };
|
|
384
|
+
r.eachCell({ includeEmpty: true }, (cell, col) => {
|
|
385
|
+
cell.border = border;
|
|
386
|
+
cell.fill = fill;
|
|
387
|
+
cell.alignment = (col <= 4)
|
|
388
|
+
? { vertical: 'middle', horizontal: 'center' }
|
|
389
|
+
: { vertical: 'middle', horizontal: 'left', wrapText: false };
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
sheet.views = [{ state: 'frozen', ySplit: 2 }];
|
|
394
|
+
sheet.autoFilter = { from: 'A2', to: 'F2' };
|
|
395
|
+
|
|
396
|
+
return warnings;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function writeExcelFile(rows, outputPath, sheetLabel, fromLabel, toLabel) {
|
|
400
|
+
const workbook = new ExcelJS.Workbook();
|
|
401
|
+
writeExcel(rows, workbook, sheetLabel, fromLabel, toLabel);
|
|
402
|
+
const warnings = writeVersionSheet(workbook, rows);
|
|
292
403
|
await workbook.xlsx.writeFile(outputPath);
|
|
293
|
-
console.log(`Written: ${outputPath} (${rows.length} users)`);
|
|
404
|
+
console.log(`Written: ${outputPath} (${rows.length} users, ${warnings.length} version warning(s))`);
|
|
405
|
+
return warnings;
|
|
294
406
|
}
|
|
295
407
|
|
|
296
408
|
// ── Canonical user list (fixed order) ─────────────────────────────────────
|
|
@@ -315,6 +427,10 @@ const CANONICAL_USERS = [
|
|
|
315
427
|
'anhnt@relipasoft.com',
|
|
316
428
|
'khanhdd@relipasoft.com',
|
|
317
429
|
'hieunv@relipasoft.com',
|
|
430
|
+
'cucdt@relipasoft.com',
|
|
431
|
+
'khanhnv@relipasoft.com',
|
|
432
|
+
'namhh@relipasoft.com',
|
|
433
|
+
'tannt@relipasoft.com',
|
|
318
434
|
];
|
|
319
435
|
|
|
320
436
|
// Log email → canonical email (for slight mismatches)
|
|
@@ -323,7 +439,7 @@ const LOG_EMAIL_ALIAS = {
|
|
|
323
439
|
};
|
|
324
440
|
|
|
325
441
|
const EMPTY_USER = (email) => ({
|
|
326
|
-
email, events: 0, days: new Set(), rawCmds: new Set(), gateCmds: [],
|
|
442
|
+
email, events: 0, days: new Set(), rawCmds: new Set(), gateCmds: [], versions: new Set(),
|
|
327
443
|
});
|
|
328
444
|
|
|
329
445
|
// ── Main ───────────────────────────────────────────────────────────────────
|
|
@@ -357,6 +473,7 @@ const EMPTY_USER = (email) => ({
|
|
|
357
473
|
u.events += data.events;
|
|
358
474
|
data.days.forEach(d => u.days.add(d));
|
|
359
475
|
data.rawCmds.forEach(c => u.rawCmds.add(c));
|
|
476
|
+
data.versions.forEach(v => u.versions.add(v));
|
|
360
477
|
u.gateCmds.push(...data.gateCmds);
|
|
361
478
|
}
|
|
362
479
|
}
|
|
@@ -367,5 +484,9 @@ const EMPTY_USER = (email) => ({
|
|
|
367
484
|
return buildRow(email, u);
|
|
368
485
|
});
|
|
369
486
|
|
|
370
|
-
await
|
|
487
|
+
const warnings = await writeExcelFile(rows, outputFile, sheetLabel, fromLabel, toLabel);
|
|
488
|
+
if (warnings.length) {
|
|
489
|
+
console.log(`\n⚠️ Version warnings (< ${CURRENT_VERSION} or unknown):`);
|
|
490
|
+
warnings.forEach(w => console.log(` - ${w.email}: ${w.versionStatus.latest || 'unknown'} (${w.versionStatus.status})`));
|
|
491
|
+
}
|
|
371
492
|
})();
|
package/scripts/detect.js
CHANGED
|
@@ -74,6 +74,17 @@ class TaskDetector {
|
|
|
74
74
|
antiKeywords: [],
|
|
75
75
|
icon: '🧪',
|
|
76
76
|
description: 'Writing tests and QA'
|
|
77
|
+
},
|
|
78
|
+
'gen-doc': {
|
|
79
|
+
keywords: [
|
|
80
|
+
'gen-doc', 'generate doc', 'generate document', 'tạo tài liệu',
|
|
81
|
+
'tài liệu flow', 'tài liệu logic', 'tài liệu điều tra',
|
|
82
|
+
'flow document', 'logic document', 'analysis document',
|
|
83
|
+
'source analysis', 'feature document', 'system document'
|
|
84
|
+
],
|
|
85
|
+
antiKeywords: ['code', 'fix', 'bug'],
|
|
86
|
+
icon: '📝',
|
|
87
|
+
description: 'Generate document from source code / feature analysis'
|
|
77
88
|
}
|
|
78
89
|
};
|
|
79
90
|
}
|