gm-thebird 2.0.1070 → 2.0.1071

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/bin/bootstrap.js CHANGED
@@ -4,20 +4,13 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
- const https = require('https');
8
7
  const crypto = require('crypto');
9
- const { URL } = require('url');
10
-
11
- const RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
12
- const ATTEMPT_TIMEOUT_MS = 5 * 60 * 1000;
13
- const STALL_TIMEOUT_MS = 15 * 1000;
14
- const MAX_ATTEMPTS = 5;
15
- const BACKOFF_MS = [2000, 5000, 15000, 30000];
16
- // Worst case: a slow link downloading 140MB at 1MB/s = ~140s. Allow 30 minutes
17
- // before another bootstrap process treats this lock as abandoned. Below this,
18
- // concurrent bootstrap calls would wipe an in-progress download mid-stream
19
- // (see the v0.1.294 incident where a race between two wrappers blew away the
20
- // .partial during a 10-minute fetch).
8
+ const { spawnSync } = require('child_process');
9
+
10
+ const NPM_PACKAGE = '@anentrypoint/plugkit-wasm';
11
+ const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
12
+ const MAX_ATTEMPTS = 3;
13
+ const BACKOFF_MS = [5000, 15000];
21
14
  const LOCK_STALE_MS = 30 * 60 * 1000;
22
15
 
23
16
  function log(msg) {
@@ -73,18 +66,6 @@ function obsEvent(subsystem, event, fields) {
73
66
  } catch (_) {}
74
67
  }
75
68
 
76
- function platformKey() {
77
- const p = os.platform();
78
- const a = os.arch();
79
- if (p === 'win32') return a === 'arm64' ? 'win32-arm64' : 'win32-x64';
80
- if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
81
- return (a === 'arm64' || a === 'aarch64') ? 'linux-arm64' : 'linux-x64';
82
- }
83
-
84
- function binaryName() {
85
- const key = platformKey();
86
- return key.startsWith('win32') ? `plugkit-${key}.exe` : `plugkit-${key}`;
87
- }
88
69
 
