@chemx/starter-kit 26.9.12-93 → 26.9.13-272

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 (49) hide show
  1. package/cli/audit/ai-slop-detector.js +25 -41
  2. package/cli/audit/ast-visitors.js +10 -13
  3. package/cli/audit/history.js +45 -11
  4. package/cli/audit/pattern-detector.js +182 -0
  5. package/cli/audit/prompts.js +129 -72
  6. package/cli/audit/reporter-banner.js +4 -16
  7. package/cli/audit/reporter-grades.js +28 -16
  8. package/cli/audit/reporter-grouping.d.ts +6 -0
  9. package/cli/audit/reporter-grouping.js +48 -5
  10. package/cli/audit/reporter-markdown.js +31 -16
  11. package/cli/audit/reporter-sections.js +28 -12
  12. package/cli/audit/reporter-utils.js +88 -1
  13. package/cli/audit/reporter.d.ts +13 -4
  14. package/cli/audit/reporter.js +32 -5
  15. package/cli/audit/roadmap.js +236 -0
  16. package/cli/audit/rules-predicates.d.ts +17 -0
  17. package/cli/audit/rules-predicates.js +107 -0
  18. package/cli/audit/rules.js +6 -2
  19. package/cli/audit/social-gh.js +2 -6
  20. package/cli/audit/social-publisher.js +1 -1
  21. package/cli/audit/social.js +25 -9
  22. package/cli/audit/types.d.ts +52 -5
  23. package/cli/audit.js +76 -7
  24. package/cli/badge.js +9 -2
  25. package/cli/build/detector.js +68 -0
  26. package/cli/build/executor.js +58 -0
  27. package/cli/build/grouper.js +78 -0
  28. package/cli/build/parser-matchers.js +116 -0
  29. package/cli/build/parser.js +80 -0
  30. package/cli/build/reporter.js +66 -0
  31. package/cli/build/types.d.ts +58 -0
  32. package/cli/build/types.js +1 -0
  33. package/cli/build.d.ts +16 -0
  34. package/cli/build.js +55 -0
  35. package/cli/help.js +14 -0
  36. package/cli/index.js +39 -14
  37. package/cli/installer-templates.js +43 -4
  38. package/cli/license.js +10 -7
  39. package/cli/navigator-actions.js +34 -6
  40. package/cli/navigator-banner.js +4 -2
  41. package/cli/navigator-menu.js +18 -0
  42. package/cli/navigator-share.js +1 -1
  43. package/cli/navigator.d.ts +8 -1
  44. package/cli/navigator.js +3 -3
  45. package/cli/terminal.js +6 -5
  46. package/cli/theme.d.ts +41 -0
  47. package/cli/theme.js +84 -0
  48. package/docs/CHANGELOG.md +46 -0
  49. package/package.json +3 -2
@@ -3,6 +3,7 @@ import {
3
3
  DISCUSSION_CATEGORY,
4
4
  DISCUSSION_CATEGORY_SLUG
5
5
  } from './social-constants.js';
6
+ import { matchesDiscussionTitle } from './rules-predicates.js';
6
7
 
