nothumanallowed 14.5.3 → 14.5.5

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",
3
+ "version": "14.5.5",
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": {
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.3';
8
+ export const VERSION = '14.5.5';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -42,7 +42,10 @@ class SandboxManager {
42
42
  }
43
43
 
44
44
  isRunning() {
45
- return this._sandbox !== null && this._sandbox.proc && !this._sandbox.proc.killed;
45
+ if (!this._sandbox || !this._sandbox.proc) return false;
46
+ if (this._sandbox.proc.killed) { this._sandbox = null; return false; }
47
+ // Verify the process is actually alive (not zombie)
48
+ try { process.kill(this._sandbox.proc.pid, 0); return true; } catch { this._sandbox = null; return false; }
46
49
  }
47
50
 
48
51
  status() {
@@ -55,17 +58,43 @@ class SandboxManager {
55
58
  get port() { return this.isRunning() ? this._sandbox.port : null; }
56
59
 
57
60
  async stop() {
58
- if (!this.isRunning()) return;
59
- const { proc } = this._sandbox;
61
+ if (!this._sandbox) return;
62
+ const { proc, port } = this._sandbox;
60
63
  this._sandbox = null;
64
+
65
+ // 1. Kill the entire process group (parent + all children)
61
66
  try {
62
- proc.kill('SIGTERM');
63
- // Give it a grace period then SIGKILL
64
- await new Promise((resolve) => {
65
- const t = setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} resolve(); }, 4000);
66
- proc.once('exit', () => { clearTimeout(t); resolve(); });
67
- });
67
+ if (proc.pid) {
68
+ // Kill the process group negative PID kills all processes in the group
69
+ try { process.kill(-proc.pid, 'SIGTERM'); } catch {}
70
+ // Grace period, then SIGKILL the group
71
+ await new Promise((resolve) => {
72
+ const t = setTimeout(() => {
73
+ try { process.kill(-proc.pid, 'SIGKILL'); } catch {}
74
+ try { proc.kill('SIGKILL'); } catch {}
75
+ resolve();
76
+ }, 1500);
77
+ proc.once('exit', () => { clearTimeout(t); resolve(); });
78
+ });
79
+ } else {
80
+ try { proc.kill('SIGKILL'); } catch {}
81
+ }
68
82
  } catch {}
83
+
84
+ // 2. Force-kill any orphan processes still holding the port
85
+ if (port) {
86
+ try {
87
+ if (process.platform === 'win32') {
88
+ await execAsync(`for /f "tokens=5" %a in ('netstat -ano ^| findstr :${port} ^| findstr LISTENING') do taskkill /F /PID %a`, { timeout: 3000 });
89
+ } else {
90
+ const { stdout } = await execAsync(`lsof -ti:${port} 2>/dev/null || true`, { timeout: 2000 });
91
+ const pids = stdout.trim().split(/\s+/).filter(Boolean);
92
+ for (const pid of pids) { try { process.kill(parseInt(pid), 'SIGKILL'); } catch {} }
93
+ }
94
+ } catch {}
95
+ // Wait for OS to release the port
96
+ await new Promise((r) => setTimeout(r, 500));
97
+ }
69
98
  }
70
99
 