89
70
  function rtkBinaryName() {
90
71
  const key = platformKey();
@@ -117,98 +98,22 @@ function gmToolsDir() {
117
98
  // through node. Self-update inside the Rust binary keeps gm-tools fresh from
118
99
  // here on. Skipped silently on any error — the next session-start hook will
119
100
  // retry via ensure_tools_current.
120
- function killHoldersOfPath(targetPath) {
121
- if (process.platform !== 'win32') return 0;
122
- try {
123
- const { spawnSync } = require('child_process');
124
- const norm = path.resolve(targetPath).replace(/\//g, '\\');
125
- const r = spawnSync('wmic', ['process', 'where', `ExecutablePath='${norm.replace(/\\/g, '\\\\')}'`, 'get', 'ProcessId', '/format:value'], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
126
- if (r.status !== 0 || !r.stdout) return 0;
127
- const pids = [];
128
- for (const line of r.stdout.split(/\r?\n/)) {
129
- const m = line.match(/ProcessId=(\d+)/);
130
- if (m) {
131
- const pid = parseInt(m[1], 10);
132
- if (Number.isFinite(pid) && pid !== process.pid) pids.push(pid);
133
- }
134
- }
135
- for (const pid of pids) {
136
- try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { windowsHide: true, timeout: 3000 }); } catch (_) {}
137
- }
138
- return pids.length;
139
- } catch (_) { return 0; }
140
- }
141
101
 
142
- function cleanOrphanNewFiles(dst, exeName) {
143
- try {
144
- for (const name of fs.readdirSync(dst)) {
145
- if (name === exeName + '.new') continue;
146
- if (/^plugkit(\.\d+\.\d+\.\d+)?\.new$/i.test(name)) {
147
- try { fs.unlinkSync(path.join(dst, name)); } catch (_) {}
148
- }
149
- }
150
- } catch (_) {}
151
- }
152
-
153
- function renameWithRetry(src, dst, attempts) {
154
- for (let i = 0; i < attempts; i++) {
155
- try { fs.renameSync(src, dst); return true; }
156
- catch (err) {
157
- if (err.code !== 'EEXIST' && err.code !== 'EPERM' && err.code !== 'EBUSY' && err.code !== 'EACCES') throw err;
158
- if (i === Math.floor(attempts / 2)) killHoldersOfPath(dst);
159
- try { const { spawnSync } = require('child_process'); spawnSync(process.execPath, ['-e', 'setTimeout(()=>{}, 200)'], { timeout: 400, killSignal: 'SIGKILL', stdio: 'ignore', windowsHide: true }); } catch (_) {}
160
- }
161
- }
162
- return false;
163
- }
164
-
165
- function copyToGmTools(finalPath, wrapperDir, version) {
102
+ function copyWasmToGmTools(wasmPath, wrapperDir, version) {
166
103
  const dst = gmToolsDir();
167
104
  fs.mkdirSync(dst, { recursive: true });
168
- const exeName = process.platform === 'win32' ? 'plugkit.exe' : 'plugkit';
169
- const target = path.join(dst, exeName);
170
- const targetTmp = target + '.new';
171
- cleanOrphanNewFiles(dst, exeName);
105
+ const target = path.join(dst, 'plugkit.wasm');
172
106
  if (fs.existsSync(target)) {
173
- let needsRefresh = true;
174
107
  try {
175
108
  const cur = sha256OfFileSync(target);
176
- const src = sha256OfFileSync(finalPath);
177
- if (cur === src) needsRefresh = false;
178
- } catch (_) {}
179
- if (!needsRefresh) {
180
- try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
181
- return;
182
- }
183
- try { killHoldersOfPath(target); } catch (_) {}
184
- }
185
- fs.copyFileSync(finalPath, targetTmp);
186
- if (!renameWithRetry(targetTmp, target, 8)) {
187
- try { killHoldersOfPath(target); } catch (_) {}
188
- try { fs.unlinkSync(target); } catch (_) {}
189
- try { fs.renameSync(targetTmp, target); }
190
- catch (_) {
191
- try { fs.unlinkSync(targetTmp); } catch (_) {}
192
- obsEvent('bootstrap', 'gmtools.rename.failed', { target });
193
- writeBootstrapError({
194
- expected_version: version,
195
- cached_version: null,
196
- error_phase: 'gmtools-rename',
197
- error_message: `cannot replace ${target} after kill+unlink retry; orphan removed`,
198
- });
199
- throw new Error(`gm-tools update blocked: cannot replace ${target} (held open by running plugkit and kill-retry exhausted)`);
200
- }
201
- }
202
- try {
203
- for (const name of fs.readdirSync(dst)) {
204
- if (/^plugkit(\.\d+\.\d+\.\d+)?\.new$/i.test(name)) {
205
- try { fs.unlinkSync(path.join(dst, name)); } catch (_) {}
109
+ const src = sha256OfFileSync(wasmPath);
110
+ if (cur === src) {
111
+ try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
112
+ return;
206
113
  }
207
- }
208
- } catch (_) {}
209
- if (process.platform !== 'win32') {
210
- try { fs.chmodSync(target, 0o755); } catch (_) {}
114
+ } catch (_) {}
211
115
  }
116
+ fs.copyFileSync(wasmPath, target);
212
117
  fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
213
118
  try {
214
119
  const srcSha = path.join(wrapperDir, 'plugkit.sha256');
@@ -319,88 +224,54 @@ function sha256OfFile(filePath) {
319
224
  });
320
225
  }
321
226
 
322
- function fetchToFile(url, destPath, expectedTotal) {
323
- return new Promise((resolve, reject) => {
324
- let existing = 0;
325
- try { existing = fs.statSync(destPath).size; } catch (_) {}
326
- const headers = { 'User-Agent': 'plugkit-bootstrap', 'Accept': '*/*' };
327
- if (existing > 0) headers['Range'] = `bytes=${existing}-`;
328
-
329
- const u = new URL(url);
330
- const req = https.request({
331
- method: 'GET',
332
- hostname: u.hostname,
333
- path: u.pathname + u.search,
334
- headers,
335
- timeout: ATTEMPT_TIMEOUT_MS,
336
- }, (res) => {
337
- if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) {
338
- res.resume();
339
- return resolve(fetchToFile(res.headers.location, destPath, expectedTotal));
340
- }
341
- if (res.statusCode === 416) {
342
- res.resume();
343
- try { fs.unlinkSync(destPath); } catch (_) {}
344
- return reject(new Error('range-not-satisfiable: cleared partial, retry'));
345
- }
346
- if (!(res.statusCode === 200 || res.statusCode === 206)) {
347
- res.resume();
348
- return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
227
+ async function extractNpmPackageWasm(destPath) {
228
+ const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
229
+ try {
230
+ ensureDir(tempDir);
231
+ const startMs = Date.now();
232
+ log(`extracting npm package ${NPM_PACKAGE}@latest to ${tempDir}`);
233
+ obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version: 'latest' });
234
+
235
+ const result = spawnSync(
236
+ process.platform === 'win32' ? 'bunx.cmd' : 'bunx',
237
+ ['--bun', NPM_PACKAGE + '@latest', '--save-exact', '--prefix', tempDir],
238
+ {
239
+ stdio: ['ignore', 'pipe', 'pipe'],
240
+ timeout: ATTEMPT_TIMEOUT_MS,
241
+ encoding: 'utf8',
242
+ windowsHide: true,
349
243
  }
350
- const append = res.statusCode === 206 && existing > 0;
351
- // Ensure parent dir exists — a concurrent prune may have removed it
352
- // between lock-acquire and now. Recreating is cheap and avoids a
353
- // confusing ENOENT later.
354
- try { ensureDir(path.dirname(destPath)); } catch (_) {}
355
- const out = fs.createWriteStream(destPath, { flags: append ? 'a' : 'w' });
356
- let bytes = append ? existing : 0;
357
- let lastStderr = Date.now();
358
- let lastByte = Date.now();
359
- const fetchStart = Date.now();
360
- const safeUrl = (() => { try { const p = new URL(url); return p.hostname + p.pathname; } catch(_) { return url.split('?')[0]; } })();
361
- obsEvent('bootstrap', 'fetch.start', { url: safeUrl, resume_from: existing, status: res.statusCode });
362
- const stallTimer = setInterval(() => {
363
- if (Date.now() - lastByte > STALL_TIMEOUT_MS) {
364
- clearInterval(stallTimer);
365
- req.destroy(new Error(`stalled: no bytes for ${STALL_TIMEOUT_MS}ms`));
366
- }
367
- }, 2000);
368
- res.on('data', c => {
369
- bytes += c.length;
370
- lastByte = Date.now();
371
- if (Date.now() - lastStderr > 5000) {
372
- const pct = expectedTotal ? ` ${Math.floor(bytes / expectedTotal * 100)}%` : '';
373
- try { process.stderr.write(`[plugkit-bootstrap] downloading: ${(bytes / 1048576).toFixed(1)} MiB${pct}\n`); } catch (_) {}
374
- lastStderr = Date.now();
375
- }
376
- });
377
- res.pipe(out);
378
- out.on('finish', () => {
379
- clearInterval(stallTimer);
380
- obsEvent('bootstrap', 'fetch.end', { url: safeUrl, bytes, dur_ms: Date.now() - fetchStart, ok: true });
381
- out.close(() => resolve(bytes));
382
- });
383
- out.on('error', err => { clearInterval(stallTimer); obsEvent('bootstrap', 'fetch.end', { url: safeUrl, bytes, dur_ms: Date.now() - fetchStart, ok: false, err: String(err.message || err) }); reject(err); });
384
- res.on('error', err => { clearInterval(stallTimer); reject(err); });
385
- res.on('end', () => clearInterval(stallTimer));
386
- });
387
- req.on('timeout', () => { req.destroy(new Error(`timeout after ${ATTEMPT_TIMEOUT_MS}ms`)); });
388
- req.on('error', reject);
389
- req.end();
390
- });
244
+ );
245
+
246
+ if (result.error) throw result.error;
247
+ if (result.status !== 0) {
248
+ throw new Error(`bunx extraction failed: ${result.stderr || result.stdout || 'unknown error'}`);
249
+ }
250
+
251
+ const nodeModulesPath = path.join(tempDir, 'node_modules', NPM_PACKAGE, 'plugkit.wasm');
252
+ if (!fs.existsSync(nodeModulesPath)) {
253
+ throw new Error(`plugkit.wasm not found in extracted npm package at ${nodeModulesPath}`);
254
+ }
255
+
256
+ fs.copyFileSync(nodeModulesPath, destPath);
257
+ log(`extracted ${nodeModulesPath} ${destPath}`);
258
+ obsEvent('bootstrap', 'npm.extract.end', { dur_ms: Date.now() - startMs, ok: true });
259
+ } finally {
260
+ try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 }); } catch (_) {}
261
+ }
391
262
  }
