nothumanallowed 14.5.5 → 14.5.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "14.5.5",
3
+ "version": "14.5.7",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,6 +59,8 @@
59
59
  "prebuild": "cd ../nha-ui && pnpm build"
60
60
  },
61
61
  "dependencies": {
62
+ "acorn": "^8.16.0",
63
+ "acorn-jsx": "^5.3.2",
62
64
  "imapflow": "^1.3.3",
63
65
  "mailparser": "^3.9.8",
64
66
  "ws": "^8.18.0"
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '14.5.5';
8
+ export const VERSION = '14.5.7';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -21,6 +21,8 @@ import { sendJSON, sendError, parseBody, sendSSE } from '../index.mjs';
21
21
  import { loadConfig } from '../../config.mjs';
22
22
  import { callLLM, callLLMStream, fixQwen3BPE } from '../../services/llm.mjs';
23
23
  import { NHA_DIR } from '../../constants.mjs';
24
+ import * as acorn from 'acorn';
25
+ import acornJsx from 'acorn-jsx';
24
26
 
25
27
  const execAsync = promisify(exec);
26
28
 
@@ -664,7 +666,9 @@ WORKFLOW — follow this for every change:
664
666
 
665
667
  RULES:
666
668
  - "old" in edit must be EXACT verbatim code — copy-paste from read output
667
- - Use edit for targeted changes, write for new files or complete rewrites
669
+ - ALWAYS use edit for modifying existing files NEVER rewrite an entire file with write
670
+ - write is ONLY for creating NEW files that don't exist yet
671
+ - If you need to fix a bug, read the file first, then use edit to change only the broken lines
668
672
  - ALWAYS read before edit if you haven't seen the file content
669
673
  - ALWAYS check/lint after modifications
670
674
  - Use run for npm install, npm test, or any shell command
@@ -868,11 +872,15 @@ RULES:
868
872
  toolResults.push({ op: 'write', path: relPath, result: 'missing_content' });
869
873
  emit({ type: 'tool', op: 'write', path: relPath, result: 'missing_content' });
870
874
  } else {
875
+ // Capture previous content for diff
876
+ const prevContent = ProjectStore.readFile(projectName, relPath);
871
877
  ProjectStore.writeFile(projectName, relPath, content);
872
878
  hasChanges = true;
873
879
  modifiedFiles.add(relPath);
874
- toolResults.push({ op: 'write', path: relPath, result: 'ok' });
875
- emit({ type: 'tool', op: 'write', path: relPath, result: 'ok' });
880
+ const oldSnippet = prevContent ? prevContent.slice(0, 500) : '';
881
+ const newSnippet = content.slice(0, 500);
882
+ toolResults.push({ op: 'write', path: relPath, result: 'ok', oldSnippet, newSnippet });
883
+ emit({ type: 'tool', op: 'write', path: relPath, result: 'ok', oldSnippet, newSnippet });
876
884
  }
877
885
 
878
886
  // ── rename ──
@@ -1918,6 +1926,386 @@ export function register(router) {
1918
1926
  });
1919
1927
 
1920
1928
  // ── Diagnostics (lint) — returns errors/warnings for a file ───────────────
