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 +1 -1
- package/src/constants.mjs +1 -1
- package/src/server/routes/config.mjs +95 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
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
|
+
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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}
|
|
159
|
+
message: newVersion ? `Updated to v${newVersion}. Restarting server...` : 'Update completed. Restarting...',
|
|
151
160
|
newVersion,
|
|
152
|
-
restart:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
:
|
|
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
|
});
|