392
263
 
393
- async function downloadWithRetry(url, destPath) {
264
+ async function extractNpmPackageWithRetry(destPath) {
394
265
  let lastErr;
395
266
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
396
267
  try {
397
- log(`fetch attempt ${attempt}/${MAX_ATTEMPTS}: ${url}`);
398
- await fetchToFile(url, destPath);
268
+ log(`npm extract attempt ${attempt}/${MAX_ATTEMPTS}: ${NPM_PACKAGE}@latest`);
269
+ await extractNpmPackageWasm(destPath);
399
270
  return;
400
271
  } catch (err) {
401
272
  lastErr = err;
402
273
  log(`attempt ${attempt} failed: ${err.message}`);
403
- obsEvent('bootstrap', 'fetch.attempt_failed', { url, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
274
+ obsEvent('bootstrap', 'npm.extract.attempt_failed', { package: NPM_PACKAGE, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
404
275
  if (attempt < MAX_ATTEMPTS) {
405
276
  const wait = BACKOFF_MS[attempt - 1] || 120000;
406
277
  log(`backing off ${wait}ms`);
@@ -447,8 +318,8 @@ async function bootstrap(opts) {
447
318
  const wrapperDir = opts.wrapperDir || __dirname;
448
319
  const version = opts.version || readVersionFile(wrapperDir);
449
320
  const shaManifest = readShaManifest(wrapperDir);
450
- const binName = binaryName();
451
- const expectedSha = shaManifest ? shaManifest[binName] : null;
321
+ const wasmName = 'plugkit.wasm';
322
+ const wasmExpectedSha = shaManifest ? shaManifest[wasmName] : null;
452
323
 
453
324
  let root = cacheRoot();
454
325
  try { ensureDir(root); }
@@ -457,154 +328,116 @@ async function bootstrap(opts) {
457
328
  const verDir = path.join(root, `v${version}`);
458
329
  ensureDir(verDir);
459
330
 
460
- const finalPath = path.join(verDir, binName);
461
- const okSentinel = path.join(verDir, '.ok');
462
- const partialPath = `${finalPath}.partial`;
463
-
464
- const wasmName = 'plugkit.wasm';
465
- const wasmExpectedSha = shaManifest ? shaManifest[wasmName] : null;
466
331
  const wasmFinalPath = path.join(verDir, wasmName);
332
+ const wasmOkSentinel = path.join(verDir, '.wasm-ok');
467
333
  const wasmPartialPath = `${wasmFinalPath}.partial`;
468
334
 
469
- if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
470
- if (expectedSha) {
471
- const actualSha = sha256OfFileSync(finalPath);
472
- if (actualSha === expectedSha) {
473
- obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path: finalPath });
474
- copyToGmTools(finalPath, wrapperDir, version);
335
+ if (fs.existsSync(wasmFinalPath) && fs.existsSync(wasmOkSentinel)) {
336
+ if (wasmExpectedSha) {
337
+ const actualSha = sha256OfFileSync(wasmFinalPath);
338
+ if (actualSha === wasmExpectedSha) {
339
+ obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path: wasmFinalPath });
340
+ copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
475
341
  clearBootstrapError();
476
- return finalPath;
342
+ return wasmFinalPath;
477
343
  }
478
- log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${expectedSha.slice(0,12)}… got ${(actualSha||'').slice(0,12)}…)`);
344
+ log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${wasmExpectedSha.slice(0,12)}… got ${(actualSha||'').slice(0,12)}…)`);
479
345
  writeBootstrapError({
480
346
  expected_version: version,
481
347
  cached_version: null,
482
348
  error_phase: 'cache-hit-sha-mismatch',
483
- error_message: `cached binary at ${finalPath} sha=${actualSha} but manifest expects ${expectedSha}`,
349
+ error_message: `cached wasm at ${wasmFinalPath} sha=${actualSha} but manifest expects ${wasmExpectedSha}`,
484
350
  });
485
- try { fs.unlinkSync(finalPath); } catch (_) {}
486
- try { fs.unlinkSync(okSentinel); } catch (_) {}
351
+ try { fs.unlinkSync(wasmFinalPath); } catch (_) {}
352
+ try { fs.unlinkSync(wasmOkSentinel); } catch (_) {}
487
353
  } else {
488
- obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path: finalPath });
489
- copyToGmTools(finalPath, wrapperDir, version);
354
+ obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path: wasmFinalPath });
355
+ copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
490
356
  clearBootstrapError();
491
- return finalPath;
357
+ return wasmFinalPath;
492
358
  }
493
359
  }
494
360
 
495
- if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit')) {
496
- obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path: finalPath });
361
+ if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, wasmOkSentinel, wasmPartialPath, 'wasm')) {
362
+ obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path: wasmFinalPath });
497
363
  spawnDetachedRtkFetch(wrapperDir);
