forge-workflow 0.0.9 → 0.0.10

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 (102) hide show
  1. package/.claude/commands/dev.md +2 -2
  2. package/.claude/commands/plan.md +2 -2
  3. package/.claude/commands/ship.md +2 -2
  4. package/.claude/commands/status.md +4 -4
  5. package/.cline/workflows/dev.md +2 -2
  6. package/.cline/workflows/plan.md +2 -2
  7. package/.cline/workflows/ship.md +2 -2
  8. package/.cline/workflows/status.md +4 -4
  9. package/.codex/skills/dev/SKILL.md +2 -2
  10. package/.codex/skills/plan/SKILL.md +2 -2
  11. package/.codex/skills/ship/SKILL.md +2 -2
  12. package/.codex/skills/status/SKILL.md +4 -4
  13. package/.cursor/commands/dev.md +2 -2
  14. package/.cursor/commands/plan.md +2 -2
  15. package/.cursor/commands/ship.md +2 -2
  16. package/.cursor/commands/status.md +4 -4
  17. package/.github/prompts/dev.prompt.md +2 -2
  18. package/.github/prompts/plan.prompt.md +2 -2
  19. package/.github/prompts/ship.prompt.md +2 -2
  20. package/.github/prompts/status.prompt.md +4 -4
  21. package/.github/workflows/beads-to-github.yml +43 -10
  22. package/.github/workflows/github-to-beads.yml +10 -7
  23. package/.kilocode/workflows/dev.md +2 -2
  24. package/.kilocode/workflows/plan.md +2 -2
  25. package/.kilocode/workflows/ship.md +2 -2
  26. package/.kilocode/workflows/status.md +4 -4
  27. package/.opencode/commands/dev.md +2 -2
  28. package/.opencode/commands/plan.md +2 -2
  29. package/.opencode/commands/ship.md +2 -2
  30. package/.opencode/commands/status.md +4 -4
  31. package/.roo/commands/dev.md +2 -2
  32. package/.roo/commands/plan.md +2 -2
  33. package/.roo/commands/ship.md +2 -2
  34. package/.roo/commands/status.md +4 -4
  35. package/AGENTS.md +1 -0
  36. package/CLAUDE.md +12 -0
  37. package/bin/forge.js +10 -5
  38. package/docs/BEADS_GITHUB_SYNC.md +26 -0
  39. package/docs/TOOLCHAIN.md +130 -148
  40. package/lib/beads-bootstrap.js +225 -0
  41. package/lib/beads-health-check.js +55 -10
  42. package/lib/beads-setup.js +104 -28
  43. package/lib/beads-sync-scaffold.js +11 -6
  44. package/lib/commands/_issue.js +11 -1
  45. package/lib/commands/issues.js +49 -0
  46. package/lib/commands/recommend.js +22 -1
  47. package/lib/commands/setup.js +16 -10
  48. package/lib/commands/status.js +181 -0
  49. package/lib/commands/team.js +11 -1
  50. package/lib/commands/test.js +37 -2
  51. package/lib/commands/validate.js +14 -8
  52. package/lib/commands/worktree.js +27 -54
  53. package/lib/dep-guard/keyword-ripple.js +184 -0
  54. package/lib/detect-worktree.js +9 -10
  55. package/lib/forge-issues.js +326 -0
  56. package/lib/issue-sync/authority.js +100 -0
  57. package/lib/issue-sync/github-pull.js +184 -0
  58. package/lib/issue-sync/import-primitives.js +98 -0
  59. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  60. package/lib/issue-sync/link-store.js +292 -0
  61. package/lib/issue-sync/project-github.js +123 -0
  62. package/lib/issue-sync/reconcile.js +195 -0
  63. package/lib/issue-sync/schema.js +126 -0
  64. package/lib/lefthook-check.js +5 -2
  65. package/lib/project-memory.js +564 -0
  66. package/lib/runtime-health.js +100 -12
  67. package/lib/smart-status/conflicts.js +205 -0
  68. package/lib/smart-status/scoring.js +177 -0
  69. package/lib/status/beads-snapshot.js +102 -0
  70. package/lib/status/presenter.js +65 -0
  71. package/lib/workflow/enforce-stage.js +3 -1
  72. package/lib/workflow/state-manager.js +164 -8
  73. package/package.json +12 -4
  74. package/scripts/beads-context.sh +124 -5
  75. package/scripts/beads-context.test.js +21 -4
  76. package/scripts/beads-migrate-to-dolt.sh +7 -0
  77. package/scripts/beads-upgrade-smoke.sh +263 -0
  78. package/scripts/behavioral-judge.sh +115 -11
  79. package/scripts/benchmark.js +349 -63
  80. package/scripts/dep-guard-analyze.js +52 -17
  81. package/scripts/dep-guard-keyword-ripple.js +29 -0
  82. package/scripts/dep-guard-render-review.js +86 -0
  83. package/scripts/dep-guard.sh +45 -232
  84. package/scripts/forge-team/lib/sync-github.sh +160 -28
  85. package/scripts/forge-team/tests/sync-github.test.sh +195 -58
  86. package/scripts/github-beads-sync/index.mjs +122 -98
  87. package/scripts/github-beads-sync/mapping.mjs +54 -0
  88. package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
  89. package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
  90. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  91. package/scripts/preflight.sh +181 -0
  92. package/scripts/smart-status-score.js +31 -0
  93. package/scripts/smart-status-sessions.js +51 -0
  94. package/scripts/smart-status.sh +74 -329
  95. package/scripts/sync-agentic-workflow.js +48 -0
  96. package/scripts/test-ci-shard.js +244 -0
  97. package/scripts/test-dashboard.js +188 -52
  98. package/scripts/test-full-suite.js +186 -0
  99. package/scripts/test-profile.js +278 -0
  100. package/scripts/test.js +219 -28
  101. package/scripts/validate.js +143 -0
  102. package/scripts/validate.sh +18 -1
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { spawn: defaultSpawn } = require('node:child_process');
8
+
9
+ const {
10
+ createDurationMap,
11
+ getShardPlan,
12
+ getUnitTestRoots,
13
+ readNewestProfile,
14
+ walkTests,
15
+ } = require('./test-ci-shard');
16
+
17
+ const rootDir = path.join(__dirname, '..');
18
+ const reportDir = path.join(rootDir, 'test-results');
19
+
20
+ function parseArgs(argv) {
21
+ const args = {
22
+ labelPrefix: 'local-full',
23
+ shards: null,
24
+ };
25
+
26
+ for (let index = 0; index < argv.length; index += 1) {
27
+ const current = argv[index];
28
+ const next = argv[index + 1];
29
+
30
+ if (current === '--label-prefix') args.labelPrefix = next;
31
+ if (current === '--shards') args.shards = Number.parseInt(next, 10);
32
+ }
33
+
34
+ return args;
35
+ }
36
+
37
+ function getDefaultShardCount(cpuCount = os.cpus().length) {
38
+ if (!Number.isInteger(cpuCount) || cpuCount <= 1) return 1;
39
+ return Math.max(2, Math.min(4, cpuCount - 1));
40
+ }
41
+
42
+ function listAllFullSuiteTests() {
43
+ const roots = [
44
+ ...getUnitTestRoots(),
45
+ path.join(rootDir, 'test-env'),
46
+ path.join(rootDir, 'scripts'),
47
+ ].filter((dir, index, array) => fs.existsSync(dir) && array.indexOf(dir) === index);
48
+
49
+ const files = [];
50
+ for (const root of roots) {
51
+ if (root === path.join(rootDir, 'test')) {
52
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
53
+ const absolute = path.join(root, entry.name);
54
+ if (entry.isDirectory()) {
55
+ files.push(...walkAllTests(absolute));
56
+ continue;
57
+ }
58
+ if (entry.name.endsWith('.test.js') || entry.name.endsWith('.spec.js')) {
59
+ files.push(path.relative(rootDir, absolute).replace(/\\/g, '/'));
60
+ }
61
+ }
62
+ continue;
63
+ }
64
+ files.push(...walkTests(root));
65
+ }
66
+
67
+ return files.sort((left, right) => left.localeCompare(right));
68
+ }
69
+
70
+ function walkAllTests(dir) {
71
+ const results = [];
72
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
73
+ const absolute = path.join(dir, entry.name);
74
+ if (entry.isDirectory()) {
75
+ results.push(...walkAllTests(absolute));
76
+ continue;
77
+ }
78
+ if (!entry.name.endsWith('.test.js') && !entry.name.endsWith('.spec.js')) continue;
79
+ results.push(path.relative(rootDir, absolute).replace(/\\/g, '/'));
80
+ }
81
+ return results;
82
+ }
83
+
84
+ function buildShardSpecs(allTests, shardTotal, durationMap = new Map()) {
85
+ const specs = [];
86
+ for (let shardIndex = 0; shardIndex < shardTotal; shardIndex += 1) {
87
+ const plan = getShardPlan({
88
+ label: `local-full-${shardIndex}`,
89
+ mode: 'shard',
90
+ shardIndex,
91
+ shardTotal,
92
+ }, {
93
+ allUnitTests: allTests,
94
+ durationMap,
95
+ });
96
+ if (plan.files.length === 0) continue;
97
+ specs.push({
98
+ files: plan.files,
99
+ index: shardIndex,
100
+ source: plan.source,
101
+ });
102
+ }
103
+ return specs;
104
+ }
105
+
106
+ function spawnShard(shard, options = {}) {
107
+ const spawn = options.spawn || defaultSpawn;
108
+ const env = options.env || process.env;
109
+ const bunCommand = options.bunCommand || env.BUN_EXE || process.env.BUN_EXE || 'bun';
110
+ const labelPrefix = options.labelPrefix || 'local-full';
111
+ fs.mkdirSync(reportDir, { recursive: true });
112
+ const junitPath = path.join(reportDir, `${labelPrefix}-shard-${shard.index}.xml`);
113
+
114
+ return new Promise((resolve, reject) => {
115
+ const child = spawn(bunCommand, [
116
+ 'test',
117
+ '--timeout',
118
+ '30000',
119
+ '--reporter=junit',
120
+ '--reporter-outfile',
121
+ junitPath,
122
+ ...shard.files,
123
+ ], {
124
+ cwd: rootDir,
125
+ env,
126
+ shell: false,
127
+ stdio: 'inherit',
128
+ });
129
+
130
+ child.on('error', reject);
131
+ child.on('close', (code) => {
132
+ resolve(code ?? 1);
133
+ });
134
+ });
135
+ }
136
+
137
+ async function runFullSuiteInParallel(args = {}, deps = {}) {
138
+ const allTests = deps.allTests || listAllFullSuiteTests();
139
+ const shardTotal = Number.isInteger(args.shards) && args.shards > 0
140
+ ? args.shards
141
+ : getDefaultShardCount(deps.cpuCount);
142
+ const profile = deps.profile || readNewestProfile(reportDir);
143
+ const durationMap = deps.durationMap || createDurationMap(profile);
144
+ const shardSpecs = buildShardSpecs(allTests, shardTotal, durationMap);
145
+
146
+ if (shardSpecs.length === 0) {
147
+ console.log('No unit test files discovered for local full-suite run');
148
+ return 0;
149
+ }
150
+
151
+ console.log(`Running local full suite in ${shardSpecs.length} shard(s)`);
152
+ const results = await Promise.all(shardSpecs.map((shard) => spawnShard(shard, {
153
+ bunCommand: deps.bunCommand,
154
+ env: deps.env,
155
+ labelPrefix: args.labelPrefix,
156
+ spawn: deps.spawn,
157
+ })));
158
+
159
+ return results.some((code) => code !== 0) ? 1 : 0;
160
+ }
161
+
162
+ async function main(argv = process.argv.slice(2), deps = {}) {
163
+ const args = parseArgs(argv);
164
+ const status = await runFullSuiteInParallel(args, deps);
165
+ return status;
166
+ }
167
+
168
+ if (require.main === module) {
169
+ main().then((status) => {
170
+ process.exit(status);
171
+ }).catch((error) => {
172
+ console.error(`test-full-suite: ${error.message}`);
173
+ process.exit(1);
174
+ });
175
+ }
176
+
177
+ module.exports = {
178
+ buildShardSpecs,
179
+ getDefaultShardCount,
180
+ listAllFullSuiteTests,
181
+ main,
182
+ parseArgs,
183
+ runFullSuiteInParallel,
184
+ spawnShard,
185
+ walkAllTests,
186
+ };
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const PATH_BUCKET_ORDER = ['fixtures', 'testEnv', 'e2e', 'scripts', 'commands', 'lib', 'other'];
7
+ const PATH_BUCKETS = {
8
+ commands: { durationMs: 0, files: 0, testCases: 0 },
9
+ e2e: { durationMs: 0, files: 0, testCases: 0 },
10
+ fixtures: { durationMs: 0, files: 0, testCases: 0 },
11
+ lib: { durationMs: 0, files: 0, testCases: 0 },
12
+ other: { durationMs: 0, files: 0, testCases: 0 },
13
+ scripts: { durationMs: 0, files: 0, testCases: 0 },
14
+ testEnv: { durationMs: 0, files: 0, testCases: 0 },
15
+ };
16
+ const FIXTURE_HINTS = ['fixture', 'fixtures', 'sandbox', 'workspace', 'repo setup'];
17
+ const SHELL_HINTS = ['bash', 'shell', '.sh', 'exec', 'spawn', 'cli', 'command', 'git', 'jq', 'bun ', 'node ', 'powershell'];
18
+
19
+ function parseArgs(argv) {
20
+ const args = {
21
+ inputDir: 'test-results',
22
+ integrationSkipped: true,
23
+ label: 'local',
24
+ output: null,
25
+ };
26
+
27
+ for (let index = 0; index < argv.length; index += 1) {
28
+ const current = argv[index];
29
+ const next = argv[index + 1];
30
+ if (current === '--input-dir') args.inputDir = next;
31
+ if (current === '--label') args.label = next;
32
+ if (current === '--output') args.output = next;
33
+ if (current === '--integration-skipped') args.integrationSkipped = next === 'true';
34
+ }
35
+
36
+ return args;
37
+ }
38
+
39
+ function walk(dir, extension) {
40
+ if (!fs.existsSync(dir)) return [];
41
+ const results = [];
42
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
43
+ const absolute = path.join(dir, entry.name);
44
+ if (entry.isDirectory()) {
45
+ results.push(...walk(absolute, extension));
46
+ continue;
47
+ }
48
+ if (absolute.endsWith(extension)) {
49
+ results.push(absolute);
50
+ }
51
+ }
52
+ return results;
53
+ }
54
+
55
+ function parseAttributes(source) {
56
+ const attributes = {};
57
+ for (const match of source.matchAll(/([a-zA-Z_:][\w:.-]*)="([^"]*)"/g)) {
58
+ attributes[match[1]] = match[2];
59
+ }
60
+ return attributes;
61
+ }
62
+
63
+ function normalizePathForProfile(value) {
64
+ if (!value) return '';
65
+ return value.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '');
66
+ }
67
+
68
+ function clonePathBuckets() {
69
+ return Object.fromEntries(
70
+ Object.entries(PATH_BUCKETS).map(([key, value]) => [key, { ...value }]),
71
+ );
72
+ }
73
+
74
+ function createSignalMetrics() {
75
+ return {
76
+ durationMs: 0,
77
+ matchedFiles: 0,
78
+ matchedTestCases: 0,
79
+ topFiles: [],
80
+ };
81
+ }
82
+
83
+ function matchesFixturePath(normalizedFile) {
84
+ return normalizedFile.includes('/fixture/')
85
+ || normalizedFile.includes('/fixtures/')
86
+ || normalizedFile.includes('test-env/fixtures/')
87
+ || normalizedFile.includes('test/e2e/fixtures/')
88
+ || normalizedFile.includes('packages/skills/test/fixtures/')
89
+ || normalizedFile.startsWith('fixtures/')
90
+ || normalizedFile.endsWith('/fixtures')
91
+ || normalizedFile.endsWith('/fixture');
92
+ }
93
+
94
+ function selectPrimaryBucket(normalizedFile) {
95
+ const matches = new Set();
96
+ if (matchesFixturePath(normalizedFile)) matches.add('fixtures');
97
+ if (normalizedFile.includes('test-env/')) matches.add('testEnv');
98
+ if (normalizedFile.includes('test/e2e/')) matches.add('e2e');
99
+ if (normalizedFile.includes('test/scripts/') || normalizedFile.includes('scripts/')) matches.add('scripts');
100
+ if (normalizedFile.includes('test/commands/') || normalizedFile.includes('lib/commands/')) matches.add('commands');
101
+ if (normalizedFile.includes('lib/')) matches.add('lib');
102
+
103
+ for (const bucket of PATH_BUCKET_ORDER) {
104
+ if (matches.has(bucket)) {
105
+ return bucket;
106
+ }
107
+ }
108
+ return 'other';
109
+ }
110
+
111
+ function classifyTestcase({ classname = '', durationMs = 0, file = '', name = '' }) {
112
+ const normalizedFile = normalizePathForProfile(file);
113
+ const normalizedClassname = normalizePathForProfile(classname);
114
+ const normalizedName = name || '';
115
+ const searchable = `${normalizedFile} ${normalizedClassname} ${normalizedName}`.toLowerCase();
116
+ const fixtureHeavy = matchesFixturePath(normalizedFile)
117
+ || normalizedFile.includes('test-env/')
118
+ || normalizedFile.includes('test/e2e/')
119
+ || FIXTURE_HINTS.some((hint) => searchable.includes(hint));
120
+ const shellHeavy = SHELL_HINTS.some((hint) => searchable.includes(hint));
121
+
122
+ return {
123
+ durationMs,
124
+ file: normalizedFile,
125
+ fixtureHeavy,
126
+ primaryBucket: selectPrimaryBucket(normalizedFile),
127
+ shellHeavy,
128
+ };
129
+ }
130
+
131
+ function accumulateFileMetrics(store, file, durationMs) {
132
+ store.set(file, (store.get(file) || 0) + durationMs);
133
+ }
134
+
135
+ function finalizeFileDurations(fileDurations, limit = Infinity) {
136
+ return Array.from(fileDurations.entries())
137
+ .map(([file, durationMs]) => ({ durationMs, file }))
138
+ .sort((left, right) => right.durationMs - left.durationMs || left.file.localeCompare(right.file))
139
+ .slice(0, limit);
140
+ }
141
+
142
+ function finalizeTopFiles(fileDurations, limit = 10) {
143
+ return finalizeFileDurations(fileDurations, limit);
144
+ }
145
+
146
+ function finalizePathBuckets(pathBuckets, bucketFiles) {
147
+ for (const bucket of Object.keys(pathBuckets)) {
148
+ pathBuckets[bucket].files = bucketFiles[bucket].size;
149
+ }
150
+ return pathBuckets;
151
+ }
152
+
153
+ function finalizeSignalMetrics(metrics, fileDurations) {
154
+ return {
155
+ durationMs: metrics.durationMs,
156
+ matchedFiles: metrics.fileSet.size,
157
+ matchedTestCases: metrics.matchedTestCases,
158
+ topFiles: finalizeTopFiles(fileDurations),
159
+ };
160
+ }
161
+
162
+ function parseJUnitFiles(files) {
163
+ const fileDurations = new Map();
164
+ const timedOutFiles = new Set();
165
+ const pathBuckets = clonePathBuckets();
166
+ const bucketFiles = Object.fromEntries(Object.keys(pathBuckets).map((key) => [key, new Set()]));
167
+ const shellHeavyFileDurations = new Map();
168
+ const shellHeavyMetrics = { durationMs: 0, fileSet: new Set(), matchedTestCases: 0 };
169
+ const fixtureHeavyFileDurations = new Map();
170
+ const fixtureHeavyMetrics = { durationMs: 0, fileSet: new Set(), matchedTestCases: 0 };
171
+ let suiteDurationMs = 0;
172
+
173
+ for (const file of files) {
174
+ const content = fs.readFileSync(file, 'utf8');
175
+ for (const suiteMatch of content.matchAll(/<testsuite\b([^>]*)>/g)) {
176
+ const attrs = parseAttributes(suiteMatch[1]);
177
+ suiteDurationMs += Math.round(Number.parseFloat(attrs.time || '0') * 1000);
178
+ }
179
+
180
+ for (const caseMatch of content.matchAll(/<testcase\b([^>]*)>([\s\S]*?)<\/testcase>|<testcase\b([^>]*)\/>/g)) {
181
+ const attrs = parseAttributes(caseMatch[1] || caseMatch[3] || '');
182
+ const body = caseMatch[2] || '';
183
+ const durationMs = Math.round(Number.parseFloat(attrs.time || '0') * 1000);
184
+ const fallbackFile = path.basename(file);
185
+ const testcase = classifyTestcase({
186
+ classname: attrs.classname || '',
187
+ durationMs,
188
+ file: attrs.file || attrs.classname || attrs.name || fallbackFile,
189
+ name: attrs.name || '',
190
+ });
191
+
192
+ const testcaseFile = testcase.file || normalizePathForProfile(fallbackFile);
193
+ accumulateFileMetrics(fileDurations, testcaseFile, durationMs);
194
+ if (/timeout/i.test(body)) {
195
+ timedOutFiles.add(testcaseFile);
196
+ }
197
+
198
+ const bucket = testcase.primaryBucket;
199
+ pathBuckets[bucket].durationMs += durationMs;
200
+ pathBuckets[bucket].testCases += 1;
201
+ bucketFiles[bucket].add(testcaseFile);
202
+
203
+ if (testcase.shellHeavy) {
204
+ shellHeavyMetrics.durationMs += durationMs;
205
+ shellHeavyMetrics.matchedTestCases += 1;
206
+ shellHeavyMetrics.fileSet.add(testcaseFile);
207
+ accumulateFileMetrics(shellHeavyFileDurations, testcaseFile, durationMs);
208
+ }
209
+
210
+ if (testcase.fixtureHeavy) {
211
+ fixtureHeavyMetrics.durationMs += durationMs;
212
+ fixtureHeavyMetrics.matchedTestCases += 1;
213
+ fixtureHeavyMetrics.fileSet.add(testcaseFile);
214
+ accumulateFileMetrics(fixtureHeavyFileDurations, testcaseFile, durationMs);
215
+ }
216
+ }
217
+ }
218
+
219
+ return {
220
+ fixtureHeavy: finalizeSignalMetrics(fixtureHeavyMetrics, fixtureHeavyFileDurations),
221
+ allFileDurations: finalizeFileDurations(fileDurations),
222
+ pathBuckets: finalizePathBuckets(pathBuckets, bucketFiles),
223
+ shellHeavy: finalizeSignalMetrics(shellHeavyMetrics, shellHeavyFileDurations),
224
+ slowestFiles: finalizeTopFiles(fileDurations),
225
+ suiteDurationMs,
226
+ timedOutFiles: Array.from(timedOutFiles).sort((left, right) => left.localeCompare(right)),
227
+ };
228
+ }
229
+
230
+ function buildProfile(args, metrics, timestamp = new Date().toISOString()) {
231
+ return {
232
+ allFileDurations: metrics.allFileDurations || [],
233
+ fixtureHeavy: metrics.fixtureHeavy || createSignalMetrics(),
234
+ integrationSkipped: args.integrationSkipped !== false,
235
+ label: args.label,
236
+ pathBuckets: metrics.pathBuckets || clonePathBuckets(),
237
+ shellHeavy: metrics.shellHeavy || createSignalMetrics(),
238
+ slowestFiles: metrics.slowestFiles || [],
239
+ suiteDurationMs: metrics.suiteDurationMs || 0,
240
+ timedOutFiles: metrics.timedOutFiles || [],
241
+ timestamp,
242
+ };
243
+ }
244
+
245
+ function main(argv = process.argv.slice(2)) {
246
+ const args = parseArgs(argv);
247
+ const inputDir = path.resolve(process.cwd(), args.inputDir);
248
+ const files = walk(inputDir, '.xml');
249
+ const metrics = parseJUnitFiles(files);
250
+ const profile = buildProfile(args, metrics);
251
+
252
+ if (args.output) {
253
+ const outputPath = path.resolve(process.cwd(), args.output);
254
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
255
+ fs.writeFileSync(outputPath, JSON.stringify(profile, null, 2));
256
+ }
257
+
258
+ process.stdout.write(JSON.stringify(profile));
259
+ return profile;
260
+ }
261
+
262
+ if (require.main === module) {
263
+ main();
264
+ }
265
+
266
+ module.exports = {
267
+ accumulateFileMetrics,
268
+ buildProfile,
269
+ classifyTestcase,
270
+ finalizeFileDurations,
271
+ main,
272
+ normalizePathForProfile,
273
+ parseArgs,
274
+ parseAttributes,
275
+ parseJUnitFiles,
276
+ selectPrimaryBucket,
277
+ walk,
278
+ };