1929
+ // ── Advanced Linter — AST-based diagnostics (acorn + scope analysis) ─────────
1930
+
1931
+ const JsxParser = acorn.Parser.extend(acornJsx());
1932
+ const JS_BUILTINS = new Set([
1933
+ 'undefined','NaN','Infinity','globalThis','eval','isFinite','isNaN','parseFloat','parseInt',
1934
+ 'decodeURI','decodeURIComponent','encodeURI','encodeURIComponent',
1935
+ 'Array','ArrayBuffer','BigInt','BigInt64Array','BigUint64Array','Boolean','DataView','Date',
1936
+ 'Error','EvalError','FinalizationRegistry','Float32Array','Float64Array','Function',
1937
+ 'Int8Array','Int16Array','Int32Array','JSON','Map','Math','Number','Object','Promise',
1938
+ 'Proxy','RangeError','ReferenceError','Reflect','RegExp','Set','SharedArrayBuffer',
1939
+ 'String','Symbol','SyntaxError','TypeError','URIError','Uint8Array','Uint8ClampedArray',
1940
+ 'Uint16Array','Uint32Array','WeakMap','WeakRef','WeakSet',
1941
+ 'console','setTimeout','setInterval','clearTimeout','clearInterval','queueMicrotask',
1942
+ 'atob','btoa','fetch','structuredClone','performance','crypto','navigator','location',
1943
+ 'window','document','self','global','process','require','module','exports','__dirname','__filename',
1944
+ 'Buffer','URL','URLSearchParams','TextEncoder','TextDecoder','AbortController','AbortSignal',
1945
+ 'Event','EventTarget','CustomEvent','FormData','Headers','Request','Response',
1946
+ 'ReadableStream','WritableStream','TransformStream','Blob','File','FileReader',
1947
+ 'WebSocket','Worker','SharedWorker','BroadcastChannel','MessageChannel','MessagePort',
1948
+ 'Intl','alert','confirm','prompt','requestAnimationFrame','cancelAnimationFrame',
1949
+ 'MutationObserver','ResizeObserver','IntersectionObserver','PerformanceObserver',
1950
+ 'HTMLElement','Element','Node','NodeList','DocumentFragment',
1951
+ 'localStorage','sessionStorage','history','screen','CSS','CSSStyleSheet',
1952
+ 'XMLHttpRequest','Image','Audio','Video','MediaSource','SourceBuffer',
1953
+ 'Map','Set','WeakMap','WeakSet','Proxy','Reflect',
1954
+ 'arguments','this','super','import','export',
1955
+ ]);
1956
+ const REACT_GLOBALS = new Set([
1957
+ 'React','useState','useEffect','useRef','useCallback','useMemo','useContext',
1958
+ 'useReducer','useLayoutEffect','useImperativeHandle','useDebugValue','useTransition',
1959
+ 'useDeferredValue','useId','useSyncExternalStore','useInsertionEffect',
1960
+ 'createContext','createRef','forwardRef','lazy','memo','startTransition',
1961
+ 'Component','PureComponent','Fragment','StrictMode','Suspense','Profiler',
1962
+ 'createElement','cloneElement','isValidElement','Children',
1963
+ 'jsx','jsxs','jsxDEV',
1964
+ ]);
1965
+ const NODE_MODULES = new Set([
1966
+ 'fs','path','os','http','https','url','util','stream','events','crypto','child_process',
1967
+ 'net','dgram','dns','tls','zlib','readline','cluster','worker_threads','perf_hooks',
1968
+ 'assert','buffer','querystring','string_decoder','timers','v8','vm','inspector',
1969
+ ]);
1970
+
1971
+ function lintJS(content, relPath, projectName) {
1972
+ const diagnostics = [];
1973
+ const ext = relPath.split('.').pop()?.toLowerCase();
1974
+ const isJsx = ext === 'jsx' || ext === 'tsx';
1975
+
1976
+ // 1. Parse with acorn (real AST)
1977
+ let ast;
1978
+ try {
1979
+ ast = JsxParser.parse(content, {
1980
+ ecmaVersion: 'latest',
1981
+ sourceType: 'module',
1982
+ locations: true,
1983
+ allowImportExportEverywhere: true,
1984
+ allowReturnOutsideFunction: true,
1985
+ allowHashBang: true,
1986
+ });
1987
+ } catch (e) {
1988
+ diagnostics.push({
1989
+ from: { line: e.loc?.line || 1, col: e.loc?.column || 0 },
1990
+ severity: 'error',
1991
+ message: e.message.replace(/\(\d+:\d+\)$/, '').trim(),
1992
+ });
1993
+ return diagnostics;
1994
+ }
1995
+
1996
+ // 2. Scope analysis — collect declarations and references
1997
+ const declared = new Set();
1998
+ const imported = new Set();
1999
+ const importSources = [];
2000
+ const references = []; // { name, loc }
2001
+ const exportedNames = new Set();
2002
+
2003
+ function walkNode(node, scope) {
2004
+ if (!node || typeof node !== 'object') return;
2005
+ if (Array.isArray(node)) { node.forEach(n => walkNode(n, scope)); return; }
2006
+ if (!node.type) return;
2007
+
2008
+ const localScope = new Set(scope);
2009
+
2010
+ switch (node.type) {
2011
+ case 'VariableDeclaration':
2012
+ for (const decl of node.declarations) {
2013
+ collectPattern(decl.id, localScope);
2014
+ if (decl.init) walkNode(decl.init, localScope);
2015
+ }
2016
+ // Walk rest of body with the declared vars
2017
+ return;
2018
+
2019
+ case 'FunctionDeclaration':
2020
+ if (node.id) localScope.add(node.id.name);
2021
+ declared.add(node.id?.name);
2022
+ const fnScope = new Set(localScope);
2023
+ for (const p of node.params) collectPattern(p, fnScope);
2024
+ walkNode(node.body, fnScope);
2025
+ return;
2026
+
2027
+ case 'FunctionExpression':
2028
+ case 'ArrowFunctionExpression': {
2029
+ const arrowScope = new Set(localScope);
2030
+ if (node.id) arrowScope.add(node.id.name);
2031
+ for (const p of node.params) collectPattern(p, arrowScope);
2032
+ walkNode(node.body, arrowScope);
2033
+ return;
2034
+ }
2035
+
2036
+ case 'ClassDeclaration':
2037
+ case 'ClassExpression':
2038
+ if (node.id) { localScope.add(node.id.name); declared.add(node.id.name); }
2039
+ if (node.superClass) walkNode(node.superClass, localScope);
2040
+ walkNode(node.body, localScope);
2041
+ return;
2042
+
2043
+ case 'ImportDeclaration':
2044
+ for (const spec of node.specifiers) {
2045
+ imported.add(spec.local.name);
2046
+ localScope.add(spec.local.name);
2047
+ }
2048
+ importSources.push({ source: node.source.value, loc: node.loc });
2049
+ return;
2050
+
2051
+ case 'ExportNamedDeclaration':
2052
+ if (node.declaration) walkNode(node.declaration, localScope);
2053
+ if (node.specifiers) for (const s of node.specifiers) exportedNames.add(s.exported.name || s.exported.value);
2054
+ return;
2055
+
2056
+ case 'ExportDefaultDeclaration':
2057
+ walkNode(node.declaration, localScope);
2058
+ return;
2059
+
2060
+ case 'Identifier':
2061
+ if (!localScope.has(node.name) && !declared.has(node.name) && !imported.has(node.name)) {
2062
+ references.push({ name: node.name, loc: node.loc });
2063
+ }
2064
+ return;
2065
+
2066
+ case 'MemberExpression':
2067
+ walkNode(node.object, localScope);
2068
+ // Don't walk computed property as reference
2069
+ if (node.computed) walkNode(node.property, localScope);
2070
+ return;
2071
+
2072
+ case 'Property':
2073
+ case 'MethodDefinition':
2074
+ // Don't treat keys as references
2075
+ if (node.computed) walkNode(node.key, localScope);
2076
+ walkNode(node.value, localScope);
2077
+ return;
2078
+
2079
+ case 'CatchClause':
2080
+ const catchScope = new Set(localScope);
2081
+ if (node.param) collectPattern(node.param, catchScope);
2082
+ walkNode(node.body, catchScope);
2083
+ return;
2084
+
2085
+ case 'ForInStatement':
2086
+ case 'ForOfStatement': {
2087
+ const forScope = new Set(localScope);
2088
+ if (node.left.type === 'VariableDeclaration') {
2089
+ for (const d of node.left.declarations) collectPattern(d.id, forScope);
2090
+ } else { walkNode(node.left, forScope); }
2091
+ walkNode(node.right, forScope);
2092
+ walkNode(node.body, forScope);
2093
+ return;
2094
+ }
2095
+
2096
+ case 'ForStatement': {
2097
+ const forScope2 = new Set(localScope);
2098
+ if (node.init?.type === 'VariableDeclaration') {
2099
+ for (const d of node.init.declarations) collectPattern(d.id, forScope2);
2100
+ } else if (node.init) { walkNode(node.init, forScope2); }
2101
+ if (node.test) walkNode(node.test, forScope2);
2102
+ if (node.update) walkNode(node.update, forScope2);
2103
+ walkNode(node.body, forScope2);
2104
+ return;
2105
+ }
2106
+
2107
+ case 'BlockStatement':
2108
+ case 'Program': {
2109
+ const blockScope = new Set(localScope);
2110
+ // Pre-scan for hoisted declarations
2111
+ if (node.body) {
2112
+ for (const stmt of node.body) {
2113
+ if (stmt.type === 'FunctionDeclaration' && stmt.id) blockScope.add(stmt.id.name);
2114
+ if (stmt.type === 'VariableDeclaration') {
2115
+ for (const d of stmt.declarations) collectPattern(d.id, blockScope);
2116
+ }
2117
+ if (stmt.type === 'ClassDeclaration' && stmt.id) blockScope.add(stmt.id.name);
2118
+ if (stmt.type === 'ImportDeclaration') {
2119
+ for (const s of stmt.specifiers) { blockScope.add(s.local.name); imported.add(s.local.name); }
2120
+ }
2121
+ }
2122
+ }
2123
+ if (node.body) for (const stmt of node.body) walkNode(stmt, blockScope);
2124
+ return;
2125
+ }
2126
+
2127
+ case 'LabeledStatement':
2128
+ walkNode(node.body, localScope);
2129
+ return;
2130
+
2131
+ case 'JSXIdentifier':
2132
+ // JSX component names (capitalized) are references
2133
+ if (/^[A-Z]/.test(node.name) && !localScope.has(node.name) && !declared.has(node.name) && !imported.has(node.name)) {
2134
+ references.push({ name: node.name, loc: node.loc });
2135
+ }
2136
+ return;
2137
+
2138
+ case 'JSXMemberExpression':
2139
+ walkNode(node.object, localScope);
2140
+ return;
2141
+ }
2142
+
2143
+ // Generic walk for other node types
2144
+ for (const key of Object.keys(node)) {
2145
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'type' || key === 'raw' || key === 'value' || key === 'name' || key === 'operator' || key === 'prefix' || key === 'sourceType') continue;
2146
+ const val = node[key];
2147
+ if (val && typeof val === 'object') walkNode(val, localScope);
2148
+ }
2149
+ }
2150
+
2151
+ function collectPattern(pattern, scope) {
2152
+ if (!pattern) return;
2153
+ if (pattern.type === 'Identifier') { scope.add(pattern.name); declared.add(pattern.name); }
2154
+ else if (pattern.type === 'ObjectPattern') { for (const p of pattern.properties) collectPattern(p.value || p.argument, scope); }
2155
+ else if (pattern.type === 'ArrayPattern') { for (const e of pattern.elements) if (e) collectPattern(e, scope); }
2156
+ else if (pattern.type === 'RestElement') collectPattern(pattern.argument, scope);
2157
+ else if (pattern.type === 'AssignmentPattern') collectPattern(pattern.left, scope);
2158
+ }
2159
+
2160
+ walkNode(ast, new Set());
2161
+
2162
+ // 3. Report undefined references (excluding builtins and React globals)
2163
+ const allKnown = new Set([...declared, ...imported, ...JS_BUILTINS]);
2164
+ if (isJsx || content.includes('from \'react\'') || content.includes('from "react"')) {
2165
+ for (const g of REACT_GLOBALS) allKnown.add(g);
2166
+ }
2167
+ for (const ref of references) {
2168
+ if (allKnown.has(ref.name)) continue;
2169
+ // Skip single-letter vars (often from minified/short code)
2170
+ if (ref.name.length === 1) continue;
2171
+ // Skip common DOM event handler names
2172
+ if (/^on[A-Z]/.test(ref.name)) continue;
2173
+ diagnostics.push({
2174
+ from: { line: ref.loc.start.line, col: ref.loc.start.column },
2175
+ severity: 'warning',
2176
+ message: `'${ref.name}' is not defined`,
2177
+ });
2178
+ }
2179
+
2180
+ // 4. Check import sources — verify local files exist
2181
+ for (const imp of importSources) {
2182
+ const src = imp.source;
2183
+ if (src.startsWith('.') || src.startsWith('/')) {
2184
+ const dir = path.dirname(relPath);
2185
+ const candidates = [
2186
+ path.join(dir, src),
2187
+ path.join(dir, src + '.js'),
2188
+ path.join(dir, src + '.mjs'),
2189
+ path.join(dir, src + '.jsx'),
2190
+ path.join(dir, src + '/index.js'),
2191
+ path.join(dir, src + '/index.mjs'),
2192
+ ].map(p => p.replace(/\\/g, '/'));
2193
+ const found = candidates.some(c => ProjectStore.readFile(projectName, c) !== null);
2194
+ if (!found) {
2195
+ diagnostics.push({
2196
+ from: { line: imp.loc.start.line, col: imp.loc.start.column },
2197
+ severity: 'error',
2198
+ message: `Cannot resolve import '${src}'`,
2199
+ });
2200
+ }
2201
+ }
2202
+ }
2203
+
2204
+ return diagnostics;
2205
+ }
2206
+
2207
+ function lintCSS(content) {
2208
+ const diagnostics = [];
2209
+ const lines = content.split('\n');
2210
+
2211
+ // Brace balance per-line tracking
2212
+ let depth = 0;
2213
+ for (let i = 0; i < lines.length; i++) {
2214
+ const line = lines[i];
2215
+ for (const ch of line) {
2216
+ if (ch === '{') depth++;
2217
+ if (ch === '}') depth--;
2218
+ }
2219
+ if (depth < 0) {
2220
+ diagnostics.push({ from: { line: i + 1, col: 0 }, severity: 'error', message: 'Unexpected closing brace }' });
2221
+ depth = 0;
2222
+ }
2223
+ }
2224
+ if (depth > 0) {
2225
+ diagnostics.push({ from: { line: lines.length, col: 0 }, severity: 'error', message: `${depth} unclosed brace(s) {` });
2226
+ }
2227
+
2228
+ // Check for common CSS errors
2229
+ for (let i = 0; i < lines.length; i++) {
2230
+ const line = lines[i].trim();
2231
+ // Empty property value
2232
+ if (/^[a-z-]+:\s*;/i.test(line)) {
2233
+ diagnostics.push({ from: { line: i + 1, col: 0 }, severity: 'warning', message: 'Empty property value' });
2234
+ }
2235
+ // Duplicate semicolons
2236
+ if (/;;/.test(line) && !line.startsWith('//') && !line.startsWith('/*')) {
2237
+ diagnostics.push({ from: { line: i + 1, col: line.indexOf(';;') }, severity: 'warning', message: 'Duplicate semicolon' });
2238
+ }
2239
+ // Missing semicolon (property line without ; that isn't a selector/comment/brace)
2240
+ if (/^[a-z-]+\s*:/.test(line) && !line.endsWith(';') && !line.endsWith('{') && !line.endsWith('}') && !line.endsWith(',') && !line.startsWith('//') && !line.startsWith('/*') && !line.startsWith('*')) {
2241
+ diagnostics.push({ from: { line: i + 1, col: line.length }, severity: 'warning', message: 'Missing semicolon' });
2242
+ }
2243
+ }
2244
+
2245
+ return diagnostics;
2246
+ }
2247
+
2248
+ function lintHTML(content, relPath, projectName) {
2249
+ const diagnostics = [];
2250
+ const lines = content.split('\n');
2251
+
2252
+ // Tag balance check
2253
+ const voidTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
2254
+ const tagStack = [];
2255
+ const tagRegex = /<\/?([a-z][a-z0-9]*)\b[^>]*\/?>/gi;
2256
+ let match;
2257
+ while ((match = tagRegex.exec(content)) !== null) {
2258
+ const full = match[0];
2259
+ const tagName = match[1].toLowerCase();
2260
+ if (voidTags.has(tagName) || full.endsWith('/>')) continue;
2261
+ const lineNum = content.slice(0, match.index).split('\n').length;
2262
+
2263
+ if (full.startsWith('</')) {
2264
+ // Closing tag
2265
+ if (tagStack.length === 0) {
2266
+ diagnostics.push({ from: { line: lineNum, col: 0 }, severity: 'error', message: `Unexpected closing tag </${tagName}>` });
2267
+ } else {
2268
+ const last = tagStack[tagStack.length - 1];
2269
+ if (last.name === tagName) {
2270
+ tagStack.pop();
2271
+ } else {
2272
+ diagnostics.push({ from: { line: lineNum, col: 0 }, severity: 'error', message: `Mismatched tag: expected </${last.name}> but found </${tagName}>` });
2273
+ }
2274
+ }
2275
+ } else {
2276
+ tagStack.push({ name: tagName, line: lineNum });
2277
+ }
2278
+ }
2279
+ // Report unclosed tags (max 5)
2280
+ for (const unclosed of tagStack.slice(-5)) {
2281
+ diagnostics.push({ from: { line: unclosed.line, col: 0 }, severity: 'warning', message: `Unclosed tag <${unclosed.name}>` });
2282
+ }
2283
+
2284
+ // Check src/href references to local files
2285
+ const refRegex = /(?:src|href)=["']([^"']*?\.(?:js|css|mjs|png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|eot))["']/gi;
2286
+ let refMatch;
2287
+ while ((refMatch = refRegex.exec(content)) !== null) {
2288
+ const ref = refMatch[1];
2289
+ if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:') || ref.startsWith('#') || ref.startsWith('mailto:')) continue;
2290
+ const htmlDir = path.dirname(relPath);
2291
+ const refPath = ref.startsWith('/') ? ref.slice(1) : path.join(htmlDir, ref).replace(/\\/g, '/');
2292
+ const publicRef = refPath.startsWith('public/') ? refPath : `public/${refPath}`;
2293
+ const fileExists = ProjectStore.readFile(projectName, refPath) !== null
2294
+ || ProjectStore.readFile(projectName, publicRef) !== null
2295
+ || ProjectStore.readFile(projectName, ref) !== null;
2296
+ if (!fileExists) {
2297
+ const lineNum = content.slice(0, refMatch.index).split('\n').length;
2298
+ diagnostics.push({
2299
+ from: { line: lineNum, col: 0 },
2300
+ severity: 'error',
2301
+ message: `Referenced file not found: ${ref}`,
2302
+ });
2303
+ }
2304
+ }
2305
+
2306
+ return diagnostics;
2307
+ }
2308
+
1921
2309
  router.post('/api/studio/webcraft/lint', async (req, res) => {
1922
2310
  try {
1923
2311
  const { projectName, path: relPath } = await parseBody(req);
@@ -1925,76 +2313,36 @@ export function register(router) {
1925
2313
  const content = ProjectStore.readFile(projectName, relPath);
1926
2314
  if (content === null) return sendJSON(res, 200, { diagnostics: [] });
1927
2315
 
1928
- const diagnostics = [];
1929
- const ext = relPath.split('.').pop()?.toLowerCase();
2316
+ const ext = (relPath.split('.').pop() || '').toLowerCase();
2317
+ let diagnostics = [];
1930
2318
 
1931
- if (ext === 'js' || ext === 'mjs' || ext === 'jsx') {
1932
- try { new Function(content); } catch (e) {
1933
- const match = e.message.match(/^(.*?)$/m);
1934
- const lineMatch = e.message.match(/:(\d+):(\d+)/);
1935
- diagnostics.push({
1936
- from: lineMatch ? { line: parseInt(lineMatch[1]), col: parseInt(lineMatch[2]) } : { line: 1, col: 0 },
1937
- severity: 'error',
1938
- message: match?.[1] || e.message,
1939
- });
1940
- }
2319
+ // JavaScript / JSX full AST analysis
2320
+ if (['js', 'mjs', 'jsx', 'cjs'].includes(ext)) {
2321
+ diagnostics = lintJS(content, relPath, projectName);
1941
2322
  }
1942
2323
 
2324
+ // JSON — parse errors with precise location
1943
2325
  if (ext === 'json') {
1944
2326
  try { JSON.parse(content); } catch (e) {
1945
- const posMatch = e.message.match(/position (\d+)/);
2327
+ const posMatch = e.message.match(/position (\d+)/i);
1946
2328
  const pos = posMatch ? parseInt(posMatch[1]) : 0;
1947
- const lines = content.slice(0, pos).split('\n');
2329
+ const before = content.slice(0, pos).split('\n');
1948
2330
  diagnostics.push({
1949
- from: { line: lines.length, col: (lines[lines.length - 1] || '').length },
2331
+ from: { line: before.length, col: (before[before.length - 1] || '').length },
1950
2332
  severity: 'error',
1951
2333
  message: e.message,
1952
2334
  });
1953
2335
  }
1954
2336
  }
1955
2337
 
2338
+ // CSS — brace balance + property validation
1956
2339
  if (ext === 'css') {
1957
- const opens = (content.match(/\{/g) || []).length;
1958
- const closes = (content.match(/\}/g) || []).length;
1959
- if (opens !== closes) {
1960
- diagnostics.push({
1961
- from: { line: content.split('\n').length, col: 0 },
1962
- severity: 'warning',
1963
- message: `Unbalanced braces: ${opens} open, ${closes} close`,
1964
- });
1965
- }
2340
+ diagnostics = lintCSS(content);
1966
2341
  }
1967
2342
 
2343
+ // HTML/HTM — tag balance + reference validation
1968
2344
  if (ext === 'html' || ext === 'htm') {
1969
- if (!content.includes('</html>')) {
1970
- diagnostics.push({
1971
- from: { line: content.split('\n').length, col: 0 },
1972
- severity: 'warning',
1973
- message: 'Missing </html> closing tag',
1974
- });
1975
- }
1976
- // Check references to local files
1977
- const refRegex = /(?:src|href)=["']([^"']*?\.(?:js|css|mjs))["']/gi;
1978
- let refMatch;
1979
- const lines = content.split('\n');
1980
- while ((refMatch = refRegex.exec(content)) !== null) {
1981
- const ref = refMatch[1];
1982
- if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:')) continue;
1983
- const htmlDir = path.dirname(relPath);
1984
- const refPath = ref.startsWith('/') ? ref.slice(1) : path.join(htmlDir, ref).replace(/\\/g, '/');
1985
- const publicRef = refPath.startsWith('public/') ? refPath : `public/${refPath}`;
1986
- const fileExists = ProjectStore.readFile(projectName, refPath) !== null
1987
- || ProjectStore.readFile(projectName, publicRef) !== null
1988
- || ProjectStore.readFile(projectName, ref) !== null;
1989
- if (!fileExists) {
1990
- const lineNum = content.slice(0, refMatch.index).split('\n').length;
1991
- diagnostics.push({
1992
- from: { line: lineNum, col: 0 },
1993
- severity: 'error',
1994
- message: `Referenced file not found: ${ref}`,
1995
- });
1996
- }
1997
- }
2345
+ diagnostics = lintHTML(content, relPath, projectName);
1998
2346
  }
1999
2347
 
2000
2348
  sendJSON(res, 200, { diagnostics });