pinokiod 3.290.0 → 3.292.0

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.
@@ -100,6 +100,8 @@ async function readXcodeSelectVersion(exec) {
100
100
  return { valid: false, reason: 'xcode-select --version failed' }
101
101
  }
102
102
 
103
+ console.log('xcode-select --version', result)
104
+
103
105
  const match = result && result.stdout && /xcode-select version\s+(\d+)/i.exec(result.stdout)
104
106
  if (!match) {
105
107
  return { valid: false, reason: 'unable to parse xcode-select version' }
package/kernel/util.js CHANGED
@@ -1107,4 +1107,5 @@ module.exports = {
1107
1107
  symlink,
1108
1108
  file_type,
1109
1109
  registerPushListener,
1110
+ emitPushEvent,
1110
1111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.290.0",
3
+ "version": "3.292.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -128,6 +128,110 @@ class Server {
128
128
 
129
129
 
130
130
  // process.env.CONDA_LIBMAMBA_SOLVER_DEBUG_LIBSOLV = 1
131
+ this.installFatalHandlers()
132
+ }
133
+ installFatalHandlers() {
134
+ if (this.fatalHandlersInstalled) {
135
+ return
136
+ }
137
+ const normalizeError = (value, origin) => {
138
+ if (value instanceof Error) {
139
+ return value
140
+ }
141
+ if (value && typeof value === 'object') {
142
+ try {
143
+ return new Error(`${origin || 'error'}: ${JSON.stringify(value)}`)
144
+ } catch (_) {
145
+ return new Error(String(value))
146
+ }
147
+ }
148
+ if (typeof value === 'string') {
149
+ return new Error(value)
150
+ }
151
+ return new Error(`${origin || 'error'}: ${String(value)}`)
152
+ }
153
+ const invoke = (value, origin) => {
154
+ const error = normalizeError(value, origin)
155
+ try {
156
+ const maybePromise = this.handleFatalError(error, origin)
157
+ if (maybePromise && typeof maybePromise.catch === 'function') {
158
+ maybePromise.catch((fatalErr) => {
159
+ console.error('Fatal handler rejection:', fatalErr)
160
+ try {
161
+ process.exit(1)
162
+ } catch (_) {
163
+ // ignore
164
+ }
165
+ })
166
+ }
167
+ } catch (fatalErr) {
168
+ console.error('Fatal handler threw:', fatalErr)
169
+ try {
170
+ process.exit(1)
171
+ } catch (_) {
172
+ // ignore
173
+ }
174
+ }
175
+ }
176
+ process.on('uncaughtException', (error) => invoke(error, 'uncaughtException'))
177
+ process.on('unhandledRejection', (reason) => invoke(reason, 'unhandledRejection'))
178
+ this.fatalHandlersInstalled = true
179
+ }
180
+ async handleFatalError(error, origin) {
181
+ if (this.handlingFatalError) {
182
+ console.error(`[Pinokiod] Additional fatal (${origin})`, (error && error.stack) ? error.stack : error)
183
+ return
184
+ }
185
+ this.handlingFatalError = true
186
+ const timestamp = Date.now()
187
+ const message = (error && error.message) ? error.message : 'Unexpected fatal error'
188
+ const stack = (error && error.stack) ? error.stack : String(error || 'Unknown fatal error')
189
+ console.error(`[Pinokiod] Fatal (${origin})`, stack)
190
+ const fallbackHome = path.resolve(os.homedir(), 'pinokio')
191
+ const homeDir = (this.kernel && this.kernel.homedir) ? this.kernel.homedir : fallbackHome
192
+ const fatalFile = path.resolve(homeDir, 'logs', 'fatal.json')
193
+ const payload = {
194
+ id: `fatal-${timestamp}`,
195
+ type: 'kernel.fatal',
196
+ severity: 'fatal',
197
+ title: 'Pinokio crashed',
198
+ message,
199
+ stack,
200
+ origin,
201
+ timestamp,
202
+ version: this.version,
203
+ pid: process.pid,
204
+ logPath: fatalFile,
205
+ }
206
+ try {
207
+ await fs.promises.mkdir(path.dirname(fatalFile), { recursive: true })
208
+ await fs.promises.writeFile(fatalFile, JSON.stringify(payload, null, 2))
209
+ } catch (err) {
210
+ console.error('Failed to persist fatal error details:', err)
211
+ }
212
+ try {
213
+ if (typeof Util.emitPushEvent === 'function') {
214
+ Util.emitPushEvent(payload)
215
+ } else {
216
+ Util.push({ title: 'Pinokio crashed', message })
217
+ }
218
+ } catch (err) {
219
+ console.error('Failed to emit fatal notification:', err)
220
+ }
221
+ if (!this.fatalExitTimer) {
222
+ this.fatalExitTimer = setTimeout(() => {
223
+ try {
224
+ this.shutdown('Fatal Error')
225
+ } catch (shutdownErr) {
226
+ console.error('Failed to shutdown after fatal error:', shutdownErr)
227
+ }
228
+ try {
229
+ process.exit(1)
230
+ } catch (_) {
231
+ // ignore
232
+ }
233
+ }, 500)
234
+ }
131
235
  }
132
236
  stop() {
133
237
  this.server.close()
@@ -3474,9 +3578,6 @@ class Server {
3474
3578
  if (!this.log) {
3475
3579
  this.log = fs.createWriteStream(path.resolve(homedir, "logs/stdout.txt"))
3476
3580
  process.stdout.write = process.stderr.write = this.log.write.bind(this.log)
3477
- process.on('uncaughtException', (err) => {
3478
- console.error((err && err.stack) ? err.stack : err);
3479
- });
3480
3581
  this.logInterval = setInterval(async () => {
3481
3582
  try {
3482
3583
  let file = path.resolve(homedir, "logs/stdout.txt")
@@ -8737,6 +8838,7 @@ class Server {
8737
8838
  // })
8738
8839
 
8739
8840
 
8841
+
8740
8842
  // install
8741
8843
  this.server = httpserver.createServer(this.app);
8742
8844
  this.socket = new Socket(this)
@@ -1662,6 +1662,12 @@ if (typeof hotkeys === 'function') {
1662
1662
  let currentSocket = null;
1663
1663
  let reconnectTimeout = null;
1664
1664
  let activeAudio = null;
1665
+ const fatalStorageKey = 'pinokio.kernel.fatal';
1666
+ const fatalStaleMs = 15 * 60 * 1000;
1667
+ let lastFatalPayload = null;
1668
+ let fatalOverlayEl = null;
1669
+ let fatalStyleInjected = false;
1670
+ let pendingFatalRender = null;
1665
1671
 
1666
1672
  // Lightweight visual indicator to confirm notification receipt (mobile-friendly)
1667
1673
  let notifyIndicatorEl = null;
@@ -1721,6 +1727,284 @@ if (typeof hotkeys === 'function') {
1721
1727
  } catch (_) {}
1722
1728
  };
1723
1729
 
1730
+ const runWhenDomReady = (fn) => {
1731
+ if (typeof fn !== 'function') {
1732
+ return;
1733
+ }
1734
+ if (document.readyState === 'loading') {
1735
+ document.addEventListener('DOMContentLoaded', () => fn(), { once: true });
1736
+ } else {
1737
+ fn();
1738
+ }
1739
+ };
1740
+
1741
+ const ensureFatalOverlay = () => {
1742
+ if (!fatalStyleInjected) {
1743
+ try {
1744
+ const style = document.createElement('style');
1745
+ style.textContent = `
1746
+ .pinokio-fatal-overlay{position:fixed;inset:0;background:rgba(15,23,42,0.94);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;z-index:2147483646;opacity:0;pointer-events:none;transition:opacity .25s ease}
1747
+ .pinokio-fatal-overlay.show{opacity:1;pointer-events:auto}
1748
+ .pinokio-fatal-panel{max-width:960px;width:100%;background:#0f172a;color:#f8fafc;border-radius:18px;box-shadow:0 40px 120px rgba(0,0,0,.55);padding:24px;display:flex;flex-direction:column;gap:16px;border:1px solid rgba(148,163,184,.35);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif}
1749
+ .pinokio-fatal-header{display:flex;flex-wrap:wrap;justify-content:space-between;gap:12px;align-items:flex-start}
1750
+ .pinokio-fatal-header h2{margin:0;font-size:20px;line-height:1.3;font-weight:700}
1751
+ .pinokio-fatal-header small{display:block;margin-top:4px;color:rgba(226,232,240,.85);font-size:13px}
1752
+ .pinokio-fatal-message{font-size:15px;line-height:1.6;margin:0;color:#cbd5f5}
1753
+ .pinokio-fatal-stack{background:#020617;color:#f1f5f9;font-family:ui-monospace,SFMono-Regular,Consolas,Menlo,monospace;font-size:13px;line-height:1.45;border-radius:12px;padding:16px;max-height:320px;overflow:auto;border:1px solid rgba(15,118,110,.4)}
1754
+ .pinokio-fatal-meta{font-size:13px;color:#cbd5f5;display:flex;flex-wrap:wrap;gap:12px}
1755
+ .pinokio-fatal-actions{display:flex;flex-wrap:wrap;gap:10px}
1756
+ .pinokio-fatal-actions button{border:none;border-radius:999px;padding:10px 18px;font-weight:600;font-size:14px;cursor:pointer;transition:opacity .2s ease}
1757
+ .pinokio-fatal-actions button.primary{background:#f97316;color:#0f172a}
1758
+ .pinokio-fatal-actions button.secondary{background:rgba(148,163,184,.2);color:#e2e8f0}
1759
+ .pinokio-fatal-actions button:hover{opacity:.9}
1760
+ .pinokio-fatal-close{background:none;border:none;color:#e2e8f0;font-size:24px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:8px}
1761
+ .pinokio-fatal-close:hover{background:rgba(148,163,184,.15)}
1762
+ @media (max-width:720px){.pinokio-fatal-panel{padding:18px}.pinokio-fatal-stack{max-height:220px;font-size:12px}}
1763
+ `;
1764
+ document.head.appendChild(style);
1765
+ fatalStyleInjected = true;
1766
+ } catch (err) {
1767
+ console.warn('Failed to inject fatal overlay styles:', err);
1768
+ }
1769
+ }
1770
+ if (!fatalOverlayEl) {
1771
+ try {
1772
+ const wrapper = document.createElement('div');
1773
+ wrapper.className = 'pinokio-fatal-overlay';
1774
+ wrapper.setAttribute('role', 'alertdialog');
1775
+ wrapper.setAttribute('aria-live', 'assertive');
1776
+ wrapper.innerHTML = `
1777
+ <div class="pinokio-fatal-panel">
1778
+ <div class="pinokio-fatal-header">
1779
+ <div>
1780
+ <h2>Pinokio crashed</h2>
1781
+ <small data-field="subtitle"></small>
1782
+ </div>
1783
+ <button class="pinokio-fatal-close" type="button" aria-label="Dismiss crash message" data-action="fatal-dismiss">×</button>
1784
+ </div>
1785
+ <p class="pinokio-fatal-message" data-field="message"></p>
1786
+ <pre class="pinokio-fatal-stack" data-field="stack"></pre>
1787
+ <div class="pinokio-fatal-meta">
1788
+ <span data-field="timestamp"></span>
1789
+ <span data-field="logPath"></span>
1790
+ </div>
1791
+ <div class="pinokio-fatal-actions">
1792
+ <button type="button" class="secondary" data-action="fatal-copy">Copy stack</button>
1793
+ <button type="button" class="secondary" data-action="fatal-dismiss">Dismiss</button>
1794
+ <button type="button" class="primary" data-action="fatal-reload">Reload</button>
1795
+ </div>
1796
+ </div>`;
1797
+ document.body.appendChild(wrapper);
1798
+ wrapper.addEventListener('click', (event) => {
1799
+ const action = (event.target && event.target.getAttribute) ? event.target.getAttribute('data-action') : null;
1800
+ if (!action) {
1801
+ return;
1802
+ }
1803
+ if (action === 'fatal-copy') {
1804
+ copyFatalDetails();
1805
+ } else if (action === 'fatal-dismiss') {
1806
+ dismissFatalNotice();
1807
+ } else if (action === 'fatal-reload') {
1808
+ dismissFatalNotice();
1809
+ try {
1810
+ window.location.reload();
1811
+ } catch (_) {}
1812
+ }
1813
+ });
1814
+ fatalOverlayEl = wrapper;
1815
+ } catch (err) {
1816
+ console.error('Failed to create fatal overlay:', err);
1817
+ }
1818
+ }
1819
+ };
1820
+
1821
+ const updateFatalOverlayContent = (payload) => {
1822
+ if (!fatalOverlayEl || !payload) {
1823
+ return;
1824
+ }
1825
+ try {
1826
+ const messageNode = fatalOverlayEl.querySelector('[data-field="message"]');
1827
+ if (messageNode) {
1828
+ messageNode.textContent = payload.message || 'An unrecoverable error occurred.';
1829
+ }
1830
+ const stackNode = fatalOverlayEl.querySelector('[data-field="stack"]');
1831
+ if (stackNode) {
1832
+ stackNode.textContent = payload.stack || 'No stack trace available';
1833
+ }
1834
+ const subtitleNode = fatalOverlayEl.querySelector('[data-field="subtitle"]');
1835
+ if (subtitleNode) {
1836
+ const origin = payload.origin ? payload.origin : 'fatal error';
1837
+ const parts = [];
1838
+ if (payload.version && typeof payload.version === 'object') {
1839
+ const pinokiod = payload.version.pinokiod ? `pinokiod ${payload.version.pinokiod}` : null;
1840
+ const pinokio = payload.version.pinokio ? `pinokio ${payload.version.pinokio}` : null;
1841
+ if (pinokiod || pinokio) {
1842
+ parts.push([pinokiod, pinokio].filter(Boolean).join(' • '));
1843
+ }
1844
+ }
1845
+ parts.push(origin);
1846
+ subtitleNode.textContent = parts.join(' • ');
1847
+ }
1848
+ const stampNode = fatalOverlayEl.querySelector('[data-field="timestamp"]');
1849
+ if (stampNode) {
1850
+ const ts = payload.timestamp ? new Date(payload.timestamp) : new Date();
1851
+ stampNode.textContent = `Recorded: ${ts.toLocaleString()}`;
1852
+ }
1853
+ const logNode = fatalOverlayEl.querySelector('[data-field="logPath"]');
1854
+ if (logNode) {
1855
+ logNode.textContent = payload.logPath ? `Details saved to ${payload.logPath}` : '';
1856
+ }
1857
+ } catch (err) {
1858
+ console.error('Failed to populate fatal overlay:', err);
1859
+ }
1860
+ };
1861
+
1862
+ const showFatalOverlay = (payload) => {
1863
+ lastFatalPayload = payload;
1864
+ const render = (data) => {
1865
+ ensureFatalOverlay();
1866
+ if (!fatalOverlayEl) {
1867
+ return;
1868
+ }
1869
+ const content = data || payload;
1870
+ updateFatalOverlayContent(content);
1871
+ fatalOverlayEl.classList.add('show');
1872
+ };
1873
+ if (document.readyState === 'loading' || !document.body) {
1874
+ pendingFatalRender = payload;
1875
+ runWhenDomReady(() => {
1876
+ if (pendingFatalRender) {
1877
+ const queued = pendingFatalRender;
1878
+ pendingFatalRender = null;
1879
+ render(queued);
1880
+ }
1881
+ });
1882
+ } else {
1883
+ render();
1884
+ }
1885
+ };
1886
+
1887
+ const hideFatalOverlay = () => {
1888
+ if (fatalOverlayEl) {
1889
+ fatalOverlayEl.classList.remove('show');
1890
+ }
1891
+ };
1892
+
1893
+ const dismissFatalNotice = () => {
1894
+ hideFatalOverlay();
1895
+ try {
1896
+ if (storageEnabled) {
1897
+ localStorage.removeItem(fatalStorageKey);
1898
+ }
1899
+ } catch (_) {}
1900
+ };
1901
+
1902
+ const copyFatalDetails = () => {
1903
+ if (!lastFatalPayload) {
1904
+ return;
1905
+ }
1906
+ const lines = [];
1907
+ lines.push(lastFatalPayload.title || 'Pinokio crashed');
1908
+ lines.push(`When: ${new Date(lastFatalPayload.timestamp || Date.now()).toLocaleString()}`);
1909
+ if (lastFatalPayload.origin) {
1910
+ lines.push(`Origin: ${lastFatalPayload.origin}`);
1911
+ }
1912
+ if (lastFatalPayload.logPath) {
1913
+ lines.push(`Saved at: ${lastFatalPayload.logPath}`);
1914
+ }
1915
+ lines.push('');
1916
+ lines.push(lastFatalPayload.message || '');
1917
+ lines.push('');
1918
+ lines.push(lastFatalPayload.stack || '');
1919
+ const text = lines.join('\n');
1920
+ const fallbackCopy = () => {
1921
+ try {
1922
+ const textarea = document.createElement('textarea');
1923
+ textarea.value = text;
1924
+ textarea.style.position = 'fixed';
1925
+ textarea.style.opacity = '0';
1926
+ document.body.appendChild(textarea);
1927
+ textarea.focus();
1928
+ textarea.select();
1929
+ document.execCommand('copy');
1930
+ document.body.removeChild(textarea);
1931
+ } catch (err) {
1932
+ console.warn('Failed to copy crash log:', err);
1933
+ }
1934
+ };
1935
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1936
+ navigator.clipboard.writeText(text).catch(fallbackCopy);
1937
+ } else {
1938
+ fallbackCopy();
1939
+ }
1940
+ };
1941
+
1942
+ const sanitizeFatalPayload = (payload) => {
1943
+ if (!payload || typeof payload !== 'object') {
1944
+ return null;
1945
+ }
1946
+ const safeTimestamp = typeof payload.timestamp === 'number' ? payload.timestamp : Date.now();
1947
+ const sanitized = {
1948
+ id: typeof payload.id === 'string' ? payload.id : `fatal-${safeTimestamp}`,
1949
+ type: 'kernel.fatal',
1950
+ title: typeof payload.title === 'string' ? payload.title : 'Pinokio crashed',
1951
+ message: typeof payload.message === 'string' ? payload.message : 'Pinokio encountered a fatal error.',
1952
+ stack: typeof payload.stack === 'string' ? payload.stack : '',
1953
+ origin: typeof payload.origin === 'string' ? payload.origin : null,
1954
+ timestamp: safeTimestamp,
1955
+ version: (payload.version && typeof payload.version === 'object') ? payload.version : null,
1956
+ logPath: typeof payload.logPath === 'string' ? payload.logPath : null,
1957
+ severity: typeof payload.severity === 'string' ? payload.severity : 'fatal',
1958
+ };
1959
+ return sanitized;
1960
+ };
1961
+
1962
+ const persistFatalPayload = (payload) => {
1963
+ if (!storageEnabled || !payload) {
1964
+ return;
1965
+ }
1966
+ try {
1967
+ localStorage.setItem(fatalStorageKey, JSON.stringify(payload));
1968
+ } catch (err) {
1969
+ console.warn('Failed to persist fatal payload:', err);
1970
+ }
1971
+ };
1972
+
1973
+ const parseFatalValue = (value) => {
1974
+ if (!value) {
1975
+ return null;
1976
+ }
1977
+ try {
1978
+ const parsed = JSON.parse(value);
1979
+ if (!parsed || typeof parsed !== 'object') {
1980
+ return null;
1981
+ }
1982
+ const sanitized = sanitizeFatalPayload(parsed);
1983
+ if (!sanitized) {
1984
+ return null;
1985
+ }
1986
+ if (sanitized.timestamp && (Date.now() - sanitized.timestamp) > fatalStaleMs) {
1987
+ return null;
1988
+ }
1989
+ return sanitized;
1990
+ } catch (_) {
1991
+ return null;
1992
+ }
1993
+ };
1994
+
1995
+ const handleFatalPayload = (payload, options) => {
1996
+ const sanitized = sanitizeFatalPayload(payload);
1997
+ if (!sanitized) {
1998
+ return;
1999
+ }
2000
+ if (!lastFatalPayload || lastFatalPayload.id !== sanitized.id) {
2001
+ showFatalOverlay(sanitized);
2002
+ }
2003
+ if (!options || options.persist !== false) {
2004
+ persistFatalPayload(sanitized);
2005
+ }
2006
+ };
2007
+
1724
2008
  const leaderStorageKey = 'pinokio.notification.leader';
1725
2009
  const leaderHeartbeatMs = 5000;
1726
2010
  const leaderStaleMs = 15000;
@@ -1884,6 +2168,9 @@ if (typeof hotkeys === 'function') {
1884
2168
  }
1885
2169
  }
1886
2170
  } catch (_) {}
2171
+ if (payload && payload.type === 'kernel.fatal') {
2172
+ handleFatalPayload(payload);
2173
+ }
1887
2174
  // Visual confirmation regardless of audio outcome (useful on mobile)
1888
2175
  flashNotifyIndicator(payload);
1889
2176
  if (typeof payload.sound === 'string' && payload.sound) {
@@ -2004,14 +2291,25 @@ if (typeof hotkeys === 'function') {
2004
2291
  }
2005
2292
 
2006
2293
  window.addEventListener('storage', (event) => {
2007
- if (event.key !== leaderStorageKey) {
2294
+ if (!event || typeof event.key !== 'string') {
2008
2295
  return;
2009
2296
  }
2010
- const data = parseLeaderValue(event.newValue);
2011
- if (data && data.id === tabId) {
2012
- startLeadership();
2013
- } else {
2014
- resignLeadership();
2297
+ if (event.key === leaderStorageKey) {
2298
+ const data = parseLeaderValue(event.newValue);
2299
+ if (data && data.id === tabId) {
2300
+ startLeadership();
2301
+ } else {
2302
+ resignLeadership();
2303
+ }
2304
+ return;
2305
+ }
2306
+ if (event.key === fatalStorageKey) {
2307
+ const data = parseFatalValue(event.newValue);
2308
+ if (data) {
2309
+ handleFatalPayload(data, { persist: false });
2310
+ } else {
2311
+ hideFatalOverlay();
2312
+ }
2015
2313
  }
2016
2314
  });
2017
2315
 
@@ -2025,6 +2323,12 @@ if (typeof hotkeys === 'function') {
2025
2323
 
2026
2324
  // Attempt to become leader immediately on load.
2027
2325
  attemptLeadership();
2326
+ if (storageEnabled) {
2327
+ const storedFatal = parseFatalValue(localStorage.getItem(fatalStorageKey));
2328
+ if (storedFatal) {
2329
+ handleFatalPayload(storedFatal, { persist: false });
2330
+ }
2331
+ }
2028
2332
  })();
2029
2333
 
2030
2334
  // Mobile "Tap to connect" curtain to prime audio on the top-level page