@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.
- package/cli/audit/ai-slop-detector.js +25 -41
- package/cli/audit/ast-visitors.js +10 -13
- package/cli/audit/history.js +45 -11
- package/cli/audit/pattern-detector.js +182 -0
- package/cli/audit/prompts.js +129 -72
- package/cli/audit/reporter-banner.js +4 -16
- package/cli/audit/reporter-grades.js +28 -16
- package/cli/audit/reporter-grouping.d.ts +6 -0
- package/cli/audit/reporter-grouping.js +48 -5
- package/cli/audit/reporter-markdown.js +31 -16
- package/cli/audit/reporter-sections.js +28 -12
- package/cli/audit/reporter-utils.js +88 -1
- package/cli/audit/reporter.d.ts +13 -4
- package/cli/audit/reporter.js +32 -5
- package/cli/audit/roadmap.js +236 -0
- package/cli/audit/rules-predicates.d.ts +17 -0
- package/cli/audit/rules-predicates.js +107 -0
- package/cli/audit/rules.js +6 -2
- package/cli/audit/social-gh.js +2 -6
- package/cli/audit/social-publisher.js +1 -1
- package/cli/audit/social.js +25 -9
- package/cli/audit/types.d.ts +52 -5
- package/cli/audit.js +76 -7
- package/cli/badge.js +9 -2
- package/cli/build/detector.js +68 -0
- package/cli/build/executor.js +58 -0
- package/cli/build/grouper.js +78 -0
- package/cli/build/parser-matchers.js +116 -0
- package/cli/build/parser.js +80 -0
- package/cli/build/reporter.js +66 -0
- package/cli/build/types.d.ts +58 -0
- package/cli/build/types.js +1 -0
- package/cli/build.d.ts +16 -0
- package/cli/build.js +55 -0
- package/cli/help.js +14 -0
- package/cli/index.js +39 -14
- package/cli/installer-templates.js +43 -4
- package/cli/license.js +10 -7
- package/cli/navigator-actions.js +34 -6
- package/cli/navigator-banner.js +4 -2
- package/cli/navigator-menu.js +18 -0
- package/cli/navigator-share.js +1 -1
- package/cli/navigator.d.ts +8 -1
- package/cli/navigator.js +3 -3
- package/cli/terminal.js +6 -5
- package/cli/theme.d.ts +41 -0
- package/cli/theme.js +84 -0
- package/docs/CHANGELOG.md +46 -0
- package/package.json +3 -2
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const TS_PATTERN_PARENS = /^([^(]+)\((\d+),(\d+)\):\s*(error|warning)\s*(TS\d+):\s*(.+)$/;
|
|
2
|
+
const TS_PATTERN_COLONS = /^([^:]+):(\d+):(\d+)\s*-\s*(error|warning)\s*(TS\d+):\s*(.+)$/;
|
|
3
|
+
const ROLLUP_RESOLVE_PATTERN = /Could not resolve ["']([^"']+)["'] from ["']([^"']+)["']/;
|
|
4
|
+
const VITE_ERROR_PATTERN = /\[(vite|rollup|plugin:[^\]]+)\]\s*(.+)/;
|
|
5
|
+
const MODULE_NOT_FOUND_PATTERN = /Module not found:\s*Can['’]t resolve ['"]([^'"]+)['"] in ['"]([^'"]+)['"]/;
|
|
6
|
+
const SASS_POSTCSS_PATTERN = /(CssSyntaxError|SassError):\s*(.+)/;
|
|
7
|
+
|
|
8
|
+
const resolveTsSuggestion = (code) => {
|
|
9
|
+
if (code === 'TS2304' || code === 'TS2552') {
|
|
10
|
+
return 'Ensure the identifier is imported or declared in scope.';
|
|
11
|
+
}
|
|
12
|
+
if (code === 'TS2307') {
|
|
13
|
+
return 'Verify package installation or check tsconfig path aliases.';
|
|
14
|
+
}
|
|
15
|
+
if (code === 'TS2322' || code === 'TS2345') {
|
|
16
|
+
return 'Check property types or update interface declarations.';
|
|
17
|
+
}
|
|
18
|
+
if (code === 'TS2339') {
|
|
19
|
+
return 'Verify property naming or extend type definition.';
|
|
20
|
+
}
|
|
21
|
+
return 'Review TypeScript compiler diagnostic.';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const matchTypeScriptError = (line) => {
|
|
25
|
+
const parensMatch = line.match(TS_PATTERN_PARENS);
|
|
26
|
+
if (parensMatch) {
|
|
27
|
+
const [, file, lineNum, colNum, severityStr, code, message] = parensMatch;
|
|
28
|
+
const severity = severityStr.toLowerCase() === 'warning' ? 'WARNING' : 'ERROR';
|
|
29
|
+
return {
|
|
30
|
+
category: 'TYPE_ERROR',
|
|
31
|
+
severity,
|
|
32
|
+
file: file.trim(),
|
|
33
|
+
line: parseInt(lineNum, 10),
|
|
34
|
+
column: parseInt(colNum, 10),
|
|
35
|
+
code,
|
|
36
|
+
message: message.trim(),
|
|
37
|
+
suggestion: resolveTsSuggestion(code)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const colonsMatch = line.match(TS_PATTERN_COLONS);
|
|
42
|
+
if (colonsMatch) {
|
|
43
|
+
const [, file, lineNum, colNum, severityStr, code, message] = colonsMatch;
|
|
44
|
+
const severity = severityStr.toLowerCase() === 'warning' ? 'WARNING' : 'ERROR';
|
|
45
|
+
return {
|
|
46
|
+
category: 'TYPE_ERROR',
|
|
47
|
+
severity,
|
|
48
|
+
file: file.trim(),
|
|
49
|
+
line: parseInt(lineNum, 10),
|
|
50
|
+
column: parseInt(colNum, 10),
|
|
51
|
+
code,
|
|
52
|
+
message: message.trim(),
|
|
53
|
+
suggestion: resolveTsSuggestion(code)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const matchResolveError = (line) => {
|
|
61
|
+
const rollupMatch = line.match(ROLLUP_RESOLVE_PATTERN);
|
|
62
|
+
if (rollupMatch) {
|
|
63
|
+
const [, moduleName, importer] = rollupMatch;
|
|
64
|
+
return {
|
|
65
|
+
category: 'RESOLVE_ERROR',
|
|
66
|
+
severity: 'ERROR',
|
|
67
|
+
file: importer.trim(),
|
|
68
|
+
code: 'MODULE_NOT_FOUND',
|
|
69
|
+
message: `Could not resolve module "${moduleName}"`,
|
|
70
|
+
suggestion: 'Verify import path alias, file extension, or run package install.'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const moduleMatch = line.match(MODULE_NOT_FOUND_PATTERN);
|
|
75
|
+
if (moduleMatch) {
|
|
76
|
+
const [, moduleName, importer] = moduleMatch;
|
|
77
|
+
return {
|
|
78
|
+
category: 'RESOLVE_ERROR',
|
|
79
|
+
severity: 'ERROR',
|
|
80
|
+
file: importer.trim(),
|
|
81
|
+
code: 'MODULE_NOT_FOUND',
|
|
82
|
+
message: `Can't resolve "${moduleName}"`,
|
|
83
|
+
suggestion: 'Verify dependency is listed in package.json and installed.'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const matchBundlerOrStyleError = (line) => {
|
|
91
|
+
const sassMatch = line.match(SASS_POSTCSS_PATTERN);
|
|
92
|
+
if (sassMatch) {
|
|
93
|
+
const [, errorType, message] = sassMatch;
|
|
94
|
+
return {
|
|
95
|
+
category: 'STYLE_ERROR',
|
|
96
|
+
severity: 'ERROR',
|
|
97
|
+
code: errorType,
|
|
98
|
+
message: message.trim(),
|
|
99
|
+
suggestion: 'Check stylesheet syntax, missing variable, or mixin import.'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const viteMatch = line.match(VITE_ERROR_PATTERN);
|
|
104
|
+
if (viteMatch) {
|
|
105
|
+
const [, plugin, message] = viteMatch;
|
|
106
|
+
return {
|
|
107
|
+
category: 'SYNTAX_ERROR',
|
|
108
|
+
severity: 'ERROR',
|
|
109
|
+
code: plugin,
|
|
110
|
+
message: message.trim(),
|
|
111
|
+
suggestion: 'Check component template syntax or plugin configuration.'
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { stripAnsi } from '../terminal.js';
|
|
2
|
+
import {
|
|
3
|
+
matchTypeScriptError,
|
|
4
|
+
matchResolveError,
|
|
5
|
+
matchBundlerOrStyleError
|
|
6
|
+
} from './parser-matchers.js';
|
|
7
|
+
|
|
8
|
+
const CHUNK_BUDGET_PATTERN = /chunks are larger than \d+\s*kBs/i;
|
|
9
|
+
const GENERIC_ERROR_PATTERN = /^(SyntaxError|ReferenceError|TypeError|Error):\s*(.+)$/;
|
|
10
|
+
|
|
11
|
+
const cleanAnsiText = (text) => {
|
|
12
|
+
return stripAnsi(text).replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const matchBudgetWarning = (line) => {
|
|
16
|
+
const isBudgetIssue = CHUNK_BUDGET_PATTERN.test(line);
|
|
17
|
+
if (!isBudgetIssue) return null;
|
|
18
|
+
return {
|
|
19
|
+
category: 'BUDGET_WARNING',
|
|
20
|
+
severity: 'WARNING',
|
|
21
|
+
code: 'CHUNK_OVER_BUDGET',
|
|
22
|
+
message: line.trim(),
|
|
23
|
+
suggestion: 'Use dynamic imports or configure build.rollupOptions.output.manualChunks.'
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const matchGenericError = (line) => {
|
|
28
|
+
const match = line.match(GENERIC_ERROR_PATTERN);
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
const [, code, message] = match;
|
|
31
|
+
return {
|
|
32
|
+
category: 'RUNTIME_ERROR',
|
|
33
|
+
severity: 'ERROR',
|
|
34
|
+
code,
|
|
35
|
+
message: message.trim(),
|
|
36
|
+
suggestion: 'Inspect stack trace or surrounding code for unhandled exceptions.'
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const parseBuildOutput = (stdout, stderr) => {
|
|
41
|
+
const combined = `${stdout}\n${stderr}`;
|
|
42
|
+
const lines = combined.split(/\r?\n/);
|
|
43
|
+
const diagnostics = [];
|
|
44
|
+
|
|
45
|
+
for (const rawLine of lines) {
|
|
46
|
+
const cleanLine = cleanAnsiText(rawLine).trim();
|
|
47
|
+
if (!cleanLine) continue;
|
|
48
|
+
|
|
49
|
+
const tsDiagnostic = matchTypeScriptError(cleanLine);
|
|
50
|
+
if (tsDiagnostic) {
|
|
51
|
+
diagnostics.push({ ...tsDiagnostic, rawSnippet: cleanLine });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resolveDiagnostic = matchResolveError(cleanLine);
|
|
56
|
+
if (resolveDiagnostic) {
|
|
57
|
+
diagnostics.push({ ...resolveDiagnostic, rawSnippet: cleanLine });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const bundlerDiagnostic = matchBundlerOrStyleError(cleanLine);
|
|
62
|
+
if (bundlerDiagnostic) {
|
|
63
|
+
diagnostics.push({ ...bundlerDiagnostic, rawSnippet: cleanLine });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const budgetDiagnostic = matchBudgetWarning(cleanLine);
|
|
68
|
+
if (budgetDiagnostic) {
|
|
69
|
+
diagnostics.push({ ...budgetDiagnostic, rawSnippet: cleanLine });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const genericDiagnostic = matchGenericError(cleanLine);
|
|
74
|
+
if (genericDiagnostic) {
|
|
75
|
+
diagnostics.push({ ...genericDiagnostic, rawSnippet: cleanLine });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return diagnostics;
|
|
80
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ANSI } from '../theme.js';
|
|
2
|
+
|
|
3
|
+
const formatDiagnosticItem = (item) => {
|
|
4
|
+
const loc = item.file
|
|
5
|
+
? `${ANSI.CYAN}${item.file}${item.line ? `:${item.line}` : ''}${item.column ? `:${item.column}` : ''}${ANSI.RESET}`
|
|
6
|
+
: `${ANSI.DIM}unknown location${ANSI.RESET}`;
|
|
7
|
+
const codeTag = item.code ? ` ${ANSI.DIM}[${item.code}]${ANSI.RESET}` : '';
|
|
8
|
+
const hint = item.suggestion ? `\n ${ANSI.MINT}↳ Hint: ${item.suggestion}${ANSI.RESET}` : '';
|
|
9
|
+
return ` • ${loc}${codeTag} ${item.message}${hint}`;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const formatCategoryGroup = (category, items) => {
|
|
13
|
+
const title = `${ANSI.BOLD}${ANSI.PINK}[${category}]${ANSI.RESET} ${ANSI.DIM}(${items.length})${ANSI.RESET}`;
|
|
14
|
+
const lines = items.map(formatDiagnosticItem);
|
|
15
|
+
return `${title}\n${lines.join('\n')}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const formatTerminalBuildReport = (report, options = {}) => {
|
|
19
|
+
const isSilent = Boolean(options.silent);
|
|
20
|
+
const durationSec = (report.durationMs / 1000).toFixed(2);
|
|
21
|
+
|
|
22
|
+
if (report.success) {
|
|
23
|
+
if (isSilent) return '';
|
|
24
|
+
const successHeader = `${ANSI.BOLD}${ANSI.LIME}✔ Build Succeeded${ANSI.RESET} ${ANSI.DIM}(${durationSec}s)${ANSI.RESET}`;
|
|
25
|
+
if (report.counts.warnings === 0) return `${successHeader}\n`;
|
|
26
|
+
|
|
27
|
+
const warningLines = report.diagnostics
|
|
28
|
+
.filter((d) => d.severity === 'WARNING')
|
|
29
|
+
.map(formatDiagnosticItem)
|
|
30
|
+
.join('\n');
|
|
31
|
+
return `${successHeader}\n${ANSI.GOLD}Warnings (${report.counts.warnings}):${ANSI.RESET}\n${warningLines}\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const out = [];
|
|
35
|
+
out.push(`${ANSI.BOLD}\x1b[31m✕ Build Failed${ANSI.RESET} ${ANSI.DIM}(exit code ${report.exitCode}, ${durationSec}s)${ANSI.RESET}`);
|
|
36
|
+
out.push(`${ANSI.DIM}Command: ${report.command}${ANSI.RESET}\n`);
|
|
37
|
+
|
|
38
|
+
if (report.diagnostics.length > 0) {
|
|
39
|
+
const grouped = {};
|
|
40
|
+
for (const d of report.diagnostics) {
|
|
41
|
+
if (!grouped[d.category]) grouped[d.category] = [];
|
|
42
|
+
grouped[d.category].push(d);
|
|
43
|
+
}
|
|
44
|
+
for (const [cat, items] of Object.entries(grouped)) {
|
|
45
|
+
out.push(formatCategoryGroup(cat, items));
|
|
46
|
+
out.push('');
|
|
47
|
+
}
|
|
48
|
+
} else if (report.rawTail.length > 0) {
|
|
49
|
+
out.push(`${ANSI.BOLD}${ANSI.GOLD}[UNCATALOGED BUILD OUTPUT]${ANSI.RESET}`);
|
|
50
|
+
for (const line of report.rawTail) {
|
|
51
|
+
out.push(` ${ANSI.DIM}${line}${ANSI.RESET}`);
|
|
52
|
+
}
|
|
53
|
+
out.push('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
out.push(
|
|
57
|
+
`${ANSI.BOLD}Audit Summary:${ANSI.RESET} ${report.counts.errors} error(s), ${report.counts.warnings} warning(s) across ${report.counts.files} file(s).`
|
|
58
|
+
);
|
|
59
|
+
out.push(`${ANSI.DIM}Tip: Run with --json to emit structured machine-readable diagnostics for AI agents.${ANSI.RESET}\n`);
|
|
60
|
+
|
|
61
|
+
return out.join('\n');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const formatJsonBuildReport = (report) => {
|
|
65
|
+
return JSON.stringify(report, null, 2);
|
|
66
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type BuildDiagnosticCategory =
|
|
2
|
+
| 'TYPE_ERROR'
|
|
3
|
+
| 'RESOLVE_ERROR'
|
|
4
|
+
| 'SYNTAX_ERROR'
|
|
5
|
+
| 'STYLE_ERROR'
|
|
6
|
+
| 'BUDGET_WARNING'
|
|
7
|
+
| 'COMPILER_WARNING'
|
|
8
|
+
| 'RUNTIME_ERROR'
|
|
9
|
+
| 'UNKNOWN';
|
|
10
|
+
|
|
11
|
+
export type BuildDiagnosticSeverity = 'ERROR' | 'WARNING' | 'INFO';
|
|
12
|
+
|
|
13
|
+
export interface BuildDiagnostic {
|
|
14
|
+
readonly category: BuildDiagnosticCategory;
|
|
15
|
+
readonly severity: BuildDiagnosticSeverity;
|
|
16
|
+
readonly file?: string;
|
|
17
|
+
readonly line?: number;
|
|
18
|
+
readonly column?: number;
|
|
19
|
+
readonly code?: string;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
readonly suggestion?: string;
|
|
22
|
+
readonly rawSnippet?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BuildSummaryCounts {
|
|
26
|
+
readonly total: number;
|
|
27
|
+
readonly errors: number;
|
|
28
|
+
readonly warnings: number;
|
|
29
|
+
readonly files: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BuildAuditReport {
|
|
33
|
+
readonly success: boolean;
|
|
34
|
+
readonly exitCode: number;
|
|
35
|
+
readonly durationMs: number;
|
|
36
|
+
readonly command: string;
|
|
37
|
+
readonly counts: BuildSummaryCounts;
|
|
38
|
+
readonly categories: Record<string, number>;
|
|
39
|
+
readonly diagnostics: readonly BuildDiagnostic[];
|
|
40
|
+
readonly rawTail: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BuildOptions {
|
|
44
|
+
readonly command?: string;
|
|
45
|
+
readonly silent?: boolean;
|
|
46
|
+
readonly json?: boolean;
|
|
47
|
+
readonly summary?: boolean;
|
|
48
|
+
readonly raw?: boolean;
|
|
49
|
+
readonly cwd?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BuildExecutionResult {
|
|
53
|
+
readonly exitCode: number;
|
|
54
|
+
readonly stdout: string;
|
|
55
|
+
readonly stderr: string;
|
|
56
|
+
readonly durationMs: number;
|
|
57
|
+
readonly command: string;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/cli/build.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BuildAuditReport, BuildOptions } from './build/types.js';
|
|
2
|
+
|
|
3
|
+
export function runBuildAudit(
|
|
4
|
+
rawArgs?: readonly string[],
|
|
5
|
+
isCli?: boolean
|
|
6
|
+
): Promise<BuildAuditReport>;
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
BuildDiagnostic,
|
|
10
|
+
BuildDiagnosticCategory,
|
|
11
|
+
BuildDiagnosticSeverity,
|
|
12
|
+
BuildSummaryCounts,
|
|
13
|
+
BuildAuditReport,
|
|
14
|
+
BuildOptions,
|
|
15
|
+
BuildExecutionResult
|
|
16
|
+
} from './build/types.js';
|
package/cli/build.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { detectProjectBuildCommand } from './build/detector.js';
|
|
2
|
+
import { executeBuild } from './build/executor.js';
|
|
3
|
+
import { parseBuildOutput } from './build/parser.js';
|
|
4
|
+
import { groupBuildDiagnostics } from './build/grouper.js';
|
|
5
|
+
import { formatTerminalBuildReport, formatJsonBuildReport } from './build/reporter.js';
|
|
6
|
+
import { ANSI } from './theme.js';
|
|
7
|
+
|
|
8
|
+
const parseCommandFromArgs = (args) => {
|
|
9
|
+
const dashDashIndex = args.indexOf('--');
|
|
10
|
+
if (dashDashIndex !== -1) {
|
|
11
|
+
const afterDash = args.slice(dashDashIndex + 1).join(' ').trim();
|
|
12
|
+
if (afterDash.length > 0) return afterDash;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const candidate = args.find((a) => !a.startsWith('-') && a !== 'build' && a !== 'run' && a !== 'wrap');
|
|
16
|
+
return candidate ? candidate.trim() : null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const runBuildAudit = async (rawArgs = [], isCli = false) => {
|
|
20
|
+
const isJson = rawArgs.includes('--json');
|
|
21
|
+
const isSilent = rawArgs.includes('--silent');
|
|
22
|
+
const isRaw = rawArgs.includes('--raw');
|
|
23
|
+
const isSummary = rawArgs.includes('--summary');
|
|
24
|
+
|
|
25
|
+
const customCommand = parseCommandFromArgs(rawArgs);
|
|
26
|
+
const command = detectProjectBuildCommand(customCommand, process.cwd());
|
|
27
|
+
|
|
28
|
+
const shouldPrintStart = !isJson && !isSilent && !isRaw;
|
|
29
|
+
if (shouldPrintStart) {
|
|
30
|
+
process.stdout.write(`${ANSI.CYAN}Auditing build:${ANSI.RESET} ${ANSI.DIM}${command}${ANSI.RESET}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const executionResult = await executeBuild(command, process.cwd(), { raw: isRaw });
|
|
34
|
+
const rawDiagnostics = parseBuildOutput(executionResult.stdout, executionResult.stderr);
|
|
35
|
+
const report = groupBuildDiagnostics(rawDiagnostics, executionResult);
|
|
36
|
+
|
|
37
|
+
if (isJson) {
|
|
38
|
+
process.stdout.write(formatJsonBuildReport(report) + '\n');
|
|
39
|
+
if (isCli) process.exit(report.exitCode);
|
|
40
|
+
return report;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const terminalOutput = formatTerminalBuildReport(report, { silent: isSilent, summary: isSummary });
|
|
44
|
+
if (terminalOutput) {
|
|
45
|
+
process.stdout.write(terminalOutput);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isCli) {
|
|
49
|
+
process.exit(report.exitCode);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return report;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export * from './build/types.js';
|
package/cli/help.js
CHANGED
|
@@ -16,6 +16,14 @@ export const printHelp = () => {
|
|
|
16
16
|
` ${CYAN}npm create chemx${RESET} [directory]`,
|
|
17
17
|
'',
|
|
18
18
|
`${BOLD}COMMANDS${RESET}`,
|
|
19
|
+
` ${CYAN}build${RESET} [options] [-- <command>] ${DIM}(aliases: run, wrap)${RESET}`,
|
|
20
|
+
` Wrap and audit arbitrary build scripts with token-conserving silent execution.`,
|
|
21
|
+
` Suppresses multi-thousand-line compiler noise, catalogs diagnostics into`,
|
|
22
|
+
` grouped categories (TypeScript, Vite/Rollup, Style, Budget), and provides`,
|
|
23
|
+
` actionable remediation hints and machine-readable JSON for AI agents.`,
|
|
24
|
+
` Flags: ${CYAN}--json${RESET}, ${CYAN}--silent${RESET}, ${CYAN}--raw${RESET}, ${CYAN}--summary${RESET}`,
|
|
25
|
+
` Examples: ${CYAN}npx chemx build${RESET} or ${CYAN}npx chemx build -- pnpm build${RESET}`,
|
|
26
|
+
'',
|
|
19
27
|
` ${CYAN}audit${RESET} [options]`,
|
|
20
28
|
` Run the 7-Pillar Molecular Architecture static analysis audit.`,
|
|
21
29
|
` Scans AST for line budgets, control flow booleans, nested ternaries,`,
|
|
@@ -139,6 +147,12 @@ export const printHelp = () => {
|
|
|
139
147
|
'',
|
|
140
148
|
` ${DIM}# Scaffold a new project:${RESET}`,
|
|
141
149
|
` ${CYAN}npm create chemx my-awesome-app${RESET}`,
|
|
150
|
+
'',
|
|
151
|
+
` ${DIM}# Run token-conserving silent build audit with structured diagnostics:${RESET}`,
|
|
152
|
+
` ${CYAN}npx chemx build --silent${RESET}`,
|
|
153
|
+
'',
|
|
154
|
+
` ${DIM}# Wrap arbitrary build command and output AI-ready JSON:${RESET}`,
|
|
155
|
+
` ${CYAN}npx chemx build --json -- pnpm run build${RESET}`,
|
|
142
156
|
''
|
|
143
157
|
].join('\n');
|
|
144
158
|
|
package/cli/index.js
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
generateMarkdownReport,
|
|
10
10
|
saveAuditSnapshot,
|
|
11
11
|
copyToClipboard,
|
|
12
|
-
buildMasterPrompt
|
|
12
|
+
buildMasterPrompt,
|
|
13
|
+
groupViolationsBySeverity
|
|
13
14
|
} from './audit.js';
|
|
14
15
|
import {
|
|
15
16
|
runScaffold,
|
|
@@ -23,8 +24,15 @@ import {
|
|
|
23
24
|
showConversionMenu,
|
|
24
25
|
handleShareToDiscussions
|
|
25
26
|
} from './navigator.js';
|
|
27
|
+
import { renderDashboardBanner } from './navigator-banner.js';
|
|
28
|
+
import {
|
|
29
|
+
isGradeBelowMinimum,
|
|
30
|
+
evaluateAuditFailure,
|
|
31
|
+
isNonInteractiveSession
|
|
32
|
+
} from './audit/rules-predicates.js';
|
|
26
33
|
import { runInstallWizard } from './installer.js';
|
|
27
34
|
import { runBadgeCommand } from './badge.js';
|
|
35
|
+
import { runBuildAudit } from './build.js';
|
|
28
36
|
|
|
29
37
|
const rawArgs = process.argv.slice(2);
|
|
30
38
|
const invokedBin = path.basename(process.argv[1] || '');
|
|
@@ -79,12 +87,11 @@ export const runAudit = async (customDir = null, isCli = false) => {
|
|
|
79
87
|
const report = executeAstAudit(targetDir, { outputFile, model, costPerMillion });
|
|
80
88
|
saveAuditSnapshot(report);
|
|
81
89
|
|
|
82
|
-
const GRADE_RANKS = { 'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, 'F': 0 };
|
|
83
90
|
const hasCriticalOrHigh = report.violations.some((v) => v.severity === 'CRITICAL' || v.severity === 'HIGH');
|
|
84
91
|
const isStrictFail = isStrict && report.violations.length > 0;
|
|
85
|
-
const isGradeFail =
|
|
92
|
+
const isGradeFail = isGradeBelowMinimum(report.health.grade, minGrade);
|
|
86
93
|
const isScoreFail = minScore !== null && !isNaN(minScore) && report.health.score < minScore;
|
|
87
|
-
const hasFailingViolations = isStrictFail
|
|
94
|
+
const hasFailingViolations = evaluateAuditFailure([isStrictFail, hasCriticalOrHigh, isGradeFail, isScoreFail]);
|
|
88
95
|
|
|
89
96
|
if (isJson) {
|
|
90
97
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -92,24 +99,21 @@ export const runAudit = async (customDir = null, isCli = false) => {
|
|
|
92
99
|
return report;
|
|
93
100
|
}
|
|
94
101
|
|
|
95
|
-
if (isMarkdown) {
|
|
102
|
+
if (isMarkdown && !outputFile) {
|
|
96
103
|
const md = generateMarkdownReport(report);
|
|
97
104
|
process.stdout.write(md + '\n');
|
|
98
105
|
if (isCli) process.exit(hasFailingViolations ? 1 : 0);
|
|
99
106
|
return report;
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
if (outputFile) {
|
|
103
|
-
process.stdout.write(`\x1b[32m✔ Exported markdown audit report to:\x1b[0m ${outputFile}\n\n`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
109
|
if (isCli) {
|
|
107
110
|
if (isShare) {
|
|
108
111
|
await handleShareToDiscussions(report);
|
|
109
112
|
process.exit(0);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
const
|
|
115
|
+
const isNonInteractive = isNonInteractiveSession(rawArgs);
|
|
116
|
+
const isInteractive = !isNonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
113
117
|
|
|
114
118
|
if (isInteractive && !isUnroll) {
|
|
115
119
|
const handleReAudit = () => {
|
|
@@ -123,9 +127,25 @@ export const runAudit = async (customDir = null, isCli = false) => {
|
|
|
123
127
|
handleReAudit
|
|
124
128
|
);
|
|
125
129
|
} else {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
if (isUnroll) {
|
|
131
|
+
process.stdout.write(formatTerminalReport(report));
|
|
132
|
+
if (isInteractive) {
|
|
133
|
+
await showConversionMenu(() => runScaffold(undefined, rawArgs, runAudit));
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
const { critical, high, medium, low } = groupViolationsBySeverity(report.violations);
|
|
137
|
+
const highMediumCount = high.length + medium.length;
|
|
138
|
+
renderDashboardBanner(
|
|
139
|
+
report.health,
|
|
140
|
+
report.metrics,
|
|
141
|
+
report.violations,
|
|
142
|
+
critical,
|
|
143
|
+
highMediumCount,
|
|
144
|
+
low,
|
|
145
|
+
report.contextAnalysis,
|
|
146
|
+
report.aiSlop,
|
|
147
|
+
{ clear: false }
|
|
148
|
+
);
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
|
|
@@ -145,7 +165,7 @@ export const runAudit = async (customDir = null, isCli = false) => {
|
|
|
145
165
|
return report;
|
|
146
166
|
};
|
|
147
167
|
|
|
148
|
-
export { auditFile };
|
|
168
|
+
export { auditFile, runBuildAudit };
|
|
149
169
|
|
|
150
170
|
const main = async () => {
|
|
151
171
|
const firstArg = rawArgs[0];
|
|
@@ -157,6 +177,11 @@ const main = async () => {
|
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
switch (firstArg) {
|
|
180
|
+
case 'build':
|
|
181
|
+
case 'run':
|
|
182
|
+
case 'wrap':
|
|
183
|
+
await runBuildAudit(rawArgs.slice(1), true);
|
|
184
|
+
break;
|
|
160
185
|
case 'audit':
|
|
161
186
|
await runAudit(null, true);
|
|
162
187
|
break;
|
|
@@ -44,7 +44,7 @@ fi
|
|
|
44
44
|
|
|
45
45
|
if [ -n "\$AUDIT_BIN" ]; then
|
|
46
46
|
printf "\\033[38;2;98;201;255m[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...\\033[0m\\n" "\$MIN_GRADE" "\$MIN_SCORE"
|
|
47
|
-
if ! \$AUDIT_BIN audit --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --
|
|
47
|
+
if ! \$AUDIT_BIN audit --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --non-interactive < /dev/null; then
|
|
48
48
|
printf "\\n\\033[1m\\033[31m[Chemical X] Commit Blocked: Codebase falls below required Grade %s (Score %s)\\033[0m\\n" "\$MIN_GRADE" "\$MIN_SCORE"
|
|
49
49
|
printf "\\033[36m💡 Tip: Want crystalline drop-in templates? Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol\\033[0m\\n\\n"
|
|
50
50
|
exit 1
|
|
@@ -78,14 +78,53 @@ jobs:
|
|
|
78
78
|
- name: Setup Node.js
|
|
79
79
|
uses: actions/setup-node@v4
|
|
80
80
|
with:
|
|
81
|
-
node-version:
|
|
82
|
-
|
|
81
|
+
node-version: 22
|
|
82
|
+
|
|
83
|
+
- name: Install dependencies
|
|
84
|
+
run: |
|
|
85
|
+
if [ -f package-lock.json ]; then
|
|
86
|
+
npm ci
|
|
87
|
+
elif [ -f package.json ]; then
|
|
88
|
+
npm install
|
|
89
|
+
fi
|
|
90
|
+
|
|
83
91
|
- name: Run Chemical X Architectural Audit
|
|
84
|
-
run: npx chemx audit --min-grade=\${{ vars.CHEMX_MIN_GRADE || '${minGrade}' }} --min-score=\${{ vars.CHEMX_MIN_SCORE || ${minScore} }} --markdown --output=AUDIT_REPORT.md
|
|
92
|
+
run: npx --yes chemx audit --min-grade=\${{ vars.CHEMX_MIN_GRADE || '${minGrade}' }} --min-score=\${{ vars.CHEMX_MIN_SCORE || ${minScore} }} --markdown --output=AUDIT_REPORT.md
|
|
85
93
|
- name: Upload Audit Scorecard Artifact
|
|
86
94
|
if: always()
|
|
87
95
|
uses: actions/upload-artifact@v4
|
|
88
96
|
with:
|
|
89
97
|
name: chemical-x-audit-report
|
|
90
98
|
path: AUDIT_REPORT.md
|
|
99
|
+
|
|
100
|
+
- name: Post Architectural Audit Comment on PR
|
|
101
|
+
if: always() && github.event_name == 'pull_request' && hashFiles('AUDIT_REPORT.md') != ''
|
|
102
|
+
continue-on-error: true
|
|
103
|
+
env:
|
|
104
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
105
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
106
|
+
REPO: \${{ github.repository }}
|
|
107
|
+
run: |
|
|
108
|
+
DISCUSSION_URL="\${{ vars.CHEMX_DISCUSSION_URL }}"
|
|
109
|
+
if [ -z "\$DISCUSSION_URL" ] && [ -f .chemx/discussion.json ]; then
|
|
110
|
+
DISCUSSION_URL=\$(node -e "try { const d = JSON.parse(require('fs').readFileSync('.chemx/discussion.json')); console.log(d.url || ''); } catch (e) {}")
|
|
111
|
+
fi
|
|
112
|
+
if [ -z "\$DISCUSSION_URL" ]; then
|
|
113
|
+
DISCUSSION_URL="https://github.com/orgs/Chemical-X-Protocol/discussions/categories/npx-chemx-audit"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
echo "<!-- chemical-x-audit-pr-comment -->"
|
|
118
|
+
echo "> 💬 **Architectural Discussion Topic:** [View Discussion & Community Showcase](\$DISCUSSION_URL)"
|
|
119
|
+
echo ""
|
|
120
|
+
cat AUDIT_REPORT.md
|
|
121
|
+
} > PR_COMMENT.md
|
|
122
|
+
|
|
123
|
+
EXISTING_COMMENT_ID=\$(gh api "repos/\$REPO/issues/\$PR_NUMBER/comments" --jq '.[] | select(.body | contains("<!-- chemical-x-audit-pr-comment -->")) | .id' | head -n 1)
|
|
124
|
+
|
|
125
|
+
if [ -n "\$EXISTING_COMMENT_ID" ]; then
|
|
126
|
+
gh api --method PATCH "repos/\$REPO/issues/comments/\$EXISTING_COMMENT_ID" -F body=@PR_COMMENT.md
|
|
127
|
+
else
|
|
128
|
+
gh pr comment "\$PR_NUMBER" --body-file PR_COMMENT.md
|
|
129
|
+
fi
|
|
91
130
|
`;
|
package/cli/license.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
promptQuestion,
|
|
10
10
|
openBrowser
|
|
11
11
|
} from './terminal.js';
|
|
12
|
+
import { toResultSync } from './audit/rules-helpers.js';
|
|
12
13
|
|
|
13
14
|
export const CONFIG_DIR = path.join(os.homedir(), '.chemical-x');
|
|
14
15
|
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
@@ -20,14 +21,14 @@ export const URL_SPONSOR = 'https://github.com/sponsors/Chemical-X-Protocol';
|
|
|
20
21
|
export const URL_STANDARD = 'https://mycompassconsulting.com/buy/chemical-x/standard';
|
|
21
22
|
export const URL_MASTER = 'https://mycompassconsulting.com/buy/chemical-x/master';
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
process.stderr.write(`Failed to create config directory: ${error.message}\n`);
|
|
24
|
+
export const ensureConfigDir = () => {
|
|
25
|
+
if (fs.existsSync(CONFIG_DIR)) return true;
|
|
26
|
+
const [, err] = toResultSync(() => fs.mkdirSync(CONFIG_DIR, { recursive: true }));
|
|
27
|
+
if (err) {
|
|
28
|
+
return false;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
31
32
|
|
|
32
33
|
export const getOrCreateDeviceId = () => {
|
|
33
34
|
if (fs.existsSync(DEVICE_FILE)) {
|
|
@@ -40,6 +41,7 @@ export const getOrCreateDeviceId = () => {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
const newId = `cli_${Math.random().toString(36).substring(2, 12)}_${Date.now()}`;
|
|
44
|
+
ensureConfigDir();
|
|
43
45
|
try {
|
|
44
46
|
fs.writeFileSync(DEVICE_FILE, newId, 'utf-8');
|
|
45
47
|
} catch (err) {
|
|
@@ -62,6 +64,7 @@ export const getCachedLicenseKey = () => {
|
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
export const saveLicenseKey = (licenseKey) => {
|
|
67
|
+
ensureConfigDir();
|
|
65
68
|
try {
|
|
66
69
|
fs.writeFileSync(
|
|
67
70
|
CONFIG_FILE,
|