498
- copyToGmTools(finalPath, wrapperDir, version);
364
+ copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
499
365
  clearBootstrapError();
500
- return finalPath;
366
+ return wasmFinalPath;
501
367
  }
502
368
 
503
369
  const lockPath = path.join(verDir, '.lock');
504
370
  acquireLock(lockPath);
505
371
  try {
506
- if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
507
- obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path: finalPath });
508
- copyToGmTools(finalPath, wrapperDir, version);
372
+ if (fs.existsSync(wasmFinalPath) && fs.existsSync(wasmOkSentinel)) {
373
+ obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path: wasmFinalPath });
374
+ copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
509
375
  clearBootstrapError();
510
- return finalPath;
376
+ return wasmFinalPath;
511
377
  }
512
- if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit')) {
513
- obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path: finalPath });
378
+ if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, wasmOkSentinel, wasmPartialPath, 'wasm')) {
379
+ obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path: wasmFinalPath });
514
380
  spawnDetachedRtkFetch(wrapperDir);
515
- copyToGmTools(finalPath, wrapperDir, version);
381
+ copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
516
382
  clearBootstrapError();
517
- return finalPath;
383
+ return wasmFinalPath;
518
384
  }
519
385
 
520
- if (fs.existsSync(partialPath)) {
386
+ if (fs.existsSync(wasmPartialPath)) {
521
387
  try {
522
- const st = fs.statSync(partialPath);
388
+ const st = fs.statSync(wasmPartialPath);
523
389
  if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
524
- fs.unlinkSync(partialPath);
525
- log(`cleared stale partial: ${partialPath}`);
390
+ fs.unlinkSync(wasmPartialPath);
391
+ log(`cleared stale partial: ${wasmPartialPath}`);
526
392
  }
527
393
  } catch (_) {}
528
394
  }
