nothumanallowed 15.1.8 → 15.1.9

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": "15.1.8",
3
+ "version": "15.1.9",
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 = '15.1.8';
8
+ export const VERSION = '15.1.9';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -88,80 +88,150 @@ export function register(router) {
88
88
  });
89
89
 
90
90
  // POST /api/npm-update
91
+ //
92
+ // Two-phase update:
93
+ // 1. Sync part: run `npm install -g nothumanallowed@latest --prefer-online`
94
+ // with cache bypass. Bypass is critical — without --prefer-online, npm
95
+ // can install the same version it had cached locally and the user sees
96
+ // "nothing changed" (the classic stale-manifest trap).
97
+ // 2. Async part: spawn a DETACHED helper that watches our PID, waits until
98
+ // we exit, sleeps 1 s so the port frees, then re-spawns this process
99
+ // with the SAME argv. The newly installed code runs because node will
100
+ // re-read the entry script from disk on the next invocation.
101
+ //
102
+ // The response is sent BEFORE the parent exits, so the client knows what
103
+ // version to expect and can poll /api/health for the restart to complete.
91
104
  router.post('/api/npm-update', async (req, res) => {
92
105
  try {
93
- const { exec } = await import('child_process');
106
+ const { exec, spawn } = await import('child_process');
94
107
  const { promisify } = await import('util');
95
108
  const execAsync = promisify(exec);
96
109
 
97
- // Detect the package manager and global prefix to handle permissions correctly
98
- let cmd = 'npm install -g nothumanallowed@latest';
99
-
100
- // On macOS/Linux, check if we need special handling for permission
110
+ // Permission preflight on POSIX
101
111
  if (process.platform !== 'win32') {
102
112
  try {
103
- // Check if npm global dir is writable by current user
104
113
  const { stdout: prefix } = await execAsync('npm config get prefix', { timeout: 5000 });
105
114
  const globalDir = prefix.trim();
106
115
  await fs.promises.access(path.join(globalDir, 'lib'), fs.constants.W_OK);
107
116
  } catch {
108
- // Global dir not writable — return error with clear instructions
109
117
  sendJSON(res, 200, {
110
118
  success: false,
111
119
  error: 'EACCES: permission denied',
112
- message: 'Permission denied. Run: sudo npm install -g nothumanallowed@latest'
120
+ message: 'Permission denied. Run: sudo npm install -g nothumanallowed@latest --prefer-online',
113
121
  });
114
122
  return;
115
123
  }
116
124
  }
117
125
 
126
+ // Clean the metadata cache first — defeats the stale-cache trap that
127
+ // makes `npm install -g <pkg>` silently install an older version.
128
+ try { await execAsync('npm cache clean --force', { timeout: 10_000 }); } catch { /* non-fatal */ }
129
+
130
+ const cmd = 'npm install -g nothumanallowed@latest --registry=https://registry.npmjs.org/ --prefer-online --no-fund --no-audit';
118
131
  const { stdout, stderr } = await execAsync(cmd, {
119
132
  timeout: 120_000,
120
133
  env: { ...process.env, NODE_ENV: 'production' },
121
134
  });
122
135
 
123
- // Get the installed version from npm output or registry
136
+ // Resolve the version that actually landed on disk
124
137
  let newVersion = '';
125
138
  try {
126
- // Extract version from npm install output (e.g. "+ nothumanallowed@14.4.18")
127
139
  const verMatch = stdout.match(/nothumanallowed@(\d+\.\d+\.\d+)/);
128
140
  if (verMatch) {
129
141
  newVersion = verMatch[1];
130
142
  } else {
131
- // Fallback: check registry
132
143
  const regResp = await fetch('https://registry.npmjs.org/nothumanallowed/latest');
133
144
  const regData = await regResp.json();
134
145
  newVersion = regData.version || '';
135
146
  }
136
147
  } catch { /* ignore */ }
137
148
 
138
- // Restart ops daemon if running (so Telegram/Discord reconnect with new code)
149
+ // Stop the ops daemon if it's running it must reconnect with new code
139
150
  try {
140
151
  const { isRunning, stopDaemon } = await import('../../services/ops-daemon.mjs');
141
- if (isRunning()) {
142
- await stopDaemon();
143
- const { execSync: ex2 } = await import('child_process');
144
- try { ex2('nha ops start', { timeout: 10_000, stdio: 'ignore' }); } catch {}
145
- }
146
- } catch {}
152
+ if (isRunning()) await stopDaemon();
153
+ } catch { /* ignore */ }
147
154
 
155
+ // Send the response NOW, before we kick off the restart. The client
156
+ // needs this payload to know what version to wait for during polling.
148
157
  sendJSON(res, 200, {
149
158
  success: true,
150
- message: newVersion ? `Updated to v${newVersion}! Restart the server to apply.` : 'Update completed',
159
+ message: newVersion ? `Updated to v${newVersion}. Restarting server...` : 'Update completed. Restarting...',
151
160
  newVersion,
152
- restart: false, // Do NOT auto-restart — it never works reliably
153
- needsManualRestart: true,
154
- stdout: stdout.trim(),
155
- stderr: stderr.trim()
161
+ restart: true,
162
+ restarting: true,
163
+ needsManualRestart: false,
164
+ stdout: stdout.trim().slice(-2000),
165
+ stderr: stderr.trim().slice(-2000),
156
166
  });
167
+
168
+ // ── Schedule the self-restart ──
169
+ // We do this OUTSIDE the response cycle: a detached child that survives
170
+ // our death, watches our PID, then re-spawns with the same argv.
171
+ // On POSIX we use `sh -c`. On Windows we use a detached spawn directly
172
+ // because cmd.exe handles the "wait for PID" idiom differently.
173
+ const parentPid = process.pid;
174
+ const respawnNode = process.execPath; // absolute path to node
175
+ const respawnArgs = process.argv.slice(1); // [cli.mjs, "ui", ...]
176
+ const argvJson = JSON.stringify({ node: respawnNode, args: respawnArgs, parentPid });
177
+
178
+ setTimeout(() => {
179
+ try {
180
+ if (process.platform === 'win32') {
181
+ // Windows: schedule via PowerShell — wait-process then start the new node.
182
+ const psScript = `
183
+ try { Wait-Process -Id ${parentPid} -Timeout 30 } catch {};
184
+ Start-Sleep -Milliseconds 1200;
185
+ Start-Process -FilePath "${respawnNode.replace(/\\/g, '\\\\')}" -ArgumentList ${JSON.stringify(respawnArgs).replace(/"/g, '\\"')} -WindowStyle Hidden
186
+ `.replace(/\s+/g, ' ').trim();
187
+ spawn('powershell.exe', ['-NoProfile', '-Command', psScript], {
188
+ detached: true,
189
+ stdio: 'ignore',
190
+ windowsHide: true,
191
+ }).unref();
192
+ } else {
193
+ // POSIX: shell loop that polls `kill -0 PID`, then re-exec the cmd.
194
+ // Using exec replaces the shell process so we don't leave a wrapper.
195
+ const escapedArgs = respawnArgs.map(a => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
196
+ const escapedNode = `'${respawnNode.replace(/'/g, `'\\''`)}'`;
197
+ const sh = `
198
+ i=0
199
+ while kill -0 ${parentPid} 2>/dev/null && [ $i -lt 60 ]; do
200
+ sleep 0.2
201
+ i=$((i+1))
202
+ done
203
+ sleep 1
204
+ exec ${escapedNode} ${escapedArgs}
205
+ `;
206
+ spawn('sh', ['-c', sh], {
207
+ detached: true,
208
+ stdio: 'ignore',
209
+ }).unref();
210
+ }
211
+
212
+ // Mark the restart so the next process knows it was self-spawned
213
+ try {
214
+ const home = os.homedir();
215
+ const nhaDir = path.join(home, '.nha');
216
+ if (!fs.existsSync(nhaDir)) fs.mkdirSync(nhaDir, { recursive: true });
217
+ fs.writeFileSync(
218
+ path.join(nhaDir, 'last-restart.json'),
219
+ JSON.stringify({ at: new Date().toISOString(), fromVersion: VERSION, toVersion: newVersion, meta: argvJson }, null, 2),
220
+ );
221
+ } catch { /* non-fatal */ }
222
+ } catch { /* spawn failed — user will need to restart manually */ }
223
+
224
+ // Give the response a moment, then exit so the helper can take over.
225
+ setTimeout(() => process.exit(0), 200);
226
+ }, 600);
157
227
  } catch (e) {
158
228
  const isPermission = e.message?.includes('EACCES') || e.message?.includes('permission denied');
159
229
  sendJSON(res, 200, {
160
230
  success: false,
161
231
  error: isPermission ? 'EACCES: permission denied' : e.message,
162
232
  message: isPermission
163
- ? 'Permission denied. Run: sudo npm install -g nothumanallowed@latest'
164
- : 'Update failed. Try running: npm install -g nothumanallowed@latest'
233
+ ? 'Permission denied. Run: sudo npm install -g nothumanallowed@latest --prefer-online'
234
+ : `Update failed: ${e.message}. Try manually: npm cache clean --force && npm install -g nothumanallowed@latest --prefer-online`,
165
235
  });
166
236
  }
167
237
  });