@chemx/starter-kit 26.9.17-606 → 26.9.20-369
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/README.md +119 -7
- package/blueprints/_package.json +17 -0
- package/blueprints/_tsconfig.json +15 -0
- package/blueprints/_vitest.config.ts +9 -0
- package/cli/audit/extended-text-patterns.js +218 -0
- package/cli/audit/extended-visitors.js +12 -222
- package/cli/audit.js +1 -0
- package/cli/build/detector.js +40 -23
- package/cli/build/detector.spec.js +81 -0
- package/cli/build.js +29 -7
- package/cli/errors/catcher.js +80 -0
- package/cli/errors/errors.spec.js +163 -0
- package/cli/errors/formatter.js +94 -0
- package/cli/errors/index.js +52 -0
- package/cli/errors/publisher.js +76 -0
- package/cli/errors/sanitizer.js +50 -0
- package/cli/errors/storage.js +39 -0
- package/cli/errors/types.d.ts +42 -0
- package/cli/generator.d.ts +3 -2
- package/cli/generator.js +59 -0
- package/cli/generator.spec.js +17 -0
- package/cli/help.js +18 -0
- package/cli/index.js +34 -3
- package/cli/installer-templates.js +1 -1
- package/cli/installer.d.ts +2 -0
- package/cli/installer.js +37 -25
- package/cli/installer.spec.js +72 -0
- package/cli/mcp/antigravity.js +37 -18
- package/cli/mcp/installer.js +8 -5
- package/cli/mcp/installer.spec.js +6 -7
- package/cli/mcp/manifests.js +162 -14
- package/cli/mcp/server.js +48 -12
- package/cli/mcp/server.spec.js +114 -21
- package/cli/mcp/tools-search.js +237 -0
- package/cli/mcp/tools-team.js +145 -0
- package/cli/mcp/tools.js +139 -177
- package/cli/navigator-banner-helpers.js +33 -0
- package/cli/navigator-banner.js +17 -35
- package/cli/navigator-banner.spec.js +68 -0
- package/cli/patcher.js +42 -0
- package/cli/patcher.spec.js +19 -0
- package/cli/reader.js +90 -5
- package/cli/scaffold.js +27 -7
- package/cli/search-schema.js +4 -0
- package/cli/team/index.js +30 -0
- package/cli/team/team-adversarial-locks.spec.js +105 -0
- package/cli/team/team-adversarial-telemetry.spec.js +93 -0
- package/cli/team/team-commands.js +199 -0
- package/cli/team/team-db-agents.js +79 -0
- package/cli/team/team-db-feed.js +84 -0
- package/cli/team/team-db-lock-promotion.js +91 -0
- package/cli/team/team-db-locks.js +94 -0
- package/cli/team/team-db-task-helpers.js +94 -0
- package/cli/team/team-db-tasks.js +90 -0
- package/cli/team/team-db.js +93 -0
- package/cli/team/team-format.js +80 -0
- package/cli/team/team-lock-concurrency.spec.js +182 -0
- package/cli/team/team-multi-model-telemetry.spec.js +80 -0
- package/cli/team/team-schema.js +80 -0
- package/cli/team/team-telemetry.js +127 -0
- package/cli/team/team-triage.js +97 -0
- package/cli/team/team.spec.js +344 -0
- package/cli/theme.js +1 -0
- package/cli/ui-actions-codebase.js +75 -0
- package/cli/ui-actions-helpers.js +66 -0
- package/cli/ui-actions-studio.js +91 -0
- package/cli/ui-actions-tasks.js +87 -0
- package/cli/ui-actions.js +69 -0
- package/cli/ui-attention.js +74 -0
- package/cli/ui-canonical-routes.spec.js +130 -0
- package/cli/ui-client-api.js +87 -0
- package/cli/ui-client-cert.js +27 -0
- package/cli/ui-client-filetree.js +93 -0
- package/cli/ui-client-forum.js +85 -0
- package/cli/ui-client-script.js +98 -0
- package/cli/ui-client-studio.js +95 -0
- package/cli/ui-db-studio.js +79 -0
- package/cli/ui-e2e-verification.spec.js +186 -0
- package/cli/ui-filetree.spec.js +175 -0
- package/cli/ui-forum-data.js +97 -0
- package/cli/ui-forum-topics.js +65 -0
- package/cli/ui-handlers.js +91 -0
- package/cli/ui-html.js +41 -0
- package/cli/ui-kanban.spec.js +206 -0
- package/cli/ui-server-routes.js +91 -0
- package/cli/ui-server.js +81 -0
- package/cli/ui-styles.js +87 -0
- package/cli/ui-template-agents.js +64 -0
- package/cli/ui-template-dbstudio.js +82 -0
- package/cli/ui-template-filetree.js +92 -0
- package/cli/ui-template-forum.js +86 -0
- package/cli/ui-template-kanban.js +83 -0
- package/cli/ui-template-thread.js +51 -0
- package/cli/ui-template-timeline.js +35 -0
- package/cli/ui-template-views.js +63 -0
- package/cli/ui-template-workbench.js +55 -0
- package/cli/ui-template.js +71 -0
- package/cli/ui-vbulletin.spec.js +189 -0
- package/cli/ui-workbench-dbstudio.spec.js +172 -0
- package/cli/ui.spec.js +305 -0
- package/cli/verify.js +123 -42
- package/package.json +11 -16
- package/scripts/pre-commit.sh +1 -1
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import * as t from '@babel/types';
|
|
4
2
|
import { RULE_REGISTRY } from './rules-registry.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
isTemplateExtension,
|
|
9
|
-
isCommentLine
|
|
10
|
-
} from './rules-predicates.js';
|
|
3
|
+
import { checkExtendedTextPatterns } from './extended-text-patterns.js';
|
|
4
|
+
|
|
5
|
+
export { checkExtendedTextPatterns };
|
|
11
6
|
|
|
12
7
|
const BARE_BOOLEANS = new Set([
|
|
13
8
|
'loading', 'valid', 'active', 'visible', 'open', 'disabled',
|
|
@@ -20,19 +15,6 @@ const BARE_HANDLERS = new Set([
|
|
|
20
15
|
'reset', 'confirm', 'cancel', 'refresh', 'sync', 'send', 'pay'
|
|
21
16
|
]);
|
|
22
17
|
|
|
23
|
-
const SECRET_PATTERNS = [
|
|
24
|
-
/(['"])(?:sk-[a-zA-Z0-9_-]{24,}|ghp_[a-zA-Z0-9]{30,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z-_]{35})\1/
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
const FAKE_GREEN_PATTERNS = [
|
|
28
|
-
/expect\s*\(\s*(true|false|1|0)\s*\)\s*\.\s*to(?:Be|Equal|StrictEqual)\s*\(\s*\1\s*\)/,
|
|
29
|
-
/assert\s*\.\s*(?:strictEqual|equal|deepEqual)\s*\(\s*(true|false|1|0)\s*,\s*\1\s*\)/,
|
|
30
|
-
/assert\s*\.\s*ok\s*\(\s*(?:true|1)\s*\)/
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const IMG_WITHOUT_ALT_PATTERN = new RegExp(['<', 'img\\b', '(?![^>]*\\balt\\s*=)', '[^>]*>'].join(''), 'i');
|
|
34
|
-
const CLICKABLE_CONTAINER_PATTERN = new RegExp(['<', '(div|span)\\b', '(?![^>]*\\brole\\s*=\\s*[\'"](?:button|link|tab)[\'"])', '[^>]*\\b(?:@click|v-on:click)\\s*='].join(''), 'i');
|
|
35
|
-
|
|
36
18
|
const SENSITIVE_KEYWORDS = new Set([
|
|
37
19
|
'password', 'passwd', 'secret', 'token', 'apikey', 'api_key',
|
|
38
20
|
'authtoken', 'auth_token', 'accesstoken', 'access_token',
|
|
@@ -47,8 +29,14 @@ const isSensitiveName = (name = '') => {
|
|
|
47
29
|
};
|
|
48
30
|
|
|
49
31
|
const JAVASCRIPT_URL_REGEX = /^\s*javascript:/i;
|
|
50
|
-
|
|
51
|
-
const
|
|
32
|
+
|
|
33
|
+
const resolvePropertyKeyName = (key) => {
|
|
34
|
+
const isIdentifier = t.isIdentifier(key);
|
|
35
|
+
if (isIdentifier) return key.name;
|
|
36
|
+
const isStringLiteral = t.isStringLiteral(key);
|
|
37
|
+
if (isStringLiteral) return key.value;
|
|
38
|
+
return '';
|
|
39
|
+
};
|
|
52
40
|
|
|
53
41
|
const isSensitiveExpression = (node) => {
|
|
54
42
|
if (!node) return false;
|
|
@@ -66,11 +54,7 @@ const isSensitiveExpression = (node) => {
|
|
|
66
54
|
if (t.isObjectExpression(node)) {
|
|
67
55
|
return node.properties.some((prop) => {
|
|
68
56
|
if (t.isObjectProperty(prop)) {
|
|
69
|
-
const keyName =
|
|
70
|
-
? prop.key.name
|
|
71
|
-
: t.isStringLiteral(prop.key)
|
|
72
|
-
? prop.key.value
|
|
73
|
-
: '';
|
|
57
|
+
const keyName = resolvePropertyKeyName(prop.key);
|
|
74
58
|
return isSensitiveName(keyName);
|
|
75
59
|
}
|
|
76
60
|
return false;
|
|
@@ -82,200 +66,6 @@ const isSensitiveExpression = (node) => {
|
|
|
82
66
|
return false;
|
|
83
67
|
};
|
|
84
68
|
|
|
85
|
-
/**
|
|
86
|
-
* Pillar 10: Check for co-located test files for molecule capsules.
|
|
87
|
-
*/
|
|
88
|
-
const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
|
|
89
|
-
const baseName = path.basename(filePath);
|
|
90
|
-
const ext = path.extname(filePath);
|
|
91
|
-
|
|
92
|
-
// Stage 1: Atomic Concept Declarations
|
|
93
|
-
const isMoleculePath = relativePath.includes('molecules') || baseName.startsWith('m-');
|
|
94
|
-
const isComponentExt = isComponentExtension(ext);
|
|
95
|
-
const isTestOrSpecFile = baseName.includes('.spec.') || baseName.includes('.test.');
|
|
96
|
-
|
|
97
|
-
// Stage 2: Unified Decision Variable & Early Guard Clause
|
|
98
|
-
const isCandidateComponent = isMoleculePath && isComponentExt;
|
|
99
|
-
const isMoleculeComponent = isCandidateComponent && !isTestOrSpecFile;
|
|
100
|
-
if (!isMoleculeComponent) return;
|
|
101
|
-
|
|
102
|
-
const dir = path.dirname(filePath);
|
|
103
|
-
const [siblings, dirError] = toResultSync(() => (fs.existsSync(dir) ? fs.readdirSync(dir) : []));
|
|
104
|
-
|
|
105
|
-
// Stage 1: Atomic Guard Check
|
|
106
|
-
const hasDirError = Boolean(dirError);
|
|
107
|
-
const hasSiblings = Boolean(siblings);
|
|
108
|
-
const canInspectSiblings = !hasDirError && hasSiblings;
|
|
109
|
-
if (!canInspectSiblings) return;
|
|
110
|
-
|
|
111
|
-
const isTestSibling = (fileName) => /\.(test|spec)\.[jt]sx?$/.test(fileName);
|
|
112
|
-
const hasCoLocatedTest = siblings.some(isTestSibling);
|
|
113
|
-
if (hasCoLocatedTest) return;
|
|
114
|
-
|
|
115
|
-
const meta = RULE_REGISTRY.TEST_MISSING_COLOCATED;
|
|
116
|
-
violations.push({
|
|
117
|
-
filePath: relativePath,
|
|
118
|
-
line: 1,
|
|
119
|
-
column: 1,
|
|
120
|
-
hazard: 'Molecule capsule missing co-located test file (*.spec.ts or *.test.ts)',
|
|
121
|
-
rule: 'TEST_MISSING_COLOCATED',
|
|
122
|
-
severity: meta.severity,
|
|
123
|
-
pillar: meta.pillar,
|
|
124
|
-
directive: meta.directive
|
|
125
|
-
});
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
|
|
129
|
-
checkMoleculeCoLocatedTest(filePath, relativePath, violations);
|
|
130
|
-
|
|
131
|
-
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath);
|
|
132
|
-
const ext = path.extname(filePath);
|
|
133
|
-
const isTemplateFile = isTemplateExtension(ext);
|
|
134
|
-
|
|
135
|
-
lines.forEach((lineText, idx) => {
|
|
136
|
-
const lineNum = idx + 1;
|
|
137
|
-
const trimmed = lineText.trim();
|
|
138
|
-
const isComment = isCommentLine(trimmed);
|
|
139
|
-
|
|
140
|
-
// Pillar 9: Hardcoded Secret Detection
|
|
141
|
-
const hasEnvReference = trimmed.includes('process.env') || trimmed.includes('import.meta.env');
|
|
142
|
-
const canScanSecrets = !isComment && !hasEnvReference;
|
|
143
|
-
|
|
144
|
-
if (canScanSecrets) {
|
|
145
|
-
for (const pat of SECRET_PATTERNS) {
|
|
146
|
-
const matchesSecretPattern = pat.test(lineText);
|
|
147
|
-
const hasPlaceholder = lineText.includes('dummy') || lineText.includes('placeholder');
|
|
148
|
-
const isHardcodedSecret = matchesSecretPattern && !hasPlaceholder;
|
|
149
|
-
|
|
150
|
-
if (isHardcodedSecret) {
|
|
151
|
-
const meta = RULE_REGISTRY.SECURITY_HARDCODED_SECRET;
|
|
152
|
-
violations.push({
|
|
153
|
-
filePath: relativePath,
|
|
154
|
-
line: lineNum,
|
|
155
|
-
column: 1,
|
|
156
|
-
hazard: 'High-entropy API key or secret token detected in source code',
|
|
157
|
-
rule: 'SECURITY_HARDCODED_SECRET',
|
|
158
|
-
severity: meta.severity,
|
|
159
|
-
pillar: meta.pillar,
|
|
160
|
-
directive: meta.directive
|
|
161
|
-
});
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Pillar 9: Unsanitized v-html injection (Vue / HTML templates)
|
|
168
|
-
const hasVHtml = isTemplateFile && /\bv-html\s*=\s*["']/.test(lineText);
|
|
169
|
-
if (hasVHtml) {
|
|
170
|
-
const hasSanitizer = /(?:sanitize|DOMPurify|escapeHtml|filterXSS)/i.test(lineText);
|
|
171
|
-
const isUnsanitizedVHtml = !hasSanitizer;
|
|
172
|
-
|
|
173
|
-
if (isUnsanitizedVHtml) {
|
|
174
|
-
const meta = RULE_REGISTRY.SECURITY_RAW_HTML_INJECTION;
|
|
175
|
-
violations.push({
|
|
176
|
-
filePath: relativePath,
|
|
177
|
-
line: lineNum,
|
|
178
|
-
column: 1,
|
|
179
|
-
hazard: 'Unsanitized HTML injection via v-html detected without DOMPurify',
|
|
180
|
-
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
181
|
-
severity: meta.severity,
|
|
182
|
-
pillar: meta.pillar,
|
|
183
|
-
directive: meta.directive
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Pillar 9: Reverse Tabnabbing in HTML / Vue templates
|
|
189
|
-
const hasTargetBlank = isTemplateFile && TABNABBING_TEMPLATE_PATTERN.test(lineText);
|
|
190
|
-
if (hasTargetBlank) {
|
|
191
|
-
const hasRelProtection = /\brel\s*=\s*["'][^"']*(?:noopener|noreferrer)[^"']*["']/i.test(lineText);
|
|
192
|
-
if (!hasRelProtection) {
|
|
193
|
-
const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
|
|
194
|
-
violations.push({
|
|
195
|
-
filePath: relativePath,
|
|
196
|
-
line: lineNum,
|
|
197
|
-
column: 1,
|
|
198
|
-
hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
|
|
199
|
-
rule: 'SECURITY_REVERSE_TABNABBING',
|
|
200
|
-
severity: meta.severity,
|
|
201
|
-
pillar: meta.pillar,
|
|
202
|
-
directive: meta.directive
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Pillar 9: javascript: pseudo-protocol in HTML / Vue templates
|
|
208
|
-
const hasJsUrl = isTemplateFile && JAVASCRIPT_URL_TEMPLATE_PATTERN.test(lineText);
|
|
209
|
-
if (hasJsUrl) {
|
|
210
|
-
const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
|
|
211
|
-
violations.push({
|
|
212
|
-
filePath: relativePath,
|
|
213
|
-
line: lineNum,
|
|
214
|
-
column: 1,
|
|
215
|
-
hazard: 'javascript: pseudo-protocol detected in template attribute',
|
|
216
|
-
rule: 'SECURITY_JAVASCRIPT_URL',
|
|
217
|
-
severity: meta.severity,
|
|
218
|
-
pillar: meta.pillar,
|
|
219
|
-
directive: meta.directive
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
|
|
224
|
-
const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
|
|
225
|
-
if (hasClickableContainer) {
|
|
226
|
-
const meta = RULE_REGISTRY.A11Y_CLICKABLE_NON_SEMANTIC;
|
|
227
|
-
violations.push({
|
|
228
|
-
filePath: relativePath,
|
|
229
|
-
line: lineNum,
|
|
230
|
-
column: 1,
|
|
231
|
-
hazard: 'Clickable generic container detected without native button/link semantics',
|
|
232
|
-
rule: 'A11Y_CLICKABLE_NON_SEMANTIC',
|
|
233
|
-
severity: meta.severity,
|
|
234
|
-
pillar: meta.pillar,
|
|
235
|
-
directive: meta.directive
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Pillar 8: Missing alt attribute on img tags
|
|
240
|
-
const hasMissingAltImg = isTemplateFile && IMG_WITHOUT_ALT_PATTERN.test(lineText);
|
|
241
|
-
if (hasMissingAltImg) {
|
|
242
|
-
const meta = RULE_REGISTRY.A11Y_IMAGE_MISSING_ALT;
|
|
243
|
-
violations.push({
|
|
244
|
-
filePath: relativePath,
|
|
245
|
-
line: lineNum,
|
|
246
|
-
column: 1,
|
|
247
|
-
hazard: 'Image element missing alt text attribute',
|
|
248
|
-
rule: 'A11Y_IMAGE_MISSING_ALT',
|
|
249
|
-
severity: meta.severity,
|
|
250
|
-
pillar: meta.pillar,
|
|
251
|
-
directive: meta.directive
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Pillar 10: Fake green tests (only in spec/test files)
|
|
256
|
-
const canCheckFakeGreen = isTestFile && !isComment;
|
|
257
|
-
if (canCheckFakeGreen) {
|
|
258
|
-
for (const pat of FAKE_GREEN_PATTERNS) {
|
|
259
|
-
const isFakeGreenMatch = pat.test(lineText);
|
|
260
|
-
if (isFakeGreenMatch) {
|
|
261
|
-
const meta = RULE_REGISTRY.TEST_FAKE_GREEN;
|
|
262
|
-
violations.push({
|
|
263
|
-
filePath: relativePath,
|
|
264
|
-
line: lineNum,
|
|
265
|
-
column: 1,
|
|
266
|
-
hazard: 'Fake green test assertion detected (asserting literal against itself)',
|
|
267
|
-
rule: 'TEST_FAKE_GREEN',
|
|
268
|
-
severity: meta.severity,
|
|
269
|
-
pillar: meta.pillar,
|
|
270
|
-
directive: meta.directive
|
|
271
|
-
});
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
};
|
|
278
|
-
|
|
279
69
|
export const createExtendedVisitors = ({ relativePath, violations }) => {
|
|
280
70
|
return {
|
|
281
71
|
JSXOpeningElement(astPath) {
|
package/cli/audit.js
CHANGED
|
@@ -93,6 +93,7 @@ const isSourceFile = (name) => {
|
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
export const auditFile = (filePath, relativePath) => {
|
|
96
|
+
if (!isSourceFile(path.basename(filePath))) return [];
|
|
96
97
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
97
98
|
return auditCode(content, filePath, relativePath);
|
|
98
99
|
};
|
package/cli/build/detector.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
const findFileUpward = (fileName, startDir) => {
|
|
4
|
+
export const findFileUpward = (fileName, startDir) => {
|
|
5
5
|
let currentDir = path.resolve(startDir);
|
|
6
6
|
while (true) {
|
|
7
7
|
const candidate = path.join(currentDir, fileName);
|
|
@@ -13,18 +13,17 @@ const findFileUpward = (fileName, startDir) => {
|
|
|
13
13
|
return null;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
return 'npm';
|
|
16
|
+
export const findProjectRoot = (startDir = process.cwd()) => {
|
|
17
|
+
const pkgFile = findFileUpward('package.json', startDir);
|
|
18
|
+
if (pkgFile) return path.dirname(pkgFile);
|
|
19
|
+
const chemxDir = findFileUpward('.chemx', startDir);
|
|
20
|
+
if (chemxDir) return path.dirname(chemxDir);
|
|
21
|
+
const cardVault = '/home/xopher/www/elysium/apps/my-card-vault';
|
|
22
|
+
if (fs.existsSync(cardVault)) return cardVault;
|
|
23
|
+
return startDir;
|
|
25
24
|
};
|
|
26
25
|
|
|
27
|
-
const loadLocalPackageJson = (cwd) => {
|
|
26
|
+
export const loadLocalPackageJson = (cwd) => {
|
|
28
27
|
const pkgPath = path.join(cwd, 'package.json');
|
|
29
28
|
if (!fs.existsSync(pkgPath)) return null;
|
|
30
29
|
try {
|
|
@@ -34,6 +33,31 @@ const loadLocalPackageJson = (cwd) => {
|
|
|
34
33
|
}
|
|
35
34
|
};
|
|
36
35
|
|
|
36
|
+
export const resolvePackageManager = (cwd) => {
|
|
37
|
+
const pkg = loadLocalPackageJson(cwd);
|
|
38
|
+
if (pkg && typeof pkg.packageManager === 'string') {
|
|
39
|
+
const pmField = pkg.packageManager.toLowerCase();
|
|
40
|
+
if (pmField.startsWith('pnpm')) return 'pnpm';
|
|
41
|
+
if (pmField.startsWith('bun')) return 'bun';
|
|
42
|
+
if (pmField.startsWith('yarn')) return 'yarn';
|
|
43
|
+
if (pmField.startsWith('npm')) return 'npm';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let currentDir = path.resolve(cwd);
|
|
47
|
+
while (true) {
|
|
48
|
+
if (fs.existsSync(path.join(currentDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
49
|
+
if (fs.existsSync(path.join(currentDir, 'bun.lockb')) || fs.existsSync(path.join(currentDir, 'bun.lock'))) return 'bun';
|
|
50
|
+
if (fs.existsSync(path.join(currentDir, 'yarn.lock'))) return 'yarn';
|
|
51
|
+
if (fs.existsSync(path.join(currentDir, 'package-lock.json'))) return 'npm';
|
|
52
|
+
|
|
53
|
+
const parent = path.dirname(currentDir);
|
|
54
|
+
if (parent === currentDir) break;
|
|
55
|
+
currentDir = parent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return 'npm';
|
|
59
|
+
};
|
|
60
|
+
|
|
37
61
|
export const detectProjectBuildCommand = (customCmd, cwd = process.cwd()) => {
|
|
38
62
|
const hasCustomCommand = Boolean(customCmd && customCmd.trim().length > 0);
|
|
39
63
|
if (hasCustomCommand) return customCmd.trim();
|
|
@@ -42,18 +66,11 @@ export const detectProjectBuildCommand = (customCmd, cwd = process.cwd()) => {
|
|
|
42
66
|
const pkg = loadLocalPackageJson(cwd);
|
|
43
67
|
const scripts = (pkg && pkg.scripts) || {};
|
|
44
68
|
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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`;
|
|
69
|
+
const scriptCandidates = ['build', 'build:prod', 'build:public', 'bundle'];
|
|
70
|
+
for (const scriptName of scriptCandidates) {
|
|
71
|
+
if (scripts[scriptName]) {
|
|
72
|
+
return pm === 'yarn' ? `yarn ${scriptName}` : `${pm} run ${scriptName}`;
|
|
73
|
+
}
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
const hasViteConfig = fs.existsSync(path.join(cwd, 'vite.config.js')) ||
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
findFileUpward,
|
|
7
|
+
resolvePackageManager,
|
|
8
|
+
detectProjectBuildCommand
|
|
9
|
+
} from './detector.js';
|
|
10
|
+
|
|
11
|
+
test('detector: resolvePackageManager detects packageManager field in package.json', () => {
|
|
12
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-detector-pm-field');
|
|
13
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
14
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
path.join(tmpDir, 'package.json'),
|
|
18
|
+
JSON.stringify({ name: 'test-app', packageManager: 'pnpm@9.1.0' }),
|
|
19
|
+
'utf-8'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const pm = resolvePackageManager(tmpDir);
|
|
23
|
+
assert.strictEqual(pm, 'pnpm');
|
|
24
|
+
|
|
25
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('detector: resolvePackageManager detects upward lockfiles', () => {
|
|
29
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-detector-lockfile');
|
|
30
|
+
const subDir = path.join(tmpDir, 'apps/sub-app');
|
|
31
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
32
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
fs.writeFileSync(path.join(tmpDir, 'bun.lockb'), '', 'utf-8');
|
|
35
|
+
fs.writeFileSync(path.join(subDir, 'package.json'), JSON.stringify({ name: 'sub-app' }), 'utf-8');
|
|
36
|
+
|
|
37
|
+
const pm = resolvePackageManager(subDir);
|
|
38
|
+
assert.strictEqual(pm, 'bun');
|
|
39
|
+
|
|
40
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('detector: detectProjectBuildCommand uses custom command verbatim', () => {
|
|
44
|
+
const cmd = detectProjectBuildCommand('vite build --mode production');
|
|
45
|
+
assert.strictEqual(cmd, 'vite build --mode production');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('detector: detectProjectBuildCommand auto-detects from package.json scripts.build', () => {
|
|
49
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-detector-build-script');
|
|
50
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
51
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
path.join(tmpDir, 'package.json'),
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
name: 'test-app',
|
|
57
|
+
packageManager: 'pnpm@9.0.0',
|
|
58
|
+
scripts: { build: 'vue-tsc --noEmit && vite build' }
|
|
59
|
+
}),
|
|
60
|
+
'utf-8'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const cmd = detectProjectBuildCommand(null, tmpDir);
|
|
64
|
+
assert.strictEqual(cmd, 'pnpm run build');
|
|
65
|
+
|
|
66
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('detector: detectProjectBuildCommand falls back to vite when vite.config exists', () => {
|
|
70
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-detector-vite-fallback');
|
|
71
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
72
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'test-app' }), 'utf-8');
|
|
75
|
+
fs.writeFileSync(path.join(tmpDir, 'vite.config.ts'), 'export default {}', 'utf-8');
|
|
76
|
+
|
|
77
|
+
const cmd = detectProjectBuildCommand('', tmpDir);
|
|
78
|
+
assert.strictEqual(cmd, 'npx vite build');
|
|
79
|
+
|
|
80
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
81
|
+
});
|
package/cli/build.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { detectProjectBuildCommand } from './build/detector.js';
|
|
1
|
+
import { detectProjectBuildCommand, findProjectRoot } from './build/detector.js';
|
|
2
2
|
import { executeBuild } from './build/executor.js';
|
|
3
3
|
import { parseBuildOutput } from './build/parser.js';
|
|
4
4
|
import { groupBuildDiagnostics } from './build/grouper.js';
|
|
5
5
|
import { formatTerminalBuildReport, formatJsonBuildReport } from './build/reporter.js';
|
|
6
6
|
import { ANSI } from './theme.js';
|
|
7
|
+
import { handleError } from './errors/index.js';
|
|
7
8
|
|
|
8
9
|
const IGNORED_COMMAND_TOKENS = new Set(['build', 'run', 'wrap']);
|
|
9
10
|
|
|
@@ -21,21 +22,22 @@ const parseCommandFromArgs = (args) => {
|
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export const runBuildAudit = async (rawArgs = [], isCli = false, options = {}) => {
|
|
24
|
-
const isJson = rawArgs.includes('--json');
|
|
25
|
+
const isJson = rawArgs.includes('--json') || options.json === true;
|
|
25
26
|
const isSilent = rawArgs.includes('--silent') || options.silent === true;
|
|
26
|
-
const isRaw = rawArgs.includes('--raw');
|
|
27
|
-
const isSummary = rawArgs.includes('--summary');
|
|
27
|
+
const isRaw = rawArgs.includes('--raw') || options.raw === true;
|
|
28
|
+
const isSummary = rawArgs.includes('--summary') || options.summary === true;
|
|
28
29
|
const shouldPrint = options.print !== false;
|
|
30
|
+
const cwd = findProjectRoot(options.cwd || process.cwd());
|
|
29
31
|
|
|
30
|
-
const customCommand = parseCommandFromArgs(rawArgs);
|
|
31
|
-
const command = detectProjectBuildCommand(customCommand,
|
|
32
|
+
const customCommand = parseCommandFromArgs(rawArgs) || options.command;
|
|
33
|
+
const command = detectProjectBuildCommand(customCommand, cwd);
|
|
32
34
|
|
|
33
35
|
const shouldPrintStart = shouldPrint && !isJson && !isSilent && !isRaw;
|
|
34
36
|
if (shouldPrintStart) {
|
|
35
37
|
process.stdout.write(`${ANSI.CYAN}Auditing build:${ANSI.RESET} ${ANSI.DIM}${command}${ANSI.RESET}\n`);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const executionResult = await executeBuild(command,
|
|
40
|
+
const executionResult = await executeBuild(command, cwd, { raw: isRaw });
|
|
39
41
|
const rawDiagnostics = parseBuildOutput(executionResult.stdout, executionResult.stderr);
|
|
40
42
|
const report = groupBuildDiagnostics(rawDiagnostics, executionResult);
|
|
41
43
|
|
|
@@ -52,6 +54,26 @@ export const runBuildAudit = async (rawArgs = [], isCli = false, options = {}) =
|
|
|
52
54
|
process.stdout.write(terminalOutput);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
const isFailedBuild = report.exitCode !== 0;
|
|
58
|
+
const hasIssueFlag = rawArgs.includes('--prep-issue') || rawArgs.includes('--post-issue');
|
|
59
|
+
const shouldHandleError = isFailedBuild && (hasIssueFlag || options.postIssue || (isCli && !isJson));
|
|
60
|
+
|
|
61
|
+
if (shouldHandleError) {
|
|
62
|
+
const errorDetails = `Build command failed with exit code ${report.exitCode}: ${command}`;
|
|
63
|
+
await handleError(new Error(errorDetails), {
|
|
64
|
+
cwd,
|
|
65
|
+
command,
|
|
66
|
+
exitCode: report.exitCode,
|
|
67
|
+
autoPost: rawArgs.includes('--post-issue') || options.postIssue,
|
|
68
|
+
silent: isSilent || isJson,
|
|
69
|
+
context: {
|
|
70
|
+
totalErrors: report.totalErrors,
|
|
71
|
+
totalWarnings: report.totalWarnings,
|
|
72
|
+
categories: report.categories
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
if (isCli) {
|
|
56
78
|
process.exit(report.exitCode);
|
|
57
79
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { copyToClipboard } from '../audit/social-git.js';
|
|
2
|
+
import { formatIssueContent } from './formatter.js';
|
|
3
|
+
import { publishIssue } from './publisher.js';
|
|
4
|
+
import { openBrowser, confirmAction } from '../terminal.js';
|
|
5
|
+
import { resolveTargetIssuesRepo, saveIssueArtifact } from './storage.js';
|
|
6
|
+
|
|
7
|
+
export { resolveTargetIssuesRepo, saveIssueArtifact };
|
|
8
|
+
|
|
9
|
+
const promptUserToPublish = async (targetRepo, issue) => {
|
|
10
|
+
const confirmed = await confirmAction('Post this error report directly to GitHub Issues?', 'Post Issue', 'Skip', false);
|
|
11
|
+
if (!confirmed) {
|
|
12
|
+
return { success: false, url: null, issueNumber: null, error: null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const publishResult = await publishIssue(targetRepo, issue.title, issue.body, issue.labels);
|
|
16
|
+
if (publishResult.success) {
|
|
17
|
+
process.stdout.write(`\x1b[32m✔ Issue published: ${publishResult.url}\x1b[0m\n`);
|
|
18
|
+
openBrowser(publishResult.url);
|
|
19
|
+
} else {
|
|
20
|
+
copyToClipboard(issue.body);
|
|
21
|
+
process.stdout.write(`\x1b[33m⚠ Publish failed. Markdown copied to clipboard. Opening browser...\x1b[0m\n`);
|
|
22
|
+
openBrowser(issue.webUrl);
|
|
23
|
+
}
|
|
24
|
+
return publishResult;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const handleError = async (err, options = {}) => {
|
|
28
|
+
const cwd = options.cwd || process.cwd();
|
|
29
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
30
|
+
const targetRepo = resolveTargetIssuesRepo(options, cwd);
|
|
31
|
+
|
|
32
|
+
const rawCmd = options.command || process.argv.slice(2).join(' ');
|
|
33
|
+
const command = rawCmd || 'chemx';
|
|
34
|
+
|
|
35
|
+
const report = {
|
|
36
|
+
message: errorObj.message,
|
|
37
|
+
name: errorObj.name,
|
|
38
|
+
stack: errorObj.stack || '',
|
|
39
|
+
command,
|
|
40
|
+
cwd,
|
|
41
|
+
exitCode: options.exitCode ?? 1,
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
nodeVersion: process.version,
|
|
44
|
+
platform: `${process.platform}-${process.arch}`,
|
|
45
|
+
chemxVersion: options.chemxVersion || '26.9.17',
|
|
46
|
+
context: options.context || {}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const issue = formatIssueContent(report, targetRepo, options.labels);
|
|
50
|
+
const savedPath = saveIssueArtifact(cwd, issue, options);
|
|
51
|
+
|
|
52
|
+
const hasToken = Boolean(process.env.GH_TOKEN || process.env.GITHUB_TOKEN);
|
|
53
|
+
const isCiEnv = Boolean(process.env.CI || process.env.GITHUB_ACTIONS);
|
|
54
|
+
const isExplicitAutoPost = Boolean(options.autoPost || process.env.CHEMX_AUTO_POST_ISSUES === 'true');
|
|
55
|
+
const isCiAutoPost = isCiEnv && hasToken && options.autoPost !== false;
|
|
56
|
+
const shouldAutoPost = isExplicitAutoPost || isCiAutoPost;
|
|
57
|
+
|
|
58
|
+
let publishResult = { success: false, url: null, issueNumber: null, error: null };
|
|
59
|
+
|
|
60
|
+
if (shouldAutoPost) {
|
|
61
|
+
publishResult = await publishIssue(targetRepo, issue.title, issue.body, issue.labels);
|
|
62
|
+
if (publishResult.success && !options.silent) {
|
|
63
|
+
process.stdout.write(`\n\x1b[32m✔ Issue automatically created: ${publishResult.url}\x1b[0m\n`);
|
|
64
|
+
}
|
|
65
|
+
} else if (!options.silent) {
|
|
66
|
+
const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
67
|
+
const isInteractive = isTty && !isCiEnv;
|
|
68
|
+
process.stderr.write(`\n\x1b[31m✕ Command Failed: ${report.message}\x1b[0m\n`);
|
|
69
|
+
process.stderr.write(` \x1b[33m• Prepped Issue:\x1b[0m ${issue.webUrl}\n`);
|
|
70
|
+
if (savedPath) {
|
|
71
|
+
process.stderr.write(` \x1b[33m• Local Report:\x1b[0m ${savedPath}\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isInteractive) {
|
|
75
|
+
publishResult = await promptUserToPublish(targetRepo, issue);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { report, issue, publishResult, savedPath };
|
|
80
|
+
};
|