529
- const url = `https://github.com/${RELEASE_REPO}/releases/download/v${version}/${binName}`;
530
395
  try {
531
- await downloadWithRetry(url, partialPath);
532
- } catch (fetchErr) {
396
+ await extractNpmPackageWithRetry(wasmPartialPath);
397
+ } catch (extractErr) {
533
398
  writeBootstrapError({
534
399
  expected_version: version,
535
400
  cached_version: null,
536
- error_phase: 'download',
537
- error_message: fetchErr && fetchErr.message ? fetchErr.message : String(fetchErr),
401
+ error_phase: 'npm-extract',
402
+ error_message: extractErr && extractErr.message ? extractErr.message : String(extractErr),
538
403
  });
539
- throw fetchErr;
404
+ throw extractErr;
540
405
  }
541
406
 
542
- if (expectedSha) {
543
- const got = await sha256OfFile(partialPath);
544
- if (got !== expectedSha) {
545
- try { fs.unlinkSync(partialPath); } catch (_) {}
407
+ if (wasmExpectedSha) {
408
+ const got = await sha256OfFile(wasmPartialPath);
409
+ if (got !== wasmExpectedSha) {
410
+ try { fs.unlinkSync(wasmPartialPath); } catch (_) {}
546
411
  writeBootstrapError({
547
412
  expected_version: version,
548
413
  cached_version: null,
549
414
  error_phase: 'sha256-mismatch',
550
- error_message: `sha256 mismatch for ${binName}: expected ${expectedSha}, got ${got}`,
415
+ error_message: `sha256 mismatch for ${wasmName}: expected ${wasmExpectedSha}, got ${got}`,
551
416
  });
552
- throw new Error(`sha256 mismatch for ${binName}: expected ${expectedSha}, got ${got}`);
417
+ throw new Error(`sha256 mismatch for ${wasmName}: expected ${wasmExpectedSha}, got ${got}`);
553
418
  }
554
419
  log('sha256 verified');
555
420
  } else {
556
421
  log('no sha256 manifest — skipping verify');
557
422
  }
558
423
 
559
- try { fs.renameSync(partialPath, finalPath); }
424
+ try { fs.renameSync(wasmPartialPath, wasmFinalPath); }
560
425
  catch (err) {
561
426
  if (err.code === 'EEXIST' || err.code === 'EPERM') {
562
- try { fs.unlinkSync(finalPath); } catch (_) {}
563
- fs.renameSync(partialPath, finalPath);
427
+ try { fs.unlinkSync(wasmFinalPath); } catch (_) {}
428
+ fs.renameSync(wasmPartialPath, wasmFinalPath);
564
429
  } else throw err;
565
430
  }
566
431
 
567
- if (os.platform() !== 'win32') {
568
- try { fs.chmodSync(finalPath, 0o755); } catch (_) {}
569
- }
570
-
571
- fs.writeFileSync(okSentinel, new Date().toISOString());
572
- log(`decision: fetch reason: install-complete (${finalPath})`);
573
- obsEvent('bootstrap', 'install.done', { path: finalPath, version, kind: 'plugkit' });
574
- proactiveKillForNewInstall(version, finalPath);
432
+ fs.writeFileSync(wasmOkSentinel, new Date().toISOString());
433
+ log(`decision: fetch reason: install-complete (${wasmFinalPath})`);
434
+ obsEvent('bootstrap', 'install.done', { path: wasmFinalPath, version, kind: 'wasm' });
575
435
  pruneOldVersions(root, version, readRtkVersion(wrapperDir));
576
436
  spawnDetachedRtkFetch(wrapperDir);
577
- copyToGmTools(finalPath, wrapperDir, version);
578
-
579
- try {
580
- if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, path.join(verDir, '.wasm-ok'), wasmPartialPath, 'wasm')) {
581
- obsEvent('bootstrap', 'wasm.heal', { path: wasmFinalPath });
582
- } else {
583
- const wasmUrl = `https://github.com/${RELEASE_REPO}/releases/download/v${version}/${wasmName}`;
584
- try {
585
- await downloadWithRetry(wasmUrl, wasmPartialPath);
586
- if (wasmExpectedSha) {
587
- const wasmGot = await sha256OfFile(wasmPartialPath);
588
- if (wasmGot !== wasmExpectedSha) {
589
- try { fs.unlinkSync(wasmPartialPath); } catch (_) {}
590
- log(`wasm sha256 mismatch: expected ${wasmExpectedSha}, got ${wasmGot}`);
591
- } else {
592
- try { fs.renameSync(wasmPartialPath, wasmFinalPath); } catch (e) {
593
- if (e.code === 'EEXIST') { try { fs.unlinkSync(wasmFinalPath); } catch (_) {} fs.renameSync(wasmPartialPath, wasmFinalPath); }
594
- }
595
- fs.writeFileSync(path.join(verDir, '.wasm-ok'), new Date().toISOString());
596
- obsEvent('bootstrap', 'wasm.install', { path: wasmFinalPath });
597
- }
598
- }
599
- } catch (err) {
600
- log(`wasm download failed (non-fatal): ${err.message}`);
601
- }
602
- }
603
- try { fs.copyFileSync(wasmFinalPath, path.join(gmToolsDir(), wasmName)); } catch (_) {}
604
- } catch (_) {}
437
+ copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
605
438
 
