@relipa/ai-flow-kit 0.0.9-beta.1 → 0.1.0-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/README.md +109 -13
- package/bin/aiflow.js +525 -475
- package/custom/mcp-presets/figma-desktop.json +9 -9
- package/custom/rules/project-conventions.md +51 -51
- package/custom/skills/read-study-requirement/SKILL.md +22 -3
- package/custom/templates/shared/gate-workflow.md +13 -12
- package/custom/templates/tools/gemini.md +22 -22
- package/docs/common/CHANGELOG.md +44 -1
- package/docs/common/QUICK_START.md +47 -0
- package/docs/common/cli-reference.md +60 -8
- package/docs/common/workflows/figma.md +105 -105
- package/package.json +10 -3
- package/scripts/create-score-excel.js +354 -0
- package/scripts/guide.js +2 -0
- package/scripts/hooks/session-start.js +315 -295
- package/scripts/hooks/session-stop.js +122 -122
- package/scripts/init.js +17 -2
- package/scripts/link-resolver.js +179 -0
- package/scripts/use.js +117 -11
|
@@ -1,105 +1,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
|
-
### Option A — REST API (recommended, no Desktop required)
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
ak init -a figma
|
|
15
|
-
# Prompts for FIGMA_API_TOKEN
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Get your token: [figma.com/settings](https://www.figma.com/settings) → Security → Personal access tokens → Create new token (File content: read).
|
|
19
|
-
|
|
20
|
-
### Option B — Official Figma MCP (requires Figma Desktop app)
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
ak init -a figma-desktop
|
|
24
|
-
# No token needed — uses Desktop app session
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Requirements: Figma Desktop app installed and the target file open before invoking the skill.
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Getting a Figma Node URL
|
|
32
|
-
|
|
33
|
-
1. Open your file in Figma (browser or Desktop)
|
|
34
|
-
2. Right-click on a frame or component → **Copy link to selection**
|
|
35
|
-
3. The URL looks like:
|
|
36
|
-
```
|
|
37
|
-
https://www.figma.com/design/FILEKEY/FileName?node-id=123-456
|
|
38
|
-
```
|
|
39
|
-
- `FILEKEY` — the file identifier (between `/design/` and the next `/`)
|
|
40
|
-
- `node-id=123-456` — the specific frame or component to generate
|
|
41
|
-
|
|
42
|
-
You can also link to the entire file (no `node-id`) to let the skill list available frames.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## Trigger the Skill
|
|
47
|
-
|
|
48
|
-
Paste the Figma URL into Claude Code with a generation request:
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
Generate component from this Figma frame:
|
|
52
|
-
https://www.figma.com/design/XXXXX/App?node-id=123-456
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
Implement the UserCard UI from Figma:
|
|
57
|
-
https://www.figma.com/design/XXXXX/App?node-id=78-910
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
Read Figma and generate component following my project conventions:
|
|
62
|
-
https://www.figma.com/design/XXXXX/App?node-id=42-0
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## What to Expect
|
|
68
|
-
|
|
69
|
-
The skill outputs in this order:
|
|
70
|
-
|
|
71
|
-
1. **Design Summary** — layout structure, color palette, typography scale used in the design
|
|
72
|
-
2. **Component file** — complete code for your detected framework (React, Next.js App Router, Vue 3, or Angular)
|
|
73
|
-
3. **Usage example** — how to import and use the component
|
|
74
|
-
4. **Notes** — anything that couldn't be mapped 1:1 from Figma (custom fonts, unsupported effects, etc.)
|
|
75
|
-
|
|
76
|
-
**Framework auto-detection order:**
|
|
77
|
-
1. Read `CLAUDE.md` for framework identifier (`nextjs-app-router`, `reactjs`, `vue-nuxt`, `angular`)
|
|
78
|
-
2. Scan project files (`app/` dir → App Router, `angular.json` → Angular, `nuxt.config.*` → Vue)
|
|
79
|
-
3. Fallback → Next.js / React
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Review Checklist
|
|
84
|
-
|
|
85
|
-
After the component is generated, verify:
|
|
86
|
-
|
|
87
|
-
- [ ] Layout matches Figma (flex direction, alignment, gap)
|
|
88
|
-
- [ ] Colors are correct (Tailwind class or arbitrary value)
|
|
89
|
-
- [ ] Typography matches (size, weight, line-height)
|
|
90
|
-
- [ ] Spacing correct (padding, margin, gap)
|
|
91
|
-
- [ ] Interactive states handled (`'use client'` in App Router, variants in React/Vue/Angular)
|
|
92
|
-
- [ ] Images use correct component (`next/image` for Next.js, `<img>` for others)
|
|
93
|
-
- [ ] `className` / `class` prop exposed for external overrides
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## Troubleshooting
|
|
98
|
-
|
|
99
|
-
| Problem | Fix |
|
|
100
|
-
|---|---|
|
|
101
|
-
| `get_figma_data` tool not found | Run `ak init -a figma` to configure the MCP |
|
|
102
|
-
| "Invalid API token" | Token expired — create a new one at figma.com/settings |
|
|
103
|
-
| Wrong node generated | Check the `node-id` in the URL matches your selection in Figma |
|
|
104
|
-
| Figma Desktop MCP not responding | Ensure Figma Desktop app is open with the file loaded |
|
|
105
|
-
| Component uses wrong framework | Add framework to `CLAUDE.md` (e.g. `nextjs-app-router`) |
|
|
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
|
+
### Option A — REST API (recommended, no Desktop required)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ak init -a figma
|
|
15
|
+
# Prompts for FIGMA_API_TOKEN
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Get your token: [figma.com/settings](https://www.figma.com/settings) → Security → Personal access tokens → Create new token (File content: read).
|
|
19
|
+
|
|
20
|
+
### Option B — Official Figma MCP (requires Figma Desktop app)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
ak init -a figma-desktop
|
|
24
|
+
# No token needed — uses Desktop app session
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requirements: Figma Desktop app installed and the target file open before invoking the skill.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Getting a Figma Node URL
|
|
32
|
+
|
|
33
|
+
1. Open your file in Figma (browser or Desktop)
|
|
34
|
+
2. Right-click on a frame or component → **Copy link to selection**
|
|
35
|
+
3. The URL looks like:
|
|
36
|
+
```
|
|
37
|
+
https://www.figma.com/design/FILEKEY/FileName?node-id=123-456
|
|
38
|
+
```
|
|
39
|
+
- `FILEKEY` — the file identifier (between `/design/` and the next `/`)
|
|
40
|
+
- `node-id=123-456` — the specific frame or component to generate
|
|
41
|
+
|
|
42
|
+
You can also link to the entire file (no `node-id`) to let the skill list available frames.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Trigger the Skill
|
|
47
|
+
|
|
48
|
+
Paste the Figma URL into Claude Code with a generation request:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Generate component from this Figma frame:
|
|
52
|
+
https://www.figma.com/design/XXXXX/App?node-id=123-456
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Implement the UserCard UI from Figma:
|
|
57
|
+
https://www.figma.com/design/XXXXX/App?node-id=78-910
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Read Figma and generate component following my project conventions:
|
|
62
|
+
https://www.figma.com/design/XXXXX/App?node-id=42-0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## What to Expect
|
|
68
|
+
|
|
69
|
+
The skill outputs in this order:
|
|
70
|
+
|
|
71
|
+
1. **Design Summary** — layout structure, color palette, typography scale used in the design
|
|
72
|
+
2. **Component file** — complete code for your detected framework (React, Next.js App Router, Vue 3, or Angular)
|
|
73
|
+
3. **Usage example** — how to import and use the component
|
|
74
|
+
4. **Notes** — anything that couldn't be mapped 1:1 from Figma (custom fonts, unsupported effects, etc.)
|
|
75
|
+
|
|
76
|
+
**Framework auto-detection order:**
|
|
77
|
+
1. Read `CLAUDE.md` for framework identifier (`nextjs-app-router`, `reactjs`, `vue-nuxt`, `angular`)
|
|
78
|
+
2. Scan project files (`app/` dir → App Router, `angular.json` → Angular, `nuxt.config.*` → Vue)
|
|
79
|
+
3. Fallback → Next.js / React
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Review Checklist
|
|
84
|
+
|
|
85
|
+
After the component is generated, verify:
|
|
86
|
+
|
|
87
|
+
- [ ] Layout matches Figma (flex direction, alignment, gap)
|
|
88
|
+
- [ ] Colors are correct (Tailwind class or arbitrary value)
|
|
89
|
+
- [ ] Typography matches (size, weight, line-height)
|
|
90
|
+
- [ ] Spacing correct (padding, margin, gap)
|
|
91
|
+
- [ ] Interactive states handled (`'use client'` in App Router, variants in React/Vue/Angular)
|
|
92
|
+
- [ ] Images use correct component (`next/image` for Next.js, `<img>` for others)
|
|
93
|
+
- [ ] `className` / `class` prop exposed for external overrides
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Troubleshooting
|
|
98
|
+
|
|
99
|
+
| Problem | Fix |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `get_figma_data` tool not found | Run `ak init -a figma` to configure the MCP |
|
|
102
|
+
| "Invalid API token" | Token expired — create a new one at figma.com/settings |
|
|
103
|
+
| Wrong node generated | Check the `node-id` in the URL matches your selection in Figma |
|
|
104
|
+
| Figma Desktop MCP not responding | Ensure Figma Desktop app is open with the file loaded |
|
|
105
|
+
| 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.0
|
|
3
|
+
"version": "0.1.0-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": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"aiflow": "node bin/aiflow.js",
|
|
44
|
-
"test": "
|
|
44
|
+
"test": "jest",
|
|
45
45
|
"lint": "echo \"Add linting in future\" && exit 0",
|
|
46
46
|
"docs": "echo \"Documentation available in ./docs\"",
|
|
47
47
|
"publish:beta": "npm publish --tag beta",
|
|
@@ -55,6 +55,13 @@
|
|
|
55
55
|
"semver": "^7.7.4"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
+
"exceljs": "^4.4.0",
|
|
59
|
+
"jest": "^30.4.2",
|
|
58
60
|
"typescript": "^6.0.2"
|
|
61
|
+
},
|
|
62
|
+
"jest": {
|
|
63
|
+
"testMatch": [
|
|
64
|
+
"<rootDir>/tests/**/*.test.js"
|
|
65
|
+
]
|
|
59
66
|
}
|
|
60
|
-
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ExcelJS = require('exceljs');
|
|
4
|
+
|
|
5
|
+
// ── Normalize command aliases ──────────────────────────────────────────────
|
|
6
|
+
const ALIAS_MAP = {
|
|
7
|
+
u: 'update', up: 'update',
|
|
8
|
+
i: 'init',
|
|
9
|
+
v: 'version',
|
|
10
|
+
h: 'help',
|
|
11
|
+
p: 'prompt', promt: 'prompt', prmopt: 'prompt', promlist: 'prompt', propt: 'prompt',
|
|
12
|
+
g: 'guide', guid: 'guide',
|
|
13
|
+
t: 'task', st: 'status', s: 'status',
|
|
14
|
+
sw: 'switch', l: 'list', ls: 'list',
|
|
15
|
+
ctx: 'context',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const EXPLORE_CMDS = new Set(['init', 'update', 'version', 'help', 'status', 'unknown_flag']);
|
|
19
|
+
const USE_CMDS = new Set(['use', 'use.help', 'prompt', 'guide', 'guideline', 'sync-skills', 'detect']);
|
|
20
|
+
|
|
21
|
+
function norm(cmd) {
|
|
22
|
+
if (!cmd || cmd === '-' || cmd.trim() === '') return null;
|
|
23
|
+
const c = cmd.toLowerCase().trim();
|
|
24
|
+
return ALIAS_MAP[c] || c;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Parse log file ─────────────────────────────────────────────────────────
|
|
28
|
+
function parseLog(filePath) {
|
|
29
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
|
|
30
|
+
const users = {};
|
|
31
|
+
|
|
32
|
+
for (let i = 1; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i].trim();
|
|
34
|
+
if (!line) continue;
|
|
35
|
+
const cols = line.split('\t');
|
|
36
|
+
// Cols: Timestamp[0] Repo[1] User[2] OS[3] Node[4] Version[5] Command[6]
|
|
37
|
+
// AI Tool[7] Gate[8] Ticket[9] ...
|
|
38
|
+
const user = cols[2];
|
|
39
|
+
if (!user || user === '-' || user === '') continue;
|
|
40
|
+
|
|
41
|
+
const timestamp = cols[0] || '';
|
|
42
|
+
const command = cols[6] || '';
|
|
43
|
+
const ticket = cols[9] || '';
|
|
44
|
+
|
|
45
|
+
const date = timestamp.split(' ')[0]; // dd/MM/yyyy
|
|
46
|
+
|
|
47
|
+
if (!users[user]) {
|
|
48
|
+
users[user] = { email: user, days: new Set(), events: 0, rawCmds: new Set(), gateCmds: [] };
|
|
49
|
+
}
|
|
50
|
+
const u = users[user];
|
|
51
|
+
u.events++;
|
|
52
|
+
if (date) u.days.add(date);
|
|
53
|
+
|
|
54
|
+
const cmd = norm(command);
|
|
55
|
+
if (cmd) u.rawCmds.add(cmd);
|
|
56
|
+
|
|
57
|
+
const gateMatch = command.match(/^gate(\d+)\.(start|approved)$/i);
|
|
58
|
+
if (gateMatch) {
|
|
59
|
+
u.gateCmds.push({ gateNum: +gateMatch[1], type: gateMatch[2].toLowerCase(), ticket });
|
|
60
|
+
}
|
|
61
|
+
// generic "gate.approved" (no number)
|
|
62
|
+
if (/^gate\.approved$/i.test(command)) {
|
|
63
|
+
u.gateCmds.push({ gateNum: 0, type: 'approved', ticket });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return users;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Score calculator ───────────────────────────────────────────────────────
|
|
70
|
+
function calcScore(u) {
|
|
71
|
+
const { events, days, rawCmds, gateCmds } = u;
|
|
72
|
+
const activeDays = days.size;
|
|
73
|
+
|
|
74
|
+
if (events === 0) return 0;
|
|
75
|
+
|
|
76
|
+
// Gate aggregation
|
|
77
|
+
const gateStarted = new Set(); // gate numbers
|
|
78
|
+
const gateApproved = new Set();
|
|
79
|
+
const ticketsStarted = new Set();
|
|
80
|
+
const ticketsCompleted = new Set(); // gate4.approved or gate5.start
|
|
81
|
+
|
|
82
|
+
for (const g of gateCmds) {
|
|
83
|
+
const hasTicket = g.ticket && g.ticket !== '-' && g.ticket !== '';
|
|
84
|
+
if (g.type === 'start') {
|
|
85
|
+
gateStarted.add(g.gateNum);
|
|
86
|
+
if (hasTicket) ticketsStarted.add(g.ticket);
|
|
87
|
+
}
|
|
88
|
+
if (g.type === 'approved') {
|
|
89
|
+
gateApproved.add(g.gateNum);
|
|
90
|
+
if (hasTicket && g.gateNum >= 4) ticketsCompleted.add(g.ticket);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// gate5.start also counts as completed
|
|
94
|
+
for (const g of gateCmds) {
|
|
95
|
+
if (g.gateNum === 5 && g.type === 'start' && g.ticket && g.ticket !== '-') {
|
|
96
|
+
ticketsCompleted.add(g.ticket);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hasUse = USE_CMDS.has('use') && rawCmds.has('use');
|
|
101
|
+
const hasPrompt = rawCmds.has('prompt');
|
|
102
|
+
const hasGuide = rawCmds.has('guide') || rawCmds.has('guideline') || rawCmds.has('sync-skills');
|
|
103
|
+
const hasGateHighApproved = gateApproved.has(3) || gateApproved.has(4);
|
|
104
|
+
|
|
105
|
+
// Score 5: gate 3-5 approved + ≥ 3 tickets completed
|
|
106
|
+
if (hasGateHighApproved && ticketsCompleted.size >= 3) return 5;
|
|
107
|
+
|
|
108
|
+
// Score 4: gate.approved ≥ 1 + ≥ 1 ticket completed
|
|
109
|
+
if (gateApproved.size > 0 && ticketsCompleted.size >= 1) return 4;
|
|
110
|
+
|
|
111
|
+
// Score 3: gate.start with real ticket + ≥ 3 active days
|
|
112
|
+
if (ticketsStarted.size > 0 && activeDays >= 3) return 3;
|
|
113
|
+
|
|
114
|
+
// Score 2: has use/prompt/guide OR ≥ 2 active days
|
|
115
|
+
if (hasUse || hasPrompt || hasGuide || activeDays >= 2) return 2;
|
|
116
|
+
|
|
117
|
+
// Score 1: only exploration commands
|
|
118
|
+
const onlyExplore = [...rawCmds].every(c => EXPLORE_CMDS.has(c));
|
|
119
|
+
if (onlyExplore && rawCmds.size > 0) return 1;
|
|
120
|
+
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const SCORE_LABELS = [
|
|
125
|
+
'Không hoạt động',
|
|
126
|
+
'Mới khám phá',
|
|
127
|
+
'Bắt đầu dùng',
|
|
128
|
+
'Dùng có workflow',
|
|
129
|
+
'Hoàn thành workflow',
|
|
130
|
+
'Sử dụng thường xuyên workflow',
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// ── Build summary row for each user ───────────────────────────────────────
|
|
134
|
+
function buildRow(email, u) {
|
|
135
|
+
const { events, days, rawCmds, gateCmds } = u;
|
|
136
|
+
const activeDays = days.size;
|
|
137
|
+
const score = calcScore(u);
|
|
138
|
+
|
|
139
|
+
// Key commands: deduplicated, meaningful, sorted
|
|
140
|
+
const ignoredCmds = new Set([
|
|
141
|
+
'unknown_flag', 'use.help', 'tel', 'tel.help', 'telementry',
|
|
142
|
+
'flush', 'flush.help', 'reset', 'context', 'memory.search',
|
|
143
|
+
'upgrade', 'gate.help', 'prompt.help', 'task.help',
|
|
144
|
+
]);
|
|
145
|
+
const gatePattern = /^gate\d*\.(start|approved|help)$|^gate\.(approved|help)$/i;
|
|
146
|
+
|
|
147
|
+
const displayCmds = [...rawCmds]
|
|
148
|
+
.filter(c => !ignoredCmds.has(c) && !gatePattern.test(c))
|
|
149
|
+
.sort();
|
|
150
|
+
|
|
151
|
+
// Compact gate summary
|
|
152
|
+
const startedGates = [...new Set(gateCmds.filter(g => g.type === 'start').map(g => g.gateNum))].sort((a, b) => a - b);
|
|
153
|
+
const approvedGates = [...new Set(gateCmds.filter(g => g.type === 'approved').map(g => g.gateNum))].sort((a, b) => a - b);
|
|
154
|
+
|
|
155
|
+
const ticketsStarted = [...new Set(gateCmds.filter(g => g.type === 'start' && g.ticket && g.ticket !== '-').map(g => g.ticket))];
|
|
156
|
+
const ticketsApproved = [...new Set(gateCmds.filter(g => g.type === 'approved' && g.ticket && g.ticket !== '-').map(g => g.ticket))];
|
|
157
|
+
const ticketsCompleted = [...new Set(gateCmds.filter(g =>
|
|
158
|
+
((g.type === 'approved' && g.gateNum >= 4) || (g.gateNum === 5 && g.type === 'start'))
|
|
159
|
+
&& g.ticket && g.ticket !== '-'
|
|
160
|
+
).map(g => g.ticket))];
|
|
161
|
+
|
|
162
|
+
const gateStartStr = startedGates.length ? `G${startedGates.join(',')} / ${ticketsStarted.length} ticket(s)` : '';
|
|
163
|
+
const gateApprovedStr = approvedGates.length ? `G${approvedGates.join(',')} / ${ticketsApproved.length} ticket(s)` : '';
|
|
164
|
+
const ticketsStr = ticketsCompleted.length ? ticketsCompleted.join(', ') : '';
|
|
165
|
+
|
|
166
|
+
// Notes
|
|
167
|
+
let notes = '';
|
|
168
|
+
if (score === 0) notes = 'Không có event nào trong tuần';
|
|
169
|
+
else if (score === 1) notes = 'Chỉ có lệnh khám phá cơ bản (init/update/version/help)';
|
|
170
|
+
else if (score === 2) {
|
|
171
|
+
if (ticketsStarted.length > 0) notes = `Có gate.start nhưng chưa gate4.approved, ${activeDays} ngày active`;
|
|
172
|
+
else notes = `${displayCmds.slice(0, 3).join('/')}, ${activeDays} ngày active, chưa dùng gate workflow`;
|
|
173
|
+
}
|
|
174
|
+
else if (score === 3) notes = `Gate workflow bắt đầu, ${ticketsStarted.length} ticket(s), chưa hoàn thành`;
|
|
175
|
+
else if (score === 4) notes = `${ticketsCompleted.length} ticket hoàn thành (gate4+)`;
|
|
176
|
+
else if (score === 5) notes = `Gate 3-5 approved, ${ticketsCompleted.length} tickets hoàn thành`;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
email,
|
|
180
|
+
events,
|
|
181
|
+
activeDays,
|
|
182
|
+
keyCommands: displayCmds.join(', '),
|
|
183
|
+
gateStart: gateStartStr,
|
|
184
|
+
gateApproved: gateApprovedStr,
|
|
185
|
+
tickets: ticketsStr,
|
|
186
|
+
depthScore: score,
|
|
187
|
+
depthLabel: SCORE_LABELS[score],
|
|
188
|
+
impactScore: '',
|
|
189
|
+
impactLabel: '',
|
|
190
|
+
totalScore: score,
|
|
191
|
+
notes,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Excel writer ───────────────────────────────────────────────────────────
|
|
196
|
+
async function writeExcel(rows, outputPath) {
|
|
197
|
+
const workbook = new ExcelJS.Workbook();
|
|
198
|
+
const sheet = workbook.addWorksheet('Score 18-24.05.2026');
|
|
199
|
+
|
|
200
|
+
sheet.columns = [
|
|
201
|
+
{ key: 'no', width: 5 },
|
|
202
|
+
{ key: 'email', width: 30 },
|
|
203
|
+
{ key: 'events', width: 8 },
|
|
204
|
+
{ key: 'activeDays', width: 10 },
|
|
205
|
+
{ key: 'keyCommands', width: 40 },
|
|
206
|
+
{ key: 'gateStart', width: 24 },
|
|
207
|
+
{ key: 'gateApproved', width: 24 },
|
|
208
|
+
{ key: 'tickets', width: 44 },
|
|
209
|
+
{ key: 'depthScore', width: 8 },
|
|
210
|
+
{ key: 'depthLabel', width: 28 },
|
|
211
|
+
{ key: 'impactScore', width: 8 },
|
|
212
|
+
{ key: 'impactLabel', width: 18 },
|
|
213
|
+
{ key: 'totalScore', width: 8 },
|
|
214
|
+
{ key: 'notes', width: 54 },
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const border = {
|
|
218
|
+
top: { style: 'thin' }, left: { style: 'thin' },
|
|
219
|
+
bottom: { style: 'thin' }, right: { style: 'thin' },
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Title row
|
|
223
|
+
sheet.addRow(['AI Flow Kit — Weekly Score Report: 18/05/2026 – 24/05/2026']);
|
|
224
|
+
sheet.mergeCells('A1:N1');
|
|
225
|
+
const title = sheet.getCell('A1');
|
|
226
|
+
title.font = { bold: true, size: 13, color: { argb: 'FF1F3864' } };
|
|
227
|
+
title.alignment = { vertical: 'middle', horizontal: 'center' };
|
|
228
|
+
title.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD6DCE4' } };
|
|
229
|
+
sheet.getRow(1).height = 28;
|
|
230
|
+
|
|
231
|
+
// Header row
|
|
232
|
+
const HEADERS = [
|
|
233
|
+
'#', 'Email', 'Events', 'Active\nDays', 'Key Commands',
|
|
234
|
+
'Gate Start', 'Gate Approved', 'Tickets Processed',
|
|
235
|
+
'Depth\nScore', 'Depth Label', 'Impact\nScore', 'Impact Label',
|
|
236
|
+
'Total\nScore', 'Notes',
|
|
237
|
+
];
|
|
238
|
+
const hRow = sheet.addRow(HEADERS);
|
|
239
|
+
hRow.height = 36;
|
|
240
|
+
hRow.eachCell(cell => {
|
|
241
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } };
|
|
242
|
+
cell.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11 };
|
|
243
|
+
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
|
244
|
+
cell.border = border;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Score → row color
|
|
248
|
+
const COLORS = {
|
|
249
|
+
0: 'FFFFC7CE',
|
|
250
|
+
1: 'FFFFEB9C',
|
|
251
|
+
2: 'FFFFFFCC',
|
|
252
|
+
3: 'FFD9EAD3',
|
|
253
|
+
4: 'FF93C47D',
|
|
254
|
+
5: 'FF6AA84F',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
rows.forEach((row, idx) => {
|
|
258
|
+
const excelRowNum = idx + 3; // 1=title, 2=header
|
|
259
|
+
const r = sheet.addRow([
|
|
260
|
+
idx + 1, row.email, row.events, row.activeDays,
|
|
261
|
+
row.keyCommands, row.gateStart, row.gateApproved, row.tickets,
|
|
262
|
+
row.depthScore, row.depthLabel, row.impactScore, row.impactLabel,
|
|
263
|
+
null, row.notes,
|
|
264
|
+
]);
|
|
265
|
+
r.height = 20;
|
|
266
|
+
|
|
267
|
+
const fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: COLORS[row.depthScore] || 'FFFFFFFF' } };
|
|
268
|
+
r.eachCell({ includeEmpty: true }, (cell, col) => {
|
|
269
|
+
cell.border = border;
|
|
270
|
+
cell.fill = fill;
|
|
271
|
+
cell.alignment = (col <= 4 || (col >= 9 && col <= 13))
|
|
272
|
+
? { vertical: 'middle', horizontal: 'center' }
|
|
273
|
+
: { vertical: 'middle', horizontal: 'left', wrapText: false };
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Total Score = Depth + Impact formula
|
|
277
|
+
r.getCell(13).value = { formula: `I${excelRowNum}+K${excelRowNum}` };
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
sheet.views = [{ state: 'frozen', ySplit: 2 }];
|
|
281
|
+
sheet.autoFilter = { from: 'A2', to: 'N2' };
|
|
282
|
+
|
|
283
|
+
await workbook.xlsx.writeFile(outputPath);
|
|
284
|
+
console.log(`Written: ${outputPath} (${rows.length} users)`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Canonical user list (fixed order) ─────────────────────────────────────
|
|
288
|
+
const CANONICAL_USERS = [
|
|
289
|
+
'anhtt@relipasoft.com',
|
|
290
|
+
'hoangdv@relipasoft.com',
|
|
291
|
+
'chinhtt@relipasoft.com',
|
|
292
|
+
'hieptv@relipasoft.com',
|
|
293
|
+
'phongnx@relipasoft.com',
|
|
294
|
+
'dungha@relipasoft.com',
|
|
295
|
+
'thangnv@relipasoft.com',
|
|
296
|
+
'nguyenlt@relipasoft.com',
|
|
297
|
+
'thuongvv@relipasoft.com',
|
|
298
|
+
'yenvtb@relipasoft.com',
|
|
299
|
+
'tructth@relipasoft.com',
|
|
300
|
+
'thainq@relipasoft.com',
|
|
301
|
+
'tuoittx@relipasoft.com',
|
|
302
|
+
'hungdv@relipasoft.com',
|
|
303
|
+
'hiepnk@relipasoft.com',
|
|
304
|
+
'khoihn@relipasoft.com',
|
|
305
|
+
'huynq@relipasoft.com',
|
|
306
|
+
'thinhvq@relipasoft.com',
|
|
307
|
+
'banv@relipasoft.com',
|
|
308
|
+
'duongnt@relipasoft.com',
|
|
309
|
+
'anhnt@relipasoft.com',
|
|
310
|
+
'khanhdd@relipasoft.com',
|
|
311
|
+
'hieunv@relipasoft.com',
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
// Log email → canonical email (for slight mismatches)
|
|
315
|
+
const LOG_EMAIL_ALIAS = {
|
|
316
|
+
'tructh@relipasoft.com': 'tructth@relipasoft.com',
|
|
317
|
+
'duongnt1@relipasoft.com': 'duongnt@relipasoft.com',
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const EMPTY_USER = (email) => ({
|
|
321
|
+
email, events: 0, days: new Set(), rawCmds: new Set(), gateCmds: [],
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
325
|
+
(async () => {
|
|
326
|
+
const logFile = path.join(__dirname, '..', 'tellog', 'log_18-05-2026_24-05-2026');
|
|
327
|
+
const outputFile = path.join(__dirname, '..', 'tellog', 'log_18-05-2026_24-05-2026_new.xlsx');
|
|
328
|
+
|
|
329
|
+
const rawUsers = parseLog(logFile);
|
|
330
|
+
|
|
331
|
+
// Re-key log users using alias map (tructh → tructth, duongnt1 → duongnt)
|
|
332
|
+
const users = {};
|
|
333
|
+
for (const [logEmail, data] of Object.entries(rawUsers)) {
|
|
334
|
+
const canonical = LOG_EMAIL_ALIAS[logEmail] || logEmail;
|
|
335
|
+
if (!users[canonical]) {
|
|
336
|
+
users[canonical] = { ...data, email: canonical };
|
|
337
|
+
} else {
|
|
338
|
+
// merge (shouldn't happen but just in case)
|
|
339
|
+
const u = users[canonical];
|
|
340
|
+
u.events += data.events;
|
|
341
|
+
data.days.forEach(d => u.days.add(d));
|
|
342
|
+
data.rawCmds.forEach(c => u.rawCmds.add(c));
|
|
343
|
+
u.gateCmds.push(...data.gateCmds);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Build rows in canonical order; fill score 0 for absent users
|
|
348
|
+
const rows = CANONICAL_USERS.map(email => {
|
|
349
|
+
const u = users[email] || EMPTY_USER(email);
|
|
350
|
+
return buildRow(email, u);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await writeExcel(rows, outputFile);
|
|
354
|
+
})();
|
package/scripts/guide.js
CHANGED
|
@@ -164,6 +164,7 @@ function showCommands() {
|
|
|
164
164
|
group: 'Workflow (use per task)',
|
|
165
165
|
items: [
|
|
166
166
|
['aiflow use <TICKET-ID>', '', 'Load context from Backlog/Jira'],
|
|
167
|
+
['aiflow use <T1> <T2> ...', '', 'Multi-target: 1st is primary, rest supplementary (v0.1.0+)'],
|
|
167
168
|
['aiflow use', '--fast', 'Fast Mode: minimal Q&A (Default)'],
|
|
168
169
|
['aiflow use', '--full', 'Full Mode: deep analysis with Q&A'],
|
|
169
170
|
['aiflow use', '--file <path>', 'Load from local text/json file'],
|
|
@@ -171,6 +172,7 @@ function showCommands() {
|
|
|
171
172
|
['aiflow use', '--with-comments', 'Include all comments'],
|
|
172
173
|
['aiflow use', '--comments-last <n>', 'Only get last N comments'],
|
|
173
174
|
['aiflow use', '--comments-from <n>', 'Get comments from index N onward'],
|
|
175
|
+
['aiflow fetch-links <url>', '', 'Fetch Backlog/Jira link → SupplementaryContext JSON (v0.1.0+)'],
|
|
174
176
|
['aiflow prompt <type>', '', 'Generate AI prompt'],
|
|
175
177
|
['aiflow prompt', '--list', 'View all prompt types'],
|
|
176
178
|
['aiflow prompt', '--output <file>', 'Save prompt to file'],
|