@chemx/starter-kit 26.9.17-1100 → 26.9.17-1437

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.
@@ -0,0 +1,218 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { RULE_REGISTRY } from './rules-registry.js';
4
+ import { toResultSync } from './rules-helpers.js';
5
+ import {
6
+ isComponentExtension,
7
+ isTemplateExtension,
8
+ isCommentLine
9
+ } from './rules-predicates.js';
10
+
11
+ const SECRET_PATTERNS = [
12
+ /(['"])(?:sk-[a-zA-Z0-9_-]{24,}|ghp_[a-zA-Z0-9]{30,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z-_]{35})\1/
13
+ ];
14
+
15
+ const FAKE_GREEN_PATTERNS = [
16
+ /expect\s*\(\s*(true|false|1|0)\s*\)\s*\.\s*to(?:Be|Equal|StrictEqual)\s*\(\s*\1\s*\)/,
17
+ /assert\s*\.\s*(?:strictEqual|equal|deepEqual)\s*\(\s*(true|false|1|0)\s*,\s*\1\s*\)/,
18
+ /assert\s*\.\s*ok\s*\(\s*(?:true|1)\s*\)/
19
+ ];
20
+
21
+ const IMG_WITHOUT_ALT_PATTERN = new RegExp(['<', 'img\\b', '(?![^>]*\\balt\\s*=)', '[^>]*>'].join(''), 'i');
22
+ const CLICKABLE_CONTAINER_PATTERN = new RegExp(['<', '(div|span)\\b', '(?![^>]*\\brole\\s*=\\s*[\'"](?:button|link|tab)[\'"])', '[^>]*\\b(?:@click|v-on:click)\\s*='].join(''), 'i');
23
+ const TABNABBING_TEMPLATE_PATTERN = /<a\b[^>]*\btarget\s*=\s*["']_blank["'][^>]*>/i;
24
+ const JAVASCRIPT_URL_TEMPLATE_PATTERN = /\b(?:href|src|action|formaction)\s*=\s*["']\s*javascript:/i;
25
+
26
+ /**
27
+ * Pillar 10: Check for co-located test files for molecule capsules.
28
+ */
29
+ export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
30
+ const baseName = path.basename(filePath);
31
+ const ext = path.extname(filePath);
32
+
33
+ // Stage 1: Atomic Concept Declarations
34
+ const isMoleculePath = relativePath.includes('molecules') || baseName.startsWith('m-');
35
+ const isComponentExt = isComponentExtension(ext);
36
+ const isTestOrSpecFile = baseName.includes('.spec.') || baseName.includes('.test.');
37
+
38
+ // Stage 2: Unified Decision Variable & Early Guard Clause
39
+ const isCandidateComponent = isMoleculePath && isComponentExt;
40
+ const isMoleculeComponent = isCandidateComponent && !isTestOrSpecFile;
41
+ if (!isMoleculeComponent) return;
42
+
43
+ const dir = path.dirname(filePath);
44
+ const [siblings, dirError] = toResultSync(() => (fs.existsSync(dir) ? fs.readdirSync(dir) : []));
45
+
46
+ // Stage 1: Atomic Guard Check
47
+ const hasDirError = Boolean(dirError);
48
+ const hasSiblings = Boolean(siblings);
49
+ const canInspectSiblings = !hasDirError && hasSiblings;
50
+ if (!canInspectSiblings) return;
51
+
52
+ const isTestSibling = (fileName) => /\.(test|spec)\.[jt]sx?$/.test(fileName);
53
+ const hasCoLocatedTest = siblings.some(isTestSibling);
54
+ if (hasCoLocatedTest) return;
55
+
56
+ const meta = RULE_REGISTRY.TEST_MISSING_COLOCATED;
57
+ violations.push({
58
+ filePath: relativePath,
59
+ line: 1,
60
+ column: 1,
61
+ hazard: 'Molecule capsule missing co-located test file (*.spec.ts or *.test.ts)',
62
+ rule: 'TEST_MISSING_COLOCATED',
63
+ severity: meta.severity,
64
+ pillar: meta.pillar,
65
+ directive: meta.directive
66
+ });
67
+ };
68
+
69
+ export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
70
+ checkMoleculeCoLocatedTest(filePath, relativePath, violations);
71
+
72
+ const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath);
73
+ const ext = path.extname(filePath);
74
+ const isTemplateFile = isTemplateExtension(ext);
75
+
76
+ lines.forEach((lineText, idx) => {
77
+ const lineNum = idx + 1;
78
+ const trimmed = lineText.trim();
79
+ const isComment = isCommentLine(trimmed);
80
+
81
+ // Pillar 9: Hardcoded Secret Detection
82
+ const hasEnvReference = trimmed.includes('process.env') || trimmed.includes('import.meta.env');
83
+ const canScanSecrets = !isComment && !hasEnvReference;
84
+
85
+ if (canScanSecrets) {
86
+ for (const pat of SECRET_PATTERNS) {
87
+ const matchesSecretPattern = pat.test(lineText);
88
+ const hasPlaceholder = lineText.includes('dummy') || lineText.includes('placeholder');
89
+ const isHardcodedSecret = matchesSecretPattern && !hasPlaceholder;
90
+
91
+ if (isHardcodedSecret) {
92
+ const meta = RULE_REGISTRY.SECURITY_HARDCODED_SECRET;
93
+ violations.push({
94
+ filePath: relativePath,
95
+ line: lineNum,
96
+ column: 1,
97
+ hazard: 'High-entropy API key or secret token detected in source code',
98
+ rule: 'SECURITY_HARDCODED_SECRET',
99
+ severity: meta.severity,
100
+ pillar: meta.pillar,
101
+ directive: meta.directive
102
+ });
103
+ break;
104
+ }
105
+ }
106
+ }
107
+
108
+ // Pillar 9: Unsanitized v-html injection (Vue / HTML templates)
109
+ const hasVHtml = isTemplateFile && /\bv-html\s*=\s*["']/.test(lineText);
110
+ if (hasVHtml) {
111
+ const hasSanitizer = /(?:sanitize|DOMPurify|escapeHtml|filterXSS)/i.test(lineText);
112
+ const isUnsanitizedVHtml = !hasSanitizer;
113
+
114
+ if (isUnsanitizedVHtml) {
115
+ const meta = RULE_REGISTRY.SECURITY_RAW_HTML_INJECTION;
116
+ violations.push({
117
+ filePath: relativePath,
118
+ line: lineNum,
119
+ column: 1,
120
+ hazard: 'Unsanitized HTML injection via v-html detected without DOMPurify',
121
+ rule: 'SECURITY_RAW_HTML_INJECTION',
122
+ severity: meta.severity,
123
+ pillar: meta.pillar,
124
+ directive: meta.directive
125
+ });
126
+ }
127
+ }
128
+
129
+ // Pillar 9: Reverse Tabnabbing in HTML / Vue templates
130
+ const hasTargetBlank = isTemplateFile && TABNABBING_TEMPLATE_PATTERN.test(lineText);
131
+ if (hasTargetBlank) {
132
+ const hasRelProtection = /\brel\s*=\s*["'][^"']*(?:noopener|noreferrer)[^"']*["']/i.test(lineText);
133
+ if (!hasRelProtection) {
134
+ const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
135
+ violations.push({
136
+ filePath: relativePath,
137
+ line: lineNum,
138
+ column: 1,
139
+ hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
140
+ rule: 'SECURITY_REVERSE_TABNABBING',
141
+ severity: meta.severity,
142
+ pillar: meta.pillar,
143
+ directive: meta.directive
144
+ });
145
+ }
146
+ }
147
+
148
+ // Pillar 9: javascript: pseudo-protocol in HTML / Vue templates
149
+ const hasJsUrl = isTemplateFile && JAVASCRIPT_URL_TEMPLATE_PATTERN.test(lineText);
150
+ if (hasJsUrl) {
151
+ const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
152
+ violations.push({
153
+ filePath: relativePath,
154
+ line: lineNum,
155
+ column: 1,
156
+ hazard: 'javascript: pseudo-protocol detected in template attribute',
157
+ rule: 'SECURITY_JAVASCRIPT_URL',
158
+ severity: meta.severity,
159
+ pillar: meta.pillar,
160
+ directive: meta.directive
161
+ });
162
+ }
163
+
164
+ // Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
165
+ const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
166
+ if (hasClickableContainer) {
167
+ const meta = RULE_REGISTRY.A11Y_CLICKABLE_NON_SEMANTIC;
168
+ violations.push({
169
+ filePath: relativePath,
170
+ line: lineNum,
171
+ column: 1,
172
+ hazard: 'Clickable generic container detected without native button/link semantics',
173
+ rule: 'A11Y_CLICKABLE_NON_SEMANTIC',
174
+ severity: meta.severity,
175
+ pillar: meta.pillar,
176
+ directive: meta.directive
177
+ });
178
+ }
179
+
180
+ // Pillar 8: Missing alt attribute on img tags
181
+ const hasMissingAltImg = isTemplateFile && IMG_WITHOUT_ALT_PATTERN.test(lineText);
182
+ if (hasMissingAltImg) {
183
+ const meta = RULE_REGISTRY.A11Y_IMAGE_MISSING_ALT;
184
+ violations.push({
185
+ filePath: relativePath,
186
+ line: lineNum,
187
+ column: 1,
188
+ hazard: 'Image element missing alt text attribute',
189
+ rule: 'A11Y_IMAGE_MISSING_ALT',
190
+ severity: meta.severity,
191
+ pillar: meta.pillar,
192
+ directive: meta.directive
193
+ });
194
+ }
195
+
196
+ // Pillar 10: Fake green tests (only in spec/test files)
197
+ const canCheckFakeGreen = isTestFile && !isComment;
198
+ if (canCheckFakeGreen) {
199
+ for (const pat of FAKE_GREEN_PATTERNS) {
200
+ const isFakeGreenMatch = pat.test(lineText);
201
+ if (isFakeGreenMatch) {
202
+ const meta = RULE_REGISTRY.TEST_FAKE_GREEN;
203
+ violations.push({
204
+ filePath: relativePath,
205
+ line: lineNum,
206
+ column: 1,
207
+ hazard: 'Fake green test assertion detected (asserting literal against itself)',
208
+ rule: 'TEST_FAKE_GREEN',
209
+ severity: meta.severity,
210
+ pillar: meta.pillar,
211
+ directive: meta.directive
212
+ });
213
+ break;
214
+ }
215
+ }
216
+ }
217
+ });
218
+ };
@@ -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 { toResultSync } from './rules-helpers.js';
6
- import {
7
- isComponentExtension,
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,6 @@ const isSensitiveName = (name = '') => {
47
29
  };
48
30
 
49
31
  const JAVASCRIPT_URL_REGEX = /^\s*javascript:/i;
50
- const TABNABBING_TEMPLATE_PATTERN = /<a\b[^>]*\btarget\s*=\s*["']_blank["'][^>]*>/i;
51
- const JAVASCRIPT_URL_TEMPLATE_PATTERN = /\b(?:href|src|action|formaction)\s*=\s*["']\s*javascript:/i;
52
32
 
53
33
  const resolvePropertyKeyName = (key) => {
54
34
  const isIdentifier = t.isIdentifier(key);
@@ -86,200 +66,6 @@ const isSensitiveExpression = (node) => {
86
66
  return false;
87
67
  };
88
68
 
89
- /**
90
- * Pillar 10: Check for co-located test files for molecule capsules.
91
- */
92
- const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
93
- const baseName = path.basename(filePath);
94
- const ext = path.extname(filePath);
95
-
96
- // Stage 1: Atomic Concept Declarations
97
- const isMoleculePath = relativePath.includes('molecules') || baseName.startsWith('m-');
98
- const isComponentExt = isComponentExtension(ext);
99
- const isTestOrSpecFile = baseName.includes('.spec.') || baseName.includes('.test.');
100
-
101
- // Stage 2: Unified Decision Variable & Early Guard Clause
102
- const isCandidateComponent = isMoleculePath && isComponentExt;
103
- const isMoleculeComponent = isCandidateComponent && !isTestOrSpecFile;
104
- if (!isMoleculeComponent) return;
105
-
106
- const dir = path.dirname(filePath);
107
- const [siblings, dirError] = toResultSync(() => (fs.existsSync(dir) ? fs.readdirSync(dir) : []));
108
-
109
- // Stage 1: Atomic Guard Check
110
- const hasDirError = Boolean(dirError);
111
- const hasSiblings = Boolean(siblings);
112
- const canInspectSiblings = !hasDirError && hasSiblings;
113
- if (!canInspectSiblings) return;
114
-
115
- const isTestSibling = (fileName) => /\.(test|spec)\.[jt]sx?$/.test(fileName);
116
- const hasCoLocatedTest = siblings.some(isTestSibling);
117
- if (hasCoLocatedTest) return;
118
-
119
- const meta = RULE_REGISTRY.TEST_MISSING_COLOCATED;
120
- violations.push({
121
- filePath: relativePath,
122
- line: 1,
123
- column: 1,
124
- hazard: 'Molecule capsule missing co-located test file (*.spec.ts or *.test.ts)',
125
- rule: 'TEST_MISSING_COLOCATED',
126
- severity: meta.severity,
127
- pillar: meta.pillar,
128
- directive: meta.directive
129
- });
130
- };
131
-
132
- export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
133
- checkMoleculeCoLocatedTest(filePath, relativePath, violations);
134
-
135
- const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath);
136
- const ext = path.extname(filePath);
137
- const isTemplateFile = isTemplateExtension(ext);
138
-
139
- lines.forEach((lineText, idx) => {
140
- const lineNum = idx + 1;
141
- const trimmed = lineText.trim();
142
- const isComment = isCommentLine(trimmed);
143
-
144
- // Pillar 9: Hardcoded Secret Detection
145
- const hasEnvReference = trimmed.includes('process.env') || trimmed.includes('import.meta.env');
146
- const canScanSecrets = !isComment && !hasEnvReference;
147
-
148
- if (canScanSecrets) {
149
- for (const pat of SECRET_PATTERNS) {
150
- const matchesSecretPattern = pat.test(lineText);
151
- const hasPlaceholder = lineText.includes('dummy') || lineText.includes('placeholder');
152
- const isHardcodedSecret = matchesSecretPattern && !hasPlaceholder;
153
-
154
- if (isHardcodedSecret) {
155
- const meta = RULE_REGISTRY.SECURITY_HARDCODED_SECRET;
156
- violations.push({
157
- filePath: relativePath,
158
- line: lineNum,
159
- column: 1,
160
- hazard: 'High-entropy API key or secret token detected in source code',
161
- rule: 'SECURITY_HARDCODED_SECRET',
162
- severity: meta.severity,
163
- pillar: meta.pillar,
164
- directive: meta.directive
165
- });
166
- break;
167
- }
168
- }
169
- }
170
-
171
- // Pillar 9: Unsanitized v-html injection (Vue / HTML templates)
172
- const hasVHtml = isTemplateFile && /\bv-html\s*=\s*["']/.test(lineText);
173
- if (hasVHtml) {
174
- const hasSanitizer = /(?:sanitize|DOMPurify|escapeHtml|filterXSS)/i.test(lineText);
175
- const isUnsanitizedVHtml = !hasSanitizer;
176
-
177
- if (isUnsanitizedVHtml) {
178
- const meta = RULE_REGISTRY.SECURITY_RAW_HTML_INJECTION;
179
- violations.push({
180
- filePath: relativePath,
181
- line: lineNum,
182
- column: 1,
183
- hazard: 'Unsanitized HTML injection via v-html detected without DOMPurify',
184
- rule: 'SECURITY_RAW_HTML_INJECTION',
185
- severity: meta.severity,
186
- pillar: meta.pillar,
187
- directive: meta.directive
188
- });
189
- }
190
- }
191
-
192
- // Pillar 9: Reverse Tabnabbing in HTML / Vue templates
193
- const hasTargetBlank = isTemplateFile && TABNABBING_TEMPLATE_PATTERN.test(lineText);
194
- if (hasTargetBlank) {
195
- const hasRelProtection = /\brel\s*=\s*["'][^"']*(?:noopener|noreferrer)[^"']*["']/i.test(lineText);
196
- if (!hasRelProtection) {
197
- const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
198
- violations.push({
199
- filePath: relativePath,
200
- line: lineNum,
201
- column: 1,
202
- hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
203
- rule: 'SECURITY_REVERSE_TABNABBING',
204
- severity: meta.severity,
205
- pillar: meta.pillar,
206
- directive: meta.directive
207
- });
208
- }
209
- }
210
-
211
- // Pillar 9: javascript: pseudo-protocol in HTML / Vue templates
212
- const hasJsUrl = isTemplateFile && JAVASCRIPT_URL_TEMPLATE_PATTERN.test(lineText);
213
- if (hasJsUrl) {
214
- const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
215
- violations.push({
216
- filePath: relativePath,
217
- line: lineNum,
218
- column: 1,
219
- hazard: 'javascript: pseudo-protocol detected in template attribute',
220
- rule: 'SECURITY_JAVASCRIPT_URL',
221
- severity: meta.severity,
222
- pillar: meta.pillar,
223
- directive: meta.directive
224
- });
225
- }
226
-
227
- // Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
228
- const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
229
- if (hasClickableContainer) {
230
- const meta = RULE_REGISTRY.A11Y_CLICKABLE_NON_SEMANTIC;
231
- violations.push({
232
- filePath: relativePath,
233
- line: lineNum,
234
- column: 1,
235
- hazard: 'Clickable generic container detected without native button/link semantics',
236
- rule: 'A11Y_CLICKABLE_NON_SEMANTIC',
237
- severity: meta.severity,
238
- pillar: meta.pillar,
239
- directive: meta.directive
240
- });
241
- }
242
-
243
- // Pillar 8: Missing alt attribute on img tags
244
- const hasMissingAltImg = isTemplateFile && IMG_WITHOUT_ALT_PATTERN.test(lineText);
245
- if (hasMissingAltImg) {
246
- const meta = RULE_REGISTRY.A11Y_IMAGE_MISSING_ALT;
247
- violations.push({
248
- filePath: relativePath,
249
- line: lineNum,
250
- column: 1,
251
- hazard: 'Image element missing alt text attribute',
252
- rule: 'A11Y_IMAGE_MISSING_ALT',
253
- severity: meta.severity,
254
- pillar: meta.pillar,
255
- directive: meta.directive
256
- });
257
- }
258
-
259
- // Pillar 10: Fake green tests (only in spec/test files)
260
- const canCheckFakeGreen = isTestFile && !isComment;
261
- if (canCheckFakeGreen) {
262
- for (const pat of FAKE_GREEN_PATTERNS) {
263
- const isFakeGreenMatch = pat.test(lineText);
264
- if (isFakeGreenMatch) {
265
- const meta = RULE_REGISTRY.TEST_FAKE_GREEN;
266
- violations.push({
267
- filePath: relativePath,
268
- line: lineNum,
269
- column: 1,
270
- hazard: 'Fake green test assertion detected (asserting literal against itself)',
271
- rule: 'TEST_FAKE_GREEN',
272
- severity: meta.severity,
273
- pillar: meta.pillar,
274
- directive: meta.directive
275
- });
276
- break;
277
- }
278
- }
279
- }
280
- });
281
- };
282
-
283
69
  export const createExtendedVisitors = ({ relativePath, violations }) => {
284
70
  return {
285
71
  JSXOpeningElement(astPath) {
package/cli/build.js CHANGED
@@ -4,6 +4,7 @@ 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
 
@@ -53,6 +54,26 @@ export const runBuildAudit = async (rawArgs = [], isCli = false, options = {}) =
53
54
  process.stdout.write(terminalOutput);
54
55
  }
55
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
+
56
77
  if (isCli) {
57
78
  process.exit(report.exitCode);
58
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);
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
+ };