71
100
  /**
@@ -155,7 +184,7 @@ class SandboxManager {
155
184
  NODE_ENV: 'development',
156
185
  NHA_SANDBOX: '1',
157
186
  },
158
- detached: false,
187
+ detached: true,
159
188
  stdio: ['ignore', 'pipe', 'pipe'],
160
189
  });
161
190
 
@@ -947,7 +976,7 @@ RULES:
947
976
  const proc = spawn('node', [patchedEntry], {
948
977
  cwd: projectDir,
949
978
  env: { ...process.env, PORT: String(port), NODE_ENV: 'development', NHA_SANDBOX: '1' },
950
- detached: false, stdio: ['ignore', 'pipe', 'pipe'],
979
+ detached: true, stdio: ['ignore', 'pipe', 'pipe'],
951
980
  });
952
981
  sandbox._sandbox = { proc, port, projectName, startedAt: new Date(), healthy: false };
953
982
  let sandboxStderr = '';
@@ -1089,7 +1118,7 @@ RULES:
1089
1118
  const proc = spawn('node', [patchedEntry], {
1090
1119
  cwd: projectDir,
1091
1120
  env: { ...process.env, PORT: String(port), NODE_ENV: 'development', NHA_SANDBOX: '1' },
1092
- detached: false,
1121
+ detached: true,
1093
1122
  stdio: ['ignore', 'pipe', 'pipe'],
1094
1123
  });
1095
1124
  sandbox._sandbox = { proc, port, projectName, startedAt: new Date(), healthy: false };
@@ -2006,6 +2035,14 @@ export function register(router) {
2006
2035
  } catch (e) { sendError(res, 500, e.message); }
2007
2036
  });
2008
2037
 
2038
+ // ── Sandbox stop (beacon — for beforeunload) ───────────────────────────────
2039
+ router.post('/api/studio/webcraft/sandbox/stop-beacon', async (_req, res) => {
2040
+ try {
2041
+ await sandbox.stop();
2042
+ sendJSON(res, 200, { ok: true });
2043
+ } catch (e) { sendError(res, 500, e.message); }
2044
+ });
2045
+
2009
2046
  // ── Sandbox status ────────────────────────────────────────────────────────
2010
2047
  router.get('/api/studio/webcraft/sandbox/status', (_req, res) => {
2011
2048
  sendJSON(res, 200, sandbox.status());
@@ -2100,6 +2137,73 @@ export function register(router) {
2100
2137
  } catch (e) { sendError(res, 500, e.message); }
2101
2138
  });
2102
2139
 
2140
+ // ── Full project scan — lint ALL files at once ───────────────────────────
2141
+ router.post('/api/studio/webcraft/scan', async (req, res) => {
2142
+ try {
2143
+ const { projectName } = await parseBody(req);
2144
+ if (!projectName) return sendError(res, 400, 'projectName required');
2145
+ const dir = ProjectStore.dir(projectName);
2146
+ if (!fs.existsSync(dir)) return sendJSON(res, 200, { issues: [] });
2147
+
2148
+ const files = _listProjectFiles(dir);
2149
+ const issues = [];
2150
+ const allFileNames = new Set(files);
2151
+
2152
+ for (const relPath of files) {
2153
+ const content = ProjectStore.readFile(projectName, relPath);
2154
+ if (!content) continue;
2155
+ const ext = relPath.split('.').pop()?.toLowerCase();
2156
+
2157
+ // JS syntax check
2158
+ if (ext === 'js' || ext === 'mjs') {
2159
+ try { new Function(content); } catch (e) {
2160
+ issues.push({ file: relPath, severity: 'error', message: `Syntax: ${e.message.replace(/\n.*/s, '')}` });
2161
+ }
2162
+ }
2163
+
2164
+ // JSON parse check
2165
+ if (ext === 'json') {
2166
+ try { JSON.parse(content); } catch (e) {
2167
+ issues.push({ file: relPath, severity: 'error', message: `JSON: ${e.message}` });
2168
+ }
2169
+ }
2170
+
2171
+ // CSS brace balance
2172
+ if (ext === 'css') {
2173
+ const o = (content.match(/\{/g) || []).length, c = (content.match(/\}/g) || []).length;
2174
+ if (o !== c) issues.push({ file: relPath, severity: 'warning', message: `Unbalanced braces: ${o} open, ${c} close` });
2175
+ }
2176
+
2177
+ // HTML closing tag + reference check
2178
+ if (ext === 'html' || ext === 'htm') {
2179
+ if (!content.includes('</html>')) {
2180
+ issues.push({ file: relPath, severity: 'warning', message: 'Missing </html>' });
2181
+ }
2182
+ // Check file references
2183
+ const refRegex = /(?:src|href)=["']([^"']*?\.(?:js|css|mjs))["']/gi;
2184
+ let m;
2185
+ while ((m = refRegex.exec(content)) !== null) {
2186
+ const ref = m[1];
2187
+ if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:')) continue;
2188
+ const htmlDir = path.dirname(relPath);
2189
+ const refPath = ref.startsWith('/') ? ref.slice(1) : path.join(htmlDir, ref).replace(/\\/g, '/');
2190
+ const publicRef = refPath.startsWith('public/') ? refPath : `public/${refPath}`;
2191
+ if (!allFileNames.has(refPath) && !allFileNames.has(publicRef) && !allFileNames.has(ref)) {
2192
+ issues.push({ file: relPath, severity: 'error', message: `Missing file: ${ref}` });
2193
+ }
2194
+ }
2195
+ }
2196
+
2197
+ // Truncation check
2198
+ if (isFileTruncated(content, relPath)) {
2199
+ issues.push({ file: relPath, severity: 'warning', message: 'File appears truncated' });
2200
+ }
2201
+ }
2202
+
2203
+ sendJSON(res, 200, { issues, scanned: files.length });
2204
+ } catch (e) { sendError(res, 500, e.message); }
2205
+ });
2206
+
2103
2207
  // ── Syntax check ──────────────────────────────────────────────────────────
2104
2208
  router.post('/api/studio/webcraft/syntax-check', async (req, res) => {
2105
2209
  try {