@zeyue0329/xiaoma-cli 1.0.36 → 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.
Files changed (89) hide show
  1. package/.idea/workspace.xml +27 -26
  2. package/JAVA-BACKEND-COMMANDS-REFERENCE.md +62 -52
  3. package/JAVA-BACKEND-ITERATION-GUIDE.md +125 -18
  4. package/README.md +1 -1
  5. package/common/utils/bmad-doc-template.md +5 -5
  6. package/dist/agents/analyst.txt +35 -5
  7. package/dist/agents/architect.txt +217 -31
  8. package/dist/agents/automation-orchestrator.txt +4 -4
  9. package/dist/agents/dev.txt +3 -3
  10. package/dist/agents/full-requirement-orchestrator.txt +11 -11
  11. package/dist/agents/qa.txt +102 -102
  12. package/dist/agents/sm.txt +6 -6
  13. package/dist/agents/ux-expert.txt +6 -1
  14. package/dist/agents/workflow-executor.txt +879 -0
  15. package/dist/agents/xiaoma-master.txt +258 -37
  16. package/dist/teams/team-all.txt +1223 -445
  17. package/dist/teams/team-fullstack-with-database.txt +384 -446
  18. package/dist/teams/team-fullstack.txt +258 -37
  19. package/dist/teams/team-ide-minimal.txt +111 -111
  20. package/dist/teams/team-no-ui.txt +252 -36
  21. package/docs/architecture-sharding-modification.md +623 -0
  22. package/docs/automated-requirements-analysis-outputs.md +896 -0
  23. package/package.json +1 -1
  24. package/tools/builders/web-builder.js +292 -142
  25. package/tools/bump-all-versions.js +50 -32
  26. package/tools/cli.js +52 -47
  27. package/tools/flattener/aggregate.js +30 -12
  28. package/tools/flattener/binary.js +46 -43
  29. package/tools/flattener/discovery.js +23 -15
  30. package/tools/flattener/files.js +6 -6
  31. package/tools/flattener/ignoreRules.js +122 -121
  32. package/tools/flattener/main.js +249 -144
  33. package/tools/flattener/projectRoot.js +74 -69
  34. package/tools/flattener/prompts.js +12 -10
  35. package/tools/flattener/stats.helpers.js +90 -61
  36. package/tools/flattener/stats.js +1 -1
  37. package/tools/flattener/test-matrix.js +225 -170
  38. package/tools/flattener/xml.js +31 -23
  39. package/tools/installer/bin/xiaoma.js +199 -153
  40. package/tools/installer/lib/config-loader.js +76 -47
  41. package/tools/installer/lib/file-manager.js +101 -44
  42. package/tools/installer/lib/ide-base-setup.js +49 -39
  43. package/tools/installer/lib/ide-setup.js +694 -380
  44. package/tools/installer/lib/installer.js +802 -469
  45. package/tools/installer/lib/memory-profiler.js +22 -12
  46. package/tools/installer/lib/module-manager.js +16 -14
  47. package/tools/installer/lib/resource-locator.js +61 -35
  48. package/tools/lib/dependency-resolver.js +34 -23
  49. package/tools/lib/yaml-utils.js +7 -2
  50. package/tools/preview-release-notes.js +33 -25
  51. package/tools/shared/bannerArt.js +3 -3
  52. package/tools/sync-installer-version.js +16 -7
  53. package/tools/upgraders/v3-to-v4-upgrader.js +244 -163
  54. package/tools/version-bump.js +24 -18
  55. package/tools/xiaoma-npx-wrapper.js +15 -10
  56. package/tools/yaml-format.js +60 -36
  57. package/xiaoma-core/agent-teams/team-fullstack-with-database.yaml +0 -1
  58. package/xiaoma-core/agents/automated-fix-validator.yaml +2 -1
  59. package/xiaoma-core/agents/automated-quality-validator.yaml +10 -5
  60. package/xiaoma-core/agents/automation-orchestrator.md +4 -4
  61. package/xiaoma-core/agents/dev.md +4 -4
  62. package/xiaoma-core/agents/enhanced-workflow-orchestrator.yaml +2 -1
  63. package/xiaoma-core/agents/full-requirement-orchestrator.md +11 -11
  64. package/xiaoma-core/agents/global-requirements-auditor.yaml +11 -3
  65. package/xiaoma-core/agents/intelligent-template-adapter.yaml +19 -5
  66. package/xiaoma-core/agents/master-execution-engine.yaml +19 -5
  67. package/xiaoma-core/agents/workflow-executor.md +126 -18
  68. package/xiaoma-core/agents/xiaoma-master.md +1 -1
  69. package/xiaoma-core/data/test-levels-framework.md +12 -12
  70. package/xiaoma-core/tasks/analyze-existing-database.md +1 -1
  71. package/xiaoma-core/tasks/apply-qa-fixes.md +3 -3
  72. package/xiaoma-core/tasks/batch-story-generation.md +22 -22
  73. package/xiaoma-core/tasks/create-enhanced-story-with-database.md +6 -6
  74. package/xiaoma-core/tasks/nfr-assess.md +6 -6
  75. package/xiaoma-core/tasks/project-integration-testing.md +42 -42
  76. package/xiaoma-core/tasks/qa-gate.md +23 -23
  77. package/xiaoma-core/tasks/review-story.md +18 -18
  78. package/xiaoma-core/tasks/risk-profile.md +25 -25
  79. package/xiaoma-core/tasks/serial-development-orchestration.md +51 -51
  80. package/xiaoma-core/tasks/test-design.md +9 -9
  81. package/xiaoma-core/tasks/trace-requirements.md +21 -21
  82. package/xiaoma-core/templates/competitor-analysis-tmpl.yaml +35 -5
  83. package/xiaoma-core/templates/front-end-architecture-tmpl.yaml +77 -11
  84. package/xiaoma-core/templates/front-end-spec-tmpl.yaml +6 -1
  85. package/xiaoma-core/templates/fullstack-architecture-tmpl.yaml +140 -20
  86. package/xiaoma-core/templates/global-qa-monitoring-tmpl.yaml +2 -1
  87. package/xiaoma-core/templates/requirements-coverage-audit.yaml +2 -1
  88. package/xiaoma-core/workflows/automated-requirements-analysis.yaml +4 -4
  89. package/dist/agents/database-architect.txt +0 -322
