@zeyue0329/xiaoma-cli 1.0.37 → 1.0.38
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/.idea/workspace.xml +27 -26
- package/JAVA-BACKEND-COMMANDS-REFERENCE.md +62 -52
- package/JAVA-BACKEND-ITERATION-GUIDE.md +125 -18
- package/README.md +1 -1
- package/common/utils/bmad-doc-template.md +5 -5
- package/dist/agents/analyst.txt +35 -5
- package/dist/agents/architect.txt +217 -31
- package/dist/agents/automation-orchestrator.txt +4 -4
- package/dist/agents/dev.txt +3 -3
- package/dist/agents/full-requirement-orchestrator.txt +11 -11
- package/dist/agents/qa.txt +102 -102
- package/dist/agents/sm.txt +6 -6
- package/dist/agents/ux-expert.txt +6 -1
- package/dist/agents/workflow-executor.txt +879 -0
- package/dist/agents/xiaoma-master.txt +258 -37
- package/dist/teams/team-all.txt +1223 -445
- package/dist/teams/team-fullstack-with-database.txt +384 -446
- package/dist/teams/team-fullstack.txt +258 -37
- package/dist/teams/team-ide-minimal.txt +111 -111
- package/dist/teams/team-no-ui.txt +252 -36
- package/docs/architecture-sharding-modification.md +623 -0
- package/docs/automated-requirements-analysis-outputs.md +896 -0
- package/package.json +1 -1
- package/tools/builders/web-builder.js +292 -142
- package/tools/bump-all-versions.js +50 -32
- package/tools/cli.js +52 -47
- package/tools/flattener/aggregate.js +30 -12
- package/tools/flattener/binary.js +46 -43
- package/tools/flattener/discovery.js +23 -15
- package/tools/flattener/files.js +6 -6
- package/tools/flattener/ignoreRules.js +122 -121
- package/tools/flattener/main.js +249 -144
- package/tools/flattener/projectRoot.js +74 -69
- package/tools/flattener/prompts.js +12 -10
- package/tools/flattener/stats.helpers.js +90 -61
- package/tools/flattener/stats.js +1 -1
- package/tools/flattener/test-matrix.js +225 -170
- package/tools/flattener/xml.js +31 -23
- package/tools/installer/bin/xiaoma.js +199 -153
- package/tools/installer/lib/config-loader.js +76 -47
- package/tools/installer/lib/file-manager.js +101 -44
- package/tools/installer/lib/ide-base-setup.js +49 -39
- package/tools/installer/lib/ide-setup.js +694 -380
- package/tools/installer/lib/installer.js +802 -469
- package/tools/installer/lib/memory-profiler.js +22 -12
- package/tools/installer/lib/module-manager.js +16 -14
- package/tools/installer/lib/resource-locator.js +61 -35
- package/tools/lib/dependency-resolver.js +34 -23
- package/tools/lib/yaml-utils.js +7 -2
- package/tools/preview-release-notes.js +33 -25
- package/tools/shared/bannerArt.js +3 -3
- package/tools/sync-installer-version.js +16 -7
- package/tools/upgraders/v3-to-v4-upgrader.js +244 -163
- package/tools/version-bump.js +24 -18
- package/tools/xiaoma-npx-wrapper.js +15 -10
- package/tools/yaml-format.js +60 -36
- package/xiaoma-core/agent-teams/team-fullstack-with-database.yaml +0 -1
- package/xiaoma-core/agents/automated-fix-validator.yaml +2 -1
- package/xiaoma-core/agents/automated-quality-validator.yaml +10 -5
- package/xiaoma-core/agents/automation-orchestrator.md +4 -4
- package/xiaoma-core/agents/dev.md +4 -4
- package/xiaoma-core/agents/enhanced-workflow-orchestrator.yaml +2 -1
- package/xiaoma-core/agents/full-requirement-orchestrator.md +11 -11
- package/xiaoma-core/agents/global-requirements-auditor.yaml +11 -3
- package/xiaoma-core/agents/intelligent-template-adapter.yaml +19 -5
- package/xiaoma-core/agents/master-execution-engine.yaml +19 -5
- package/xiaoma-core/agents/workflow-executor.md +8 -4
- package/xiaoma-core/agents/xiaoma-master.md +1 -1
- package/xiaoma-core/data/test-levels-framework.md +12 -12
- package/xiaoma-core/tasks/analyze-existing-database.md +1 -1
- package/xiaoma-core/tasks/apply-qa-fixes.md +3 -3
- package/xiaoma-core/tasks/batch-story-generation.md +22 -22
- package/xiaoma-core/tasks/create-enhanced-story-with-database.md +6 -6
- package/xiaoma-core/tasks/nfr-assess.md +6 -6
- package/xiaoma-core/tasks/project-integration-testing.md +42 -42
- package/xiaoma-core/tasks/qa-gate.md +23 -23
- package/xiaoma-core/tasks/review-story.md +18 -18
- package/xiaoma-core/tasks/risk-profile.md +25 -25
- package/xiaoma-core/tasks/serial-development-orchestration.md +51 -51
- package/xiaoma-core/tasks/test-design.md +9 -9
- package/xiaoma-core/tasks/trace-requirements.md +21 -21
- package/xiaoma-core/templates/competitor-analysis-tmpl.yaml +35 -5
- package/xiaoma-core/templates/front-end-architecture-tmpl.yaml +77 -11
- package/xiaoma-core/templates/front-end-spec-tmpl.yaml +6 -1
- package/xiaoma-core/templates/fullstack-architecture-tmpl.yaml +140 -20
- package/xiaoma-core/templates/global-qa-monitoring-tmpl.yaml +2 -1
- package/xiaoma-core/templates/requirements-coverage-audit.yaml +2 -1
- package/xiaoma-core/workflows/automated-requirements-analysis.yaml +4 -4
- package/dist/agents/database-architect.txt +0 -322
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("node:path");
|
|
3
3
|
|
|
4
4
|
// Deno/Node compatibility: explicitly import process
|
|
5
|
-
const process = require(
|
|
6
|
-
const { execFile } = require(
|
|
7
|
-
const { promisify } = require(
|
|
5
|
+
const process = require("node:process");
|
|
6
|
+
const { execFile } = require("node:child_process");
|
|
7
|
+
const { promisify } = require("node:util");
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
|
|
10
10
|
// Simple memoization across calls (keyed by realpath of startDir)
|
|
@@ -18,7 +18,7 @@ async function _tryRun(cmd, args, cwd, timeoutMs = 500) {
|
|
|
18
18
|
windowsHide: true,
|
|
19
19
|
maxBuffer: 1024 * 1024,
|
|
20
20
|
});
|
|
21
|
-
const out = String(stdout ||
|
|
21
|
+
const out = String(stdout || "").trim();
|
|
22
22
|
return out || null;
|
|
23
23
|
} catch {
|
|
24
24
|
return null;
|
|
@@ -27,17 +27,21 @@ async function _tryRun(cmd, args, cwd, timeoutMs = 500) {
|
|
|
27
27
|
|
|
28
28
|
async function _detectVcsTopLevel(startDir) {
|
|
29
29
|
// Run common VCS root queries in parallel; ignore failures
|
|
30
|
-
const gitP = _tryRun(
|
|
31
|
-
const hgP = _tryRun(
|
|
30
|
+
const gitP = _tryRun("git", ["rev-parse", "--show-toplevel"], startDir);
|
|
31
|
+
const hgP = _tryRun("hg", ["root"], startDir);
|
|
32
32
|
const svnP = (async () => {
|
|
33
|
-
const show = await _tryRun(
|
|
33
|
+
const show = await _tryRun(
|
|
34
|
+
"svn",
|
|
35
|
+
["info", "--show-item", "wc-root"],
|
|
36
|
+
startDir,
|
|
37
|
+
);
|
|
34
38
|
if (show) return show;
|
|
35
|
-
const info = await _tryRun(
|
|
39
|
+
const info = await _tryRun("svn", ["info"], startDir);
|
|
36
40
|
if (info) {
|
|
37
41
|
const line = info
|
|
38
42
|
.split(/\r?\n/)
|
|
39
|
-
.find((l) => l.toLowerCase().startsWith(
|
|
40
|
-
if (line) return line.split(
|
|
43
|
+
.find((l) => l.toLowerCase().startsWith("working copy root path:"));
|
|
44
|
+
if (line) return line.split(":").slice(1).join(":").trim();
|
|
41
45
|
}
|
|
42
46
|
return null;
|
|
43
47
|
})();
|
|
@@ -73,80 +77,81 @@ async function findProjectRoot(startDir) {
|
|
|
73
77
|
const checks = [];
|
|
74
78
|
|
|
75
79
|
const add = (rel, weight) => {
|
|
76
|
-
const makePath = (d) =>
|
|
80
|
+
const makePath = (d) =>
|
|
81
|
+
Array.isArray(rel) ? path.join(d, ...rel) : path.join(d, rel);
|
|
77
82
|
checks.push({ makePath, weight });
|
|
78
83
|
};
|
|
79
84
|
|
|
80
85
|
// Highest priority: explicit sentinel markers
|
|
81
|
-
add(
|
|
82
|
-
add(
|
|
83
|
-
add(
|
|
86
|
+
add(".project-root", 110);
|
|
87
|
+
add(".workspace-root", 110);
|
|
88
|
+
add(".repo-root", 110);
|
|
84
89
|
|
|
85
90
|
// Highest priority: VCS roots
|
|
86
|
-
add(
|
|
87
|
-
add(
|
|
88
|
-
add(
|
|
91
|
+
add(".git", 100);
|
|
92
|
+
add(".hg", 95);
|
|
93
|
+
add(".svn", 95);
|
|
89
94
|
|
|
90
95
|
// Monorepo/workspace indicators
|
|
91
|
-
add(
|
|
92
|
-
add(
|
|
93
|
-
add(
|
|
94
|
-
add(
|
|
95
|
-
add(
|
|
96
|
-
add(
|
|
97
|
-
add(
|
|
98
|
-
add(
|
|
99
|
-
add(
|
|
100
|
-
add(
|
|
96
|
+
add("pnpm-workspace.yaml", 90);
|
|
97
|
+
add("lerna.json", 90);
|
|
98
|
+
add("turbo.json", 90);
|
|
99
|
+
add("nx.json", 90);
|
|
100
|
+
add("rush.json", 90);
|
|
101
|
+
add("go.work", 90);
|
|
102
|
+
add("WORKSPACE", 90);
|
|
103
|
+
add("WORKSPACE.bazel", 90);
|
|
104
|
+
add("MODULE.bazel", 90);
|
|
105
|
+
add("pants.toml", 90);
|
|
101
106
|
|
|
102
107
|
// Lockfiles and package-manager/top-level locks
|
|
103
|
-
add(
|
|
104
|
-
add(
|
|
105
|
-
add(
|
|
106
|
-
add(
|
|
107
|
-
add(
|
|
108
|
-
add(
|
|
109
|
-
add(
|
|
110
|
-
add(
|
|
111
|
-
add(
|
|
108
|
+
add("yarn.lock", 85);
|
|
109
|
+
add("pnpm-lock.yaml", 85);
|
|
110
|
+
add("package-lock.json", 85);
|
|
111
|
+
add("bun.lockb", 85);
|
|
112
|
+
add("Cargo.lock", 85);
|
|
113
|
+
add("composer.lock", 85);
|
|
114
|
+
add("poetry.lock", 85);
|
|
115
|
+
add("Pipfile.lock", 85);
|
|
116
|
+
add("Gemfile.lock", 85);
|
|
112
117
|
|
|
113
118
|
// Build-system root indicators
|
|
114
|
-
add(
|
|
115
|
-
add(
|
|
116
|
-
add(
|
|
117
|
-
add(
|
|
118
|
-
add(
|
|
119
|
-
add([
|
|
119
|
+
add("settings.gradle", 80);
|
|
120
|
+
add("settings.gradle.kts", 80);
|
|
121
|
+
add("gradlew", 80);
|
|
122
|
+
add("pom.xml", 80);
|
|
123
|
+
add("build.sbt", 80);
|
|
124
|
+
add(["project", "build.properties"], 80);
|
|
120
125
|
|
|
121
126
|
// Language/project config markers
|
|
122
|
-
add(
|
|
123
|
-
add(
|
|
124
|
-
add(
|
|
125
|
-
add(
|
|
126
|
-
add(
|
|
127
|
-
add(
|
|
128
|
-
add(
|
|
129
|
-
add(
|
|
130
|
-
add(
|
|
131
|
-
add(
|
|
132
|
-
add(
|
|
133
|
-
add(
|
|
134
|
-
add(
|
|
135
|
-
add(
|
|
136
|
-
add(
|
|
137
|
-
add(
|
|
138
|
-
add(
|
|
139
|
-
add(
|
|
140
|
-
add(
|
|
141
|
-
add(
|
|
127
|
+
add("deno.json", 75);
|
|
128
|
+
add("deno.jsonc", 75);
|
|
129
|
+
add("pyproject.toml", 75);
|
|
130
|
+
add("Pipfile", 75);
|
|
131
|
+
add("requirements.txt", 75);
|
|
132
|
+
add("go.mod", 75);
|
|
133
|
+
add("Cargo.toml", 75);
|
|
134
|
+
add("composer.json", 75);
|
|
135
|
+
add("mix.exs", 75);
|
|
136
|
+
add("Gemfile", 75);
|
|
137
|
+
add("CMakeLists.txt", 75);
|
|
138
|
+
add("stack.yaml", 75);
|
|
139
|
+
add("cabal.project", 75);
|
|
140
|
+
add("rebar.config", 75);
|
|
141
|
+
add("pubspec.yaml", 75);
|
|
142
|
+
add("flake.nix", 75);
|
|
143
|
+
add("shell.nix", 75);
|
|
144
|
+
add("default.nix", 75);
|
|
145
|
+
add(".tool-versions", 75);
|
|
146
|
+
add("package.json", 74); // generic Node project (lower than lockfiles/workspaces)
|
|
142
147
|
|
|
143
148
|
// Changesets
|
|
144
|
-
add([
|
|
145
|
-
add(
|
|
149
|
+
add([".changeset", "config.json"], 70);
|
|
150
|
+
add(".changeset", 70);
|
|
146
151
|
|
|
147
152
|
// Custom markers via env (comma-separated names)
|
|
148
153
|
if (process.env.PROJECT_ROOT_MARKERS) {
|
|
149
|
-
for (const name of process.env.PROJECT_ROOT_MARKERS.split(
|
|
154
|
+
for (const name of process.env.PROJECT_ROOT_MARKERS.split(",")
|
|
150
155
|
.map((s) => s.trim())
|
|
151
156
|
.filter(Boolean)) {
|
|
152
157
|
add(name, 72);
|
|
@@ -155,10 +160,10 @@ async function findProjectRoot(startDir) {
|
|
|
155
160
|
|
|
156
161
|
/** Check for package.json with "workspaces" */
|
|
157
162
|
const hasWorkspacePackageJson = async (d) => {
|
|
158
|
-
const pkgPath = path.join(d,
|
|
163
|
+
const pkgPath = path.join(d, "package.json");
|
|
159
164
|
if (!(await exists(pkgPath))) return false;
|
|
160
165
|
try {
|
|
161
|
-
const raw = await fs.readFile(pkgPath,
|
|
166
|
+
const raw = await fs.readFile(pkgPath, "utf8");
|
|
162
167
|
const pkg = JSON.parse(raw);
|
|
163
168
|
return Boolean(pkg && pkg.workspaces);
|
|
164
169
|
} catch {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const os = require(
|
|
2
|
-
const path = require(
|
|
3
|
-
const readline = require(
|
|
4
|
-
const process = require(
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const readline = require("node:readline");
|
|
4
|
+
const process = require("node:process");
|
|
5
5
|
|
|
6
6
|
function expandHome(p) {
|
|
7
7
|
if (!p) return p;
|
|
8
|
-
if (p.startsWith(
|
|
8
|
+
if (p.startsWith("~")) return path.join(os.homedir(), p.slice(1));
|
|
9
9
|
return p;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -27,16 +27,18 @@ function promptQuestion(question) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async function promptYesNo(question, defaultYes = true) {
|
|
30
|
-
const suffix = defaultYes ?
|
|
31
|
-
const ans = (await promptQuestion(`${question}${suffix}`))
|
|
30
|
+
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
31
|
+
const ans = (await promptQuestion(`${question}${suffix}`))
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase();
|
|
32
34
|
if (!ans) return defaultYes;
|
|
33
|
-
if ([
|
|
34
|
-
if ([
|
|
35
|
+
if (["y", "yes"].includes(ans)) return true;
|
|
36
|
+
if (["n", "no"].includes(ans)) return false;
|
|
35
37
|
return promptYesNo(question, defaultYes);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
async function promptPath(question, defaultValue) {
|
|
39
|
-
const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` :
|
|
41
|
+
const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ""}: `;
|
|
40
42
|
const ans = (await promptQuestion(prompt)).trim();
|
|
41
43
|
return expandHome(ans || defaultValue);
|
|
42
44
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const zlib = require(
|
|
6
|
-
const { Buffer } = require(
|
|
7
|
-
const crypto = require(
|
|
8
|
-
const cp = require(
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const zlib = require("node:zlib");
|
|
6
|
+
const { Buffer } = require("node:buffer");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
const cp = require("node:child_process");
|
|
9
9
|
|
|
10
10
|
const KB = 1024;
|
|
11
11
|
const MB = 1024 * KB;
|
|
@@ -13,13 +13,17 @@ const MB = 1024 * KB;
|
|
|
13
13
|
const formatSize = (bytes) => {
|
|
14
14
|
if (bytes < 1024) return `${bytes} B`;
|
|
15
15
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
16
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
16
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
17
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
17
18
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const percentile = (sorted, p) => {
|
|
21
22
|
if (sorted.length === 0) return 0;
|
|
22
|
-
const idx = Math.min(
|
|
23
|
+
const idx = Math.min(
|
|
24
|
+
sorted.length - 1,
|
|
25
|
+
Math.max(0, Math.ceil((p / 100) * sorted.length) - 1),
|
|
26
|
+
);
|
|
23
27
|
return sorted[idx];
|
|
24
28
|
};
|
|
25
29
|
|
|
@@ -34,10 +38,10 @@ async function enrichAllFiles(textFiles, binaryFiles) {
|
|
|
34
38
|
const allFiles = [];
|
|
35
39
|
|
|
36
40
|
async function enrich(file, isBinary) {
|
|
37
|
-
const ext = (path.extname(file.path) ||
|
|
38
|
-
const dir = path.dirname(file.path) ||
|
|
41
|
+
const ext = (path.extname(file.path) || "").toLowerCase();
|
|
42
|
+
const dir = path.dirname(file.path) || ".";
|
|
39
43
|
const depth = file.path.split(path.sep).filter(Boolean).length;
|
|
40
|
-
const hidden = file.path.split(path.sep).some((seg) => seg.startsWith(
|
|
44
|
+
const hidden = file.path.split(path.sep).some((seg) => seg.startsWith("."));
|
|
41
45
|
let mtimeMs = 0;
|
|
42
46
|
let isSymlink = false;
|
|
43
47
|
try {
|
|
@@ -69,15 +73,19 @@ async function enrichAllFiles(textFiles, binaryFiles) {
|
|
|
69
73
|
|
|
70
74
|
function buildHistogram(allFiles) {
|
|
71
75
|
const buckets = [
|
|
72
|
-
[1 * KB,
|
|
73
|
-
[10 * KB,
|
|
74
|
-
[100 * KB,
|
|
75
|
-
[1 * MB,
|
|
76
|
-
[10 * MB,
|
|
77
|
-
[100 * MB,
|
|
78
|
-
[Infinity,
|
|
76
|
+
[1 * KB, "0–1KB"],
|
|
77
|
+
[10 * KB, "1–10KB"],
|
|
78
|
+
[100 * KB, "10–100KB"],
|
|
79
|
+
[1 * MB, "100KB–1MB"],
|
|
80
|
+
[10 * MB, "1–10MB"],
|
|
81
|
+
[100 * MB, "10–100MB"],
|
|
82
|
+
[Infinity, ">=100MB"],
|
|
79
83
|
];
|
|
80
|
-
const histogram = buckets.map(([_, label]) => ({
|
|
84
|
+
const histogram = buckets.map(([_, label]) => ({
|
|
85
|
+
label,
|
|
86
|
+
count: 0,
|
|
87
|
+
bytes: 0,
|
|
88
|
+
}));
|
|
81
89
|
for (const f of allFiles) {
|
|
82
90
|
for (const [i, bucket] of buckets.entries()) {
|
|
83
91
|
if (f.size < bucket[0]) {
|
|
@@ -93,7 +101,7 @@ function buildHistogram(allFiles) {
|
|
|
93
101
|
function aggregateByExtension(allFiles) {
|
|
94
102
|
const byExtension = new Map();
|
|
95
103
|
for (const f of allFiles) {
|
|
96
|
-
const key = f.ext ||
|
|
104
|
+
const key = f.ext || "<none>";
|
|
97
105
|
const v = byExtension.get(key) || { ext: key, count: 0, bytes: 0 };
|
|
98
106
|
v.count++;
|
|
99
107
|
v.bytes += f.size;
|
|
@@ -111,13 +119,13 @@ function aggregateByDirectory(allFiles) {
|
|
|
111
119
|
byDirectory.set(dir, v);
|
|
112
120
|
}
|
|
113
121
|
for (const f of allFiles) {
|
|
114
|
-
const parts = f.dir ===
|
|
115
|
-
let acc =
|
|
122
|
+
const parts = f.dir === "." ? [] : f.dir.split(path.sep);
|
|
123
|
+
let acc = "";
|
|
116
124
|
for (let i = 0; i < parts.length; i++) {
|
|
117
125
|
acc = i === 0 ? parts[0] : acc + path.sep + parts[i];
|
|
118
126
|
addDirBytes(acc, f.size);
|
|
119
127
|
}
|
|
120
|
-
if (parts.length === 0) addDirBytes(
|
|
128
|
+
if (parts.length === 0) addDirBytes(".", f.size);
|
|
121
129
|
}
|
|
122
130
|
return [...byDirectory.values()].sort((a, b) => b.bytes - a.bytes);
|
|
123
131
|
}
|
|
@@ -141,15 +149,18 @@ function computeTemporal(allFiles, nowMs) {
|
|
|
141
149
|
let oldest = null,
|
|
142
150
|
newest = null;
|
|
143
151
|
const ageBuckets = [
|
|
144
|
-
{ label:
|
|
145
|
-
{ label:
|
|
146
|
-
{ label:
|
|
147
|
-
{ label:
|
|
148
|
-
{ label:
|
|
149
|
-
{ label:
|
|
152
|
+
{ label: "> 1 year", minDays: 365, maxDays: Infinity, count: 0, bytes: 0 },
|
|
153
|
+
{ label: "6–12 months", minDays: 180, maxDays: 365, count: 0, bytes: 0 },
|
|
154
|
+
{ label: "1–6 months", minDays: 30, maxDays: 180, count: 0, bytes: 0 },
|
|
155
|
+
{ label: "7–30 days", minDays: 7, maxDays: 30, count: 0, bytes: 0 },
|
|
156
|
+
{ label: "1–7 days", minDays: 1, maxDays: 7, count: 0, bytes: 0 },
|
|
157
|
+
{ label: "< 1 day", minDays: 0, maxDays: 1, count: 0, bytes: 0 },
|
|
150
158
|
];
|
|
151
159
|
for (const f of allFiles) {
|
|
152
|
-
const ageDays = Math.max(
|
|
160
|
+
const ageDays = Math.max(
|
|
161
|
+
0,
|
|
162
|
+
(nowMs - (f.mtimeMs || nowMs)) / (24 * 60 * 60 * 1000),
|
|
163
|
+
);
|
|
153
164
|
for (const b of ageBuckets) {
|
|
154
165
|
if (ageDays >= b.minDays && ageDays < b.maxDays) {
|
|
155
166
|
b.count++;
|
|
@@ -162,10 +173,16 @@ function computeTemporal(allFiles, nowMs) {
|
|
|
162
173
|
}
|
|
163
174
|
return {
|
|
164
175
|
oldest: oldest
|
|
165
|
-
? {
|
|
176
|
+
? {
|
|
177
|
+
path: oldest.path,
|
|
178
|
+
mtime: oldest.mtimeMs ? new Date(oldest.mtimeMs).toISOString() : null,
|
|
179
|
+
}
|
|
166
180
|
: null,
|
|
167
181
|
newest: newest
|
|
168
|
-
? {
|
|
182
|
+
? {
|
|
183
|
+
path: newest.path,
|
|
184
|
+
mtime: newest.mtimeMs ? new Date(newest.mtimeMs).toISOString() : null,
|
|
185
|
+
}
|
|
169
186
|
: null,
|
|
170
187
|
ageBuckets,
|
|
171
188
|
};
|
|
@@ -180,8 +197,12 @@ function computeQuality(allFiles, textFiles) {
|
|
|
180
197
|
const symlinks = allFiles.filter((f) => f.isSymlink).length;
|
|
181
198
|
const largeThreshold = 50 * MB;
|
|
182
199
|
const suspiciousThreshold = 100 * MB;
|
|
183
|
-
const largeFilesCount = allFiles.filter(
|
|
184
|
-
|
|
200
|
+
const largeFilesCount = allFiles.filter(
|
|
201
|
+
(f) => f.size >= largeThreshold,
|
|
202
|
+
).length;
|
|
203
|
+
const suspiciousLargeFilesCount = allFiles.filter(
|
|
204
|
+
(f) => f.size >= suspiciousThreshold,
|
|
205
|
+
).length;
|
|
185
206
|
return {
|
|
186
207
|
zeroByteFiles,
|
|
187
208
|
emptyTextFiles,
|
|
@@ -210,8 +231,8 @@ function computeDuplicates(allFiles, textFiles) {
|
|
|
210
231
|
for (const tf of textGroup) {
|
|
211
232
|
try {
|
|
212
233
|
const src = textFiles.find((x) => x.absolutePath === tf.absolutePath);
|
|
213
|
-
const content = src ? src.content :
|
|
214
|
-
const h = crypto.createHash(
|
|
234
|
+
const content = src ? src.content : "";
|
|
235
|
+
const h = crypto.createHash("sha1").update(content).digest("hex");
|
|
215
236
|
const g = contentHashGroups.get(h) || [];
|
|
216
237
|
g.push(tf);
|
|
217
238
|
contentHashGroups.set(h, g);
|
|
@@ -222,7 +243,7 @@ function computeDuplicates(allFiles, textFiles) {
|
|
|
222
243
|
for (const [_h, g] of contentHashGroups.entries()) {
|
|
223
244
|
if (g.length > 1)
|
|
224
245
|
duplicateCandidates.push({
|
|
225
|
-
reason:
|
|
246
|
+
reason: "same-size+text-hash",
|
|
226
247
|
size: Number(sizeKey),
|
|
227
248
|
count: g.length,
|
|
228
249
|
files: g.map((f) => f.path),
|
|
@@ -230,7 +251,7 @@ function computeDuplicates(allFiles, textFiles) {
|
|
|
230
251
|
}
|
|
231
252
|
if (otherGroup.length > 1) {
|
|
232
253
|
duplicateCandidates.push({
|
|
233
|
-
reason:
|
|
254
|
+
reason: "same-size",
|
|
234
255
|
size: Number(sizeKey),
|
|
235
256
|
count: otherGroup.length,
|
|
236
257
|
files: otherGroup.map((f) => f.path),
|
|
@@ -248,7 +269,7 @@ function estimateCompressibility(textFiles) {
|
|
|
248
269
|
const sampleLen = Math.min(256 * 1024, tf.size || 0);
|
|
249
270
|
if (sampleLen <= 0) continue;
|
|
250
271
|
const sample = tf.content.slice(0, sampleLen);
|
|
251
|
-
const gz = zlib.gzipSync(Buffer.from(sample,
|
|
272
|
+
const gz = zlib.gzipSync(Buffer.from(sample, "utf8"));
|
|
252
273
|
compSampleBytes += sampleLen;
|
|
253
274
|
compCompressedBytes += gz.length;
|
|
254
275
|
} catch {
|
|
@@ -270,19 +291,19 @@ function computeGitInfo(allFiles, rootDir, largeThreshold) {
|
|
|
270
291
|
try {
|
|
271
292
|
if (!rootDir) return info;
|
|
272
293
|
const top = cp
|
|
273
|
-
.execFileSync(
|
|
294
|
+
.execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
274
295
|
cwd: rootDir,
|
|
275
|
-
stdio: [
|
|
296
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
276
297
|
})
|
|
277
298
|
.toString()
|
|
278
299
|
.trim();
|
|
279
300
|
if (!top) return info;
|
|
280
301
|
info.isRepo = true;
|
|
281
|
-
const out = cp.execFileSync(
|
|
302
|
+
const out = cp.execFileSync("git", ["ls-files", "-z"], {
|
|
282
303
|
cwd: rootDir,
|
|
283
|
-
stdio: [
|
|
304
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
284
305
|
});
|
|
285
|
-
const tracked = new Set(out.toString().split(
|
|
306
|
+
const tracked = new Set(out.toString().split("\0").filter(Boolean));
|
|
286
307
|
let trackedBytes = 0,
|
|
287
308
|
trackedCount = 0,
|
|
288
309
|
untrackedBytes = 0,
|
|
@@ -293,7 +314,8 @@ function computeGitInfo(allFiles, rootDir, largeThreshold) {
|
|
|
293
314
|
if (isTracked) {
|
|
294
315
|
trackedCount++;
|
|
295
316
|
trackedBytes += f.size;
|
|
296
|
-
if (f.size >= largeThreshold)
|
|
317
|
+
if (f.size >= largeThreshold)
|
|
318
|
+
lfsCandidates.push({ path: f.path, size: f.size });
|
|
297
319
|
} else {
|
|
298
320
|
untrackedCount++;
|
|
299
321
|
untrackedBytes += f.size;
|
|
@@ -303,7 +325,9 @@ function computeGitInfo(allFiles, rootDir, largeThreshold) {
|
|
|
303
325
|
info.trackedBytes = trackedBytes;
|
|
304
326
|
info.untrackedCount = untrackedCount;
|
|
305
327
|
info.untrackedBytes = untrackedBytes;
|
|
306
|
-
info.lfsCandidates = lfsCandidates
|
|
328
|
+
info.lfsCandidates = lfsCandidates
|
|
329
|
+
.sort((a, b) => b.size - a.size)
|
|
330
|
+
.slice(0, 50);
|
|
307
331
|
} catch {
|
|
308
332
|
/* git not available or not a repo, ignore */
|
|
309
333
|
}
|
|
@@ -320,35 +344,40 @@ function computeLargestFiles(allFiles, totalBytes) {
|
|
|
320
344
|
size: f.size,
|
|
321
345
|
sizeFormatted: formatSize(f.size),
|
|
322
346
|
percentOfTotal: toPct(f.size, totalBytes),
|
|
323
|
-
ext: f.ext ||
|
|
347
|
+
ext: f.ext || "",
|
|
324
348
|
isBinary: f.isBinary,
|
|
325
349
|
mtime: f.mtimeMs ? new Date(f.mtimeMs).toISOString() : null,
|
|
326
350
|
}));
|
|
327
351
|
}
|
|
328
352
|
|
|
329
353
|
function mdTable(rows, headers) {
|
|
330
|
-
const header = `| ${headers.join(
|
|
331
|
-
const sep = `| ${headers.map(() =>
|
|
332
|
-
const body = rows.map((r) => `| ${r.join(
|
|
354
|
+
const header = `| ${headers.join(" | ")} |`;
|
|
355
|
+
const sep = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
356
|
+
const body = rows.map((r) => `| ${r.join(" | ")} |`).join("\n");
|
|
333
357
|
return `${header}\n${sep}\n${body}`;
|
|
334
358
|
}
|
|
335
359
|
|
|
336
|
-
function buildMarkdownReport(
|
|
360
|
+
function buildMarkdownReport(
|
|
361
|
+
largestFiles,
|
|
362
|
+
byExtensionArr,
|
|
363
|
+
byDirectoryArr,
|
|
364
|
+
totalBytes,
|
|
365
|
+
) {
|
|
337
366
|
const toPct = (num, den) => (den === 0 ? 0 : (num / den) * 100);
|
|
338
367
|
const md = [];
|
|
339
368
|
md.push(
|
|
340
|
-
|
|
369
|
+
"\n### Top Largest Files (Top 50)\n",
|
|
341
370
|
mdTable(
|
|
342
371
|
largestFiles.map((f) => [
|
|
343
372
|
f.path,
|
|
344
373
|
f.sizeFormatted,
|
|
345
374
|
`${f.percentOfTotal.toFixed(2)}%`,
|
|
346
|
-
f.ext ||
|
|
347
|
-
f.isBinary ?
|
|
375
|
+
f.ext || "",
|
|
376
|
+
f.isBinary ? "binary" : "text",
|
|
348
377
|
]),
|
|
349
|
-
[
|
|
378
|
+
["Path", "Size", "% of total", "Ext", "Type"],
|
|
350
379
|
),
|
|
351
|
-
|
|
380
|
+
"\n\n### Top Extensions by Bytes (Top 20)\n",
|
|
352
381
|
);
|
|
353
382
|
const topExtRows = byExtensionArr
|
|
354
383
|
.slice(0, 20)
|
|
@@ -359,8 +388,8 @@ function buildMarkdownReport(largestFiles, byExtensionArr, byDirectoryArr, total
|
|
|
359
388
|
`${toPct(e.bytes, totalBytes).toFixed(2)}%`,
|
|
360
389
|
]);
|
|
361
390
|
md.push(
|
|
362
|
-
mdTable(topExtRows, [
|
|
363
|
-
|
|
391
|
+
mdTable(topExtRows, ["Ext", "Count", "Bytes", "% of total"]),
|
|
392
|
+
"\n\n### Top Directories by Bytes (Top 20)\n",
|
|
364
393
|
);
|
|
365
394
|
const topDirRows = byDirectoryArr
|
|
366
395
|
.slice(0, 20)
|
|
@@ -370,8 +399,8 @@ function buildMarkdownReport(largestFiles, byExtensionArr, byDirectoryArr, total
|
|
|
370
399
|
formatSize(d.bytes),
|
|
371
400
|
`${toPct(d.bytes, totalBytes).toFixed(2)}%`,
|
|
372
401
|
]);
|
|
373
|
-
md.push(mdTable(topDirRows, [
|
|
374
|
-
return md.join(
|
|
402
|
+
md.push(mdTable(topDirRows, ["Directory", "Files", "Bytes", "% of total"]));
|
|
403
|
+
return md.join("\n");
|
|
375
404
|
}
|
|
376
405
|
|
|
377
406
|
module.exports = {
|
package/tools/flattener/stats.js
CHANGED