7
8
  export const publishDiscussionViaGh = (repo, title, body, category = DISCUSSION_CATEGORY) => {
8
9
  try {
@@ -110,12 +111,7 @@ export const findExistingDiscussionViaGh = (repo, projectName) => {
110
111
  const targetLower = projectName.toLowerCase();
111
112
  const match = list.find((d) => {
112
113
  const titleLower = (d.title || '').toLowerCase();
113
- return (
114
- titleLower.includes(` ${targetLower} :`) ||
115
- titleLower.includes(` ${targetLower} [`) ||
116
- titleLower.includes(`: ${targetLower}`) ||
117
- titleLower.includes(`${targetLower} audit`)
118
- );
114
+ return matchesDiscussionTitle(titleLower, targetLower);
119
115
  });
120
116
  return match || null;
121
117
  }
@@ -59,7 +59,7 @@ export {
59
59
  export const formatArchiveComment = (previousTitle, previousBody) => {
60
60
  const archiveTimestamp = new Date().toUTCString();
61
61
  return [
62
- '## 📜 Previous Audit Snapshot (Archived)',
62
+ '## ⏱️ Audit Checkpoint Snapshot (Archived)',
63
63
  '',
64
64
  `> **Archived on:** ${archiveTimestamp}`,
65
65
  `> **Previous Status:** ${previousTitle || 'Prior Report'}`,
@@ -1,5 +1,10 @@
1
1
  import { groupViolationsBySeverity } from './reporter.js';
2
- import { PILLAR_EMOJIS } from './reporter-utils.js';
2
+ import {
3
+ PILLAR_EMOJIS,
4
+ formatPillarReactionBadgesMarkdown,
5
+ formatPillarShieldBadges,
6
+ resolveBadgeColor
7
+ } from './reporter-utils.js';
3
8
 
4
9
  export {
5
10
  parseGitRemoteUrl,
@@ -31,11 +36,7 @@ export const DISCUSSION_CATEGORY_SLUG = 'npx-chemx-audit';
31
36
  export const ORG_DISCUSSIONS_URL = 'https://github.com/orgs/Chemical-X-Protocol/discussions';
32
37
  export const DEFAULT_DISCUSSION_REPO = 'Chemical-X-Protocol/.github';
33
38
 
34
- export const resolveBadgeColor = (score) => {
35
- if (score >= 90) return '06b6d4';
36
- if (score >= 70) return 'f59e0b';
37
- return 'ef4444';
38
- };
39
+ export { resolveBadgeColor };
39
40
 
40
41
  export const resolveHotspotTierText = (lineCount) => {
41
42
  if (lineCount >= 2000) return '🔴 **CRITICAL (>= 2,000 lines of code)**';
@@ -101,8 +102,13 @@ export const generateDiscussionContent = (report, username, projectName = 'Codeb
101
102
  const lines = [];
102
103
  lines.push(`# ${isHighScoring ? 'Crystalline' : 'Architecture'} Audit Report: ${projectName}`);
103
104
  lines.push('');
104
- lines.push(`[![Chemical X MHI](https://img.shields.io/badge/Chemical%20X%20MHI-${encodedGrade}-${badgeColor}?style=for-the-badge)](https://chemicalx.xophz.com) [![AI Slop Index](https://img.shields.io/badge/AI%20Slop%20Index-${encodedSlop}-${slopBadgeColor}?style=for-the-badge)](https://chemicalx.xophz.com)`);
105
+ lines.push(`[![Chemical X Showcase](https://img.shields.io/badge/Chemical%20X-Showcase-62c9ff?style=for-the-badge)](https://chemicalx.xophz.com) [![Chemical X MHI](https://img.shields.io/badge/Chemical%20X%20MHI-${encodedGrade}-${badgeColor}?style=for-the-badge)](https://chemicalx.xophz.com) [![AI Slop Index](https://img.shields.io/badge/AI%20Slop%20Index-${encodedSlop}-${slopBadgeColor}?style=for-the-badge)](https://chemicalx.xophz.com)`);
105
106
  lines.push('');
107
+ const pillarShields = formatPillarShieldBadges(pillars);
108
+ if (pillarShields) {
109
+ lines.push(pillarShields);
110
+ lines.push('');
111
+ }
106
112
  lines.push(`* **Audited by**: @${username}`);
107
113
  const resolvedRepoUrl = repoUrl || (/^[\w\-.]+\/[\w\-.]+$/.test(projectName) ? `https://github.com/${projectName}` : '');
108
114
  if (resolvedRepoUrl) {
@@ -241,11 +247,21 @@ export const generateTransformationDiscussionContent = (
241
247
  return isGood ? `🟢 **${sign}**` : `🔴 **${sign}**`;
242
248
  };
243
249
 
250
+ const beforeColor = resolveBadgeColor(scoreBefore);
251
+ const afterColor = resolveBadgeColor(scoreAfter);
252
+ const encodedBeforeGrade = encodeURIComponent(`${scoreBefore}/100 (${beforeSnapshot.health.grade})`);
253
+ const encodedAfterGrade = encodeURIComponent(`${scoreAfter}/100 (${afterSnapshot.health.grade})`);
254
+
244
255
  const lines = [];
245
256
  lines.push(`# 🚀 Architectural Transformation: ${projectName}`);
246
257
  lines.push('');
247
- lines.push(`[![Chemical X Transformation](https://img.shields.io/badge/Chemical%20X-Transformation%20Showcase-62c9ff?style=for-the-badge)](https://chemicalx.xophz.com)`);
258
+ lines.push(`[![Chemical X Transformation](https://img.shields.io/badge/Chemical%20X-Transformation%20Showcase-62c9ff?style=for-the-badge)](https://chemicalx.xophz.com) [![Before MHI](https://img.shields.io/badge/Before%20MHI-${encodedBeforeGrade}-${beforeColor}?style=for-the-badge)](https://chemicalx.xophz.com) [![After MHI](https://img.shields.io/badge/After%20MHI-${encodedAfterGrade}-${afterColor}?style=for-the-badge)](https://chemicalx.xophz.com)`);
248
259
  lines.push('');
260
+ const afterPillarShields = formatPillarShieldBadges(afterSnapshot.pillars);
261
+ if (afterPillarShields) {
262
+ lines.push(afterPillarShields);
263
+ lines.push('');
264
+ }
249
265
  lines.push(`* **Audited by**: @${username}`);
250
266
  const resolvedRepoUrl = repoUrl || (/^[\w\-.]+\/[\w\-.]+$/.test(projectName) ? `https://github.com/${projectName}` : '');
251
267
  if (resolvedRepoUrl) {
@@ -263,7 +279,7 @@ export const generateTransformationDiscussionContent = (
263
279
  lines.push('');
264
280
  lines.push('## 1. Before vs. After Metric Comparison');
265
281
  lines.push('');
266
- lines.push('| Metric | Before (Baseline) | After (Refactored) | Delta |');
282
+ lines.push('| Metric | Before (Baseline Floor) | After (Refactored) | Delta |');
267
283
  lines.push('| :--- | :---: | :---: | :---: |');
268
284
  lines.push(`| **Molecular Health (MHI)** | ${scoreBefore} / 100 (${beforeSnapshot.health.grade}) | ${scoreAfter} / 100 (${afterSnapshot.health.grade}) | ${formatDelta(scoreDelta)} |`);
269
285
  const slopBefore = beforeSnapshot.aiSlop?.score ?? 100;
@@ -87,6 +87,41 @@ export interface AiSlopScore {
87
87
  };
88
88
  }
89
89
 
90
+ export interface PatternOccurrence {
91
+ readonly filePath: string;
92
+ readonly line: number;
93
+ readonly column?: number;
94
+ }
95
+
96
+ export interface PatternCandidate {
97
+ readonly id: string;
98
+ readonly type: 'UI_STRUCTURE' | 'STATE_UNION' | 'PREDICATE_LOGIC' | 'HOOK_SIGNATURE';
99
+ readonly label: string;
100
+ readonly detail: string;
101
+ readonly suggestedCapsule: string;
102
+ readonly recommendation: string;
103
+ readonly fileCount: number;
104
+ readonly totalHits: number;
105
+ readonly hasHotspot: boolean;
106
+ readonly impactScore: number;
107
+ readonly occurrences: readonly PatternOccurrence[];
108
+ }
109
+
110
+ export interface RoadmapItem {
111
+ readonly title: string;
112
+ readonly target: string;
113
+ readonly action: string;
114
+ readonly locations: readonly string[];
115
+ }
116
+
117
+ export interface RoadmapPhase {
118
+ readonly step: number;
119
+ readonly phase: string;
120
+ readonly roi: string;
121
+ readonly rationale: string;
122
+ readonly items: readonly RoadmapItem[];
123
+ }
124
+
90
125
  export interface AuditReport {
91
126
  readonly scannedFiles: number;
92
127
  readonly totalViolations: number;
@@ -96,6 +131,8 @@ export interface AuditReport {
96
131
  readonly pillars: Record<string, PillarData>;
97
132
  readonly contextAnalysis: ContextTokenAnalysis;
98
133
  readonly hotspots: readonly HotspotFile[];
134
+ readonly patterns?: readonly PatternCandidate[];
135
+ readonly roadmap?: readonly RoadmapPhase[];
99
136
  readonly violations: readonly HazardViolation[];
100
137
  }
101
138
 
@@ -123,14 +160,24 @@ export declare function calculateAiSlopScore(violations: readonly HazardViolatio
123
160
  export * from './reporter';
124
161
  export * from './social';
125
162
 
126
- export declare function buildGradeFPrompt(report: AuditReport): string;
127
- export declare function buildGradeDPrompt(report: AuditReport): string;
128
- export declare function buildGradeCPrompt(report: AuditReport): string;
129
- export declare function buildGradeBPrompt(report: AuditReport): string;
163
+ export interface PromptOptions {
164
+ readonly excludeAiSlop?: boolean;
165
+ }
166
+
167
+ export declare function formatGroupedPromptViolations(violations?: readonly HazardViolation[]): string[];
168
+ export declare function buildGradeFPrompt(report: AuditReport, options?: PromptOptions): string;
169
+ export declare function buildGradeDPrompt(report: AuditReport, options?: PromptOptions): string;
170
+ export declare function buildGradeCPrompt(report: AuditReport, options?: PromptOptions): string;
171
+ export declare function buildGradeBPrompt(report: AuditReport, options?: PromptOptions): string;
130
172
  export declare function buildAiSlopPrompt(report: AuditReport): string;
131
173
  export declare function buildHotspotsPrompt(report: AuditReport): string;
132
174
  export declare function buildMasterPrompt(report: AuditReport): string;
133
175
  export declare function formatPromptBox(title: string, promptText: string): string;
134
176
 
135
- export * from './history';
177
+ export declare function buildRemediationRoadmap(report: AuditReport): RoadmapPhase[];
178
+ export declare function formatRoadmapSection(report: AuditReport, themeColor?: string | null): string;
179
+ export declare function formatRoadmapMarkdown(report: AuditReport): string;
180
+ export declare function buildSelfHealingRoadmapPrompt(report: AuditReport): string;
136
181
 
182
+ export * from './history';
183
+ export * from './rules-predicates';
package/cli/audit.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { auditCode, PILLARS, RULE_REGISTRY } from './audit/rules.js';
4
+ import { createPatternRegistry } from './audit/pattern-detector.js';
5
+ import {
6
+ buildRemediationRoadmap,
7
+ formatRoadmapSection,
8
+ formatRoadmapMarkdown,
9
+ buildSelfHealingRoadmapPrompt
10
+ } from './audit/roadmap.js';
4
11
  import {
5
12
  calculateMolecularHealthScore,
6
13
  calculateQuantumHealthScore,
@@ -38,8 +45,24 @@ import {
38
45
  getAsciiGradeLines,
39
46
  formatAsciiGrade,
40
47
  getReportCardAsciiLines,
41
- REPORT_CARD_ASCII
48
+ REPORT_CARD_ASCII,
49
+ formatPillarReactionBadgesTerminal,
50
+ formatPillarReactionBadgesMarkdown,
51
+ formatPillarShieldBadges,
52
+ resolveBadgeColor,
53
+ PILLAR_SHORT_NAMES
42
54
  } from './audit/reporter.js';
55
+ import {
56
+ buildGradeFPrompt,
57
+ buildGradeDPrompt,
58
+ buildGradeCPrompt,
59
+ buildGradeBPrompt,
60
+ buildAiSlopPrompt,
61
+ buildHotspotsPrompt,
62
+ buildMasterPrompt,
63
+ formatPromptBox,
64
+ formatGroupedPromptViolations
65
+ } from './audit/prompts.js';
43
66
 
44
67
  const IGNORED_DIRS = new Set([
45
68
  'node_modules',
@@ -70,10 +93,11 @@ export const auditFile = (filePath, relativePath) => {
70
93
  return auditCode(content, filePath, relativePath);
71
94
  };
72
95
 
73
- export const scanTree = (targetDir, baseDir) => {
96
+ export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
74
97
  let violations = [];
75
98
  let fileStats = [];
76
99
  let totalHooks = 0;
100
+ const { patternRegistry } = scanOptions;
77
101
 
78
102
  if (!fs.existsSync(targetDir)) {
79
103
  return { violations, fileStats, totalHooks };
@@ -86,7 +110,7 @@ export const scanTree = (targetDir, baseDir) => {
86
110
 
87
111
  if (entry.isDirectory()) {
88
112
  if (!IGNORED_DIRS.has(entry.name)) {
89
- const sub = scanTree(fullPath, baseDir);
113
+ const sub = scanTree(fullPath, baseDir, scanOptions);
90
114
  violations = violations.concat(sub.violations);
91
115
  fileStats = fileStats.concat(sub.fileStats);
92
116
  totalHooks += sub.totalHooks;
@@ -110,7 +134,7 @@ export const scanTree = (targetDir, baseDir) => {
110
134
  totalHooks += hookMatches.length;
111
135
  }
112
136
 
113
- const fileViolations = auditCode(content, fullPath, relPath);
137
+ const fileViolations = auditCode(content, fullPath, relPath, { patternRegistry });
114
138
  violations = violations.concat(fileViolations);
115
139
  }
116
140
  }
@@ -126,7 +150,8 @@ export const scanDirectory = (targetDir, baseDir) => {
126
150
  export const runAudit = (targetDir = 'src', options = {}) => {
127
151
  const cwd = process.cwd();
128
152
  const absoluteTarget = path.resolve(cwd, targetDir);
129
- const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd);
153
+ const patternRegistry = createPatternRegistry();
154
+ const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, { patternRegistry });
130
155
 
131
156
  const scannedFiles = fileStats.length;
132
157
  const totalLoc = fileStats.reduce((acc, f) => acc + f.lineCount, 0);
@@ -168,6 +193,8 @@ export const runAudit = (targetDir = 'src', options = {}) => {
168
193
  const contextAnalysis = calculateTokenBurnAnalytics(fileStats, options);
169
194
  const hotspots = calculateHotspots(violations, fileStats, 5);
170
195
  const aiSlop = calculateAiSlopScore(violations, scannedFiles);
196
+ const patterns = patternRegistry.resolveHarmonizationCandidates(hotspots);
197
+ const roadmap = buildRemediationRoadmap({ hotspots, violations, patterns });
171
198
 
172
199
  const report = {
173
200
  targetDir,
@@ -180,6 +207,8 @@ export const runAudit = (targetDir = 'src', options = {}) => {
180
207
  pillars,
181
208
  contextAnalysis,
182
209
  hotspots,
210
+ patterns,
211
+ roadmap,
183
212
  violations
184
213
  };
185
214
 
@@ -233,9 +262,30 @@ export {
233
262
  getAsciiGradeLines,
234
263
  formatAsciiGrade,
235
264
  getReportCardAsciiLines,
236
- REPORT_CARD_ASCII
265
+ REPORT_CARD_ASCII,
266
+ formatPillarReactionBadgesTerminal,
267
+ formatPillarReactionBadgesMarkdown,
268
+ formatPillarShieldBadges,
269
+ resolveBadgeColor,
270
+ PILLAR_SHORT_NAMES,
271
+ buildGradeFPrompt,
272
+ buildGradeDPrompt,
273
+ buildGradeCPrompt,
274
+ buildGradeBPrompt,
275
+ buildAiSlopPrompt,
276
+ buildHotspotsPrompt,
277
+ buildMasterPrompt,
278
+ formatPromptBox,
279
+ formatGroupedPromptViolations,
280
+ createPatternRegistry,
281
+ buildRemediationRoadmap,
282
+ formatRoadmapSection,
283
+ formatRoadmapMarkdown,
284
+ buildSelfHealingRoadmapPrompt
237
285
  };
238
286
 
287
+ export * from './audit/rules-predicates.js';
288
+
239
289
  export default {
240
290
  auditFile,
241
291
  scanDirectory,
@@ -269,6 +319,25 @@ export default {
269
319
  formatAsciiGrade,
270
320
  getReportCardAsciiLines,
271
321
  REPORT_CARD_ASCII,
322
+ formatPillarReactionBadgesTerminal,
323
+ formatPillarReactionBadgesMarkdown,
324
+ formatPillarShieldBadges,
325
+ resolveBadgeColor,
326
+ PILLAR_SHORT_NAMES,
272
327
  PILLARS,
273
- RULE_REGISTRY
328
+ RULE_REGISTRY,
329
+ buildGradeFPrompt,
330
+ buildGradeDPrompt,
331
+ buildGradeCPrompt,
332
+ buildGradeBPrompt,
333
+ buildAiSlopPrompt,
334
+ buildHotspotsPrompt,
335
+ buildMasterPrompt,
336
+ formatPromptBox,
337
+ formatGroupedPromptViolations,
338
+ createPatternRegistry,
339
+ buildRemediationRoadmap,
340
+ formatRoadmapSection,
341
+ formatRoadmapMarkdown,
342
+ buildSelfHealingRoadmapPrompt
274
343
  };
package/cli/badge.js CHANGED
@@ -53,7 +53,7 @@ export const generateHtmlBadgeSnippet = (label, grade, discussionUrl = null) =>
53
53
  const targetHref = discussionUrl || 'https://chemicalx.xophz.com';
54
54
  const targetTitle = discussionUrl ? 'Verified Chemical X Audit Report on GitHub Discussions' : 'Verified by Chemical X Protocol';
55
55
 
56
- return `<a href="${targetHref}" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;gap:8px;padding:4px 12px;border-radius:9999px;font-family:monospace;font-size:11px;text-decoration:none;border:1px solid rgba(255,255,255,0.15);background:rgba(15,23,42,0.65);backdrop-filter:blur(8px);color:#e2e8f0;transition:all 0.2s ease;" title="${targetTitle}">
56
+ return `<a href="${targetHref}" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;gap:8px;padding:4px 12px;border-radius:9999px;font-family:monospace;font-size:11px;text-decoration:none;border:1px solid rgba(56,189,248,0.35);background:rgba(9,13,22,0.85);backdrop-filter:blur(8px);color:#e2e8f0;transition:all 0.2s ease;" title="${targetTitle}">
57
57
  <span style="width:8px;height:8px;border-radius:50%;background:#a3e635;box-shadow:0 0 6px rgba(163,230,53,0.6);"></span>
58
58
  <span>${label}</span>
59
59
  <span style="padding:2px 7px;border-radius:9999px;font-weight:bold;font-size:10px;background:rgba(163,230,53,0.15);color:#a3e635;border:1px solid rgba(163,230,53,0.3);">${grade}</span>
@@ -155,7 +155,14 @@ export default ChemicalXBadge;
155
155
  export const generateSvgBadgeSnippet = (label, grade) => {
156
156
  const width = Math.max(280, label.length * 8 + 60);
157
157
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="28" viewBox="0 0 ${width} 28" fill="none">
158
- <rect width="${width}" height="28" rx="14" fill="#090d16" stroke="#26354a" stroke-width="1"/>
158
+ <defs>
159
+ <linearGradient id="chemx-comic" x1="0%" y1="0%" x2="100%" y2="0%">
160
+ <stop offset="0%" stop-color="#f43f85"/>
161
+ <stop offset="50%" stop-color="#38bdf8"/>
162
+ <stop offset="100%" stop-color="#a3e635"/>
163
+ </linearGradient>
164
+ </defs>
165
+ <rect width="${width}" height="28" rx="14" fill="#090d16" stroke="url(#chemx-comic)" stroke-width="1.2"/>
159
166
  <circle cx="16" cy="14" r="4" fill="#a3e635"/>
160
167
  <text x="28" y="17" fill="#cbd5e1" font-family="monospace" font-size="11" font-weight="500">${label}</text>
161
168
  <rect x="${width - 44}" y="6" width="34" height="16" rx="8" fill="#a3e635" fill-opacity="0.18" stroke="#a3e635" stroke-opacity="0.4"/>
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const findFileUpward = (fileName, startDir) => {
5
+ let currentDir = path.resolve(startDir);
6
+ while (true) {
7
+ const candidate = path.join(currentDir, fileName);
8
+ if (fs.existsSync(candidate)) return candidate;
9
+ const parent = path.dirname(currentDir);
10
+ if (parent === currentDir) break;
11
+ currentDir = parent;
12
+ }
13
+ return null;
14
+ };
15
+
16
+ const resolvePackageManager = (cwd) => {
17
+ const hasPnpmLock = Boolean(findFileUpward('pnpm-lock.yaml', cwd));
18
+ const hasYarnLock = Boolean(findFileUpward('yarn.lock', cwd));
19
+ const hasBunLock = Boolean(findFileUpward('bun.lockb', cwd) || findFileUpward('bun.lock', cwd));
20
+
21
+ if (hasPnpmLock) return 'pnpm';
22
+ if (hasYarnLock) return 'yarn';
23
+ if (hasBunLock) return 'bun';
24
+ return 'npm';
25
+ };
26
+
27
+ const loadLocalPackageJson = (cwd) => {
28
+ const pkgPath = path.join(cwd, 'package.json');
29
+ if (!fs.existsSync(pkgPath)) return null;
30
+ try {
31
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
32
+ } catch {
33
+ return null;
34
+ }
35
+ };
36
+
37
+ export const detectProjectBuildCommand = (customCmd, cwd = process.cwd()) => {
38
+ const hasCustomCommand = Boolean(customCmd && customCmd.trim().length > 0);
39
+ if (hasCustomCommand) return customCmd.trim();
40
+
41
+ const pm = resolvePackageManager(cwd);
42
+ const pkg = loadLocalPackageJson(cwd);
43
+ const scripts = (pkg && pkg.scripts) || {};
44
+
45
+ const hasBuildScript = Boolean(scripts.build);
46
+ const hasBuildPublicScript = Boolean(scripts['build:public']);
47
+ const hasBuildProdScript = Boolean(scripts['build:prod']);
48
+
49
+ if (hasBuildScript) {
50
+ return pm === 'yarn' ? 'yarn build' : `${pm} run build`;
51
+ }
52
+ if (hasBuildPublicScript) {
53
+ return pm === 'yarn' ? 'yarn build:public' : `${pm} run build:public`;
54
+ }
55
+ if (hasBuildProdScript) {
56
+ return pm === 'yarn' ? 'yarn build:prod' : `${pm} run build:prod`;
57
+ }
58
+
59
+ const hasViteConfig = fs.existsSync(path.join(cwd, 'vite.config.js')) ||
60
+ fs.existsSync(path.join(cwd, 'vite.config.ts')) ||
61
+ fs.existsSync(path.join(cwd, 'vite.config.mjs'));
62
+ if (hasViteConfig) return 'npx vite build';
63
+
64
+ const hasTsConfig = fs.existsSync(path.join(cwd, 'tsconfig.json'));
65
+ if (hasTsConfig) return 'npx tsc --noEmit';
66
+
67
+ return `${pm} run build`;
68
+ };
@@ -0,0 +1,58 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export const executeBuild = (command, cwd = process.cwd(), options = {}) => {
4
+ return new Promise((resolve) => {
5
+ const startTime = Date.now();
6
+ let stdoutBuffer = '';
7
+ let stderrBuffer = '';
8
+
9
+ const isRawStream = Boolean(options.raw);
10
+
11
+ const child = spawn(command, {
12
+ shell: true,
13
+ cwd,
14
+ stdio: ['inherit', 'pipe', 'pipe'],
15
+ env: { ...process.env, FORCE_COLOR: '1' }
16
+ });
17
+
18
+ child.stdout.on('data', (chunk) => {
19
+ const text = chunk.toString();
20
+ stdoutBuffer += text;
21
+ if (isRawStream) {
22
+ process.stdout.write(text);
23
+ }
24
+ });
25
+
26
+ child.stderr.on('data', (chunk) => {
27
+ const text = chunk.toString();
28
+ stderrBuffer += text;
29
+ if (isRawStream) {
30
+ process.stderr.write(text);
31
+ }
32
+ });
33
+
34
+ child.on('error', (err) => {
35
+ const durationMs = Date.now() - startTime;
36
+ stderrBuffer += `\nProcess error: ${err.message}\n`;
37
+ resolve({
38
+ exitCode: 1,
39
+ stdout: stdoutBuffer,
40
+ stderr: stderrBuffer,
41
+ durationMs,
42
+ command
43
+ });
44
+ });
45
+
46
+ child.on('close', (code) => {
47
+ const durationMs = Date.now() - startTime;
48
+ const normalizedCode = code === null ? 1 : code;
49
+ resolve({
50
+ exitCode: normalizedCode,
51
+ stdout: stdoutBuffer,
52
+ stderr: stderrBuffer,
53
+ durationMs,
54
+ command
55
+ });
56
+ });
57
+ });
58
+ };
@@ -0,0 +1,78 @@
1
+ import { stripAnsi } from '../terminal.js';
2
+
3
+ const buildDiagnosticKey = (d) => {
4
+ return `${d.category}|${d.file || ''}|${d.line || 0}|${d.column || 0}|${d.code || ''}|${d.message}`;
5
+ };
6
+
7
+ export const deduplicateDiagnostics = (diagnostics) => {
8
+ const seen = new Set();
9
+ const unique = [];
10
+
11
+ for (const item of diagnostics) {
12
+ const key = buildDiagnosticKey(item);
13
+ if (!seen.has(key)) {
14
+ seen.add(key);
15
+ unique.push(item);
16
+ }
17
+ }
18
+
19
+ return unique;
20
+ };
21
+
22
+ export const computeSummaryCounts = (diagnostics) => {
23
+ let errorCount = 0;
24
+ let warningCount = 0;
25
+ const uniqueFiles = new Set();
26
+
27
+ for (const item of diagnostics) {
28
+ if (item.severity === 'ERROR') errorCount += 1;
29
+ if (item.severity === 'WARNING') warningCount += 1;
30
+ if (item.file) uniqueFiles.add(item.file);
31
+ }
32
+
33
+ return {
34
+ total: diagnostics.length,
35
+ errors: errorCount,
36
+ warnings: warningCount,
37
+ files: uniqueFiles.size
38
+ };
39
+ };
40
+
41
+ export const computeCategoryBreakdown = (diagnostics) => {
42
+ const categories = {};
43
+ for (const item of diagnostics) {
44
+ categories[item.category] = (categories[item.category] || 0) + 1;
45
+ }
46
+ return categories;
47
+ };
48
+
49
+ export const extractRawTailLines = (stdout, stderr, maxLines = 15) => {
50
+ const combined = `${stdout}\n${stderr}`;
51
+ const lines = combined
52
+ .split(/\r?\n/)
53
+ .map((l) => stripAnsi(l).trim())
54
+ .filter((l) => l.length > 0);
55
+ return lines.slice(-maxLines);
56
+ };
57
+
58
+ export const groupBuildDiagnostics = (rawDiagnostics, executionResult) => {
59
+ const unique = deduplicateDiagnostics(rawDiagnostics);
60
+ const counts = computeSummaryCounts(unique);
61
+ const categories = computeCategoryBreakdown(unique);
62
+
63
+ const shouldCaptureTail = executionResult.exitCode !== 0 && unique.length === 0;
64
+ const rawTail = shouldCaptureTail
65
+ ? extractRawTailLines(executionResult.stdout, executionResult.stderr)
66
+ : [];
67
+
68
+ return {
69
+ success: executionResult.exitCode === 0,
70
+ exitCode: executionResult.exitCode,
71
+ durationMs: executionResult.durationMs,
72
+ command: executionResult.command,
73
+ counts,
74
+ categories,
75
+ diagnostics: unique,
76
+ rawTail
77
+ };
78
+ };