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.
|
|
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.
|
|
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
|
-
-
|
|
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
|
-
|
|
875
|
-
|
|
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
|
|
1929
|
-
|
|
2316
|
+
const ext = (relPath.split('.').pop() || '').toLowerCase();
|
|
2317
|
+
let diagnostics = [];
|
|
1930
2318
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
|
2329
|
+
const before = content.slice(0, pos).split('\n');
|
|
1948
2330
|
diagnostics.push({
|
|
1949
|
-
from: { line:
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|