pan-stack-cli 0.2.1 → 0.2.3
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 +37 -731
- package/bin/pan.js +4978 -269
- package/package.json +17 -5
- package/lib/ai-skills.js +0 -829
- package/lib/create.js +0 -105
- package/lib/discover.js +0 -294
- package/lib/git/git-checkout.js +0 -395
- package/lib/git/git-clone.js +0 -206
- package/lib/git/git-fetch-all.js +0 -129
- package/lib/git/git-grv.js +0 -125
- package/lib/git/git-gst.js +0 -165
- package/lib/git/git-mr.js +0 -546
- package/lib/git/git-pull.js +0 -137
- package/lib/git/git-push.js +0 -185
- package/lib/git/git-sync-main.js +0 -334
- package/lib/git/helpers/bulk-report.js +0 -202
- package/lib/git/helpers/cli.js +0 -188
- package/lib/git/helpers/colors.js +0 -19
- package/lib/git/helpers/git.js +0 -136
- package/lib/git/helpers/manifest.js +0 -223
- package/lib/git/helpers/repo-select.js +0 -416
- package/lib/git/helpers/repo-summary.js +0 -161
- package/lib/i18n.js +0 -61
- package/lib/inquirer-theme.js +0 -20
- package/lib/language.js +0 -121
- package/lib/languages.js +0 -14
- package/lib/manifest.js +0 -467
- package/lib/menu-prompt.js +0 -201
- package/lib/menu.js +0 -134
- package/lib/messages.js +0 -647
- package/lib/platform.js +0 -140
- package/lib/run-command.js +0 -334
- package/lib/select.js +0 -0
- package/lib/serve.js +0 -75
- package/lib/skill-registry-client.js +0 -173
- package/lib/stats/aggregate.js +0 -51
- package/lib/stats/identity.js +0 -20
- package/lib/stats/index.js +0 -189
- package/lib/stats/parse.js +0 -46
- package/lib/stats/range.js +0 -38
- package/lib/stats/report.js +0 -264
- package/lib/stats/review.js +0 -157
- package/lib/stats/scan.js +0 -92
- package/lib/stats/serve.js +0 -25
- package/lib/ui/api.js +0 -202
- package/lib/ui/clone.js +0 -115
- package/lib/ui/config.js +0 -182
- package/lib/ui/diff.js +0 -128
- package/lib/ui/export-gate.js +0 -79
- package/lib/ui/gen-react-mobile-code.js +0 -6
- package/lib/ui/gen-react-web-code.js +0 -75
- package/lib/ui/index.js +0 -89
- package/lib/ui/init.js +0 -252
- package/lib/ui/login.js +0 -128
- package/lib/ui/logout.js +0 -22
- package/lib/ui/palettes.js +0 -103
- package/lib/ui/published-html.js +0 -24
- package/lib/ui/sync.js +0 -137
- package/lib/ui/workspace.js +0 -213
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// Client cho registry server pan-agent-skill-registry (apps/pan-agent-skill-registry):
|
|
2
|
-
// gọi HTTP /api/registry + /api/packages/*.zip, quản lý lockfile local
|
|
3
|
-
// <workspaceRoot>/.pan/skills-lock.json để biết cái gì đã cài (installed vs updatable).
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const os = require("os");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const { execFileSync } = require("child_process");
|
|
8
|
-
const { isWindows } = require("./platform");
|
|
9
|
-
|
|
10
|
-
const DEFAULT_REGISTRY_URL = "http://localhost:4600";
|
|
11
|
-
const LOCK_RELATIVE_PATH = path.join(".pan", "skills-lock.json");
|
|
12
|
-
|
|
13
|
-
function getRegistryBaseUrl() {
|
|
14
|
-
return (process.env.PAN_SKILL_REGISTRY_URL || DEFAULT_REGISTRY_URL).replace(/\/+$/, "");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function friendlyNetworkError(baseUrl, err) {
|
|
18
|
-
return new Error(
|
|
19
|
-
`✗ Không kết nối được tới registry server (${baseUrl}). Cần server đang chạy trước — xem apps/pan-agent-skill-registry/README.md.` +
|
|
20
|
-
(err && err.message ? ` (${err.message})` : "")
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function fetchJson(url) {
|
|
25
|
-
let res;
|
|
26
|
-
try {
|
|
27
|
-
res = await fetch(url);
|
|
28
|
-
} catch (err) {
|
|
29
|
-
throw friendlyNetworkError(url, err);
|
|
30
|
-
}
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
throw new Error(`✗ Registry server trả lỗi HTTP ${res.status} cho ${url}`);
|
|
33
|
-
}
|
|
34
|
-
return res.json();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function fetchRegistry(baseUrl = getRegistryBaseUrl()) {
|
|
38
|
-
const data = await fetchJson(`${baseUrl}/api/registry`);
|
|
39
|
-
return Array.isArray(data.items) ? data.items : [];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function checkHealth(baseUrl = getRegistryBaseUrl()) {
|
|
43
|
-
try {
|
|
44
|
-
const data = await fetchJson(`${baseUrl}/health`);
|
|
45
|
-
return !!(data && data.ok);
|
|
46
|
-
} catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function lockPath(workspaceRoot) {
|
|
52
|
-
return path.join(workspaceRoot, LOCK_RELATIVE_PATH);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function loadLock(workspaceRoot) {
|
|
56
|
-
const p = lockPath(workspaceRoot);
|
|
57
|
-
if (!fs.existsSync(p)) {
|
|
58
|
-
return { installed: {} };
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
62
|
-
if (!raw || typeof raw !== "object") return { installed: {} };
|
|
63
|
-
if (!raw.installed || typeof raw.installed !== "object") raw.installed = {};
|
|
64
|
-
return raw;
|
|
65
|
-
} catch {
|
|
66
|
-
return { installed: {} };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function saveLock(workspaceRoot, lock) {
|
|
71
|
-
const dir = path.join(workspaceRoot, ".pan");
|
|
72
|
-
if (!fs.existsSync(dir)) {
|
|
73
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
-
}
|
|
75
|
-
fs.writeFileSync(lockPath(workspaceRoot), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// @returns { notInstalled: item[], updatable: item[], upToDate: item[] }
|
|
79
|
-
// (updatable/upToDate item objects are augmented with `oldVersion`).
|
|
80
|
-
function diffRegistry(items, lock) {
|
|
81
|
-
const installed = (lock && lock.installed) || {};
|
|
82
|
-
const notInstalled = [];
|
|
83
|
-
const updatable = [];
|
|
84
|
-
const upToDate = [];
|
|
85
|
-
for (const item of items) {
|
|
86
|
-
const entry = installed[item.name];
|
|
87
|
-
if (!entry) {
|
|
88
|
-
notInstalled.push(item);
|
|
89
|
-
} else if (entry.version !== item.version) {
|
|
90
|
-
updatable.push({ ...item, oldVersion: entry.version });
|
|
91
|
-
} else {
|
|
92
|
-
upToDate.push(item);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return { notInstalled, updatable, upToDate };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function downloadToTemp(url) {
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
fetch(url)
|
|
101
|
-
.then(async (res) => {
|
|
102
|
-
if (!res.ok) {
|
|
103
|
-
reject(new Error(`✗ Tải zip thất bại (HTTP ${res.status}) từ ${url}`));
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
107
|
-
const tmpFile = path.join(os.tmpdir(), `pan-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`);
|
|
108
|
-
fs.writeFileSync(tmpFile, buf);
|
|
109
|
-
resolve(tmpFile);
|
|
110
|
-
})
|
|
111
|
-
.catch((err) => reject(friendlyNetworkError(url, err)));
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function extractZip(zipFile, destDir) {
|
|
116
|
-
if (fs.existsSync(destDir)) {
|
|
117
|
-
fs.rmSync(destDir, { recursive: true, force: true });
|
|
118
|
-
}
|
|
119
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
120
|
-
|
|
121
|
-
if (isWindows()) {
|
|
122
|
-
try {
|
|
123
|
-
execFileSync(
|
|
124
|
-
"powershell",
|
|
125
|
-
["-NoProfile", "-Command", `Expand-Archive -Force -Path '${zipFile}' -DestinationPath '${destDir}'`],
|
|
126
|
-
{ stdio: "pipe" }
|
|
127
|
-
);
|
|
128
|
-
return;
|
|
129
|
-
} catch (err) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`✗ Không giải nén được (PowerShell Expand-Archive). Cần PowerShell khả dụng trên máy. (${err.message})`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
execFileSync("unzip", ["-o", "-q", zipFile, "-d", destDir], { stdio: "pipe" });
|
|
138
|
-
} catch (err) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`✗ Không giải nén được (thiếu lệnh 'unzip' hoặc file zip lỗi). Cài unzip rồi thử lại. (${err.message})`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Tải zip của `item` (từ baseUrl + item.zip) và giải nén vào `<destDir>/<item.name>/`
|
|
146
|
-
// (zip không có lớp thư mục bọc — ta tự tạo).
|
|
147
|
-
async function downloadAndExtract(baseUrl, item, destDir) {
|
|
148
|
-
const zipUrl = `${baseUrl}${item.zip}`;
|
|
149
|
-
const tmpFile = await downloadToTemp(zipUrl);
|
|
150
|
-
try {
|
|
151
|
-
const target = path.join(destDir, item.name);
|
|
152
|
-
extractZip(tmpFile, target);
|
|
153
|
-
return target;
|
|
154
|
-
} finally {
|
|
155
|
-
try {
|
|
156
|
-
fs.unlinkSync(tmpFile);
|
|
157
|
-
} catch {
|
|
158
|
-
/* bỏ qua */
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
module.exports = {
|
|
164
|
-
DEFAULT_REGISTRY_URL,
|
|
165
|
-
getRegistryBaseUrl,
|
|
166
|
-
fetchRegistry,
|
|
167
|
-
checkHealth,
|
|
168
|
-
lockPath,
|
|
169
|
-
loadLock,
|
|
170
|
-
saveLock,
|
|
171
|
-
diffRegistry,
|
|
172
|
-
downloadAndExtract,
|
|
173
|
-
};
|
package/lib/stats/aggregate.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const { canonicalEmail } = require("./identity");
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Gộp commit theo thành viên (email chuẩn) và theo dự án.
|
|
7
|
-
* (Commit đã được lọc theo range ở tầng git; range ở đây chỉ để chắc chắn.)
|
|
8
|
-
*/
|
|
9
|
-
function aggregate(commits, aliasMap = {}, range = {}) {
|
|
10
|
-
const { from = -Infinity, to = Infinity } = range;
|
|
11
|
-
const byEmail = new Map();
|
|
12
|
-
|
|
13
|
-
for (const c of commits) {
|
|
14
|
-
if (c.ts < from || c.ts > to) continue;
|
|
15
|
-
const email = canonicalEmail(c.email, aliasMap);
|
|
16
|
-
if (!byEmail.has(email)) {
|
|
17
|
-
byEmail.set(email, { email, name: c.name || email, commits: 0, additions: 0, deletions: 0, projects: new Map() });
|
|
18
|
-
}
|
|
19
|
-
const m = byEmail.get(email);
|
|
20
|
-
m.commits += 1;
|
|
21
|
-
m.additions += c.additions;
|
|
22
|
-
m.deletions += c.deletions;
|
|
23
|
-
if (c.name) m.name = c.name;
|
|
24
|
-
|
|
25
|
-
if (!m.projects.has(c.repoName)) {
|
|
26
|
-
m.projects.set(c.repoName, { name: c.repoName, commits: 0, additions: 0, deletions: 0 });
|
|
27
|
-
}
|
|
28
|
-
const p = m.projects.get(c.repoName);
|
|
29
|
-
p.commits += 1;
|
|
30
|
-
p.additions += c.additions;
|
|
31
|
-
p.deletions += c.deletions;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const members = [...byEmail.values()]
|
|
35
|
-
.map((m) => ({ ...m, projects: [...m.projects.values()].sort((a, b) => b.commits - a.commits) }))
|
|
36
|
-
.sort((a, b) => b.commits - a.commits);
|
|
37
|
-
|
|
38
|
-
const totals = members.reduce(
|
|
39
|
-
(acc, m) => {
|
|
40
|
-
acc.commits += m.commits;
|
|
41
|
-
acc.additions += m.additions;
|
|
42
|
-
acc.deletions += m.deletions;
|
|
43
|
-
return acc;
|
|
44
|
-
},
|
|
45
|
-
{ members: members.length, commits: 0, additions: 0, deletions: 0 }
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
return { members, totals };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
module.exports = { aggregate };
|
package/lib/stats/identity.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
|
|
5
|
-
/** Đọc alias-map.json (email biến thể -> email chuẩn). Không có thì {}. */
|
|
6
|
-
function loadAliasMap(file) {
|
|
7
|
-
try {
|
|
8
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
9
|
-
} catch {
|
|
10
|
-
return {};
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Chuẩn hoá email theo alias-map (fallback chính nó). */
|
|
15
|
-
function canonicalEmail(email, aliasMap = {}) {
|
|
16
|
-
const key = (email || "").toLowerCase().trim();
|
|
17
|
-
return aliasMap[key] || key || "unknown@local";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
module.exports = { loadAliasMap, canonicalEmail };
|
package/lib/stats/index.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const os = require("os");
|
|
6
|
-
const { findNearestWorkspace } = require("../discover");
|
|
7
|
-
const { reposFromWorkspace, discoverGitRepos, scanAll } = require("./scan");
|
|
8
|
-
const { resolveRange } = require("./range");
|
|
9
|
-
const { loadAliasMap, canonicalEmail } = require("./identity");
|
|
10
|
-
const { aggregate } = require("./aggregate");
|
|
11
|
-
const { buildReport } = require("./report");
|
|
12
|
-
const { serveHtml } = require("./serve");
|
|
13
|
-
const { buildReviewMarkdown, writeReview } = require("./review");
|
|
14
|
-
|
|
15
|
-
function parseArgs(argv) {
|
|
16
|
-
const o = { _: [], open: true, diff: true };
|
|
17
|
-
for (let i = 0; i < argv.length; i++) {
|
|
18
|
-
const a = argv[i];
|
|
19
|
-
if (a === "--since") o.since = argv[++i];
|
|
20
|
-
else if (a === "--from") o.from = argv[++i];
|
|
21
|
-
else if (a === "--to") o.to = argv[++i];
|
|
22
|
-
else if (a === "--member" || a === "-m") o.member = argv[++i];
|
|
23
|
-
else if (a === "--project" || a === "-p") o.project = argv[++i];
|
|
24
|
-
else if (a === "--port") o.port = parseInt(argv[++i], 10);
|
|
25
|
-
else if (a === "--out" || a === "-o") o.out = argv[++i];
|
|
26
|
-
else if (a === "--no-open") o.open = false;
|
|
27
|
-
else if (a === "--no-diff") o.diff = false;
|
|
28
|
-
else if (a === "--head-only") o.headOnly = true;
|
|
29
|
-
else if (a === "--max-diff-lines") o.maxDiffLines = parseInt(argv[++i], 10);
|
|
30
|
-
else if (a.startsWith("--since=")) o.since = a.slice(8);
|
|
31
|
-
else o._.push(a);
|
|
32
|
-
}
|
|
33
|
-
return o;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function dataDir() {
|
|
37
|
-
const dir = path.join(os.homedir(), ".pan-code-graph");
|
|
38
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
-
return dir;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Nguồn repo để thống kê: ưu tiên .project.yaml; nếu không có thì fallback quét
|
|
44
|
-
* git quanh pwd (thư mục cha + tối đa 2 cấp).
|
|
45
|
-
* @returns {{repos:Array<{name,dir}>, source:string}}
|
|
46
|
-
*/
|
|
47
|
-
function resolveRepos() {
|
|
48
|
-
const ws = findNearestWorkspace();
|
|
49
|
-
if (ws) return { repos: reposFromWorkspace(ws), source: `.project.yaml (${ws.project})` };
|
|
50
|
-
|
|
51
|
-
const repos = discoverGitRepos(process.cwd());
|
|
52
|
-
if (!repos.length) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Không tìm thấy pan-stack .project.yaml, cũng không thấy git repo nào từ ${process.cwd()} (đã quét pwd + 2 cấp con).`
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
console.log(`ℹ️ Không tìm thấy pan-stack .project.yaml — fallback quét git từ ${process.cwd()} (pwd + 2 cấp con).`);
|
|
58
|
-
return { repos, source: "fallback (quét từ pwd)" };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function printHelp() {
|
|
62
|
-
console.log(`pan stats — thống kê đóng góp Git từ .project.yaml
|
|
63
|
-
(không có .project.yaml → fallback quét git từ pwd xuống 2 cấp con)
|
|
64
|
-
|
|
65
|
-
lệnh:
|
|
66
|
-
pan stats [report] Quét & mở báo cáo HTML (mặc định)
|
|
67
|
-
pan stats people Liệt kê thành viên tham gia
|
|
68
|
-
pan stats review Xuất Markdown 1 thành viên × 1 dự án cho AI review
|
|
69
|
-
|
|
70
|
-
lọc thời gian (dùng chung):
|
|
71
|
-
--since <n> Preset ngày: 1 2 3 4 5 7 14 30 (30 = 1 tháng)
|
|
72
|
-
--from <YYYY-MM-DD> --to <YYYY-MM-DD> Khoảng ngày tuỳ chọn
|
|
73
|
-
|
|
74
|
-
options:
|
|
75
|
-
-m, --member <email|tên> Lọc theo thành viên (review: bắt buộc)
|
|
76
|
-
-p, --project <tên repo> Lọc theo dự án (review: bắt buộc)
|
|
77
|
-
--port <n> Cổng serve (mặc định 4321)
|
|
78
|
-
--no-open Không tự mở trình duyệt
|
|
79
|
-
-o, --out <file> Đường dẫn file xuất
|
|
80
|
-
--no-diff (review) chỉ tóm tắt, không kèm diff
|
|
81
|
-
--max-diff-lines <n> (review) giới hạn dòng diff (mặc định 6000)
|
|
82
|
-
--head-only Chỉ quét HEAD hiện tại (mặc định: --all — mọi nhánh
|
|
83
|
-
local + remote-tracking + tags). Nếu commit ở remote
|
|
84
|
-
chưa thấy, chạy 'pan fetch' trước.
|
|
85
|
-
|
|
86
|
-
ví dụ:
|
|
87
|
-
pan stats --since 7
|
|
88
|
-
pan stats report --from 2026-07-01 --to 2026-07-22
|
|
89
|
-
pan stats people
|
|
90
|
-
pan stats review -m an@fpt.com -p csdl-cc-backend --since 7 -o review.md
|
|
91
|
-
`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function runStats(argv) {
|
|
95
|
-
const o = parseArgs(argv);
|
|
96
|
-
const sub = ["report", "people", "review"].includes(o._[0]) ? o._.shift() : "report";
|
|
97
|
-
|
|
98
|
-
if (o._[0] === "help" || o.help) return printHelp();
|
|
99
|
-
|
|
100
|
-
if (sub === "review") return runReview(o);
|
|
101
|
-
if (sub === "people") return runPeople(o);
|
|
102
|
-
return runReport(o);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function loadAndScan(o) {
|
|
106
|
-
const { repos } = resolveRepos();
|
|
107
|
-
const range = resolveRange(o);
|
|
108
|
-
const allBranches = !o.headOnly;
|
|
109
|
-
const scope = allBranches ? "mọi nhánh" : "chỉ HEAD";
|
|
110
|
-
console.log(`🔍 Quét ${repos.length} repo · ${scope} · ${range.label}:`);
|
|
111
|
-
const { commits, repoStatus } = scanAll(repos, range, { allBranches });
|
|
112
|
-
const aliasMap = loadAliasMap(path.join(dataDir(), "alias-map.json"));
|
|
113
|
-
return { repos, range, commits, repoStatus, aliasMap };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function runReport(o) {
|
|
117
|
-
// Quét TOÀN BỘ lịch sử để nhúng commit thô → bộ lọc thời gian chạy phía client.
|
|
118
|
-
const { repos } = resolveRepos();
|
|
119
|
-
const fullRange = resolveRange({});
|
|
120
|
-
const allBranches = !o.headOnly;
|
|
121
|
-
const scope = allBranches ? "mọi nhánh" : "chỉ HEAD";
|
|
122
|
-
console.log(`🔍 Quét ${repos.length} repo · ${scope} · toàn bộ (lọc thời gian trong report):`);
|
|
123
|
-
const { commits, repoStatus } = scanAll(repos, fullRange, { allBranches });
|
|
124
|
-
const aliasMap = loadAliasMap(path.join(dataDir(), "alias-map.json"));
|
|
125
|
-
|
|
126
|
-
// Commit thô, gọn: e=email chuẩn, n=tên, t=ts(giây), r=repo, a=add, d=del
|
|
127
|
-
const rawCommits = commits.map((c) => ({
|
|
128
|
-
e: canonicalEmail(c.email, aliasMap),
|
|
129
|
-
n: c.name || c.email,
|
|
130
|
-
t: c.ts,
|
|
131
|
-
r: c.repoName,
|
|
132
|
-
a: c.additions,
|
|
133
|
-
d: c.deletions,
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
const unavailable = repoStatus.filter((r) => !r.available).map((r) => ({ name: r.name, reason: r.reason }));
|
|
137
|
-
const initial = resolveRange(o); // range khởi tạo từ CLI (mặc định: toàn bộ)
|
|
138
|
-
const meta = {
|
|
139
|
-
reposTotal: repoStatus.length,
|
|
140
|
-
reposAvailable: repoStatus.filter((r) => r.available).length,
|
|
141
|
-
unavailable,
|
|
142
|
-
initialSince: o.since != null ? Number(o.since) : null,
|
|
143
|
-
initialFrom: o.from || null,
|
|
144
|
-
initialTo: o.to || null,
|
|
145
|
-
initialMembers: o.member ? [o.member] : [],
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const html = buildReport(rawCommits, meta);
|
|
149
|
-
const outFile = o.out || path.join(dataDir(), "report.html");
|
|
150
|
-
fs.writeFileSync(outFile, html);
|
|
151
|
-
console.log(`💾 Đã ghi ${outFile} (${rawCommits.length} commit)`);
|
|
152
|
-
if (unavailable.length) console.log(`⚠️ ${unavailable.length} repo không đọc được (đã ghi cảnh báo trong report).`);
|
|
153
|
-
serveHtml(html, { port: o.port || 4321, open: o.open });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function runPeople(o) {
|
|
157
|
-
const { commits, aliasMap } = loadAndScan(o);
|
|
158
|
-
const { members } = aggregate(commits, aliasMap);
|
|
159
|
-
console.log(`\n👥 ${members.length} thành viên tham gia:\n`);
|
|
160
|
-
for (const m of members) {
|
|
161
|
-
console.log(` ${String(m.commits).padStart(5)} commit ${m.name} <${m.email}>`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function runReview(o) {
|
|
166
|
-
if (!o.member || !o.project) {
|
|
167
|
-
throw new Error("review cần --member <email|tên> và --project <tên repo>.");
|
|
168
|
-
}
|
|
169
|
-
const { repos } = resolveRepos();
|
|
170
|
-
const repo = repos.find((r) => r.name === o.project);
|
|
171
|
-
if (!repo) throw new Error(`Không có dự án '${o.project}' (repo khả dụng: ${repos.map((r) => r.name).join(", ")}).`);
|
|
172
|
-
if (!fs.existsSync(repo.dir)) throw new Error(`Dự án '${o.project}' chưa clone (${repo.dir}).`);
|
|
173
|
-
|
|
174
|
-
const range = resolveRange(o);
|
|
175
|
-
console.log(`📝 Tạo review: ${o.member} × ${o.project} (${range.label})`);
|
|
176
|
-
const md = buildReviewMarkdown(repo, o.member, range, {
|
|
177
|
-
withDiff: o.diff,
|
|
178
|
-
maxDiffLines: o.maxDiffLines,
|
|
179
|
-
allBranches: !o.headOnly,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const safe = `${o.project}_${o.member.replace(/[^a-z0-9]+/gi, "-")}`.toLowerCase();
|
|
183
|
-
const outFile = o.out || path.join(dataDir(), `review-${safe}.md`);
|
|
184
|
-
writeReview(outFile, md);
|
|
185
|
-
console.log(`✅ Đã ghi ${outFile}`);
|
|
186
|
-
console.log(` → Mở file này và nhờ AI review/chấm điểm.`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
module.exports = { runStats };
|
package/lib/stats/parse.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const US = "\x1f"; // Unit Separator — ranh giới field trong header
|
|
4
|
-
const HEAD = "@@@"; // tiền tố dòng header commit
|
|
5
|
-
|
|
6
|
-
// Header: @@@<hash>US<email>US<name>US<unixDate>US<subject>
|
|
7
|
-
// Theo sau là các dòng numstat: "additions\tdeletions\tpath" (binary = "-").
|
|
8
|
-
const PRETTY_FORMAT = `${HEAD}%H${US}%ae${US}%an${US}%ad${US}%s`;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Parse output thô của `git log --numstat` thành mảng commit.
|
|
12
|
-
*/
|
|
13
|
-
function parseGitLog(raw) {
|
|
14
|
-
const commits = [];
|
|
15
|
-
let cur = null;
|
|
16
|
-
|
|
17
|
-
for (const line of raw.split("\n")) {
|
|
18
|
-
if (line.startsWith(HEAD)) {
|
|
19
|
-
if (cur) commits.push(cur);
|
|
20
|
-
const [hash, email, name, date, subject] = line.slice(HEAD.length).split(US);
|
|
21
|
-
cur = {
|
|
22
|
-
hash,
|
|
23
|
-
email: (email || "").toLowerCase().trim(),
|
|
24
|
-
name: (name || "").trim(),
|
|
25
|
-
ts: Number(date) || 0,
|
|
26
|
-
subject: subject || "",
|
|
27
|
-
additions: 0,
|
|
28
|
-
deletions: 0,
|
|
29
|
-
files: 0,
|
|
30
|
-
};
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (!cur || !line.trim()) continue;
|
|
34
|
-
const parts = line.split("\t");
|
|
35
|
-
if (parts.length < 3) continue;
|
|
36
|
-
const add = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
|
|
37
|
-
const del = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
|
|
38
|
-
cur.additions += add;
|
|
39
|
-
cur.deletions += del;
|
|
40
|
-
cur.files += 1;
|
|
41
|
-
}
|
|
42
|
-
if (cur) commits.push(cur);
|
|
43
|
-
return commits;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
module.exports = { parseGitLog, PRETTY_FORMAT, US, HEAD };
|
package/lib/stats/range.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const PRESET_DAYS = new Set([1, 2, 3, 4, 5, 7, 14, 30]);
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Chuẩn hoá khoảng thời gian từ options.
|
|
7
|
-
* --since <n> (preset ngày) HOẶC --from/--to (YYYY-MM-DD). Không có → toàn bộ.
|
|
8
|
-
* @returns {{from:number,to:number,label:string}} unix giây
|
|
9
|
-
*/
|
|
10
|
-
function resolveRange({ since, from, to } = {}) {
|
|
11
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
12
|
-
|
|
13
|
-
if (from || to) {
|
|
14
|
-
const fromSec = from ? Math.floor(Date.parse(from + "T00:00:00") / 1000) : -Infinity;
|
|
15
|
-
const toSec = to ? Math.floor(Date.parse(to + "T23:59:59") / 1000) : nowSec;
|
|
16
|
-
const label = `${from || "…"} → ${to || "nay"}`;
|
|
17
|
-
return { from: fromSec, to: toSec, label };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (since != null && since !== "") {
|
|
21
|
-
const n = Number(since);
|
|
22
|
-
if (!Number.isFinite(n) || n <= 0) {
|
|
23
|
-
throw new Error(`--since không hợp lệ: ${since} (dùng 1 2 3 4 5 7 14 30)`);
|
|
24
|
-
}
|
|
25
|
-
const label = n === 30 ? "1 tháng gần đây" : `${n} ngày gần đây`;
|
|
26
|
-
return { from: nowSec - n * 86400, to: nowSec, label };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return { from: -Infinity, to: Infinity, label: "Toàn bộ lịch sử" };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** ISO date (giây) cho git --since/--until; -Infinity/Infinity → null. */
|
|
33
|
-
function toGitDate(sec) {
|
|
34
|
-
if (!Number.isFinite(sec)) return null;
|
|
35
|
-
return new Date(sec * 1000).toISOString();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
module.exports = { resolveRange, toGitDate, PRESET_DAYS };
|