606
439
  clearBootstrapError();
607
- return finalPath;
440
+ return wasmFinalPath;
608
441
  } finally {
609
442
  releaseLock(lockPath);
610
443
  }
@@ -649,8 +482,33 @@ async function bootstrapRtk(plugkitVerDir, plugkitVersion, wrapperDir, silent, r
649
482
  if (!silent) log(`rtk cache heal (sha match): ${rtkPath}`);
650
483
  return rtkPath;
651
484
  }
652
- const url = `https://github.com/${RELEASE_REPO}/releases/download/v${plugkitVersion}/${rtkName}`;
653
- await downloadWithRetry(url, tmp);
485
+ const RTKS_RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
486
+ const url = `https://github.com/${RTKS_RELEASE_REPO}/releases/download/v${plugkitVersion}/${rtkName}`;
487
+ const startMs = Date.now();
488
+ let lastErr;
489
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
490
+ try {
491
+ log(`rtk download attempt ${attempt}/${MAX_ATTEMPTS}: ${url}`);
492
+ const result = spawnSync(
493
+ 'curl',
494
+ ['-fSL', '--max-time', String(Math.floor(ATTEMPT_TIMEOUT_MS / 1000)), '-o', tmp, url],
495
+ { stdio: 'pipe', timeout: ATTEMPT_TIMEOUT_MS + 5000, windowsHide: true }
496
+ );
497
+ if (result.error) throw result.error;
498
+ if (result.status !== 0) throw new Error(`curl failed with status ${result.status}`);
499
+ break;
500
+ } catch (err) {
501
+ lastErr = err;
502
+ log(`rtk attempt ${attempt} failed: ${err.message}`);
503
+ obsEvent('bootstrap', 'rtk.download.attempt_failed', { attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
504
+ if (attempt < MAX_ATTEMPTS) {
505
+ const wait = BACKOFF_MS[attempt - 1] || 120000;
506
+ log(`backing off ${wait}ms`);
507
+ await new Promise(r => setTimeout(r, wait));
508
+ }
509
+ }
510
+ }
511
+ if (lastErr) throw lastErr;
654
512
  if (expected) {
655
513
  const got = await sha256OfFile(tmp);
656
514
  if (got !== expected) {
@@ -668,7 +526,7 @@ async function bootstrapRtk(plugkitVerDir, plugkitVersion, wrapperDir, silent, r
668
526
  if (os.platform() !== 'win32') { try { fs.chmodSync(rtkPath, 0o755); } catch (_) {} }
669
527
  fs.writeFileSync(rtkOk, new Date().toISOString());
670
528
  log(`installed ${rtkPath}`);
671
- obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion(wrapperDir) || plugkitVersion, kind: 'rtk' });
529
+ obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion(wrapperDir) || plugkitVersion, kind: 'rtk', dur_ms: Date.now() - startMs });
672
530
  return rtkPath;
