nothumanallowed 14.1.75 → 14.1.76

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.1.75",
3
+ "version": "14.1.76",
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.1.75';
8
+ export const VERSION = '14.1.76';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -74,10 +74,12 @@ class SandboxManager {
74
74
  * @param {string} projectDir
75
75
  * @param {(event: object) => void} emit
76
76
  */
77
- async start(projectName, projectDir, emit) {
77
+ async start(projectName, projectDir, emit, _attempt = 1) {
78
+ const MAX_RETRIES = 3;
79
+
78
80
  // Kill any existing sandbox
79
81
  if (this.isRunning()) {
80
- emit({ type: 'status', msg: 'Stopping previous sandbox...' });
82
+ emit({ type: 'phase', phase: 'cleanup', msg: 'Stopping previous sandbox...' });
81
83
  await this.stop();
82
84
  }
83
85
 
@@ -86,7 +88,8 @@ class SandboxManager {
86
88
  return;
87
89
  }
88
90
 
89
- // ── Inject shims so user projects run without real DB / Redis ──────────
91
+ // ── Phase 1: Shims ────────────────────────────────────────────────────
92
+ emit({ type: 'phase', phase: 'shims', msg: 'Injecting runtime shims (pg, redis, mongoose, helmet...)' });
90
93
  const shimDir = path.join(projectDir, '.nha-shims');
91
94
  ensureDir(shimDir);
92
95
  _writeShims(shimDir);
@@ -96,34 +99,36 @@ class SandboxManager {
96
99
  emit({ type: 'error', msg: 'No entry point found (server.js / app.js / index.js).' });
97
100
  return;
98
101
  }
102
+ emit({ type: 'status', msg: `Entry point: ${entryFile}` });
99
103
 
100
- // ── Install dependencies ────────────────────────────────────────────────
104
+ // ── Phase 2: Dependencies ─────────────────────────────────────────────
101
105
  if (fs.existsSync(path.join(projectDir, 'package.json'))) {
102
- emit({ type: 'status', msg: 'Installing dependencies (npm install)...' });
106
+ emit({ type: 'phase', phase: 'deps', msg: 'Installing dependencies...' });
103
107
  try {
104
- await execAsync('npm install --prefer-offline --no-audit --no-fund', {
108
+ const { stdout } = await execAsync('npm install --prefer-offline --no-audit --no-fund 2>&1', {
105
109
  cwd: projectDir,
106
110
  timeout: 120_000,
107
111
  env: { ...process.env, NODE_ENV: 'development' },
108
112
  });
109
- emit({ type: 'status', msg: 'Dependencies installed.' });
113
+ const added = stdout.match(/added (\d+) package/)?.[1] || '0';
114
+ emit({ type: 'status', msg: `Dependencies installed (${added} packages)` });
110
115
  } catch (e) {
111
- emit({ type: 'warn', msg: `npm install warning: ${e.message.slice(0, 200)}` });
116
+ emit({ type: 'warn', msg: `npm install warning: ${e.message.slice(0, 300)}` });
112
117
  }
113
118
  }
114
119
 
115
- // ── Find a free port ────────────────────────────────────────────────────
120
+ // ── Phase 3: Start server ─────────────────────────────────────────────
116
121
  const port = await _findFreePort(4000, 4999);
117
122
  if (!port) {
118
123
  emit({ type: 'error', msg: 'No free ports available in range 4000-4999.' });
119
124
  return;
120
125
  }
121
126
 
122
- emit({ type: 'status', msg: `Starting on port ${port}...` });
123
-
124
- // Patch entry file to use our shims and bind to the found port
127
+ emit({ type: 'phase', phase: 'start', msg: `Starting server on port ${port}...` });
125
128
  const patchedEntry = _patchEntry(projectDir, entryFile, shimDir, port);
126
129
 
130
+ // Capture stderr for missing module detection
131
+ let stderrBuf = '';
127
132
  const proc = spawn('node', [patchedEntry], {
128
133
  cwd: projectDir,
129
134
  env: {
@@ -149,22 +154,62 @@ class SandboxManager {
149
154
 
150
155
  proc.stderr.on('data', (d) => {
151
156
  const line = d.toString().trim();
157
+ stderrBuf += d.toString();
152
158
  if (line) emit({ type: 'log', msg: `[stderr] ${line}` });
153
159
  });
154
160
 
155
- proc.once('exit', (code) => {
156
- if (this._sandbox?.proc === proc) this._sandbox = null;
157
- emit({ type: 'exit', code: code ?? -1 });
161
+ // Wait for exit or healthy
162
+ const exitPromise = new Promise((resolve) => {
163
+ proc.once('exit', (code) => {
164
+ if (this._sandbox?.proc === proc) this._sandbox = null;
165
+ resolve(code ?? -1);
166
+ });
158
167
  });
159
168
 
160
- // Healthcheck: wait up to 15s for the process to bind the port
161
- const healthy = await _waitForPort(port, 15_000);
162
- if (healthy && this._sandbox) {
163
- this._sandbox.healthy = true;
169
+ const healthy = await Promise.race([
170
+ _waitForPort(port, 15_000).then((h) => h ? 'healthy' : 'timeout'),
171
+ exitPromise.then((code) => ({ exitCode: code })),
172
+ ]);
173
+
174
+ if (healthy === 'healthy') {
175
+ if (this._sandbox) this._sandbox.healthy = true;
176
+ emit({ type: 'phase', phase: 'ready', msg: `Server running on port ${port}` });
164
177
  emit({ type: 'ready', port });
165
- } else if (!healthy) {
166
- emit({ type: 'warn', msg: 'Sandbox started but port not yet bound — may still be loading.' });
178
+ return;
167
179
  }
180
+
181
+ if (healthy === 'timeout') {
182
+ emit({ type: 'warn', msg: 'Server started but port not yet bound — may still be loading.' });
183
+ return;
184
+ }
185
+
186
+ // ── Crash handling — auto-fix missing modules ─────────────────────────
187
+ const exitCode = typeof healthy === 'object' ? healthy.exitCode : -1;
188
+ emit({ type: 'status', msg: `Process exited with code ${exitCode}` });
189
+
190
+ // Extract missing module name from stderr
191
+ const missingMatch = stderrBuf.match(/Cannot find module ['"]([^'"]+)['"]/);
192
+ if (missingMatch && _attempt < MAX_RETRIES) {
193
+ const missingMod = missingMatch[1];
194
+ // Skip shim-able or built-in modules
195
+ if (!missingMod.startsWith('.') && !missingMod.startsWith('/') && !missingMod.startsWith('node:')) {
196
+ const pkgName = missingMod.startsWith('@') ? missingMod.split('/').slice(0, 2).join('/') : missingMod.split('/')[0];
197
+ emit({ type: 'phase', phase: 'autofix', msg: `Missing module "${pkgName}" — installing...` });
198
+ try {
199
+ await execAsync(`npm install --save ${pkgName} --no-audit --no-fund`, {
200
+ cwd: projectDir,
201
+ timeout: 60_000,
202
+ env: { ...process.env, NODE_ENV: 'development' },
203
+ });
204
+ emit({ type: 'status', msg: `Installed ${pkgName} — retrying (attempt ${_attempt + 1}/${MAX_RETRIES})...` });
205
+ return this.start(projectName, projectDir, emit, _attempt + 1);
206
+ } catch (installErr) {
207
+ emit({ type: 'warn', msg: `Failed to install ${pkgName}: ${installErr.message.slice(0, 200)}` });
208
+ }
209
+ }
210
+ }
211
+
212
+ emit({ type: 'error', msg: _attempt >= MAX_RETRIES ? `Failed after ${MAX_RETRIES} attempts` : 'Server crashed on startup' });
168
213
  }
169
214
  }
170
215