@@ -1,10 +1,10 @@
1
- const fs = require('fs-extra');
2
- const path = require('node:path');
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('node:process');
6
- const { execFile } = require('node:child_process');
7
- const { promisify } = require('node:util');
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 || '').trim();
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('git', ['rev-parse', '--show-toplevel'], startDir);
31
- const hgP = _tryRun('hg', ['root'], startDir);
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('svn', ['info', '--show-item', 'wc-root'], startDir);
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('svn', ['info'], startDir);
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('working copy root path:'));
40
- if (line) return line.split(':').slice(1).join(':').trim();
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) => (Array.isArray(rel) ? path.join(d, ...rel) : path.join(d, rel));
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('.project-root', 110);
82
- add('.workspace-root', 110);
83
- add('.repo-root', 110);
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('.git', 100);
87
- add('.hg', 95);
88
- add('.svn', 95);
91
+ add(".git", 100);
92
+ add(".hg", 95);
93
+ add(".svn", 95);
89
94
 
90
95
  // Monorepo/workspace indicators
91
- add('pnpm-workspace.yaml', 90);
92
- add('lerna.json', 90);
93
- add('turbo.json', 90);
94
- add('nx.json', 90);
95
- add('rush.json', 90);
96
- add('go.work', 90);
97
- add('WORKSPACE', 90);
98
- add('WORKSPACE.bazel', 90);
99
- add('MODULE.bazel', 90);
100
- add('pants.toml', 90);
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('yarn.lock', 85);
104
- add('pnpm-lock.yaml', 85);
105
- add('package-lock.json', 85);
106
- add('bun.lockb', 85);
107
- add('Cargo.lock', 85);
108
- add('composer.lock', 85);
109
- add('poetry.lock', 85);
110
- add('Pipfile.lock', 85);
111
- add('Gemfile.lock', 85);
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('settings.gradle', 80);
115
- add('settings.gradle.kts', 80);
116
- add('gradlew', 80);
117
- add('pom.xml', 80);
118
- add('build.sbt', 80);
119
- add(['project', 'build.properties'], 80);
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('deno.json', 75);
123
- add('deno.jsonc', 75);
124
- add('pyproject.toml', 75);
125
- add('Pipfile', 75);
126
- add('requirements.txt', 75);
127
- add('go.mod', 75);
128
- add('Cargo.toml', 75);
129
- add('composer.json', 75);
130
- add('mix.exs', 75);
131
- add('Gemfile', 75);
132
- add('CMakeLists.txt', 75);
133
- add('stack.yaml', 75);
134
- add('cabal.project', 75);
135
- add('rebar.config', 75);
136
- add('pubspec.yaml', 75);
137
- add('flake.nix', 75);
138
- add('shell.nix', 75);
139
- add('default.nix', 75);
140
- add('.tool-versions', 75);
141
- add('package.json', 74); // generic Node project (lower than lockfiles/workspaces)
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(['.changeset', 'config.json'], 70);
145
- add('.changeset', 70);
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, 'package.json');
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, 'utf8');
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('node:os');
2
- const path = require('node:path');
3
- const readline = require('node:readline');
4
- const process = require('node:process');
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('~')) return path.join(os.homedir(), p.slice(1));
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 ? ' [Y/n] ' : ' [y/N] ';
31
- const ans = (await promptQuestion(`${question}${suffix}`)).trim().toLowerCase();
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 (['y', 'yes'].includes(ans)) return true;
34
- if (['n', 'no'].includes(ans)) return false;
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
- 'use strict';
1
+ "use strict";
2
2
 
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');
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) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
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(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
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) || '').toLowerCase();
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, '0–1KB'],
73
- [10 * KB, '1–10KB'],
74
- [100 * KB, '10–100KB'],
75
- [1 * MB, '100KB–1MB'],
76
- [10 * MB, '1–10MB'],
77
- [100 * MB, '10–100MB'],
78
- [Infinity, '>=100MB'],
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]) => ({ label, count: 0, bytes: 0 }));
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 || '<none>';
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 === '.' ? [] : f.dir.split(path.sep);
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('.', f.size);
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: '> 1 year', minDays: 365, maxDays: Infinity, count: 0, bytes: 0 },
145
- { label: '6–12 months', minDays: 180, maxDays: 365, count: 0, bytes: 0 },
146
- { label: '1–6 months', minDays: 30, maxDays: 180, count: 0, bytes: 0 },
147
- { label: '7–30 days', minDays: 7, maxDays: 30, count: 0, bytes: 0 },
148
- { label: '1–7 days', minDays: 1, maxDays: 7, count: 0, bytes: 0 },
149
- { label: '< 1 day', minDays: 0, maxDays: 1, count: 0, bytes: 0 },
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(0, (nowMs - (f.mtimeMs || nowMs)) / (24 * 60 * 60 * 1000));
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
- ? { path: oldest.path, mtime: oldest.mtimeMs ? new Date(oldest.mtimeMs).toISOString() : null }
176
+ ? {
177
+ path: oldest.path,
178
+ mtime: oldest.mtimeMs ? new Date(oldest.mtimeMs).toISOString() : null,
179
+ }
166
180
  : null,