673
531
  }
674
532
 
@@ -688,7 +546,7 @@ function resolveCachedRtk(opts) {
688
546
  return null;
689
547
  }
690
548
 
691
- function resolveCachedBinary(opts) {
549
+ function getWasmPath(opts) {
692
550
  opts = opts || {};
693
551
  const wrapperDir = opts.wrapperDir || __dirname;
694
552
  const version = opts.version || readVersionFile(wrapperDir);
@@ -697,9 +555,9 @@ function resolveCachedBinary(opts) {
697
555
  catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
698
556
  })();
699
557
  const verDir = path.join(root, `v${version}`);
700
- const finalPath = path.join(verDir, binaryName());
701
- const okSentinel = path.join(verDir, '.ok');
702
- if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) return finalPath;
558
+ const wasmPath = path.join(verDir, 'plugkit.wasm');
559
+ const okSentinel = path.join(verDir, '.wasm-ok');
560
+ if (fs.existsSync(wasmPath) && fs.existsSync(okSentinel)) return wasmPath;
703
561
  return null;
704
562
  }
705
563
 
@@ -762,72 +620,6 @@ function killRunningDaemons(reason) {
762
620
  return killedPids;
763
621
  }
764
622
 
765
- function listRunningPlugkitImagePaths() {
766
- const out = [];
767
- try {
768
- const { spawnSync } = require('child_process');
769
- if (os.platform() === 'win32') {
770
- let parsed = null;
771
- try {
772
- const p = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', "Get-Process plugkit* -ErrorAction SilentlyContinue | Select-Object Id,Path | ConvertTo-Json -Compress"], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
773
- const text = ((p && p.stdout) || '').trim();
774
- if (text) {
775
- const j = JSON.parse(text);
776
- parsed = Array.isArray(j) ? j : [j];
777
- }
778
- } catch (_) {}
779
- if (parsed) {
780
- for (const item of parsed) {
781
- if (!item) continue;
782
- const pid = parseInt(item.Id, 10);
783
- if (!Number.isFinite(pid)) continue;
784
- out.push({ pid, path: (item.Path || '').trim() });
785
- }
786
- } else {
787
- const r = spawnSync('tasklist', ['/FI', 'IMAGENAME eq plugkit*', '/FO', 'CSV', '/NH'], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
788
- const text = (r && r.stdout) || '';
789
- const seen = new Set();
790
- for (const line of text.split(/\r?\n/)) {
791
- const m = line.match(/^"([^"]+)","(\d+)"/);
792
- if (!m) continue;
793
- const pid = parseInt(m[2], 10);
794
- if (!Number.isFinite(pid) || seen.has(pid)) continue;
795
- seen.add(pid);
796
- out.push({ pid, path: '' });
797
- }
798
- }
799
- } else if (os.platform() === 'linux') {
800
- let entries = [];
801
- try { entries = fs.readdirSync('/proc'); } catch (_) {}
802
- for (const e of entries) {
803
- if (!/^\d+$/.test(e)) continue;
804
- const pid = parseInt(e, 10);
805
- let comm = '';
806
- try { comm = fs.readFileSync(`/proc/${pid}/comm`, 'utf8').trim(); } catch (_) { continue; }
807
- if (!/^plugkit/i.test(comm)) continue;
808
- let imagePath = '';
809
- try { imagePath = fs.readlinkSync(`/proc/${pid}/exe`); } catch (_) {}
810
- out.push({ pid, path: imagePath });
811
- }
812
- } else {
813
- const r = spawnSync('ps', ['-axo', 'pid=,comm='], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
814
- const text = (r && r.stdout) || '';
815
- for (const line of text.split(/\r?\n/)) {
816
- const m = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
817
- if (!m) continue;
818
- if (!/plugkit/i.test(m[2])) continue;
819
- const pid = parseInt(m[1], 10);
820
- let imagePath = '';
821
- try {
822
- const p = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
823
- imagePath = ((p && p.stdout) || '').trim().split(/\s+/)[0] || '';
824
- } catch (_) {}
825
- out.push({ pid, path: imagePath });
826
- }
827
- }
828
- } catch (_) {}
829
- return out;
830
- }
831
623
 
832
624
  function killSpoolWatcherInCwd(reason) {
833
625
  try {
@@ -844,39 +636,21 @@ function killSpoolWatcherInCwd(reason) {
844
636
  return null;
845
637
  }
846
638
 
847
- function proactiveKillForNewInstall(installedVersion, finalPath) {
639
+ function proactiveKillForNewInstall(installedVersion) {
848
640
  try {
849
641
  const reason = `install:v${installedVersion}`;
850
- const target = finalPath ? path.resolve(finalPath).toLowerCase() : null;
851
- const cacheRootNorm = (() => {
852
- try { return path.resolve(cacheRoot()).toLowerCase(); } catch (_) { return null; }
853
- })();
854
- const procs = listRunningPlugkitImagePaths();
855
- for (const { pid, path: imagePath } of procs) {
856
- if (!Number.isFinite(pid) || pid === process.pid) continue;
857
- if (!imagePath) continue;
858
- const norm = path.resolve(imagePath).toLowerCase();
859
- if (target && norm === target) continue;
860
- if (!cacheRootNorm || !norm.startsWith(cacheRootNorm + path.sep.toLowerCase())) continue;
861
- if (killPid(pid)) {
862
- try { process.stderr.write(`[bootstrap] killed stale daemon pid=${pid} path=${imagePath} (current install: v${installedVersion})\n`); } catch (_) {}
863
- obsEvent('bootstrap', 'daemon.killed', { pid, oldPath: imagePath, installedVersion, mechanism: 'process-path' });
864
- }
865
- }
866
642
  killRunningDaemons(reason);
867
643
  killSpoolWatcherInCwd(reason);
868
644
  writeDaemonVersion(installedVersion);
869
645
  } catch (_) {}
870
646
  }
871
647
 
872
- // Compare wrapper-pinned version against last-recorded daemon version. If
873
- // they differ, kill the daemon so it respawns under the new binary.
874
648
  function killStaleDaemonIfVersionChanged(wrapperDir) {
875
649
  let currentVersion;
876
650
  try { currentVersion = readVersionFile(wrapperDir); } catch (_) { return; }
877
- const cached = resolveCachedBinary({ wrapperDir, version: currentVersion });
651
+ const cached = getWasmPath({ wrapperDir, version: currentVersion });
878
652
  if (cached) {
879
- proactiveKillForNewInstall(currentVersion, cached);
653
+ proactiveKillForNewInstall(currentVersion);
880
654
  return;
881
655
  }
882
656
  const recorded = readDaemonVersion();
@@ -885,7 +659,7 @@ function killStaleDaemonIfVersionChanged(wrapperDir) {
885
659
  writeDaemonVersion(currentVersion);
886
660
  }
887
661
 
888
- module.exports = { bootstrap, resolveCachedBinary, resolveCachedRtk, platformKey, binaryName, rtkBinaryName, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
662
+ module.exports = { bootstrap, getWasmPath, resolveCachedRtk, rtkBinaryName, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
889
663
 
890
664
  if (require.main === module) {
891
665
  const argv = process.argv.slice(2);
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1070",
3
+ "version": "2.0.1071",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-thebird",
3
- "version": "2.0.1070",
3
+ "version": "2.0.1071",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1070",
3
+ "version": "2.0.1071",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": {
6
6
  "name": "AnEntrypoint",