nothumanallowed 14.5.4 → 14.5.6
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.6",
|
|
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.6';
|
|
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
|
|
|
@@ -42,7 +44,10 @@ class SandboxManager {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
isRunning() {
|
|
45
|
-
|
|
47
|
+
if (!this._sandbox || !this._sandbox.proc) return false;
|
|
48
|
+
if (this._sandbox.proc.killed) { this._sandbox = null; return false; }
|
|
49
|
+
// Verify the process is actually alive (not zombie)
|
|
50
|
+
try { process.kill(this._sandbox.proc.pid, 0); return true; } catch { this._sandbox = null; return false; }
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
status() {
|
|
@@ -55,17 +60,43 @@ class SandboxManager {
|
|
|
55
60
|
get port() { return this.isRunning() ? this._sandbox.port : null; }
|
|
56
61
|
|
|
57
62
|
async stop() {
|
|
58
|
-
if (!this.
|
|
59
|
-
const { proc } = this._sandbox;
|
|
63
|
+
if (!this._sandbox) return;
|
|
64
|
+
const { proc, port } = this._sandbox;
|
|
60
65
|
this._sandbox = null;
|
|
66
|
+
|
|
67
|
+
// 1. Kill the entire process group (parent + all children)
|
|
61
68
|
try {
|
|
62
|
-
proc.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
if (proc.pid) {
|
|
70
|
+
// Kill the process group — negative PID kills all processes in the group
|
|
71
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch {}
|
|
72
|
+
// Grace period, then SIGKILL the group
|
|
73
|
+
await new Promise((resolve) => {
|
|
74
|
+
const t = setTimeout(() => {
|
|
75
|
+
try { process.kill(-proc.pid, 'SIGKILL'); } catch {}
|
|
76
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
77
|
+
resolve();
|
|
78
|
+
}, 1500);
|
|
79
|
+
proc.once('exit', () => { clearTimeout(t); resolve(); });
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
83
|
+
}
|
|
68
84
|
} catch {}
|
|
85
|
+
|
|
86
|
+
// 2. Force-kill any orphan processes still holding the port
|
|
87
|
+
if (port) {
|
|
88
|
+
try {
|
|
89
|
+
if (process.platform === 'win32') {
|
|
90
|
+
await execAsync(`for /f "tokens=5" %a in ('netstat -ano ^| findstr :${port} ^| findstr LISTENING') do taskkill /F /PID %a`, { timeout: 3000 });
|
|
91
|
+
} else {
|
|
92
|
+
const { stdout } = await execAsync(`lsof -ti:${port} 2>/dev/null || true`, { timeout: 2000 });
|
|
93
|
+
const pids = stdout.trim().split(/\s+/).filter(Boolean);
|
|
94
|
+
for (const pid of pids) { try { process.kill(parseInt(pid), 'SIGKILL'); } catch {} }
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
// Wait for OS to release the port
|
|
98
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
99
|
+
}
|
|
69
100
|
}
|
|
70
101
|
|
|
71
102
|
/**
|
|
@@ -155,7 +186,7 @@ class SandboxManager {
|
|
|
155
186
|
NODE_ENV: 'development',
|
|
156
187
|
NHA_SANDBOX: '1',
|
|
157
188
|
},
|
|
158
|
-
detached:
|
|
189
|
+
detached: true,
|
|
159
190
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
160
191
|
});
|
|
161
192
|
|
|
@@ -947,7 +978,7 @@ RULES:
|
|
|
947
978
|
const proc = spawn('node', [patchedEntry], {
|
|
948
979
|
cwd: projectDir,
|
|
949
980
|
env: { ...process.env, PORT: String(port), NODE_ENV: 'development', NHA_SANDBOX: '1' },
|
|
950
|
-
detached:
|
|
981
|
+
detached: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
951
982
|
});
|
|
952
983
|
sandbox._sandbox = { proc, port, projectName, startedAt: new Date(), healthy: false };
|
|
953
984
|
let sandboxStderr = '';
|
|
@@ -1089,7 +1120,7 @@ RULES:
|
|
|
1089
1120
|
const proc = spawn('node', [patchedEntry], {
|
|
1090
1121
|
cwd: projectDir,
|
|
1091
1122
|
env: { ...process.env, PORT: String(port), NODE_ENV: 'development', NHA_SANDBOX: '1' },
|
|
1092
|
-
detached:
|
|
1123
|
+
detached: true,
|
|
1093
1124
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1094
1125
|
});
|
|
1095
1126
|
sandbox._sandbox = { proc, port, projectName, startedAt: new Date(), healthy: false };
|
|
@@ -1889,6 +1920,386 @@ export function register(router) {
|
|
|
1889
1920
|
});
|
|
1890
1921
|
|
|
1891
1922
|
// ── Diagnostics (lint) — returns errors/warnings for a file ───────────────
|
|
1923
|
+
// ── Advanced Linter — AST-based diagnostics (acorn + scope analysis) ─────────
|
|
1924
|
+
|
|
1925
|
+
const JsxParser = acorn.Parser.extend(acornJsx());
|
|
1926
|
+
const JS_BUILTINS = new Set([
|
|
1927
|
+
'undefined','NaN','Infinity','globalThis','eval','isFinite','isNaN','parseFloat','parseInt',
|
|
1928
|
+
'decodeURI','decodeURIComponent','encodeURI','encodeURIComponent',
|
|
1929
|
+
'Array','ArrayBuffer','BigInt','BigInt64Array','BigUint64Array','Boolean','DataView','Date',
|
|
1930
|
+
'Error','EvalError','FinalizationRegistry','Float32Array','Float64Array','Function',
|
|
1931
|
+
'Int8Array','Int16Array','Int32Array','JSON','Map','Math','Number','Object','Promise',
|
|
1932
|
+
'Proxy','RangeError','ReferenceError','Reflect','RegExp','Set','SharedArrayBuffer',
|
|
1933
|
+
'String','Symbol','SyntaxError','TypeError','URIError','Uint8Array','Uint8ClampedArray',
|
|
1934
|
+
'Uint16Array','Uint32Array','WeakMap','WeakRef','WeakSet',
|
|
1935
|
+
'console','setTimeout','setInterval','clearTimeout','clearInterval','queueMicrotask',
|
|
1936
|
+
'atob','btoa','fetch','structuredClone','performance','crypto','navigator','location',
|
|
1937
|
+
'window','document','self','global','process','require','module','exports','__dirname','__filename',
|
|
1938
|
+
'Buffer','URL','URLSearchParams','TextEncoder','TextDecoder','AbortController','AbortSignal',
|
|
1939
|
+
'Event','EventTarget','CustomEvent','FormData','Headers','Request','Response',
|
|
1940
|
+
'ReadableStream','WritableStream','TransformStream','Blob','File','FileReader',
|
|
1941
|
+
'WebSocket','Worker','SharedWorker','BroadcastChannel','MessageChannel','MessagePort',
|
|
1942
|
+
'Intl','alert','confirm','prompt','requestAnimationFrame','cancelAnimationFrame',
|
|
1943
|
+
'MutationObserver','ResizeObserver','IntersectionObserver','PerformanceObserver',
|
|
1944
|
+
'HTMLElement','Element','Node','NodeList','DocumentFragment',
|
|
1945
|
+
'localStorage','sessionStorage','history','screen','CSS','CSSStyleSheet',
|
|
1946
|
+
'XMLHttpRequest','Image','Audio','Video','MediaSource','SourceBuffer',
|
|
1947
|
+
'Map','Set','WeakMap','WeakSet','Proxy','Reflect',
|
|
1948
|
+
'arguments','this','super','import','export',
|
|
1949
|
+
]);
|
|
1950
|
+
const REACT_GLOBALS = new Set([
|
|
1951
|
+
'React','useState','useEffect','useRef','useCallback','useMemo','useContext',
|
|
1952
|
+
'useReducer','useLayoutEffect','useImperativeHandle','useDebugValue','useTransition',
|
|
1953
|
+
'useDeferredValue','useId','useSyncExternalStore','useInsertionEffect',
|
|
1954
|
+
'createContext','createRef','forwardRef','lazy','memo','startTransition',
|
|
1955
|
+
'Component','PureComponent','Fragment','StrictMode','Suspense','Profiler',
|
|
1956
|
+
'createElement','cloneElement','isValidElement','Children',
|
|
1957
|
+
'jsx','jsxs','jsxDEV',
|
|
1958
|
+
]);
|
|
1959
|
+
const NODE_MODULES = new Set([
|
|
1960
|
+
'fs','path','os','http','https','url','util','stream','events','crypto','child_process',
|
|
1961
|
+
'net','dgram','dns','tls','zlib','readline','cluster','worker_threads','perf_hooks',
|
|
1962
|
+
'assert','buffer','querystring','string_decoder','timers','v8','vm','inspector',
|
|
1963
|
+
]);
|
|
1964
|
+
|
|
1965
|
+
function lintJS(content, relPath, projectName) {
|
|
1966
|
+
const diagnostics = [];
|
|
1967
|
+
const ext = relPath.split('.').pop()?.toLowerCase();
|
|
1968
|
+
const isJsx = ext === 'jsx' || ext === 'tsx';
|
|
1969
|
+
|
|
1970
|
+
// 1. Parse with acorn (real AST)
|
|
1971
|
+
let ast;
|
|
1972
|
+
try {
|
|
1973
|
+
ast = JsxParser.parse(content, {
|
|
1974
|
+
ecmaVersion: 'latest',
|
|
1975
|
+
sourceType: 'module',
|
|
1976
|
+
locations: true,
|
|
1977
|
+
allowImportExportEverywhere: true,
|
|
1978
|
+
allowReturnOutsideFunction: true,
|
|
1979
|
+
allowHashBang: true,
|
|
1980
|
+
});
|
|
1981
|
+
} catch (e) {
|
|
1982
|
+
diagnostics.push({
|
|
1983
|
+
from: { line: e.loc?.line || 1, col: e.loc?.column || 0 },
|
|
1984
|
+
severity: 'error',
|
|
1985
|
+
message: e.message.replace(/\(\d+:\d+\)$/, '').trim(),
|
|
1986
|
+
});
|
|
1987
|
+
return diagnostics;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// 2. Scope analysis — collect declarations and references
|
|
1991
|
+
const declared = new Set();
|
|
1992
|
+
const imported = new Set();
|
|
1993
|
+
const importSources = [];
|
|
1994
|
+
const references = []; // { name, loc }
|
|
1995
|
+
const exportedNames = new Set();
|
|
1996
|
+
|
|
1997
|
+
function walkNode(node, scope) {
|
|
1998
|
+
if (!node || typeof node !== 'object') return;
|
|
1999
|
+
if (Array.isArray(node)) { node.forEach(n => walkNode(n, scope)); return; }
|
|
2000
|
+
if (!node.type) return;
|
|
2001
|
+
|
|
2002
|
+
const localScope = new Set(scope);
|
|
2003
|
+
|
|
2004
|
+
switch (node.type) {
|
|
2005
|
+
case 'VariableDeclaration':
|
|
2006
|
+
for (const decl of node.declarations) {
|
|
2007
|
+
collectPattern(decl.id, localScope);
|
|
2008
|
+
if (decl.init) walkNode(decl.init, localScope);
|
|
2009
|
+
}
|
|
2010
|
+
// Walk rest of body with the declared vars
|
|
2011
|
+
return;
|
|
2012
|
+
|
|
2013
|
+
case 'FunctionDeclaration':
|
|
2014
|
+
if (node.id) localScope.add(node.id.name);
|
|
2015
|
+
declared.add(node.id?.name);
|
|
2016
|
+
const fnScope = new Set(localScope);
|
|
2017
|
+
for (const p of node.params) collectPattern(p, fnScope);
|
|
2018
|
+
walkNode(node.body, fnScope);
|
|
2019
|
+
return;
|
|
2020
|
+
|
|
2021
|
+
case 'FunctionExpression':
|
|
2022
|
+
case 'ArrowFunctionExpression': {
|
|
2023
|
+
const arrowScope = new Set(localScope);
|
|
2024
|
+
if (node.id) arrowScope.add(node.id.name);
|
|
2025
|
+
for (const p of node.params) collectPattern(p, arrowScope);
|
|
2026
|
+
walkNode(node.body, arrowScope);
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
case 'ClassDeclaration':
|
|
2031
|
+
case 'ClassExpression':
|
|
2032
|
+
if (node.id) { localScope.add(node.id.name); declared.add(node.id.name); }
|
|
2033
|
+
if (node.superClass) walkNode(node.superClass, localScope);
|
|
2034
|
+
walkNode(node.body, localScope);
|
|
2035
|
+
return;
|
|
2036
|
+
|
|
2037
|
+
case 'ImportDeclaration':
|
|
2038
|
+
for (const spec of node.specifiers) {
|
|
2039
|
+
imported.add(spec.local.name);
|
|
2040
|
+
localScope.add(spec.local.name);
|
|
2041
|
+
}
|
|
2042
|
+
importSources.push({ source: node.source.value, loc: node.loc });
|
|
2043
|
+
return;
|
|
2044
|
+
|
|
2045
|
+
case 'ExportNamedDeclaration':
|
|
2046
|
+
if (node.declaration) walkNode(node.declaration, localScope);
|
|
2047
|
+
if (node.specifiers) for (const s of node.specifiers) exportedNames.add(s.exported.name || s.exported.value);
|
|
2048
|
+
return;
|
|
2049
|
+
|
|
2050
|
+
case 'ExportDefaultDeclaration':
|
|
2051
|
+
walkNode(node.declaration, localScope);
|
|
2052
|
+
return;
|
|
2053
|
+
|
|
2054
|
+
case 'Identifier':
|
|
2055
|
+
if (!localScope.has(node.name) && !declared.has(node.name) && !imported.has(node.name)) {
|
|
2056
|
+
references.push({ name: node.name, loc: node.loc });
|
|
2057
|
+
}
|
|
2058
|
+
return;
|
|
2059
|
+
|
|
2060
|
+
case 'MemberExpression':
|
|
2061
|
+
walkNode(node.object, localScope);
|
|
2062
|
+
// Don't walk computed property as reference
|
|
2063
|
+
if (node.computed) walkNode(node.property, localScope);
|
|
2064
|
+
return;
|
|
2065
|
+
|
|
2066
|
+
case 'Property':
|
|
2067
|
+
case 'MethodDefinition':
|
|
2068
|
+
// Don't treat keys as references
|
|
2069
|
+
if (node.computed) walkNode(node.key, localScope);
|
|
2070
|
+
walkNode(node.value, localScope);
|
|
2071
|
+
return;
|
|
2072
|
+
|
|
2073
|
+
case 'CatchClause':
|
|
2074
|
+
const catchScope = new Set(localScope);
|
|
2075
|
+
if (node.param) collectPattern(node.param, catchScope);
|
|
2076
|
+
walkNode(node.body, catchScope);
|
|
2077
|
+
return;
|
|
2078
|
+
|
|
2079
|
+
case 'ForInStatement':
|
|
2080
|
+
case 'ForOfStatement': {
|
|
2081
|
+
const forScope = new Set(localScope);
|
|
2082
|
+
if (node.left.type === 'VariableDeclaration') {
|
|
2083
|
+
for (const d of node.left.declarations) collectPattern(d.id, forScope);
|
|
2084
|
+
} else { walkNode(node.left, forScope); }
|
|
2085
|
+
walkNode(node.right, forScope);
|
|
2086
|
+
walkNode(node.body, forScope);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
case 'ForStatement': {
|
|
2091
|
+
const forScope2 = new Set(localScope);
|
|
2092
|
+
if (node.init?.type === 'VariableDeclaration') {
|
|
2093
|
+
for (const d of node.init.declarations) collectPattern(d.id, forScope2);
|
|
2094
|
+
} else if (node.init) { walkNode(node.init, forScope2); }
|
|
2095
|
+
if (node.test) walkNode(node.test, forScope2);
|
|
2096
|
+
if (node.update) walkNode(node.update, forScope2);
|
|
2097
|
+
walkNode(node.body, forScope2);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
case 'BlockStatement':
|
|
2102
|
+
case 'Program': {
|
|
2103
|
+
const blockScope = new Set(localScope);
|
|
2104
|
+
// Pre-scan for hoisted declarations
|
|
2105
|
+
if (node.body) {
|
|
2106
|
+
for (const stmt of node.body) {
|
|
2107
|
+
if (stmt.type === 'FunctionDeclaration' && stmt.id) blockScope.add(stmt.id.name);
|
|
2108
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
2109
|
+
for (const d of stmt.declarations) collectPattern(d.id, blockScope);
|
|
2110
|
+
}
|
|
2111
|
+
if (stmt.type === 'ClassDeclaration' && stmt.id) blockScope.add(stmt.id.name);
|
|
2112
|
+
if (stmt.type === 'ImportDeclaration') {
|
|
2113
|
+
for (const s of stmt.specifiers) { blockScope.add(s.local.name); imported.add(s.local.name); }
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (node.body) for (const stmt of node.body) walkNode(stmt, blockScope);
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
case 'LabeledStatement':
|
|
2122
|
+
walkNode(node.body, localScope);
|
|
2123
|
+
return;
|
|
2124
|
+
|
|
2125
|
+
case 'JSXIdentifier':
|
|
2126
|
+
// JSX component names (capitalized) are references
|
|
2127
|
+
if (/^[A-Z]/.test(node.name) && !localScope.has(node.name) && !declared.has(node.name) && !imported.has(node.name)) {
|
|
2128
|
+
references.push({ name: node.name, loc: node.loc });
|
|
2129
|
+
}
|
|
2130
|
+
return;
|
|
2131
|
+
|
|
2132
|
+
case 'JSXMemberExpression':
|
|
2133
|
+
walkNode(node.object, localScope);
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// Generic walk for other node types
|
|
2138
|
+
for (const key of Object.keys(node)) {
|
|
2139
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'type' || key === 'raw' || key === 'value' || key === 'name' || key === 'operator' || key === 'prefix' || key === 'sourceType') continue;
|
|
2140
|
+
const val = node[key];
|
|
2141
|
+
if (val && typeof val === 'object') walkNode(val, localScope);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
function collectPattern(pattern, scope) {
|
|
2146
|
+
if (!pattern) return;
|
|
2147
|
+
if (pattern.type === 'Identifier') { scope.add(pattern.name); declared.add(pattern.name); }
|
|
2148
|
+
else if (pattern.type === 'ObjectPattern') { for (const p of pattern.properties) collectPattern(p.value || p.argument, scope); }
|
|
2149
|
+
else if (pattern.type === 'ArrayPattern') { for (const e of pattern.elements) if (e) collectPattern(e, scope); }
|
|
2150
|
+
else if (pattern.type === 'RestElement') collectPattern(pattern.argument, scope);
|
|
2151
|
+
else if (pattern.type === 'AssignmentPattern') collectPattern(pattern.left, scope);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
walkNode(ast, new Set());
|
|
2155
|
+
|
|
2156
|
+
// 3. Report undefined references (excluding builtins and React globals)
|
|
2157
|
+
const allKnown = new Set([...declared, ...imported, ...JS_BUILTINS]);
|
|
2158
|
+
if (isJsx || content.includes('from \'react\'') || content.includes('from "react"')) {
|
|
2159
|
+
for (const g of REACT_GLOBALS) allKnown.add(g);
|
|
2160
|
+
}
|
|
2161
|
+
for (const ref of references) {
|
|
2162
|
+
if (allKnown.has(ref.name)) continue;
|
|
2163
|
+
// Skip single-letter vars (often from minified/short code)
|
|
2164
|
+
if (ref.name.length === 1) continue;
|
|
2165
|
+
// Skip common DOM event handler names
|
|
2166
|
+
if (/^on[A-Z]/.test(ref.name)) continue;
|
|
2167
|
+
diagnostics.push({
|
|
2168
|
+
from: { line: ref.loc.start.line, col: ref.loc.start.column },
|
|
2169
|
+
severity: 'warning',
|
|
2170
|
+
message: `'${ref.name}' is not defined`,
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// 4. Check import sources — verify local files exist
|
|
2175
|
+
for (const imp of importSources) {
|
|
2176
|
+
const src = imp.source;
|
|
2177
|
+
if (src.startsWith('.') || src.startsWith('/')) {
|
|
2178
|
+
const dir = path.dirname(relPath);
|
|
2179
|
+
const candidates = [
|
|
2180
|
+
path.join(dir, src),
|
|
2181
|
+
path.join(dir, src + '.js'),
|
|
2182
|
+
path.join(dir, src + '.mjs'),
|
|
2183
|
+
path.join(dir, src + '.jsx'),
|
|
2184
|
+
path.join(dir, src + '/index.js'),
|
|
2185
|
+
path.join(dir, src + '/index.mjs'),
|
|
2186
|
+
].map(p => p.replace(/\\/g, '/'));
|
|
2187
|
+
const found = candidates.some(c => ProjectStore.readFile(projectName, c) !== null);
|
|
2188
|
+
if (!found) {
|
|
2189
|
+
diagnostics.push({
|
|
2190
|
+
from: { line: imp.loc.start.line, col: imp.loc.start.column },
|
|
2191
|
+
severity: 'error',
|
|
2192
|
+
message: `Cannot resolve import '${src}'`,
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
return diagnostics;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function lintCSS(content) {
|
|
2202
|
+
const diagnostics = [];
|
|
2203
|
+
const lines = content.split('\n');
|
|
2204
|
+
|
|
2205
|
+
// Brace balance per-line tracking
|
|
2206
|
+
let depth = 0;
|
|
2207
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2208
|
+
const line = lines[i];
|
|
2209
|
+
for (const ch of line) {
|
|
2210
|
+
if (ch === '{') depth++;
|
|
2211
|
+
if (ch === '}') depth--;
|
|
2212
|
+
}
|
|
2213
|
+
if (depth < 0) {
|
|
2214
|
+
diagnostics.push({ from: { line: i + 1, col: 0 }, severity: 'error', message: 'Unexpected closing brace }' });
|
|
2215
|
+
depth = 0;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
if (depth > 0) {
|
|
2219
|
+
diagnostics.push({ from: { line: lines.length, col: 0 }, severity: 'error', message: `${depth} unclosed brace(s) {` });
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// Check for common CSS errors
|
|
2223
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2224
|
+
const line = lines[i].trim();
|
|
2225
|
+
// Empty property value
|
|
2226
|
+
if (/^[a-z-]+:\s*;/i.test(line)) {
|
|
2227
|
+
diagnostics.push({ from: { line: i + 1, col: 0 }, severity: 'warning', message: 'Empty property value' });
|
|
2228
|
+
}
|
|
2229
|
+
// Duplicate semicolons
|
|
2230
|
+
if (/;;/.test(line) && !line.startsWith('//') && !line.startsWith('/*')) {
|
|
2231
|
+
diagnostics.push({ from: { line: i + 1, col: line.indexOf(';;') }, severity: 'warning', message: 'Duplicate semicolon' });
|
|
2232
|
+
}
|
|
2233
|
+
// Missing semicolon (property line without ; that isn't a selector/comment/brace)
|
|
2234
|
+
if (/^[a-z-]+\s*:/.test(line) && !line.endsWith(';') && !line.endsWith('{') && !line.endsWith('}') && !line.endsWith(',') && !line.startsWith('//') && !line.startsWith('/*') && !line.startsWith('*')) {
|
|
2235
|
+
diagnostics.push({ from: { line: i + 1, col: line.length }, severity: 'warning', message: 'Missing semicolon' });
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
return diagnostics;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
function lintHTML(content, relPath, projectName) {
|
|
2243
|
+
const diagnostics = [];
|
|
2244
|
+
const lines = content.split('\n');
|
|
2245
|
+
|
|
2246
|
+
// Tag balance check
|
|
2247
|
+
const voidTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
|
|
2248
|
+
const tagStack = [];
|
|
2249
|
+
const tagRegex = /<\/?([a-z][a-z0-9]*)\b[^>]*\/?>/gi;
|
|
2250
|
+
let match;
|
|
2251
|
+
while ((match = tagRegex.exec(content)) !== null) {
|
|
2252
|
+
const full = match[0];
|
|
2253
|
+
const tagName = match[1].toLowerCase();
|
|
2254
|
+
if (voidTags.has(tagName) || full.endsWith('/>')) continue;
|
|
2255
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
2256
|
+
|
|
2257
|
+
if (full.startsWith('</')) {
|
|
2258
|
+
// Closing tag
|
|
2259
|
+
if (tagStack.length === 0) {
|
|
2260
|
+
diagnostics.push({ from: { line: lineNum, col: 0 }, severity: 'error', message: `Unexpected closing tag </${tagName}>` });
|
|
2261
|
+
} else {
|
|
2262
|
+
const last = tagStack[tagStack.length - 1];
|
|
2263
|
+
if (last.name === tagName) {
|
|
2264
|
+
tagStack.pop();
|
|
2265
|
+
} else {
|
|
2266
|
+
diagnostics.push({ from: { line: lineNum, col: 0 }, severity: 'error', message: `Mismatched tag: expected </${last.name}> but found </${tagName}>` });
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
} else {
|
|
2270
|
+
tagStack.push({ name: tagName, line: lineNum });
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
// Report unclosed tags (max 5)
|
|
2274
|
+
for (const unclosed of tagStack.slice(-5)) {
|
|
2275
|
+
diagnostics.push({ from: { line: unclosed.line, col: 0 }, severity: 'warning', message: `Unclosed tag <${unclosed.name}>` });
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// Check src/href references to local files
|
|
2279
|
+
const refRegex = /(?:src|href)=["']([^"']*?\.(?:js|css|mjs|png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|eot))["']/gi;
|
|
2280
|
+
let refMatch;
|
|
2281
|
+
while ((refMatch = refRegex.exec(content)) !== null) {
|
|
2282
|
+
const ref = refMatch[1];
|
|
2283
|
+
if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:') || ref.startsWith('#') || ref.startsWith('mailto:')) continue;
|
|
2284
|
+
const htmlDir = path.dirname(relPath);
|
|
2285
|
+
const refPath = ref.startsWith('/') ? ref.slice(1) : path.join(htmlDir, ref).replace(/\\/g, '/');
|
|
2286
|
+
const publicRef = refPath.startsWith('public/') ? refPath : `public/${refPath}`;
|
|
2287
|
+
const fileExists = ProjectStore.readFile(projectName, refPath) !== null
|
|
2288
|
+
|| ProjectStore.readFile(projectName, publicRef) !== null
|
|
2289
|
+
|| ProjectStore.readFile(projectName, ref) !== null;
|
|
2290
|
+
if (!fileExists) {
|
|
2291
|
+
const lineNum = content.slice(0, refMatch.index).split('\n').length;
|
|
2292
|
+
diagnostics.push({
|
|
2293
|
+
from: { line: lineNum, col: 0 },
|
|
2294
|
+
severity: 'error',
|
|
2295
|
+
message: `Referenced file not found: ${ref}`,
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
return diagnostics;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
1892
2303
|
router.post('/api/studio/webcraft/lint', async (req, res) => {
|
|
1893
2304
|
try {
|
|
1894
2305
|
const { projectName, path: relPath } = await parseBody(req);
|
|
@@ -1896,76 +2307,36 @@ export function register(router) {
|
|
|
1896
2307
|
const content = ProjectStore.readFile(projectName, relPath);
|
|
1897
2308
|
if (content === null) return sendJSON(res, 200, { diagnostics: [] });
|
|
1898
2309
|
|
|
1899
|
-
const
|
|
1900
|
-
|
|
2310
|
+
const ext = (relPath.split('.').pop() || '').toLowerCase();
|
|
2311
|
+
let diagnostics = [];
|
|
1901
2312
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
const lineMatch = e.message.match(/:(\d+):(\d+)/);
|
|
1906
|
-
diagnostics.push({
|
|
1907
|
-
from: lineMatch ? { line: parseInt(lineMatch[1]), col: parseInt(lineMatch[2]) } : { line: 1, col: 0 },
|
|
1908
|
-
severity: 'error',
|
|
1909
|
-
message: match?.[1] || e.message,
|
|
1910
|
-
});
|
|
1911
|
-
}
|
|
2313
|
+
// JavaScript / JSX — full AST analysis
|
|
2314
|
+
if (['js', 'mjs', 'jsx', 'cjs'].includes(ext)) {
|
|
2315
|
+
diagnostics = lintJS(content, relPath, projectName);
|
|
1912
2316
|
}
|
|
1913
2317
|
|
|
2318
|
+
// JSON — parse errors with precise location
|
|
1914
2319
|
if (ext === 'json') {
|
|
1915
2320
|
try { JSON.parse(content); } catch (e) {
|
|
1916
|
-
const posMatch = e.message.match(/position (\d+)/);
|
|
2321
|
+
const posMatch = e.message.match(/position (\d+)/i);
|
|
1917
2322
|
const pos = posMatch ? parseInt(posMatch[1]) : 0;
|
|
1918
|
-
const
|
|
2323
|
+
const before = content.slice(0, pos).split('\n');
|
|
1919
2324
|
diagnostics.push({
|
|
1920
|
-
from: { line:
|
|
2325
|
+
from: { line: before.length, col: (before[before.length - 1] || '').length },
|
|
1921
2326
|
severity: 'error',
|
|
1922
2327
|
message: e.message,
|
|
1923
2328
|
});
|
|
1924
2329
|
}
|
|
1925
2330
|
}
|
|
1926
2331
|
|
|
2332
|
+
// CSS — brace balance + property validation
|
|
1927
2333
|
if (ext === 'css') {
|
|
1928
|
-
|
|
1929
|
-
const closes = (content.match(/\}/g) || []).length;
|
|
1930
|
-
if (opens !== closes) {
|
|
1931
|
-
diagnostics.push({
|
|
1932
|
-
from: { line: content.split('\n').length, col: 0 },
|
|
1933
|
-
severity: 'warning',
|
|
1934
|
-
message: `Unbalanced braces: ${opens} open, ${closes} close`,
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
2334
|
+
diagnostics = lintCSS(content);
|
|
1937
2335
|
}
|
|
1938
2336
|
|
|
2337
|
+
// HTML/HTM — tag balance + reference validation
|
|
1939
2338
|
if (ext === 'html' || ext === 'htm') {
|
|
1940
|
-
|
|
1941
|
-
diagnostics.push({
|
|
1942
|
-
from: { line: content.split('\n').length, col: 0 },
|
|
1943
|
-
severity: 'warning',
|
|
1944
|
-
message: 'Missing </html> closing tag',
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
// Check references to local files
|
|
1948
|
-
const refRegex = /(?:src|href)=["']([^"']*?\.(?:js|css|mjs))["']/gi;
|
|
1949
|
-
let refMatch;
|
|
1950
|
-
const lines = content.split('\n');
|
|
1951
|
-
while ((refMatch = refRegex.exec(content)) !== null) {
|
|
1952
|
-
const ref = refMatch[1];
|
|
1953
|
-
if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:')) continue;
|
|
1954
|
-
const htmlDir = path.dirname(relPath);
|
|
1955
|
-
const refPath = ref.startsWith('/') ? ref.slice(1) : path.join(htmlDir, ref).replace(/\\/g, '/');
|
|
1956
|
-
const publicRef = refPath.startsWith('public/') ? refPath : `public/${refPath}`;
|
|
1957
|
-
const fileExists = ProjectStore.readFile(projectName, refPath) !== null
|
|
1958
|
-
|| ProjectStore.readFile(projectName, publicRef) !== null
|
|
1959
|
-
|| ProjectStore.readFile(projectName, ref) !== null;
|
|
1960
|
-
if (!fileExists) {
|
|
1961
|
-
const lineNum = content.slice(0, refMatch.index).split('\n').length;
|
|
1962
|
-
diagnostics.push({
|
|
1963
|
-
from: { line: lineNum, col: 0 },
|
|
1964
|
-
severity: 'error',
|
|
1965
|
-
message: `Referenced file not found: ${ref}`,
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
2339
|
+
diagnostics = lintHTML(content, relPath, projectName);
|
|
1969
2340
|
}
|
|
1970
2341
|
|
|
1971
2342
|
sendJSON(res, 200, { diagnostics });
|
|
@@ -2006,6 +2377,14 @@ export function register(router) {
|
|
|
2006
2377
|
} catch (e) { sendError(res, 500, e.message); }
|
|
2007
2378
|
});
|
|
2008
2379
|
|
|
2380
|
+
// ── Sandbox stop (beacon — for beforeunload) ───────────────────────────────
|
|
2381
|
+
router.post('/api/studio/webcraft/sandbox/stop-beacon', async (_req, res) => {
|
|
2382
|
+
try {
|
|
2383
|
+
await sandbox.stop();
|
|
2384
|
+
sendJSON(res, 200, { ok: true });
|
|
2385
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2009
2388
|
// ── Sandbox status ────────────────────────────────────────────────────────
|
|
2010
2389
|
router.get('/api/studio/webcraft/sandbox/status', (_req, res) => {
|
|
2011
2390
|
sendJSON(res, 200, sandbox.status());
|