167
181
  newest: newest
168
- ? { path: newest.path, mtime: newest.mtimeMs ? new Date(newest.mtimeMs).toISOString() : null }
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((f) => f.size >= largeThreshold).length;
184
- const suspiciousLargeFilesCount = allFiles.filter((f) => f.size >= suspiciousThreshold).length;
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('sha1').update(content).digest('hex');
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: 'same-size+text-hash',
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: 'same-size',
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, 'utf8'));
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('git', ['rev-parse', '--show-toplevel'], {
294
+ .execFileSync("git", ["rev-parse", "--show-toplevel"], {
274
295
  cwd: rootDir,
275
- stdio: ['ignore', 'pipe', 'ignore'],
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('git', ['ls-files', '-z'], {
302
+ const out = cp.execFileSync("git", ["ls-files", "-z"], {
282
303
  cwd: rootDir,
283
- stdio: ['ignore', 'pipe', 'ignore'],
304
+ stdio: ["ignore", "pipe", "ignore"],
284
305
  });
285
- const tracked = new Set(out.toString().split('\0').filter(Boolean));
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) lfsCandidates.push({ path: f.path, size: f.size });
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.sort((a, b) => b.size - a.size).slice(0, 50);
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(() => '---').join(' | ')} |`;
332
- const body = rows.map((r) => `| ${r.join(' | ')} |`).join('\n');
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(largestFiles, byExtensionArr, byDirectoryArr, totalBytes) {
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
- '\n### Top Largest Files (Top 50)\n',
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 ? 'binary' : 'text',
375
+ f.ext || "",
376
+ f.isBinary ? "binary" : "text",
348
377
  ]),
349
- ['Path', 'Size', '% of total', 'Ext', 'Type'],
378
+ ["Path", "Size", "% of total", "Ext", "Type"],
350
379
  ),
351
- '\n\n### Top Extensions by Bytes (Top 20)\n',
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, ['Ext', 'Count', 'Bytes', '% of total']),
363
- '\n\n### Top Directories by Bytes (Top 20)\n',
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, ['Directory', 'Files', 'Bytes', '% of total']));
374
- return md.join('\n');
402
+ md.push(mdTable(topDirRows, ["Directory", "Files", "Bytes", "% of total"]));
403
+ return md.join("\n");
375
404
  }
376
405
 
377
406
  module.exports = {
@@ -1,4 +1,4 @@
1
- const H = require('./stats.helpers.js');
1
+ const H = require("./stats.helpers.js");
2
2
 
3
3
  async function calculateStatistics(aggregatedContent, xmlFileSize, rootDir) {
4
4
  const { textFiles, binaryFiles, errors } = aggregatedContent;